Save local drafts in scheduled / replies sections.

Fix inline bot switch inline in scheduled / replies sections.
This commit is contained in:
John Preston 2020-11-13 20:27:08 +03:00
parent 4a0efb9114
commit b3eb7858e6
15 changed files with 1025 additions and 390 deletions

View File

@ -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);
}
}
}

View File

@ -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<DraftKey, std::unique_ptr<Draft>>;
inline bool draftStringIsEmpty(const QString &text) {
for_const (auto ch, text) {
if (!ch.isSpace()) {

View File

@ -179,22 +179,22 @@ void History::itemVanished(not_null<HistoryItem*> item) {
}
}
void History::setLocalDraft(std::unique_ptr<Data::Draft> &&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<History*> 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<Data::Draft> &&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<Data::Draft> &&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<Data::Draft> &&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);

View File

@ -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<Data::Draft> &&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<Data::Draft> &&draft) {
setDraft(Data::DraftKey::Local(), std::move(draft));
}
void setLocalDraft(std::unique_ptr<Data::Draft> &&draft);
void takeLocalDraft(History *from);
void setCloudDraft(std::unique_ptr<Data::Draft> &&draft);
void setLocalEditDraft(std::unique_ptr<Data::Draft> &&draft) {
setDraft(Data::DraftKey::LocalEdit(), std::move(draft));
}
void setCloudDraft(std::unique_ptr<Data::Draft> &&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<Data::Draft> &&draft);
void clearLocalDraft();
void clearCloudDraft();
void takeLocalDraft(not_null<History*> 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<BuildingBlock> _buildingFrontBlock;
std::unique_ptr<Data::Draft> _localDraft, _cloudDraft;
std::unique_ptr<Data::Draft> _editDraft;
Data::HistoryDrafts _drafts;
std::optional<QString> _lastSentDraftText;
TimeId _lastSentDraftTime = 0;
MessageIdsList _forwardDraft;

View File

@ -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<Data::Draft>(_field, _editMsgId, _previewCancelled, _saveEditMsgRequestId));
_history->setLocalEditDraft(std::make_unique<Data::Draft>(_field, _editMsgId, _previewCancelled, _saveEditMsgRequestId));
} else {
if (_replyToId || !_field->empty()) {
_history->setLocalDraft(std::make_unique<Data::Draft>(_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*> 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<HistoryItem*> item) {
editData.text.size(),
QFIXED_MAX
};
_history->setEditDraft(std::make_unique<Data::Draft>(
_history->setLocalEditDraft(std::make_unique<Data::Draft>(
editData,
item->id,
cursor,
@ -5693,7 +5669,7 @@ void HistoryWidget::cancelEdit() {
_replyEditMsg = nullptr;
_editMsgId = 0;
_history->clearEditDraft();
_history->clearLocalEditDraft();
applyDraft();
if (_saveEditMsgRequestId) {

View File

@ -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,

View File

@ -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<bool> 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<QString> _title;
rpl::variable<QString> _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<QString> title,
rpl::producer<QString> 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<Data::Draft>(
_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<HistoryItem*> 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<HistoryItem*> 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<Data::Draft>(
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<Data::Draft>(
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<QString, WebPageId>;
const auto previewCache = lifetime.make_state<PreviewCache>();
const auto previewRequest = lifetime.make_state<mtpRequestId>(0);
const auto previewCancelled = lifetime.make_state<bool>(false);
const auto mtpSender =
lifetime.make_state<MTP::Sender>(&_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();
}
}
});

View File

@ -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<TextUpdateEvent>;
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<HistoryItem*> item);
void escape();
void fieldChanged();
@ -185,11 +197,8 @@ private:
void createTabbedPanel();
void setTabbedPanel(std::unique_ptr<ChatHelpers::TabbedPanel> panel);
void setTextFromEditingMessage(not_null<HistoryItem*> 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<QWidget*> _parent;
const not_null<Window::SessionController*> _window;
History *_history = nullptr;
@ -238,12 +265,14 @@ private:
rpl::event_stream<SendActionUpdate> _sendActionUpdates;
rpl::event_stream<QString> _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<void()> _previewCancel;
bool _previewCancelled = false;
rpl::lifetime _uploaderSubscriptions;
Fn<void()> _raiseEmojiSuggestions;

View File

@ -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);

View File

@ -533,7 +533,7 @@ bool MainWidget::shareUrl(
auto history = peer->owner().history(peer);
history->setLocalDraft(
std::make_unique<Data::Draft>(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<Data::Draft>(textWithTags, 0, cursor, false));
h->clearEditDraft();
h->clearLocalEditDraft();
const auto opened = _history->peer() && (_history->peer() == peer);
if (opened) {
_history->applyDraft();

View File

@ -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);
}

View File

@ -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) ? &params->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) ? &params->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;
}

View File

@ -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<MTP::Config> Account::readMtpConfig() {
return MTP::Config::FromSerialized(serialized);
}
void Account::writeDrafts(not_null<History*> 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 <typename Callback>
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*> 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*> 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*> 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::Draft>(
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*> 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*> 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*> 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<Data::Draft>(
msgData,
msgReplyTo,
msgCursor,
msgPreviewCancelled));
}
auto map = base::flat_map<Data::DraftKey, std::unique_ptr<Data::Draft>>();
if (!msgData.text.isEmpty() || msgReplyTo) {
map.emplace(Data::DraftKey::Local(), std::make_unique<Data::Draft>(
msgData,
msgReplyTo,
MessageCursor(),
msgPreviewCancelled));
}
if (!editMsgId) {
history->clearEditDraft();
} else {
history->setEditDraft(std::make_unique<Data::Draft>(
if (editMsgId) {
map.emplace(Data::DraftKey::LocalEdit(), std::make_unique<Data::Draft>(
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);
}

View File

@ -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<AuthKey>;
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*> history);
void writeDrafts(
const PeerId &peer,
const MessageDraft &localDraft,
const MessageDraft &editDraft);
not_null<History*> history,
Data::DraftKey replaceKey = Data::DraftKey::None(),
MessageDraft replaceDraft = MessageDraft());
void readDraftsWithCursors(not_null<History*> 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*> 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<Main::SessionSettings> 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*> history,
details::FileReadDescriptor &draft,
quint64 draftPeer);
void writeStickerSet(
QDataStream &stream,

View File

@ -617,7 +617,9 @@ void Filler::addPollAction(not_null<PeerData*> 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,