Added ability to send GIF with caption.

This commit is contained in:
23rd 2024-08-22 16:14:11 +03:00
parent ec7fbb8952
commit 7c604fc86a
6 changed files with 326 additions and 0 deletions

View file

@ -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

View file

@ -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";

View file

@ -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;

View file

@ -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<Ui::RpWidget*> AddGifWidget(
not_null<Ui::VerticalLayout*> container,
not_null<DocumentData*> document,
int width) {
struct State final {
std::shared_ptr<Data::DocumentMedia> mediaView;
::Media::Clip::ReaderPointer gif;
rpl::lifetime loadingLifetime;
};
const auto state = container->lifetime().make_state<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<int>::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<Ui::InputField*> AddInputField(
not_null<Ui::GenericBox*> box,
not_null<Window::SessionController*> controller) {
using Limit = HistoryView::Controls::CharactersLimitLabel;
const auto wrap = box->verticalLayout()->add(
object_ptr<Ui::RpWidget>(box),
st::boxRowPadding);
const auto input = Ui::CreateChild<Ui::InputField>(
wrap,
st::defaultComposeFiles.caption,
Ui::InputField::Mode::MultiLine,
tr::lng_photo_caption());
Ui::ResizeFitChild(wrap, input);
struct State final {
base::unique_qptr<ChatHelpers::TabbedPanel> emojiPanel;
base::unique_qptr<Limit> charsLimitation;
};
const auto state = box->lifetime().make_state<State>();
{
const auto container = box->getDelegate()->outerContainer();
using Selector = ChatHelpers::TabbedSelector;
state->emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(
container,
controller,
object_ptr<Selector>(
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<Limit>(
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<Ui::GenericBox*> box,
not_null<DocumentData*> document,
const SendMenu::Details &details,
Fn<void(Api::SendOptions, TextWithTags)> 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<DocumentData*>) {
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

View file

@ -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<Ui::GenericBox*> box,
not_null<DocumentData*> document,
const SendMenu::Details &details,
Fn<void(Api::SendOptions, TextWithTags)> done);
} // namespace Ui

View file

@ -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<Ui::PopupMenu> 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.