mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2025-04-15 21:57:10 +02:00
Save local drafts in scheduled / replies sections.
Fix inline bot switch inline in scheduled / replies sections.
This commit is contained in:
parent
4a0efb9114
commit
b3eb7858e6
15 changed files with 1025 additions and 390 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue