diff --git a/Telegram/CMakeLists.txt b/Telegram/CMakeLists.txt index cc668aa910..a8f126cee7 100644 --- a/Telegram/CMakeLists.txt +++ b/Telegram/CMakeLists.txt @@ -1298,10 +1298,14 @@ PRIVATE intro/intro_widget.h iv/iv_cached_media.cpp iv/iv_cached_media.h + iv/editor/iv_editor_session.cpp + iv/editor/iv_editor_session.h iv/iv_delegate_impl.cpp iv/iv_delegate_impl.h iv/iv_instance.cpp iv/iv_instance.h + iv/iv_rich_message_serializer.cpp + iv/iv_rich_message_serializer.h iv/iv_rich_page.cpp iv/iv_rich_page.h lang/lang_cloud_manager.cpp diff --git a/Telegram/SourceFiles/api/api_editing.cpp b/Telegram/SourceFiles/api/api_editing.cpp index 5f7e526727..25090a4be7 100644 --- a/Telegram/SourceFiles/api/api_editing.cpp +++ b/Telegram/SourceFiles/api/api_editing.cpp @@ -49,6 +49,55 @@ template constexpr auto ErrorWithoutId = is_callable_plain_v; +[[nodiscard]] auto ComputeEditMessageFlags( + not_null item, + const MTPVector &sentEntities, + Data::WebPageDraft webpage, + SendOptions options, + bool withMessage, + bool withMedia, + bool withRichMessage) +-> MTPmessages_EditMessage::Flags { + const auto emptyFlag = MTPmessages_EditMessage::Flag(0); + return emptyFlag + | (withMessage + ? MTPmessages_EditMessage::Flag::f_message + : emptyFlag) + | (withMedia + ? MTPmessages_EditMessage::Flag::f_media + : emptyFlag) + | (webpage.removed + ? MTPmessages_EditMessage::Flag::f_no_webpage + : emptyFlag) + | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert) + || options.invertCaption) + ? MTPmessages_EditMessage::Flag::f_invert_media + : emptyFlag) + | (!sentEntities.v.isEmpty() + ? MTPmessages_EditMessage::Flag::f_entities + : emptyFlag) + | (options.scheduled + ? MTPmessages_EditMessage::Flag::f_schedule_date + : emptyFlag) + | ((options.scheduled && options.scheduleRepeatPeriod) + ? MTPmessages_EditMessage::Flag::f_schedule_repeat_period + : emptyFlag) + | (item->isBusinessShortcut() + ? MTPmessages_EditMessage::Flag::f_quick_reply_shortcut_id + : emptyFlag) + | (withRichMessage + ? MTPmessages_EditMessage::Flag::f_rich_message + : emptyFlag); +} + +[[nodiscard]] MsgId EditMessageRequestId(not_null item) { + return item->isScheduled() + ? item->history()->session().scheduledMessages().lookupId(item) + : item->isBusinessShortcut() + ? item->history()->session().data().shortcutMessages().lookupId(item) + : item->id; +} + template mtpRequestId SuggestMessage( not_null item, @@ -273,42 +322,17 @@ mtpRequestId EditMessage( ? Api::HasAttachedStickers(*inputMedia) : false; - const auto emptyFlag = MTPmessages_EditMessage::Flag(0); - const auto flags = emptyFlag - | ((!text.isEmpty() || media) - ? MTPmessages_EditMessage::Flag::f_message - : emptyFlag) - | ((media && inputMedia.has_value()) - ? MTPmessages_EditMessage::Flag::f_media - : emptyFlag) - | (webpage.removed - ? MTPmessages_EditMessage::Flag::f_no_webpage - : emptyFlag) - | ((!webpage.removed && !webpage.url.isEmpty()) - ? MTPmessages_EditMessage::Flag::f_media - : emptyFlag) - | (((!webpage.removed && !webpage.url.isEmpty() && webpage.invert) - || options.invertCaption) - ? MTPmessages_EditMessage::Flag::f_invert_media - : emptyFlag) - | (!sentEntities.v.isEmpty() - ? MTPmessages_EditMessage::Flag::f_entities - : emptyFlag) - | (options.scheduled - ? MTPmessages_EditMessage::Flag::f_schedule_date - : emptyFlag) - | ((options.scheduled && options.scheduleRepeatPeriod) - ? MTPmessages_EditMessage::Flag::f_schedule_repeat_period - : emptyFlag) - | (item->isBusinessShortcut() - ? MTPmessages_EditMessage::Flag::f_quick_reply_shortcut_id - : emptyFlag); + const auto flags = ComputeEditMessageFlags( + item, + sentEntities, + webpage, + options, + (!text.isEmpty() || media), + ((media && inputMedia.has_value()) + || (!webpage.removed && !webpage.url.isEmpty())), + false); - const auto id = item->isScheduled() - ? session->scheduledMessages().lookupId(item) - : item->isBusinessShortcut() - ? session->data().shortcutMessages().lookupId(item) - : item->id; + const auto id = EditMessageRequestId(item); return api->request(MTPmessages_EditMessage( MTP_flags(flags), item->history()->peer->input(), @@ -569,6 +593,70 @@ mtpRequestId EditTextMessage( std::nullopt); } +mtpRequestId EditRichMessage( + not_null item, + Fn()> richMessage, + SendOptions options, + Fn done, + Fn fail) { + const auto session = &item->history()->session(); + const auto api = &session->api(); + const auto sentEntities = MTPVector(); + const auto flags = ComputeEditMessageFlags( + item, + sentEntities, + Data::WebPageDraft(), + options, + false, + false, + true); + const auto id = EditMessageRequestId(item); + const auto origin = item->fullId(); + const auto performRequest = [=]( + const auto &repeatRequest, + mtpRequestId originalRequestId, + bool refreshed) -> mtpRequestId { + const auto current = richMessage ? richMessage() : std::nullopt; + const auto requestId = originalRequestId ? originalRequestId : 0; + if (!current) { + if (fail) { + fail(QString(), requestId); + } + return requestId; + } + return api->request(MTPmessages_EditMessage( + MTP_flags(flags), + item->history()->peer->input(), + MTP_int(id), + MTPstring(), + MTPInputMedia(), + MTPReplyMarkup(), + sentEntities, + MTP_int(options.scheduled), + MTP_int(options.scheduleRepeatPeriod), + MTP_int(item->shortcutId()), + *current + )).done([=](const MTPUpdates &result, mtpRequestId requestId) { + api->applyUpdates(result); + if (done) { + done(originalRequestId ? originalRequestId : requestId); + } + }).fail([=](const MTP::Error &error, mtpRequestId requestId) { + if (!refreshed && error.type().startsWith(u"FILE_REFERENCE_"_q)) { + api->refreshFileReference(origin, [=](const auto &) { + repeatRequest( + repeatRequest, + originalRequestId ? originalRequestId : requestId, + true); + }); + } else if (fail) { + fail(error.type(), originalRequestId ? originalRequestId : requestId); + } + }).send(); + }; + return performRequest(performRequest, 0, false); +} + void EditTodoList( not_null item, const TodoListData &data, diff --git a/Telegram/SourceFiles/api/api_editing.h b/Telegram/SourceFiles/api/api_editing.h index ca3ff7c121..b551acab41 100644 --- a/Telegram/SourceFiles/api/api_editing.h +++ b/Telegram/SourceFiles/api/api_editing.h @@ -7,6 +7,8 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include + class HistoryItem; namespace Data { @@ -57,6 +59,12 @@ mtpRequestId EditTextMessage( Fn done, Fn fail, bool spoilered); +mtpRequestId EditRichMessage( + not_null item, + Fn()> richMessage, + SendOptions options, + Fn done, + Fn fail); void EditTodoList( not_null item, diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index d9e595eedb..22b78da9b5 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -4075,6 +4075,121 @@ void ApiWrap::sendShortcutMessages( }).send(); } +void ApiWrap::sendRichMessage( + not_null item, + const MTPInputRichMessage &richMessage, + SendAction action) { + Expects(item->history() == action.history); + + const auto history = item->history(); + const auto peer = history->peer; + action.generateLocal = true; + sendAction(action); + + const auto clearCloudDraft = action.clearDraft; + const auto draftTopicRootId = action.replyTo.topicRootId; + const auto draftMonoforumPeerId = action.replyTo.monoforumPeerId; + const auto randomId = base::RandomValue(); + auto starsPaid = std::min( + peer->starsPerMessageChecked(), + action.options.starsApproved); + if (starsPaid) { + action.options.starsApproved -= starsPaid; + } + _session->data().registerMessageRandomId(randomId, item->fullId()); + _session->data().registerMessageSentData( + randomId, + peer->id, + item->originalText().text); + + using Flag = MTPmessages_SendMessage::Flag; + auto sendFlags = MTPmessages_SendMessage::Flags(0) + | Flag::f_rich_message; + if (action.replyTo) { + sendFlags |= Flag::f_reply_to; + } + if (ShouldSendSilent(peer, action.options)) { + sendFlags |= Flag::f_silent; + } + if (clearCloudDraft) { + sendFlags |= Flag::f_clear_draft; + history->clearCloudDraft(draftTopicRootId, draftMonoforumPeerId); + history->startSavingCloudDraft( + draftTopicRootId, + draftMonoforumPeerId); + } + if (const auto sendAs = action.options.sendAs) { + sendFlags |= Flag::f_send_as; + } + if (action.options.scheduled) { + sendFlags |= Flag::f_schedule_date; + if (action.options.scheduleRepeatPeriod) { + sendFlags |= Flag::f_schedule_repeat_period; + } + } + if (action.options.shortcutId) { + sendFlags |= Flag::f_quick_reply_shortcut; + } + if (action.options.effectId) { + sendFlags |= Flag::f_effect; + } + if (action.options.suggest) { + sendFlags |= Flag::f_suggested_post; + } + if (starsPaid) { + sendFlags |= Flag::f_allow_paid_stars; + } + const auto done = [=]( + const MTPUpdates &result, + const MTP::Response &response) { + if (clearCloudDraft) { + history->finishSavingCloudDraft( + draftTopicRootId, + draftMonoforumPeerId, + Api::UnixtimeFromMsgId(response.outerMsgId)); + } + }; + const auto fail = [=]( + const MTP::Error &error, + const MTP::Response &response) { + sendMessageFail(error, peer, randomId, item->fullId()); + if (clearCloudDraft) { + history->finishSavingCloudDraft( + draftTopicRootId, + draftMonoforumPeerId, + Api::UnixtimeFromMsgId(response.outerMsgId)); + } + }; + const auto mtpShortcut = Data::ShortcutIdToMTP( + _session, + action.options.shortcutId); + history->owner().histories().sendPreparedMessage( + history, + action.replyTo, + randomId, + Data::Histories::PrepareMessage( + MTP_flags(sendFlags), + peer->input(), + Data::Histories::ReplyToPlaceholder(), + MTP_string(QString()), + MTP_long(randomId), + MTPReplyMarkup(), + MTPVector(), + MTP_int(action.options.scheduled), + MTP_int(action.options.scheduleRepeatPeriod), + (action.options.sendAs + ? action.options.sendAs->input() + : MTP_inputPeerEmpty()), + mtpShortcut, + MTP_long(action.options.effectId), + MTP_long(starsPaid), + Api::SuggestToMTP(action.options.suggest), + richMessage), + done, + fail); + finishForwarding(action); +} + void ApiWrap::sendMessage( MessageToSend &&message, std::optional localMessageId) { diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 4c577c37be..a1609c7a62 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -14,6 +14,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_messages.h" class TaskQueue; +class HistoryItem; struct MessageGroupId; struct SendingAlbum; enum class SendMediaType; @@ -378,6 +379,10 @@ public: void sendShortcutMessages( not_null peer, BusinessShortcutId id); + void sendRichMessage( + not_null item, + const MTPInputRichMessage &richMessage, + SendAction action); void sendMessage( MessageToSend &&message, std::optional localMessageId = std::nullopt); diff --git a/Telegram/SourceFiles/history/history_item.cpp b/Telegram/SourceFiles/history/history_item.cpp index 02ed136f53..6875d86ca2 100644 --- a/Telegram/SourceFiles/history/history_item.cpp +++ b/Telegram/SourceFiles/history/history_item.cpp @@ -21,6 +21,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/history_unread_things.h" #include "history/history.h" #include "iv/iv_data.h" +#include "iv/editor/iv_editor_state.h" #include "iv/iv_rich_page.h" #include "mtproto/mtproto_config.h" #include "ui/text/format_values.h" @@ -2922,10 +2923,11 @@ bool HistoryItem::isTooOldForEdit(TimeId now) const { } bool HistoryItem::allowsEdit(TimeId now) const { + const auto richPageSource = Get(); return !isService() && canBeEdited() && !isTooOldForEdit(now) - && !richPage() + && (!richPageSource || richPageSource->canEdit) && (!_media || _media->allowsEdit()) && !isLegacyMessage() && !isEditingMedia() @@ -4136,10 +4138,32 @@ std::shared_ptr HistoryItem::richPage() const { return source ? source->page : nullptr; } +void HistoryItem::applyLocalRichPage(std::shared_ptr page) { + const auto summary = page + ? Iv::FlattenRichPageSummary(page) + : TextWithEntities(); + applyLocalRichPage(std::move(page), summary); +} + +void HistoryItem::applyLocalRichPage( + std::shared_ptr page, + const TextWithEntities &summary) { + if (page) { + setRichPage(std::move(page)); + } else { + clearRichPage(); + } + setText(summary); + _history->owner().requestItemTextRefresh(this); + invalidateChatListEntry(); +} + void HistoryItem::setRichPage(std::shared_ptr page) { if (page) { AddComponents(HistoryMessageRichPageSource::Bit()); - Get()->page = std::move(page); + const auto source = Get(); + source->page = std::move(page); + source->canEdit = Iv::Editor::CanEditRichPage(source->page); } else { clearRichPage(); } diff --git a/Telegram/SourceFiles/history/history_item.h b/Telegram/SourceFiles/history/history_item.h index 2adce81651..bdc93a9511 100644 --- a/Telegram/SourceFiles/history/history_item.h +++ b/Telegram/SourceFiles/history/history_item.h @@ -541,6 +541,10 @@ public: [[nodiscard]] std::shared_ptr richPage() const; [[nodiscard]] bool computeDropForwardedInfo() const; void setText(TextWithEntities textWithEntities); + void applyLocalRichPage(std::shared_ptr page); + void applyLocalRichPage( + std::shared_ptr page, + const TextWithEntities &summary); void setRichPage(std::shared_ptr page); void clearRichPage(); diff --git a/Telegram/SourceFiles/history/history_item_components.h b/Telegram/SourceFiles/history/history_item_components.h index f50ba94add..aa2e0406ed 100644 --- a/Telegram/SourceFiles/history/history_item_components.h +++ b/Telegram/SourceFiles/history/history_item_components.h @@ -146,6 +146,7 @@ struct HistoryMessageMediaForInstantView struct HistoryMessageRichPageSource : RuntimeComponent { std::shared_ptr page; + bool canEdit = false; }; class HiddenSenderInfo { diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index bb17ed2e3f..cd28f110ba 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -139,6 +139,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "history/view/history_view_subsection_tabs.h" #include "history/view/history_view_translate_bar.h" #include "history/view/media/history_view_media.h" +#include "iv/editor/iv_editor_session.h" #include "core/click_handler_types.h" #include "chat_helpers/field_autocomplete.h" #include "chat_helpers/tabbed_panel.h" @@ -5183,7 +5184,7 @@ SendMenu::Details HistoryWidget::sendMenuDetails() const { } SendMenu::Details HistoryWidget::saveMenuDetails() const { - return (_editMsgId && _replyEditMsg) + return (_editMsgId && _replyEditMsg && !_replyEditMsg->richPage()) ? _mediaEditManager.sendMenuDetails(HasSendText(_field)) : SendMenu::Details(); } @@ -9259,7 +9260,7 @@ void HistoryWidget::editMessage( not_null item, const TextSelection &selection) { if (item->richPage()) { - controller()->showToast(tr::lng_edit_error(tr::now)); + Iv::Editor::ShowEditBox(controller(), item); return; } else if (_chooseTheme) { toggleChooseChatTheme(_peer); @@ -9869,14 +9870,19 @@ void HistoryWidget::updateReplyEditTexts(bool force) { } } if (_replyEditMsg) { + const auto richPage = _replyEditMsg->richPage(); const auto editMedia = _editMsgId ? _replyEditMsg->media() : nullptr; - if (_editMsgId && _replyEditMsg) { + if (_editMsgId && _replyEditMsg && !richPage) { _mediaEditManager.start(_replyEditMsg); + } else { + _mediaEditManager.cancel(); } - _canReplaceMedia = _editMsgId && _replyEditMsg->allowsEditMedia(); - if (editMedia && editMedia->allowsEditMedia()) { + _canReplaceMedia = _editMsgId + && !richPage + && _replyEditMsg->allowsEditMedia(); + if (_canReplaceMedia && editMedia && editMedia->allowsEditMedia()) { _canAddMedia = false; } else { _canAddMedia = base::take(_canReplaceMedia); 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 cb0ba85907..cf3e0aa4fa 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -78,6 +78,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "inline_bots/bot_attach_web_view.h" #include "inline_bots/inline_results_widget.h" #include "inline_bots/inline_bot_result.h" +#include "iv/editor/iv_editor_session.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" #include "main/main_session.h" @@ -831,8 +832,11 @@ void FieldHeader::editMessage( _editMsgId = id; if (!id) { _mediaEditManager.cancel(); - } else if (const auto item = _show->session().data().message(id)) { + } else if (const auto item = _show->session().data().message(id); + item && !item->richPage()) { _mediaEditManager.start(item); + } else { + _mediaEditManager.cancel(); } if (!photoEditAllowed) { _inPhotoEdit = false; @@ -916,7 +920,10 @@ MessageToEdit FieldHeader::queryToEdit() { } SendMenu::Details FieldHeader::saveMenuDetails(bool hasSendText) const { + const auto item = _data->message(_editMsgId.current()); return isEditingMessage() + && item + && !item->richPage() ? _mediaEditManager.sendMenuDetails(hasSendText) : SendMenu::Details(); } @@ -2762,9 +2769,10 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { if (draft == editDraft) { const auto resolve = [=] { if (const auto item = _history->owner().message(editingId)) { + const auto richPage = item->richPage(); const auto media = item->media(); - _canReplaceMedia = item->allowsEditMedia(); - if (media && media->allowsEditMedia()) { + _canReplaceMedia = !richPage && item->allowsEditMedia(); + if (_canReplaceMedia && media && media->allowsEditMedia()) { _canAddMedia = false; } else { _canAddMedia = base::take(_canReplaceMedia); @@ -2775,6 +2783,7 @@ void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { } _photoEditMedia = (_canReplaceMedia && _regularWindow + && media && media->photo() && !media->photo()->isNull()) ? media->photo()->createMediaView() @@ -4082,7 +4091,9 @@ void ComposeControls::editMessage( const TextSelection &selection) { if (const auto item = session().data().message(id)) { editMessage(item); - SelectTextInFieldWithMargins(_field, selection); + if (!item->richPage()) { + SelectTextInFieldWithMargins(_field, selection); + } } } @@ -4091,7 +4102,11 @@ void ComposeControls::editMessage(not_null item) { Expects(draftKeyCurrent() != Data::DraftKey::None()); if (item->richPage()) { - _show->showToast(tr::lng_edit_error(tr::now)); + if (_regularWindow) { + Iv::Editor::ShowEditBox(_regularWindow, item); + } else { + _show->showToast(tr::lng_edit_error(tr::now)); + } return; } else if (_voiceRecordBar->isActive()) { _show->showBox(Ui::MakeInformBox(tr::lng_edit_caption_voice())); diff --git a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp index 19307a5c96..0a3b28b7e6 100644 --- a/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp +++ b/Telegram/SourceFiles/inline_bots/bot_attach_web_view.cpp @@ -52,7 +52,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "inline_bots/inline_bot_confirm_prepared.h" #include "inline_bots/inline_bot_downloads.h" #include "inline_bots/inline_bot_storage.h" -#include "iv/iv_editor_box.h" +#include "iv/editor/iv_editor_session.h" #include "iv/iv_instance.h" #include "lang/lang_keys.h" #include "main/main_app_config.h" @@ -2960,7 +2960,11 @@ std::unique_ptr MakeAttachBotsMenu( } if (Data::CanSendAnyOf(peer, ChatRestriction::SendOther, false)) { raw->addAction(tr::lng_article_menu_item(tr::now), [=] { - Iv::Editor::ShowBox(controller, peer); + Iv::Editor::ShowComposeBox( + controller, + peer, + actionFactory(), + sendMenuDetails); }, &st::menuIconArticle); } const auto session = &controller->session(); diff --git a/Telegram/SourceFiles/iv/iv_editor_box.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp similarity index 71% rename from Telegram/SourceFiles/iv/iv_editor_box.cpp rename to Telegram/SourceFiles/iv/editor/iv_editor_box.cpp index d221c524e8..f91341aa73 100644 --- a/Telegram/SourceFiles/iv/iv_editor_box.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_box.cpp @@ -5,33 +5,18 @@ the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ -#include "iv/iv_editor_box.h" +#include "iv/editor/iv_editor_box.h" -#include #include -#include #include -#include -#include "base/flat_map.h" -#include "base/const_string.h" #include "base/unique_qptr.h" -#include "base/weak_ptr.h" -#include "core/file_utilities.h" -#include "core/mime_type.h" -#include "core/shortcuts.h" +#include "data/data_msg_id.h" #include "ui/image/image_location.h" -#include "data/data_location.h" #include "data/data_types.h" -#include "iv/iv_editor_state.h" -#include "iv/iv_editor_widget.h" +#include "iv/editor/iv_editor_state.h" +#include "iv/editor/iv_editor_widget.h" #include "lang/lang_keys.h" -#include "main/main_app_config.h" -#include "main/main_session.h" -#include "settings.h" -#include "ui/emoji_config.h" -#include "storage/storage_account.h" -#include "ui/controls/location_picker.h" #include "ui/layers/generic_box.h" #include "ui/rect_part.h" #include "ui/ui_utility.h" @@ -40,12 +25,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "ui/widgets/tooltip.h" #include "window/window_session_controller.h" -#include "mainwindow.h" -#include "mainwidget.h" #include #include -#include #include #include "styles/style_iv.h" @@ -65,10 +47,10 @@ class Toolbar final : public Ui::RpWidget { public: Toolbar( QWidget *parent, - not_null controller, - not_null peer, not_null editor, - QPointer tooltipParent); + QPointer tooltipParent, + Fn)> requestMedia, + Fn)> requestMap); int resizeGetHeight(int width) override; @@ -83,19 +65,15 @@ private: Fn callback); void addInsertButtons(); void showHeadingMenu(not_null button); - void chooseMedia(); - void applyMediaResult(FileDialog::OpenResult &&result); - void chooseMap(); void showTooltip(not_null button); void hideTooltip(); void updateTooltipGeometry(); [[nodiscard]] ToolbarButton *buttonData(not_null button); - const not_null _controller; - const not_null _peer; const QPointer _editor; const QPointer _tooltipParent; - const Ui::LocationPickerConfig _mapsConfig; + const Fn)> _requestMedia; + const Fn)> _requestMap; std::vector _buttons; base::unique_qptr _tooltip; base::unique_qptr _menu; @@ -103,18 +81,6 @@ private: }; -[[nodiscard]] Ui::LocationPickerConfig ResolveMapsConfig( - not_null session) { - const auto &appConfig = session->appConfig(); - auto map = appConfig.get>( - u"tdesktop_config_map"_q, - base::flat_map()); - return { - .mapsToken = map[u"maps"_q], - .geoToken = map[u"geo"_q], - }; -} - [[nodiscard]] QString HeadingLabel(int level) { switch (level) { case 1: return tr::lng_article_insert_heading1(tr::now); @@ -127,31 +93,30 @@ private: return tr::lng_article_insert_heading1(tr::now); } -[[nodiscard]] std::optional MediaTypeForPath( - const QString &path) { - const auto mime = Core::MimeTypeForFile(QFileInfo(path)).name(); - if (Core::FileIsImage(path, mime)) { - return State::InsertBlockType::Photo; - } else if (mime.startsWith(u"video/"_q)) { - return State::InsertBlockType::Video; - } else if (mime.startsWith(u"audio/"_q)) { - return State::InsertBlockType::Audio; +[[nodiscard]] QString SubmitText(const ShowBoxDescriptor &descriptor) { + if (!descriptor.submitLabel.isEmpty()) { + return descriptor.submitLabel; } - return std::nullopt; + switch (descriptor.submitType) { + case ShowBoxDescriptor::SubmitType::Send: + return tr::lng_send_button(tr::now); + case ShowBoxDescriptor::SubmitType::Save: + return tr::lng_settings_save(tr::now); + } + return tr::lng_send_button(tr::now); } Toolbar::Toolbar( QWidget *parent, - not_null controller, - not_null peer, not_null editor, - QPointer tooltipParent) + QPointer tooltipParent, + Fn)> requestMedia, + Fn)> requestMap) : Ui::RpWidget(parent) -, _controller(controller) -, _peer(peer) , _editor(editor.get()) , _tooltipParent(std::move(tooltipParent)) -, _mapsConfig(ResolveMapsConfig(&controller->session())) { +, _requestMedia(std::move(requestMedia)) +, _requestMap(std::move(requestMap)) { setMouseTracking(true); addInsertButtons(); } @@ -231,11 +196,17 @@ void Toolbar::addInsertButtons() { [] { return tr::lng_article_insert_pullquote(tr::marked); }, &st::ivEditorToolbarPullquoteIcon, [=] { insertType(State::InsertBlockType::Pullquote); }); - addButton( - tr::lng_article_insert_media(tr::now), - [] { return tr::lng_article_insert_media(tr::marked); }, - &st::ivEditorToolbarAttachIcon, - [=] { chooseMedia(); }); + if (_requestMedia) { + addButton( + tr::lng_article_insert_media(tr::now), + [] { return tr::lng_article_insert_media(tr::marked); }, + &st::ivEditorToolbarAttachIcon, + [=] { + if (_editor) { + _requestMedia(not_null(_editor.data())); + } + }); + } addButton( tr::lng_article_insert_details(tr::now), [] { return tr::lng_article_insert_details(tr::marked); }, @@ -247,12 +218,16 @@ void Toolbar::addInsertButtons() { &st::ivEditorToolbarTableIcon, [=] { insertType(State::InsertBlockType::Table); }); - if (Ui::LocationPicker::Available(_mapsConfig)) { + if (_requestMap) { addButton( tr::lng_article_insert_map(tr::now), [] { return tr::lng_article_insert_map(tr::marked); }, &st::menuIconAddress, - [=] { chooseMap(); }); + [=] { + if (_editor) { + _requestMap(not_null(_editor.data())); + } + }); } } @@ -273,53 +248,6 @@ void Toolbar::showHeadingMenu(not_null button) { _menu->popup(button->mapToGlobal(QPoint(0, button->height()))); } -void Toolbar::chooseMedia() { - const auto weak = QPointer(this); - FileDialog::GetOpenPath( - QPointer(this), - tr::lng_article_insert_media(tr::now), - FileDialog::AllFilesFilter(), - [=](FileDialog::OpenResult &&result) { - if (weak) { - weak->applyMediaResult(std::move(result)); - } - }); -} - -void Toolbar::applyMediaResult(FileDialog::OpenResult &&result) { - if (result.paths.isEmpty()) { - return; - } - const auto type = MediaTypeForPath(result.paths.front()); - if (!type) { - _controller->showToast(tr::lng_edit_media_invalid_file(tr::now)); - return; - } - if (_editor) { - _editor->insertMedia(*type); - } -} - -void Toolbar::chooseMap() { - const auto weak = QPointer(this); - const auto session = &_controller->session(); - Ui::LocationPicker::Show({ - .parent = _controller->widget().get(), - .config = _mapsConfig, - .chooseLabel = tr::lng_maps_point_send(), - .recipient = _peer, - .session = session, - .callback = [=](Data::InputVenue venue) { - if (weak && weak->_editor) { - weak->_editor->insertMap(venue.lat, venue.lon); - } - }, - .quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); }, - .storageId = session->local().resolveStorageIdBots(), - .closeRequests = _controller->content()->death(), - }); -} - int Toolbar::resizeGetHeight(int width) { const auto padding = st::ivEditorToolbarPadding; const auto buttonWidth = st::ivEditorToolbarButton.width; @@ -411,25 +339,43 @@ ToolbarButton *Toolbar::buttonData(not_null button) { void SetupBox( not_null box, - not_null controller, - not_null peer) { + ShowBoxDescriptor descriptor) { box->setWidth(st::boxWideWidth); box->setNoContentMargin(true); + box->setCloseByEscape(false); + box->setCloseByOutsideClick(false); - const auto state = std::make_shared(); const auto editor = box->addRow(object_ptr( box, - controller, - peer, - state), + descriptor.controller, + descriptor.peer, + descriptor.state), style::margins()); const auto tooltipParent = box->getDelegate()->outerContainer(); box->setPinnedToTopContent(object_ptr( box, - controller, - peer, editor, - tooltipParent)); + tooltipParent, + std::move(descriptor.requestMedia), + std::move(descriptor.requestMap))); + + const auto weak = QPointer(box.get()); + const auto submit = box->addButton( + rpl::single(SubmitText(descriptor)), + [=, confirmed = std::move(descriptor.confirmed)] { + editor->commitInlineField(); + if ((!confirmed || confirmed()) && weak) { + weak->closeBox(); + } + }); + if (submit && descriptor.setupSubmitButton) { + descriptor.setupSubmitButton(not_null(submit.data())); + } + box->addButton(tr::lng_cancel(), [=, cancelled = std::move(descriptor.cancelled)] { + if ((!cancelled || cancelled()) && weak) { + weak->closeBox(); + } + }); box->setFocusCallback([=] { editor->activateInitialNode(); @@ -441,10 +387,24 @@ void SetupBox( } // namespace +void ShowBox(ShowBoxDescriptor descriptor) { + if (!descriptor.state) { + descriptor.state = std::make_shared(); + } + descriptor.controller->show(Box( + SetupBox, + std::move(descriptor))); +} + void ShowBox( not_null controller, not_null peer) { - controller->show(Box(SetupBox, controller, peer)); + auto descriptor = ShowBoxDescriptor{ + .controller = controller, + .peer = peer, + .state = std::make_shared(), + }; + ShowBox(std::move(descriptor)); } } // namespace Iv::Editor diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_box.h b/Telegram/SourceFiles/iv/editor/iv_editor_box.h new file mode 100644 index 0000000000..fc7406944d --- /dev/null +++ b/Telegram/SourceFiles/iv/editor/iv_editor_box.h @@ -0,0 +1,54 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "base/basic_types.h" + +#include + +#include + +class PeerData; + +namespace Window { +class SessionController; +} // namespace Window + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Iv::Editor { + +class State; +class Widget; + +struct ShowBoxDescriptor { + enum class SubmitType { + Send, + Save, + }; + + not_null controller; + not_null peer; + std::shared_ptr state; + QString submitLabel; + SubmitType submitType = SubmitType::Send; + Fn cancelled; + Fn confirmed; + Fn)> setupSubmitButton; + Fn)> requestMedia; + Fn)> requestMap; +}; + +void ShowBox(ShowBoxDescriptor descriptor); +void ShowBox( + not_null controller, + not_null peer); + +} // namespace Iv::Editor diff --git a/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp new file mode 100644 index 0000000000..9db1922dab --- /dev/null +++ b/Telegram/SourceFiles/iv/editor/iv_editor_session.cpp @@ -0,0 +1,1527 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "iv/editor/iv_editor_session.h" + +#include +#include +#include +#include +#include +#include + +#include "api/api_sending.h" +#include "api/api_editing.h" +#include "apiwrap.h" +#include "base/weak_ptr.h" +#include "core/shortcuts.h" +#include "data/data_document.h" +#include "data/data_location.h" +#include "data/data_photo.h" +#include "data/data_session.h" +#include "history/history.h" +#include "history/history_item.h" +#include "history/history_item_helpers.h" +#include "iv/iv_cached_media.h" +#include "iv/editor/iv_editor_box.h" +#include "iv/editor/iv_editor_state.h" +#include "iv/editor/iv_editor_widget.h" +#include "iv/iv_rich_message_serializer.h" +#include "lang/lang_keys.h" +#include "main/main_app_config.h" +#include "main/main_session.h" +#include "mainwidget.h" +#include "menu/menu_send.h" +#include "storage/file_upload.h" +#include "storage/localimageloader.h" +#include "storage/storage_account.h" +#include "storage/storage_media_prepare.h" +#include "ui/chat/attach/attach_prepare.h" +#include "ui/controls/location_picker.h" +#include "ui/widgets/separate_panel.h" +#include "window/window_session_controller.h" + +#include +#include +#include +#include +#include +#include + +#include "styles/style_boxes.h" + +namespace Iv::Editor { +namespace { + +using PreparedFile = Ui::PreparedFile; +using PreparedFileType = Ui::PreparedFile::Type; +using PreparedList = Ui::PreparedList; + +enum class AttachmentState : uchar { + Uploading, + Finalizing, + Ready, + Failed, +}; + +struct PreparedDocumentInfo { + QSize dimensions; + QString title; + QString performer; + QString fileName; + int duration = 0; + bool animation = false; + bool video = false; +}; + +struct AttachmentMeta { + PreparedFileType type = PreparedFileType::None; + RichPage::BlockKind blockKind = RichPage::BlockKind::Unsupported; + QString caption; + QString displayName; + QSize dimensions; + QString audioTitle; + QString audioPerformer; + QString audioFileName; + int audioDuration = 0; + bool spoiler = false; + bool autoplay = false; + bool loop = false; +}; + +class PrepareAttachmentTask final : public Task { +public: + PrepareAttachmentTask( + FileLoadTask::Args &&args, + Fn)> done) + : _task(std::move(args)) + , _done(std::move(done)) { + } + + void process() override { + _task.process({ .generateGoodThumbnail = false }); + } + + void finish() override { + _done(_task.peekResult()); + } + +private: + FileLoadTask _task; + Fn)> _done; + +}; + +[[nodiscard]] Ui::LocationPickerConfig ResolveMapsConfig( + not_null session) { + const auto &appConfig = session->appConfig(); + auto map = appConfig.get>( + u"tdesktop_config_map"_q, + base::flat_map()); + return { + .mapsToken = map[u"maps"_q], + .geoToken = map[u"geo"_q], + }; +} + +[[nodiscard]] QString PreparedFileName(const PreparedFile &file) { + return file.displayName.isEmpty() + ? QFileInfo(file.path).fileName() + : file.displayName; +} + +[[nodiscard]] bool AcceptedPreparedFileType(PreparedFileType type) { + return (type == PreparedFileType::Photo) + || (type == PreparedFileType::Video) + || (type == PreparedFileType::Music); +} + +[[nodiscard]] RichPage::RichText ToRichText(QString text) { + auto result = RichPage::RichText(); + result.text.text = std::move(text); + return result; +} + +[[nodiscard]] RichPage::BlockKind BlockKindForPreparedType( + PreparedFileType type) { + switch (type) { + case PreparedFileType::Photo: + return RichPage::BlockKind::Photo; + case PreparedFileType::Video: + return RichPage::BlockKind::Video; + case PreparedFileType::Music: + return RichPage::BlockKind::Audio; + default: + return RichPage::BlockKind::Unsupported; + } +} + +[[nodiscard]] QSize PhotoSizeFromPrepared(const MTPPhoto &photo) { + auto result = QSize(); + photo.match([](const MTPDphotoEmpty &) { + }, [&](const MTPDphoto &data) { + const auto assign = [&](const QString &type, int width, int height) { + if (result.isEmpty() && (type == u"x"_q || type == u"w"_q)) { + result = QSize(width, height); + } + if (type == u"y"_q) { + result = QSize(width, height); + } + }; + for (const auto &size : data.vsizes().v) { + size.match([](const MTPDphotoSizeEmpty &) { + }, [&](const MTPDphotoSize &row) { + assign(qs(row.vtype()), row.vw().v, row.vh().v); + }, [&](const MTPDphotoCachedSize &row) { + assign(qs(row.vtype()), row.vw().v, row.vh().v); + }, [&](const MTPDphotoStrippedSize &) { + }, [&](const MTPDphotoSizeProgressive &row) { + assign(qs(row.vtype()), row.vw().v, row.vh().v); + }, [&](const MTPDphotoPathSize &) { + }); + } + }); + return result; +} + +[[nodiscard]] PreparedDocumentInfo DocumentInfoFromPrepared( + const MTPDocument &document) { + auto result = PreparedDocumentInfo(); + document.match([](const MTPDdocumentEmpty &) { + }, [&](const MTPDdocument &data) { + const auto assign = [&](int width, int height, bool force) { + if (width <= 0 || height <= 0) { + return; + } + if (force || result.dimensions.isEmpty()) { + result.dimensions = QSize(width, height); + } + }; + for (const auto &attribute : data.vattributes().v) { + attribute.match([&](const MTPDdocumentAttributeAudio &row) { + result.duration = row.vduration().v; + result.title = qs(row.vtitle().value_or_empty()); + result.performer = qs(row.vperformer().value_or_empty()); + }, [&](const MTPDdocumentAttributeFilename &row) { + result.fileName = qs(row.vfile_name()); + }, [&](const MTPDdocumentAttributeImageSize &row) { + assign(row.vw().v, row.vh().v, false); + }, [&](const MTPDdocumentAttributeAnimated &) { + result.animation = true; + }, [&](const MTPDdocumentAttributeVideo &row) { + result.video = true; + assign(row.vw().v, row.vh().v, true); + }, [&](const auto &) { + }); + } + }); + return result; +} + +[[nodiscard]] QVector DocumentAttributesFromPrepared( + const FilePrepareResult &prepared) { + auto result = QVector(); + prepared.document.match([&](const MTPDdocument &data) { + result = data.vattributes().v; + }, [](const auto &) { + }); + return result; +} + +[[nodiscard]] QVector ToInputDocumentVector( + const std::vector &stickers) { + auto result = QVector(); + result.reserve(int(stickers.size())); + for (const auto &sticker : stickers) { + result.push_back(sticker); + } + return result; +} + +[[nodiscard]] AttachmentMeta BuildAttachmentMeta(const PreparedFile &file) { + auto result = AttachmentMeta{ + .type = file.type, + .blockKind = BlockKindForPreparedType(file.type), + .caption = file.caption.text, + .displayName = PreparedFileName(file), + .dimensions = !file.shownDimensions.isEmpty() + ? file.shownDimensions + : file.originalDimensions, + .spoiler = file.spoiler, + }; + if (!file.information) { + result.audioFileName = result.displayName; + return result; + } + if (const auto song = std::get_if( + &file.information->media)) { + result.audioTitle = song->title; + result.audioPerformer = song->performer; + result.audioDuration = int(song->duration / 1000); + result.audioFileName = result.displayName; + } else if (const auto video = std::get_if( + &file.information->media)) { + result.autoplay = video->isGifv; + result.loop = video->isGifv; + } + return result; +} + +[[nodiscard]] std::unique_ptr BuildVideoCoverTask( + not_null session, + PeerId peer, + std::unique_ptr file) { + if (!file) { + return nullptr; + } + return std::make_unique(FileLoadTask::Args{ + .session = session, + .filepath = file->path, + .content = std::move(file->content), + .information = std::move(file->information), + .videoCover = nullptr, + .type = SendMediaType::Photo, + .to = FileLoadTo( + peer, + Api::SendOptions(), + FullReplyTo(), + MsgId()), + .caption = TextWithTags(), + .spoiler = false, + .album = nullptr, + .forceFile = false, + .sendLargePhotos = false, + .idOverride = 0, + .displayName = file->displayName, + }); +} + +[[nodiscard]] FileLoadTask::Args BuildPrepareTaskArgs( + not_null session, + PeerId peer, + PreparedFile file) { + const auto sendType = (file.type == PreparedFileType::Photo) + ? SendMediaType::Photo + : SendMediaType::File; + return { + .session = session, + .filepath = file.path, + .content = std::move(file.content), + .information = std::move(file.information), + .videoCover = BuildVideoCoverTask( + session, + peer, + std::move(file.videoCover)), + .type = sendType, + .to = FileLoadTo( + peer, + Api::SendOptions(), + FullReplyTo(), + MsgId()), + .caption = TextWithTags(), + .spoiler = file.spoiler, + .album = nullptr, + .forceFile = false, + .sendLargePhotos = file.sendLargePhotos, + .idOverride = 0, + .displayName = file.displayName, + }; +} + +class ArticleSession final + : public std::enable_shared_from_this + , public base::has_weak_ptr { +public: + static void ShowCompose( + not_null controller, + not_null peer, + Api::SendAction action, + Fn sendMenuDetails) { + auto session = std::shared_ptr(new ArticleSession( + controller, + peer, + Mode::Compose, + FullMsgId(peer->id, controller->session().data().nextLocalMessageId()), + std::make_shared(), + std::move(action), + std::move(sendMenuDetails), + std::nullopt)); + session->showBox(); + } + + static void ShowEdit( + not_null controller, + not_null item) { + const auto richPage = item->richPage(); + if (!richPage || !CanEditRichPage(richPage)) { + controller->showToast(tr::lng_edit_error(tr::now)); + return; + } + auto session = std::shared_ptr(new ArticleSession( + controller, + item->history()->peer, + Mode::Edit, + item->fullId(), + std::make_shared(*richPage), + std::nullopt, + nullptr, + EditedItemSnapshot{ + .item = item, + .page = richPage, + .summary = item->originalText(), + })); + session->showBox(); + } + + ~ArticleSession() { + _submitDeferred = false; + for (const auto &attachment : _attachments) { + _session->uploader().cancel(attachment.uploadId); + } + } + +private: + enum class Mode { + Compose, + Edit, + }; + + struct AttachmentRecord { + FullMsgId uploadId; + PreparedFileType type = PreparedFileType::None; + RichPage::BlockKind blockKind = RichPage::BlockKind::Unsupported; + uint64 localMediaId = 0; + AttachmentState state = AttachmentState::Uploading; + float64 progress = 0.; + QString caption; + QString filename; + QString filemime; + QVector attributes; + bool forceFile = false; + QString audioTitle; + QString audioPerformer; + QString audioFileName; + int audioDuration = 0; + QSize dimensions; + bool spoiler = false; + bool autoplay = false; + bool loop = false; + std::vector blockLocators; + MTPInputPhoto inputPhoto; + MTPInputDocument inputDocument; + uint64 serverMediaId = 0; + uint64 accessHash = 0; + QByteArray fileReference; + PhotoData *serverPhoto = nullptr; + DocumentData *serverDocument = nullptr; + }; + + struct QueuedPrepare { + QPointer editor; + PreparedFile file; + uint64 batchId = 0; + }; + + struct EditedItemSnapshot { + not_null item; + std::shared_ptr page; + TextWithEntities summary; + }; + + ArticleSession( + not_null controller, + not_null peer, + Mode mode, + FullMsgId articleId, + std::shared_ptr page, + std::optional action, + Fn sendMenuDetails, + std::optional edited) + : _controller(controller) + , _session(&controller->session()) + , _peer(peer) + , _mode(mode) + , _articleId(articleId) + , _composeAction(std::move(action)) + , _sendMenuDetails(std::move(sendMenuDetails)) + , _edited(std::move(edited)) + , _page(page ? std::move(page) : std::make_shared()) + , _runtime(CreateMessageMediaRuntime( + _session, + _articleId, + [](QString) { + }, + [](QString) { + })) + , _state(std::make_shared(_page, _runtime)) + , _submitOptions(_composeAction ? _composeAction->options : Api::SendOptions()) { + subscribeToUploader(); + } + + [[nodiscard]] bool submitRequested() { + if (_submittedPage || _submitApiRequested) { + return false; + } + if (hasPendingPreparation()) { + _submitDeferred = true; + return false; + } + if (hasVisibleFailedAttachments()) { + showAttachmentFailedToast(); + return false; + } + auto page = std::shared_ptr( + std::make_shared(_state->richPage())); + if (!applySubmittedLocalState(page)) { + _controller->showToast(tr::lng_edit_error(tr::now)); + return false; + } + _submitDeferred = false; + _submittedPage = std::move(page); + _backgroundHold = shared_from_this(); + maybeContinueSubmittedRequest(); + return true; + } + + [[nodiscard]] bool cancelRequested() { + _submitDeferred = false; + return true; + } + + [[nodiscard]] HistoryItem *currentSubmittedItem() const { + return _session->data().message(_articleId); + } + + [[nodiscard]] HistoryItem *ensureComposeLocalItem() { + if (const auto item = currentSubmittedItem()) { + return item; + } + if (!_composeAction) { + return nullptr; + } + auto action = *_composeAction; + const auto history = action.history; + const auto peer = history->peer; + auto flags = NewMessageFlags(peer); + if (action.replyTo) { + flags |= MessageFlag::HasReplyInfo; + } + Api::FillMessagePostFlags(action, peer, flags); + if (action.options.scheduled) { + flags |= MessageFlag::IsOrWasScheduled; + } + if (action.options.shortcutId) { + flags |= MessageFlag::ShortcutMessage; + } + const auto starsPaid = std::min( + peer->starsPerMessageChecked(), + action.options.starsApproved); + return history->addNewLocalMessage({ + .id = _articleId.msg, + .flags = flags, + .from = NewMessageFromId(action), + .replyTo = action.replyTo, + .date = NewMessageDate(action.options), + .scheduleRepeatPeriod = action.options.scheduleRepeatPeriod, + .shortcutId = action.options.shortcutId, + .starsPaid = starsPaid, + .postAuthor = NewMessagePostAuthor(action), + .effectId = action.options.effectId, + .suggest = HistoryMessageSuggestInfo(action.options), + }, TextWithEntities(), MTP_messageMediaEmpty()); + } + + [[nodiscard]] bool applySubmittedLocalState( + const std::shared_ptr &page) { + const auto item = (_mode == Mode::Compose) + ? ensureComposeLocalItem() + : currentSubmittedItem(); + if (!item) { + return false; + } + item->applyLocalRichPage(page); + return true; + } + + void restoreEditedItem() { + if (!_edited) { + return; + } + if (const auto item = currentSubmittedItem()) { + item->applyLocalRichPage(_edited->page, _edited->summary); + } + } + + void finishSubmittedWork() { + _submitApiRequested = false; + _submittedPage = nullptr; + _backgroundHold = nullptr; + } + + void failSubmittedWork(bool showToast) { + if (showToast) { + showAttachmentFailedToast(); + } + if (_mode == Mode::Edit) { + restoreEditedItem(); + } else if (const auto item = currentSubmittedItem()) { + item->sendFailed(); + } + finishSubmittedWork(); + } + + [[nodiscard]] bool pageContainsAttachment( + const std::vector &blocks, + const AttachmentRecord &attachment) const { + for (const auto &block : blocks) { + if (blockMatchesAttachment(block, attachment) + || pageContainsAttachment(block.blocks, attachment)) { + return true; + } + for (const auto &item : block.listItems) { + if (pageContainsAttachment(item.blocks, attachment)) { + return true; + } + } + } + return false; + } + + [[nodiscard]] bool submittedPageContainsAttachment( + const AttachmentRecord &attachment) const { + return _submittedPage + && pageContainsAttachment(_submittedPage->blocks, attachment); + } + + [[nodiscard]] bool hasFailedSubmittedAttachments() const { + for (const auto &attachment : _attachments) { + if (attachment.state == AttachmentState::Failed + && submittedPageContainsAttachment(attachment)) { + return true; + } + } + return false; + } + + [[nodiscard]] bool submittedAttachmentsReady() const { + for (const auto &attachment : _attachments) { + if (submittedPageContainsAttachment(attachment) + && attachment.state != AttachmentState::Ready) { + return false; + } + } + return true; + } + + [[nodiscard]] const AttachmentRecord *attachmentForBlock( + const RichPage::Block &block) const { + for (const auto &attachment : _attachments) { + if (blockMatchesAttachment(block, attachment)) { + return &attachment; + } + } + return nullptr; + } + + [[nodiscard]] bool patchSubmittedBlocks( + std::vector &blocks) const { + for (auto &block : blocks) { + if (const auto attachment = attachmentForBlock(block)) { + if (attachment->state != AttachmentState::Ready) { + return false; + } + if (block.kind == RichPage::BlockKind::Photo) { + if (!attachment->serverPhoto || !attachment->serverMediaId) { + return false; + } + block.photoId = attachment->serverMediaId; + block.photo = attachment->serverPhoto; + } else { + if (!attachment->serverDocument + || !attachment->serverMediaId) { + return false; + } + block.documentId = attachment->serverMediaId; + block.document = attachment->serverDocument; + } + } + if (!patchSubmittedBlocks(block.blocks)) { + return false; + } + for (auto &item : block.listItems) { + if (!patchSubmittedBlocks(item.blocks)) { + return false; + } + } + } + return true; + } + + [[nodiscard]] std::optional serializeSubmittedPage() const { + if (!_submittedPage) { + return std::nullopt; + } + auto page = RichPage(*_submittedPage); + return patchSubmittedBlocks(page.blocks) + ? SerializeInputRichMessage(_session, page) + : std::optional(); + } + + void maybeContinueSubmittedRequest() { + if (!_submittedPage || _submitApiRequested) { + return; + } + if (hasFailedSubmittedAttachments()) { + failSubmittedWork(false); + return; + } + if (!submittedAttachmentsReady()) { + return; + } + const auto richMessage = serializeSubmittedPage(); + if (!richMessage) { + failSubmittedWork(true); + return; + } + const auto item = currentSubmittedItem(); + if (!item) { + finishSubmittedWork(); + return; + } + _submitApiRequested = true; + if (_mode == Mode::Compose) { + auto action = *_composeAction; + action.options = _submitOptions; + _session->api().sendRichMessage(item, *richMessage, std::move(action)); + finishSubmittedWork(); + return; + } + Api::EditRichMessage( + not_null{ item }, + [weak = base::make_weak(this)] { + if (const auto session = weak.get()) { + return session->serializeSubmittedPage(); + } + return std::optional(); + }, + _submitOptions, + [weak = base::make_weak(this)](mtpRequestId) { + if (const auto session = weak.get()) { + session->finishSubmittedWork(); + } + }, + [weak = base::make_weak(this)](const QString &error, mtpRequestId) { + if (const auto session = weak.get()) { + session->restoreEditedItem(); + session->_controller->showToast(error.isEmpty() + ? tr::lng_edit_error(tr::now) + : error); + session->finishSubmittedWork(); + } + }); + } + + void requestSubmit(Api::SendOptions options) { + _submitOptions = std::move(options); + if (_composeAction) { + _composeAction->options = _submitOptions; + } + if (hasPendingPreparation()) { + _submitDeferred = true; + return; + } + if (hasVisibleFailedAttachments()) { + showAttachmentFailedToast(); + return; + } + simulateSubmitClick(); + } + + void setupSubmitButton(not_null button) { + _submitButton = button; + if (_mode != Mode::Compose || !_sendMenuDetails) { + return; + } + const auto weak = base::make_weak(this); + const auto submit = [weak](Api::SendOptions options) { + if (const auto session = weak.get()) { + session->requestSubmit(std::move(options)); + } + }; + SendMenu::SetupMenuAndShortcuts( + button, + _controller->uiShow(), + [weak] { + if (const auto session = weak.get()) { + return session->_sendMenuDetails + ? session->_sendMenuDetails() + : SendMenu::Details(); + } + return SendMenu::Details(); + }, + SendMenu::DefaultCallback(_controller->uiShow(), submit)); + } + + void requestMedia(not_null editor) { + _editor = editor; + const auto weak = base::make_weak(this); + const auto editorPointer = QPointer(editor.get()); + FileDialog::GetOpenPath( + QPointer(_controller->content().get()), + tr::lng_choose_file(tr::now), + FileDialog::AllFilesFilter(), + [weak, editorPointer](FileDialog::OpenResult &&result) mutable { + if (const auto session = weak.get()) { + session->handleMediaDialogResult( + editorPointer, + std::move(result)); + } + }); + } + + void requestMap(not_null editor) { + _editor = editor; + const auto config = ResolveMapsConfig(_session); + if (!Ui::LocationPicker::Available(config)) { + return; + } + const auto weak = base::make_weak(this); + const auto editorPointer = QPointer(editor.get()); + Ui::LocationPicker::Show({ + .parent = _controller->content().get(), + .config = config, + .chooseLabel = tr::lng_maps_point_send(), + .session = _session, + .callback = [weak, editorPointer](::Data::InputVenue venue) { + if (const auto session = weak.get()) { + session->applyMapSelection(editorPointer, std::move(venue)); + } + }, + .quit = [] { Shortcuts::Launch(Shortcuts::Command::Quit); }, + .storageId = _session->local().resolveStorageIdBots(), + .closeRequests = _controller->content()->death(), + }); + } + + void showBox() { + auto descriptor = ShowBoxDescriptor{ + .controller = _controller, + .peer = _peer, + .state = _state, + .submitType = (_mode == Mode::Compose) + ? ShowBoxDescriptor::SubmitType::Send + : ShowBoxDescriptor::SubmitType::Save, + .cancelled = [session = shared_from_this()] { + return session->cancelRequested(); + }, + .confirmed = [session = shared_from_this()] { + return session->submitRequested(); + }, + .setupSubmitButton = [session = shared_from_this()]( + not_null button) { + session->setupSubmitButton(button); + }, + .requestMedia = [session = shared_from_this()]( + not_null editor) { + session->requestMedia(editor); + }, + .requestMap = [session = shared_from_this()]( + not_null editor) { + session->requestMap(editor); + }, + }; + ShowBox(std::move(descriptor)); + } + + void handleMediaDialogResult( + QPointer editor, + FileDialog::OpenResult &&result) { + auto showError = [=](tr::phrase<> phrase) { + _controller->showToast(phrase(tr::now)); + }; + auto list = Storage::PreparedFileFromFilesDialog( + std::move(result), + [](const PreparedList &) { + return true; + }, + showError, + st::sendMediaPreviewSize, + _session->premium()); + if (!list) { + return; + } + applyPreparedList(editor, std::move(*list), ++_prepareBatchId); + } + + void applyPreparedList( + QPointer editor, + PreparedList list, + uint64 batchId) { + for (auto &file : list.files) { + applyPreparedFile(editor, std::move(file), batchId); + } + for (auto &file : list.filesToProcess) { + _prepareQueue.push_back({ + .editor = editor, + .file = std::move(file), + .batchId = batchId, + }); + } + enqueueNextPrepare(); + } + + void enqueueNextPrepare() { + if (_preparing) { + return; + } + while (!_prepareQueue.empty() + && _prepareQueue.front().file.information) { + auto queued = std::move(_prepareQueue.front()); + _prepareQueue.pop_front(); + applyPreparedFile( + queued.editor, + std::move(queued.file), + queued.batchId); + } + if (_prepareQueue.empty()) { + maybeContinueDeferredSubmit(); + return; + } + auto queued = std::move(_prepareQueue.front()); + _prepareQueue.pop_front(); + const auto weak = base::make_weak(this); + _preparing = true; + const auto sideLimit = PhotoSideLimit(); + crl::async([weak, queued = std::move(queued), sideLimit]() mutable { + Storage::PrepareDetails( + queued.file, + st::sendMediaPreviewSize, + sideLimit); + crl::on_main([weak, queued = std::move(queued)]() mutable { + if (const auto session = weak.get()) { + session->preparedAsyncFile(std::move(queued)); + } + }); + }); + } + + void preparedAsyncFile(QueuedPrepare queued) { + _preparing = false; + applyPreparedFile( + queued.editor, + std::move(queued.file), + queued.batchId); + enqueueNextPrepare(); + } + + void applyPreparedFile( + QPointer editor, + PreparedFile file, + uint64 batchId) { + if (!AcceptedPreparedFileType(file.type)) { + showRejectedToast(batchId); + return; + } + prepareAttachment(editor, std::move(file)); + } + + void prepareAttachment( + QPointer editor, + PreparedFile file) { + const auto meta = BuildAttachmentMeta(file); + const auto weak = base::make_weak(this); + ++_pendingAttachmentPrepareCount; + _attachmentPrepareQueue.addTask( + std::make_unique( + BuildPrepareTaskArgs(_session, _peer->id, std::move(file)), + [weak, editor, meta](std::shared_ptr prepared) mutable { + if (const auto session = weak.get()) { + session->attachmentPrepared( + editor, + std::move(meta), + std::move(prepared)); + } + })); + } + + void attachmentPrepared( + QPointer editor, + AttachmentMeta meta, + std::shared_ptr prepared) { + _pendingAttachmentPrepareCount = std::max( + _pendingAttachmentPrepareCount - 1, + 0); + if (!prepared) { + showAttachmentFailedToast(); + maybeContinueDeferredSubmit(); + return; + } + if (!editor) { + maybeContinueDeferredSubmit(); + return; + } + if ((meta.blockKind == RichPage::BlockKind::Photo) + != (prepared->type == SendMediaType::Photo)) { + showAttachmentFailedToast(); + maybeContinueDeferredSubmit(); + return; + } + startAttachmentUpload(editor, std::move(meta), std::move(prepared)); + maybeContinueDeferredSubmit(); + } + + void startAttachmentUpload( + QPointer editor, + AttachmentMeta meta, + std::shared_ptr prepared) { + if (!editor || !prepared) { + return; + } + _editor = editor; + const auto uploadId = FullMsgId( + _peer->id, + _session->data().nextLocalMessageId()); + auto record = AttachmentRecord{ + .uploadId = uploadId, + .type = meta.type, + .blockKind = meta.blockKind, + .localMediaId = prepared->id, + .state = AttachmentState::Uploading, + .caption = meta.caption, + .filename = prepared->filename, + .filemime = prepared->filemime, + .attributes = DocumentAttributesFromPrepared(*prepared), + .forceFile = prepared->forceFile, + .audioTitle = meta.audioTitle, + .audioPerformer = meta.audioPerformer, + .audioFileName = meta.audioFileName.isEmpty() + ? meta.displayName + : meta.audioFileName, + .audioDuration = meta.audioDuration, + .dimensions = meta.dimensions, + .spoiler = meta.spoiler, + .autoplay = meta.autoplay, + .loop = meta.loop, + }; + if (record.blockKind == RichPage::BlockKind::Photo) { + const auto size = PhotoSizeFromPrepared(prepared->photo); + if (!size.isEmpty()) { + record.dimensions = size; + } + } else { + const auto info = DocumentInfoFromPrepared(prepared->document); + if (!info.dimensions.isEmpty()) { + record.dimensions = info.dimensions; + } + if (record.blockKind == RichPage::BlockKind::Audio) { + if (record.audioTitle.isEmpty()) { + record.audioTitle = info.title; + } + if (record.audioPerformer.isEmpty()) { + record.audioPerformer = info.performer; + } + if (record.audioFileName.isEmpty()) { + record.audioFileName = !info.fileName.isEmpty() + ? info.fileName + : prepared->filename; + } + if (!record.audioDuration) { + record.audioDuration = info.duration; + } + } else { + record.autoplay = record.autoplay || info.animation; + record.loop = record.loop || info.animation; + } + } + + _session->uploader().upload(uploadId, prepared); + _attachments.push_back(std::move(record)); + auto &stored = _attachments.back(); + updateAttachmentProgress(stored); + editor->insertPreparedBlock(makeAttachmentBlock(stored)); + refreshAttachmentLocators(stored); + requestEditorUpdate(); + } + + void applyMapSelection( + QPointer editor, + ::Data::InputVenue venue) { + if (!editor) { + return; + } + _editor = editor; + editor->insertPreparedBlock(makeMapBlock(std::move(venue))); + } + + [[nodiscard]] RichPage::Block makeAttachmentBlock( + const AttachmentRecord &attachment) const { + auto block = RichPage::Block(); + block.kind = attachment.blockKind; + block.caption = ToRichText(attachment.caption); + if (attachment.blockKind == RichPage::BlockKind::Photo) { + block.photoId = attachment.localMediaId; + block.width = attachment.dimensions.width(); + block.height = attachment.dimensions.height(); + block.spoiler = attachment.spoiler; + } else if (attachment.blockKind == RichPage::BlockKind::Video) { + block.documentId = attachment.localMediaId; + block.width = attachment.dimensions.width(); + block.height = attachment.dimensions.height(); + block.spoiler = attachment.spoiler; + block.autoplay = attachment.autoplay; + block.loop = attachment.loop; + } else if (attachment.blockKind == RichPage::BlockKind::Audio) { + block.documentId = attachment.localMediaId; + block.audioTitle = attachment.audioTitle; + block.audioPerformer = attachment.audioPerformer; + block.audioFileName = attachment.audioFileName; + block.audioDuration = attachment.audioDuration; + } + return block; + } + + [[nodiscard]] RichPage::Block makeMapBlock(::Data::InputVenue venue) const { + const auto point = ::Data::LocationPoint( + venue.lat, + venue.lon, + ::Data::LocationPoint::NoAccessHash); + const auto preview = ::Data::ComputeLocation(point); + auto caption = QString(); + if (!venue.title.isEmpty() && !venue.address.isEmpty()) { + caption = venue.title + u"\n"_q + venue.address; + } else { + caption = !venue.title.isEmpty() ? venue.title : venue.address; + } + auto block = RichPage::Block(); + block.kind = RichPage::BlockKind::Map; + block.latitude = venue.lat; + block.longitude = venue.lon; + block.accessHash = point.accessHash(); + block.width = preview.width; + block.height = preview.height; + block.zoom = preview.zoom; + block.caption = ToRichText(std::move(caption)); + return block; + } + + void subscribeToUploader() { + _session->uploader().photoReady( + ) | rpl::on_next([=](const Storage::UploadedMedia &data) { + if (const auto attachment = findAttachment(data.fullId)) { + finalizeUploadedPhoto(*attachment, data); + } + }, _lifetime); + _session->uploader().documentReady( + ) | rpl::on_next([=](const Storage::UploadedMedia &data) { + if (const auto attachment = findAttachment(data.fullId)) { + finalizeUploadedDocument(*attachment, data); + } + }, _lifetime); + _session->uploader().photoProgress( + ) | rpl::on_next([=](const FullMsgId &id) { + if (const auto attachment = findAttachment(id)) { + updateAttachmentProgress(*attachment); + } + }, _lifetime); + _session->uploader().documentProgress( + ) | rpl::on_next([=](const FullMsgId &id) { + if (const auto attachment = findAttachment(id)) { + updateAttachmentProgress(*attachment); + } + }, _lifetime); + _session->uploader().photoFailed( + ) | rpl::on_next([=](const FullMsgId &id) { + markAttachmentFailed(id); + }, _lifetime); + _session->uploader().documentFailed( + ) | rpl::on_next([=](const FullMsgId &id) { + markAttachmentFailed(id); + }, _lifetime); + } + + void finalizeUploadedPhoto( + AttachmentRecord &attachment, + const Storage::UploadedMedia &data) { + using Flag = MTPDinputMediaUploadedPhoto::Flag; + attachment.state = AttachmentState::Finalizing; + auto flags = MTPDinputMediaUploadedPhoto::Flags(); + const auto stickers = ToInputDocumentVector(data.info.attachedStickers); + if (attachment.spoiler) { + flags |= Flag::f_spoiler; + } + if (!stickers.isEmpty()) { + flags |= Flag::f_stickers; + } + _session->api().request(MTPmessages_UploadMedia( + MTP_flags(0), + MTPstring(), + _peer->input(), + MTP_inputMediaUploadedPhoto( + MTP_flags(flags), + data.info.file, + MTP_vector(std::move(stickers)), + MTP_int(0), + MTPInputDocument()) + )).done([weak = base::make_weak(this), uploadId = attachment.uploadId]( + const MTPMessageMedia &result) { + if (const auto session = weak.get()) { + session->applyUploadedPhotoResult(uploadId, result); + } + }).fail([weak = base::make_weak(this), uploadId = attachment.uploadId]( + const MTP::Error &) { + if (const auto session = weak.get()) { + session->markAttachmentFailed(uploadId); + } + }).send(); + } + + void finalizeUploadedDocument( + AttachmentRecord &attachment, + const Storage::UploadedMedia &data) { + using Flag = MTPDinputMediaUploadedDocument::Flag; + attachment.state = AttachmentState::Finalizing; + auto flags = MTPDinputMediaUploadedDocument::Flags(); + if (attachment.forceFile) { + flags |= Flag::f_force_file; + } + const auto stickers = ToInputDocumentVector(data.info.attachedStickers); + if (data.info.thumb) { + flags |= Flag::f_thumb; + } + if (attachment.spoiler) { + flags |= Flag::f_spoiler; + } + if (!stickers.isEmpty()) { + flags |= Flag::f_stickers; + } + if (data.info.videoCover) { + flags |= Flag::f_video_cover; + } + auto attributes = !attachment.attributes.isEmpty() + ? attachment.attributes + : QVector( + 1, + MTP_documentAttributeFilename(MTP_string(attachment.filename))); + _session->api().request(MTPmessages_UploadMedia( + MTP_flags(0), + MTPstring(), + _peer->input(), + MTP_inputMediaUploadedDocument( + MTP_flags(flags), + data.info.file, + data.info.thumb.value_or(MTPInputFile()), + MTP_string(attachment.filemime), + MTP_vector(std::move(attributes)), + MTP_vector(std::move(stickers)), + data.info.videoCover.value_or(MTPInputPhoto()), + MTP_int(0), + MTP_int(0)) + )).done([weak = base::make_weak(this), uploadId = attachment.uploadId]( + const MTPMessageMedia &result) { + if (const auto session = weak.get()) { + session->applyUploadedDocumentResult(uploadId, result); + } + }).fail([weak = base::make_weak(this), uploadId = attachment.uploadId]( + const MTP::Error &) { + if (const auto session = weak.get()) { + session->markAttachmentFailed(uploadId); + } + }).send(); + } + + void applyUploadedPhotoResult( + FullMsgId uploadId, + const MTPMessageMedia &result) { + const auto attachment = findAttachment(uploadId); + if (!attachment) { + return; + } + auto ok = false; + result.match([&](const MTPDmessageMediaPhoto &media) { + const auto photo = media.vphoto(); + if (!photo || photo->type() != mtpc_photo) { + return; + } + const auto &fields = photo->c_photo(); + attachment->state = AttachmentState::Ready; + attachment->serverMediaId = fields.vid().v; + attachment->accessHash = fields.vaccess_hash().v; + attachment->fileReference = fields.vfile_reference().v; + attachment->serverPhoto = _session->data().processPhoto(*photo); + attachment->inputPhoto = MTP_inputPhoto( + fields.vid(), + fields.vaccess_hash(), + fields.vfile_reference()); + ok = true; + }, [&](const auto &) { + }); + if (!ok) { + markAttachmentFailed(uploadId); + return; + } + requestEditorUpdate(); + maybeContinueSubmittedRequest(); + } + + void applyUploadedDocumentResult( + FullMsgId uploadId, + const MTPMessageMedia &result) { + const auto attachment = findAttachment(uploadId); + if (!attachment) { + return; + } + auto ok = false; + result.match([&](const MTPDmessageMediaDocument &media) { + const auto document = media.vdocument(); + if (!document || document->type() != mtpc_document) { + return; + } + const auto &fields = document->c_document(); + attachment->state = AttachmentState::Ready; + attachment->serverMediaId = fields.vid().v; + attachment->accessHash = fields.vaccess_hash().v; + attachment->fileReference = fields.vfile_reference().v; + attachment->serverDocument = _session->data().processDocument(*document); + attachment->inputDocument = MTP_inputDocument( + fields.vid(), + fields.vaccess_hash(), + fields.vfile_reference()); + ok = true; + }, [&](const auto &) { + }); + if (!ok) { + markAttachmentFailed(uploadId); + return; + } + requestEditorUpdate(); + maybeContinueSubmittedRequest(); + } + + void markAttachmentFailed(FullMsgId uploadId) { + if (const auto attachment = findAttachment(uploadId)) { + attachment->state = AttachmentState::Failed; + updateAttachmentProgress(*attachment); + showAttachmentFailedToast(); + requestEditorUpdate(); + maybeContinueSubmittedRequest(); + } + } + + void updateAttachmentProgress(AttachmentRecord &attachment) { + if (attachment.blockKind == RichPage::BlockKind::Photo) { + attachment.progress = _session->data().photo( + attachment.localMediaId)->progress(); + } else { + attachment.progress = _session->data().document( + attachment.localMediaId)->progress(); + } + requestEditorUpdate(); + } + + void requestEditorUpdate() { + if (_editor) { + _editor->update(); + } + } + + [[nodiscard]] AttachmentRecord *findAttachment(FullMsgId uploadId) { + for (auto &attachment : _attachments) { + if (attachment.uploadId == uploadId) { + return &attachment; + } + } + return nullptr; + } + + [[nodiscard]] bool blockMatchesAttachment( + const RichPage::Block &block, + const AttachmentRecord &attachment) const { + switch (attachment.blockKind) { + case RichPage::BlockKind::Photo: + return (block.kind == RichPage::BlockKind::Photo) + && (block.photoId == attachment.localMediaId); + case RichPage::BlockKind::Video: + return (block.kind == RichPage::BlockKind::Video) + && (block.documentId == attachment.localMediaId); + case RichPage::BlockKind::Audio: + return (block.kind == RichPage::BlockKind::Audio) + && (block.documentId == attachment.localMediaId); + default: + return false; + } + } + + void collectBlockLocators( + const std::vector &blocks, + const State::BlockContainerPath &container, + const AttachmentRecord &attachment, + std::vector &result) const { + for (auto i = 0, count = int(blocks.size()); i != count; ++i) { + const auto path = State::BlockPath{ + .container = container, + .index = i, + }; + const auto &block = blocks[i]; + if (blockMatchesAttachment(block, attachment)) { + result.push_back(path); + } + if (!block.blocks.empty()) { + auto child = container; + child.steps.push_back({ + .kind = State::BlockContainerKind::BlockChildren, + .blockIndex = i, + }); + collectBlockLocators( + block.blocks, + child, + attachment, + result); + } + for (auto itemIndex = 0, items = int(block.listItems.size()); + itemIndex != items; + ++itemIndex) { + const auto &itemBlocks = block.listItems[itemIndex].blocks; + if (itemBlocks.empty()) { + continue; + } + auto child = container; + child.steps.push_back({ + .kind = State::BlockContainerKind::ListItemChildren, + .blockIndex = i, + .listItemIndex = itemIndex, + }); + collectBlockLocators( + itemBlocks, + child, + attachment, + result); + } + } + } + + void refreshAttachmentLocators(AttachmentRecord &attachment) { + auto locators = std::vector(); + collectBlockLocators( + _page->blocks, + State::BlockContainerPath(), + attachment, + locators); + attachment.blockLocators = std::move(locators); + } + + [[nodiscard]] bool hasVisibleAttachmentBlock(AttachmentRecord &attachment) { + refreshAttachmentLocators(attachment); + return !attachment.blockLocators.empty(); + } + + [[nodiscard]] bool hasVisibleFailedAttachments() { + for (auto &attachment : _attachments) { + if (attachment.state == AttachmentState::Failed + && hasVisibleAttachmentBlock(attachment)) { + return true; + } + } + return false; + } + + void showAttachmentFailedToast() { + _controller->showToast(tr::lng_attach_failed(tr::now)); + } + + void showRejectedToast(uint64 batchId) { + if (_rejectedToastBatchId == batchId) { + return; + } + _rejectedToastBatchId = batchId; + _controller->showToast(tr::lng_edit_media_invalid_file(tr::now)); + } + + [[nodiscard]] bool hasPendingPreparation() const { + return _preparing + || !_prepareQueue.empty() + || (_pendingAttachmentPrepareCount > 0); + } + + void maybeContinueDeferredSubmit() { + if (!_submitDeferred || hasPendingPreparation()) { + return; + } + _submitDeferred = false; + simulateSubmitClick(); + } + + void simulateSubmitClick() { + if (!_submitButton) { + return; + } + const auto post = [button = _submitButton](QEvent::Type type) { + if (!button) { + return; + } + QApplication::postEvent( + button, + new QMouseEvent( + type, + QPointF(0, 0), + Qt::LeftButton, + Qt::LeftButton, + Qt::NoModifier)); + }; + post(QEvent::MouseButtonPress); + post(QEvent::MouseButtonRelease); + } + + const not_null _controller; + const not_null _session; + const not_null _peer; + const Mode _mode; + const FullMsgId _articleId; + std::optional _composeAction; + const Fn _sendMenuDetails; + const std::optional _edited; + const std::shared_ptr _page; + const std::shared_ptr _runtime; + const std::shared_ptr _state; + Api::SendOptions _submitOptions; + QPointer _submitButton; + QPointer _editor; + std::shared_ptr _backgroundHold; + std::shared_ptr _submittedPage; + std::vector _attachments; + std::deque _prepareQueue; + TaskQueue _attachmentPrepareQueue; + rpl::lifetime _lifetime; + uint64 _prepareBatchId = 0; + uint64 _rejectedToastBatchId = 0; + int _pendingAttachmentPrepareCount = 0; + bool _preparing = false; + bool _submitDeferred = false; + bool _submitApiRequested = false; + +}; + +} // namespace + +void ShowComposeBox( + not_null controller, + not_null peer, + Api::SendAction action, + Fn sendMenuDetails) { + ArticleSession::ShowCompose( + controller, + peer, + std::move(action), + std::move(sendMenuDetails)); +} + +void ShowEditBox( + not_null controller, + not_null item) { + ArticleSession::ShowEdit(controller, item); +} + +} // namespace Iv::Editor diff --git a/Telegram/SourceFiles/iv/iv_editor_box.h b/Telegram/SourceFiles/iv/editor/iv_editor_session.h similarity index 57% rename from Telegram/SourceFiles/iv/iv_editor_box.h rename to Telegram/SourceFiles/iv/editor/iv_editor_session.h index 2663b9f579..42b5c36bc4 100644 --- a/Telegram/SourceFiles/iv/iv_editor_box.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_session.h @@ -7,6 +7,11 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ #pragma once +#include "api/api_common.h" +#include "base/basic_types.h" +#include "menu/menu_send_details.h" + +class HistoryItem; class PeerData; namespace Window { @@ -15,8 +20,13 @@ class SessionController; namespace Iv::Editor { -void ShowBox( +void ShowComposeBox( not_null controller, - not_null peer); + not_null peer, + Api::SendAction action, + Fn sendMenuDetails); +void ShowEditBox( + not_null controller, + not_null item); } // namespace Iv::Editor diff --git a/Telegram/SourceFiles/iv/iv_editor_state.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp similarity index 88% rename from Telegram/SourceFiles/iv/iv_editor_state.cpp rename to Telegram/SourceFiles/iv/editor/iv_editor_state.cpp index 5ff7e19e7a..c172cade76 100644 --- a/Telegram/SourceFiles/iv/iv_editor_state.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.cpp @@ -5,7 +5,7 @@ the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ -#include "iv/iv_editor_state.h" +#include "iv/editor/iv_editor_state.h" #include #include @@ -69,11 +69,61 @@ using TextNodeDescriptor = State::TextNodeDescriptor; return text.trimmed().isEmpty(); } +[[nodiscard]] bool CanEditBlocks(const std::vector &blocks); + +[[nodiscard]] bool CanEditBlock(const Block &block) { + switch (block.kind) { + case BlockKind::Heading: + case BlockKind::Paragraph: + case BlockKind::Footer: + case BlockKind::Divider: + case BlockKind::Anchor: + case BlockKind::Photo: + case BlockKind::Video: + case BlockKind::Audio: + case BlockKind::Math: + case BlockKind::Table: + case BlockKind::Map: + return true; + case BlockKind::Quote: + case BlockKind::Details: + return CanEditBlocks(block.blocks); + case BlockKind::List: + return ranges::all_of(block.listItems, [](const ListItem &item) { + return CanEditBlocks(item.blocks); + }); + case BlockKind::Unsupported: + case BlockKind::Thinking: + case BlockKind::AuthorDate: + case BlockKind::Code: + case BlockKind::Embed: + case BlockKind::EmbedPost: + case BlockKind::GroupedMedia: + case BlockKind::Channel: + case BlockKind::RelatedArticles: + return false; + } + return false; +} + +[[nodiscard]] bool CanEditBlocks(const std::vector &blocks) { + return ranges::all_of(blocks, &CanEditBlock); +} + } // namespace State::State() -: _richPage(std::make_shared()) { - _richPage->blocks.push_back(MakeParagraphBlock()); +: State(std::make_shared(), nullptr) { +} + +State::State( + std::shared_ptr richPage, + std::shared_ptr mediaRuntime) +: _richPage(richPage ? std::move(richPage) : std::make_shared()) +, _mediaRuntime(std::move(mediaRuntime)) { + if (_richPage->blocks.empty()) { + _richPage->blocks.push_back(MakeParagraphBlock()); + } rebuild(); } @@ -283,6 +333,25 @@ void State::insertBlockquoteAfterActive() { } void State::insertBlockAfterActive(InsertAction action) { + auto blocks = std::vector(); + blocks.push_back(makeBlock(action)); + insertBlocksAfterActive(std::move(blocks)); +} + +void State::insertPreparedBlockAfterActive(Block block) { + auto blocks = std::vector(); + blocks.push_back(std::move(block)); + insertBlocksAfterActive(std::move(blocks)); +} + +void State::insertPreparedBlocksAfterActive(std::vector blocks) { + insertBlocksAfterActive(std::move(blocks)); +} + +void State::insertBlocksAfterActive(std::vector blocks) { + if (blocks.empty()) { + return; + } auto anchor = InsertionAnchor{ .container = BlockContainerPath(), .blockIndex = int(_richPage->blocks.size()) - 1, @@ -291,24 +360,25 @@ void State::insertBlockAfterActive(InsertAction action) { if (descriptor) { anchor = descriptor->insertionAnchor; } - auto *blocks = blockContainer(anchor.container); - if (!blocks) { + auto *container = blockContainer(anchor.container); + if (!container) { anchor = { .container = BlockContainerPath(), .blockIndex = int(_richPage->blocks.size()) - 1, }; - blocks = &_richPage->blocks; + container = &_richPage->blocks; } const auto insertAt = std::clamp( anchor.blockIndex + 1, 0, - int(blocks->size())); - blocks->insert(blocks->begin() + insertAt, makeBlock(action)); + int(container->size())); + const auto count = int(blocks.size()); + container->insert( + container->begin() + insertAt, + std::make_move_iterator(blocks.begin()), + std::make_move_iterator(blocks.end())); rebuild(); - focusInsertedBlock({ - .container = anchor.container, - .index = insertAt, - }); + focusInsertedBlocks(anchor.container, insertAt, count); } std::vector *State::blockContainer(const BlockContainerPath &path) { @@ -527,7 +597,7 @@ void State::rebuild() { ensureActiveTextOrdinal(); _prepared = Markdown::TryPrepareNativeInstantView({ .richPage = _richPage, - .mediaRuntime = nullptr, + .mediaRuntime = _mediaRuntime, .editMode = true, }).content; } @@ -550,25 +620,14 @@ void State::rebuildTextNodes( switch (block.kind) { case BlockKind::Heading: case BlockKind::Paragraph: + case BlockKind::Footer: appendBlockTextNode(path, LeafKind::BlockText); break; case BlockKind::Quote: - appendBlockTextNode( - path, - LeafKind::BlockText, - FieldMode::Rich, - InsertionAnchor{ - .container = BlockChildrenContainer(path), - .blockIndex = -1, - }); - appendBlockTextNode( - path, - LeafKind::BlockCaption, - FieldMode::Rich, - InsertionAnchor{ - .container = BlockChildrenContainer(path), - .blockIndex = -1, - }); + if (block.blocks.empty()) { + appendBlockTextNode(path, LeafKind::BlockText); + } + appendBlockTextNode(path, LeafKind::BlockCaption); rebuildTextNodes(block.blocks, BlockChildrenContainer(path)); break; case BlockKind::List: @@ -711,10 +770,18 @@ void State::ensureEditableNodes() { rebuildTextNodes(); } -void State::focusInsertedBlock(const BlockPath &path) { - for (auto i = 0, count = textNodeCount(); i != count; ++i) { - if (descriptorBelongsToBlock(_textNodes[i], path)) { - if (setActiveTextByOrdinal(i)) { +void State::focusInsertedBlocks( + const BlockContainerPath &container, + int from, + int count) { + for (auto blockIndex = from; blockIndex != from + count; ++blockIndex) { + const auto path = BlockPath{ + .container = container, + .index = blockIndex, + }; + for (auto i = 0, textCount = textNodeCount(); i != textCount; ++i) { + if (descriptorBelongsToBlock(_textNodes[i], path) + && setActiveTextByOrdinal(i)) { return; } } @@ -865,7 +932,7 @@ Block State::makeBlock(InsertAction action) const { case InsertBlockType::Math: return MakeMathBlock(); case InsertBlockType::Footer: - return MakeParagraphBlock(); + return MakeFooterBlock(); case InsertBlockType::Divider: return MakeDividerBlock(); case InsertBlockType::Anchor: @@ -907,6 +974,13 @@ Block State::MakeParagraphBlock() { return block; } +Block State::MakeFooterBlock() { + auto block = Block(); + block.kind = BlockKind::Footer; + block.text.text = MakeText(u"Text"_q); + return block; +} + Block State::MakeHeadingBlock(int level) { auto block = Block(); block.kind = BlockKind::Heading; @@ -961,7 +1035,6 @@ Block State::MakeListBlock(ListKind kind, TaskState taskState) { Block State::MakeDetailsBlock() { auto block = Block(); block.kind = BlockKind::Details; - block.open = true; block.text.text = MakeText(u"Header"_q); block.blocks.push_back(MakeParagraphBlock()); return block; @@ -1089,4 +1162,12 @@ TextWithEntities State::StripEditModeWrapperEntities(TextWithEntities text) { return text; } +bool CanEditRichPage(const RichPage &page) { + return CanEditBlocks(page.blocks); +} + +bool CanEditRichPage(const std::shared_ptr &page) { + return page && CanEditRichPage(*page); +} + } // namespace Iv::Editor diff --git a/Telegram/SourceFiles/iv/iv_editor_state.h b/Telegram/SourceFiles/iv/editor/iv_editor_state.h similarity index 92% rename from Telegram/SourceFiles/iv/iv_editor_state.h rename to Telegram/SourceFiles/iv/editor/iv_editor_state.h index 4b0b765e77..721cc4cdab 100644 --- a/Telegram/SourceFiles/iv/iv_editor_state.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_state.h @@ -154,6 +154,9 @@ public: }; State(); + State( + std::shared_ptr richPage, + std::shared_ptr mediaRuntime); [[nodiscard]] const RichPage &richPage() const; [[nodiscard]] const Markdown::MarkdownArticleContent &prepared() const; @@ -177,6 +180,8 @@ public: void insertHeading1AfterActive(); void insertBlockquoteAfterActive(); void insertBlockAfterActive(InsertAction action); + void insertPreparedBlockAfterActive(RichPage::Block block); + void insertPreparedBlocksAfterActive(std::vector blocks); private: [[nodiscard]] std::vector *blockContainer( @@ -212,6 +217,7 @@ private: void rebuildTextNodes( const std::vector &blocks, const BlockContainerPath &container); + void insertBlocksAfterActive(std::vector blocks); void appendBlockTextNode( const BlockPath &path, LeafKind kind, @@ -224,7 +230,10 @@ private: int cellIndex); void ensureActiveTextOrdinal(); void ensureEditableNodes(); - void focusInsertedBlock(const BlockPath &path); + void focusInsertedBlocks( + const BlockContainerPath &container, + int from, + int count); [[nodiscard]] std::optional adjacentEditableOrdinal( bool forward) const; [[nodiscard]] bool descriptorBelongsToBlock( @@ -242,6 +251,7 @@ private: [[nodiscard]] static TextWithEntities MakeText(QString text); [[nodiscard]] static RichPage::Block MakeParagraphBlock(); + [[nodiscard]] static RichPage::Block MakeFooterBlock(); [[nodiscard]] static RichPage::Block MakeHeadingBlock(int level); [[nodiscard]] static RichPage::Block MakeQuoteBlock(bool pullquote); [[nodiscard]] static RichPage::Block MakeMathBlock(); @@ -266,10 +276,15 @@ private: TextWithEntities text); std::shared_ptr _richPage; + std::shared_ptr _mediaRuntime; Markdown::MarkdownArticleContent _prepared; std::vector _textNodes; int _activeTextOrdinal = -1; }; +[[nodiscard]] bool CanEditRichPage(const RichPage &page); +[[nodiscard]] bool CanEditRichPage( + const std::shared_ptr &page); + } // namespace Iv::Editor diff --git a/Telegram/SourceFiles/iv/iv_editor_widget.cpp b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp similarity index 92% rename from Telegram/SourceFiles/iv/iv_editor_widget.cpp rename to Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp index a89b0771cd..e486aaa360 100644 --- a/Telegram/SourceFiles/iv/iv_editor_widget.cpp +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.cpp @@ -5,7 +5,7 @@ the official desktop application for the Telegram messaging service. For license and copyright information please follow this link: https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL */ -#include "iv/iv_editor_widget.h" +#include "iv/editor/iv_editor_widget.h" #include "data/data_msg_id.h" #include "ui/image/image_location.h" @@ -69,6 +69,42 @@ namespace { return st::ivEditorBodyPadding; } +void EnableQTextEditLineMetrics(style::TextStyle &style) { + style.qtextEditLineMetrics = true; +} + +void EnableQTextEditLineMetrics(style::Markdown &style) { + EnableQTextEditLineMetrics(style.body); + EnableQTextEditLineMetrics(style.heading1); + EnableQTextEditLineMetrics(style.heading2); + EnableQTextEditLineMetrics(style.heading3); + EnableQTextEditLineMetrics(style.heading4); + EnableQTextEditLineMetrics(style.heading5); + EnableQTextEditLineMetrics(style.heading6); + EnableQTextEditLineMetrics(style.code); + EnableQTextEditLineMetrics(style.displayMath.fallbackStyle); + EnableQTextEditLineMetrics(style.table.headerStyle); + EnableQTextEditLineMetrics(style.table.bodyStyle); + EnableQTextEditLineMetrics(style.details.summaryStyle); + EnableQTextEditLineMetrics(style.embedPost.authorStyle); + EnableQTextEditLineMetrics(style.embedPost.dateStyle); + EnableQTextEditLineMetrics(style.placeholder.labelStyle); + EnableQTextEditLineMetrics(style.audio.titleStyle); + EnableQTextEditLineMetrics(style.audio.subtitleStyle); + EnableQTextEditLineMetrics(style.channel.titleStyle); + EnableQTextEditLineMetrics(style.channel.subtitleStyle); + EnableQTextEditLineMetrics(style.channel.button.textStyle); + EnableQTextEditLineMetrics(style.relatedArticle.titleStyle); + EnableQTextEditLineMetrics(style.relatedArticle.subtitleStyle); + EnableQTextEditLineMetrics(style.relatedArticle.footerStyle); +} + +[[nodiscard]] style::Markdown CreateEditorMarkdownStyle() { + auto result = st::messageMarkdown; + EnableQTextEditLineMetrics(result); + return result; +} + [[nodiscard]] int CompareSelectionPositions( Markdown::MarkdownArticleSelectionPosition a, Markdown::MarkdownArticleSelectionPosition b) { @@ -120,7 +156,9 @@ Widget::Widget( , _controller(controller) , _peer(peer) , _state(std::move(state)) -, _article(std::make_shared(st::messageMarkdown)) +, _articleStyle(std::make_shared( + CreateEditorMarkdownStyle())) +, _article(std::make_shared(*_articleStyle)) , _theme(CreateStandaloneChatTheme()) , _style(std::make_unique(style::main_palette::get())) { _style->apply(_theme.get()); @@ -225,24 +263,20 @@ void Widget::insertBlock(State::InsertAction action) { activateTextOrdinal(_state->activeTextOrdinal(), 0); } -void Widget::insertMedia(State::InsertBlockType type) { - switch (type) { - case State::InsertBlockType::Photo: - case State::InsertBlockType::Video: - case State::InsertBlockType::Audio: - insertBlock({ .type = type }); - break; - default: - break; - } +void Widget::insertPreparedBlock(RichPage::Block block) { + auto blocks = std::vector(); + blocks.push_back(std::move(block)); + insertPreparedBlocks(std::move(blocks)); } -void Widget::insertMap(double latitude, double longitude) { - insertBlock({ - .type = State::InsertBlockType::Map, - .latitude = latitude, - .longitude = longitude, - }); +void Widget::insertPreparedBlocks(std::vector blocks) { + if (blocks.empty()) { + return; + } + commitInlineField(); + _state->insertPreparedBlocksAfterActive(std::move(blocks)); + refreshPreparedContent(); + activateTextOrdinal(_state->activeTextOrdinal(), 0); } void Widget::insertHeading1() { @@ -517,14 +551,14 @@ Widget::InlineFieldStyleData Widget::normalizedInlineFieldStyle( const auto valid = leafStyle.valid(); const auto textStyle = valid ? leafStyle.textStyle - : &st::messageMarkdown.body; + : &_articleStyle->body; const auto lineHeight = (valid && leafStyle.lineHeight > 0) ? leafStyle.lineHeight : std::max(textStyle->lineHeight, textStyle->font->height); return { .textStyle = textStyle, .lineHeight = lineHeight, - .textFg = valid ? leafStyle.textColor : st::messageMarkdown.textColor, + .textFg = valid ? leafStyle.textColor : _articleStyle->textColor, .align = valid ? leafStyle.align : style::al_left, .italic = valid ? leafStyle.italic : false, }; @@ -534,7 +568,7 @@ Widget::InlineFieldStyleKey Widget::inlineFieldStyleKey( const InlineFieldStyleData &data) const { const auto textStyle = data.textStyle ? data.textStyle - : &st::messageMarkdown.body; + : &_articleStyle->body; return { .font = data.italic ? textStyle->font->italic() diff --git a/Telegram/SourceFiles/iv/iv_editor_widget.h b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h similarity index 96% rename from Telegram/SourceFiles/iv/iv_editor_widget.h rename to Telegram/SourceFiles/iv/editor/iv_editor_widget.h index 5ad782b2eb..1f6dab89ac 100644 --- a/Telegram/SourceFiles/iv/iv_editor_widget.h +++ b/Telegram/SourceFiles/iv/editor/iv_editor_widget.h @@ -8,7 +8,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #pragma once #include "base/unique_qptr.h" -#include "iv/iv_editor_state.h" +#include "iv/editor/iv_editor_state.h" #include "iv/markdown/iv_markdown_article.h" #include "ui/style/style_core_types.h" #include "ui/rp_widget.h" @@ -29,6 +29,7 @@ class InputField; namespace style { struct InputField; +struct Markdown; } // namespace style class PeerData; @@ -53,8 +54,8 @@ public: void refreshPreparedContent(); void syncInlineFieldGeometry(); void insertBlock(State::InsertAction action); - void insertMedia(State::InsertBlockType type); - void insertMap(double latitude, double longitude); + void insertPreparedBlock(RichPage::Block block); + void insertPreparedBlocks(std::vector blocks); void insertHeading1(); void insertBlockquote(); @@ -149,6 +150,7 @@ private: const not_null _controller; const not_null _peer; const std::shared_ptr _state; + std::shared_ptr _articleStyle; std::shared_ptr _article; base::unique_qptr _field; std::unique_ptr _theme; diff --git a/Telegram/SourceFiles/iv/iv.style b/Telegram/SourceFiles/iv/iv.style index 69c48d8152..4490f33ce8 100644 --- a/Telegram/SourceFiles/iv/iv.style +++ b/Telegram/SourceFiles/iv/iv.style @@ -691,27 +691,27 @@ defaultMarkdown: Markdown { body: defaultMarkdownBodyStyle; heading1: TextStyle(defaultMarkdownBodyStyle) { font: font(30px semibold); - lineHeight: 36px; + lineHeight: 42px; } heading2: TextStyle(defaultMarkdownBodyStyle) { font: font(26px semibold); - lineHeight: 32px; + lineHeight: 36px; } heading3: TextStyle(defaultMarkdownBodyStyle) { font: font(22px semibold); - lineHeight: 28px; + lineHeight: 31px; } heading4: TextStyle(defaultMarkdownBodyStyle) { font: font(19px semibold); - lineHeight: 25px; + lineHeight: 27px; } heading5: TextStyle(defaultMarkdownBodyStyle) { font: font(17px semibold); - lineHeight: 23px; + lineHeight: 24px; } heading6: TextStyle(defaultMarkdownBodyStyle) { font: font(15px semibold); - lineHeight: 21px; + lineHeight: 22px; } code: defaultMarkdownCodeStyle; pagePadding: margins(0px, 0px, 0px, 16px); @@ -752,27 +752,27 @@ messageMarkdownDisplayMathFallbackStyle: TextStyle(messageMarkdownCodeStyle) { } messageMarkdownHeading1Style: TextStyle(messageMarkdownBodyStyle) { font: font(19px semibold); - lineHeight: 24px; + lineHeight: 27px; } messageMarkdownHeading2Style: TextStyle(messageMarkdownBodyStyle) { font: font(18px semibold); - lineHeight: 23px; + lineHeight: 26px; } messageMarkdownHeading3Style: TextStyle(messageMarkdownBodyStyle) { font: font(17px semibold); - lineHeight: 22px; + lineHeight: 24px; } messageMarkdownHeading4Style: TextStyle(messageMarkdownBodyStyle) { font: font(16px semibold); - lineHeight: 21px; + lineHeight: 23px; } messageMarkdownHeading5Style: TextStyle(messageMarkdownBodyStyle) { font: font(15px semibold); - lineHeight: 20px; + lineHeight: 22px; } messageMarkdownHeading6Style: TextStyle(messageMarkdownBodyStyle) { font: font(14px semibold); - lineHeight: 19px; + lineHeight: 21px; } messageMarkdownDetailsSummaryStyle: TextStyle(messageMarkdownBodyStyle) { linkUnderline: kLinkUnderlineNever; diff --git a/Telegram/SourceFiles/iv/iv_rich_message_serializer.cpp b/Telegram/SourceFiles/iv/iv_rich_message_serializer.cpp new file mode 100644 index 0000000000..800739d2ac --- /dev/null +++ b/Telegram/SourceFiles/iv/iv_rich_message_serializer.cpp @@ -0,0 +1,1006 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#include "iv/iv_rich_message_serializer.h" + +#include "base/flat_map.h" +#include "data/data_document.h" +#include "data/data_photo.h" +#include "data/data_session.h" +#include "data/data_user.h" +#include "data/stickers/data_custom_emoji.h" +#include "history/history.h" +#include "history/history_item.h" +#include "iv/markdown/iv_markdown_prepare.h" +#include "iv/markdown/iv_markdown_prepare_links.h" +#include "iv/markdown/iv_markdown_prepare_serialize.h" +#include "main/main_session.h" +#include "ui/text/text_utilities.h" + +#include + +namespace Iv { +namespace { + +using Block = RichPage::Block; +using BlockKind = RichPage::BlockKind; +using ListKind = RichPage::ListKind; +using RichText = RichPage::RichText; +using TableAlignment = RichPage::TableAlignment; +using TableCell = RichPage::TableCell; +using TableVerticalAlignment = RichPage::TableVerticalAlignment; +using TaskState = RichPage::TaskState; + +struct SerializeContext { + not_null session; + base::flat_map photos; + base::flat_map documents; + base::flat_map users; +}; + +[[nodiscard]] int EntitySerializationOrder(EntityType type) { + switch (type) { + case EntityType::CustomUrl: return 0; + case EntityType::MentionName: return 1; + case EntityType::Bold: return 2; + case EntityType::Italic: return 3; + case EntityType::Underline: return 4; + case EntityType::StrikeOut: return 5; + case EntityType::Code: return 6; + case EntityType::Subscript: return 7; + case EntityType::Superscript: return 8; + case EntityType::Marked: return 9; + case EntityType::Spoiler: return 10; + case EntityType::CustomEmoji: return 11; + case EntityType::FormattedDate: return 12; + case EntityType::Mention: return 13; + case EntityType::Hashtag: return 14; + case EntityType::BotCommand: return 15; + case EntityType::Cashtag: return 16; + case EntityType::Url: return 17; + case EntityType::Email: return 18; + case EntityType::Phone: return 19; + case EntityType::BankCard: return 20; + case EntityType::Invalid: + case EntityType::Semibold: + case EntityType::MediaTimestamp: + case EntityType::Colorized: + case EntityType::Pre: + case EntityType::Blockquote: + break; + } + return 100; +} + +[[nodiscard]] MTPRichText MakePlainRichText(const QString &text) { + return text.isEmpty() + ? MTP_textEmpty() + : MTP_textPlain(MTP_string(text)); +} + +[[nodiscard]] MTPRichText JoinRichTextParts(QVector &&parts) { + if (parts.isEmpty()) { + return MTP_textEmpty(); + } else if (parts.size() == 1) { + return std::move(parts.front()); + } + return MTP_textConcat(MTP_vector(std::move(parts))); +} + +[[nodiscard]] MTPRichText WrapRichTextAnchor( + MTPRichText text, + const QString &anchorId) { + return anchorId.isEmpty() + ? text + : MTP_textAnchor(std::move(text), MTP_string(anchorId)); +} + +[[nodiscard]] bool HasRichTextContent(const RichText &text) { + return !text.text.empty() || !text.anchorId.isEmpty(); +} + +[[nodiscard]] PhotoData *ResolvePhotoData( + SerializeContext *context, + uint64 id, + PhotoData *photo) { + return photo + ? photo + : (id ? context->session->data().photo(id).get() : nullptr); +} + +[[nodiscard]] DocumentData *ResolveDocumentData( + SerializeContext *context, + uint64 id, + DocumentData *document) { + return document + ? document + : (id ? context->session->data().document(id).get() : nullptr); +} + +[[nodiscard]] std::optional ResolveInputPhoto( + SerializeContext *context, + uint64 id, + PhotoData *photo) { + const auto resolved = ResolvePhotoData(context, id, photo); + if (!resolved) { + return std::nullopt; + } + const auto input = resolved->mtpInput(); + return (input.type() == mtpc_inputPhoto + && input.c_inputPhoto().vid().v + && input.c_inputPhoto().vaccess_hash().v + && !resolved->fileReference().isEmpty()) + ? std::make_optional(input) + : std::nullopt; +} + +[[nodiscard]] std::optional ResolveInputDocument( + SerializeContext *context, + uint64 id, + DocumentData *document) { + const auto resolved = ResolveDocumentData(context, id, document); + if (!resolved) { + return std::nullopt; + } + const auto input = resolved->mtpInput(); + return (resolved->hasRemoteLocation() + && input.type() == mtpc_inputDocument + && input.c_inputDocument().vid().v + && input.c_inputDocument().vaccess_hash().v + && !resolved->fileReference().isEmpty()) + ? std::make_optional(input) + : std::nullopt; +} + +[[nodiscard]] std::optional CollectPhoto( + SerializeContext *context, + uint64 id, + PhotoData *photo) { + if (const auto input = ResolveInputPhoto(context, id, photo)) { + const auto serverId = uint64(input->c_inputPhoto().vid().v); + context->photos.emplace(serverId, *input); + return serverId; + } + return std::nullopt; +} + +[[nodiscard]] std::optional CollectDocument( + SerializeContext *context, + uint64 id, + DocumentData *document = nullptr) { + if (const auto input = ResolveInputDocument(context, id, document)) { + const auto serverId = uint64(input->c_inputDocument().vid().v); + context->documents.emplace(serverId, *input); + return serverId; + } + return std::nullopt; +} + +[[nodiscard]] std::optional CollectMentionUser( + SerializeContext *context, + const QString &data) { + const auto fields = TextUtilities::MentionNameDataToFields(data); + if (!fields.userId || fields.selfId != context->session->userId().bare) { + return std::nullopt; + } + if (context->users.find(fields.userId) != end(context->users)) { + return fields.userId; + } + if (fields.userId == fields.selfId) { + context->users.emplace(fields.userId, MTP_inputUserSelf()); + return fields.userId; + } + const auto user = context->session->data().user(UserId(fields.userId)); + if (user->isLoaded()) { + context->users.emplace(fields.userId, user->inputUser()); + return fields.userId; + } + if (const auto item = user->owner().messageWithPeer(user->id)) { + context->users.emplace( + fields.userId, + MTP_inputUserFromMessage( + item->history()->peer->input(), + MTP_int(int(item->id.bare)), + MTP_long(fields.userId))); + return fields.userId; + } + if (!fields.accessHash) { + return std::nullopt; + } + context->users.emplace( + fields.userId, + MTP_inputUser( + MTP_long(fields.userId), + MTP_long(fields.accessHash))); + return fields.userId; +} + +[[nodiscard]] std::vector SortedRichTextEntities( + const TextWithEntities &text) { + auto result = std::vector(); + result.reserve(text.entities.size()); + const auto textLength = text.text.size(); + for (const auto &entity : text.entities) { + const auto till = entity.offset() + entity.length(); + if (entity.offset() < 0 + || entity.length() <= 0 + || till > textLength) { + continue; + } + result.push_back(entity); + } + std::sort(result.begin(), result.end(), [](const EntityInText &a, const EntityInText &b) { + if (a.offset() != b.offset()) { + return a.offset() < b.offset(); + } + if (a.length() != b.length()) { + return a.length() > b.length(); + } + return EntitySerializationOrder(a.type()) + < EntitySerializationOrder(b.type()); + }); + return result; +} + +[[nodiscard]] const EntityInText *FindOuterEntityAt( + const std::vector &entities, + int position, + int till, + const EntityInText *skip) { + for (const auto &entity : entities) { + if (&entity == skip) { + continue; + } + if (entity.offset() == position + && entity.offset() + entity.length() <= till) { + return &entity; + } + if (entity.offset() > position) { + break; + } + } + return nullptr; +} + +[[nodiscard]] std::optional SerializeRichTextRange( + const QString &text, + const std::vector &entities, + int from, + int till, + SerializeContext *context, + const EntityInText *skip); + +[[nodiscard]] std::optional SerializeRichTextEntity( + const QString &text, + const std::vector &entities, + const EntityInText &entity, + SerializeContext *context) { + const auto from = entity.offset(); + const auto length = entity.length(); + const auto segment = text.mid(from, length); + const auto inner = SerializeRichTextRange( + text, + entities, + from, + from + length, + context, + &entity); + if (!inner) { + return std::nullopt; + } + switch (entity.type()) { + case EntityType::Bold: + return MTP_textBold(*inner); + case EntityType::Italic: + return MTP_textItalic(*inner); + case EntityType::Underline: + return MTP_textUnderline(*inner); + case EntityType::StrikeOut: + return MTP_textStrike(*inner); + case EntityType::Code: + return MTP_textFixed(*inner); + case EntityType::Subscript: + return MTP_textSubscript(*inner); + case EntityType::Superscript: + return MTP_textSuperscript(*inner); + case EntityType::Marked: + return MTP_textMarked(*inner); + case EntityType::Spoiler: + return MTP_textSpoiler(*inner); + case EntityType::Mention: + return MTP_textMention(*inner); + case EntityType::Hashtag: + return MTP_textHashtag(*inner); + case EntityType::BotCommand: + return MTP_textBotCommand(*inner); + case EntityType::Cashtag: + return MTP_textCashtag(*inner); + case EntityType::Url: + return MTP_textAutoUrl(*inner); + case EntityType::Email: + return MTP_textAutoEmail(*inner); + case EntityType::Phone: + return MTP_textAutoPhone(*inner); + case EntityType::BankCard: + return MTP_textBankCard(*inner); + case EntityType::CustomUrl: { + const auto data = entity.data(); + if (data.startsWith(u"mailto:"_q)) { + return MTP_textEmail(*inner, MTP_string(data.mid(7))); + } else if (data.startsWith(u"tel:"_q)) { + return MTP_textPhone(*inner, MTP_string(data.mid(4))); + } + const auto decoded = DecodeRichPageLinkUrl(data); + return MTP_textUrl( + *inner, + MTP_string(decoded ? decoded->url : data), + MTP_long(decoded ? decoded->webpageId : 0)); + } + case EntityType::MentionName: { + const auto userId = CollectMentionUser(context, entity.data()); + return userId + ? std::make_optional(MTP_textMentionName( + *inner, + MTP_long(*userId))) + : std::nullopt; + } + case EntityType::CustomEmoji: { + if (const auto parsed = Markdown::ParseInlineTextObjectEntity( + entity.data())) { + switch (parsed->kind) { + case Markdown::InlineTextObjectKind::Formula: { + const auto formula = std::get_if< + Markdown::InlineTextObjectFormulaData>(&parsed->data); + if (!formula) { + return std::nullopt; + } + const auto source = !formula->copySource.isEmpty() + ? formula->copySource + : formula->trimmedTex; + return source.isEmpty() + ? std::optional( + MakePlainRichText(segment)) + : std::optional( + MTP_textMath(MTP_string(source))); + } + case Markdown::InlineTextObjectKind::IvImage: { + const auto image = std::get_if< + Markdown::InlineTextObjectIvImageData>(&parsed->data); + const auto documentId = image + ? CollectDocument(context, image->documentId) + : std::nullopt; + return (image + && documentId + && image->width > 0 + && image->height > 0) + ? std::make_optional(MTP_textImage( + MTP_long(*documentId), + MTP_int(image->width), + MTP_int(image->height))) + : std::nullopt; + } + } + } + const auto documentId = ::Data::ParseCustomEmojiData(entity.data()); + const auto collected = documentId + ? CollectDocument(context, documentId) + : std::nullopt; + return collected + ? std::optional(MTP_textCustomEmoji( + MTP_long(*collected), + MTP_string(segment.isEmpty() ? u"@"_q : segment))) + : std::optional(MakePlainRichText(segment)); + } + case EntityType::FormattedDate: { + const auto [date, flags] = DeserializeFormattedDateData(entity.data()); + if (!date) { + return *inner; + } + using Flag = MTPDtextDate::Flag; + auto mtpFlags = MTPDtextDate::Flags(); + if (flags & FormattedDateFlag::Relative) { + mtpFlags |= Flag::f_relative; + } + if (flags & FormattedDateFlag::ShortTime) { + mtpFlags |= Flag::f_short_time; + } + if (flags & FormattedDateFlag::LongTime) { + mtpFlags |= Flag::f_long_time; + } + if (flags & FormattedDateFlag::ShortDate) { + mtpFlags |= Flag::f_short_date; + } + if (flags & FormattedDateFlag::LongDate) { + mtpFlags |= Flag::f_long_date; + } + if (flags & FormattedDateFlag::DayOfWeek) { + mtpFlags |= Flag::f_day_of_week; + } + return MTP_textDate( + MTP_flags(mtpFlags), + *inner, + MTP_int(date)); + } + case EntityType::Invalid: + case EntityType::Semibold: + case EntityType::MediaTimestamp: + case EntityType::Colorized: + case EntityType::Pre: + case EntityType::Blockquote: + break; + } + return *inner; +} + +[[nodiscard]] std::optional SerializeRichTextRange( + const QString &text, + const std::vector &entities, + int from, + int till, + SerializeContext *context, + const EntityInText *skip) { + auto parts = QVector(); + auto position = from; + while (position < till) { + auto nextEntityStart = till; + for (const auto &entity : entities) { + if (&entity == skip) { + continue; + } + if (entity.offset() >= position + && entity.offset() + entity.length() <= till) { + nextEntityStart = entity.offset(); + break; + } + } + if (nextEntityStart > position) { + parts.push_back(MakePlainRichText( + text.mid(position, nextEntityStart - position))); + position = nextEntityStart; + continue; + } + const auto entity = FindOuterEntityAt( + entities, + position, + till, + skip); + if (!entity) { + parts.push_back(MakePlainRichText(text.mid(position, 1))); + ++position; + continue; + } + const auto wrapped = SerializeRichTextEntity( + text, + entities, + *entity, + context); + if (!wrapped) { + return std::nullopt; + } + parts.push_back(*wrapped); + position = entity->offset() + entity->length(); + } + return JoinRichTextParts(std::move(parts)); +} + +[[nodiscard]] std::optional SerializeRichTextWithAnchor( + const RichText &text, + const QString &anchorId, + SerializeContext *context) { + const auto entities = SortedRichTextEntities(text.text); + auto result = SerializeRichTextRange( + text.text.text, + entities, + 0, + text.text.text.size(), + context, + nullptr); + if (!result) { + return std::nullopt; + } + *result = WrapRichTextAnchor(std::move(*result), text.anchorId); + *result = WrapRichTextAnchor(std::move(*result), anchorId); + return result; +} + +[[nodiscard]] std::optional SerializeCaption( + const RichText &caption, + const QString &anchorId, + SerializeContext *context) { + const auto text = SerializeRichTextWithAnchor(caption, anchorId, context); + return text + ? std::make_optional(MTP_pageCaption(*text, MTP_textEmpty())) + : std::nullopt; +} + +[[nodiscard]] std::optional> SerializeBlocks( + const std::vector &blocks, + SerializeContext *context); + +[[nodiscard]] std::optional SerializeParagraphBlock( + const RichText &text, + const QString &anchorId, + SerializeContext *context) { + const auto serialized = SerializeRichTextWithAnchor(text, anchorId, context); + return serialized + ? std::make_optional(MTP_pageBlockParagraph(*serialized)) + : std::nullopt; +} + +[[nodiscard]] std::optional SerializeTableCell( + const TableCell &cell, + SerializeContext *context) { + using Flag = MTPDpageTableCell::Flag; + auto flags = MTPDpageTableCell::Flags(); + if (cell.header) { + flags |= Flag::f_header; + } + switch (cell.alignment) { + case TableAlignment::Center: + flags |= Flag::f_align_center; + break; + case TableAlignment::Right: + flags |= Flag::f_align_right; + break; + case TableAlignment::Left: + break; + } + switch (cell.verticalAlignment) { + case TableVerticalAlignment::Middle: + flags |= Flag::f_valign_middle; + break; + case TableVerticalAlignment::Bottom: + flags |= Flag::f_valign_bottom; + break; + case TableVerticalAlignment::Top: + break; + } + const auto colspan = std::max(cell.colspan, 1); + const auto rowspan = std::max(cell.rowspan, 1); + if (colspan != 1) { + flags |= Flag::f_colspan; + } + if (rowspan != 1) { + flags |= Flag::f_rowspan; + } + const auto hasText = HasRichTextContent(cell.text); + auto text = MTPRichText(MTP_textEmpty()); + if (hasText) { + flags |= Flag::f_text; + const auto serialized = SerializeRichTextWithAnchor( + cell.text, + QString(), + context); + if (!serialized) { + return std::nullopt; + } + text = *serialized; + } + return MTP_pageTableCell( + MTP_flags(flags), + std::move(text), + (colspan != 1 ? MTP_int(colspan) : MTPint()), + (rowspan != 1 ? MTP_int(rowspan) : MTPint())); +} + +[[nodiscard]] std::optional SerializeBlock( + const Block &block, + SerializeContext *context) { + switch (block.kind) { + case BlockKind::Heading: { + const auto text = SerializeRichTextWithAnchor( + block.text, + block.anchorId, + context); + if (!text) { + return std::nullopt; + } + switch (std::clamp(block.headingLevel, 1, 6)) { + case 1: return MTP_pageBlockHeading1(*text); + case 2: return MTP_pageBlockHeading2(*text); + case 3: return MTP_pageBlockHeading3(*text); + case 4: return MTP_pageBlockHeading4(*text); + case 5: return MTP_pageBlockHeading5(*text); + case 6: return MTP_pageBlockHeading6(*text); + } + return std::nullopt; + } + case BlockKind::Paragraph: { + return SerializeParagraphBlock(block.text, block.anchorId, context); + } + case BlockKind::Footer: { + const auto text = SerializeRichTextWithAnchor( + block.text, + block.anchorId, + context); + return text + ? std::make_optional(MTP_pageBlockFooter(*text)) + : std::nullopt; + } + case BlockKind::Divider: + return MTP_pageBlockDivider(); + case BlockKind::Anchor: + return block.anchorId.isEmpty() + ? std::nullopt + : std::make_optional(MTP_pageBlockAnchor( + MTP_string(block.anchorId))); + case BlockKind::Quote: { + const auto caption = SerializeRichTextWithAnchor( + block.caption, + block.blocks.empty() ? QString() : block.anchorId, + context); + if (!caption) { + return std::nullopt; + } + if (block.pullquote) { + if (!block.blocks.empty()) { + return std::nullopt; + } + const auto text = SerializeRichTextWithAnchor( + block.text, + block.anchorId, + context); + return text + ? std::make_optional(MTP_pageBlockPullquote( + *text, + *caption)) + : std::nullopt; + } + if (block.blocks.empty()) { + const auto text = SerializeRichTextWithAnchor( + block.text, + block.anchorId, + context); + return text + ? std::make_optional(MTP_pageBlockBlockquote( + *text, + *caption)) + : std::nullopt; + } + if (HasRichTextContent(block.text)) { + return std::nullopt; + } + auto blocks = QVector(); + const auto nested = SerializeBlocks(block.blocks, context); + if (!nested) { + return std::nullopt; + } + blocks += *nested; + return MTP_pageBlockBlockquoteBlocks( + MTP_vector(std::move(blocks)), + *caption); + } + case BlockKind::List: { + if (block.listKind == ListKind::Ordered) { + auto items = QVector(); + items.reserve(block.listItems.size()); + for (auto i = 0, count = int(block.listItems.size()); i != count; ++i) { + const auto &item = block.listItems[i]; + const auto number = !item.number.isEmpty() + ? item.number + : QString::number(i + 1); + if (!item.blocks.empty()) { + using Flag = MTPDpageListOrderedItemBlocks::Flag; + auto flags = MTPDpageListOrderedItemBlocks::Flags(); + if (item.taskState != TaskState::None) { + flags |= Flag::f_checkbox; + } + if (item.taskState == TaskState::Checked) { + flags |= Flag::f_checked; + } + flags |= Flag::f_num; + auto blocks = QVector(); + if (HasRichTextContent(item.text) || !item.anchorId.isEmpty()) { + const auto paragraph = SerializeParagraphBlock( + item.text, + item.anchorId, + context); + if (!paragraph) { + return std::nullopt; + } + blocks.push_back(*paragraph); + } + const auto nested = SerializeBlocks(item.blocks, context); + if (!nested) { + return std::nullopt; + } + blocks += *nested; + items.push_back(MTP_pageListOrderedItemBlocks( + MTP_flags(flags), + MTP_string(number), + MTP_vector(std::move(blocks)), + MTPint(), + MTPstring())); + } else { + using Flag = MTPDpageListOrderedItemText::Flag; + auto flags = MTPDpageListOrderedItemText::Flags(); + if (item.taskState != TaskState::None) { + flags |= Flag::f_checkbox; + } + if (item.taskState == TaskState::Checked) { + flags |= Flag::f_checked; + } + flags |= Flag::f_num; + const auto text = SerializeRichTextWithAnchor( + item.text, + item.anchorId, + context); + if (!text) { + return std::nullopt; + } + items.push_back(MTP_pageListOrderedItemText( + MTP_flags(flags), + MTP_string(number), + *text, + MTPint(), + MTPstring())); + } + } + return MTP_pageBlockOrderedList( + MTP_flags(0), + MTP_vector(std::move(items)), + MTPint(), + MTPstring()); + } + auto items = QVector(); + items.reserve(block.listItems.size()); + for (const auto &item : block.listItems) { + if (!item.blocks.empty()) { + using Flag = MTPDpageListItemBlocks::Flag; + auto flags = MTPDpageListItemBlocks::Flags(); + if (item.taskState != TaskState::None) { + flags |= Flag::f_checkbox; + } + if (item.taskState == TaskState::Checked) { + flags |= Flag::f_checked; + } + auto blocks = QVector(); + if (HasRichTextContent(item.text) || !item.anchorId.isEmpty()) { + const auto paragraph = SerializeParagraphBlock( + item.text, + item.anchorId, + context); + if (!paragraph) { + return std::nullopt; + } + blocks.push_back(*paragraph); + } + const auto nested = SerializeBlocks(item.blocks, context); + if (!nested) { + return std::nullopt; + } + blocks += *nested; + items.push_back(MTP_pageListItemBlocks( + MTP_flags(flags), + MTP_vector(std::move(blocks)))); + } else { + using Flag = MTPDpageListItemText::Flag; + auto flags = MTPDpageListItemText::Flags(); + if (item.taskState != TaskState::None) { + flags |= Flag::f_checkbox; + } + if (item.taskState == TaskState::Checked) { + flags |= Flag::f_checked; + } + const auto text = SerializeRichTextWithAnchor( + item.text, + item.anchorId, + context); + if (!text) { + return std::nullopt; + } + items.push_back(MTP_pageListItemText(MTP_flags(flags), *text)); + } + } + return MTP_pageBlockList(MTP_vector(std::move(items))); + } + case BlockKind::Photo: { + const auto photoId = CollectPhoto(context, block.photoId, block.photo); + const auto caption = SerializeCaption(block.caption, block.anchorId, context); + if (!photoId || !caption) { + return std::nullopt; + } + using Flag = MTPDpageBlockPhoto::Flag; + auto flags = MTPDpageBlockPhoto::Flags(); + if (block.spoiler) { + flags |= Flag::f_spoiler; + } + if (!block.url.isEmpty()) { + flags |= Flag::f_url; + } + return MTP_pageBlockPhoto( + MTP_flags(flags), + MTP_long(*photoId), + *caption, + (block.url.isEmpty() ? MTPstring() : MTP_string(block.url)), + (block.url.isEmpty() ? MTPlong() : MTP_long(0))); + } + case BlockKind::Video: { + const auto documentId = CollectDocument( + context, + block.documentId, + block.document); + const auto caption = SerializeCaption(block.caption, block.anchorId, context); + if (!documentId || !caption) { + return std::nullopt; + } + using Flag = MTPDpageBlockVideo::Flag; + auto flags = MTPDpageBlockVideo::Flags(); + if (block.autoplay) { + flags |= Flag::f_autoplay; + } + if (block.loop) { + flags |= Flag::f_loop; + } + if (block.spoiler) { + flags |= Flag::f_spoiler; + } + return MTP_pageBlockVideo( + MTP_flags(flags), + MTP_long(*documentId), + *caption); + } + case BlockKind::Audio: { + const auto documentId = CollectDocument( + context, + block.documentId, + block.document); + const auto caption = SerializeCaption(block.caption, block.anchorId, context); + return (documentId && caption) + ? std::make_optional(MTP_pageBlockAudio( + MTP_long(*documentId), + *caption)) + : std::nullopt; + } + case BlockKind::Math: + return MTP_pageBlockMath(MTP_string(block.formula)); + case BlockKind::Table: { + using Flag = MTPDpageBlockTable::Flag; + auto flags = MTPDpageBlockTable::Flags(); + if (block.bordered) { + flags |= Flag::f_bordered; + } + if (block.striped) { + flags |= Flag::f_striped; + } + const auto title = SerializeRichTextWithAnchor( + block.text, + block.anchorId, + context); + if (!title) { + return std::nullopt; + } + auto rows = QVector(); + rows.reserve(block.tableRows.size()); + for (const auto &row : block.tableRows) { + auto cells = QVector(); + cells.reserve(row.cells.size()); + for (const auto &cell : row.cells) { + const auto serialized = SerializeTableCell(cell, context); + if (!serialized) { + return std::nullopt; + } + cells.push_back(*serialized); + } + rows.push_back(MTP_pageTableRow( + MTP_vector(std::move(cells)))); + } + return MTP_pageBlockTable( + MTP_flags(flags), + *title, + MTP_vector(std::move(rows))); + } + case BlockKind::Details: { + using Flag = MTPDpageBlockDetails::Flag; + auto flags = block.open ? Flag::f_open : Flag(); + const auto title = SerializeRichTextWithAnchor( + block.text, + block.anchorId, + context); + const auto blocks = SerializeBlocks(block.blocks, context); + return (title && blocks) + ? std::make_optional(MTP_pageBlockDetails( + MTP_flags(flags), + MTP_vector(*blocks), + *title)) + : std::nullopt; + } + case BlockKind::Map: { + if (block.width <= 0 || block.height <= 0 || block.zoom <= 0) { + return std::nullopt; + } + const auto caption = SerializeCaption(block.caption, block.anchorId, context); + return caption + ? std::make_optional(MTP_inputPageBlockMap( + MTP_inputGeoPoint( + MTP_flags(0), + MTP_double(block.latitude), + MTP_double(block.longitude), + MTPint()), + MTP_int(block.zoom), + MTP_int(block.width), + MTP_int(block.height), + *caption)) + : std::nullopt; + } + case BlockKind::Unsupported: + case BlockKind::Thinking: + case BlockKind::AuthorDate: + case BlockKind::Code: + case BlockKind::Embed: + case BlockKind::EmbedPost: + case BlockKind::GroupedMedia: + case BlockKind::Channel: + case BlockKind::RelatedArticles: + break; + } + return std::nullopt; +} + +[[nodiscard]] std::optional> SerializeBlocks( + const std::vector &blocks, + SerializeContext *context) { + auto result = QVector(); + result.reserve(blocks.size()); + for (const auto &block : blocks) { + const auto serialized = SerializeBlock(block, context); + if (!serialized) { + return std::nullopt; + } + result.push_back(*serialized); + } + return result; +} + +} // namespace + +std::optional SerializeInputRichMessage( + not_null session, + const RichPage &page) { + auto context = SerializeContext{ session }; + auto blocks = SerializeBlocks(page.blocks, &context); + if (!blocks) { + return std::nullopt; + } + auto photos = QVector(); + photos.reserve(context.photos.size()); + for (const auto &[id, input] : context.photos) { + photos.push_back(input); + } + auto documents = QVector(); + documents.reserve(context.documents.size()); + for (const auto &[id, input] : context.documents) { + documents.push_back(input); + } + auto users = QVector(); + users.reserve(context.users.size()); + for (const auto &[id, input] : context.users) { + users.push_back(input); + } + using Flag = MTPDinputRichMessage::Flag; + auto flags = MTPDinputRichMessage::Flags(); + if (page.rtl) { + flags |= Flag::f_rtl; + } + if (!photos.isEmpty()) { + flags |= Flag::f_photos; + } + if (!documents.isEmpty()) { + flags |= Flag::f_documents; + } + if (!users.isEmpty()) { + flags |= Flag::f_users; + } + return MTP_inputRichMessage( + MTP_flags(flags), + MTP_vector(std::move(*blocks)), + MTP_vector(std::move(photos)), + MTP_vector(std::move(documents)), + MTP_vector(std::move(users))); +} + +} // namespace Iv diff --git a/Telegram/SourceFiles/iv/iv_rich_message_serializer.h b/Telegram/SourceFiles/iv/iv_rich_message_serializer.h new file mode 100644 index 0000000000..298b84e4fa --- /dev/null +++ b/Telegram/SourceFiles/iv/iv_rich_message_serializer.h @@ -0,0 +1,24 @@ +/* +This file is part of Telegram Desktop, +the official desktop application for the Telegram messaging service. + +For license and copyright information please follow this link: +https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL +*/ +#pragma once + +#include "iv/iv_rich_page.h" + +#include + +namespace Main { +class Session; +} // namespace Main + +namespace Iv { + +[[nodiscard]] std::optional SerializeInputRichMessage( + not_null session, + const RichPage &page); + +} // namespace Iv \ No newline at end of file diff --git a/Telegram/SourceFiles/iv/iv_rich_page.cpp b/Telegram/SourceFiles/iv/iv_rich_page.cpp index 01e0294b8f..c1f8d8b186 100644 --- a/Telegram/SourceFiles/iv/iv_rich_page.cpp +++ b/Telegram/SourceFiles/iv/iv_rich_page.cpp @@ -11,11 +11,14 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/flat_map.h" #include "base/qthelp_url.h" #include "base/unixtime.h" +#include "data/data_document.h" #include "data/data_peer.h" +#include "data/data_photo.h" #include "data/data_session.h" #include "data/data_user.h" #include "data/stickers/data_custom_emoji.h" #include "iv/markdown/iv_markdown_prepare.h" +#include "iv/markdown/iv_markdown_prepare_serialize.h" #include "iv/markdown/iv_markdown_prepare_links.h" #include "lang/lang_keys.h" #include "main/main_session.h" @@ -639,6 +642,16 @@ void AdoptAnchor(QString *anchorId, RichText *text) { } } +void AdoptLeadingParagraphListItemText(ListItem *item) { + if (item->blocks.empty() + || item->blocks.front().kind != BlockKind::Paragraph) { + return; + } + item->text = std::move(item->blocks.front().text); + item->anchorId = std::move(item->blocks.front().anchorId); + item->blocks.erase(item->blocks.begin()); +} + void AppendBlocks( const QVector &blocks, std::vector *result, @@ -693,7 +706,7 @@ void AppendBlock( AdoptAnchor(&parsed.anchorId, &parsed.text); result->push_back(std::move(parsed)); }, [&](const MTPDpageBlockFooter &data) { - auto parsed = MakeBlock(BlockKind::Paragraph); + auto parsed = MakeBlock(BlockKind::Footer); parsed.text = ParseRichText(data.vtext(), context); AdoptAnchor(&parsed.anchorId, &parsed.text); result->push_back(std::move(parsed)); @@ -723,6 +736,7 @@ void AppendBlock( row.is_checkbox(), row.is_checked()); AppendBlocks(row.vblocks().v, &listItem.blocks, context); + AdoptLeadingParagraphListItemText(&listItem); }); parsed.listItems.push_back(std::move(listItem)); } @@ -1006,6 +1020,7 @@ void AppendBlock( row.is_checked()); fillNumber(row); AppendBlocks(row.vblocks().v, &listItem.blocks, context); + AdoptLeadingParagraphListItemText(&listItem); }); parsed.listItems.push_back(std::move(listItem)); nextNumber += step; @@ -1136,6 +1151,7 @@ void AppendSummaryBlock(TextWithEntities *result, const Block &block) { return; case BlockKind::Heading: case BlockKind::Paragraph: + case BlockKind::Footer: case BlockKind::Code: AppendSummaryLine(result, block.text); return; diff --git a/Telegram/SourceFiles/iv/iv_rich_page.h b/Telegram/SourceFiles/iv/iv_rich_page.h index 58078e8f90..e9379146dc 100644 --- a/Telegram/SourceFiles/iv/iv_rich_page.h +++ b/Telegram/SourceFiles/iv/iv_rich_page.h @@ -36,6 +36,7 @@ struct RichPage { Unsupported, Heading, Paragraph, + Footer, Thinking, AuthorDate, Code, diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp index a5e0505c22..a874276acc 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.cpp @@ -502,6 +502,26 @@ int TextLineHeight(const style::TextStyle &style) { return std::max(style.lineHeight, style.font->height); } +int TextLineAscent(const style::TextStyle &style) { + if (style.qtextEditLineMetrics) { + const auto lineHeight = QFixed(TextLineHeight(style)); + const auto leading = std::max(style.font->fleading, QFixed()); + return std::clamp( + (lineHeight * 4 / 5) - leading, + QFixed(), + lineHeight).toInt(); + } + const auto lineHeight = TextLineHeight(style); + const auto textTop = std::max(lineHeight - style.font->height, 0) / 2; + return textTop + style.font->ascent; +} + +int TextLineBaseline( + const style::TextStyle &style, + int top) { + return top + TextLineAscent(style); +} + int ResolveTextLeafHeight( int naturalHeight, LayoutContext context) { @@ -515,15 +535,6 @@ int ResolveTextLeafHeight( : naturalHeight; } -[[nodiscard]] int NominalTextBaseline( - const style::TextStyle &style, - int top) { - const auto lineHeight = TextLineHeight(style); - const auto textTop = top - + (std::max(lineHeight - style.font->height, 0) / 2); - return textTop + style.font->ascent; -} - [[nodiscard]] int LeafFirstLineBaseline( const Ui::Text::String &leaf, const QRect &textRect, @@ -531,7 +542,7 @@ int ResolveTextLeafHeight( bool breakEverywhere = true) { const auto lines = leaf.countLinesGeometry(textRect.width(), breakEverywhere); return textRect.y() + (lines.empty() - ? NominalTextBaseline(style, 0) + ? TextLineBaseline(style) : lines.front().baseline); } @@ -542,7 +553,7 @@ QPoint BulletMarkerCenter( const auto &list = st.list; const auto lineHeight = TextLineHeight(st.body); const auto markerWidth = SingleDigitOrderedMarkerWidth(st); - const auto nominalBaseline = NominalTextBaseline(st.body, 0); + const auto nominalBaseline = TextLineBaseline(st.body); return QPoint( left + list.markerWidth - list.bulletLeftShift - ((markerWidth + 1) / 2), baseline + (lineHeight / 2) - nominalBaseline); diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.h b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.h index d94b7c1008..6637fac2dc 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.h +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_blocks.h @@ -176,6 +176,10 @@ struct TableRowLayoutData { [[nodiscard]] bool IsFlowKind(PreparedBlockKind kind); [[nodiscard]] QString ListMarkerText(const PreparedBlock &block); [[nodiscard]] int TextLineHeight(const style::TextStyle &style); +[[nodiscard]] int TextLineAscent(const style::TextStyle &style); +[[nodiscard]] int TextLineBaseline( + const style::TextStyle &style, + int top = 0); [[nodiscard]] int ResolveTextLeafHeight( int naturalHeight, LayoutContext context); diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_structure.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_structure.cpp index bbda14dddc..1bc20cb7a4 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_structure.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_layout_structure.cpp @@ -15,29 +15,20 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace Iv::Markdown { namespace { -[[nodiscard]] int NominalTextBaseline( - const style::TextStyle &style, - int top) { - const auto lineHeight = TextLineHeight(style); - const auto textTop = top - + (std::max(lineHeight - style.font->height, 0) / 2); - return textTop + style.font->ascent; -} - [[nodiscard]] int LeafFirstLineBaseline( const Ui::Text::String &leaf, const QRect &textRect, const style::TextStyle &style) { const auto lines = leaf.countLinesGeometry(textRect.width(), true); return textRect.y() + (lines.empty() - ? NominalTextBaseline(style, 0) + ? TextLineBaseline(style) : lines.front().baseline); } [[nodiscard]] int MarkdownBodyBaseline( int top, const style::Markdown &st) { - return NominalTextBaseline(st.body, top); + return TextLineBaseline(st.body, top); } [[nodiscard]] int BlockBottom(const LaidOutBlock &block) { diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_text.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_text.cpp index 3f5a27c820..0034b0bfce 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_article_text.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_article_text.cpp @@ -720,10 +720,10 @@ auto InlineFormulaSharedState::vertical(const style::TextStyle &textStyle) const .descent = geometry.descent, }; } - const auto ascent = std::max(textStyle.font->ascent, 0); + const auto ascent = std::max(TextLineAscent(textStyle), 0); return Ui::Text::CustomEmojiVerticalMetrics{ .ascent = ascent, - .descent = std::max(textStyle.font->height - ascent, 0), + .descent = std::max(TextLineHeight(textStyle) - ascent, 0), }; } @@ -970,18 +970,18 @@ QString InlineIvImageObject::entityData() { auto InlineIvImageObject::vertical(const style::TextStyle &textStyle) -> std::optional { if (_height > 0) { - const auto line = textStyle.font->height; + const auto line = TextLineHeight(textStyle); const auto above = _height - (_height / 2); - const auto ascent = above - (line / 2) + textStyle.font->ascent; + const auto ascent = above - (line / 2) + TextLineAscent(textStyle); return Ui::Text::CustomEmojiVerticalMetrics{ .ascent = ascent, .descent = _height - ascent, }; } - const auto ascent = std::max(textStyle.font->ascent, 0); + const auto ascent = std::max(TextLineAscent(textStyle), 0); return Ui::Text::CustomEmojiVerticalMetrics{ .ascent = ascent, - .descent = std::max(textStyle.font->height - ascent, 0), + .descent = std::max(TextLineHeight(textStyle) - ascent, 0), }; } @@ -1094,10 +1094,10 @@ auto InlineFormulaObjectCache::lookupOrCreate( if (measuredData) { measured = *measuredData; } else { + const auto fallbackAscent = std::max(TextLineAscent(textStyle), 0); const auto fallbackSize = QSize( std::max(textStyle.font->width(signature.trimmedTex), 1), - std::max(textStyle.font->height, 1)); - const auto fallbackAscent = std::max(textStyle.font->ascent, 0); + std::max(TextLineHeight(textStyle), 1)); measured.logicalSize = fallbackSize; measured.logicalDepth = std::max( fallbackSize.height() - fallbackAscent, diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_media_block.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_media_block.cpp index c150437b87..cfb9003189 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_media_block.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_media_block.cpp @@ -267,8 +267,7 @@ void SetPlainTextLeaf( const style::TextStyle &textStyle) { const auto lines = leaf.countLinesGeometry(textRect.width(), true); return textRect.y() + (lines.empty() - ? std::max(TextLineHeight(textStyle) - textStyle.font->height, 0) / 2 - + textStyle.font->ascent + ? TextLineBaseline(textStyle) : lines.front().baseline); } diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp index 93f2479720..5fce246d92 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_blocks.cpp @@ -897,6 +897,7 @@ void MarkNativeIvTableSlots( block.anchorId, state); case RichPageBlockKind::Paragraph: + case RichPageBlockKind::Footer: return AppendNativeIvFlowBlock( result, PreparedBlockKind::Paragraph, diff --git a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp index d91f3eddc2..fd86a2d038 100644 --- a/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp +++ b/Telegram/SourceFiles/iv/markdown/iv_markdown_prepare_native_richtext.cpp @@ -496,7 +496,8 @@ bool PrepareNativeIvMapBlock( const Iv::RichPage::Block &data, std::vector *result, NativeIvPrepareState *state) { - if (!data.accessHash || data.width <= 0 || data.height <= 0) { + if (data.width <= 0 || data.height <= 0 || data.zoom <= 0 + || (!data.accessHash && !state->editMode)) { return state->editMode ? PrepareNativeIvCanonicalPlaceholderBlock( u"Map"_q, diff --git a/Telegram/cmake/td_iv.cmake b/Telegram/cmake/td_iv.cmake index fb2912d76b..ebe3060523 100644 --- a/Telegram/cmake/td_iv.cmake +++ b/Telegram/cmake/td_iv.cmake @@ -11,17 +11,18 @@ add_library(tdesktop::td_iv ALIAS td_iv) target_precompile_headers(td_iv PRIVATE ${src_loc}/iv/iv_pch.h) nice_target_sources(td_iv ${src_loc} PRIVATE + iv/editor/iv_editor_box.cpp + iv/editor/iv_editor_box.h + iv/editor/iv_editor_state.cpp + iv/editor/iv_editor_state.h + iv/editor/iv_editor_widget.cpp + iv/editor/iv_editor_widget.h + iv/iv_controller.cpp iv/iv_controller.h iv/iv_data.cpp iv/iv_data.h iv/iv_delegate.h - iv/iv_editor_box.cpp - iv/iv_editor_box.h - iv/iv_editor_state.cpp - iv/iv_editor_state.h - iv/iv_editor_widget.cpp - iv/iv_editor_widget.h iv/iv_pch.h iv/iv_zoom_controls.cpp iv/iv_zoom_controls.h