edit_caption_box.cpp 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087
  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/edit_caption_box.h"
  8. #include "api/api_editing.h"
  9. #include "api/api_text_entities.h"
  10. #include "apiwrap.h"
  11. #include "base/event_filter.h"
  12. #include "boxes/premium_limits_box.h"
  13. #include "boxes/premium_preview_box.h"
  14. #include "chat_helpers/emoji_suggestions_widget.h"
  15. #include "chat_helpers/field_autocomplete.h"
  16. #include "chat_helpers/message_field.h"
  17. #include "chat_helpers/tabbed_panel.h"
  18. #include "chat_helpers/tabbed_selector.h"
  19. #include "core/application.h"
  20. #include "core/core_settings.h"
  21. #include "core/file_utilities.h"
  22. #include "core/mime_type.h"
  23. #include "data/data_document.h"
  24. #include "data/data_photo_media.h"
  25. #include "data/data_session.h"
  26. #include "data/data_user.h"
  27. #include "data/data_premium_limits.h"
  28. #include "data/stickers/data_stickers.h"
  29. #include "data/stickers/data_custom_emoji.h"
  30. #include "editor/editor_layer_widget.h"
  31. #include "editor/photo_editor.h"
  32. #include "editor/photo_editor_layer_widget.h"
  33. #include "history/history_drag_area.h"
  34. #include "history/history_item.h"
  35. #include "history/history.h"
  36. #include "lang/lang_keys.h"
  37. #include "main/main_session.h"
  38. #include "main/main_session_settings.h"
  39. #include "mainwidget.h" // controller->content() -> QWidget*
  40. #include "menu/menu_send.h"
  41. #include "mtproto/mtproto_config.h"
  42. #include "platform/platform_specific.h"
  43. #include "storage/localimageloader.h" // SendMediaType
  44. #include "storage/storage_media_prepare.h"
  45. #include "ui/boxes/confirm_box.h"
  46. #include "ui/chat/attach/attach_item_single_file_preview.h"
  47. #include "ui/chat/attach/attach_item_single_media_preview.h"
  48. #include "ui/chat/attach/attach_single_file_preview.h"
  49. #include "ui/chat/attach/attach_single_media_preview.h"
  50. #include "ui/controls/emoji_button.h"
  51. #include "ui/effects/scroll_content_shadow.h"
  52. #include "ui/image/image.h"
  53. #include "ui/toast/toast.h"
  54. #include "ui/painter.h"
  55. #include "ui/ui_utility.h"
  56. #include "ui/widgets/checkbox.h"
  57. #include "ui/widgets/fields/input_field.h"
  58. #include "ui/widgets/scroll_area.h"
  59. #include "ui/wrap/slide_wrap.h"
  60. #include "ui/wrap/vertical_layout.h"
  61. #include "window/window_session_controller.h"
  62. #include "styles/style_boxes.h"
  63. #include "styles/style_chat.h"
  64. #include "styles/style_chat_helpers.h"
  65. #include "styles/style_layers.h"
  66. #include <QtCore/QMimeData>
  67. namespace {
  68. constexpr auto kChangesDebounceTimeout = crl::time(1000);
  69. [[nodiscard]] Ui::PreparedList ListFromMimeData(
  70. not_null<const QMimeData*> data,
  71. bool premium) {
  72. using Error = Ui::PreparedList::Error;
  73. const auto list = Core::ReadMimeUrls(data);
  74. auto result = !list.isEmpty()
  75. ? Storage::PrepareMediaList(
  76. list.mid(0, 1), // When we edit media, we need only 1 file.
  77. st::sendMediaPreviewSize,
  78. premium)
  79. : Ui::PreparedList(Error::EmptyFile, QString());
  80. if (result.error == Error::None) {
  81. return result;
  82. } else if (auto read = Core::ReadMimeImage(data)) {
  83. return Storage::PrepareMediaFromImage(
  84. std::move(read.image),
  85. std::move(read.content),
  86. st::sendMediaPreviewSize);
  87. }
  88. return result;
  89. }
  90. [[nodiscard]] Ui::AlbumType ComputeAlbumType(not_null<HistoryItem*> item) {
  91. if (item->groupId().empty()) {
  92. return Ui::AlbumType();
  93. }
  94. const auto media = item->media();
  95. if (media->photo()) {
  96. return Ui::AlbumType::PhotoVideo;
  97. } else if (const auto document = media->document()) {
  98. if (document->isVideoFile()) {
  99. return Ui::AlbumType::PhotoVideo;
  100. } else if (document->isSong()) {
  101. return Ui::AlbumType::Music;
  102. } else {
  103. return Ui::AlbumType::File;
  104. }
  105. }
  106. return Ui::AlbumType();
  107. }
  108. [[nodiscard]] bool CanBeCompressed(Ui::AlbumType type) {
  109. return (type == Ui::AlbumType::None)
  110. || (type == Ui::AlbumType::PhotoVideo);
  111. }
  112. void ChooseReplacement(
  113. not_null<Window::SessionController*> controller,
  114. Ui::AlbumType type,
  115. Fn<void(Ui::PreparedList&&)> chosen) {
  116. const auto weak = base::make_weak(controller);
  117. const auto callback = [=](FileDialog::OpenResult &&result) {
  118. const auto strong = weak.get();
  119. if (!strong) {
  120. return;
  121. }
  122. const auto showError = [=](tr::phrase<> t) {
  123. if (const auto strong = weak.get()) {
  124. strong->showToast(t(tr::now));
  125. }
  126. };
  127. const auto checkResult = [=](const Ui::PreparedList &list) {
  128. if (list.files.size() != 1) {
  129. return false;
  130. }
  131. const auto &file = list.files.front();
  132. const auto mime = file.information->filemime;
  133. if (Core::IsMimeSticker(mime)) {
  134. showError(tr::lng_edit_media_invalid_file);
  135. return false;
  136. } else if (type != Ui::AlbumType::None
  137. && !file.canBeInAlbumType(type)) {
  138. showError(tr::lng_edit_media_album_error);
  139. return false;
  140. }
  141. return true;
  142. };
  143. const auto premium = strong->session().premium();
  144. auto list = Storage::PreparedFileFromFilesDialog(
  145. std::move(result),
  146. checkResult,
  147. showError,
  148. st::sendMediaPreviewSize,
  149. premium);
  150. if (list) {
  151. chosen(std::move(*list));
  152. }
  153. };
  154. const auto filters = (type == Ui::AlbumType::PhotoVideo)
  155. ? FileDialog::PhotoVideoFilesFilter()
  156. : FileDialog::AllFilesFilter();
  157. FileDialog::GetOpenPath(
  158. controller->content().get(),
  159. tr::lng_choose_file(tr::now),
  160. filters,
  161. crl::guard(controller, callback));
  162. }
  163. void EditPhotoImage(
  164. not_null<Window::SessionController*> controller,
  165. std::shared_ptr<Data::PhotoMedia> media,
  166. bool spoilered,
  167. Fn<void(Ui::PreparedList)> done) {
  168. const auto large = media
  169. ? media->image(Data::PhotoSize::Large)
  170. : nullptr;
  171. const auto parent = controller->content();
  172. const auto previewWidth = st::sendMediaPreviewSize;
  173. auto callback = [=](const Editor::PhotoModifications &mods) {
  174. if (!mods) {
  175. return;
  176. }
  177. const auto large = media->image(Data::PhotoSize::Large);
  178. if (!large) {
  179. return;
  180. }
  181. auto copy = large->original();
  182. auto list = Storage::PrepareMediaFromImage(
  183. std::move(copy),
  184. QByteArray(),
  185. previewWidth);
  186. using ImageInfo = Ui::PreparedFileInformation::Image;
  187. auto &file = list.files.front();
  188. file.spoiler = spoilered;
  189. const auto image = std::get_if<ImageInfo>(&file.information->media);
  190. image->modifications = mods;
  191. const auto sideLimit = PhotoSideLimit();
  192. Storage::UpdateImageDetails(file, previewWidth, sideLimit);
  193. done(std::move(list));
  194. };
  195. if (!large) {
  196. return;
  197. }
  198. const auto fileImage = std::make_shared<Image>(*large);
  199. auto editor = base::make_unique_q<Editor::PhotoEditor>(
  200. parent,
  201. &controller->window(),
  202. fileImage,
  203. Editor::PhotoModifications());
  204. const auto raw = editor.get();
  205. auto layer = std::make_unique<Editor::LayerWidget>(
  206. parent,
  207. std::move(editor));
  208. Editor::InitEditorLayer(layer.get(), raw, std::move(callback));
  209. controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);
  210. }
  211. } // namespace
  212. EditCaptionBox::EditCaptionBox(
  213. QWidget*,
  214. not_null<Window::SessionController*> controller,
  215. not_null<HistoryItem*> item,
  216. TextWithTags &&text,
  217. bool spoilered,
  218. bool invertCaption,
  219. Ui::PreparedList &&list,
  220. Fn<void()> saved)
  221. : _controller(controller)
  222. , _historyItem(item)
  223. , _isAllowedEditMedia(item->allowsEditMedia())
  224. , _albumType(ComputeAlbumType(item))
  225. , _controls(base::make_unique_q<Ui::VerticalLayout>(this))
  226. , _scroll(base::make_unique_q<Ui::ScrollArea>(this, st::boxScroll))
  227. , _field(base::make_unique_q<Ui::InputField>(
  228. this,
  229. st::defaultComposeFiles.caption,
  230. Ui::InputField::Mode::MultiLine,
  231. tr::lng_photo_caption()))
  232. , _emojiToggle(base::make_unique_q<Ui::EmojiButton>(
  233. this,
  234. st::defaultComposeFiles.emoji))
  235. , _initialText(std::move(text))
  236. , _initialList(std::move(list))
  237. , _saved(std::move(saved)) {
  238. Expects(!_initialList.files.empty());
  239. Expects(item->allowsEditMedia());
  240. _mediaEditManager.start(item, spoilered, invertCaption);
  241. _controller->session().data().itemRemoved(
  242. _historyItem->fullId()
  243. ) | rpl::start_with_next([=] {
  244. closeBox();
  245. }, lifetime());
  246. }
  247. EditCaptionBox::~EditCaptionBox() = default;
  248. void EditCaptionBox::StartMediaReplace(
  249. not_null<Window::SessionController*> controller,
  250. FullMsgId itemId,
  251. TextWithTags text,
  252. bool spoilered,
  253. bool invertCaption,
  254. Fn<void()> saved) {
  255. const auto session = &controller->session();
  256. const auto item = session->data().message(itemId);
  257. if (!item) {
  258. return;
  259. }
  260. const auto show = [=](Ui::PreparedList &&list) mutable {
  261. controller->show(Box<EditCaptionBox>(
  262. controller,
  263. item,
  264. std::move(text),
  265. spoilered,
  266. invertCaption,
  267. std::move(list),
  268. std::move(saved)));
  269. };
  270. ChooseReplacement(
  271. controller,
  272. ComputeAlbumType(item),
  273. crl::guard(controller, show));
  274. }
  275. void EditCaptionBox::StartMediaReplace(
  276. not_null<Window::SessionController*> controller,
  277. FullMsgId itemId,
  278. Ui::PreparedList &&list,
  279. TextWithTags text,
  280. bool spoilered,
  281. bool invertCaption,
  282. Fn<void()> saved) {
  283. const auto session = &controller->session();
  284. const auto item = session->data().message(itemId);
  285. if (!item) {
  286. return;
  287. }
  288. const auto type = ComputeAlbumType(item);
  289. const auto showError = [=](tr::phrase<> t) {
  290. controller->showToast(t(tr::now));
  291. };
  292. const auto checkResult = [=](const Ui::PreparedList &list) {
  293. if (list.files.size() != 1) {
  294. return false;
  295. }
  296. const auto &file = list.files.front();
  297. const auto mime = file.information->filemime;
  298. if (Core::IsMimeSticker(mime)) {
  299. showError(tr::lng_edit_media_invalid_file);
  300. return false;
  301. } else if (type != Ui::AlbumType::None
  302. && !file.canBeInAlbumType(type)) {
  303. showError(tr::lng_edit_media_album_error);
  304. return false;
  305. }
  306. return true;
  307. };
  308. if (list.error != Ui::PreparedList::Error::None) {
  309. showError(tr::lng_send_media_invalid_files);
  310. } else if (checkResult(list)) {
  311. controller->show(Box<EditCaptionBox>(
  312. controller,
  313. item,
  314. std::move(text),
  315. spoilered,
  316. invertCaption,
  317. std::move(list),
  318. std::move(saved)));
  319. }
  320. }
  321. void EditCaptionBox::StartPhotoEdit(
  322. not_null<Window::SessionController*> controller,
  323. std::shared_ptr<Data::PhotoMedia> media,
  324. FullMsgId itemId,
  325. TextWithTags text,
  326. bool spoilered,
  327. bool invertCaption,
  328. Fn<void()> saved) {
  329. const auto session = &controller->session();
  330. const auto item = session->data().message(itemId);
  331. if (!item) {
  332. return;
  333. }
  334. EditPhotoImage(controller, media, spoilered, [=](
  335. Ui::PreparedList &&list) mutable {
  336. const auto item = session->data().message(itemId);
  337. if (!item) {
  338. return;
  339. }
  340. controller->show(Box<EditCaptionBox>(
  341. controller,
  342. item,
  343. std::move(text),
  344. spoilered,
  345. invertCaption,
  346. std::move(list),
  347. std::move(saved)));
  348. });
  349. }
  350. void EditCaptionBox::showFinished() {
  351. if (const auto raw = _autocomplete.get()) {
  352. InvokeQueued(raw, [=] {
  353. raw->raise();
  354. });
  355. }
  356. }
  357. void EditCaptionBox::prepare() {
  358. const auto button = addButton(tr::lng_settings_save(), [=] { save(); });
  359. addButton(tr::lng_cancel(), [=] { closeBox(); });
  360. const auto details = crl::guard(this, [=] {
  361. auto result = SendMenu::Details();
  362. const auto allWithSpoilers = ranges::all_of(
  363. _preparedList.files,
  364. &Ui::PreparedFile::spoiler);
  365. result.spoiler = !_preparedList.hasSpoilerMenu(!_asFile)
  366. ? SendMenu::SpoilerState::None
  367. : allWithSpoilers
  368. ? SendMenu::SpoilerState::Enabled
  369. : SendMenu::SpoilerState::Possible;
  370. const auto canMoveCaption = _preparedList.canMoveCaption(
  371. false,
  372. !_asFile
  373. ) && _field && HasSendText(_field);
  374. result.caption = !canMoveCaption
  375. ? SendMenu::CaptionState::None
  376. : _mediaEditManager.invertCaption()
  377. ? SendMenu::CaptionState::Above
  378. : SendMenu::CaptionState::Below;
  379. return result;
  380. });
  381. const auto callback = [=](SendMenu::Action action, const auto &) {
  382. _mediaEditManager.apply(action);
  383. rebuildPreview();
  384. };
  385. SendMenu::SetupMenuAndShortcuts(
  386. button,
  387. nullptr,
  388. details,
  389. crl::guard(this, callback));
  390. updateBoxSize();
  391. setupField();
  392. setupEmojiPanel();
  393. setInitialText();
  394. if (!setPreparedList(std::move(_initialList))) {
  395. crl::on_main(this, [=] { closeBox(); });
  396. return;
  397. }
  398. setupEditEventHandler();
  399. SetupShadowsToScrollContent(this, _scroll, _contentHeight.events());
  400. setupControls();
  401. setupPhotoEditorEventHandler();
  402. setupDragArea();
  403. captionResized();
  404. }
  405. void EditCaptionBox::rebuildPreview() {
  406. const auto gifPaused = [controller = _controller] {
  407. return controller->isGifPausedAtLeastFor(
  408. Window::GifPauseReason::Layer);
  409. };
  410. applyChanges();
  411. if (_preparedList.files.empty()) {
  412. const auto media = _historyItem->media();
  413. const auto photo = media->photo();
  414. const auto document = media->document();
  415. _isPhoto = (photo != nullptr);
  416. if (photo || document->isVideoFile() || document->isAnimation()) {
  417. const auto media = Ui::CreateChild<Ui::ItemSingleMediaPreview>(
  418. this,
  419. st::defaultComposeControls,
  420. gifPaused,
  421. _historyItem,
  422. Ui::AttachControls::Type::EditOnly);
  423. _photoMedia = media->sharedPhotoMedia();
  424. _content.reset(media);
  425. } else {
  426. _content.reset(Ui::CreateChild<Ui::ItemSingleFilePreview>(
  427. this,
  428. st::defaultComposeControls,
  429. _historyItem,
  430. Ui::AttachControls::Type::EditOnly));
  431. }
  432. } else {
  433. const auto &file = _preparedList.files.front();
  434. const auto isVideoFile = file.isVideoFile();
  435. const auto media = Ui::SingleMediaPreview::Create(
  436. this,
  437. st::defaultComposeControls,
  438. gifPaused,
  439. file,
  440. [=](Ui::AttachActionType type) {
  441. return (type != Ui::AttachActionType::EditCover)
  442. || isVideoFile;
  443. },
  444. Ui::AttachControls::Type::EditOnly);
  445. _isPhoto = (media && media->isPhoto());
  446. const auto withCheckbox = _isPhoto && CanBeCompressed(_albumType);
  447. if (media && (!withCheckbox || !_asFile)) {
  448. media->spoileredChanges(
  449. ) | rpl::start_with_next([=](bool spoilered) {
  450. _mediaEditManager.apply({ .type = spoilered
  451. ? SendMenu::ActionType::SpoilerOn
  452. : SendMenu::ActionType::SpoilerOff
  453. });
  454. }, media->lifetime());
  455. _content.reset(media);
  456. } else {
  457. _content.reset(Ui::CreateChild<Ui::SingleFilePreview>(
  458. this,
  459. st::defaultComposeControls,
  460. file,
  461. Ui::AttachControls::Type::EditOnly));
  462. }
  463. }
  464. Assert(_content != nullptr);
  465. rpl::combine(
  466. _content->heightValue(),
  467. _footerHeight.value(),
  468. rpl::single(st::boxPhotoPadding.top()),
  469. rpl::mappers::_1 + rpl::mappers::_2 + rpl::mappers::_3
  470. ) | rpl::start_with_next([=](int height) {
  471. setDimensions(
  472. st::boxWideWidth,
  473. std::min(st::sendMediaPreviewHeightMax, height),
  474. true);
  475. }, _content->lifetime());
  476. _content->editRequests(
  477. ) | rpl::start_to_stream(_editMediaClicks, _content->lifetime());
  478. _content->modifyRequests(
  479. ) | rpl::start_to_stream(_photoEditorOpens, _content->lifetime());
  480. _content->heightValue(
  481. ) | rpl::start_to_stream(_contentHeight, _content->lifetime());
  482. _scroll->setOwnedWidget(
  483. object_ptr<Ui::RpWidget>::fromRaw(_content.get()));
  484. _previewRebuilds.fire({});
  485. captionResized();
  486. }
  487. void EditCaptionBox::setupField() {
  488. const auto peer = _historyItem->history()->peer;
  489. const auto allow = [=](not_null<DocumentData*> emoji) {
  490. return Data::AllowEmojiWithoutPremium(peer, emoji);
  491. };
  492. InitMessageFieldHandlers(
  493. _controller,
  494. _field.get(),
  495. Window::GifPauseReason::Layer,
  496. allow);
  497. setupFieldAutocomplete();
  498. Ui::Emoji::SuggestionsController::Init(
  499. getDelegate()->outerContainer(),
  500. _field,
  501. &_controller->session(),
  502. { .suggestCustomEmoji = true, .allowCustomWithoutPremium = allow });
  503. _field->setSubmitSettings(
  504. Core::App().settings().sendSubmitWay());
  505. _field->setMaxHeight(st::defaultComposeFiles.caption.heightMax);
  506. _field->submits(
  507. ) | rpl::start_with_next([=] { save(); }, _field->lifetime());
  508. _field->cancelled(
  509. ) | rpl::start_with_next([=] {
  510. closeBox();
  511. }, _field->lifetime());
  512. _field->heightChanges(
  513. ) | rpl::start_with_next([=] {
  514. captionResized();
  515. }, _field->lifetime());
  516. _field->setMimeDataHook([=](
  517. not_null<const QMimeData*> data,
  518. Ui::InputField::MimeAction action) {
  519. if (action == Ui::InputField::MimeAction::Check) {
  520. if (!data->hasText() && !_isAllowedEditMedia) {
  521. return false;
  522. } else if (Storage::ValidateEditMediaDragData(data, _albumType)) {
  523. return true;
  524. }
  525. return data->hasText();
  526. } else if (action == Ui::InputField::MimeAction::Insert) {
  527. return fileFromClipboard(data);
  528. }
  529. Unexpected("Action in MimeData hook.");
  530. });
  531. }
  532. void EditCaptionBox::setupFieldAutocomplete() {
  533. const auto parent = getDelegate()->outerContainer();
  534. ChatHelpers::InitFieldAutocomplete(_autocomplete, {
  535. .parent = parent,
  536. .show = _controller->uiShow(),
  537. .field = _field.get(),
  538. .peer = _historyItem->history()->peer,
  539. .features = [=] {
  540. auto result = ChatHelpers::ComposeFeatures();
  541. result.autocompleteCommands = false;
  542. result.suggestStickersByEmoji = false;
  543. return result;
  544. },
  545. });
  546. const auto raw = _autocomplete.get();
  547. const auto scheduled = std::make_shared<bool>();
  548. const auto recountPostponed = [=] {
  549. if (*scheduled) {
  550. return;
  551. }
  552. *scheduled = true;
  553. Ui::PostponeCall(raw, [=] {
  554. *scheduled = false;
  555. auto field = Ui::MapFrom(parent, this, _field->geometry());
  556. _autocomplete->setBoundings(QRect(
  557. field.x() - _field->x(),
  558. st::defaultBox.margin.top(),
  559. width(),
  560. (field.y()
  561. + st::defaultComposeFiles.caption.textMargins.top()
  562. + st::defaultComposeFiles.caption.placeholderShift
  563. + st::defaultComposeFiles.caption.placeholderFont->height
  564. - st::defaultBox.margin.top())));
  565. });
  566. };
  567. for (auto w = (QWidget*)_field.get(); w; w = w->parentWidget()) {
  568. base::install_event_filter(raw, w, [=](not_null<QEvent*> e) {
  569. if (e->type() == QEvent::Move || e->type() == QEvent::Resize) {
  570. recountPostponed();
  571. }
  572. return base::EventFilterResult::Continue;
  573. });
  574. if (w == parent) {
  575. break;
  576. }
  577. }
  578. }
  579. void EditCaptionBox::setInitialText() {
  580. _field->setTextWithTags(
  581. _initialText,
  582. Ui::InputField::HistoryAction::Clear);
  583. auto cursor = _field->textCursor();
  584. cursor.movePosition(QTextCursor::End);
  585. _field->setTextCursor(cursor);
  586. _checkChangedTimer.setCallback([=] {
  587. if (_field->getTextWithAppliedMarkdown() == _initialText
  588. && _preparedList.files.empty()) {
  589. setCloseByOutsideClick(true);
  590. }
  591. });
  592. _field->changes(
  593. ) | rpl::start_with_next([=] {
  594. _checkChangedTimer.callOnce(kChangesDebounceTimeout);
  595. setCloseByOutsideClick(false);
  596. }, _field->lifetime());
  597. }
  598. void EditCaptionBox::setupControls() {
  599. auto hintLabelToggleOn = _previewRebuilds.events_starting_with(
  600. {}
  601. ) | rpl::map([=] {
  602. return _controller->session().settings().photoEditorHintShown()
  603. ? (_isPhoto && !_asFile)
  604. : false;
  605. });
  606. _controls->add(object_ptr<Ui::SlideWrap<Ui::FlatLabel>>(
  607. this,
  608. object_ptr<Ui::FlatLabel>(
  609. this,
  610. tr::lng_edit_photo_editor_hint(tr::now),
  611. st::editMediaHintLabel),
  612. st::editMediaLabelMargins)
  613. )->toggleOn(std::move(hintLabelToggleOn), anim::type::instant);
  614. _controls->add(object_ptr<Ui::SlideWrap<Ui::Checkbox>>(
  615. this,
  616. object_ptr<Ui::Checkbox>(
  617. this,
  618. tr::lng_send_compressed_one(tr::now),
  619. true,
  620. st::defaultBoxCheckbox),
  621. st::editMediaCheckboxMargins)
  622. )->toggleOn(
  623. _previewRebuilds.events_starting_with({}) | rpl::map([=] {
  624. return _isPhoto
  625. && CanBeCompressed(_albumType)
  626. && !_preparedList.files.empty();
  627. }),
  628. anim::type::instant
  629. )->entity()->checkedChanges(
  630. ) | rpl::start_with_next([&](bool checked) {
  631. applyChanges();
  632. _asFile = !checked;
  633. rebuildPreview();
  634. }, _controls->lifetime());
  635. _controls->resizeToWidth(st::sendMediaPreviewSize);
  636. }
  637. void EditCaptionBox::setupEditEventHandler() {
  638. _editMediaClicks.events(
  639. ) | rpl::start_with_next([=] {
  640. ChooseReplacement(_controller, _albumType, crl::guard(this, [=](
  641. Ui::PreparedList &&list) {
  642. setPreparedList(std::move(list));
  643. }));
  644. }, lifetime());
  645. }
  646. void EditCaptionBox::setupPhotoEditorEventHandler() {
  647. const auto openedOnce = lifetime().make_state<bool>(false);
  648. _photoEditorOpens.events(
  649. ) | rpl::start_with_next([=, controller = _controller] {
  650. if (_preparedList.files.empty()
  651. && (!_photoMedia
  652. || !_photoMedia->image(Data::PhotoSize::Large))) {
  653. return;
  654. } else if (!*openedOnce) {
  655. *openedOnce = true;
  656. controller->session().settings().incrementPhotoEditorHintShown();
  657. controller->session().saveSettings();
  658. }
  659. if (!_error.isEmpty()) {
  660. _error = QString();
  661. update();
  662. }
  663. if (!_preparedList.files.empty()) {
  664. Editor::OpenWithPreparedFile(
  665. this,
  666. controller->uiShow(),
  667. &_preparedList.files.front(),
  668. st::sendMediaPreviewSize,
  669. [=](bool ok) { if (ok) rebuildPreview(); });
  670. } else {
  671. EditPhotoImage(_controller, _photoMedia, hasSpoiler(), [=](
  672. Ui::PreparedList &&list) {
  673. setPreparedList(std::move(list));
  674. });
  675. }
  676. }, lifetime());
  677. }
  678. void EditCaptionBox::setupDragArea() {
  679. auto enterFilter = [=](not_null<const QMimeData*> data) {
  680. return !_isAllowedEditMedia
  681. ? false
  682. : Storage::ValidateEditMediaDragData(data, _albumType);
  683. };
  684. // Avoid both drag areas appearing at one time.
  685. auto computeState = [=](const QMimeData *data) {
  686. using DragState = Storage::MimeDataState;
  687. const auto state = Storage::ComputeMimeDataState(data);
  688. return (state == DragState::PhotoFiles || state == DragState::Image)
  689. ? (_asFile ? DragState::Files : DragState::Image)
  690. : state;
  691. };
  692. const auto areas = DragArea::SetupDragAreaToContainer(
  693. this,
  694. std::move(enterFilter),
  695. [=](bool f) { _field->setAcceptDrops(f); },
  696. nullptr,
  697. std::move(computeState));
  698. const auto droppedCallback = [=](bool compress) {
  699. return [=](const QMimeData *data) {
  700. fileFromClipboard(data);
  701. Window::ActivateWindow(_controller);
  702. };
  703. };
  704. areas.document->setDroppedCallback(droppedCallback(false));
  705. areas.photo->setDroppedCallback(droppedCallback(true));
  706. }
  707. void EditCaptionBox::setupEmojiPanel() {
  708. const auto container = getDelegate()->outerContainer();
  709. using Selector = ChatHelpers::TabbedSelector;
  710. _emojiPanel = base::make_unique_q<ChatHelpers::TabbedPanel>(
  711. container,
  712. _controller,
  713. object_ptr<Selector>(
  714. nullptr,
  715. _controller->uiShow(),
  716. Window::GifPauseReason::Layer,
  717. Selector::Mode::EmojiOnly));
  718. _emojiPanel->setDesiredHeightValues(
  719. 1.,
  720. st::emojiPanMinHeight / 2,
  721. st::emojiPanMinHeight);
  722. _emojiPanel->hide();
  723. _emojiPanel->selector()->setCurrentPeer(_historyItem->history()->peer);
  724. _emojiPanel->selector()->emojiChosen(
  725. ) | rpl::start_with_next([=](ChatHelpers::EmojiChosen data) {
  726. Ui::InsertEmojiAtCursor(_field->textCursor(), data.emoji);
  727. }, lifetime());
  728. _emojiPanel->selector()->customEmojiChosen(
  729. ) | rpl::start_with_next([=](ChatHelpers::FileChosen data) {
  730. const auto info = data.document->sticker();
  731. if (info
  732. && info->setType == Data::StickersType::Emoji
  733. && !_controller->session().premium()) {
  734. ShowPremiumPreviewBox(
  735. _controller,
  736. PremiumFeature::AnimatedEmoji);
  737. } else {
  738. Data::InsertCustomEmoji(_field.get(), data.document);
  739. }
  740. }, lifetime());
  741. const auto filterCallback = [=](not_null<QEvent*> event) {
  742. emojiFilterForGeometry(event);
  743. return base::EventFilterResult::Continue;
  744. };
  745. _emojiFilter.reset(base::install_event_filter(container, filterCallback));
  746. _emojiToggle->installEventFilter(_emojiPanel);
  747. _emojiToggle->addClickHandler([=] {
  748. _emojiPanel->toggleAnimated();
  749. });
  750. }
  751. void EditCaptionBox::emojiFilterForGeometry(not_null<QEvent*> event) {
  752. const auto type = event->type();
  753. if (type == QEvent::Move || type == QEvent::Resize) {
  754. // updateEmojiPanelGeometry uses not only container geometry, but
  755. // also container children geometries that will be updated later.
  756. crl::on_main(this, [=] { updateEmojiPanelGeometry(); });
  757. }
  758. }
  759. void EditCaptionBox::updateEmojiPanelGeometry() {
  760. const auto parent = _emojiPanel->parentWidget();
  761. const auto global = _emojiToggle->mapToGlobal({ 0, 0 });
  762. const auto local = parent->mapFromGlobal(global);
  763. _emojiPanel->moveBottomRight(
  764. local.y(),
  765. local.x() + _emojiToggle->width() * 3);
  766. }
  767. bool EditCaptionBox::fileFromClipboard(not_null<const QMimeData*> data) {
  768. const auto premium = _controller->session().premium();
  769. return setPreparedList(ListFromMimeData(data, premium));
  770. }
  771. bool EditCaptionBox::setPreparedList(Ui::PreparedList &&list) {
  772. if (!_isAllowedEditMedia) {
  773. return false;
  774. }
  775. using Error = Ui::PreparedList::Error;
  776. if (list.error != Error::None || list.files.empty()) {
  777. return false;
  778. }
  779. auto file = &list.files.front();
  780. const auto invalidForAlbum = (_albumType != Ui::AlbumType::None)
  781. && !file->canBeInAlbumType(_albumType);
  782. if (_albumType == Ui::AlbumType::PhotoVideo) {
  783. using Video = Ui::PreparedFileInformation::Video;
  784. if (const auto video = std::get_if<Video>(
  785. &file->information->media)) {
  786. video->isGifv = false;
  787. }
  788. }
  789. if (invalidForAlbum) {
  790. showToast(tr::lng_edit_media_album_error(tr::now));
  791. return false;
  792. }
  793. const auto wasSpoiler = hasSpoiler();
  794. _preparedList = std::move(list);
  795. _preparedList.files.front().spoiler = wasSpoiler;
  796. setCloseByOutsideClick(false);
  797. rebuildPreview();
  798. return true;
  799. }
  800. bool EditCaptionBox::hasSpoiler() const {
  801. return _mediaEditManager.spoilered();
  802. }
  803. void EditCaptionBox::captionResized() {
  804. updateBoxSize();
  805. resizeEvent(0);
  806. updateEmojiPanelGeometry();
  807. update();
  808. }
  809. void EditCaptionBox::updateBoxSize() {
  810. auto footerHeight = 0;
  811. footerHeight += st::normalFont->height + errorTopSkip();
  812. if (_field) {
  813. footerHeight += st::boxPhotoCaptionSkip + _field->height();
  814. }
  815. if (_controls && !_controls->isHidden()) {
  816. footerHeight += _controls->heightNoMargins();
  817. }
  818. _footerHeight = footerHeight;
  819. }
  820. int EditCaptionBox::errorTopSkip() const {
  821. return (st::defaultBox.buttonPadding.top() / 2);
  822. }
  823. void EditCaptionBox::paintEvent(QPaintEvent *e) {
  824. BoxContent::paintEvent(e);
  825. Painter p(this);
  826. if (!_error.isEmpty()) {
  827. p.setFont(st::normalFont);
  828. p.setPen(st::boxTextFgError);
  829. p.drawTextLeft(
  830. _field->x(),
  831. _field->y() + _field->height() + errorTopSkip(),
  832. width(),
  833. _error);
  834. }
  835. }
  836. void EditCaptionBox::resizeEvent(QResizeEvent *e) {
  837. BoxContent::resizeEvent(e);
  838. const auto errorHeight = st::normalFont->height + errorTopSkip();
  839. auto bottom = height();
  840. {
  841. const auto resultScrollHeight = bottom
  842. - _field->height()
  843. - st::boxPhotoCaptionSkip
  844. - (_controls->isHidden() ? 0 : _controls->heightNoMargins())
  845. - st::boxPhotoPadding.top()
  846. - errorHeight;
  847. const auto minThumbH = st::sendBoxAlbumGroupSize.height()
  848. + st::sendBoxAlbumGroupSkipTop * 2;
  849. const auto diff = resultScrollHeight - minThumbH;
  850. if (diff < 0) {
  851. bottom -= diff;
  852. }
  853. }
  854. bottom -= errorHeight;
  855. _field->resize(st::sendMediaPreviewSize, _field->height());
  856. _field->moveToLeft(
  857. st::boxPhotoPadding.left(),
  858. bottom - _field->height());
  859. bottom -= st::boxPhotoCaptionSkip + _field->height();
  860. _emojiToggle->moveToLeft(
  861. (st::boxPhotoPadding.left()
  862. + st::sendMediaPreviewSize
  863. - _emojiToggle->width()),
  864. _field->y() + st::boxAttachEmojiTop);
  865. _emojiToggle->update();
  866. if (!_controls->isHidden()) {
  867. _controls->resizeToWidth(width());
  868. _controls->moveToLeft(
  869. st::boxPhotoPadding.left(),
  870. bottom - _controls->heightNoMargins());
  871. bottom -= _controls->heightNoMargins();
  872. }
  873. _scroll->resize(width(), bottom - st::boxPhotoPadding.top());
  874. _scroll->move(0, st::boxPhotoPadding.top());
  875. if (_content) {
  876. _content->resize(_scroll->width(), _content->height());
  877. }
  878. }
  879. void EditCaptionBox::setInnerFocus() {
  880. _field->setFocusFast();
  881. }
  882. bool EditCaptionBox::validateLength(const QString &text) const {
  883. const auto session = &_controller->session();
  884. const auto limit = Data::PremiumLimits(session).captionLengthCurrent();
  885. const auto remove = int(text.size()) - limit;
  886. if (remove <= 0) {
  887. return true;
  888. }
  889. _controller->show(
  890. Box(CaptionLimitReachedBox, session, remove, nullptr));
  891. return false;
  892. }
  893. void EditCaptionBox::applyChanges() {
  894. if (!_preparedList.files.empty()) {
  895. _preparedList.files.front().spoiler = _mediaEditManager.spoilered();
  896. }
  897. }
  898. void EditCaptionBox::save() {
  899. if (_saveRequestId) {
  900. return;
  901. }
  902. const auto item = _controller->session().data().message(
  903. _historyItem->fullId());
  904. if (!item) {
  905. _error = tr::lng_edit_deleted(tr::now);
  906. update();
  907. return;
  908. }
  909. const auto textWithTags = _field->getTextWithAppliedMarkdown();
  910. if (!validateLength(textWithTags.text)) {
  911. return;
  912. }
  913. const auto sending = TextWithEntities{
  914. textWithTags.text,
  915. TextUtilities::ConvertTextTagsToEntities(textWithTags.tags)
  916. };
  917. auto options = Api::SendOptions();
  918. options.scheduled = item->isScheduled() ? item->date() : 0;
  919. options.shortcutId = item->shortcutId();
  920. options.invertCaption = _mediaEditManager.invertCaption();
  921. if (!_preparedList.files.empty()) {
  922. if ((_albumType != Ui::AlbumType::None)
  923. && !_preparedList.files.front().canBeInAlbumType(
  924. _albumType)) {
  925. _error = tr::lng_edit_media_album_error(tr::now);
  926. update();
  927. return;
  928. }
  929. auto action = Api::SendAction(item->history(), options);
  930. action.replaceMediaOf = item->fullId().msg;
  931. Storage::ApplyModifications(_preparedList);
  932. if (!_preparedList.files.empty()) {
  933. _preparedList.files.front().spoiler = false;
  934. applyChanges();
  935. }
  936. _controller->session().api().editMedia(
  937. std::move(_preparedList),
  938. (_isPhoto && !_asFile && CanBeCompressed(_albumType))
  939. ? SendMediaType::Photo
  940. : SendMediaType::File,
  941. _field->getTextWithAppliedMarkdown(),
  942. action);
  943. closeAfterSave();
  944. return;
  945. }
  946. const auto done = crl::guard(this, [=] {
  947. _saveRequestId = 0;
  948. closeAfterSave();
  949. });
  950. const auto fail = crl::guard(this, [=](const QString &error) {
  951. _saveRequestId = 0;
  952. if (ranges::contains(Api::kDefaultEditMessagesErrors, error)) {
  953. _error = tr::lng_edit_error(tr::now);
  954. update();
  955. } else if (error == u"MESSAGE_NOT_MODIFIED"_q) {
  956. closeAfterSave();
  957. } else if (error == u"MESSAGE_EMPTY"_q) {
  958. _field->setFocus();
  959. _field->showError();
  960. update();
  961. } else {
  962. _error = tr::lng_edit_error(tr::now);
  963. update();
  964. }
  965. });
  966. lifetime().add([=] {
  967. if (_saveRequestId) {
  968. auto &session = _controller->session();
  969. session.api().request(base::take(_saveRequestId)).cancel();
  970. }
  971. });
  972. _saveRequestId = Api::EditCaption(item, sending, options, done, fail);
  973. }
  974. void EditCaptionBox::closeAfterSave() {
  975. const auto weak = MakeWeak(this);
  976. if (_saved) {
  977. _saved();
  978. }
  979. if (weak) {
  980. closeBox();
  981. }
  982. }
  983. void EditCaptionBox::keyPressEvent(QKeyEvent *e) {
  984. const auto ctrl = e->modifiers().testFlag(Qt::ControlModifier);
  985. if ((e->key() == Qt::Key_E) && ctrl) {
  986. _photoEditorOpens.fire({});
  987. } else if ((e->key() == Qt::Key_O) && ctrl) {
  988. _editMediaClicks.fire({});
  989. } else {
  990. e->ignore();
  991. }
  992. }