/* 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 "api/api_stickers_creator.h" #include "apiwrap.h" #include "base/random.h" #include "base/unixtime.h" #include "chat_helpers/compose/compose_show.h" #include "data/data_document.h" #include "data/data_file_origin.h" #include "data/data_session.h" #include "data/stickers/data_stickers.h" #include "data/stickers/data_stickers_set.h" #include "lang/lang_keys.h" #include "main/main_session.h" #include "menu/menu_action_with_thumbnail.h" #include "storage/file_upload.h" #include "storage/localimageloader.h" #include "styles/style_menu_icons.h" #include "styles/style_widgets.h" #include "ui/dynamic_thumbnails.h" #include "ui/widgets/menu/menu_add_action_callback.h" #include "ui/widgets/menu/menu_common.h" #include "ui/widgets/popup_menu.h" namespace Api { namespace { constexpr auto kStickerSide = 512; [[nodiscard]] MTPInputStickerSetItem InputItem( const MTPInputDocument &document, const QString &emoji) { return MTP_inputStickerSetItem( MTP_flags(0), document, MTP_string(emoji), MTPMaskCoords(), MTPstring()); } [[nodiscard]] std::shared_ptr PrepareStickerWebp( MTP::DcId dcId, DocumentId id, const QByteArray &bytes) { const auto filename = u"sticker.webp"_q; auto attributes = QVector( 1, MTP_documentAttributeFilename(MTP_string(filename))); attributes.push_back(MTP_documentAttributeImageSize( MTP_int(kStickerSide), MTP_int(kStickerSide))); auto result = MakePreparedFile({ .id = id, .type = SendMediaType::File, }); result->filename = filename; result->filemime = u"image/webp"_q; result->content = bytes; result->filesize = bytes.size(); result->setFileData(bytes); result->document = MTP_document( MTP_flags(0), MTP_long(id), MTP_long(0), MTP_bytes(), MTP_int(base::unixtime::now()), MTP_string("image/webp"), MTP_long(bytes.size()), MTP_vector(), MTPVector(), MTP_int(dcId), MTP_vector(std::move(attributes))); return result; } void FeedSetIfFull( not_null session, const MTPmessages_StickerSet &result) { result.match([&](const MTPDmessages_stickerSet &data) { session->data().stickers().feedSetFull(data); session->data().stickers().notifyUpdated( Data::StickersType::Stickers); }, [](const auto &) { }); } template void EnumerateOwnedSets( not_null session, Data::StickersType type, Callback &&callback) { const auto &stickers = session->data().stickers(); const auto &sets = stickers.sets(); const auto &order = (type == Data::StickersType::Emoji) ? stickers.emojiSetsOrder() : stickers.setsOrder(); for (const auto setId : order) { const auto it = sets.find(setId); if (it == sets.end()) { continue; } const auto set = it->second.get(); if (!(set->flags & Data::StickersSetFlag::AmCreator) || (set->type() != type)) { continue; } using namespace Data; if constexpr (std::is_same_v< bool, std::invoke_result_t>>) { if (!callback(set)) { return; } } else { callback(set); } } } void FillChooseOwnedSetMenu( not_null menu, std::shared_ptr show, not_null document, Data::StickersType type) { const auto session = &show->session(); const auto emoji = StickerEmojiOrDefault(document); const auto isEmoji = (type == Data::StickersType::Emoji); const auto maxCount = isEmoji ? kEmojiInOwnedSetMax : kStickersInOwnedSetMax; const auto fullMessage = isEmoji ? tr::lng_emoji_set_is_full : tr::lng_stickers_set_is_full; const auto addedMessage = isEmoji ? tr::lng_emoji_added : tr::lng_stickers_create_added; const auto alreadyMessage = isEmoji ? tr::lng_emoji_already_in_set : tr::lng_stickers_already_in_set; const auto failToast = [=](QString err) { show->showToast(err.isEmpty() ? tr::lng_attach_failed(tr::now) : err); }; EnumerateOwnedSets(session, type, [&](not_null set) { const auto identifier = set->identifier(); const auto coverDocument = set->lookupThumbnailDocument(); auto thumbnail = coverDocument ? Ui::MakeDocumentThumbnail( coverDocument, Data::FileOriginStickerSet(set->id, set->accessHash)) : nullptr; const auto targetSetId = set->id; const auto handler = crl::guard(session, [=] { const auto &map = session->data().stickers().sets(); const auto i = map.find(targetSetId); if (i != map.end() && i->second->count >= maxCount) { show->showToast(fullMessage(tr::now)); return; } const auto oldCount = (i != map.end()) ? i->second->count : 0; AddExistingStickerToSet( session, identifier, document, emoji, crl::guard(session, [=](MTPmessages_StickerSet) { const auto &map = session->data().stickers().sets(); const auto i = map.find(targetSetId); const auto newCount = (i != map.end()) ? i->second->count : oldCount; show->showToast(newCount > oldCount ? addedMessage(tr::now) : alreadyMessage(tr::now)); }), crl::guard(session, failToast)); }); const auto rawAction = Ui::Menu::CreateAction( menu.get(), set->title, handler); auto item = base::make_unique_q( menu->menu(), menu->menu()->st(), rawAction, std::move(thumbnail), st::menuIconStickerAdd.width()); menu->addAction(std::move(item)); }); } } // namespace void AddExistingStickerToSet( not_null session, const StickerSetIdentifier &set, not_null document, const QString &emoji, Fn done, Fn fail) { session->api().request(MTPstickers_AddStickerToSet( Data::InputStickerSet(set), InputItem(document->mtpInput(), emoji)) ).done([=](const MTPmessages_StickerSet &result) { FeedSetIfFull(session, result); if (done) { done(result); } }).fail([=](const MTP::Error &error) { if (fail) { fail(error.type()); } }).handleFloodErrors().send(); } QString StickerEmojiOrDefault(not_null document) { if (const auto sticker = document->sticker()) { if (!sticker->alt.isEmpty()) { return sticker->alt; } } return QString::fromUtf8("\xF0\x9F\x99\x82"); } bool HasOwnedStickerSets(not_null session) { auto found = false; EnumerateOwnedSets( session, Data::StickersType::Stickers, [&](not_null) { found = true; return false; }); return found; } bool HasOwnedEmojiSets(not_null session) { auto found = false; EnumerateOwnedSets( session, Data::StickersType::Emoji, [&](not_null) { found = true; return false; }); return found; } void FillChooseStickerSetMenu( not_null menu, std::shared_ptr show, not_null document) { using namespace Data; FillChooseOwnedSetMenu(menu, show, document, StickersType::Stickers); } void FillChooseEmojiSetMenu( not_null menu, std::shared_ptr show, not_null document) { FillChooseOwnedSetMenu(menu, show, document, Data::StickersType::Emoji); } void AddAddToStickerSetAction( const Ui::Menu::MenuCallback &addAction, std::shared_ptr show, not_null document) { const auto session = &show->session(); if (!HasOwnedStickerSets(session)) { return; } addAction({ .text = tr::lng_stickers_add_to_set(tr::now), .icon = &st::menuIconStickerAdd, .fillSubmenu = [show, document](not_null submenu) { FillChooseStickerSetMenu(submenu, show, document); }, .submenuSt = &st::popupMenuWithIcons, }); } void AddAddToEmojiSetAction( const Ui::Menu::MenuCallback &addAction, std::shared_ptr show, not_null document) { const auto session = &show->session(); if (!HasOwnedEmojiSets(session)) { return; } addAction({ .text = tr::lng_emoji_add_to_set(tr::now), .icon = &st::menuIconEmoji, .fillSubmenu = [show, document](not_null submenu) { FillChooseEmojiSetMenu(submenu, show, document); }, .submenuSt = &st::popupMenuWithIcons, }); } void AddAddToOwnedSetAction( const Ui::Menu::MenuCallback &addAction, std::shared_ptr show, not_null document) { const auto sticker = document->sticker(); const auto isLottie = sticker && sticker->isLottie(); const auto size = document->dimensions; const auto fitsEmoji = !size.isEmpty() && (size.width() <= kEmojiStickerSideMax) && (size.height() <= kEmojiStickerSideMax); if (isLottie || !fitsEmoji) { AddAddToStickerSetAction(addAction, show, document); } if (isLottie || fitsEmoji) { AddAddToEmojiSetAction(addAction, std::move(show), document); } } void DeleteStickerSet( not_null session, const StickerSetIdentifier &set, Fn done, Fn fail) { session->api().request(MTPstickers_DeleteStickerSet( Data::InputStickerSet(set)) ).done([=] { session->data().stickers().notifyUpdated( Data::StickersType::Stickers); if (done) { done(); } }).fail([=](const MTP::Error &error) { if (fail) { fail(error.type()); } }).send(); } StickerUpload::StickerUpload( not_null session, StickerSetIdentifier set, QByteArray webpBytes, QString emoji) : _session(session) , _set(std::move(set)) , _bytes(std::move(webpBytes)) , _emoji(std::move(emoji)) , _api(&session->mtp()) { } StickerUpload::~StickerUpload() { cancel(); } void StickerUpload::start( Fn done, Fn fail, Fn progress) { Expects(!_uploadId); _done = std::move(done); _fail = std::move(fail); _progress = std::move(progress); _documentId = base::RandomValue(); auto ready = PrepareStickerWebp( _session->mtp().mainDcId(), _documentId, _bytes); _uploadId = FullMsgId( _session->userPeerId(), _session->data().nextLocalMessageId()); const auto document = _session->data().document(_documentId); document->uploadingData = std::make_unique( document->size > 0 ? document->size : int64(_bytes.size())); _session->uploader().documentReady( ) | rpl::filter([=](const Storage::UploadedMedia &data) { return data.fullId == _uploadId; }) | rpl::on_next([=](const Storage::UploadedMedia &data) { uploadReady(data.info.file); }, _uploadLifetime); _session->uploader().documentFailed( ) | rpl::filter([=](const FullMsgId &id) { return id == _uploadId; }) | rpl::on_next([=] { uploadFailed(); }, _uploadLifetime); if (_progress) { _session->uploader().documentProgress( ) | rpl::filter([=](const FullMsgId &id) { return id == _uploadId; }) | rpl::on_next([=] { uploadProgressed(); }, _uploadLifetime); } _session->uploader().upload(_uploadId, ready); } void StickerUpload::cancel() { if (_uploadId) { _session->uploader().cancel(_uploadId); _uploadId = FullMsgId(); } if (_addRequestId) { _api.request(_addRequestId).cancel(); _addRequestId = 0; } _uploadLifetime.destroy(); _done = nullptr; _fail = nullptr; _progress = nullptr; } void StickerUpload::uploadProgressed() { if (!_progress) { return; } const auto document = _session->data().document(_documentId); if (!document->uploading() || !document->uploadingData) { return; } const auto size = document->uploadingData->size; if (size <= 0) { return; } const auto percent = int( (document->uploadingData->offset * 100) / size); if (percent != _lastReportedPercent) { _lastReportedPercent = percent; _progress(percent); } } void StickerUpload::uploadFailed() { const auto fail = std::move(_fail); cancel(); if (fail) { fail(QString()); } } void StickerUpload::uploadReady(const MTPInputFile &file) { _uploadLifetime.destroy(); _uploadId = FullMsgId(); auto attributes = QVector(); attributes.push_back(MTP_documentAttributeSticker( MTP_flags(0), MTP_string(_emoji), MTP_inputStickerSetEmpty(), MTPMaskCoords())); attributes.push_back(MTP_documentAttributeImageSize( MTP_int(kStickerSide), MTP_int(kStickerSide))); const auto media = MTP_inputMediaUploadedDocument( MTP_flags(0), file, MTPInputFile(), MTP_string("image/webp"), MTP_vector(std::move(attributes)), MTP_vector(), MTPInputPhoto(), MTP_int(0), MTP_int(0)); _addRequestId = _api.request(MTPmessages_UploadMedia( MTP_flags(0), MTPstring(), MTP_inputPeerSelf(), media )).done(crl::guard(this, [=](const MTPMessageMedia &result) { _addRequestId = 0; auto inputDoc = (MTPInputDocument*)(nullptr); auto storage = MTPInputDocument(); result.match([&](const MTPDmessageMediaDocument &data) { if (const auto doc = data.vdocument()) { doc->match([&](const MTPDdocument &d) { storage = MTP_inputDocument( d.vid(), d.vaccess_hash(), d.vfile_reference()); inputDoc = &storage; }, [](const auto &) { }); } }, [](const auto &) { }); if (inputDoc) { requestAddSticker(*inputDoc); } else if (const auto fail = std::move(_fail)) { cancel(); fail(QString()); } })).fail(crl::guard(this, [=](const MTP::Error &error) { _addRequestId = 0; const auto fail = std::move(_fail); const auto type = error.type(); cancel(); if (fail) { fail(type); } })).handleFloodErrors().send(); } void StickerUpload::requestAddSticker(const MTPInputDocument &document) { _addRequestId = _api.request(MTPstickers_AddStickerToSet( Data::InputStickerSet(_set), InputItem(document, _emoji)) ).done(crl::guard(this, [=](const MTPmessages_StickerSet &result) { _addRequestId = 0; FeedSetIfFull(_session, result); const auto done = std::move(_done); cancel(); if (done) { done(result); } })).fail(crl::guard(this, [=](const MTP::Error &error) { _addRequestId = 0; const auto fail = std::move(_fail); const auto type = error.type(); cancel(); if (fail) { fail(type); } })).handleFloodErrors().send(); } } // namespace Api