Enable rich editing for template messages

Task: 2026/07/23/enable-rich-template-message-editing
This commit is contained in:
John Preston
2026-07-23 13:20:08 +04:00
parent 0ecd67331e
commit 0baaef6b0b
9 changed files with 284 additions and 28 deletions

View File

@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/boxes/confirm_box.h"
#include "data/business/data_shortcut_messages.h"
#include "data/components/scheduled_messages.h"
#include "data/components/welcome_messages.h"
#include "data/data_file_origin.h"
#include "data/data_histories.h"
#include "data/data_saved_sublist.h"
@@ -534,6 +535,25 @@ mtpRequestId EditTextMessage(
Fn<void(const QString &error, mtpRequestId requestId)> fail,
bool spoilered,
VideoCoverEdit videoCover) {
if (item->isWelcomeTemplate()) {
const auto history = item->history();
auto &welcome = history->session().welcomeMessages();
welcome.edit(
history,
welcome.lookupId(item),
caption,
[=] {
if (done) {
done(0);
}
},
[=](const QString &error) {
if (fail) {
fail(error, 0);
}
});
return 0;
}
const auto media = item->media();
const auto spoilerChanged = media
&& HistoryView::MediaEditManager::CanBeSpoilered(item)
@@ -686,6 +706,25 @@ mtpRequestId EditRichMessage(
SendOptions options,
Fn<void(mtpRequestId requestId)> done,
Fn<void(const QString &error, mtpRequestId requestId)> fail) {
if (item->isWelcomeTemplate()) {
const auto history = item->history();
auto &welcome = history->session().welcomeMessages();
welcome.editRich(
history,
welcome.lookupId(item),
std::move(richMessage),
[=] {
if (done) {
done(0);
}
},
[=](const QString &error) {
if (fail) {
fail(error, 0);
}
});
return 0;
}
const auto session = &item->history()->session();
const auto api = &session->api();
const auto sentEntities = MTPVector<MTPMessageEntity>();

View File

@@ -15,6 +15,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_session.h"
#include "history/history.h"
#include "history/history_item_edition.h"
#include "iv/iv_rich_page.h"
#include "main/main_app_config.h"
#include "main/main_session.h"
@@ -181,6 +182,54 @@ void WelcomeMessages::send(
}).send();
}
void WelcomeMessages::sendRich(
not_null<History*> history,
Fn<std::optional<MTPInputRichMessage>()> richMessage) {
auto first = richMessage
? richMessage()
: std::optional<MTPInputRichMessage>();
if (!first) {
LOG(("API Error: no rich welcome template to send."));
return;
}
using Flag = MTPephemeral_SendMessage::Flag;
const auto flags = Flag::f_welcome_template | Flag::f_rich_message;
const auto randomId = base::RandomValue<uint64>();
const auto send = [=](
const auto &send,
const MTPInputRichMessage &rich,
int attempt) -> void {
_session->api().request(MTPephemeral_SendMessage(
MTP_flags(flags),
history->peer->input(),
MTP_inputUserEmpty(),
MTPlong(), // query_id
MTP_string(),
MTPVector<MTPMessageEntity>(),
MTPInputMedia(),
MTPReplyMarkup(),
rich,
MTP_long(randomId),
MTPInputReplyTo()
)).done([=](const MTPUpdates &result) {
_session->api().applyUpdates(result);
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
if (!attempt
&& (error.code() == 400)
&& type.startsWith(u"FILE_REFERENCE_"_q)) {
if (auto rebuilt = richMessage()) {
send(send, *rebuilt, 1);
return;
}
}
LOG(("API Error: send rich welcome template - %1"
).arg(type));
}).send();
};
send(send, *first, 0);
}
void WelcomeMessages::edit(
not_null<History*> history,
int32 ephemeralId,
@@ -217,6 +266,60 @@ void WelcomeMessages::edit(
}).send();
}
void WelcomeMessages::editRich(
not_null<History*> history,
int32 ephemeralId,
Fn<std::optional<MTPInputRichMessage>()> richMessage,
Fn<void()> done,
Fn<void(const QString &)> fail) {
auto first = richMessage
? richMessage()
: std::optional<MTPInputRichMessage>();
if (!first) {
if (fail) {
fail(QString());
}
return;
}
using Flag = MTPephemeral_EditMessage::Flag;
const auto flags = Flag::f_welcome_template | Flag::f_rich_message;
const auto send = [=](
const auto &send,
const MTPInputRichMessage &rich,
int attempt) -> void {
_session->api().request(MTPephemeral_EditMessage(
MTP_flags(flags),
history->peer->input(),
MTP_inputUserEmpty(),
MTP_int(ephemeralId),
MTPstring(),
MTPInputMedia(),
MTPVector<MTPMessageEntity>(),
MTPReplyMarkup(),
rich
)).done([=](const MTPUpdates &result) {
_session->api().applyUpdates(result);
if (done) {
done();
}
}).fail([=](const MTP::Error &error) {
const auto type = error.type();
if (!attempt
&& (error.code() == 400)
&& type.startsWith(u"FILE_REFERENCE_"_q)) {
if (auto rebuilt = richMessage()) {
send(send, *rebuilt, 1);
return;
}
}
if (fail) {
fail(type);
}
}).send();
};
send(send, *first, 0);
}
void WelcomeMessages::deleteTemplate(not_null<HistoryItem*> item) {
const auto history = item->history();
const auto id = lookupId(item);
@@ -368,6 +471,9 @@ HistoryItem *WelcomeMessages::append(
| (data.is_out()
? MessageFlag::Outgoing
: MessageFlag())
| (data.is_invert_media()
? MessageFlag::InvertMedia
: MessageFlag())
| (fromId ? MessageFlag::HasFromId : MessageFlag())
| (data.vreply_markup()
? MessageFlag::HasReplyMarkup
@@ -385,6 +491,10 @@ HistoryItem *WelcomeMessages::append(
(data.vmedia()
? *data.vmedia()
: MTPMessageMedia(MTP_messageMediaEmpty())));
if (const auto richMessage = data.vrich_message()) {
item->applyLocalRichPage(
Iv::ParseRichPage(_session, *richMessage));
}
list.items.emplace_back(item);
list.itemById.emplace(id, item);
return item;
@@ -409,6 +519,10 @@ void WelcomeMessages::applyEdition(
};
edition.replyMarkup = HistoryMessageMarkupData(data.vreply_markup());
edition.mtpMedia = data.vmedia();
edition.invertMedia = data.is_invert_media();
if (const auto richMessage = data.vrich_message()) {
edition.richPage = Iv::ParseRichPage(_session, *richMessage);
}
item->applyEdition(std::move(edition));
}

View File

@@ -44,12 +44,21 @@ public:
bool applyDelete(not_null<PeerData*> peer, int32 ephemeralId);
void send(not_null<History*> history, TextWithEntities text);
void sendRich(
not_null<History*> history,
Fn<std::optional<MTPInputRichMessage>()> richMessage);
void edit(
not_null<History*> history,
int32 ephemeralId,
TextWithEntities text,
Fn<void()> done,
Fn<void(const QString &)> fail);
void editRich(
not_null<History*> history,
int32 ephemeralId,
Fn<std::optional<MTPInputRichMessage>()> richMessage,
Fn<void()> done,
Fn<void(const QString &)> fail);
void deleteTemplate(not_null<HistoryItem*> item);
void deleteAll(not_null<History*> history);

View File

@@ -1446,17 +1446,10 @@ void HistoryWidget::showRichEditor() {
}
return;
}
auto action = prepareSendAction({});
if (ShowEphemeralReplyTextOnlyError(
window->uiShow(),
&session(),
action.replyTo.messageId)) {
return;
}
Iv::Editor::ShowComposeBox(
window,
_history->peer,
std::move(action),
prepareSendAction({}),
sendMenuDetails(),
_field->getTextWithAppliedMarkdown(),
crl::guard(this, [=] {

View File

@@ -67,7 +67,6 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "ui/power_saving.h"
#include "history/history.h"
#include "history/history_item.h"
#include "history/history_item_helpers.h"
#include "history/view/controls/history_view_characters_limit.h"
#include "history/view/controls/history_view_compose_ai_button.h"
#include "history/view/controls/history_view_compose_ai_tooltip.h"
@@ -2162,6 +2161,15 @@ bool ComposeControls::isShortcutComposeEligible() const {
&& (_shortcutId > 0);
}
bool ComposeControls::isWelcomeComposeEligible() const {
return _features.richEditor
&& _history
&& !isEditingMessage()
&& (_mode == Mode::Normal)
&& (_currentDialogsEntryState.section
== Dialogs::EntryState::Section::WelcomeMessages);
}
bool ComposeControls::hasEditDraft() const {
return _history
&& (_history->draft(draftKey(DraftType::Edit)) != nullptr);
@@ -2257,6 +2265,16 @@ void ComposeControls::migrateShortcutFieldToRichEditor(
_history->clearDraft(Data::DraftKey::Shortcut(expectedShortcutId));
}
void ComposeControls::migrateWelcomeFieldToRichEditor() {
if (!isWelcomeComposeEligible()) {
return;
}
cancelPendingDraftSaves();
clearFieldText();
_history->clearDraft(Data::DraftKey::WelcomeMessages());
}
void ComposeControls::clearFieldText(
TextUpdateEvents events,
FieldHistoryAction fieldHistoryAction) {
@@ -3872,22 +3890,12 @@ void ComposeControls::showRichEditor() {
}
return;
}
const auto ephemeralError = [&](const Api::SendAction &action) {
return ShowEphemeralReplyTextOnlyError(
_show,
&_show->session(),
action.replyTo.messageId);
};
if (_mode == Mode::Scheduled) {
auto action = _sendActionFactory();
if (ephemeralError(action)) {
return;
}
using Options = Iv::Editor::ComposeBoxOptions;
Iv::Editor::ShowComposeBox(
_regularWindow,
_history->peer,
std::move(action),
_sendActionFactory(),
sendMenuDetails(),
getTextWithAppliedMarkdown(),
crl::guard(_wrap.get(), [=] {
@@ -3916,9 +3924,6 @@ void ComposeControls::showRichEditor() {
|| action.options.shortcutId != expectedShortcutId) {
return;
}
if (ephemeralError(action)) {
return;
}
auto fieldText = getTextWithAppliedMarkdown();
using Options = Iv::Editor::ComposeBoxOptions;
Iv::Editor::ShowComposeBox(
@@ -3943,17 +3948,41 @@ void ComposeControls::showRichEditor() {
});
return;
}
if (_mode != Mode::Normal || !hasRichDraftThreadScope()) {
if (_currentDialogsEntryState.section
== Dialogs::EntryState::Section::WelcomeMessages) {
if (!isWelcomeComposeEligible()) {
return;
}
using Options = Iv::Editor::ComposeBoxOptions;
Iv::Editor::ShowComposeBox(
_regularWindow,
_history->peer,
_sendActionFactory(),
sendMenuDetails(),
getTextWithAppliedMarkdown(),
crl::guard(_wrap.get(), [=] {
migrateWelcomeFieldToRichEditor();
}),
Options{
.scope = Options::Scope::Detached,
.returnText = crl::guard(
_wrap.get(),
[=](TextWithTags text) {
if (isWelcomeComposeEligible()) {
setText(text);
}
}),
.welcomeTemplates = true,
});
return;
}
auto action = _sendActionFactory();
if (ephemeralError(action)) {
if (_mode != Mode::Normal || !hasRichDraftThreadScope()) {
return;
}
Iv::Editor::ShowComposeBox(
_regularWindow,
_history->peer,
std::move(action),
_sendActionFactory(),
sendMenuDetails(),
getTextWithAppliedMarkdown(),
crl::guard(_wrap.get(), [=] {
@@ -4334,7 +4363,8 @@ bool ComposeControls::canShowRichEditor() const {
const auto media = item ? item->media() : nullptr;
const auto composeEligible = (_mode == Mode::Scheduled)
|| ((_mode == Mode::Normal) && hasRichDraftThreadScope())
|| isShortcutComposeEligible();
|| isShortcutComposeEligible()
|| isWelcomeComposeEligible();
return _history
&& _regularWindow
&& _sendActionFactory

View File

@@ -453,6 +453,7 @@ private:
[[nodiscard]] bool isComposeBoxOpen() const;
[[nodiscard]] bool hasRichDraftThreadScope() const;
[[nodiscard]] bool isShortcutComposeEligible() const;
[[nodiscard]] bool isWelcomeComposeEligible() const;
[[nodiscard]] bool bypassNormalDraftHandling() const;
[[nodiscard]] bool hasEditDraft() const;
[[nodiscard]] bool shouldShowRichDraftPreview() const;
@@ -461,6 +462,7 @@ private:
void migrateScheduledFieldToRichEditor();
void migrateShortcutFieldToRichEditor(
BusinessShortcutId expectedShortcutId);
void migrateWelcomeFieldToRichEditor();
const style::ComposeControls &_st;
ChatHelpers::ComposeFeatures _features;

View File

@@ -204,6 +204,7 @@ void WelcomeMessagesWidget::setupComposeControls() {
});
_composeControls->setHistory({
.history = _history.get(),
.sendActionFactory = [=] { return Api::SendAction(_history); },
.writeRestriction = std::move(writeRestriction),
});

View File

@@ -30,6 +30,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "data/data_file_origin.h"
#include "core/shortcuts.h"
#include "data/components/ephemeral_messages.h"
#include "data/components/welcome_messages.h"
#include "data/data_changes.h"
#include "data/data_drafts.h"
#include "data/data_document.h"
@@ -905,6 +906,27 @@ private:
== ComposeBoxOptions::Scope::Detached;
}
[[nodiscard]] bool welcomeTemplatesCompose() const {
return (_mode == Mode::Compose)
&& _composeOptions.welcomeTemplates
&& _composeAction;
}
[[nodiscard]] bool welcomeTemplatesLimitReached() const {
Expects(_composeAction.has_value());
const auto history = _composeAction->history;
return _session->welcomeMessages().count(history)
>= ::Data::WelcomeMessagesLimit(_session);
}
void showWelcomeTemplatesLimitToast() const {
showToast(tr::lng_business_limit_reached(
tr::now,
lt_count,
::Data::WelcomeMessagesLimit(_session)));
}
void dropDetachedReturnText() {
if (detachedCompose()) {
(void)base::take(_composeOptions.returnText);
@@ -991,6 +1013,7 @@ private:
if (_mode != Mode::Compose
|| !_composeAction
|| _submitOptions.scheduled
|| welcomeTemplatesCompose()
|| submitWouldBeEphemeral(simple)) {
return true;
}
@@ -1065,6 +1088,10 @@ private:
showRichMessageLimitToast(*error);
return false;
}
if (welcomeTemplatesCompose() && welcomeTemplatesLimitReached()) {
showWelcomeTemplatesLimitToast();
return false;
}
_submittedPage = page;
if (submittedAttachmentsReady()
&& serializeSubmittedPage().status
@@ -1095,6 +1122,18 @@ private:
showToast(tr::lng_edit_error(tr::now));
return false;
}
if (welcomeTemplatesCompose()) {
if (welcomeTemplatesLimitReached()) {
showWelcomeTemplatesLimitToast();
return false;
}
cancelRichDraftAutosave();
dropDetachedReturnText();
_session->welcomeMessages().send(
_composeAction->history,
std::move(text));
return true;
}
auto action = *_composeAction;
action.options = _submitOptions;
action.clearDraft = !detachedCompose();
@@ -1269,6 +1308,9 @@ private:
[[nodiscard]] bool applySubmittedLocalState(
const std::shared_ptr<const RichPage> &page) {
if (welcomeTemplatesCompose()) {
return true;
}
const auto item = (_mode == Mode::Compose)
? ensureComposeLocalItem()
: currentSubmittedItem();
@@ -1520,6 +1562,31 @@ private:
failSubmittedWork(true);
return;
}
if (welcomeTemplatesCompose()) {
if (welcomeTemplatesLimitReached()) {
showWelcomeTemplatesLimitToast();
finishSubmittedWork();
restartRichDraftAutosave();
return;
}
dropDetachedReturnText();
const auto session = _session;
_session->welcomeMessages().sendRich(
_composeAction->history,
[session, page = _submittedPage] {
auto serialized = SerializeInputRichMessage(
session,
*page,
SerializeInputRichMessageMode::FinalSubmit);
const auto success = (serialized.status
== SerializeInputRichMessageStatus::Success);
return (success && serialized.value)
? std::move(serialized.value)
: std::optional<MTPInputRichMessage>();
});
finishSubmittedWork();
return;
}
const auto item = currentSubmittedItem();
if (!item) {
finishSubmittedWork();

View File

@@ -60,6 +60,7 @@ struct ComposeBoxOptions {
Scope scope = Scope::Thread;
SubmitPolicy submitPolicy = SubmitPolicy::Immediate;
Fn<void(TextWithTags)> returnText;
bool welcomeTemplates = false;
};
[[nodiscard]] std::shared_ptr<ChatHelpers::Show> ActiveWindowShow(