From f690f93f32cc079800178c646190da772edbb863 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 16 Aug 2019 20:07:30 +0300 Subject: [PATCH] Allow schedule of files, stickers, GIFs. --- Telegram/SourceFiles/boxes/generic_box.h | 2 +- Telegram/SourceFiles/facades.h | 2 +- .../view/history_view_compose_controls.cpp | 33 +- .../view/history_view_compose_controls.h | 9 + .../view/history_view_schedule_box.cpp | 8 +- .../history/view/history_view_schedule_box.h | 2 +- .../view/history_view_scheduled_section.cpp | 391 +++++++++++++++++- .../view/history_view_scheduled_section.h | 48 +++ 8 files changed, 477 insertions(+), 18 deletions(-) diff --git a/Telegram/SourceFiles/boxes/generic_box.h b/Telegram/SourceFiles/boxes/generic_box.h index f2985c8ec..8c02ccca6 100644 --- a/Telegram/SourceFiles/boxes/generic_box.h +++ b/Telegram/SourceFiles/boxes/generic_box.h @@ -129,7 +129,7 @@ template inline void GenericBox::Initer::call( not_null box, std::index_sequence) { - std::invoke(method, box, std::get(args)...); + std::invoke(method, box, std::get(std::move(args))...); } template diff --git a/Telegram/SourceFiles/facades.h b/Telegram/SourceFiles/facades.h index 73984bf93..fd44c0866 100644 --- a/Telegram/SourceFiles/facades.h +++ b/Telegram/SourceFiles/facades.h @@ -50,7 +50,7 @@ inline void CallDelayed(int duration, Guard &&object, Lambda &&lambda) { } template -inline auto LambdaDelayed(int duration, Guard &&object, Lambda &&lambda) { +[[nodiscard]] inline auto LambdaDelayed(int duration, Guard &&object, Lambda &&lambda) { auto guarded = crl::guard( std::forward(object), std::forward(lambda)); diff --git a/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp index b2e7f69ba..0f67629e1 100644 --- a/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/history_view_compose_controls.cpp @@ -83,6 +83,26 @@ rpl::producer<> ComposeControls::sendRequests() const { return _send->clicks() | rpl::map([] { return rpl::empty_value(); }); } +rpl::producer<> ComposeControls::attachRequests() const { + return _attachToggle->clicks( + ) | rpl::map([] { + return rpl::empty_value(); + }); +} + +rpl::producer> ComposeControls::fileChosen() const { + return _fileChosen.events(); +} + +rpl::producer> ComposeControls::photoChosen() const { + return _photoChosen.events(); +} + +auto ComposeControls::inlineResultChosen() const +->rpl::producer { + return _inlineResultChosen.events(); +} + void ComposeControls::showStarted() { if (_inlineResults) { _inlineResults->hideFast(); @@ -202,20 +222,13 @@ void ComposeControls::initTabbedSelector() { }, wrap->lifetime()); selector->fileChosen( - ) | rpl::start_with_next([=](not_null document) { - //sendExistingDocument(document); - }, wrap->lifetime()); + ) | rpl::start_to_stream(_fileChosen, wrap->lifetime()); selector->photoChosen( - ) | rpl::start_with_next([=](not_null photo) { - //sendExistingPhoto(photo); - }, wrap->lifetime()); + ) | rpl::start_to_stream(_photoChosen, wrap->lifetime()); selector->inlineResultChosen( - ) | rpl::start_with_next([=]( - ChatHelpers::TabbedSelector::InlineChosen data) { - //sendInlineResult(data.result, data.bot); - }, wrap->lifetime()); + ) | rpl::start_to_stream(_inlineResultChosen, wrap->lifetime()); } void ComposeControls::initSendButton() { diff --git a/Telegram/SourceFiles/history/view/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/history_view_compose_controls.h index e9aeb844c..e54132114 100644 --- a/Telegram/SourceFiles/history/view/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/history_view_compose_controls.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unique_qptr.h" #include "ui/rp_widget.h" #include "ui/effects/animations.h" +#include "chat_helpers/tabbed_selector.h" namespace ChatHelpers { class TabbedPanel; @@ -65,6 +66,11 @@ public: void focus(); [[nodiscard]] rpl::producer<> cancelRequests() const; [[nodiscard]] rpl::producer<> sendRequests() const; + [[nodiscard]] rpl::producer<> attachRequests() const; + [[nodiscard]] rpl::producer> fileChosen() const; + [[nodiscard]] rpl::producer> photoChosen() const; + [[nodiscard]] auto inlineResultChosen() const + -> rpl::producer; void pushTabbedSelectorToThirdSection(const Window::SectionShow ¶ms); bool returnTabbedSelector(); @@ -108,6 +114,9 @@ private: std::unique_ptr _tabbedPanel; rpl::event_stream<> _cancelRequests; + rpl::event_stream> _fileChosen; + rpl::event_stream> _photoChosen; + rpl::event_stream _inlineResultChosen; bool _recording = false; bool _inField = false; diff --git a/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp b/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp index 8843c8e26..879d3bec2 100644 --- a/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp +++ b/Telegram/SourceFiles/history/view/history_view_schedule_box.cpp @@ -512,7 +512,7 @@ TimeId DefaultScheduleTime() { void ScheduleBox( not_null box, - Fn done, + FnMut done, TimeId time) { box->setTitle(tr::lng_schedule_title()); box->setWidth(st::boxWideWidth); @@ -582,6 +582,8 @@ void ScheduleBox( }), (*calendar)->lifetime()); }); + const auto shared = std::make_shared>( + std::move(done)); const auto save = [=] { auto result = Api::SendOptions(); @@ -602,9 +604,9 @@ void ScheduleBox( return; } - auto copy = done; + auto copy = shared; box->closeBox(); - copy(result); + (*copy)(result); }; box->setFocusCallback([=] { timeInput->setFocusFast(); }); diff --git a/Telegram/SourceFiles/history/view/history_view_schedule_box.h b/Telegram/SourceFiles/history/view/history_view_schedule_box.h index 001d8e96e..cde32b0ce 100644 --- a/Telegram/SourceFiles/history/view/history_view_schedule_box.h +++ b/Telegram/SourceFiles/history/view/history_view_schedule_box.h @@ -18,7 +18,7 @@ namespace HistoryView { [[nodiscard]] TimeId DefaultScheduleTime(); void ScheduleBox( not_null box, - Fn done, + FnMut done, TimeId time); } // namespace HistoryView diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index 0987bdd22..29e376905 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -15,22 +15,42 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_item.h" #include "ui/widgets/scroll_area.h" #include "ui/widgets/shadow.h" +#include "ui/toast/toast.h" #include "ui/special_buttons.h" #include "api/api_common.h" +#include "api/api_sending.h" #include "apiwrap.h" #include "boxes/confirm_box.h" +#include "boxes/send_files_box.h" +#include "boxes/generic_box.h" #include "window/window_session_controller.h" #include "window/window_peer_menu.h" #include "core/event_filter.h" +#include "core/file_utilities.h" #include "main/main_session.h" #include "data/data_session.h" #include "data/data_scheduled_messages.h" +#include "storage/storage_media_prepare.h" +#include "storage/localstorage.h" +#include "inline_bots/inline_bot_result.h" #include "lang/lang_keys.h" #include "styles/style_history.h" #include "styles/style_window.h" #include "styles/style_info.h" +#include "styles/style_boxes.h" namespace HistoryView { +namespace { + +void ShowErrorToast(const QString &text) { + auto config = Ui::Toast::Config(); + config.multiline = true; + config.minWidth = st::msgMinWidth; + config.text = text; + Ui::Toast::Show(config); +} + +} // namespace object_ptr ScheduledMemento::createWidget( QWidget *parent, @@ -111,6 +131,256 @@ void ScheduledWidget::setupComposeControls() { ) | rpl::start_with_next([=] { send(); }, lifetime()); + + _composeControls->attachRequests( + ) | rpl::filter([=] { + return !_choosingAttach; + }) | rpl::start_with_next([=] { + _choosingAttach = true; + App::CallDelayed( + st::historyAttach.ripple.hideDuration, + this, + [=] { _choosingAttach = false; chooseAttach(); }); + }, lifetime()); + + _composeControls->fileChosen( + ) | rpl::start_with_next([=](not_null document) { + sendExistingDocument(document); + }, lifetime()); + + _composeControls->photoChosen( + ) | rpl::start_with_next([=](not_null photo) { + sendExistingPhoto(photo); + }, lifetime()); + + _composeControls->inlineResultChosen( + ) | rpl::start_with_next([=]( + ChatHelpers::TabbedSelector::InlineChosen chosen) { + sendInlineResult(chosen.result, chosen.bot); + }, lifetime()); +} + +void ScheduledWidget::chooseAttach() { + if (const auto error = Data::RestrictionError( + _history->peer, + ChatRestriction::f_send_media)) { + Ui::Toast::Show(*error); + return; + } + + const auto filter = FileDialog::AllFilesFilter() + + qsl(";;Image files (*") + + cImgExtensions().join(qsl(" *")) + + qsl(")"); + + FileDialog::GetOpenPaths(this, tr::lng_choose_files(tr::now), filter, crl::guard(this, [=]( + FileDialog::OpenResult &&result) { + if (result.paths.isEmpty() && result.remoteContent.isEmpty()) { + return; + } + + if (!result.remoteContent.isEmpty()) { + auto animated = false; + auto image = App::readImage( + result.remoteContent, + nullptr, + false, + &animated); + if (!image.isNull() && !animated) { + confirmSendingFiles( + std::move(image), + std::move(result.remoteContent), + CompressConfirm::Auto); + } else { + uploadFile(result.remoteContent, SendMediaType::File); + } + } else { + auto list = Storage::PrepareMediaList( + result.paths, + st::sendMediaPreviewSize); + if (list.allFilesForCompress || list.albumIsPossible) { + confirmSendingFiles(std::move(list), CompressConfirm::Auto); + } else if (!showSendingFilesError(list)) { + confirmSendingFiles(std::move(list), CompressConfirm::No); + } + } + }), nullptr); +} + +bool ScheduledWidget::confirmSendingFiles( + Storage::PreparedList &&list, + CompressConfirm compressed, + const QString &insertTextOnCancel) { + if (showSendingFilesError(list)) { + return false; + } + + const auto noCompressOption = (list.files.size() > 1) + && !list.allFilesForCompress + && !list.albumIsPossible; + const auto boxCompressConfirm = noCompressOption + ? CompressConfirm::None + : compressed; + + //const auto cursor = _field->textCursor(); + //const auto position = cursor.position(); + //const auto anchor = cursor.anchor(); + const auto text = _composeControls->getTextWithAppliedMarkdown();//_field->getTextWithTags(); + using SendLimit = SendFilesBox::SendLimit; + auto box = Box( + controller(), + std::move(list), + text, + boxCompressConfirm, + _history->peer->slowmodeApplied() ? SendLimit::One : SendLimit::Many); + //_field->setTextWithTags({}); + + box->setConfirmedCallback(crl::guard(this, [=]( + Storage::PreparedList &&list, + SendFilesWay way, + TextWithTags &&caption, + Api::SendOptions options, + bool ctrlShiftEnter) { + if (showSendingFilesError(list)) { + return; + } + const auto type = (way == SendFilesWay::Files) + ? SendMediaType::File + : SendMediaType::Photo; + const auto album = (way == SendFilesWay::Album) + ? std::make_shared() + : nullptr; + uploadFilesAfterConfirmation( + std::move(list), + type, + std::move(caption), + MsgId(0),//replyToId(), + options, + album); + })); + //box->setCancelledCallback(crl::guard(this, [=] { + // _field->setTextWithTags(text); + // auto cursor = _field->textCursor(); + // cursor.setPosition(anchor); + // if (position != anchor) { + // cursor.setPosition(position, QTextCursor::KeepAnchor); + // } + // _field->setTextCursor(cursor); + // if (!insertTextOnCancel.isEmpty()) { + // _field->textCursor().insertText(insertTextOnCancel); + // } + //})); + + //ActivateWindow(controller()); + const auto shown = Ui::show(std::move(box)); + shown->setCloseByOutsideClick(false); + + return true; +} + +bool ScheduledWidget::confirmSendingFiles( + QImage &&image, + QByteArray &&content, + CompressConfirm compressed, + const QString &insertTextOnCancel) { + if (image.isNull()) { + return false; + } + + auto list = Storage::PrepareMediaFromImage( + std::move(image), + std::move(content), + st::sendMediaPreviewSize); + return confirmSendingFiles( + std::move(list), + compressed, + insertTextOnCancel); +} + +void ScheduledWidget::uploadFilesAfterConfirmation( + Storage::PreparedList &&list, + SendMediaType type, + TextWithTags &&caption, + MsgId replyTo, + Api::SendOptions options, + std::shared_ptr album) { + const auto isAlbum = (album != nullptr); + const auto compressImages = (type == SendMediaType::Photo); + if (_history->peer->slowmodeApplied() + && ((list.files.size() > 1 && !album) + || (!list.files.empty() + && !caption.text.isEmpty() + && !list.canAddCaption(isAlbum, compressImages)))) { + ShowErrorToast(tr::lng_slowmode_no_many(tr::now)); + return; + } + auto callback = crl::guard(this, [ + =, + list = std::move(list), + caption = std::move(caption), + // Strange thing, otherwise std::is_copy_constructible is true. O_o + msvc_bug_workaround = std::make_unique() + ](Api::SendOptions options) mutable { + auto action = Api::SendAction(_history); + action.replyTo = replyTo; + action.options = options; + session().api().sendFiles( + std::move(list), + type, + std::move(caption), + album, + action); + }); + Ui::show( + Box(ScheduleBox, std::move(callback), DefaultScheduleTime()), + LayerOption::KeepOther); +} + +void ScheduledWidget::uploadFile( + const QByteArray &fileContent, + SendMediaType type) { + const auto callback = crl::guard(this, [=](Api::SendOptions options) { + auto action = Api::SendAction(_history); + //action.replyTo = replyToId(); + action.options = options; + session().api().sendFile(fileContent, type, action); + }); + Ui::show( + Box(ScheduleBox, callback, DefaultScheduleTime()), + LayerOption::KeepOther); +} + +bool ScheduledWidget::showSendingFilesError( + const Storage::PreparedList &list) const { + const auto text = [&] { + const auto error = Data::RestrictionError( + _history->peer, + ChatRestriction::f_send_media); + if (error) { + return *error; + } + using Error = Storage::PreparedList::Error; + switch (list.error) { + case Error::None: return QString(); + case Error::EmptyFile: + case Error::Directory: + case Error::NonLocalUrl: return tr::lng_send_image_empty( + tr::now, + lt_name, + list.errorData); + case Error::TooLargeFile: return tr::lng_send_image_too_large( + tr::now, + lt_name, + list.errorData); + } + return tr::lng_forward_send_files_cant(tr::now); + }(); + if (text.isEmpty()) { + return false; + } + + ShowErrorToast(text); + return true; } void ScheduledWidget::send() { @@ -160,6 +430,123 @@ void ScheduledWidget::send(Api::SendOptions options) { _composeControls->focus(); } +void ScheduledWidget::sendExistingDocument( + not_null document) { + const auto callback = crl::guard(this, [=](Api::SendOptions options) { + sendExistingDocument(document, options); + }); + Ui::show( + Box(ScheduleBox, callback, DefaultScheduleTime()), + LayerOption::KeepOther); +} + +bool ScheduledWidget::sendExistingDocument( + not_null document, + Api::SendOptions options) { + const auto error = Data::RestrictionError( + _history->peer, + ChatRestriction::f_send_stickers); + if (error) { + Ui::show(Box(*error), LayerOption::KeepOther); + return false; + } + + auto message = Api::MessageToSend(_history); + //message.action.replyTo = replyToId(); + message.action.options = options; + Api::SendExistingDocument(std::move(message), document); + + //if (_fieldAutocomplete->stickersShown()) { + // clearFieldText(); + // //_saveDraftText = true; + // //_saveDraftStart = crl::now(); + // //onDraftSave(); + // onCloudDraftSave(); // won't be needed if SendInlineBotResult will clear the cloud draft + //} + + _composeControls->hidePanelsAnimated(); + _composeControls->focus(); + return true; +} + +void ScheduledWidget::sendExistingPhoto(not_null photo) { + const auto callback = crl::guard(this, [=](Api::SendOptions options) { + sendExistingPhoto(photo, options); + }); + Ui::show( + Box(ScheduleBox, callback, DefaultScheduleTime()), + LayerOption::KeepOther); +} + +bool ScheduledWidget::sendExistingPhoto( + not_null photo, + Api::SendOptions options) { + const auto error = Data::RestrictionError( + _history->peer, + ChatRestriction::f_send_media); + if (error) { + Ui::show(Box(*error), LayerOption::KeepOther); + return false; + } + + auto message = Api::MessageToSend(_history); + //message.action.replyTo = replyToId(); + message.action.options = options; + Api::SendExistingPhoto(std::move(message), photo); + + _composeControls->hidePanelsAnimated(); + _composeControls->focus(); + return true; +} + +void ScheduledWidget::sendInlineResult( + not_null result, + not_null bot) { + const auto errorText = result->getErrorOnSend(_history); + if (!errorText.isEmpty()) { + Ui::show(Box(errorText)); + return; + } + const auto callback = crl::guard(this, [=](Api::SendOptions options) { + sendInlineResult(result, bot, options); + }); + Ui::show( + Box(ScheduleBox, callback, DefaultScheduleTime()), + LayerOption::KeepOther); +} + +void ScheduledWidget::sendInlineResult( + not_null result, + not_null bot, + Api::SendOptions options) { + auto action = Api::SendAction(_history); + action.clearDraft = true; + //action.replyTo = replyToId(); + action.options = options; + action.generateLocal = true; + session().api().sendInlineResult(bot, result, action); + + _composeControls->clear(); + //_saveDraftText = true; + //_saveDraftStart = crl::now(); + //onDraftSave(); + + auto &bots = cRefRecentInlineBots(); + const auto index = bots.indexOf(bot); + if (index) { + if (index > 0) { + bots.removeAt(index); + } else if (bots.size() >= RecentInlineBotsLimit) { + bots.resize(RecentInlineBotsLimit - 1); + } + bots.push_front(bot); + Local::writeRecentHashtagsAndBots(); + } + + _composeControls->hidePanelsAnimated(); + _composeControls->focus(); +} + void ScheduledWidget::setupScrollDownButton() { _scrollDown->setClickedCallback([=] { scrollDownClicked(); @@ -470,14 +857,14 @@ void ScheduledWidget::highlightSingleNewMessage( return; } auto firstDifferent = 0; - for (; firstDifferent != _lastSlice.ids.size(); ++firstDifferent) { + while (firstDifferent != _lastSlice.ids.size()) { if (slice.ids[firstDifferent] != _lastSlice.ids[firstDifferent]) { break; } ++firstDifferent; } auto lastDifferent = slice.ids.size() - 1; - for (; lastDifferent != firstDifferent;) { + while (lastDifferent != firstDifferent) { if (slice.ids[lastDifferent] != _lastSlice.ids[lastDifferent - 1]) { break; } diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h index d649d173c..8a15d0828 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.h +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.h @@ -13,6 +13,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_messages.h" class History; +enum class CompressConfirm; +enum class SendMediaType; +struct SendingAlbum; namespace Api { struct SendOptions; @@ -22,6 +25,10 @@ namespace Notify { struct PeerUpdate; } // namespace Notify +namespace Storage { +struct PreparedList; +} // namespace Storage + namespace Ui { class ScrollArea; class PlainShadow; @@ -33,6 +40,10 @@ namespace Profile { class BackButton; } // namespace Profile +namespace InlineBots { +class Result; +} // namespace InlineBots + namespace HistoryView { class Element; @@ -133,6 +144,42 @@ private: void send(); void send(Api::SendOptions options); void highlightSingleNewMessage(const Data::MessagesSlice &slice); + void chooseAttach(); + + void uploadFile(const QByteArray &fileContent, SendMediaType type); + bool confirmSendingFiles( + QImage &&image, + QByteArray &&content, + CompressConfirm compressed, + const QString &insertTextOnCancel = QString()); + bool confirmSendingFiles( + Storage::PreparedList &&list, + CompressConfirm compressed, + const QString &insertTextOnCancel = QString()); + bool showSendingFilesError(const Storage::PreparedList &list) const; + void uploadFilesAfterConfirmation( + Storage::PreparedList &&list, + SendMediaType type, + TextWithTags &&caption, + MsgId replyTo, + Api::SendOptions options, + std::shared_ptr album); + + void sendExistingDocument(not_null document); + bool sendExistingDocument( + not_null document, + Api::SendOptions options); + void sendExistingPhoto(not_null photo); + bool sendExistingPhoto( + not_null photo, + Api::SendOptions options); + void sendInlineResult( + not_null result, + not_null bot); + void sendInlineResult( + not_null result, + not_null bot, + Api::SendOptions options); const not_null _history; object_ptr _scroll; @@ -151,6 +198,7 @@ private: object_ptr _scrollDown; Data::MessagesSlice _lastSlice; + bool _choosingAttach = false; };