1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "history/history_item.h"
9
10 #include "lang/lang_keys.h"
11 #include "mainwidget.h"
12 #include "history/view/history_view_element.h"
13 #include "history/view/history_view_service_message.h"
14 #include "history/history_item_components.h"
15 #include "history/view/media/history_view_media_grouped.h"
16 #include "history/history_service.h"
17 #include "history/history_message.h"
18 #include "history/history.h"
19 #include "mtproto/mtproto_config.h"
20 #include "media/clip/media_clip_reader.h"
21 #include "ui/effects/ripple_animation.h"
22 #include "ui/text/text_isolated_emoji.h"
23 #include "ui/text/text_options.h"
24 #include "storage/file_upload.h"
25 #include "storage/storage_facade.h"
26 #include "storage/storage_shared_media.h"
27 #include "main/main_session.h"
28 #include "apiwrap.h"
29 #include "media/audio/media_audio.h"
30 #include "core/application.h"
31 #include "mainwindow.h"
32 #include "window/window_session_controller.h"
33 #include "core/crash_reports.h"
34 #include "base/unixtime.h"
35 #include "api/api_text_entities.h"
36 #include "dialogs/ui/dialogs_message_view.h"
37 #include "data/data_scheduled_messages.h" // kScheduledUntilOnlineTimestamp
38 #include "data/data_changes.h"
39 #include "data/data_session.h"
40 #include "data/data_messages.h"
41 #include "data/data_media_types.h"
42 #include "data/data_folder.h"
43 #include "data/data_channel.h"
44 #include "data/data_chat.h"
45 #include "data/data_user.h"
46 #include "styles/style_dialogs.h"
47 #include "styles/style_chat.h"
48
49 namespace {
50
51 constexpr auto kNotificationTextLimit = 255;
52
53 using ItemPreview = HistoryView::ItemPreview;
54
55 enum class MediaCheckResult {
56 Good,
57 Unsupported,
58 Empty,
59 HasTimeToLive,
60 };
61
CreateUnsupportedMessage(not_null<History * > history,MsgId msgId,MessageFlags flags,MsgId replyTo,UserId viaBotId,TimeId date,PeerId from)62 not_null<HistoryItem*> CreateUnsupportedMessage(
63 not_null<History*> history,
64 MsgId msgId,
65 MessageFlags flags,
66 MsgId replyTo,
67 UserId viaBotId,
68 TimeId date,
69 PeerId from) {
70 const auto siteLink = qsl("https://desktop.telegram.org");
71 auto text = TextWithEntities{
72 tr::lng_message_unsupported(tr::now, lt_link, siteLink)
73 };
74 TextUtilities::ParseEntities(text, Ui::ItemTextNoMonoOptions().flags);
75 text.entities.push_front(
76 EntityInText(EntityType::Italic, 0, text.text.size()));
77 flags &= ~MessageFlag::HasPostAuthor;
78 flags |= MessageFlag::Legacy;
79 const auto groupedId = uint64();
80 return history->makeMessage(
81 msgId,
82 flags,
83 replyTo,
84 viaBotId,
85 date,
86 from,
87 QString(),
88 text,
89 MTP_messageMediaEmpty(),
90 HistoryMessageMarkupData(),
91 groupedId);
92 }
93
CheckMessageMedia(const MTPMessageMedia & media)94 MediaCheckResult CheckMessageMedia(const MTPMessageMedia &media) {
95 using Result = MediaCheckResult;
96 return media.match([](const MTPDmessageMediaEmpty &) {
97 return Result::Good;
98 }, [](const MTPDmessageMediaContact &) {
99 return Result::Good;
100 }, [](const MTPDmessageMediaGeo &data) {
101 return data.vgeo().match([](const MTPDgeoPoint &) {
102 return Result::Good;
103 }, [](const MTPDgeoPointEmpty &) {
104 return Result::Empty;
105 });
106 }, [](const MTPDmessageMediaVenue &data) {
107 return data.vgeo().match([](const MTPDgeoPoint &) {
108 return Result::Good;
109 }, [](const MTPDgeoPointEmpty &) {
110 return Result::Empty;
111 });
112 }, [](const MTPDmessageMediaGeoLive &data) {
113 return data.vgeo().match([](const MTPDgeoPoint &) {
114 return Result::Good;
115 }, [](const MTPDgeoPointEmpty &) {
116 return Result::Empty;
117 });
118 }, [](const MTPDmessageMediaPhoto &data) {
119 const auto photo = data.vphoto();
120 if (data.vttl_seconds()) {
121 return Result::HasTimeToLive;
122 } else if (!photo) {
123 return Result::Empty;
124 }
125 return photo->match([](const MTPDphoto &) {
126 return Result::Good;
127 }, [](const MTPDphotoEmpty &) {
128 return Result::Empty;
129 });
130 }, [](const MTPDmessageMediaDocument &data) {
131 const auto document = data.vdocument();
132 if (data.vttl_seconds()) {
133 return Result::HasTimeToLive;
134 } else if (!document) {
135 return Result::Empty;
136 }
137 return document->match([](const MTPDdocument &) {
138 return Result::Good;
139 }, [](const MTPDdocumentEmpty &) {
140 return Result::Empty;
141 });
142 }, [](const MTPDmessageMediaWebPage &data) {
143 return data.vwebpage().match([](const MTPDwebPage &) {
144 return Result::Good;
145 }, [](const MTPDwebPageEmpty &) {
146 return Result::Good;
147 }, [](const MTPDwebPagePending &) {
148 return Result::Good;
149 }, [](const MTPDwebPageNotModified &) {
150 return Result::Unsupported;
151 });
152 }, [](const MTPDmessageMediaGame &data) {
153 return data.vgame().match([](const MTPDgame &) {
154 return Result::Good;
155 });
156 }, [](const MTPDmessageMediaInvoice &) {
157 return Result::Good;
158 }, [](const MTPDmessageMediaPoll &) {
159 return Result::Good;
160 }, [](const MTPDmessageMediaDice &) {
161 return Result::Good;
162 }, [](const MTPDmessageMediaUnsupported &) {
163 return Result::Unsupported;
164 });
165 }
166
FinalizeMessageFlags(MessageFlags flags)167 [[nodiscard]] MessageFlags FinalizeMessageFlags(MessageFlags flags) {
168 if (!(flags & MessageFlag::FakeHistoryItem)
169 && !(flags & MessageFlag::IsOrWasScheduled)
170 && !(flags & MessageFlag::AdminLogEntry)) {
171 flags |= MessageFlag::HistoryEntry;
172 }
173 return flags;
174 }
175
176 } // namespace
177
operator ()(HistoryItem * value)178 void HistoryItem::HistoryItem::Destroyer::operator()(HistoryItem *value) {
179 if (value) {
180 value->destroy();
181 }
182 }
183
HistoryItem(not_null<History * > history,MsgId id,MessageFlags flags,TimeId date,PeerId from)184 HistoryItem::HistoryItem(
185 not_null<History*> history,
186 MsgId id,
187 MessageFlags flags,
188 TimeId date,
189 PeerId from)
190 : id(id)
191 , _history(history)
192 , _from(from ? history->owner().peer(from) : history->peer)
193 , _flags(FinalizeMessageFlags(flags))
194 , _date(date) {
195 if (isHistoryEntry() && IsClientMsgId(id)) {
196 _history->registerClientSideMessage(this);
197 }
198 }
199
date() const200 TimeId HistoryItem::date() const {
201 return _date;
202 }
203
NewMessageDate(TimeId scheduled)204 TimeId HistoryItem::NewMessageDate(TimeId scheduled) {
205 return scheduled ? scheduled : base::unixtime::now();
206 }
207
applyServiceDateEdition(const MTPDmessageService & data)208 void HistoryItem::applyServiceDateEdition(const MTPDmessageService &data) {
209 const auto date = data.vdate().v;
210 if (_date == date) {
211 return;
212 }
213 _date = date;
214 }
215
finishEdition(int oldKeyboardTop)216 void HistoryItem::finishEdition(int oldKeyboardTop) {
217 if (const auto group = _history->owner().groups().find(this)) {
218 for (const auto &item : group->items) {
219 _history->owner().requestItemViewRefresh(item);
220 item->invalidateChatListEntry();
221 }
222 } else {
223 _history->owner().requestItemViewRefresh(this);
224 invalidateChatListEntry();
225 }
226
227 // Should be completely redesigned as the oldTop no longer exists.
228 //if (oldKeyboardTop >= 0) { // #TODO edit bot message
229 // if (auto keyboard = Get<HistoryMessageReplyMarkup>()) {
230 // keyboard->oldTop = oldKeyboardTop;
231 // }
232 //}
233
234 _history->owner().updateDependentMessages(this);
235 }
236
setGroupId(MessageGroupId groupId)237 void HistoryItem::setGroupId(MessageGroupId groupId) {
238 Expects(!_groupId);
239
240 _groupId = groupId;
241 _history->owner().groups().registerMessage(this);
242 }
243
inlineReplyMarkup()244 HistoryMessageReplyMarkup *HistoryItem::inlineReplyMarkup() {
245 if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
246 if (markup->data.flags & ReplyMarkupFlag::Inline) {
247 return markup;
248 }
249 }
250 return nullptr;
251 }
252
inlineReplyKeyboard()253 ReplyKeyboard *HistoryItem::inlineReplyKeyboard() {
254 if (const auto markup = inlineReplyMarkup()) {
255 return markup->inlineKeyboard.get();
256 }
257 return nullptr;
258 }
259
discussionPostOriginalSender() const260 ChannelData *HistoryItem::discussionPostOriginalSender() const {
261 if (!history()->peer->isMegagroup()) {
262 return nullptr;
263 }
264 if (const auto forwarded = Get<HistoryMessageForwarded>()) {
265 const auto from = forwarded->savedFromPeer;
266 if (const auto result = from ? from->asChannel() : nullptr) {
267 return result;
268 }
269 }
270 return nullptr;
271 }
272
isDiscussionPost() const273 bool HistoryItem::isDiscussionPost() const {
274 return (discussionPostOriginalSender() != nullptr);
275 }
276
lookupDiscussionPostOriginal() const277 HistoryItem *HistoryItem::lookupDiscussionPostOriginal() const {
278 if (!history()->peer->isMegagroup()) {
279 return nullptr;
280 }
281 const auto forwarded = Get<HistoryMessageForwarded>();
282 if (!forwarded
283 || !forwarded->savedFromPeer
284 || !forwarded->savedFromMsgId) {
285 return nullptr;
286 }
287 return _history->owner().message(
288 forwarded->savedFromPeer->asChannel(),
289 forwarded->savedFromMsgId);
290 }
291
displayFrom() const292 PeerData *HistoryItem::displayFrom() const {
293 if (const auto sender = discussionPostOriginalSender()) {
294 return sender;
295 } else if (const auto forwarded = Get<HistoryMessageForwarded>()) {
296 if (history()->peer->isSelf() || history()->peer->isRepliesChat() || forwarded->imported) {
297 return forwarded->originalSender;
298 }
299 }
300 return author().get();
301 }
302
invalidateChatListEntry()303 void HistoryItem::invalidateChatListEntry() {
304 history()->session().changes().messageUpdated(
305 this,
306 Data::MessageUpdate::Flag::DialogRowRefresh);
307 history()->lastItemDialogsView.itemInvalidated(this);
308 }
309
finishEditionToEmpty()310 void HistoryItem::finishEditionToEmpty() {
311 finishEdition(-1);
312 _history->itemVanished(this);
313 }
314
hasUnreadMediaFlag() const315 bool HistoryItem::hasUnreadMediaFlag() const {
316 if (_history->peer->isChannel()) {
317 const auto passed = base::unixtime::now() - date();
318 const auto &config = _history->session().serverConfig();
319 if (passed >= config.channelsReadMediaPeriod) {
320 return false;
321 }
322 }
323 return _flags & MessageFlag::MediaIsUnread;
324 }
325
isUnreadMention() const326 bool HistoryItem::isUnreadMention() const {
327 return mentionsMe() && (_flags & MessageFlag::MediaIsUnread);
328 }
329
mentionsMe() const330 bool HistoryItem::mentionsMe() const {
331 if (Has<HistoryServicePinned>()
332 && !Core::App().settings().notifyAboutPinned()) {
333 return false;
334 }
335 return _flags & MessageFlag::MentionsMe;
336 }
337
isUnreadMedia() const338 bool HistoryItem::isUnreadMedia() const {
339 if (!hasUnreadMediaFlag()) {
340 return false;
341 } else if (const auto media = this->media()) {
342 if (const auto document = media->document()) {
343 if (document->isVoiceMessage() || document->isVideoMessage()) {
344 return (media->webpage() == nullptr);
345 }
346 }
347 }
348 return false;
349 }
350
markMediaRead()351 void HistoryItem::markMediaRead() {
352 _flags &= ~MessageFlag::MediaIsUnread;
353
354 if (mentionsMe()) {
355 history()->updateChatListEntry();
356 history()->eraseFromUnreadMentions(id);
357 }
358 }
359
setIsPinned(bool pinned)360 void HistoryItem::setIsPinned(bool pinned) {
361 const auto changed = (isPinned() != pinned);
362 if (pinned) {
363 _flags |= MessageFlag::Pinned;
364 history()->session().storage().add(Storage::SharedMediaAddExisting(
365 history()->peer->id,
366 Storage::SharedMediaType::Pinned,
367 id,
368 { id, id }));
369 history()->setHasPinnedMessages(true);
370 } else {
371 _flags &= ~MessageFlag::Pinned;
372 history()->session().storage().remove(Storage::SharedMediaRemoveOne(
373 history()->peer->id,
374 Storage::SharedMediaType::Pinned,
375 id));
376 }
377 if (changed) {
378 history()->owner().requestItemResize(this);
379 }
380 }
381
definesReplyKeyboard() const382 bool HistoryItem::definesReplyKeyboard() const {
383 if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
384 if (markup->data.flags & ReplyMarkupFlag::Inline) {
385 return false;
386 }
387 return true;
388 }
389
390 // optimization: don't create markup component for the case
391 // MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag
392 return (_flags & MessageFlag::HasReplyMarkup);
393 }
394
replyKeyboardFlags() const395 ReplyMarkupFlags HistoryItem::replyKeyboardFlags() const {
396 Expects(definesReplyKeyboard());
397
398 if (const auto markup = Get<HistoryMessageReplyMarkup>()) {
399 return markup->data.flags;
400 }
401
402 // optimization: don't create markup component for the case
403 // MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag
404 return ReplyMarkupFlag::None;
405 }
406
addLogEntryOriginal(WebPageId localId,const QString & label,const TextWithEntities & content)407 void HistoryItem::addLogEntryOriginal(
408 WebPageId localId,
409 const QString &label,
410 const TextWithEntities &content) {
411 Expects(isAdminLogEntry());
412
413 AddComponents(HistoryMessageLogEntryOriginal::Bit());
414 Get<HistoryMessageLogEntryOriginal>()->page = _history->owner().webpage(
415 localId,
416 label,
417 content);
418 }
419
specialNotificationPeer() const420 PeerData *HistoryItem::specialNotificationPeer() const {
421 return (mentionsMe() && !_history->peer->isUser())
422 ? from().get()
423 : nullptr;
424 }
425
viaBot() const426 UserData *HistoryItem::viaBot() const {
427 if (const auto via = Get<HistoryMessageVia>()) {
428 return via->bot;
429 }
430 return nullptr;
431 }
432
getMessageBot() const433 UserData *HistoryItem::getMessageBot() const {
434 if (const auto bot = viaBot()) {
435 return bot;
436 }
437 auto bot = from()->asUser();
438 if (!bot) {
439 bot = history()->peer->asUser();
440 }
441 return (bot && bot->isBot()) ? bot : nullptr;
442 }
443
isHistoryEntry() const444 bool HistoryItem::isHistoryEntry() const {
445 return (_flags & MessageFlag::HistoryEntry);
446 }
447
isAdminLogEntry() const448 bool HistoryItem::isAdminLogEntry() const {
449 return (_flags & MessageFlag::AdminLogEntry);
450 }
451
isFromScheduled() const452 bool HistoryItem::isFromScheduled() const {
453 return isHistoryEntry()
454 && (_flags & MessageFlag::IsOrWasScheduled);
455 }
456
isScheduled() const457 bool HistoryItem::isScheduled() const {
458 return !isHistoryEntry()
459 && !isAdminLogEntry()
460 && (_flags & MessageFlag::IsOrWasScheduled);
461 }
462
isSponsored() const463 bool HistoryItem::isSponsored() const {
464 return (_flags & MessageFlag::IsSponsored);
465 }
466
skipNotification() const467 bool HistoryItem::skipNotification() const {
468 if (isSilent() && (_flags & MessageFlag::IsContactSignUp)) {
469 return true;
470 } else if (const auto forwarded = Get<HistoryMessageForwarded>()) {
471 if (forwarded->imported) {
472 return true;
473 }
474 }
475 return false;
476 }
477
destroy()478 void HistoryItem::destroy() {
479 _history->destroyMessage(this);
480 }
481
refreshMainView()482 void HistoryItem::refreshMainView() {
483 if (const auto view = mainView()) {
484 _history->owner().notifyHistoryChangeDelayed(_history);
485 view->refreshInBlock();
486 }
487 }
488
removeMainView()489 void HistoryItem::removeMainView() {
490 if (const auto view = mainView()) {
491 _history->owner().notifyHistoryChangeDelayed(_history);
492 view->removeFromBlock();
493 }
494 }
495
clearMainView()496 void HistoryItem::clearMainView() {
497 _mainView = nullptr;
498 }
499
addToUnreadMentions(UnreadMentionType type)500 void HistoryItem::addToUnreadMentions(UnreadMentionType type) {
501 }
502
applyEditionToHistoryCleared()503 void HistoryItem::applyEditionToHistoryCleared() {
504 applyEdition(
505 MTP_messageService(
506 MTP_flags(0),
507 MTP_int(id),
508 peerToMTP(PeerId(0)), // from_id
509 peerToMTP(history()->peer->id),
510 MTPMessageReplyHeader(),
511 MTP_int(date()),
512 MTP_messageActionHistoryClear(),
513 MTPint() // ttl_period
514 ).c_messageService());
515 }
516
applySentMessage(const MTPDmessage & data)517 void HistoryItem::applySentMessage(const MTPDmessage &data) {
518 updateSentContent({
519 qs(data.vmessage()),
520 Api::EntitiesFromMTP(
521 &history()->session(),
522 data.ventities().value_or_empty())
523 }, data.vmedia());
524 updateReplyMarkup(HistoryMessageMarkupData(data.vreply_markup()));
525 updateForwardedInfo(data.vfwd_from());
526 setViewsCount(data.vviews().value_or(-1));
527 if (const auto replies = data.vreplies()) {
528 setReplies(HistoryMessageRepliesData(replies));
529 } else {
530 clearReplies();
531 }
532 setForwardsCount(data.vforwards().value_or(-1));
533 if (const auto reply = data.vreply_to()) {
534 reply->match([&](const MTPDmessageReplyHeader &data) {
535 setReplyToTop(
536 data.vreply_to_top_id().value_or(
537 data.vreply_to_msg_id().v));
538 });
539 }
540 setPostAuthor(data.vpost_author().value_or_empty());
541 contributeToSlowmode(data.vdate().v);
542 indexAsNewItem();
543 history()->owner().requestItemTextRefresh(this);
544 history()->owner().updateDependentMessages(this);
545 }
546
applySentMessage(const QString & text,const MTPDupdateShortSentMessage & data,bool wasAlready)547 void HistoryItem::applySentMessage(
548 const QString &text,
549 const MTPDupdateShortSentMessage &data,
550 bool wasAlready) {
551 updateSentContent({
552 text,
553 Api::EntitiesFromMTP(
554 &history()->session(),
555 data.ventities().value_or_empty())
556 }, data.vmedia());
557 contributeToSlowmode(data.vdate().v);
558 if (!wasAlready) {
559 indexAsNewItem();
560 }
561 }
562
indexAsNewItem()563 void HistoryItem::indexAsNewItem() {
564 if (isRegular()) {
565 addToUnreadMentions(UnreadMentionType::New);
566 if (const auto types = sharedMediaTypes()) {
567 _history->session().storage().add(Storage::SharedMediaAddNew(
568 _history->peer->id,
569 types,
570 id));
571 if (types.test(Storage::SharedMediaType::Pinned)) {
572 _history->setHasPinnedMessages(true);
573 }
574 }
575 }
576 }
577
setRealId(MsgId newId)578 void HistoryItem::setRealId(MsgId newId) {
579 Expects(_flags & MessageFlag::BeingSent);
580 Expects(IsClientMsgId(id));
581
582 const auto oldId = std::exchange(id, newId);
583 _flags &= ~(MessageFlag::BeingSent | MessageFlag::Local);
584 if (isRegular()) {
585 _history->unregisterClientSideMessage(this);
586 }
587 _history->owner().notifyItemIdChange({ this, oldId });
588
589 // We don't fire MessageUpdate::Flag::ReplyMarkup and update keyboard
590 // in history widget, because it can't exist for an outgoing message.
591 // Only inline keyboards can be in outgoing messages.
592 if (const auto markup = inlineReplyMarkup()) {
593 if (markup->inlineKeyboard) {
594 markup->inlineKeyboard->updateMessageId();
595 }
596 }
597
598 _history->owner().requestItemRepaint(this);
599 }
600
canPin() const601 bool HistoryItem::canPin() const {
602 if (!isRegular() || isService()) {
603 return false;
604 } else if (const auto m = media(); m && m->call()) {
605 return false;
606 }
607 return _history->peer->canPinMessages();
608 }
609
allowsSendNow() const610 bool HistoryItem::allowsSendNow() const {
611 return false;
612 }
613
allowsForward() const614 bool HistoryItem::allowsForward() const {
615 return false;
616 }
617
allowsEdit(TimeId now) const618 bool HistoryItem::allowsEdit(TimeId now) const {
619 return false;
620 }
621
canBeEdited() const622 bool HistoryItem::canBeEdited() const {
623 if ((!isRegular() && !isScheduled())
624 || Has<HistoryMessageVia>()
625 || Has<HistoryMessageForwarded>()) {
626 return false;
627 }
628
629 const auto peer = _history->peer;
630 if (peer->isSelf()) {
631 return true;
632 } else if (const auto channel = peer->asChannel()) {
633 if (isPost() && channel->canEditMessages()) {
634 return true;
635 } else if (out()) {
636 return isPost() ? channel->canPublish() : channel->canWrite();
637 } else {
638 return false;
639 }
640 }
641 return out();
642 }
643
canStopPoll() const644 bool HistoryItem::canStopPoll() const {
645 return canBeEdited() && isRegular();
646 }
647
canDelete() const648 bool HistoryItem::canDelete() const {
649 if (isSponsored()) {
650 return false;
651 } else if (isService() && !isRegular()) {
652 return false;
653 } else if (!isHistoryEntry() && !isScheduled()) {
654 return false;
655 }
656 auto channel = _history->peer->asChannel();
657 if (!channel) {
658 return !isGroupMigrate();
659 }
660
661 if (id == 1) {
662 return false;
663 }
664 if (channel->canDeleteMessages()) {
665 return true;
666 }
667 if (out() && !isService()) {
668 return isPost() ? channel->canPublish() : true;
669 }
670 return false;
671 }
672
canDeleteForEveryone(TimeId now) const673 bool HistoryItem::canDeleteForEveryone(TimeId now) const {
674 const auto peer = history()->peer;
675 const auto &config = history()->session().serverConfig();
676 const auto messageToMyself = peer->isSelf();
677 const auto messageTooOld = messageToMyself
678 ? false
679 : peer->isUser()
680 ? (now - date() >= config.revokePrivateTimeLimit)
681 : (now - date() >= config.revokeTimeLimit);
682 if (!isRegular() || messageToMyself || messageTooOld || isPost()) {
683 return false;
684 }
685 if (peer->isChannel()) {
686 return false;
687 } else if (const auto user = peer->asUser()) {
688 // Bots receive all messages and there is no sense in revoking them.
689 // See https://github.com/telegramdesktop/tdesktop/issues/3818
690 if (user->isBot() && !user->isSupport()) {
691 return false;
692 }
693 }
694 if (const auto media = this->media()) {
695 if (!media->allowsRevoke(now)) {
696 return false;
697 }
698 }
699 if (!out()) {
700 if (const auto chat = peer->asChat()) {
701 if (!chat->canDeleteMessages()) {
702 return false;
703 }
704 } else if (peer->isUser()) {
705 return config.revokePrivateInbox;
706 } else {
707 return false;
708 }
709 }
710 return true;
711 }
712
suggestReport() const713 bool HistoryItem::suggestReport() const {
714 if (out() || isService() || !isRegular()) {
715 return false;
716 } else if (const auto channel = history()->peer->asChannel()) {
717 return true;
718 } else if (const auto user = history()->peer->asUser()) {
719 return user->isBot();
720 }
721 return false;
722 }
723
suggestBanReport() const724 bool HistoryItem::suggestBanReport() const {
725 const auto channel = history()->peer->asChannel();
726 const auto fromUser = from()->asUser();
727 if (!channel
728 || !fromUser
729 || !channel->canRestrictParticipant(fromUser)) {
730 return false;
731 }
732 return !isPost() && !out();
733 }
734
suggestDeleteAllReport() const735 bool HistoryItem::suggestDeleteAllReport() const {
736 auto channel = history()->peer->asChannel();
737 if (!channel || !channel->canDeleteMessages()) {
738 return false;
739 }
740 return !isPost() && !out() && from()->isUser();
741 }
742
hasDirectLink() const743 bool HistoryItem::hasDirectLink() const {
744 return isRegular() && _history->peer->isChannel();
745 }
746
channelId() const747 ChannelId HistoryItem::channelId() const {
748 return _history->channelId();
749 }
750
position() const751 Data::MessagePosition HistoryItem::position() const {
752 return { .fullId = fullId(), .date = date() };
753 }
754
replyToId() const755 MsgId HistoryItem::replyToId() const {
756 if (const auto reply = Get<HistoryMessageReply>()) {
757 return reply->replyToId();
758 }
759 return 0;
760 }
761
replyToTop() const762 MsgId HistoryItem::replyToTop() const {
763 if (const auto reply = Get<HistoryMessageReply>()) {
764 return reply->replyToTop();
765 }
766 return 0;
767 }
768
author() const769 not_null<PeerData*> HistoryItem::author() const {
770 return (isPost() && !isSponsored()) ? history()->peer : from();
771 }
772
dateOriginal() const773 TimeId HistoryItem::dateOriginal() const {
774 if (const auto forwarded = Get<HistoryMessageForwarded>()) {
775 return forwarded->originalDate;
776 }
777 return date();
778 }
779
senderOriginal() const780 PeerData *HistoryItem::senderOriginal() const {
781 if (const auto forwarded = Get<HistoryMessageForwarded>()) {
782 return forwarded->originalSender;
783 }
784 const auto peer = history()->peer;
785 return (peer->isChannel() && !peer->isMegagroup()) ? peer : from();
786 }
787
hiddenForwardedInfo() const788 const HiddenSenderInfo *HistoryItem::hiddenForwardedInfo() const {
789 if (const auto forwarded = Get<HistoryMessageForwarded>()) {
790 return forwarded->hiddenSenderInfo.get();
791 }
792 return nullptr;
793 }
794
fromOriginal() const795 not_null<PeerData*> HistoryItem::fromOriginal() const {
796 if (const auto forwarded = Get<HistoryMessageForwarded>()) {
797 if (forwarded->originalSender) {
798 if (const auto user = forwarded->originalSender->asUser()) {
799 return user;
800 }
801 }
802 }
803 return from();
804 }
805
authorOriginal() const806 QString HistoryItem::authorOriginal() const {
807 if (const auto forwarded = Get<HistoryMessageForwarded>()) {
808 return forwarded->originalAuthor;
809 } else if (const auto msgsigned = Get<HistoryMessageSigned>()) {
810 if (!msgsigned->isAnonymousRank) {
811 return msgsigned->author;
812 }
813 }
814 return QString();
815 }
816
idOriginal() const817 MsgId HistoryItem::idOriginal() const {
818 if (const auto forwarded = Get<HistoryMessageForwarded>()) {
819 return forwarded->originalId;
820 }
821 return id;
822 }
823
updateDate(TimeId newDate)824 void HistoryItem::updateDate(TimeId newDate) {
825 if (canUpdateDate() && _date != newDate) {
826 _date = newDate;
827 _history->owner().requestItemViewRefresh(this);
828 }
829 }
830
canUpdateDate() const831 bool HistoryItem::canUpdateDate() const {
832 return isScheduled();
833 }
834
applyTTL(const MTPDmessage & data)835 void HistoryItem::applyTTL(const MTPDmessage &data) {
836 if (const auto period = data.vttl_period()) {
837 if (period->v > 0) {
838 applyTTL(data.vdate().v + period->v);
839 }
840 }
841 }
842
applyTTL(const MTPDmessageService & data)843 void HistoryItem::applyTTL(const MTPDmessageService &data) {
844 if (const auto period = data.vttl_period()) {
845 if (period->v > 0) {
846 applyTTL(data.vdate().v + period->v);
847 }
848 }
849 }
850
applyTTL(TimeId destroyAt)851 void HistoryItem::applyTTL(TimeId destroyAt) {
852 const auto previousDestroyAt = std::exchange(_ttlDestroyAt, destroyAt);
853 if (previousDestroyAt) {
854 history()->owner().unregisterMessageTTL(previousDestroyAt, this);
855 }
856 if (!_ttlDestroyAt) {
857 return;
858 } else if (base::unixtime::now() >= _ttlDestroyAt) {
859 const auto session = &history()->session();
860 crl::on_main(session, [session, id = fullId()]{
861 if (const auto item = session->data().message(id)) {
862 item->destroy();
863 }
864 });
865 } else {
866 history()->owner().registerMessageTTL(_ttlDestroyAt, this);
867 }
868 }
869
isUploading() const870 bool HistoryItem::isUploading() const {
871 return _media && _media->uploading();
872 }
873
isRegular() const874 bool HistoryItem::isRegular() const {
875 return isHistoryEntry() && !isLocal();
876 }
877
sendFailed()878 void HistoryItem::sendFailed() {
879 Expects(_flags & MessageFlag::BeingSent);
880 Expects(!(_flags & MessageFlag::SendingFailed));
881
882 _flags = (_flags | MessageFlag::SendingFailed) & ~MessageFlag::BeingSent;
883 history()->session().changes().historyUpdated(
884 history(),
885 Data::HistoryUpdate::Flag::ClientSideMessages);
886 }
887
needCheck() const888 bool HistoryItem::needCheck() const {
889 return (out() && !isEmpty()) || (!isRegular() && history()->peer->isSelf());
890 }
891
unread() const892 bool HistoryItem::unread() const {
893 // Messages from myself are always read, unless scheduled.
894 if (history()->peer->isSelf() && !isFromScheduled()) {
895 return false;
896 }
897
898 if (out()) {
899 // Outgoing messages in converted chats are always read.
900 if (history()->peer->migrateTo()) {
901 return false;
902 }
903
904 if (isRegular()) {
905 if (!history()->isServerSideUnread(this)) {
906 return false;
907 }
908 if (const auto user = history()->peer->asUser()) {
909 if (user->isBot() && !user->isSupport()) {
910 return false;
911 }
912 } else if (const auto channel = history()->peer->asChannel()) {
913 if (!channel->isMegagroup()) {
914 return false;
915 }
916 }
917 }
918 return true;
919 }
920
921 if (isRegular()) {
922 if (!history()->isServerSideUnread(this)) {
923 return false;
924 }
925 return true;
926 }
927 return (_flags & MessageFlag::ClientSideUnread);
928 }
929
showNotification() const930 bool HistoryItem::showNotification() const {
931 const auto channel = _history->peer->asChannel();
932 if (channel && !channel->amIn()) {
933 return false;
934 }
935 return (out() || _history->peer->isSelf())
936 ? isFromScheduled()
937 : unread();
938 }
939
markClientSideAsRead()940 void HistoryItem::markClientSideAsRead() {
941 _flags &= ~MessageFlag::ClientSideUnread;
942 }
943
groupId() const944 MessageGroupId HistoryItem::groupId() const {
945 return _groupId;
946 }
947
isEmpty() const948 bool HistoryItem::isEmpty() const {
949 return _text.isEmpty()
950 && !_media
951 && !Has<HistoryMessageLogEntryOriginal>();
952 }
953
notificationText() const954 QString HistoryItem::notificationText() const {
955 const auto result = [&] {
956 if (_media && !isService()) {
957 return _media->notificationText();
958 } else if (!emptyText()) {
959 return _text.toString();
960 }
961 return QString();
962 }();
963 return (result.size() <= kNotificationTextLimit)
964 ? result
965 : result.mid(0, kNotificationTextLimit) + qsl("...");
966 }
967
toPreview(ToPreviewOptions options) const968 ItemPreview HistoryItem::toPreview(ToPreviewOptions options) const {
969 auto result = [&]() -> ItemPreview {
970 if (_media) {
971 return _media->toPreview(options);
972 } else if (!emptyText()) {
973 return { .text = TextUtilities::Clean(_text.toString()) };
974 }
975 return {};
976 }();
977 const auto sender = [&]() -> std::optional<QString> {
978 const auto fromSender = [](not_null<PeerData*> sender) {
979 return sender->isSelf()
980 ? tr::lng_from_you(tr::now)
981 : sender->shortName();
982 };
983 if (options.hideSender || isPost() || isEmpty()) {
984 return {};
985 } else if (!_history->peer->isUser() || _history->peer->isSelf()) {
986 if (const auto forwarded = Get<HistoryMessageForwarded>()) {
987 return forwarded->originalSender
988 ? fromSender(forwarded->originalSender)
989 : forwarded->hiddenSenderInfo->name;
990 } else if (!_history->peer->isUser()) {
991 return fromSender(displayFrom());
992 }
993 }
994 return {};
995 }();
996 if (!sender) {
997 return result;
998 }
999 const auto fromWrapped = textcmdLink(
1000 1,
1001 tr::lng_dialogs_text_from_wrapped(
1002 tr::now,
1003 lt_from,
1004 TextUtilities::Clean(*sender)));
1005 return Dialogs::Ui::PreviewWithSender(std::move(result), fromWrapped);
1006 }
1007
isolatedEmoji() const1008 Ui::Text::IsolatedEmoji HistoryItem::isolatedEmoji() const {
1009 return Ui::Text::IsolatedEmoji();
1010 }
1011
~HistoryItem()1012 HistoryItem::~HistoryItem() {
1013 applyTTL(0);
1014 }
1015
ItemDateTime(not_null<const HistoryItem * > item)1016 QDateTime ItemDateTime(not_null<const HistoryItem*> item) {
1017 return base::unixtime::parse(item->date());
1018 }
1019
ItemDateText(not_null<const HistoryItem * > item,bool isUntilOnline)1020 QString ItemDateText(not_null<const HistoryItem*> item, bool isUntilOnline) {
1021 const auto dateText = langDayOfMonthFull(ItemDateTime(item).date());
1022 return !item->isScheduled()
1023 ? dateText
1024 : isUntilOnline
1025 ? tr::lng_scheduled_date_until_online(tr::now)
1026 : tr::lng_scheduled_date(tr::now, lt_date, dateText);
1027 }
1028
IsItemScheduledUntilOnline(not_null<const HistoryItem * > item)1029 bool IsItemScheduledUntilOnline(not_null<const HistoryItem*> item) {
1030 return item->isScheduled()
1031 && (item->date() ==
1032 Data::ScheduledMessages::kScheduledUntilOnlineTimestamp);
1033 }
1034
goToMessageClickHandler(not_null<HistoryItem * > item,FullMsgId returnToId)1035 ClickHandlerPtr goToMessageClickHandler(
1036 not_null<HistoryItem*> item,
1037 FullMsgId returnToId) {
1038 return goToMessageClickHandler(
1039 item->history()->peer,
1040 item->id,
1041 returnToId);
1042 }
1043
goToMessageClickHandler(not_null<PeerData * > peer,MsgId msgId,FullMsgId returnToId)1044 ClickHandlerPtr goToMessageClickHandler(
1045 not_null<PeerData*> peer,
1046 MsgId msgId,
1047 FullMsgId returnToId) {
1048 return std::make_shared<LambdaClickHandler>([=] {
1049 if (const auto main = App::main()) { // multi good
1050 if (&main->session() == &peer->session()) {
1051 auto params = Window::SectionShow{
1052 Window::SectionShow::Way::Forward
1053 };
1054 params.origin = Window::SectionShow::OriginMessage{
1055 returnToId
1056 };
1057 main->controller()->showPeerHistory(peer, params, msgId);
1058 }
1059 }
1060 });
1061 }
1062
FlagsFromMTP(MsgId id,MTPDmessage::Flags flags,MessageFlags localFlags)1063 MessageFlags FlagsFromMTP(
1064 MsgId id,
1065 MTPDmessage::Flags flags,
1066 MessageFlags localFlags) {
1067 using Flag = MessageFlag;
1068 using MTP = MTPDmessage::Flag;
1069 return localFlags
1070 | (IsServerMsgId(id) ? Flag::HistoryEntry : Flag())
1071 | ((flags & MTP::f_out) ? Flag::Outgoing : Flag())
1072 | ((flags & MTP::f_mentioned) ? Flag::MentionsMe : Flag())
1073 | ((flags & MTP::f_media_unread) ? Flag::MediaIsUnread : Flag())
1074 | ((flags & MTP::f_silent) ? Flag::Silent : Flag())
1075 | ((flags & MTP::f_post) ? Flag::Post : Flag())
1076 | ((flags & MTP::f_legacy) ? Flag::Legacy : Flag())
1077 | ((flags & MTP::f_edit_hide) ? Flag::HideEdited : Flag())
1078 | ((flags & MTP::f_pinned) ? Flag::Pinned : Flag())
1079 | ((flags & MTP::f_from_id) ? Flag::HasFromId : Flag())
1080 | ((flags & MTP::f_via_bot_id) ? Flag::HasViaBot : Flag())
1081 | ((flags & MTP::f_reply_to) ? Flag::HasReplyInfo : Flag())
1082 | ((flags & MTP::f_reply_markup) ? Flag::HasReplyMarkup : Flag())
1083 | ((flags & MTP::f_from_scheduled) ? Flag::IsOrWasScheduled : Flag())
1084 | ((flags & MTP::f_views) ? Flag::HasViews : Flag());
1085 }
1086
FlagsFromMTP(MsgId id,MTPDmessageService::Flags flags,MessageFlags localFlags)1087 MessageFlags FlagsFromMTP(
1088 MsgId id,
1089 MTPDmessageService::Flags flags,
1090 MessageFlags localFlags) {
1091 using Flag = MessageFlag;
1092 using MTP = MTPDmessageService::Flag;
1093 return localFlags
1094 | (IsServerMsgId(id) ? Flag::HistoryEntry : Flag())
1095 | ((flags & MTP::f_out) ? Flag::Outgoing : Flag())
1096 | ((flags & MTP::f_mentioned) ? Flag::MentionsMe : Flag())
1097 | ((flags & MTP::f_media_unread) ? Flag::MediaIsUnread : Flag())
1098 | ((flags & MTP::f_silent) ? Flag::Silent : Flag())
1099 | ((flags & MTP::f_post) ? Flag::Post : Flag())
1100 | ((flags & MTP::f_legacy) ? Flag::Legacy : Flag())
1101 | ((flags & MTP::f_from_id) ? Flag::HasFromId : Flag())
1102 | ((flags & MTP::f_reply_to) ? Flag::HasReplyInfo : Flag());
1103 }
1104
Create(not_null<History * > history,MsgId id,const MTPMessage & message,MessageFlags localFlags)1105 not_null<HistoryItem*> HistoryItem::Create(
1106 not_null<History*> history,
1107 MsgId id,
1108 const MTPMessage &message,
1109 MessageFlags localFlags) {
1110 return message.match([&](const MTPDmessage &data) -> HistoryItem* {
1111 const auto media = data.vmedia();
1112 const auto checked = media
1113 ? CheckMessageMedia(*media)
1114 : MediaCheckResult::Good;
1115 if (checked == MediaCheckResult::Unsupported) {
1116 return CreateUnsupportedMessage(
1117 history,
1118 id,
1119 FlagsFromMTP(id, data.vflags().v, localFlags),
1120 MsgId(0), // No need to pass reply_to data here.
1121 data.vvia_bot_id().value_or_empty(),
1122 data.vdate().v,
1123 data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0));
1124 } else if (checked == MediaCheckResult::Empty) {
1125 const auto text = HistoryService::PreparedText{
1126 tr::lng_message_empty(tr::now)
1127 };
1128 return history->makeServiceMessage(
1129 id,
1130 FlagsFromMTP(id, data.vflags().v, localFlags),
1131 data.vdate().v,
1132 text,
1133 data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0));
1134 } else if (checked == MediaCheckResult::HasTimeToLive) {
1135 return history->makeServiceMessage(id, data, localFlags);
1136 }
1137 return history->makeMessage(id, data, localFlags);
1138 }, [&](const MTPDmessageService &data) -> HistoryItem* {
1139 if (data.vaction().type() == mtpc_messageActionPhoneCall) {
1140 return history->makeMessage(id, data, localFlags);
1141 }
1142 return history->makeServiceMessage(id, data, localFlags);
1143 }, [&](const MTPDmessageEmpty &data) -> HistoryItem* {
1144 const auto text = HistoryService::PreparedText{
1145 tr::lng_message_empty(tr::now)
1146 };
1147 return history->makeServiceMessage(id, localFlags, TimeId(0), text);
1148 });
1149 }
1150