Send previewed rich drafts from compose

This commit is contained in:
John Preston
2026-06-24 01:36:51 +04:00
parent ff86735953
commit 1f87c04dfc
11 changed files with 342 additions and 15 deletions
+49
View File
@@ -4206,6 +4206,55 @@ void ApiWrap::sendShortcutMessages(
}).send();
}
void ApiWrap::sendRichMessage(
std::shared_ptr<const Iv::RichPage> page,
const MTPInputRichMessage &richMessage,
SendAction action) {
Expects(page != nullptr);
const auto history = action.history;
const auto peer = history->peer;
const auto newId = FullMsgId(
peer->id,
_session->data().nextLocalMessageId());
auto flags = NewMessageFlags(peer);
if (action.replyTo) {
flags |= MessageFlag::HasReplyInfo;
}
FillMessagePostFlags(action, peer, flags);
if (action.options.scheduled) {
flags |= MessageFlag::IsOrWasScheduled;
}
if (action.options.shortcutId) {
flags |= MessageFlag::ShortcutMessage;
}
const auto item = history->addNewLocalMessage({
.id = newId.msg,
.flags = flags,
.from = NewMessageFromId(action),
.replyTo = action.replyTo,
.date = NewMessageDate(action.options),
.scheduleRepeatPeriod = action.options.scheduleRepeatPeriod,
.shortcutId = action.options.shortcutId,
.starsPaid = std::min(
peer->starsPerMessageChecked(),
action.options.starsApproved),
.postAuthor = NewMessagePostAuthor(action),
.effectId = action.options.effectId,
.suggest = HistoryMessageSuggestInfo(action.options),
}, TextWithEntities(), MTP_messageMediaEmpty());
item->applyLocalRichPage(std::move(page));
sendRichMessage(item, richMessage, action);
_session->data().sendHistoryChangeNotifications();
_session->changes().historyUpdated(
history,
(action.options.scheduled
? Data::HistoryUpdate::Flag::ScheduledSent
: Data::HistoryUpdate::Flag::MessageSent));
}
void ApiWrap::sendRichMessage(
not_null<HistoryItem*> item,
const MTPInputRichMessage &richMessage,
+8
View File
@@ -25,6 +25,10 @@ namespace Main {
class Session;
} // namespace Main
namespace Iv {
struct RichPage;
} // namespace Iv
namespace Data {
struct ReactionId;
struct UpdatedFileReferences;
@@ -388,6 +392,10 @@ public:
not_null<HistoryItem*> item,
const MTPInputRichMessage &richMessage,
SendAction action);
void sendRichMessage(
std::shared_ptr<const Iv::RichPage> page,
const MTPInputRichMessage &richMessage,
SendAction action);
void sendMessage(
MessageToSend &&message,
std::optional<MsgId> localMessageId = std::nullopt);
@@ -72,7 +72,9 @@ int ComputeSendingMessagesCount(
not_null<History*> history,
const SendingErrorRequest &request) {
auto result = 0;
if (request.text && !request.text->empty()) {
if (request.richMessage) {
++result;
} else if (request.text && !request.text->empty()) {
auto sending = TextWithEntities();
auto left = TextWithEntities{
request.text->text,
@@ -123,7 +125,8 @@ Data::SendError GetErrorForSending(
}
}
}
const auto hasText = (request.text && !request.text->empty());
const auto hasText = request.richMessage
|| (request.text && !request.text->empty());
if (hasText) {
const auto error = Data::RestrictionError(
peer,
@@ -136,6 +136,7 @@ struct SendingErrorRequest {
const TextWithTags *text = nullptr;
int messagesCount = 0;
bool ignoreSlowmodeCountdown = false;
bool richMessage = false;
};
[[nodiscard]] int ComputeSendingMessagesCount(
not_null<History*> history,
+139 -11
View File
@@ -144,6 +144,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/view/history_view_translate_bar.h"
#include "history/view/media/history_view_media.h"
#include "iv/editor/iv_editor_session.h"
#include "iv/iv_rich_message_serializer.h"
#include "core/click_handler_types.h"
#include "chat_helpers/field_autocomplete.h"
#include "chat_helpers/tabbed_panel.h"
@@ -2163,6 +2164,13 @@ Data::Draft *HistoryWidget::cloudDraft() const {
return _history ? _history->cloudDraft(MsgId(), PeerId()) : nullptr;
}
std::shared_ptr<const Iv::RichPage> HistoryWidget::shownRichMessage() const {
if (const auto draft = shouldShowRichDraftPreview() ? cloudDraft() : nullptr) {
return draft->richMessage;
}
return nullptr;
}
bool HistoryWidget::isComposeBoxOpen() const {
return _history
&& Iv::Editor::IsComposeBoxOpen(
@@ -2592,6 +2600,7 @@ void HistoryWidget::fastShowAtEnd(not_null<History*> history) {
bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
if (bypassNormalDraftHandling()) {
updateCmdStartShown();
updateControlsVisibility();
updateControlsGeometry();
return true;
@@ -2650,6 +2659,7 @@ bool HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) {
}
_textUpdateEvents = TextUpdateEvent::SaveDraft
| TextUpdateEvent::SendTyping;
updateCmdStartShown();
updateControlsVisibility();
updateControlsGeometry();
refreshTopBarActiveChat();
@@ -3441,6 +3451,7 @@ void HistoryWidget::registerThreadFieldBridge() {
}
unregisterDraftSources();
registerDraftSource();
updateCmdStartShown();
updateSendButtonType();
updateControlsVisibility();
updateControlsGeometry();
@@ -3775,7 +3786,7 @@ bool HistoryWidget::canWriteMessage() const {
void HistoryWidget::updateControlsVisibility() {
auto fieldDisabledRemoved = (_fieldDisabled != nullptr);
auto fieldVisibilityChanged = false;
const auto hideExtraButtons = _fieldCharsCountManager.isLimitExceeded();
const auto hideExtra = hideExtraButtons();
const auto guard = gsl::finally([&] {
if (fieldDisabledRemoved) {
_fieldDisabled = nullptr;
@@ -4014,7 +4025,7 @@ void HistoryWidget::updateControlsVisibility() {
auto rightButtonsChanged = false;
if (_silent) {
const auto was = _silent->isVisible();
const auto now = (!_editMsgId) && (!hideExtraButtons);
const auto now = (!_editMsgId) && (!hideExtra);
if (was != now) {
_silent->setVisible(now);
rightButtonsChanged = true;
@@ -4022,7 +4033,7 @@ void HistoryWidget::updateControlsVisibility() {
}
if (_scheduled) {
const auto was = _scheduled->isVisible();
const auto now = (!_editMsgId) && (!hideExtraButtons);
const auto now = (!_editMsgId) && (!hideExtra);
if (was != now) {
_scheduled->setVisible(now);
rightButtonsChanged = true;
@@ -4038,7 +4049,7 @@ void HistoryWidget::updateControlsVisibility() {
}
if (_giftToUser) {
const auto was = _giftToUser->isVisible();
const auto now = (!_editMsgId) && (!hideExtraButtons);
const auto now = (!_editMsgId) && (!hideExtra);
if (was != now) {
_giftToUser->setVisible(now);
rightButtonsChanged = true;
@@ -4046,7 +4057,7 @@ void HistoryWidget::updateControlsVisibility() {
}
if (_ttlInfo) {
const auto was = _ttlInfo->isVisible();
const auto now = (!_editMsgId) && (!hideExtraButtons);
const auto now = (!_editMsgId) && (!hideExtra);
if (was != now) {
_ttlInfo->setVisible(now);
rightButtonsChanged = true;
@@ -5352,6 +5363,9 @@ void HistoryWidget::send(Api::SendOptions options) {
} else if (_editMsgId) {
saveEditMessage({});
return;
} else if (const auto page = shownRichMessage()) {
sendRichDraft(page, options);
return;
} else if (!options.scheduled && showSlowmodeError()) {
return;
} else if (_voiceRecordBar->isListenState()) {
@@ -5366,6 +5380,78 @@ void HistoryWidget::send(Api::SendOptions options) {
nullptr);
}
void HistoryWidget::sendRichDraft(
std::shared_ptr<const Iv::RichPage> page,
Api::SendOptions options) {
if (!page) {
return;
}
if (!options.scheduled) {
_cornerButtons.clearReplyReturns();
if (showSlowmodeError()) {
return;
}
}
auto action = prepareSendAction(options);
auto withPaymentApproved = Fn<void(int)>();
if (!options.scheduled) {
withPaymentApproved = [=](int approved) {
auto copy = options;
copy.starsApproved = approved;
sendRichDraft(page, copy);
};
}
if (showSendRichDraftError(
options.scheduled != 0,
std::move(withPaymentApproved),
action.options)) {
return;
}
const auto serialized = Iv::SerializeInputRichMessage(
&session(),
*page,
Iv::SerializeInputRichMessageMode::FinalSubmit);
if (serialized.status == Iv::SerializeInputRichMessageStatus::EmptyContent) {
controller()->showToast(tr::lng_article_submit_empty(tr::now));
return;
} else if (serialized.status != Iv::SerializeInputRichMessageStatus::Success
|| !serialized.value) {
controller()->showToast(tr::lng_attach_failed(tr::now));
return;
}
session().api().sendRichMessage(
page,
*serialized.value,
action);
clearFieldText();
if (_preview) {
_preview->apply({ .removed = true });
}
saveDraftWithTextNow();
if (session().supportMode()) {
updateCmdStartShown();
updateControlsVisibility();
updateControlsGeometry();
} else {
applyCloudDraft(_history);
}
hideSelectorControlsAnimated();
setInnerFocus();
if (!_keyboard->hasMarkup() && _keyboard->forceReply() && !_kbReplyTo) {
toggleKeyboard();
}
session().sendProgressManager().update(
_history,
Api::SendProgressType::Typing,
-1);
}
void HistoryWidget::sendTextWithTags(
TextWithTags textWithTags,
bool useWebPageDraft,
@@ -5460,7 +5546,9 @@ void HistoryWidget::sendScheduled(Api::SendOptions initialOptions) {
return;
}
const auto ignoreSlowmodeCountdown = true;
if (showSendMessageError(
if (shownRichMessage()
? showSendRichDraftError(ignoreSlowmodeCountdown)
: showSendMessageError(
_field->getTextWithAppliedMarkdown(),
ignoreSlowmodeCountdown)) {
return;
@@ -5534,7 +5622,7 @@ SendMenu::Details HistoryWidget::sendButtonMenuDetails() const {
SendMenu::Details HistoryWidget::sendButtonDefaultDetails() const {
auto result = sendMenuDetails();
if (!fieldHasSendText() && !_previewDrawPreview) {
if (!hasSendableContent() && !_previewDrawPreview) {
result.effectAllowed = false;
}
return result;
@@ -6234,7 +6322,7 @@ bool HistoryWidget::showRecordButton() const {
return (_recordAvailability != Webrtc::RecordAvailability::None)
&& !_voiceRecordBar->isListenState()
&& !_voiceRecordBar->isRecordingByAnotherBar()
&& !fieldHasSendText()
&& !hasSendableContent()
&& !_previewDrawPreview
&& (_replyTo || !readyToForward())
&& !_editMsgId;
@@ -6270,13 +6358,15 @@ void HistoryWidget::updateSendButtonType() {
: 0;
}();
const auto perMessage = _peer ? _peer->starsPerMessageChecked() : 0;
const auto richMessage = (shownRichMessage() != nullptr);
const auto messages = !_peer
? 0
: _voiceRecordBar->isListenState()
? 1
: ComputeSendingMessagesCount(_history, {
.forward = &_forwardPanel->items(),
.text = &_field->getTextWithTags(),
.text = richMessage ? nullptr : &_field->getTextWithTags(),
.richMessage = richMessage,
});
const auto stars = perMessage ? (perMessage * messages) : 0;
_send->setState({
@@ -6314,7 +6404,7 @@ bool HistoryWidget::updateCmdStartShown() {
&& !_keyboard->hasMarkup()
&& !_keyboard->forceReply()
&& !_editMsgId) {
if (!fieldHasSendText()) {
if (!hasSendableContent()) {
cmdStartShown = true;
}
}
@@ -6719,6 +6809,15 @@ bool HistoryWidget::fieldHasSendText() const {
return !_field->isHidden() && HasSendText(_field);
}
bool HistoryWidget::hasSendableContent() const {
return fieldHasSendText() || shouldShowRichDraftPreview();
}
bool HistoryWidget::hideExtraButtons() const {
return _fieldCharsCountManager.isLimitExceeded()
|| shouldShowRichDraftPreview();
}
bool HistoryWidget::hasEnoughLinesForAi() const {
return _history
&& !_voiceRecordBar->isActive()
@@ -7119,6 +7218,35 @@ bool HistoryWidget::showSendMessageError(
withPaymentApproved);
}
bool HistoryWidget::showSendRichDraftError(
bool ignoreSlowmodeCountdown,
Fn<void(int starsApproved)> withPaymentApproved,
Api::SendOptions options) {
if (!_canSendMessages || !_history || !_peer) {
return false;
}
const auto topicRootId = resolveReplyToTopicRootId();
auto request = SendingErrorRequest{
.topicRootId = topicRootId,
.forward = &_forwardPanel->items(),
.messagesCount = 1,
.ignoreSlowmodeCountdown = ignoreSlowmodeCountdown,
.richMessage = true,
};
request.messagesCount = ComputeSendingMessagesCount(_history, request);
const auto error = GetErrorForSending(_peer, request);
if (error) {
Data::ShowSendErrorToast(controller(), _peer, error);
return true;
}
return withPaymentApproved
&& !checkSendPayment(
request.messagesCount,
options,
withPaymentApproved);
}
bool HistoryWidget::confirmSendingFiles(const QStringList &files) {
return confirmSendingFiles(files, QString());
}
@@ -8076,7 +8204,7 @@ void HistoryWidget::updateBotKeyboard(History *h, bool force) {
&& _canSendMessages
&& (wasVisible
|| (_replyTo && _replyEditMsg)
|| (!fieldHasSendText() && !kbWasHidden()))) {
|| (!hasSendableContent() && !kbWasHidden()))) {
if (!_showAnimation) {
if (hasMarkup) {
_kbScroll->show();
@@ -54,6 +54,10 @@ class Widget;
struct ResultSelected;
} // namespace InlineBots
namespace Iv {
struct RichPage;
} // namespace Iv
namespace Support {
class Autocomplete;
struct Contact;
@@ -448,6 +452,9 @@ private:
bool useWebPageDraft,
Api::SendOptions options,
Fn<void()> done);
void sendRichDraft(
std::shared_ptr<const Iv::RichPage> page,
Api::SendOptions options);
void sendVoice(const VoiceToSend &data);
void sendWithTextOverride(
TextWithEntities text,
@@ -521,6 +528,10 @@ private:
bool ignoreSlowmodeCountdown,
Fn<void(int starsApproved)> withPaymentApproved = nullptr,
Api::SendOptions options = {});
bool showSendRichDraftError(
bool ignoreSlowmodeCountdown,
Fn<void(int starsApproved)> withPaymentApproved = nullptr,
Api::SendOptions options = {});
void sendingFilesConfirmed(
std::shared_ptr<Ui::PreparedBundle> bundle,
@@ -674,12 +685,15 @@ private:
[[nodiscard]] int fieldHeight() const;
[[nodiscard]] bool fieldOrDisabledShown() const;
[[nodiscard]] bool fieldHasSendText() const;
[[nodiscard]] bool hasSendableContent() const;
[[nodiscard]] bool hideExtraButtons() const;
void unregisterDraftSources();
void registerDraftSource();
void unregisterThreadFieldBridge();
void registerThreadFieldBridge();
[[nodiscard]] Data::Draft *cloudDraft() const;
[[nodiscard]] std::shared_ptr<const Iv::RichPage> shownRichMessage() const;
[[nodiscard]] bool isComposeBoxOpen() const;
[[nodiscard]] bool bypassNormalDraftHandling() const;
[[nodiscard]] bool shouldShowRichDraftPreview() const;
@@ -2087,6 +2087,13 @@ Data::Draft *ComposeControls::cloudDraft() const {
: nullptr;
}
std::shared_ptr<const Iv::RichPage> ComposeControls::shownRichMessage() const {
if (const auto draft = shouldShowRichDraftPreview() ? cloudDraft() : nullptr) {
return draft->richMessage;
}
return nullptr;
}
bool ComposeControls::isComposeBoxOpen() const {
return _history
&& hasRichDraftThreadScope()
@@ -84,6 +84,10 @@ class Session;
struct SendAsKey;
} // namespace Main
namespace Iv {
struct RichPage;
} // namespace Iv
namespace Webrtc {
enum class RecordAvailability : uchar;
} // namespace Webrtc
@@ -259,6 +263,7 @@ public:
[[nodiscard]] TextWithTags getTextWithAppliedMarkdown() const;
[[nodiscard]] Data::WebPageDraft webPageDraft() const;
[[nodiscard]] std::shared_ptr<const Iv::RichPage> shownRichMessage() const;
void setText(const TextWithTags &text);
void clear();
void hidePanelsAnimated();
@@ -28,6 +28,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "history/history_drag_area.h"
#include "history/history_item_components.h"
#include "history/history_item_helpers.h" // GetErrorForSending.
#include "iv/iv_rich_message_serializer.h"
#include "ui/chat/pinned_bar.h"
#include "ui/chat/chat_style.h"
#include "ui/controls/swipe_handler.h"
@@ -1392,6 +1393,9 @@ Api::SendAction ChatWidget::prepareSendAction(
void ChatWidget::send() {
if (_composeControls->getTextWithAppliedMarkdown().text.isEmpty()) {
if (const auto page = _composeControls->shownRichMessage()) {
sendRichDraft(page, {});
}
return;
}
send({});
@@ -1425,6 +1429,10 @@ void ChatWidget::sendVoice(const ComposeControls::VoiceToSend &data) {
}
void ChatWidget::send(Api::SendOptions options) {
if (const auto page = _composeControls->shownRichMessage()) {
sendRichDraft(page, options);
return;
}
if (!options.scheduled && showSlowmodeError()) {
return;
}
@@ -1436,6 +1444,76 @@ void ChatWidget::send(Api::SendOptions options) {
nullptr);
}
void ChatWidget::sendRichDraft(
std::shared_ptr<const Iv::RichPage> page,
Api::SendOptions options) {
if (!page) {
return;
}
if (!options.scheduled) {
_cornerButtons.clearReplyReturns();
if (showSlowmodeError()) {
return;
}
}
auto request = SendingErrorRequest{
.topicRootId = _topic ? _topic->rootId() : MsgId(0),
.forward = &_composeControls->forwardItems(),
.messagesCount = 1,
.ignoreSlowmodeCountdown = (options.scheduled != 0),
.richMessage = true,
};
const auto error = GetErrorForSending(_peer, request);
if (error) {
Data::ShowSendErrorToast(controller(), _peer, error);
return;
}
const auto serialized = Iv::SerializeInputRichMessage(
&session(),
*page,
Iv::SerializeInputRichMessageMode::FinalSubmit);
if (serialized.status == Iv::SerializeInputRichMessageStatus::EmptyContent) {
controller()->showToast(tr::lng_article_submit_empty(tr::now));
return;
} else if (serialized.status != Iv::SerializeInputRichMessageStatus::Success
|| !serialized.value) {
controller()->showToast(tr::lng_attach_failed(tr::now));
return;
}
if (!options.scheduled) {
const auto withPaymentApproved = [=](int approved) {
auto copy = options;
copy.starsApproved = approved;
sendRichDraft(page, copy);
};
const auto checked = checkSendPayment(
1,
options,
withPaymentApproved);
if (!checked) {
return;
}
}
session().api().sendRichMessage(
page,
*serialized.value,
prepareSendAction(options));
_composeControls->clear();
_composeControls->applyCloudDraft();
if (_repliesRootId) {
session().sendProgressManager().update(
_history,
_repliesRootId,
Api::SendProgressType::Typing,
-1);
}
finishSending();
}
void ChatWidget::sendTextWithTags(
TextWithTags textWithTags,
bool useCurrentWebPageDraft,
@@ -51,6 +51,10 @@ namespace InlineBots {
class Result;
} // namespace InlineBots
namespace Iv {
struct RichPage;
} // namespace Iv
namespace Data {
class RepliesList;
class ForumTopic;
@@ -300,6 +304,9 @@ private:
bool useCurrentWebPageDraft,
Api::SendOptions options,
Fn<void()> done);
void sendRichDraft(
std::shared_ptr<const Iv::RichPage> page,
Api::SendOptions options);
void sendWithTextOverride(
TextWithEntities text,
Api::SendOptions options,
@@ -1387,6 +1387,30 @@ void PaintTableCaption(
&& (block.horizontalScrollLeft < block.horizontalScrollMax);
}
[[nodiscard]] QRect HorizontalOverflowContentClip(
const LaidOutBlock &block,
const style::Markdown &st,
QRect viewport) {
if (block.horizontalScrollMax <= 0 || viewport.isEmpty()) {
return viewport;
}
const auto indicatorWidth = std::min(
std::max(OverflowIndicatorWidth(block, st), 1),
viewport.width());
const auto left = (block.horizontalScrollLeft > 0)
? indicatorWidth
: 0;
const auto right = HorizontalRightEdgeHidden(block)
? indicatorWidth
: 0;
const auto width = std::max(viewport.width() - left - right, 0);
return QRect(
viewport.x() + left,
viewport.y(),
width,
viewport.height());
}
[[nodiscard]] QRect HorizontalScrollLogicalPaintRect(
const LaidOutBlock &block) {
if (block.insideHorizontalScroll) {
@@ -1509,7 +1533,9 @@ void PaintWholeTable(
if (tableClip.isEmpty()) {
return;
}
const auto tableContext = ClippedContext(context, tableClip);
const auto textClip = tableClip.intersected(
HorizontalOverflowContentClip(block, st, block.visibleTableRect));
const auto tableContext = ClippedContext(context, textClip);
const auto border = TableBorder(block, st);
const auto radius = st.table.radius;
@@ -1630,7 +1656,8 @@ void PaintTableRowBand(
const auto cells = TableCellsForRowBand(ownership, rowIndex, rowBand);
const auto rowContext = ClippedContext(
RevealSuppressedContext(context),
rowClip);
rowClip.intersected(
HorizontalOverflowContentClip(block, st, block.visibleTableRect)));
p.save();
p.setClipRect(rowClip, Qt::IntersectClip);