From 42c96b4c7fd03c722068e911382a82cc6fcb4507 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 7 Apr 2023 18:32:53 +0400 Subject: [PATCH] Edit media captions in message field. --- Telegram/Resources/icons/chat/input_draw.png | Bin 0 -> 953 bytes .../Resources/icons/chat/input_draw@2x.png | Bin 0 -> 1865 bytes .../Resources/icons/chat/input_draw@3x.png | Bin 0 -> 2767 bytes .../Resources/icons/chat/input_replace.png | Bin 0 -> 749 bytes .../Resources/icons/chat/input_replace@2x.png | Bin 0 -> 1390 bytes .../Resources/icons/chat/input_replace@3x.png | Bin 0 -> 1931 bytes .../SourceFiles/boxes/edit_caption_box.cpp | 331 +++++++++++------- Telegram/SourceFiles/boxes/edit_caption_box.h | 24 ++ .../SourceFiles/history/history_widget.cpp | 114 +++++- Telegram/SourceFiles/history/history_widget.h | 7 + Telegram/SourceFiles/ui/chat/chat.style | 9 + 11 files changed, 355 insertions(+), 130 deletions(-) create mode 100644 Telegram/Resources/icons/chat/input_draw.png create mode 100644 Telegram/Resources/icons/chat/input_draw@2x.png create mode 100644 Telegram/Resources/icons/chat/input_draw@3x.png create mode 100644 Telegram/Resources/icons/chat/input_replace.png create mode 100644 Telegram/Resources/icons/chat/input_replace@2x.png create mode 100644 Telegram/Resources/icons/chat/input_replace@3x.png diff --git a/Telegram/Resources/icons/chat/input_draw.png b/Telegram/Resources/icons/chat/input_draw.png new file mode 100644 index 0000000000000000000000000000000000000000..37f5c854c2d3b9223f900399c58cc710f7f0caf3 GIT binary patch literal 953 zcmV;q14jIbP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR917@z|H1ONa40RR917ytkO0OB=utpET6BS}O-R5%f>R7pr`Q5e3>Wf`Rs z4WusG*dojzl<1XE5mFElB+(`asZEQ*T~rI9s8vCWre%UetqcN#hD8!IY%H`SGol$A zq?88jvy*x6DP6t0IQRVDKTn5Y{?ixtr=66Plz(4eVPRorX6F6<{qgbf=H{l=Y7GX1 zf2z=6T3Xt`z`*tOH56`cZnn0zR##VDE>|EB0OE8yySlpKH2yl_xkTf4Eb0WM5aiX5`2sOakIYJY#fyu6&l;1bo<)t#N4T#QBK=jS8-!^1+9=$ zK0hNP9?DUXG5A zCW5H1uQ!=YL!26H8C*}67zb!eSLjI0WB{s zE@n5LZD@9O_Vx7@jU(XX^fcm+@WMAtz{khO$jAr*T3cH&>%+stVSf0JSSl(iFcf~jU#HWB z;qSjfp+K`y9g;G74>7bQS_K%jS`D{at(F)-vU78DJ3BisFE3afG7~bH3}s$hTT4z( zru==A&CSg|pU+~kkV>HzZ*Ol02L~uHDu~3##>R4UbFlybv$C@OwgMD>c6WC%kho*& z(Mp*3S@fQs9@xSJU0hs<#p0+|z!)4H{F$b%P6cjilh5dI>PZPft&r7bBJ#hCyp!2z_a9 zZ!aq=%gM>XLLfSfUw3zRY+ag*9VT(XBKgkG&l?*XX*n((d6l_1Bauk(Y-1V_oy}&W bxWCCyho^=&qnUhY00000NkvXXu0mjfH-MpS literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/input_draw@2x.png b/Telegram/Resources/icons/chat/input_draw@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0087392e5ef83c0b721627fbf98f66dc3cabfb56 GIT binary patch literal 1865 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 literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/input_replace.png b/Telegram/Resources/icons/chat/input_replace.png new file mode 100644 index 0000000000000000000000000000000000000000..e0bc68cb34897c575afb220447364d0b2815b9f4 GIT binary patch literal 749 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1SIoCSFHz9jKx9jP7LeL$-D$|Tv8)E(|mmy zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(-uv49!D1}S`GTDq2jfhpY6#WBP} z@N9_h>dZiivKr$>XHGCM7qk6qn9}3na;jxz=Msm6!ho`S|($3C9 zE`|b6U*CCtZ|mA`cVEh-7wyj8P<{Qg-S58@_qK}lf1j_{e9%CyUw``P=gDu&cKfIu ze*QUrO@v3q(WJn&VeX4BUViB!us2S=`{=XJl?wwVoPK&YFMMUlp;8N(sI_5cvt5s= zTLyTm2tBk&+Z?GkomoSGW75ed{r6X2etGBp_X3MEipHXxtxi$gsSJ#LZ{L=6AAc+& z-|NQL-uz_2gVjP8OQ=^4u$$j*m9%k2i^Kkuz1aEwEMLc8S%}kR*k$_1Bs2zy4|_#oPD2I(n{Ox{1{O zAM89I_9@sF8?O}i_Yq+UY;*Yqyh`nG?hjAs-+#;XxBqMFn|4}tQXtE*q>T&a8kO^kZj^lfd1mv5 zz5coHL@nj|wWhw({Q6!YSWwkbc8^$v_5v#hj|mk8EkGDUt=blEG~#HH`KxW4Vj^*!@F W5?mS|7;Z`dB{ff1KbLh*2~7a}%`x%- literal 0 HcmV?d00001 diff --git a/Telegram/Resources/icons/chat/input_replace@2x.png b/Telegram/Resources/icons/chat/input_replace@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3a634692ab1e58b9f231174d097b95cb6323e1f9 GIT binary patch literal 1390 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 -> QWidget* #include "mtproto/mtproto_config.h" #include "platform/platform_specific.h" #include "storage/localimageloader.h" // SendMediaType @@ -69,7 +70,9 @@ namespace { constexpr auto kChangesDebounceTimeout = crl::time(1000); -auto ListFromMimeData(not_null data, bool premium) { +[[nodiscard]] Ui::PreparedList ListFromMimeData( + not_null data, + bool premium) { using Error = Ui::PreparedList::Error; const auto list = Core::ReadMimeUrls(data); auto result = !list.isEmpty() @@ -89,7 +92,7 @@ auto ListFromMimeData(not_null data, bool premium) { return result; } -Ui::AlbumType ComputeAlbumType(not_null item) { +[[nodiscard]] Ui::AlbumType ComputeAlbumType(not_null item) { if (item->groupId().empty()) { return Ui::AlbumType(); } @@ -109,17 +112,130 @@ Ui::AlbumType ComputeAlbumType(not_null item) { return Ui::AlbumType(); } -bool CanBeCompressed(Ui::AlbumType type) { +[[nodiscard]] bool CanBeCompressed(Ui::AlbumType type) { return (type == Ui::AlbumType::None) || (type == Ui::AlbumType::PhotoVideo); } +void ChooseReplacement( + not_null controller, + Ui::AlbumType type, + Fn chosen) { + const auto weak = base::make_weak(controller); + const auto callback = [=](FileDialog::OpenResult &&result) { + const auto strong = weak.get(); + if (!strong) { + return; + } + const auto showError = [=](tr::phrase<> t) { + if (const auto strong = weak.get()) { + strong->showToast({ t(tr::now) }); + } + }; + + const auto checkResult = [=](const Ui::PreparedList &list) { + if (list.files.size() != 1) { + return false; + } + const auto &file = list.files.front(); + const auto mime = file.information->filemime; + if (Core::IsMimeSticker(mime)) { + showError(tr::lng_edit_media_invalid_file); + return false; + } else if (type != Ui::AlbumType::None + && !file.canBeInAlbumType(type)) { + showError(tr::lng_edit_media_album_error); + return false; + } + return true; + }; + const auto premium = strong->session().premium(); + auto list = Storage::PreparedFileFromFilesDialog( + std::move(result), + checkResult, + showError, + st::sendMediaPreviewSize, + premium); + + if (list) { + chosen(std::move(*list)); + } + }; + + const auto filters = (type == Ui::AlbumType::PhotoVideo) + ? FileDialog::PhotoVideoFilesFilter() + : FileDialog::AllFilesFilter(); + FileDialog::GetOpenPath( + controller->content().get(), + tr::lng_choose_file(tr::now), + filters, + crl::guard(controller, callback)); +} + +void EditPhotoImage( + not_null controller, + std::shared_ptr media, + bool wasSpoiler, + Fn done) { + const auto large = media + ? media->image(Data::PhotoSize::Large) + : nullptr; + const auto parent = controller->content(); + const auto previewWidth = st::sendMediaPreviewSize; + auto callback = [=](const Editor::PhotoModifications &mods) { + if (!mods) { + return; + } + const auto large = media->image(Data::PhotoSize::Large); + if (!large) { + return; + } + auto copy = large->original(); + auto list = Storage::PrepareMediaFromImage( + std::move(copy), + QByteArray(), + previewWidth); + + using ImageInfo = Ui::PreparedFileInformation::Image; + auto &file = list.files.front(); + file.spoiler = wasSpoiler; + const auto image = std::get_if(&file.information->media); + + image->modifications = mods; + const auto sideLimit = PhotoSideLimit(); + Storage::UpdateImageDetails(file, previewWidth, sideLimit); + done(std::move(list)); + }; + const auto fileImage = std::make_shared(*large); + auto editor = base::make_unique_q( + parent, + &controller->window(), + fileImage, + Editor::PhotoModifications()); + const auto raw = editor.get(); + auto layer = std::make_unique( + parent, + std::move(editor)); + Editor::InitEditorLayer(layer.get(), raw, std::move(callback)); + controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther); +} + } // namespace EditCaptionBox::EditCaptionBox( QWidget*, not_null controller, not_null item) +: EditCaptionBox({}, controller, item, PrepareEditText(item), {}, {}) { +} + +EditCaptionBox::EditCaptionBox( + QWidget*, + not_null controller, + not_null item, + TextWithTags &&text, + Ui::PreparedList &&list, + Fn saved) : _controller(controller) , _historyItem(item) , _isAllowedEditMedia(item->media() @@ -135,7 +251,10 @@ EditCaptionBox::EditCaptionBox( tr::lng_photo_caption())) , _emojiToggle(base::make_unique_q( this, - st::boxAttachEmoji)) { + st::boxAttachEmoji)) +, _initialText(std::move(text)) +, _initialList(std::move(list)) +, _saved(std::move(saved)) { Expects(item->media() != nullptr); Expects(item->media()->allowsEditCaption()); @@ -148,6 +267,57 @@ EditCaptionBox::EditCaptionBox( EditCaptionBox::~EditCaptionBox() = default; +void EditCaptionBox::StartMediaReplace( + not_null controller, + FullMsgId itemId, + TextWithTags text, + Fn saved) { + const auto session = &controller->session(); + const auto item = session->data().message(itemId); + if (!item) { + return; + } + const auto show = [=](Ui::PreparedList &&list) mutable { + controller->show(Box( + controller, + item, + std::move(text), + std::move(list), + std::move(saved))); + }; + ChooseReplacement( + controller, + ComputeAlbumType(item), + crl::guard(controller, show)); +} + +void EditCaptionBox::StartPhotoEdit( + not_null controller, + std::shared_ptr media, + FullMsgId itemId, + TextWithTags text, + Fn saved) { + const auto session = &controller->session(); + const auto item = session->data().message(itemId); + if (!item) { + return; + } + const auto hasSpoiler = item->media() && item->media()->hasSpoiler(); + EditPhotoImage(controller, media, hasSpoiler, [=]( + Ui::PreparedList &&list) mutable { + const auto item = session->data().message(itemId); + if (!item) { + return; + } + controller->show(Box( + controller, + item, + std::move(text), + std::move(list), + std::move(saved))); + }); +} + void EditCaptionBox::prepare() { addButton(tr::lng_settings_save(), [=] { save(); }); addButton(tr::lng_cancel(), [=] { closeBox(); }); @@ -158,7 +328,9 @@ void EditCaptionBox::prepare() { setupEmojiPanel(); setInitialText(); - rebuildPreview(); + if (!setPreparedList(std::move(_initialList))) { + rebuildPreview(); + } setupEditEventHandler(); SetupShadowsToScrollContent(this, _scroll, _contentHeight.events()); @@ -290,16 +462,15 @@ void EditCaptionBox::setupField() { } void EditCaptionBox::setInitialText() { - const auto initial = PrepareEditText(_historyItem); _field->setTextWithTags( - initial, + _initialText, Ui::InputField::HistoryAction::Clear); auto cursor = _field->textCursor(); cursor.movePosition(QTextCursor::End); _field->setTextCursor(cursor); _checkChangedTimer.setCallback([=] { - if (_field->getTextWithAppliedMarkdown() == initial) { + if (_field->getTextWithAppliedMarkdown() == _initialText) { setCloseByOutsideClick(true); } }); @@ -353,132 +524,44 @@ void EditCaptionBox::setupControls() { } void EditCaptionBox::setupEditEventHandler() { - const auto toastParent = Ui::BoxShow(this).toastParent(); - const auto callback = [=](FileDialog::OpenResult &&result) { - auto showError = [toastParent](tr::phrase<> t) { - Ui::Toast::Show(toastParent, t(tr::now)); - }; - - const auto checkResult = [=](const Ui::PreparedList &list) { - if (list.files.size() != 1) { - return false; - } - const auto &file = list.files.front(); - const auto mime = file.information->filemime; - if (Core::IsMimeSticker(mime)) { - showError(tr::lng_edit_media_invalid_file); - return false; - } else if (_albumType != Ui::AlbumType::None - && !file.canBeInAlbumType(_albumType)) { - showError(tr::lng_edit_media_album_error); - return false; - } - return true; - }; - const auto premium = _controller->session().premium(); - auto list = Storage::PreparedFileFromFilesDialog( - std::move(result), - checkResult, - showError, - st::sendMediaPreviewSize, - premium); - - if (list) { - setPreparedList(std::move(*list)); - } - }; - - const auto buttonCallback = [=] { - const auto filters = (_albumType == Ui::AlbumType::PhotoVideo) - ? FileDialog::PhotoVideoFilesFilter() - : FileDialog::AllFilesFilter(); - FileDialog::GetOpenPath( - this, - tr::lng_choose_file(tr::now), - filters, - crl::guard(this, callback)); - }; - _editMediaClicks.events( - ) | rpl::start_with_next( - buttonCallback, - lifetime()); + ) | rpl::start_with_next([=] { + ChooseReplacement(_controller, _albumType, crl::guard(this, [=]( + Ui::PreparedList &&list) { + setPreparedList(std::move(list)); + })); + }, lifetime()); } void EditCaptionBox::setupPhotoEditorEventHandler() { const auto openedOnce = lifetime().make_state(false); _photoEditorOpens.events( ) | rpl::start_with_next([=, controller = _controller] { - const auto increment = [=] { - if (*openedOnce) { - return; - } + if (_preparedList.files.empty() + && (!_photoMedia + || !_photoMedia->image(Data::PhotoSize::Large))) { + return; + } else if (!*openedOnce) { *openedOnce = true; controller->session().settings().incrementPhotoEditorHintShown(); controller->session().saveSettings(); - }; - const auto clearError = [=] { + } + if (!_error.isEmpty()) { _error = QString(); update(); - }; - const auto previewWidth = st::sendMediaPreviewSize; + } if (!_preparedList.files.empty()) { - increment(); - clearError(); Editor::OpenWithPreparedFile( this, controller, &_preparedList.files.front(), - previewWidth, + st::sendMediaPreviewSize, [=] { rebuildPreview(); }); - } else if (_photoMedia) { - const auto large = _photoMedia->image(Data::PhotoSize::Large); - if (!large) { - return; - } - increment(); - clearError(); - auto callback = [=](const Editor::PhotoModifications &mods) { - if (!mods || !_photoMedia) { - return; - } - const auto large = _photoMedia->image(Data::PhotoSize::Large); - if (!large) { - return; - } - auto copy = large->original(); - const auto wasSpoiler = hasSpoiler(); - - _preparedList = Storage::PrepareMediaFromImage( - std::move(copy), - QByteArray(), - previewWidth); - - using ImageInfo = Ui::PreparedFileInformation::Image; - auto &file = _preparedList.files.front(); - file.spoiler = wasSpoiler; - const auto image = std::get_if( - &file.information->media); - - image->modifications = mods; - const auto sideLimit = PhotoSideLimit(); - Storage::UpdateImageDetails(file, previewWidth, sideLimit); - rebuildPreview(); - }; - const auto fileImage = std::make_shared(*large); - auto editor = base::make_unique_q( - this, - &controller->window(), - fileImage, - Editor::PhotoModifications()); - const auto raw = editor.get(); - auto layer = std::make_unique( - this, - std::move(editor)); - Editor::InitEditorLayer(layer.get(), raw, std::move(callback)); - controller->showLayer( - std::move(layer), - Ui::LayerOption::KeepOther); + } else { + EditPhotoImage(_controller, _photoMedia, hasSpoiler(), [=]( + Ui::PreparedList &&list) { + setPreparedList(std::move(list)); + }); } }, lifetime()); } @@ -780,13 +863,13 @@ void EditCaptionBox::save() { : SendMediaType::File, _field->getTextWithAppliedMarkdown(), action); - closeBox(); + closeAfterSave(); return; } const auto done = crl::guard(this, [=] { _saveRequestId = 0; - closeBox(); + closeAfterSave(); }); const auto fail = crl::guard(this, [=](const QString &error) { @@ -795,7 +878,7 @@ void EditCaptionBox::save() { _error = tr::lng_edit_error(tr::now); update(); } else if (error == u"MESSAGE_NOT_MODIFIED"_q) { - closeBox(); + closeAfterSave(); } else if (error == u"MESSAGE_EMPTY"_q) { _field->setFocus(); _field->showError(); @@ -816,6 +899,16 @@ void EditCaptionBox::save() { _saveRequestId = Api::EditCaption(item, sending, options, done, fail); } +void EditCaptionBox::closeAfterSave() { + const auto weak = MakeWeak(this); + if (_saved) { + _saved(); + } + if (weak) { + closeBox(); + } +} + void EditCaptionBox::keyPressEvent(QKeyEvent *e) { const auto ctrl = e->modifiers().testFlag(Qt::ControlModifier); if ((e->key() == Qt::Key_E) && ctrl) { diff --git a/Telegram/SourceFiles/boxes/edit_caption_box.h b/Telegram/SourceFiles/boxes/edit_caption_box.h index deb756b50..ff3924b3d 100644 --- a/Telegram/SourceFiles/boxes/edit_caption_box.h +++ b/Telegram/SourceFiles/boxes/edit_caption_box.h @@ -36,8 +36,27 @@ public: QWidget*, not_null controller, not_null item); + EditCaptionBox( + QWidget*, + not_null controller, + not_null item, + TextWithTags &&text, + Ui::PreparedList &&list, + Fn saved); ~EditCaptionBox(); + static void StartMediaReplace( + not_null controller, + FullMsgId itemId, + TextWithTags text, + Fn saved); + static void StartPhotoEdit( + not_null controller, + std::shared_ptr media, + FullMsgId itemId, + TextWithTags text, + Fn saved); + protected: void prepare() override; void setInnerFocus() override; @@ -66,6 +85,7 @@ private: bool validateLength(const QString &text) const; void applyChanges(); void save(); + void closeAfterSave(); bool fileFromClipboard(not_null data); @@ -89,6 +109,10 @@ private: base::unique_qptr _emojiPanel; base::unique_qptr _emojiFilter; + const TextWithTags _initialText; + Ui::PreparedList _initialList; + Fn _saved; + std::shared_ptr _photoMedia; Ui::PreparedList _preparedList; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 71e023d04..30bd8cb0e 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -61,6 +61,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_web_page.h" #include "data/data_document.h" #include "data/data_photo.h" +#include "data/data_photo_media.h" #include "data/data_media_types.h" #include "data/data_channel.h" #include "data/data_chat.h" @@ -2121,6 +2122,8 @@ void HistoryWidget::showHistory( _saveEditMsgRequestId = 0; _processingReplyItem = _replyEditMsg = nullptr; _processingReplyId = _editMsgId = _replyToId = 0; + _photoEditMedia = nullptr; + updateReplaceMediaButton(); _previewData = nullptr; _previewCache.clear(); _fieldBarCancel->hide(); @@ -2504,6 +2507,25 @@ void HistoryWidget::clearAllLoadRequests() { } } +bool HistoryWidget::updateReplaceMediaButton() { + if (!_canReplaceMedia) { + const auto result = (_replaceMedia != nullptr); + _replaceMedia.destroy(); + return result; + } else if (_replaceMedia) { + return false; + } + _replaceMedia.create(this, st::historyReplaceMedia); + _replaceMedia->setClickedCallback([=] { + EditCaptionBox::StartMediaReplace( + controller(), + { _history->peer->id, _editMsgId }, + _field->getTextWithTags(), + crl::guard(_list, [=] { cancelEdit(); })); + }); + return true; +} + void HistoryWidget::updateFieldSubmitSettings() { const auto settings = _isInlineBot ? Ui::InputField::SubmitSettings::None @@ -2731,6 +2753,9 @@ void HistoryWidget::updateControlsVisibility() { _kbScroll->hide(); _fieldBarCancel->hide(); _attachToggle->hide(); + if (_replaceMedia) { + _replaceMedia->hide(); + } _tabbedSelectorToggle->hide(); _botKeyboardShow->hide(); _botKeyboardHide->hide(); @@ -2795,7 +2820,12 @@ void HistoryWidget::updateControlsVisibility() { _botCommandStart->setVisible(_cmdStartShown); } } - _attachToggle->show(); + if (_replaceMedia) { + _replaceMedia->show(); + _attachToggle->hide(); + } else { + _attachToggle->show(); + } if (_botMenuButton) { _botMenuButton->show(); } @@ -3685,7 +3715,8 @@ void HistoryWidget::saveEditMsg() { TextUtilities::ConvertTextTagsToEntities(textWithTags.tags) }; TextUtilities::PrepareForSending(left, prepareFlags); - if (!TextUtilities::CutPart(sending, left, MaxMessageSize)) { + if (!TextUtilities::CutPart(sending, left, MaxMessageSize) + && (!item->media() || !item->media()->allowsEditCaption())) { const auto suggestModerateActions = false; controller()->show( Box(item, suggestModerateActions)); @@ -4239,9 +4270,34 @@ void HistoryWidget::mouseMoveEvent(QMouseEvent *e) { } void HistoryWidget::updateOverStates(QPoint pos) { - auto inReplyEditForward = QRect(st::historyReplySkip, _field->y() - st::historySendPadding - st::historyReplyHeight, width() - st::historyReplySkip - _fieldBarCancel->width(), st::historyReplyHeight).contains(pos) && (_editMsgId || replyToId() || readyToForward()); + const auto replyEditForwardInfoRect = QRect( + st::historyReplySkip, + _field->y() - st::historySendPadding - st::historyReplyHeight, + width() - st::historyReplySkip - _fieldBarCancel->width(), + st::historyReplyHeight); + auto inReplyEditForward = (_editMsgId || replyToId() || readyToForward()) + && replyEditForwardInfoRect.contains(pos); + auto inPhotoEdit = inReplyEditForward + && _photoEditMedia + && QRect( + replyEditForwardInfoRect.x(), + replyEditForwardInfoRect.y() + st::msgReplyPadding.top(), + st::msgReplyBarSize.height(), + st::msgReplyBarSize.height()).contains(pos); auto inClickable = inReplyEditForward; - _inReplyEditForward = inReplyEditForward; + if (_inPhotoEdit != inPhotoEdit) { + _inPhotoEdit = inPhotoEdit; + if (_photoEditMedia) { + _inPhotoEditOver.start( + [=] { updateField(); }, + _inPhotoEdit ? 0. : 1., + _inPhotoEdit ? 1. : 0., + st::defaultMessageBar.duration); + } else { + _inPhotoEditOver.stop(); + } + } + _inReplyEditForward = inReplyEditForward && !inPhotoEdit; if (inClickable != _inClickable) { _inClickable = inClickable; setCursor(_inClickable ? style::cur_pointer : style::cur_default); @@ -4862,7 +4918,7 @@ void HistoryWidget::moveFieldControls() { _kbScroll->setGeometryToLeft(0, bottom, width(), keyboardHeight); } -// (_botMenuButton) _attachToggle (_sendAs) ---- _inlineResults ------------------------------ _tabbedPanel ------ _fieldBarCancel +// (_botMenuButton) (_attachToggle|_replaceMedia) (_sendAs) ---- _inlineResults ------------------------------ _tabbedPanel ------ _fieldBarCancel // (_attachDocument|_attachPhoto) _field (_ttlInfo) (_scheduled) (_silent|_cmdStart|_kbShow) (_kbHide|_tabbedSelectorToggle) _send // (_botStart|_unblock|_joinChannel|_muteUnmute|_reportMessages) @@ -4872,6 +4928,9 @@ void HistoryWidget::moveFieldControls() { const auto skip = st::historyBotMenuSkip; _botMenuButton->moveToLeft(left + skip, buttonsBottom + skip); left += skip + _botMenuButton->width(); } + if (_replaceMedia) { + _replaceMedia->moveToLeft(left, buttonsBottom); + } _attachToggle->moveToLeft(left, buttonsBottom); left += _attachToggle->width(); if (_sendAs) { _sendAs->moveToLeft(left, buttonsBottom); left += _sendAs->width(); @@ -6038,6 +6097,13 @@ void HistoryWidget::mousePressEvent(QMouseEvent *e) { st::historyReplyHeight).contains(e->pos()); if (_replyForwardPressed && !_fieldBarCancel->isHidden()) { updateField(); + } else if (_inPhotoEdit && _photoEditMedia) { + EditCaptionBox::StartPhotoEdit( + controller(), + _photoEditMedia, + { _history->peer->id, _editMsgId }, + _field->getTextWithTags(), + crl::guard(_list, [=] { cancelEdit(); })); } else if (_inReplyEditForward) { if (readyToForward()) { _forwardPanel->editOptions(controller()); @@ -6957,12 +7023,7 @@ void HistoryWidget::editMessage(FullMsgId itemId) { } void HistoryWidget::editMessage(not_null item) { - if (const auto media = item->media()) { - if (media->allowsEditCaption()) { - controller()->show(Box(controller(), item)); - return; - } - } else if (_chooseTheme) { + if (_chooseTheme) { toggleChooseChatTheme(_peer); } else if (_voiceRecordBar->isActive()) { controller()->showToast({ tr::lng_edit_caption_voice(tr::now) }); @@ -7132,6 +7193,9 @@ void HistoryWidget::cancelEdit() { return; } + _canReplaceMedia = false; + _photoEditMedia = nullptr; + updateReplaceMediaButton(); _replyEditMsg = nullptr; setEditMsgId(0); _history->clearLocalEditDraft({}); @@ -7588,6 +7652,22 @@ void HistoryWidget::updateReplyEditTexts(bool force) { _editMsgId ? _editMsgId : _replyToId); } if (_replyEditMsg) { + const auto media = _replyEditMsg->media(); + _canReplaceMedia = media && media->allowsEditMedia(); + _photoEditMedia = (_canReplaceMedia + && media->photo() + && !media->photo()->isNull()) + ? media->photo()->createMediaView() + : nullptr; + if (_photoEditMedia) { + _photoEditMedia->wanted( + Data::PhotoSize::Large, + _replyEditMsg->fullId()); + } + if (updateReplaceMediaButton()) { + updateControlsVisibility(); + updateControlsGeometry(); + } updateReplyEditText(_replyEditMsg); updateBotKeyboard(); updateReplyToName(); @@ -7695,6 +7775,9 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { if (drawMsgText) { if (hasPreview) { if (preview) { + const auto overEdit = _photoEditMedia + ? _inPhotoEditOver.value(_inPhotoEdit ? 1. : 0.) + : 0.; auto to = QRect(replyLeft, backy + st::msgReplyPadding.top(), st::msgReplyBarSize.height(), st::msgReplyBarSize.height()); p.drawPixmap(to.x(), to.y(), preview->pixSingle( preview->size() / style::DevicePixelRatio(), @@ -7703,12 +7786,21 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { .outer = to.size(), })); if (_replySpoiler) { + if (overEdit > 0.) { + p.setOpacity(1. - overEdit); + } Ui::FillSpoilerRect( p, to, Ui::DefaultImageSpoiler().frame( _replySpoiler->index(now, pausedSpoiler))); } + if (overEdit > 0.) { + p.setOpacity(overEdit); + p.fillRect(to, st::historyEditMediaBg); + st::historyEditMedia.paintInCenter(p, to); + p.setOpacity(1.); + } } replyLeft += st::msgReplyBarSize.height() + st::msgReplyBarSkip - st::msgReplyBarSize.width() - st::msgReplyBarPos.x(); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index dd19eb3bd..778422cd8 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -31,6 +31,7 @@ class Error; namespace Data { enum class PreviewState : char; +class PhotoMedia; } // namespace Data namespace SendMenu { @@ -145,6 +146,7 @@ public: void firstLoadMessages(); void delayedShowAt(MsgId showAtMsgId); + bool updateReplaceMediaButton(); void updateFieldPlaceholder(); bool updateStickersByEmoji(); @@ -634,6 +636,8 @@ private: HistoryItem *_processingReplyItem = nullptr; MsgId _editMsgId = 0; + std::shared_ptr _photoEditMedia; + bool _canReplaceMedia = false; HistoryItem *_replyEditMsg = nullptr; Ui::Text::String _replyEditMsgText; @@ -732,6 +736,7 @@ private: object_ptr _botMenuButton = { nullptr }; QString _botMenuButtonText; object_ptr _attachToggle; + object_ptr _replaceMedia = { nullptr }; object_ptr _sendAs = { nullptr }; object_ptr _tabbedSelectorToggle; object_ptr _botKeyboardShow; @@ -746,7 +751,9 @@ private: bool _cmdStartShown = false; object_ptr _field; base::unique_qptr _fieldDisabled; + Ui::Animations::Simple _inPhotoEditOver; bool _inReplyEditForward = false; + bool _inPhotoEdit = false; bool _inClickable = false; bool _kbShown = false; diff --git a/Telegram/SourceFiles/ui/chat/chat.style b/Telegram/SourceFiles/ui/chat/chat.style index a60ee8383..a06f0f1eb 100644 --- a/Telegram/SourceFiles/ui/chat/chat.style +++ b/Telegram/SourceFiles/ui/chat/chat.style @@ -482,6 +482,15 @@ historyMessagesTTL: IconButtonWithText { font: font(10px semibold); } +historyReplaceMedia: IconButton(historyAttach) { + icon: icon {{ "chat/input_replace", windowBgActive }}; + iconOver: icon {{ "chat/input_replace", windowBgActive }}; + ripple: RippleAnimation(defaultRippleAnimation) { + color: lightButtonBgOver; + } +} +historyEditMediaBg: videoPlayIconBg; +historyEditMedia: icon{{ "chat/input_draw", videoPlayIconFg }}; historyMessagesTTLPickerHeight: 200px; historyMessagesTTLPickerItemHeight: 40px; historyMessagesTTLLabel: FlatLabel(defaultFlatLabel) {