mirror of
https://github.com/telegramdesktop/tdesktop
synced 2025-03-23 11:47:57 +00:00
Saving FlatTextarea tags to drafts, applying them in setText.
Now instead of plain text a TextWithTags struct is used almost everywhere. Started writing and reading serialized tags to drafts from 9048, switched version to 0.9.48 for testing.
This commit is contained in:
parent
5a47d8e29b
commit
463450e607
Telegram
Resources/winrc
SourceFiles
core
history.hhistorywidget.cpphistorywidget.hlocalstorage.cpplocalstorage.hmainwidget.cppmainwidget.hpspecific_mac.cppserialize
ui
Telegram.xcodeproj
build
@ -34,8 +34,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico"
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 0,9,47,0
|
||||
PRODUCTVERSION 0,9,47,0
|
||||
FILEVERSION 0,9,48,0
|
||||
PRODUCTVERSION 0,9,48,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@ -51,10 +51,10 @@ BEGIN
|
||||
BLOCK "040904b0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram Messenger LLP"
|
||||
VALUE "FileVersion", "0.9.47.0"
|
||||
VALUE "FileVersion", "0.9.48.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2016"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "0.9.47.0"
|
||||
VALUE "ProductVersion", "0.9.48.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
@ -25,8 +25,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
//
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 0,9,47,0
|
||||
PRODUCTVERSION 0,9,47,0
|
||||
FILEVERSION 0,9,48,0
|
||||
PRODUCTVERSION 0,9,48,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS 0x1L
|
||||
@ -43,10 +43,10 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "Telegram Messenger LLP"
|
||||
VALUE "FileDescription", "Telegram Updater"
|
||||
VALUE "FileVersion", "0.9.47.0"
|
||||
VALUE "FileVersion", "0.9.48.0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2014-2016"
|
||||
VALUE "ProductName", "Telegram Desktop"
|
||||
VALUE "ProductVersion", "0.9.47.0"
|
||||
VALUE "ProductVersion", "0.9.48.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
@ -166,6 +166,16 @@ template <typename T, size_t N> char(&ArraySizeHelper(T(&array)[N]))[N];
|
||||
#define qsl(s) QStringLiteral(s)
|
||||
#define qstr(s) QLatin1String(s, sizeof(s) - 1)
|
||||
|
||||
// For QFlags<> declared in private section of a class we need to declare
|
||||
// operators from Q_DECLARE_OPERATORS_FOR_FLAGS as friend functions.
|
||||
#define Q_DECLARE_FRIEND_INCOMPATIBLE_FLAGS(Flags) \
|
||||
friend QIncompatibleFlag operator|(Flags::enum_type f1, int f2) Q_DECL_NOTHROW;
|
||||
|
||||
#define Q_DECLARE_FRIEND_OPERATORS_FOR_FLAGS(Flags) \
|
||||
friend QFlags<Flags::enum_type> operator|(Flags::enum_type f1, Flags::enum_type f2) Q_DECL_NOTHROW; \
|
||||
friend QFlags<Flags::enum_type> operator|(Flags::enum_type f1, QFlags<Flags::enum_type> f2) Q_DECL_NOTHROW; \
|
||||
Q_DECLARE_FRIEND_INCOMPATIBLE_FLAGS(Flags)
|
||||
|
||||
// using for_const instead of plain range-based for loop to ensure usage of const_iterator
|
||||
// it is important for the copy-on-write Qt containers
|
||||
// if you have "QVector<T*> v" then "for (T * const p : v)" will still call QVector::detach(),
|
||||
|
@ -24,7 +24,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
|
||||
|
||||
#define BETA_VERSION_MACRO (0ULL)
|
||||
|
||||
constexpr int AppVersion = 9047;
|
||||
constexpr str_const AppVersionStr = "0.9.47";
|
||||
constexpr int AppVersion = 9048;
|
||||
constexpr str_const AppVersionStr = "0.9.48";
|
||||
constexpr bool AppAlphaVersion = true;
|
||||
constexpr uint64 AppBetaVersion = BETA_VERSION_MACRO;
|
||||
|
@ -151,22 +151,23 @@ struct SendAction {
|
||||
int32 progress;
|
||||
};
|
||||
|
||||
using TextWithTags = FlatTextarea::TextWithTags;
|
||||
struct HistoryDraft {
|
||||
HistoryDraft() : msgId(0), previewCancelled(false) {
|
||||
}
|
||||
HistoryDraft(const QString &text, MsgId msgId, const MessageCursor &cursor, bool previewCancelled)
|
||||
: text(text)
|
||||
HistoryDraft(const TextWithTags &textWithTags, MsgId msgId, const MessageCursor &cursor, bool previewCancelled)
|
||||
: textWithTags(textWithTags)
|
||||
, msgId(msgId)
|
||||
, cursor(cursor)
|
||||
, previewCancelled(previewCancelled) {
|
||||
}
|
||||
HistoryDraft(const FlatTextarea &field, MsgId msgId, bool previewCancelled)
|
||||
: text(field.getLastText())
|
||||
: textWithTags(field.getTextWithTags())
|
||||
, msgId(msgId)
|
||||
, cursor(field)
|
||||
, previewCancelled(previewCancelled) {
|
||||
}
|
||||
QString text;
|
||||
TextWithTags textWithTags;
|
||||
MsgId msgId; // replyToId for message draft, editMsgId for edit draft
|
||||
MessageCursor cursor;
|
||||
bool previewCancelled;
|
||||
@ -176,8 +177,8 @@ struct HistoryEditDraft : public HistoryDraft {
|
||||
: HistoryDraft()
|
||||
, saveRequest(0) {
|
||||
}
|
||||
HistoryEditDraft(const QString &text, MsgId msgId, const MessageCursor &cursor, bool previewCancelled, mtpRequestId saveRequest = 0)
|
||||
: HistoryDraft(text, msgId, cursor, previewCancelled)
|
||||
HistoryEditDraft(const TextWithTags &textWithTags, MsgId msgId, const MessageCursor &cursor, bool previewCancelled, mtpRequestId saveRequest = 0)
|
||||
: HistoryDraft(textWithTags, msgId, cursor, previewCancelled)
|
||||
, saveRequest(saveRequest) {
|
||||
}
|
||||
HistoryEditDraft(const FlatTextarea &field, MsgId msgId, bool previewCancelled, mtpRequestId saveRequest = 0)
|
||||
@ -373,7 +374,7 @@ public:
|
||||
}
|
||||
void takeMsgDraft(History *from) {
|
||||
if (auto &draft = from->_msgDraft) {
|
||||
if (!draft->text.isEmpty() && !_msgDraft) {
|
||||
if (!draft->textWithTags.text.isEmpty() && !_msgDraft) {
|
||||
_msgDraft = std_::move(draft);
|
||||
_msgDraft->msgId = 0; // edit and reply to drafts can't migrate
|
||||
}
|
||||
|
@ -2053,8 +2053,8 @@ MessageField::MessageField(HistoryWidget *history, const style::flatTextarea &st
|
||||
}
|
||||
|
||||
bool MessageField::hasSendText() const {
|
||||
const QString &text(getLastText());
|
||||
for (const QChar *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) {
|
||||
auto &text(getTextWithTags().text);
|
||||
for (auto *ch = text.constData(), *e = ch + text.size(); ch != e; ++ch) {
|
||||
ushort code = ch->unicode();
|
||||
if (code != ' ' && code != '\n' && code != '\r' && !chReplacedBySpace(code)) {
|
||||
return true;
|
||||
@ -2735,7 +2735,7 @@ QPoint SilentToggle::tooltipPos() const {
|
||||
return QCursor::pos();
|
||||
}
|
||||
|
||||
EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags) {
|
||||
EntitiesInText entitiesFromTextTags(const FlatTextarea::TagList &tags) {
|
||||
EntitiesInText result;
|
||||
if (tags.isEmpty()) {
|
||||
return result;
|
||||
@ -2754,6 +2754,24 @@ EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags) {
|
||||
return result;
|
||||
}
|
||||
|
||||
TextWithTags::Tags textTagsFromEntities(const EntitiesInText &entities) {
|
||||
TextWithTags::Tags result;
|
||||
if (entities.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result.reserve(entities.size());
|
||||
for_const (auto &entity, entities) {
|
||||
if (entity.type() == EntityInTextMentionName) {
|
||||
auto match = QRegularExpression("^(\\d+\\.\\d+)$").match(entity.data());
|
||||
if (match.hasMatch()) {
|
||||
result.push_back({ entity.offset(), entity.length(), qstr("mention://user.") + entity.data() });
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// For mention tags save and validate userId, ignore tags for different userId.
|
||||
@ -2974,7 +2992,7 @@ void HistoryWidget::onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::
|
||||
// Send bot command at once, if it was not inserted by pressing Tab.
|
||||
if (str.at(0) == '/' && method != FieldAutocomplete::ChooseMethod::ByTab) {
|
||||
App::sendBotCommand(_peer, nullptr, str);
|
||||
setFieldText(_field.getLastText().mid(_field.textCursor().position()));
|
||||
setFieldText(_field.getTextWithTagsPart(_field.textCursor().position()));
|
||||
} else {
|
||||
_field.insertTag(str);
|
||||
}
|
||||
@ -3022,15 +3040,16 @@ void HistoryWidget::updateInlineBotQuery() {
|
||||
|
||||
void HistoryWidget::updateStickersByEmoji() {
|
||||
int32 len = 0;
|
||||
if (EmojiPtr emoji = emojiFromText(_field.getLastText(), &len)) {
|
||||
if (_field.getLastText().size() <= len) {
|
||||
_fieldAutocomplete->showStickers(emoji);
|
||||
} else {
|
||||
auto &text = _field.getTextWithTags().text;
|
||||
if (EmojiPtr emoji = emojiFromText(text, &len)) {
|
||||
if (text.size() > len) {
|
||||
len = 0;
|
||||
} else {
|
||||
_fieldAutocomplete->showStickers(emoji);
|
||||
}
|
||||
}
|
||||
if (!len) {
|
||||
_fieldAutocomplete->showStickers(EmojiPtr(0));
|
||||
_fieldAutocomplete->showStickers(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
@ -3039,7 +3058,7 @@ void HistoryWidget::onTextChange() {
|
||||
updateStickersByEmoji();
|
||||
|
||||
if (_peer && (!_peer->isChannel() || _peer->isMegagroup() || !_peer->asChannel()->canPublish() || (!_peer->asChannel()->isBroadcast() && !_broadcast.checked()))) {
|
||||
if (!_inlineBot && !_editMsgId && (_textUpdateEventsFlags & TextUpdateEventsSendTyping)) {
|
||||
if (!_inlineBot && !_editMsgId && (_textUpdateEvents.testFlag(TextUpdateEvent::SendTyping))) {
|
||||
updateSendAction(_history, SendActionTyping);
|
||||
}
|
||||
}
|
||||
@ -3065,13 +3084,13 @@ void HistoryWidget::onTextChange() {
|
||||
update();
|
||||
}
|
||||
|
||||
if (!_peer || !(_textUpdateEventsFlags & TextUpdateEventsSaveDraft)) return;
|
||||
if (!_peer || !(_textUpdateEvents.testFlag(TextUpdateEvent::SaveDraft))) return;
|
||||
_saveDraftText = true;
|
||||
onDraftSave(true);
|
||||
}
|
||||
|
||||
void HistoryWidget::onDraftSaveDelayed() {
|
||||
if (!_peer || !(_textUpdateEventsFlags & TextUpdateEventsSaveDraft)) return;
|
||||
if (!_peer || !(_textUpdateEvents.testFlag(TextUpdateEvent::SaveDraft))) return;
|
||||
if (!_field.textCursor().anchor() && !_field.textCursor().position() && !_field.verticalScrollBar()->value()) {
|
||||
if (!Local::hasDraftCursors(_peer->id)) {
|
||||
return;
|
||||
@ -3106,17 +3125,17 @@ void HistoryWidget::writeDrafts(HistoryDraft **msgDraft, HistoryEditDraft **edit
|
||||
Local::MessageDraft localMsgDraft, localEditDraft;
|
||||
if (msgDraft) {
|
||||
if (*msgDraft) {
|
||||
localMsgDraft = Local::MessageDraft((*msgDraft)->msgId, (*msgDraft)->text, (*msgDraft)->previewCancelled);
|
||||
localMsgDraft = Local::MessageDraft((*msgDraft)->msgId, (*msgDraft)->textWithTags, (*msgDraft)->previewCancelled);
|
||||
}
|
||||
} else {
|
||||
localMsgDraft = Local::MessageDraft(_replyToId, _field.getLastText(), _previewCancelled);
|
||||
localMsgDraft = Local::MessageDraft(_replyToId, _field.getTextWithTags(), _previewCancelled);
|
||||
}
|
||||
if (editDraft) {
|
||||
if (*editDraft) {
|
||||
localEditDraft = Local::MessageDraft((*editDraft)->msgId, (*editDraft)->text, (*editDraft)->previewCancelled);
|
||||
localEditDraft = Local::MessageDraft((*editDraft)->msgId, (*editDraft)->textWithTags, (*editDraft)->previewCancelled);
|
||||
}
|
||||
} else if (_editMsgId) {
|
||||
localEditDraft = Local::MessageDraft(_editMsgId, _field.getLastText(), _previewCancelled);
|
||||
localEditDraft = Local::MessageDraft(_editMsgId, _field.getTextWithTags(), _previewCancelled);
|
||||
}
|
||||
Local::writeDrafts(_peer->id, localMsgDraft, localEditDraft);
|
||||
if (_migrated) {
|
||||
@ -3152,11 +3171,11 @@ void HistoryWidget::writeDrafts(History *history) {
|
||||
Local::MessageDraft localMsgDraft, localEditDraft;
|
||||
MessageCursor msgCursor, editCursor;
|
||||
if (auto msgDraft = history->msgDraft()) {
|
||||
localMsgDraft = Local::MessageDraft(msgDraft->msgId, msgDraft->text, msgDraft->previewCancelled);
|
||||
localMsgDraft = Local::MessageDraft(msgDraft->msgId, msgDraft->textWithTags, msgDraft->previewCancelled);
|
||||
msgCursor = msgDraft->cursor;
|
||||
}
|
||||
if (auto editDraft = history->editDraft()) {
|
||||
localEditDraft = Local::MessageDraft(editDraft->msgId, editDraft->text, editDraft->previewCancelled);
|
||||
localEditDraft = Local::MessageDraft(editDraft->msgId, editDraft->textWithTags, editDraft->previewCancelled);
|
||||
editCursor = editDraft->cursor;
|
||||
}
|
||||
Local::writeDrafts(history->peer->id, localMsgDraft, localEditDraft);
|
||||
@ -3331,8 +3350,9 @@ bool HistoryWidget::notify_switchInlineBotButtonReceived(const QString &query) {
|
||||
}
|
||||
bot->botInfo->inlineReturnPeerId = 0;
|
||||
History *h = App::history(toPeerId);
|
||||
auto text = '@' + bot->username + ' ' + query;
|
||||
h->setMsgDraft(std_::make_unique<HistoryDraft>(text, 0, MessageCursor(text.size(), text.size(), QFIXED_MAX), false));
|
||||
TextWithTags textWithTags = { '@' + bot->username + ' ' + query, TextWithTags::Tags() };
|
||||
MessageCursor cursor = { textWithTags.text.size(), textWithTags.text.size(), QFIXED_MAX };
|
||||
h->setMsgDraft(std_::make_unique<HistoryDraft>(textWithTags, 0, cursor, false));
|
||||
if (h == _history) {
|
||||
applyDraft();
|
||||
} else {
|
||||
@ -3633,11 +3653,11 @@ void HistoryWidget::applyDraft(bool parseLinks) {
|
||||
return;
|
||||
}
|
||||
|
||||
_textUpdateEventsFlags = 0;
|
||||
setFieldText(draft->text);
|
||||
_textUpdateEvents = 0;
|
||||
setFieldText(draft->textWithTags);
|
||||
_field.setFocus();
|
||||
draft->cursor.applyTo(_field);
|
||||
_textUpdateEventsFlags = TextUpdateEventsSaveDraft | TextUpdateEventsSendTyping;
|
||||
_textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping;
|
||||
_previewCancelled = draft->previewCancelled;
|
||||
if (auto editDraft = _history->editDraft()) {
|
||||
_editMsgId = editDraft->msgId;
|
||||
@ -3730,7 +3750,7 @@ void HistoryWidget::showHistory(const PeerId &peerId, MsgId showAtMsgId, bool re
|
||||
if (_editMsgId) {
|
||||
_history->setEditDraft(std_::make_unique<HistoryEditDraft>(_field, _editMsgId, _previewCancelled, _saveEditMsgRequestId));
|
||||
} else {
|
||||
if (_replyToId || !_field.getLastText().isEmpty()) {
|
||||
if (_replyToId || !_field.isEmpty()) {
|
||||
_history->setMsgDraft(std_::make_unique<HistoryDraft>(_field, _replyToId, _previewCancelled));
|
||||
} else {
|
||||
_history->clearMsgDraft();
|
||||
@ -4738,11 +4758,11 @@ void HistoryWidget::preloadHistoryIfNeeded() {
|
||||
}
|
||||
|
||||
void HistoryWidget::onInlineBotCancel() {
|
||||
QString text = _field.getLastText();
|
||||
if (text.size() > _inlineBotUsername.size() + 2) {
|
||||
setFieldText('@' + _inlineBotUsername + ' ', TextUpdateEventsSaveDraft, false);
|
||||
auto &textWithTags = _field.getTextWithTags();
|
||||
if (textWithTags.text.size() > _inlineBotUsername.size() + 2) {
|
||||
setFieldText({ '@' + _inlineBotUsername + ' ', TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory);
|
||||
} else {
|
||||
clearFieldText(TextUpdateEventsSaveDraft, false);
|
||||
clearFieldText(TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4783,11 +4803,10 @@ void HistoryWidget::saveEditMsg() {
|
||||
|
||||
WebPageId webPageId = _previewCancelled ? CancelledWebPageId : ((_previewData && _previewData->pendingTill >= 0) ? _previewData->id : 0);
|
||||
|
||||
auto fieldText = _field.getLastText();
|
||||
auto fieldTags = _field.getLastTags();
|
||||
auto &textWithTags = _field.getTextWithTags();
|
||||
auto prepareFlags = itemTextOptions(_history, App::self()).flags;
|
||||
EntitiesInText sendingEntities, leftEntities = entitiesFromFieldTags(fieldTags);
|
||||
QString sendingText, leftText = prepareTextWithEntities(fieldText, prepareFlags, &leftEntities);
|
||||
EntitiesInText sendingEntities, leftEntities = entitiesFromTextTags(textWithTags.tags);
|
||||
QString sendingText, leftText = prepareTextWithEntities(textWithTags.text, prepareFlags, &leftEntities);
|
||||
|
||||
if (!textSplit(sendingText, sendingEntities, leftText, leftEntities, MaxMessageSize)) {
|
||||
_field.selectAll();
|
||||
@ -4865,8 +4884,7 @@ void HistoryWidget::onSend(bool ctrlShiftEnter, MsgId replyTo) {
|
||||
|
||||
MainWidget::MessageToSend message;
|
||||
message.history = _history;
|
||||
message.text = _field.getLastText();
|
||||
message.entities = _field.getLastTags();
|
||||
message.textWithTags = _field.getTextWithTags();
|
||||
message.replyTo = replyTo;
|
||||
message.broadcast = _broadcast.checked();
|
||||
message.silent = _silent.checked();
|
||||
@ -5379,7 +5397,7 @@ void HistoryWidget::sendBotCommand(PeerData *peer, UserData *bot, const QString
|
||||
|
||||
MainWidget::MessageToSend message;
|
||||
message.history = _history;
|
||||
message.text = toSend;
|
||||
message.textWithTags = { toSend, TextWithTags::Tags() };
|
||||
message.replyTo = replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? replyTo : -1) : 0;
|
||||
message.broadcast = false;
|
||||
message.silent = false;
|
||||
@ -5460,8 +5478,9 @@ bool HistoryWidget::botCallbackFail(BotCallbackInfo info, const RPCError &error,
|
||||
bool HistoryWidget::insertBotCommand(const QString &cmd, bool specialGif) {
|
||||
if (!_history) return false;
|
||||
|
||||
bool insertingInlineBot = !cmd.isEmpty() && (cmd.at(0) == '@');
|
||||
QString toInsert = cmd;
|
||||
if (!toInsert.isEmpty() && toInsert.at(0) != '@') {
|
||||
if (!toInsert.isEmpty() && !insertingInlineBot) {
|
||||
PeerData *bot = _peer->isUser() ? _peer : (App::hoveredLinkItem() ? App::hoveredLinkItem()->fromOriginal() : 0);
|
||||
if (!bot->isUser() || !bot->asUser()->botInfo) bot = 0;
|
||||
QString username = bot ? bot->asUser()->username : QString();
|
||||
@ -5472,28 +5491,33 @@ bool HistoryWidget::insertBotCommand(const QString &cmd, bool specialGif) {
|
||||
}
|
||||
toInsert += ' ';
|
||||
|
||||
if (toInsert.at(0) != '@') {
|
||||
QString text = _field.getLastText();
|
||||
if (!insertingInlineBot) {
|
||||
auto &textWithTags = _field.getTextWithTags();
|
||||
if (specialGif) {
|
||||
if (text.trimmed() == '@' + cInlineGifBotUsername() && text.at(0) == '@') {
|
||||
clearFieldText(TextUpdateEventsSaveDraft, false);
|
||||
if (textWithTags.text.trimmed() == '@' + cInlineGifBotUsername() && textWithTags.text.at(0) == '@') {
|
||||
clearFieldText(TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory);
|
||||
}
|
||||
} else {
|
||||
QRegularExpressionMatch m = QRegularExpression(qsl("^/[A-Za-z_0-9]{0,64}(@[A-Za-z_0-9]{0,32})?(\\s|$)")).match(text);
|
||||
TextWithTags textWithTagsToSet;
|
||||
QRegularExpressionMatch m = QRegularExpression(qsl("^/[A-Za-z_0-9]{0,64}(@[A-Za-z_0-9]{0,32})?(\\s|$)")).match(textWithTags.text);
|
||||
if (m.hasMatch()) {
|
||||
text = toInsert + text.mid(m.capturedLength());
|
||||
textWithTagsToSet = _field.getTextWithTagsPart(m.capturedLength());
|
||||
} else {
|
||||
text = toInsert + text;
|
||||
textWithTagsToSet = textWithTags;
|
||||
}
|
||||
_field.setTextFast(text);
|
||||
textWithTagsToSet.text = toInsert + textWithTagsToSet.text;
|
||||
for (auto &tag : textWithTagsToSet.tags) {
|
||||
tag.offset += toInsert.size();
|
||||
}
|
||||
_field.setTextWithTags(textWithTagsToSet);
|
||||
|
||||
QTextCursor cur(_field.textCursor());
|
||||
cur.movePosition(QTextCursor::End);
|
||||
_field.setTextCursor(cur);
|
||||
}
|
||||
} else {
|
||||
if (!specialGif || _field.getLastText().isEmpty()) {
|
||||
setFieldText(toInsert, TextUpdateEventsSaveDraft, false);
|
||||
if (!specialGif || _field.isEmpty()) {
|
||||
setFieldText({ toInsert, TextWithTags::Tags() }, TextUpdateEvent::SaveDraft, FlatTextarea::AddToUndoHistory);
|
||||
_field.setFocus();
|
||||
return true;
|
||||
}
|
||||
@ -5787,7 +5811,7 @@ void HistoryWidget::onKbToggle(bool manual) {
|
||||
}
|
||||
|
||||
void HistoryWidget::onCmdStart() {
|
||||
setFieldText(qsl("/"));
|
||||
setFieldText({ qsl("/"), TextWithTags::Tags() }, 0, FlatTextarea::AddToUndoHistory);
|
||||
}
|
||||
|
||||
void HistoryWidget::contextMenuEvent(QContextMenuEvent *e) {
|
||||
@ -6998,7 +7022,7 @@ void HistoryWidget::keyPressEvent(QKeyEvent *e) {
|
||||
} else if (e->key() == Qt::Key_Up) {
|
||||
if (!(e->modifiers() & (Qt::ShiftModifier | Qt::MetaModifier | Qt::ControlModifier))) {
|
||||
if (_history && _history->lastSentMsg && _history->lastSentMsg->canEdit(::date(unixtime()))) {
|
||||
if (_field.getLastText().isEmpty() && !_editMsgId && !_replyToId) {
|
||||
if (_field.isEmpty() && !_editMsgId && !_replyToId) {
|
||||
App::contextItem(_history->lastSentMsg);
|
||||
onEditMessage();
|
||||
}
|
||||
@ -7308,11 +7332,11 @@ void HistoryWidget::sendExistingPhoto(PhotoData *photo, const QString &caption)
|
||||
_field.setFocus();
|
||||
}
|
||||
|
||||
void HistoryWidget::setFieldText(const QString &text, int32 textUpdateEventsFlags, bool clearUndoHistory) {
|
||||
_textUpdateEventsFlags = textUpdateEventsFlags;
|
||||
_field.setTextFast(text, clearUndoHistory);
|
||||
void HistoryWidget::setFieldText(const TextWithTags &textWithTags, TextUpdateEvents events, FlatTextarea::UndoHistoryAction undoHistoryAction) {
|
||||
_textUpdateEvents = events;
|
||||
_field.setTextWithTags(textWithTags, undoHistoryAction);
|
||||
_field.moveCursor(QTextCursor::End);
|
||||
_textUpdateEventsFlags = TextUpdateEventsSaveDraft | TextUpdateEventsSendTyping;
|
||||
_textUpdateEvents = TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping;
|
||||
|
||||
_previewCancelled = false;
|
||||
_previewData = nullptr;
|
||||
@ -7351,7 +7375,7 @@ void HistoryWidget::onReplyToMessage() {
|
||||
if (auto msgDraft = _history->msgDraft()) {
|
||||
msgDraft->msgId = to->id;
|
||||
} else {
|
||||
_history->setMsgDraft(std_::make_unique<HistoryDraft>(QString(), to->id, MessageCursor(), false));
|
||||
_history->setMsgDraft(std_::make_unique<HistoryDraft>(TextWithTags(), to->id, MessageCursor(), false));
|
||||
}
|
||||
} else {
|
||||
_replyEditMsg = to;
|
||||
@ -7384,14 +7408,17 @@ void HistoryWidget::onEditMessage() {
|
||||
} else {
|
||||
delete box;
|
||||
|
||||
if (_replyToId || !_field.getLastText().isEmpty()) {
|
||||
if (_replyToId || !_field.isEmpty()) {
|
||||
_history->setMsgDraft(std_::make_unique<HistoryDraft>(_field, _replyToId, _previewCancelled));
|
||||
} else {
|
||||
_history->clearMsgDraft();
|
||||
}
|
||||
|
||||
QString text(textApplyEntities(to->originalText(), to->originalEntities()));
|
||||
_history->setEditDraft(std_::make_unique<HistoryEditDraft>(text, to->id, MessageCursor(text.size(), text.size(), QFIXED_MAX), false));
|
||||
auto originalText = to->originalText();
|
||||
auto originalEntities = to->originalEntities();
|
||||
TextWithTags original = { textApplyEntities(originalText, originalEntities), textTagsFromEntities(originalEntities) };
|
||||
MessageCursor cursor = { original.text.size(), original.text.size(), QFIXED_MAX };
|
||||
_history->setEditDraft(std_::make_unique<HistoryEditDraft>(original, to->id, cursor, false));
|
||||
applyDraft(false);
|
||||
|
||||
_previewData = 0;
|
||||
@ -7510,7 +7537,7 @@ void HistoryWidget::cancelReply(bool lastKeyboardUsed) {
|
||||
update();
|
||||
} else if (auto msgDraft = (_history ? _history->msgDraft() : nullptr)) {
|
||||
if (msgDraft->msgId) {
|
||||
if (msgDraft->text.isEmpty()) {
|
||||
if (msgDraft->textWithTags.text.isEmpty()) {
|
||||
_history->clearMsgDraft();
|
||||
} else {
|
||||
msgDraft->msgId = 0;
|
||||
@ -7533,7 +7560,7 @@ void HistoryWidget::cancelEdit() {
|
||||
if (!_editMsgId) return;
|
||||
|
||||
_editMsgId = 0;
|
||||
_replyEditMsg = 0;
|
||||
_replyEditMsg = nullptr;
|
||||
_history->clearEditDraft();
|
||||
applyDraft();
|
||||
|
||||
@ -7546,21 +7573,21 @@ void HistoryWidget::cancelEdit() {
|
||||
_saveDraftStart = getms();
|
||||
onDraftSave();
|
||||
|
||||
mouseMoveEvent(0);
|
||||
mouseMoveEvent(nullptr);
|
||||
if (!readyToForward() && (!_previewData || _previewData->pendingTill < 0) && !replyToId()) {
|
||||
_fieldBarCancel.hide();
|
||||
updateMouseTracking();
|
||||
}
|
||||
|
||||
int32 old = _textUpdateEventsFlags;
|
||||
_textUpdateEventsFlags = 0;
|
||||
auto old = _textUpdateEvents;
|
||||
_textUpdateEvents = 0;
|
||||
onTextChange();
|
||||
_textUpdateEventsFlags = old;
|
||||
_textUpdateEvents = old;
|
||||
|
||||
updateBotKeyboard();
|
||||
updateFieldPlaceholder();
|
||||
|
||||
resizeEvent(0);
|
||||
resizeEvent(nullptr);
|
||||
update();
|
||||
}
|
||||
|
||||
@ -7728,7 +7755,10 @@ void HistoryWidget::onCancel() {
|
||||
if (_inlineBotCancel) {
|
||||
onInlineBotCancel();
|
||||
} else if (_editMsgId) {
|
||||
if (_replyEditMsg && textApplyEntities(_replyEditMsg->originalText(), _replyEditMsg->originalEntities()) != _field.getLastText()) {
|
||||
auto originalText = _replyEditMsg ? _replyEditMsg->originalText() : QString();
|
||||
auto originalEntities = _replyEditMsg ? _replyEditMsg->originalEntities() : EntitiesInText();
|
||||
TextWithTags original = { textApplyEntities(originalText, originalEntities), textTagsFromEntities(originalEntities) };
|
||||
if (_replyEditMsg && original != _field.getTextWithTags()) {
|
||||
auto box = new ConfirmBox(lang(lng_cancel_edit_post_sure), lang(lng_cancel_edit_post_yes), st::defaultBoxButton, lang(lng_cancel_edit_post_no));
|
||||
connect(box, SIGNAL(confirmed()), this, SLOT(onFieldBarCancel()));
|
||||
Ui::showLayer(box);
|
||||
|
@ -486,12 +486,8 @@ public:
|
||||
|
||||
};
|
||||
|
||||
EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags);
|
||||
|
||||
enum TextUpdateEventsFlags {
|
||||
TextUpdateEventsSaveDraft = 0x01,
|
||||
TextUpdateEventsSendTyping = 0x02,
|
||||
};
|
||||
EntitiesInText entitiesFromTextTags(const TextWithTags::Tags &tags);
|
||||
TextWithTags::Tags textTagsFromEntities(const EntitiesInText &entities);
|
||||
|
||||
class HistoryWidget : public TWidget, public RPCSender {
|
||||
Q_OBJECT
|
||||
@ -954,11 +950,18 @@ private:
|
||||
void savedGifsGot(const MTPmessages_SavedGifs &gifs);
|
||||
bool savedGifsFailed(const RPCError &error);
|
||||
|
||||
enum class TextUpdateEvent {
|
||||
SaveDraft = 0x01,
|
||||
SendTyping = 0x02,
|
||||
};
|
||||
Q_DECLARE_FLAGS(TextUpdateEvents, TextUpdateEvent);
|
||||
Q_DECLARE_FRIEND_OPERATORS_FOR_FLAGS(TextUpdateEvents);
|
||||
|
||||
void writeDrafts(HistoryDraft **msgDraft, HistoryEditDraft **editDraft);
|
||||
void writeDrafts(History *history);
|
||||
void setFieldText(const QString &text, int32 textUpdateEventsFlags = 0, bool clearUndoHistory = true);
|
||||
void clearFieldText(int32 textUpdateEventsFlags = 0, bool clearUndoHistory = true) {
|
||||
setFieldText(QString());
|
||||
void setFieldText(const TextWithTags &textWithTags, TextUpdateEvents events = 0, FlatTextarea::UndoHistoryAction undoHistoryAction = FlatTextarea::ClearUndoHistory);
|
||||
void clearFieldText(TextUpdateEvents events = 0, FlatTextarea::UndoHistoryAction undoHistoryAction = FlatTextarea::ClearUndoHistory) {
|
||||
setFieldText(TextWithTags(), events, undoHistoryAction);
|
||||
}
|
||||
|
||||
QStringList getMediasFromMime(const QMimeData *d);
|
||||
@ -1062,7 +1065,7 @@ private:
|
||||
int32 _selCount; // < 0 - text selected, focus list, not _field
|
||||
|
||||
TaskQueue _fileLoader;
|
||||
int32 _textUpdateEventsFlags = (TextUpdateEventsSaveDraft | TextUpdateEventsSendTyping);
|
||||
TextUpdateEvents _textUpdateEvents = (TextUpdateEvent::SaveDraft | TextUpdateEvent::SendTyping);
|
||||
|
||||
int64 _serviceImageCacheSize = 0;
|
||||
QString _confirmSource;
|
||||
@ -1095,3 +1098,4 @@ private:
|
||||
|
||||
};
|
||||
|
||||
Q_DECLARE_OPERATORS_FOR_FLAGS(HistoryWidget::TextUpdateEvents)
|
||||
|
@ -122,14 +122,6 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32 _dateTimeSize() {
|
||||
return (sizeof(qint64) + sizeof(quint32) + sizeof(qint8));
|
||||
}
|
||||
|
||||
uint32 _bytearraySize(const QByteArray &arr) {
|
||||
return sizeof(quint32) + arr.size();
|
||||
}
|
||||
|
||||
QByteArray _settingsSalt, _passKeySalt, _passKeyEncrypted;
|
||||
|
||||
MTP::AuthKey _oldKey, _settingsKey, _passKey, _localKey;
|
||||
@ -628,18 +620,18 @@ namespace {
|
||||
size += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(i.value().name());
|
||||
if (AppVersion > 9013) {
|
||||
// bookmark
|
||||
size += _bytearraySize(i.value().bookmark());
|
||||
size += Serialize::bytearraySize(i.value().bookmark());
|
||||
}
|
||||
// date + size
|
||||
size += _dateTimeSize() + sizeof(quint32);
|
||||
size += Serialize::dateTimeSize() + sizeof(quint32);
|
||||
}
|
||||
|
||||
//end mark
|
||||
size += sizeof(quint64) * 2 + sizeof(quint32) + Serialize::stringSize(QString());
|
||||
if (AppVersion > 9013) {
|
||||
size += _bytearraySize(QByteArray());
|
||||
size += Serialize::bytearraySize(QByteArray());
|
||||
}
|
||||
size += _dateTimeSize() + sizeof(quint32);
|
||||
size += Serialize::dateTimeSize() + sizeof(quint32);
|
||||
|
||||
size += sizeof(quint32); // aliases count
|
||||
for (FileLocationAliases::const_iterator i = _fileLocationAliases.cbegin(), e = _fileLocationAliases.cend(); i != e; ++i) {
|
||||
@ -1530,7 +1522,7 @@ namespace {
|
||||
}
|
||||
|
||||
uint32 size = 16 * (sizeof(quint32) + sizeof(qint32));
|
||||
size += sizeof(quint32) + Serialize::stringSize(cAskDownloadPath() ? QString() : cDownloadPath()) + _bytearraySize(cAskDownloadPath() ? QByteArray() : cDownloadPathBookmark());
|
||||
size += sizeof(quint32) + Serialize::stringSize(cAskDownloadPath() ? QString() : cDownloadPath()) + Serialize::bytearraySize(cAskDownloadPath() ? QByteArray() : cDownloadPathBookmark());
|
||||
size += sizeof(quint32) + sizeof(qint32) + (cRecentEmojisPreload().isEmpty() ? cGetRecentEmojis().size() : cRecentEmojisPreload().size()) * (sizeof(uint64) + sizeof(ushort));
|
||||
size += sizeof(quint32) + sizeof(qint32) + cEmojiVariants().size() * (sizeof(uint32) + sizeof(uint64));
|
||||
size += sizeof(quint32) + sizeof(qint32) + (cRecentStickersPreload().isEmpty() ? cGetRecentStickers().size() : cRecentStickersPreload().size()) * (sizeof(uint64) + sizeof(ushort));
|
||||
@ -2281,8 +2273,8 @@ namespace Local {
|
||||
void writeDrafts(const PeerId &peer, const MessageDraft &msgDraft, const MessageDraft &editDraft) {
|
||||
if (!_working()) return;
|
||||
|
||||
if (msgDraft.msgId <= 0 && msgDraft.text.isEmpty() && editDraft.msgId <= 0) {
|
||||
DraftsMap::iterator i = _draftsMap.find(peer);
|
||||
if (msgDraft.msgId <= 0 && msgDraft.textWithTags.text.isEmpty() && editDraft.msgId <= 0) {
|
||||
auto i = _draftsMap.find(peer);
|
||||
if (i != _draftsMap.cend()) {
|
||||
clearKey(i.value());
|
||||
_draftsMap.erase(i);
|
||||
@ -2292,17 +2284,26 @@ namespace Local {
|
||||
|
||||
_draftsNotReadMap.remove(peer);
|
||||
} else {
|
||||
DraftsMap::const_iterator i = _draftsMap.constFind(peer);
|
||||
auto i = _draftsMap.constFind(peer);
|
||||
if (i == _draftsMap.cend()) {
|
||||
i = _draftsMap.insert(peer, genKey());
|
||||
_mapChanged = true;
|
||||
_writeMap(WriteMapFast);
|
||||
}
|
||||
|
||||
EncryptedDescriptor data(sizeof(quint64) + Serialize::stringSize(msgDraft.text) + 2 * sizeof(qint32) + Serialize::stringSize(editDraft.text) + 2 * sizeof(qint32));
|
||||
auto msgTags = FlatTextarea::serializeTagsList(msgDraft.textWithTags.tags);
|
||||
auto editTags = FlatTextarea::serializeTagsList(editDraft.textWithTags.tags);
|
||||
|
||||
int size = sizeof(quint64);
|
||||
size += Serialize::stringSize(msgDraft.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 << msgDraft.text << qint32(msgDraft.msgId) << qint32(msgDraft.previewCancelled ? 1 : 0);
|
||||
data.stream << editDraft.text << qint32(editDraft.msgId) << qint32(editDraft.previewCancelled ? 1 : 0);
|
||||
data.stream << msgDraft.textWithTags.text << msgTags;
|
||||
data.stream << qint32(msgDraft.msgId) << qint32(msgDraft.previewCancelled ? 1 : 0);
|
||||
data.stream << editDraft.textWithTags.text << editTags;
|
||||
data.stream << qint32(editDraft.msgId) << qint32(editDraft.previewCancelled ? 1 : 0);
|
||||
|
||||
FileWriteDescriptor file(i.value());
|
||||
file.writeEncrypted(data);
|
||||
@ -2370,15 +2371,23 @@ namespace Local {
|
||||
}
|
||||
|
||||
quint64 draftPeer = 0;
|
||||
QString msgText, editText;
|
||||
TextWithTags msgData, editData;
|
||||
QByteArray msgTagsSerialized, editTagsSerialized;
|
||||
qint32 msgReplyTo = 0, msgPreviewCancelled = 0, editMsgId = 0, editPreviewCancelled = 0;
|
||||
draft.stream >> draftPeer >> msgText;
|
||||
draft.stream >> draftPeer >> msgData.text;
|
||||
if (draft.version >= 9048) {
|
||||
draft.stream >> msgTagsSerialized;
|
||||
}
|
||||
if (draft.version >= 7021) {
|
||||
draft.stream >> msgReplyTo;
|
||||
if (draft.version >= 8001) {
|
||||
draft.stream >> msgPreviewCancelled;
|
||||
if (!draft.stream.atEnd()) {
|
||||
draft.stream >> editText >> editMsgId >> editPreviewCancelled;
|
||||
draft.stream >> editData.text;
|
||||
if (draft.version >= 9048) {
|
||||
draft.stream >> editTagsSerialized;
|
||||
}
|
||||
draft.stream >> editMsgId >> editPreviewCancelled;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2389,18 +2398,21 @@ namespace Local {
|
||||
return;
|
||||
}
|
||||
|
||||
msgData.tags = FlatTextarea::deserializeTagsList(msgTagsSerialized, msgData.text.size());
|
||||
editData.tags = FlatTextarea::deserializeTagsList(editTagsSerialized, editData.text.size());
|
||||
|
||||
MessageCursor msgCursor, editCursor;
|
||||
_readDraftCursors(peer, msgCursor, editCursor);
|
||||
|
||||
if (msgText.isEmpty() && !msgReplyTo) {
|
||||
if (msgData.text.isEmpty() && !msgReplyTo) {
|
||||
h->clearMsgDraft();
|
||||
} else {
|
||||
h->setMsgDraft(std_::make_unique<HistoryDraft>(msgText, msgReplyTo, msgCursor, msgPreviewCancelled));
|
||||
h->setMsgDraft(std_::make_unique<HistoryDraft>(msgData, msgReplyTo, msgCursor, msgPreviewCancelled));
|
||||
}
|
||||
if (!editMsgId) {
|
||||
h->clearEditDraft();
|
||||
} else {
|
||||
h->setEditDraft(std_::make_unique<HistoryEditDraft>(editText, editMsgId, editCursor, editPreviewCancelled));
|
||||
h->setEditDraft(std_::make_unique<HistoryEditDraft>(editData, editMsgId, editCursor, editPreviewCancelled));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3019,7 +3031,7 @@ namespace Local {
|
||||
} else {
|
||||
int32 setsCount = 0;
|
||||
QByteArray hashToWrite;
|
||||
quint32 size = sizeof(quint32) + _bytearraySize(hashToWrite);
|
||||
quint32 size = sizeof(quint32) + Serialize::bytearraySize(hashToWrite);
|
||||
for (auto i = sets.cbegin(); i != sets.cend(); ++i) {
|
||||
bool notLoaded = (i->flags & MTPDstickerSet_ClientFlag::f_not_loaded);
|
||||
if (notLoaded) {
|
||||
@ -3682,7 +3694,7 @@ namespace Local {
|
||||
}
|
||||
quint32 size = sizeof(quint32);
|
||||
for (SavedPeers::const_iterator i = saved.cbegin(); i != saved.cend(); ++i) {
|
||||
size += _peerSize(i.key()) + _dateTimeSize();
|
||||
size += _peerSize(i.key()) + Serialize::dateTimeSize();
|
||||
}
|
||||
|
||||
EncryptedDescriptor data(size);
|
||||
|
@ -105,11 +105,15 @@ namespace Local {
|
||||
|
||||
int32 oldSettingsVersion();
|
||||
|
||||
using TextWithTags = FlatTextarea::TextWithTags;
|
||||
struct MessageDraft {
|
||||
MessageDraft(MsgId msgId = 0, QString text = QString(), bool previewCancelled = false) : msgId(msgId), text(text), previewCancelled(previewCancelled) {
|
||||
MessageDraft(MsgId msgId = 0, TextWithTags textWithTags = TextWithTags(), bool previewCancelled = false)
|
||||
: msgId(msgId)
|
||||
, textWithTags(textWithTags)
|
||||
, previewCancelled(previewCancelled) {
|
||||
}
|
||||
MsgId msgId;
|
||||
QString text;
|
||||
TextWithTags textWithTags;
|
||||
bool previewCancelled;
|
||||
};
|
||||
void writeDrafts(const PeerId &peer, const MessageDraft &msgDraft, const MessageDraft &editDraft);
|
||||
|
@ -159,7 +159,9 @@ bool MainWidget::onShareUrl(const PeerId &peer, const QString &url, const QStrin
|
||||
return false;
|
||||
}
|
||||
History *h = App::history(peer);
|
||||
h->setMsgDraft(std_::make_unique<HistoryDraft>(url + '\n' + text, 0, MessageCursor(url.size() + 1, url.size() + 1 + text.size(), QFIXED_MAX), false));
|
||||
TextWithTags textWithTags = { url + '\n' + text, TextWithTags::Tags() };
|
||||
MessageCursor cursor = { url.size() + 1, url.size() + 1 + text.size(), QFIXED_MAX };
|
||||
h->setMsgDraft(std_::make_unique<HistoryDraft>(textWithTags, 0, cursor, false));
|
||||
h->clearEditDraft();
|
||||
bool opened = _history->peer() && (_history->peer()->id == peer);
|
||||
if (opened) {
|
||||
@ -177,7 +179,9 @@ bool MainWidget::onInlineSwitchChosen(const PeerId &peer, const QString &botAndQ
|
||||
return false;
|
||||
}
|
||||
History *h = App::history(peer);
|
||||
h->setMsgDraft(std_::make_unique<HistoryDraft>(botAndQuery, 0, MessageCursor(botAndQuery.size(), botAndQuery.size(), QFIXED_MAX), false));
|
||||
TextWithTags textWithTags = { botAndQuery, TextWithTags::Tags() };
|
||||
MessageCursor cursor = { botAndQuery.size(), botAndQuery.size(), QFIXED_MAX };
|
||||
h->setMsgDraft(std_::make_unique<HistoryDraft>(textWithTags, 0, cursor, false));
|
||||
h->clearEditDraft();
|
||||
bool opened = _history->peer() && (_history->peer()->id == peer);
|
||||
if (opened) {
|
||||
@ -1086,7 +1090,7 @@ void executeParsedCommand(const QString &command) {
|
||||
|
||||
void MainWidget::sendMessage(const MessageToSend &message) {
|
||||
auto history = message.history;
|
||||
const auto &text = message.text;
|
||||
const auto &textWithTags = message.textWithTags;
|
||||
|
||||
readServerHistory(history, false);
|
||||
_history->fastShowAtEnd(history);
|
||||
@ -1095,13 +1099,13 @@ void MainWidget::sendMessage(const MessageToSend &message) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveRecentHashtags(text);
|
||||
saveRecentHashtags(textWithTags.text);
|
||||
|
||||
EntitiesInText sendingEntities, leftEntities = entitiesFromFieldTags(message.entities);
|
||||
EntitiesInText sendingEntities, leftEntities = entitiesFromTextTags(textWithTags.tags);
|
||||
auto prepareFlags = itemTextOptions(history, App::self()).flags;
|
||||
QString sendingText, leftText = prepareTextWithEntities(text, prepareFlags, &leftEntities);
|
||||
QString sendingText, leftText = prepareTextWithEntities(textWithTags.text, prepareFlags, &leftEntities);
|
||||
|
||||
QString command = parseCommandFromMessage(history, text);
|
||||
QString command = parseCommandFromMessage(history, textWithTags.text);
|
||||
HistoryItem *lastMessage = nullptr;
|
||||
|
||||
MsgId replyTo = (message.replyTo < 0) ? _history->replyToId() : 0;
|
||||
|
@ -283,8 +283,7 @@ public:
|
||||
|
||||
struct MessageToSend {
|
||||
History *history = nullptr;
|
||||
QString text;
|
||||
FlatTextarea::TagList entities;
|
||||
TextWithTags textWithTags;
|
||||
MsgId replyTo = 0;
|
||||
bool broadcast = false;
|
||||
bool silent = false;
|
||||
|
@ -86,7 +86,7 @@ void MacPrivate::notifyReplied(unsigned long long peer, int msgid, const char *s
|
||||
|
||||
MainWidget::MessageToSend message;
|
||||
message.history = history;
|
||||
message.text = QString::fromUtf8(str);
|
||||
message.textWithTags = { QString::fromUtf8(str), TextWithTags::Tags() };
|
||||
message.replyTo = (msgid > 0 && !history->peer->isUser()) ? msgid : 0;
|
||||
message.broadcast = false;
|
||||
message.silent = false;
|
||||
|
@ -23,10 +23,6 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
|
||||
|
||||
namespace Serialize {
|
||||
|
||||
int stringSize(const QString &str) {
|
||||
return sizeof(quint32) + str.size() * sizeof(ushort);
|
||||
}
|
||||
|
||||
void writeStorageImageLocation(QDataStream &stream, const StorageImageLocation &loc) {
|
||||
stream << qint32(loc.width()) << qint32(loc.height());
|
||||
stream << qint32(loc.dc()) << quint64(loc.volume()) << qint32(loc.local()) << quint64(loc.secret());
|
||||
|
@ -24,7 +24,17 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
|
||||
|
||||
namespace Serialize {
|
||||
|
||||
int stringSize(const QString &str);
|
||||
inline int stringSize(const QString &str) {
|
||||
return sizeof(quint32) + str.size() * sizeof(ushort);
|
||||
}
|
||||
|
||||
inline int bytearraySize(const QByteArray &arr) {
|
||||
return sizeof(quint32) + arr.size();
|
||||
}
|
||||
|
||||
inline int dateTimeSize() {
|
||||
return (sizeof(qint64) + sizeof(quint32) + sizeof(qint8));
|
||||
}
|
||||
|
||||
void writeStorageImageLocation(QDataStream &stream, const StorageImageLocation &loc);
|
||||
StorageImageLocation readStorageImageLocation(QDataStream &stream);
|
||||
|
@ -23,9 +23,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org
|
||||
|
||||
#include "mainwindow.h"
|
||||
|
||||
namespace {
|
||||
|
||||
QByteArray serializeTagsList(const FlatTextarea::TagList &tags) {
|
||||
QByteArray FlatTextarea::serializeTagsList(const TagList &tags) {
|
||||
if (tags.isEmpty()) {
|
||||
return QByteArray();
|
||||
}
|
||||
@ -44,8 +42,11 @@ QByteArray serializeTagsList(const FlatTextarea::TagList &tags) {
|
||||
return tagsSerialized;
|
||||
}
|
||||
|
||||
FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) {
|
||||
FlatTextarea::TagList result;
|
||||
FlatTextarea::TagList FlatTextarea::deserializeTagsList(QByteArray data, int textLength) {
|
||||
TagList result;
|
||||
if (data.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
QBuffer buffer(&data);
|
||||
buffer.open(QIODevice::ReadOnly);
|
||||
@ -58,7 +59,7 @@ FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) {
|
||||
if (stream.status() != QDataStream::Ok) {
|
||||
return result;
|
||||
}
|
||||
if (tagCount <= 0 || tagCount > textSize) {
|
||||
if (tagCount <= 0 || tagCount > textLength) {
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -69,7 +70,7 @@ FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) {
|
||||
if (stream.status() != QDataStream::Ok) {
|
||||
return result;
|
||||
}
|
||||
if (offset < 0 || length <= 0 || offset + length > textSize) {
|
||||
if (offset < 0 || length <= 0 || offset + length > textLength) {
|
||||
return result;
|
||||
}
|
||||
result.push_back({ offset, length, id });
|
||||
@ -77,12 +78,12 @@ FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) {
|
||||
return result;
|
||||
}
|
||||
|
||||
constexpr str_const TagsMimeType = "application/x-td-field-tags";
|
||||
QString FlatTextarea::tagsMimeType() {
|
||||
return qsl("application/x-td-field-tags");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &pholder, const QString &v) : QTextEdit(parent)
|
||||
, _oldtext(v)
|
||||
FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &pholder, const QString &v, const TagList &tags) : QTextEdit(parent)
|
||||
, _lastTextWithTags { v, tags }
|
||||
, _phVisible(!v.length())
|
||||
, a_phLeft(_phVisible ? 0 : st.phShift)
|
||||
, a_phAlpha(_phVisible ? 1 : 0)
|
||||
@ -126,22 +127,41 @@ FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const
|
||||
connect(this, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool)));
|
||||
if (App::wnd()) connect(this, SIGNAL(selectionChanged()), App::wnd(), SLOT(updateGlobalMenu()));
|
||||
|
||||
if (!v.isEmpty()) {
|
||||
setTextFast(v);
|
||||
if (!_lastTextWithTags.text.isEmpty()) {
|
||||
setTextWithTags(_lastTextWithTags, ClearUndoHistory);
|
||||
}
|
||||
}
|
||||
|
||||
void FlatTextarea::setTextFast(const QString &text, bool clearUndoHistory) {
|
||||
if (clearUndoHistory) {
|
||||
setPlainText(text);
|
||||
FlatTextarea::TextWithTags FlatTextarea::getTextWithTagsPart(int start, int end) {
|
||||
TextWithTags result;
|
||||
result.text = getTextPart(start, end, &result.tags);
|
||||
return result;
|
||||
}
|
||||
|
||||
void FlatTextarea::setTextWithTags(const TextWithTags &textWithTags, UndoHistoryAction undoHistoryAction) {
|
||||
_insertedTags = textWithTags.tags;
|
||||
_insertedTagsAreFromMime = false;
|
||||
_realInsertPosition = 0;
|
||||
_realCharsAdded = textWithTags.text.size();
|
||||
auto doc = document();
|
||||
auto cursor = QTextCursor(doc->docHandle(), 0);
|
||||
if (undoHistoryAction == ClearUndoHistory) {
|
||||
doc->setUndoRedoEnabled(false);
|
||||
cursor.beginEditBlock();
|
||||
} else if (undoHistoryAction == MergeWithUndoHistory) {
|
||||
cursor.joinPreviousEditBlock();
|
||||
} else {
|
||||
QTextCursor c(document()->docHandle(), 0);
|
||||
c.joinPreviousEditBlock();
|
||||
c.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
||||
c.insertText(text);
|
||||
c.movePosition(QTextCursor::End);
|
||||
c.endEditBlock();
|
||||
cursor.beginEditBlock();
|
||||
}
|
||||
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
||||
cursor.insertText(textWithTags.text);
|
||||
cursor.movePosition(QTextCursor::End);
|
||||
cursor.endEditBlock();
|
||||
if (undoHistoryAction == ClearUndoHistory) {
|
||||
doc->setUndoRedoEnabled(true);
|
||||
}
|
||||
_insertedTags.clear();
|
||||
_realInsertPosition = -1;
|
||||
finishPlaceholder();
|
||||
}
|
||||
|
||||
@ -266,7 +286,8 @@ void FlatTextarea::paintEvent(QPaintEvent *e) {
|
||||
p.setFont(_st.font);
|
||||
p.setPen(a_phColor.current());
|
||||
if (_st.phAlign == style::al_topleft && _phAfter > 0) {
|
||||
p.drawText(_st.textMrg.left() - _fakeMargin + a_phLeft.current() + _st.font->width(getLastText().mid(0, _phAfter)), _st.textMrg.top() - _fakeMargin - st::lineWidth + _st.font->ascent, _ph);
|
||||
int skipWidth = _st.font->width(getTextWithTags().text.mid(0, _phAfter));
|
||||
p.drawText(_st.textMrg.left() - _fakeMargin + a_phLeft.current() + skipWidth, _st.textMrg.top() - _fakeMargin - st::lineWidth + _st.font->ascent, _ph);
|
||||
} else {
|
||||
QRect phRect(_st.textMrg.left() - _fakeMargin + _st.phPos.x() + a_phLeft.current(), _st.textMrg.top() - _fakeMargin + _st.phPos.y(), width() - _st.textMrg.left() - _st.textMrg.right(), height() - _st.textMrg.top() - _st.textMrg.bottom());
|
||||
p.drawText(phRect, _ph, QTextOption(_st.phAlign));
|
||||
@ -317,7 +338,7 @@ QString FlatTextarea::getInlineBotQuery(UserData **outInlineBot, QString *outInl
|
||||
t_assert(outInlineBot != nullptr);
|
||||
t_assert(outInlineBotUsername != nullptr);
|
||||
|
||||
const QString &text(getLastText());
|
||||
auto &text = getTextWithTags().text;
|
||||
|
||||
int32 inlineUsernameStart = 1, inlineUsernameLength = 0, size = text.size();
|
||||
if (size > 2 && text.at(0) == '@' && text.at(1).isLetter()) {
|
||||
@ -474,10 +495,8 @@ void FlatTextarea::insertTag(const QString &text, QString tagId) {
|
||||
cursor.insertText(text + ' ', format);
|
||||
} else {
|
||||
_insertedTags.clear();
|
||||
if (_tagMimeProcessor) {
|
||||
tagId = _tagMimeProcessor->mimeTagFromTag(tagId);
|
||||
}
|
||||
_insertedTags.push_back({ 0, text.size(), tagId });
|
||||
_insertedTagsAreFromMime = false;
|
||||
cursor.insertText(text + ' ');
|
||||
_insertedTags.clear();
|
||||
}
|
||||
@ -601,7 +620,7 @@ private:
|
||||
|
||||
} // namespace
|
||||
|
||||
QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *outTagsChanged) const {
|
||||
QString FlatTextarea::getTextPart(int start, int end, TagList *outTagsList, bool *outTagsChanged) const {
|
||||
if (end >= 0 && end <= start) return QString();
|
||||
|
||||
if (start < 0) start = 0;
|
||||
@ -632,7 +651,7 @@ QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *ou
|
||||
int32 p = full ? 0 : fragment.position(), e = full ? 0 : (p + fragment.length());
|
||||
if (!full) {
|
||||
tillFragmentEnd = (e <= end);
|
||||
if (p == end && outTagsList) {
|
||||
if (p == end) {
|
||||
tagAccumulator.feed(fragment.charFormat().anchorName(), result.size());
|
||||
}
|
||||
if (p >= end) {
|
||||
@ -642,7 +661,7 @@ QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *ou
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (outTagsList && (full || p >= start)) {
|
||||
if (full || p >= start) {
|
||||
tagAccumulator.feed(fragment.charFormat().anchorName(), result.size());
|
||||
}
|
||||
|
||||
@ -689,11 +708,9 @@ QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *ou
|
||||
}
|
||||
result.chop(1);
|
||||
|
||||
if (outTagsList) {
|
||||
if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size());
|
||||
if (outTagsChanged) {
|
||||
*outTagsChanged = tagAccumulator.changed();
|
||||
}
|
||||
if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size());
|
||||
if (outTagsChanged) {
|
||||
*outTagsChanged = tagAccumulator.changed();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@ -816,11 +833,12 @@ QStringList FlatTextarea::linksList() const {
|
||||
}
|
||||
|
||||
void FlatTextarea::insertFromMimeData(const QMimeData *source) {
|
||||
auto mime = str_const_toString(TagsMimeType);
|
||||
auto mime = tagsMimeType();
|
||||
auto text = source->text();
|
||||
if (source->hasFormat(mime)) {
|
||||
auto tagsData = source->data(mime);
|
||||
_insertedTags = deserializeTagsList(tagsData, text.size());
|
||||
_insertedTagsAreFromMime = true;
|
||||
} else {
|
||||
_insertedTags.clear();
|
||||
}
|
||||
@ -982,7 +1000,9 @@ void FlatTextarea::processFormatting(int insertPosition, int insertEnd) {
|
||||
auto doc = document();
|
||||
|
||||
// Apply inserted tags.
|
||||
int breakTagOnNotLetterTill = processInsertedTags(doc, insertPosition, insertEnd, _insertedTags, _tagMimeProcessor.get());
|
||||
auto insertedTagsProcessor = _insertedTagsAreFromMime ? _tagMimeProcessor.get() : nullptr;
|
||||
int breakTagOnNotLetterTill = processInsertedTags(doc, insertPosition, insertEnd,
|
||||
_insertedTags, insertedTagsProcessor);
|
||||
using ActionType = FormattingAction::Type;
|
||||
while (true) {
|
||||
FormattingAction action;
|
||||
@ -1181,15 +1201,15 @@ void FlatTextarea::onDocumentContentsChanged() {
|
||||
if (_correcting) return;
|
||||
|
||||
auto tagsChanged = false;
|
||||
auto curText = getText(0, -1, &_oldtags, &tagsChanged);
|
||||
auto curText = getTextPart(0, -1, &_lastTextWithTags.tags, &tagsChanged);
|
||||
|
||||
_correcting = true;
|
||||
correctValue(_oldtext, curText, _oldtags);
|
||||
correctValue(_lastTextWithTags.text, curText, _lastTextWithTags.tags);
|
||||
_correcting = false;
|
||||
|
||||
bool textOrTagsChanged = tagsChanged || (_oldtext != curText);
|
||||
bool textOrTagsChanged = tagsChanged || (_lastTextWithTags.text != curText);
|
||||
if (textOrTagsChanged) {
|
||||
_oldtext = curText;
|
||||
_lastTextWithTags.text = curText;
|
||||
emit changed();
|
||||
checkContentHeight();
|
||||
}
|
||||
@ -1231,12 +1251,16 @@ void FlatTextarea::setPlaceholder(const QString &ph, int32 afterSymbols) {
|
||||
_phAfter = afterSymbols;
|
||||
updatePlaceholder();
|
||||
}
|
||||
_phelided = _st.font->elided(_ph, width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1 - (_phAfter ? _st.font->width(getLastText().mid(0, _phAfter)) : 0));
|
||||
int skipWidth = 0;
|
||||
if (_phAfter) {
|
||||
skipWidth = _st.font->width(getTextWithTags().text.mid(0, _phAfter));
|
||||
}
|
||||
_phelided = _st.font->elided(_ph, width() - _st.textMrg.left() - _st.textMrg.right() - _st.phPos.x() - 1 - skipWidth);
|
||||
if (_phVisible) update();
|
||||
}
|
||||
|
||||
void FlatTextarea::updatePlaceholder() {
|
||||
bool vis = (getLastText().size() <= _phAfter);
|
||||
bool vis = (getTextWithTags().text.size() <= _phAfter);
|
||||
if (vis == _phVisible) return;
|
||||
|
||||
a_phLeft.start(vis ? 0 : _st.phShift);
|
||||
@ -1252,14 +1276,14 @@ QMimeData *FlatTextarea::createMimeDataFromSelection() const {
|
||||
int32 start = c.selectionStart(), end = c.selectionEnd();
|
||||
if (end > start) {
|
||||
TagList tags;
|
||||
result->setText(getText(start, end, &tags, nullptr));
|
||||
result->setText(getTextPart(start, end, &tags));
|
||||
if (!tags.isEmpty()) {
|
||||
if (_tagMimeProcessor) {
|
||||
for (auto &tag : tags) {
|
||||
tag.id = _tagMimeProcessor->mimeTagFromTag(tag.id);
|
||||
}
|
||||
}
|
||||
result->setData(str_const_toString(TagsMimeType), serializeTagsList(tags));
|
||||
result->setData(tagsMimeType(), serializeTagsList(tags));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
@ -31,16 +31,27 @@ class FlatTextarea : public QTextEdit {
|
||||
|
||||
public:
|
||||
|
||||
FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &ph = QString(), const QString &val = QString());
|
||||
struct Tag {
|
||||
int offset, length;
|
||||
QString id;
|
||||
};
|
||||
using TagList = QVector<Tag>;
|
||||
struct TextWithTags {
|
||||
using Tags = FlatTextarea::TagList;
|
||||
QString text;
|
||||
Tags tags;
|
||||
};
|
||||
|
||||
static QByteArray serializeTagsList(const TagList &tags);
|
||||
static TagList deserializeTagsList(QByteArray data, int textLength);
|
||||
static QString tagsMimeType();
|
||||
|
||||
FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &ph = QString(), const QString &val = QString(), const TagList &tags = TagList());
|
||||
|
||||
void setMaxLength(int32 maxLength);
|
||||
void setMinHeight(int32 minHeight);
|
||||
void setMaxHeight(int32 maxHeight);
|
||||
|
||||
const QString &getLastText() const {
|
||||
return _oldtext;
|
||||
}
|
||||
|
||||
void setPlaceholder(const QString &ph, int32 afterSymbols = 0);
|
||||
void updatePlaceholder();
|
||||
void finishPlaceholder();
|
||||
@ -82,18 +93,23 @@ public:
|
||||
};
|
||||
void setSubmitSettings(SubmitSettings settings);
|
||||
|
||||
void setTextFast(const QString &text, bool clearUndoHistory = true);
|
||||
|
||||
struct Tag {
|
||||
int offset, length;
|
||||
QString id;
|
||||
};
|
||||
using TagList = QVector<Tag>;
|
||||
const TagList &getLastTags() const {
|
||||
return _oldtags;
|
||||
const TextWithTags &getTextWithTags() const {
|
||||
return _lastTextWithTags;
|
||||
}
|
||||
TextWithTags getTextWithTagsPart(int start, int end = -1);
|
||||
void insertTag(const QString &text, QString tagId = QString());
|
||||
|
||||
bool isEmpty() const {
|
||||
return _lastTextWithTags.text.isEmpty();
|
||||
}
|
||||
|
||||
enum UndoHistoryAction {
|
||||
AddToUndoHistory,
|
||||
MergeWithUndoHistory,
|
||||
ClearUndoHistory
|
||||
};
|
||||
void setTextWithTags(const TextWithTags &textWithTags, UndoHistoryAction undoHistoryAction = AddToUndoHistory);
|
||||
|
||||
// If you need to make some preparations of tags before putting them to QMimeData
|
||||
// (and then to clipboard or to drag-n-drop object), here is a strategy for that.
|
||||
class TagMimeProcessor {
|
||||
@ -147,9 +163,9 @@ protected:
|
||||
|
||||
private:
|
||||
|
||||
// "start" and "end" are in coordinates of text where emoji are replaced by ObjectReplacementCharacter.
|
||||
// If "end" = -1 means get text till the end. "outTagsList" and "outTagsChanged" may be nullptr.
|
||||
QString getText(int start, int end, TagList *outTagsList, bool *outTagsChanged) const;
|
||||
// "start" and "end" are in coordinates of text where emoji are replaced
|
||||
// by ObjectReplacementCharacter. If "end" = -1 means get text till the end.
|
||||
QString getTextPart(int start, int end, TagList *outTagsList, bool *outTagsChanged = nullptr) const;
|
||||
|
||||
void getSingleEmojiFragment(QString &text, QTextFragment &fragment) const;
|
||||
|
||||
@ -169,8 +185,7 @@ private:
|
||||
int _maxLength = -1;
|
||||
SubmitSettings _submitSettings = SubmitSettings::Enter;
|
||||
|
||||
QString _ph, _phelided, _oldtext;
|
||||
TagList _oldtags;
|
||||
QString _ph, _phelided;
|
||||
int _phAfter = 0;
|
||||
bool _phVisible;
|
||||
anim::ivalue a_phLeft;
|
||||
@ -178,8 +193,11 @@ private:
|
||||
anim::cvalue a_phColor;
|
||||
Animation _a_appearance;
|
||||
|
||||
TextWithTags _lastTextWithTags;
|
||||
|
||||
// Tags list which we should apply while setText() call or insert from mime data.
|
||||
TagList _insertedTags;
|
||||
bool _insertedTagsAreFromMime;
|
||||
|
||||
// Override insert position and charsAdded from complex text editing
|
||||
// (like drag-n-drop in the same text edit field).
|
||||
@ -222,6 +240,13 @@ inline bool operator!=(const FlatTextarea::Tag &a, const FlatTextarea::Tag &b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
inline bool operator==(const FlatTextarea::TextWithTags &a, const FlatTextarea::TextWithTags &b) {
|
||||
return (a.text == b.text) && (a.tags == b.tags);
|
||||
}
|
||||
inline bool operator!=(const FlatTextarea::TextWithTags &a, const FlatTextarea::TextWithTags &b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
inline bool operator==(const FlatTextarea::LinkRange &a, const FlatTextarea::LinkRange &b) {
|
||||
return (a.start == b.start) && (a.length == b.length);
|
||||
}
|
||||
|
@ -2023,7 +2023,7 @@
|
||||
SDKROOT = macosx;
|
||||
SYMROOT = ./../Mac;
|
||||
TDESKTOP_MAJOR_VERSION = 0.9;
|
||||
TDESKTOP_VERSION = 0.9.47;
|
||||
TDESKTOP_VERSION = 0.9.48;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@ -2164,7 +2164,7 @@
|
||||
SDKROOT = macosx;
|
||||
SYMROOT = ./../Mac;
|
||||
TDESKTOP_MAJOR_VERSION = 0.9;
|
||||
TDESKTOP_VERSION = 0.9.47;
|
||||
TDESKTOP_VERSION = 0.9.48;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
AppVersion 9047
|
||||
AppVersion 9048
|
||||
AppVersionStrMajor 0.9
|
||||
AppVersionStrSmall 0.9.47
|
||||
AppVersionStr 0.9.47
|
||||
AppVersionStrSmall 0.9.48
|
||||
AppVersionStr 0.9.48
|
||||
AlphaChannel 1
|
||||
BetaVersion 0
|
||||
|
Loading…
Reference in New Issue
Block a user