Suggest sending large texts as files.

This commit is contained in:
John Preston
2026-04-10 19:25:51 +07:00
parent 9a9307ead3
commit 05cf8aaa0f
17 changed files with 444 additions and 34 deletions
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="96.5 111.3 766.5 766.5" xmlns="http://www.w3.org/2000/svg">
<g stroke="none" fill="none" fill-rule="evenodd">
<path d="M 229.278 428.274 C 220.345 431.087 209.724 433.028 201.037 428.414 C 195.269 425.35 191.59 419.715 189.754 413.573 C 183.55 392.827 189.646 312.841 194.598 289.706 C 197.361 276.798 201.636 264.234 208.088 252.692 C 225.944 220.748 254.345 196.434 289.828 186.42 C 299.548 183.676 309.688 182.14 319.754 181.423 C 366.51 178.095 414.437 180.111 461.345 180.132 C 499.997 180.149 540.285 177.606 578.657 181.934 C 589.925 183.204 601.183 185.766 611.125 191.369 C 638.232 206.647 660.549 233.285 682.155 255.355 L 741.115 315.299 L 790.708 365.561 C 856.391 432.25 853.313 440.991 854.144 532.569 L 854.982 618.399 C 855.281 646.04 857.577 702.741 854.521 727.102 C 852.435 744.448 846.915 761.205 838.282 776.393 C 820.298 808.656 790.121 832.354 754.514 842.176 C 724.089 850.54 664.593 848.635 631.504 848.632 L 502.763 848.626 L 395.022 848.66 C 368.985 848.68 338.582 849.327 312.926 846.33 C 294.671 844.264 277.017 838.552 261.012 829.532 C 228.881 811.132 205.527 780.551 196.236 744.708 C 190.977 725.111 181.752 660.54 192.22 640.91 C 194.552 636.536 200.443 631.193 204.584 628.088 C 212.69 627.794 223.61 625.977 229.827 632.364 C 238.468 641.24 238.784 653.684 239.449 665.298 C 241.05 693.25 237.429 726.072 251.724 751.22 C 263.068 771.178 283.96 788.616 306.011 795.036 C 326.159 800.066 359.183 798.753 380.622 798.749 L 460.193 798.696 L 703.391 798.695 C 736.68 798.43 755.087 795.322 779.818 770.591 C 807.116 743.293 806.834 720.038 807.012 684.161 C 807.237 638.481 802.094 586.261 804.093 541.122 C 797.284 541.268 784.894 540.995 778.473 540.182 L 670.807 540.171 C 620.731 540.215 563.716 547.86 526.38 508.874 C 515.394 497.402 505.295 482.717 501.211 466.939 C 495.154 439.227 496.977 401.501 496.999 372.596 L 497.103 265.614 C 497.25 253.886 497.577 242.161 498.083 230.443 C 460.209 229.51 422.322 229.222 384.438 229.578 C 335.986 229.564 309.732 221.795 269.362 254.551 C 217.116 296.943 257.721 389.29 229.559 427.897 L 229.278 428.274 z M 543.987 230.048 C 569.591 229.725 580.035 227.497 600.857 244.842 C 616.603 257.958 630.079 272.64 644.427 287.248 L 711.181 355.245 L 759.072 403.921 C 769.77 414.798 785.756 430.108 793.914 442.441 C 801.17 453.41 801.179 474.426 802.716 487.848 L 663.551 488.081 C 655.173 488.09 645.943 488.449 637.627 488.107 C 619.637 487.367 594.727 490.338 577.846 484.153 C 545.853 472.432 543.843 436.102 544.247 407.389 C 544.438 393.841 544.247 379.666 544.256 365.887 C 544.495 320.607 544.405 275.326 543.987 230.048 z" fill="#FFFFFF"></path>
<path d="M 306.755 391.204 C 311.745 389.305 333.398 408.89 337.28 412.89 C 364.174 440.939 392.794 467.428 419.422 495.698 C 442.291 519.978 433.144 542.942 411.651 563.866 C 381.068 593.638 352.265 625.515 320.324 653.743 C 313.888 659.431 296.743 655.829 288.97 654.425 C 287.039 649.138 280.827 633.452 282.505 628.63 C 289.515 608.489 314.352 590.824 328.353 575.581 C 338.311 564.74 345.544 555.412 356.226 547.597 L 356.28 545.853 C 344.978 549.112 335.336 551.079 323.481 551.267 C 274.259 551.846 224.991 551.501 175.755 551.624 C 155.295 551.676 133.425 553.912 116.627 541.311 C 114.956 537.351 114 533.126 113.802 528.833 C 113.091 514.333 117.1 501.472 133.314 500.562 C 154.052 499.399 174.698 499.879 195.467 499.925 C 239.08 500.336 282.696 500.334 326.309 499.918 C 332.541 499.822 343.551 500.1 347.833 495.685 C 347.436 491.434 313.367 461.839 307.219 455.232 C 291.043 437.85 275.937 429.066 286.23 402.426 C 291.881 397.453 299.873 394.302 306.755 391.204 z" fill="#FFFFFF"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+2
View File
@@ -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...";
@@ -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;
@@ -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;
@@ -273,6 +273,9 @@ HistoryWidget::HistoryWidget(
, _aiButton(Ui::CreateChild<HistoryView::Controls::ComposeAiButton>(
this,
st::historyAiComposeButton))
, _sendAsFile(Ui::CreateChild<Ui::IconButton>(
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<const QMimeData*> 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<HistoryView::Controls::AiTooltipManager>(
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<HistoryView::Controls::AiTooltipManager>(
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<SendFilesBox>(
controller(),
std::move(result),
TextWithTags{},
_peer,
Api::SendType::Normal,
sendMenuDetails());
box->setConfirmedCallback(crl::guard(this, [=](
std::shared_ptr<Ui::PreparedBundle> 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();
}
+13 -1
View File
@@ -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<Ui::SendButton> _send;
HistoryView::Controls::ComposeAiButton * const _aiButton = nullptr;
Ui::IconButton * const _sendAsFile = nullptr;
object_ptr<Ui::FlatButton> _unblock;
object_ptr<Ui::FlatButton> _botStart;
object_ptr<Ui::FlatButton> _joinChannel;
@@ -867,6 +878,7 @@ private:
rpl::lifetime _subsectionTabsLifetime;
rpl::lifetime _subsectionCheckLifetime;
std::unique_ptr<HistoryView::Controls::AiTooltipManager> _aiTooltipManager;
std::unique_ptr<HistoryView::Controls::AiTooltipManager> _sendAsFileTooltipManager;
std::shared_ptr<Ui::ChatStyle> _fieldChatStyle;
bool _cmdStartShown = false;
object_ptr<Ui::InputField> _field;
@@ -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<QWidget*> parent,
not_null<ComposeAiButton*> button,
not_null<Ui::RpWidget*> button,
rpl::producer<TextWithEntities> text,
base::const_string prefKey,
Fn<int()> widthProvider)
: _button(button)
, _prefKey(prefKey)
, _widthProvider(std::move(widthProvider)) {
_tooltip.reset(Ui::CreateChild<Ui::ImportantTooltip>(
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<bool>(
kAiComposeTooltipHiddenPref)) {
Core::App().settings().writePref<bool>(
kAiComposeTooltipHiddenPref,
true);
void ComposeTooltipManager::hideAndRemember() {
if (!Core::App().settings().readPref<bool>(_prefKey)) {
Core::App().settings().writePref<bool>(_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<bool>(
kAiComposeTooltipHiddenPref);
&& !Core::App().settings().readPref<bool>(_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();
}
@@ -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<QWidget*> parent,
not_null<ComposeAiButton*> button,
not_null<Ui::RpWidget*> button,
rpl::producer<TextWithEntities> text,
base::const_string prefKey,
Fn<int()> widthProvider);
void hideAndRemember();
@@ -31,11 +32,14 @@ public:
void raise();
private:
const not_null<ComposeAiButton*> _button;
const not_null<Ui::RpWidget*> _button;
const base::const_string _prefKey;
const Fn<int()> _widthProvider;
base::unique_qptr<Ui::ImportantTooltip> _tooltip;
bool _shown = false;
};
using AiTooltipManager = ComposeTooltipManager;
} // namespace HistoryView::Controls
@@ -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<Controls::ComposeAiButton>(
_wrap.get(),
st::historyAiComposeButton))
, _sendAsFile(_features.attachments
? Ui::CreateChild<Ui::IconButton>(
_wrap.get(),
st::historySendAsFileButton)
: nullptr)
, _like(_features.likes
? Ui::CreateChild<Ui::IconButton>(_wrap.get(), _st.like)
: nullptr)
@@ -1833,8 +1840,23 @@ rpl::producer<std::optional<bool>> 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<const QMimeData*> 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<Controls::AiTooltipManager>(
_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<Controls::AiTooltipManager>(
_wrap.get(),
_sendAsFile,
tr::lng_send_as_file_tooltip(tr::rich),
"send_as_file_tooltip_hidden"_cs,
[=] { return _wrap->width(); });
}
void ComposeControls::setSendAsFileConfirmed(
Fn<void(std::shared_ptr<Ui::PreparedBundle>, Api::SendOptions)> confirmed) {
_sendAsFileConfirmed = std::move(confirmed);
}
void ComposeControls::fireSendTextAsFile(
const QString &fileText,
Fn<void()> 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<SendFilesBox>(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<const QMimeData*> 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;
@@ -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<QString> sendCommandRequests() const;
[[nodiscard]] rpl::producer<MessageToEdit> editRequests() const;
[[nodiscard]] rpl::producer<std::optional<bool>> attachRequests() const;
void setSendAsFileConfirmed(
Fn<void(
std::shared_ptr<Ui::PreparedBundle>,
Api::SendOptions)> confirmed);
[[nodiscard]] rpl::producer<FileChosen> fileChosen() const;
[[nodiscard]] rpl::producer<PhotoChosen> photoChosen() const;
[[nodiscard]] rpl::producer<FullReplyTo> jumpToItemRequests() const;
@@ -328,6 +334,15 @@ private:
void updateControlsGeometry(QSize size);
void updateAiButtonVisibility();
void updateAiButtonGeometry();
void initSendAsFileButton();
void fireSendTextAsFile(
const QString &fileText,
Fn<void()> restoreText);
[[nodiscard]] bool checkLargeTextPaste(
not_null<const QMimeData*> data,
Ui::InputField::MimeAction action);
void updateSendAsFileVisibility();
void updateSendAsFileGeometry();
void setupSendMenu(
not_null<Ui::RpWidget*> button,
Fn<void(Api::SendOptions)> 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<Ui::SendButton> _send;
Controls::ComposeAiButton * const _aiButton = nullptr;
Ui::IconButton * const _sendAsFile = nullptr;
Ui::IconButton *_editStars = nullptr;
Ui::IconButton *_like = nullptr;
rpl::variable<int> _minStarsCount;
@@ -476,6 +493,7 @@ private:
const std::unique_ptr<FieldHeader> _header;
const std::unique_ptr<Controls::VoiceRecordBar> _voiceRecordBar;
std::unique_ptr<Controls::AiTooltipManager> _aiTooltipManager;
std::unique_ptr<Controls::AiTooltipManager> _sendAsFileTooltipManager;
std::shared_ptr<Ui::ChatStyle> _chatStyle;
const Fn<SendMenu::Details()> _sendMenuDetails;
@@ -491,6 +509,7 @@ private:
rpl::event_stream<not_null<QKeyEvent*>> _scrollKeyEvents;
rpl::event_stream<not_null<QKeyEvent*>> _editLastMessageRequests;
rpl::event_stream<std::optional<bool>> _attachRequests;
Fn<void(std::shared_ptr<Ui::PreparedBundle>, Api::SendOptions)> _sendAsFileConfirmed;
rpl::event_stream<> _likeToggled;
rpl::event_stream<ReplyNextRequest> _replyNextRequests;
rpl::event_stream<> _focusRequests;
@@ -916,6 +916,12 @@ void ChatWidget::setupComposeControls() {
[=] { chooseAttach(overrideCompress); });
}, lifetime());
_composeControls->setSendAsFileConfirmed(crl::guard(this, [=](
std::shared_ptr<Ui::PreparedBundle> bundle,
Api::SendOptions options) {
sendingFilesConfirmed(std::move(bundle), options);
}));
_composeControls->fileChosen(
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
controller()->hideLayer(anim::type::normal);
@@ -403,6 +403,12 @@ void ScheduledWidget::setupComposeControls() {
[=] { _choosingAttach = false; chooseAttach(); });
}, lifetime());
_composeControls->setSendAsFileConfirmed(crl::guard(this, [=](
std::shared_ptr<Ui::PreparedBundle> bundle,
Api::SendOptions options) {
sendingFilesConfirmed(std::move(bundle), options);
}));
_composeControls->fileChosen(
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
controller()->hideLayer(anim::type::normal);
@@ -766,6 +766,12 @@ void ReplyArea::initActions() {
[=] { chooseAttach(overrideCompress); });
}, _lifetime);
_controls->setSendAsFileConfirmed(crl::guard(this, [=](
std::shared_ptr<Ui::PreparedBundle> bundle,
Api::SendOptions options) {
sendingFilesConfirmed(std::move(bundle), options);
}));
_controls->fileChosen(
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
_controller->uiShow()->hideLayer();
@@ -691,6 +691,12 @@ void ShortcutMessages::setupComposeControls() {
});
}, lifetime());
_composeControls->setSendAsFileConfirmed(crl::guard(this, [=](
std::shared_ptr<Ui::PreparedBundle> bundle,
Api::SendOptions options) {
sendingFilesConfirmed(std::move(bundle), options);
}));
_composeControls->fileChosen(
) | rpl::on_next([=](ChatHelpers::FileChosen data) {
_controller->hideLayer(anim::type::normal);
@@ -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()
@@ -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<PreparedFileInformation>();
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<Ui::InputField*> field,
not_null<const QMimeData*> 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(
@@ -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<HistoryView::Controls::ComposeAiButton*> button,
not_null<Ui::InputField*> field);
[[nodiscard]] PreparedList PrepareTextAsFile(const QString &text);
[[nodiscard]] int SendAsFilePasteThreshold();
struct LargeTextPasteResult {
bool exceeds = false;
QString resultingText;
};
[[nodiscard]] LargeTextPasteResult CheckLargeTextPaste(
not_null<Ui::InputField*> field,
not_null<const QMimeData*> data);
} // namespace Ui