diff --git a/Telegram/Resources/icons/chat/text_to_file.svg b/Telegram/Resources/icons/chat/text_to_file.svg new file mode 100644 index 0000000000..d3d7ce7039 --- /dev/null +++ b/Telegram/Resources/icons/chat/text_to_file.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 67a859e482..eb99def845 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -7841,6 +7841,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_ai_compose_apply_style" = "Apply Style"; "lng_ai_compose_style_tooltip" = "Choose Style"; +"lng_send_as_file_tooltip" = "Send text as a file."; + // Wnd specific "lng_wnd_choose_program_menu" = "Choose Default Program..."; diff --git a/Telegram/SourceFiles/chat_helpers/chat_helpers.style b/Telegram/SourceFiles/chat_helpers/chat_helpers.style index 798e6f66f5..4bc15a36ab 100644 --- a/Telegram/SourceFiles/chat_helpers/chat_helpers.style +++ b/Telegram/SourceFiles/chat_helpers/chat_helpers.style @@ -1443,6 +1443,10 @@ historyAiComposeButtonLetters: icon{{ "chat/ai_letters-20x20", historyComposeIco historyAiComposeButtonStar1: icon{{ "chat/ai_star1-20x20", historyComposeIconFg }}; historyAiComposeButtonStar2: icon{{ "chat/ai_star2-20x20", historyComposeIconFg }}; historyAiComposeButtonPosition: point(-8px, -4px); +historySendAsFileButton: IconButton(historyAiComposeButton) { + icon: icon {{ "chat/text_to_file-18x18", historyComposeIconFg }}; + iconOver: icon {{ "chat/text_to_file-18x18", historyComposeIconFgOver }}; +} historyAiComposeTooltipSkip: 8px; importantTooltipHide: IconButton(defaultIconButton) { width: 34px; diff --git a/Telegram/SourceFiles/data/data_document_media.cpp b/Telegram/SourceFiles/data/data_document_media.cpp index 38e34ac5c1..478b2fc79d 100644 --- a/Telegram/SourceFiles/data/data_document_media.cpp +++ b/Telegram/SourceFiles/data/data_document_media.cpp @@ -285,7 +285,10 @@ void DocumentMedia::checkStickerLarge() { void DocumentMedia::automaticLoad( Data::FileOrigin origin, const HistoryItem *item) { - if (_owner->status != FileReady || loaded() || _owner->cancelled()) { + if (_owner->status != FileReady + || loaded() + || _owner->uploading() + || _owner->cancelled()) { return; } else if (!item && !_owner->sticker() && !_owner->isAnimation()) { return; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 4d15429833..30f12037d0 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -273,6 +273,9 @@ HistoryWidget::HistoryWidget( , _aiButton(Ui::CreateChild( this, st::historyAiComposeButton)) +, _sendAsFile(Ui::CreateChild( + this, + st::historySendAsFileButton)) , _unblock( this, tr::lng_unblock_button(tr::now).toUpper(), @@ -497,6 +500,7 @@ HistoryWidget::HistoryWidget( setupFastButtonMode(); initAiButton(); + initSendAsFileButton(); _fieldCharsCountManager.limitExceeds( ) | rpl::on_next([=] { @@ -534,6 +538,20 @@ HistoryWidget::HistoryWidget( _field->setMimeDataHook(WrappedMessageFieldMimeHook([=]( not_null data, Ui::InputField::MimeAction action) { + const auto pasteResult = Ui::CheckLargeTextPaste(_field, data); + if (pasteResult.exceeds) { + if (action == Ui::InputField::MimeAction::Check) { + return true; + } + const auto text = _field->getTextWithTags(); + const auto cursor = _field->textCursor(); + sendTextAsFile( + pasteResult.resultingText, + text, + cursor.position(), + cursor.anchor()); + return true; + } if (action == Ui::InputField::MimeAction::Check) { return canSendFiles(data); } else if (action == Ui::InputField::MimeAction::Insert) { @@ -1295,6 +1313,7 @@ void HistoryWidget::initVoiceRecordBar() { _field->setDisabled(active); controller()->widget()->setInnerFocus(); updateAiButtonVisibility(); + updateSendAsFileVisibility(); }, lifetime()); _voiceRecordBar->hideFast(); @@ -1314,9 +1333,65 @@ void HistoryWidget::initAiButton() { _aiTooltipManager = std::make_unique( this, _aiButton, + tr::lng_ai_compose_tooltip(tr::rich), + "ai_compose_tooltip_hidden"_cs, [=] { return width(); }); } +void HistoryWidget::initSendAsFileButton() { + _sendAsFile->hide(); + _sendAsFile->setClickedCallback([=] { + if (_sendAsFileTooltipManager) { + _sendAsFileTooltipManager->hideAndRemember(); + } + const auto cursor = _field->textCursor(); + const auto text = _field->getTextWithTags(); + sendTextAsFile(text.text, text, cursor.position(), cursor.anchor()); + }); + + _sendAsFileTooltipManager = std::make_unique( + this, + _sendAsFile, + tr::lng_send_as_file_tooltip(tr::rich), + "send_as_file_tooltip_hidden"_cs, + [=] { return width(); }); +} + +void HistoryWidget::sendTextAsFile( + const QString &fileText, + TextWithTags restoreText, + int restorePosition, + int restoreAnchor) { + auto result = Ui::PrepareTextAsFile(fileText); + + _field->setTextWithTags({}); + + auto box = Box( + controller(), + std::move(result), + TextWithTags{}, + _peer, + Api::SendType::Normal, + sendMenuDetails()); + box->setConfirmedCallback(crl::guard(this, [=]( + std::shared_ptr bundle, + Api::SendOptions options) { + sendingFilesConfirmed(std::move(bundle), options); + })); + box->setCancelledCallback(crl::guard(this, [=] { + _field->setTextWithTags(restoreText); + auto cursor = _field->textCursor(); + cursor.setPosition(restoreAnchor); + if (restorePosition != restoreAnchor) { + cursor.setPosition(restorePosition, QTextCursor::KeepAnchor); + } + _field->setTextCursor(cursor); + })); + + Window::ActivateWindow(controller()); + controller()->show(std::move(box)); +} + void HistoryWidget::initTabbedSelector() { refreshTabbedPanel(); @@ -1830,6 +1905,7 @@ void HistoryWidget::orderWidgets() { _voiceRecordBar->raise(); _send->raise(); _aiButton->raise(); + _sendAsFile->raise(); _topBars->raise(); if (_businessBotStatus) { _businessBotStatus->bar().raise(); @@ -1883,6 +1959,9 @@ void HistoryWidget::orderWidgets() { if (_aiTooltipManager) { _aiTooltipManager->raise(); } + if (_sendAsFileTooltipManager) { + _sendAsFileTooltipManager->raise(); + } _attachDragAreas.document->raise(); _attachDragAreas.photo->raise(); } @@ -1957,6 +2036,7 @@ void HistoryWidget::fieldChanged() { updateControlsGeometry(); } updateAiButtonVisibility(); + updateSendAsFileVisibility(); _saveCloudDraftTimer.cancel(); if (!_peer || !(_textUpdateEvents & TextUpdateEvent::SaveDraft)) { @@ -3748,6 +3828,7 @@ void HistoryWidget::updateControlsVisibility() { } //checkTabbedSelectorToggleTooltip(); updateAiButtonVisibility(); + updateSendAsFileVisibility(); updateMouseTracking(); } @@ -6115,6 +6196,7 @@ void HistoryWidget::toggleKeyboard(bool manual) { } updateControlsGeometry(); updateAiButtonVisibility(); + updateSendAsFileVisibility(); updateFieldPlaceholder(); if (_botKeyboardHide->isHidden() && canWriteMessage() @@ -6264,6 +6346,12 @@ bool HistoryWidget::hasEnoughLinesForAi() const { && Ui::HasEnoughLinesForAi(&session(), _field); } +bool HistoryWidget::textExceedsMaxSize() const { + return _history + && !_voiceRecordBar->isActive() + && _field->getLastText().size() > MaxMessageSize; +} + void HistoryWidget::updateAiButtonVisibility() { const auto hidden = !hasEnoughLinesForAi() || !_send->isVisible() @@ -6292,6 +6380,34 @@ void HistoryWidget::updateAiButtonGeometry() { } } +void HistoryWidget::updateSendAsFileVisibility() { + const auto hidden = !textExceedsMaxSize() + || _send->isHidden() + || _field->isHidden() + || editingMessage(); + if (_sendAsFile->isHidden() == hidden) { + return; + } + _sendAsFile->setVisible(!hidden); + if (!hidden) { + updateSendAsFileGeometry(); + } + if (_sendAsFileTooltipManager) { + _sendAsFileTooltipManager->updateVisibility(!hidden); + } +} + +void HistoryWidget::updateSendAsFileGeometry() { + if (_sendAsFile->isHidden()) { + return; + } + const auto x = _send->x() + _send->width() - _sendAsFile->width(); + _sendAsFile->move(QPoint(x, _field->y()) + st::historyAiComposeButtonPosition); + if (_sendAsFileTooltipManager) { + _sendAsFileTooltipManager->updateGeometry(); + } +} + void HistoryWidget::moveFieldControls() { auto keyboardHeight = 0; auto bottom = height(); @@ -6362,6 +6478,7 @@ void HistoryWidget::moveFieldControls() { _ttlInfo->move(width() - right - _ttlInfo->width(), buttonsBottom); } updateAiButtonGeometry(); + updateSendAsFileGeometry(); _fieldBarCancel->moveToRight( 0, @@ -6466,6 +6583,7 @@ void HistoryWidget::inlineBotChanged() { void HistoryWidget::fieldResized() { moveFieldControls(); updateAiButtonVisibility(); + updateSendAsFileVisibility(); updateHistoryGeometry(); updateField(); } diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index d002f39a9d..ae56e2bbae 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -128,7 +128,8 @@ class WebpageProcessor; class CharactersLimitLabel; class PhotoEditSpoilerManager; class ComposeAiButton; -class AiTooltipManager; +class ComposeTooltipManager; +using AiTooltipManager = ComposeTooltipManager; struct VoiceToSend; } // namespace HistoryView::Controls @@ -529,12 +530,21 @@ private: void updateAiButtonVisibility(); void updateAiButtonGeometry(); void showAiComposeBox(); + void initSendAsFileButton(); + void sendTextAsFile( + const QString &fileText, + TextWithTags restoreText, + int restorePosition, + int restoreAnchor); + void updateSendAsFileVisibility(); + void updateSendAsFileGeometry(); [[nodiscard]] bool canSendAiComposeDirect() const; [[nodiscard]] MsgId resolveReplyToTopicRootId(); [[nodiscard]] Data::ForumTopic *resolveReplyToTopic(); [[nodiscard]] bool canWriteMessage() const; [[nodiscard]] bool hasEnoughLinesForAi() const; + [[nodiscard]] bool textExceedsMaxSize() const; void orderWidgets(); [[nodiscard]] InlineBotQuery parseInlineBotQuery() const; @@ -835,6 +845,7 @@ private: const std::shared_ptr _send; HistoryView::Controls::ComposeAiButton * const _aiButton = nullptr; + Ui::IconButton * const _sendAsFile = nullptr; object_ptr _unblock; object_ptr _botStart; object_ptr _joinChannel; @@ -867,6 +878,7 @@ private: rpl::lifetime _subsectionTabsLifetime; rpl::lifetime _subsectionCheckLifetime; std::unique_ptr _aiTooltipManager; + std::unique_ptr _sendAsFileTooltipManager; std::shared_ptr _fieldChatStyle; bool _cmdStartShown = false; object_ptr _field; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_ai_tooltip.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_ai_tooltip.cpp index e10d64eb39..d26bf9df18 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_ai_tooltip.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_ai_tooltip.cpp @@ -9,30 +9,27 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "core/core_settings.h" -#include "history/view/controls/history_view_compose_ai_button.h" #include "lang/lang_keys.h" #include "ui/widgets/tooltip.h" #include "styles/style_chat_helpers.h" #include "styles/style_widgets.h" namespace HistoryView::Controls { -namespace { -constexpr auto kAiComposeTooltipHiddenPref = "ai_compose_tooltip_hidden"_cs; - -} // namespace - -AiTooltipManager::AiTooltipManager( +ComposeTooltipManager::ComposeTooltipManager( not_null parent, - not_null button, + not_null button, + rpl::producer text, + base::const_string prefKey, Fn widthProvider) : _button(button) +, _prefKey(prefKey) , _widthProvider(std::move(widthProvider)) { _tooltip.reset(Ui::CreateChild( parent, Ui::MakeTooltipWithClose( parent, - tr::lng_ai_compose_tooltip(tr::rich), + std::move(text), st::historyMessagesTTLLabel.minWidth, st::ttlMediaImportantTooltipLabel, st::importantTooltipHide, @@ -48,21 +45,17 @@ AiTooltipManager::AiTooltipManager( }, _tooltip->lifetime()); } -void AiTooltipManager::hideAndRemember() { - if (!Core::App().settings().readPref( - kAiComposeTooltipHiddenPref)) { - Core::App().settings().writePref( - kAiComposeTooltipHiddenPref, - true); +void ComposeTooltipManager::hideAndRemember() { + if (!Core::App().settings().readPref(_prefKey)) { + Core::App().settings().writePref(_prefKey, true); } _shown = false; _tooltip->toggleAnimated(false); } -void AiTooltipManager::updateVisibility(bool buttonShown) { +void ComposeTooltipManager::updateVisibility(bool buttonShown) { const auto showTooltip = buttonShown - && !Core::App().settings().readPref( - kAiComposeTooltipHiddenPref); + && !Core::App().settings().readPref(_prefKey); if (showTooltip) { updateGeometry(); } @@ -73,7 +66,7 @@ void AiTooltipManager::updateVisibility(bool buttonShown) { } } -void AiTooltipManager::updateGeometry() { +void ComposeTooltipManager::updateGeometry() { if (_button->isHidden()) { return; } @@ -90,7 +83,7 @@ void AiTooltipManager::updateGeometry() { _tooltip->pointAt(geometry, RectPart::Top, countPosition); } -void AiTooltipManager::raise() { +void ComposeTooltipManager::raise() { _tooltip->raise(); } diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_ai_tooltip.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_ai_tooltip.h index e32bf74eb5..cc91d9105f 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_ai_tooltip.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_ai_tooltip.h @@ -7,6 +7,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "base/const_string.h" #include "base/unique_qptr.h" namespace Ui { @@ -16,13 +17,13 @@ class RpWidget; namespace HistoryView::Controls { -class ComposeAiButton; - -class AiTooltipManager final { +class ComposeTooltipManager final { public: - AiTooltipManager( + ComposeTooltipManager( not_null parent, - not_null button, + not_null button, + rpl::producer text, + base::const_string prefKey, Fn widthProvider); void hideAndRemember(); @@ -31,11 +32,14 @@ public: void raise(); private: - const not_null _button; + const not_null _button; + const base::const_string _prefKey; const Fn _widthProvider; base::unique_qptr _tooltip; bool _shown = false; }; +using AiTooltipManager = ComposeTooltipManager; + } // namespace HistoryView::Controls diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp index b96697a8ef..ff637d9966 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -16,6 +16,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/unixtime.h" #include "boxes/compose_ai_box.h" #include "boxes/edit_caption_box.h" +#include "boxes/send_files_box.h" #include "calls/group/ui/calls_group_stars_coloring.h" #include "calls/group/calls_group_stars_box.h" #include "chat_helpers/compose/compose_show.h" @@ -71,6 +72,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/controls/history_view_voice_record_bar.h" #include "history/view/controls/history_view_webpage_processor.h" #include "history/view/history_view_reply.h" +#include "history/view/history_view_schedule_box.h" #include "history/view/history_view_webpage_preview.h" #include "inline_bots/bot_attach_web_view.h" #include "inline_bots/inline_results_widget.h" @@ -1034,6 +1036,11 @@ ComposeControls::ComposeControls( , _aiButton(Ui::CreateChild( _wrap.get(), st::historyAiComposeButton)) +, _sendAsFile(_features.attachments + ? Ui::CreateChild( + _wrap.get(), + st::historySendAsFileButton) + : nullptr) , _like(_features.likes ? Ui::CreateChild(_wrap.get(), _st.like) : nullptr) @@ -1833,8 +1840,23 @@ rpl::producer> ComposeControls::attachRequests() const { } void ComposeControls::setMimeDataHook(MimeDataHook hook) { - _field->setMimeDataHook( - WrappedMessageFieldMimeHook(std::move(hook), _field)); + if (_sendAsFile) { + _field->setMimeDataHook( + WrappedMessageFieldMimeHook( + [=, originalHook = std::move(hook)]( + not_null data, + Ui::InputField::MimeAction action) { + if (checkLargeTextPaste(data, action)) { + return true; + } + return originalHook + ? originalHook(data, action) + : false; + }, _field)); + } else { + _field->setMimeDataHook( + WrappedMessageFieldMimeHook(std::move(hook), _field)); + } } bool ComposeControls::confirmMediaEdit(Ui::PreparedList &list) { @@ -1906,9 +1928,15 @@ void ComposeControls::showFinished() { } updateWrappingVisibility(); _aiButton->raise(); + if (_sendAsFile) { + _sendAsFile->raise(); + } if (_aiTooltipManager) { _aiTooltipManager->raise(); } + if (_sendAsFileTooltipManager) { + _sendAsFileTooltipManager->raise(); + } _voiceRecordBar->orderControls(); } @@ -2040,6 +2068,7 @@ void ComposeControls::init() { initTabbedSelector(); initSendButton(); initAiButton(); + initSendAsFileButton(); initWriteRestriction(); initVoiceRecordBar(); initKeyHandler(); @@ -2349,11 +2378,13 @@ void ComposeControls::initField() { ) | rpl::on_next([=] { updateHeight(); updateAiButtonVisibility(); + updateSendAsFileVisibility(); }, _field->lifetime()); _field->changes( ) | rpl::on_next([=] { fieldChanged(); updateAiButtonVisibility(); + updateSendAsFileVisibility(); }, _field->lifetime()); #ifdef Q_OS_MAC // Removed an ability to insert text from the menu bar @@ -3210,6 +3241,7 @@ void ComposeControls::initVoiceRecordBar() { _recording = false; } updateAiButtonVisibility(); + updateSendAsFileVisibility(); }, _wrap->lifetime()); _voiceRecordBar->setStartRecordingFilter([=] { @@ -3308,9 +3340,80 @@ void ComposeControls::initAiButton() { _aiTooltipManager = std::make_unique( _wrap.get(), _aiButton, + tr::lng_ai_compose_tooltip(tr::rich), + "ai_compose_tooltip_hidden"_cs, [=] { return _wrap->width(); }); } +void ComposeControls::initSendAsFileButton() { + if (!_sendAsFile) { + return; + } + _sendAsFile->hide(); + _sendAsFile->setClickedCallback([=] { + if (_sendAsFileTooltipManager) { + _sendAsFileTooltipManager->hideAndRemember(); + } + const auto text = _field->getTextWithTags(); + auto restore = restoreTextCallback(QString()); + fireSendTextAsFile(text.text, std::move(restore)); + }); + + _sendAsFileTooltipManager = std::make_unique( + _wrap.get(), + _sendAsFile, + tr::lng_send_as_file_tooltip(tr::rich), + "send_as_file_tooltip_hidden"_cs, + [=] { return _wrap->width(); }); +} + +void ComposeControls::setSendAsFileConfirmed( + Fn, Api::SendOptions)> confirmed) { + _sendAsFileConfirmed = std::move(confirmed); +} + +void ComposeControls::fireSendTextAsFile( + const QString &fileText, + Fn restoreText) { + if (!_sendAsFileConfirmed || !_history) { + return; + } + const auto peer = _history->peer; + const auto sendType = (_mode == Mode::Scheduled) + ? (CanScheduleUntilOnline(peer) + ? Api::SendType::ScheduledToUser + : Api::SendType::Scheduled) + : Api::SendType::Normal; + _show->show(Box(SendFilesBoxDescriptor{ + .show = _show, + .list = Ui::PrepareTextAsFile(fileText), + .caption = TextWithTags{}, + .toPeer = peer, + .limits = DefaultLimitsForPeer(peer), + .check = DefaultCheckForPeer(_show, peer), + .sendType = sendType, + .sendMenuDetails = _sendMenuDetails, + .stOverride = &_st, + .confirmed = _sendAsFileConfirmed, + .cancelled = std::move(restoreText), + })); +} + +bool ComposeControls::checkLargeTextPaste( + not_null data, + Ui::InputField::MimeAction action) { + const auto result = Ui::CheckLargeTextPaste(_field, data); + if (!result.exceeds) { + return false; + } + if (action == Ui::InputField::MimeAction::Check) { + return true; + } + auto restore = restoreTextCallback(QString()); + fireSendTextAsFile(result.resultingText, std::move(restore)); + return true; +} + void ComposeControls::updateWrappingVisibility() { const auto &restriction = _writeRestriction.current(); const auto restricted = !restriction.empty() && _writeRestricted; @@ -3323,6 +3426,7 @@ void ComposeControls::updateWrappingVisibility() { _wrap->setVisible(!hidden && !restricted); updateControlsParents(); updateAiButtonVisibility(); + updateSendAsFileVisibility(); if (!hidden && !restricted) { updateControlsGeometry(_wrap->size()); _wrap->raise(); @@ -3514,6 +3618,7 @@ void ComposeControls::updateControlsGeometry(QSize size) { _ttlInfo->move(size.width() - right - _ttlInfo->width(), buttonsTop); } updateAiButtonGeometry(); + updateSendAsFileGeometry(); _voiceRecordBar->resizeToWidth(size.width()); _voiceRecordBar->moveToLeft( @@ -3553,6 +3658,7 @@ void ComposeControls::updateControlsVisibility() { _starsReaction->show(); } updateAiButtonVisibility(); + updateSendAsFileVisibility(); } void ComposeControls::updateAiButtonVisibility() { @@ -3584,6 +3690,37 @@ void ComposeControls::updateAiButtonGeometry() { } } +void ComposeControls::updateSendAsFileVisibility() { + if (!_sendAsFile) { + return; + } + const auto hidden = !textExceedsMaxSize() + || _wrap->isHidden() + || _recording.current() + || _field->isHidden(); + if (_sendAsFile->isHidden() == hidden) { + return; + } + _sendAsFile->setVisible(!hidden); + if (!hidden) { + updateSendAsFileGeometry(); + } + if (_sendAsFileTooltipManager) { + _sendAsFileTooltipManager->updateVisibility(!hidden); + } +} + +void ComposeControls::updateSendAsFileGeometry() { + if (!_sendAsFile || _sendAsFile->isHidden()) { + return; + } + const auto x = _send->x() + _send->width() - _sendAsFile->width(); + _sendAsFile->move(QPoint(x, _field->y()) + st::historyAiComposeButtonPosition); + if (_sendAsFileTooltipManager) { + _sendAsFileTooltipManager->updateGeometry(); + } +} + bool ComposeControls::updateLikeShown() { auto shown = _like && !HasSendText(_field); if (_likeShown != shown) { @@ -3646,6 +3783,12 @@ bool ComposeControls::hasEnoughLinesForAi() const { && Ui::HasEnoughLinesForAi(&session(), _field); } +bool ComposeControls::textExceedsMaxSize() const { + return _history + && !_recording.current() + && _field->getLastText().size() > MaxMessageSize; +} + bool ComposeControls::updateBotCommandShown() { auto shown = false; const auto peer = _history ? _history->peer.get() : nullptr; diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 4a566fbafa..2adc246881 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -68,6 +68,7 @@ class EmojiButton; class SendAsButton; class SilentToggle; class DropdownMenu; +struct PreparedBundle; struct PreparedList; struct SendStarButtonState; class ReactionFlyAnimation; @@ -102,7 +103,8 @@ class TTLButton; class WebpageProcessor; class CharactersLimitLabel; class ComposeAiButton; -class AiTooltipManager; +class ComposeTooltipManager; +using AiTooltipManager = ComposeTooltipManager; } // namespace HistoryView::Controls namespace HistoryView { @@ -196,6 +198,10 @@ public: [[nodiscard]] rpl::producer sendCommandRequests() const; [[nodiscard]] rpl::producer editRequests() const; [[nodiscard]] rpl::producer> attachRequests() const; + void setSendAsFileConfirmed( + Fn, + Api::SendOptions)> confirmed); [[nodiscard]] rpl::producer fileChosen() const; [[nodiscard]] rpl::producer photoChosen() const; [[nodiscard]] rpl::producer jumpToItemRequests() const; @@ -328,6 +334,15 @@ private: void updateControlsGeometry(QSize size); void updateAiButtonVisibility(); void updateAiButtonGeometry(); + void initSendAsFileButton(); + void fireSendTextAsFile( + const QString &fileText, + Fn restoreText); + [[nodiscard]] bool checkLargeTextPaste( + not_null data, + Ui::InputField::MimeAction action); + void updateSendAsFileVisibility(); + void updateSendAsFileGeometry(); void setupSendMenu( not_null button, Fn send); @@ -363,6 +378,7 @@ private: bool updateBotCommandShown(); bool updateLikeShown(); [[nodiscard]] bool hasEnoughLinesForAi() const; + [[nodiscard]] bool textExceedsMaxSize() const; void cancelInlineBot(); void clearInlineBot(); @@ -441,6 +457,7 @@ private: const std::shared_ptr _send; Controls::ComposeAiButton * const _aiButton = nullptr; + Ui::IconButton * const _sendAsFile = nullptr; Ui::IconButton *_editStars = nullptr; Ui::IconButton *_like = nullptr; rpl::variable _minStarsCount; @@ -476,6 +493,7 @@ private: const std::unique_ptr _header; const std::unique_ptr _voiceRecordBar; std::unique_ptr _aiTooltipManager; + std::unique_ptr _sendAsFileTooltipManager; std::shared_ptr _chatStyle; const Fn _sendMenuDetails; @@ -491,6 +509,7 @@ private: rpl::event_stream> _scrollKeyEvents; rpl::event_stream> _editLastMessageRequests; rpl::event_stream> _attachRequests; + Fn, Api::SendOptions)> _sendAsFileConfirmed; rpl::event_stream<> _likeToggled; rpl::event_stream _replyNextRequests; rpl::event_stream<> _focusRequests; diff --git a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp index 3fcaf16576..af5b4adee0 100644 --- a/Telegram/SourceFiles/history/view/history_view_chat_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_chat_section.cpp @@ -916,6 +916,12 @@ void ChatWidget::setupComposeControls() { [=] { chooseAttach(overrideCompress); }); }, lifetime()); + _composeControls->setSendAsFileConfirmed(crl::guard(this, [=]( + std::shared_ptr bundle, + Api::SendOptions options) { + sendingFilesConfirmed(std::move(bundle), options); + })); + _composeControls->fileChosen( ) | rpl::on_next([=](ChatHelpers::FileChosen data) { controller()->hideLayer(anim::type::normal); diff --git a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp index ab9ed36b9d..904dddc0e6 100644 --- a/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_scheduled_section.cpp @@ -403,6 +403,12 @@ void ScheduledWidget::setupComposeControls() { [=] { _choosingAttach = false; chooseAttach(); }); }, lifetime()); + _composeControls->setSendAsFileConfirmed(crl::guard(this, [=]( + std::shared_ptr bundle, + Api::SendOptions options) { + sendingFilesConfirmed(std::move(bundle), options); + })); + _composeControls->fileChosen( ) | rpl::on_next([=](ChatHelpers::FileChosen data) { controller()->hideLayer(anim::type::normal); diff --git a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp index fc80a65e40..1164fc3398 100644 --- a/Telegram/SourceFiles/media/stories/media_stories_reply.cpp +++ b/Telegram/SourceFiles/media/stories/media_stories_reply.cpp @@ -766,6 +766,12 @@ void ReplyArea::initActions() { [=] { chooseAttach(overrideCompress); }); }, _lifetime); + _controls->setSendAsFileConfirmed(crl::guard(this, [=]( + std::shared_ptr bundle, + Api::SendOptions options) { + sendingFilesConfirmed(std::move(bundle), options); + })); + _controls->fileChosen( ) | rpl::on_next([=](ChatHelpers::FileChosen data) { _controller->uiShow()->hideLayer(); diff --git a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp index 5663edbb61..908bd3eee2 100644 --- a/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp +++ b/Telegram/SourceFiles/settings/business/settings_shortcut_messages.cpp @@ -691,6 +691,12 @@ void ShortcutMessages::setupComposeControls() { }); }, lifetime()); + _composeControls->setSendAsFileConfirmed(crl::guard(this, [=]( + std::shared_ptr bundle, + Api::SendOptions options) { + sendingFilesConfirmed(std::move(bundle), options); + })); + _composeControls->fileChosen( ) | rpl::on_next([=](ChatHelpers::FileChosen data) { _controller->hideLayer(anim::type::normal); diff --git a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp index 75db8c0915..2ccba7412a 100644 --- a/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp +++ b/Telegram/SourceFiles/ui/chat/attach/attach_single_file_preview.cpp @@ -64,8 +64,13 @@ void SingleFilePreview::preparePreview(const PreparedFile &file) { ? fallbackName : file.displayName; data.name = displayName; - data.statusText = FormatImageSizeText(file.originalDimensions); - data.fileIsImage = true; + if (file.originalDimensions.isValid()) { + data.statusText = FormatImageSizeText(file.originalDimensions); + data.fileIsImage = true; + } else { + data.statusText = FormatSizeText(file.size); + data.fileIsImage = false; + } } else { auto fileinfo = QFileInfo(filepath); auto filename = file.displayName.isEmpty() diff --git a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp index 8601b9a2ef..ed640d242b 100644 --- a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp +++ b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.cpp @@ -9,10 +9,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/options.h" #include "boxes/compose_ai_box.h" +#include "config.h" +#include "core/mime_type.h" #include "history/view/controls/history_view_compose_ai_button.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" #include "main/main_session.h" +#include "ui/chat/attach/attach_prepare.h" +#include "ui/text/text.h" #include "ui/text/text_entity.h" #include "ui/widgets/fields/input_field.h" @@ -43,7 +47,63 @@ bool HasEnoughLinesForAi( const auto contentHeight = field->height() - margins.top() - margins.bottom(); - return contentHeight >= (3 * lineHeight); + if (contentHeight < (3 * lineHeight)) { + return false; + } + const auto &text = field->getLastText(); + if (text.size() > MaxMessageSize) { + return false; + } + for (const auto &ch : text) { + if (!Text::IsTrimmed(ch) && !Text::IsReplacedBySpace(ch)) { + return true; + } + } + return false; +} + +PreparedList PrepareTextAsFile(const QString &text) { + auto content = text.toUtf8(); + auto result = PreparedList(); + auto file = PreparedFile(QString()); + file.content = content; + file.displayName = u"message.txt"_q; + file.size = content.size(); + file.information = std::make_unique(); + file.information->filemime = u"text/plain"_q; + result.files.push_back(std::move(file)); + return result; +} + +constexpr auto kSendAsFilePasteMultiplier = 8; + +int SendAsFilePasteThreshold() { + return kSendAsFilePasteMultiplier * MaxMessageSize; +} + +LargeTextPasteResult CheckLargeTextPaste( + not_null field, + not_null data) { + const auto pasteText = Core::ReadMimeText(data); + if (pasteText.isEmpty()) { + return {}; + } + const auto cursor = field->textCursor(); + const auto currentText = field->getLastText(); + const auto selStart = cursor.selectionStart(); + const auto selEnd = cursor.selectionEnd(); + const auto resultingSize = currentText.size() + - (selEnd - selStart) + + pasteText.size(); + if (resultingSize < SendAsFilePasteThreshold()) { + return {}; + } + return { + .exceeds = true, + .resultingText = currentText.mid(0, selStart) + + pasteText + + currentText.mid(selEnd), + }; } void UpdateCaptionAiButtonGeometry( diff --git a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.h b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.h index 6044c8fe14..8e6d9bb7cc 100644 --- a/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.h +++ b/Telegram/SourceFiles/ui/controls/compose_ai_button_factory.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +class QMimeData; + namespace Main { class Session; } // namespace Main @@ -23,6 +25,8 @@ class ComposeAiButton; namespace Ui { +struct PreparedList; + extern const char kOptionHideAiButton[]; [[nodiscard]] bool HasEnoughLinesForAi( @@ -44,4 +48,16 @@ void UpdateCaptionAiButtonGeometry( not_null button, not_null field); +[[nodiscard]] PreparedList PrepareTextAsFile(const QString &text); +[[nodiscard]] int SendAsFilePasteThreshold(); + +struct LargeTextPasteResult { + bool exceeds = false; + QString resultingText; +}; + +[[nodiscard]] LargeTextPasteResult CheckLargeTextPaste( + not_null field, + not_null data); + } // namespace Ui