send_gif_with_caption_box.cpp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. /*
  2. This file is part of Telegram Desktop,
  3. the official desktop application for the Telegram messaging service.
  4. For license and copyright information please follow this link:
  5. https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
  6. */
  7. #include "boxes/send_gif_with_caption_box.h"
  8. #include "base/event_filter.h"
  9. #include "boxes/premium_preview_box.h"
  10. #include "chat_helpers/field_autocomplete.h"
  11. #include "chat_helpers/message_field.h"
  12. #include "chat_helpers/tabbed_panel.h"
  13. #include "chat_helpers/tabbed_selector.h"
  14. #include "core/application.h"
  15. #include "core/core_settings.h"
  16. #include "data/data_document.h"
  17. #include "data/data_document_media.h"
  18. #include "data/data_file_origin.h"
  19. #include "data/data_peer_values.h"
  20. #include "data/data_premium_limits.h"
  21. #include "data/data_session.h"
  22. #include "data/data_user.h"
  23. #include "data/stickers/data_custom_emoji.h"
  24. #include "data/stickers/data_stickers.h"
  25. #include "history/view/controls/history_view_characters_limit.h"
  26. #include "lang/lang_keys.h"
  27. #include "main/main_session.h"
  28. #include "media/clip/media_clip_reader.h"
  29. #include "menu/menu_send.h"
  30. #include "ui/controls/emoji_button.h"
  31. #include "ui/controls/emoji_button_factory.h"
  32. #include "ui/layers/generic_box.h"
  33. #include "ui/widgets/fields/input_field.h"
  34. #include "ui/rect.h"
  35. #include "ui/ui_utility.h"
  36. #include "ui/vertical_list.h"
  37. #include "window/window_controller.h"
  38. #include "window/window_session_controller.h"
  39. #include "styles/style_boxes.h"
  40. #include "styles/style_chat_helpers.h"
  41. #include "styles/style_layers.h"
  42. namespace Ui {
  43. namespace {
  44. [[nodiscard]] not_null<Ui::RpWidget*> AddGifWidget(
  45. not_null<Ui::VerticalLayout*> container,
  46. not_null<DocumentData*> document,
  47. int width) {
  48. struct State final {
  49. std::shared_ptr<Data::DocumentMedia> mediaView;
  50. ::Media::Clip::ReaderPointer gif;
  51. rpl::lifetime loadingLifetime;
  52. };
  53. const auto state = container->lifetime().make_state<State>();
  54. state->mediaView = document->createMediaView();
  55. state->mediaView->automaticLoad(Data::FileOriginSavedGifs(), nullptr);
  56. state->mediaView->thumbnailWanted(Data::FileOriginSavedGifs());
  57. state->mediaView->videoThumbnailWanted(Data::FileOriginSavedGifs());
  58. const auto widget = container->add(
  59. Ui::CreateSkipWidget(
  60. container,
  61. document->dimensions.scaled(
  62. width - rect::m::sum::h(st::boxRowPadding),
  63. std::numeric_limits<int>::max(),
  64. Qt::KeepAspectRatio).height()),
  65. st::boxRowPadding);
  66. widget->paintRequest(
  67. ) | rpl::start_with_next([=] {
  68. auto p = QPainter(widget);
  69. if (state->gif && state->gif->started()) {
  70. p.drawImage(
  71. 0,
  72. 0,
  73. state->gif->current({ .frame = widget->size() }, crl::now()));
  74. } else if (const auto thumb = state->mediaView->thumbnail()) {
  75. p.drawImage(
  76. widget->rect(),
  77. thumb->pixNoCache(
  78. widget->size() * style::DevicePixelRatio(),
  79. { .outer = widget->size() }).toImage());
  80. } else if (const auto thumb = state->mediaView->thumbnailInline()) {
  81. p.drawImage(
  82. widget->rect(),
  83. thumb->pixNoCache(
  84. widget->size() * style::DevicePixelRatio(),
  85. {
  86. .options = Images::Option::Blur,
  87. .outer = widget->size(),
  88. }).toImage());
  89. }
  90. }, widget->lifetime());
  91. const auto updateThumbnail = [=] {
  92. if (document->dimensions.isEmpty()) {
  93. return false;
  94. }
  95. if (!state->mediaView->loaded()) {
  96. return false;
  97. }
  98. const auto callback = [=](::Media::Clip::Notification) {
  99. if (state->gif && state->gif->ready() && !state->gif->started()) {
  100. state->gif->start({ .frame = widget->size() });
  101. }
  102. widget->update();
  103. };
  104. state->gif = ::Media::Clip::MakeReader(
  105. state->mediaView->owner()->location(),
  106. state->mediaView->bytes(),
  107. callback);
  108. return true;
  109. };
  110. if (!updateThumbnail()) {
  111. document->owner().session().downloaderTaskFinished(
  112. ) | rpl::start_with_next([=] {
  113. if (updateThumbnail()) {
  114. state->loadingLifetime.destroy();
  115. widget->update();
  116. }
  117. }, state->loadingLifetime);
  118. }
  119. return widget;
  120. }
  121. [[nodiscard]] not_null<Ui::InputField*> AddInputField(
  122. not_null<Ui::GenericBox*> box,
  123. not_null<Window::SessionController*> controller) {
  124. using Limit = HistoryView::Controls::CharactersLimitLabel;
  125. const auto bottomContainer = box->setPinnedToBottomContent(
  126. object_ptr<Ui::VerticalLayout>(box));
  127. const auto wrap = bottomContainer->add(
  128. object_ptr<Ui::RpWidget>(box),
  129. st::boxRowPadding);
  130. const auto input = Ui::CreateChild<Ui::InputField>(
  131. wrap,
  132. st::defaultComposeFiles.caption,
  133. Ui::InputField::Mode::MultiLine,
  134. tr::lng_photo_caption());
  135. Ui::ResizeFitChild(wrap, input);
  136. struct State final {
  137. base::unique_qptr<ChatHelpers::TabbedPanel> emojiPanel;
  138. base::unique_qptr<Limit> charsLimitation;
  139. };
  140. const auto state = box->lifetime().make_state<State>();
  141. {
  142. const auto container = box->getDelegate()->outerContainer();
  143. using Selector = ChatHelpers::TabbedSelector;
  144. state->emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(
  145. container,
  146. controller,
  147. object_ptr<Selector>(
  148. nullptr,
  149. controller->uiShow(),
  150. Window::GifPauseReason::Layer,
  151. Selector::Mode::EmojiOnly));
  152. const auto emojiPanel = state->emojiPanel.get();
  153. emojiPanel->setDesiredHeightValues(
  154. 1.,
  155. st::emojiPanMinHeight / 2,
  156. st::emojiPanMinHeight);
  157. emojiPanel->hide();
  158. emojiPanel->selector()->setCurrentPeer(controller->session().user());
  159. emojiPanel->selector()->emojiChosen(
  160. ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
  161. Ui::InsertEmojiAtCursor(input->textCursor(), data.emoji);
  162. }, input->lifetime());
  163. emojiPanel->selector()->customEmojiChosen(
  164. ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
  165. const auto info = data.document->sticker();
  166. if (info
  167. && info->setType == Data::StickersType::Emoji
  168. && !controller->session().premium()) {
  169. ShowPremiumPreviewBox(
  170. controller,
  171. PremiumFeature::AnimatedEmoji);
  172. } else {
  173. Data::InsertCustomEmoji(input, data.document);
  174. }
  175. }, input->lifetime());
  176. }
  177. const auto emojiButton = Ui::AddEmojiToggleToField(
  178. input,
  179. box,
  180. controller,
  181. state->emojiPanel.get(),
  182. st::sendGifWithCaptionEmojiPosition);
  183. emojiButton->show();
  184. const auto session = &controller->session();
  185. const auto checkCharsLimitation = [=](auto repeat) -> void {
  186. const auto remove = Ui::ComputeFieldCharacterCount(input)
  187. - Data::PremiumLimits(session).captionLengthCurrent();
  188. if (remove > 0) {
  189. if (!state->charsLimitation) {
  190. state->charsLimitation = base::make_unique_q<Limit>(
  191. input,
  192. emojiButton,
  193. style::al_top);
  194. state->charsLimitation->show();
  195. Data::AmPremiumValue(session) | rpl::start_with_next([=] {
  196. repeat(repeat);
  197. }, state->charsLimitation->lifetime());
  198. }
  199. state->charsLimitation->setLeft(remove);
  200. state->charsLimitation->show();
  201. } else {
  202. state->charsLimitation = nullptr;
  203. }
  204. };
  205. input->changes() | rpl::start_with_next([=] {
  206. checkCharsLimitation(checkCharsLimitation);
  207. }, input->lifetime());
  208. return input;
  209. }
  210. } // namespace
  211. void SendGifWithCaptionBox(
  212. not_null<Ui::GenericBox*> box,
  213. not_null<DocumentData*> document,
  214. not_null<PeerData*> peer,
  215. const SendMenu::Details &details,
  216. Fn<void(Api::SendOptions, TextWithTags)> done) {
  217. const auto window = Core::App().findWindow(box);
  218. const auto controller = window ? window->sessionController() : nullptr;
  219. if (!controller) {
  220. return;
  221. }
  222. box->setTitle(tr::lng_send_gif_with_caption());
  223. box->setWidth(st::boxWidth);
  224. box->getDelegate()->setStyle(st::sendGifBox);
  225. const auto container = box->verticalLayout();
  226. [[maybe_unused]] const auto gifWidget = AddGifWidget(
  227. container,
  228. document,
  229. st::boxWidth);
  230. Ui::AddSkip(container);
  231. const auto input = AddInputField(box, controller);
  232. box->setFocusCallback([=] {
  233. input->setFocus();
  234. });
  235. input->setSubmitSettings(Core::App().settings().sendSubmitWay());
  236. InitMessageField(controller, input, [=](not_null<DocumentData*>) {
  237. return true;
  238. });
  239. const auto sendMenuDetails = [=] { return details; };
  240. struct Autocomplete {
  241. std::unique_ptr<ChatHelpers::FieldAutocomplete> dropdown;
  242. bool geometryUpdateScheduled = false;
  243. };
  244. const auto autocomplete = box->lifetime().make_state<Autocomplete>();
  245. const auto outer = box->getDelegate()->outerContainer();
  246. ChatHelpers::InitFieldAutocomplete(autocomplete->dropdown, {
  247. .parent = outer,
  248. .show = controller->uiShow(),
  249. .field = input,
  250. .peer = peer,
  251. .features = [=] {
  252. auto result = ChatHelpers::ComposeFeatures();
  253. result.autocompleteCommands = false;
  254. result.suggestStickersByEmoji = false;
  255. return result;
  256. },
  257. .sendMenuDetails = sendMenuDetails,
  258. });
  259. const auto raw = autocomplete->dropdown.get();
  260. const auto recountPostponed = [=] {
  261. if (autocomplete->geometryUpdateScheduled) {
  262. return;
  263. }
  264. autocomplete->geometryUpdateScheduled = true;
  265. Ui::PostponeCall(raw, [=] {
  266. autocomplete->geometryUpdateScheduled = false;
  267. const auto from = input->parentWidget();
  268. auto field = Ui::MapFrom(outer, from, input->geometry());
  269. const auto &st = st::defaultComposeFiles;
  270. autocomplete->dropdown->setBoundings(QRect(
  271. field.x() - input->x(),
  272. st::defaultBox.margin.top(),
  273. input->width(),
  274. (field.y()
  275. + st.caption.textMargins.top()
  276. + st.caption.placeholderShift
  277. + st.caption.placeholderFont->height
  278. - st::defaultBox.margin.top())));
  279. });
  280. };
  281. for (auto w = (QWidget*)input; w; w = w->parentWidget()) {
  282. base::install_event_filter(raw, w, [=](not_null<QEvent*> e) {
  283. if (e->type() == QEvent::Move || e->type() == QEvent::Resize) {
  284. recountPostponed();
  285. }
  286. return base::EventFilterResult::Continue;
  287. });
  288. if (w == outer) {
  289. break;
  290. }
  291. }
  292. const auto send = [=](Api::SendOptions options) {
  293. done(std::move(options), input->getTextWithTags());
  294. };
  295. const auto confirm = box->addButton(
  296. tr::lng_send_button(),
  297. [=] { send({}); });
  298. SendMenu::SetupMenuAndShortcuts(
  299. confirm,
  300. controller->uiShow(),
  301. sendMenuDetails,
  302. SendMenu::DefaultCallback(controller->uiShow(), send));
  303. box->setShowFinishedCallback([=] {
  304. if (const auto raw = autocomplete->dropdown.get()) {
  305. InvokeQueued(raw, [=] {
  306. raw->raise();
  307. });
  308. }
  309. });
  310. box->addButton(tr::lng_cancel(), [=] {
  311. box->closeBox();
  312. });
  313. input->submits(
  314. ) | rpl::start_with_next([=] { send({}); }, input->lifetime());
  315. }
  316. } // namespace Ui