diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index d21aec624e..0f6ee25810 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -2441,16 +2441,14 @@ void ApiWrap::saveCurrentDraftToCloud() { Core::App().saveCurrentDraftsToHistories(); for (const auto controller : _session->windows()) { - if (const auto peer = controller->activeChatCurrent().peer()) { - if (const auto history = _session->data().historyLoaded(peer)) { - _session->local().writeDrafts(history); + if (const auto history = controller->activeChatCurrent().history()) { + _session->local().writeDrafts(history); - const auto localDraft = history->localDraft(); - const auto cloudDraft = history->cloudDraft(); - if (!Data::draftsAreEqual(localDraft, cloudDraft) - && !_session->supportMode()) { - saveDraftToCloudDelayed(history); - } + const auto localDraft = history->localDraft(); + const auto cloudDraft = history->cloudDraft(); + if (!Data::draftsAreEqual(localDraft, cloudDraft) + && !_session->supportMode()) { + saveDraftToCloudDelayed(history); } } } diff --git a/Telegram/SourceFiles/data/data_drafts.h b/Telegram/SourceFiles/data/data_drafts.h index 57428e62ed..b797cb3d37 100644 --- a/Telegram/SourceFiles/data/data_drafts.h +++ b/Telegram/SourceFiles/data/data_drafts.h @@ -48,6 +48,77 @@ struct Draft { mtpRequestId saveRequestId = 0; }; +class DraftKey { +public: + [[nodiscard]] static DraftKey None() { + return 0; + } + [[nodiscard]] static DraftKey Local() { + return kLocalDraftIndex; + } + [[nodiscard]] static DraftKey LocalEdit() { + return kLocalDraftIndex + kEditDraftShift; + } + [[nodiscard]] static DraftKey Cloud() { + return kCloudDraftIndex; + } + [[nodiscard]] static DraftKey Scheduled() { + return kScheduledDraftIndex; + } + [[nodiscard]] static DraftKey ScheduledEdit() { + return kScheduledDraftIndex + kEditDraftShift; + } + [[nodiscard]] static DraftKey Replies(MsgId rootId) { + return rootId; + } + [[nodiscard]] static DraftKey RepliesEdit(MsgId rootId) { + return rootId + kEditDraftShift; + } + + [[nodiscard]] static DraftKey FromSerialized(int32 value) { + return value; + } + [[nodiscard]] int32 serialize() const { + return _value; + } + + inline bool operator<(const DraftKey &other) const { + return _value < other._value; + } + inline bool operator==(const DraftKey &other) const { + return _value == other._value; + } + inline bool operator>(const DraftKey &other) const { + return (other < *this); + } + inline bool operator<=(const DraftKey &other) const { + return !(other < *this); + } + inline bool operator>=(const DraftKey &other) const { + return !(*this < other); + } + inline bool operator!=(const DraftKey &other) const { + return !(*this == other); + } + inline explicit operator bool() const { + return _value != 0; + } + +private: + DraftKey(int value) : _value(value) { + } + + static constexpr auto kLocalDraftIndex = -1; + static constexpr auto kCloudDraftIndex = -2; + static constexpr auto kScheduledDraftIndex = -3; + static constexpr auto kEditDraftShift = ServerMaxMsgId; + + int _value = 0; + +}; + +using HistoryDrafts = base::flat_map>; + inline bool draftStringIsEmpty(const QString &text) { for_const (auto ch, text) { if (!ch.isSpace()) { diff --git a/Telegram/SourceFiles/history/history.cpp b/Telegram/SourceFiles/history/history.cpp index 8e999053b7..9cc17dac84 100644 --- a/Telegram/SourceFiles/history/history.cpp +++ b/Telegram/SourceFiles/history/history.cpp @@ -179,22 +179,22 @@ void History::itemVanished(not_null item) { } } -void History::setLocalDraft(std::unique_ptr &&draft) { - _localDraft = std::move(draft); -} - -void History::takeLocalDraft(History *from) { - if (auto &draft = from->_localDraft) { - if (!draft->textWithTags.text.isEmpty() && !_localDraft) { - _localDraft = std::move(draft); - - // Edit and reply to drafts can't migrate. - // Cloud drafts do not migrate automatically. - _localDraft->msgId = 0; - } - from->clearLocalDraft(); - session().api().saveDraftToCloudDelayed(from); +void History::takeLocalDraft(not_null from) { + const auto i = from->_drafts.find(Data::DraftKey::Local()); + if (i == end(from->_drafts)) { + return; } + auto &draft = i->second; + if (!draft->textWithTags.text.isEmpty() + && !_drafts.contains(Data::DraftKey::Local())) { + // Edit and reply to drafts can't migrate. + // Cloud drafts do not migrate automatically. + draft->msgId = 0; + + setLocalDraft(std::move(draft)); + } + from->clearLocalDraft(); + session().api().saveDraftToCloudDelayed(from); } void History::createLocalDraftFromCloud() { @@ -227,9 +227,51 @@ void History::createLocalDraftFromCloud() { } } -void History::setCloudDraft(std::unique_ptr &&draft) { - _cloudDraft = std::move(draft); - cloudDraftTextCache.clear(); +Data::Draft *History::draft(Data::DraftKey key) const { + if (!key) { + return nullptr; + } + const auto i = _drafts.find(key); + return (i != _drafts.end()) ? i->second.get() : nullptr; +} + +void History::setDraft(Data::DraftKey key, std::unique_ptr &&draft) { + if (!key) { + return; + } + const auto changingCloudDraft = (key == Data::DraftKey::Cloud()); + if (changingCloudDraft) { + cloudDraftTextCache.clear(); + } + if (draft) { + _drafts[key] = std::move(draft); + } else if (_drafts.remove(key) && changingCloudDraft) { + updateChatListSortPosition(); + } +} + +const Data::HistoryDrafts &History::draftsMap() const { + return _drafts; +} + +void History::setDraftsMap(Data::HistoryDrafts &&map) { + for (auto &[key, draft] : _drafts) { + map[key] = std::move(draft); + } + _drafts = std::move(map); +} + +void History::clearDraft(Data::DraftKey key) { + setDraft(key, nullptr); +} + +void History::clearDrafts() { + const auto changingCloudDraft = _drafts.contains(Data::DraftKey::Cloud()); + _drafts.clear(); + if (changingCloudDraft) { + cloudDraftTextCache.clear(); + updateChatListSortPosition(); + } } Data::Draft *History::createCloudDraft(const Data::Draft *fromDraft) { @@ -287,22 +329,6 @@ void History::clearSentDraftText(const QString &text) { accumulate_max(_lastSentDraftTime, base::unixtime::now()); } -void History::setEditDraft(std::unique_ptr &&draft) { - _editDraft = std::move(draft); -} - -void History::clearLocalDraft() { - _localDraft = nullptr; -} - -void History::clearCloudDraft() { - if (_cloudDraft) { - _cloudDraft = nullptr; - cloudDraftTextCache.clear(); - updateChatListSortPosition(); - } -} - void History::applyCloudDraft() { if (session().supportMode()) { updateChatListEntry(); @@ -314,10 +340,6 @@ void History::applyCloudDraft() { } } -void History::clearEditDraft() { - _editDraft = nullptr; -} - void History::draftSavedToCloud() { updateChatListEntry(); session().local().writeDrafts(this); diff --git a/Telegram/SourceFiles/history/history.h b/Telegram/SourceFiles/history/history.h index ebb5e0181e..9c9f920559 100644 --- a/Telegram/SourceFiles/history/history.h +++ b/Telegram/SourceFiles/history/history.h @@ -9,6 +9,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "data/data_types.h" #include "data/data_peer.h" +#include "data/data_drafts.h" #include "dialogs/dialogs_entry.h" #include "history/view/history_view_send_action.h" #include "base/observer.h" @@ -302,31 +303,48 @@ public: void eraseFromUnreadMentions(MsgId msgId); void addUnreadMentionsSlice(const MTPmessages_Messages &result); + Data::Draft *draft(Data::DraftKey key) const; + void setDraft(Data::DraftKey key, std::unique_ptr &&draft); + void clearDraft(Data::DraftKey key); + + [[nodiscard]] const Data::HistoryDrafts &draftsMap() const; + void setDraftsMap(Data::HistoryDrafts &&map); + Data::Draft *localDraft() const { - return _localDraft.get(); + return draft(Data::DraftKey::Local()); + } + Data::Draft *localEditDraft() const { + return draft(Data::DraftKey::LocalEdit()); } Data::Draft *cloudDraft() const { - return _cloudDraft.get(); + return draft(Data::DraftKey::Cloud()); } - Data::Draft *editDraft() const { - return _editDraft.get(); + void setLocalDraft(std::unique_ptr &&draft) { + setDraft(Data::DraftKey::Local(), std::move(draft)); } - void setLocalDraft(std::unique_ptr &&draft); - void takeLocalDraft(History *from); - void setCloudDraft(std::unique_ptr &&draft); + void setLocalEditDraft(std::unique_ptr &&draft) { + setDraft(Data::DraftKey::LocalEdit(), std::move(draft)); + } + void setCloudDraft(std::unique_ptr &&draft) { + setDraft(Data::DraftKey::Cloud(), std::move(draft)); + } + void clearLocalDraft() { + clearDraft(Data::DraftKey::Local()); + } + void clearCloudDraft() { + clearDraft(Data::DraftKey::Cloud()); + } + void clearLocalEditDraft() { + clearDraft(Data::DraftKey::LocalEdit()); + } + void clearDrafts(); Data::Draft *createCloudDraft(const Data::Draft *fromDraft); bool skipCloudDraft(const QString &text, MsgId replyTo, TimeId date) const; void setSentDraftText(const QString &text); void clearSentDraftText(const QString &text); - void setEditDraft(std::unique_ptr &&draft); - void clearLocalDraft(); - void clearCloudDraft(); + void takeLocalDraft(not_null from); void applyCloudDraft(); - void clearEditDraft(); void draftSavedToCloud(); - Data::Draft *draft() { - return _editDraft ? editDraft() : localDraft(); - } const MessageIdsList &forwardDraft() const { return _forwardDraft; @@ -560,8 +578,7 @@ private: }; std::unique_ptr _buildingFrontBlock; - std::unique_ptr _localDraft, _cloudDraft; - std::unique_ptr _editDraft; + Data::HistoryDrafts _drafts; std::optional _lastSentDraftText; TimeId _lastSentDraftTime = 0; MessageIdsList _forwardDraft; diff --git a/Telegram/SourceFiles/history/history_widget.cpp b/Telegram/SourceFiles/history/history_widget.cpp index 325f18aaec..b9b47f9184 100644 --- a/Telegram/SourceFiles/history/history_widget.cpp +++ b/Telegram/SourceFiles/history/history_widget.cpp @@ -1331,8 +1331,9 @@ void HistoryWidget::saveDraftDelayed() { } void HistoryWidget::saveDraft(bool delayed) { - if (!_peer) return; - if (delayed) { + if (!_peer) { + return; + } else if (delayed) { auto ms = crl::now(); if (!_saveDraftStart) { _saveDraftStart = ms; @@ -1341,21 +1342,21 @@ void HistoryWidget::saveDraft(bool delayed) { return _saveDraftTimer.callOnce(kSaveDraftTimeout); } } - writeDrafts(nullptr, nullptr); + writeDrafts(); } void HistoryWidget::saveFieldToHistoryLocalDraft() { if (!_history) return; if (_editMsgId) { - _history->setEditDraft(std::make_unique(_field, _editMsgId, _previewCancelled, _saveEditMsgRequestId)); + _history->setLocalEditDraft(std::make_unique(_field, _editMsgId, _previewCancelled, _saveEditMsgRequestId)); } else { if (_replyToId || !_field->empty()) { _history->setLocalDraft(std::make_unique(_field, _replyToId, _previewCancelled)); } else { _history->clearLocalDraft(); } - _history->clearEditDraft(); + _history->clearLocalEditDraft(); } } @@ -1363,74 +1364,47 @@ void HistoryWidget::saveCloudDraft() { controller()->session().api().saveCurrentDraftToCloud(); } -void HistoryWidget::writeDrafts(Data::Draft **localDraft, Data::Draft **editDraft) { - Data::Draft *historyLocalDraft = _history ? _history->localDraft() : nullptr; - if (!localDraft && _editMsgId) localDraft = &historyLocalDraft; +void HistoryWidget::writeDraftTexts() { + Expects(_history != nullptr); - bool save = _peer && (_saveDraftStart > 0); + session().local().writeDrafts( + _history, + _editMsgId ? Data::DraftKey::LocalEdit() : Data::DraftKey::Local(), + Storage::MessageDraft{ + _editMsgId ? _editMsgId : _replyToId, + _field->getTextWithTags(), + _previewCancelled, + }); + if (_migrated) { + _migrated->clearDrafts(); + session().local().writeDrafts(_migrated); + } +} + +void HistoryWidget::writeDraftCursors() { + Expects(_history != nullptr); + + session().local().writeDraftCursors( + _history, + _editMsgId ? Data::DraftKey::LocalEdit() : Data::DraftKey::Local(), + MessageCursor(_field)); + if (_migrated) { + _migrated->clearDrafts(); + session().local().writeDraftCursors(_migrated); + } +} + +void HistoryWidget::writeDrafts() { + const auto save = (_history != nullptr) && (_saveDraftStart > 0); _saveDraftStart = 0; _saveDraftTimer.cancel(); - if (_saveDraftText) { - if (save) { - Storage::MessageDraft storedLocalDraft, storedEditDraft; - if (localDraft) { - if (*localDraft) { - storedLocalDraft = Storage::MessageDraft{ - (*localDraft)->msgId, - (*localDraft)->textWithTags, - (*localDraft)->previewCancelled - }; - } - } else { - storedLocalDraft = Storage::MessageDraft{ - _replyToId, - _field->getTextWithTags(), - _previewCancelled - }; - } - if (editDraft) { - if (*editDraft) { - storedEditDraft = Storage::MessageDraft{ - (*editDraft)->msgId, - (*editDraft)->textWithTags, - (*editDraft)->previewCancelled - }; - } - } else if (_editMsgId) { - storedEditDraft = Storage::MessageDraft{ - _editMsgId, - _field->getTextWithTags(), - _previewCancelled - }; - } - session().local().writeDrafts(_peer->id, storedLocalDraft, storedEditDraft); - if (_migrated) { - session().local().writeDrafts(_migrated->peer->id, {}, {}); - } - } - _saveDraftText = false; - } if (save) { - MessageCursor localCursor, editCursor; - if (localDraft) { - if (*localDraft) { - localCursor = (*localDraft)->cursor; - } - } else { - localCursor = MessageCursor(_field); - } - if (editDraft) { - if (*editDraft) { - editCursor = (*editDraft)->cursor; - } - } else if (_editMsgId) { - editCursor = MessageCursor(_field); - } - session().local().writeDraftCursors(_peer->id, localCursor, editCursor); - if (_migrated) { - session().local().writeDraftCursors(_migrated->peer->id, {}, {}); + if (_saveDraftText) { + writeDraftTexts(); } + writeDraftCursors(); } + _saveDraftText = false; if (!_editMsgId && !_inlineBot) { _saveCloudDraftTimer.callOnce(kSaveCloudDraftIdleTimeout); @@ -1498,13 +1472,17 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived(const QString &query, U false); if (to.section == Section::Replies) { + history->setDraft( + Data::DraftKey::Replies(to.rootId), + std::move(draft)); controller()->showRepliesForMessage(history, to.rootId); + } else if (to.section == Section::Scheduled) { + history->setDraft(Data::DraftKey::Scheduled(), std::move(draft)); + controller()->showSection( + HistoryView::ScheduledMemento(history)); } else { history->setLocalDraft(std::move(draft)); - if (to.section == Section::Scheduled) { - controller()->showSection( - HistoryView::ScheduledMemento(history)); - } else if (history == _history) { + if (history == _history) { applyDraft(); } else { Ui::showPeerHistory(history->peer, ShowAtUnreadMsgId); @@ -1619,9 +1597,13 @@ void HistoryWidget::fastShowAtEnd(not_null history) { void HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { InvokeQueued(this, [=] { updateStickersByEmoji(); }); - auto draft = _history ? _history->draft() : nullptr; + auto draft = !_history + ? nullptr + : _history->localEditDraft() + ? _history->localEditDraft() + : _history->localDraft(); auto fieldAvailable = canWriteMessage(); - if (!draft || (!_history->editDraft() && !fieldAvailable)) { + if (!draft || (!_history->localEditDraft() && !fieldAvailable)) { auto fieldWillBeHiddenAfterEdit = (!fieldAvailable && _editMsgId != 0); clearFieldText(0, fieldHistoryAction); _field->setFocus(); @@ -1642,7 +1624,7 @@ void HistoryWidget::applyDraft(FieldHistoryAction fieldHistoryAction) { _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; _previewCancelled = draft->previewCancelled; _replyEditMsg = nullptr; - if (auto editDraft = _history->editDraft()) { + if (const auto editDraft = _history->localEditDraft()) { _editMsgId = editDraft->msgId; _replyToId = 0; } else { @@ -1778,8 +1760,7 @@ void HistoryWidget::showHistory( } controller()->session().api().saveCurrentDraftToCloud(); if (_migrated) { - _migrated->clearLocalDraft(); // use migrated draft only once - _migrated->clearEditDraft(); + _migrated->clearDrafts(); // use migrated draft only once } _history->showAtMsgId = _showAtMsgId; @@ -1910,11 +1891,6 @@ void HistoryWidget::showHistory( handlePeerUpdate(); session().local().readDraftsWithCursors(_history); - if (_migrated) { - session().local().readDraftsWithCursors(_migrated); - _migrated->clearEditDraft(); - _history->takeLocalDraft(_migrated); - } applyDraft(); _send->finishAnimating(); @@ -3006,16 +2982,16 @@ void HistoryWidget::saveEditMsg() { cancelEdit(); } })(); - if (auto editDraft = history->editDraft()) { + if (const auto editDraft = history->localEditDraft()) { if (editDraft->saveRequestId == requestId) { - history->clearEditDraft(); + history->clearLocalEditDraft(); history->session().local().writeDrafts(history); } } }; const auto fail = [=](const RPCError &error, mtpRequestId requestId) { - if (const auto editDraft = history->editDraft()) { + if (const auto editDraft = history->localEditDraft()) { if (editDraft->saveRequestId == requestId) { editDraft->saveRequestId = 0; } @@ -5563,7 +5539,7 @@ void HistoryWidget::editMessage(not_null item) { editData.text.size(), QFIXED_MAX }; - _history->setEditDraft(std::make_unique( + _history->setLocalEditDraft(std::make_unique( editData, item->id, cursor, @@ -5693,7 +5669,7 @@ void HistoryWidget::cancelEdit() { _replyEditMsg = nullptr; _editMsgId = 0; - _history->clearEditDraft(); + _history->clearLocalEditDraft(); applyDraft(); if (_saveEditMsgRequestId) { diff --git a/Telegram/SourceFiles/history/history_widget.h b/Telegram/SourceFiles/history/history_widget.h index 0f6b22add5..73d967fc7a 100644 --- a/Telegram/SourceFiles/history/history_widget.h +++ b/Telegram/SourceFiles/history/history_widget.h @@ -41,10 +41,6 @@ class Widget; struct ResultSelected; } // namespace InlineBots -namespace Data { -struct Draft; -} // namespace Data - namespace Support { class Autocomplete; struct Contact; @@ -526,8 +522,9 @@ private: // This one is syntetic. void synteticScrollToY(int y); - void writeDrafts(Data::Draft **localDraft, Data::Draft **editDraft); - void writeDrafts(History *history); + void writeDrafts(); + void writeDraftTexts(); + void writeDraftCursors(); void setFieldText( const TextWithTags &textWithTags, TextUpdateEvents events = 0, 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 ba71b97f3d..fdd5d198d1 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.cpp @@ -19,6 +19,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "core/application.h" #include "core/core_settings.h" #include "data/data_changes.h" +#include "data/data_drafts.h" #include "data/data_messages.h" #include "data/data_session.h" #include "data/data_user.h" @@ -54,6 +55,8 @@ namespace HistoryView { namespace { constexpr auto kRecordingUpdateDelta = crl::time(100); +constexpr auto kSaveDraftTimeout = crl::time(1000); +constexpr auto kSaveDraftAnywayTimeout = 5 * crl::time(1000); constexpr auto kMouseEvents = { QEvent::MouseMove, QEvent::MouseButtonPress, @@ -107,12 +110,24 @@ public: [[nodiscard]] MessageToEdit queryToEdit(); [[nodiscard]] WebPageId webPageId() const; + [[nodiscard]] MsgId getDraftMessageId() const; + [[nodiscard]] rpl::producer<> editCancelled() const { + return _editCancelled.events(); + } + [[nodiscard]] rpl::producer<> replyCancelled() const { + return _replyCancelled.events(); + } + [[nodiscard]] rpl::producer<> previewCancelled() const { + return _previewCancelled.events(); + } + [[nodiscard]] rpl::producer visibleChanged(); private: void updateControlsGeometry(QSize size); void updateVisible(); void setShownMessage(HistoryItem *message); + void resolveMessageData(); void updateShownMessageText(); void paintWebPage(Painter &p); @@ -122,12 +137,16 @@ private: WebPageData *data = nullptr; Ui::Text::String title; Ui::Text::String description; + bool cancelled = false; }; rpl::variable _title; rpl::variable _description; Preview _preview; + rpl::event_stream<> _editCancelled; + rpl::event_stream<> _replyCancelled; + rpl::event_stream<> _previewCancelled; bool hasPreview() const; @@ -202,10 +221,10 @@ void FieldHeader::init() { }) | rpl::start_with_next([=](const Data::MessageUpdate &update) { if (update.flags & Data::MessageUpdate::Flag::Destroyed) { if (_editMsgId.current() == update.item->fullId()) { - editMessage({}); + _editCancelled.fire({}); } if (_replyToId.current() == update.item->fullId()) { - replyToMessage({}); + _replyCancelled.fire({}); } } else { updateShownMessageText(); @@ -215,13 +234,14 @@ void FieldHeader::init() { _cancel->addClickHandler([=] { if (hasPreview()) { _preview = {}; - update(); + _previewCancelled.fire({}); } else if (_editMsgId.current()) { - editMessage({}); + _editCancelled.fire({}); } else if (_replyToId.current()) { - replyToMessage({}); + _replyCancelled.fire({}); } updateVisible(); + update(); }); _title.value( @@ -308,6 +328,7 @@ void FieldHeader::setShownMessage(HistoryItem *item) { } } else { _shownMessageText.clear(); + resolveMessageData(); } if (isEditingMessage()) { _shownMessageName.setText( @@ -322,6 +343,34 @@ void FieldHeader::setShownMessage(HistoryItem *item) { update(); } +void FieldHeader::resolveMessageData() { + const auto id = (isEditingMessage() ? _editMsgId : _replyToId).current(); + if (!id) { + return; + } + const auto channel = id.channel + ? _data->channel(id.channel).get() + : nullptr; + const auto callback = [=](ChannelData *channel, MsgId msgId) { + const auto now = (isEditingMessage() + ? _editMsgId + : _replyToId).current(); + if (now == id && !_shownMessage) { + if (const auto message = _data->message(channel, msgId)) { + setShownMessage(message); + } else if (isEditingMessage()) { + _editCancelled.fire({}); + } else { + _replyCancelled.fire({}); + } + } + }; + _data->session().api().requestMessageData( + channel, + id.msg, + crl::guard(this, callback)); +} + void FieldHeader::previewRequested( rpl::producer title, rpl::producer description, @@ -329,19 +378,25 @@ void FieldHeader::previewRequested( std::move( title - ) | rpl::start_with_next([=](const QString &t) { + ) | rpl::filter([=] { + return !_preview.cancelled; + }) | start_with_next([=](const QString &t) { _title = t; }, lifetime()); std::move( description - ) | rpl::start_with_next([=](const QString &d) { + ) | rpl::filter([=] { + return !_preview.cancelled; + }) | rpl::start_with_next([=](const QString &d) { _description = d; }, lifetime()); std::move( page - ) | rpl::start_with_next([=](WebPageData *p) { + ) | rpl::filter([=] { + return !_preview.cancelled; + }) | rpl::start_with_next([=](WebPageData *p) { _preview.data = p; updateVisible(); }, lifetime()); @@ -392,14 +447,26 @@ void FieldHeader::paintWebPage(Painter &p) { } void FieldHeader::paintEditOrReplyToMessage(Painter &p) { - Expects(_shownMessage != nullptr); - const auto replySkip = st::historyReplySkip; const auto availableWidth = width() - replySkip - _cancel->width() - st::msgReplyPadding.right(); + if (!_shownMessage) { + p.setFont(st::msgDateFont); + p.setPen(st::historyComposeAreaFgService); + const auto top = (st::msgReplyPadding.top() + + (st::msgReplyBarSize.height() - st::msgDateFont->height) / 2); + p.drawText( + replySkip, + top + st::msgDateFont->ascent, + st::msgDateFont->elided( + tr::lng_profile_loading(tr::now), + availableWidth)); + return; + } + if (!isEditingMessage()) { const auto user = _shownMessage->displayFrom() ? _shownMessage->displayFrom() @@ -460,6 +527,10 @@ WebPageId FieldHeader::webPageId() const { return hasPreview() ? _preview.data->id : CancelledWebPageId; } +MsgId FieldHeader::getDraftMessageId() const { + return (isEditingMessage() ? _editMsgId : _replyToId).current().msg; +} + void FieldHeader::updateControlsGeometry(QSize size) { _cancel->moveToRight(0, 0); _clickableRect = QRect( @@ -538,11 +609,12 @@ ComposeControls::ComposeControls( window, _send, st::historySendSize.height())) -, _textUpdateEvents(TextUpdateEvent::SendTyping) { +, _saveDraftTimer([=] { saveDraft(); }) { init(); } ComposeControls::~ComposeControls() { + saveFieldToHistoryLocalDraft(); setTabbedPanel(nullptr); session().api().request(_inlineBotResolveRequestId).cancel(); } @@ -582,6 +654,8 @@ void ComposeControls::setHistory(SetHistoryArgs &&args) { session().api().requestBots(channel); } } + session().local().readDraftsWithCursors(_history); + applyDraft(); } void ComposeControls::setCurrentDialogsEntryState(Dialogs::EntryState state) { @@ -752,21 +826,52 @@ TextWithTags ComposeControls::getTextWithAppliedMarkdown() const { } void ComposeControls::clear() { - setText(TextWithTags()); + setText({}); cancelReplyMessage(); } void ComposeControls::setText(const TextWithTags &textWithTags) { - _textUpdateEvents = TextUpdateEvents(); - _field->setTextWithTags(textWithTags, Ui::InputField::HistoryAction::Clear/*fieldHistoryAction*/); + setFieldText(textWithTags); +} + +void ComposeControls::setFieldText( + const TextWithTags &textWithTags, + TextUpdateEvents events, + FieldHistoryAction fieldHistoryAction) { + _textUpdateEvents = events; + _field->setTextWithTags(textWithTags, fieldHistoryAction); auto cursor = _field->textCursor(); cursor.movePosition(QTextCursor::End); _field->setTextCursor(cursor); _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; - //previewCancel(); - //_previewCancelled = false; + _previewCancel(); + _previewCancelled = false; +} + +void ComposeControls::saveFieldToHistoryLocalDraft() { + const auto key = draftKeyCurrent(); + if (!_history || key == Data::DraftKey::None()) { + return; + } + const auto id = _header->getDraftMessageId(); + if (id || !_field->empty()) { + _history->setDraft( + draftKeyCurrent(), + std::make_unique( + _field, + _header->getDraftMessageId(), + _previewCancelled)); + } else { + _history->clearDraft(draftKeyCurrent()); + } +} + +void ComposeControls::clearFieldText( + TextUpdateEvents events, + FieldHistoryAction fieldHistoryAction) { + setFieldText({}, events, fieldHistoryAction); } void ComposeControls::hidePanelsAnimated() { @@ -836,15 +941,27 @@ void ComposeControls::init() { _header->editMsgId( ) | rpl::start_with_next([=](const auto &id) { - if (_header->isEditingMessage()) { - setTextFromEditingMessage(session().data().message(id)); - } else { - setText(_localSavedText); - _localSavedText = {}; - } updateSendButtonType(); }, _wrap->lifetime()); + _header->previewCancelled( + ) | rpl::start_with_next([=] { + _previewCancelled = true; + _saveDraftText = true; + _saveDraftStart = crl::now(); + saveDraft(); + }, _wrap->lifetime()); + + _header->editCancelled( + ) | rpl::start_with_next([=] { + cancelEditMessage(); + }, _wrap->lifetime()); + + _header->replyCancelled( + ) | rpl::start_with_next([=] { + cancelReplyMessage(); + }, _wrap->lifetime()); + _header->visibleChanged( ) | rpl::start_with_next([=] { updateHeight(); @@ -894,19 +1011,6 @@ void ComposeControls::drawRestrictedWrite(Painter &p, const QString &error) { style::al_center); } -void ComposeControls::setTextFromEditingMessage(not_null item) { - if (!_header->isEditingMessage()) { - return; - } - _localSavedText = getTextWithAppliedMarkdown(); - const auto t = item->originalText(); - const auto text = TextWithTags{ - t.text, - TextUtilities::ConvertEntitiesToTextTags(t.entities) - }; - setText(text); -} - void ComposeControls::initField() { _field->setMaxHeight(st::historyComposeFieldMaxHeight); updateSubmitSettings(); @@ -924,6 +1028,16 @@ void ComposeControls::initField() { &_window->session()); _raiseEmojiSuggestions = [=] { suggestions->raise(); }; InitSpellchecker(_window, _field); + + const auto rawTextEdit = _field->rawTextEdit().get(); + rpl::merge( + _field->scrollTop().changes() | rpl::to_empty, + base::qt_signal_producer( + rawTextEdit, + &QTextEdit::cursorPositionChanged) + ) | rpl::start_with_next([=] { + saveDraftDelayed(); + }, _field->lifetime()); } void ComposeControls::updateSubmitSettings() { @@ -1077,7 +1191,7 @@ void ComposeControls::fieldChanged() { } updateSendButtonType(); if (showRecordButton()) { - //_previewCancelled = false; + _previewCancelled = false; } if (updateBotCommandShown()) { updateControlsVisibility(); @@ -1087,6 +1201,133 @@ void ComposeControls::fieldChanged() { updateInlineBotQuery(); updateStickersByEmoji(); }); + + if (!(_textUpdateEvents & TextUpdateEvent::SaveDraft)) { + return; + } + _saveDraftText = true; + saveDraft(true); +} + +void ComposeControls::saveDraftDelayed() { + if (!(_textUpdateEvents & TextUpdateEvent::SaveDraft)) { + return; + } + saveDraft(true); +} + +Data::DraftKey ComposeControls::draftKey(DraftType type) const { + using Section = Dialogs::EntryState::Section; + using Key = Data::DraftKey; + + switch (_currentDialogsEntryState.section) { + case Section::History: + return (type == DraftType::Edit) ? Key::LocalEdit() : Key::Local(); + case Section::Scheduled: + return (type == DraftType::Edit) + ? Key::ScheduledEdit() + : Key::Scheduled(); + case Section::Replies: + return (type == DraftType::Edit) + ? Key::RepliesEdit(_currentDialogsEntryState.rootId) + : Key::Replies(_currentDialogsEntryState.rootId); + } + return Key::None(); +} + +Data::DraftKey ComposeControls::draftKeyCurrent() const { + return draftKey(isEditingMessage() ? DraftType::Edit : DraftType::Normal); +} + +void ComposeControls::saveDraft(bool delayed) { + if (delayed) { + const auto now = crl::now(); + if (!_saveDraftStart) { + _saveDraftStart = now; + return _saveDraftTimer.callOnce(kSaveDraftTimeout); + } else if (now - _saveDraftStart < kSaveDraftAnywayTimeout) { + return _saveDraftTimer.callOnce(kSaveDraftTimeout); + } + } + writeDrafts(); +} + +void ComposeControls::writeDraftTexts() { + Expects(_history != nullptr); + + session().local().writeDrafts( + _history, + draftKeyCurrent(), + Storage::MessageDraft{ + _header->getDraftMessageId(), + _field->getTextWithTags(), + _previewCancelled, + }); +} + +void ComposeControls::writeDraftCursors() { + Expects(_history != nullptr); + + session().local().writeDraftCursors( + _history, + draftKeyCurrent(), + MessageCursor(_field)); +} + +void ComposeControls::writeDrafts() { + const auto save = (_history != nullptr) + && (_saveDraftStart > 0) + && (draftKeyCurrent() != Data::DraftKey::None()); + _saveDraftStart = 0; + _saveDraftTimer.cancel(); + if (save) { + if (_saveDraftText) { + writeDraftTexts(); + } + writeDraftCursors(); + } + _saveDraftText = false; + + //if (!isEditingMessage() && !_inlineBot) { + // _saveCloudDraftTimer.callOnce(kSaveCloudDraftIdleTimeout); + //} +} + +void ComposeControls::applyDraft(FieldHistoryAction fieldHistoryAction) { + Expects(_history != nullptr); + + InvokeQueued(_autocomplete.get(), [=] { updateStickersByEmoji(); }); + const auto guard = gsl::finally([&] { + updateSendButtonType(); + updateControlsVisibility(); + updateControlsGeometry(_wrap->size()); + }); + + const auto editDraft = _history->draft(draftKey(DraftType::Edit)); + const auto draft = editDraft + ? editDraft + : _history->draft(draftKey(DraftType::Normal)); + if (!draft) { + clearFieldText(0, fieldHistoryAction); + _field->setFocus(); + _header->editMessage({}); + _header->replyToMessage({}); + return; + } + + _textUpdateEvents = 0; + setFieldText(draft->textWithTags, 0, fieldHistoryAction); + _field->setFocus(); + draft->cursor.applyTo(_field); + _textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping; + _previewCancelled = draft->previewCancelled; + if (draft == editDraft) { + _header->editMessage({ _history->channelId(), draft->msgId }); + _header->replyToMessage({}); + } else { + _header->replyToMessage({ _history->channelId(), draft->msgId }); + _header->editMessage({}); + } } void ComposeControls::fieldTabbed() { @@ -1192,11 +1433,16 @@ void ComposeControls::inlineBotResolveFail( } void ComposeControls::cancelInlineBot() { - auto &textWithTags = _field->getTextWithTags(); + const auto &textWithTags = _field->getTextWithTags(); if (textWithTags.text.size() > _inlineBotUsername.size() + 2) { - setText({ '@' + _inlineBotUsername + ' ', TextWithTags::Tags() }); + setFieldText( + { '@' + _inlineBotUsername + ' ', TextWithTags::Tags() }, + TextUpdateEvent::SaveDraft, + Ui::InputField::HistoryAction::NewEntry); } else { - setText({}); + clearFieldText( + TextUpdateEvent::SaveDraft, + Ui::InputField::HistoryAction::NewEntry); } } @@ -1487,29 +1733,101 @@ void ComposeControls::updateHeight() { } void ComposeControls::editMessage(FullMsgId id) { - cancelEditMessage(); - _header->editMessage(id); + if (const auto item = session().data().message(id)) { + editMessage(item); + } +} + +void ComposeControls::editMessage(not_null item) { + Expects(_history != nullptr); + Expects(draftKeyCurrent() != Data::DraftKey::None()); + + if (!isEditingMessage()) { + saveFieldToHistoryLocalDraft(); + } + const auto editData = PrepareEditText(item); + const auto cursor = MessageCursor{ + editData.text.size(), + editData.text.size(), + QFIXED_MAX + }; + _history->setDraft( + draftKey(DraftType::Edit), + std::make_unique( + editData, + item->id, + cursor, + false)); + applyDraft(); + if (_autocomplete) { InvokeQueued(_autocomplete.get(), [=] { checkAutocomplete(); }); } - updateFieldPlaceholder(); } void ComposeControls::cancelEditMessage() { - _header->editMessage({}); - if (_autocomplete) { - InvokeQueued(_autocomplete.get(), [=] { checkAutocomplete(); }); - } - updateFieldPlaceholder(); + Expects(_history != nullptr); + Expects(draftKeyCurrent() != Data::DraftKey::None()); + + _history->clearDraft(draftKey(DraftType::Edit)); + applyDraft(); + + _saveDraftText = true; + _saveDraftStart = crl::now(); + saveDraft(); } void ComposeControls::replyToMessage(FullMsgId id) { - cancelReplyMessage(); - _header->replyToMessage(id); + Expects(_history != nullptr); + Expects(draftKeyCurrent() != Data::DraftKey::None()); + + if (!id) { + cancelReplyMessage(); + return; + } + if (isEditingMessage()) { + const auto key = draftKey(DraftType::Normal); + if (const auto localDraft = _history->draft(key)) { + localDraft->msgId = id.msg; + } else { + _history->setDraft( + key, + std::make_unique( + TextWithTags(), + id.msg, + MessageCursor(), + false)); + } + } else { + _header->replyToMessage(id); + } + + _saveDraftText = true; + _saveDraftStart = crl::now(); + saveDraft(); } void ComposeControls::cancelReplyMessage() { + Expects(_history != nullptr); + Expects(draftKeyCurrent() != Data::DraftKey::None()); + + const auto wasReply = replyingToMessage(); _header->replyToMessage({}); + const auto key = draftKey(DraftType::Normal); + if (const auto localDraft = _history->draft(key)) { + if (localDraft->msgId) { + if (localDraft->textWithTags.text.isEmpty()) { + _history->clearDraft(key); + } else { + localDraft->msgId = 0; + } + } + } + if (wasReply) { + _saveDraftText = true; + _saveDraftStart = crl::now(); + saveDraft(); + } } bool ComposeControls::handleCancelRequest() { @@ -1544,7 +1862,6 @@ void ComposeControls::initWebpageProcess() { using PreviewCache = std::map; const auto previewCache = lifetime.make_state(); const auto previewRequest = lifetime.make_state(0); - const auto previewCancelled = lifetime.make_state(false); const auto mtpSender = lifetime.make_state(&_window->session().mtp()); @@ -1591,7 +1908,7 @@ void ComposeControls::initWebpageProcess() { if (till > 0 && till <= base::unixtime::now()) { till = -1; } - if (links == *previewLinks && !*previewCancelled) { + if (links == *previewLinks && !_previewCancelled) { *previewData = (page->id && page->pendingTill >= 0) ? page.get() : nullptr; @@ -1599,7 +1916,7 @@ void ComposeControls::initWebpageProcess() { } }, [=](const MTPDmessageMediaEmpty &d) { previewCache->insert({ links, 0 }); - if (links == *previewLinks && !*previewCancelled) { + if (links == *previewLinks && !_previewCancelled) { *previewData = nullptr; updatePreview(); } @@ -1607,7 +1924,7 @@ void ComposeControls::initWebpageProcess() { }); }); - const auto previewCancel = [=] { + _previewCancel = [=] { mtpSender->request(base::take(*previewRequest)).cancel(); *previewData = nullptr; previewLinks->clear(); @@ -1628,8 +1945,8 @@ void ComposeControls::initWebpageProcess() { const auto checkPreview = crl::guard(_wrap.get(), [=] { const auto previewRestricted = peer && peer->amRestricted(ChatRestriction::f_embed_links); - if (/*_previewCancelled ||*/ previewRestricted) { - previewCancel(); + if (_previewCancelled || previewRestricted) { + _previewCancel(); return; } const auto newLinks = parsedLinks->join(' '); @@ -1640,7 +1957,7 @@ void ComposeControls::initWebpageProcess() { *previewLinks = newLinks; if (previewLinks->isEmpty()) { if (ShowWebPagePreview(*previewData)) { - previewCancel(); + _previewCancel(); } } else { const auto i = previewCache->find(*previewLinks); @@ -1650,7 +1967,7 @@ void ComposeControls::initWebpageProcess() { *previewData = _history->owner().webpage(i->second); updatePreview(); } else if (ShowWebPagePreview(*previewData)) { - previewCancel(); + _previewCancel(); } } }); diff --git a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h index 01e148585d..109a318a06 100644 --- a/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h +++ b/Telegram/SourceFiles/history/view/controls/history_view_compose_controls.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/required.h" #include "api/api_common.h" #include "base/unique_qptr.h" +#include "base/timer.h" #include "dialogs/dialogs_key.h" #include "history/view/controls/compose_controls_common.h" #include "ui/rp_widget.h" @@ -27,6 +28,8 @@ class TabbedSelector; namespace Data { struct MessagePosition; +struct Draft; +class DraftKey; } // namespace Data namespace InlineBots { @@ -74,6 +77,7 @@ public: using VoiceToSend = Controls::VoiceToSend; using SendActionUpdate = Controls::SendActionUpdate; using SetHistoryArgs = Controls::SetHistoryArgs; + using FieldHistoryAction = Ui::InputField::HistoryAction; enum class Mode { Normal, @@ -148,11 +152,18 @@ public: [[nodiscard]] bool isLockPresent() const; [[nodiscard]] bool isRecording() const; + void applyDraft( + FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear); + private: enum class TextUpdateEvent { SaveDraft = (1 << 0), SendTyping = (1 << 1), }; + enum class DraftType { + Normal, + Edit, + }; using TextUpdateEvents = base::flags; friend inline constexpr bool is_flag_type(TextUpdateEvent) { return true; }; @@ -177,6 +188,7 @@ private: void checkAutocomplete(); void updateStickersByEmoji(); void updateFieldPlaceholder(); + void editMessage(not_null item); void escape(); void fieldChanged(); @@ -185,11 +197,8 @@ private: void createTabbedPanel(); void setTabbedPanel(std::unique_ptr panel); - void setTextFromEditingMessage(not_null item); - bool showRecordButton() const; void drawRestrictedWrite(Painter &p, const QString &error); - void updateOverStates(QPoint pos); bool updateBotCommandShown(); void cancelInlineBot(); @@ -205,6 +214,24 @@ private: void inlineBotResolveDone(const MTPcontacts_ResolvedPeer &result); void inlineBotResolveFail(const RPCError &error, const QString &username); + [[nodiscard]] Data::DraftKey draftKey( + DraftType type = DraftType::Normal) const; + [[nodiscard]] Data::DraftKey draftKeyCurrent() const; + void saveDraft(bool delayed = false); + void saveDraftDelayed(); + + void writeDrafts(); + void writeDraftTexts(); + void writeDraftCursors(); + void setFieldText( + const TextWithTags &textWithTags, + TextUpdateEvents events = 0, + FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear); + void clearFieldText( + TextUpdateEvents events = 0, + FieldHistoryAction fieldHistoryAction = FieldHistoryAction::Clear); + void saveFieldToHistoryLocalDraft(); + const not_null _parent; const not_null _window; History *_history = nullptr; @@ -238,12 +265,14 @@ private: rpl::event_stream _sendActionUpdates; rpl::event_stream _sendCommandRequests; - TextWithTags _localSavedText; - TextUpdateEvents _textUpdateEvents; + TextUpdateEvents _textUpdateEvents = TextUpdateEvents() + | TextUpdateEvent::SaveDraft + | TextUpdateEvent::SendTyping; Dialogs::EntryState _currentDialogsEntryState; - //bool _inReplyEditForward = false; - //bool _inClickable = false; + crl::time _saveDraftStart = 0; + bool _saveDraftText = false; + base::Timer _saveDraftTimer; UserData *_inlineBot = nullptr; QString _inlineBotUsername; @@ -252,6 +281,9 @@ private: bool _isInlineBot = false; bool _botCommandShown = false; + Fn _previewCancel; + bool _previewCancelled = false; + rpl::lifetime _uploaderSubscriptions; Fn _raiseEmojiSuggestions; diff --git a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp index a076837d67..e5a976a574 100644 --- a/Telegram/SourceFiles/history/view/history_view_replies_section.cpp +++ b/Telegram/SourceFiles/history/view/history_view_replies_section.cpp @@ -1133,7 +1133,7 @@ void RepliesWidget::refreshTopBarActiveChat() { .key = _history, .section = Dialogs::EntryState::Section::Replies, .rootId = _rootId, - .currentReplyToId = replyToId(), + .currentReplyToId = _composeControls->replyingToMessage().msg, }; _topBar->setActiveChat(state, _sendAction.get()); _composeControls->setCurrentDialogsEntryState(state); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 4c47f34214..d17c8af276 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -533,7 +533,7 @@ bool MainWidget::shareUrl( auto history = peer->owner().history(peer); history->setLocalDraft( std::make_unique(textWithTags, 0, cursor, false)); - history->clearEditDraft(); + history->clearLocalEditDraft(); if (_history->peer() == peer) { _history->applyDraft(); } else { @@ -562,7 +562,7 @@ bool MainWidget::inlineSwitchChosen(PeerId peerId, const QString &botAndQuery) { TextWithTags textWithTags = { botAndQuery, TextWithTags::Tags() }; MessageCursor cursor = { botAndQuery.size(), botAndQuery.size(), QFIXED_MAX }; h->setLocalDraft(std::make_unique(textWithTags, 0, cursor, false)); - h->clearEditDraft(); + h->clearLocalEditDraft(); const auto opened = _history->peer() && (_history->peer() == peer); if (opened) { _history->applyDraft(); diff --git a/Telegram/SourceFiles/platform/win/window_title_win.cpp b/Telegram/SourceFiles/platform/win/window_title_win.cpp index 80df1d90d5..a1c51f4917 100644 --- a/Telegram/SourceFiles/platform/win/window_title_win.cpp +++ b/Telegram/SourceFiles/platform/win/window_title_win.cpp @@ -42,6 +42,8 @@ TitleWidget::TitleWidget(QWidget *parent) }); _close->setPointerCursor(false); + window()->windowHandle()->setFlag(Qt::FramelessWindowHint, true); + setAttribute(Qt::WA_OpaquePaintEvent); resize(width(), _st.height); } diff --git a/Telegram/SourceFiles/platform/win/windows_event_filter.cpp b/Telegram/SourceFiles/platform/win/windows_event_filter.cpp index bdd55b9579..0cd10f60dc 100644 --- a/Telegram/SourceFiles/platform/win/windows_event_filter.cpp +++ b/Telegram/SourceFiles/platform/win/windows_event_filter.cpp @@ -142,30 +142,42 @@ bool EventFilter::customWindowFrameEvent( if (result) *result = 0; } return true; + case WM_SHOWWINDOW: { + SetWindowLongPtr( + hWnd, + GWL_STYLE, + WS_POPUP + | WS_THICKFRAME + | WS_CAPTION + | WS_SYSMENU + | WS_MAXIMIZEBOX + | WS_MINIMIZEBOX); + } return false; + case WM_NCCALCSIZE: { - WINDOWPLACEMENT wp; - wp.length = sizeof(WINDOWPLACEMENT); - if (GetWindowPlacement(hWnd, &wp) && wp.showCmd == SW_SHOWMAXIMIZED) { - LPNCCALCSIZE_PARAMS params = (LPNCCALCSIZE_PARAMS)lParam; - LPRECT r = (wParam == TRUE) ? ¶ms->rgrc[0] : (LPRECT)lParam; - HMONITOR hMonitor = MonitorFromPoint({ (r->left + r->right) / 2, (r->top + r->bottom) / 2 }, MONITOR_DEFAULTTONEAREST); - if (hMonitor) { - MONITORINFO mi; - mi.cbSize = sizeof(mi); - if (GetMonitorInfo(hMonitor, &mi)) { - *r = mi.rcWork; - UINT uEdge = (UINT)-1; - if (IsTaskbarAutoHidden(&mi.rcMonitor, &uEdge)) { - switch (uEdge) { - case ABE_LEFT: r->left += 1; break; - case ABE_RIGHT: r->right -= 1; break; - case ABE_TOP: r->top += 1; break; - case ABE_BOTTOM: r->bottom -= 1; break; - } - } - } - } - } + //WINDOWPLACEMENT wp; + //wp.length = sizeof(WINDOWPLACEMENT); + //if (GetWindowPlacement(hWnd, &wp) && wp.showCmd == SW_SHOWMAXIMIZED) { + // LPNCCALCSIZE_PARAMS params = (LPNCCALCSIZE_PARAMS)lParam; + // LPRECT r = (wParam == TRUE) ? ¶ms->rgrc[0] : (LPRECT)lParam; + // HMONITOR hMonitor = MonitorFromPoint({ (r->left + r->right) / 2, (r->top + r->bottom) / 2 }, MONITOR_DEFAULTTONEAREST); + // if (hMonitor) { + // MONITORINFO mi; + // mi.cbSize = sizeof(mi); + // if (GetMonitorInfo(hMonitor, &mi)) { + // *r = mi.rcWork; + // UINT uEdge = (UINT)-1; + // if (IsTaskbarAutoHidden(&mi.rcMonitor, &uEdge)) { + // switch (uEdge) { + // case ABE_LEFT: r->left += 1; break; + // case ABE_RIGHT: r->right -= 1; break; + // case ABE_TOP: r->top += 1; break; + // case ABE_BOTTOM: r->bottom -= 1; break; + // } + // } + // } + // } + //} if (result) *result = 0; return true; } diff --git a/Telegram/SourceFiles/storage/storage_account.cpp b/Telegram/SourceFiles/storage/storage_account.cpp index 0a541a0232..ee882fc6a8 100644 --- a/Telegram/SourceFiles/storage/storage_account.cpp +++ b/Telegram/SourceFiles/storage/storage_account.cpp @@ -51,6 +51,7 @@ constexpr auto kSinglePeerTypeChat = qint32(2); constexpr auto kSinglePeerTypeChannel = qint32(3); constexpr auto kSinglePeerTypeSelf = qint32(4); constexpr auto kSinglePeerTypeEmpty = qint32(0); +constexpr auto kMultiDraftTag = quint64(0xFFFFFFFFFFFFFF01ULL); enum { // Local Storage Keys lskUserMap = 0x00, @@ -936,80 +937,200 @@ std::unique_ptr Account::readMtpConfig() { return MTP::Config::FromSerialized(serialized); } -void Account::writeDrafts(not_null history) { - Storage::MessageDraft storedLocalDraft, storedEditDraft; - MessageCursor localCursor, editCursor; - if (const auto localDraft = history->localDraft()) { - if (_owner->session().supportMode() - || !Data::draftsAreEqual(localDraft, history->cloudDraft())) { - storedLocalDraft = Storage::MessageDraft{ - localDraft->msgId, - localDraft->textWithTags, - localDraft->previewCancelled - }; - localCursor = localDraft->cursor; +template +void EnumerateDrafts( + const Data::HistoryDrafts &map, + Data::Draft *cloudDraft, + bool supportMode, + Data::DraftKey replaceKey, + const MessageDraft &replaceDraft, + const MessageCursor &replaceCursor, + Callback &&callback) { + for (const auto &[key, draft] : map) { + if (key == Data::DraftKey::Cloud() || key == replaceKey) { + continue; + } else if (key == Data::DraftKey::Local() + && !supportMode + && Data::draftsAreEqual(draft.get(), cloudDraft)) { + continue; } + callback( + key, + draft->msgId, + draft->textWithTags, + draft->previewCancelled, + draft->cursor); } - if (const auto editDraft = history->editDraft()) { - storedEditDraft = Storage::MessageDraft{ - editDraft->msgId, - editDraft->textWithTags, - editDraft->previewCancelled - }; - editCursor = editDraft->cursor; + if (replaceKey + && (replaceDraft.msgId + || !replaceDraft.textWithTags.text.isEmpty() + || replaceCursor != MessageCursor())) { + callback( + replaceKey, + replaceDraft.msgId, + replaceDraft.textWithTags, + replaceDraft.previewCancelled, + replaceCursor); } - writeDrafts( - history->peer->id, - storedLocalDraft, - storedEditDraft); - writeDraftCursors(history->peer->id, localCursor, editCursor); } void Account::writeDrafts( - const PeerId &peer, - const MessageDraft &localDraft, - const MessageDraft &editDraft) { - if (localDraft.msgId <= 0 && localDraft.textWithTags.text.isEmpty() && editDraft.msgId <= 0) { - auto i = _draftsMap.find(peer); + not_null history, + Data::DraftKey replaceKey, + MessageDraft replaceDraft) { + const auto peerId = history->peer->id; + const auto &map = history->draftsMap(); + const auto cloudIt = map.find(Data::DraftKey::Cloud()); + const auto cloudDraft = (cloudIt != end(map)) + ? cloudIt->second.get() + : nullptr; + const auto supportMode = _owner->session().supportMode(); + auto count = 0; + EnumerateDrafts( + map, + cloudDraft, + supportMode, + replaceKey, + replaceDraft, + MessageCursor(), + [&](auto&&...) { ++count; }); + if (!count) { + auto i = _draftsMap.find(peerId); if (i != _draftsMap.cend()) { ClearKey(i->second, _basePath); _draftsMap.erase(i); writeMapDelayed(); } - _draftsNotReadMap.remove(peer); - } else { - auto i = _draftsMap.find(peer); - if (i == _draftsMap.cend()) { - i = _draftsMap.emplace(peer, GenerateKey(_basePath)).first; - writeMapQueued(); - } - - auto msgTags = TextUtilities::SerializeTags( - localDraft.textWithTags.tags); - auto editTags = TextUtilities::SerializeTags( - editDraft.textWithTags.tags); - - int size = sizeof(quint64); - size += Serialize::stringSize(localDraft.textWithTags.text) + Serialize::bytearraySize(msgTags) + 2 * sizeof(qint32); - size += Serialize::stringSize(editDraft.textWithTags.text) + Serialize::bytearraySize(editTags) + 2 * sizeof(qint32); - - EncryptedDescriptor data(size); - data.stream << quint64(peer); - data.stream << localDraft.textWithTags.text << msgTags; - data.stream << qint32(localDraft.msgId) << qint32(localDraft.previewCancelled ? 1 : 0); - data.stream << editDraft.textWithTags.text << editTags; - data.stream << qint32(editDraft.msgId) << qint32(editDraft.previewCancelled ? 1 : 0); - - FileWriteDescriptor file(i->second, _basePath); - file.writeEncrypted(data, _localKey); - - _draftsNotReadMap.remove(peer); + _draftsNotReadMap.remove(peerId); + return; } + + auto i = _draftsMap.find(peerId); + if (i == _draftsMap.cend()) { + i = _draftsMap.emplace(peerId, GenerateKey(_basePath)).first; + writeMapQueued(); + } + + auto size = int(sizeof(quint64) * 2 + sizeof(quint32)); + const auto sizeCallback = [&]( + auto&&, // key + MsgId, // msgId + const TextWithTags &text, + bool, // previewCancelled + auto&&) { // cursor + size += sizeof(qint32) // key + + Serialize::stringSize(text.text) + + sizeof(quint32) + TextUtilities::SerializeTagsSize(text.tags) + + 2 * sizeof(qint32); // msgId, previewCancelled + }; + EnumerateDrafts( + map, + cloudDraft, + supportMode, + replaceKey, + replaceDraft, + MessageCursor(), + sizeCallback); + + EncryptedDescriptor data(size); + data.stream + << quint64(kMultiDraftTag) + << quint64(peerId) + << quint32(count); + + const auto writeCallback = [&]( + const Data::DraftKey &key, + MsgId msgId, + const TextWithTags &text, + bool previewCancelled, + auto&&) { // cursor + data.stream + << key.serialize() + << text.text + << TextUtilities::SerializeTags(text.tags) + << qint32(msgId) + << qint32(previewCancelled ? 1 : 0); + }; + EnumerateDrafts( + map, + cloudDraft, + supportMode, + replaceKey, + replaceDraft, + MessageCursor(), + writeCallback); + + FileWriteDescriptor file(i->second, _basePath); + file.writeEncrypted(data, _localKey); + + _draftsNotReadMap.remove(peerId); } -void Account::clearDraftCursors(const PeerId &peer) { - const auto i = _draftCursorsMap.find(peer); +void Account::writeDraftCursors( + not_null history, + Data::DraftKey replaceKey, + MessageCursor replaceCursor) { + const auto peerId = history->peer->id; + const auto &map = history->draftsMap(); + const auto cloudIt = map.find(Data::DraftKey::Cloud()); + const auto cloudDraft = (cloudIt != end(map)) + ? cloudIt->second.get() + : nullptr; + const auto supportMode = _owner->session().supportMode(); + auto count = 0; + EnumerateDrafts( + map, + cloudDraft, + supportMode, + replaceKey, + MessageDraft(), + replaceCursor, + [&](auto&&...) { ++count; }); + if (!count) { + clearDraftCursors(peerId); + return; + } + auto i = _draftCursorsMap.find(peerId); + if (i == _draftCursorsMap.cend()) { + i = _draftCursorsMap.emplace(peerId, GenerateKey(_basePath)).first; + writeMapQueued(); + } + + auto size = int(sizeof(quint64) * 2 + sizeof(quint32) * 4); + + EncryptedDescriptor data(size); + data.stream + << quint64(kMultiDraftTag) + << quint64(peerId) + << quint32(count); + + const auto writeCallback = [&]( + auto&&, // key + MsgId, // msgId + auto&&, // text + bool, // previewCancelled + const MessageCursor &cursor) { // cursor + data.stream + << qint32(cursor.position) + << qint32(cursor.anchor) + << qint32(cursor.scroll); + }; + EnumerateDrafts( + map, + cloudDraft, + supportMode, + replaceKey, + MessageDraft(), + replaceCursor, + writeCallback); + + FileWriteDescriptor file(i->second, _basePath); + file.writeEncrypted(data, _localKey); +} + +void Account::clearDraftCursors(PeerId peerId) { + const auto i = _draftCursorsMap.find(peerId); if (i != _draftCursorsMap.cend()) { ClearKey(i->second, _basePath); _draftCursorsMap.erase(i); @@ -1017,21 +1138,44 @@ void Account::clearDraftCursors(const PeerId &peer) { } } -void Account::readDraftCursors( - const PeerId &peer, - MessageCursor &localCursor, - MessageCursor &editCursor) { - const auto j = _draftCursorsMap.find(peer); +void Account::readDraftCursors(PeerId peerId, Data::HistoryDrafts &map) { + const auto j = _draftCursorsMap.find(peerId); if (j == _draftCursorsMap.cend()) { return; } FileReadDescriptor draft; if (!ReadEncryptedFile(draft, j->second, _basePath, _localKey)) { - clearDraftCursors(peer); + clearDraftCursors(peerId); return; } - quint64 draftPeer; + quint64 tag = 0; + draft.stream >> tag; + if (tag != kMultiDraftTag) { + readDraftCursorsLegacy(peerId, draft, tag, map); + return; + } + quint64 draftPeer = 0; + quint32 count = 0; + draft.stream >> draftPeer >> count; + if (!count || count > 1000 || draftPeer != peerId) { + clearDraftCursors(peerId); + return; + } + for (auto i = 0; i != count; ++i) { + qint32 position = 0, anchor = 0, scroll = QFIXED_MAX; + draft.stream >> position >> anchor >> scroll; + if (const auto i = map.find(Data::DraftKey::Local()); i != end(map)) { + i->second->cursor = MessageCursor(position, anchor, scroll); + } + } +} + +void Account::readDraftCursorsLegacy( + PeerId peerId, + details::FileReadDescriptor &draft, + quint64 draftPeer, + Data::HistoryDrafts &map) { qint32 localPosition = 0, localAnchor = 0, localScroll = QFIXED_MAX; qint32 editPosition = 0, editAnchor = 0, editScroll = QFIXED_MAX; draft.stream >> draftPeer >> localPosition >> localAnchor >> localScroll; @@ -1039,40 +1183,109 @@ void Account::readDraftCursors( draft.stream >> editPosition >> editAnchor >> editScroll; } - if (draftPeer != peer) { - clearDraftCursors(peer); + if (draftPeer != peerId) { + clearDraftCursors(peerId); return; } - localCursor = MessageCursor(localPosition, localAnchor, localScroll); - editCursor = MessageCursor(editPosition, editAnchor, editScroll); + if (const auto i = map.find(Data::DraftKey::Local()); i != end(map)) { + i->second->cursor = MessageCursor( + localPosition, + localAnchor, + localScroll); + } + if (const auto i = map.find(Data::DraftKey::LocalEdit()); i != end(map)) { + i->second->cursor = MessageCursor( + editPosition, + editAnchor, + editScroll); + } } void Account::readDraftsWithCursors(not_null history) { - PeerId peer = history->peer->id; - if (!_draftsNotReadMap.remove(peer)) { - clearDraftCursors(peer); + const auto guard = gsl::finally([&] { + if (const auto migrated = history->migrateFrom()) { + readDraftsWithCursors(migrated); + migrated->clearLocalEditDraft(); + history->takeLocalDraft(migrated); + } + }); + + PeerId peerId = history->peer->id; + if (!_draftsNotReadMap.remove(peerId)) { + clearDraftCursors(peerId); return; } - const auto j = _draftsMap.find(peer); + const auto j = _draftsMap.find(peerId); if (j == _draftsMap.cend()) { - clearDraftCursors(peer); + clearDraftCursors(peerId); return; } FileReadDescriptor draft; if (!ReadEncryptedFile(draft, j->second, _basePath, _localKey)) { ClearKey(j->second, _basePath); _draftsMap.erase(j); - clearDraftCursors(peer); + clearDraftCursors(peerId); return; } + quint64 tag = 0; + draft.stream >> tag; + if (tag != kMultiDraftTag) { + readDraftsWithCursorsLegacy(history, draft, tag); + return; + } + quint32 count = 0; quint64 draftPeer = 0; + draft.stream >> draftPeer >> count; + if (!count || count > 1000 || draftPeer != peerId) { + ClearKey(j->second, _basePath); + _draftsMap.erase(j); + clearDraftCursors(peerId); + return; + } + auto map = Data::HistoryDrafts(); + for (auto i = 0; i != count; ++i) { + TextWithTags data; + QByteArray tagsSerialized; + qint32 keyValue = 0, messageId = 0, previewCancelled = 0; + draft.stream + >> keyValue + >> data.text + >> tagsSerialized + >> messageId + >> previewCancelled; + data.tags = TextUtilities::DeserializeTags( + tagsSerialized, + data.text.size()); + const auto key = Data::DraftKey::FromSerialized(keyValue); + if (key && key != Data::DraftKey::Cloud()) { + map.emplace(key, std::make_unique( + data, + messageId, + MessageCursor(), + previewCancelled)); + } + } + if (draft.stream.status() != QDataStream::Ok) { + ClearKey(j->second, _basePath); + _draftsMap.erase(j); + clearDraftCursors(peerId); + return; + } + readDraftCursors(peerId, map); + history->setDraftsMap(std::move(map)); +} + +void Account::readDraftsWithCursorsLegacy( + not_null history, + details::FileReadDescriptor &draft, + quint64 draftPeer) { TextWithTags msgData, editData; QByteArray msgTagsSerialized, editTagsSerialized; qint32 msgReplyTo = 0, msgPreviewCancelled = 0, editMsgId = 0, editPreviewCancelled = 0; - draft.stream >> draftPeer >> msgData.text; + draft.stream >> msgData.text; if (draft.version >= 9048) { draft.stream >> msgTagsSerialized; } @@ -1089,10 +1302,14 @@ void Account::readDraftsWithCursors(not_null history) { } } } - if (draftPeer != peer) { - ClearKey(j->second, _basePath); - _draftsMap.erase(j); - clearDraftCursors(peer); + const auto peerId = history->peer->id; + if (draftPeer != peerId) { + const auto j = _draftsMap.find(peerId); + if (j != _draftsMap.cend()) { + ClearKey(j->second, _basePath); + _draftsMap.erase(j); + } + clearDraftCursors(peerId); return; } @@ -1103,65 +1320,30 @@ void Account::readDraftsWithCursors(not_null history) { editTagsSerialized, editData.text.size()); - MessageCursor msgCursor, editCursor; - readDraftCursors(peer, msgCursor, editCursor); - - if (!history->localDraft()) { - if (msgData.text.isEmpty() && !msgReplyTo) { - history->clearLocalDraft(); - } else { - history->setLocalDraft(std::make_unique( - msgData, - msgReplyTo, - msgCursor, - msgPreviewCancelled)); - } + auto map = base::flat_map>(); + if (!msgData.text.isEmpty() || msgReplyTo) { + map.emplace(Data::DraftKey::Local(), std::make_unique( + msgData, + msgReplyTo, + MessageCursor(), + msgPreviewCancelled)); } - if (!editMsgId) { - history->clearEditDraft(); - } else { - history->setEditDraft(std::make_unique( + if (editMsgId) { + map.emplace(Data::DraftKey::LocalEdit(), std::make_unique( editData, editMsgId, - editCursor, + MessageCursor(), editPreviewCancelled)); } + readDraftCursors(peerId, map); + history->setDraftsMap(std::move(map)); } -void Account::writeDraftCursors( - const PeerId &peer, - const MessageCursor &msgCursor, - const MessageCursor &editCursor) { - if (msgCursor == MessageCursor() && editCursor == MessageCursor()) { - clearDraftCursors(peer); - } else { - auto i = _draftCursorsMap.find(peer); - if (i == _draftCursorsMap.cend()) { - i = _draftCursorsMap.emplace(peer, GenerateKey(_basePath)).first; - writeMapQueued(); - } - - EncryptedDescriptor data(sizeof(quint64) + sizeof(qint32) * 3); - data.stream - << quint64(peer) - << qint32(msgCursor.position) - << qint32(msgCursor.anchor) - << qint32(msgCursor.scroll); - data.stream - << qint32(editCursor.position) - << qint32(editCursor.anchor) - << qint32(editCursor.scroll); - - FileWriteDescriptor file(i->second, _basePath); - file.writeEncrypted(data, _localKey); - } -} - -bool Account::hasDraftCursors(const PeerId &peer) { +bool Account::hasDraftCursors(PeerId peer) { return _draftCursorsMap.contains(peer); } -bool Account::hasDraft(const PeerId &peer) { +bool Account::hasDraft(PeerId peer) { return _draftsMap.contains(peer); } diff --git a/Telegram/SourceFiles/storage/storage_account.h b/Telegram/SourceFiles/storage/storage_account.h index d40e4638be..1aeb64d0b2 100644 --- a/Telegram/SourceFiles/storage/storage_account.h +++ b/Telegram/SourceFiles/storage/storage_account.h @@ -10,6 +10,7 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "base/timer.h" #include "storage/cache/storage_cache_database.h" #include "data/stickers/data_stickers_set.h" +#include "data/data_drafts.h" class History; @@ -39,6 +40,7 @@ using AuthKeyPtr = std::shared_ptr; namespace Storage { namespace details { struct ReadSettingsContext; +struct FileReadDescriptor; } // namespace details class EncryptionKey; @@ -76,18 +78,17 @@ public: void writeMtpData(); void writeMtpConfig(); - void writeDrafts(not_null history); void writeDrafts( - const PeerId &peer, - const MessageDraft &localDraft, - const MessageDraft &editDraft); + not_null history, + Data::DraftKey replaceKey = Data::DraftKey::None(), + MessageDraft replaceDraft = MessageDraft()); void readDraftsWithCursors(not_null history); void writeDraftCursors( - const PeerId &peer, - const MessageCursor &localCursor, - const MessageCursor &editCursor); - [[nodiscard]] bool hasDraftCursors(const PeerId &peer); - [[nodiscard]] bool hasDraft(const PeerId &peer); + not_null history, + Data::DraftKey replaceKey = Data::DraftKey::None(), + MessageCursor replaceCursor = MessageCursor()); + [[nodiscard]] bool hasDraftCursors(PeerId peerId); + [[nodiscard]] bool hasDraft(PeerId peerId); void writeFileLocation(MediaKey location, const Core::FileLocation &local); [[nodiscard]] Core::FileLocation readFileLocation(MediaKey location); @@ -182,11 +183,17 @@ private: std::unique_ptr applyReadContext( details::ReadSettingsContext &&context); - void readDraftCursors( - const PeerId &peer, - MessageCursor &localCursor, - MessageCursor &editCursor); - void clearDraftCursors(const PeerId &peer); + void readDraftCursors(PeerId peerId, Data::HistoryDrafts &map); + void readDraftCursorsLegacy( + PeerId peerId, + details::FileReadDescriptor &draft, + quint64 draftPeer, + Data::HistoryDrafts &map); + void clearDraftCursors(PeerId peerId); + void readDraftsWithCursorsLegacy( + not_null history, + details::FileReadDescriptor &draft, + quint64 draftPeer); void writeStickerSet( QDataStream &stream, diff --git a/Telegram/SourceFiles/window/window_peer_menu.cpp b/Telegram/SourceFiles/window/window_peer_menu.cpp index 6600262815..02218bd024 100644 --- a/Telegram/SourceFiles/window/window_peer_menu.cpp +++ b/Telegram/SourceFiles/window/window_peer_menu.cpp @@ -617,7 +617,9 @@ void Filler::addPollAction(not_null peer) { ? Api::SendType::Scheduled : Api::SendType::Normal; const auto flag = PollData::Flags(); - const auto replyToId = _request.currentReplyToId; + const auto replyToId = _request.currentReplyToId + ? _request.currentReplyToId + : _request.rootId; auto callback = [=] { PeerMenuCreatePoll( controller,