From 7c604fc86af08587ee7aff2815f225e53aac47aa Mon Sep 17 00:00:00 2001 From: 23rd <23rd@vivaldi.net> Date: Thu, 22 Aug 2024 16:14:11 +0300 Subject: [PATCH] Added ability to send GIF with caption. --- Telegram/CMakeLists.txt | 2 + Telegram/Resources/langs/lang.strings | 1 + Telegram/SourceFiles/boxes/boxes.style | 2 + .../boxes/send_gif_with_caption_box.cpp | 273 ++++++++++++++++++ .../boxes/send_gif_with_caption_box.h | 30 ++ .../chat_helpers/gifs_list_widget.cpp | 18 ++ 6 files changed, 326 insertions(+) create mode 100644 Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp create mode 100644 Telegram/SourceFiles/boxes/send_gif_with_caption_box.h diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index e67fd1520..e81947dda 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -310,6 +310,8 @@ PRIVATE boxes/self_destruction_box.h boxes/send_credits_box.cpp boxes/send_credits_box.h + boxes/send_gif_with_caption_box.cpp + boxes/send_gif_with_caption_box.h boxes/send_files_box.cpp boxes/send_files_box.h boxes/sessions_box.cpp diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 6787d39da..6aaaa555e 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -3114,6 +3114,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_send_text_type_files" = "Files"; "lng_send_text_type_stickers" = "Stickers & GIFs"; "lng_send_text_type_polls" = "Polls"; +"lng_send_gif_with_caption" = "Send GIF with caption"; "lng_send_as_title" = "Send message as..."; "lng_send_as_anonymous_admin" = "Anonymous admin"; diff --git a/Telegram/SourceFiles/boxes/boxes.style b/Telegram/SourceFiles/boxes/boxes.style index c3cad02eb..0a3a357cf 100644 --- a/Telegram/SourceFiles/boxes/boxes.style +++ b/Telegram/SourceFiles/boxes/boxes.style @@ -756,6 +756,8 @@ createPollWarningPosition: point(16px, 6px); createPollCheckboxMargin: margins(22px, 10px, 22px, 10px); createPollFieldTitlePadding: margins(22px, 7px, 10px, 6px); +sendGifWithCaptionEmojiPosition: point(-30px, 23px); + backgroundCheckbox: Checkbox(defaultCheckbox) { textFg: msgServiceFg; textFgActive: msgServiceFg; diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp new file mode 100644 index 000000000..9eec6b4a4 --- /dev/null +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.cpp @@ -0,0 +1,273 @@ +/* +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 "boxes/send_gif_with_caption_box.h" + +#include "boxes/premium_preview_box.h" +#include "chat_helpers/message_field.h" +#include "chat_helpers/tabbed_panel.h" +#include "chat_helpers/tabbed_selector.h" +#include "core/application.h" +#include "core/core_settings.h" +#include "data/data_document.h" +#include "data/data_document_media.h" +#include "data/data_file_origin.h" +#include "data/data_peer_values.h" +#include "data/data_premium_limits.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "data/stickers/data_custom_emoji.h" +#include "data/stickers/data_stickers.h" +#include "history/view/controls/history_view_characters_limit.h" +#include "lang/lang_keys.h" +#include "main/main_session.h" +#include "media/clip/media_clip_reader.h" +#include "menu/menu_send.h" +#include "ui/controls/emoji_button.h" +#include "ui/controls/emoji_button_factory.h" +#include "ui/layers/generic_box.h" +#include "ui/rect.h" +#include "ui/vertical_list.h" +#include "ui/widgets/fields/input_field.h" +#include "window/window_controller.h" +#include "window/window_session_controller.h" +#include "styles/style_boxes.h" +#include "styles/style_chat_helpers.h" +#include "styles/style_layers.h" + +namespace Ui { +namespace { + +[[nodiscard]] not_null AddGifWidget( + not_null container, + not_null document, + int width) { + struct State final { + std::shared_ptr mediaView; + ::Media::Clip::ReaderPointer gif; + rpl::lifetime loadingLifetime; + }; + + const auto state = container->lifetime().make_state(); + state->mediaView = document->createMediaView(); + state->mediaView->automaticLoad(Data::FileOriginSavedGifs(), nullptr); + state->mediaView->thumbnailWanted(Data::FileOriginSavedGifs()); + state->mediaView->videoThumbnailWanted(Data::FileOriginSavedGifs()); + + const auto widget = container->add( + Ui::CreateSkipWidget( + container, + document->dimensions.scaled( + width - rect::m::sum::h(st::boxRowPadding), + std::numeric_limits::max(), + Qt::KeepAspectRatio).height()), + st::boxRowPadding); + widget->paintRequest( + ) | rpl::start_with_next([=] { + auto p = QPainter(widget); + if (state->gif && state->gif->started()) { + p.drawImage( + 0, + 0, + state->gif->current({ .frame = widget->size() }, crl::now())); + } else if (const auto thumb = state->mediaView->thumbnail()) { + p.drawImage( + widget->rect(), + thumb->pixNoCache( + widget->size() * style::DevicePixelRatio(), + { .outer = widget->size() }).toImage()); + } else if (const auto thumb = state->mediaView->thumbnailInline()) { + p.drawImage( + widget->rect(), + thumb->pixNoCache( + widget->size() * style::DevicePixelRatio(), + { + .options = Images::Option::Blur, + .outer = widget->size(), + }).toImage()); + } + }, widget->lifetime()); + + const auto updateThumbnail = [=] { + if (document->dimensions.isEmpty()) { + return false; + } + if (!state->mediaView->loaded()) { + return false; + } + const auto callback = [=](::Media::Clip::Notification) { + if (state->gif && state->gif->ready() && !state->gif->started()) { + state->gif->start({ .frame = widget->size() }); + } + widget->update(); + }; + state->gif = ::Media::Clip::MakeReader( + state->mediaView->owner()->location(), + state->mediaView->bytes(), + callback); + return true; + }; + if (!updateThumbnail()) { + document->owner().session().downloaderTaskFinished( + ) | rpl::start_with_next([=] { + if (updateThumbnail()) { + state->loadingLifetime.destroy(); + widget->update(); + } + }, state->loadingLifetime); + } + + return widget; +} + +[[nodiscard]] not_null AddInputField( + not_null box, + not_null controller) { + using Limit = HistoryView::Controls::CharactersLimitLabel; + + const auto wrap = box->verticalLayout()->add( + object_ptr(box), + st::boxRowPadding); + const auto input = Ui::CreateChild( + wrap, + st::defaultComposeFiles.caption, + Ui::InputField::Mode::MultiLine, + tr::lng_photo_caption()); + Ui::ResizeFitChild(wrap, input); + + struct State final { + base::unique_qptr emojiPanel; + base::unique_qptr charsLimitation; + }; + const auto state = box->lifetime().make_state(); + + { + const auto container = box->getDelegate()->outerContainer(); + using Selector = ChatHelpers::TabbedSelector; + state->emojiPanel = base::make_unique_q( + container, + controller, + object_ptr( + nullptr, + controller->uiShow(), + Window::GifPauseReason::Layer, + Selector::Mode::EmojiOnly)); + const auto emojiPanel = state->emojiPanel.get(); + emojiPanel->setDesiredHeightValues( + 1., + st::emojiPanMinHeight / 2, + st::emojiPanMinHeight); + emojiPanel->hide(); + emojiPanel->selector()->setCurrentPeer(controller->session().user()); + emojiPanel->selector()->emojiChosen( + ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) { + Ui::InsertEmojiAtCursor(input->textCursor(), data.emoji); + }, input->lifetime()); + emojiPanel->selector()->customEmojiChosen( + ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) { + const auto info = data.document->sticker(); + if (info + && info->setType == Data::StickersType::Emoji + && !controller->session().premium()) { + ShowPremiumPreviewBox( + controller, + PremiumFeature::AnimatedEmoji); + } else { + Data::InsertCustomEmoji(input, data.document); + } + }, input->lifetime()); + } + + const auto emojiButton = Ui::AddEmojiToggleToField( + input, + box, + controller, + state->emojiPanel.get(), + st::sendGifWithCaptionEmojiPosition); + emojiButton->show(); + + const auto session = &controller->session(); + const auto checkCharsLimitation = [=](auto repeat) -> void { + const auto remove = Ui::ComputeFieldCharacterCount(input) + - Data::PremiumLimits(session).captionLengthCurrent(); + if (remove > 0) { + if (!state->charsLimitation) { + state->charsLimitation = base::make_unique_q( + input, + emojiButton, + style::al_top); + state->charsLimitation->show(); + Data::AmPremiumValue(session) | rpl::start_with_next([=] { + repeat(repeat); + }, state->charsLimitation->lifetime()); + } + state->charsLimitation->setLeft(remove); + state->charsLimitation->show(); + } else { + state->charsLimitation = nullptr; + } + }; + + input->changes() | rpl::start_with_next([=] { + checkCharsLimitation(checkCharsLimitation); + }, input->lifetime()); + + return input; +} + +} // namespace + +void SendGifWithCaptionBox( + not_null box, + not_null document, + const SendMenu::Details &details, + Fn done) { + const auto window = Core::App().findWindow(box); + const auto controller = window ? window->sessionController() : nullptr; + if (!controller) { + return; + } + box->setTitle(tr::lng_send_gif_with_caption()); + box->setWidth(st::boxWidth); + + const auto container = box->verticalLayout(); + [[maybe_unused]] const auto gifWidget = AddGifWidget( + container, + document, + st::boxWidth); + + Ui::AddSkip(container); + + const auto input = AddInputField(box, controller); + box->setFocusCallback([=] { + input->setFocus(); + }); + + input->setSubmitSettings(Core::App().settings().sendSubmitWay()); + InitMessageField(controller, input, [=](not_null) { + return true; + }); + + const auto send = [=](Api::SendOptions options) { + done(std::move(options), input->getTextWithTags()); + }; + const auto confirm = box->addButton( + tr::lng_send_button(), + [=] { send({}); }); + SendMenu::SetupMenuAndShortcuts( + confirm, + controller->uiShow(), + [=] { return details; }, + SendMenu::DefaultCallback(controller->uiShow(), send)); + box->addButton(tr::lng_cancel(), [=] { + box->closeBox(); + }); + input->submits( + ) | rpl::start_with_next([=] { send({}); }, input->lifetime()); +} + +} // namespace Ui diff --git a/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h new file mode 100644 index 000000000..df78366d5 --- /dev/null +++ b/Telegram/SourceFiles/boxes/send_gif_with_caption_box.h @@ -0,0 +1,30 @@ +/* +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 +*/ +#pragma once + +class DocumentData; + +namespace Api { +struct SendOptions; +} // namespace Api + +namespace SendMenu { +struct Details; +} // namespace SendMenu + +namespace Ui { + +class GenericBox; + +void SendGifWithCaptionBox( + not_null box, + not_null document, + const SendMenu::Details &details, + Fn done); + +} // namespace Ui diff --git a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp index 4f04b0266..4ed82d5f1 100644 --- a/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp +++ b/Telegram/SourceFiles/chat_helpers/gifs_list_widget.cpp @@ -24,12 +24,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "mtproto/mtproto_config.h" #include "core/click_handler_types.h" #include "ui/controls/tabbed_search.h" +#include "ui/layers/generic_box.h" #include "ui/widgets/buttons.h" #include "ui/widgets/fields/input_field.h" #include "ui/widgets/popup_menu.h" #include "ui/effects/ripple_animation.h" #include "ui/image/image.h" #include "ui/painter.h" +#include "boxes/send_gif_with_caption_box.h" #include "boxes/stickers_box.h" #include "inline_bots/inline_bot_result.h" #include "storage/localstorage.h" @@ -407,6 +409,22 @@ base::unique_qptr GifsListWidget::fillContextMenu( SendMenu::DefaultCallback(_show, send), icons); + if (!isInlineResult) { + auto done = crl::guard(this, [=]( + Api::SendOptions options, + TextWithTags text) { + selectInlineResult(selected, options, true, std::move(text)); + }); + const auto show = _show; + menu->addAction(tr::lng_send_gif_with_caption(tr::now), [=] { + show->show(Box( + Ui::SendGifWithCaptionBox, + item->getDocument(), + copyDetails, + std::move(done))); + }, &st::menuIconEdit); + } + if (const auto item = _mosaic.maybeItemAt(_selected)) { const auto document = item->getDocument() ? item->getDocument() // Saved GIF.