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