/* This file is part of Telegram Desktop, the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #include "history/history_message.h" #include "lang/lang_keys.h" #include "apiwrap.h" #include "api/api_text_entities.h" #include "history/history.h" #include "history/history_service.h" #include "history/history_item_components.h" #include "history/history_unread_things.h" #include "history/view/history_view_service_message.h" #include "history/view/history_view_context_menu.h" // CopyPostLink. #include "history/view/media/history_view_media.h" // AddTimestampLinks. #include "chat_helpers/stickers_emoji_pack.h" #include "main/main_session.h" #include "main/main_session_settings.h" #include "api/api_updates.h" #include "boxes/share_box.h" #include "ui/text/text_isolated_emoji.h" #include "ui/text/format_values.h" #include "storage/storage_shared_media.h" #include "mtproto/mtproto_config.h" #include "data/notify/data_notify_settings.h" #include "data/data_session.h" #include "data/data_changes.h" #include "data/data_media_types.h" #include "data/data_channel.h" #include "data/data_forum_topic.h" #include "data/data_forum.h" #include "data/data_user.h" #include "data/data_web_page.h" #include "data/data_sponsored_messages.h" #include "data/data_scheduled_messages.h" #include "styles/style_dialogs.h" #include "styles/style_widgets.h" #include "styles/style_chat.h" #include "styles/style_window.h" namespace { [[nodiscard]] MessageFlags NewForwardedFlags( not_null peer, PeerId from, not_null fwd) { auto result = NewMessageFlags(peer); if (from) { result |= MessageFlag::HasFromId; } if (const auto media = fwd->media()) { if ((!peer->isChannel() || peer->isMegagroup()) && media->forwardedBecomesUnread()) { result |= MessageFlag::MediaIsUnread; } } if (fwd->hasViews()) { result |= MessageFlag::HasViews; } return result; } [[nodiscard]] bool CopyMarkupToForward(not_null item) { auto mediaOriginal = item->media(); if (mediaOriginal && mediaOriginal->game()) { // Copy inline keyboard when forwarding messages with a game. return true; } const auto markup = item->inlineReplyMarkup(); if (!markup) { return false; } using Type = HistoryMessageMarkupButton::Type; for (const auto &row : markup->data.rows) { for (const auto &button : row) { const auto switchInline = (button.type == Type::SwitchInline) || (button.type == Type::SwitchInlineSame); const auto url = (button.type == Type::Url) || (button.type == Type::Auth); if ((!switchInline || !item->viaBot()) && !url) { return false; } } } return true; } [[nodiscard]] bool HasInlineItems(const HistoryItemsList &items) { for (const auto &item : items) { if (item->viaBot()) { return true; } } return false; } [[nodiscard]] TextWithEntities EnsureNonEmpty( const TextWithEntities &text = TextWithEntities()) { return !text.text.isEmpty() ? text : TextWithEntities{ u":-("_q }; } } // namespace QString GetErrorTextForSending( not_null peer, const HistoryItemsList &items, const TextWithTags &comment, bool ignoreSlowmodeCountdown) { if (!peer->canWrite()) { return tr::lng_forward_cant(tr::now); } for (const auto &item : items) { if (const auto media = item->media()) { const auto error = media->errorTextForForward(peer); if (!error.isEmpty() && error != qstr("skip")) { return error; } } } const auto error = Data::RestrictionError( peer, ChatRestriction::SendInline); if (error && HasInlineItems(items)) { return *error; } if (peer->slowmodeApplied()) { if (const auto history = peer->owner().historyLoaded(peer)) { if (!ignoreSlowmodeCountdown && (history->latestSendingMessage() != nullptr) && (!items.empty() || !comment.text.isEmpty())) { return tr::lng_slowmode_no_many(tr::now); } } if (comment.text.size() > MaxMessageSize) { return tr::lng_slowmode_too_long(tr::now); } else if (!items.empty() && !comment.text.isEmpty()) { return tr::lng_slowmode_no_many(tr::now); } else if (items.size() > 1) { const auto albumForward = [&] { if (const auto groupId = items.front()->groupId()) { for (const auto &item : items) { if (item->groupId() != groupId) { return false; } } return true; } return false; }(); if (!albumForward) { return tr::lng_slowmode_no_many(tr::now); } } } if (const auto left = peer->slowmodeSecondsLeft()) { if (!ignoreSlowmodeCountdown) { return tr::lng_slowmode_enabled( tr::now, lt_left, Ui::FormatDurationWordsSlowmode(left)); } } return QString(); } void RequestDependentMessageData( not_null item, PeerId peerId, MsgId msgId) { if (!IsServerMsgId(msgId)) { return; } const auto fullId = item->fullId(); const auto history = item->history(); const auto session = &history->session(); const auto done = [=] { if (const auto item = session->data().message(fullId)) { item->updateDependencyItem(); } }; history->session().api().requestMessageData( (peerId ? history->owner().peer(peerId) : history->peer), msgId, done); } MessageFlags NewMessageFlags(not_null peer) { return MessageFlag::BeingSent | (peer->isSelf() ? MessageFlag() : MessageFlag::Outgoing); } bool ShouldSendSilent( not_null peer, const Api::SendOptions &options) { return options.silent || (peer->isBroadcast() && peer->owner().notifySettings().silentPosts(peer)) || (peer->session().supportMode() && peer->session().settings().supportAllSilent()); } HistoryItem *LookupReplyTo(not_null history, MsgId replyToId) { const auto &owner = history->owner(); return owner.message(history->peer, replyToId); } MsgId LookupReplyToTop(HistoryItem *replyTo) { return replyTo ? replyTo->replyToTop() : 0; } bool LookupReplyIsTopicPost(HistoryItem *replyTo) { return replyTo && (replyTo->topicRootId() != 0); } MTPMessageReplyHeader NewMessageReplyHeader(const Api::SendAction &action) { if (const auto id = action.replyTo) { const auto to = LookupReplyTo(action.history, id); if (const auto replyToTop = LookupReplyToTop(to)) { using Flag = MTPDmessageReplyHeader::Flag; return MTP_messageReplyHeader( MTP_flags(Flag::f_reply_to_top_id | (LookupReplyIsTopicPost(to) ? Flag::f_forum_topic : Flag(0))), MTP_int(id), MTPPeer(), MTP_int(replyToTop)); } return MTP_messageReplyHeader( MTP_flags(0), MTP_int(id), MTPPeer(), MTPint()); } return MTPMessageReplyHeader(); } QString GetErrorTextForSending( not_null peer, const HistoryItemsList &items, bool ignoreSlowmodeCountdown) { return GetErrorTextForSending(peer, items, {}, ignoreSlowmodeCountdown); } TextWithEntities DropCustomEmoji(TextWithEntities text) { text.entities.erase( ranges::remove( text.entities, EntityType::CustomEmoji, &EntityInText::type), text.entities.end()); return text; } struct HistoryMessage::CreateConfig { PeerId replyToPeer = 0; MsgId replyTo = 0; MsgId replyToTop = 0; bool replyIsTopicPost = false; UserId viaBotId = 0; int viewsCount = -1; QString author; PeerId senderOriginal = 0; QString senderNameOriginal; QString forwardPsaType; MsgId originalId = 0; PeerId savedFromPeer = 0; MsgId savedFromMsgId = 0; QString authorOriginal; TimeId originalDate = 0; TimeId editDate = 0; HistoryMessageMarkupData markup; HistoryMessageRepliesData replies; bool imported = false; // For messages created from existing messages (forwarded). const HistoryMessageReplyMarkup *inlineMarkup = nullptr; }; void HistoryMessage::FillForwardedInfo( CreateConfig &config, const MTPDmessageFwdHeader &data) { if (const auto fromId = data.vfrom_id()) { config.senderOriginal = peerFromMTP(*fromId); } config.originalDate = data.vdate().v; config.senderNameOriginal = qs(data.vfrom_name().value_or_empty()); config.forwardPsaType = qs(data.vpsa_type().value_or_empty()); config.originalId = data.vchannel_post().value_or_empty(); config.authorOriginal = qs(data.vpost_author().value_or_empty()); const auto savedFromPeer = data.vsaved_from_peer(); const auto savedFromMsgId = data.vsaved_from_msg_id(); if (savedFromPeer && savedFromMsgId) { config.savedFromPeer = peerFromMTP(*savedFromPeer); config.savedFromMsgId = savedFromMsgId->v; } config.imported = data.is_imported(); } HistoryMessage::HistoryMessage( not_null history, MsgId id, const MTPDmessage &data, MessageFlags localFlags) : HistoryItem( history, id, FlagsFromMTP(id, data.vflags().v, localFlags), data.vdate().v, data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0)) { auto config = CreateConfig(); if (const auto forwarded = data.vfwd_from()) { forwarded->match([&](const MTPDmessageFwdHeader &data) { FillForwardedInfo(config, data); }); } if (const auto reply = data.vreply_to()) { reply->match([&](const MTPDmessageReplyHeader &data) { if (const auto peer = data.vreply_to_peer_id()) { config.replyToPeer = peerFromMTP(*peer); if (config.replyToPeer == history->peer->id) { config.replyToPeer = 0; } } const auto id = data.vreply_to_msg_id().v; config.replyTo = data.is_reply_to_scheduled() ? history->owner().scheduledMessages().localMessageId(id) : id; config.replyToTop = data.vreply_to_top_id().value_or( data.vreply_to_msg_id().v); config.replyIsTopicPost = data.is_forum_topic(); }); } config.viaBotId = data.vvia_bot_id().value_or_empty(); config.viewsCount = data.vviews().value_or(-1); config.replies = isScheduled() ? HistoryMessageRepliesData() : HistoryMessageRepliesData(data.vreplies()); config.markup = HistoryMessageMarkupData(data.vreply_markup()); config.editDate = data.vedit_date().value_or_empty(); config.author = qs(data.vpost_author().value_or_empty()); createComponents(std::move(config)); if (const auto media = data.vmedia()) { setMedia(*media); } auto textWithEntities = TextWithEntities{ qs(data.vmessage()), Api::EntitiesFromMTP( &history->session(), data.ventities().value_or_empty()) }; setText(_media ? textWithEntities : EnsureNonEmpty(textWithEntities)); if (const auto groupedId = data.vgrouped_id()) { setGroupId( MessageGroupId::FromRaw(history->peer->id, groupedId->v)); } setReactions(data.vreactions()); applyTTL(data); } HistoryMessage::HistoryMessage( not_null history, MsgId id, const MTPDmessageService &data, MessageFlags localFlags) : HistoryItem( history, id, FlagsFromMTP(id, data.vflags().v, localFlags), data.vdate().v, data.vfrom_id() ? peerFromMTP(*data.vfrom_id()) : PeerId(0)) { auto config = CreateConfig(); if (const auto reply = data.vreply_to()) { reply->match([&](const MTPDmessageReplyHeader &data) { const auto peer = data.vreply_to_peer_id() ? peerFromMTP(*data.vreply_to_peer_id()) : history->peer->id; if (!peer || peer == history->peer->id) { config.replyTo = data.vreply_to_msg_id().v; config.replyToTop = data.vreply_to_top_id().value_or( data.vreply_to_msg_id().v); } }); } createComponents(std::move(config)); data.vaction().match([&](const MTPDmessageActionPhoneCall &data) { _media = std::make_unique( this, Data::ComputeCallData(data)); setTextValue({}); }, [](const auto &) { Unexpected("Service message action type in HistoryMessage."); }); applyTTL(data); } HistoryMessage::HistoryMessage( not_null history, MsgId id, MessageFlags flags, TimeId date, PeerId from, const QString &postAuthor, not_null original) : HistoryItem( history, id, (NewForwardedFlags(history->peer, from, original) | flags), date, from) { const auto peer = history->peer; auto config = CreateConfig(); const auto originalMedia = original->media(); const auto dropForwardInfo = original->computeDropForwardedInfo(); if (!dropForwardInfo) { config.originalDate = original->dateOriginal(); if (const auto info = original->hiddenSenderInfo()) { config.senderNameOriginal = info->name; } else if (const auto senderOriginal = original->senderOriginal()) { config.senderOriginal = senderOriginal->id; if (senderOriginal->isChannel()) { config.originalId = original->idOriginal(); } } else { Unexpected("Corrupt forwarded information in message."); } config.authorOriginal = original->authorOriginal(); } if (peer->isSelf()) { // // iOS app sends you to the original post if we forward a forward from channel. // But server returns not the original post but the forward in saved_from_... // //if (config.originalId) { // config.savedFromPeer = config.senderOriginal; // config.savedFromMsgId = config.originalId; //} else { config.savedFromPeer = original->history()->peer->id; config.savedFromMsgId = original->id; //} } if (flags & MessageFlag::HasPostAuthor) { config.author = postAuthor; } if (const auto fwdViaBot = original->viaBot()) { config.viaBotId = peerToUser(fwdViaBot->id); } else if (originalMedia && originalMedia->game()) { if (const auto sender = original->senderOriginal()) { if (const auto user = sender->asUser()) { if (user->isBot()) { config.viaBotId = peerToUser(user->id); } } } } const auto fwdViewsCount = original->viewsCount(); if (fwdViewsCount > 0) { config.viewsCount = fwdViewsCount; } else if ((isPost() && !isScheduled()) || (original->senderOriginal() && original->senderOriginal()->isChannel())) { config.viewsCount = 1; } const auto mediaOriginal = original->media(); if (CopyMarkupToForward(original)) { config.inlineMarkup = original->inlineReplyMarkup(); } createComponents(std::move(config)); const auto ignoreMedia = [&] { if (mediaOriginal && mediaOriginal->webpage()) { if (peer->amRestricted(ChatRestriction::EmbedLinks)) { return true; } } return false; }; if (mediaOriginal && !ignoreMedia()) { _media = mediaOriginal->clone(this); } const auto dropCustomEmoji = dropForwardInfo && !history->session().premium() && !history->peer->isSelf(); setText(dropCustomEmoji ? DropCustomEmoji(original->originalText()) : original->originalText()); } HistoryMessage::HistoryMessage( not_null history, MsgId id, MessageFlags flags, MsgId replyTo, UserId viaBotId, TimeId date, PeerId from, const QString &postAuthor, const TextWithEntities &textWithEntities, const MTPMessageMedia &media, HistoryMessageMarkupData &&markup, uint64 groupedId) : HistoryItem( history, id, flags, date, (flags & MessageFlag::HasFromId) ? from : 0) { createComponentsHelper( flags, replyTo, viaBotId, postAuthor, std::move(markup)); setMedia(media); setText(textWithEntities); if (groupedId) { setGroupId(MessageGroupId::FromRaw(history->peer->id, groupedId)); } } HistoryMessage::HistoryMessage( not_null history, MsgId id, MessageFlags flags, MsgId replyTo, UserId viaBotId, TimeId date, PeerId from, const QString &postAuthor, not_null document, const TextWithEntities &caption, HistoryMessageMarkupData &&markup) : HistoryItem( history, id, flags, date, (flags & MessageFlag::HasFromId) ? from : 0) { createComponentsHelper( flags, replyTo, viaBotId, postAuthor, std::move(markup)); const auto skipPremiumEffect = !history->session().premium(); _media = std::make_unique( this, document, skipPremiumEffect); setText(caption); } HistoryMessage::HistoryMessage( not_null history, MsgId id, MessageFlags flags, MsgId replyTo, UserId viaBotId, TimeId date, PeerId from, const QString &postAuthor, not_null photo, const TextWithEntities &caption, HistoryMessageMarkupData &&markup) : HistoryItem( history, id, flags, date, (flags & MessageFlag::HasFromId) ? from : 0) { createComponentsHelper( flags, replyTo, viaBotId, postAuthor, std::move(markup)); _media = std::make_unique(this, photo); setText(caption); } HistoryMessage::HistoryMessage( not_null history, MsgId id, MessageFlags flags, MsgId replyTo, UserId viaBotId, TimeId date, PeerId from, const QString &postAuthor, not_null game, HistoryMessageMarkupData &&markup) : HistoryItem( history, id, flags, date, (flags & MessageFlag::HasFromId) ? from : 0) { createComponentsHelper( flags, replyTo, viaBotId, postAuthor, std::move(markup)); _media = std::make_unique(this, game); setTextValue({}); } HistoryMessage::HistoryMessage( not_null history, MsgId id, Data::SponsoredFrom from, const TextWithEntities &textWithEntities) : HistoryItem( history, id, ((history->peer->isChannel() ? MessageFlag::Post : MessageFlag(0)) //| (from.peer ? MessageFlag::HasFromId : MessageFlag(0)) | MessageFlag::Local), HistoryItem::NewMessageDate(0), /*from.peer ? from.peer->id : */PeerId(0)) { createComponentsHelper( _flags, MsgId(0), // replyTo UserId(0), // viaBotId QString(), // postAuthor HistoryMessageMarkupData()); setText(textWithEntities); setSponsoredFrom(from); } void HistoryMessage::createComponentsHelper( MessageFlags flags, MsgId replyTo, UserId viaBotId, const QString &postAuthor, HistoryMessageMarkupData &&markup) { auto config = CreateConfig(); config.viaBotId = viaBotId; if (flags & MessageFlag::HasReplyInfo) { config.replyTo = replyTo; const auto to = LookupReplyTo(history(), replyTo); const auto replyToTop = LookupReplyToTop(to); config.replyToTop = replyToTop ? replyToTop : replyTo; const auto forum = history()->peer->forum(); config.replyIsTopicPost = LookupReplyIsTopicPost(to) || (to && to->Has()) || (forum && forum->creating(replyToTop)); } config.markup = std::move(markup); if (flags & MessageFlag::HasPostAuthor) config.author = postAuthor; if (flags & MessageFlag::HasViews) config.viewsCount = 1; createComponents(std::move(config)); } int HistoryMessage::viewsCount() const { if (const auto views = Get()) { return std::max(views->views.count, 0); } return HistoryItem::viewsCount(); } bool HistoryMessage::checkCommentsLinkedChat(ChannelId id) const { if (!id) { return true; } else if (const auto channel = history()->peer->asChannel()) { if (channel->linkedChatKnown() || !(channel->flags() & ChannelDataFlag::HasLink)) { const auto linked = channel->linkedChat(); if (!linked || peerToChannel(linked->id) != id) { return false; } } return true; } return false; } int HistoryMessage::repliesCount() const { if (const auto views = Get()) { if (!checkCommentsLinkedChat(views->commentsMegagroupId)) { return 0; } return std::max(views->replies.count, 0); } return HistoryItem::repliesCount(); } bool HistoryMessage::repliesAreComments() const { if (const auto views = Get()) { return (views->commentsMegagroupId != 0) && checkCommentsLinkedChat(views->commentsMegagroupId); } return HistoryItem::repliesAreComments(); } bool HistoryMessage::externalReply() const { if (!history()->peer->isRepliesChat()) { return false; } else if (const auto forwarded = Get()) { return forwarded->savedFromPeer && forwarded->savedFromMsgId; } return false; } void HistoryMessage::setCommentsInboxReadTill(MsgId readTillId) { const auto views = Get(); if (!views) { return; } const auto newReadTillId = std::max(readTillId.bare, int64(1)); const auto ignore = (newReadTillId < views->commentsInboxReadTillId); if (ignore) { return; } const auto changed = (newReadTillId > views->commentsInboxReadTillId); if (!changed) { return; } const auto wasUnread = areCommentsUnread(); views->commentsInboxReadTillId = newReadTillId; if (wasUnread && !areCommentsUnread()) { history()->owner().requestItemRepaint(this); } } void HistoryMessage::setCommentsMaxId(MsgId maxId) { if (const auto views = Get()) { if (views->commentsMaxId != maxId) { const auto wasUnread = areCommentsUnread(); views->commentsMaxId = maxId; if (wasUnread != areCommentsUnread()) { history()->owner().requestItemRepaint(this); } } } } void HistoryMessage::setCommentsPossibleMaxId(MsgId possibleMaxId) { if (const auto views = Get()) { if (views->commentsMaxId < possibleMaxId) { const auto wasUnread = areCommentsUnread(); views->commentsMaxId = possibleMaxId; if (!wasUnread && areCommentsUnread()) { history()->owner().requestItemRepaint(this); } } } } bool HistoryMessage::areCommentsUnread() const { const auto views = Get(); if (!views || !views->commentsMegagroupId || !checkCommentsLinkedChat(views->commentsMegagroupId)) { return false; } const auto till = views->commentsInboxReadTillId; if (views->commentsInboxReadTillId < 2 || views->commentsMaxId <= till) { return false; } const auto group = views->commentsMegagroupId ? history()->owner().historyLoaded( peerFromChannel(views->commentsMegagroupId)) : history().get(); return !group || (views->commentsMaxId > group->inboxReadTillId()); } FullMsgId HistoryMessage::commentsItemId() const { if (const auto views = Get()) { return FullMsgId( PeerId(views->commentsMegagroupId), views->commentsRootId); } return FullMsgId(); } void HistoryMessage::setCommentsItemId(FullMsgId id) { if (id.peer == _history->peer->id) { if (id.msg != this->id) { if (const auto reply = Get()) { reply->replyToMsgTop = id.msg; } } } else if (const auto views = Get()) { if (const auto channelId = peerToChannel(id.peer)) { if (views->commentsMegagroupId != channelId) { views->commentsMegagroupId = channelId; history()->owner().requestItemResize(this); } views->commentsRootId = id.msg; } } } bool HistoryMessage::updateDependencyItem() { if (const auto reply = Get()) { const auto documentId = reply->replyToDocumentId; const auto webpageId = reply->replyToWebPageId; const auto result = reply->updateData(this, true); const auto mediaIdChanged = (documentId != reply->replyToDocumentId) || (webpageId != reply->replyToWebPageId); if (mediaIdChanged && generateLocalEntitiesByReply()) { history()->owner().requestItemTextRefresh(this); } return result; } return true; } void HistoryMessage::applySentMessage(const MTPDmessage &data) { HistoryItem::applySentMessage(data); if (const auto period = data.vttl_period(); period && period->v > 0) { applyTTL(data.vdate().v + period->v); } else { applyTTL(0); } } void HistoryMessage::applySentMessage( const QString &text, const MTPDupdateShortSentMessage &data, bool wasAlready) { HistoryItem::applySentMessage(text, data, wasAlready); if (const auto period = data.vttl_period(); period && period->v > 0) { applyTTL(data.vdate().v + period->v); } else { applyTTL(0); } } bool HistoryMessage::allowsForward() const { return isRegular() && !forbidsForward() && history()->peer->allowsForwarding() && (!_media || _media->allowsForward()); } bool HistoryMessage::allowsSendNow() const { return isScheduled() && !isSending() && !hasFailed() && !isEditingMedia(); } bool HistoryMessage::isTooOldForEdit(TimeId now) const { return !_history->peer->canEditMessagesIndefinitely() && !isScheduled() && (now - date() >= _history->session().serverConfig().editTimeLimit); } bool HistoryMessage::allowsEdit(TimeId now) const { return canBeEdited() && !isTooOldForEdit(now) && (!_media || _media->allowsEdit()) && !isLegacyMessage() && !isEditingMedia(); } void HistoryMessage::createComponents(CreateConfig &&config) { uint64 mask = 0; if (config.replyTo) { mask |= HistoryMessageReply::Bit(); } if (config.viaBotId) { mask |= HistoryMessageVia::Bit(); } if (config.viewsCount >= 0 || !config.replies.isNull) { mask |= HistoryMessageViews::Bit(); } if (!config.author.isEmpty()) { mask |= HistoryMessageSigned::Bit(); } else if (_history->peer->isMegagroup() // Discussion posts signatures. && config.savedFromPeer && !config.authorOriginal.isEmpty()) { const auto savedFrom = _history->owner().peerLoaded( config.savedFromPeer); if (savedFrom && savedFrom->isChannel()) { mask |= HistoryMessageSigned::Bit(); } } else if ((_history->peer->isSelf() || _history->peer->isRepliesChat()) && !config.authorOriginal.isEmpty()) { mask |= HistoryMessageSigned::Bit(); } if (config.editDate != TimeId(0)) { mask |= HistoryMessageEdited::Bit(); } if (config.originalDate != 0) { mask |= HistoryMessageForwarded::Bit(); } if (!config.markup.isTrivial()) { mask |= HistoryMessageReplyMarkup::Bit(); } else if (config.inlineMarkup) { mask |= HistoryMessageReplyMarkup::Bit(); } UpdateComponents(mask); if (const auto reply = Get()) { reply->replyToPeerId = config.replyToPeer; reply->replyToMsgId = config.replyTo; reply->replyToMsgTop = isScheduled() ? 0 : config.replyToTop; reply->topicPost = config.replyIsTopicPost; if (!reply->updateData(this)) { RequestDependentMessageData( this, reply->replyToPeerId, reply->replyToMsgId); } } if (const auto via = Get()) { via->create(&history()->owner(), config.viaBotId); } if (const auto views = Get()) { changeViewsCount(config.viewsCount); if (config.replies.isNull && isSending() && config.markup.isNull()) { if (const auto broadcast = history()->peer->asBroadcast()) { if (const auto linked = broadcast->linkedChat()) { config.replies.isNull = false; config.replies.channelId = peerToChannel(linked->id); } } } setReplies(std::move(config.replies)); } if (const auto edited = Get()) { edited->date = config.editDate; } if (const auto msgsigned = Get()) { msgsigned->author = config.author.isEmpty() ? config.authorOriginal : config.author; msgsigned->isAnonymousRank = !isDiscussionPost() && author()->isMegagroup(); } setupForwardedComponent(config); if (const auto markup = Get()) { if (!config.markup.isTrivial()) { markup->updateData(std::move(config.markup)); } else if (config.inlineMarkup) { markup->createForwarded(*config.inlineMarkup); } if (markup->data.flags & ReplyMarkupFlag::HasSwitchInlineButton) { _flags |= MessageFlag::HasSwitchInlineButton; } } else if (!config.markup.isNull()) { _flags |= MessageFlag::HasReplyMarkup; } else { _flags &= ~MessageFlag::HasReplyMarkup; } } bool HistoryMessage::checkRepliesPts( const HistoryMessageRepliesData &data) const { const auto channel = history()->peer->asChannel(); const auto pts = channel ? channel->pts() : history()->session().updates().pts(); return (data.pts >= pts); } void HistoryMessage::setupForwardedComponent(const CreateConfig &config) { const auto forwarded = Get(); if (!forwarded) { return; } forwarded->originalDate = config.originalDate; const auto originalSender = config.senderOriginal ? config.senderOriginal : !config.senderNameOriginal.isEmpty() ? PeerId() : from()->id; forwarded->originalSender = originalSender ? history()->owner().peer(originalSender).get() : nullptr; if (!forwarded->originalSender) { forwarded->hiddenSenderInfo = std::make_unique( config.senderNameOriginal, config.imported); } forwarded->originalId = config.originalId; forwarded->originalAuthor = config.authorOriginal; forwarded->psaType = config.forwardPsaType; forwarded->savedFromPeer = history()->owner().peerLoaded( config.savedFromPeer); forwarded->savedFromMsgId = config.savedFromMsgId; forwarded->imported = config.imported; } void HistoryMessage::refreshMedia(const MTPMessageMedia *media) { const auto was = (_media != nullptr); if (const auto invoice = was ? _media->invoice() : nullptr) { if (invoice->extendedMedia) { return; } } _media = nullptr; if (media) { setMedia(*media); } if (was || _media) { if (const auto views = Get()) { refreshRepliesText(views); } } } void HistoryMessage::refreshSentMedia(const MTPMessageMedia *media) { const auto wasGrouped = history()->owner().groups().isGrouped(this); refreshMedia(media); if (wasGrouped) { history()->owner().groups().refreshMessage(this); } else { history()->owner().requestItemViewRefresh(this); } } void HistoryMessage::returnSavedMedia() { if (!isEditingMedia()) { return; } const auto wasGrouped = history()->owner().groups().isGrouped(this); _media = std::move(_savedLocalEditMediaData->media); setText(_savedLocalEditMediaData->text); clearSavedMedia(); if (wasGrouped) { history()->owner().groups().refreshMessage(this, true); } else { history()->owner().requestItemViewRefresh(this); history()->owner().updateDependentMessages(this); } } void HistoryMessage::setMedia(const MTPMessageMedia &media) { _media = CreateMedia(this, media); checkBuyButton(); } void HistoryMessage::checkBuyButton() { if (const auto invoice = _media ? _media->invoice() : nullptr) { if (invoice->receiptMsgId) { replaceBuyWithReceiptInMarkup(); } } } std::unique_ptr HistoryMessage::CreateMedia( not_null item, const MTPMessageMedia &media) { using Result = std::unique_ptr; return media.match([&](const MTPDmessageMediaContact &media) -> Result { return std::make_unique( item, media.vuser_id().v, qs(media.vfirst_name()), qs(media.vlast_name()), qs(media.vphone_number())); }, [&](const MTPDmessageMediaGeo &media) -> Result { return media.vgeo().match([&](const MTPDgeoPoint &point) -> Result { return std::make_unique( item, Data::LocationPoint(point)); }, [](const MTPDgeoPointEmpty &) -> Result { return nullptr; }); }, [&](const MTPDmessageMediaGeoLive &media) -> Result { return media.vgeo().match([&](const MTPDgeoPoint &point) -> Result { return std::make_unique( item, Data::LocationPoint(point)); }, [](const MTPDgeoPointEmpty &) -> Result { return nullptr; }); }, [&](const MTPDmessageMediaVenue &media) -> Result { return media.vgeo().match([&](const MTPDgeoPoint &point) -> Result { return std::make_unique( item, Data::LocationPoint(point), qs(media.vtitle()), qs(media.vaddress())); }, [](const MTPDgeoPointEmpty &data) -> Result { return nullptr; }); }, [&](const MTPDmessageMediaPhoto &media) -> Result { const auto photo = media.vphoto(); if (media.vttl_seconds()) { LOG(("App Error: " "Unexpected MTPMessageMediaPhoto " "with ttl_seconds in HistoryMessage.")); return nullptr; } else if (!photo) { LOG(("API Error: " "Got MTPMessageMediaPhoto " "without photo and without ttl_seconds.")); return nullptr; } return photo->match([&](const MTPDphoto &photo) -> Result { return std::make_unique( item, item->history()->owner().processPhoto(photo)); }, [](const MTPDphotoEmpty &) -> Result { return nullptr; }); }, [&](const MTPDmessageMediaDocument &media) -> Result { const auto document = media.vdocument(); if (media.vttl_seconds()) { LOG(("App Error: " "Unexpected MTPMessageMediaDocument " "with ttl_seconds in HistoryMessage.")); return nullptr; } else if (!document) { LOG(("API Error: " "Got MTPMessageMediaDocument " "without document and without ttl_seconds.")); return nullptr; } return document->match([&](const MTPDdocument &document) -> Result { return std::make_unique( item, item->history()->owner().processDocument(document), media.is_nopremium()); }, [](const MTPDdocumentEmpty &) -> Result { return nullptr; }); }, [&](const MTPDmessageMediaWebPage &media) { return media.vwebpage().match([](const MTPDwebPageEmpty &) -> Result { return nullptr; }, [&](const MTPDwebPagePending &webpage) -> Result { return std::make_unique( item, item->history()->owner().processWebpage(webpage)); }, [&](const MTPDwebPage &webpage) -> Result { return std::make_unique( item, item->history()->owner().processWebpage(webpage)); }, [](const MTPDwebPageNotModified &) -> Result { LOG(("API Error: " "webPageNotModified is unexpected in message media.")); return nullptr; }); }, [&](const MTPDmessageMediaGame &media) -> Result { return media.vgame().match([&](const MTPDgame &game) { return std::make_unique( item, item->history()->owner().processGame(game)); }); }, [&](const MTPDmessageMediaInvoice &media) -> Result { return std::make_unique( item, Data::ComputeInvoiceData(item, media)); }, [&](const MTPDmessageMediaPoll &media) -> Result { return std::make_unique( item, item->history()->owner().processPoll(media)); }, [&](const MTPDmessageMediaDice &media) -> Result { return std::make_unique( item, qs(media.vemoticon()), media.vvalue().v); }, [](const MTPDmessageMediaEmpty &) -> Result { return nullptr; }, [](const MTPDmessageMediaUnsupported &) -> Result { return nullptr; }); } void HistoryMessage::replaceBuyWithReceiptInMarkup() { if (const auto markup = inlineReplyMarkup()) { for (auto &row : markup->data.rows) { for (auto &button : row) { if (button.type == HistoryMessageMarkupButton::Type::Buy) { const auto receipt = tr::lng_payments_receipt_button(tr::now); if (button.text != receipt) { button.text = receipt; if (markup->inlineKeyboard) { markup->inlineKeyboard = nullptr; history()->owner().requestItemResize(this); } } } } } } } void HistoryMessage::applyEdition(HistoryMessageEdition &&edition) { int keyboardTop = -1; //if (!pendingResize()) {// #TODO edit bot message // if (auto keyboard = inlineReplyKeyboard()) { // int h = st::msgBotKbButton.margin + keyboard->naturalHeight(); // keyboardTop = _height - h + st::msgBotKbButton.margin - marginBottom(); // } //} if (edition.isEditHide) { _flags |= MessageFlag::HideEdited; } else { _flags &= ~MessageFlag::HideEdited; } if (edition.editDate != -1) { //_flags |= MTPDmessage::Flag::f_edit_date; if (!Has()) { AddComponents(HistoryMessageEdited::Bit()); } auto edited = Get(); edited->date = edition.editDate; } if (!edition.useSameMarkup) { setReplyMarkup(base::take(edition.replyMarkup)); } if (!isLocalUpdateMedia()) { refreshMedia(edition.mtpMedia); } if (!edition.useSameReactions) { updateReactions(edition.mtpReactions); } changeViewsCount(edition.views); setForwardsCount(edition.forwards); setText(_media ? edition.textWithEntities : EnsureNonEmpty(edition.textWithEntities)); if (!edition.useSameReplies) { if (!edition.replies.isNull) { if (checkRepliesPts(edition.replies)) { setReplies(base::take(edition.replies)); } } else { clearReplies(); } } applyTTL(edition.ttl); finishEdition(keyboardTop); } void HistoryMessage::applyEdition(const MTPDmessageService &message) { if (message.vaction().type() == mtpc_messageActionHistoryClear) { const auto wasGrouped = history()->owner().groups().isGrouped(this); setReplyMarkup({}); refreshMedia(nullptr); setTextValue({}); changeViewsCount(-1); setForwardsCount(-1); if (wasGrouped) { history()->owner().groups().unregisterMessage(this); } finishEditionToEmpty(); } } void HistoryMessage::applyEdition(const MTPMessageExtendedMedia &media) { if (const auto existing = this->media()) { if (existing->updateExtendedMedia(this, media)) { checkBuyButton(); finishEdition(-1); } } } void HistoryMessage::updateSentContent( const TextWithEntities &textWithEntities, const MTPMessageMedia *media) { setText(textWithEntities); if (_flags & MessageFlag::FromInlineBot) { if (!media || !_media || !_media->updateInlineResultMedia(*media)) { refreshSentMedia(media); } _flags &= ~MessageFlag::FromInlineBot; } else if (media || _media) { if (!media || !_media || !_media->updateSentMedia(*media)) { refreshSentMedia(media); } } history()->owner().requestItemResize(this); } void HistoryMessage::updateForwardedInfo(const MTPMessageFwdHeader *fwd) { const auto forwarded = Get(); if (!fwd) { if (forwarded) { LOG(("API Error: Server removed forwarded information.")); } return; } else if (!forwarded) { LOG(("API Error: Server added forwarded information.")); return; } fwd->match([&](const MTPDmessageFwdHeader &data) { auto config = CreateConfig(); FillForwardedInfo(config, data); setupForwardedComponent(config); history()->owner().requestItemResize(this); }); } void HistoryMessage::updateReplyMarkup(HistoryMessageMarkupData &&markup) { setReplyMarkup(std::move(markup)); } void HistoryMessage::contributeToSlowmode(TimeId realDate) { if (const auto channel = history()->peer->asChannel()) { if (out() && isRegular()) { channel->growSlowmodeLastMessage(realDate ? realDate : date()); } } } void HistoryMessage::addToUnreadThings(HistoryUnreadThings::AddType type) { if (!isRegular()) { return; } const auto mention = isUnreadMention(); const auto reaction = hasUnreadReaction(); if (!mention && !reaction) { return; } const auto topic = this->topic(); const auto history = this->history(); const auto changes = &history->session().changes(); if (mention) { if (history->unreadMentions().add(id, type)) { changes->historyUpdated( history, Data::HistoryUpdate::Flag::UnreadMentions); } if (topic && topic->unreadMentions().add(id, type)) { changes->topicUpdated( topic, Data::TopicUpdate::Flag::UnreadMentions); } } if (reaction) { const auto toHistory = history->unreadReactions().add(id, type); const auto toTopic = topic && topic->unreadReactions().add(id, type); if (toHistory || toTopic) { if (type == HistoryUnreadThings::AddType::New) { changes->messageUpdated( this, Data::MessageUpdate::Flag::NewUnreadReaction); } if (hasUnreadReaction()) { if (toHistory) { changes->historyUpdated( history, Data::HistoryUpdate::Flag::UnreadReactions); } if (toTopic) { changes->topicUpdated( topic, Data::TopicUpdate::Flag::UnreadReactions); } } } } } void HistoryMessage::destroyHistoryEntry() { if (isUnreadMention()) { history()->unreadMentions().erase(id); if (const auto topic = this->topic()) { topic->unreadMentions().erase(id); } } if (hasUnreadReaction()) { history()->unreadReactions().erase(id); if (const auto topic = this->topic()) { topic->unreadReactions().erase(id); } } if (const auto reply = Get()) { changeReplyToTopCounter(reply, -1); } } Storage::SharedMediaTypesMask HistoryMessage::sharedMediaTypes() const { auto result = Storage::SharedMediaTypesMask {}; if (const auto media = this->media()) { result.set(media->sharedMediaTypes()); } if (hasTextLinks()) { result.set(Storage::SharedMediaType::Link); } if (isPinned()) { result.set(Storage::SharedMediaType::Pinned); } return result; } bool HistoryMessage::generateLocalEntitiesByReply() const { using namespace HistoryView; if (!_media) { return true; } else if (const auto document = _media->document()) { return !DurationForTimestampLinks(document); } else if (const auto webpage = _media->webpage()) { return (webpage->type != WebPageType::Video) && !DurationForTimestampLinks(webpage); } return true; } TextWithEntities HistoryMessage::withLocalEntities( const TextWithEntities &textWithEntities) const { using namespace HistoryView; if (!generateLocalEntitiesByReply()) { if (!_media) { } else if (const auto document = _media->document()) { if (const auto duration = DurationForTimestampLinks(document)) { return AddTimestampLinks( textWithEntities, duration, TimestampLinkBase(document, fullId())); } } else if (const auto webpage = _media->webpage()) { if (const auto duration = DurationForTimestampLinks(webpage)) { return AddTimestampLinks( textWithEntities, duration, TimestampLinkBase(webpage, fullId())); } } return textWithEntities; } if (const auto reply = Get()) { const auto document = reply->replyToDocumentId ? history()->owner().document(reply->replyToDocumentId).get() : nullptr; const auto webpage = reply->replyToWebPageId ? history()->owner().webpage(reply->replyToWebPageId).get() : nullptr; if (document) { if (const auto duration = DurationForTimestampLinks(document)) { const auto context = reply->replyToMsg->fullId(); return AddTimestampLinks( textWithEntities, duration, TimestampLinkBase(document, context)); } } else if (webpage) { if (const auto duration = DurationForTimestampLinks(webpage)) { const auto context = reply->replyToMsg->fullId(); return AddTimestampLinks( textWithEntities, duration, TimestampLinkBase(webpage, context)); } } } return textWithEntities; } MsgId HistoryMessage::replyToId() const { if (const auto reply = Get()) { return reply->replyToId(); } return 0; } MsgId HistoryMessage::replyToTop() const { if (const auto reply = Get()) { return reply->replyToTop(); } return 0; } MsgId HistoryMessage::topicRootId() const { if (const auto reply = Get() ; reply && reply->topicPost) { return reply->replyToTop(); } return 0; } void HistoryMessage::setText(const TextWithEntities &textWithEntities) { for (const auto &entity : textWithEntities.entities) { auto type = entity.type(); if (type == EntityType::Url || type == EntityType::CustomUrl || type == EntityType::Email) { _flags |= MessageFlag::HasTextLinks; break; } } setTextValue((_media && _media->consumeMessageText(textWithEntities)) ? TextWithEntities() : std::move(textWithEntities)); } void HistoryMessage::setTextValue(TextWithEntities text) { const auto had = !_text.empty(); _text = std::move(text); if (had) { history()->owner().requestItemTextRefresh(this); } } void HistoryMessage::setReplyMarkup(HistoryMessageMarkupData &&markup) { const auto requestUpdate = [&] { history()->owner().requestItemResize(this); history()->session().changes().messageUpdated( this, Data::MessageUpdate::Flag::ReplyMarkup); }; if (markup.isNull()) { if (_flags & MessageFlag::HasReplyMarkup) { _flags &= ~MessageFlag::HasReplyMarkup; if (Has()) { RemoveComponents(HistoryMessageReplyMarkup::Bit()); } requestUpdate(); } return; } // optimization: don't create markup component for the case // MTPDreplyKeyboardHide with flags = 0, assume it has f_zero flag if (markup.isTrivial()) { bool changed = false; if (Has()) { RemoveComponents(HistoryMessageReplyMarkup::Bit()); changed = true; } if (!(_flags & MessageFlag::HasReplyMarkup)) { _flags |= MessageFlag::HasReplyMarkup; changed = true; } if (changed) { requestUpdate(); } } else { if (!(_flags & MessageFlag::HasReplyMarkup)) { _flags |= MessageFlag::HasReplyMarkup; } if (!Has()) { AddComponents(HistoryMessageReplyMarkup::Bit()); } Get()->updateData(std::move(markup)); requestUpdate(); } } TextWithEntities HistoryMessage::originalText() const { return _text; } TextWithEntities HistoryMessage::originalTextWithLocalEntities() const { return withLocalEntities(originalText()); } TextForMimeData HistoryMessage::clipboardText() const { return TextForMimeData::WithExpandedLinks(_text); } bool HistoryMessage::changeViewsCount(int count) { const auto views = Get(); if (!views || views->views.count == count || (count >= 0 && views->views.count > count)) { return false; } views->views.count = count; return true; } void HistoryMessage::setForwardsCount(int count) { } void HistoryMessage::setPostAuthor(const QString &author) { auto msgsigned = Get(); if (author.isEmpty()) { if (!msgsigned) { return; } RemoveComponents(HistoryMessageSigned::Bit()); history()->owner().requestItemResize(this); return; } if (!msgsigned) { AddComponents(HistoryMessageSigned::Bit()); msgsigned = Get(); } else if (msgsigned->author == author) { return; } msgsigned->author = author; msgsigned->isAnonymousRank = !isDiscussionPost() && this->author()->isMegagroup(); history()->owner().requestItemResize(this); } void HistoryMessage::setReplies(HistoryMessageRepliesData &&data) { if (data.isNull) { return; } auto views = Get(); if (!views) { AddComponents(HistoryMessageViews::Bit()); views = Get(); } const auto &repliers = data.recentRepliers; const auto count = data.repliesCount; const auto channelId = data.channelId; const auto readTillId = data.readMaxId ? std::max({ views->commentsInboxReadTillId.bare, data.readMaxId.bare, int64(1), }) : views->commentsInboxReadTillId; const auto maxId = data.maxId ? data.maxId : views->commentsMaxId; const auto countsChanged = (views->replies.count != count) || (views->commentsInboxReadTillId != readTillId) || (views->commentsMaxId != maxId); const auto megagroupChanged = (views->commentsMegagroupId != channelId); const auto recentChanged = (views->recentRepliers != repliers); if (!countsChanged && !megagroupChanged && !recentChanged) { return; } views->replies.count = count; if (recentChanged) { views->recentRepliers = repliers; } const auto wasUnread = areCommentsUnread(); views->commentsMegagroupId = channelId; views->commentsInboxReadTillId = readTillId; views->commentsMaxId = maxId; if (wasUnread != areCommentsUnread()) { history()->owner().requestItemRepaint(this); } refreshRepliesText(views, megagroupChanged); } void HistoryMessage::clearReplies() { auto views = Get(); if (!views) { return; } const auto viewsPart = views->views; if (viewsPart.count < 0) { RemoveComponents(HistoryMessageViews::Bit()); } else { *views = HistoryMessageViews(); views->views = viewsPart; } history()->owner().requestItemResize(this); } void HistoryMessage::refreshRepliesText( not_null views, bool forceResize) { if (views->commentsMegagroupId) { views->replies.text = (views->replies.count > 0) ? tr::lng_comments_open_count( tr::now, lt_count_short, views->replies.count) : tr::lng_comments_open_none(tr::now); views->replies.textWidth = st::semiboldFont->width( views->replies.text); views->repliesSmall.text = (views->replies.count > 0) ? Lang::FormatCountToShort(views->replies.count).string : QString(); const auto hadText = (views->repliesSmall.textWidth > 0); views->repliesSmall.textWidth = (views->replies.count > 0) ? st::semiboldFont->width(views->repliesSmall.text) : 0; const auto hasText = (views->repliesSmall.textWidth > 0); if (hasText != hadText) { forceResize = true; } } if (forceResize) { history()->owner().requestItemResize(this); } else { history()->owner().requestItemRepaint(this); } } void HistoryMessage::changeRepliesCount(int delta, PeerId replier) { const auto views = Get(); const auto limit = HistoryMessageViews::kMaxRecentRepliers; if (!views) { return; } // Update full count. if (views->replies.count < 0) { return; } views->replies.count = std::max(views->replies.count + delta, 0); if (replier && views->commentsMegagroupId) { if (delta < 0) { views->recentRepliers.erase( ranges::remove(views->recentRepliers, replier), end(views->recentRepliers)); } else if (!ranges::contains(views->recentRepliers, replier)) { views->recentRepliers.insert(views->recentRepliers.begin(), replier); while (views->recentRepliers.size() > limit) { views->recentRepliers.pop_back(); } } } refreshRepliesText(views); history()->owner().notifyItemDataChange(this); } void HistoryMessage::setSponsoredFrom(const Data::SponsoredFrom &from) { AddComponents(HistoryMessageSponsored::Bit()); const auto sponsored = Get(); sponsored->sender = std::make_unique( from.title, false); sponsored->recommended = from.isRecommended; if (from.userpic.location.valid()) { sponsored->sender->customUserpic.set( &history()->session(), from.userpic); } using Type = HistoryMessageSponsored::Type; sponsored->type = from.isExactPost ? Type::Post : from.isBot ? Type::Bot : from.isBroadcast ? Type::Broadcast : (from.peer && from.peer->isUser()) ? Type::User : Type::Group; } void HistoryMessage::setReplyFields( MsgId replyTo, MsgId replyToTop, bool isForumPost) { const auto reply = Get(); if (!reply || isScheduled()) { return; } reply->topicPost = isForumPost; if ((reply->replyToMsgId != replyTo) && !IsServerMsgId(reply->replyToMsgId)) { reply->replyToMsgId = replyTo; if (!reply->updateData(this)) { RequestDependentMessageData( this, reply->replyToPeerId, reply->replyToMsgId); } } if ((reply->replyToMsgTop != replyToTop) && !IsServerMsgId(reply->replyToMsgTop)) { reply->replyToMsgTop = replyToTop; changeReplyToTopCounter(reply, 1); } } void HistoryMessage::setRealId(MsgId newId) { HistoryItem::setRealId(newId); history()->owner().groups().refreshMessage(this); history()->owner().requestItemResize(this); if (const auto reply = Get()) { if (reply->replyToLink()) { reply->setReplyToLinkFrom(this); } changeReplyToTopCounter(reply, 1); } } void HistoryMessage::incrementReplyToTopCounter() { if (const auto reply = Get()) { changeReplyToTopCounter(reply, 1); } } void HistoryMessage::changeReplyToTopCounter( not_null reply, int delta) { if (!isRegular() || !_history->peer->isMegagroup()) { return; } if (!out() && delta > 0) { _history->session().changes().messageUpdated( this, Data::MessageUpdate::Flag::ReplyToTopAdded); } const auto topId = reply->replyToTop(); if (!topId) { return; } const auto top = _history->owner().message(_history->peer->id, topId); if (!top) { return; } const auto from = displayFrom(); const auto replier = from ? from->id : PeerId(); top->changeRepliesCount(delta, replier); if (const auto original = top->lookupDiscussionPostOriginal()) { original->changeRepliesCount(delta, replier); } } void HistoryMessage::dependencyItemRemoved(HistoryItem *dependency) { if (const auto reply = Get()) { const auto documentId = reply->replyToDocumentId; reply->itemRemoved(this, dependency); if (documentId != reply->replyToDocumentId && generateLocalEntitiesByReply()) { history()->owner().requestItemTextRefresh(this); } } } QString HistoryMessage::notificationHeader() const { if (out() && isFromScheduled() && !_history->peer->isSelf()) { return tr::lng_from_you(tr::now); } else if (!_history->peer->isUser() && !isPost()) { return from()->name(); } return QString(); } std::unique_ptr HistoryMessage::createView( not_null delegate, HistoryView::Element *replacing) { return delegate->elementCreate(this, replacing); } HistoryMessage::~HistoryMessage() { _media.reset(); clearSavedMedia(); if (auto reply = Get()) { reply->clearData(this); } }