/* 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 "data/data_media_types.h" #include "history/history_media_types.h" #include "history/history_item.h" #include "history/history_location_manager.h" #include "history/view/history_view_element.h" #include "storage/storage_shared_media.h" #include "storage/localstorage.h" #include "data/data_session.h" #include "lang/lang_keys.h" #include "auth_session.h" #include "layout.h" namespace Data { namespace { Call ComputeCallData(const MTPDmessageActionPhoneCall &call) { auto result = Call(); result.finishReason = [&] { if (call.has_reason()) { switch (call.vreason.type()) { case mtpc_phoneCallDiscardReasonBusy: return CallFinishReason::Busy; case mtpc_phoneCallDiscardReasonDisconnect: return CallFinishReason::Disconnected; case mtpc_phoneCallDiscardReasonHangup: return CallFinishReason::Hangup; case mtpc_phoneCallDiscardReasonMissed: return CallFinishReason::Missed; } Unexpected("Call reason type."); } return CallFinishReason::Hangup; }(); result.duration = call.has_duration() ? call.vduration.v : 0;; return result; } Invoice ComputeInvoiceData(const MTPDmessageMediaInvoice &data) { auto result = Invoice(); result.isTest = data.is_test(); result.amount = data.vtotal_amount.v; result.currency = qs(data.vcurrency); result.description = qs(data.vdescription); result.title = TextUtilities::SingleLine(qs(data.vtitle)); if (data.has_receipt_msg_id()) { result.receiptMsgId = data.vreceipt_msg_id.v; } if (data.has_photo() && data.vphoto.type() == mtpc_webDocument) { auto &doc = data.vphoto.c_webDocument(); auto imageSize = QSize(); for (auto &attribute : doc.vattributes.v) { if (attribute.type() == mtpc_documentAttributeImageSize) { auto &size = attribute.c_documentAttributeImageSize(); imageSize = QSize(size.vw.v, size.vh.v); break; } } if (!imageSize.isEmpty()) { auto thumbsize = shrinkToKeepAspect(imageSize.width(), imageSize.height(), 100, 100); auto thumb = ImagePtr(thumbsize.width(), thumbsize.height()); auto mediumsize = shrinkToKeepAspect(imageSize.width(), imageSize.height(), 320, 320); auto medium = ImagePtr(mediumsize.width(), mediumsize.height()); // We don't use size from WebDocument, because it is not reliable. // It can be > 0 and different from the real size that we get in upload.WebFile result. auto filesize = 0; // doc.vsize.v; auto full = ImagePtr( WebFileImageLocation( imageSize.width(), imageSize.height(), doc.vdc_id.v, doc.vurl.v, doc.vaccess_hash.v), filesize); auto photoId = rand_value(); result.photo = Auth().data().photo( photoId, uint64(0), unixtime(), thumb, medium, full); } } return result; } QString WithCaptionDialogsText( const QString &attachType, const QString &caption) { if (caption.isEmpty()) { return textcmdLink(1, TextUtilities::Clean(attachType)); } auto captionText = TextUtilities::Clean(caption); auto attachTypeWrapped = textcmdLink(1, lng_dialogs_text_media_wrapped( lt_media, TextUtilities::Clean(attachType))); return lng_dialogs_text_media( lt_media_part, attachTypeWrapped, lt_caption, captionText); } QString WithCaptionNotificationText( const QString &attachType, const QString &caption) { if (caption.isEmpty()) { return attachType; } auto attachTypeWrapped = lng_dialogs_text_media_wrapped( lt_media, attachType); return lng_dialogs_text_media( lt_media_part, attachTypeWrapped, lt_caption, caption); } } // namespace Media::Media(not_null parent) : _parent(parent) { } not_null Media::parent() const { return _parent; } DocumentData *Media::document() const { return nullptr; } PhotoData *Media::photo() const { return nullptr; } WebPageData *Media::webpage() const { return nullptr; } const SharedContact *Media::sharedContact() const { return nullptr; } const Call *Media::call() const { return nullptr; } GameData *Media::game() const { return nullptr; } const Invoice *Media::invoice() const { return nullptr; } LocationData *Media::location() const { return nullptr; } bool Media::uploading() const { return false; } Storage::SharedMediaTypesMask Media::sharedMediaTypes() const { return {}; } bool Media::canBeGrouped() const { return false; } QString Media::chatsListText() const { auto result = notificationText(); return result.isEmpty() ? QString() : textcmdLink(1, TextUtilities::Clean(std::move(result))); } bool Media::hasReplyPreview() const { return false; } ImagePtr Media::replyPreview() const { return ImagePtr(); } bool Media::allowsForward() const { return true; } bool Media::allowsEdit() const { return allowsEditCaption(); } bool Media::allowsEditCaption() const { return false; } bool Media::allowsRevoke() const { return true; } bool Media::forwardedBecomesUnread() const { return false; } QString Media::errorTextForForward(not_null channel) const { return QString(); } bool Media::consumeMessageText(const TextWithEntities &text) { return false; } std::unique_ptr Media::createView( not_null message) { return createView(message, message->data()); } MediaPhoto::MediaPhoto( not_null parent, not_null photo) : Media(parent) , _photo(photo) { } MediaPhoto::MediaPhoto( not_null parent, not_null chat, not_null photo) : Media(parent) , _photo(photo) , _chat(chat) { } MediaPhoto::~MediaPhoto() { } std::unique_ptr MediaPhoto::clone(not_null parent) { return _chat ? std::make_unique(parent, _chat, _photo) : std::make_unique(parent, _photo); } PhotoData *MediaPhoto::photo() const { return _photo; } bool MediaPhoto::uploading() const { return _photo->uploading(); } Storage::SharedMediaTypesMask MediaPhoto::sharedMediaTypes() const { using Type = Storage::SharedMediaType; if (_chat) { return Type::ChatPhoto; } return Storage::SharedMediaTypesMask{} .added(Type::Photo) .added(Type::PhotoVideo); } bool MediaPhoto::canBeGrouped() const { return true; } QString MediaPhoto::notificationText() const { return WithCaptionNotificationText( lang(lng_in_dlg_photo), parent()->originalText().text); //return WithCaptionNotificationText(lang(lng_in_dlg_album), _caption); } QString MediaPhoto::chatsListText() const { return WithCaptionDialogsText( lang(lng_in_dlg_photo), parent()->originalText().text); //return WithCaptionDialogsText(lang(lng_in_dlg_album), _caption); } QString MediaPhoto::pinnedTextSubstring() const { return lang(lng_action_pinned_media_photo); } bool MediaPhoto::allowsEditCaption() const { return true; } QString MediaPhoto::errorTextForForward( not_null channel) const { if (channel->restricted(ChannelRestriction::f_send_media)) { return lang(lng_restricted_send_media); } return QString(); } bool MediaPhoto::updateInlineResultMedia(const MTPMessageMedia &media) { if (media.type() != mtpc_messageMediaPhoto) { return false; } auto &photo = media.c_messageMediaPhoto(); if (photo.has_photo() && !photo.has_ttl_seconds()) { if (auto existing = Auth().data().photo(photo.vphoto)) { if (existing == _photo) { return true; } else { // collect data } } } else { LOG(("API Error: " "Got MTPMessageMediaPhoto without photo " "or with ttl_seconds in updateInlineResultMedia()")); } // Can return false if we collect the data. return true; } bool MediaPhoto::updateSentMedia(const MTPMessageMedia &media) { if (media.type() != mtpc_messageMediaPhoto) { return false; } auto &mediaPhoto = media.c_messageMediaPhoto(); if (!mediaPhoto.has_photo() || mediaPhoto.has_ttl_seconds()) { LOG(("Api Error: " "Got MTPMessageMediaPhoto without photo " "or with ttl_seconds in updateSentMedia()")); return false; } const auto &photo = mediaPhoto.vphoto; Auth().data().photoConvert(_photo, photo); if (photo.type() != mtpc_photo) { return false; } auto &sizes = photo.c_photo().vsizes.v; auto max = 0; const MTPDfileLocation *maxLocation = 0; for (const auto &data : sizes) { char size = 0; const MTPFileLocation *loc = 0; switch (data.type()) { case mtpc_photoSize: { const auto &s = data.c_photoSize().vtype.v; loc = &data.c_photoSize().vlocation; if (s.size()) size = s[0]; } break; case mtpc_photoCachedSize: { const auto &s = data.c_photoCachedSize().vtype.v; loc = &data.c_photoCachedSize().vlocation; if (s.size()) size = s[0]; } break; } if (!loc || loc->type() != mtpc_fileLocation) { continue; } if (size == 's') { Local::writeImage(storageKey(loc->c_fileLocation()), _photo->thumb); } else if (size == 'm') { Local::writeImage(storageKey(loc->c_fileLocation()), _photo->medium); } else if (size == 'x' && max < 1) { max = 1; maxLocation = &loc->c_fileLocation(); } else if (size == 'y' && max < 2) { max = 2; maxLocation = &loc->c_fileLocation(); //} else if (size == 'w' && max < 3) { // max = 3; // maxLocation = &loc->c_fileLocation(); } } if (maxLocation) { Local::writeImage(storageKey(*maxLocation), _photo->full); } return true; } std::unique_ptr MediaPhoto::createView( not_null message, not_null realParent) { if (_chat) { return std::make_unique( message, _chat, _photo, st::msgServicePhotoWidth); } return std::make_unique( message, realParent, _photo); } MediaFile::MediaFile( not_null parent, not_null document) : Media(parent) , _document(document) , _emoji(document->sticker() ? document->sticker()->alt : QString()) { Auth().data().registerDocumentItem(_document, parent); if (!_emoji.isEmpty()) { if (const auto emoji = Ui::Emoji::Find(_emoji)) { _emoji = emoji->text(); } } } MediaFile::~MediaFile() { Auth().data().unregisterDocumentItem(_document, parent()); } std::unique_ptr MediaFile::clone(not_null parent) { return std::make_unique(parent, _document); } DocumentData *MediaFile::document() const { return _document; } bool MediaFile::uploading() const { return _document->uploading(); } Storage::SharedMediaTypesMask MediaFile::sharedMediaTypes() const { using Type = Storage::SharedMediaType; if (_document->sticker()) { return {}; } else if (_document->isVideoMessage()) { return Storage::SharedMediaTypesMask{} .added(Type::RoundFile) .added(Type::RoundVoiceFile); } else if (_document->isGifv()) { return Type::GIF; } else if (_document->isVideoFile()) { return Storage::SharedMediaTypesMask{} .added(Type::Video) .added(Type::PhotoVideo); } else if (_document->isVoiceMessage()) { return Storage::SharedMediaTypesMask{} .added(Type::VoiceFile) .added(Type::RoundVoiceFile); } else if (_document->isSharedMediaMusic()) { return Type::MusicFile; } return Type::File; } bool MediaFile::canBeGrouped() const { return _document->isVideoFile(); } QString MediaFile::chatsListText() const { if (const auto sticker = _document->sticker()) { return Media::chatsListText(); } const auto type = [&] { if (_document->isVideoMessage()) { return lang(lng_in_dlg_video_message); } else if (_document->isAnimation()) { return qsl("GIF"); } else if (_document->isVideoFile()) { return lang(lng_in_dlg_video); } else if (_document->isVoiceMessage()) { return lang(lng_in_dlg_audio); } else if (!_document->filename().isEmpty()) { return _document->filename(); } else if (_document->isAudioFile()) { return lang(lng_in_dlg_audio_file); } return lang(lng_in_dlg_file); }(); return WithCaptionDialogsText(type, parent()->originalText().text); } QString MediaFile::notificationText() const { if (const auto sticker = _document->sticker()) { return _emoji.isEmpty() ? lang(lng_in_dlg_sticker) : lng_in_dlg_sticker_emoji(lt_emoji, _emoji); } const auto type = [&] { if (_document->isVideoMessage()) { return lang(lng_in_dlg_video_message); } else if (_document->isAnimation()) { return qsl("GIF"); } else if (_document->isVideoFile()) { return lang(lng_in_dlg_video); } else if (_document->isVoiceMessage()) { return lang(lng_in_dlg_audio); } else if (!_document->filename().isEmpty()) { return _document->filename(); } else if (_document->isAudioFile()) { return lang(lng_in_dlg_audio_file); } return lang(lng_in_dlg_file); }(); return WithCaptionNotificationText(type, parent()->originalText().text); } QString MediaFile::pinnedTextSubstring() const { if (const auto sticker = _document->sticker()) { if (!_emoji.isEmpty()) { return lng_action_pinned_media_emoji_sticker(lt_emoji, _emoji); } return lang(lng_action_pinned_media_sticker); } else if (_document->isAnimation()) { if (_document->isVideoMessage()) { return lang(lng_action_pinned_media_video_message); } return lang(lng_action_pinned_media_gif); } else if (_document->isVideoFile()) { return lang(lng_action_pinned_media_video); } else if (_document->isVoiceMessage()) { return lang(lng_action_pinned_media_voice); } else if (_document->isSong()) { return lang(lng_action_pinned_media_audio); } return lang(lng_action_pinned_media_file); } bool MediaFile::allowsEditCaption() const { return !_document->isVideoMessage() && !_document->sticker(); } bool MediaFile::forwardedBecomesUnread() const { return _document->isVoiceMessage() //|| _document->isVideoFile() || _document->isVideoMessage(); } QString MediaFile::errorTextForForward( not_null channel) const { if (const auto sticker = _document->sticker()) { if (channel->restricted(ChannelRestriction::f_send_stickers)) { return lang(lng_restricted_send_stickers); } } else if (_document->isAnimation()) { if (_document->isVideoMessage()) { if (channel->restricted(ChannelRestriction::f_send_media)) { return lang(lng_restricted_send_media); } } else { if (channel->restricted(ChannelRestriction::f_send_gifs)) { return lang(lng_restricted_send_gifs); } } } else if (channel->restricted(ChannelRestriction::f_send_media)) { return lang(lng_restricted_send_media); } return QString(); } bool MediaFile::updateInlineResultMedia(const MTPMessageMedia &media) { if (media.type() != mtpc_messageMediaDocument) { return false; } auto &data = media.c_messageMediaDocument(); if (data.has_document() && !data.has_ttl_seconds()) { const auto document = Auth().data().document(data.vdocument); if (document == _document) { return false; } else { document->collectLocalData(_document); } } else { LOG(("API Error: " "Got MTPMessageMediaDocument without document " "or with ttl_seconds in updateInlineResultMedia()")); } return false; } bool MediaFile::updateSentMedia(const MTPMessageMedia &media) { if (media.type() != mtpc_messageMediaDocument) { return false; } auto &data = media.c_messageMediaDocument(); if (!data.has_document() || data.has_ttl_seconds()) { LOG(("Api Error: " "Got MTPMessageMediaDocument without document " "or with ttl_seconds in updateSentMedia()")); return false; } Auth().data().documentConvert(_document, data.vdocument); if (!_document->data().isEmpty()) { if (_document->isVoiceMessage()) { Local::writeAudio(_document->mediaKey(), _document->data()); } else { Local::writeStickerImage(_document->mediaKey(), _document->data()); } } return true; } std::unique_ptr MediaFile::createView( not_null message, not_null realParent) { if (_document->sticker()) { return std::make_unique(message, _document); } else if (_document->isAnimation()) { return std::make_unique(message, _document); } else if (_document->isVideoFile()) { return std::make_unique( message, realParent, _document); } return std::make_unique(message, _document); } MediaContact::MediaContact( not_null parent, UserId userId, const QString &firstName, const QString &lastName, const QString &phoneNumber) : Media(parent) { Auth().data().registerContactItem(userId, parent); _contact.userId = userId; _contact.firstName = firstName; _contact.lastName = lastName; _contact.phoneNumber = phoneNumber; } MediaContact::~MediaContact() { Auth().data().unregisterContactItem(_contact.userId, parent()); } std::unique_ptr MediaContact::clone(not_null parent) { return std::make_unique( parent, _contact.userId, _contact.firstName, _contact.lastName, _contact.phoneNumber); } const SharedContact *MediaContact::sharedContact() const { return &_contact; } QString MediaContact::notificationText() const { return lang(lng_in_dlg_contact); } QString MediaContact::pinnedTextSubstring() const { return lang(lng_action_pinned_media_contact); } bool MediaContact::updateInlineResultMedia(const MTPMessageMedia &media) { return false; } bool MediaContact::updateSentMedia(const MTPMessageMedia &media) { if (media.type() != mtpc_messageMediaContact) { return false; } if (_contact.userId != media.c_messageMediaContact().vuser_id.v) { Auth().data().unregisterContactItem(_contact.userId, parent()); _contact.userId = media.c_messageMediaContact().vuser_id.v; Auth().data().registerContactItem(_contact.userId, parent()); } return true; } std::unique_ptr MediaContact::createView( not_null message, not_null realParent) { return std::make_unique( message, _contact.userId, _contact.firstName, _contact.lastName, _contact.phoneNumber); } MediaLocation::MediaLocation( not_null parent, const LocationCoords &coords) : MediaLocation(parent, coords, QString(), QString()) { } MediaLocation::MediaLocation( not_null parent, const LocationCoords &coords, const QString &title, const QString &description) : Media(parent) , _location(App::location(coords)) , _title(title) , _description(description) { } std::unique_ptr MediaLocation::clone(not_null parent) { return std::make_unique( parent, _location->coords, _title, _description); } LocationData *MediaLocation::location() const { return _location; } QString MediaLocation::chatsListText() const { return WithCaptionDialogsText(lang(lng_maps_point), _title); } QString MediaLocation::notificationText() const { return WithCaptionNotificationText(lang(lng_maps_point), _title); } QString MediaLocation::pinnedTextSubstring() const { return lang(lng_action_pinned_media_location); } bool MediaLocation::updateInlineResultMedia(const MTPMessageMedia &media) { return false; } bool MediaLocation::updateSentMedia(const MTPMessageMedia &media) { return false; } std::unique_ptr MediaLocation::createView( not_null message, not_null realParent) { return std::make_unique( message, _location, _title, _description); } MediaCall::MediaCall( not_null parent, const MTPDmessageActionPhoneCall &call) : Media(parent) , _call(ComputeCallData(call)) { } std::unique_ptr MediaCall::clone(not_null parent) { Unexpected("Clone of call media."); } const Call *MediaCall::call() const { return &_call; } QString MediaCall::notificationText() const { auto result = Text(parent(), _call.finishReason); if (_call.duration > 0) { result = lng_call_type_and_duration( lt_type, result, lt_duration, formatDurationWords(_call.duration)); } return result; } QString MediaCall::pinnedTextSubstring() const { return QString(); } bool MediaCall::allowsForward() const { return false; } bool MediaCall::allowsRevoke() const { return false; } bool MediaCall::updateInlineResultMedia(const MTPMessageMedia &media) { return false; } bool MediaCall::updateSentMedia(const MTPMessageMedia &media) { return false; } std::unique_ptr MediaCall::createView( not_null message, not_null realParent) { return std::make_unique(message, &_call); } QString MediaCall::Text( not_null item, CallFinishReason reason) { if (item->out()) { return lang(reason == CallFinishReason::Missed ? lng_call_cancelled : lng_call_outgoing); } else if (reason == CallFinishReason::Missed) { return lang(lng_call_missed); } else if (reason == CallFinishReason::Busy) { return lang(lng_call_declined); } return lang(lng_call_incoming); } MediaWebPage::MediaWebPage( not_null parent, not_null page) : Media(parent) , _page(page) { Auth().data().registerWebPageItem(_page, parent); } MediaWebPage::~MediaWebPage() { Auth().data().unregisterWebPageItem(_page, parent()); } std::unique_ptr MediaWebPage::clone(not_null parent) { return std::make_unique(parent, _page); } WebPageData *MediaWebPage::webpage() const { return _page; } QString MediaWebPage::chatsListText() const { return notificationText(); } QString MediaWebPage::notificationText() const { return parent()->originalText().text; } QString MediaWebPage::pinnedTextSubstring() const { return QString(); } bool MediaWebPage::allowsEdit() const { return true; } bool MediaWebPage::updateInlineResultMedia(const MTPMessageMedia &media) { return false; } bool MediaWebPage::updateSentMedia(const MTPMessageMedia &media) { return false; } std::unique_ptr MediaWebPage::createView( not_null message, not_null realParent) { return std::make_unique(message, _page); } MediaGame::MediaGame( not_null parent, not_null game) : Media(parent) , _game(game) { } std::unique_ptr MediaGame::clone(not_null parent) { return std::make_unique(parent, _game); } QString MediaGame::notificationText() const { // Add a game controller emoji before game title. auto result = QString(); result.reserve(_game->title.size() + 3); result.append( QChar(0xD83C) ).append( QChar(0xDFAE) ).append( QChar(' ') ).append(_game->title); return result; } GameData *MediaGame::game() const { return _game; } QString MediaGame::pinnedTextSubstring() const { auto title = _game->title; return lng_action_pinned_media_game(lt_game, title); } QString MediaGame::errorTextForForward( not_null channel) const { if (channel->restricted(ChannelRestriction::f_send_games)) { return lang(lng_restricted_send_inline); } return QString(); } bool MediaGame::consumeMessageText(const TextWithEntities &text) { _consumedText = text; return true; } bool MediaGame::updateInlineResultMedia(const MTPMessageMedia &media) { return updateSentMedia(media); } bool MediaGame::updateSentMedia(const MTPMessageMedia &media) { if (media.type() != mtpc_messageMediaGame) { return false; } Auth().data().gameConvert(_game, media.c_messageMediaGame().vgame); return true; } std::unique_ptr MediaGame::createView( not_null message, not_null realParent) { return std::make_unique(message, _game, _consumedText); } MediaInvoice::MediaInvoice( not_null parent, const MTPDmessageMediaInvoice &data) : Media(parent) , _invoice(ComputeInvoiceData(data)) { } MediaInvoice::MediaInvoice( not_null parent, const Invoice &data) : Media(parent) , _invoice(data) { } std::unique_ptr MediaInvoice::clone(not_null parent) { return std::make_unique(parent, _invoice); } const Invoice *MediaInvoice::invoice() const { return &_invoice; } QString MediaInvoice::notificationText() const { return _invoice.title; } QString MediaInvoice::pinnedTextSubstring() const { return QString(); } bool MediaInvoice::updateInlineResultMedia(const MTPMessageMedia &media) { return true; } bool MediaInvoice::updateSentMedia(const MTPMessageMedia &media) { return true; } std::unique_ptr MediaInvoice::createView( not_null message, not_null realParent) { return std::make_unique(message, &_invoice); } } // namespace Data