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