From af248a6714a112985f5c421401ccd1970815777f Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 29 Apr 2016 15:00:48 +0300 Subject: [PATCH 01/20] EntityInText made class instead of struct. Scheme updated. No left trim of monospace text block when sending / displaying text. New entity type (mention name) introduced, but not supported yet. --- Telegram/SourceFiles/core/click_handler.cpp | 8 + Telegram/SourceFiles/core/click_handler.h | 64 +- .../SourceFiles/core/click_handler_types.cpp | 51 +- .../SourceFiles/core/click_handler_types.h | 57 +- Telegram/SourceFiles/dropdown.cpp | 20 +- Telegram/SourceFiles/history.cpp | 33 +- Telegram/SourceFiles/historywidget.cpp | 8 +- Telegram/SourceFiles/mtproto/scheme.tl | 24 +- Telegram/SourceFiles/mtproto/scheme_auto.cpp | 183 ++++- Telegram/SourceFiles/mtproto/scheme_auto.h | 733 +++++++++++++++++- .../SourceFiles/overview/overview_layout.cpp | 31 +- Telegram/SourceFiles/overviewwidget.cpp | 6 +- Telegram/SourceFiles/structs.cpp | 15 + Telegram/SourceFiles/structs.h | 21 +- Telegram/SourceFiles/ui/emoji_config.h | 21 +- Telegram/SourceFiles/ui/flatinput.h | 8 +- Telegram/SourceFiles/ui/text/text.cpp | 139 ++-- Telegram/SourceFiles/ui/text/text.h | 5 - Telegram/SourceFiles/ui/text/text_entity.cpp | 111 ++- Telegram/SourceFiles/ui/text/text_entity.h | 78 +- 20 files changed, 1325 insertions(+), 291 deletions(-) diff --git a/Telegram/SourceFiles/core/click_handler.cpp b/Telegram/SourceFiles/core/click_handler.cpp index 09809de14..99b6b5a42 100644 --- a/Telegram/SourceFiles/core/click_handler.cpp +++ b/Telegram/SourceFiles/core/click_handler.cpp @@ -61,3 +61,11 @@ bool ClickHandler::setActive(const ClickHandlerPtr &p, ClickHandlerHost *host) { } return true; } + +QString ClickHandler::getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const { + return QString(); +} + +EntityInText ClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { + return EntityInText(EntityInTextInvalid, offset, 0); +} diff --git a/Telegram/SourceFiles/core/click_handler.h b/Telegram/SourceFiles/core/click_handler.h index 8c997e6b9..b40ff9b5d 100644 --- a/Telegram/SourceFiles/core/click_handler.h +++ b/Telegram/SourceFiles/core/click_handler.h @@ -23,6 +23,12 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org class ClickHandler; using ClickHandlerPtr = QSharedPointer; +enum ExpandLinksMode { + ExpandLinksNone, + ExpandLinksShortened, + ExpandLinksAll, +}; + class ClickHandlerHost { protected: @@ -35,35 +41,45 @@ protected: }; +class EntityInText; class ClickHandler { public: - virtual void onClick(Qt::MouseButton) const = 0; - - virtual QString tooltip() const { - return QString(); - } - virtual void copyToClipboard() const { - } - virtual QString copyToClipboardContextItem() const { - return QString(); - } - virtual QString text() const { - return QString(); - } - virtual QString dragText() const { - return text(); - } - virtual ~ClickHandler() { } - // this method should be called on mouse over a click handler - // it returns true if something was changed or false otherwise + virtual void onClick(Qt::MouseButton) const = 0; + + // What text to show in a tooltip when mouse is over that click handler as a link in Text. + virtual QString tooltip() const { + return QString(); + } + + // What to drop in the input fields when dragging that click handler as a link from Text. + virtual QString dragText() const { + return QString(); + } + + // Copy to clipboard support. + virtual void copyToClipboard() const { + } + virtual QString copyToClipboardContextItemText() const { + return QString(); + } + + // Entities in text support. + + // This method returns empty string if just textPart should be used (nothing to expand). + virtual QString getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const; + + virtual EntityInText getEntityInText(int offset, const QStringRef &textPart) const; + + // This method should be called on mouse over a click handler. + // It returns true if the active handler was changed or false otherwise. static bool setActive(const ClickHandlerPtr &p, ClickHandlerHost *host = nullptr); - // this method should be called when mouse leaves the host - // it returns true if something was changed or false otherwise + // This method should be called when mouse leaves the host. + // It returns true if the active handler was changed or false otherwise. static bool clearActive(ClickHandlerHost *host = nullptr) { if (host && _activeHost != host) { return false; @@ -71,7 +87,7 @@ public: return setActive(ClickHandlerPtr(), host); } - // this method should be called on mouse pressed + // This method should be called on mouse press event. static void pressed() { unpressed(); if (!_active || !*_active) { @@ -84,8 +100,8 @@ public: } } - // this method should be called on mouse released - // the activated click handler is returned + // This method should be called on mouse release event. + // The activated click handler (if any) is returned. static ClickHandlerPtr unpressed() { if (_pressed && *_pressed) { bool activated = (_active && *_active == *_pressed); diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 5bbf2df64..ad67985e7 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -25,7 +25,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "pspecific.h" #include "boxes/confirmbox.h" -QString UrlClickHandler::copyToClipboardContextItem() const { +QString UrlClickHandler::copyToClipboardContextItemText() const { return lang(isEmail() ? lng_context_copy_email : lng_context_copy_link); } @@ -74,8 +74,23 @@ void UrlClickHandler::doOpen(QString url) { } } +QString UrlClickHandler::getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const { + if (mode == ExpandLinksNone) { + return QString(); + } + return _originalUrl; +} + +EntityInText UrlClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { + auto u = _originalUrl; + if (isEmail(u)) { + return EntityInText(EntityInTextUrl, offset, u.size()); + } + return EntityInText(EntityInTextUrl, offset, u.size()); +} + void HiddenUrlClickHandler::onClick(Qt::MouseButton button) const { - QString u = url(); + auto u = url(); u = tryConvertUrlToLocal(u); @@ -86,22 +101,18 @@ void HiddenUrlClickHandler::onClick(Qt::MouseButton button) const { } } -QString LocationClickHandler::copyToClipboardContextItem() const { - return lang(lng_context_copy_link); -} - -void LocationClickHandler::onClick(Qt::MouseButton button) const { - if (!psLaunchMaps(_coords)) { - QDesktopServices::openUrl(_text); +QString HiddenUrlClickHandler::getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const { + if (mode != ExpandLinksAll) { + return QString(); } + return textPart.toString() + qsl(" (") + url() + ')'; } -void LocationClickHandler::setup() { - QString latlon(qsl("%1,%2").arg(_coords.lat).arg(_coords.lon)); - _text = qsl("https://maps.google.com/maps?q=") + latlon + qsl("&ll=") + latlon + qsl("&z=16"); +EntityInText HiddenUrlClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { + return EntityInText(EntityInTextCustomUrl, offset, textPart.size(), url()); } -QString MentionClickHandler::copyToClipboardContextItem() const { +QString MentionClickHandler::copyToClipboardContextItemText() const { return lang(lng_context_copy_mention); } @@ -111,7 +122,11 @@ void MentionClickHandler::onClick(Qt::MouseButton button) const { } } -QString HashtagClickHandler::copyToClipboardContextItem() const { +EntityInText MentionClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { + return EntityInText(EntityInTextMention, offset, textPart.size()); +} + +QString HashtagClickHandler::copyToClipboardContextItemText() const { return lang(lng_context_copy_hashtag); } @@ -121,6 +136,10 @@ void HashtagClickHandler::onClick(Qt::MouseButton button) const { } } +EntityInText HashtagClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { + return EntityInText(EntityInTextHashtag, offset, textPart.size()); +} + void BotCommandClickHandler::onClick(Qt::MouseButton button) const { if (button == Qt::LeftButton || button == Qt::MiddleButton) { if (PeerData *peer = Ui::getPeerForMouseAction()) { @@ -137,3 +156,7 @@ void BotCommandClickHandler::onClick(Qt::MouseButton button) const { } } } + +EntityInText BotCommandClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { + return EntityInText(EntityInTextHashtag, offset, textPart.size()); +} diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 9ca66ad85..fcb42d67d 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -55,23 +55,23 @@ protected: class UrlClickHandler : public TextClickHandler { public: - UrlClickHandler(const QString &url, bool fullDisplayed = true) : TextClickHandler(fullDisplayed), _url(url) { + UrlClickHandler(const QString &url, bool fullDisplayed = true) : TextClickHandler(fullDisplayed), _originalUrl(url) { if (isEmail()) { - _readable = _url; + _readable = _originalUrl; } else { - QUrl u(_url), good(u.isValid() ? u.toEncoded() : QString()); - _readable = good.isValid() ? good.toDisplayString() : _url; + QUrl u(_originalUrl), good(u.isValid() ? u.toEncoded() : QString()); + _readable = good.isValid() ? good.toDisplayString() : _originalUrl; } } - QString copyToClipboardContextItem() const override; + QString copyToClipboardContextItemText() const override; - QString text() const override { - return _url; - } QString dragText() const override { return url(); } + QString getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const override; + EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + static void doOpen(QString url); void onClick(Qt::MouseButton button) const override { if (button == Qt::LeftButton || button == Qt::MiddleButton) { @@ -82,11 +82,11 @@ public: protected: QString url() const override { if (isEmail()) { - return _url; + return _originalUrl; } - QUrl u(_url), good(u.isValid() ? u.toEncoded() : QString()); - QString result(good.isValid() ? QString::fromUtf8(good.toEncoded()) : _url); + QUrl u(_originalUrl), good(u.isValid() ? u.toEncoded() : QString()); + QString result(good.isValid() ? QString::fromUtf8(good.toEncoded()) : _originalUrl); if (!QRegularExpression(qsl("^[a-zA-Z]+:")).match(result).hasMatch()) { // no protocol return qsl("http://") + result; @@ -103,10 +103,10 @@ private: return ((at > 0) && (slash < 0 || slash > at)); } bool isEmail() const { - return isEmail(_url); + return isEmail(_originalUrl); } - QString _url, _readable; + QString _originalUrl, _readable; }; typedef QSharedPointer TextClickHandlerPtr; @@ -117,18 +117,25 @@ public: } void onClick(Qt::MouseButton button) const override; + QString getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const override; + EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + }; class MentionClickHandler : public TextClickHandler { public: MentionClickHandler(const QString &tag) : _tag(tag) { } - QString copyToClipboardContextItem() const override; - QString text() const override { + void onClick(Qt::MouseButton button) const override; + + QString dragText() const override { return _tag; } - void onClick(Qt::MouseButton button) const override; + + QString copyToClipboardContextItemText() const override; + + EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; protected: QString url() const override { @@ -144,12 +151,16 @@ class HashtagClickHandler : public TextClickHandler { public: HashtagClickHandler(const QString &tag) : _tag(tag) { } - QString copyToClipboardContextItem() const override; - QString text() const override { + void onClick(Qt::MouseButton button) const override; + + QString dragText() const override { return _tag; } - void onClick(Qt::MouseButton button) const override; + + QString copyToClipboardContextItemText() const override; + + EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; protected: QString url() const override { @@ -165,10 +176,14 @@ class BotCommandClickHandler : public TextClickHandler { public: BotCommandClickHandler(const QString &cmd) : _cmd(cmd) { } - QString text() const override { + + void onClick(Qt::MouseButton button) const override; + + QString dragText() const override { return _cmd; } - void onClick(Qt::MouseButton button) const override; + + EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; protected: QString url() const override { diff --git a/Telegram/SourceFiles/dropdown.cpp b/Telegram/SourceFiles/dropdown.cpp index 8ffc92aef..eb5c4fc5f 100644 --- a/Telegram/SourceFiles/dropdown.cpp +++ b/Telegram/SourceFiles/dropdown.cpp @@ -3944,6 +3944,8 @@ void MentionsInner::paintEvent(QPaintEvent *e) { int32 from = qFloor(e->rect().top() / st::mentionHeight), to = qFloor(e->rect().bottom() / st::mentionHeight) + 1; int32 last = _mrows->isEmpty() ? (_hrows->isEmpty() ? _brows->size() : _hrows->size()) : _mrows->size(); bool hasUsername = _parent->filter().indexOf('@') > 1; + int filterSize = qMax(_parent->filter().size() - 1, 0); + bool filterIsEmpty = (filterSize == 0); for (int32 i = from; i < to; ++i) { if (i >= last) break; @@ -3958,7 +3960,8 @@ void MentionsInner::paintEvent(QPaintEvent *e) { p.setPen(st::black->p); if (!_mrows->isEmpty()) { UserData *user = _mrows->at(i); - QString first = (_parent->filter().size() < 2) ? QString() : ('@' + user->username.mid(0, _parent->filter().size() - 1)), second = (_parent->filter().size() < 2) ? ('@' + user->username) : user->username.mid(_parent->filter().size() - 1); + QString first = filterIsEmpty ? QString() : ('@' + user->username.mid(0, filterSize)); + QString second = (filterIsEmpty && !user->username.isEmpty()) ? ('@' + user->username) : user->username.mid(filterSize); int32 firstwidth = st::mentionFont->width(first), secondwidth = st::mentionFont->width(second), unamewidth = firstwidth + secondwidth, namewidth = user->nameText.maxWidth(); if (mentionwidth < unamewidth + namewidth) { namewidth = (mentionwidth * namewidth) / (namewidth + unamewidth); @@ -3987,7 +3990,8 @@ void MentionsInner::paintEvent(QPaintEvent *e) { } } else if (!_hrows->isEmpty()) { QString hrow = _hrows->at(i); - QString first = (_parent->filter().size() < 2) ? QString() : ('#' + hrow.mid(0, _parent->filter().size() - 1)), second = (_parent->filter().size() < 2) ? ('#' + hrow) : hrow.mid(_parent->filter().size() - 1); + QString first = filterIsEmpty ? QString() : ('#' + hrow.mid(0, filterSize)); + QString second = filterIsEmpty ? ('#' + hrow) : hrow.mid(filterSize); int32 firstwidth = st::mentionFont->width(first), secondwidth = st::mentionFont->width(second); if (htagwidth < firstwidth + secondwidth) { if (htagwidth < firstwidth + st::mentionFont->elidew) { @@ -4020,7 +4024,8 @@ void MentionsInner::paintEvent(QPaintEvent *e) { user->paintUserpicLeft(p, st::mentionPhotoSize, st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), width()); int32 addleft = 0, widthleft = mentionwidth; - QString first = (_parent->filter().size() < 2) ? QString() : ('/' + toHighlight.mid(0, _parent->filter().size() - 1)), second = (_parent->filter().size() < 2) ? ('/' + toHighlight) : toHighlight.mid(_parent->filter().size() - 1); + QString first = filterIsEmpty ? QString() : ('/' + toHighlight.mid(0, filterSize)); + QString second = filterIsEmpty ? ('/' + toHighlight) : toHighlight.mid(filterSize); int32 firstwidth = st::mentionFont->width(first), secondwidth = st::mentionFont->width(second); if (widthleft < firstwidth + secondwidth) { if (widthleft < firstwidth + st::mentionFont->elidew) { @@ -4424,6 +4429,7 @@ void MentionsDropdown::updateFiltered(bool resetScroll) { App::api()->requestStickerSets(); } } else if (_filter.at(0) == '@') { + bool listAllSuggestions = (_filter.size() < 2); if (_chat) { mrows.reserve((_addInlineBots ? cRecentInlineBots().size() : 0) + (_chat->participants.isEmpty() ? _chat->lastAuthors.size() : _chat->participants.size())); } else if (_channel && _channel->isMegagroup()) { @@ -4438,7 +4444,7 @@ void MentionsDropdown::updateFiltered(bool resetScroll) { for (RecentInlineBots::const_iterator i = cRecentInlineBots().cbegin(), e = cRecentInlineBots().cend(); i != e; ++i) { UserData *user = *i; if (user->username.isEmpty()) continue; - if (_filter.size() > 1 && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; mrows.push_back(user); ++recentInlineBots; } @@ -4452,7 +4458,7 @@ void MentionsDropdown::updateFiltered(bool resetScroll) { for (ChatData::Participants::const_iterator i = _chat->participants.cbegin(), e = _chat->participants.cend(); i != e; ++i) { UserData *user = i.key(); if (user->username.isEmpty()) continue; - if (_filter.size() > 1 && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; ordered.insertMulti(App::onlineForSort(user, now), user); } @@ -4460,7 +4466,7 @@ void MentionsDropdown::updateFiltered(bool resetScroll) { for (MentionRows::const_iterator i = _chat->lastAuthors.cbegin(), e = _chat->lastAuthors.cend(); i != e; ++i) { UserData *user = *i; if (user->username.isEmpty()) continue; - if (_filter.size() > 1 && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; mrows.push_back(user); if (!ordered.isEmpty()) { @@ -4482,7 +4488,7 @@ void MentionsDropdown::updateFiltered(bool resetScroll) { for (MegagroupInfo::LastParticipants::const_iterator i = _channel->mgInfo->lastParticipants.cbegin(), e = _channel->mgInfo->lastParticipants.cend(); i != e; ++i) { UserData *user = *i; if (user->username.isEmpty()) continue; - if (_filter.size() > 1 && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; mrows.push_back(user); } diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index b9e922790..4c930bc71 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -3305,7 +3305,7 @@ int32 gifMaxStatusWidth(DocumentData *document) { QString captionedSelectedText(const QString &attachType, const Text &caption, TextSelection selection) { if (selection != FullSelection) { - return caption.original(selection, Text::ExpandLinksAll); + return caption.original(selection, ExpandLinksAll); } QString result; result.reserve(5 + attachType.size() + caption.length()); @@ -3734,7 +3734,7 @@ void HistoryPhoto::detachFromParent() { } QString HistoryPhoto::inDialogsText() const { - return _caption.isEmpty() ? lang(lng_in_dlg_photo) : _caption.original(AllTextSelection, Text::ExpandLinksNone); + return _caption.isEmpty() ? lang(lng_in_dlg_photo) : _caption.original(AllTextSelection, ExpandLinksNone); } QString HistoryPhoto::selectedText(TextSelection selection) const { @@ -3980,7 +3980,7 @@ void HistoryVideo::setStatusSize(int32 newSize) const { } QString HistoryVideo::inDialogsText() const { - return _caption.isEmpty() ? lang(lng_in_dlg_video) : _caption.original(AllTextSelection, Text::ExpandLinksNone); + return _caption.isEmpty() ? lang(lng_in_dlg_video) : _caption.original(AllTextSelection, ExpandLinksNone); } QString HistoryVideo::selectedText(TextSelection selection) const { @@ -4471,7 +4471,7 @@ QString HistoryDocument::inDialogsText() const { } if (auto captioned = Get()) { if (!captioned->_caption.isEmpty()) { - result.append(' ').append(captioned->_caption.original(AllTextSelection, Text::ExpandLinksNone)); + result.append(' ').append(captioned->_caption.original(AllTextSelection, ExpandLinksNone)); } } return result; @@ -4929,7 +4929,7 @@ HistoryTextState HistoryGif::getState(int x, int y, HistoryStateRequest request) } QString HistoryGif::inDialogsText() const { - return qsl("GIF") + (_caption.isEmpty() ? QString() : (' ' + _caption.original(AllTextSelection, Text::ExpandLinksNone))); + return qsl("GIF") + (_caption.isEmpty() ? QString() : (' ' + _caption.original(AllTextSelection, ExpandLinksNone))); } QString HistoryGif::selectedText(TextSelection selection) const { @@ -5959,8 +5959,8 @@ QString HistoryWebPage::selectedText(TextSelection selection) const { if (selection == FullSelection) { return QString(); } - auto titleResult = _title.original(selection, Text::ExpandLinksAll); - auto descriptionResult = _description.original(toDescriptionSelection(selection), Text::ExpandLinksAll); + auto titleResult = _title.original(selection, ExpandLinksAll); + auto descriptionResult = _description.original(toDescriptionSelection(selection), ExpandLinksAll); if (titleResult.isEmpty()) { return descriptionResult; } else if (descriptionResult.isEmpty()) { @@ -6406,7 +6406,7 @@ QString HistoryLocation::selectedText(TextSelection selection) const { auto result = qsl("[ ") + lang(lng_maps_point) + qsl(" ]\n"); auto info = selectedText(AllTextSelection); if (!info.isEmpty()) result.append(info).append('\n'); - return result + _link->text(); + return result + _link->dragText(); } auto titleResult = _title.original(selection); auto descriptionResult = _description.original(toDescriptionSelection(selection)); @@ -6727,7 +6727,7 @@ HistoryMessage::HistoryMessage(History *history, const MTPDmessage &msg) createComponents(config); QString text(textClean(qs(msg.vmessage))); - initMedia(msg.has_media() ? (&msg.vmedia) : 0, text); + initMedia(msg.has_media() ? (&msg.vmedia) : nullptr, text); setText(text, msg.has_entities() ? entitiesFromMTP(msg.ventities.c_vector().v) : EntitiesInText()); } @@ -7157,9 +7157,9 @@ void HistoryMessage::eraseFromOverview() { QString HistoryMessage::selectedText(TextSelection selection) const { QString result, textResult, mediaResult; if (selection == FullSelection) { - textResult = _text.original(AllTextSelection, Text::ExpandLinksAll); + textResult = _text.original(AllTextSelection, ExpandLinksAll); } else { - textResult = _text.original(selection, Text::ExpandLinksAll); + textResult = _text.original(selection, ExpandLinksAll); } if (_media) { mediaResult = _media->selectedText(toMediaSelection(selection)); @@ -7173,7 +7173,7 @@ QString HistoryMessage::selectedText(TextSelection selection) const { } if (auto fwd = Get()) { if (selection == FullSelection) { - QString fwdinfo = fwd->_text.original(AllTextSelection, Text::ExpandLinksAll), wrapped; + QString fwdinfo = fwd->_text.original(AllTextSelection, ExpandLinksAll), wrapped; wrapped.reserve(fwdinfo.size() + 4 + result.size()); wrapped.append('[').append(fwdinfo).append(qsl("]\n")).append(result); result = wrapped; @@ -7191,7 +7191,7 @@ QString HistoryMessage::selectedText(TextSelection selection) const { } QString HistoryMessage::inDialogsText() const { - return emptyText() ? (_media ? _media->inDialogsText() : QString()) : _text.original(AllTextSelection, Text::ExpandLinksNone); + return emptyText() ? (_media ? _media->inDialogsText() : QString()) : _text.original(AllTextSelection, ExpandLinksNone); } HistoryMedia *HistoryMessage::getMedia() const { @@ -7228,8 +7228,9 @@ void HistoryMessage::setText(const QString &text, const EntitiesInText &entities } textstyleRestore(); - for (int32 i = 0, l = entities.size(); i != l; ++i) { - if (entities.at(i).type == EntityInTextUrl || entities.at(i).type == EntityInTextCustomUrl || entities.at(i).type == EntityInTextEmail) { + for_const (const auto &entity, entities) { + auto type = entity.type(); + if (type == EntityInTextUrl || type == EntityInTextCustomUrl || type == EntityInTextEmail) { _flags |= MTPDmessage_ClientFlag::f_has_text_links; break; } @@ -8194,7 +8195,7 @@ QString HistoryService::selectedText(TextSelection selection) const { } QString HistoryService::inDialogsText() const { - return _text.original(AllTextSelection, Text::ExpandLinksNone); + return _text.original(AllTextSelection, ExpandLinksNone); } QString HistoryService::inReplyText() const { diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index c8c21e839..4ee8cc083 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -1072,9 +1072,9 @@ void HistoryInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { } } - QString copyToClipboardContextItem = _contextMenuLnk ? _contextMenuLnk->copyToClipboardContextItem() : QString(); - if (!copyToClipboardContextItem.isEmpty()) { - _menu->addAction(copyToClipboardContextItem, this, SLOT(copyContextUrl()))->setEnabled(true); + QString linkCopyToClipboardText = _contextMenuLnk ? _contextMenuLnk->copyToClipboardContextItemText() : QString(); + if (!linkCopyToClipboardText.isEmpty()) { + _menu->addAction(linkCopyToClipboardText, this, SLOT(copyContextUrl()))->setEnabled(true); } if (item && item->hasDirectLink() && isUponSelected != 2 && isUponSelected != -2) { _menu->addAction(lang(lng_context_copy_post_link), _widget, SLOT(onCopyPostLink())); @@ -2027,7 +2027,7 @@ QString HistoryInner::tooltipText() const { } else if (_dragCursorState == HistoryInForwardedCursorState && _dragAction == NoDrag) { if (App::hoveredItem()) { if (HistoryMessageForwarded *fwd = App::hoveredItem()->Get()) { - return fwd->_text.original(AllTextSelection, Text::ExpandLinksNone); + return fwd->_text.original(AllTextSelection, ExpandLinksNone); } } } else if (ClickHandlerPtr lnk = ClickHandler::getActive()) { diff --git a/Telegram/SourceFiles/mtproto/scheme.tl b/Telegram/SourceFiles/mtproto/scheme.tl index 07dfee843..6eefd52d8 100644 --- a/Telegram/SourceFiles/mtproto/scheme.tl +++ b/Telegram/SourceFiles/mtproto/scheme.tl @@ -414,7 +414,7 @@ upload.file#96a18d5 type:storage.FileType mtime:int bytes:bytes = upload.File; dcOption#5d8c6cc flags:# ipv6:flags.0?true media_only:flags.1?true tcpo_only:flags.2?true id:int ip_address:string port:int = DcOption; -config#317ceef4 date:int expires:int test_mode:Bool this_dc:int dc_options:Vector chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int disabled_features:Vector = Config; +config#c9411388 date:int expires:int test_mode:Bool this_dc:int dc_options:Vector chat_size_max:int megagroup_size_max:int forwarded_count_max:int online_update_period_ms:int offline_blur_timeout_ms:int offline_idle_timeout_ms:int online_cloud_timeout_ms:int notify_cloud_delay_ms:int notify_default_delay_ms:int chat_big_size:int push_chat_period_ms:int push_chat_limit:int saved_gifs_limit:int edit_time_limit:int rating_e_decay:int disabled_features:Vector = Config; nearestDc#8e1a1775 country:string this_dc:int nearest_dc:int = NearestDc; @@ -587,6 +587,8 @@ messageEntityItalic#826f8b60 offset:int length:int = MessageEntity; messageEntityCode#28a20571 offset:int length:int = MessageEntity; messageEntityPre#73924be0 offset:int length:int language:string = MessageEntity; messageEntityTextUrl#76a6d327 offset:int length:int url:string = MessageEntity; +messageEntityMentionName#352dca58 offset:int length:int user_id:int = MessageEntity; +inputMessageEntityMentionName#208e68c9 offset:int length:int user_id:InputUser = MessageEntity; inputChannelEmpty#ee8c1e86 = InputChannel; inputChannel#afeb712e channel_id:int access_hash:long = InputChannel; @@ -677,6 +679,21 @@ inputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotIn inlineBotSwitchPM#3c20629f text:string start_param:string = InlineBotSwitchPM; +messages.peerDialogs#3371c354 dialogs:Vector messages:Vector chats:Vector users:Vector state:updates.State = messages.PeerDialogs; + +topPeer#edcdc05b peer:Peer rating:double = TopPeer; + +topPeerCategoryBotsPM#ab661b5b = TopPeerCategory; +topPeerCategoryBotsInline#148677e2 = TopPeerCategory; +topPeerCategoryCorrespondents#637b7ed = TopPeerCategory; +topPeerCategoryGroups#bd17a14a = TopPeerCategory; +topPeerCategoryChannels#161d9628 = TopPeerCategory; + +topPeerCategoryPeers#fb834291 category:TopPeerCategory count:int peers:Vector = TopPeerCategoryPeers; + +contacts.topPeersNotModified#de266ef5 = contacts.TopPeers; +contacts.topPeers#70b772a8 categories:Vector chats:Vector users:Vector = contacts.TopPeers; + ---functions--- invokeAfterMsg#cb9f372d {X:Type} msg_id:long query:!X = X; @@ -742,6 +759,8 @@ contacts.exportCard#84e53737 = Vector; contacts.importCard#4fe196fe export_card:Vector = User; contacts.search#11f812d8 q:string limit:int = contacts.Found; contacts.resolveUsername#f93ccba3 username:string = contacts.ResolvedPeer; +contacts.getTopPeers#d4982db5 flags:# correspondents:flags.0?true bots_pm:flags.1?true bots_inline:flags.2?true groups:flags.10?true channels:flags.15?true offset:int limit:int hash:int = contacts.TopPeers; +contacts.resetTopPeerRating#1ae373ac category:TopPeerCategory peer:InputPeer = Bool; messages.getMessages#4222fa74 id:Vector = messages.Messages; messages.getDialogs#6b47f94d offset_date:int offset_id:int offset_peer:InputPeer limit:int = messages.Dialogs; @@ -806,6 +825,7 @@ messages.editMessage#ce91e4ca flags:# no_webpage:flags.1?true peer:InputPeer id: messages.editInlineBotMessage#130c2c85 flags:# no_webpage:flags.1?true id:InputBotInlineMessageID message:flags.11?string reply_markup:flags.2?ReplyMarkup entities:flags.3?Vector = Bool; messages.getBotCallbackAnswer#a6e94f04 peer:InputPeer msg_id:int data:bytes = messages.BotCallbackAnswer; messages.setBotCallbackAnswer#481c591a flags:# alert:flags.1?true query_id:long message:flags.0?string = Bool; +messages.getPeerDialogs#19250887 peer:Vector = messages.PeerDialogs; updates.getState#edd4882a = updates.State; updates.getDifference#a041495 pts:int date:int qts:int = updates.Difference; @@ -859,4 +879,4 @@ channels.exportMessageLink#c846d22d channel:InputChannel id:int = ExportedMessag channels.toggleSignatures#1f69b606 channel:InputChannel enabled:Bool = Updates; channels.updatePinnedMessage#a72ded52 flags:# silent:flags.0?true channel:InputChannel id:int = Updates; -// LAYER 51 +// LAYER 52 diff --git a/Telegram/SourceFiles/mtproto/scheme_auto.cpp b/Telegram/SourceFiles/mtproto/scheme_auto.cpp index da72b7722..a92355752 100644 --- a/Telegram/SourceFiles/mtproto/scheme_auto.cpp +++ b/Telegram/SourceFiles/mtproto/scheme_auto.cpp @@ -3314,7 +3314,8 @@ void _serialize_config(MTPStringLogger &to, int32 stage, int32 lev, Types &types case 16: to.add(" push_chat_limit: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; case 17: to.add(" saved_gifs_limit: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; case 18: to.add(" edit_time_limit: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; - case 19: to.add(" disabled_features: "); ++stages.back(); types.push_back(00); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 19: to.add(" rating_e_decay: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 20: to.add(" disabled_features: "); ++stages.back(); types.push_back(00); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break; } } @@ -4768,6 +4769,36 @@ void _serialize_messageEntityTextUrl(MTPStringLogger &to, int32 stage, int32 lev } } +void _serialize_messageEntityMentionName(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + if (stage) { + to.add(",\n").addSpaces(lev); + } else { + to.add("{ messageEntityMentionName"); + to.add("\n").addSpaces(lev); + } + switch (stage) { + case 0: to.add(" offset: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 1: to.add(" length: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 2: to.add(" user_id: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break; + } +} + +void _serialize_inputMessageEntityMentionName(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + if (stage) { + to.add(",\n").addSpaces(lev); + } else { + to.add("{ inputMessageEntityMentionName"); + to.add("\n").addSpaces(lev); + } + switch (stage) { + case 0: to.add(" offset: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 1: to.add(" length: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 2: to.add(" user_id: "); ++stages.back(); types.push_back(0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break; + } +} + void _serialize_inputChannelEmpty(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { to.add("{ inputChannelEmpty }"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); } @@ -5622,6 +5653,91 @@ void _serialize_inlineBotSwitchPM(MTPStringLogger &to, int32 stage, int32 lev, T } } +void _serialize_messages_peerDialogs(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + if (stage) { + to.add(",\n").addSpaces(lev); + } else { + to.add("{ messages_peerDialogs"); + to.add("\n").addSpaces(lev); + } + switch (stage) { + case 0: to.add(" dialogs: "); ++stages.back(); types.push_back(00); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 1: to.add(" messages: "); ++stages.back(); types.push_back(00); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 2: to.add(" chats: "); ++stages.back(); types.push_back(00); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 3: to.add(" users: "); ++stages.back(); types.push_back(00); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 4: to.add(" state: "); ++stages.back(); types.push_back(0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break; + } +} + +void _serialize_topPeer(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + if (stage) { + to.add(",\n").addSpaces(lev); + } else { + to.add("{ topPeer"); + to.add("\n").addSpaces(lev); + } + switch (stage) { + case 0: to.add(" peer: "); ++stages.back(); types.push_back(0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 1: to.add(" rating: "); ++stages.back(); types.push_back(mtpc_double+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break; + } +} + +void _serialize_topPeerCategoryBotsPM(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + to.add("{ topPeerCategoryBotsPM }"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); +} + +void _serialize_topPeerCategoryBotsInline(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + to.add("{ topPeerCategoryBotsInline }"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); +} + +void _serialize_topPeerCategoryCorrespondents(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + to.add("{ topPeerCategoryCorrespondents }"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); +} + +void _serialize_topPeerCategoryGroups(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + to.add("{ topPeerCategoryGroups }"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); +} + +void _serialize_topPeerCategoryChannels(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + to.add("{ topPeerCategoryChannels }"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); +} + +void _serialize_topPeerCategoryPeers(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + if (stage) { + to.add(",\n").addSpaces(lev); + } else { + to.add("{ topPeerCategoryPeers"); + to.add("\n").addSpaces(lev); + } + switch (stage) { + case 0: to.add(" category: "); ++stages.back(); types.push_back(0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 1: to.add(" count: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 2: to.add(" peers: "); ++stages.back(); types.push_back(00); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break; + } +} + +void _serialize_contacts_topPeersNotModified(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + to.add("{ contacts_topPeersNotModified }"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); +} + +void _serialize_contacts_topPeers(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + if (stage) { + to.add(",\n").addSpaces(lev); + } else { + to.add("{ contacts_topPeers"); + to.add("\n").addSpaces(lev); + } + switch (stage) { + case 0: to.add(" categories: "); ++stages.back(); types.push_back(00); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 1: to.add(" chats: "); ++stages.back(); types.push_back(00); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 2: to.add(" users: "); ++stages.back(); types.push_back(00); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break; + } +} + void _serialize_req_pq(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { if (stage) { to.add(",\n").addSpaces(lev); @@ -5999,6 +6115,20 @@ void _serialize_contacts_unblock(MTPStringLogger &to, int32 stage, int32 lev, Ty } } +void _serialize_contacts_resetTopPeerRating(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + if (stage) { + to.add(",\n").addSpaces(lev); + } else { + to.add("{ contacts_resetTopPeerRating"); + to.add("\n").addSpaces(lev); + } + switch (stage) { + case 0: to.add(" category: "); ++stages.back(); types.push_back(0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 1: to.add(" peer: "); ++stages.back(); types.push_back(0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break; + } +} + void _serialize_messages_setTyping(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { if (stage) { to.add(",\n").addSpaces(lev); @@ -6826,6 +6956,29 @@ void _serialize_contacts_resolveUsername(MTPStringLogger &to, int32 stage, int32 } } +void _serialize_contacts_getTopPeers(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + MTPcontacts_getTopPeers::Flags flag(iflag); + + if (stage) { + to.add(",\n").addSpaces(lev); + } else { + to.add("{ contacts_getTopPeers"); + to.add("\n").addSpaces(lev); + } + switch (stage) { + case 0: to.add(" flags: "); ++stages.back(); if (start >= end) throw Exception("start >= end in flags"); else flags.back() = *start; types.push_back(mtpc_flags); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 1: to.add(" correspondents: "); ++stages.back(); if (flag & MTPcontacts_getTopPeers::Flag::f_correspondents) { to.add("YES [ BY BIT 0 IN FIELD flags ]"); } else { to.add("[ SKIPPED BY BIT 0 IN FIELD flags ]"); } break; + case 2: to.add(" bots_pm: "); ++stages.back(); if (flag & MTPcontacts_getTopPeers::Flag::f_bots_pm) { to.add("YES [ BY BIT 1 IN FIELD flags ]"); } else { to.add("[ SKIPPED BY BIT 1 IN FIELD flags ]"); } break; + case 3: to.add(" bots_inline: "); ++stages.back(); if (flag & MTPcontacts_getTopPeers::Flag::f_bots_inline) { to.add("YES [ BY BIT 2 IN FIELD flags ]"); } else { to.add("[ SKIPPED BY BIT 2 IN FIELD flags ]"); } break; + case 4: to.add(" groups: "); ++stages.back(); if (flag & MTPcontacts_getTopPeers::Flag::f_groups) { to.add("YES [ BY BIT 10 IN FIELD flags ]"); } else { to.add("[ SKIPPED BY BIT 10 IN FIELD flags ]"); } break; + case 5: to.add(" channels: "); ++stages.back(); if (flag & MTPcontacts_getTopPeers::Flag::f_channels) { to.add("YES [ BY BIT 15 IN FIELD flags ]"); } else { to.add("[ SKIPPED BY BIT 15 IN FIELD flags ]"); } break; + case 6: to.add(" offset: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 7: to.add(" limit: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + case 8: to.add(" hash: "); ++stages.back(); types.push_back(mtpc_int+0); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break; + } +} + void _serialize_messages_getMessages(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { if (stage) { to.add(",\n").addSpaces(lev); @@ -7883,6 +8036,19 @@ void _serialize_messages_getBotCallbackAnswer(MTPStringLogger &to, int32 stage, } } +void _serialize_messages_getPeerDialogs(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { + if (stage) { + to.add(",\n").addSpaces(lev); + } else { + to.add("{ messages_getPeerDialogs"); + to.add("\n").addSpaces(lev); + } + switch (stage) { + case 0: to.add(" peer: "); ++stages.back(); types.push_back(00); vtypes.push_back(0); stages.push_back(0); flags.push_back(0); break; + default: to.add("}"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); break; + } +} + void _serialize_updates_getState(MTPStringLogger &to, int32 stage, int32 lev, Types &types, Types &vtypes, StagesFlags &stages, StagesFlags &flags, const mtpPrime *start, const mtpPrime *end, int32 iflag) { to.add("{ updates_getState }"); types.pop_back(); vtypes.pop_back(); stages.pop_back(); flags.pop_back(); } @@ -8520,6 +8686,8 @@ namespace { _serializers.insert(mtpc_messageEntityCode, _serialize_messageEntityCode); _serializers.insert(mtpc_messageEntityPre, _serialize_messageEntityPre); _serializers.insert(mtpc_messageEntityTextUrl, _serialize_messageEntityTextUrl); + _serializers.insert(mtpc_messageEntityMentionName, _serialize_messageEntityMentionName); + _serializers.insert(mtpc_inputMessageEntityMentionName, _serialize_inputMessageEntityMentionName); _serializers.insert(mtpc_inputChannelEmpty, _serialize_inputChannelEmpty); _serializers.insert(mtpc_inputChannel, _serialize_inputChannel); _serializers.insert(mtpc_contacts_resolvedPeer, _serialize_contacts_resolvedPeer); @@ -8581,6 +8749,16 @@ namespace { _serializers.insert(mtpc_messages_messageEditData, _serialize_messages_messageEditData); _serializers.insert(mtpc_inputBotInlineMessageID, _serialize_inputBotInlineMessageID); _serializers.insert(mtpc_inlineBotSwitchPM, _serialize_inlineBotSwitchPM); + _serializers.insert(mtpc_messages_peerDialogs, _serialize_messages_peerDialogs); + _serializers.insert(mtpc_topPeer, _serialize_topPeer); + _serializers.insert(mtpc_topPeerCategoryBotsPM, _serialize_topPeerCategoryBotsPM); + _serializers.insert(mtpc_topPeerCategoryBotsInline, _serialize_topPeerCategoryBotsInline); + _serializers.insert(mtpc_topPeerCategoryCorrespondents, _serialize_topPeerCategoryCorrespondents); + _serializers.insert(mtpc_topPeerCategoryGroups, _serialize_topPeerCategoryGroups); + _serializers.insert(mtpc_topPeerCategoryChannels, _serialize_topPeerCategoryChannels); + _serializers.insert(mtpc_topPeerCategoryPeers, _serialize_topPeerCategoryPeers); + _serializers.insert(mtpc_contacts_topPeersNotModified, _serialize_contacts_topPeersNotModified); + _serializers.insert(mtpc_contacts_topPeers, _serialize_contacts_topPeers); _serializers.insert(mtpc_req_pq, _serialize_req_pq); _serializers.insert(mtpc_req_DH_params, _serialize_req_DH_params); @@ -8611,6 +8789,7 @@ namespace { _serializers.insert(mtpc_contacts_deleteContacts, _serialize_contacts_deleteContacts); _serializers.insert(mtpc_contacts_block, _serialize_contacts_block); _serializers.insert(mtpc_contacts_unblock, _serialize_contacts_unblock); + _serializers.insert(mtpc_contacts_resetTopPeerRating, _serialize_contacts_resetTopPeerRating); _serializers.insert(mtpc_messages_setTyping, _serialize_messages_setTyping); _serializers.insert(mtpc_messages_reportSpam, _serialize_messages_reportSpam); _serializers.insert(mtpc_messages_hideReportSpam, _serialize_messages_hideReportSpam); @@ -8673,6 +8852,7 @@ namespace { _serializers.insert(mtpc_messages_getMessagesViews, _serialize_messages_getMessagesViews); _serializers.insert(mtpc_contacts_search, _serialize_contacts_search); _serializers.insert(mtpc_contacts_resolveUsername, _serialize_contacts_resolveUsername); + _serializers.insert(mtpc_contacts_getTopPeers, _serialize_contacts_getTopPeers); _serializers.insert(mtpc_messages_getMessages, _serialize_messages_getMessages); _serializers.insert(mtpc_messages_getHistory, _serialize_messages_getHistory); _serializers.insert(mtpc_messages_search, _serialize_messages_search); @@ -8743,6 +8923,7 @@ namespace { _serializers.insert(mtpc_messages_getInlineBotResults, _serialize_messages_getInlineBotResults); _serializers.insert(mtpc_messages_getMessageEditData, _serialize_messages_getMessageEditData); _serializers.insert(mtpc_messages_getBotCallbackAnswer, _serialize_messages_getBotCallbackAnswer); + _serializers.insert(mtpc_messages_getPeerDialogs, _serialize_messages_getPeerDialogs); _serializers.insert(mtpc_updates_getState, _serialize_updates_getState); _serializers.insert(mtpc_updates_getDifference, _serialize_updates_getDifference); _serializers.insert(mtpc_updates_getChannelDifference, _serialize_updates_getChannelDifference); diff --git a/Telegram/SourceFiles/mtproto/scheme_auto.h b/Telegram/SourceFiles/mtproto/scheme_auto.h index 81bc519ae..43b12621f 100644 --- a/Telegram/SourceFiles/mtproto/scheme_auto.h +++ b/Telegram/SourceFiles/mtproto/scheme_auto.h @@ -30,7 +30,7 @@ Copyright (c) 2014 John Preston, https://desktop.telegram.org namespace MTP { namespace internal { -static constexpr mtpPrime CurrentLayer = 51; +static constexpr mtpPrime CurrentLayer = 52; class TypeCreator; @@ -302,7 +302,7 @@ enum { mtpc_photos_photo = 0x20212ca8, mtpc_upload_file = 0x96a18d5, mtpc_dcOption = 0x5d8c6cc, - mtpc_config = 0x317ceef4, + mtpc_config = 0xc9411388, mtpc_nearestDc = 0x8e1a1775, mtpc_help_appUpdate = 0x8987f311, mtpc_help_noAppUpdate = 0xc45a6536, @@ -426,6 +426,8 @@ enum { mtpc_messageEntityCode = 0x28a20571, mtpc_messageEntityPre = 0x73924be0, mtpc_messageEntityTextUrl = 0x76a6d327, + mtpc_messageEntityMentionName = 0x352dca58, + mtpc_inputMessageEntityMentionName = 0x208e68c9, mtpc_inputChannelEmpty = 0xee8c1e86, mtpc_inputChannel = 0xafeb712e, mtpc_contacts_resolvedPeer = 0x7f077ad9, @@ -487,6 +489,16 @@ enum { mtpc_messages_messageEditData = 0x26b5dde6, mtpc_inputBotInlineMessageID = 0x890c3d89, mtpc_inlineBotSwitchPM = 0x3c20629f, + mtpc_messages_peerDialogs = 0x3371c354, + mtpc_topPeer = 0xedcdc05b, + mtpc_topPeerCategoryBotsPM = 0xab661b5b, + mtpc_topPeerCategoryBotsInline = 0x148677e2, + mtpc_topPeerCategoryCorrespondents = 0x637b7ed, + mtpc_topPeerCategoryGroups = 0xbd17a14a, + mtpc_topPeerCategoryChannels = 0x161d9628, + mtpc_topPeerCategoryPeers = 0xfb834291, + mtpc_contacts_topPeersNotModified = 0xde266ef5, + mtpc_contacts_topPeers = 0x70b772a8, mtpc_invokeAfterMsg = 0xcb9f372d, mtpc_invokeAfterMsgs = 0x3dc4b4f0, mtpc_initConnection = 0x69796de9, @@ -546,6 +558,8 @@ enum { mtpc_contacts_importCard = 0x4fe196fe, mtpc_contacts_search = 0x11f812d8, mtpc_contacts_resolveUsername = 0xf93ccba3, + mtpc_contacts_getTopPeers = 0xd4982db5, + mtpc_contacts_resetTopPeerRating = 0x1ae373ac, mtpc_messages_getMessages = 0x4222fa74, mtpc_messages_getDialogs = 0x6b47f94d, mtpc_messages_getHistory = 0xafa92846, @@ -609,6 +623,7 @@ enum { mtpc_messages_editInlineBotMessage = 0x130c2c85, mtpc_messages_getBotCallbackAnswer = 0xa6e94f04, mtpc_messages_setBotCallbackAnswer = 0x481c591a, + mtpc_messages_getPeerDialogs = 0x19250887, mtpc_updates_getState = 0xedd4882a, mtpc_updates_getDifference = 0xa041495, mtpc_updates_getChannelDifference = 0xbb32d7c0, @@ -1217,6 +1232,8 @@ class MTPDmessageEntityItalic; class MTPDmessageEntityCode; class MTPDmessageEntityPre; class MTPDmessageEntityTextUrl; +class MTPDmessageEntityMentionName; +class MTPDinputMessageEntityMentionName; class MTPinputChannel; class MTPDinputChannel; @@ -1321,6 +1338,20 @@ class MTPDinputBotInlineMessageID; class MTPinlineBotSwitchPM; class MTPDinlineBotSwitchPM; +class MTPmessages_peerDialogs; +class MTPDmessages_peerDialogs; + +class MTPtopPeer; +class MTPDtopPeer; + +class MTPtopPeerCategory; + +class MTPtopPeerCategoryPeers; +class MTPDtopPeerCategoryPeers; + +class MTPcontacts_topPeers; +class MTPDcontacts_topPeers; + // Boxed types definitions typedef MTPBoxed MTPResPQ; @@ -1490,6 +1521,11 @@ typedef MTPBoxed MTPmessages_BotCallbackAnswer; typedef MTPBoxed MTPmessages_MessageEditData; typedef MTPBoxed MTPInputBotInlineMessageID; typedef MTPBoxed MTPInlineBotSwitchPM; +typedef MTPBoxed MTPmessages_PeerDialogs; +typedef MTPBoxed MTPTopPeer; +typedef MTPBoxed MTPTopPeerCategory; +typedef MTPBoxed MTPTopPeerCategoryPeers; +typedef MTPBoxed MTPcontacts_TopPeers; // Type classes definitions @@ -8089,6 +8125,30 @@ public: return *(const MTPDmessageEntityTextUrl*)data; } + MTPDmessageEntityMentionName &_messageEntityMentionName() { + if (!data) throw mtpErrorUninitialized(); + if (_type != mtpc_messageEntityMentionName) throw mtpErrorWrongTypeId(_type, mtpc_messageEntityMentionName); + split(); + return *(MTPDmessageEntityMentionName*)data; + } + const MTPDmessageEntityMentionName &c_messageEntityMentionName() const { + if (!data) throw mtpErrorUninitialized(); + if (_type != mtpc_messageEntityMentionName) throw mtpErrorWrongTypeId(_type, mtpc_messageEntityMentionName); + return *(const MTPDmessageEntityMentionName*)data; + } + + MTPDinputMessageEntityMentionName &_inputMessageEntityMentionName() { + if (!data) throw mtpErrorUninitialized(); + if (_type != mtpc_inputMessageEntityMentionName) throw mtpErrorWrongTypeId(_type, mtpc_inputMessageEntityMentionName); + split(); + return *(MTPDinputMessageEntityMentionName*)data; + } + const MTPDinputMessageEntityMentionName &c_inputMessageEntityMentionName() const { + if (!data) throw mtpErrorUninitialized(); + if (_type != mtpc_inputMessageEntityMentionName) throw mtpErrorWrongTypeId(_type, mtpc_inputMessageEntityMentionName); + return *(const MTPDinputMessageEntityMentionName*)data; + } + uint32 innerLength() const; mtpTypeId type() const; void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons); @@ -8109,6 +8169,8 @@ private: explicit MTPmessageEntity(MTPDmessageEntityCode *_data); explicit MTPmessageEntity(MTPDmessageEntityPre *_data); explicit MTPmessageEntity(MTPDmessageEntityTextUrl *_data); + explicit MTPmessageEntity(MTPDmessageEntityMentionName *_data); + explicit MTPmessageEntity(MTPDinputMessageEntityMentionName *_data); friend class MTP::internal::TypeCreator; @@ -9315,6 +9377,160 @@ private: }; typedef MTPBoxed MTPInlineBotSwitchPM; +class MTPmessages_peerDialogs : private mtpDataOwner { +public: + MTPmessages_peerDialogs(); + MTPmessages_peerDialogs(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_messages_peerDialogs) : mtpDataOwner(0) { + read(from, end, cons); + } + + MTPDmessages_peerDialogs &_messages_peerDialogs() { + if (!data) throw mtpErrorUninitialized(); + split(); + return *(MTPDmessages_peerDialogs*)data; + } + const MTPDmessages_peerDialogs &c_messages_peerDialogs() const { + if (!data) throw mtpErrorUninitialized(); + return *(const MTPDmessages_peerDialogs*)data; + } + + uint32 innerLength() const; + mtpTypeId type() const; + void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_messages_peerDialogs); + void write(mtpBuffer &to) const; + + typedef void ResponseType; + +private: + explicit MTPmessages_peerDialogs(MTPDmessages_peerDialogs *_data); + + friend class MTP::internal::TypeCreator; +}; +typedef MTPBoxed MTPmessages_PeerDialogs; + +class MTPtopPeer : private mtpDataOwner { +public: + MTPtopPeer(); + MTPtopPeer(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_topPeer) : mtpDataOwner(0) { + read(from, end, cons); + } + + MTPDtopPeer &_topPeer() { + if (!data) throw mtpErrorUninitialized(); + split(); + return *(MTPDtopPeer*)data; + } + const MTPDtopPeer &c_topPeer() const { + if (!data) throw mtpErrorUninitialized(); + return *(const MTPDtopPeer*)data; + } + + uint32 innerLength() const; + mtpTypeId type() const; + void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_topPeer); + void write(mtpBuffer &to) const; + + typedef void ResponseType; + +private: + explicit MTPtopPeer(MTPDtopPeer *_data); + + friend class MTP::internal::TypeCreator; +}; +typedef MTPBoxed MTPTopPeer; + +class MTPtopPeerCategory { +public: + MTPtopPeerCategory() : _type(0) { + } + MTPtopPeerCategory(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons) : _type(0) { + read(from, end, cons); + } + + uint32 innerLength() const; + mtpTypeId type() const; + void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons); + void write(mtpBuffer &to) const; + + typedef void ResponseType; + +private: + explicit MTPtopPeerCategory(mtpTypeId type); + + friend class MTP::internal::TypeCreator; + + mtpTypeId _type; +}; +typedef MTPBoxed MTPTopPeerCategory; + +class MTPtopPeerCategoryPeers : private mtpDataOwner { +public: + MTPtopPeerCategoryPeers(); + MTPtopPeerCategoryPeers(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_topPeerCategoryPeers) : mtpDataOwner(0) { + read(from, end, cons); + } + + MTPDtopPeerCategoryPeers &_topPeerCategoryPeers() { + if (!data) throw mtpErrorUninitialized(); + split(); + return *(MTPDtopPeerCategoryPeers*)data; + } + const MTPDtopPeerCategoryPeers &c_topPeerCategoryPeers() const { + if (!data) throw mtpErrorUninitialized(); + return *(const MTPDtopPeerCategoryPeers*)data; + } + + uint32 innerLength() const; + mtpTypeId type() const; + void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_topPeerCategoryPeers); + void write(mtpBuffer &to) const; + + typedef void ResponseType; + +private: + explicit MTPtopPeerCategoryPeers(MTPDtopPeerCategoryPeers *_data); + + friend class MTP::internal::TypeCreator; +}; +typedef MTPBoxed MTPTopPeerCategoryPeers; + +class MTPcontacts_topPeers : private mtpDataOwner { +public: + MTPcontacts_topPeers() : mtpDataOwner(0), _type(0) { + } + MTPcontacts_topPeers(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons) : mtpDataOwner(0), _type(0) { + read(from, end, cons); + } + + MTPDcontacts_topPeers &_contacts_topPeers() { + if (!data) throw mtpErrorUninitialized(); + if (_type != mtpc_contacts_topPeers) throw mtpErrorWrongTypeId(_type, mtpc_contacts_topPeers); + split(); + return *(MTPDcontacts_topPeers*)data; + } + const MTPDcontacts_topPeers &c_contacts_topPeers() const { + if (!data) throw mtpErrorUninitialized(); + if (_type != mtpc_contacts_topPeers) throw mtpErrorWrongTypeId(_type, mtpc_contacts_topPeers); + return *(const MTPDcontacts_topPeers*)data; + } + + uint32 innerLength() const; + mtpTypeId type() const; + void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons); + void write(mtpBuffer &to) const; + + typedef void ResponseType; + +private: + explicit MTPcontacts_topPeers(mtpTypeId type); + explicit MTPcontacts_topPeers(MTPDcontacts_topPeers *_data); + + friend class MTP::internal::TypeCreator; + + mtpTypeId _type; +}; +typedef MTPBoxed MTPcontacts_TopPeers; + // Type constructors with data class MTPDresPQ : public mtpDataImpl { @@ -12042,7 +12258,7 @@ class MTPDconfig : public mtpDataImpl { public: MTPDconfig() { } - MTPDconfig(MTPint _date, MTPint _expires, MTPBool _test_mode, MTPint _this_dc, const MTPVector &_dc_options, MTPint _chat_size_max, MTPint _megagroup_size_max, MTPint _forwarded_count_max, MTPint _online_update_period_ms, MTPint _offline_blur_timeout_ms, MTPint _offline_idle_timeout_ms, MTPint _online_cloud_timeout_ms, MTPint _notify_cloud_delay_ms, MTPint _notify_default_delay_ms, MTPint _chat_big_size, MTPint _push_chat_period_ms, MTPint _push_chat_limit, MTPint _saved_gifs_limit, MTPint _edit_time_limit, const MTPVector &_disabled_features) : vdate(_date), vexpires(_expires), vtest_mode(_test_mode), vthis_dc(_this_dc), vdc_options(_dc_options), vchat_size_max(_chat_size_max), vmegagroup_size_max(_megagroup_size_max), vforwarded_count_max(_forwarded_count_max), vonline_update_period_ms(_online_update_period_ms), voffline_blur_timeout_ms(_offline_blur_timeout_ms), voffline_idle_timeout_ms(_offline_idle_timeout_ms), vonline_cloud_timeout_ms(_online_cloud_timeout_ms), vnotify_cloud_delay_ms(_notify_cloud_delay_ms), vnotify_default_delay_ms(_notify_default_delay_ms), vchat_big_size(_chat_big_size), vpush_chat_period_ms(_push_chat_period_ms), vpush_chat_limit(_push_chat_limit), vsaved_gifs_limit(_saved_gifs_limit), vedit_time_limit(_edit_time_limit), vdisabled_features(_disabled_features) { + MTPDconfig(MTPint _date, MTPint _expires, MTPBool _test_mode, MTPint _this_dc, const MTPVector &_dc_options, MTPint _chat_size_max, MTPint _megagroup_size_max, MTPint _forwarded_count_max, MTPint _online_update_period_ms, MTPint _offline_blur_timeout_ms, MTPint _offline_idle_timeout_ms, MTPint _online_cloud_timeout_ms, MTPint _notify_cloud_delay_ms, MTPint _notify_default_delay_ms, MTPint _chat_big_size, MTPint _push_chat_period_ms, MTPint _push_chat_limit, MTPint _saved_gifs_limit, MTPint _edit_time_limit, MTPint _rating_e_decay, const MTPVector &_disabled_features) : vdate(_date), vexpires(_expires), vtest_mode(_test_mode), vthis_dc(_this_dc), vdc_options(_dc_options), vchat_size_max(_chat_size_max), vmegagroup_size_max(_megagroup_size_max), vforwarded_count_max(_forwarded_count_max), vonline_update_period_ms(_online_update_period_ms), voffline_blur_timeout_ms(_offline_blur_timeout_ms), voffline_idle_timeout_ms(_offline_idle_timeout_ms), vonline_cloud_timeout_ms(_online_cloud_timeout_ms), vnotify_cloud_delay_ms(_notify_cloud_delay_ms), vnotify_default_delay_ms(_notify_default_delay_ms), vchat_big_size(_chat_big_size), vpush_chat_period_ms(_push_chat_period_ms), vpush_chat_limit(_push_chat_limit), vsaved_gifs_limit(_saved_gifs_limit), vedit_time_limit(_edit_time_limit), vrating_e_decay(_rating_e_decay), vdisabled_features(_disabled_features) { } MTPint vdate; @@ -12064,6 +12280,7 @@ public: MTPint vpush_chat_limit; MTPint vsaved_gifs_limit; MTPint vedit_time_limit; + MTPint vrating_e_decay; MTPVector vdisabled_features; }; @@ -13212,6 +13429,30 @@ public: MTPstring vurl; }; +class MTPDmessageEntityMentionName : public mtpDataImpl { +public: + MTPDmessageEntityMentionName() { + } + MTPDmessageEntityMentionName(MTPint _offset, MTPint _length, MTPint _user_id) : voffset(_offset), vlength(_length), vuser_id(_user_id) { + } + + MTPint voffset; + MTPint vlength; + MTPint vuser_id; +}; + +class MTPDinputMessageEntityMentionName : public mtpDataImpl { +public: + MTPDinputMessageEntityMentionName() { + } + MTPDinputMessageEntityMentionName(MTPint _offset, MTPint _length, const MTPInputUser &_user_id) : voffset(_offset), vlength(_length), vuser_id(_user_id) { + } + + MTPint voffset; + MTPint vlength; + MTPInputUser vuser_id; +}; + class MTPDinputChannel : public mtpDataImpl { public: MTPDinputChannel() { @@ -14100,6 +14341,55 @@ public: MTPstring vstart_param; }; +class MTPDmessages_peerDialogs : public mtpDataImpl { +public: + MTPDmessages_peerDialogs() { + } + MTPDmessages_peerDialogs(const MTPVector &_dialogs, const MTPVector &_messages, const MTPVector &_chats, const MTPVector &_users, const MTPupdates_State &_state) : vdialogs(_dialogs), vmessages(_messages), vchats(_chats), vusers(_users), vstate(_state) { + } + + MTPVector vdialogs; + MTPVector vmessages; + MTPVector vchats; + MTPVector vusers; + MTPupdates_State vstate; +}; + +class MTPDtopPeer : public mtpDataImpl { +public: + MTPDtopPeer() { + } + MTPDtopPeer(const MTPPeer &_peer, const MTPdouble &_rating) : vpeer(_peer), vrating(_rating) { + } + + MTPPeer vpeer; + MTPdouble vrating; +}; + +class MTPDtopPeerCategoryPeers : public mtpDataImpl { +public: + MTPDtopPeerCategoryPeers() { + } + MTPDtopPeerCategoryPeers(const MTPTopPeerCategory &_category, MTPint _count, const MTPVector &_peers) : vcategory(_category), vcount(_count), vpeers(_peers) { + } + + MTPTopPeerCategory vcategory; + MTPint vcount; + MTPVector vpeers; +}; + +class MTPDcontacts_topPeers : public mtpDataImpl { +public: + MTPDcontacts_topPeers() { + } + MTPDcontacts_topPeers(const MTPVector &_categories, const MTPVector &_chats, const MTPVector &_users) : vcategories(_categories), vchats(_chats), vusers(_users) { + } + + MTPVector vcategories; + MTPVector vchats; + MTPVector vusers; +}; + // RPC methods class MTPreq_pq { // RPC method 'req_pq' @@ -16905,6 +17195,117 @@ public: } }; +class MTPcontacts_getTopPeers { // RPC method 'contacts.getTopPeers' +public: + enum class Flag : int32 { + f_correspondents = (1 << 0), + f_bots_pm = (1 << 1), + f_bots_inline = (1 << 2), + f_groups = (1 << 10), + f_channels = (1 << 15), + + + MAX_FIELD = (1 << 15), + }; + Q_DECLARE_FLAGS(Flags, Flag); + friend inline Flags operator~(Flag v) { return QFlag(~static_cast(v)); } + + bool is_correspondents() const { return vflags.v & Flag::f_correspondents; } + bool is_bots_pm() const { return vflags.v & Flag::f_bots_pm; } + bool is_bots_inline() const { return vflags.v & Flag::f_bots_inline; } + bool is_groups() const { return vflags.v & Flag::f_groups; } + bool is_channels() const { return vflags.v & Flag::f_channels; } + + MTPflags vflags; + MTPint voffset; + MTPint vlimit; + MTPint vhash; + + MTPcontacts_getTopPeers() { + } + MTPcontacts_getTopPeers(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_contacts_getTopPeers) { + read(from, end, cons); + } + MTPcontacts_getTopPeers(const MTPflags &_flags, MTPint _offset, MTPint _limit, MTPint _hash) : vflags(_flags), voffset(_offset), vlimit(_limit), vhash(_hash) { + } + + uint32 innerLength() const { + return vflags.innerLength() + voffset.innerLength() + vlimit.innerLength() + vhash.innerLength(); + } + mtpTypeId type() const { + return mtpc_contacts_getTopPeers; + } + void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_contacts_getTopPeers) { + vflags.read(from, end); + voffset.read(from, end); + vlimit.read(from, end); + vhash.read(from, end); + } + void write(mtpBuffer &to) const { + vflags.write(to); + voffset.write(to); + vlimit.write(to); + vhash.write(to); + } + + typedef MTPcontacts_TopPeers ResponseType; +}; +Q_DECLARE_OPERATORS_FOR_FLAGS(MTPcontacts_getTopPeers::Flags) + +class MTPcontacts_GetTopPeers : public MTPBoxed { +public: + MTPcontacts_GetTopPeers() { + } + MTPcontacts_GetTopPeers(const MTPcontacts_getTopPeers &v) : MTPBoxed(v) { + } + MTPcontacts_GetTopPeers(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = 0) : MTPBoxed(from, end, cons) { + } + MTPcontacts_GetTopPeers(const MTPflags &_flags, MTPint _offset, MTPint _limit, MTPint _hash) : MTPBoxed(MTPcontacts_getTopPeers(_flags, _offset, _limit, _hash)) { + } +}; + +class MTPcontacts_resetTopPeerRating { // RPC method 'contacts.resetTopPeerRating' +public: + MTPTopPeerCategory vcategory; + MTPInputPeer vpeer; + + MTPcontacts_resetTopPeerRating() { + } + MTPcontacts_resetTopPeerRating(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_contacts_resetTopPeerRating) { + read(from, end, cons); + } + MTPcontacts_resetTopPeerRating(const MTPTopPeerCategory &_category, const MTPInputPeer &_peer) : vcategory(_category), vpeer(_peer) { + } + + uint32 innerLength() const { + return vcategory.innerLength() + vpeer.innerLength(); + } + mtpTypeId type() const { + return mtpc_contacts_resetTopPeerRating; + } + void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_contacts_resetTopPeerRating) { + vcategory.read(from, end); + vpeer.read(from, end); + } + void write(mtpBuffer &to) const { + vcategory.write(to); + vpeer.write(to); + } + + typedef MTPBool ResponseType; +}; +class MTPcontacts_ResetTopPeerRating : public MTPBoxed { +public: + MTPcontacts_ResetTopPeerRating() { + } + MTPcontacts_ResetTopPeerRating(const MTPcontacts_resetTopPeerRating &v) : MTPBoxed(v) { + } + MTPcontacts_ResetTopPeerRating(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = 0) : MTPBoxed(from, end, cons) { + } + MTPcontacts_ResetTopPeerRating(const MTPTopPeerCategory &_category, const MTPInputPeer &_peer) : MTPBoxed(MTPcontacts_resetTopPeerRating(_category, _peer)) { + } +}; + class MTPmessages_getMessages { // RPC method 'messages.getMessages' public: MTPVector vid; @@ -19861,6 +20262,45 @@ public: } }; +class MTPmessages_getPeerDialogs { // RPC method 'messages.getPeerDialogs' +public: + MTPVector vpeer; + + MTPmessages_getPeerDialogs() { + } + MTPmessages_getPeerDialogs(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_messages_getPeerDialogs) { + read(from, end, cons); + } + MTPmessages_getPeerDialogs(const MTPVector &_peer) : vpeer(_peer) { + } + + uint32 innerLength() const { + return vpeer.innerLength(); + } + mtpTypeId type() const { + return mtpc_messages_getPeerDialogs; + } + void read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = mtpc_messages_getPeerDialogs) { + vpeer.read(from, end); + } + void write(mtpBuffer &to) const { + vpeer.write(to); + } + + typedef MTPmessages_PeerDialogs ResponseType; +}; +class MTPmessages_GetPeerDialogs : public MTPBoxed { +public: + MTPmessages_GetPeerDialogs() { + } + MTPmessages_GetPeerDialogs(const MTPmessages_getPeerDialogs &v) : MTPBoxed(v) { + } + MTPmessages_GetPeerDialogs(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons = 0) : MTPBoxed(from, end, cons) { + } + MTPmessages_GetPeerDialogs(const MTPVector &_peer) : MTPBoxed(MTPmessages_getPeerDialogs(_peer)) { + } +}; + class MTPupdates_getState { // RPC method 'updates.getState' public: MTPupdates_getState() { @@ -22637,8 +23077,8 @@ public: inline static MTPdcOption new_dcOption(const MTPflags &_flags, MTPint _id, const MTPstring &_ip_address, MTPint _port) { return MTPdcOption(new MTPDdcOption(_flags, _id, _ip_address, _port)); } - inline static MTPconfig new_config(MTPint _date, MTPint _expires, MTPBool _test_mode, MTPint _this_dc, const MTPVector &_dc_options, MTPint _chat_size_max, MTPint _megagroup_size_max, MTPint _forwarded_count_max, MTPint _online_update_period_ms, MTPint _offline_blur_timeout_ms, MTPint _offline_idle_timeout_ms, MTPint _online_cloud_timeout_ms, MTPint _notify_cloud_delay_ms, MTPint _notify_default_delay_ms, MTPint _chat_big_size, MTPint _push_chat_period_ms, MTPint _push_chat_limit, MTPint _saved_gifs_limit, MTPint _edit_time_limit, const MTPVector &_disabled_features) { - return MTPconfig(new MTPDconfig(_date, _expires, _test_mode, _this_dc, _dc_options, _chat_size_max, _megagroup_size_max, _forwarded_count_max, _online_update_period_ms, _offline_blur_timeout_ms, _offline_idle_timeout_ms, _online_cloud_timeout_ms, _notify_cloud_delay_ms, _notify_default_delay_ms, _chat_big_size, _push_chat_period_ms, _push_chat_limit, _saved_gifs_limit, _edit_time_limit, _disabled_features)); + inline static MTPconfig new_config(MTPint _date, MTPint _expires, MTPBool _test_mode, MTPint _this_dc, const MTPVector &_dc_options, MTPint _chat_size_max, MTPint _megagroup_size_max, MTPint _forwarded_count_max, MTPint _online_update_period_ms, MTPint _offline_blur_timeout_ms, MTPint _offline_idle_timeout_ms, MTPint _online_cloud_timeout_ms, MTPint _notify_cloud_delay_ms, MTPint _notify_default_delay_ms, MTPint _chat_big_size, MTPint _push_chat_period_ms, MTPint _push_chat_limit, MTPint _saved_gifs_limit, MTPint _edit_time_limit, MTPint _rating_e_decay, const MTPVector &_disabled_features) { + return MTPconfig(new MTPDconfig(_date, _expires, _test_mode, _this_dc, _dc_options, _chat_size_max, _megagroup_size_max, _forwarded_count_max, _online_update_period_ms, _offline_blur_timeout_ms, _offline_idle_timeout_ms, _online_cloud_timeout_ms, _notify_cloud_delay_ms, _notify_default_delay_ms, _chat_big_size, _push_chat_period_ms, _push_chat_limit, _saved_gifs_limit, _edit_time_limit, _rating_e_decay, _disabled_features)); } inline static MTPnearestDc new_nearestDc(const MTPstring &_country, MTPint _this_dc, MTPint _nearest_dc) { return MTPnearestDc(new MTPDnearestDc(_country, _this_dc, _nearest_dc)); @@ -23009,6 +23449,12 @@ public: inline static MTPmessageEntity new_messageEntityTextUrl(MTPint _offset, MTPint _length, const MTPstring &_url) { return MTPmessageEntity(new MTPDmessageEntityTextUrl(_offset, _length, _url)); } + inline static MTPmessageEntity new_messageEntityMentionName(MTPint _offset, MTPint _length, MTPint _user_id) { + return MTPmessageEntity(new MTPDmessageEntityMentionName(_offset, _length, _user_id)); + } + inline static MTPmessageEntity new_inputMessageEntityMentionName(MTPint _offset, MTPint _length, const MTPInputUser &_user_id) { + return MTPmessageEntity(new MTPDinputMessageEntityMentionName(_offset, _length, _user_id)); + } inline static MTPinputChannel new_inputChannelEmpty() { return MTPinputChannel(mtpc_inputChannelEmpty); } @@ -23192,6 +23638,36 @@ public: inline static MTPinlineBotSwitchPM new_inlineBotSwitchPM(const MTPstring &_text, const MTPstring &_start_param) { return MTPinlineBotSwitchPM(new MTPDinlineBotSwitchPM(_text, _start_param)); } + inline static MTPmessages_peerDialogs new_messages_peerDialogs(const MTPVector &_dialogs, const MTPVector &_messages, const MTPVector &_chats, const MTPVector &_users, const MTPupdates_State &_state) { + return MTPmessages_peerDialogs(new MTPDmessages_peerDialogs(_dialogs, _messages, _chats, _users, _state)); + } + inline static MTPtopPeer new_topPeer(const MTPPeer &_peer, const MTPdouble &_rating) { + return MTPtopPeer(new MTPDtopPeer(_peer, _rating)); + } + inline static MTPtopPeerCategory new_topPeerCategoryBotsPM() { + return MTPtopPeerCategory(mtpc_topPeerCategoryBotsPM); + } + inline static MTPtopPeerCategory new_topPeerCategoryBotsInline() { + return MTPtopPeerCategory(mtpc_topPeerCategoryBotsInline); + } + inline static MTPtopPeerCategory new_topPeerCategoryCorrespondents() { + return MTPtopPeerCategory(mtpc_topPeerCategoryCorrespondents); + } + inline static MTPtopPeerCategory new_topPeerCategoryGroups() { + return MTPtopPeerCategory(mtpc_topPeerCategoryGroups); + } + inline static MTPtopPeerCategory new_topPeerCategoryChannels() { + return MTPtopPeerCategory(mtpc_topPeerCategoryChannels); + } + inline static MTPtopPeerCategoryPeers new_topPeerCategoryPeers(const MTPTopPeerCategory &_category, MTPint _count, const MTPVector &_peers) { + return MTPtopPeerCategoryPeers(new MTPDtopPeerCategoryPeers(_category, _count, _peers)); + } + inline static MTPcontacts_topPeers new_contacts_topPeersNotModified() { + return MTPcontacts_topPeers(mtpc_contacts_topPeersNotModified); + } + inline static MTPcontacts_topPeers new_contacts_topPeers(const MTPVector &_categories, const MTPVector &_chats, const MTPVector &_users) { + return MTPcontacts_topPeers(new MTPDcontacts_topPeers(_categories, _chats, _users)); + } }; } // namespace internal @@ -29528,7 +30004,7 @@ inline MTPconfig::MTPconfig() : mtpDataOwner(new MTPDconfig()) { inline uint32 MTPconfig::innerLength() const { const MTPDconfig &v(c_config()); - return v.vdate.innerLength() + v.vexpires.innerLength() + v.vtest_mode.innerLength() + v.vthis_dc.innerLength() + v.vdc_options.innerLength() + v.vchat_size_max.innerLength() + v.vmegagroup_size_max.innerLength() + v.vforwarded_count_max.innerLength() + v.vonline_update_period_ms.innerLength() + v.voffline_blur_timeout_ms.innerLength() + v.voffline_idle_timeout_ms.innerLength() + v.vonline_cloud_timeout_ms.innerLength() + v.vnotify_cloud_delay_ms.innerLength() + v.vnotify_default_delay_ms.innerLength() + v.vchat_big_size.innerLength() + v.vpush_chat_period_ms.innerLength() + v.vpush_chat_limit.innerLength() + v.vsaved_gifs_limit.innerLength() + v.vedit_time_limit.innerLength() + v.vdisabled_features.innerLength(); + return v.vdate.innerLength() + v.vexpires.innerLength() + v.vtest_mode.innerLength() + v.vthis_dc.innerLength() + v.vdc_options.innerLength() + v.vchat_size_max.innerLength() + v.vmegagroup_size_max.innerLength() + v.vforwarded_count_max.innerLength() + v.vonline_update_period_ms.innerLength() + v.voffline_blur_timeout_ms.innerLength() + v.voffline_idle_timeout_ms.innerLength() + v.vonline_cloud_timeout_ms.innerLength() + v.vnotify_cloud_delay_ms.innerLength() + v.vnotify_default_delay_ms.innerLength() + v.vchat_big_size.innerLength() + v.vpush_chat_period_ms.innerLength() + v.vpush_chat_limit.innerLength() + v.vsaved_gifs_limit.innerLength() + v.vedit_time_limit.innerLength() + v.vrating_e_decay.innerLength() + v.vdisabled_features.innerLength(); } inline mtpTypeId MTPconfig::type() const { return mtpc_config; @@ -29557,6 +30033,7 @@ inline void MTPconfig::read(const mtpPrime *&from, const mtpPrime *end, mtpTypeI v.vpush_chat_limit.read(from, end); v.vsaved_gifs_limit.read(from, end); v.vedit_time_limit.read(from, end); + v.vrating_e_decay.read(from, end); v.vdisabled_features.read(from, end); } inline void MTPconfig::write(mtpBuffer &to) const { @@ -29580,12 +30057,13 @@ inline void MTPconfig::write(mtpBuffer &to) const { v.vpush_chat_limit.write(to); v.vsaved_gifs_limit.write(to); v.vedit_time_limit.write(to); + v.vrating_e_decay.write(to); v.vdisabled_features.write(to); } inline MTPconfig::MTPconfig(MTPDconfig *_data) : mtpDataOwner(_data) { } -inline MTPconfig MTP_config(MTPint _date, MTPint _expires, MTPBool _test_mode, MTPint _this_dc, const MTPVector &_dc_options, MTPint _chat_size_max, MTPint _megagroup_size_max, MTPint _forwarded_count_max, MTPint _online_update_period_ms, MTPint _offline_blur_timeout_ms, MTPint _offline_idle_timeout_ms, MTPint _online_cloud_timeout_ms, MTPint _notify_cloud_delay_ms, MTPint _notify_default_delay_ms, MTPint _chat_big_size, MTPint _push_chat_period_ms, MTPint _push_chat_limit, MTPint _saved_gifs_limit, MTPint _edit_time_limit, const MTPVector &_disabled_features) { - return MTP::internal::TypeCreator::new_config(_date, _expires, _test_mode, _this_dc, _dc_options, _chat_size_max, _megagroup_size_max, _forwarded_count_max, _online_update_period_ms, _offline_blur_timeout_ms, _offline_idle_timeout_ms, _online_cloud_timeout_ms, _notify_cloud_delay_ms, _notify_default_delay_ms, _chat_big_size, _push_chat_period_ms, _push_chat_limit, _saved_gifs_limit, _edit_time_limit, _disabled_features); +inline MTPconfig MTP_config(MTPint _date, MTPint _expires, MTPBool _test_mode, MTPint _this_dc, const MTPVector &_dc_options, MTPint _chat_size_max, MTPint _megagroup_size_max, MTPint _forwarded_count_max, MTPint _online_update_period_ms, MTPint _offline_blur_timeout_ms, MTPint _offline_idle_timeout_ms, MTPint _online_cloud_timeout_ms, MTPint _notify_cloud_delay_ms, MTPint _notify_default_delay_ms, MTPint _chat_big_size, MTPint _push_chat_period_ms, MTPint _push_chat_limit, MTPint _saved_gifs_limit, MTPint _edit_time_limit, MTPint _rating_e_decay, const MTPVector &_disabled_features) { + return MTP::internal::TypeCreator::new_config(_date, _expires, _test_mode, _this_dc, _dc_options, _chat_size_max, _megagroup_size_max, _forwarded_count_max, _online_update_period_ms, _offline_blur_timeout_ms, _offline_idle_timeout_ms, _online_cloud_timeout_ms, _notify_cloud_delay_ms, _notify_default_delay_ms, _chat_big_size, _push_chat_period_ms, _push_chat_limit, _saved_gifs_limit, _edit_time_limit, _rating_e_decay, _disabled_features); } inline MTPnearestDc::MTPnearestDc() : mtpDataOwner(new MTPDnearestDc()) { @@ -32352,6 +32830,14 @@ inline uint32 MTPmessageEntity::innerLength() const { const MTPDmessageEntityTextUrl &v(c_messageEntityTextUrl()); return v.voffset.innerLength() + v.vlength.innerLength() + v.vurl.innerLength(); } + case mtpc_messageEntityMentionName: { + const MTPDmessageEntityMentionName &v(c_messageEntityMentionName()); + return v.voffset.innerLength() + v.vlength.innerLength() + v.vuser_id.innerLength(); + } + case mtpc_inputMessageEntityMentionName: { + const MTPDinputMessageEntityMentionName &v(c_inputMessageEntityMentionName()); + return v.voffset.innerLength() + v.vlength.innerLength() + v.vuser_id.innerLength(); + } } return 0; } @@ -32430,6 +32916,20 @@ inline void MTPmessageEntity::read(const mtpPrime *&from, const mtpPrime *end, m v.vlength.read(from, end); v.vurl.read(from, end); } break; + case mtpc_messageEntityMentionName: _type = cons; { + if (!data) setData(new MTPDmessageEntityMentionName()); + MTPDmessageEntityMentionName &v(_messageEntityMentionName()); + v.voffset.read(from, end); + v.vlength.read(from, end); + v.vuser_id.read(from, end); + } break; + case mtpc_inputMessageEntityMentionName: _type = cons; { + if (!data) setData(new MTPDinputMessageEntityMentionName()); + MTPDinputMessageEntityMentionName &v(_inputMessageEntityMentionName()); + v.voffset.read(from, end); + v.vlength.read(from, end); + v.vuser_id.read(from, end); + } break; default: throw mtpErrorUnexpected(cons, "MTPmessageEntity"); } } @@ -32492,6 +32992,18 @@ inline void MTPmessageEntity::write(mtpBuffer &to) const { v.vlength.write(to); v.vurl.write(to); } break; + case mtpc_messageEntityMentionName: { + const MTPDmessageEntityMentionName &v(c_messageEntityMentionName()); + v.voffset.write(to); + v.vlength.write(to); + v.vuser_id.write(to); + } break; + case mtpc_inputMessageEntityMentionName: { + const MTPDinputMessageEntityMentionName &v(c_inputMessageEntityMentionName()); + v.voffset.write(to); + v.vlength.write(to); + v.vuser_id.write(to); + } break; } } inline MTPmessageEntity::MTPmessageEntity(mtpTypeId type) : mtpDataOwner(0), _type(type) { @@ -32507,6 +33019,8 @@ inline MTPmessageEntity::MTPmessageEntity(mtpTypeId type) : mtpDataOwner(0), _ty case mtpc_messageEntityCode: setData(new MTPDmessageEntityCode()); break; case mtpc_messageEntityPre: setData(new MTPDmessageEntityPre()); break; case mtpc_messageEntityTextUrl: setData(new MTPDmessageEntityTextUrl()); break; + case mtpc_messageEntityMentionName: setData(new MTPDmessageEntityMentionName()); break; + case mtpc_inputMessageEntityMentionName: setData(new MTPDinputMessageEntityMentionName()); break; default: throw mtpErrorBadTypeId(type, "MTPmessageEntity"); } } @@ -32532,6 +33046,10 @@ inline MTPmessageEntity::MTPmessageEntity(MTPDmessageEntityPre *_data) : mtpData } inline MTPmessageEntity::MTPmessageEntity(MTPDmessageEntityTextUrl *_data) : mtpDataOwner(_data), _type(mtpc_messageEntityTextUrl) { } +inline MTPmessageEntity::MTPmessageEntity(MTPDmessageEntityMentionName *_data) : mtpDataOwner(_data), _type(mtpc_messageEntityMentionName) { +} +inline MTPmessageEntity::MTPmessageEntity(MTPDinputMessageEntityMentionName *_data) : mtpDataOwner(_data), _type(mtpc_inputMessageEntityMentionName) { +} inline MTPmessageEntity MTP_messageEntityUnknown(MTPint _offset, MTPint _length) { return MTP::internal::TypeCreator::new_messageEntityUnknown(_offset, _length); } @@ -32565,6 +33083,12 @@ inline MTPmessageEntity MTP_messageEntityPre(MTPint _offset, MTPint _length, con inline MTPmessageEntity MTP_messageEntityTextUrl(MTPint _offset, MTPint _length, const MTPstring &_url) { return MTP::internal::TypeCreator::new_messageEntityTextUrl(_offset, _length, _url); } +inline MTPmessageEntity MTP_messageEntityMentionName(MTPint _offset, MTPint _length, MTPint _user_id) { + return MTP::internal::TypeCreator::new_messageEntityMentionName(_offset, _length, _user_id); +} +inline MTPmessageEntity MTP_inputMessageEntityMentionName(MTPint _offset, MTPint _length, const MTPInputUser &_user_id) { + return MTP::internal::TypeCreator::new_inputMessageEntityMentionName(_offset, _length, _user_id); +} inline uint32 MTPinputChannel::innerLength() const { switch (_type) { @@ -34263,6 +34787,199 @@ inline MTPinlineBotSwitchPM::MTPinlineBotSwitchPM(MTPDinlineBotSwitchPM *_data) inline MTPinlineBotSwitchPM MTP_inlineBotSwitchPM(const MTPstring &_text, const MTPstring &_start_param) { return MTP::internal::TypeCreator::new_inlineBotSwitchPM(_text, _start_param); } + +inline MTPmessages_peerDialogs::MTPmessages_peerDialogs() : mtpDataOwner(new MTPDmessages_peerDialogs()) { +} + +inline uint32 MTPmessages_peerDialogs::innerLength() const { + const MTPDmessages_peerDialogs &v(c_messages_peerDialogs()); + return v.vdialogs.innerLength() + v.vmessages.innerLength() + v.vchats.innerLength() + v.vusers.innerLength() + v.vstate.innerLength(); +} +inline mtpTypeId MTPmessages_peerDialogs::type() const { + return mtpc_messages_peerDialogs; +} +inline void MTPmessages_peerDialogs::read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons) { + if (cons != mtpc_messages_peerDialogs) throw mtpErrorUnexpected(cons, "MTPmessages_peerDialogs"); + + if (!data) setData(new MTPDmessages_peerDialogs()); + MTPDmessages_peerDialogs &v(_messages_peerDialogs()); + v.vdialogs.read(from, end); + v.vmessages.read(from, end); + v.vchats.read(from, end); + v.vusers.read(from, end); + v.vstate.read(from, end); +} +inline void MTPmessages_peerDialogs::write(mtpBuffer &to) const { + const MTPDmessages_peerDialogs &v(c_messages_peerDialogs()); + v.vdialogs.write(to); + v.vmessages.write(to); + v.vchats.write(to); + v.vusers.write(to); + v.vstate.write(to); +} +inline MTPmessages_peerDialogs::MTPmessages_peerDialogs(MTPDmessages_peerDialogs *_data) : mtpDataOwner(_data) { +} +inline MTPmessages_peerDialogs MTP_messages_peerDialogs(const MTPVector &_dialogs, const MTPVector &_messages, const MTPVector &_chats, const MTPVector &_users, const MTPupdates_State &_state) { + return MTP::internal::TypeCreator::new_messages_peerDialogs(_dialogs, _messages, _chats, _users, _state); +} + +inline MTPtopPeer::MTPtopPeer() : mtpDataOwner(new MTPDtopPeer()) { +} + +inline uint32 MTPtopPeer::innerLength() const { + const MTPDtopPeer &v(c_topPeer()); + return v.vpeer.innerLength() + v.vrating.innerLength(); +} +inline mtpTypeId MTPtopPeer::type() const { + return mtpc_topPeer; +} +inline void MTPtopPeer::read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons) { + if (cons != mtpc_topPeer) throw mtpErrorUnexpected(cons, "MTPtopPeer"); + + if (!data) setData(new MTPDtopPeer()); + MTPDtopPeer &v(_topPeer()); + v.vpeer.read(from, end); + v.vrating.read(from, end); +} +inline void MTPtopPeer::write(mtpBuffer &to) const { + const MTPDtopPeer &v(c_topPeer()); + v.vpeer.write(to); + v.vrating.write(to); +} +inline MTPtopPeer::MTPtopPeer(MTPDtopPeer *_data) : mtpDataOwner(_data) { +} +inline MTPtopPeer MTP_topPeer(const MTPPeer &_peer, const MTPdouble &_rating) { + return MTP::internal::TypeCreator::new_topPeer(_peer, _rating); +} + +inline uint32 MTPtopPeerCategory::innerLength() const { + return 0; +} +inline mtpTypeId MTPtopPeerCategory::type() const { + if (!_type) throw mtpErrorUninitialized(); + return _type; +} +inline void MTPtopPeerCategory::read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons) { + switch (cons) { + case mtpc_topPeerCategoryBotsPM: _type = cons; break; + case mtpc_topPeerCategoryBotsInline: _type = cons; break; + case mtpc_topPeerCategoryCorrespondents: _type = cons; break; + case mtpc_topPeerCategoryGroups: _type = cons; break; + case mtpc_topPeerCategoryChannels: _type = cons; break; + default: throw mtpErrorUnexpected(cons, "MTPtopPeerCategory"); + } +} +inline void MTPtopPeerCategory::write(mtpBuffer &to) const { +} +inline MTPtopPeerCategory::MTPtopPeerCategory(mtpTypeId type) : _type(type) { + switch (type) { + case mtpc_topPeerCategoryBotsPM: break; + case mtpc_topPeerCategoryBotsInline: break; + case mtpc_topPeerCategoryCorrespondents: break; + case mtpc_topPeerCategoryGroups: break; + case mtpc_topPeerCategoryChannels: break; + default: throw mtpErrorBadTypeId(type, "MTPtopPeerCategory"); + } +} +inline MTPtopPeerCategory MTP_topPeerCategoryBotsPM() { + return MTP::internal::TypeCreator::new_topPeerCategoryBotsPM(); +} +inline MTPtopPeerCategory MTP_topPeerCategoryBotsInline() { + return MTP::internal::TypeCreator::new_topPeerCategoryBotsInline(); +} +inline MTPtopPeerCategory MTP_topPeerCategoryCorrespondents() { + return MTP::internal::TypeCreator::new_topPeerCategoryCorrespondents(); +} +inline MTPtopPeerCategory MTP_topPeerCategoryGroups() { + return MTP::internal::TypeCreator::new_topPeerCategoryGroups(); +} +inline MTPtopPeerCategory MTP_topPeerCategoryChannels() { + return MTP::internal::TypeCreator::new_topPeerCategoryChannels(); +} + +inline MTPtopPeerCategoryPeers::MTPtopPeerCategoryPeers() : mtpDataOwner(new MTPDtopPeerCategoryPeers()) { +} + +inline uint32 MTPtopPeerCategoryPeers::innerLength() const { + const MTPDtopPeerCategoryPeers &v(c_topPeerCategoryPeers()); + return v.vcategory.innerLength() + v.vcount.innerLength() + v.vpeers.innerLength(); +} +inline mtpTypeId MTPtopPeerCategoryPeers::type() const { + return mtpc_topPeerCategoryPeers; +} +inline void MTPtopPeerCategoryPeers::read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons) { + if (cons != mtpc_topPeerCategoryPeers) throw mtpErrorUnexpected(cons, "MTPtopPeerCategoryPeers"); + + if (!data) setData(new MTPDtopPeerCategoryPeers()); + MTPDtopPeerCategoryPeers &v(_topPeerCategoryPeers()); + v.vcategory.read(from, end); + v.vcount.read(from, end); + v.vpeers.read(from, end); +} +inline void MTPtopPeerCategoryPeers::write(mtpBuffer &to) const { + const MTPDtopPeerCategoryPeers &v(c_topPeerCategoryPeers()); + v.vcategory.write(to); + v.vcount.write(to); + v.vpeers.write(to); +} +inline MTPtopPeerCategoryPeers::MTPtopPeerCategoryPeers(MTPDtopPeerCategoryPeers *_data) : mtpDataOwner(_data) { +} +inline MTPtopPeerCategoryPeers MTP_topPeerCategoryPeers(const MTPTopPeerCategory &_category, MTPint _count, const MTPVector &_peers) { + return MTP::internal::TypeCreator::new_topPeerCategoryPeers(_category, _count, _peers); +} + +inline uint32 MTPcontacts_topPeers::innerLength() const { + switch (_type) { + case mtpc_contacts_topPeers: { + const MTPDcontacts_topPeers &v(c_contacts_topPeers()); + return v.vcategories.innerLength() + v.vchats.innerLength() + v.vusers.innerLength(); + } + } + return 0; +} +inline mtpTypeId MTPcontacts_topPeers::type() const { + if (!_type) throw mtpErrorUninitialized(); + return _type; +} +inline void MTPcontacts_topPeers::read(const mtpPrime *&from, const mtpPrime *end, mtpTypeId cons) { + if (cons != _type) setData(0); + switch (cons) { + case mtpc_contacts_topPeersNotModified: _type = cons; break; + case mtpc_contacts_topPeers: _type = cons; { + if (!data) setData(new MTPDcontacts_topPeers()); + MTPDcontacts_topPeers &v(_contacts_topPeers()); + v.vcategories.read(from, end); + v.vchats.read(from, end); + v.vusers.read(from, end); + } break; + default: throw mtpErrorUnexpected(cons, "MTPcontacts_topPeers"); + } +} +inline void MTPcontacts_topPeers::write(mtpBuffer &to) const { + switch (_type) { + case mtpc_contacts_topPeers: { + const MTPDcontacts_topPeers &v(c_contacts_topPeers()); + v.vcategories.write(to); + v.vchats.write(to); + v.vusers.write(to); + } break; + } +} +inline MTPcontacts_topPeers::MTPcontacts_topPeers(mtpTypeId type) : mtpDataOwner(0), _type(type) { + switch (type) { + case mtpc_contacts_topPeersNotModified: break; + case mtpc_contacts_topPeers: setData(new MTPDcontacts_topPeers()); break; + default: throw mtpErrorBadTypeId(type, "MTPcontacts_topPeers"); + } +} +inline MTPcontacts_topPeers::MTPcontacts_topPeers(MTPDcontacts_topPeers *_data) : mtpDataOwner(_data), _type(mtpc_contacts_topPeers) { +} +inline MTPcontacts_topPeers MTP_contacts_topPeersNotModified() { + return MTP::internal::TypeCreator::new_contacts_topPeersNotModified(); +} +inline MTPcontacts_topPeers MTP_contacts_topPeers(const MTPVector &_categories, const MTPVector &_chats, const MTPVector &_users) { + return MTP::internal::TypeCreator::new_contacts_topPeers(_categories, _chats, _users); +} inline MTPDmessage::Flags mtpCastFlags(MTPDmessageService::Flags flags) { return MTPDmessage::Flags(QFlag(flags)); } inline MTPDmessage::Flags mtpCastFlags(MTPflags flags) { return mtpCastFlags(flags.v); } inline MTPDmessage::Flags mtpCastFlags(MTPDupdateShortMessage::Flags flags) { return MTPDmessage::Flags(QFlag(flags)); } diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index cbb0da758..77358278a 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -898,31 +898,38 @@ bool Document::updateStatusText() const { Link::Link(HistoryMedia *media, HistoryItem *parent) : ItemBase(parent) { AddComponents(Info::Bit()); - QString text = _parent->originalText(); + QString text = _parent->originalText(), mainUrl; EntitiesInText entities = _parent->originalEntities(); int32 from = 0, till = text.size(), lnk = entities.size(); - for (int32 i = 0; i < lnk; ++i) { - if (entities[i].type != EntityInTextUrl && entities[i].type != EntityInTextCustomUrl && entities[i].type != EntityInTextEmail) { + for_const (const auto &entity, entities) { + auto type = entity.type(); + if (type != EntityInTextUrl && type != EntityInTextCustomUrl && type != EntityInTextEmail) { continue; } - QString u = entities[i].text, t = text.mid(entities[i].offset, entities[i].length); - _links.push_back(LinkEntry(u.isEmpty() ? t : u, t)); + auto customUrl = entity.data(), entityText = text.mid(entity.offset(), entity.length()); + auto url = customUrl.isEmpty() ? entityText : customUrl; + if (_links.isEmpty()) { + mainUrl = url; + } + _links.push_back(LinkEntry(url, entityText)); } while (lnk > 0 && till > from) { --lnk; - if (entities[lnk].type != EntityInTextUrl && entities[lnk].type != EntityInTextCustomUrl && entities[lnk].type != EntityInTextEmail) { + const auto &entity = entities.at(lnk); + auto type = entity.type(); + if (type != EntityInTextUrl && type != EntityInTextCustomUrl && type != EntityInTextEmail) { ++lnk; break; } - int32 afterLinkStart = entities[lnk].offset + entities[lnk].length; + int32 afterLinkStart = entity.offset() + entity.length(); if (till > afterLinkStart) { if (!QRegularExpression(qsl("^[,.\\s_=+\\-;:`'\"\\(\\)\\[\\]\\{\\}<>*&^%\\$#@!\\\\/]+$")).match(text.mid(afterLinkStart, till - afterLinkStart)).hasMatch()) { ++lnk; break; } } - till = entities[lnk].offset; + till = entity.offset(); } if (!lnk) { if (QRegularExpression(qsl("^[,.\\s\\-;:`'\"\\(\\)\\[\\]\\{\\}<>*&^%\\$#@!\\\\/]+$")).match(text.mid(from, till - from)).hasMatch()) { @@ -932,6 +939,7 @@ Link::Link(HistoryMedia *media, HistoryItem *parent) : ItemBase(parent) { _page = (media && media->type() == MediaTypeWebPage) ? static_cast(media)->webpage() : 0; if (_page) { + mainUrl = _page->url; if (_page->document) { _photol.reset(new DocumentOpenClickHandler(_page->document)); } else if (_page->photo) { @@ -945,8 +953,8 @@ Link::Link(HistoryMedia *media, HistoryItem *parent) : ItemBase(parent) { } else { _photol = MakeShared(_page->url); } - } else if (!_links.isEmpty()) { - _photol = MakeShared(_links.front().lnk->text()); + } else if (!mainUrl.isEmpty()) { + _photol = MakeShared(mainUrl); } if (from >= till && _page) { text = _page->description; @@ -984,8 +992,7 @@ Link::Link(HistoryMedia *media, HistoryItem *parent) : ItemBase(parent) { if (_page) { _title = _page->title; } - QString url(_page ? _page->url : (_links.isEmpty() ? QString() : _links.at(0).lnk->text())); - QVector parts = url.splitRef('/'); + QVector parts = mainUrl.splitRef('/'); if (!parts.isEmpty()) { QStringRef domain = parts.at(0); if (parts.size() > 2 && domain.endsWith(':') && parts.at(1).isEmpty()) { // http:// and others diff --git a/Telegram/SourceFiles/overviewwidget.cpp b/Telegram/SourceFiles/overviewwidget.cpp index dcd166361..1fe700ab8 100644 --- a/Telegram/SourceFiles/overviewwidget.cpp +++ b/Telegram/SourceFiles/overviewwidget.cpp @@ -1260,9 +1260,9 @@ void OverviewInner::showContextMenu(QContextMenuEvent *e, bool showFromTouch) { if (_selectedMsgId) repaintItem(_selectedMsgId, -1); } else if (!ignoreMousedItem && App::mousedItem() && App::mousedItem()->channelId() == itemChannel(_mousedItem) && App::mousedItem()->id == itemMsgId(_mousedItem)) { _menu = new PopupMenu(); - QString copyToClipboardContextItem = _contextMenuLnk ? _contextMenuLnk->copyToClipboardContextItem() : QString(); - if (!copyToClipboardContextItem.isEmpty()) { - _menu->addAction(copyToClipboardContextItem, this, SLOT(copyContextUrl()))->setEnabled(true); + QString linkCopyToClipboardText = _contextMenuLnk ? _contextMenuLnk->copyToClipboardContextItemText() : QString(); + if (!linkCopyToClipboardText.isEmpty()) { + _menu->addAction(linkCopyToClipboardText, this, SLOT(copyContextUrl()))->setEnabled(true); } _menu->addAction(lang(lng_context_to_msg), this, SLOT(goToMessage()))->setEnabled(true); if (isUponSelected > 1) { diff --git a/Telegram/SourceFiles/structs.cpp b/Telegram/SourceFiles/structs.cpp index 6a50154f9..b42abb395 100644 --- a/Telegram/SourceFiles/structs.cpp +++ b/Telegram/SourceFiles/structs.cpp @@ -1512,3 +1512,18 @@ MsgId clientMsgId() { Q_ASSERT(currentClientMsgId < EndClientMsgId); return currentClientMsgId++; } + +QString LocationClickHandler::copyToClipboardContextItemText() const { + return lang(lng_context_copy_link); +} + +void LocationClickHandler::onClick(Qt::MouseButton button) const { + if (!psLaunchMaps(_coords)) { + QDesktopServices::openUrl(_text); + } +} + +void LocationClickHandler::setup() { + QString latlon(qsl("%1,%2").arg(_coords.lat).arg(_coords.lon)); + _text = qsl("https://maps.google.com/maps?q=") + latlon + qsl("&ll=") + latlon + qsl("&z=16"); +} diff --git a/Telegram/SourceFiles/structs.h b/Telegram/SourceFiles/structs.h index fe2255bb0..b9eb32b17 100644 --- a/Telegram/SourceFiles/structs.h +++ b/Telegram/SourceFiles/structs.h @@ -1348,22 +1348,23 @@ public: LocationClickHandler(const LocationCoords &coords) : _coords(coords) { setup(); } - QString copyToClipboardContextItem() const override; + + void onClick(Qt::MouseButton button) const override; + + QString tooltip() const override { + return QString(); + } + + QString dragText() const override { + return _text; + } void copyToClipboard() const override { if (!_text.isEmpty()) { QApplication::clipboard()->setText(_text); } } - - QString tooltip() const override { - return QString(); - } - - QString text() const override { - return _text; - } - void onClick(Qt::MouseButton button) const override; + QString copyToClipboardContextItemText() const override; private: diff --git a/Telegram/SourceFiles/ui/emoji_config.h b/Telegram/SourceFiles/ui/emoji_config.h index f7e6c2597..64cc5a974 100644 --- a/Telegram/SourceFiles/ui/emoji_config.h +++ b/Telegram/SourceFiles/ui/emoji_config.h @@ -167,15 +167,14 @@ inline bool emojiEdge(const QChar *ch) { inline void appendPartToResult(QString &result, const QChar *start, const QChar *from, const QChar *to, EntitiesInText &entities) { if (to > from) { - for (EntitiesInText::iterator i = entities.begin(), e = entities.end(); i != e; ++i) { - if (i->offset >= to - start) break; - if (i->offset + i->length < from - start) continue; - if (i->offset >= from - start) { - i->offset -= (from - start - result.size()); - i->length += (from - start - result.size()); + for (auto &entity : entities) { + if (entity.offset() >= to - start) break; + if (entity.offset() + entity.length() < from - start) continue; + if (entity.offset() >= from - start) { + entity.extendToLeft(from - start - result.size()); } - if (i->offset + i->length < to - start) { - i->length -= (from - start - result.size()); + if (entity.offset() + entity.length() < to - start) { + entity.shrinkFromRight(from - start - result.size()); } } result.append(from, to - from); @@ -184,7 +183,7 @@ inline void appendPartToResult(QString &result, const QChar *start, const QChar inline QString replaceEmojis(const QString &text, EntitiesInText &entities) { QString result; - int32 currentEntity = 0, entitiesCount = entities.size(); + auto currentEntity = entities.begin(), entitiesEnd = entities.end(); const QChar *emojiStart = text.constData(), *emojiEnd = emojiStart, *e = text.constData() + text.size(); bool canFindEmoji = true; for (const QChar *ch = emojiEnd; ch != e;) { @@ -194,14 +193,14 @@ inline QString replaceEmojis(const QString &text, EntitiesInText &entities) { emojiFind(ch, e, newEmojiEnd, emojiCode); } - while (currentEntity < entitiesCount && ch >= emojiStart + entities[currentEntity].offset + entities[currentEntity].length) { + while (currentEntity != entitiesEnd && ch >= emojiStart + currentEntity->offset() + currentEntity->length()) { ++currentEntity; } EmojiPtr emoji = emojiCode ? emojiGet(emojiCode) : 0; if (emoji && emoji != TwoSymbolEmoji && (ch == emojiStart || !ch->isLetterOrNumber() || !(ch - 1)->isLetterOrNumber()) && (newEmojiEnd == e || !newEmojiEnd->isLetterOrNumber() || newEmojiEnd == emojiStart || !(newEmojiEnd - 1)->isLetterOrNumber()) && - (currentEntity >= entitiesCount || (ch < emojiStart + entities[currentEntity].offset && newEmojiEnd <= emojiStart + entities[currentEntity].offset) || (ch >= emojiStart + entities[currentEntity].offset + entities[currentEntity].length && newEmojiEnd > emojiStart + entities[currentEntity].offset + entities[currentEntity].length)) + (currentEntity == entitiesEnd || (ch < emojiStart + currentEntity->offset() && newEmojiEnd <= emojiStart + currentEntity->offset()) || (ch >= emojiStart + currentEntity->offset() + currentEntity->length() && newEmojiEnd > emojiStart + currentEntity->offset() + currentEntity->length())) ) { if (result.isEmpty()) result.reserve(text.size()); diff --git a/Telegram/SourceFiles/ui/flatinput.h b/Telegram/SourceFiles/ui/flatinput.h index 3b70cd6f9..cce9c6f95 100644 --- a/Telegram/SourceFiles/ui/flatinput.h +++ b/Telegram/SourceFiles/ui/flatinput.h @@ -475,20 +475,20 @@ private: bool _undoAvailable, _redoAvailable; bool _customUpDown; - + QString _placeholder, _placeholderFull; bool _placeholderVisible; anim::ivalue a_placeholderLeft; anim::fvalue a_placeholderOpacity; anim::cvalue a_placeholderFg; Animation _a_placeholderFg, _a_placeholderShift; - + anim::fvalue a_borderOpacityActive; anim::cvalue a_borderFg; Animation _a_border; - + bool _focused, _error; - + const style::InputField &_st; QTimer _touchTimer; diff --git a/Telegram/SourceFiles/ui/text/text.cpp b/Telegram/SourceFiles/ui/text/text.cpp index 2aaf88c84..979cb3735 100644 --- a/Telegram/SourceFiles/ui/text/text.cpp +++ b/Telegram/SourceFiles/ui/text/text.cpp @@ -293,10 +293,10 @@ public: } removeFlags.erase(removeFlags.begin()); } - while (waitingEntity != entitiesEnd && start + waitingEntity->offset + waitingEntity->length <= ptr) { + while (waitingEntity != entitiesEnd && start + waitingEntity->offset() + waitingEntity->length() <= ptr) { ++waitingEntity; } - if (waitingEntity == entitiesEnd || ptr < start + waitingEntity->offset) { + if (waitingEntity == entitiesEnd || ptr < start + waitingEntity->offset()) { return; } @@ -304,18 +304,19 @@ public: int32 startFlags = 0; int32 fullDisplayed; QString lnkUrl, lnkText; - if (waitingEntity->type == EntityInTextCustomUrl) { + auto type = waitingEntity->type(); + if (type == EntityInTextCustomUrl) { lnk = true; - lnkUrl = waitingEntity->text; - lnkText = QString(start + waitingEntity->offset, waitingEntity->length); + lnkUrl = waitingEntity->data(); + lnkText = QString(start + waitingEntity->offset(), waitingEntity->length()); fullDisplayed = -5; - } else if (waitingEntity->type == EntityInTextBold) { + } else if (type == EntityInTextBold) { startFlags = TextBlockFSemibold; - } else if (waitingEntity->type == EntityInTextItalic) { + } else if (type == EntityInTextItalic) { startFlags = TextBlockFItalic; - } else if (waitingEntity->type == EntityInTextCode) { + } else if (type == EntityInTextCode) { startFlags = TextBlockFCode; - } else if (waitingEntity->type == EntityInTextPre) { + } else if (type == EntityInTextPre) { startFlags = TextBlockFPre; createBlock(); if (!_t->_blocks.isEmpty() && _t->_blocks.back()->type() != TextBlockTNewline) { @@ -323,7 +324,7 @@ public: } } else { lnk = true; - lnkUrl = QString(start + waitingEntity->offset, waitingEntity->length); + lnkUrl = QString(start + waitingEntity->offset(), waitingEntity->length()); getLinkData(lnkUrl, lnkText, fullDisplayed); } @@ -334,7 +335,7 @@ public: lnkIndex = 0x8000 + links.size(); _t->_text += lnkText; - ptr = start + waitingEntity->offset + waitingEntity->length; + ptr = start + waitingEntity->offset() + waitingEntity->length(); createBlock(); @@ -343,24 +344,25 @@ public: if (!(flags & startFlags)) { createBlock(); flags |= startFlags; - removeFlags[start + waitingEntity->offset + waitingEntity->length].push_front(startFlags); + removeFlags[start + waitingEntity->offset() + waitingEntity->length()].push_front(startFlags); } } ++waitingEntity; if (links.size() >= 0x7FFF) { while (waitingEntity != entitiesEnd && ( - waitingEntity->type == EntityInTextUrl || - waitingEntity->type == EntityInTextCustomUrl || - waitingEntity->type == EntityInTextEmail || - waitingEntity->type == EntityInTextHashtag || - waitingEntity->type == EntityInTextMention || - waitingEntity->type == EntityInTextBotCommand || - waitingEntity->length <= 0)) { + waitingEntity->type() == EntityInTextUrl || + waitingEntity->type() == EntityInTextCustomUrl || + waitingEntity->type() == EntityInTextEmail || + waitingEntity->type() == EntityInTextHashtag || + waitingEntity->type() == EntityInTextMention || + waitingEntity->type() == EntityInTextMentionName || + waitingEntity->type() == EntityInTextBotCommand || + waitingEntity->length() <= 0)) { ++waitingEntity; } } else { - while (waitingEntity != entitiesEnd && waitingEntity->length <= 0) ++waitingEntity; + while (waitingEntity != entitiesEnd && waitingEntity->length() <= 0) ++waitingEntity; } } @@ -587,11 +589,11 @@ public: entities.reserve(l); const QChar s = text.size(); for (; i < l; ++i) { - EntityInTextType t = preparsed.at(i).type; - if ((t == EntityInTextMention && !parseMentions) || - (t == EntityInTextHashtag && !parseHashtags) || - (t == EntityInTextBotCommand && !parseBotCommands) || - ((t == EntityInTextBold || t == EntityInTextItalic || t == EntityInTextCode || t == EntityInTextPre) && !parseMono)) { + auto type = preparsed.at(i).type(); + if (((type == EntityInTextMention || type == EntityInTextMentionName) && !parseMentions) || + (type == EntityInTextHashtag && !parseHashtags) || + (type == EntityInTextBotCommand && !parseBotCommands) || + ((type == EntityInTextBold || type == EntityInTextItalic || type == EntityInTextCode || type == EntityInTextPre) && !parseMono)) { continue; } entities.push_back(preparsed.at(i)); @@ -608,8 +610,13 @@ public: start = src.constData(); end = start + src.size(); + entitiesEnd = entities.cend(); + waitingEntity = entities.cbegin(); + while (waitingEntity != entitiesEnd && waitingEntity->length() <= 0) ++waitingEntity; + int firstMonospaceOffset = EntityInText::firstMonospaceOffset(entities, end - start); + ptr = start; - while (ptr != end && chIsTrimmed(*ptr, rich)) { + while (ptr != end && chIsTrimmed(*ptr, rich) && ptr != start + firstMonospaceOffset) { ++ptr; } while (ptr != end && chIsTrimmed(*(end - 1), rich)) { @@ -628,9 +635,6 @@ public: ch = emojiLookback = 0; lastSkipped = false; checkTilde = !cRetina() && _t->_font->size() == 13 && _t->_font->flags() == 0; // tilde Open Sans fix - entitiesEnd = entities.cend(); - waitingEntity = entities.cbegin(); - while (waitingEntity != entitiesEnd && waitingEntity->length <= 0) ++waitingEntity; for (; ptr <= end; ++ptr) { checkEntities(); if (rich) { @@ -2928,29 +2932,21 @@ QString Text::original(TextSelection selection, ExpandLinksMode mode) const { for (TextBlocks::const_iterator i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) { int32 blockLnkIndex = (i == e) ? 0 : (*i)->lnkIndex(); int32 blockFrom = (i == e) ? _text.size() : (*i)->from(); + if (blockLnkIndex && !_links.at(blockLnkIndex - 1)) { // ignore empty links + blockLnkIndex = 0; + } if (blockLnkIndex != lnkIndex) { - if (lnkIndex) { // write link - const ClickHandlerPtr &lnk(_links.at(lnkIndex - 1)); - const QString &url = (mode == ExpandLinksNone || !lnk) ? emptyurl : lnk->text(); - - int32 rangeFrom = qMax(int32(selection.from), lnkFrom), rangeTo = qMin(blockFrom, int32(selection.to)); - - if (rangeTo > rangeFrom) { - QStringRef r = _text.midRef(rangeFrom, rangeTo - rangeFrom); - if (url.isEmpty() || lnkFrom != rangeFrom || blockFrom != rangeTo) { + int32 rangeFrom = qMax(int32(selection.from), lnkFrom), rangeTo = qMin(blockFrom, int32(selection.to)); + if (lnkIndex && rangeTo > rangeFrom) { // write link + QStringRef r = _text.midRef(rangeFrom, rangeTo - rangeFrom); + if (lnkFrom != rangeFrom || blockFrom != rangeTo) { + result += r; + } else { + QString expanded = _links.at(lnkIndex - 1)->getExpandedLinkText(mode, r); + if (expanded.isEmpty()) { result += r; } else { - QUrl u(url); - QString displayed = (u.isValid() ? u.toDisplayString() : url); - bool shortened = (r.size() > 3) && (_text.midRef(lnkFrom, r.size() - 3) == displayed.midRef(0, r.size() - 3)); - bool same = (r == displayed.midRef(0, r.size())) || (r == url.midRef(0, r.size())); - if (same || shortened) { - result += url; - } else if (mode == ExpandLinksAll) { - result.append(r).append(qsl(" ( ")).append(url).append(qsl(" )")); - } else { - result += r; - } + result += expanded; } } } @@ -2974,8 +2970,6 @@ QString Text::original(TextSelection selection, ExpandLinksMode mode) const { EntitiesInText Text::originalEntities() const { EntitiesInText result; - QString emptyurl; - int32 originalLength = 0, lnkStart = 0, italicStart = 0, boldStart = 0, codeStart = 0, preStart = 0; int32 lnkFrom = 0, lnkIndex = 0, flags = 0; for (TextBlocks::const_iterator i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) { @@ -3005,39 +2999,18 @@ EntitiesInText Text::originalEntities() const { } flags = blockFlags; } + if (blockLnkIndex && !_links.at(blockLnkIndex - 1)) { // ignore empty links + blockLnkIndex = 0; + } if (blockLnkIndex != lnkIndex) { - if (lnkIndex) { // write link - const ClickHandlerPtr &lnk(_links.at(lnkIndex - 1)); - const QString &url(lnk ? lnk->text() : emptyurl); - - int32 rangeFrom = lnkFrom, rangeTo = blockFrom; - if (rangeTo > rangeFrom) { - QStringRef r = _text.midRef(rangeFrom, rangeTo - rangeFrom); - if (url.isEmpty()) { - originalLength += r.size(); - } else { - QUrl u(url); - QString displayed = (u.isValid() ? u.toDisplayString() : url); - bool shortened = (r.size() > 3) && (_text.midRef(lnkFrom, r.size() - 3) == displayed.midRef(0, r.size() - 3)); - bool same = (r == displayed.midRef(0, r.size())) || (r == url.midRef(0, r.size())); - if (same || shortened) { - originalLength += url.size(); - if (url.at(0) == '@') { - result.push_back(EntityInText(EntityInTextMention, lnkStart, originalLength - lnkStart)); - } else if (url.at(0) == '#') { - result.push_back(EntityInText(EntityInTextHashtag, lnkStart, originalLength - lnkStart)); - } else if (url.at(0) == '/') { - result.push_back(EntityInText(EntityInTextBotCommand, lnkStart, originalLength - lnkStart)); - } else if (url.indexOf('@') > 0 && url.indexOf('/') <= 0) { - result.push_back(EntityInText(EntityInTextEmail, lnkStart, originalLength - lnkStart)); - } else { - result.push_back(EntityInText(EntityInTextUrl, lnkStart, originalLength - lnkStart)); - } - } else { - originalLength += r.size(); - result.push_back(EntityInText(EntityInTextCustomUrl, lnkStart, originalLength - lnkStart, url)); - } - } + int32 rangeFrom = lnkFrom, rangeTo = blockFrom; + if (lnkIndex && rangeTo > rangeFrom) { // write link + QStringRef r = _text.midRef(rangeFrom, rangeTo - rangeFrom); + if (auto entity = _links.at(lnkIndex - 1)->getEntityInText(lnkStart, r)) { + result.push_back(entity); + originalLength += entity.length(); + } else { + originalLength += r.size(); } } lnkIndex = blockLnkIndex; diff --git a/Telegram/SourceFiles/ui/text/text.h b/Telegram/SourceFiles/ui/text/text.h index 569154f64..691e076b5 100644 --- a/Telegram/SourceFiles/ui/text/text.h +++ b/Telegram/SourceFiles/ui/text/text.h @@ -178,11 +178,6 @@ public: int length() const { return _text.size(); } - enum ExpandLinksMode { - ExpandLinksNone, - ExpandLinksShortened, - ExpandLinksAll, - }; QString original(TextSelection selection = AllTextSelection, ExpandLinksMode mode = ExpandLinksShortened) const; EntitiesInText originalEntities() const; diff --git a/Telegram/SourceFiles/ui/text/text_entity.cpp b/Telegram/SourceFiles/ui/text/text_entity.cpp index bc241a950..9afb72ad2 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.cpp +++ b/Telegram/SourceFiles/ui/text/text_entity.cpp @@ -1211,7 +1211,7 @@ bool textSplit(QString &sendingText, EntitiesInText &sendingEntities, QString &l int32 s = 0, half = limit / 2, goodLevel = 0; for (const QChar *start = leftText.constData(), *ch = start, *end = leftText.constEnd(), *good = ch; ch != end; ++ch, ++s) { - while (currentEntity < entityCount && ch >= start + leftEntities[currentEntity].offset + leftEntities[currentEntity].length) { + while (currentEntity < entityCount && ch >= start + leftEntities.at(currentEntity).offset() + leftEntities.at(currentEntity).length()) { ++currentEntity; } @@ -1225,8 +1225,8 @@ goodCanBreakEntity = canBreakEntity;\ } if (s > half) { - bool inEntity = (currentEntity < entityCount) && (ch > start + leftEntities[currentEntity].offset) && (ch < start + leftEntities[currentEntity].offset + leftEntities[currentEntity].length); - EntityInTextType entityType = (currentEntity < entityCount) ? leftEntities[currentEntity].type : EntityInTextBold; + bool inEntity = (currentEntity < entityCount) && (ch > start + leftEntities.at(currentEntity).offset()) && (ch < start + leftEntities.at(currentEntity).offset() + leftEntities.at(currentEntity).length()); + EntityInTextType entityType = (currentEntity < entityCount) ? leftEntities.at(currentEntity).type() : EntityInTextInvalid; bool canBreakEntity = (entityType == EntityInTextPre || entityType == EntityInTextCode); int32 noEntityLevel = inEntity ? 0 : 1; if (inEntity && !canBreakEntity) { @@ -1241,9 +1241,9 @@ goodCanBreakEntity = canBreakEntity;\ } } else if (ch + 1 < end && chIsNewline(*(ch + 1))) { MARK_GOOD_AS_LEVEL(15); - } else if (currentEntity < entityCount && ch + 1 == start + leftEntities[currentEntity].offset && leftEntities[currentEntity].type == EntityInTextPre) { + } else if (currentEntity < entityCount && ch + 1 == start + leftEntities.at(currentEntity).offset() && leftEntities.at(currentEntity).type() == EntityInTextPre) { MARK_GOOD_AS_LEVEL(14); - } else if (currentEntity > 0 && ch == start + leftEntities[currentEntity - 1].offset + leftEntities[currentEntity - 1].length && leftEntities[currentEntity - 1].type == EntityInTextPre) { + } else if (currentEntity > 0 && ch == start + leftEntities.at(currentEntity - 1).offset() + leftEntities.at(currentEntity - 1).length() && leftEntities.at(currentEntity - 1).type() == EntityInTextPre) { MARK_GOOD_AS_LEVEL(14); } else { MARK_GOOD_AS_LEVEL(13); @@ -1285,14 +1285,10 @@ goodCanBreakEntity = canBreakEntity;\ if (goodInEntity) { if (goodCanBreakEntity) { sendingEntities = leftEntities.mid(0, goodEntity + 1); - sendingEntities.back().length = good - start - sendingEntities.back().offset; + sendingEntities.back().updateTextEnd(good - start); leftEntities = leftEntities.mid(goodEntity); - for (EntitiesInText::iterator i = leftEntities.begin(), e = leftEntities.end(); i != e; ++i) { - i->offset -= good - start; - if (i->offset < 0) { - i->length += i->offset; - i->offset = 0; - } + for (auto &entity : leftEntities) { + entity.shiftLeft(good - start); } } else { sendingEntities = leftEntities.mid(0, goodEntity); @@ -1301,8 +1297,8 @@ goodCanBreakEntity = canBreakEntity;\ } else { sendingEntities = leftEntities.mid(0, goodEntity); leftEntities = leftEntities.mid(goodEntity); - for (EntitiesInText::iterator i = leftEntities.begin(), e = leftEntities.end(); i != e; ++i) { - i->offset -= good - start; + for (auto &entity : leftEntities) { + entity.shiftLeft(good - start); } } return true; @@ -1367,6 +1363,7 @@ EntitiesInText entitiesFromMTP(const QVector &entities) { case mtpc_messageEntityEmail: { const auto &d(entity.c_messageEntityEmail()); result.push_back(EntityInText(EntityInTextEmail, d.voffset.v, d.vlength.v)); } break; case mtpc_messageEntityHashtag: { const auto &d(entity.c_messageEntityHashtag()); result.push_back(EntityInText(EntityInTextHashtag, d.voffset.v, d.vlength.v)); } break; case mtpc_messageEntityMention: { const auto &d(entity.c_messageEntityMention()); result.push_back(EntityInText(EntityInTextMention, d.voffset.v, d.vlength.v)); } break; + case mtpc_messageEntityMentionName: { const auto &d(entity.c_messageEntityMentionName()); result.push_back(EntityInText(EntityInTextMentionName, d.voffset.v, d.vlength.v, QString::number(d.vuser_id.v))); } break; case mtpc_messageEntityBotCommand: { const auto &d(entity.c_messageEntityBotCommand()); result.push_back(EntityInText(EntityInTextBotCommand, d.voffset.v, d.vlength.v)); } break; case mtpc_messageEntityBold: { const auto &d(entity.c_messageEntityBold()); result.push_back(EntityInText(EntityInTextBold, d.voffset.v, d.vlength.v)); } break; case mtpc_messageEntityItalic: { const auto &d(entity.c_messageEntityItalic()); result.push_back(EntityInText(EntityInTextItalic, d.voffset.v, d.vlength.v)); } break; @@ -1382,21 +1379,22 @@ MTPVector linksToMTP(const EntitiesInText &links, bool sending MTPVector result(MTP_vector(0)); auto &v = result._vector().v; for_const (const auto &link, links) { - if (link.length <= 0) continue; - if (sending && link.type != EntityInTextCode && link.type != EntityInTextPre) continue; + if (link.length() <= 0) continue; + if (sending && link.type() != EntityInTextCode && link.type() != EntityInTextPre) continue; - auto offset = MTP_int(link.offset), length = MTP_int(link.length); - switch (link.type) { + auto offset = MTP_int(link.offset()), length = MTP_int(link.length()); + switch (link.type()) { case EntityInTextUrl: v.push_back(MTP_messageEntityUrl(offset, length)); break; - case EntityInTextCustomUrl: v.push_back(MTP_messageEntityTextUrl(offset, length, MTP_string(link.text))); break; + case EntityInTextCustomUrl: v.push_back(MTP_messageEntityTextUrl(offset, length, MTP_string(link.data()))); break; case EntityInTextEmail: v.push_back(MTP_messageEntityEmail(offset, length)); break; case EntityInTextHashtag: v.push_back(MTP_messageEntityHashtag(offset, length)); break; case EntityInTextMention: v.push_back(MTP_messageEntityMention(offset, length)); break; + case EntityInTextMentionName: v.push_back(MTP_messageEntityMentionName(offset, length, MTP_int(link.data().toInt()))); break; case EntityInTextBotCommand: v.push_back(MTP_messageEntityBotCommand(offset, length)); break; case EntityInTextBold: v.push_back(MTP_messageEntityBold(offset, length)); break; case EntityInTextItalic: v.push_back(MTP_messageEntityItalic(offset, length)); break; case EntityInTextCode: v.push_back(MTP_messageEntityCode(offset, length)); break; - case EntityInTextPre: v.push_back(MTP_messageEntityPre(offset, length, MTP_string(link.text))); break; + case EntityInTextPre: v.push_back(MTP_messageEntityPre(offset, length, MTP_string(link.data()))); break; } } return result; @@ -1701,8 +1699,8 @@ EntitiesInText textParseEntities(QString &text, int32 flags, bool rich) { // som lnkLength = (p - start) - lnkStart; } } - for (; monoEntity < monoCount && mono[monoEntity].offset <= lnkStart; ++monoEntity) { - monoTill = qMax(monoTill, mono[monoEntity].offset + mono[monoEntity].length); + for (; monoEntity < monoCount && mono[monoEntity].offset() <= lnkStart; ++monoEntity) { + monoTill = qMax(monoTill, mono[monoEntity].offset() + mono[monoEntity].length()); result.push_back(mono[monoEntity]); } if (lnkStart >= monoTill) { @@ -1712,7 +1710,7 @@ EntitiesInText textParseEntities(QString &text, int32 flags, bool rich) { // som offset = matchOffset = lnkStart + lnkLength; } for (; monoEntity < monoCount; ++monoEntity) { - monoTill = qMax(monoTill, mono[monoEntity].offset + mono[monoEntity].length); + monoTill = qMax(monoTill, mono[monoEntity].offset() + mono[monoEntity].length()); result.push_back(mono[monoEntity]); } @@ -1728,15 +1726,15 @@ QString textApplyEntities(const QString &text, const EntitiesInText &entities) { QString result; int32 size = text.size(); const QChar *b = text.constData(), *already = b, *e = b + size; - EntitiesInText::const_iterator entity = entities.cbegin(), end = entities.cend(); - while (entity != end && ((entity->type != EntityInTextCode && entity->type != EntityInTextPre) || entity->length <= 0 || entity->offset >= size)) { + auto entity = entities.cbegin(), end = entities.cend(); + while (entity != end && ((entity->type() != EntityInTextCode && entity->type() != EntityInTextPre) || entity->length() <= 0 || entity->offset() >= size)) { ++entity; } while (entity != end || !closingTags.isEmpty()) { - int32 nextOpenEntity = (entity == end) ? (size + 1) : entity->offset; + int32 nextOpenEntity = (entity == end) ? (size + 1) : entity->offset(); int32 nextCloseEntity = closingTags.isEmpty() ? (size + 1) : closingTags.cbegin().key(); if (nextOpenEntity <= nextCloseEntity) { - QString tag = (entity->type == EntityInTextCode) ? code : pre; + QString tag = (entity->type() == EntityInTextCode) ? code : pre; if (result.isEmpty()) result.reserve(text.size() + entities.size() * pre.size() * 2); const QChar *offset = b + nextOpenEntity; @@ -1745,10 +1743,10 @@ QString textApplyEntities(const QString &text, const EntitiesInText &entities) { already = offset; } result.append(tag); - closingTags.insert(qMin(entity->offset + entity->length, size), tag); + closingTags.insert(qMin(entity->offset() + entity->length(), size), tag); ++entity; - while (entity != end && ((entity->type != EntityInTextCode && entity->type != EntityInTextPre) || entity->length <= 0 || entity->offset >= size)) { + while (entity != end && ((entity->type() != EntityInTextCode && entity->type() != EntityInTextPre) || entity->length() <= 0 || entity->offset() >= size)) { ++entity; } } else { @@ -1792,8 +1790,8 @@ void replaceStringWithEntities(const QLatin1String &from, QChar to, QString &res bool skip = false; for (; i != e; ++i) { // find and check next finishing entity - if (i->offset + i->length > nextOffset) { - skip = (i->offset < nextOffset + len); + if (i->offset() + i->length() > nextOffset) { + skip = (i->offset() < nextOffset + len); break; } } @@ -1836,14 +1834,13 @@ void moveStringPart(QChar *start, int32 &to, int32 &from, int32 count, EntitiesI if (to < from) { memmove(start + to, start + from, count * sizeof(QChar)); for (auto &entity : entities) { - if (entity.offset >= from + count) break; - if (entity.offset + entity.length < from) continue; - if (entity.offset >= from) { - entity.offset -= (from - to); - entity.length += (from - to); + if (entity.offset() >= from + count) break; + if (entity.offset() + entity.length() < from) continue; + if (entity.offset() >= from) { + entity.extendToLeft(from - to); } - if (entity.offset + entity.length < from + count) { - entity.length -= (from - to); + if (entity.offset() + entity.length() < from + count) { + entity.shrinkFromRight(from - to); } } } @@ -1870,46 +1867,38 @@ void cleanTextWithEntities(QString &result, EntitiesInText &entities) { } void trimTextWithEntities(QString &result, EntitiesInText &entities) { - bool foundNotTrimmed = false; - for (QChar *s = result.data(), *e = s + result.size(), *ch = e; ch != s;) { // rtrim + bool foundNotTrimmedChar = false; + + // right trim + for (QChar *s = result.data(), *e = s + result.size(), *ch = e; ch != s;) { --ch; if (!chIsTrimmed(*ch)) { if (ch + 1 < e) { int32 l = ch + 1 - s; - for (EntitiesInText::iterator i = entities.begin(), e = entities.end(); i != e; ++i) { - if (i->offset > l) { - i->offset = l; - i->length = 0; - } else if (i->offset + i->length > l) { - i->length = l - i->offset; - } + for (auto &entity : entities) { + entity.updateTextEnd(l); } result.resize(l); } - foundNotTrimmed = true; + foundNotTrimmedChar = true; break; } } - if (!foundNotTrimmed) { + if (!foundNotTrimmedChar) { result.clear(); entities.clear(); return; } - for (QChar *s = result.data(), *ch = s, *e = s + result.size(); ch != e; ++ch) { // ltrim - if (!chIsTrimmed(*ch)) { + int firstMonospaceOffset = EntityInText::firstMonospaceOffset(entities, result.size()); + + // left trim + for (QChar *s = result.data(), *ch = s, *e = s + result.size(); ch != e; ++ch) { + if (!chIsTrimmed(*ch) || (ch - s) == firstMonospaceOffset) { if (ch > s) { int32 l = ch - s; - for (EntitiesInText::iterator i = entities.begin(), e = entities.end(); i != e; ++i) { - if (i->offset + i->length <= l) { - i->length = 0; - i->offset = 0; - } else if (i->offset < l) { - i->length = i->offset + i->length - l; - i->offset = 0; - } else { - i->offset -= l; - } + for (auto &entity : entities) { + entity.shiftLeft(l); } result = result.mid(l); } diff --git a/Telegram/SourceFiles/ui/text/text_entity.h b/Telegram/SourceFiles/ui/text/text_entity.h index aaf9406f6..20caba8cd 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.h +++ b/Telegram/SourceFiles/ui/text/text_entity.h @@ -21,11 +21,14 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #pragma once enum EntityInTextType { + EntityInTextInvalid = 0, + EntityInTextUrl, EntityInTextCustomUrl, EntityInTextEmail, EntityInTextHashtag, EntityInTextMention, + EntityInTextMentionName, EntityInTextBotCommand, EntityInTextBold, @@ -33,12 +36,77 @@ enum EntityInTextType { EntityInTextCode, // inline EntityInTextPre, // block }; -struct EntityInText { - EntityInText(EntityInTextType type, int offset, int length, const QString &text = QString()) : type(type), offset(offset), length(length), text(text) { + +class EntityInText; +using EntitiesInText = QList; + +class EntityInText { +public: + EntityInText(EntityInTextType type, int offset, int length, const QString &data = QString()) + : _type(type) + , _offset(offset) + , _length(length) + , _data(data) { } - EntityInTextType type; - int offset, length; - QString text; + + EntityInTextType type() const { + return _type; + } + int offset() const { + return _offset; + } + int length() const { + return _length; + } + QString data() const { + return _data; + } + + void extendToLeft(int extent) { + _offset -= extent; + _length += extent; + } + void shrinkFromRight(int shrink) { + _length -= shrink; + } + void shiftLeft(int shift) { + _offset -= shift; + if (_offset < 0) { + _length += _offset; + _offset = 0; + if (_length < 0) { + _length = 0; + } + } + } + void updateTextEnd(int textEnd) { + if (_offset > textEnd) { + _offset = textEnd; + _length = 0; + } else if (_offset + _length > textEnd) { + _length = textEnd - _offset; + } + } + + static int firstMonospaceOffset(const EntitiesInText &entities, int textLength) { + int result = textLength; + for_const (auto &entity, entities) { + if (entity.type() == EntityInTextPre || entity.type() == EntityInTextCode) { + accumulate_min(result, entity.offset()); + } + } + return result; + } + + explicit operator bool() const { + return type() != EntityInTextInvalid; + } + +private: + EntityInTextType _type; + int _offset, _length; + QString _data; + }; typedef QList EntitiesInText; From b4bc515079283afbaf6ac180d22110ada6f40306 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 29 Apr 2016 16:46:16 +0300 Subject: [PATCH 02/20] Renamed MentionsDropdown to FieldAutocomplete, moved to separate file. Support for message field mentions without usernames was started. --- Telegram/SourceFiles/dropdown.cpp | 877 ----------------- Telegram/SourceFiles/dropdown.h | 153 --- .../history/field_autocomplete.cpp | 904 ++++++++++++++++++ .../SourceFiles/history/field_autocomplete.h | 195 ++++ Telegram/SourceFiles/historywidget.cpp | 91 +- Telegram/SourceFiles/historywidget.h | 22 +- Telegram/SourceFiles/mainwindow.h | 4 +- Telegram/SourceFiles/pspecific_win.cpp | 1 - Telegram/SourceFiles/pspecific_winrt.cpp | 1 - Telegram/SourceFiles/ui/flattextarea.cpp | 20 +- Telegram/SourceFiles/ui/flattextarea.h | 4 +- Telegram/Telegram.vcxproj | 27 + Telegram/Telegram.vcxproj.filters | 15 + 13 files changed, 1222 insertions(+), 1092 deletions(-) create mode 100644 Telegram/SourceFiles/history/field_autocomplete.cpp create mode 100644 Telegram/SourceFiles/history/field_autocomplete.h diff --git a/Telegram/SourceFiles/dropdown.cpp b/Telegram/SourceFiles/dropdown.cpp index eb5c4fc5f..9c82d9aa7 100644 --- a/Telegram/SourceFiles/dropdown.cpp +++ b/Telegram/SourceFiles/dropdown.cpp @@ -3870,880 +3870,3 @@ void EmojiPan::recountContentMaxHeight() { } updateContentHeight(); } - -MentionsInner::MentionsInner(MentionsDropdown *parent, MentionRows *mrows, HashtagRows *hrows, BotCommandRows *brows, StickerPack *srows) -: _parent(parent) -, _mrows(mrows) -, _hrows(hrows) -, _brows(brows) -, _srows(srows) -, _stickersPerRow(1) -, _recentInlineBotsInRows(0) -, _sel(-1) -, _down(-1) -, _mouseSel(false) -, _overDelete(false) -, _previewShown(false) { - _previewTimer.setSingleShot(true); - connect(&_previewTimer, SIGNAL(timeout()), this, SLOT(onPreview())); -} - -void MentionsInner::paintEvent(QPaintEvent *e) { - Painter p(this); - - QRect r(e->rect()); - if (r != rect()) p.setClipRect(r); - - int32 atwidth = st::mentionFont->width('@'), hashwidth = st::mentionFont->width('#'); - int32 mentionleft = 2 * st::mentionPadding.left() + st::mentionPhotoSize; - int32 mentionwidth = width() - mentionleft - 2 * st::mentionPadding.right(); - int32 htagleft = st::btnAttachPhoto.width + st::taMsgField.textMrg.left() - st::lineWidth, htagwidth = width() - st::mentionPadding.right() - htagleft - st::mentionScroll.width; - - if (!_srows->isEmpty()) { - int32 rows = rowscount(_srows->size(), _stickersPerRow); - int32 fromrow = floorclamp(r.y() - st::stickerPanPadding, st::stickerPanSize.height(), 0, rows); - int32 torow = ceilclamp(r.y() + r.height() - st::stickerPanPadding, st::stickerPanSize.height(), 0, rows); - int32 fromcol = floorclamp(r.x() - st::stickerPanPadding, st::stickerPanSize.width(), 0, _stickersPerRow); - int32 tocol = ceilclamp(r.x() + r.width() - st::stickerPanPadding, st::stickerPanSize.width(), 0, _stickersPerRow); - for (int32 row = fromrow; row < torow; ++row) { - for (int32 col = fromcol; col < tocol; ++col) { - int32 index = row * _stickersPerRow + col; - if (index >= _srows->size()) break; - - DocumentData *sticker = _srows->at(index); - if (!sticker->sticker()) continue; - - QPoint pos(st::stickerPanPadding + col * st::stickerPanSize.width(), st::stickerPanPadding + row * st::stickerPanSize.height()); - if (_sel == index) { - QPoint tl(pos); - if (rtl()) tl.setX(width() - tl.x() - st::stickerPanSize.width()); - App::roundRect(p, QRect(tl, st::stickerPanSize), st::emojiPanHover, StickerHoverCorners); - } - - bool goodThumb = !sticker->thumb->isNull() && ((sticker->thumb->width() >= 128) || (sticker->thumb->height() >= 128)); - if (goodThumb) { - sticker->thumb->load(); - } else { - sticker->checkSticker(); - } - - float64 coef = qMin((st::stickerPanSize.width() - st::msgRadius * 2) / float64(sticker->dimensions.width()), (st::stickerPanSize.height() - st::msgRadius * 2) / float64(sticker->dimensions.height())); - if (coef > 1) coef = 1; - int32 w = qRound(coef * sticker->dimensions.width()), h = qRound(coef * sticker->dimensions.height()); - if (w < 1) w = 1; - if (h < 1) h = 1; - QPoint ppos = pos + QPoint((st::stickerPanSize.width() - w) / 2, (st::stickerPanSize.height() - h) / 2); - if (goodThumb) { - p.drawPixmapLeft(ppos, width(), sticker->thumb->pix(w, h)); - } else if (!sticker->sticker()->img->isNull()) { - p.drawPixmapLeft(ppos, width(), sticker->sticker()->img->pix(w, h)); - } - } - } - } else { - int32 from = qFloor(e->rect().top() / st::mentionHeight), to = qFloor(e->rect().bottom() / st::mentionHeight) + 1; - int32 last = _mrows->isEmpty() ? (_hrows->isEmpty() ? _brows->size() : _hrows->size()) : _mrows->size(); - bool hasUsername = _parent->filter().indexOf('@') > 1; - int filterSize = qMax(_parent->filter().size() - 1, 0); - bool filterIsEmpty = (filterSize == 0); - for (int32 i = from; i < to; ++i) { - if (i >= last) break; - - bool selected = (i == _sel); - if (selected) { - p.fillRect(0, i * st::mentionHeight, width(), st::mentionHeight, st::mentionBgOver->b); - int skip = (st::mentionHeight - st::notifyClose.icon.pxHeight()) / 2; - if (!_hrows->isEmpty() || (!_mrows->isEmpty() && i < _recentInlineBotsInRows)) { - p.drawSprite(QPoint(width() - st::notifyClose.icon.pxWidth() - skip, i * st::mentionHeight + skip), st::notifyClose.icon); - } - } - p.setPen(st::black->p); - if (!_mrows->isEmpty()) { - UserData *user = _mrows->at(i); - QString first = filterIsEmpty ? QString() : ('@' + user->username.mid(0, filterSize)); - QString second = (filterIsEmpty && !user->username.isEmpty()) ? ('@' + user->username) : user->username.mid(filterSize); - int32 firstwidth = st::mentionFont->width(first), secondwidth = st::mentionFont->width(second), unamewidth = firstwidth + secondwidth, namewidth = user->nameText.maxWidth(); - if (mentionwidth < unamewidth + namewidth) { - namewidth = (mentionwidth * namewidth) / (namewidth + unamewidth); - unamewidth = mentionwidth - namewidth; - if (firstwidth < unamewidth + st::mentionFont->elidew) { - if (firstwidth < unamewidth) { - first = st::mentionFont->elided(first, unamewidth); - } else if (!second.isEmpty()) { - first = st::mentionFont->elided(first + second, unamewidth); - second = QString(); - } - } else { - second = st::mentionFont->elided(second, unamewidth - firstwidth); - } - } - user->loadUserpic(); - user->paintUserpicLeft(p, st::mentionPhotoSize, st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), width()); - user->nameText.drawElided(p, 2 * st::mentionPadding.left() + st::mentionPhotoSize, i * st::mentionHeight + st::mentionTop, namewidth); - - p.setFont(st::mentionFont->f); - p.setPen((selected ? st::mentionFgOverActive : st::mentionFgActive)->p); - p.drawText(mentionleft + namewidth + st::mentionPadding.right(), i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first); - if (!second.isEmpty()) { - p.setPen((selected ? st::mentionFgOver : st::mentionFg)->p); - p.drawText(mentionleft + namewidth + st::mentionPadding.right() + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second); - } - } else if (!_hrows->isEmpty()) { - QString hrow = _hrows->at(i); - QString first = filterIsEmpty ? QString() : ('#' + hrow.mid(0, filterSize)); - QString second = filterIsEmpty ? ('#' + hrow) : hrow.mid(filterSize); - int32 firstwidth = st::mentionFont->width(first), secondwidth = st::mentionFont->width(second); - if (htagwidth < firstwidth + secondwidth) { - if (htagwidth < firstwidth + st::mentionFont->elidew) { - first = st::mentionFont->elided(first + second, htagwidth); - second = QString(); - } else { - second = st::mentionFont->elided(second, htagwidth - firstwidth); - } - } - - p.setFont(st::mentionFont->f); - if (!first.isEmpty()) { - p.setPen((selected ? st::mentionFgOverActive : st::mentionFgActive)->p); - p.drawText(htagleft, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first); - } - if (!second.isEmpty()) { - p.setPen((selected ? st::mentionFgOver : st::mentionFg)->p); - p.drawText(htagleft + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second); - } - } else { - UserData *user = _brows->at(i).first; - - const BotCommand *command = _brows->at(i).second; - QString toHighlight = command->command; - int32 botStatus = _parent->chat() ? _parent->chat()->botStatus : ((_parent->channel() && _parent->channel()->isMegagroup()) ? _parent->channel()->mgInfo->botStatus : -1); - if (hasUsername || botStatus == 0 || botStatus == 2) { - toHighlight += '@' + user->username; - } - user->loadUserpic(); - user->paintUserpicLeft(p, st::mentionPhotoSize, st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), width()); - - int32 addleft = 0, widthleft = mentionwidth; - QString first = filterIsEmpty ? QString() : ('/' + toHighlight.mid(0, filterSize)); - QString second = filterIsEmpty ? ('/' + toHighlight) : toHighlight.mid(filterSize); - int32 firstwidth = st::mentionFont->width(first), secondwidth = st::mentionFont->width(second); - if (widthleft < firstwidth + secondwidth) { - if (widthleft < firstwidth + st::mentionFont->elidew) { - first = st::mentionFont->elided(first + second, widthleft); - second = QString(); - } else { - second = st::mentionFont->elided(second, widthleft - firstwidth); - } - } - p.setFont(st::mentionFont->f); - if (!first.isEmpty()) { - p.setPen((selected ? st::mentionFgOverActive : st::mentionFgActive)->p); - p.drawText(mentionleft, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first); - } - if (!second.isEmpty()) { - p.setPen((selected ? st::mentionFgOver : st::mentionFg)->p); - p.drawText(mentionleft + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second); - } - addleft += firstwidth + secondwidth + st::mentionPadding.left(); - widthleft -= firstwidth + secondwidth + st::mentionPadding.left(); - if (widthleft > st::mentionFont->elidew && !command->descriptionText().isEmpty()) { - p.setPen((selected ? st::mentionFgOver : st::mentionFg)->p); - command->descriptionText().drawElided(p, mentionleft + addleft, i * st::mentionHeight + st::mentionTop, widthleft, 1, style::al_right); - } - } - } - p.fillRect(Adaptive::OneColumn() ? 0 : st::lineWidth, _parent->innerBottom() - st::lineWidth, width() - (Adaptive::OneColumn() ? 0 : st::lineWidth), st::lineWidth, st::shadowColor->b); - } - p.fillRect(Adaptive::OneColumn() ? 0 : st::lineWidth, _parent->innerTop(), width() - (Adaptive::OneColumn() ? 0 : st::lineWidth), st::lineWidth, st::shadowColor->b); -} - -void MentionsInner::resizeEvent(QResizeEvent *e) { - _stickersPerRow = qMax(1, int32(width() - 2 * st::stickerPanPadding) / int32(st::stickerPanSize.width())); -} - -void MentionsInner::mouseMoveEvent(QMouseEvent *e) { - _mousePos = mapToGlobal(e->pos()); - _mouseSel = true; - onUpdateSelected(true); -} - -void MentionsInner::clearSel(bool hidden) { - _mouseSel = _overDelete = false; - setSel((_mrows->isEmpty() && _brows->isEmpty() && _hrows->isEmpty()) ? -1 : 0); - if (hidden) { - _down = -1; - _previewShown = false; - } -} - -bool MentionsInner::moveSel(int key) { - _mouseSel = false; - int32 maxSel = (_mrows->isEmpty() ? (_hrows->isEmpty() ? (_brows->isEmpty() ? _srows->size() : _brows->size()) : _hrows->size()) : _mrows->size()); - int32 direction = (key == Qt::Key_Up) ? -1 : (key == Qt::Key_Down ? 1 : 0); - if (!_srows->isEmpty()) { - if (key == Qt::Key_Left) { - direction = -1; - } else if (key == Qt::Key_Right) { - direction = 1; - } else { - direction *= _stickersPerRow; - } - } - if (_sel >= maxSel || _sel < 0) { - if (direction < -1) { - setSel(((maxSel - 1) / _stickersPerRow) * _stickersPerRow, true); - } else if (direction < 0) { - setSel(maxSel - 1, true); - } else { - setSel(0, true); - } - return (_sel >= 0 && _sel < maxSel); - } - setSel((_sel + direction >= maxSel || _sel + direction < 0) ? -1 : (_sel + direction), true); - return true; -} - -bool MentionsInner::select() { - if (!_srows->isEmpty()) { - if (_sel >= 0 && _sel < _srows->size()) { - emit selected(_srows->at(_sel)); - return true; - } - } else { - QString sel = getSelected(); - if (!sel.isEmpty()) { - emit chosen(sel); - return true; - } - } - return false; -} - -void MentionsInner::setRecentInlineBotsInRows(int32 bots) { - _recentInlineBotsInRows = bots; -} - -QString MentionsInner::getSelected() const { - int32 maxSel = (_mrows->isEmpty() ? (_hrows->isEmpty() ? _brows->size() : _hrows->size()) : _mrows->size()); - if (_sel >= 0 && _sel < maxSel) { - QString result; - if (!_mrows->isEmpty()) { - result = '@' + _mrows->at(_sel)->username; - } else if (!_hrows->isEmpty()) { - result = '#' + _hrows->at(_sel); - } else { - UserData *user = _brows->at(_sel).first; - const BotCommand *command(_brows->at(_sel).second); - int32 botStatus = _parent->chat() ? _parent->chat()->botStatus : ((_parent->channel() && _parent->channel()->isMegagroup()) ? _parent->channel()->mgInfo->botStatus : -1); - if (botStatus == 0 || botStatus == 2 || _parent->filter().indexOf('@') > 1) { - result = '/' + command->command + '@' + user->username; - } else { - result = '/' + command->command; - } - } - return result; - } - return QString(); -} - -void MentionsInner::mousePressEvent(QMouseEvent *e) { - _mousePos = mapToGlobal(e->pos()); - _mouseSel = true; - onUpdateSelected(true); - if (e->button() == Qt::LeftButton) { - if (_overDelete && _sel >= 0 && _sel < (_mrows->isEmpty() ? _hrows->size() : _recentInlineBotsInRows)) { - _mousePos = mapToGlobal(e->pos()); - bool removed = false; - if (_mrows->isEmpty()) { - QString toRemove = _hrows->at(_sel); - RecentHashtagPack &recent(cRefRecentWriteHashtags()); - for (RecentHashtagPack::iterator i = recent.begin(); i != recent.cend();) { - if (i->first == toRemove) { - i = recent.erase(i); - removed = true; - } else { - ++i; - } - } - } else { - UserData *toRemove = _mrows->at(_sel); - RecentInlineBots &recent(cRefRecentInlineBots()); - int32 index = recent.indexOf(toRemove); - if (index >= 0) { - recent.remove(index); - removed = true; - } - } - if (removed) { - Local::writeRecentHashtagsAndBots(); - } - _parent->updateFiltered(); - - _mouseSel = true; - onUpdateSelected(true); - } else if (_srows->isEmpty()) { - select(); - } else { - _down = _sel; - _previewTimer.start(QApplication::startDragTime()); - } - } -} - -void MentionsInner::mouseReleaseEvent(QMouseEvent *e) { - _previewTimer.stop(); - - int32 pressed = _down; - _down = -1; - - _mousePos = mapToGlobal(e->pos()); - _mouseSel = true; - onUpdateSelected(true); - - if (_previewShown) { - _previewShown = false; - return; - } - - if (_sel < 0 || _sel != pressed || _srows->isEmpty()) return; - - select(); -} - -void MentionsInner::enterEvent(QEvent *e) { - setMouseTracking(true); - _mousePos = QCursor::pos(); - onUpdateSelected(true); -} - -void MentionsInner::leaveEvent(QEvent *e) { - setMouseTracking(false); - if (_sel >= 0) { - setSel(-1); - } -} - -void MentionsInner::updateSelectedRow() { - if (_sel >= 0) { - if (_srows->isEmpty()) { - update(0, _sel * st::mentionHeight, width(), st::mentionHeight); - } else { - int32 row = _sel / _stickersPerRow, col = _sel % _stickersPerRow; - update(st::stickerPanPadding + col * st::stickerPanSize.width(), st::stickerPanPadding + row * st::stickerPanSize.height(), st::stickerPanSize.width(), st::stickerPanSize.height()); - } - } -} - -void MentionsInner::setSel(int sel, bool scroll) { - updateSelectedRow(); - _sel = sel; - updateSelectedRow(); - - if (scroll && _sel >= 0) { - if (_srows->isEmpty()) { - emit mustScrollTo(_sel * st::mentionHeight, (_sel + 1) * st::mentionHeight); - } else { - int32 row = _sel / _stickersPerRow; - emit mustScrollTo(st::stickerPanPadding + row * st::stickerPanSize.height(), st::stickerPanPadding + (row + 1) * st::stickerPanSize.height()); - } - } -} - -void MentionsInner::onUpdateSelected(bool force) { - QPoint mouse(mapFromGlobal(_mousePos)); - if ((!force && !rect().contains(mouse)) || !_mouseSel) return; - - if (_down >= 0 && !_previewShown) return; - - int32 sel = -1, maxSel = 0; - if (!_srows->isEmpty()) { - int32 rows = rowscount(_srows->size(), _stickersPerRow); - int32 row = (mouse.y() >= st::stickerPanPadding) ? ((mouse.y() - st::stickerPanPadding) / st::stickerPanSize.height()) : -1; - int32 col = (mouse.x() >= st::stickerPanPadding) ? ((mouse.x() - st::stickerPanPadding) / st::stickerPanSize.width()) : -1; - if (row >= 0 && col >= 0) { - sel = row * _stickersPerRow + col; - } - maxSel = _srows->size(); - _overDelete = false; - } else { - sel = mouse.y() / int32(st::mentionHeight); - maxSel = _mrows->isEmpty() ? (_hrows->isEmpty() ? _brows->size() : _hrows->size()) : _mrows->size(); - _overDelete = (!_hrows->isEmpty() || (!_mrows->isEmpty() && sel < _recentInlineBotsInRows)) ? (mouse.x() >= width() - st::mentionHeight) : false; - } - if (sel < 0 || sel >= maxSel) { - sel = -1; - } - if (sel != _sel) { - setSel(sel); - if (_down >= 0 && _sel >= 0 && _down != _sel) { - _down = _sel; - if (_down >= 0 && _down < _srows->size()) { - Ui::showMediaPreview(_srows->at(_down)); - } - } - } -} - -void MentionsInner::onParentGeometryChanged() { - _mousePos = QCursor::pos(); - if (rect().contains(mapFromGlobal(_mousePos))) { - setMouseTracking(true); - onUpdateSelected(true); - } -} - -void MentionsInner::onPreview() { - if (_down >= 0 && _down < _srows->size()) { - Ui::showMediaPreview(_srows->at(_down)); - _previewShown = true; - } -} - -MentionsDropdown::MentionsDropdown(QWidget *parent) : TWidget(parent) -, _scroll(this, st::mentionScroll) -, _inner(this, &_mrows, &_hrows, &_brows, &_srows) -, _chat(0) -, _user(0) -, _channel(0) -, _hiding(false) -, a_opacity(0) -, _a_appearance(animation(this, &MentionsDropdown::step_appearance)) -, _shadow(st::dropdownDef.shadow) { - _hideTimer.setSingleShot(true); - connect(&_hideTimer, SIGNAL(timeout()), this, SLOT(hideStart())); - connect(&_inner, SIGNAL(chosen(QString)), this, SIGNAL(chosen(QString))); - connect(&_inner, SIGNAL(selected(DocumentData*)), this, SIGNAL(stickerSelected(DocumentData*))); - connect(&_inner, SIGNAL(mustScrollTo(int,int)), &_scroll, SLOT(scrollToY(int,int))); - - connect(App::wnd(), SIGNAL(imageLoaded()), &_inner, SLOT(update())); - - setFocusPolicy(Qt::NoFocus); - _scroll.setFocusPolicy(Qt::NoFocus); - _scroll.viewport()->setFocusPolicy(Qt::NoFocus); - - _inner.setGeometry(rect()); - _scroll.setGeometry(rect()); - - _scroll.setWidget(&_inner); - _scroll.show(); - _inner.show(); - - connect(&_scroll, SIGNAL(geometryChanged()), &_inner, SLOT(onParentGeometryChanged())); - connect(&_scroll, SIGNAL(scrolled()), &_inner, SLOT(onUpdateSelected())); -} - -void MentionsDropdown::paintEvent(QPaintEvent *e) { - Painter p(this); - - if (_a_appearance.animating()) { - p.setOpacity(a_opacity.current()); - p.drawPixmap(0, 0, _cache); - return; - } - - p.fillRect(rect(), st::white); -} - -void MentionsDropdown::showFiltered(PeerData *peer, QString query, bool start) { - _chat = peer->asChat(); - _user = peer->asUser(); - _channel = peer->asChannel(); - if (query.isEmpty()) { - rowsUpdated(MentionRows(), HashtagRows(), BotCommandRows(), _srows, false); - return; - } - - _emoji = EmojiPtr(); - - query = query.toLower(); - bool resetScroll = (_filter != query); - if (resetScroll) { - _filter = query; - } - _addInlineBots = start; - - updateFiltered(resetScroll); -} - -void MentionsDropdown::showStickers(EmojiPtr emoji) { - bool resetScroll = (_emoji != emoji); - _emoji = emoji; - if (!emoji) { - rowsUpdated(_mrows, _hrows, _brows, StickerPack(), false); - return; - } - - _chat = 0; - _user = 0; - _channel = 0; - - updateFiltered(resetScroll); -} - -bool MentionsDropdown::clearFilteredBotCommands() { - if (_brows.isEmpty()) return false; - _brows.clear(); - return true; -} - -namespace { - template - inline int indexOfInFirstN(const T &v, const U &elem, int last) { - for (auto b = v.cbegin(), i = b, e = b + qMax(v.size(), last); i != e; ++i) { - if (*i == elem) { - return (i - b); - } - } - return -1; - } -} - -void MentionsDropdown::updateFiltered(bool resetScroll) { - int32 now = unixtime(), recentInlineBots = 0; - MentionRows mrows; - HashtagRows hrows; - BotCommandRows brows; - StickerPack srows; - if (_emoji) { - QMap setsToRequest; - Stickers::Sets &sets(Global::RefStickerSets()); - const Stickers::Order &order(Global::StickerSetsOrder()); - for (int i = 0, l = order.size(); i < l; ++i) { - auto it = sets.find(order.at(i)); - if (it != sets.cend()) { - if (it->emoji.isEmpty()) { - setsToRequest.insert(it->id, it->access); - it->flags |= MTPDstickerSet_ClientFlag::f_not_loaded; - } else if (!(it->flags & MTPDstickerSet::Flag::f_disabled)) { - StickersByEmojiMap::const_iterator i = it->emoji.constFind(emojiGetNoColor(_emoji)); - if (i != it->emoji.cend()) { - srows += *i; - } - } - } - } - if (!setsToRequest.isEmpty() && App::api()) { - for (QMap::const_iterator i = setsToRequest.cbegin(), e = setsToRequest.cend(); i != e; ++i) { - App::api()->scheduleStickerSetRequest(i.key(), i.value()); - } - App::api()->requestStickerSets(); - } - } else if (_filter.at(0) == '@') { - bool listAllSuggestions = (_filter.size() < 2); - if (_chat) { - mrows.reserve((_addInlineBots ? cRecentInlineBots().size() : 0) + (_chat->participants.isEmpty() ? _chat->lastAuthors.size() : _chat->participants.size())); - } else if (_channel && _channel->isMegagroup()) { - if (_channel->mgInfo->lastParticipants.isEmpty() || _channel->lastParticipantsCountOutdated()) { - } else { - mrows.reserve((_addInlineBots ? cRecentInlineBots().size() : 0) + _channel->mgInfo->lastParticipants.size()); - } - } else if (_addInlineBots) { - mrows.reserve(cRecentInlineBots().size()); - } - if (_addInlineBots) { - for (RecentInlineBots::const_iterator i = cRecentInlineBots().cbegin(), e = cRecentInlineBots().cend(); i != e; ++i) { - UserData *user = *i; - if (user->username.isEmpty()) continue; - if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; - mrows.push_back(user); - ++recentInlineBots; - } - } - if (_chat) { - QMultiMap ordered; - mrows.reserve(mrows.size() + (_chat->participants.isEmpty() ? _chat->lastAuthors.size() : _chat->participants.size())); - if (_chat->noParticipantInfo()) { - if (App::api()) App::api()->requestFullPeer(_chat); - } else if (!_chat->participants.isEmpty()) { - for (ChatData::Participants::const_iterator i = _chat->participants.cbegin(), e = _chat->participants.cend(); i != e; ++i) { - UserData *user = i.key(); - if (user->username.isEmpty()) continue; - if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; - if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; - ordered.insertMulti(App::onlineForSort(user, now), user); - } - } - for (MentionRows::const_iterator i = _chat->lastAuthors.cbegin(), e = _chat->lastAuthors.cend(); i != e; ++i) { - UserData *user = *i; - if (user->username.isEmpty()) continue; - if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; - if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; - mrows.push_back(user); - if (!ordered.isEmpty()) { - ordered.remove(App::onlineForSort(user, now), user); - } - } - if (!ordered.isEmpty()) { - for (QMultiMap::const_iterator i = ordered.cend(), b = ordered.cbegin(); i != b;) { - --i; - mrows.push_back(i.value()); - } - } - } else if (_channel && _channel->isMegagroup()) { - QMultiMap ordered; - if (_channel->mgInfo->lastParticipants.isEmpty() || _channel->lastParticipantsCountOutdated()) { - if (App::api()) App::api()->requestLastParticipants(_channel); - } else { - mrows.reserve(mrows.size() + _channel->mgInfo->lastParticipants.size()); - for (MegagroupInfo::LastParticipants::const_iterator i = _channel->mgInfo->lastParticipants.cbegin(), e = _channel->mgInfo->lastParticipants.cend(); i != e; ++i) { - UserData *user = *i; - if (user->username.isEmpty()) continue; - if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; - if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; - mrows.push_back(user); - } - } - } - } else if (_filter.at(0) == '#') { - const RecentHashtagPack &recent(cRecentWriteHashtags()); - hrows.reserve(recent.size()); - for (RecentHashtagPack::const_iterator i = recent.cbegin(), e = recent.cend(); i != e; ++i) { - if (_filter.size() > 1 && (!i->first.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || i->first.size() + 1 == _filter.size())) continue; - hrows.push_back(i->first); - } - } else if (_filter.at(0) == '/') { - bool hasUsername = _filter.indexOf('@') > 1; - QMap bots; - int32 cnt = 0; - if (_chat) { - if (_chat->noParticipantInfo()) { - if (App::api()) App::api()->requestFullPeer(_chat); - } else if (!_chat->participants.isEmpty()) { - for (ChatData::Participants::const_iterator i = _chat->participants.cbegin(), e = _chat->participants.cend(); i != e; ++i) { - UserData *user = i.key(); - if (!user->botInfo) continue; - if (!user->botInfo->inited && App::api()) App::api()->requestFullPeer(user); - if (user->botInfo->commands.isEmpty()) continue; - bots.insert(user, true); - cnt += user->botInfo->commands.size(); - } - } - } else if (_user && _user->botInfo) { - if (!_user->botInfo->inited && App::api()) App::api()->requestFullPeer(_user); - cnt = _user->botInfo->commands.size(); - bots.insert(_user, true); - } else if (_channel && _channel->isMegagroup()) { - if (_channel->mgInfo->bots.isEmpty()) { - if (!_channel->mgInfo->botStatus && App::api()) App::api()->requestBots(_channel); - } else { - for_const (auto user, _channel->mgInfo->bots) { - if (!user->botInfo) continue; - if (!user->botInfo->inited && App::api()) App::api()->requestFullPeer(user); - if (user->botInfo->commands.isEmpty()) continue; - bots.insert(user, true); - cnt += user->botInfo->commands.size(); - } - } - } - if (cnt) { - brows.reserve(cnt); - int32 botStatus = _chat ? _chat->botStatus : ((_channel && _channel->isMegagroup()) ? _channel->mgInfo->botStatus : -1); - if (_chat) { - for (MentionRows::const_iterator i = _chat->lastAuthors.cbegin(), e = _chat->lastAuthors.cend(); i != e; ++i) { - UserData *user = *i; - if (!user->botInfo) continue; - if (!bots.contains(user)) continue; - if (!user->botInfo->inited && App::api()) App::api()->requestFullPeer(user); - if (user->botInfo->commands.isEmpty()) continue; - bots.remove(user); - for (int32 j = 0, l = user->botInfo->commands.size(); j < l; ++j) { - if (_filter.size() > 1) { - QString toFilter = (hasUsername || botStatus == 0 || botStatus == 2) ? user->botInfo->commands.at(j).command + '@' + user->username : user->botInfo->commands.at(j).command; - if (!toFilter.startsWith(_filter.midRef(1), Qt::CaseInsensitive)/* || toFilter.size() + 1 == _filter.size()*/) continue; - } - brows.push_back(qMakePair(user, &user->botInfo->commands.at(j))); - } - } - } - if (!bots.isEmpty()) { - for (QMap::const_iterator i = bots.cbegin(), e = bots.cend(); i != e; ++i) { - UserData *user = i.key(); - for (int32 j = 0, l = user->botInfo->commands.size(); j < l; ++j) { - if (_filter.size() > 1) { - QString toFilter = (hasUsername || botStatus == 0 || botStatus == 2) ? user->botInfo->commands.at(j).command + '@' + user->username : user->botInfo->commands.at(j).command; - if (!toFilter.startsWith(_filter.midRef(1), Qt::CaseInsensitive)/* || toFilter.size() + 1 == _filter.size()*/) continue; - } - brows.push_back(qMakePair(user, &user->botInfo->commands.at(j))); - } - } - } - } - } - rowsUpdated(mrows, hrows, brows, srows, resetScroll); - _inner.setRecentInlineBotsInRows(recentInlineBots); -} - -void MentionsDropdown::rowsUpdated(const MentionRows &mrows, const HashtagRows &hrows, const BotCommandRows &brows, const StickerPack &srows, bool resetScroll) { - if (mrows.isEmpty() && hrows.isEmpty() && brows.isEmpty() && srows.isEmpty()) { - if (!isHidden()) { - hideStart(); - } - _mrows.clear(); - _hrows.clear(); - _brows.clear(); - _srows.clear(); - } else { - _mrows = mrows; - _hrows = hrows; - _brows = brows; - _srows = srows; - - bool hidden = _hiding || isHidden(); - if (hidden) { - show(); - _scroll.show(); - } - recount(resetScroll); - update(); - if (hidden) { - hide(); - showStart(); - } - } -} - -void MentionsDropdown::setBoundings(QRect boundings) { - _boundings = boundings; - recount(); -} - -void MentionsDropdown::recount(bool resetScroll) { - int32 h = 0, oldst = _scroll.scrollTop(), st = oldst, maxh = 4.5 * st::mentionHeight; - if (!_srows.isEmpty()) { - int32 stickersPerRow = qMax(1, int32(_boundings.width() - 2 * st::stickerPanPadding) / int32(st::stickerPanSize.width())); - int32 rows = rowscount(_srows.size(), stickersPerRow); - h = st::stickerPanPadding + rows * st::stickerPanSize.height(); - } else if (!_mrows.isEmpty()) { - h = _mrows.size() * st::mentionHeight; - } else if (!_hrows.isEmpty()) { - h = _hrows.size() * st::mentionHeight; - } else if (!_brows.isEmpty()) { - h = _brows.size() * st::mentionHeight; - } - - if (_inner.width() != _boundings.width() || _inner.height() != h) { - _inner.resize(_boundings.width(), h); - } - if (h > _boundings.height()) h = _boundings.height(); - if (h > maxh) h = maxh; - if (width() != _boundings.width() || height() != h) { - setGeometry(_boundings.x(), _boundings.y() + _boundings.height() - h, _boundings.width(), h); - _scroll.resize(_boundings.width(), h); - } else if (y() != _boundings.y() + _boundings.height() - h) { - move(_boundings.x(), _boundings.y() + _boundings.height() - h); - } - if (resetScroll) st = 0; - if (st != oldst) _scroll.scrollToY(st); - if (resetScroll) _inner.clearSel(); -} - -void MentionsDropdown::fastHide() { - if (_a_appearance.animating()) { - _a_appearance.stop(); - } - a_opacity = anim::fvalue(0, 0); - _hideTimer.stop(); - hideFinish(); -} - -void MentionsDropdown::hideStart() { - if (!_hiding) { - if (_cache.isNull()) { - _scroll.show(); - _cache = myGrab(this); - } - _scroll.hide(); - _hiding = true; - a_opacity.start(0); - setAttribute(Qt::WA_OpaquePaintEvent, false); - _a_appearance.start(); - } -} - -void MentionsDropdown::hideFinish() { - hide(); - _hiding = false; - _filter = qsl("-"); - _inner.clearSel(true); -} - -void MentionsDropdown::showStart() { - if (!isHidden() && a_opacity.current() == 1 && !_hiding) { - return; - } - if (_cache.isNull()) { - _scroll.show(); - _cache = myGrab(this); - } - _scroll.hide(); - _hiding = false; - show(); - a_opacity.start(1); - setAttribute(Qt::WA_OpaquePaintEvent, false); - _a_appearance.start(); -} - -void MentionsDropdown::step_appearance(float64 ms, bool timer) { - float64 dt = ms / st::dropdownDef.duration; - if (dt >= 1) { - _a_appearance.stop(); - a_opacity.finish(); - _cache = QPixmap(); - setAttribute(Qt::WA_OpaquePaintEvent); - if (_hiding) { - hideFinish(); - } else { - _scroll.show(); - _inner.clearSel(); - } - } else { - a_opacity.update(dt, anim::linear); - } - if (timer) update(); -} - -const QString &MentionsDropdown::filter() const { - return _filter; -} - -ChatData *MentionsDropdown::chat() const { - return _chat; -} - -ChannelData *MentionsDropdown::channel() const { - return _channel; -} - -UserData *MentionsDropdown::user() const { - return _user; -} - -int32 MentionsDropdown::innerTop() { - return _scroll.scrollTop(); -} - -int32 MentionsDropdown::innerBottom() { - return _scroll.scrollTop() + _scroll.height(); -} - -QString MentionsDropdown::getSelected() const { - return _inner.getSelected(); -} - -bool MentionsDropdown::eventFilter(QObject *obj, QEvent *e) { - if (isHidden()) return QWidget::eventFilter(obj, e); - if (e->type() == QEvent::KeyPress) { - QKeyEvent *ev = static_cast(e); - if (!(ev->modifiers() & (Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier | Qt::MetaModifier))) { - if (ev->key() == Qt::Key_Up || ev->key() == Qt::Key_Down || (!_srows.isEmpty() && (ev->key() == Qt::Key_Left || ev->key() == Qt::Key_Right))) { - return _inner.moveSel(ev->key()); - } else if (ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return) { - return _inner.select(); - } - } - } - return QWidget::eventFilter(obj, e); -} - -MentionsDropdown::~MentionsDropdown() { -} diff --git a/Telegram/SourceFiles/dropdown.h b/Telegram/SourceFiles/dropdown.h index ed7fce885..ae7a33159 100644 --- a/Telegram/SourceFiles/dropdown.h +++ b/Telegram/SourceFiles/dropdown.h @@ -740,156 +740,3 @@ private: bool inlineResultsFail(const RPCError &error); }; - -typedef QList MentionRows; -typedef QList HashtagRows; -typedef QList > BotCommandRows; - -class MentionsDropdown; -class MentionsInner : public TWidget { - Q_OBJECT - -public: - - MentionsInner(MentionsDropdown *parent, MentionRows *mrows, HashtagRows *hrows, BotCommandRows *brows, StickerPack *srows); - - void paintEvent(QPaintEvent *e); - void resizeEvent(QResizeEvent *e); - - void enterEvent(QEvent *e); - void leaveEvent(QEvent *e); - - void mousePressEvent(QMouseEvent *e); - void mouseMoveEvent(QMouseEvent *e); - void mouseReleaseEvent(QMouseEvent *e); - - void clearSel(bool hidden = false); - bool moveSel(int key); - bool select(); - - void setRecentInlineBotsInRows(int32 bots); - - QString getSelected() const; - -signals: - - void chosen(QString mentionOrHashtag); - void selected(DocumentData *sticker); - void mustScrollTo(int scrollToTop, int scrollToBottom); - -public slots: - - void onParentGeometryChanged(); - void onUpdateSelected(bool force = false); - void onPreview(); - -private: - - void updateSelectedRow(); - void setSel(int sel, bool scroll = false); - - MentionsDropdown *_parent; - MentionRows *_mrows; - HashtagRows *_hrows; - BotCommandRows *_brows; - StickerPack *_srows; - int32 _stickersPerRow, _recentInlineBotsInRows; - int32 _sel, _down; - bool _mouseSel; - QPoint _mousePos; - - bool _overDelete; - - bool _previewShown; - - QTimer _previewTimer; -}; - -class MentionsDropdown : public TWidget { - Q_OBJECT - -public: - - MentionsDropdown(QWidget *parent); - - void paintEvent(QPaintEvent *e); - - void fastHide(); - - bool clearFilteredBotCommands(); - void showFiltered(PeerData *peer, QString query, bool start); - void showStickers(EmojiPtr emoji); - void updateFiltered(bool resetScroll = false); - void setBoundings(QRect boundings); - - void step_appearance(float64 ms, bool timer); - - const QString &filter() const; - ChatData *chat() const; - ChannelData *channel() const; - UserData *user() const; - - int32 innerTop(); - int32 innerBottom(); - - bool eventFilter(QObject *obj, QEvent *e); - QString getSelected() const; - - bool stickersShown() const { - return !_srows.isEmpty(); - } - - bool overlaps(const QRect &globalRect) { - if (isHidden() || !testAttribute(Qt::WA_OpaquePaintEvent)) return false; - - return rect().contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size())); - } - - ~MentionsDropdown(); - -signals: - - void chosen(QString mentionOrHashtag); - void stickerSelected(DocumentData *sticker); - -public slots: - - void hideStart(); - void hideFinish(); - - void showStart(); - -private: - - void recount(bool resetScroll = false); - - QPixmap _cache; - MentionRows _mrows; - HashtagRows _hrows; - BotCommandRows _brows; - StickerPack _srows; - - void rowsUpdated(const MentionRows &mrows, const HashtagRows &hrows, const BotCommandRows &brows, const StickerPack &srows, bool resetScroll); - - ScrollArea _scroll; - MentionsInner _inner; - - ChatData *_chat; - UserData *_user; - ChannelData *_channel; - EmojiPtr _emoji; - QString _filter; - QRect _boundings; - bool _addInlineBots; - - int32 _width, _height; - bool _hiding; - - anim::fvalue a_opacity; - Animation _a_appearance; - - QTimer _hideTimer; - - BoxShadow _shadow; - -}; diff --git a/Telegram/SourceFiles/history/field_autocomplete.cpp b/Telegram/SourceFiles/history/field_autocomplete.cpp new file mode 100644 index 000000000..f14509ac2 --- /dev/null +++ b/Telegram/SourceFiles/history/field_autocomplete.cpp @@ -0,0 +1,904 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org +*/ +#include "stdafx.h" +#include "history/field_autocomplete.h" + +#include "mainwindow.h" +#include "apiwrap.h" +#include "localstorage.h" + +FieldAutocomplete::FieldAutocomplete(QWidget *parent) : TWidget(parent) +, _scroll(this, st::mentionScroll) +, _inner(this, &_mrows, &_hrows, &_brows, &_srows) +, _chat(0) +, _user(0) +, _channel(0) +, _hiding(false) +, a_opacity(0) +, _a_appearance(animation(this, &FieldAutocomplete::step_appearance)) +, _shadow(st::dropdownDef.shadow) { + _hideTimer.setSingleShot(true); + connect(&_hideTimer, SIGNAL(timeout()), this, SLOT(hideStart())); + + connect(_inner, SIGNAL(mentionChosen(UserData*,FieldAutocomplete::ChooseMethod)), this, SIGNAL(mentionChosen(UserData*,FieldAutocomplete::ChooseMethod))); + connect(_inner, SIGNAL(hashtagChosen(QString,FieldAutocomplete::ChooseMethod)), this, SIGNAL(hashtagChosen(QString,FieldAutocomplete::ChooseMethod))); + connect(_inner, SIGNAL(botCommandChosen(QString,FieldAutocomplete::ChooseMethod)), this, SIGNAL(botCommandChosen(QString,FieldAutocomplete::ChooseMethod))); + connect(_inner, SIGNAL(stickerChosen(DocumentData*,FieldAutocomplete::ChooseMethod)), this, SIGNAL(stickerChosen(DocumentData*,FieldAutocomplete::ChooseMethod))); + connect(_inner, SIGNAL(mustScrollTo(int, int)), _scroll, SLOT(scrollToY(int, int))); + + connect(App::wnd(), SIGNAL(imageLoaded()), _inner, SLOT(update())); + + setFocusPolicy(Qt::NoFocus); + _scroll->setFocusPolicy(Qt::NoFocus); + _scroll->viewport()->setFocusPolicy(Qt::NoFocus); + + _inner->setGeometry(rect()); + _scroll->setGeometry(rect()); + + _scroll->setWidget(_inner); + _scroll->show(); + _inner->show(); + + connect(_scroll, SIGNAL(geometryChanged()), _inner, SLOT(onParentGeometryChanged())); + connect(_scroll, SIGNAL(scrolled()), _inner, SLOT(onUpdateSelected())); +} + +void FieldAutocomplete::paintEvent(QPaintEvent *e) { + Painter p(this); + + if (_a_appearance.animating()) { + p.setOpacity(a_opacity.current()); + p.drawPixmap(0, 0, _cache); + return; + } + + p.fillRect(rect(), st::white); +} + +void FieldAutocomplete::showFiltered(PeerData *peer, QString query, bool start) { +// _inner->showFiltered(peer, query, start); + _chat = peer->asChat(); + _user = peer->asUser(); + _channel = peer->asChannel(); + if (query.isEmpty()) { + rowsUpdated(internal::MentionRows(), internal::HashtagRows(), internal::BotCommandRows(), _srows, false); + return; + } + + _emoji = EmojiPtr(); + + query = query.toLower(); + bool resetScroll = (_filter != query); + if (resetScroll) { + _filter = query; + } + _addInlineBots = start; + + updateFiltered(resetScroll); +} + +void FieldAutocomplete::showStickers(EmojiPtr emoji) { + bool resetScroll = (_emoji != emoji); + _emoji = emoji; + if (!emoji) { + rowsUpdated(_mrows, _hrows, _brows, StickerPack(), false); + return; + } + + _chat = 0; + _user = 0; + _channel = 0; + + updateFiltered(resetScroll); +} + +bool FieldAutocomplete::clearFilteredBotCommands() { + if (_brows.isEmpty()) return false; + _brows.clear(); + return true; +} + +namespace { +template +inline int indexOfInFirstN(const T &v, const U &elem, int last) { + for (auto b = v.cbegin(), i = b, e = b + qMax(v.size(), last); i != e; ++i) { + if (*i == elem) { + return (i - b); + } + } + return -1; +} +} + +void FieldAutocomplete::updateFiltered(bool resetScroll) { + int32 now = unixtime(), recentInlineBots = 0; + internal::MentionRows mrows; + internal::HashtagRows hrows; + internal::BotCommandRows brows; + StickerPack srows; + if (_emoji) { + QMap setsToRequest; + Stickers::Sets &sets(Global::RefStickerSets()); + const Stickers::Order &order(Global::StickerSetsOrder()); + for (int i = 0, l = order.size(); i < l; ++i) { + auto it = sets.find(order.at(i)); + if (it != sets.cend()) { + if (it->emoji.isEmpty()) { + setsToRequest.insert(it->id, it->access); + it->flags |= MTPDstickerSet_ClientFlag::f_not_loaded; + } else if (!(it->flags & MTPDstickerSet::Flag::f_disabled)) { + StickersByEmojiMap::const_iterator i = it->emoji.constFind(emojiGetNoColor(_emoji)); + if (i != it->emoji.cend()) { + srows += *i; + } + } + } + } + if (!setsToRequest.isEmpty() && App::api()) { + for (QMap::const_iterator i = setsToRequest.cbegin(), e = setsToRequest.cend(); i != e; ++i) { + App::api()->scheduleStickerSetRequest(i.key(), i.value()); + } + App::api()->requestStickerSets(); + } + } else if (_filter.at(0) == '@') { + bool listAllSuggestions = (_filter.size() < 2); + if (_chat) { + mrows.reserve((_addInlineBots ? cRecentInlineBots().size() : 0) + (_chat->participants.isEmpty() ? _chat->lastAuthors.size() : _chat->participants.size())); + } else if (_channel && _channel->isMegagroup()) { + if (_channel->mgInfo->lastParticipants.isEmpty() || _channel->lastParticipantsCountOutdated()) { + } else { + mrows.reserve((_addInlineBots ? cRecentInlineBots().size() : 0) + _channel->mgInfo->lastParticipants.size()); + } + } else if (_addInlineBots) { + mrows.reserve(cRecentInlineBots().size()); + } + if (_addInlineBots) { + for (auto i = cRecentInlineBots().cbegin(), e = cRecentInlineBots().cend(); i != e; ++i) { + UserData *user = *i; + if (user->username.isEmpty()) continue; + if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + mrows.push_back(user); + ++recentInlineBots; + } + } + if (_chat) { + QMultiMap ordered; + mrows.reserve(mrows.size() + (_chat->participants.isEmpty() ? _chat->lastAuthors.size() : _chat->participants.size())); + if (_chat->noParticipantInfo()) { + if (App::api()) App::api()->requestFullPeer(_chat); + } else if (!_chat->participants.isEmpty()) { + for (auto i = _chat->participants.cbegin(), e = _chat->participants.cend(); i != e; ++i) { + UserData *user = i.key(); + if (!listAllSuggestions && user->username.isEmpty()) continue; + if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; + ordered.insertMulti(App::onlineForSort(user, now), user); + } + } + for (auto i = _chat->lastAuthors.cbegin(), e = _chat->lastAuthors.cend(); i != e; ++i) { + UserData *user = *i; + if (!listAllSuggestions && user->username.isEmpty()) continue; + if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; + mrows.push_back(user); + if (!ordered.isEmpty()) { + ordered.remove(App::onlineForSort(user, now), user); + } + } + if (!ordered.isEmpty()) { + for (QMultiMap::const_iterator i = ordered.cend(), b = ordered.cbegin(); i != b;) { + --i; + mrows.push_back(i.value()); + } + } + } else if (_channel && _channel->isMegagroup()) { + QMultiMap ordered; + if (_channel->mgInfo->lastParticipants.isEmpty() || _channel->lastParticipantsCountOutdated()) { + if (App::api()) App::api()->requestLastParticipants(_channel); + } else { + mrows.reserve(mrows.size() + _channel->mgInfo->lastParticipants.size()); + for (auto i = _channel->mgInfo->lastParticipants.cbegin(), e = _channel->mgInfo->lastParticipants.cend(); i != e; ++i) { + UserData *user = *i; + if (!listAllSuggestions && user->username.isEmpty()) continue; + if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; + mrows.push_back(user); + } + } + } + } else if (_filter.at(0) == '#') { + auto &recent(cRecentWriteHashtags()); + hrows.reserve(recent.size()); + for (auto i = recent.cbegin(), e = recent.cend(); i != e; ++i) { + if (_filter.size() > 1 && (!i->first.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || i->first.size() + 1 == _filter.size())) continue; + hrows.push_back(i->first); + } + } else if (_filter.at(0) == '/') { + bool hasUsername = _filter.indexOf('@') > 1; + QMap bots; + int32 cnt = 0; + if (_chat) { + if (_chat->noParticipantInfo()) { + if (App::api()) App::api()->requestFullPeer(_chat); + } else if (!_chat->participants.isEmpty()) { + for (auto i = _chat->participants.cbegin(), e = _chat->participants.cend(); i != e; ++i) { + UserData *user = i.key(); + if (!user->botInfo) continue; + if (!user->botInfo->inited && App::api()) App::api()->requestFullPeer(user); + if (user->botInfo->commands.isEmpty()) continue; + bots.insert(user, true); + cnt += user->botInfo->commands.size(); + } + } + } else if (_user && _user->botInfo) { + if (!_user->botInfo->inited && App::api()) App::api()->requestFullPeer(_user); + cnt = _user->botInfo->commands.size(); + bots.insert(_user, true); + } else if (_channel && _channel->isMegagroup()) { + if (_channel->mgInfo->bots.isEmpty()) { + if (!_channel->mgInfo->botStatus && App::api()) App::api()->requestBots(_channel); + } else { + for_const (auto user, _channel->mgInfo->bots) { + if (!user->botInfo) continue; + if (!user->botInfo->inited && App::api()) App::api()->requestFullPeer(user); + if (user->botInfo->commands.isEmpty()) continue; + bots.insert(user, true); + cnt += user->botInfo->commands.size(); + } + } + } + if (cnt) { + brows.reserve(cnt); + int32 botStatus = _chat ? _chat->botStatus : ((_channel && _channel->isMegagroup()) ? _channel->mgInfo->botStatus : -1); + if (_chat) { + for (auto i = _chat->lastAuthors.cbegin(), e = _chat->lastAuthors.cend(); i != e; ++i) { + UserData *user = *i; + if (!user->botInfo) continue; + if (!bots.contains(user)) continue; + if (!user->botInfo->inited && App::api()) App::api()->requestFullPeer(user); + if (user->botInfo->commands.isEmpty()) continue; + bots.remove(user); + for (int32 j = 0, l = user->botInfo->commands.size(); j < l; ++j) { + if (_filter.size() > 1) { + QString toFilter = (hasUsername || botStatus == 0 || botStatus == 2) ? user->botInfo->commands.at(j).command + '@' + user->username : user->botInfo->commands.at(j).command; + if (!toFilter.startsWith(_filter.midRef(1), Qt::CaseInsensitive)/* || toFilter.size() + 1 == _filter.size()*/) continue; + } + brows.push_back(qMakePair(user, &user->botInfo->commands.at(j))); + } + } + } + if (!bots.isEmpty()) { + for (QMap::const_iterator i = bots.cbegin(), e = bots.cend(); i != e; ++i) { + UserData *user = i.key(); + for (int32 j = 0, l = user->botInfo->commands.size(); j < l; ++j) { + if (_filter.size() > 1) { + QString toFilter = (hasUsername || botStatus == 0 || botStatus == 2) ? user->botInfo->commands.at(j).command + '@' + user->username : user->botInfo->commands.at(j).command; + if (!toFilter.startsWith(_filter.midRef(1), Qt::CaseInsensitive)/* || toFilter.size() + 1 == _filter.size()*/) continue; + } + brows.push_back(qMakePair(user, &user->botInfo->commands.at(j))); + } + } + } + } + } + rowsUpdated(mrows, hrows, brows, srows, resetScroll); + _inner->setRecentInlineBotsInRows(recentInlineBots); +} + +void FieldAutocomplete::rowsUpdated(const internal::MentionRows &mrows, const internal::HashtagRows &hrows, const internal::BotCommandRows &brows, const StickerPack &srows, bool resetScroll) { + if (mrows.isEmpty() && hrows.isEmpty() && brows.isEmpty() && srows.isEmpty()) { + if (!isHidden()) { + hideStart(); + } + _mrows.clear(); + _hrows.clear(); + _brows.clear(); + _srows.clear(); + } else { + _mrows = mrows; + _hrows = hrows; + _brows = brows; + _srows = srows; + + bool hidden = _hiding || isHidden(); + if (hidden) { + show(); + _scroll->show(); + } + recount(resetScroll); + update(); + if (hidden) { + hide(); + showStart(); + } + } +} + +void FieldAutocomplete::setBoundings(QRect boundings) { + _boundings = boundings; + recount(); +} + +void FieldAutocomplete::recount(bool resetScroll) { + int32 h = 0, oldst = _scroll->scrollTop(), st = oldst, maxh = 4.5 * st::mentionHeight; + if (!_srows.isEmpty()) { + int32 stickersPerRow = qMax(1, int32(_boundings.width() - 2 * st::stickerPanPadding) / int32(st::stickerPanSize.width())); + int32 rows = rowscount(_srows.size(), stickersPerRow); + h = st::stickerPanPadding + rows * st::stickerPanSize.height(); + } else if (!_mrows.isEmpty()) { + h = _mrows.size() * st::mentionHeight; + } else if (!_hrows.isEmpty()) { + h = _hrows.size() * st::mentionHeight; + } else if (!_brows.isEmpty()) { + h = _brows.size() * st::mentionHeight; + } + + if (_inner->width() != _boundings.width() || _inner->height() != h) { + _inner->resize(_boundings.width(), h); + } + if (h > _boundings.height()) h = _boundings.height(); + if (h > maxh) h = maxh; + if (width() != _boundings.width() || height() != h) { + setGeometry(_boundings.x(), _boundings.y() + _boundings.height() - h, _boundings.width(), h); + _scroll->resize(_boundings.width(), h); + } else if (y() != _boundings.y() + _boundings.height() - h) { + move(_boundings.x(), _boundings.y() + _boundings.height() - h); + } + if (resetScroll) st = 0; + if (st != oldst) _scroll->scrollToY(st); + if (resetScroll) _inner->clearSel(); +} + +void FieldAutocomplete::fastHide() { + if (_a_appearance.animating()) { + _a_appearance.stop(); + } + a_opacity = anim::fvalue(0, 0); + _hideTimer.stop(); + hideFinish(); +} + +void FieldAutocomplete::hideStart() { + if (!_hiding) { + if (_cache.isNull()) { + _scroll->show(); + _cache = myGrab(this); + } + _scroll->hide(); + _hiding = true; + a_opacity.start(0); + setAttribute(Qt::WA_OpaquePaintEvent, false); + _a_appearance.start(); + } +} + +void FieldAutocomplete::hideFinish() { + hide(); + _hiding = false; + _filter = qsl("-"); + _inner->clearSel(true); +} + +void FieldAutocomplete::showStart() { + if (!isHidden() && a_opacity.current() == 1 && !_hiding) { + return; + } + if (_cache.isNull()) { + _scroll->show(); + _cache = myGrab(this); + } + _scroll->hide(); + _hiding = false; + show(); + a_opacity.start(1); + setAttribute(Qt::WA_OpaquePaintEvent, false); + _a_appearance.start(); +} + +void FieldAutocomplete::step_appearance(float64 ms, bool timer) { + float64 dt = ms / st::dropdownDef.duration; + if (dt >= 1) { + _a_appearance.stop(); + a_opacity.finish(); + _cache = QPixmap(); + setAttribute(Qt::WA_OpaquePaintEvent); + if (_hiding) { + hideFinish(); + } else { + _scroll->show(); + _inner->clearSel(); + } + } else { + a_opacity.update(dt, anim::linear); + } + if (timer) update(); +} + +const QString &FieldAutocomplete::filter() const { + return _filter; +} + +ChatData *FieldAutocomplete::chat() const { + return _chat; +} + +ChannelData *FieldAutocomplete::channel() const { + return _channel; +} + +UserData *FieldAutocomplete::user() const { + return _user; +} + +int32 FieldAutocomplete::innerTop() { + return _scroll->scrollTop(); +} + +int32 FieldAutocomplete::innerBottom() { + return _scroll->scrollTop() + _scroll->height(); +} + +bool FieldAutocomplete::chooseSelected(ChooseMethod method) const { + return _inner->chooseSelected(method); +} + +bool FieldAutocomplete::eventFilter(QObject *obj, QEvent *e) { + if (isHidden()) return QWidget::eventFilter(obj, e); + if (e->type() == QEvent::KeyPress) { + QKeyEvent *ev = static_cast(e); + if (!(ev->modifiers() & (Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier | Qt::MetaModifier))) { + if (ev->key() == Qt::Key_Up || ev->key() == Qt::Key_Down || (!_srows.isEmpty() && (ev->key() == Qt::Key_Left || ev->key() == Qt::Key_Right))) { + return _inner->moveSel(ev->key()); + } else if (ev->key() == Qt::Key_Enter || ev->key() == Qt::Key_Return) { + return _inner->chooseSelected(ChooseMethod::ByEnter); + } + } + } + return QWidget::eventFilter(obj, e); +} + +FieldAutocomplete::~FieldAutocomplete() { +} + +namespace internal { + +FieldAutocompleteInner::FieldAutocompleteInner(FieldAutocomplete *parent, MentionRows *mrows, HashtagRows *hrows, BotCommandRows *brows, StickerPack *srows) +: _parent(parent) +, _mrows(mrows) +, _hrows(hrows) +, _brows(brows) +, _srows(srows) +, _stickersPerRow(1) +, _recentInlineBotsInRows(0) +, _sel(-1) +, _down(-1) +, _mouseSel(false) +, _overDelete(false) +, _previewShown(false) { + _previewTimer.setSingleShot(true); + connect(&_previewTimer, SIGNAL(timeout()), this, SLOT(onPreview())); +} + +void FieldAutocompleteInner::paintEvent(QPaintEvent *e) { + Painter p(this); + + QRect r(e->rect()); + if (r != rect()) p.setClipRect(r); + + int32 atwidth = st::mentionFont->width('@'), hashwidth = st::mentionFont->width('#'); + int32 mentionleft = 2 * st::mentionPadding.left() + st::mentionPhotoSize; + int32 mentionwidth = width() - mentionleft - 2 * st::mentionPadding.right(); + int32 htagleft = st::btnAttachPhoto.width + st::taMsgField.textMrg.left() - st::lineWidth, htagwidth = width() - st::mentionPadding.right() - htagleft - st::mentionScroll.width; + + if (!_srows->isEmpty()) { + int32 rows = rowscount(_srows->size(), _stickersPerRow); + int32 fromrow = floorclamp(r.y() - st::stickerPanPadding, st::stickerPanSize.height(), 0, rows); + int32 torow = ceilclamp(r.y() + r.height() - st::stickerPanPadding, st::stickerPanSize.height(), 0, rows); + int32 fromcol = floorclamp(r.x() - st::stickerPanPadding, st::stickerPanSize.width(), 0, _stickersPerRow); + int32 tocol = ceilclamp(r.x() + r.width() - st::stickerPanPadding, st::stickerPanSize.width(), 0, _stickersPerRow); + for (int32 row = fromrow; row < torow; ++row) { + for (int32 col = fromcol; col < tocol; ++col) { + int32 index = row * _stickersPerRow + col; + if (index >= _srows->size()) break; + + DocumentData *sticker = _srows->at(index); + if (!sticker->sticker()) continue; + + QPoint pos(st::stickerPanPadding + col * st::stickerPanSize.width(), st::stickerPanPadding + row * st::stickerPanSize.height()); + if (_sel == index) { + QPoint tl(pos); + if (rtl()) tl.setX(width() - tl.x() - st::stickerPanSize.width()); + App::roundRect(p, QRect(tl, st::stickerPanSize), st::emojiPanHover, StickerHoverCorners); + } + + bool goodThumb = !sticker->thumb->isNull() && ((sticker->thumb->width() >= 128) || (sticker->thumb->height() >= 128)); + if (goodThumb) { + sticker->thumb->load(); + } else { + sticker->checkSticker(); + } + + float64 coef = qMin((st::stickerPanSize.width() - st::msgRadius * 2) / float64(sticker->dimensions.width()), (st::stickerPanSize.height() - st::msgRadius * 2) / float64(sticker->dimensions.height())); + if (coef > 1) coef = 1; + int32 w = qRound(coef * sticker->dimensions.width()), h = qRound(coef * sticker->dimensions.height()); + if (w < 1) w = 1; + if (h < 1) h = 1; + QPoint ppos = pos + QPoint((st::stickerPanSize.width() - w) / 2, (st::stickerPanSize.height() - h) / 2); + if (goodThumb) { + p.drawPixmapLeft(ppos, width(), sticker->thumb->pix(w, h)); + } else if (!sticker->sticker()->img->isNull()) { + p.drawPixmapLeft(ppos, width(), sticker->sticker()->img->pix(w, h)); + } + } + } + } else { + int32 from = qFloor(e->rect().top() / st::mentionHeight), to = qFloor(e->rect().bottom() / st::mentionHeight) + 1; + int32 last = _mrows->isEmpty() ? (_hrows->isEmpty() ? _brows->size() : _hrows->size()) : _mrows->size(); + bool hasUsername = _parent->filter().indexOf('@') > 1; + int filterSize = qMax(_parent->filter().size() - 1, 0); + bool filterIsEmpty = (filterSize == 0); + for (int32 i = from; i < to; ++i) { + if (i >= last) break; + + bool selected = (i == _sel); + if (selected) { + p.fillRect(0, i * st::mentionHeight, width(), st::mentionHeight, st::mentionBgOver->b); + int skip = (st::mentionHeight - st::notifyClose.icon.pxHeight()) / 2; + if (!_hrows->isEmpty() || (!_mrows->isEmpty() && i < _recentInlineBotsInRows)) { + p.drawSprite(QPoint(width() - st::notifyClose.icon.pxWidth() - skip, i * st::mentionHeight + skip), st::notifyClose.icon); + } + } + p.setPen(st::black->p); + if (!_mrows->isEmpty()) { + UserData *user = _mrows->at(i); + QString first = filterIsEmpty ? QString() : ('@' + user->username.mid(0, filterSize)); + QString second = (filterIsEmpty && !user->username.isEmpty()) ? ('@' + user->username) : user->username.mid(filterSize); + int32 firstwidth = st::mentionFont->width(first), secondwidth = st::mentionFont->width(second), unamewidth = firstwidth + secondwidth, namewidth = user->nameText.maxWidth(); + if (mentionwidth < unamewidth + namewidth) { + namewidth = (mentionwidth * namewidth) / (namewidth + unamewidth); + unamewidth = mentionwidth - namewidth; + if (firstwidth < unamewidth + st::mentionFont->elidew) { + if (firstwidth < unamewidth) { + first = st::mentionFont->elided(first, unamewidth); + } else if (!second.isEmpty()) { + first = st::mentionFont->elided(first + second, unamewidth); + second = QString(); + } + } else { + second = st::mentionFont->elided(second, unamewidth - firstwidth); + } + } + user->loadUserpic(); + user->paintUserpicLeft(p, st::mentionPhotoSize, st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), width()); + user->nameText.drawElided(p, 2 * st::mentionPadding.left() + st::mentionPhotoSize, i * st::mentionHeight + st::mentionTop, namewidth); + + p.setFont(st::mentionFont->f); + p.setPen((selected ? st::mentionFgOverActive : st::mentionFgActive)->p); + p.drawText(mentionleft + namewidth + st::mentionPadding.right(), i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first); + if (!second.isEmpty()) { + p.setPen((selected ? st::mentionFgOver : st::mentionFg)->p); + p.drawText(mentionleft + namewidth + st::mentionPadding.right() + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second); + } + } else if (!_hrows->isEmpty()) { + QString hrow = _hrows->at(i); + QString first = filterIsEmpty ? QString() : ('#' + hrow.mid(0, filterSize)); + QString second = filterIsEmpty ? ('#' + hrow) : hrow.mid(filterSize); + int32 firstwidth = st::mentionFont->width(first), secondwidth = st::mentionFont->width(second); + if (htagwidth < firstwidth + secondwidth) { + if (htagwidth < firstwidth + st::mentionFont->elidew) { + first = st::mentionFont->elided(first + second, htagwidth); + second = QString(); + } else { + second = st::mentionFont->elided(second, htagwidth - firstwidth); + } + } + + p.setFont(st::mentionFont->f); + if (!first.isEmpty()) { + p.setPen((selected ? st::mentionFgOverActive : st::mentionFgActive)->p); + p.drawText(htagleft, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first); + } + if (!second.isEmpty()) { + p.setPen((selected ? st::mentionFgOver : st::mentionFg)->p); + p.drawText(htagleft + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second); + } + } else { + UserData *user = _brows->at(i).first; + + const BotCommand *command = _brows->at(i).second; + QString toHighlight = command->command; + int32 botStatus = _parent->chat() ? _parent->chat()->botStatus : ((_parent->channel() && _parent->channel()->isMegagroup()) ? _parent->channel()->mgInfo->botStatus : -1); + if (hasUsername || botStatus == 0 || botStatus == 2) { + toHighlight += '@' + user->username; + } + user->loadUserpic(); + user->paintUserpicLeft(p, st::mentionPhotoSize, st::mentionPadding.left(), i * st::mentionHeight + st::mentionPadding.top(), width()); + + int32 addleft = 0, widthleft = mentionwidth; + QString first = filterIsEmpty ? QString() : ('/' + toHighlight.mid(0, filterSize)); + QString second = filterIsEmpty ? ('/' + toHighlight) : toHighlight.mid(filterSize); + int32 firstwidth = st::mentionFont->width(first), secondwidth = st::mentionFont->width(second); + if (widthleft < firstwidth + secondwidth) { + if (widthleft < firstwidth + st::mentionFont->elidew) { + first = st::mentionFont->elided(first + second, widthleft); + second = QString(); + } else { + second = st::mentionFont->elided(second, widthleft - firstwidth); + } + } + p.setFont(st::mentionFont->f); + if (!first.isEmpty()) { + p.setPen((selected ? st::mentionFgOverActive : st::mentionFgActive)->p); + p.drawText(mentionleft, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, first); + } + if (!second.isEmpty()) { + p.setPen((selected ? st::mentionFgOver : st::mentionFg)->p); + p.drawText(mentionleft + firstwidth, i * st::mentionHeight + st::mentionTop + st::mentionFont->ascent, second); + } + addleft += firstwidth + secondwidth + st::mentionPadding.left(); + widthleft -= firstwidth + secondwidth + st::mentionPadding.left(); + if (widthleft > st::mentionFont->elidew && !command->descriptionText().isEmpty()) { + p.setPen((selected ? st::mentionFgOver : st::mentionFg)->p); + command->descriptionText().drawElided(p, mentionleft + addleft, i * st::mentionHeight + st::mentionTop, widthleft, 1, style::al_right); + } + } + } + p.fillRect(Adaptive::OneColumn() ? 0 : st::lineWidth, _parent->innerBottom() - st::lineWidth, width() - (Adaptive::OneColumn() ? 0 : st::lineWidth), st::lineWidth, st::shadowColor->b); + } + p.fillRect(Adaptive::OneColumn() ? 0 : st::lineWidth, _parent->innerTop(), width() - (Adaptive::OneColumn() ? 0 : st::lineWidth), st::lineWidth, st::shadowColor->b); +} + +void FieldAutocompleteInner::resizeEvent(QResizeEvent *e) { + _stickersPerRow = qMax(1, int32(width() - 2 * st::stickerPanPadding) / int32(st::stickerPanSize.width())); +} + +void FieldAutocompleteInner::mouseMoveEvent(QMouseEvent *e) { + _mousePos = mapToGlobal(e->pos()); + _mouseSel = true; + onUpdateSelected(true); +} + +void FieldAutocompleteInner::clearSel(bool hidden) { + _mouseSel = _overDelete = false; + setSel((_mrows->isEmpty() && _brows->isEmpty() && _hrows->isEmpty()) ? -1 : 0); + if (hidden) { + _down = -1; + _previewShown = false; + } +} + +bool FieldAutocompleteInner::moveSel(int key) { + _mouseSel = false; + int32 maxSel = (_mrows->isEmpty() ? (_hrows->isEmpty() ? (_brows->isEmpty() ? _srows->size() : _brows->size()) : _hrows->size()) : _mrows->size()); + int32 direction = (key == Qt::Key_Up) ? -1 : (key == Qt::Key_Down ? 1 : 0); + if (!_srows->isEmpty()) { + if (key == Qt::Key_Left) { + direction = -1; + } else if (key == Qt::Key_Right) { + direction = 1; + } else { + direction *= _stickersPerRow; + } + } + if (_sel >= maxSel || _sel < 0) { + if (direction < -1) { + setSel(((maxSel - 1) / _stickersPerRow) * _stickersPerRow, true); + } else if (direction < 0) { + setSel(maxSel - 1, true); + } else { + setSel(0, true); + } + return (_sel >= 0 && _sel < maxSel); + } + setSel((_sel + direction >= maxSel || _sel + direction < 0) ? -1 : (_sel + direction), true); + return true; +} + +bool FieldAutocompleteInner::chooseSelected(FieldAutocomplete::ChooseMethod method) const { + if (!_srows->isEmpty()) { + if (_sel >= 0 && _sel < _srows->size()) { + emit stickerChosen(_srows->at(_sel), method); + return true; + } + } else if (!_mrows->isEmpty()) { + if (_sel >= 0 && _sel < _mrows->size()) { + emit mentionChosen(_mrows->at(_sel), method); + return true; + } + } else if (!_hrows->isEmpty()) { + if (_sel >= 0 && _sel < _hrows->size()) { + emit hashtagChosen('#' + _hrows->at(_sel), method); + return true; + } + } else if (!_brows->isEmpty()) { + if (_sel >= 0 && _sel < _brows->size()) { + UserData *user = _brows->at(_sel).first; + const BotCommand *command(_brows->at(_sel).second); + int32 botStatus = _parent->chat() ? _parent->chat()->botStatus : ((_parent->channel() && _parent->channel()->isMegagroup()) ? _parent->channel()->mgInfo->botStatus : -1); + if (botStatus == 0 || botStatus == 2 || _parent->filter().indexOf('@') > 1) { + emit botCommandChosen('/' + command->command + '@' + user->username, method); + } else { + emit botCommandChosen('/' + command->command, method); + } + return true; + } + } + return false; +} + +void FieldAutocompleteInner::setRecentInlineBotsInRows(int32 bots) { + _recentInlineBotsInRows = bots; +} + +void FieldAutocompleteInner::mousePressEvent(QMouseEvent *e) { + _mousePos = mapToGlobal(e->pos()); + _mouseSel = true; + onUpdateSelected(true); + if (e->button() == Qt::LeftButton) { + if (_overDelete && _sel >= 0 && _sel < (_mrows->isEmpty() ? _hrows->size() : _recentInlineBotsInRows)) { + _mousePos = mapToGlobal(e->pos()); + bool removed = false; + if (_mrows->isEmpty()) { + QString toRemove = _hrows->at(_sel); + RecentHashtagPack &recent(cRefRecentWriteHashtags()); + for (RecentHashtagPack::iterator i = recent.begin(); i != recent.cend();) { + if (i->first == toRemove) { + i = recent.erase(i); + removed = true; + } else { + ++i; + } + } + } else { + UserData *toRemove = _mrows->at(_sel); + RecentInlineBots &recent(cRefRecentInlineBots()); + int32 index = recent.indexOf(toRemove); + if (index >= 0) { + recent.remove(index); + removed = true; + } + } + if (removed) { + Local::writeRecentHashtagsAndBots(); + } + _parent->updateFiltered(); + + _mouseSel = true; + onUpdateSelected(true); + } else if (_srows->isEmpty()) { + chooseSelected(FieldAutocomplete::ChooseMethod::ByClick); + } else { + _down = _sel; + _previewTimer.start(QApplication::startDragTime()); + } + } +} + +void FieldAutocompleteInner::mouseReleaseEvent(QMouseEvent *e) { + _previewTimer.stop(); + + int32 pressed = _down; + _down = -1; + + _mousePos = mapToGlobal(e->pos()); + _mouseSel = true; + onUpdateSelected(true); + + if (_previewShown) { + _previewShown = false; + return; + } + + if (_sel < 0 || _sel != pressed || _srows->isEmpty()) return; + + chooseSelected(FieldAutocomplete::ChooseMethod::ByClick); +} + +void FieldAutocompleteInner::enterEvent(QEvent *e) { + setMouseTracking(true); + _mousePos = QCursor::pos(); + onUpdateSelected(true); +} + +void FieldAutocompleteInner::leaveEvent(QEvent *e) { + setMouseTracking(false); + if (_sel >= 0) { + setSel(-1); + } +} + +void FieldAutocompleteInner::updateSelectedRow() { + if (_sel >= 0) { + if (_srows->isEmpty()) { + update(0, _sel * st::mentionHeight, width(), st::mentionHeight); + } else { + int32 row = _sel / _stickersPerRow, col = _sel % _stickersPerRow; + update(st::stickerPanPadding + col * st::stickerPanSize.width(), st::stickerPanPadding + row * st::stickerPanSize.height(), st::stickerPanSize.width(), st::stickerPanSize.height()); + } + } +} + +void FieldAutocompleteInner::setSel(int sel, bool scroll) { + updateSelectedRow(); + _sel = sel; + updateSelectedRow(); + + if (scroll && _sel >= 0) { + if (_srows->isEmpty()) { + emit mustScrollTo(_sel * st::mentionHeight, (_sel + 1) * st::mentionHeight); + } else { + int32 row = _sel / _stickersPerRow; + emit mustScrollTo(st::stickerPanPadding + row * st::stickerPanSize.height(), st::stickerPanPadding + (row + 1) * st::stickerPanSize.height()); + } + } +} + +void FieldAutocompleteInner::onUpdateSelected(bool force) { + QPoint mouse(mapFromGlobal(_mousePos)); + if ((!force && !rect().contains(mouse)) || !_mouseSel) return; + + if (_down >= 0 && !_previewShown) return; + + int32 sel = -1, maxSel = 0; + if (!_srows->isEmpty()) { + int32 rows = rowscount(_srows->size(), _stickersPerRow); + int32 row = (mouse.y() >= st::stickerPanPadding) ? ((mouse.y() - st::stickerPanPadding) / st::stickerPanSize.height()) : -1; + int32 col = (mouse.x() >= st::stickerPanPadding) ? ((mouse.x() - st::stickerPanPadding) / st::stickerPanSize.width()) : -1; + if (row >= 0 && col >= 0) { + sel = row * _stickersPerRow + col; + } + maxSel = _srows->size(); + _overDelete = false; + } else { + sel = mouse.y() / int32(st::mentionHeight); + maxSel = _mrows->isEmpty() ? (_hrows->isEmpty() ? _brows->size() : _hrows->size()) : _mrows->size(); + _overDelete = (!_hrows->isEmpty() || (!_mrows->isEmpty() && sel < _recentInlineBotsInRows)) ? (mouse.x() >= width() - st::mentionHeight) : false; + } + if (sel < 0 || sel >= maxSel) { + sel = -1; + } + if (sel != _sel) { + setSel(sel); + if (_down >= 0 && _sel >= 0 && _down != _sel) { + _down = _sel; + if (_down >= 0 && _down < _srows->size()) { + Ui::showMediaPreview(_srows->at(_down)); + } + } + } +} + +void FieldAutocompleteInner::onParentGeometryChanged() { + _mousePos = QCursor::pos(); + if (rect().contains(mapFromGlobal(_mousePos))) { + setMouseTracking(true); + onUpdateSelected(true); + } +} + +void FieldAutocompleteInner::onPreview() { + if (_down >= 0 && _down < _srows->size()) { + Ui::showMediaPreview(_srows->at(_down)); + _previewShown = true; + } +} + +} // namespace internal diff --git a/Telegram/SourceFiles/history/field_autocomplete.h b/Telegram/SourceFiles/history/field_autocomplete.h new file mode 100644 index 000000000..548dfdc32 --- /dev/null +++ b/Telegram/SourceFiles/history/field_autocomplete.h @@ -0,0 +1,195 @@ +/* +This file is part of Telegram Desktop, +the official desktop version of Telegram messaging app, see https://telegram.org + +Telegram Desktop is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +It is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +In addition, as a special exception, the copyright holders give permission +to link the code of portions of this program with the OpenSSL library. + +Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE +Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org +*/ +#pragma once + +#include "ui/twidget.h" +#include "ui/boxshadow.h" + +namespace internal { + +using MentionRows = QList; +using HashtagRows = QList; +using BotCommandRows = QList>; + +class FieldAutocompleteInner; + +} // namespace internal + +class FieldAutocomplete final : public TWidget { + Q_OBJECT + +public: + + FieldAutocomplete(QWidget *parent); + + void fastHide(); + + bool clearFilteredBotCommands(); + void showFiltered(PeerData *peer, QString query, bool start); + void showStickers(EmojiPtr emoji); + void setBoundings(QRect boundings); + + void step_appearance(float64 ms, bool timer); + + const QString &filter() const; + ChatData *chat() const; + ChannelData *channel() const; + UserData *user() const; + + int32 innerTop(); + int32 innerBottom(); + + bool eventFilter(QObject *obj, QEvent *e); + + enum class ChooseMethod { + ByEnter, + ByTab, + ByClick, + }; + bool chooseSelected(ChooseMethod method) const; + + bool stickersShown() const { + return !_srows.isEmpty(); + } + + bool overlaps(const QRect &globalRect) { + if (isHidden() || !testAttribute(Qt::WA_OpaquePaintEvent)) return false; + + return rect().contains(QRect(mapFromGlobal(globalRect.topLeft()), globalRect.size())); + } + + ~FieldAutocomplete(); + +signals: + + void mentionChosen(UserData *user, FieldAutocomplete::ChooseMethod method) const; + void hashtagChosen(QString hashtag, FieldAutocomplete::ChooseMethod method) const; + void botCommandChosen(QString command, FieldAutocomplete::ChooseMethod method) const; + void stickerChosen(DocumentData *sticker, FieldAutocomplete::ChooseMethod method) const; + +public slots: + + void hideStart(); + void hideFinish(); + + void showStart(); + +private: + + void paintEvent(QPaintEvent *e) override; + + void updateFiltered(bool resetScroll = false); + void recount(bool resetScroll = false); + + QPixmap _cache; + internal::MentionRows _mrows; + internal::HashtagRows _hrows; + internal::BotCommandRows _brows; + StickerPack _srows; + + void rowsUpdated(const internal::MentionRows &mrows, const internal::HashtagRows &hrows, const internal::BotCommandRows &brows, const StickerPack &srows, bool resetScroll); + + ChildWidget _scroll; + ChildWidget _inner; + + ChatData *_chat; + UserData *_user; + ChannelData *_channel; + EmojiPtr _emoji; + QString _filter; + QRect _boundings; + bool _addInlineBots; + + int32 _width, _height; + bool _hiding; + + anim::fvalue a_opacity; + Animation _a_appearance; + + QTimer _hideTimer; + + BoxShadow _shadow; + friend class internal::FieldAutocompleteInner; + +}; + +namespace internal { + +class FieldAutocompleteInner final : public TWidget { + Q_OBJECT + +public: + + FieldAutocompleteInner(FieldAutocomplete *parent, MentionRows *mrows, HashtagRows *hrows, BotCommandRows *brows, StickerPack *srows); + + void clearSel(bool hidden = false); + bool moveSel(int key); + bool chooseSelected(FieldAutocomplete::ChooseMethod method) const; + + void setRecentInlineBotsInRows(int32 bots); + +signals: + + void mentionChosen(UserData *user, FieldAutocomplete::ChooseMethod method) const; + void hashtagChosen(QString hashtag, FieldAutocomplete::ChooseMethod method) const; + void botCommandChosen(QString command, FieldAutocomplete::ChooseMethod method) const; + void stickerChosen(DocumentData *sticker, FieldAutocomplete::ChooseMethod method) const; + void mustScrollTo(int scrollToTop, int scrollToBottom); + +public slots: + + void onParentGeometryChanged(); + void onUpdateSelected(bool force = false); + void onPreview(); + +private: + + void paintEvent(QPaintEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + + void enterEvent(QEvent *e) override; + void leaveEvent(QEvent *e) override; + + void mousePressEvent(QMouseEvent *e) override; + void mouseMoveEvent(QMouseEvent *e) override; + void mouseReleaseEvent(QMouseEvent *e) override; + + void updateSelectedRow(); + void setSel(int sel, bool scroll = false); + + FieldAutocomplete *_parent; + MentionRows *_mrows; + HashtagRows *_hrows; + BotCommandRows *_brows; + StickerPack *_srows; + int32 _stickersPerRow, _recentInlineBotsInRows; + int32 _sel, _down; + bool _mouseSel; + QPoint _mousePos; + + bool _overDelete; + + bool _previewShown; + + QTimer _previewTimer; +}; + +} // namespace internal diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 4ee8cc083..824c07b3a 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -2740,7 +2740,7 @@ HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent) , _scroll(this, st::historyScroll, false) , _toHistoryEnd(this, st::historyToEnd) , _collapseComments(this) -, _attachMention(this) +, _fieldAutocomplete(this) , _reportSpamPanel(this) , _send(this, lang(lng_send_button), st::btnSend) , _unblock(this, lang(lng_unblock_button), st::btnUnblock) @@ -2827,7 +2827,7 @@ HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent) connect(&_saveDraftTimer, SIGNAL(timeout()), this, SLOT(onDraftSave())); connect(_field.verticalScrollBar(), SIGNAL(valueChanged(int)), this, SLOT(onDraftSaveDelayed())); connect(&_field, SIGNAL(cursorPositionChanged()), this, SLOT(onDraftSaveDelayed())); - connect(&_field, SIGNAL(cursorPositionChanged()), this, SLOT(onCheckMentionDropdown()), Qt::QueuedConnection); + connect(&_field, SIGNAL(cursorPositionChanged()), this, SLOT(onCheckFieldAutocomplete()), Qt::QueuedConnection); _fieldBarCancel.hide(); @@ -2849,10 +2849,12 @@ HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent) _collapseComments.hide(); _collapseComments.installEventFilter(this); - _attachMention.hide(); - connect(&_attachMention, SIGNAL(chosen(QString)), this, SLOT(onMentionHashtagOrBotCommandInsert(QString))); - connect(&_attachMention, SIGNAL(stickerSelected(DocumentData*)), this, SLOT(onStickerSend(DocumentData*))); - _field.installEventFilter(&_attachMention); + _fieldAutocomplete->hide(); + connect(_fieldAutocomplete, SIGNAL(mentionChosen(UserData*,FieldAutocomplete::ChooseMethod)), this, SLOT(onMentionInsert(UserData*))); + connect(_fieldAutocomplete, SIGNAL(hashtagChosen(QString,FieldAutocomplete::ChooseMethod)), this, SLOT(onHashtagOrBotCommandInsert(QString,FieldAutocomplete::ChooseMethod))); + connect(_fieldAutocomplete, SIGNAL(botCommandChosen(QString,FieldAutocomplete::ChooseMethod)), this, SLOT(onHashtagOrBotCommandInsert(QString,FieldAutocomplete::ChooseMethod))); + connect(_fieldAutocomplete, SIGNAL(stickerChosen(DocumentData*,FieldAutocomplete::ChooseMethod)), this, SLOT(onStickerSend(DocumentData*))); + _field.installEventFilter(_fieldAutocomplete); updateFieldSubmitSettings(); _field.hide(); @@ -2911,12 +2913,24 @@ void HistoryWidget::onStickersUpdated() { updateStickersByEmoji(); } -void HistoryWidget::onMentionHashtagOrBotCommandInsert(QString str) { - if (str.at(0) == '/') { // bot command +void HistoryWidget::onMentionInsert(UserData *user) { + QString replacement, entityTag; + if (user->username.isEmpty()) { + replacement = App::peerName(user); + entityTag = qsl("mention://peer.") + QString::number(user->id); + } else { + replacement = '@' + user->username; + } + _field.insertMentionHashtagOrBotCommand(replacement, entityTag); +} + +void HistoryWidget::onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::ChooseMethod method) { + // 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())); } else { - _field.onMentionHashtagOrBotCommandInsert(str); + _field.insertMentionHashtagOrBotCommand(str); } } @@ -2952,8 +2966,8 @@ void HistoryWidget::updateInlineBotQuery() { } else { _emojiPan.queryInlineBot(_inlineBot, _peer, query); } - if (!_attachMention.isHidden()) { - _attachMention.hideStart(); + if (!_fieldAutocomplete->isHidden()) { + _fieldAutocomplete->hideStart(); } } else { clearInlineBot(); @@ -2964,13 +2978,13 @@ void HistoryWidget::updateStickersByEmoji() { int32 len = 0; if (EmojiPtr emoji = emojiFromText(_field.getLastText(), &len)) { if (_field.getLastText().size() <= len) { - _attachMention.showStickers(emoji); + _fieldAutocomplete->showStickers(emoji); } else { len = 0; } } if (!len) { - _attachMention.showStickers(EmojiPtr(0)); + _fieldAutocomplete->showStickers(EmojiPtr(0)); } } @@ -3239,8 +3253,8 @@ void HistoryWidget::updateStickers() { void HistoryWidget::notify_botCommandsChanged(UserData *user) { if (_peer && (_peer == user || !_peer->isUser())) { - if (_attachMention.clearFilteredBotCommands()) { - onCheckMentionDropdown(); + if (_fieldAutocomplete->clearFilteredBotCommands()) { + onCheckFieldAutocomplete(); } } } @@ -3873,7 +3887,7 @@ bool HistoryWidget::contentOverlapped(const QRect &globalRect) { return (_attachDragDocument.overlaps(globalRect) || _attachDragPhoto.overlaps(globalRect) || _attachType.overlaps(globalRect) || - _attachMention.overlaps(globalRect) || + _fieldAutocomplete->overlaps(globalRect) || _emojiPan.overlaps(globalRect)); } @@ -3986,7 +4000,7 @@ void HistoryWidget::updateControlsVisibility() { _botStart.hide(); _joinChannel.hide(); _muteUnmute.hide(); - _attachMention.hide(); + _fieldAutocomplete->hide(); _field.hide(); _fieldBarCancel.hide(); _attachDocument.hide(); @@ -4047,7 +4061,7 @@ void HistoryWidget::updateControlsVisibility() { } } _kbShown = false; - _attachMention.hide(); + _fieldAutocomplete->hide(); _send.hide(); if (_inlineBotCancel) _inlineBotCancel->hide(); _botStart.hide(); @@ -4071,7 +4085,7 @@ void HistoryWidget::updateControlsVisibility() { update(); } } else if (_canSendMessages) { - onCheckMentionDropdown(); + onCheckFieldAutocomplete(); if (isBotStart()) { if (isBotStart()) { _unblock.hide(); @@ -4190,7 +4204,7 @@ void HistoryWidget::updateControlsVisibility() { } } } else { - _attachMention.hide(); + _fieldAutocomplete->hide(); _send.hide(); if (_inlineBotCancel) _inlineBotCancel->hide(); _unblock.hide(); @@ -4807,7 +4821,7 @@ void HistoryWidget::onSend(bool ctrlShiftEnter, MsgId replyTo) { _saveDraftStart = getms(); onDraftSave(); - if (!_attachMention.isHidden()) _attachMention.hideStart(); + if (!_fieldAutocomplete->isHidden()) _fieldAutocomplete->hideStart(); if (!_attachType.isHidden()) _attachType.hideStart(); if (!_emojiPan.isHidden()) _emojiPan.hideStart(); @@ -5018,7 +5032,7 @@ void HistoryWidget::animShow(const QPixmap &bgAnimCache, const QPixmap &bgAnimTo _attachDocument.hide(); _attachPhoto.hide(); _attachEmoji.hide(); - _attachMention.hide(); + _fieldAutocomplete->hide(); _broadcast.hide(); _silent.hide(); _kbShow.hide(); @@ -5826,7 +5840,7 @@ void HistoryWidget::updateOnlineDisplay(int32 x, int32 w) { } int32 onlineCount = 0; bool onlyMe = true; - for (MentionRows::const_iterator i = _peer->asChannel()->mgInfo->lastParticipants.cbegin(), e = _peer->asChannel()->mgInfo->lastParticipants.cend(); i != e; ++i) { + for (auto i = _peer->asChannel()->mgInfo->lastParticipants.cbegin(), e = _peer->asChannel()->mgInfo->lastParticipants.cend(); i != e; ++i) { if ((*i)->onlineTill > t) { ++onlineCount; if (onlyMe && (*i) != App::self()) onlyMe = false; @@ -5937,7 +5951,7 @@ void HistoryWidget::clearInlineBot() { _field.finishPlaceholder(); } _emojiPan.clearInlineBot(); - onCheckMentionDropdown(); + onCheckFieldAutocomplete(); } void HistoryWidget::inlineBotChanged() { @@ -5967,7 +5981,7 @@ void HistoryWidget::onFieldFocused() { if (_list) _list->clearSelectedItems(true); } -void HistoryWidget::onCheckMentionDropdown() { +void HistoryWidget::onCheckFieldAutocomplete() { if (!_history || _a_show.animating()) return; bool start = false; @@ -5977,7 +5991,7 @@ void HistoryWidget::onCheckMentionDropdown() { if (query.at(0) == '@' && cRecentInlineBots().isEmpty()) Local::readRecentHashtagsAndBots(); if (query.at(0) == '/' && _peer->isUser() && !_peer->asUser()->botInfo) return; } - _attachMention.showFiltered(_peer, query, start); + _fieldAutocomplete->showFiltered(_peer, query, start); } void HistoryWidget::updateFieldPlaceholder() { @@ -6451,14 +6465,14 @@ void HistoryWidget::resizeEvent(QResizeEvent *e) { if (_scroll.y() != st::replyHeight) { _scroll.move(0, st::replyHeight); _reportSpamPanel.move(0, st::replyHeight); - _attachMention.setBoundings(_scroll.geometry()); + _fieldAutocomplete->setBoundings(_scroll.geometry()); } _pinnedBar->cancel.move(width() - _pinnedBar->cancel.width(), 0); _pinnedBar->shadow.setGeometry(0, st::replyHeight, width(), st::lineWidth); } else if (_scroll.y() != 0) { _scroll.move(0, 0); _reportSpamPanel.move(0, 0); - _attachMention.setBoundings(_scroll.geometry()); + _fieldAutocomplete->setBoundings(_scroll.geometry()); } updateListSize(false, false, { ScrollChangeAdd, App::main() ? App::main()->contentScrollAddToY() : 0 }); @@ -6566,7 +6580,7 @@ void HistoryWidget::updateListSize(bool initial, bool loadedDown, const ScrollCh visibleAreaUpdated(); } - _attachMention.setBoundings(_scroll.geometry()); + _fieldAutocomplete->setBoundings(_scroll.geometry()); _toHistoryEnd.move((width() - _toHistoryEnd.width()) / 2, _scroll.y() + _scroll.height() - _toHistoryEnd.height() - st::historyToEndSkip); updateCollapseCommentsVisibility(); } @@ -6934,9 +6948,8 @@ void HistoryWidget::keyPressEvent(QKeyEvent *e) { } void HistoryWidget::onFieldTabbed() { - QString sel = _attachMention.isHidden() ? QString() : _attachMention.getSelected(); - if (!sel.isEmpty()) { - _field.onMentionHashtagOrBotCommandInsert(sel); + if (!_fieldAutocomplete->isHidden()) { + _fieldAutocomplete->chooseSelected(FieldAutocomplete::ChooseMethod::ByTab); } } @@ -7014,7 +7027,7 @@ void HistoryWidget::onInlineResultSend(InlineBots::Result *result, UserData *bot Local::writeRecentHashtagsAndBots(); } - if (!_attachMention.isHidden()) _attachMention.hideStart(); + if (!_fieldAutocomplete->isHidden()) _fieldAutocomplete->hideStart(); if (!_attachType.isHidden()) _attachType.hideStart(); if (!_emojiPan.isHidden()) _emojiPan.hideStart(); @@ -7171,14 +7184,14 @@ void HistoryWidget::sendExistingDocument(DocumentData *doc, const QString &capti App::historyRegRandom(randomId, newId); - if (_attachMention.stickersShown()) { + if (_fieldAutocomplete->stickersShown()) { clearFieldText(); _saveDraftText = true; _saveDraftStart = getms(); onDraftSave(); } - if (!_attachMention.isHidden()) _attachMention.hideStart(); + if (!_fieldAutocomplete->isHidden()) _fieldAutocomplete->hideStart(); if (!_attachType.isHidden()) _attachType.hideStart(); if (!_emojiPan.isHidden()) _emojiPan.hideStart(); @@ -7225,7 +7238,7 @@ void HistoryWidget::sendExistingPhoto(PhotoData *photo, const QString &caption) App::historyRegRandom(randomId, newId); - if (!_attachMention.isHidden()) _attachMention.hideStart(); + if (!_fieldAutocomplete->isHidden()) _fieldAutocomplete->hideStart(); if (!_attachType.isHidden()) _attachType.hideStart(); if (!_emojiPan.isHidden()) _emojiPan.hideStart(); @@ -7659,8 +7672,8 @@ void HistoryWidget::onCancel() { } else { onFieldBarCancel(); } - } else if (!_attachMention.isHidden()) { - _attachMention.hideStart(); + } else if (!_fieldAutocomplete->isHidden()) { + _fieldAutocomplete->hideStart(); } else { Ui::showChatsList(); emit cancelled(); @@ -7677,7 +7690,7 @@ void HistoryWidget::onFullPeerUpdated(PeerData *data) { } updateControlsVisibility(); } - onCheckMentionDropdown(); + onCheckFieldAutocomplete(); updateReportSpamStatus(); _list->updateBotInfo(); } diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h index 667471617..9994182a8 100644 --- a/Telegram/SourceFiles/historywidget.h +++ b/Telegram/SourceFiles/historywidget.h @@ -24,6 +24,14 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "ui/boxshadow.h" #include "dropdown.h" #include "history/history_common.h" +#include "history/field_autocomplete.h" + +namespace InlineBots { +namespace Layout { +class ItemBase; +} // namespace Layout +class Result; +} // namespace InlineBots class HistoryWidget; class HistoryInner : public TWidget, public AbstractTooltipShower { @@ -483,13 +491,6 @@ enum TextUpdateEventsFlags { TextUpdateEventsSendTyping = 0x02, }; -namespace InlineBots { -namespace Layout { -class ItemBase; -} // namespace Layout -class Result; -} // namespace InlineBots - class HistoryWidget : public TWidget, public RPCSender { Q_OBJECT @@ -759,7 +760,6 @@ public slots: void activate(); void onStickersUpdated(); - void onMentionHashtagOrBotCommandInsert(QString str); void onTextChange(); void onFieldTabbed(); @@ -777,7 +777,7 @@ public slots: void onFieldFocused(); void onFieldResize(); - void onCheckMentionDropdown(); + void onCheckFieldAutocomplete(); void onScrollTimer(); void onForwardSelected(); @@ -806,6 +806,8 @@ public slots: private slots: + void onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::ChooseMethod method); + void onMentionInsert(UserData *user); void onInlineBotCancel(); private: @@ -1003,7 +1005,7 @@ private: IconedButton _toHistoryEnd; CollapseButton _collapseComments; - MentionsDropdown _attachMention; + ChildWidget _fieldAutocomplete; UserData *_inlineBot = nullptr; QString _inlineBotUsername; diff --git a/Telegram/SourceFiles/mainwindow.h b/Telegram/SourceFiles/mainwindow.h index 341ccaf8e..16d7d5d00 100644 --- a/Telegram/SourceFiles/mainwindow.h +++ b/Telegram/SourceFiles/mainwindow.h @@ -230,6 +230,8 @@ public: void updateUnreadCounter(); + QImage iconWithCounter(int size, int count, style::color bg, bool smallIcon); + bool contentOverlapped(const QRect &globalRect); bool contentOverlapped(QWidget *w, QPaintEvent *e) { return contentOverlapped(QRect(w->mapToGlobal(e->rect().topLeft()), e->rect().size())); @@ -282,8 +284,6 @@ public slots: void onLogoutSure(); void updateGlobalMenu(); // for OS X top menu - QImage iconWithCounter(int size, int count, style::color bg, bool smallIcon); - void notifyUpdateAllPhotos(); void app_activateClickHandler(ClickHandlerPtr handler, Qt::MouseButton button); diff --git a/Telegram/SourceFiles/pspecific_win.cpp b/Telegram/SourceFiles/pspecific_win.cpp index 07155067f..82d93606d 100644 --- a/Telegram/SourceFiles/pspecific_win.cpp +++ b/Telegram/SourceFiles/pspecific_win.cpp @@ -851,7 +851,6 @@ namespace { _psShadowWindows.setColor(_shInactive); } if (Global::started()) { - QMetaObject::invokeMethod(App::wnd(), "updateCounter", Qt::QueuedConnection); App::wnd()->update(); } } return false; diff --git a/Telegram/SourceFiles/pspecific_winrt.cpp b/Telegram/SourceFiles/pspecific_winrt.cpp index 816e62d22..af7869a43 100644 --- a/Telegram/SourceFiles/pspecific_winrt.cpp +++ b/Telegram/SourceFiles/pspecific_winrt.cpp @@ -852,7 +852,6 @@ namespace { // _psShadowWindows.setColor(_shInactive); //} if (Global::started()) { - QMetaObject::invokeMethod(App::wnd(), "updateCounter", Qt::QueuedConnection); App::wnd()->update(); } } return false; diff --git a/Telegram/SourceFiles/ui/flattextarea.cpp b/Telegram/SourceFiles/ui/flattextarea.cpp index 182258438..b3f9d1171 100644 --- a/Telegram/SourceFiles/ui/flattextarea.cpp +++ b/Telegram/SourceFiles/ui/flattextarea.cpp @@ -360,7 +360,7 @@ QString FlatTextarea::getMentionHashtagBotCommandPart(bool &start) const { return QString(); } -void FlatTextarea::onMentionHashtagOrBotCommandInsert(QString str) { +void FlatTextarea::insertMentionHashtagOrBotCommand(const QString &data, const QString &entityTag) { QTextCursor c(textCursor()); int32 pos = c.position(); @@ -383,17 +383,16 @@ void FlatTextarea::onMentionHashtagOrBotCommandInsert(QString str) { if ((i == pos - p || (t.at(i - 1) == '/' ? t.at(i).isLetterOrNumber() : t.at(i).isLetter()) || t.at(i - 1) == '#') && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_'))) { c.setPosition(p + i - 1, QTextCursor::MoveAnchor); int till = p + i; - for (; (till < e) && (till - p - i + 1 < str.size()); ++till) { - if (t.at(till - p).toLower() != str.at(till - p - i + 1).toLower()) { + for (; (till < e) && (till - p - i + 1 < data.size()); ++till) { + if (t.at(till - p).toLower() != data.at(till - p - i + 1).toLower()) { break; } } - if (till - p - i + 1 == str.size() && till < e && t.at(till - p) == ' ') { + if (till - p - i + 1 == data.size() && till < e && t.at(till - p) == ' ') { ++till; } c.setPosition(till, QTextCursor::KeepAnchor); - c.insertText(str + ' '); - return; + break; } else if ((i == pos - p || t.at(i).isLetter()) && t.at(i - 1) == '@' && i > 2 && (t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_') && !mentionInCommand) { mentionInCommand = true; --i; @@ -406,7 +405,14 @@ void FlatTextarea::onMentionHashtagOrBotCommandInsert(QString str) { } break; } - c.insertText(str + ' '); + if (entityTag.isEmpty()) { + c.insertText(data + ' '); + } else { + QTextCharFormat fmt; + fmt.setForeground(st::defaultTextStyle.linkFg); + c.insertText(data, fmt); + c.insertText(qsl(" ")); + } } void FlatTextarea::getSingleEmojiFragment(QString &text, QTextFragment &fragment) const { diff --git a/Telegram/SourceFiles/ui/flattextarea.h b/Telegram/SourceFiles/ui/flattextarea.h index 701bedfba..65234f550 100644 --- a/Telegram/SourceFiles/ui/flattextarea.h +++ b/Telegram/SourceFiles/ui/flattextarea.h @@ -94,6 +94,8 @@ public: void setTextFast(const QString &text, bool clearUndoHistory = true); + void insertMentionHashtagOrBotCommand(const QString &data, const QString &entityTag = QString()); + public slots: void onTouchTimer(); @@ -104,8 +106,6 @@ public slots: void onUndoAvailable(bool avail); void onRedoAvailable(bool avail); - void onMentionHashtagOrBotCommandInsert(QString str); - signals: void resized(); diff --git a/Telegram/Telegram.vcxproj b/Telegram/Telegram.vcxproj index 91950d7b4..f49da0710 100644 --- a/Telegram/Telegram.vcxproj +++ b/Telegram/Telegram.vcxproj @@ -270,6 +270,10 @@ true true + + true + true + true true @@ -557,6 +561,10 @@ true true + + true + true + true true @@ -870,6 +878,10 @@ true true + + true + true + true true @@ -1086,6 +1098,7 @@ + @@ -1266,6 +1279,20 @@ + + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing field_autocomplete.h... + .\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" "-fstdafx.h" "-f../../SourceFiles/history/field_autocomplete.h" -DAL_LIBTYPE_STATIC -DCUSTOM_API_ID -DUNICODE -DWIN32 -DWIN64 -DHAVE_STDINT_H -DZLIB_WINAPI -DQT_NO_DEBUG -DNDEBUG -D_SCL_SECURE_NO_WARNINGS "-I.\SourceFiles" "-I.\GeneratedFiles" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtCore\5.6.0\QtCore" "-I$(QTDIR)\include\QtGui\5.6.0\QtGui" "-I.\..\..\Libraries\breakpad\src" "-I.\..\..\Libraries\lzma\C" "-I.\..\..\Libraries\libexif-0.6.20" "-I.\..\..\Libraries\zlib-1.2.8" "-I.\..\..\Libraries\ffmpeg" "-I.\..\..\Libraries\openal-soft\include" "-I.\ThirdParty\minizip" "-I.\..\..\Libraries\openssl\Release\include" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing field_autocomplete.h... + .\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" "-fstdafx.h" "-f../../SourceFiles/history/field_autocomplete.h" -DAL_LIBTYPE_STATIC -DUNICODE -DWIN32 -DWIN64 -DHAVE_STDINT_H -DZLIB_WINAPI -D_SCL_SECURE_NO_WARNINGS "-I.\SourceFiles" "-I.\GeneratedFiles" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtCore\5.6.0\QtCore" "-I$(QTDIR)\include\QtGui\5.6.0\QtGui" "-I.\..\..\Libraries\breakpad\src" "-I.\..\..\Libraries\lzma\C" "-I.\..\..\Libraries\libexif-0.6.20" "-I.\..\..\Libraries\zlib-1.2.8" "-I.\..\..\Libraries\ffmpeg" "-I.\..\..\Libraries\openal-soft\include" "-I.\ThirdParty\minizip" "-I.\..\..\Libraries\openssl_debug\Debug\include" + $(QTDIR)\bin\moc.exe;%(FullPath) + Moc%27ing field_autocomplete.h... + .\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp + "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\GeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" "-fstdafx.h" "-f../../SourceFiles/history/field_autocomplete.h" -DAL_LIBTYPE_STATIC -DUNICODE -DWIN32 -DWIN64 -DHAVE_STDINT_H -DZLIB_WINAPI -DQT_NO_DEBUG -DNDEBUG -D_SCL_SECURE_NO_WARNINGS "-I.\SourceFiles" "-I.\GeneratedFiles" "-I.\GeneratedFiles\$(ConfigurationName)\." "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtCore\5.6.0\QtCore" "-I$(QTDIR)\include\QtGui\5.6.0\QtGui" "-I.\..\..\Libraries\breakpad\src" "-I.\..\..\Libraries\lzma\C" "-I.\..\..\Libraries\libexif-0.6.20" "-I.\..\..\Libraries\zlib-1.2.8" "-I.\..\..\Libraries\ffmpeg" "-I.\..\..\Libraries\openal-soft\include" "-I.\ThirdParty\minizip" "-I.\..\..\Libraries\openssl\Release\include" + diff --git a/Telegram/Telegram.vcxproj.filters b/Telegram/Telegram.vcxproj.filters index 1d0e635e7..24a3ca5e0 100644 --- a/Telegram/Telegram.vcxproj.filters +++ b/Telegram/Telegram.vcxproj.filters @@ -1104,6 +1104,18 @@ SourceFiles\ui\style + + GeneratedFiles\Deploy + + + SourceFiles\history + + + GeneratedFiles\Debug + + + GeneratedFiles\Release + @@ -1513,6 +1525,9 @@ SourceFiles\core + + SourceFiles\history + From 21f462a77e6806083c969108390754a9cded9d41 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sat, 30 Apr 2016 20:04:14 +0300 Subject: [PATCH 03/20] Mention names support added to FlatTextarea, messages. Copy of mention names to clipboard done, pasting started. --- Telegram/SourceFiles/core/basic_types.h | 8 + .../SourceFiles/core/click_handler_types.cpp | 23 +++ .../SourceFiles/core/click_handler_types.h | 21 ++ Telegram/SourceFiles/core/version.h | 8 +- Telegram/SourceFiles/facades.cpp | 4 + Telegram/SourceFiles/facades.h | 8 + Telegram/SourceFiles/history.cpp | 3 +- Telegram/SourceFiles/historywidget.cpp | 46 ++++- Telegram/SourceFiles/historywidget.h | 2 + Telegram/SourceFiles/mainwidget.cpp | 51 ++--- Telegram/SourceFiles/mainwidget.h | 11 +- Telegram/SourceFiles/pspecific_mac.cpp | 8 +- Telegram/SourceFiles/ui/emoji_config.h | 12 +- Telegram/SourceFiles/ui/flattextarea.cpp | 187 +++++++++++++++-- Telegram/SourceFiles/ui/flattextarea.h | 52 +++-- Telegram/SourceFiles/ui/text/text.cpp | 146 +++++++------ Telegram/SourceFiles/ui/text/text_entity.cpp | 192 +++++++++++------- Telegram/SourceFiles/ui/text/text_entity.h | 29 ++- 18 files changed, 584 insertions(+), 227 deletions(-) diff --git a/Telegram/SourceFiles/core/basic_types.h b/Telegram/SourceFiles/core/basic_types.h index af03d5808..8c44ca5a0 100644 --- a/Telegram/SourceFiles/core/basic_types.h +++ b/Telegram/SourceFiles/core/basic_types.h @@ -213,6 +213,14 @@ private: }; +inline QString str_const_latin1_toString(const str_const &str) { + return QString::fromLatin1(str.c_str(), str.size()); +} + +inline QString str_const_utf8_toString(const str_const &str) { + return QString::fromUtf8(str.c_str(), str.size()); +} + template inline void accumulate_max(T &a, const T &b) { if (a < b) a = b; } diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index ad67985e7..3b5e82cad 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -126,6 +126,29 @@ EntityInText MentionClickHandler::getEntityInText(int offset, const QStringRef & return EntityInText(EntityInTextMention, offset, textPart.size()); } +void MentionNameClickHandler::onClick(Qt::MouseButton button) const { + if (button == Qt::LeftButton || button == Qt::MiddleButton) { + if (auto user = App::userLoaded(_userId)) { + Ui::showPeerProfile(user); + } + } +} + +EntityInText MentionNameClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { + auto data = QString::number(_userId) + '.' + QString::number(_accessHash); + return EntityInText(EntityInTextMentionName, offset, textPart.size(), data); +} + +QString MentionNameClickHandler::tooltip() const { + if (auto user = App::userLoaded(_userId)) { + auto name = App::peerName(user); + if (name != _text) { + return name; + } + } + return QString(); +} + QString HashtagClickHandler::copyToClipboardContextItemText() const { return lang(lng_context_copy_hashtag); } diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index fcb42d67d..316b1f275 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -147,6 +147,27 @@ private: }; +class MentionNameClickHandler : public ClickHandler { +public: + MentionNameClickHandler(QString text, UserId userId, uint64 accessHash) + : _text(text) + , _userId(userId) + , _accessHash(accessHash) { + } + + void onClick(Qt::MouseButton button) const override; + + EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + + QString tooltip() const override; + +private: + QString _text; + UserId _userId; + uint64 _accessHash; + +}; + class HashtagClickHandler : public TextClickHandler { public: HashtagClickHandler(const QString &tag) : _tag(tag) { diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index bdc6e5654..8d9baf02c 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -24,7 +24,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #define BETA_VERSION_MACRO (0ULL) -static constexpr int AppVersion = 9046; -static constexpr str_const AppVersionStr = "0.9.46"; -static constexpr bool AppAlphaVersion = true; -static constexpr uint64 AppBetaVersion = BETA_VERSION_MACRO; +constexpr int AppVersion = 9046; +constexpr str_const AppVersionStr = "0.9.46"; +constexpr bool AppAlphaVersion = true; +constexpr uint64 AppBetaVersion = BETA_VERSION_MACRO; diff --git a/Telegram/SourceFiles/facades.cpp b/Telegram/SourceFiles/facades.cpp index be5fba8a1..e6f6f7758 100644 --- a/Telegram/SourceFiles/facades.cpp +++ b/Telegram/SourceFiles/facades.cpp @@ -236,6 +236,10 @@ void autoplayMediaInlineAsync(const FullMsgId &msgId) { } } +void showPeerProfile(const PeerId &peer) { + if (MainWidget *m = App::main()) m->showPeerProfile(App::peer(peer)); +} + void showPeerHistory(const PeerId &peer, MsgId msgId, bool back) { if (MainWidget *m = App::main()) m->ui_showPeerHistory(peer, msgId, back); } diff --git a/Telegram/SourceFiles/facades.h b/Telegram/SourceFiles/facades.h index 412c0d65a..16d10cc20 100644 --- a/Telegram/SourceFiles/facades.h +++ b/Telegram/SourceFiles/facades.h @@ -67,6 +67,14 @@ void repaintInlineItem(const InlineBots::Layout::ItemBase *layout); bool isInlineItemVisible(const InlineBots::Layout::ItemBase *reader); void autoplayMediaInlineAsync(const FullMsgId &msgId); +void showPeerProfile(const PeerId &peer); +inline void showPeerProfile(const PeerData *peer) { + showPeerProfile(peer->id); +} +inline void showPeerProfile(const History *history) { + showPeerProfile(history->peer->id); +} + void showPeerHistory(const PeerId &peer, MsgId msgId, bool back = false); inline void showPeerHistory(const PeerData *peer, MsgId msgId, bool back = false) { showPeerHistory(peer->id, msgId, back); diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index 4c930bc71..e04c081e3 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -1029,7 +1029,8 @@ HistoryItem *History::createItem(const MTPMessage &msg, bool applyServiceAction, } if (badMedia == 1) { QString text(lng_message_unsupported(lt_link, qsl("https://desktop.telegram.org"))); - EntitiesInText entities = textParseEntities(text, _historyTextNoMonoOptions.flags); + EntitiesInText entities; + textParseEntities(text, _historyTextNoMonoOptions.flags, &entities); entities.push_front(EntityInText(EntityInTextItalic, 0, text.size())); result = HistoryMessage::create(this, m.vid.v, m.vflags.v, m.vreply_to_msg_id.v, m.vvia_bot_id.v, date(m.vdate), m.vfrom_id.v, text, entities); } else if (badMedia) { diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 824c07b3a..f6f3974f0 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -2735,6 +2735,25 @@ QPoint SilentToggle::tooltipPos() const { return QCursor::pos(); } +EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags) { + EntitiesInText result; + if (tags.isEmpty()) { + return result; + } + + result.reserve(tags.size()); + auto mentionStart = qstr("mention://user."); + for_const (auto &tag, tags) { + if (tag.id.startsWith(mentionStart)) { + auto match = QRegularExpression("^(\\d+\\.\\d+)(/|$)").match(tag.id.midRef(mentionStart.size())); + if (match.hasMatch()) { + result.push_back(EntityInText(EntityInTextMentionName, tag.offset, tag.length, match.captured(1))); + } + } + } + return result; +} + HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent) , _fieldBarCancel(this, st::replyCancel) , _scroll(this, st::historyScroll, false) @@ -2917,7 +2936,7 @@ void HistoryWidget::onMentionInsert(UserData *user) { QString replacement, entityTag; if (user->username.isEmpty()) { replacement = App::peerName(user); - entityTag = qsl("mention://peer.") + QString::number(user->id); + entityTag = qsl("mention://user.") + QString::number(user->bareId()) + '.' + QString::number(user->access); } else { replacement = '@' + user->username; } @@ -4737,8 +4756,11 @@ void HistoryWidget::saveEditMsg() { WebPageId webPageId = _previewCancelled ? CancelledWebPageId : ((_previewData && _previewData->pendingTill >= 0) ? _previewData->id : 0); - EntitiesInText sendingEntities, leftEntities; - QString sendingText, leftText = prepareTextWithEntities(_field.getLastText(), leftEntities, itemTextOptions(_history, App::self()).flags); + auto fieldText = _field.getLastText(); + auto fieldTags = _field.getLastTags(); + auto prepareFlags = itemTextOptions(_history, App::self()).flags; + EntitiesInText sendingEntities, leftEntities = entitiesFromFieldTags(fieldTags); + QString sendingText, leftText = prepareTextWithEntities(fieldText, prepareFlags, &leftEntities); if (!textSplit(sendingText, sendingEntities, leftText, leftEntities, MaxMessageSize)) { _field.selectAll(); @@ -4814,7 +4836,15 @@ void HistoryWidget::onSend(bool ctrlShiftEnter, MsgId replyTo) { WebPageId webPageId = _previewCancelled ? CancelledWebPageId : ((_previewData && _previewData->pendingTill >= 0) ? _previewData->id : 0); - App::main()->sendMessage(_history, _field.getLastText(), replyTo, _broadcast.checked(), _silent.checked(), webPageId); + MainWidget::MessageToSend message; + message.history = _history; + message.text = _field.getLastText(); + message.entities = _field.getLastTags(); + message.replyTo = replyTo; + message.broadcast = _broadcast.checked(); + message.silent = _silent.checked(); + message.webPageId = webPageId; + App::main()->sendMessage(message); clearFieldText(); _saveDraftText = true; @@ -5320,7 +5350,13 @@ void HistoryWidget::sendBotCommand(PeerData *peer, UserData *bot, const QString toSend += '@' + username; } - App::main()->sendMessage(_history, toSend, replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? replyTo : -1) : 0, false, false); + MainWidget::MessageToSend message; + message.history = _history; + message.text = toSend; + message.replyTo = replyTo ? ((!_peer->isUser()/* && (botStatus == 0 || botStatus == 2)*/) ? replyTo : -1) : 0; + message.broadcast = false; + message.silent = false; + App::main()->sendMessage(message); if (replyTo) { if (_replyToId == replyTo) { cancelReply(); diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h index 9994182a8..189afb9e4 100644 --- a/Telegram/SourceFiles/historywidget.h +++ b/Telegram/SourceFiles/historywidget.h @@ -486,6 +486,8 @@ public: }; +EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags); + enum TextUpdateEventsFlags { TextUpdateEventsSaveDraft = 0x01, TextUpdateEventsSendTyping = 0x02, diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 8565a53f6..e59cae222 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -1084,50 +1084,54 @@ void executeParsedCommand(const QString &command) { } } // namespace -void MainWidget::sendMessage(History *hist, const QString &text, MsgId replyTo, bool broadcast, bool silent, WebPageId webPageId) { - readServerHistory(hist, false); - _history->fastShowAtEnd(hist); +void MainWidget::sendMessage(const MessageToSend &message) { + auto history = message.history; + const auto &text = message.text; - if (!hist || !_history->canSendMessages(hist->peer)) { + readServerHistory(history, false); + _history->fastShowAtEnd(history); + + if (!history || !_history->canSendMessages(history->peer)) { return; } saveRecentHashtags(text); - EntitiesInText sendingEntities, leftEntities; - QString sendingText, leftText = prepareTextWithEntities(text, leftEntities, itemTextOptions(hist, App::self()).flags); + EntitiesInText sendingEntities, leftEntities = entitiesFromFieldTags(message.entities); + auto prepareFlags = itemTextOptions(history, App::self()).flags; + QString sendingText, leftText = prepareTextWithEntities(text, prepareFlags, &leftEntities); - QString command = parseCommandFromMessage(hist, text); + QString command = parseCommandFromMessage(history, text); HistoryItem *lastMessage = nullptr; - if (replyTo < 0) replyTo = _history->replyToId(); + MsgId replyTo = (message.replyTo < 0) ? _history->replyToId() : 0; while (command.isEmpty() && textSplit(sendingText, sendingEntities, leftText, leftEntities, MaxMessageSize)) { - FullMsgId newId(peerToChannel(hist->peer->id), clientMsgId()); + FullMsgId newId(peerToChannel(history->peer->id), clientMsgId()); uint64 randomId = rand_value(); - trimTextWithEntities(sendingText, sendingEntities); + trimTextWithEntities(sendingText, &sendingEntities); App::historyRegRandom(randomId, newId); - App::historyRegSentData(randomId, hist->peer->id, sendingText); + App::historyRegSentData(randomId, history->peer->id, sendingText); MTPstring msgText(MTP_string(sendingText)); - MTPDmessage::Flags flags = newMessageFlags(hist->peer) | MTPDmessage::Flag::f_entities; // unread, out + MTPDmessage::Flags flags = newMessageFlags(history->peer) | MTPDmessage::Flag::f_entities; // unread, out MTPmessages_SendMessage::Flags sendFlags = 0; if (replyTo) { flags |= MTPDmessage::Flag::f_reply_to_msg_id; sendFlags |= MTPmessages_SendMessage::Flag::f_reply_to_msg_id; } MTPMessageMedia media = MTP_messageMediaEmpty(); - if (webPageId == CancelledWebPageId) { + if (message.webPageId == CancelledWebPageId) { sendFlags |= MTPmessages_SendMessage::Flag::f_no_webpage; - } else if (webPageId) { - WebPageData *page = App::webPage(webPageId); + } else if (message.webPageId) { + WebPageData *page = App::webPage(message.webPageId); media = MTP_messageMediaWebPage(MTP_webPagePending(MTP_long(page->id), MTP_int(page->pendingTill))); flags |= MTPDmessage::Flag::f_media; } - bool channelPost = hist->peer->isChannel() && !hist->peer->isMegagroup() && hist->peer->asChannel()->canPublish() && (hist->peer->asChannel()->isBroadcast() || broadcast); - bool showFromName = !channelPost || hist->peer->asChannel()->addsSignature(); - bool silentPost = channelPost && silent; + bool channelPost = history->peer->isChannel() && !history->peer->isMegagroup() && history->peer->asChannel()->canPublish() && (history->peer->asChannel()->isBroadcast() || message.broadcast); + bool showFromName = !channelPost || history->peer->asChannel()->addsSignature(); + bool silentPost = channelPost && message.silent; if (channelPost) { sendFlags |= MTPmessages_SendMessage::Flag::f_broadcast; flags |= MTPDmessage::Flag::f_views; @@ -1143,13 +1147,13 @@ void MainWidget::sendMessage(History *hist, const QString &text, MsgId replyTo, if (!sentEntities.c_vector().v.isEmpty()) { sendFlags |= MTPmessages_SendMessage::Flag::f_entities; } - lastMessage = hist->addNewMessage(MTP_message(MTP_flags(flags), MTP_int(newId.msg), MTP_int(showFromName ? MTP::authedId() : 0), peerToMTP(hist->peer->id), MTPnullFwdHeader, MTPint(), MTP_int(replyTo), MTP_int(unixtime()), msgText, media, MTPnullMarkup, localEntities, MTP_int(1), MTPint()), NewMessageUnread); - hist->sendRequestId = MTP::send(MTPmessages_SendMessage(MTP_flags(sendFlags), hist->peer->input, MTP_int(replyTo), msgText, MTP_long(randomId), MTPnullMarkup, sentEntities), rpcDone(&MainWidget::sentUpdatesReceived, randomId), rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); + lastMessage = history->addNewMessage(MTP_message(MTP_flags(flags), MTP_int(newId.msg), MTP_int(showFromName ? MTP::authedId() : 0), peerToMTP(history->peer->id), MTPnullFwdHeader, MTPint(), MTP_int(replyTo), MTP_int(unixtime()), msgText, media, MTPnullMarkup, localEntities, MTP_int(1), MTPint()), NewMessageUnread); + history->sendRequestId = MTP::send(MTPmessages_SendMessage(MTP_flags(sendFlags), history->peer->input, MTP_int(replyTo), msgText, MTP_long(randomId), MTPnullMarkup, sentEntities), rpcDone(&MainWidget::sentUpdatesReceived, randomId), rpcFail(&MainWidget::sendMessageFail), 0, 0, history->sendRequestId); } - hist->lastSentMsg = lastMessage; + history->lastSentMsg = lastMessage; - finishForwarding(hist, broadcast, silent); + finishForwarding(history, message.broadcast, message.silent); executeParsedCommand(command); } @@ -1728,7 +1732,8 @@ void MainWidget::dialogsCancelled() { void MainWidget::serviceNotification(const QString &msg, const MTPMessageMedia &media) { MTPDmessage::Flags flags = MTPDmessage::Flag::f_unread | MTPDmessage::Flag::f_entities | MTPDmessage::Flag::f_from_id; QString sendingText, leftText = msg; - EntitiesInText sendingEntities, leftEntities = textParseEntities(leftText, _historyTextNoMonoOptions.flags); + EntitiesInText sendingEntities, leftEntities; + textParseEntities(leftText, _historyTextNoMonoOptions.flags, &leftEntities); HistoryItem *item = 0; while (textSplit(sendingText, sendingEntities, leftText, leftEntities, MaxMessageSize)) { MTPVector localEntities = linksToMTP(sendingEntities); diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 537b56be5..17af829aa 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -281,7 +281,16 @@ public: Dialogs::IndexedList *contactsList(); Dialogs::IndexedList *dialogsList(); - void sendMessage(History *hist, const QString &text, MsgId replyTo, bool broadcast, bool silent, WebPageId webPageId = 0); + struct MessageToSend { + History *history = nullptr; + QString text; + FlatTextarea::TagList entities; + MsgId replyTo = 0; + bool broadcast = false; + bool silent = false; + WebPageId webPageId = 0; + }; + void sendMessage(const MessageToSend &message); void saveRecentHashtags(const QString &text); void readServerHistory(History *history, bool force = true); diff --git a/Telegram/SourceFiles/pspecific_mac.cpp b/Telegram/SourceFiles/pspecific_mac.cpp index 91b2124b1..5e5147f76 100644 --- a/Telegram/SourceFiles/pspecific_mac.cpp +++ b/Telegram/SourceFiles/pspecific_mac.cpp @@ -84,7 +84,13 @@ void MacPrivate::notifyClicked(unsigned long long peer, int msgid) { void MacPrivate::notifyReplied(unsigned long long peer, int msgid, const char *str) { History *history = App::history(PeerId(peer)); - App::main()->sendMessage(history, QString::fromUtf8(str), (msgid > 0 && !history->peer->isUser()) ? msgid : 0, false, false); + MainWidget::MessageToSend message; + message.history = history; + message.text = QString::fromUtf8(str); + message.replyTo = (msgid > 0 && !history->peer->isUser()) ? msgid : 0; + message.broadcast = false; + message.silent = false; + App::main()->sendMessage(message); } PsMainWindow::PsMainWindow(QWidget *parent) : QMainWindow(parent), diff --git a/Telegram/SourceFiles/ui/emoji_config.h b/Telegram/SourceFiles/ui/emoji_config.h index 64cc5a974..ed7bae85e 100644 --- a/Telegram/SourceFiles/ui/emoji_config.h +++ b/Telegram/SourceFiles/ui/emoji_config.h @@ -165,9 +165,9 @@ inline bool emojiEdge(const QChar *ch) { return false; } -inline void appendPartToResult(QString &result, const QChar *start, const QChar *from, const QChar *to, EntitiesInText &entities) { +inline void appendPartToResult(QString &result, const QChar *start, const QChar *from, const QChar *to, EntitiesInText *inOutEntities) { if (to > from) { - for (auto &entity : entities) { + for (auto &entity : *inOutEntities) { if (entity.offset() >= to - start) break; if (entity.offset() + entity.length() < from - start) continue; if (entity.offset() >= from - start) { @@ -181,9 +181,9 @@ inline void appendPartToResult(QString &result, const QChar *start, const QChar } } -inline QString replaceEmojis(const QString &text, EntitiesInText &entities) { +inline QString replaceEmojis(const QString &text, EntitiesInText *inOutEntities) { QString result; - auto currentEntity = entities.begin(), entitiesEnd = entities.end(); + auto currentEntity = inOutEntities->begin(), entitiesEnd = inOutEntities->end(); const QChar *emojiStart = text.constData(), *emojiEnd = emojiStart, *e = text.constData() + text.size(); bool canFindEmoji = true; for (const QChar *ch = emojiEnd; ch != e;) { @@ -204,7 +204,7 @@ inline QString replaceEmojis(const QString &text, EntitiesInText &entities) { ) { if (result.isEmpty()) result.reserve(text.size()); - appendPartToResult(result, emojiStart, emojiEnd, ch, entities); + appendPartToResult(result, emojiStart, emojiEnd, ch, inOutEntities); if (emoji->color) { EmojiColorVariants::const_iterator it = cEmojiVariants().constFind(emoji->code); @@ -232,7 +232,7 @@ inline QString replaceEmojis(const QString &text, EntitiesInText &entities) { } if (result.isEmpty()) return text; - appendPartToResult(result, emojiStart, emojiEnd, e, entities); + appendPartToResult(result, emojiStart, emojiEnd, e, inOutEntities); return result; } diff --git a/Telegram/SourceFiles/ui/flattextarea.cpp b/Telegram/SourceFiles/ui/flattextarea.cpp index b3f9d1171..f4ceae390 100644 --- a/Telegram/SourceFiles/ui/flattextarea.cpp +++ b/Telegram/SourceFiles/ui/flattextarea.cpp @@ -23,6 +23,64 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "mainwindow.h" +namespace { + +QByteArray serializeTagsList(const FlatTextarea::TagList &tags) { + if (tags.isEmpty()) { + return QByteArray(); + } + + QByteArray tagsSerialized; + { + QBuffer buffer(&tagsSerialized); + buffer.open(QIODevice::WriteOnly); + QDataStream stream(&buffer); + stream.setVersion(QDataStream::Qt_5_1); + stream << qint32(tags.size()); + for_const (auto &tag, tags) { + stream << qint32(tag.offset) << qint32(tag.length) << tag.id; + } + } + return tagsSerialized; +} + +FlatTextarea::TagList deserializeTagsList(QByteArray data, int textSize) { + FlatTextarea::TagList result; + + QBuffer buffer(&data); + buffer.open(QIODevice::ReadOnly); + + QDataStream stream(&buffer); + stream.setVersion(QDataStream::Qt_5_1); + + qint32 tagCount = 0; + stream >> tagCount; + if (stream.status() != QDataStream::Ok) { + return result; + } + if (tagCount <= 0 || tagCount > textSize) { + return result; + } + + for (int i = 0; i < tagCount; ++i) { + qint32 offset = 0, length = 0; + QString id; + stream >> offset >> length >> id; + if (stream.status() != QDataStream::Ok) { + return result; + } + if (offset < 0 || length <= 0 || offset + length > textSize) { + return result; + } + result.push_back({ offset, length, id }); + } + return result; +} + +constexpr str_const TagsMimeType = "application/x-td-field-tags"; + +} // namespace + FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &pholder, const QString &v) : QTextEdit(parent) , _oldtext(v) , _phVisible(!v.length()) @@ -62,7 +120,7 @@ FlatTextarea::FlatTextarea(QWidget *parent, const style::flatTextarea &st, const _touchTimer.setSingleShot(true); connect(&_touchTimer, SIGNAL(timeout()), this, SLOT(onTouchTimer())); - connect(document(), SIGNAL(contentsChange(int, int, int)), this, SLOT(onDocumentContentsChange(int, int, int))); + connect(document(), SIGNAL(contentsChange(int,int,int)), this, SLOT(onDocumentContentsChange(int,int,int))); connect(document(), SIGNAL(contentsChanged()), this, SLOT(onDocumentContentsChanged())); connect(this, SIGNAL(undoAvailable(bool)), this, SLOT(onUndoAvailable(bool))); connect(this, SIGNAL(redoAvailable(bool)), this, SLOT(onRedoAvailable(bool))); @@ -360,7 +418,7 @@ QString FlatTextarea::getMentionHashtagBotCommandPart(bool &start) const { return QString(); } -void FlatTextarea::insertMentionHashtagOrBotCommand(const QString &data, const QString &entityTag) { +void FlatTextarea::insertMentionHashtagOrBotCommand(const QString &data, const QString &tagId) { QTextCursor c(textCursor()); int32 pos = c.position(); @@ -405,13 +463,15 @@ void FlatTextarea::insertMentionHashtagOrBotCommand(const QString &data, const Q } break; } - if (entityTag.isEmpty()) { + if (tagId.isEmpty()) { c.insertText(data + ' '); } else { - QTextCharFormat fmt; - fmt.setForeground(st::defaultTextStyle.linkFg); - c.insertText(data, fmt); - c.insertText(qsl(" ")); + QTextCharFormat defaultFormat = c.charFormat(), linkFormat = defaultFormat; + linkFormat.setAnchor(true); + linkFormat.setAnchorName(tagId + '/' + QString::number(rand_value())); + linkFormat.setForeground(st::defaultTextStyle.linkFg); + c.insertText(data, linkFormat); + c.insertText(qsl(" "), defaultFormat); } } @@ -471,12 +531,59 @@ void FlatTextarea::removeSingleEmoji() { } } -QString FlatTextarea::getText(int32 start, int32 end) const { +namespace { + +class TagAccumulator { +public: + TagAccumulator(FlatTextarea::TagList *tags) : _tags(tags) { + } + + bool changed() const { + return _changed; + } + + void feed(const QString &tagId, int currentPosition) { + if (tagId == _currentTagId) return; + + if (!_currentTagId.isEmpty()) { + FlatTextarea::Tag tag = { + _currentStart, + currentPosition - _currentStart, + _currentTagId, + }; + if (_currentTag >= _tags->size()) { + _changed = true; + _tags->push_back(tag); + } else if (_tags->at(_currentTag) != tag) { + _changed = true; + (*_tags)[_currentTag] = tag; + } + ++_currentTag; + } + _currentTagId = tagId; + _currentStart = currentPosition; + }; + +private: + FlatTextarea::TagList *_tags; + bool _changed = false; + + int _currentTag = 0; + int _currentStart = 0; + QString _currentTagId; + +}; + +} // namespace + +QString FlatTextarea::getText(int start, int end, TagList *outTagsList, bool *outTagsChanged) const { if (end >= 0 && end <= start) return QString(); if (start < 0) start = 0; bool full = (start == 0) && (end < 0); + TagAccumulator tagAccumulator(outTagsList); + QTextDocument *doc(document()); QTextBlock from = full ? doc->begin() : doc->findBlock(start), till = (end < 0) ? doc->end() : doc->findBlock(end); if (till.isValid()) till = till.next(); @@ -491,17 +598,28 @@ QString FlatTextarea::getText(int32 start, int32 end) const { end = possibleLen; } - for (QTextBlock b = from; b != till; b = b.next()) { - for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) { + bool tillFragmentEnd = full; + for (auto b = from; b != till; b = b.next()) { + for (auto iter = b.begin(); !iter.atEnd(); ++iter) { QTextFragment fragment(iter.fragment()); if (!fragment.isValid()) continue; int32 p = full ? 0 : fragment.position(), e = full ? 0 : (p + fragment.length()); if (!full) { - if (p >= end || e <= start) { + tillFragmentEnd = (e <= end); + if (p == end && outTagsList) { + tagAccumulator.feed(fragment.charFormat().anchorName(), result.size()); + } + if (p >= end) { + break; + } + if (e <= start) { continue; } } + if (outTagsList && (full || p >= start)) { + tagAccumulator.feed(fragment.charFormat().anchorName(), result.size()); + } QTextCharFormat f = fragment.charFormat(); QString emojiText; @@ -545,6 +663,13 @@ QString FlatTextarea::getText(int32 start, int32 end) const { result.append('\n'); } result.chop(1); + + if (outTagsList) { + if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size()); + if (outTagsChanged) { + *outTagsChanged = tagAccumulator.changed(); + } + } return result; } @@ -666,11 +791,17 @@ QStringList FlatTextarea::linksList() const { } void FlatTextarea::insertFromMimeData(const QMimeData *source) { + auto mime = str_const_latin1_toString(TagsMimeType); + if (source->hasFormat(mime)) { + auto tagsData = source->data(mime); + _settingTags = deserializeTagsList(tagsData, source->text().size()); + } else { + _settingTags.clear(); + } QTextEdit::insertFromMimeData(source); - if (!_inDrop) emit spacedReturnedPasted(); -} + _settingTags.clear(); -void FlatTextarea::correctValue(const QString &was, QString &now) { + if (!_inDrop) emit spacedReturnedPasted(); } void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) { @@ -680,6 +811,7 @@ void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) { imageFormat.setHeight(eh / cIntRetinaFactor()); imageFormat.setName(qsl("emoji://e.") + QString::number(emojiKey(emoji), 16)); imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline); + imageFormat.setAnchorName(c.charFormat().anchorName()); static QString objectReplacement(QChar::ObjectReplacementCharacter); c.insertText(objectReplacement, imageFormat); @@ -703,7 +835,7 @@ void FlatTextarea::checkContentHeight() { void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) { int32 replacePosition = -1, replaceLen = 0; - const EmojiData *emoji = 0; + const EmojiData *emoji = nullptr; static QString regular = qsl("Open Sans"), semibold = qsl("Open Sans Semibold"); bool checkTilde = !cRetina() && (font().pixelSize() == 13) && (font().family() == regular), wasTildeFragment = false; @@ -731,7 +863,7 @@ void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) { QString t(fragment.text()); const QChar *ch = t.constData(), *e = ch + t.size(); for (; ch != e; ++ch, ++fp) { - int32 emojiLen = 0; + int emojiLen = 0; emoji = emojiFromText(ch, e, &emojiLen); if (emoji) { if (replacePosition >= 0) { @@ -767,9 +899,12 @@ void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) { if (replacePosition >= 0) break; } if (replacePosition >= 0) { + // Optimization: with null page size document does not re-layout + // on each insertText / mergeCharFormat. if (!document()->pageSize().isNull()) { document()->setPageSize(QSizeF(0, 0)); } + QTextCursor c(doc->docHandle(), replacePosition); c.setPosition(replacePosition + replaceLen, QTextCursor::KeepAnchor); if (emoji) { @@ -782,7 +917,7 @@ void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) { charsAdded -= replacePosition + replaceLen - position; position = replacePosition + (emoji ? 1 : replaceLen); - emoji = 0; + emoji = nullptr; replacePosition = -1; } else { break; @@ -821,7 +956,7 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int if (!_links.isEmpty()) { bool changed = false; - for (LinkRanges::iterator i = _links.begin(); i != _links.end();) { + for (auto i = _links.begin(); i != _links.end();) { if (i->first + i->second <= position) { ++i; } else if (i->first >= position + charsRemoved) { @@ -867,11 +1002,15 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int void FlatTextarea::onDocumentContentsChanged() { if (_correcting) return; - QString curText(getText()); + auto tagsChanged = false; + auto curText = getText(0, -1, &_oldtags, &tagsChanged); + _correcting = true; - correctValue(_oldtext, curText); + correctValue(_oldtext, curText, _oldtags); _correcting = false; - if (_oldtext != curText) { + + bool textOrTagsChanged = tagsChanged || (_oldtext != curText); + if (textOrTagsChanged) { _oldtext = curText; emit changed(); checkContentHeight(); @@ -934,7 +1073,11 @@ QMimeData *FlatTextarea::createMimeDataFromSelection() const { QTextCursor c(textCursor()); int32 start = c.selectionStart(), end = c.selectionEnd(); if (end > start) { - result->setText(getText(start, end)); + TagList tags; + result->setText(getText(start, end, &tags, nullptr)); + if (!tags.isEmpty()) { + result->setData(qsl("application/x-td-field-tags"), serializeTagsList(tags)); + } } return result; } diff --git a/Telegram/SourceFiles/ui/flattextarea.h b/Telegram/SourceFiles/ui/flattextarea.h index 65234f550..659d724e1 100644 --- a/Telegram/SourceFiles/ui/flattextarea.h +++ b/Telegram/SourceFiles/ui/flattextarea.h @@ -33,17 +33,6 @@ public: FlatTextarea(QWidget *parent, const style::flatTextarea &st, const QString &ph = QString(), const QString &val = QString()); - bool viewportEvent(QEvent *e) override; - void touchEvent(QTouchEvent *e); - void paintEvent(QPaintEvent *e) override; - void focusInEvent(QFocusEvent *e) override; - void focusOutEvent(QFocusEvent *e) override; - void keyPressEvent(QKeyEvent *e) override; - void resizeEvent(QResizeEvent *e) override; - void mousePressEvent(QMouseEvent *e) override; - void dropEvent(QDropEvent *e) override; - void contextMenuEvent(QContextMenuEvent *e) override; - void setMaxLength(int32 maxLength); void setMinHeight(int32 minHeight); void setMaxHeight(int32 maxHeight); @@ -51,6 +40,7 @@ public: const QString &getLastText() const { return _oldtext; } + void setPlaceholder(const QString &ph, int32 afterSymbols = 0); void updatePlaceholder(); void finishPlaceholder(); @@ -94,7 +84,15 @@ public: void setTextFast(const QString &text, bool clearUndoHistory = true); - void insertMentionHashtagOrBotCommand(const QString &data, const QString &entityTag = QString()); + struct Tag { + int offset, length; + QString id; + }; + using TagList = QVector; + const TagList &getLastTags() const { + return _oldtags; + } + void insertMentionHashtagOrBotCommand(const QString &data, const QString &tagId = QString()); public slots: @@ -118,8 +116,19 @@ signals: protected: - QString getText(int32 start = 0, int32 end = -1) const; - virtual void correctValue(const QString &was, QString &now); + bool viewportEvent(QEvent *e) override; + void touchEvent(QTouchEvent *e); + void paintEvent(QPaintEvent *e) override; + void focusInEvent(QFocusEvent *e) override; + void focusOutEvent(QFocusEvent *e) override; + void keyPressEvent(QKeyEvent *e) override; + void resizeEvent(QResizeEvent *e) override; + void mousePressEvent(QMouseEvent *e) override; + void dropEvent(QDropEvent *e) override; + void contextMenuEvent(QContextMenuEvent *e) override; + + virtual void correctValue(const QString &was, QString &now, TagList &nowTags) { + } void insertEmoji(EmojiPtr emoji, QTextCursor c); @@ -129,6 +138,10 @@ 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; + void getSingleEmojiFragment(QString &text, QTextFragment &fragment) const; void processDocumentContentsChange(int position, int charsAdded); bool heightAutoupdated(); @@ -139,6 +152,7 @@ private: SubmitSettings _submitSettings = SubmitSettings::Enter; QString _ph, _phelided, _oldtext; + TagList _oldtags; int _phAfter = 0; bool _phVisible; anim::ivalue a_phLeft; @@ -146,6 +160,9 @@ private: anim::cvalue a_phColor; Animation _a_appearance; + // Tags list which we should apply while setText() call or insert from mime data. + TagList _settingTags; + style::flatTextarea _st; bool _undoAvailable = false; @@ -167,3 +184,10 @@ private: typedef QList LinkRanges; LinkRanges _links; }; + +inline bool operator==(const FlatTextarea::Tag &a, const FlatTextarea::Tag &b) { + return (a.offset == b.offset) && (a.length == b.length) && (a.id == b.id); +} +inline bool operator!=(const FlatTextarea::Tag &a, const FlatTextarea::Tag &b) { + return !(a == b); +} diff --git a/Telegram/SourceFiles/ui/text/text.cpp b/Telegram/SourceFiles/ui/text/text.cpp index 979cb3735..291280ceb 100644 --- a/Telegram/SourceFiles/ui/text/text.cpp +++ b/Telegram/SourceFiles/ui/text/text.cpp @@ -246,27 +246,6 @@ public: createBlock(); } - void getLinkData(const QString &original, QString &result, int32 &fullDisplayed) { - if (!original.isEmpty() && original.at(0) == '/') { - result = original; - fullDisplayed = -4; // bot command - } else if (!original.isEmpty() && original.at(0) == '@') { - result = original; - fullDisplayed = -3; // mention - } else if (!original.isEmpty() && original.at(0) == '#') { - result = original; - fullDisplayed = -2; // hashtag - } else if (reMailStart().match(original).hasMatch()) { - result = original; - fullDisplayed = -1; // email - } else { - QUrl url(original), good(url.isValid() ? url.toEncoded() : ""); - QString readable = good.isValid() ? good.toDisplayString() : original; - result = _t->_font->elided(readable, st::linkCropLimit); - fullDisplayed = (result == readable) ? 1 : 0; - } - } - bool checkCommand() { bool result = false; for (QChar c = ((ptr < end) ? *ptr : 0); c == TextCommand; c = ((ptr < end) ? *ptr : 0)) { @@ -300,17 +279,11 @@ public: return; } - bool lnk = false; int32 startFlags = 0; - int32 fullDisplayed; - QString lnkUrl, lnkText; - auto type = waitingEntity->type(); - if (type == EntityInTextCustomUrl) { - lnk = true; - lnkUrl = waitingEntity->data(); - lnkText = QString(start + waitingEntity->offset(), waitingEntity->length()); - fullDisplayed = -5; - } else if (type == EntityInTextBold) { + QString linkData, linkText; + auto type = waitingEntity->type(), linkType = EntityInTextInvalid; + LinkDisplayStatus linkDisplayStatus = LinkDisplayedFull; + if (type == EntityInTextBold) { startFlags = TextBlockFSemibold; } else if (type == EntityInTextItalic) { startFlags = TextBlockFItalic; @@ -322,21 +295,36 @@ public: if (!_t->_blocks.isEmpty() && _t->_blocks.back()->type() != TextBlockTNewline) { createNewlineBlock(); } - } else { - lnk = true; - lnkUrl = QString(start + waitingEntity->offset(), waitingEntity->length()); - getLinkData(lnkUrl, lnkText, fullDisplayed); + } else if (type == EntityInTextUrl + || type == EntityInTextEmail + || type == EntityInTextMention + || type == EntityInTextHashtag + || type == EntityInTextBotCommand) { + linkType = type; + linkData = QString(start + waitingEntity->offset(), waitingEntity->length()); + if (linkType == EntityInTextUrl) { + computeLinkText(linkData, &linkText, &linkDisplayStatus); + } else { + linkText = linkData; + } + } else if (type == EntityInTextCustomUrl || type == EntityInTextMentionName) { + linkType = type; + linkData = waitingEntity->data(); + linkText = QString(start + waitingEntity->offset(), waitingEntity->length()); } - if (lnk) { + if (linkType != EntityInTextInvalid) { createBlock(); - links.push_back(TextLinkData(lnkUrl, fullDisplayed)); + links.push_back(TextLinkData(linkType, linkText, linkData, linkDisplayStatus)); lnkIndex = 0x8000 + links.size(); - _t->_text += lnkText; - ptr = start + waitingEntity->offset() + waitingEntity->length(); + for (auto entityEnd = start + waitingEntity->offset() + waitingEntity->length(); ptr < entityEnd; ++ptr) { + parseCurrentChar(); + parseEmojiFromCurrent(); + if (sumFinished || _t->_text.size() >= 0x8000) break; // 32k max + } createBlock(); lnkIndex = 0; @@ -461,7 +449,7 @@ public: case TextCommandLinkText: { createBlock(); int32 len = ptr->unicode(); - links.push_back(TextLinkData(QString(++ptr, len), false)); + links.push_back(TextLinkData(EntityInTextCustomUrl, QString(), QString(++ptr, len), LinkDisplayedFull)); lnkIndex = 0x8000 + links.size(); } break; @@ -565,7 +553,7 @@ public: lnkIndex(0), stopAfterWidth(QFIXED_MAX) { if (options.flags & TextParseLinks) { - entities = textParseEntities(src, options.flags, rich); + textParseEntities(src, options.flags, &entities, rich); } parse(options); } @@ -664,32 +652,44 @@ public: lnkIndex = maxLnkIndex + (b->lnkIndex() - 0x8000); if (_t->_links.size() < lnkIndex) { _t->_links.resize(lnkIndex); - const TextLinkData &data(links[lnkIndex - maxLnkIndex - 1]); - ClickHandlerPtr lnk; - if (data.fullDisplayed < -4) { // hidden link - lnk.reset(new HiddenUrlClickHandler(data.url)); - } else if (data.fullDisplayed < -3) { // bot command - lnk.reset(new BotCommandClickHandler(data.url)); - } else if (data.fullDisplayed < -2) { // mention + const TextLinkData &link(links[lnkIndex - maxLnkIndex - 1]); + ClickHandlerPtr handler; + switch (link.type) { + case EntityInTextCustomUrl: handler.reset(new HiddenUrlClickHandler(link.data)); break; + case EntityInTextEmail: + case EntityInTextUrl: handler.reset(new UrlClickHandler(link.data, link.displayStatus == LinkDisplayedFull)); break; + case EntityInTextBotCommand: handler.reset(new BotCommandClickHandler(link.data)); break; + case EntityInTextHashtag: if (options.flags & TextTwitterMentions) { - lnk.reset(new UrlClickHandler(qsl("https://twitter.com/") + data.url.mid(1), true)); + handler.reset(new UrlClickHandler(qsl("https://twitter.com/hashtag/") + link.data.mid(1) + qsl("?src=hash"), true)); } else if (options.flags & TextInstagramMentions) { - lnk.reset(new UrlClickHandler(qsl("https://instagram.com/") + data.url.mid(1) + '/', true)); + handler.reset(new UrlClickHandler(qsl("https://instagram.com/explore/tags/") + link.data.mid(1) + '/', true)); } else { - lnk.reset(new MentionClickHandler(data.url)); + handler.reset(new HashtagClickHandler(link.data)); } - } else if (data.fullDisplayed < -1) { // hashtag + break; + case EntityInTextMention: if (options.flags & TextTwitterMentions) { - lnk.reset(new UrlClickHandler(qsl("https://twitter.com/hashtag/") + data.url.mid(1) + qsl("?src=hash"), true)); + handler.reset(new UrlClickHandler(qsl("https://twitter.com/") + link.data.mid(1), true)); } else if (options.flags & TextInstagramMentions) { - lnk.reset(new UrlClickHandler(qsl("https://instagram.com/explore/tags/") + data.url.mid(1) + '/', true)); + handler.reset(new UrlClickHandler(qsl("https://instagram.com/") + link.data.mid(1) + '/', true)); } else { - lnk.reset(new HashtagClickHandler(data.url)); + handler.reset(new MentionClickHandler(link.data)); } - } else { // email or url - lnk.reset(new UrlClickHandler(data.url, data.fullDisplayed != 0)); + break; + case EntityInTextMentionName: { + UserId userId = 0; + uint64 accessHash = 0; + if (mentionNameToFields(link.data, &userId, &accessHash)) { + handler.reset(new MentionNameClickHandler(link.text, userId, accessHash)); + } else { + LOG(("Bad mention name: %1").arg(link.data)); + } + } break; } - _t->setLink(lnkIndex, lnk); + + t_assert(!handler.isNull()); + _t->setLink(lnkIndex, handler); } b->setLnkIndex(lnkIndex); } @@ -701,6 +701,30 @@ public: private: + enum LinkDisplayStatus { + LinkDisplayedFull, + LinkDisplayedElided, + }; + struct TextLinkData { + TextLinkData() = default; + TextLinkData(EntityInTextType type, const QString &text, const QString &data, LinkDisplayStatus displayStatus) + : type(type) + , text(text) + , data(data) + , displayStatus(displayStatus) { + } + EntityInTextType type = EntityInTextInvalid; + QString text, data; + LinkDisplayStatus displayStatus = LinkDisplayedFull; + }; + + void computeLinkText(const QString &linkData, QString *outLinkText, LinkDisplayStatus *outDisplayStatus) { + QUrl url(linkData), good(url.isValid() ? url.toEncoded() : ""); + QString readable = good.isValid() ? good.toDisplayString() : linkData; + *outLinkText = _t->_font->elided(readable, st::linkCropLimit); + *outDisplayStatus = (*outLinkText == readable) ? LinkDisplayedFull : LinkDisplayedElided; + } + Text *_t; QString src; const QChar *start, *end, *ptr; @@ -709,12 +733,6 @@ private: EntitiesInText entities; EntitiesInText::const_iterator waitingEntity, entitiesEnd; - struct TextLinkData { - TextLinkData(const QString &url = QString(), int32 fullDisplayed = 1) : url(url), fullDisplayed(fullDisplayed) { - } - QString url; - int32 fullDisplayed; // -5 - custom text link, -4 - bot command, -3 - mention, -2 - hashtag, -1 - email - }; typedef QVector TextLinks; TextLinks links; diff --git a/Telegram/SourceFiles/ui/text/text_entity.cpp b/Telegram/SourceFiles/ui/text/text_entity.cpp index 9afb72ad2..7e4170cb6 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.cpp +++ b/Telegram/SourceFiles/ui/text/text_entity.cpp @@ -1364,6 +1364,21 @@ EntitiesInText entitiesFromMTP(const QVector &entities) { case mtpc_messageEntityHashtag: { const auto &d(entity.c_messageEntityHashtag()); result.push_back(EntityInText(EntityInTextHashtag, d.voffset.v, d.vlength.v)); } break; case mtpc_messageEntityMention: { const auto &d(entity.c_messageEntityMention()); result.push_back(EntityInText(EntityInTextMention, d.voffset.v, d.vlength.v)); } break; case mtpc_messageEntityMentionName: { const auto &d(entity.c_messageEntityMentionName()); result.push_back(EntityInText(EntityInTextMentionName, d.voffset.v, d.vlength.v, QString::number(d.vuser_id.v))); } break; + case mtpc_inputMessageEntityMentionName: { + const auto &d(entity.c_inputMessageEntityMentionName()); + auto data = ([&d]() -> QString { + if (d.vuser_id.type() == mtpc_inputUserSelf) { + return QString::number(MTP::authedId()); + } else if (d.vuser_id.type() == mtpc_inputUser) { + const auto &user(d.vuser_id.c_inputUser()); + return QString::number(user.vuser_id.v) + '.' + QString::number(user.vaccess_hash.v); + } + return QString(); + })(); + if (!data.isEmpty()) { + result.push_back(EntityInText(EntityInTextMentionName, d.voffset.v, d.vlength.v, data)); + } + } break; case mtpc_messageEntityBotCommand: { const auto &d(entity.c_messageEntityBotCommand()); result.push_back(EntityInText(EntityInTextBotCommand, d.voffset.v, d.vlength.v)); } break; case mtpc_messageEntityBold: { const auto &d(entity.c_messageEntityBold()); result.push_back(EntityInText(EntityInTextBold, d.voffset.v, d.vlength.v)); } break; case mtpc_messageEntityItalic: { const auto &d(entity.c_messageEntityItalic()); result.push_back(EntityInText(EntityInTextItalic, d.voffset.v, d.vlength.v)); } break; @@ -1380,7 +1395,12 @@ MTPVector linksToMTP(const EntitiesInText &links, bool sending auto &v = result._vector().v; for_const (const auto &link, links) { if (link.length() <= 0) continue; - if (sending && link.type() != EntityInTextCode && link.type() != EntityInTextPre) continue; + if (sending + && link.type() != EntityInTextCode + && link.type() != EntityInTextPre + && link.type() != EntityInTextMentionName) { + continue; + } auto offset = MTP_int(link.offset()), length = MTP_int(link.length()); switch (link.type()) { @@ -1389,7 +1409,22 @@ MTPVector linksToMTP(const EntitiesInText &links, bool sending case EntityInTextEmail: v.push_back(MTP_messageEntityEmail(offset, length)); break; case EntityInTextHashtag: v.push_back(MTP_messageEntityHashtag(offset, length)); break; case EntityInTextMention: v.push_back(MTP_messageEntityMention(offset, length)); break; - case EntityInTextMentionName: v.push_back(MTP_messageEntityMentionName(offset, length, MTP_int(link.data().toInt()))); break; + case EntityInTextMentionName: { + auto inputUser = ([](const QString &data) -> MTPInputUser { + UserId userId = 0; + uint64 accessHash = 0; + if (mentionNameToFields(data, &userId, &accessHash)) { + if (userId == MTP::authedId()) { + return MTP_inputUserSelf(); + } + return MTP_inputUser(MTP_int(userId), MTP_long(accessHash)); + } + return MTP_inputUserEmpty(); + })(link.data()); + if (inputUser.type() != mtpc_inputUserEmpty) { + v.push_back(MTP_inputMessageEntityMentionName(offset, length, inputUser)); + } + } break; case EntityInTextBotCommand: v.push_back(MTP_messageEntityBotCommand(offset, length)); break; case EntityInTextBold: v.push_back(MTP_messageEntityBold(offset, length)); break; case EntityInTextItalic: v.push_back(MTP_messageEntityItalic(offset, length)); break; @@ -1400,8 +1435,9 @@ MTPVector linksToMTP(const EntitiesInText &links, bool sending return result; } -EntitiesInText textParseEntities(QString &text, int32 flags, bool rich) { // some code is duplicated in flattextarea.cpp! - EntitiesInText result, mono; +// Some code is duplicated in flattextarea.cpp! +void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities, bool rich) { + EntitiesInText mono; bool withHashtags = (flags & TextParseHashtags); bool withMentions = (flags & TextParseMentions); @@ -1701,20 +1737,18 @@ EntitiesInText textParseEntities(QString &text, int32 flags, bool rich) { // som } for (; monoEntity < monoCount && mono[monoEntity].offset() <= lnkStart; ++monoEntity) { monoTill = qMax(monoTill, mono[monoEntity].offset() + mono[monoEntity].length()); - result.push_back(mono[monoEntity]); + inOutEntities->push_back(mono[monoEntity]); } if (lnkStart >= monoTill) { - result.push_back(EntityInText(lnkType, lnkStart, lnkLength)); + inOutEntities->push_back(EntityInText(lnkType, lnkStart, lnkLength)); } offset = matchOffset = lnkStart + lnkLength; } for (; monoEntity < monoCount; ++monoEntity) { monoTill = qMax(monoTill, mono[monoEntity].offset() + mono[monoEntity].length()); - result.push_back(mono[monoEntity]); + inOutEntities->push_back(mono[monoEntity]); } - - return result; } QString textApplyEntities(const QString &text, const EntitiesInText &entities) { @@ -1769,71 +1803,11 @@ QString textApplyEntities(const QString &text, const EntitiesInText &entities) { return result; } -void replaceStringWithEntities(const QLatin1String &from, QChar to, QString &result, EntitiesInText &entities, bool checkSpace = false) { - int32 len = from.size(), s = result.size(), offset = 0, length = 0; - EntitiesInText::iterator i = entities.begin(), e = entities.end(); - for (QChar *start = result.data(); offset < s;) { - int32 nextOffset = result.indexOf(from, offset); - if (nextOffset < 0) { - moveStringPart(start, length, offset, s - offset, entities); - break; - } - - if (checkSpace) { - bool spaceBefore = (nextOffset > 0) && (start + nextOffset - 1)->isSpace(); - bool spaceAfter = (nextOffset + len < s) && (start + nextOffset + len)->isSpace(); - if (!spaceBefore && !spaceAfter) { - moveStringPart(start, length, offset, nextOffset - offset + len + 1, entities); - continue; - } - } - - bool skip = false; - for (; i != e; ++i) { // find and check next finishing entity - if (i->offset() + i->length() > nextOffset) { - skip = (i->offset() < nextOffset + len); - break; - } - } - if (skip) { - moveStringPart(start, length, offset, nextOffset - offset + len, entities); - continue; - } - - moveStringPart(start, length, offset, nextOffset - offset, entities); - - *(start + length) = to; - ++length; - offset += len; - } - if (length < s) result.resize(length); -} - -QString prepareTextWithEntities(QString result, EntitiesInText &entities, int32 flags) { - cleanTextWithEntities(result, entities); - - if (flags) { - entities = textParseEntities(result, flags); - } - - replaceStringWithEntities(qstr("--"), QChar(8212), result, entities, true); - replaceStringWithEntities(qstr("<<"), QChar(171), result, entities); - replaceStringWithEntities(qstr(">>"), QChar(187), result, entities); - - if (cReplaceEmojis()) { - result = replaceEmojis(result, entities); - } - - trimTextWithEntities(result, entities); - - return result; -} - -void moveStringPart(QChar *start, int32 &to, int32 &from, int32 count, EntitiesInText &entities) { +void moveStringPart(QChar *start, int32 &to, int32 &from, int32 count, EntitiesInText *inOutEntities) { if (count > 0) { if (to < from) { memmove(start + to, start + from, count * sizeof(QChar)); - for (auto &entity : entities) { + for (auto &entity : *inOutEntities) { if (entity.offset() >= from + count) break; if (entity.offset() + entity.length() < from) continue; if (entity.offset() >= from) { @@ -1849,24 +1823,84 @@ void moveStringPart(QChar *start, int32 &to, int32 &from, int32 count, EntitiesI } } +void replaceStringWithEntities(const QLatin1String &from, QChar to, QString &result, EntitiesInText *inOutEntities, bool checkSpace = false) { + int32 len = from.size(), s = result.size(), offset = 0, length = 0; + EntitiesInText::iterator i = inOutEntities->begin(), e = inOutEntities->end(); + for (QChar *start = result.data(); offset < s;) { + int32 nextOffset = result.indexOf(from, offset); + if (nextOffset < 0) { + moveStringPart(start, length, offset, s - offset, inOutEntities); + break; + } + + if (checkSpace) { + bool spaceBefore = (nextOffset > 0) && (start + nextOffset - 1)->isSpace(); + bool spaceAfter = (nextOffset + len < s) && (start + nextOffset + len)->isSpace(); + if (!spaceBefore && !spaceAfter) { + moveStringPart(start, length, offset, nextOffset - offset + len + 1, inOutEntities); + continue; + } + } + + bool skip = false; + for (; i != e; ++i) { // find and check next finishing entity + if (i->offset() + i->length() > nextOffset) { + skip = (i->offset() < nextOffset + len); + break; + } + } + if (skip) { + moveStringPart(start, length, offset, nextOffset - offset + len, inOutEntities); + continue; + } + + moveStringPart(start, length, offset, nextOffset - offset, inOutEntities); + + *(start + length) = to; + ++length; + offset += len; + } + if (length < s) result.resize(length); +} + +QString prepareTextWithEntities(QString result, int32 flags, EntitiesInText *inOutEntities) { + cleanTextWithEntities(result, inOutEntities); + + if (flags) { + textParseEntities(result, flags, inOutEntities); + } + + replaceStringWithEntities(qstr("--"), QChar(8212), result, inOutEntities, true); + replaceStringWithEntities(qstr("<<"), QChar(171), result, inOutEntities); + replaceStringWithEntities(qstr(">>"), QChar(187), result, inOutEntities); + + if (cReplaceEmojis()) { + result = replaceEmojis(result, inOutEntities); + } + + trimTextWithEntities(result, inOutEntities); + + return result; +} + // replace bad symbols with space and remove \r -void cleanTextWithEntities(QString &result, EntitiesInText &entities) { +void cleanTextWithEntities(QString &result, EntitiesInText *inOutEntities) { result = result.replace('\t', qstr(" ")); int32 len = result.size(), to = 0, from = 0; QChar *start = result.data(); for (QChar *ch = start, *end = start + len; ch < end; ++ch) { if (ch->unicode() == '\r') { - moveStringPart(start, to, from, (ch - start) - from, entities); + moveStringPart(start, to, from, (ch - start) - from, inOutEntities); ++from; } else if (chReplacedBySpace(*ch)) { *ch = ' '; } } - moveStringPart(start, to, from, len - from, entities); + moveStringPart(start, to, from, len - from, inOutEntities); if (to < len) result.resize(to); } -void trimTextWithEntities(QString &result, EntitiesInText &entities) { +void trimTextWithEntities(QString &result, EntitiesInText *inOutEntities) { bool foundNotTrimmedChar = false; // right trim @@ -1875,7 +1909,7 @@ void trimTextWithEntities(QString &result, EntitiesInText &entities) { if (!chIsTrimmed(*ch)) { if (ch + 1 < e) { int32 l = ch + 1 - s; - for (auto &entity : entities) { + for (auto &entity : *inOutEntities) { entity.updateTextEnd(l); } result.resize(l); @@ -1886,18 +1920,18 @@ void trimTextWithEntities(QString &result, EntitiesInText &entities) { } if (!foundNotTrimmedChar) { result.clear(); - entities.clear(); + inOutEntities->clear(); return; } - int firstMonospaceOffset = EntityInText::firstMonospaceOffset(entities, result.size()); + int firstMonospaceOffset = EntityInText::firstMonospaceOffset(*inOutEntities, result.size()); // left trim for (QChar *s = result.data(), *ch = s, *e = s + result.size(); ch != e; ++ch) { if (!chIsTrimmed(*ch) || (ch - s) == firstMonospaceOffset) { if (ch > s) { int32 l = ch - s; - for (auto &entity : entities) { + for (auto &entity : *inOutEntities) { entity.shiftLeft(l); } result = result.mid(l); diff --git a/Telegram/SourceFiles/ui/text/text_entity.h b/Telegram/SourceFiles/ui/text/text_entity.h index 20caba8cd..fe2f36c7f 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.h +++ b/Telegram/SourceFiles/ui/text/text_entity.h @@ -133,21 +133,36 @@ enum { TextInstagramHashtags = 0x800, }; +inline bool mentionNameToFields(const QString &data, int32 *outUserId, uint64 *outAccessHash) { + auto components = data.split('.'); + if (!components.isEmpty()) { + *outUserId = components.at(0).toInt(); + *outAccessHash = (components.size() > 1) ? components.at(1).toULongLong() : 0; + return (*outUserId != 0); + } + return false; +} + +inline QString mentionNameFromFields(int32 userId, uint64 accessHash) { + return QString::number(userId) + '.' + QString::number(accessHash); +} + EntitiesInText entitiesFromMTP(const QVector &entities); MTPVector linksToMTP(const EntitiesInText &links, bool sending = false); -EntitiesInText textParseEntities(QString &text, int32 flags, bool rich = false); // changes text if (flags & TextParseMono) +// New entities are added to the ones that are already in inOutEntities. +// Changes text if (flags & TextParseMono). +void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities, bool rich = false); QString textApplyEntities(const QString &text, const EntitiesInText &entities); -QString prepareTextWithEntities(QString result, EntitiesInText &entities, int32 flags); +QString prepareTextWithEntities(QString result, int32 flags, EntitiesInText *inOutEntities); inline QString prepareText(QString result, bool checkLinks = false) { EntitiesInText entities; - return prepareTextWithEntities(result, entities, checkLinks ? (TextParseLinks | TextParseMentions | TextParseHashtags | TextParseBotCommands) : 0); + auto prepareFlags = checkLinks ? (TextParseLinks | TextParseMentions | TextParseHashtags | TextParseBotCommands) : 0; + return prepareTextWithEntities(result, prepareFlags, &entities); } -void moveStringPart(QChar *start, int32 &to, int32 &from, int32 count, EntitiesInText &entities); - // replace bad symbols with space and remove \r -void cleanTextWithEntities(QString &result, EntitiesInText &entities); -void trimTextWithEntities(QString &result, EntitiesInText &entities); \ No newline at end of file +void cleanTextWithEntities(QString &result, EntitiesInText *inOutEntities); +void trimTextWithEntities(QString &result, EntitiesInText *inOutEntities); \ No newline at end of file From 45143c40c967f9dab1085959a37779f0861d725d Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 4 May 2016 19:46:24 +0300 Subject: [PATCH 04/20] FlatTextarea handles tags on insertFromMime and tags editing. Fixed dependent messages update when message was edited. Fixed entities parsing in Text when they follow one after another. --- Telegram/SourceFiles/history.cpp | 21 +- Telegram/SourceFiles/history.h | 3 +- Telegram/SourceFiles/ui/emoji_config.h | 26 +- Telegram/SourceFiles/ui/flattextarea.cpp | 340 ++++++++++++++++------- Telegram/SourceFiles/ui/flattextarea.h | 18 +- Telegram/SourceFiles/ui/text/text.cpp | 12 +- 6 files changed, 293 insertions(+), 127 deletions(-) diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index e04c081e3..7907cdc2d 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -6960,12 +6960,12 @@ void HistoryMessage::initDimensions() { if (_media->isDisplayed()) { if (_text.hasSkipBlock()) { _text.removeSkipBlock(); - _textWidth = 0; + _textWidth = -1; _textHeight = 0; } } else if (!_text.hasSkipBlock()) { _text.setSkipBlock(skipBlockWidth(), skipBlockHeight()); - _textWidth = 0; + _textWidth = -1; _textHeight = 0; } } @@ -7103,6 +7103,8 @@ void HistoryMessage::applyEdition(const MTPDmessage &message) { keyboard->oldTop = keyboardTop; } } + + App::historyUpdateDependent(this); } void HistoryMessage::updateMedia(const MTPMessageMedia *media) { @@ -7211,11 +7213,11 @@ void HistoryMessage::setMedia(const MTPMessageMedia *media) { initMedia(media, t); if (_media && _media->isDisplayed() && !mediaWasDisplayed) { _text.removeSkipBlock(); - _textWidth = 0; + _textWidth = -1; _textHeight = 0; } else if (mediaWasDisplayed && (!_media || !_media->isDisplayed())) { _text.setSkipBlock(skipBlockWidth(), skipBlockHeight()); - _textWidth = 0; + _textWidth = -1; _textHeight = 0; } } @@ -7236,7 +7238,7 @@ void HistoryMessage::setText(const QString &text, const EntitiesInText &entities break; } } - _textWidth = 0; + _textWidth = -1; _textHeight = 0; } @@ -7395,7 +7397,7 @@ void HistoryMessage::setViewsCount(int32 count) { } else { if (_text.hasSkipBlock()) { _text.setSkipBlock(HistoryMessage::skipBlockWidth(), HistoryMessage::skipBlockHeight()); - _textWidth = 0; + _textWidth = -1; _textHeight = 0; } setPendingInitDimensions(); @@ -7410,7 +7412,7 @@ void HistoryMessage::setId(MsgId newId) { } else { if (_text.hasSkipBlock()) { _text.setSkipBlock(HistoryMessage::skipBlockWidth(), HistoryMessage::skipBlockHeight()); - _textWidth = 0; + _textWidth = -1; _textHeight = 0; } setPendingInitDimensions(); @@ -8079,7 +8081,6 @@ bool HistoryService::updatePinned(bool force) { updatePinnedText(); } if (force) { - setPendingInitDimensions(); if (gotDependencyItem && App::wnd()) { App::wnd()->notifySettingGot(); } @@ -8208,7 +8209,9 @@ void HistoryService::setServiceText(const QString &text) { textstyleSet(&st::serviceTextStyle); _text.setText(st::msgServiceFont, text, _historySrvOptions); textstyleRestore(); - initDimensions(); + setPendingInitDimensions(); + _textWidth = -1; + _textHeight = 0; } void HistoryService::draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const { diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h index 9c850a53a..cc8a8bec1 100644 --- a/Telegram/SourceFiles/history.h +++ b/Telegram/SourceFiles/history.h @@ -1566,7 +1566,8 @@ protected: } Text _text = { int(st::msgMinWidth) }; - int32 _textWidth, _textHeight; + int _textWidth = -1; + int _textHeight = 0; HistoryMediaPtr _media; diff --git a/Telegram/SourceFiles/ui/emoji_config.h b/Telegram/SourceFiles/ui/emoji_config.h index ed7bae85e..955811f11 100644 --- a/Telegram/SourceFiles/ui/emoji_config.h +++ b/Telegram/SourceFiles/ui/emoji_config.h @@ -83,52 +83,52 @@ inline EmojiPtr emojiFromUrl(const QString &url) { return emojiFromKey(url.midRef(10).toULongLong(0, 16)); // skip emoji://e. } -inline EmojiPtr emojiFromText(const QChar *ch, const QChar *e, int *plen = 0) { - EmojiPtr emoji = 0; - if (ch + 1 < e && ((ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) || (((ch->unicode() >= 0x30 && ch->unicode() < 0x3A) || ch->unicode() == 0x23 || ch->unicode() == 0x2A) && (ch + 1)->unicode() == 0x20E3))) { +inline EmojiPtr emojiFromText(const QChar *ch, const QChar *end, int *outLength = nullptr) { + EmojiPtr emoji = nullptr; + if (ch + 1 < end && ((ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) || (((ch->unicode() >= 0x30 && ch->unicode() < 0x3A) || ch->unicode() == 0x23 || ch->unicode() == 0x2A) && (ch + 1)->unicode() == 0x20E3))) { uint32 code = (ch->unicode() << 16) | (ch + 1)->unicode(); emoji = emojiGet(code); if (emoji) { if (emoji == TwoSymbolEmoji) { // check two symbol - if (ch + 3 >= e) { + if (ch + 3 >= end) { emoji = 0; } else { uint32 code2 = ((uint32((ch + 2)->unicode()) << 16) | uint32((ch + 3)->unicode())); emoji = emojiGet(code, code2); } } else { - if (ch + 2 < e && (ch + 2)->unicode() == 0x200D) { // check sequence - EmojiPtr seq = emojiGet(ch, e); + if (ch + 2 < end && (ch + 2)->unicode() == 0x200D) { // check sequence + EmojiPtr seq = emojiGet(ch, end); if (seq) { emoji = seq; } } } } - } else if (ch + 2 < e && ((ch->unicode() >= 0x30 && ch->unicode() < 0x3A) || ch->unicode() == 0x23 || ch->unicode() == 0x2A) && (ch + 1)->unicode() == 0xFE0F && (ch + 2)->unicode() == 0x20E3) { + } else if (ch + 2 < end && ((ch->unicode() >= 0x30 && ch->unicode() < 0x3A) || ch->unicode() == 0x23 || ch->unicode() == 0x2A) && (ch + 1)->unicode() == 0xFE0F && (ch + 2)->unicode() == 0x20E3) { uint32 code = (ch->unicode() << 16) | (ch + 2)->unicode(); emoji = emojiGet(code); - if (plen) *plen = emoji->len + 1; + if (outLength) *outLength = emoji->len + 1; return emoji; - } else if (ch < e) { + } else if (ch < end) { emoji = emojiGet(ch->unicode()); t_assert(emoji != TwoSymbolEmoji); } if (emoji) { - int32 len = emoji->len + ((ch + emoji->len < e && (ch + emoji->len)->unicode() == 0xFE0F) ? 1 : 0); - if (emoji->color && (ch + len + 1 < e && (ch + len)->isHighSurrogate() && (ch + len + 1)->isLowSurrogate())) { // color + int32 len = emoji->len + ((ch + emoji->len < end && (ch + emoji->len)->unicode() == 0xFE0F) ? 1 : 0); + if (emoji->color && (ch + len + 1 < end && (ch + len)->isHighSurrogate() && (ch + len + 1)->isLowSurrogate())) { // color uint32 color = ((uint32((ch + len)->unicode()) << 16) | uint32((ch + len + 1)->unicode())); EmojiPtr col = emojiGet(emoji, color); if (col && col != emoji) { len += col->len - emoji->len; emoji = col; - if (ch + len < e && (ch + len)->unicode() == 0xFE0F) { + if (ch + len < end && (ch + len)->unicode() == 0xFE0F) { ++len; } } } - if (plen) *plen = len; + if (outLength) *outLength = len; } return emoji; diff --git a/Telegram/SourceFiles/ui/flattextarea.cpp b/Telegram/SourceFiles/ui/flattextarea.cpp index 45ef62746..51c7ee0f5 100644 --- a/Telegram/SourceFiles/ui/flattextarea.cpp +++ b/Telegram/SourceFiles/ui/flattextarea.cpp @@ -310,7 +310,7 @@ EmojiPtr FlatTextarea::getSingleEmoji() const { return emojiFromUrl(imageName); } } - return 0; + return nullptr; } QString FlatTextarea::getInlineBotQuery(UserData **outInlineBot, QString *outInlineBotUsername) const { @@ -464,14 +464,16 @@ void FlatTextarea::insertMentionHashtagOrBotCommand(const QString &data, const Q break; } if (tagId.isEmpty()) { - c.insertText(data + ' '); + QTextCharFormat format = c.charFormat(); + format.setAnchor(false); + format.setAnchorName(QString()); + format.clearForeground(); + c.insertText(data + ' ', format); } else { - QTextCharFormat defaultFormat = c.charFormat(), linkFormat = defaultFormat; - linkFormat.setAnchor(true); - linkFormat.setAnchorName(tagId + '/' + QString::number(rand_value())); - linkFormat.setForeground(st::defaultTextStyle.linkFg); - c.insertText(data, linkFormat); - c.insertText(qsl(" "), defaultFormat); + _insertedTags.clear(); + _insertedTags.push_back({ 0, data.size(), tagId + '/' + QString::number(rand_value()) }); + c.insertText(data + ' '); + _insertedTags.clear(); } } @@ -792,16 +794,21 @@ QStringList FlatTextarea::linksList() const { void FlatTextarea::insertFromMimeData(const QMimeData *source) { auto mime = str_const_toString(TagsMimeType); + auto text = source->text(); if (source->hasFormat(mime)) { auto tagsData = source->data(mime); - _settingTags = deserializeTagsList(tagsData, source->text().size()); + _insertedTags = deserializeTagsList(tagsData, text.size()); } else { - _settingTags.clear(); + _insertedTags.clear(); } + _realInsertPosition = textCursor().position(); + _realCharsAdded = text.size(); QTextEdit::insertFromMimeData(source); - _settingTags.clear(); - - if (!_inDrop) emit spacedReturnedPasted(); + if (!_inDrop) { + emit spacedReturnedPasted(); + _insertedTags.clear(); + _realInsertPosition = -1; + } } void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) { @@ -811,8 +818,11 @@ void FlatTextarea::insertEmoji(EmojiPtr emoji, QTextCursor c) { imageFormat.setHeight(eh / cIntRetinaFactor()); imageFormat.setName(qsl("emoji://e.") + QString::number(emojiKey(emoji), 16)); imageFormat.setVerticalAlignment(QTextCharFormat::AlignBaseline); - imageFormat.setAnchorName(c.charFormat().anchorName()); - + if (c.charFormat().isAnchor()) { + imageFormat.setAnchor(true); + imageFormat.setAnchorName(c.charFormat().anchorName()); + imageFormat.setForeground(st::defaultTextStyle.linkFg); + } static QString objectReplacement(QChar::ObjectReplacementCharacter); c.insertText(objectReplacement, imageFormat); } @@ -833,92 +843,234 @@ void FlatTextarea::checkContentHeight() { } } -void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) { - int32 replacePosition = -1, replaceLen = 0; - const EmojiData *emoji = nullptr; +namespace { - static QString regular = qsl("Open Sans"), semibold = qsl("Open Sans Semibold"); - bool checkTilde = !cRetina() && (font().pixelSize() == 13) && (font().family() == regular), wasTildeFragment = false; +// Optimization: with null page size document does not re-layout +// on each insertText / mergeCharFormat. +void prepareFormattingOptimization(QTextDocument *document) { + if (!document->pageSize().isNull()) { + document->setPageSize(QSizeF(0, 0)); + } +} - QTextDocument *doc(document()); +void removeTags(QTextDocument *document, int from, int end) { + QTextCursor c(document->docHandle(), from); + c.setPosition(end, QTextCursor::KeepAnchor); + + QTextCharFormat format; + format.setAnchor(false); + format.setAnchorName(QString()); + format.setForeground(st::black); + c.mergeCharFormat(format); +} + +// Returns the position of the first inserted tag or "changedEnd" value if none found. +int processInsertedTags(QTextDocument *document, int changedPosition, int changedEnd, const FlatTextarea::TagList &tags) { + int firstTagStart = changedEnd; + int applyNoTagFrom = changedEnd; + for_const (auto &tag, tags) { + int tagFrom = changedPosition + tag.offset; + int tagTo = tagFrom + tag.length; + accumulate_max(tagFrom, changedPosition); + accumulate_min(tagTo, changedEnd); + if (tagTo > tagFrom) { + accumulate_min(firstTagStart, tagFrom); + + prepareFormattingOptimization(document); + + if (applyNoTagFrom < tagFrom) { + removeTags(document, applyNoTagFrom, tagFrom); + } + QTextCursor c(document->docHandle(), tagFrom); + c.setPosition(tagTo, QTextCursor::KeepAnchor); + + QTextCharFormat format; + format.setAnchor(true); + format.setAnchorName(tag.id); + format.setForeground(st::defaultTextStyle.linkFg); + c.mergeCharFormat(format); + + applyNoTagFrom = tagTo; + } + } + if (applyNoTagFrom < changedEnd) { + removeTags(document, applyNoTagFrom, changedEnd); + } + + return firstTagStart; +} + +// When inserting a part of text inside a tag we need to have +// a way to know if the insertion replaced the end of the tag +// or it was strictly inside (in the middle) of the tag. +bool wasInsertTillTheEndOfTag(QTextBlock block, QTextBlock::iterator fragmentIt, int insertionEnd) { + auto insertTagName = fragmentIt.fragment().charFormat().anchorName(); while (true) { - int32 start = position, end = position + charsAdded; - QTextBlock from = doc->findBlock(start), till = doc->findBlock(end); - if (till.isValid()) till = till.next(); + for (; !fragmentIt.atEnd(); ++fragmentIt) { + auto fragment = fragmentIt.fragment(); + bool fragmentOutsideInsertion = (fragment.position() >= insertionEnd); + if (fragmentOutsideInsertion) { + return (fragment.charFormat().anchorName() != insertTagName); + } + int fragmentEnd = fragment.position() + fragment.length(); + bool notFullFragmentInserted = (fragmentEnd > insertionEnd); + if (notFullFragmentInserted) { + return false; + } + } + if (block.isValid()) { + fragmentIt = block.begin(); + block = block.next(); + } else { + break; + } + } + // Insertion goes till the end of the text => not strictly inside a tag. + return true; +} - for (QTextBlock b = from; b != till; b = b.next()) { - for (QTextBlock::Iterator iter = b.begin(); !iter.atEnd(); ++iter) { - QTextFragment fragment(iter.fragment()); - if (!fragment.isValid()) continue; +struct FormattingAction { + enum class Type { + Invalid, + InsertEmoji, + TildeFont, + RemoveTag, + }; + Type type = Type::Invalid; + EmojiPtr emoji = nullptr; + bool isTilde = false; + int intervalStart = 0; + int intervalEnd = 0; +}; - int32 fp = fragment.position(), fe = fp + fragment.length(); - if (fp >= end || fe <= start) { +} // namespace + +void FlatTextarea::processFormatting(int changedPosition, int changedEnd) { + // Tilde formatting. + auto regularFont = qsl("Open Sans"), semiboldFont = qsl("Open Sans Semibold"); + bool tildeFormatting = !cRetina() && (font().pixelSize() == 13) && (font().family() == regularFont); + bool isTildeFragment = false; + + // First tag handling (the one we inserted text to). + bool startTagFound = false; + bool breakTagOnNotLetter = false; + + auto doc = document(); + + // Apply inserted tags. + int breakTagOnNotLetterTill = processInsertedTags(doc, changedPosition, changedEnd, _insertedTags); + using ActionType = FormattingAction::Type; + while (true) { + FormattingAction action; + + auto fromBlock = doc->findBlock(changedPosition); + auto tillBlock = doc->findBlock(changedEnd); + if (tillBlock.isValid()) tillBlock = tillBlock.next(); + + for (auto block = fromBlock; block != tillBlock; block = block.next()) { + for (auto fragmentIt = block.begin(); !fragmentIt.atEnd(); ++fragmentIt) { + auto fragment = fragmentIt.fragment(); + t_assert(fragment.isValid()); + + int fragmentPosition = fragment.position(); + if (changedPosition >= fragmentPosition + fragment.length()) { continue; } - - if (checkTilde) { - wasTildeFragment = (fragment.charFormat().fontFamily() == semibold); + int changedPositionInFragment = changedPosition - fragmentPosition; // Can be negative. + int changedEndInFragment = changedEnd - fragmentPosition; + if (changedEndInFragment <= 0) { + break; } - QString t(fragment.text()); - const QChar *ch = t.constData(), *e = ch + t.size(); - for (; ch != e; ++ch, ++fp) { - int emojiLen = 0; - emoji = emojiFromText(ch, e, &emojiLen); - if (emoji) { - if (replacePosition >= 0) { - emoji = 0; // replace tilde char format first - } else { - replacePosition = fp; - replaceLen = emojiLen; + auto charFormat = fragment.charFormat(); + if (tildeFormatting) { + isTildeFragment = (charFormat.fontFamily() == semiboldFont); + } + + auto fragmentText = fragment.text(); + auto *textStart = fragmentText.constData(); + auto *textEnd = textStart + fragmentText.size(); + + if (!startTagFound) { + startTagFound = true; + auto tagName = charFormat.anchorName(); + if (!tagName.isEmpty()) { + breakTagOnNotLetter = wasInsertTillTheEndOfTag(block, fragmentIt, changedEnd); + } + } + + auto *ch = textStart + qMax(changedPositionInFragment, 0); + for (; ch < textEnd; ++ch) { + int emojiLength = 0; + if (auto emoji = emojiFromText(ch, textEnd, &emojiLength)) { + // Replace emoji if no current action is prepared. + if (action.type == ActionType::Invalid) { + action.type = ActionType::InsertEmoji; + action.emoji = emoji; + action.intervalStart = fragmentPosition + (ch - textStart); + action.intervalEnd = action.intervalStart + emojiLength; } break; } - if (checkTilde && fp >= position) { // tilde fix in OpenSans + if (breakTagOnNotLetter && !ch->isLetter()) { + // Remove tag name till the end if no current action is prepared. + if (action.type != ActionType::Invalid) { + break; + } + breakTagOnNotLetter = false; + if (fragmentPosition + (ch - textStart) < breakTagOnNotLetterTill) { + action.type = ActionType::RemoveTag; + action.intervalStart = fragmentPosition + (ch - textStart); + action.intervalEnd = breakTagOnNotLetterTill; + break; + } + } + if (tildeFormatting) { // Tilde symbol fix in OpenSans. bool tilde = (ch->unicode() == '~'); - if ((tilde && !wasTildeFragment) || (!tilde && wasTildeFragment)) { - if (replacePosition < 0) { - replacePosition = fp; - replaceLen = 1; + if ((tilde && !isTildeFragment) || (!tilde && isTildeFragment)) { + if (action.type == ActionType::Invalid) { + action.type = ActionType::TildeFont; + action.intervalStart = fragmentPosition + (ch - textStart); + action.intervalEnd = action.intervalStart + 1; + action.isTilde = tilde; } else { - ++replaceLen; + ++action.intervalEnd; } - } else if (replacePosition >= 0) { + } else if (action.type == ActionType::TildeFont) { break; } } - if (ch + 1 < e && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) { + if (ch + 1 < textEnd && ch->isHighSurrogate() && (ch + 1)->isLowSurrogate()) { ++ch; - ++fp; + ++fragmentPosition; } } - if (replacePosition >= 0) break; + if (action.type != ActionType::Invalid) break; } - if (replacePosition >= 0) break; + if (action.type != ActionType::Invalid) break; } - if (replacePosition >= 0) { - // Optimization: with null page size document does not re-layout - // on each insertText / mergeCharFormat. - if (!document()->pageSize().isNull()) { - document()->setPageSize(QSizeF(0, 0)); - } + if (action.type != ActionType::Invalid) { + prepareFormattingOptimization(doc); - QTextCursor c(doc->docHandle(), replacePosition); - c.setPosition(replacePosition + replaceLen, QTextCursor::KeepAnchor); - if (emoji) { - insertEmoji(emoji, c); - } else { + QTextCursor c(doc->docHandle(), action.intervalStart); + c.setPosition(action.intervalEnd, QTextCursor::KeepAnchor); + if (action.type == ActionType::InsertEmoji) { + insertEmoji(action.emoji, c); + changedPosition = action.intervalStart + 1; + } else if (action.type == ActionType::RemoveTag) { QTextCharFormat format; - format.setFontFamily(wasTildeFragment ? regular : semibold); + format.setAnchor(false); + format.setAnchorName(QString()); + format.setForeground(st::black); c.mergeCharFormat(format); + } else if (action.type == ActionType::TildeFont) { + QTextCharFormat format; + format.setFontFamily(action.isTilde ? semiboldFont : regularFont); + c.mergeCharFormat(format); + changedPosition = action.intervalEnd; } - charsAdded -= replacePosition + replaceLen - position; - position = replacePosition + (emoji ? 1 : replaceLen); - - emoji = nullptr; - replacePosition = -1; } else { break; } @@ -928,6 +1080,9 @@ void FlatTextarea::processDocumentContentsChange(int position, int charsAdded) { void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int charsAdded) { if (_correcting) return; + int insertPosition = (_realInsertPosition >= 0) ? _realInsertPosition : position; + int insertLength = (_realInsertPosition >= 0) ? _realCharsAdded : charsAdded; + QTextCursor(document()->docHandle(), 0).joinPreviousEditBlock(); _correcting = true; @@ -936,18 +1091,18 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int c.movePosition(QTextCursor::End); int32 fullSize = c.position(), toRemove = fullSize - _maxLength; if (toRemove > 0) { - if (toRemove > charsAdded) { - if (charsAdded) { - c.setPosition(position); - c.setPosition((position + charsAdded), QTextCursor::KeepAnchor); + if (toRemove > insertLength) { + if (insertLength) { + c.setPosition(insertPosition); + c.setPosition((insertPosition + insertLength), QTextCursor::KeepAnchor); c.removeSelectedText(); } - c.setPosition(fullSize - (toRemove - charsAdded)); + c.setPosition(fullSize - (toRemove - insertLength)); c.setPosition(fullSize, QTextCursor::KeepAnchor); c.removeSelectedText(); } else { - c.setPosition(position + (charsAdded - toRemove)); - c.setPosition(position + charsAdded, QTextCursor::KeepAnchor); + c.setPosition(insertPosition + (insertLength - toRemove)); + c.setPosition(insertPosition + insertLength, QTextCursor::KeepAnchor); c.removeSelectedText(); } } @@ -957,10 +1112,10 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int if (!_links.isEmpty()) { bool changed = false; for (auto i = _links.begin(); i != _links.end();) { - if (i->first + i->second <= position) { + if (i->first + i->second <= insertPosition) { ++i; - } else if (i->first >= position + charsRemoved) { - i->first += charsAdded - charsRemoved; + } else if (i->first >= insertPosition + charsRemoved) { + i->first += insertLength - charsRemoved; ++i; } else { i = _links.erase(i); @@ -975,24 +1130,16 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int return; } - const int takeBack = 3; - - position -= takeBack; - charsAdded += takeBack; - if (position < 0) { - charsAdded += position; - position = 0; - } - if (charsAdded <= 0) { + if (insertLength <= 0) { QTextCursor(document()->docHandle(), 0).endEditBlock(); return; } _correcting = true; - QSizeF s = document()->pageSize(); - processDocumentContentsChange(position, charsAdded); - if (document()->pageSize() != s) { - document()->setPageSize(s); + auto pageSize = document()->pageSize(); + processFormatting(insertPosition, insertPosition + insertLength); + if (document()->pageSize() != pageSize) { + document()->setPageSize(pageSize); } _correcting = false; @@ -1164,6 +1311,9 @@ void FlatTextarea::dropEvent(QDropEvent *e) { _inDrop = true; QTextEdit::dropEvent(e); _inDrop = false; + _insertedTags.clear(); + _realInsertPosition = -1; + emit spacedReturnedPasted(); } diff --git a/Telegram/SourceFiles/ui/flattextarea.h b/Telegram/SourceFiles/ui/flattextarea.h index 659d724e1..e9fe232da 100644 --- a/Telegram/SourceFiles/ui/flattextarea.h +++ b/Telegram/SourceFiles/ui/flattextarea.h @@ -143,7 +143,16 @@ private: QString getText(int start, int end, TagList *outTagsList, bool *outTagsChanged) const; void getSingleEmojiFragment(QString &text, QTextFragment &fragment) const; - void processDocumentContentsChange(int position, int charsAdded); + + // After any characters added we must postprocess them. This includes: + // 1. Replacing font family to semibold for ~ characters, if we used Open Sans 13px. + // 2. Replacing font family from semibold for all non-~ characters, if we used ... + // 3. Replacing emoji code sequences by ObjectReplacementCharacters with emoji pics. + // 4. Interrupting tags in which the text was inserted by any char except a letter. + // 5. Applying tags from "_insertedTags" in case we pasted text with tags, not just text. + // Rule 4 applies only if we inserted chars not in the middle of a tag (but at the end). + void processFormatting(int changedPosition, int changedEnd); + bool heightAutoupdated(); int _minHeight = -1; // < 0 - no autosize @@ -161,7 +170,12 @@ private: Animation _a_appearance; // Tags list which we should apply while setText() call or insert from mime data. - TagList _settingTags; + TagList _insertedTags; + + // Override insert position and charsAdded from complex text editing + // (like drag-n-drop in the same text edit field). + int _realInsertPosition = -1; + int _realCharsAdded = 0; style::flatTextarea _st; diff --git a/Telegram/SourceFiles/ui/text/text.cpp b/Telegram/SourceFiles/ui/text/text.cpp index ed72e2a50..79b72b78f 100644 --- a/Telegram/SourceFiles/ui/text/text.cpp +++ b/Telegram/SourceFiles/ui/text/text.cpp @@ -253,7 +253,8 @@ public: return result; } - void checkEntities() { + // Returns true if at least one entity was parsed in the current position. + bool checkEntities() { while (!removeFlags.isEmpty() && (ptr >= removeFlags.firstKey() || ptr >= end)) { const QList &removing(removeFlags.first()); for (int32 i = removing.size(); i > 0;) { @@ -272,7 +273,7 @@ public: ++waitingEntity; } if (waitingEntity == entitiesEnd || ptr < start + waitingEntity->offset()) { - return; + return false; } int32 startFlags = 0; @@ -348,6 +349,7 @@ public: } else { while (waitingEntity != entitiesEnd && waitingEntity->length() <= 0) ++waitingEntity; } + return true; } bool readSkipBlockCommand() { @@ -620,11 +622,7 @@ public: lastSkipped = false; checkTilde = !cRetina() && _t->_font->size() == 13 && _t->_font->flags() == 0; // tilde Open Sans fix for (; ptr <= end; ++ptr) { - checkEntities(); - if (rich) { - if (checkCommand()) { - checkEntities(); - } + while (checkEntities() || (rich && checkCommand())) { } parseCurrentChar(); parseEmojiFromCurrent(); From 5a47d8e29bb41fd561ab1c725810ae61908652d1 Mon Sep 17 00:00:00 2001 From: John Preston Date: Wed, 4 May 2016 23:38:37 +0300 Subject: [PATCH 05/20] Marking tags by random values only inside of FlatTextarea. Added a strategy to convert tags to and from tags-for-mime-data. --- Telegram/SourceFiles/historywidget.cpp | 31 +++- Telegram/SourceFiles/ui/flattextarea.cpp | 178 ++++++++++++++--------- Telegram/SourceFiles/ui/flattextarea.h | 29 +++- 3 files changed, 162 insertions(+), 76 deletions(-) diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index f6f3974f0..949c04817 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -2754,6 +2754,32 @@ EntitiesInText entitiesFromFieldTags(const FlatTextarea::TagList &tags) { return result; } +namespace { + +// For mention tags save and validate userId, ignore tags for different userId. +class FieldTagMimeProcessor : public FlatTextarea::TagMimeProcessor { +public: + QString mimeTagFromTag(const QString &tagId) override { + if (tagId.startsWith(qstr("mention://"))) { + return tagId + ':' + QString::number(MTP::authedId()); + } + return tagId; + } + + QString tagFromMimeTag(const QString &mimeTag) override { + if (mimeTag.startsWith(qstr("mention://"))) { + auto match = QRegularExpression(":(\\d+)$").match(mimeTag); + if (!match.hasMatch() || match.capturedRef(1).toInt() != MTP::authedId()) { + return QString(); + } + return mimeTag.mid(0, mimeTag.size() - match.capturedLength()); + } + return mimeTag; + } +}; + +} // namespace + HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent) , _fieldBarCancel(this, st::replyCancel) , _scroll(this, st::historyScroll, false) @@ -2874,6 +2900,7 @@ HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent) connect(_fieldAutocomplete, SIGNAL(botCommandChosen(QString,FieldAutocomplete::ChooseMethod)), this, SLOT(onHashtagOrBotCommandInsert(QString,FieldAutocomplete::ChooseMethod))); connect(_fieldAutocomplete, SIGNAL(stickerChosen(DocumentData*,FieldAutocomplete::ChooseMethod)), this, SLOT(onStickerSend(DocumentData*))); _field.installEventFilter(_fieldAutocomplete); + _field.setTagMimeProcessor(std_::make_unique()); updateFieldSubmitSettings(); _field.hide(); @@ -2940,7 +2967,7 @@ void HistoryWidget::onMentionInsert(UserData *user) { } else { replacement = '@' + user->username; } - _field.insertMentionHashtagOrBotCommand(replacement, entityTag); + _field.insertTag(replacement, entityTag); } void HistoryWidget::onHashtagOrBotCommandInsert(QString str, FieldAutocomplete::ChooseMethod method) { @@ -2949,7 +2976,7 @@ void HistoryWidget::onHashtagOrBotCommandInsert(QString str, FieldAutocomplete:: App::sendBotCommand(_peer, nullptr, str); setFieldText(_field.getLastText().mid(_field.textCursor().position())); } else { - _field.insertMentionHashtagOrBotCommand(str); + _field.insertTag(str); } } diff --git a/Telegram/SourceFiles/ui/flattextarea.cpp b/Telegram/SourceFiles/ui/flattextarea.cpp index 51c7ee0f5..3e7b4c4ca 100644 --- a/Telegram/SourceFiles/ui/flattextarea.cpp +++ b/Telegram/SourceFiles/ui/flattextarea.cpp @@ -418,65 +418,75 @@ QString FlatTextarea::getMentionHashtagBotCommandPart(bool &start) const { return QString(); } -void FlatTextarea::insertMentionHashtagOrBotCommand(const QString &data, const QString &tagId) { - QTextCursor c(textCursor()); - int32 pos = c.position(); +void FlatTextarea::insertTag(const QString &text, QString tagId) { + auto cursor = textCursor(); + int32 pos = cursor.position(); - QTextDocument *doc(document()); - QTextBlock block = doc->findBlock(pos); - for (QTextBlock::Iterator iter = block.begin(); !iter.atEnd(); ++iter) { - QTextFragment fr(iter.fragment()); - if (!fr.isValid()) continue; + auto doc = document(); + auto block = doc->findBlock(pos); + for (auto iter = block.begin(); !iter.atEnd(); ++iter) { + auto fragment = iter.fragment(); + t_assert(fragment.isValid()); - int32 p = fr.position(), e = (p + fr.length()); - if (p >= pos || e < pos) continue; + int fragmentPosition = fragment.position(); + int fragmentEnd = (fragmentPosition + fragment.length()); + if (fragmentPosition >= pos || fragmentEnd < pos) continue; - QTextCharFormat f = fr.charFormat(); - if (f.isImageFormat()) continue; + auto format = fragment.charFormat(); + if (format.isImageFormat()) continue; bool mentionInCommand = false; - QString t(fr.text()); - for (int i = pos - p; i > 0; --i) { - if (t.at(i - 1) == '@' || t.at(i - 1) == '#' || t.at(i - 1) == '/') { - if ((i == pos - p || (t.at(i - 1) == '/' ? t.at(i).isLetterOrNumber() : t.at(i).isLetter()) || t.at(i - 1) == '#') && (i < 2 || !(t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_'))) { - c.setPosition(p + i - 1, QTextCursor::MoveAnchor); - int till = p + i; - for (; (till < e) && (till - p - i + 1 < data.size()); ++till) { - if (t.at(till - p).toLower() != data.at(till - p - i + 1).toLower()) { + auto fragmentText = fragment.text(); + for (int i = pos - fragmentPosition; i > 0; --i) { + auto previousChar = fragmentText.at(i - 1); + if (previousChar == '@' || previousChar == '#' || previousChar == '/') { + if ((i == pos - fragmentPosition || (previousChar == '/' ? fragmentText.at(i).isLetterOrNumber() : fragmentText.at(i).isLetter()) || previousChar == '#') && + (i < 2 || !(fragmentText.at(i - 2).isLetterOrNumber() || fragmentText.at(i - 2) == '_'))) { + cursor.setPosition(fragmentPosition + i - 1, QTextCursor::MoveAnchor); + int till = fragmentPosition + i; + for (; (till < fragmentEnd) && (till - fragmentPosition - i + 1 < text.size()); ++till) { + if (fragmentText.at(till - fragmentPosition).toLower() != text.at(till - fragmentPosition - i + 1).toLower()) { break; } } - if (till - p - i + 1 == data.size() && till < e && t.at(till - p) == ' ') { + if (till - fragmentPosition - i + 1 == text.size() && till < fragmentEnd && fragmentText.at(till - fragmentPosition) == ' ') { ++till; } - c.setPosition(till, QTextCursor::KeepAnchor); + cursor.setPosition(till, QTextCursor::KeepAnchor); break; - } else if ((i == pos - p || t.at(i).isLetter()) && t.at(i - 1) == '@' && i > 2 && (t.at(i - 2).isLetterOrNumber() || t.at(i - 2) == '_') && !mentionInCommand) { + } else if ((i == pos - fragmentPosition || fragmentText.at(i).isLetter()) && fragmentText.at(i - 1) == '@' && i > 2 && (fragmentText.at(i - 2).isLetterOrNumber() || fragmentText.at(i - 2) == '_') && !mentionInCommand) { mentionInCommand = true; --i; continue; } break; } - if (pos - p - i > 127 || (!mentionInCommand && (pos - p - i > 63))) break; - if (!t.at(i - 1).isLetterOrNumber() && t.at(i - 1) != '_') break; + if (pos - fragmentPosition - i > 127 || (!mentionInCommand && (pos - fragmentPosition - i > 63))) break; + if (!fragmentText.at(i - 1).isLetterOrNumber() && fragmentText.at(i - 1) != '_') break; } break; } if (tagId.isEmpty()) { - QTextCharFormat format = c.charFormat(); + QTextCharFormat format = cursor.charFormat(); format.setAnchor(false); format.setAnchorName(QString()); format.clearForeground(); - c.insertText(data + ' ', format); + cursor.insertText(text + ' ', format); } else { _insertedTags.clear(); - _insertedTags.push_back({ 0, data.size(), tagId + '/' + QString::number(rand_value()) }); - c.insertText(data + ' '); + if (_tagMimeProcessor) { + tagId = _tagMimeProcessor->mimeTagFromTag(tagId); + } + _insertedTags.push_back({ 0, text.size(), tagId }); + cursor.insertText(text + ' '); _insertedTags.clear(); } } +void FlatTextarea::setTagMimeProcessor(std_::unique_ptr &&processor) { + _tagMimeProcessor = std_::move(processor); +} + void FlatTextarea::getSingleEmojiFragment(QString &text, QTextFragment &fragment) const { int32 end = textCursor().position(), start = end - 1; if (textCursor().anchor() != end) return; @@ -544,25 +554,38 @@ public: return _changed; } - void feed(const QString &tagId, int currentPosition) { - if (tagId == _currentTagId) return; + void feed(const QString &randomTagId, int currentPosition) { + if (randomTagId == _currentTagId) return; if (!_currentTagId.isEmpty()) { - FlatTextarea::Tag tag = { - _currentStart, - currentPosition - _currentStart, - _currentTagId, - }; - if (_currentTag >= _tags->size()) { + int randomPartPosition = _currentTagId.lastIndexOf('/'); + t_assert(randomPartPosition > 0); + + bool tagChanged = true; + if (_currentTag < _tags->size()) { + auto &alreadyTag = _tags->at(_currentTag); + if (alreadyTag.offset == _currentStart && + alreadyTag.length == currentPosition - _currentStart && + alreadyTag.id == _currentTagId.midRef(0, randomPartPosition)) { + tagChanged = false; + } + } + if (tagChanged) { _changed = true; - _tags->push_back(tag); - } else if (_tags->at(_currentTag) != tag) { - _changed = true; - (*_tags)[_currentTag] = tag; + FlatTextarea::Tag tag = { + _currentStart, + currentPosition - _currentStart, + _currentTagId.mid(0, randomPartPosition), + }; + if (_currentTag < _tags->size()) { + (*_tags)[_currentTag] = tag; + } else { + _tags->push_back(tag); + } } ++_currentTag; } - _currentTagId = tagId; + _currentTagId = randomTagId; _currentStart = currentPosition; }; @@ -771,7 +794,7 @@ void FlatTextarea::parseLinks() { // some code is duplicated in text.cpp! continue; } } - newLinks.push_back(qMakePair(domainOffset - 1, p - start - domainOffset + 2)); + newLinks.push_back({ domainOffset - 1, p - start - domainOffset + 2 }); offset = matchOffset = p - start; } @@ -785,8 +808,8 @@ QStringList FlatTextarea::linksList() const { QStringList result; if (!_links.isEmpty()) { QString text(toPlainText()); - for (LinkRanges::const_iterator i = _links.cbegin(), e = _links.cend(); i != e; ++i) { - result.push_back(text.mid(i->first + 1, i->second - 2)); + for_const (auto &link, _links) { + result.push_back(text.mid(link.start + 1, link.length - 2)); } } return result; @@ -865,7 +888,7 @@ void removeTags(QTextDocument *document, int from, int end) { } // Returns the position of the first inserted tag or "changedEnd" value if none found. -int processInsertedTags(QTextDocument *document, int changedPosition, int changedEnd, const FlatTextarea::TagList &tags) { +int processInsertedTags(QTextDocument *document, int changedPosition, int changedEnd, const FlatTextarea::TagList &tags, FlatTextarea::TagMimeProcessor *processor) { int firstTagStart = changedEnd; int applyNoTagFrom = changedEnd; for_const (auto &tag, tags) { @@ -873,7 +896,8 @@ int processInsertedTags(QTextDocument *document, int changedPosition, int change int tagTo = tagFrom + tag.length; accumulate_max(tagFrom, changedPosition); accumulate_min(tagTo, changedEnd); - if (tagTo > tagFrom) { + auto tagId = processor ? processor->tagFromMimeTag(tag.id) : tag.id; + if (tagTo > tagFrom && !tagId.isEmpty()) { accumulate_min(firstTagStart, tagFrom); prepareFormattingOptimization(document); @@ -886,7 +910,7 @@ int processInsertedTags(QTextDocument *document, int changedPosition, int change QTextCharFormat format; format.setAnchor(true); - format.setAnchorName(tag.id); + format.setAnchorName(tagId + '/' + QString::number(rand_value())); format.setForeground(st::defaultTextStyle.linkFg); c.mergeCharFormat(format); @@ -945,7 +969,7 @@ struct FormattingAction { } // namespace -void FlatTextarea::processFormatting(int changedPosition, int changedEnd) { +void FlatTextarea::processFormatting(int insertPosition, int insertEnd) { // Tilde formatting. auto regularFont = qsl("Open Sans"), semiboldFont = qsl("Open Sans Semibold"); bool tildeFormatting = !cRetina() && (font().pixelSize() == 13) && (font().family() == regularFont); @@ -958,13 +982,13 @@ void FlatTextarea::processFormatting(int changedPosition, int changedEnd) { auto doc = document(); // Apply inserted tags. - int breakTagOnNotLetterTill = processInsertedTags(doc, changedPosition, changedEnd, _insertedTags); + int breakTagOnNotLetterTill = processInsertedTags(doc, insertPosition, insertEnd, _insertedTags, _tagMimeProcessor.get()); using ActionType = FormattingAction::Type; while (true) { FormattingAction action; - auto fromBlock = doc->findBlock(changedPosition); - auto tillBlock = doc->findBlock(changedEnd); + auto fromBlock = doc->findBlock(insertPosition); + auto tillBlock = doc->findBlock(insertEnd); if (tillBlock.isValid()) tillBlock = tillBlock.next(); for (auto block = fromBlock; block != tillBlock; block = block.next()) { @@ -973,11 +997,11 @@ void FlatTextarea::processFormatting(int changedPosition, int changedEnd) { t_assert(fragment.isValid()); int fragmentPosition = fragment.position(); - if (changedPosition >= fragmentPosition + fragment.length()) { + if (insertPosition >= fragmentPosition + fragment.length()) { continue; } - int changedPositionInFragment = changedPosition - fragmentPosition; // Can be negative. - int changedEndInFragment = changedEnd - fragmentPosition; + int changedPositionInFragment = insertPosition - fragmentPosition; // Can be negative. + int changedEndInFragment = insertEnd - fragmentPosition; if (changedEndInFragment <= 0) { break; } @@ -995,7 +1019,7 @@ void FlatTextarea::processFormatting(int changedPosition, int changedEnd) { startTagFound = true; auto tagName = charFormat.anchorName(); if (!tagName.isEmpty()) { - breakTagOnNotLetter = wasInsertTillTheEndOfTag(block, fragmentIt, changedEnd); + breakTagOnNotLetter = wasInsertTillTheEndOfTag(block, fragmentIt, insertEnd); } } @@ -1058,7 +1082,7 @@ void FlatTextarea::processFormatting(int changedPosition, int changedEnd) { c.setPosition(action.intervalEnd, QTextCursor::KeepAnchor); if (action.type == ActionType::InsertEmoji) { insertEmoji(action.emoji, c); - changedPosition = action.intervalStart + 1; + insertPosition = action.intervalStart + 1; } else if (action.type == ActionType::RemoveTag) { QTextCharFormat format; format.setAnchor(false); @@ -1069,7 +1093,7 @@ void FlatTextarea::processFormatting(int changedPosition, int changedEnd) { QTextCharFormat format; format.setFontFamily(action.isTilde ? semiboldFont : regularFont); c.mergeCharFormat(format); - changedPosition = action.intervalEnd; + insertPosition = action.intervalEnd; } } else { break; @@ -1083,6 +1107,9 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int int insertPosition = (_realInsertPosition >= 0) ? _realInsertPosition : position; int insertLength = (_realInsertPosition >= 0) ? _realCharsAdded : charsAdded; + int removePosition = position; + int removeLength = charsRemoved; + QTextCursor(document()->docHandle(), 0).joinPreviousEditBlock(); _correcting = true; @@ -1109,20 +1136,24 @@ void FlatTextarea::onDocumentContentsChange(int position, int charsRemoved, int } _correcting = false; - if (!_links.isEmpty()) { - bool changed = false; - for (auto i = _links.begin(); i != _links.end();) { - if (i->first + i->second <= insertPosition) { - ++i; - } else if (i->first >= insertPosition + charsRemoved) { - i->first += insertLength - charsRemoved; - ++i; - } else { - i = _links.erase(i); - changed = true; + if (insertPosition == removePosition) { + if (!_links.isEmpty()) { + bool changed = false; + for (auto i = _links.begin(); i != _links.end();) { + if (i->start + i->length <= insertPosition) { + ++i; + } else if (i->start >= removePosition + removeLength) { + i->start += insertLength - removeLength; + ++i; + } else { + i = _links.erase(i); + changed = true; + } } + if (changed) emit linksChanged(); } - if (changed) emit linksChanged(); + } else { + parseLinks(); } if (document()->availableRedoSteps() > 0) { @@ -1223,7 +1254,12 @@ QMimeData *FlatTextarea::createMimeDataFromSelection() const { TagList tags; result->setText(getText(start, end, &tags, nullptr)); if (!tags.isEmpty()) { - result->setData(qsl("application/x-td-field-tags"), serializeTagsList(tags)); + if (_tagMimeProcessor) { + for (auto &tag : tags) { + tag.id = _tagMimeProcessor->mimeTagFromTag(tag.id); + } + } + result->setData(str_const_toString(TagsMimeType), serializeTagsList(tags)); } } return result; diff --git a/Telegram/SourceFiles/ui/flattextarea.h b/Telegram/SourceFiles/ui/flattextarea.h index e9fe232da..aac48d4df 100644 --- a/Telegram/SourceFiles/ui/flattextarea.h +++ b/Telegram/SourceFiles/ui/flattextarea.h @@ -92,7 +92,16 @@ public: const TagList &getLastTags() const { return _oldtags; } - void insertMentionHashtagOrBotCommand(const QString &data, const QString &tagId = QString()); + void insertTag(const QString &text, QString tagId = QString()); + + // 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 { + public: + virtual QString mimeTagFromTag(const QString &tagId) = 0; + virtual QString tagFromMimeTag(const QString &mimeTag) = 0; + }; + void setTagMimeProcessor(std_::unique_ptr &&processor); public slots: @@ -177,6 +186,8 @@ private: int _realInsertPosition = -1; int _realCharsAdded = 0; + std_::unique_ptr _tagMimeProcessor; + style::flatTextarea _st; bool _undoAvailable = false; @@ -194,8 +205,13 @@ private: bool _correcting = false; - typedef QPair LinkRange; - typedef QList LinkRanges; + struct LinkRange { + int start; + int length; + }; + friend bool operator==(const LinkRange &a, const LinkRange &b); + friend bool operator!=(const LinkRange &a, const LinkRange &b); + using LinkRanges = QVector; LinkRanges _links; }; @@ -205,3 +221,10 @@ inline bool operator==(const FlatTextarea::Tag &a, const FlatTextarea::Tag &b) { inline bool operator!=(const FlatTextarea::Tag &a, const FlatTextarea::Tag &b) { return !(a == b); } + +inline bool operator==(const FlatTextarea::LinkRange &a, const FlatTextarea::LinkRange &b) { + return (a.start == b.start) && (a.length == b.length); +} +inline bool operator!=(const FlatTextarea::LinkRange &a, const FlatTextarea::LinkRange &b) { + return !(a == b); +} From 463450e607a826a54e49714020fec747de9028c8 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 5 May 2016 19:04:17 +0300 Subject: [PATCH 06/20] 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. --- Telegram/Resources/winrc/Telegram.rc | 8 +- Telegram/Resources/winrc/Updater.rc | 8 +- Telegram/SourceFiles/core/basic_types.h | 10 ++ Telegram/SourceFiles/core/version.h | 4 +- Telegram/SourceFiles/history.h | 15 +- Telegram/SourceFiles/historywidget.cpp | 158 +++++++++++------- Telegram/SourceFiles/historywidget.h | 24 +-- Telegram/SourceFiles/localstorage.cpp | 66 +++++--- Telegram/SourceFiles/localstorage.h | 8 +- Telegram/SourceFiles/mainwidget.cpp | 18 +- Telegram/SourceFiles/mainwidget.h | 3 +- Telegram/SourceFiles/pspecific_mac.cpp | 2 +- .../serialize/serialize_common.cpp | 4 - .../SourceFiles/serialize/serialize_common.h | 12 +- Telegram/SourceFiles/ui/flattextarea.cpp | 116 ++++++++----- Telegram/SourceFiles/ui/flattextarea.h | 63 ++++--- Telegram/Telegram.xcodeproj/project.pbxproj | 4 +- Telegram/build/version | 6 +- 18 files changed, 324 insertions(+), 205 deletions(-) diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 6352baeaf..6df5730f8 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -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" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 182787008..5a58c3a01 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -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" diff --git a/Telegram/SourceFiles/core/basic_types.h b/Telegram/SourceFiles/core/basic_types.h index 0e2d05e64..d828ffd20 100644 --- a/Telegram/SourceFiles/core/basic_types.h +++ b/Telegram/SourceFiles/core/basic_types.h @@ -166,6 +166,16 @@ template 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 operator|(Flags::enum_type f1, Flags::enum_type f2) Q_DECL_NOTHROW; \ +friend QFlags operator|(Flags::enum_type f1, QFlags 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 v" then "for (T * const p : v)" will still call QVector::detach(), diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 4f4c0a710..cb1108299 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -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; diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h index cc8a8bec1..fd61a44bc 100644 --- a/Telegram/SourceFiles/history.h +++ b/Telegram/SourceFiles/history.h @@ -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 } diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 949c04817..448dbf54b 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -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(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(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(_field, _editMsgId, _previewCancelled, _saveEditMsgRequestId)); } else { - if (_replyToId || !_field.getLastText().isEmpty()) { + if (_replyToId || !_field.isEmpty()) { _history->setMsgDraft(std_::make_unique(_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(QString(), to->id, MessageCursor(), false)); + _history->setMsgDraft(std_::make_unique(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(_field, _replyToId, _previewCancelled)); } else { _history->clearMsgDraft(); } - QString text(textApplyEntities(to->originalText(), to->originalEntities())); - _history->setEditDraft(std_::make_unique(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(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); diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h index 189afb9e4..76093067c 100644 --- a/Telegram/SourceFiles/historywidget.h +++ b/Telegram/SourceFiles/historywidget.h @@ -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) diff --git a/Telegram/SourceFiles/localstorage.cpp b/Telegram/SourceFiles/localstorage.cpp index 1aee7a285..8df89e4c0 100644 --- a/Telegram/SourceFiles/localstorage.cpp +++ b/Telegram/SourceFiles/localstorage.cpp @@ -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(msgText, msgReplyTo, msgCursor, msgPreviewCancelled)); + h->setMsgDraft(std_::make_unique(msgData, msgReplyTo, msgCursor, msgPreviewCancelled)); } if (!editMsgId) { h->clearEditDraft(); } else { - h->setEditDraft(std_::make_unique(editText, editMsgId, editCursor, editPreviewCancelled)); + h->setEditDraft(std_::make_unique(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); diff --git a/Telegram/SourceFiles/localstorage.h b/Telegram/SourceFiles/localstorage.h index 10264515a..ee5e8ba6e 100644 --- a/Telegram/SourceFiles/localstorage.h +++ b/Telegram/SourceFiles/localstorage.h @@ -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); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index e59cae222..a724cec8f 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -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(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(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(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(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; diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 17af829aa..7da61f427 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -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; diff --git a/Telegram/SourceFiles/pspecific_mac.cpp b/Telegram/SourceFiles/pspecific_mac.cpp index e1e94fbd2..6ebacc89f 100644 --- a/Telegram/SourceFiles/pspecific_mac.cpp +++ b/Telegram/SourceFiles/pspecific_mac.cpp @@ -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; diff --git a/Telegram/SourceFiles/serialize/serialize_common.cpp b/Telegram/SourceFiles/serialize/serialize_common.cpp index 0d7ee09f6..ea9d68e17 100644 --- a/Telegram/SourceFiles/serialize/serialize_common.cpp +++ b/Telegram/SourceFiles/serialize/serialize_common.cpp @@ -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()); diff --git a/Telegram/SourceFiles/serialize/serialize_common.h b/Telegram/SourceFiles/serialize/serialize_common.h index 7412104fe..dc57155f1 100644 --- a/Telegram/SourceFiles/serialize/serialize_common.h +++ b/Telegram/SourceFiles/serialize/serialize_common.h @@ -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); diff --git a/Telegram/SourceFiles/ui/flattextarea.cpp b/Telegram/SourceFiles/ui/flattextarea.cpp index 3e7b4c4ca..9486b8dbf 100644 --- a/Telegram/SourceFiles/ui/flattextarea.cpp +++ b/Telegram/SourceFiles/ui/flattextarea.cpp @@ -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; diff --git a/Telegram/SourceFiles/ui/flattextarea.h b/Telegram/SourceFiles/ui/flattextarea.h index aac48d4df..8315022ea 100644 --- a/Telegram/SourceFiles/ui/flattextarea.h +++ b/Telegram/SourceFiles/ui/flattextarea.h @@ -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; + 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; - 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); } diff --git a/Telegram/Telegram.xcodeproj/project.pbxproj b/Telegram/Telegram.xcodeproj/project.pbxproj index 41b3df598..9bcf3780d 100644 --- a/Telegram/Telegram.xcodeproj/project.pbxproj +++ b/Telegram/Telegram.xcodeproj/project.pbxproj @@ -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; }; diff --git a/Telegram/build/version b/Telegram/build/version index b2d958b19..406870c55 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -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 From 3e5f51f45a995e26b1367d89a884062531c33355 Mon Sep 17 00:00:00 2001 From: John Preston Date: Fri, 6 May 2016 20:33:48 +0300 Subject: [PATCH 07/20] Everywhere TextWithTags and TextWithEntities are used. Copy tags from messages to clipboard, to drag mime data. Sorting entities while processing (links, monospace, mentions). --- Telegram/SourceFiles/app.cpp | 4 +- Telegram/SourceFiles/boxes/photosendbox.cpp | 5 +- Telegram/SourceFiles/core/click_handler.cpp | 10 +- Telegram/SourceFiles/core/click_handler.h | 11 +- .../SourceFiles/core/click_handler_types.cpp | 53 +++-- .../SourceFiles/core/click_handler_types.h | 12 +- Telegram/SourceFiles/history.cpp | 206 ++++++++++-------- Telegram/SourceFiles/history.h | 82 ++++--- Telegram/SourceFiles/historywidget.cpp | 171 +++++++++------ Telegram/SourceFiles/historywidget.h | 4 +- Telegram/SourceFiles/mainwidget.cpp | 5 +- Telegram/SourceFiles/mediaview.cpp | 2 +- .../SourceFiles/overview/overview_layout.cpp | 6 +- Telegram/SourceFiles/ui/flatinput.cpp | 3 +- Telegram/SourceFiles/ui/flattextarea.cpp | 23 +- Telegram/SourceFiles/ui/text/text.cpp | 201 +++++++++-------- Telegram/SourceFiles/ui/text/text.h | 11 +- Telegram/SourceFiles/ui/text/text_entity.cpp | 85 ++++++-- Telegram/SourceFiles/ui/text/text_entity.h | 17 +- 19 files changed, 542 insertions(+), 369 deletions(-) diff --git a/Telegram/SourceFiles/app.cpp b/Telegram/SourceFiles/app.cpp index 1d9f2082f..5c9ab777c 100644 --- a/Telegram/SourceFiles/app.cpp +++ b/Telegram/SourceFiles/app.cpp @@ -968,7 +968,9 @@ namespace { peerId = peerFromUser(m.vfrom_id); } if (HistoryItem *existing = App::histItemById(peerToChannel(peerId), m.vid.v)) { - existing->setText(qs(m.vmessage), m.has_entities() ? entitiesFromMTP(m.ventities.c_vector().v) : EntitiesInText()); + auto text = qs(m.vmessage); + auto entities = m.has_entities() ? entitiesFromMTP(m.ventities.c_vector().v) : EntitiesInText(); + existing->setText({ text, entities }); existing->updateMedia(m.has_media() ? (&m.vmedia) : nullptr); existing->setViewsCount(m.has_views() ? m.vviews.v : -1); existing->addToOverview(AddToOverviewNew); diff --git a/Telegram/SourceFiles/boxes/photosendbox.cpp b/Telegram/SourceFiles/boxes/photosendbox.cpp index 987295b9c..f3e6b47d0 100644 --- a/Telegram/SourceFiles/boxes/photosendbox.cpp +++ b/Telegram/SourceFiles/boxes/photosendbox.cpp @@ -414,7 +414,7 @@ EditCaptionBox::EditCaptionBox(HistoryItem *msg) : AbstractBox(st::boxWideWidth) image = doc->thumb; } break; } - caption = media->getCaption(); + caption = media->getCaption().text; } if ((!_animated && (dimensions.isEmpty() || doc)) || image->isNull()) { _animated = false; @@ -492,7 +492,8 @@ EditCaptionBox::EditCaptionBox(HistoryItem *msg) : AbstractBox(st::boxWideWidth) _field->setMaxLength(MaxPhotoCaption); _field->setCtrlEnterSubmit(CtrlEnterSubmitBoth); } else { - QString text = textApplyEntities(msg->originalText(), msg->originalEntities()); + auto original = msg->originalText(); + QString text = textApplyEntities(original.text, original.entities); _field = new InputArea(this, st::editTextArea, lang(lng_photo_caption), text); // _field->setMaxLength(MaxMessageSize); // entities can make text in input field larger but still valid _field->setCtrlEnterSubmit(cCtrlEnter() ? CtrlEnterSubmitCtrlEnter : CtrlEnterSubmitEnter); diff --git a/Telegram/SourceFiles/core/click_handler.cpp b/Telegram/SourceFiles/core/click_handler.cpp index 99b6b5a42..82cc2231c 100644 --- a/Telegram/SourceFiles/core/click_handler.cpp +++ b/Telegram/SourceFiles/core/click_handler.cpp @@ -66,6 +66,12 @@ QString ClickHandler::getExpandedLinkText(ExpandLinksMode mode, const QStringRef return QString(); } -EntityInText ClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { - return EntityInText(EntityInTextInvalid, offset, 0); +TextWithEntities ClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { + return { QString(), EntitiesInText() }; +} + +TextWithEntities ClickHandler::simpleTextWithEntity(const EntityInText &entity) const { + TextWithEntities result; + result.entities.push_back(entity); + return result; } diff --git a/Telegram/SourceFiles/core/click_handler.h b/Telegram/SourceFiles/core/click_handler.h index b40ff9b5d..c0199fac9 100644 --- a/Telegram/SourceFiles/core/click_handler.h +++ b/Telegram/SourceFiles/core/click_handler.h @@ -42,9 +42,9 @@ protected: }; class EntityInText; +struct TextWithEntities; class ClickHandler { public: - virtual ~ClickHandler() { } @@ -71,8 +71,7 @@ public: // This method returns empty string if just textPart should be used (nothing to expand). virtual QString getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const; - - virtual EntityInText getEntityInText(int offset, const QStringRef &textPart) const; + virtual TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const; // This method should be called on mouse over a click handler. // It returns true if the active handler was changed or false otherwise. @@ -152,8 +151,12 @@ public: } } -private: +protected: + // For click handlers like mention or hashtag in getExpandedLinkTextWithEntities() + // we return just an empty string ("use original string part") with single entity. + TextWithEntities simpleTextWithEntity(const EntityInText &entity) const; +private: static NeverFreedPointer _active; static NeverFreedPointer _pressed; static ClickHandlerHost *_activeHost; diff --git a/Telegram/SourceFiles/core/click_handler_types.cpp b/Telegram/SourceFiles/core/click_handler_types.cpp index 3b5e82cad..05043abf2 100644 --- a/Telegram/SourceFiles/core/click_handler_types.cpp +++ b/Telegram/SourceFiles/core/click_handler_types.cpp @@ -75,18 +75,23 @@ void UrlClickHandler::doOpen(QString url) { } QString UrlClickHandler::getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const { - if (mode == ExpandLinksNone) { - return QString(); + QString result; + if (mode != ExpandLinksNone) { + result = _originalUrl; } - return _originalUrl; + return result; } -EntityInText UrlClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { - auto u = _originalUrl; - if (isEmail(u)) { - return EntityInText(EntityInTextUrl, offset, u.size()); +TextWithEntities UrlClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { + TextWithEntities result; + auto entityType = isEmail(_originalUrl) ? EntityInTextEmail : EntityInTextUrl; + int entityLength = textPart.size(); + if (mode != ExpandLinksNone) { + result.text = _originalUrl; + entityLength = _originalUrl.size(); } - return EntityInText(EntityInTextUrl, offset, u.size()); + result.entities.push_back({ entityType, entityOffset, entityLength }); + return result; } void HiddenUrlClickHandler::onClick(Qt::MouseButton button) const { @@ -102,14 +107,20 @@ void HiddenUrlClickHandler::onClick(Qt::MouseButton button) const { } QString HiddenUrlClickHandler::getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const { - if (mode != ExpandLinksAll) { - return QString(); + QString result; + if (mode == ExpandLinksAll) { + result = textPart.toString() + qsl(" (") + url() + ')'; } - return textPart.toString() + qsl(" (") + url() + ')'; + return result; } -EntityInText HiddenUrlClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { - return EntityInText(EntityInTextCustomUrl, offset, textPart.size(), url()); +TextWithEntities HiddenUrlClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { + TextWithEntities result; + result.entities.push_back({ EntityInTextCustomUrl, entityOffset, textPart.size(), url() }); + if (mode == ExpandLinksAll) { + result.text = textPart.toString() + qsl(" (") + url() + ')'; + } + return result; } QString MentionClickHandler::copyToClipboardContextItemText() const { @@ -122,8 +133,8 @@ void MentionClickHandler::onClick(Qt::MouseButton button) const { } } -EntityInText MentionClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { - return EntityInText(EntityInTextMention, offset, textPart.size()); +TextWithEntities MentionClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { + return simpleTextWithEntity({ EntityInTextMention, entityOffset, textPart.size() }); } void MentionNameClickHandler::onClick(Qt::MouseButton button) const { @@ -134,9 +145,9 @@ void MentionNameClickHandler::onClick(Qt::MouseButton button) const { } } -EntityInText MentionNameClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { +TextWithEntities MentionNameClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { auto data = QString::number(_userId) + '.' + QString::number(_accessHash); - return EntityInText(EntityInTextMentionName, offset, textPart.size(), data); + return simpleTextWithEntity({ EntityInTextMentionName, entityOffset, textPart.size(), data }); } QString MentionNameClickHandler::tooltip() const { @@ -159,8 +170,8 @@ void HashtagClickHandler::onClick(Qt::MouseButton button) const { } } -EntityInText HashtagClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { - return EntityInText(EntityInTextHashtag, offset, textPart.size()); +TextWithEntities HashtagClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { + return simpleTextWithEntity({ EntityInTextHashtag, entityOffset, textPart.size() }); } void BotCommandClickHandler::onClick(Qt::MouseButton button) const { @@ -180,6 +191,6 @@ void BotCommandClickHandler::onClick(Qt::MouseButton button) const { } } -EntityInText BotCommandClickHandler::getEntityInText(int offset, const QStringRef &textPart) const { - return EntityInText(EntityInTextHashtag, offset, textPart.size()); +TextWithEntities BotCommandClickHandler::getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const { + return simpleTextWithEntity({ EntityInTextHashtag, entityOffset, textPart.size() }); } diff --git a/Telegram/SourceFiles/core/click_handler_types.h b/Telegram/SourceFiles/core/click_handler_types.h index 316b1f275..d25078278 100644 --- a/Telegram/SourceFiles/core/click_handler_types.h +++ b/Telegram/SourceFiles/core/click_handler_types.h @@ -70,7 +70,7 @@ public: } QString getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const override; - EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const override; static void doOpen(QString url); void onClick(Qt::MouseButton button) const override { @@ -118,7 +118,7 @@ public: void onClick(Qt::MouseButton button) const override; QString getExpandedLinkText(ExpandLinksMode mode, const QStringRef &textPart) const override; - EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const override; }; @@ -135,7 +135,7 @@ public: QString copyToClipboardContextItemText() const override; - EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const override; protected: QString url() const override { @@ -157,7 +157,7 @@ public: void onClick(Qt::MouseButton button) const override; - EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const override; QString tooltip() const override; @@ -181,7 +181,7 @@ public: QString copyToClipboardContextItemText() const override; - EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const override; protected: QString url() const override { @@ -204,7 +204,7 @@ public: return _cmd; } - EntityInText getEntityInText(int offset, const QStringRef &textPart) const override; + TextWithEntities getExpandedLinkTextWithEntities(ExpandLinksMode mode, int entityOffset, const QStringRef &textPart) const override; protected: QString url() const override { diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index 7907cdc2d..60d53120a 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -1032,7 +1032,7 @@ HistoryItem *History::createItem(const MTPMessage &msg, bool applyServiceAction, EntitiesInText entities; textParseEntities(text, _historyTextNoMonoOptions.flags, &entities); entities.push_front(EntityInText(EntityInTextItalic, 0, text.size())); - result = HistoryMessage::create(this, m.vid.v, m.vflags.v, m.vreply_to_msg_id.v, m.vvia_bot_id.v, date(m.vdate), m.vfrom_id.v, text, entities); + result = HistoryMessage::create(this, m.vid.v, m.vflags.v, m.vreply_to_msg_id.v, m.vvia_bot_id.v, date(m.vdate), m.vfrom_id.v, { text, entities }); } else if (badMedia) { result = HistoryService::create(this, m.vid.v, date(m.vdate), lang(lng_message_empty), m.vflags.v, m.has_from_id() ? m.vfrom_id.v : 0); } else { @@ -3095,22 +3095,21 @@ void HistoryItem::setId(MsgId newId) { } bool HistoryItem::canEdit(const QDateTime &cur) const { - int32 s = date.secsTo(cur); auto channel = _history->peer->asChannel(); if (!channel || id < 0 || date.secsTo(cur) >= Global::EditTimeLimit()) return false; - if (const HistoryMessage *msg = toHistoryMessage()) { + if (auto msg = toHistoryMessage()) { if (msg->Has() || msg->Has()) return false; if (HistoryMedia *media = msg->getMedia()) { - HistoryMediaType t = media->type(); - if (t != MediaTypePhoto && - t != MediaTypeVideo && - t != MediaTypeFile && - t != MediaTypeGif && - t != MediaTypeMusicFile && - t != MediaTypeVoiceFile && - t != MediaTypeWebPage) { + auto type = media->type(); + if (type != MediaTypePhoto && + type != MediaTypeVideo && + type != MediaTypeFile && + type != MediaTypeGif && + type != MediaTypeMusicFile && + type != MediaTypeVoiceFile && + type != MediaTypeWebPage) { return false; } } @@ -3304,18 +3303,24 @@ int32 gifMaxStatusWidth(DocumentData *document) { return result; } -QString captionedSelectedText(const QString &attachType, const Text &caption, TextSelection selection) { +TextWithEntities captionedSelectedText(const QString &attachType, const Text &caption, TextSelection selection) { if (selection != FullSelection) { - return caption.original(selection, ExpandLinksAll); + return caption.originalTextWithEntities(selection, ExpandLinksAll); } - QString result; - result.reserve(5 + attachType.size() + caption.length()); - result.append(qstr("[ ")).append(attachType).append(qstr(" ]")); + + TextWithEntities result, original; if (!caption.isEmpty()) { - result.append(qstr("\n")).append(caption.original(AllTextSelection)); + original = caption.originalTextWithEntities(AllTextSelection, ExpandLinksAll); + } + result.text.reserve(5 + attachType.size() + original.text.size()); + result.text.append(qstr("[ ")).append(attachType).append(qstr(" ]")); + if (!caption.isEmpty()) { + result.text.append(qstr("\n")); + appendTextWithEntities(result, std_::move(original)); } return result; } + } // namespace void HistoryFileMedia::clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) { @@ -3735,10 +3740,10 @@ void HistoryPhoto::detachFromParent() { } QString HistoryPhoto::inDialogsText() const { - return _caption.isEmpty() ? lang(lng_in_dlg_photo) : _caption.original(AllTextSelection, ExpandLinksNone); + return _caption.isEmpty() ? lang(lng_in_dlg_photo) : _caption.originalText(AllTextSelection, ExpandLinksNone); } -QString HistoryPhoto::selectedText(TextSelection selection) const { +TextWithEntities HistoryPhoto::selectedText(TextSelection selection) const { return captionedSelectedText(lang(lng_in_dlg_photo), _caption, selection); } @@ -3981,10 +3986,10 @@ void HistoryVideo::setStatusSize(int32 newSize) const { } QString HistoryVideo::inDialogsText() const { - return _caption.isEmpty() ? lang(lng_in_dlg_video) : _caption.original(AllTextSelection, ExpandLinksNone); + return _caption.isEmpty() ? lang(lng_in_dlg_video) : _caption.originalText(AllTextSelection, ExpandLinksNone); } -QString HistoryVideo::selectedText(TextSelection selection) const { +TextWithEntities HistoryVideo::selectedText(TextSelection selection) const { return captionedSelectedText(lang(lng_in_dlg_video), _caption, selection); } @@ -4472,13 +4477,13 @@ QString HistoryDocument::inDialogsText() const { } if (auto captioned = Get()) { if (!captioned->_caption.isEmpty()) { - result.append(' ').append(captioned->_caption.original(AllTextSelection, ExpandLinksNone)); + result.append(' ').append(captioned->_caption.originalText(AllTextSelection, ExpandLinksNone)); } } return result; } -QString HistoryDocument::selectedText(TextSelection selection) const { +TextWithEntities HistoryDocument::selectedText(TextSelection selection) const { const Text emptyCaption; const Text *caption = &emptyCaption; if (auto captioned = Get()) { @@ -4930,10 +4935,10 @@ HistoryTextState HistoryGif::getState(int x, int y, HistoryStateRequest request) } QString HistoryGif::inDialogsText() const { - return qsl("GIF") + (_caption.isEmpty() ? QString() : (' ' + _caption.original(AllTextSelection, ExpandLinksNone))); + return qsl("GIF") + (_caption.isEmpty() ? QString() : (' ' + _caption.originalText(AllTextSelection, ExpandLinksNone))); } -QString HistoryGif::selectedText(TextSelection selection) const { +TextWithEntities HistoryGif::selectedText(TextSelection selection) const { return captionedSelectedText(qsl("GIF"), _caption, selection); } @@ -5250,11 +5255,11 @@ QString HistorySticker::inDialogsText() const { return _emoji.isEmpty() ? lang(lng_in_dlg_sticker) : lng_in_dlg_sticker_emoji(lt_emoji, _emoji); } -QString HistorySticker::selectedText(TextSelection selection) const { +TextWithEntities HistorySticker::selectedText(TextSelection selection) const { if (selection != FullSelection) { - return QString(); + return TextWithEntities(); } - return qsl("[ ") + inDialogsText() + qsl(" ]"); + return { qsl("[ ") + inDialogsText() + qsl(" ]"), EntitiesInText() }; } void HistorySticker::attachToParent() { @@ -5435,11 +5440,11 @@ QString HistoryContact::inDialogsText() const { return lang(lng_in_dlg_contact); } -QString HistoryContact::selectedText(TextSelection selection) const { +TextWithEntities HistoryContact::selectedText(TextSelection selection) const { if (selection != FullSelection) { - return QString(); + return TextWithEntities(); } - return qsl("[ ") + lang(lng_in_dlg_contact) + qsl(" ]\n") + _name.original() + '\n' + _phone; + return { qsl("[ ") + lang(lng_in_dlg_contact) + qsl(" ]\n") + _name.originalText() + '\n' + _phone, EntitiesInText() }; } void HistoryContact::attachToParent() { @@ -5956,18 +5961,21 @@ QString HistoryWebPage::inDialogsText() const { return QString(); } -QString HistoryWebPage::selectedText(TextSelection selection) const { +TextWithEntities HistoryWebPage::selectedText(TextSelection selection) const { if (selection == FullSelection) { - return QString(); + return TextWithEntities(); } - auto titleResult = _title.original(selection, ExpandLinksAll); - auto descriptionResult = _description.original(toDescriptionSelection(selection), ExpandLinksAll); - if (titleResult.isEmpty()) { + auto titleResult = _title.originalTextWithEntities(selection, ExpandLinksAll); + auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection(selection), ExpandLinksAll); + if (titleResult.text.isEmpty()) { return descriptionResult; - } else if (descriptionResult.isEmpty()) { + } else if (descriptionResult.text.isEmpty()) { return titleResult; } - return titleResult + '\n' + descriptionResult; + + titleResult.text += '\n'; + appendTextWithEntities(titleResult, std_::move(descriptionResult)); + return titleResult; } ImagePtr HistoryWebPage::replyPreview() { @@ -6399,24 +6407,31 @@ TextSelection HistoryLocation::adjustSelection(TextSelection selection, TextSele } QString HistoryLocation::inDialogsText() const { - return _title.isEmpty() ? lang(lng_maps_point) : _title.original(AllTextSelection); + return _title.isEmpty() ? lang(lng_maps_point) : _title.originalText(AllTextSelection); } -QString HistoryLocation::selectedText(TextSelection selection) const { +TextWithEntities HistoryLocation::selectedText(TextSelection selection) const { if (selection == FullSelection) { - auto result = qsl("[ ") + lang(lng_maps_point) + qsl(" ]\n"); + TextWithEntities result = { qsl("[ ") + lang(lng_maps_point) + qsl(" ]\n"), EntitiesInText() }; auto info = selectedText(AllTextSelection); - if (!info.isEmpty()) result.append(info).append('\n'); - return result + _link->dragText(); + if (!info.text.isEmpty()) { + appendTextWithEntities(result, std_::move(info)); + result.text.append('\n'); + } + result.text += _link->dragText(); + return result; } - auto titleResult = _title.original(selection); - auto descriptionResult = _description.original(toDescriptionSelection(selection)); - if (titleResult.isEmpty()) { + + auto titleResult = _title.originalTextWithEntities(selection); + auto descriptionResult = _description.originalTextWithEntities(toDescriptionSelection(selection)); + if (titleResult.text.isEmpty()) { return descriptionResult; - } else if (descriptionResult.isEmpty()) { + } else if (descriptionResult.text.isEmpty()) { return titleResult; } - return titleResult + '\n' + descriptionResult; + titleResult.text += '\n'; + appendTextWithEntities(titleResult, std_::move(descriptionResult)); + return titleResult; } int32 HistoryLocation::fullWidth() const { @@ -6729,7 +6744,12 @@ HistoryMessage::HistoryMessage(History *history, const MTPDmessage &msg) QString text(textClean(qs(msg.vmessage))); initMedia(msg.has_media() ? (&msg.vmedia) : nullptr, text); - setText(text, msg.has_entities() ? entitiesFromMTP(msg.ventities.c_vector().v) : EntitiesInText()); + + TextWithEntities textWithEntities = { text, EntitiesInText() }; + if (msg.has_entities()) { + textWithEntities.entities = entitiesFromMTP(msg.ventities.c_vector().v); + } + setText(textWithEntities); } HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *fwd) @@ -6755,14 +6775,14 @@ HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags fl if (HistoryMedia *mediaOriginal = fwd->getMedia()) { _media.reset(mediaOriginal->clone(this)); } - setText(fwd->originalText(), fwd->originalEntities()); + setText(fwd->originalText()); } -HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const QString &msg, const EntitiesInText &entities) +HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const TextWithEntities &textWithEntities) : HistoryItem(history, id, flags, date, (flags & MTPDmessage::Flag::f_from_id) ? from : 0) { createComponentsHelper(flags, replyTo, viaBotId, MTPnullMarkup); - setText(msg, entities); + setText(textWithEntities); } HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) @@ -6770,7 +6790,7 @@ HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags createComponentsHelper(flags, replyTo, viaBotId, markup); initMediaFromDocument(doc, caption); - setText(QString(), EntitiesInText()); + setText(TextWithEntities()); } HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup) @@ -6778,7 +6798,7 @@ HistoryMessage::HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags createComponentsHelper(flags, replyTo, viaBotId, markup); _media.reset(new HistoryPhoto(this, photo, caption)); - setText(QString(), EntitiesInText()); + setText(TextWithEntities()); } void HistoryMessage::createComponentsHelper(MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, const MTPReplyMarkup &markup) { @@ -7069,11 +7089,6 @@ void HistoryMessage::applyEdition(const MTPDmessage &message) { } } - EntitiesInText entities; - if (message.has_entities()) { - entities = entitiesFromMTP(message.ventities.c_vector().v); - } - if (message.has_edit_date()) { _flags |= MTPDmessage::Flag::f_edit_date; if (!Has()) { @@ -7083,7 +7098,11 @@ void HistoryMessage::applyEdition(const MTPDmessage &message) { initTime(); } - setText(qs(message.vmessage), entities); + TextWithEntities textWithEntities = { qs(message.vmessage), EntitiesInText() }; + if (message.has_entities()) { + textWithEntities.entities = entitiesFromMTP(message.ventities.c_vector().v); + } + setText(textWithEntities); setMedia(message.has_media() ? (&message.vmedia) : nullptr); setReplyMarkup(message.has_reply_markup() ? (&message.vreply_markup) : nullptr); setViewsCount(message.has_views() ? message.vviews.v : -1); @@ -7157,36 +7176,44 @@ void HistoryMessage::eraseFromOverview() { } } -QString HistoryMessage::selectedText(TextSelection selection) const { - QString result, textResult, mediaResult; +TextWithEntities HistoryMessage::selectedText(TextSelection selection) const { + TextWithEntities result, textResult, mediaResult; if (selection == FullSelection) { - textResult = _text.original(AllTextSelection, ExpandLinksAll); + textResult = _text.originalTextWithEntities(AllTextSelection, ExpandLinksAll); } else { - textResult = _text.original(selection, ExpandLinksAll); + textResult = _text.originalTextWithEntities(selection, ExpandLinksAll); } if (_media) { mediaResult = _media->selectedText(toMediaSelection(selection)); } - if (textResult.isEmpty()) { + if (textResult.text.isEmpty()) { result = mediaResult; - } else if (mediaResult.isEmpty()) { + } else if (mediaResult.text.isEmpty()) { result = textResult; } else { - result = textResult + qstr("\n\n") + mediaResult; + result.text = textResult.text + qstr("\n\n"); + result.entities = textResult.entities; + appendTextWithEntities(result, std_::move(mediaResult)); } if (auto fwd = Get()) { if (selection == FullSelection) { - QString fwdinfo = fwd->_text.original(AllTextSelection, ExpandLinksAll), wrapped; - wrapped.reserve(fwdinfo.size() + 4 + result.size()); - wrapped.append('[').append(fwdinfo).append(qsl("]\n")).append(result); + auto fwdinfo = fwd->_text.originalTextWithEntities(AllTextSelection, ExpandLinksAll); + TextWithEntities wrapped; + wrapped.text.reserve(fwdinfo.text.size() + 4 + result.text.size()); + wrapped.entities.reserve(fwdinfo.entities.size() + result.entities.size()); + wrapped.text.append('['); + appendTextWithEntities(wrapped, std_::move(fwdinfo)); + wrapped.text.append(qsl("]\n")); + appendTextWithEntities(wrapped, std_::move(result)); result = wrapped; } } if (auto reply = Get()) { if (selection == FullSelection && reply->replyToMsg) { - QString wrapped; - wrapped.reserve(lang(lng_in_reply_to).size() + reply->replyToMsg->author()->name.size() + 4 + result.size()); - wrapped.append('[').append(lang(lng_in_reply_to)).append(' ').append(reply->replyToMsg->author()->name).append(qsl("]\n")).append(result); + TextWithEntities wrapped; + wrapped.text.reserve(lang(lng_in_reply_to).size() + reply->replyToMsg->author()->name.size() + 4 + result.text.size()); + wrapped.text.append('[').append(lang(lng_in_reply_to)).append(' ').append(reply->replyToMsg->author()->name).append(qsl("]\n")); + appendTextWithEntities(wrapped, std_::move(result)); result = wrapped; } } @@ -7194,7 +7221,7 @@ QString HistoryMessage::selectedText(TextSelection selection) const { } QString HistoryMessage::inDialogsText() const { - return emptyText() ? (_media ? _media->inDialogsText() : QString()) : _text.original(AllTextSelection, ExpandLinksNone); + return emptyText() ? (_media ? _media->inDialogsText() : QString()) : _text.originalText(AllTextSelection, ExpandLinksNone); } HistoryMedia *HistoryMessage::getMedia() const { @@ -7222,16 +7249,16 @@ void HistoryMessage::setMedia(const MTPMessageMedia *media) { } } -void HistoryMessage::setText(const QString &text, const EntitiesInText &entities) { +void HistoryMessage::setText(const TextWithEntities &textWithEntities) { textstyleSet(&((out() && !isPost()) ? st::outTextStyle : st::inTextStyle)); if (_media && _media->isDisplayed()) { - _text.setMarkedText(st::msgFont, text, entities, itemTextOptions(this)); + _text.setMarkedText(st::msgFont, textWithEntities, itemTextOptions(this)); } else { - _text.setMarkedText(st::msgFont, text + skipBlock(), entities, itemTextOptions(this)); + _text.setMarkedText(st::msgFont, { textWithEntities.text + skipBlock(), textWithEntities.entities }, itemTextOptions(this)); } textstyleRestore(); - for_const (const auto &entity, entities) { + for_const (auto &entity, textWithEntities.entities) { auto type = entity.type(); if (type == EntityInTextUrl || type == EntityInTextCustomUrl || type == EntityInTextEmail) { _flags |= MTPDmessage_ClientFlag::f_has_text_links; @@ -7286,15 +7313,14 @@ void HistoryMessage::setReplyMarkup(const MTPReplyMarkup *markup) { } } -QString HistoryMessage::originalText() const { - return emptyText() ? QString() : _text.original(); +TextWithEntities HistoryMessage::originalText() const { + if (emptyText()) { + return { QString(), EntitiesInText() }; + } + return _text.originalTextWithEntities(); } -EntitiesInText HistoryMessage::originalEntities() const { - return emptyText() ? EntitiesInText() : _text.originalEntities(); -} - -bool HistoryMessage::textHasLinks() { +bool HistoryMessage::textHasLinks() const { return emptyText() ? false : _text.hasLinks(); } @@ -8114,7 +8140,7 @@ bool HistoryService::updatePinnedText(const QString *pfrom, QString *ptext) { case MediaTypeVoiceFile: mediaText = lang(lng_action_pinned_media_voice); break; } if (mediaText.isEmpty()) { - QString original = pinned->msg->originalText(); + QString original = pinned->msg->originalText().text; int32 cutat = 0, limit = PinnedMessageTextLimit, size = original.size(); for (; limit > 0;) { --limit; @@ -8192,12 +8218,12 @@ void HistoryService::countPositionAndSize(int32 &left, int32 &width) const { width = maxwidth - st::msgServiceMargin.left() - st::msgServiceMargin.left(); } -QString HistoryService::selectedText(TextSelection selection) const { - return _text.original((selection == FullSelection) ? AllTextSelection : selection); +TextWithEntities HistoryService::selectedText(TextSelection selection) const { + return _text.originalTextWithEntities((selection == FullSelection) ? AllTextSelection : selection); } QString HistoryService::inDialogsText() const { - return _text.original(AllTextSelection, ExpandLinksNone); + return _text.originalText(AllTextSelection, ExpandLinksNone); } QString HistoryService::inReplyText() const { @@ -8381,7 +8407,7 @@ void HistoryService::drawInDialog(Painter &p, const QRect &r, bool act, const Hi } QString HistoryService::notificationText() const { - QString msg = _text.original(); + QString msg = _text.originalText(); if (msg.size() > 0xFF) msg = msg.mid(0, 0xFF) + qsl("..."); return msg; } diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h index fd61a44bc..7a8d8c7cc 100644 --- a/Telegram/SourceFiles/history.h +++ b/Telegram/SourceFiles/history.h @@ -1293,8 +1293,8 @@ public: } virtual void previousItemChanged(); - virtual QString selectedText(TextSelection selection) const { - return qsl("[-]"); + virtual TextWithEntities selectedText(TextSelection selection) const { + return { qsl("[-]"), EntitiesInText() }; } virtual QString inDialogsText() const { return qsl("-"); @@ -1302,6 +1302,9 @@ public: virtual QString inReplyText() const { return inDialogsText(); } + virtual TextWithEntities originalText() const { + return { QString(), EntitiesInText() }; + } virtual void drawInfo(Painter &p, int32 right, int32 bottom, int32 width, bool selected, InfoDisplayType type) const { } @@ -1363,15 +1366,9 @@ public: virtual HistoryMedia *getMedia() const { return nullptr; } - virtual void setText(const QString &text, const EntitiesInText &links) { + virtual void setText(const TextWithEntities &textWithEntities) { } - virtual QString originalText() const { - return QString(); - } - virtual EntitiesInText originalEntities() const { - return EntitiesInText(); - } - virtual bool textHasLinks() { + virtual bool textHasLinks() const { return false; } @@ -1663,7 +1660,7 @@ public: virtual HistoryMediaType type() const = 0; virtual QString inDialogsText() const = 0; - virtual QString selectedText(TextSelection selection) const = 0; + virtual TextWithEntities selectedText(TextSelection selection) const = 0; bool hasPoint(int x, int y) const { return (x >= 0 && y >= 0 && x < _width && y < _height); @@ -1750,8 +1747,8 @@ public: virtual ImagePtr replyPreview() { return ImagePtr(); } - virtual QString getCaption() const { - return QString(); + virtual TextWithEntities getCaption() const { + return TextWithEntities(); } virtual bool needsBubble() const = 0; virtual bool customInfoLayout() const = 0; @@ -1900,7 +1897,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; PhotoData *photo() const { return _data; @@ -1917,8 +1914,8 @@ public: } ImagePtr replyPreview() override; - QString getCaption() const override { - return _caption.original(); + TextWithEntities getCaption() const override { + return _caption.originalTextWithEntities(); } bool needsBubble() const override { if (!_caption.isEmpty()) { @@ -1980,7 +1977,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; DocumentData *getDocument() override { return _data; @@ -2000,8 +1997,8 @@ public: } ImagePtr replyPreview() override; - QString getCaption() const override { - return _caption.original(); + TextWithEntities getCaption() const override { + return _caption.originalTextWithEntities(); } bool needsBubble() const override { if (!_caption.isEmpty()) { @@ -2103,7 +2100,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; bool uploading() const override { return _data->uploading(); @@ -2124,11 +2121,11 @@ public: } ImagePtr replyPreview() override; - QString getCaption() const override { + TextWithEntities getCaption() const override { if (const HistoryDocumentCaptioned *captioned = Get()) { - return captioned->_caption.original(); + return captioned->_caption.originalTextWithEntities(); } - return QString(); + return TextWithEntities(); } bool needsBubble() const override { return true; @@ -2190,7 +2187,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; bool uploading() const override { return _data->uploading(); @@ -2217,8 +2214,8 @@ public: } ImagePtr replyPreview() override; - QString getCaption() const override { - return _caption.original(); + TextWithEntities getCaption() const override { + return _caption.originalTextWithEntities(); } bool needsBubble() const override { if (!_caption.isEmpty()) { @@ -2288,7 +2285,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; DocumentData *getDocument() override { return _data; @@ -2357,7 +2354,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; void attachToParent() override; void detachFromParent() override; @@ -2425,7 +2422,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; void clickHandlerActiveChanged(const ClickHandlerPtr &p, bool active) override; void clickHandlerPressedChanged(const ClickHandlerPtr &p, bool pressed) override; @@ -2559,7 +2556,7 @@ public: } QString inDialogsText() const override; - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; bool needsBubble() const override { if (!_title.isEmpty() || !_description.isEmpty()) { @@ -2613,8 +2610,8 @@ public: static HistoryMessage *create(History *history, MsgId msgId, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *fwd) { return _create(history, msgId, flags, date, from, fwd); } - static HistoryMessage *create(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const QString &msg, const EntitiesInText &entities) { - return _create(history, msgId, flags, replyTo, viaBotId, date, from, msg, entities); + static HistoryMessage *create(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const TextWithEntities &textWithEntities) { + return _create(history, msgId, flags, replyTo, viaBotId, date, from, textWithEntities); } static HistoryMessage *create(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup) { return _create(history, msgId, flags, replyTo, viaBotId, date, from, doc, caption, markup); @@ -2685,13 +2682,12 @@ public: int32 addToOverview(AddToOverviewMethod method) override; void eraseFromOverview(); - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; QString inDialogsText() const override; HistoryMedia *getMedia() const override; - void setText(const QString &text, const EntitiesInText &entities) override; - QString originalText() const override; - EntitiesInText originalEntities() const override; - bool textHasLinks() override; + void setText(const TextWithEntities &textWithEntities) override; + TextWithEntities originalText() const override; + bool textHasLinks() const override; int32 infoWidth() const override { int32 result = _timeWidth; @@ -2753,7 +2749,7 @@ private: HistoryMessage(History *history, const MTPDmessage &msg); HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *fwd); // local forwarded - HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const QString &msg, const EntitiesInText &entities); // local message + HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, const TextWithEntities &textWithEntities); // local message HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, DocumentData *doc, const QString &caption, const MTPReplyMarkup &markup); // local document HistoryMessage(History *history, MsgId msgId, MTPDmessage::Flags flags, MsgId replyTo, int32 viaBotId, QDateTime date, int32 from, PhotoData *photo, const QString &caption, const MTPReplyMarkup &markup); // local photo friend class HistoryItemInstantiated; @@ -2902,7 +2898,7 @@ public: bool serviceMsg() const override { return true; } - QString selectedText(TextSelection selection) const override; + TextWithEntities selectedText(TextSelection selection) const override; QString inDialogsText() const override; QString inReplyText() const override; @@ -2939,8 +2935,8 @@ public: HistoryTextState getState(int x, int y, HistoryStateRequest request) const override; - QString selectedText(TextSelection selection) const override { - return QString(); + TextWithEntities selectedText(TextSelection selection) const override { + return { QString(), EntitiesInText() }; } HistoryItemType type() const override { return HistoryItemGroup; @@ -2989,8 +2985,8 @@ public: void draw(Painter &p, const QRect &r, TextSelection selection, uint64 ms) const override; HistoryTextState getState(int x, int y, HistoryStateRequest request) const override; - QString selectedText(TextSelection selection) const override { - return QString(); + TextWithEntities selectedText(TextSelection selection) const override { + return { QString(), EntitiesInText() }; } HistoryItemType type() const override { return HistoryItemCollapse; diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 448dbf54b..14f0c9be5 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -39,6 +39,54 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "window/top_bar_widget.h" #include "playerwidget.h" +namespace { + +QString mimeTagFromTag(const QString &tagId) { + if (tagId.startsWith(qstr("mention://"))) { + return tagId + ':' + QString::number(MTP::authedId()); + } + return tagId; +} + +QMimeData *mimeDataFromTextWithEntities(const TextWithEntities &forClipboard) { + if (forClipboard.text.isEmpty()) { + return nullptr; + } + + auto result = new QMimeData(); + result->setText(forClipboard.text); + auto tags = textTagsFromEntities(forClipboard.entities); + if (!tags.isEmpty()) { + for (auto &tag : tags) { + tag.id = mimeTagFromTag(tag.id); + } + result->setData(FlatTextarea::tagsMimeType(), FlatTextarea::serializeTagsList(tags)); + } + return result; +} + +// For mention tags save and validate userId, ignore tags for different userId. +class FieldTagMimeProcessor : public FlatTextarea::TagMimeProcessor { +public: + QString mimeTagFromTag(const QString &tagId) override { + return ::mimeTagFromTag(tagId); + } + + QString tagFromMimeTag(const QString &mimeTag) override { + if (mimeTag.startsWith(qstr("mention://"))) { + auto match = QRegularExpression(":(\\d+)$").match(mimeTag); + if (!match.hasMatch() || match.capturedRef(1).toInt() != MTP::authedId()) { + return QString(); + } + return mimeTag.mid(0, mimeTag.size() - match.capturedLength()); + } + return mimeTag; + } + +}; + +} // namespace + // flick scroll taken from http://qt-project.org/doc/qt-4.8/demos-embedded-anomaly-src-flickcharm-cpp.html HistoryInner::HistoryInner(HistoryWidget *historyWidget, ScrollArea *scroll, History *history) : TWidget(nullptr) @@ -704,24 +752,21 @@ void HistoryInner::onDragExec() { } } ClickHandlerPtr pressedHandler = ClickHandler::getPressed(); - QString sel; + TextWithEntities sel; QList urls; if (uponSelected) { sel = getSelectedText(); } else if (pressedHandler) { - sel = pressedHandler->dragText(); + sel = { pressedHandler->dragText(), EntitiesInText() }; //if (!sel.isEmpty() && sel.at(0) != '/' && sel.at(0) != '@' && sel.at(0) != '#') { // urls.push_back(QUrl::fromEncoded(sel.toUtf8())); // Google Chrome crashes in Mac OS X O_o //} } - if (!sel.isEmpty()) { + if (auto mimeData = mimeDataFromTextWithEntities(sel)) { updateDragSelection(0, 0, false); _widget->noSelectingScroll(); QDrag *drag = new QDrag(App::wnd()); - QMimeData *mimeData = new QMimeData; - - mimeData->setText(sel); if (!urls.isEmpty()) mimeData->setUrls(urls); if (uponSelected && !_selected.isEmpty() && _selected.cbegin().value() == FullSelection && !Adaptive::OneColumn()) { mimeData->setData(qsl("application/x-td-forward-selected"), "1"); @@ -748,7 +793,7 @@ void HistoryInner::onDragExec() { } if (!forwardMimeType.isEmpty()) { QDrag *drag = new QDrag(App::wnd()); - QMimeData *mimeData = new QMimeData; + QMimeData *mimeData = new QMimeData(); mimeData->setData(forwardMimeType, "1"); if (DocumentData *document = (pressedMedia ? pressedMedia->getDocument() : nullptr)) { @@ -1124,10 +1169,7 @@ void HistoryInner::onMenuDestroy(QObject *obj) { } void HistoryInner::copySelectedText() { - QString sel = getSelectedText(); - if (!sel.isEmpty()) { - QApplication::clipboard()->setText(sel); - } + setToClipboard(getSelectedText()); } void HistoryInner::copyContextUrl() { @@ -1217,9 +1259,12 @@ void HistoryInner::copyContextText() { return; } - QString contextMenuText = item->selectedText(FullSelection); - if (!contextMenuText.isEmpty()) { - QApplication::clipboard()->setText(contextMenuText); + setToClipboard(item->selectedText(FullSelection)); +} + +void HistoryInner::setToClipboard(const TextWithEntities &forClipboard) { + if (auto data = mimeDataFromTextWithEntities(forClipboard)) { + QApplication::clipboard()->setMimeData(data); } } @@ -1227,42 +1272,48 @@ void HistoryInner::resizeEvent(QResizeEvent *e) { onUpdateSelected(); } -QString HistoryInner::getSelectedText() const { +TextWithEntities HistoryInner::getSelectedText() const { SelectedItems sel = _selected; if (_dragAction == Selecting && _dragSelFrom && _dragSelTo) { applyDragSelection(&sel); } - if (sel.isEmpty()) return QString(); + if (sel.isEmpty()) { + return TextWithEntities(); + } if (sel.cbegin().value() != FullSelection) { return sel.cbegin().key()->selectedText(sel.cbegin().value()); } - int32 fullSize = 0, mtop = migratedTop(), htop = historyTop(); + int fullSize = 0; QString timeFormat(qsl(", [dd.MM.yy hh:mm]\n")); - QMap texts; - for (SelectedItems::const_iterator i = sel.cbegin(), e = sel.cend(); i != e; ++i) { + QMap texts; + for (auto i = sel.cbegin(), e = sel.cend(); i != e; ++i) { HistoryItem *item = i.key(); if (item->detached()) continue; - QString text, sel = item->selectedText(FullSelection), time = item->date.toString(timeFormat); - int32 size = item->author()->name.size() + time.size() + sel.size(); - text.reserve(size); + QString time = item->date.toString(timeFormat); + TextWithEntities part, unwrapped = item->selectedText(FullSelection); + int size = item->author()->name.size() + time.size() + unwrapped.text.size(); + part.text.reserve(size); - int32 y = itemTop(item); + int y = itemTop(item); if (y >= 0) { - texts.insert(y, text.append(item->author()->name).append(time).append(sel)); + part.text.append(item->author()->name).append(time); + appendTextWithEntities(part, std_::move(unwrapped)); + texts.insert(y, part); fullSize += size; } } - QString result, sep(qsl("\n\n")); - result.reserve(fullSize + (texts.size() - 1) * 2); - for (QMap::const_iterator i = texts.cbegin(), e = texts.cend(); i != e; ++i) { - result.append(i.value()); + TextWithEntities result; + auto sep = qsl("\n\n"); + result.text.reserve(fullSize + (texts.size() - 1) * sep.size()); + for (auto i = texts.begin(), e = texts.end(); i != e; ++i) { + appendTextWithEntities(result, std_::move(i.value())); if (i + 1 != e) { - result.append(sep); + result.text.append(sep); } } return result; @@ -2027,7 +2078,7 @@ QString HistoryInner::tooltipText() const { } else if (_dragCursorState == HistoryInForwardedCursorState && _dragAction == NoDrag) { if (App::hoveredItem()) { if (HistoryMessageForwarded *fwd = App::hoveredItem()->Get()) { - return fwd->_text.original(AllTextSelection, ExpandLinksNone); + return fwd->_text.originalText(AllTextSelection, ExpandLinksNone); } } } else if (ClickHandlerPtr lnk = ClickHandler::getActive()) { @@ -2679,7 +2730,7 @@ bool HistoryHider::offerPeer(PeerId peer) { } QString HistoryHider::offeredText() const { - return toText.original(); + return toText.originalText(); } bool HistoryHider::wasOffered() const { @@ -2772,32 +2823,6 @@ TextWithTags::Tags textTagsFromEntities(const EntitiesInText &entities) { return result; } -namespace { - -// For mention tags save and validate userId, ignore tags for different userId. -class FieldTagMimeProcessor : public FlatTextarea::TagMimeProcessor { -public: - QString mimeTagFromTag(const QString &tagId) override { - if (tagId.startsWith(qstr("mention://"))) { - return tagId + ':' + QString::number(MTP::authedId()); - } - return tagId; - } - - QString tagFromMimeTag(const QString &mimeTag) override { - if (mimeTag.startsWith(qstr("mention://"))) { - auto match = QRegularExpression(":(\\d+)$").match(mimeTag); - if (!match.hasMatch() || match.capturedRef(1).toInt() != MTP::authedId()) { - return QString(); - } - return mimeTag.mid(0, mimeTag.size() - match.capturedLength()); - } - return mimeTag; - } -}; - -} // namespace - HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent) , _fieldBarCancel(this, st::replyCancel) , _scroll(this, st::historyScroll, false) @@ -6258,8 +6283,8 @@ void HistoryWidget::onPhotoUploaded(const FullMsgId &newId, bool silent, const M if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } - QString caption = item->getMedia() ? item->getMedia()->getCaption() : QString(); - hist->sendRequestId = MTP::send(MTPmessages_SendMedia(MTP_flags(sendFlags), item->history()->peer->input, MTP_int(replyTo), MTP_inputMediaUploadedPhoto(file, MTP_string(caption)), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); + auto caption = item->getMedia() ? item->getMedia()->getCaption() : TextWithEntities(); + hist->sendRequestId = MTP::send(MTPmessages_SendMedia(MTP_flags(sendFlags), item->history()->peer->input, MTP_int(replyTo), MTP_inputMediaUploadedPhoto(file, MTP_string(caption.text)), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); } } @@ -6310,8 +6335,8 @@ void HistoryWidget::onDocumentUploaded(const FullMsgId &newId, bool silent, cons if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } - QString caption = item->getMedia() ? item->getMedia()->getCaption() : QString(); - hist->sendRequestId = MTP::send(MTPmessages_SendMedia(MTP_flags(sendFlags), item->history()->peer->input, MTP_int(replyTo), MTP_inputMediaUploadedDocument(file, MTP_string(document->mime), _composeDocumentAttributes(document), MTP_string(caption)), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); + auto caption = item->getMedia() ? item->getMedia()->getCaption() : TextWithEntities(); + hist->sendRequestId = MTP::send(MTPmessages_SendMedia(MTP_flags(sendFlags), item->history()->peer->input, MTP_int(replyTo), MTP_inputMediaUploadedDocument(file, MTP_string(document->mime), _composeDocumentAttributes(document), MTP_string(caption.text)), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); } } } @@ -6339,8 +6364,8 @@ void HistoryWidget::onThumbDocumentUploaded(const FullMsgId &newId, bool silent, if (silentPost) { sendFlags |= MTPmessages_SendMedia::Flag::f_silent; } - QString caption = item->getMedia() ? item->getMedia()->getCaption() : QString(); - hist->sendRequestId = MTP::send(MTPmessages_SendMedia(MTP_flags(sendFlags), item->history()->peer->input, MTP_int(replyTo), MTP_inputMediaUploadedThumbDocument(file, thumb, MTP_string(document->mime), _composeDocumentAttributes(document), MTP_string(caption)), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); + auto caption = item->getMedia() ? item->getMedia()->getCaption() : TextWithEntities(); + hist->sendRequestId = MTP::send(MTPmessages_SendMedia(MTP_flags(sendFlags), item->history()->peer->input, MTP_int(replyTo), MTP_inputMediaUploadedThumbDocument(file, thumb, MTP_string(document->mime), _composeDocumentAttributes(document), MTP_string(caption.text)), MTP_long(randomId), MTPnullMarkup), App::main()->rpcDone(&MainWidget::sentUpdatesReceived), App::main()->rpcFail(&MainWidget::sendMessageFail), 0, 0, hist->sendRequestId); } } } @@ -7414,11 +7439,12 @@ void HistoryWidget::onEditMessage() { _history->clearMsgDraft(); } - 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(original, to->id, cursor, false)); + auto original = to->originalText(); + auto editText = textApplyEntities(original.text, original.entities); + auto editTags = textTagsFromEntities(original.entities); + TextWithTags editData = { editText, editTags }; + MessageCursor cursor = { editText.size(), editText.size(), QFIXED_MAX }; + _history->setEditDraft(std_::make_unique(editData, to->id, cursor, false)); applyDraft(false); _previewData = 0; @@ -7755,10 +7781,11 @@ void HistoryWidget::onCancel() { if (_inlineBotCancel) { onInlineBotCancel(); } else if (_editMsgId) { - 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 original = _replyEditMsg ? _replyEditMsg->originalText() : TextWithEntities(); + auto editText = textApplyEntities(original.text, original.entities); + auto editTags = textTagsFromEntities(original.entities); + TextWithTags editData = { editText, editTags }; + if (_replyEditMsg && editData != _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); diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h index 76093067c..5ed237aa6 100644 --- a/Telegram/SourceFiles/historywidget.h +++ b/Telegram/SourceFiles/historywidget.h @@ -57,7 +57,7 @@ public: void keyPressEvent(QKeyEvent *e) override; void showContextMenu(QContextMenuEvent *e, bool showFromTouch = false); - QString getSelectedText() const; + TextWithEntities getSelectedText() const; void dragActionStart(const QPoint &screenPos, Qt::MouseButton button = Qt::LeftButton); void dragActionUpdate(const QPoint &screenPos); @@ -144,6 +144,8 @@ private: HistoryItem *nextItem(HistoryItem *item); void updateDragSelection(HistoryItem *dragSelFrom, HistoryItem *dragSelTo, bool dragSelecting, bool force = false); + void setToClipboard(const TextWithEntities &forClipboard); + PeerData *_peer = nullptr; History *_migrated = nullptr; History *_history = nullptr; diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index a724cec8f..53cf80b79 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -3947,8 +3947,9 @@ void MainWidget::feedUpdates(const MTPUpdates &updates, uint64 randomId) { feedUpdate(MTP_updateMessageID(d.vid, MTP_long(randomId))); // ignore real date if (peerId) { - if (HistoryItem *item = App::histItemById(peerToChannel(peerId), d.vid.v)) { - item->setText(text, d.has_entities() ? entitiesFromMTP(d.ventities.c_vector().v) : EntitiesInText()); + if (auto item = App::histItemById(peerToChannel(peerId), d.vid.v)) { + auto entities = d.has_entities() ? entitiesFromMTP(d.ventities.c_vector().v) : EntitiesInText(); + item->setText({ text, entities }); item->updateMedia(d.has_media() ? (&d.vmedia) : nullptr); item->addToOverview(AddToOverviewNew); } diff --git a/Telegram/SourceFiles/mediaview.cpp b/Telegram/SourceFiles/mediaview.cpp index 005e78a09..0c8c0b804 100644 --- a/Telegram/SourceFiles/mediaview.cpp +++ b/Telegram/SourceFiles/mediaview.cpp @@ -912,7 +912,7 @@ void MediaView::displayPhoto(PhotoData *photo, HistoryItem *item) { _caption = Text(); if (HistoryMessage *itemMsg = item ? item->toHistoryMessage() : nullptr) { if (HistoryPhoto *photoMsg = dynamic_cast(itemMsg->getMedia())) { - _caption.setText(st::mvCaptionFont, photoMsg->getCaption(), (item->author()->isUser() && item->author()->asUser()->botInfo) ? _captionBotOptions : _captionTextOptions); + _caption.setMarkedText(st::mvCaptionFont, photoMsg->getCaption(), (item->author()->isUser() && item->author()->asUser()->botInfo) ? _captionBotOptions : _captionTextOptions); } } diff --git a/Telegram/SourceFiles/overview/overview_layout.cpp b/Telegram/SourceFiles/overview/overview_layout.cpp index 77358278a..42c69b4cc 100644 --- a/Telegram/SourceFiles/overview/overview_layout.cpp +++ b/Telegram/SourceFiles/overview/overview_layout.cpp @@ -898,9 +898,11 @@ bool Document::updateStatusText() const { Link::Link(HistoryMedia *media, HistoryItem *parent) : ItemBase(parent) { AddComponents(Info::Bit()); - QString text = _parent->originalText(), mainUrl; - EntitiesInText entities = _parent->originalEntities(); + const auto textWithEntities = _parent->originalText(); + QString mainUrl; + auto text = textWithEntities.text; + auto &entities = textWithEntities.entities; int32 from = 0, till = text.size(), lnk = entities.size(); for_const (const auto &entity, entities) { auto type = entity.type(); diff --git a/Telegram/SourceFiles/ui/flatinput.cpp b/Telegram/SourceFiles/ui/flatinput.cpp index 0a3d90f02..1afe8650e 100644 --- a/Telegram/SourceFiles/ui/flatinput.cpp +++ b/Telegram/SourceFiles/ui/flatinput.cpp @@ -1011,7 +1011,8 @@ void InputArea::processDocumentContentsChange(int position, int charsAdded) { if (!_inner.document()->pageSize().isNull()) { _inner.document()->setPageSize(QSizeF(0, 0)); } - QTextCursor c(doc->docHandle(), replacePosition); + QTextCursor c(doc->docHandle(), 0); + c.setPosition(replacePosition); c.setPosition(replacePosition + replaceLen, QTextCursor::KeepAnchor); if (emoji) { insertEmoji(emoji, c); diff --git a/Telegram/SourceFiles/ui/flattextarea.cpp b/Telegram/SourceFiles/ui/flattextarea.cpp index 9486b8dbf..ccababd19 100644 --- a/Telegram/SourceFiles/ui/flattextarea.cpp +++ b/Telegram/SourceFiles/ui/flattextarea.cpp @@ -463,7 +463,7 @@ void FlatTextarea::insertTag(const QString &text, QString tagId) { if (previousChar == '@' || previousChar == '#' || previousChar == '/') { if ((i == pos - fragmentPosition || (previousChar == '/' ? fragmentText.at(i).isLetterOrNumber() : fragmentText.at(i).isLetter()) || previousChar == '#') && (i < 2 || !(fragmentText.at(i - 2).isLetterOrNumber() || fragmentText.at(i - 2) == '_'))) { - cursor.setPosition(fragmentPosition + i - 1, QTextCursor::MoveAnchor); + cursor.setPosition(fragmentPosition + i - 1); int till = fragmentPosition + i; for (; (till < fragmentEnd) && (till - fragmentPosition - i + 1 < text.size()); ++till) { if (fragmentText.at(till - fragmentPosition).toLower() != text.at(till - fragmentPosition - i + 1).toLower()) { @@ -608,6 +608,13 @@ public: _currentStart = currentPosition; }; + void finish() { + if (_currentTag < _tags->size()) { + _tags->resize(_currentTag); + _changed = true; + } + } + private: FlatTextarea::TagList *_tags; bool _changed = false; @@ -709,6 +716,8 @@ QString FlatTextarea::getTextPart(int start, int end, TagList *outTagsList, bool result.chop(1); if (tillFragmentEnd) tagAccumulator.feed(QString(), result.size()); + tagAccumulator.finish(); + if (outTagsChanged) { *outTagsChanged = tagAccumulator.changed(); } @@ -842,7 +851,8 @@ void FlatTextarea::insertFromMimeData(const QMimeData *source) { } else { _insertedTags.clear(); } - _realInsertPosition = textCursor().position(); + auto cursor = textCursor(); + _realInsertPosition = qMin(cursor.position(), cursor.anchor()); _realCharsAdded = text.size(); QTextEdit::insertFromMimeData(source); if (!_inDrop) { @@ -895,7 +905,8 @@ void prepareFormattingOptimization(QTextDocument *document) { } void removeTags(QTextDocument *document, int from, int end) { - QTextCursor c(document->docHandle(), from); + QTextCursor c(document->docHandle(), 0); + c.setPosition(from); c.setPosition(end, QTextCursor::KeepAnchor); QTextCharFormat format; @@ -923,7 +934,8 @@ int processInsertedTags(QTextDocument *document, int changedPosition, int change if (applyNoTagFrom < tagFrom) { removeTags(document, applyNoTagFrom, tagFrom); } - QTextCursor c(document->docHandle(), tagFrom); + QTextCursor c(document->docHandle(), 0); + c.setPosition(tagFrom); c.setPosition(tagTo, QTextCursor::KeepAnchor); QTextCharFormat format; @@ -1098,7 +1110,8 @@ void FlatTextarea::processFormatting(int insertPosition, int insertEnd) { if (action.type != ActionType::Invalid) { prepareFormattingOptimization(doc); - QTextCursor c(doc->docHandle(), action.intervalStart); + QTextCursor c(doc->docHandle(), 0); + c.setPosition(action.intervalStart); c.setPosition(action.intervalEnd, QTextCursor::KeepAnchor); if (action.type == ActionType::InsertEmoji) { insertEmoji(action.emoji, c); diff --git a/Telegram/SourceFiles/ui/text/text.cpp b/Telegram/SourceFiles/ui/text/text.cpp index 79b72b78f..f828a2f28 100644 --- a/Telegram/SourceFiles/ui/text/text.cpp +++ b/Telegram/SourceFiles/ui/text/text.cpp @@ -555,14 +555,15 @@ public: } parse(options); } - TextParser(Text *t, const QString &text, const EntitiesInText &preparsed, const TextParseOptions &options) : _t(t), - src(text), + TextParser(Text *t, const TextWithEntities &textWithEntities, const TextParseOptions &options) : _t(t), + src(textWithEntities.text), rich(options.flags & TextParseRichText), multiline(options.flags & TextParseMultiline), maxLnkIndex(0), flags(0), lnkIndex(0), stopAfterWidth(QFIXED_MAX) { + auto preparsed = textWithEntities.entities; if ((options.flags & TextParseLinks) && !preparsed.isEmpty()) { bool parseMentions = (options.flags & TextParseMentions); bool parseHashtags = (options.flags & TextParseHashtags); @@ -573,7 +574,7 @@ public: } else { int32 i = 0, l = preparsed.size(); entities.reserve(l); - const QChar s = text.size(); + const QChar s = src.size(); for (; i < l; ++i) { auto type = preparsed.at(i).type(); if (((type == EntityInTextMention || type == EntityInTextMentionName) && !parseMentions) || @@ -2497,7 +2498,7 @@ void Text::recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir) { } } -void Text::setMarkedText(style::font font, const QString &text, const EntitiesInText &entities, const TextParseOptions &options) { +void Text::setMarkedText(style::font font, const TextWithEntities &textWithEntities, const TextParseOptions &options) { if (!_textStyle) initDefault(); _font = font; clear(); @@ -2532,7 +2533,7 @@ void Text::setMarkedText(style::font font, const QString &text, const EntitiesIn // newText.append("\n\n").append(text); // TextParser parser(this, newText, EntitiesInText(), options); - TextParser parser(this, text, entities, options); + TextParser parser(this, textWithEntities, options); } recountNaturalSize(true, options.dir); } @@ -2932,117 +2933,133 @@ TextSelection Text::adjustSelection(TextSelection selection, TextSelectType sele return { from, to }; } -QString Text::original(TextSelection selection, ExpandLinksMode mode) const { +template +void Text::enumerateText(TextSelection selection, AppendPartCallback appendPartCallback, ClickHandlerStartCallback clickHandlerStartCallback, ClickHandlerFinishCallback clickHandlerFinishCallback, FlagsChangeCallback flagsChangeCallback) const { if (isEmpty() || selection.empty()) { - return QString(); + return; } - QString result, emptyurl; - result.reserve(_text.size()); - - int32 lnkFrom = 0, lnkIndex = 0; - for (TextBlocks::const_iterator i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) { - int32 blockLnkIndex = (i == e) ? 0 : (*i)->lnkIndex(); - int32 blockFrom = (i == e) ? _text.size() : (*i)->from(); - if (blockLnkIndex && !_links.at(blockLnkIndex - 1)) { // ignore empty links - blockLnkIndex = 0; - } - if (blockLnkIndex != lnkIndex) { - int32 rangeFrom = qMax(int32(selection.from), lnkFrom), rangeTo = qMin(blockFrom, int32(selection.to)); - if (lnkIndex && rangeTo > rangeFrom) { // write link - QStringRef r = _text.midRef(rangeFrom, rangeTo - rangeFrom); - if (lnkFrom != rangeFrom || blockFrom != rangeTo) { - result += r; - } else { - QString expanded = _links.at(lnkIndex - 1)->getExpandedLinkText(mode, r); - if (expanded.isEmpty()) { - result += r; - } else { - result += expanded; - } - } - } - lnkIndex = blockLnkIndex; - lnkFrom = blockFrom; - } - if (i == e) break; - - TextBlockType type = (*i)->type(); - if (type == TextBlockTSkip) continue; - - if (!blockLnkIndex) { - int32 rangeFrom = qMax(selection.from, (*i)->from()), rangeTo = qMin(selection.to, uint16((*i)->from() + TextPainter::_blockLength(this, i, e))); - if (rangeTo > rangeFrom) { - result += _text.midRef(rangeFrom, rangeTo - rangeFrom); - } - } - } - return result; -} - -EntitiesInText Text::originalEntities() const { - EntitiesInText result; - int32 originalLength = 0, lnkStart = 0, italicStart = 0, boldStart = 0, codeStart = 0, preStart = 0; - int32 lnkFrom = 0, lnkIndex = 0, flags = 0; - for (TextBlocks::const_iterator i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) { - int32 blockLnkIndex = (i == e) ? 0 : (*i)->lnkIndex(); - int32 blockFrom = (i == e) ? _text.size() : (*i)->from(); + int lnkIndex = 0; + uint16 lnkFrom = 0; + int32 flags = 0; + for (auto i = _blocks.cbegin(), e = _blocks.cend(); true; ++i) { + int blockLnkIndex = (i == e) ? 0 : (*i)->lnkIndex(); + uint16 blockFrom = (i == e) ? _text.size() : (*i)->from(); int32 blockFlags = (i == e) ? 0 : (*i)->flags(); - if (blockFlags != flags) { - if ((flags & TextBlockFItalic) && !(blockFlags & TextBlockFItalic)) { // write italic - result.push_back(EntityInText(EntityInTextItalic, italicStart, originalLength - italicStart)); - } else if ((blockFlags & TextBlockFItalic) && !(flags & TextBlockFItalic)) { - italicStart = originalLength; - } - if ((flags & TextBlockFSemibold) && !(blockFlags & TextBlockFSemibold)) { - result.push_back(EntityInText(EntityInTextBold, boldStart, originalLength - boldStart)); - } else if ((blockFlags & TextBlockFSemibold) && !(flags & TextBlockFSemibold)) { - boldStart = originalLength; - } - if ((flags & TextBlockFCode) && !(blockFlags & TextBlockFCode)) { - result.push_back(EntityInText(EntityInTextCode, codeStart, originalLength - codeStart)); - } else if ((blockFlags & TextBlockFCode) && !(flags & TextBlockFCode)) { - codeStart = originalLength; - } - if ((flags & TextBlockFPre) && !(blockFlags & TextBlockFPre)) { - result.push_back(EntityInText(EntityInTextPre, preStart, originalLength - preStart)); - } else if ((blockFlags & TextBlockFPre) && !(flags & TextBlockFPre)) { - preStart = originalLength; - } + + bool checkBlockFlags = (blockFrom >= selection.from) && (blockFrom <= selection.to); + if (checkBlockFlags && blockFlags != flags) { + flagsChangeCallback(flags, blockFlags); flags = blockFlags; } if (blockLnkIndex && !_links.at(blockLnkIndex - 1)) { // ignore empty links blockLnkIndex = 0; } if (blockLnkIndex != lnkIndex) { - int32 rangeFrom = lnkFrom, rangeTo = blockFrom; - if (lnkIndex && rangeTo > rangeFrom) { // write link - QStringRef r = _text.midRef(rangeFrom, rangeTo - rangeFrom); - if (auto entity = _links.at(lnkIndex - 1)->getEntityInText(lnkStart, r)) { - result.push_back(entity); - originalLength += entity.length(); - } else { - originalLength += r.size(); + if (lnkIndex) { + auto rangeFrom = qMax(selection.from, lnkFrom); + auto rangeTo = qMin(blockFrom, selection.to); + if (rangeTo > rangeFrom) { // handle click handler + QStringRef r = _text.midRef(rangeFrom, rangeTo - rangeFrom); + if (lnkFrom != rangeFrom || blockFrom != rangeTo) { + appendPartCallback(r); + } else { + clickHandlerFinishCallback(r, _links.at(lnkIndex - 1)); + } } } lnkIndex = blockLnkIndex; if (lnkIndex) { lnkFrom = blockFrom; - lnkStart = originalLength; + clickHandlerStartCallback(); } } - if (i == e) break; + if (i == e || blockFrom >= selection.to) break; - TextBlockType type = (*i)->type(); - if (type == TextBlockTSkip) continue; + if ((*i)->type() == TextBlockTSkip) continue; if (!blockLnkIndex) { - int32 rangeFrom = (*i)->from(), rangeTo = uint16((*i)->from() + TextPainter::_blockLength(this, i, e)); + auto rangeFrom = qMax(selection.from, blockFrom); + auto rangeTo = qMin(selection.to, uint16(blockFrom + TextPainter::_blockLength(this, i, e))); if (rangeTo > rangeFrom) { - originalLength += rangeTo - rangeFrom; + appendPartCallback(_text.midRef(rangeFrom, rangeTo - rangeFrom)); } } } +} + +TextWithEntities Text::originalTextWithEntities(TextSelection selection, ExpandLinksMode mode) const { + TextWithEntities result; + result.text.reserve(_text.size()); + + int lnkStart = 0, italicStart = 0, boldStart = 0, codeStart = 0, preStart = 0; + auto flagsChangeCallback = [&result, &italicStart, &boldStart, &codeStart, &preStart](int32 oldFlags, int32 newFlags) { + if ((oldFlags & TextBlockFItalic) && !(newFlags & TextBlockFItalic)) { // write italic + result.entities.push_back(EntityInText(EntityInTextItalic, italicStart, result.text.size() - italicStart)); + } else if ((newFlags & TextBlockFItalic) && !(oldFlags & TextBlockFItalic)) { + italicStart = result.text.size(); + } + if ((oldFlags & TextBlockFSemibold) && !(newFlags & TextBlockFSemibold)) { + result.entities.push_back(EntityInText(EntityInTextBold, boldStart, result.text.size() - boldStart)); + } else if ((newFlags & TextBlockFSemibold) && !(oldFlags & TextBlockFSemibold)) { + boldStart = result.text.size(); + } + if ((oldFlags & TextBlockFCode) && !(newFlags & TextBlockFCode)) { + result.entities.push_back(EntityInText(EntityInTextCode, codeStart, result.text.size() - codeStart)); + } else if ((newFlags & TextBlockFCode) && !(oldFlags & TextBlockFCode)) { + codeStart = result.text.size(); + } + if ((oldFlags & TextBlockFPre) && !(newFlags & TextBlockFPre)) { + result.entities.push_back(EntityInText(EntityInTextPre, preStart, result.text.size() - preStart)); + } else if ((newFlags & TextBlockFPre) && !(oldFlags & TextBlockFPre)) { + preStart = result.text.size(); + } + }; + auto clickHandlerStartCallback = [&result, &lnkStart]() { + lnkStart = result.text.size(); + }; + auto clickHandlerFinishCallback = [mode, &result, &lnkStart](const QStringRef &r, const ClickHandlerPtr &handler) { + auto expanded = handler->getExpandedLinkTextWithEntities(mode, lnkStart, r); + if (expanded.text.isEmpty()) { + result.text += r; + } else { + result.text += expanded.text; + } + if (!expanded.entities.isEmpty()) { + result.entities.append(expanded.entities); + } + }; + auto appendPartCallback = [&result](const QStringRef &r) { + result.text += r; + }; + + enumerateText(selection, appendPartCallback, clickHandlerStartCallback, clickHandlerFinishCallback, flagsChangeCallback); + + return result; +} + +QString Text::originalText(TextSelection selection, ExpandLinksMode mode) const { + QString result; + result.reserve(_text.size()); + + auto appendPartCallback = [&result](const QStringRef &r) { + result += r; + }; + auto clickHandlerStartCallback = []() { + }; + auto clickHandlerFinishCallback = [mode, &result](const QStringRef &r, const ClickHandlerPtr &handler) { + auto expanded = handler->getExpandedLinkText(mode, r); + if (expanded.isEmpty()) { + result += r; + } else { + result += expanded; + } + }; + auto flagsChangeCallback = [](int32 oldFlags, int32 newFlags) { + }; + + enumerateText(selection, appendPartCallback, clickHandlerStartCallback, clickHandlerFinishCallback, flagsChangeCallback); + return result; } diff --git a/Telegram/SourceFiles/ui/text/text.h b/Telegram/SourceFiles/ui/text/text.h index 691e076b5..d9ea6e798 100644 --- a/Telegram/SourceFiles/ui/text/text.h +++ b/Telegram/SourceFiles/ui/text/text.h @@ -97,7 +97,7 @@ public: int32 countHeight(int32 width) const; void setText(style::font font, const QString &text, const TextParseOptions &options = _defaultOptions); void setRichText(style::font font, const QString &text, TextParseOptions options = _defaultOptions, const TextCustomTagsMap &custom = TextCustomTagsMap()); - void setMarkedText(style::font font, const QString &text, const EntitiesInText &entities, const TextParseOptions &options = _defaultOptions); + void setMarkedText(style::font font, const TextWithEntities &textWithEntities, const TextParseOptions &options = _defaultOptions); void setLink(uint16 lnkIndex, const ClickHandlerPtr &lnk); bool hasLinks() const; @@ -178,8 +178,9 @@ public: int length() const { return _text.size(); } - QString original(TextSelection selection = AllTextSelection, ExpandLinksMode mode = ExpandLinksShortened) const; - EntitiesInText originalEntities() const; + + TextWithEntities originalTextWithEntities(TextSelection selection = AllTextSelection, ExpandLinksMode mode = ExpandLinksShortened) const; + QString originalText(TextSelection selection = AllTextSelection, ExpandLinksMode mode = ExpandLinksShortened) const; bool lastDots(int32 dots, int32 maxdots = 3) { // hack for typing animation if (_text.size() < maxdots) return false; @@ -207,6 +208,10 @@ public: private: + // Template method for originalText(), originalTextWithEntities(). + template + void enumerateText(TextSelection selection, AppendPartCallback appendPartCallback, ClickHandlerStartCallback clickHandlerStartCallback, ClickHandlerFinishCallback clickHandlerFinishCallback, FlagsChangeCallback flagsChangeCallback) const; + void recountNaturalSize(bool initial, Qt::LayoutDirection optionsDir = Qt::LayoutDirectionAuto); // clear() deletes all blocks and calls this method diff --git a/Telegram/SourceFiles/ui/text/text_entity.cpp b/Telegram/SourceFiles/ui/text/text_entity.cpp index 7e4170cb6..d3a79d030 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.cpp +++ b/Telegram/SourceFiles/ui/text/text_entity.cpp @@ -1437,7 +1437,7 @@ MTPVector linksToMTP(const EntitiesInText &links, bool sending // Some code is duplicated in flattextarea.cpp! void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities, bool rich) { - EntitiesInText mono; + EntitiesInText result; bool withHashtags = (flags & TextParseHashtags); bool withMentions = (flags & TextParseMentions); @@ -1445,6 +1445,9 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities bool withMono = (flags & TextParseMono); if (withMono) { // parse mono entities (code and pre) + int existingEntityIndex = 0, existingEntitiesCount = inOutEntities->size(); + int existingEntityShiftLeft = 0; + QString newText; int32 offset = 0, matchOffset = offset, len = text.size(), commandOffset = rich ? 0 : len; @@ -1468,7 +1471,7 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities auto mCode = _reCode.match(text, matchOffset); if (!mPre.hasMatch() && !mCode.hasMatch()) break; - int32 preStart = mPre.hasMatch() ? mPre.capturedStart() : INT_MAX, + int preStart = mPre.hasMatch() ? mPre.capturedStart() : INT_MAX, preEnd = mPre.hasMatch() ? mPre.capturedEnd() : INT_MAX, codeStart = mCode.hasMatch() ? mCode.capturedStart() : INT_MAX, codeEnd = mCode.hasMatch() ? mCode.capturedEnd() : INT_MAX, @@ -1506,11 +1509,25 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities continue; } - if (newText.isEmpty()) newText.reserve(text.size()); - bool addNewlineBefore = false, addNewlineAfter = false; int32 outerStart = tagStart, outerEnd = tagEnd; int32 innerStart = tagStart + mTag.capturedLength(2), innerEnd = tagEnd - mTag.capturedLength(3); + + // Check if start or end sequences intersect any existing entity. + int intersectedEntityEnd = 0; + for_const (auto &entity, *inOutEntities) { + if (qMin(innerStart, entity.offset() + entity.length()) > qMax(outerStart, entity.offset()) || + qMin(outerEnd, entity.offset() + entity.length()) > qMax(innerEnd, entity.offset())) { + intersectedEntityEnd = entity.offset() + entity.length(); + break; + } + } + if (intersectedEntityEnd > 0) { + matchOffset = qMax(innerStart, intersectedEntityEnd); + continue; + } + + if (newText.isEmpty()) newText.reserve(text.size()); if (pre) { while (outerStart > 0 && chIsSpace(*(start + outerStart - 1), rich) && !chIsNewline(*(start + outerStart - 1))) { --outerStart; @@ -1540,14 +1557,27 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities } addNewlineAfter = (outerEnd < len && !chIsNewline(*(start + outerEnd))); } + + for (; existingEntityIndex < existingEntitiesCount && inOutEntities->at(existingEntityIndex).offset() < innerStart; ++existingEntityIndex) { + auto &entity = inOutEntities->at(existingEntityIndex); + result.push_back(entity); + result.back().shiftLeft(existingEntityShiftLeft); + } if (outerStart > offset) newText.append(start + offset, outerStart - offset); if (addNewlineBefore) newText.append('\n'); + existingEntityShiftLeft += (innerStart - outerStart) - (addNewlineBefore ? 1 : 0); - int32 tagLength = innerEnd - innerStart; - mono.push_back(EntityInText(pre ? EntityInTextPre : EntityInTextCode, newText.size(), tagLength)); + int entityStart = newText.size(), entityLength = innerEnd - innerStart; + result.push_back(EntityInText(pre ? EntityInTextPre : EntityInTextCode, entityStart, entityLength)); - newText.append(start + innerStart, tagLength); + for (; existingEntityIndex < existingEntitiesCount && inOutEntities->at(existingEntityIndex).offset() <= innerEnd; ++existingEntityIndex) { + auto &entity = inOutEntities->at(existingEntityIndex); + result.push_back(entity); + result.back().shiftLeft(existingEntityShiftLeft); + } + newText.append(start + innerStart, entityLength); if (addNewlineAfter) newText.append('\n'); + existingEntityShiftLeft += (outerEnd - innerEnd) - (addNewlineAfter ? 1 : 0); offset = matchOffset = outerEnd; } @@ -1555,8 +1585,19 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities newText.append(start + offset, len - offset); text = newText; } + if (!result.isEmpty()) { + for (; existingEntityIndex < existingEntitiesCount; ++existingEntityIndex) { + auto &entity = inOutEntities->at(existingEntityIndex); + result.push_back(entity); + result.back().shiftLeft(existingEntityShiftLeft); + } + *inOutEntities = result; + result = EntitiesInText(); + } } - int32 monoEntity = 0, monoCount = mono.size(), monoTill = 0; + + int existingEntityIndex = 0, existingEntitiesCount = inOutEntities->size(); + int existingEntityEnd = 0; initLinkSets(); int32 len = text.size(), commandOffset = rich ? 0 : len; @@ -1572,11 +1613,11 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities } } } - QRegularExpressionMatch mDomain = _reDomain.match(text, matchOffset); - QRegularExpressionMatch mExplicitDomain = _reExplicitDomain.match(text, matchOffset); - QRegularExpressionMatch mHashtag = withHashtags ? _reHashtag.match(text, matchOffset) : QRegularExpressionMatch(); - QRegularExpressionMatch mMention = withMentions ? _reMention.match(text, qMax(mentionSkip, matchOffset)) : QRegularExpressionMatch(); - QRegularExpressionMatch mBotCommand = withBotCommands ? _reBotCommand.match(text, matchOffset) : QRegularExpressionMatch(); + auto mDomain = _reDomain.match(text, matchOffset); + auto mExplicitDomain = _reExplicitDomain.match(text, matchOffset); + auto mHashtag = withHashtags ? _reHashtag.match(text, matchOffset) : QRegularExpressionMatch(); + auto mMention = withMentions ? _reMention.match(text, qMax(mentionSkip, matchOffset)) : QRegularExpressionMatch(); + auto mBotCommand = withBotCommands ? _reBotCommand.match(text, matchOffset) : QRegularExpressionMatch(); EntityInTextType lnkType = EntityInTextUrl; int32 lnkStart = 0, lnkLength = 0; @@ -1735,19 +1776,23 @@ void textParseEntities(QString &text, int32 flags, EntitiesInText *inOutEntities lnkLength = (p - start) - lnkStart; } } - for (; monoEntity < monoCount && mono[monoEntity].offset() <= lnkStart; ++monoEntity) { - monoTill = qMax(monoTill, mono[monoEntity].offset() + mono[monoEntity].length()); - inOutEntities->push_back(mono[monoEntity]); + for (; existingEntityIndex < existingEntitiesCount && inOutEntities->at(existingEntityIndex).offset() <= lnkStart; ++existingEntityIndex) { + auto &entity = inOutEntities->at(existingEntityIndex); + accumulate_max(existingEntityEnd, entity.offset() + entity.length()); + result.push_back(entity); } - if (lnkStart >= monoTill) { + if (lnkStart >= existingEntityEnd) { inOutEntities->push_back(EntityInText(lnkType, lnkStart, lnkLength)); } offset = matchOffset = lnkStart + lnkLength; } - for (; monoEntity < monoCount; ++monoEntity) { - monoTill = qMax(monoTill, mono[monoEntity].offset() + mono[monoEntity].length()); - inOutEntities->push_back(mono[monoEntity]); + if (!result.isEmpty()) { + for (; existingEntityIndex < existingEntitiesCount; ++existingEntityIndex) { + auto &entity = inOutEntities->at(existingEntityIndex); + result.push_back(entity); + } + *inOutEntities = result; } } diff --git a/Telegram/SourceFiles/ui/text/text_entity.h b/Telegram/SourceFiles/ui/text/text_entity.h index fe2f36c7f..ae9439ac3 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.h +++ b/Telegram/SourceFiles/ui/text/text_entity.h @@ -79,6 +79,9 @@ public: } } } + void shiftRight(int shift) { + _offset += shift; + } void updateTextEnd(int textEnd) { if (_offset > textEnd) { _offset = textEnd; @@ -108,7 +111,19 @@ private: QString _data; }; -typedef QList EntitiesInText; + +struct TextWithEntities { + QString text; + EntitiesInText entities; +}; +inline void appendTextWithEntities(TextWithEntities &to, TextWithEntities &&append) { + int entitiesShiftRight = to.text.size(); + for (auto &entity : append.entities) { + entity.shiftRight(entitiesShiftRight); + } + to.text += append.text; + to.entities += append.entities; +} // text preprocess QString textClean(const QString &text); From 90a3a80bf6e1ecd41892be8acac6eced26ee4d7b Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 8 May 2016 19:11:47 +0300 Subject: [PATCH 08/20] Suggest mentions not only by username, but by user names as well. --- .../history/field_autocomplete.cpp | 114 ++++++++++++------ .../SourceFiles/history/field_autocomplete.h | 9 +- Telegram/SourceFiles/historywidget.cpp | 37 +++++- Telegram/SourceFiles/historywidget.h | 7 +- Telegram/SourceFiles/ui/flattextarea.cpp | 7 +- 5 files changed, 124 insertions(+), 50 deletions(-) diff --git a/Telegram/SourceFiles/history/field_autocomplete.cpp b/Telegram/SourceFiles/history/field_autocomplete.cpp index f14509ac2..7d2a7c532 100644 --- a/Telegram/SourceFiles/history/field_autocomplete.cpp +++ b/Telegram/SourceFiles/history/field_autocomplete.cpp @@ -73,12 +73,12 @@ void FieldAutocomplete::paintEvent(QPaintEvent *e) { p.fillRect(rect(), st::white); } -void FieldAutocomplete::showFiltered(PeerData *peer, QString query, bool start) { -// _inner->showFiltered(peer, query, start); +void FieldAutocomplete::showFiltered(PeerData *peer, QString query, bool addInlineBots) { _chat = peer->asChat(); _user = peer->asUser(); _channel = peer->asChannel(); if (query.isEmpty()) { + _type = Type::Mentions; rowsUpdated(internal::MentionRows(), internal::HashtagRows(), internal::BotCommandRows(), _srows, false); return; } @@ -86,11 +86,28 @@ void FieldAutocomplete::showFiltered(PeerData *peer, QString query, bool start) _emoji = EmojiPtr(); query = query.toLower(); - bool resetScroll = (_filter != query); - if (resetScroll) { - _filter = query; + auto type = Type::Stickers; + auto plainQuery = query.midRef(0); + switch (query.at(0).unicode()) { + case '@': + type = Type::Mentions; + plainQuery = query.midRef(1); + break; + case '#': + type = Type::Hashtags; + plainQuery = query.midRef(1); + break; + case '/': + type = Type::BotCommands; + plainQuery = query.midRef(1); + break; } - _addInlineBots = start; + bool resetScroll = (_type != type || _filter != plainQuery); + if (resetScroll) { + _type = type; + _filter = plainQuery.toString(); + } + _addInlineBots = addInlineBots; updateFiltered(resetScroll); } @@ -98,6 +115,7 @@ void FieldAutocomplete::showFiltered(PeerData *peer, QString query, bool start) void FieldAutocomplete::showStickers(EmojiPtr emoji) { bool resetScroll = (_emoji != emoji); _emoji = emoji; + _type = Type::Stickers; if (!emoji) { rowsUpdated(_mrows, _hrows, _brows, StickerPack(), false); return; @@ -158,23 +176,41 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { } App::api()->requestStickerSets(); } - } else if (_filter.at(0) == '@') { - bool listAllSuggestions = (_filter.size() < 2); + } else if (_type == Type::Mentions) { + int maxListSize = _addInlineBots ? cRecentInlineBots().size() : 0; if (_chat) { - mrows.reserve((_addInlineBots ? cRecentInlineBots().size() : 0) + (_chat->participants.isEmpty() ? _chat->lastAuthors.size() : _chat->participants.size())); + maxListSize += (_chat->participants.isEmpty() ? _chat->lastAuthors.size() : _chat->participants.size()); } else if (_channel && _channel->isMegagroup()) { if (_channel->mgInfo->lastParticipants.isEmpty() || _channel->lastParticipantsCountOutdated()) { } else { - mrows.reserve((_addInlineBots ? cRecentInlineBots().size() : 0) + _channel->mgInfo->lastParticipants.size()); + maxListSize += _channel->mgInfo->lastParticipants.size(); } - } else if (_addInlineBots) { - mrows.reserve(cRecentInlineBots().size()); } + if (maxListSize) { + mrows.reserve(maxListSize); + } + + auto filterNotPassedByUsername = [this](UserData *user) -> bool { + if (user->username.startsWith(_filter, Qt::CaseInsensitive)) { + bool exactUsername = (user->username.size() == _filter.size()); + return exactUsername; + } + return true; + }; + auto filterNotPassedByName = [this](UserData *user) -> bool { + for_const (auto &namePart, user->names) { + if (namePart.startsWith(_filter, Qt::CaseInsensitive)) { + bool exactUsername = (user->username.compare(_filter, Qt::CaseInsensitive) == 0); + return exactUsername; + } + } + return true; + }; + + bool listAllSuggestions = _filter.isEmpty(); if (_addInlineBots) { - for (auto i = cRecentInlineBots().cbegin(), e = cRecentInlineBots().cend(); i != e; ++i) { - UserData *user = *i; - if (user->username.isEmpty()) continue; - if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + for_const (auto user, cRecentInlineBots()) { + if (!listAllSuggestions && filterNotPassedByUsername(user)) continue; mrows.push_back(user); ++recentInlineBots; } @@ -187,16 +223,13 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { } else if (!_chat->participants.isEmpty()) { for (auto i = _chat->participants.cbegin(), e = _chat->participants.cend(); i != e; ++i) { UserData *user = i.key(); - if (!listAllSuggestions && user->username.isEmpty()) continue; - if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + if (!listAllSuggestions && filterNotPassedByName(user)) continue; if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; ordered.insertMulti(App::onlineForSort(user, now), user); } } - for (auto i = _chat->lastAuthors.cbegin(), e = _chat->lastAuthors.cend(); i != e; ++i) { - UserData *user = *i; - if (!listAllSuggestions && user->username.isEmpty()) continue; - if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + for_const (auto user, _chat->lastAuthors) { + if (!listAllSuggestions && filterNotPassedByName(user)) continue; if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; mrows.push_back(user); if (!ordered.isEmpty()) { @@ -215,24 +248,24 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { if (App::api()) App::api()->requestLastParticipants(_channel); } else { mrows.reserve(mrows.size() + _channel->mgInfo->lastParticipants.size()); - for (auto i = _channel->mgInfo->lastParticipants.cbegin(), e = _channel->mgInfo->lastParticipants.cend(); i != e; ++i) { - UserData *user = *i; - if (!listAllSuggestions && user->username.isEmpty()) continue; - if (!listAllSuggestions && (!user->username.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || user->username.size() + 1 == _filter.size())) continue; + for_const (auto user, _channel->mgInfo->lastParticipants) { + if (!listAllSuggestions && filterNotPassedByName(user)) continue; if (indexOfInFirstN(mrows, user, recentInlineBots) >= 0) continue; mrows.push_back(user); } } } - } else if (_filter.at(0) == '#') { + } else if (_type == Type::Hashtags) { + bool listAllSuggestions = _filter.isEmpty(); auto &recent(cRecentWriteHashtags()); hrows.reserve(recent.size()); for (auto i = recent.cbegin(), e = recent.cend(); i != e; ++i) { - if (_filter.size() > 1 && (!i->first.startsWith(_filter.midRef(1), Qt::CaseInsensitive) || i->first.size() + 1 == _filter.size())) continue; + if (!listAllSuggestions && (!i->first.startsWith(_filter, Qt::CaseInsensitive) || i->first.size() == _filter.size())) continue; hrows.push_back(i->first); } - } else if (_filter.at(0) == '/') { - bool hasUsername = _filter.indexOf('@') > 1; + } else if (_type == Type::BotCommands) { + bool listAllSuggestions = _filter.isEmpty(); + bool hasUsername = _filter.indexOf('@') > 0; QMap bots; int32 cnt = 0; if (_chat) { @@ -277,9 +310,9 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { if (user->botInfo->commands.isEmpty()) continue; bots.remove(user); for (int32 j = 0, l = user->botInfo->commands.size(); j < l; ++j) { - if (_filter.size() > 1) { + if (!listAllSuggestions) { QString toFilter = (hasUsername || botStatus == 0 || botStatus == 2) ? user->botInfo->commands.at(j).command + '@' + user->username : user->botInfo->commands.at(j).command; - if (!toFilter.startsWith(_filter.midRef(1), Qt::CaseInsensitive)/* || toFilter.size() + 1 == _filter.size()*/) continue; + if (!toFilter.startsWith(_filter, Qt::CaseInsensitive)/* || toFilter.size() == _filter.size()*/) continue; } brows.push_back(qMakePair(user, &user->botInfo->commands.at(j))); } @@ -289,9 +322,9 @@ void FieldAutocomplete::updateFiltered(bool resetScroll) { for (QMap::const_iterator i = bots.cbegin(), e = bots.cend(); i != e; ++i) { UserData *user = i.key(); for (int32 j = 0, l = user->botInfo->commands.size(); j < l; ++j) { - if (_filter.size() > 1) { + if (!listAllSuggestions) { QString toFilter = (hasUsername || botStatus == 0 || botStatus == 2) ? user->botInfo->commands.at(j).command + '@' + user->username : user->botInfo->commands.at(j).command; - if (!toFilter.startsWith(_filter.midRef(1), Qt::CaseInsensitive)/* || toFilter.size() + 1 == _filter.size()*/) continue; + if (!toFilter.startsWith(_filter, Qt::CaseInsensitive)/* || toFilter.size() == _filter.size()*/) continue; } brows.push_back(qMakePair(user, &user->botInfo->commands.at(j))); } @@ -552,9 +585,10 @@ void FieldAutocompleteInner::paintEvent(QPaintEvent *e) { } else { int32 from = qFloor(e->rect().top() / st::mentionHeight), to = qFloor(e->rect().bottom() / st::mentionHeight) + 1; int32 last = _mrows->isEmpty() ? (_hrows->isEmpty() ? _brows->size() : _hrows->size()) : _mrows->size(); - bool hasUsername = _parent->filter().indexOf('@') > 1; - int filterSize = qMax(_parent->filter().size() - 1, 0); - bool filterIsEmpty = (filterSize == 0); + auto filter = _parent->filter(); + bool hasUsername = filter.indexOf('@') > 0; + int filterSize = filter.size(); + bool filterIsEmpty = filter.isEmpty(); for (int32 i = from; i < to; ++i) { if (i >= last) break; @@ -569,8 +603,8 @@ void FieldAutocompleteInner::paintEvent(QPaintEvent *e) { p.setPen(st::black->p); if (!_mrows->isEmpty()) { UserData *user = _mrows->at(i); - QString first = filterIsEmpty ? QString() : ('@' + user->username.mid(0, filterSize)); - QString second = (filterIsEmpty && !user->username.isEmpty()) ? ('@' + user->username) : user->username.mid(filterSize); + QString first = (!filterIsEmpty && user->username.startsWith(filter, Qt::CaseInsensitive)) ? ('@' + user->username.mid(0, filterSize)) : QString(); + QString second = first.isEmpty() ? (user->username.isEmpty() ? QString() : ('@' + user->username)) : user->username.mid(filterSize); int32 firstwidth = st::mentionFont->width(first), secondwidth = st::mentionFont->width(second), unamewidth = firstwidth + secondwidth, namewidth = user->nameText.maxWidth(); if (mentionwidth < unamewidth + namewidth) { namewidth = (mentionwidth * namewidth) / (namewidth + unamewidth); @@ -733,7 +767,7 @@ bool FieldAutocompleteInner::chooseSelected(FieldAutocomplete::ChooseMethod meth UserData *user = _brows->at(_sel).first; const BotCommand *command(_brows->at(_sel).second); int32 botStatus = _parent->chat() ? _parent->chat()->botStatus : ((_parent->channel() && _parent->channel()->isMegagroup()) ? _parent->channel()->mgInfo->botStatus : -1); - if (botStatus == 0 || botStatus == 2 || _parent->filter().indexOf('@') > 1) { + if (botStatus == 0 || botStatus == 2 || _parent->filter().indexOf('@') > 0) { emit botCommandChosen('/' + command->command + '@' + user->username, method); } else { emit botCommandChosen('/' + command->command, method); diff --git a/Telegram/SourceFiles/history/field_autocomplete.h b/Telegram/SourceFiles/history/field_autocomplete.h index 548dfdc32..b3ffaf58f 100644 --- a/Telegram/SourceFiles/history/field_autocomplete.h +++ b/Telegram/SourceFiles/history/field_autocomplete.h @@ -43,7 +43,7 @@ public: void fastHide(); bool clearFilteredBotCommands(); - void showFiltered(PeerData *peer, QString query, bool start); + void showFiltered(PeerData *peer, QString query, bool addInlineBots); void showStickers(EmojiPtr emoji); void setBoundings(QRect boundings); @@ -114,6 +114,13 @@ private: UserData *_user; ChannelData *_channel; EmojiPtr _emoji; + enum class Type { + Mentions, + Hashtags, + BotCommands, + Stickers, + }; + Type _type = Type::Mentions; QString _filter; QRect _boundings; bool _addInlineBots; diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 14f0c9be5..44d58b0ef 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -3041,10 +3041,16 @@ void HistoryWidget::updateInlineBotQuery() { return; } } else if (bot == LookingUpInlineBot) { - _inlineBot = LookingUpInlineBot; - return; + if (_inlineBot == LookingUpInlineBot) { + return; + } + bot = _inlineBot; } + applyInlineBotQuery(bot, query); +} + +void HistoryWidget::applyInlineBotQuery(UserData *bot, const QString &query) { if (bot) { if (_inlineBot != bot) { _inlineBot = bot; @@ -5651,12 +5657,32 @@ void HistoryWidget::inlineBotResolveDone(const MTPcontacts_ResolvedPeer &result) _inlineBotResolveRequestId = 0; // Notify::inlineBotRequesting(false); _inlineBotUsername = QString(); + UserData *resolvedBot = nullptr; if (result.type() == mtpc_contacts_resolvedPeer) { const auto &d(result.c_contacts_resolvedPeer()); - App::feedUsers(d.vusers); + if (resolvedBot = App::feedUsers(d.vusers)) { + if (!resolvedBot->botInfo || resolvedBot->botInfo->inlinePlaceholder.isEmpty()) { + resolvedBot = nullptr; + } + } App::feedChats(d.vchats); } - updateInlineBotQuery(); + + UserData *bot = nullptr; + QString inlineBotUsername; + QString query = _field.getInlineBotQuery(&bot, &inlineBotUsername); + if (inlineBotUsername == _inlineBotUsername) { + if (bot == LookingUpInlineBot) { + bot = resolvedBot; + } + } else { + bot = nullptr; + } + if (bot) { + applyInlineBotQuery(bot, query); + } else { + clearInlineBot(); + } } bool HistoryWidget::inlineBotResolveFail(QString name, const RPCError &error) { @@ -6097,7 +6123,8 @@ void HistoryWidget::onCheckFieldAutocomplete() { if (!_history || _a_show.animating()) return; bool start = false; - QString query = _inlineBot ? QString() : _field.getMentionHashtagBotCommandPart(start); + bool isInlineBot = _inlineBot && (_inlineBot != LookingUpInlineBot); + QString query = isInlineBot ? QString() : _field.getMentionHashtagBotCommandPart(start); if (!query.isEmpty()) { if (query.at(0) == '#' && cRecentWriteHashtags().isEmpty() && cRecentSearchHashtags().isEmpty()) Local::readRecentHashtagsAndBots(); if (query.at(0) == '@' && cRecentInlineBots().isEmpty()) Local::readRecentHashtagsAndBots(); diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h index 5ed237aa6..89ab558d7 100644 --- a/Telegram/SourceFiles/historywidget.h +++ b/Telegram/SourceFiles/historywidget.h @@ -548,7 +548,6 @@ public: void destroyData(); void updateFieldPlaceholder(); - void updateInlineBotQuery(); void updateStickersByEmoji(); void uploadImage(const QImage &img, PrepareMediaType type, FileLoadForceConfirmType confirm = FileLoadNoForceConfirm, const QString &source = QString(), bool withText = false); @@ -820,6 +819,12 @@ private: void clearInlineBot(); void inlineBotChanged(); + // Look in the _field for the inline bot and query string. + void updateInlineBotQuery(); + + // Request to show results in the emoji panel. + void applyInlineBotQuery(UserData *bot, const QString &query); + MsgId _replyToId = 0; Text _replyToName; int _replyToNameVersion = 0; diff --git a/Telegram/SourceFiles/ui/flattextarea.cpp b/Telegram/SourceFiles/ui/flattextarea.cpp index ccababd19..e777fe49c 100644 --- a/Telegram/SourceFiles/ui/flattextarea.cpp +++ b/Telegram/SourceFiles/ui/flattextarea.cpp @@ -465,12 +465,13 @@ void FlatTextarea::insertTag(const QString &text, QString tagId) { (i < 2 || !(fragmentText.at(i - 2).isLetterOrNumber() || fragmentText.at(i - 2) == '_'))) { cursor.setPosition(fragmentPosition + i - 1); int till = fragmentPosition + i; - for (; (till < fragmentEnd) && (till - fragmentPosition - i + 1 < text.size()); ++till) { - if (fragmentText.at(till - fragmentPosition).toLower() != text.at(till - fragmentPosition - i + 1).toLower()) { + for (; (till < fragmentEnd); ++till) { + auto ch = fragmentText.at(till - fragmentPosition); + if (!ch.isLetterOrNumber() && ch != '_') { break; } } - if (till - fragmentPosition - i + 1 == text.size() && till < fragmentEnd && fragmentText.at(till - fragmentPosition) == ' ') { + if (till < fragmentEnd && fragmentText.at(till - fragmentPosition) == ' ') { ++till; } cursor.setPosition(till, QTextCursor::KeepAnchor); From b28e9a6167a563fbf5bac90f0589b4dba222dd45 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 9 May 2016 13:07:25 +0300 Subject: [PATCH 09/20] Requesting difference or messages when users from mentions not loaded. --- Telegram/SourceFiles/apiwrap.cpp | 32 ++-- Telegram/SourceFiles/apiwrap.h | 2 +- Telegram/SourceFiles/history.cpp | 4 +- Telegram/SourceFiles/historywidget.cpp | 4 +- Telegram/SourceFiles/mainwidget.cpp | 204 ++++++++++++++----------- 5 files changed, 135 insertions(+), 111 deletions(-) diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index eb3210be1..d49c68cc4 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -37,17 +37,19 @@ ApiWrap::ApiWrap(QObject *parent) : QObject(parent) void ApiWrap::init() { } -void ApiWrap::requestMessageData(ChannelData *channel, MsgId msgId, RequestMessageDataCallback *callback) { - MessageDataRequest::CallbackPtr pcallback(callback); +void ApiWrap::requestMessageData(ChannelData *channel, MsgId msgId, std_::unique_ptr callback) { MessageDataRequest &req(channel ? _channelMessageDataRequests[channel][msgId] : _messageDataRequests[msgId]); - req.callbacks.append(pcallback); + if (callback) { + MessageDataRequest::CallbackPtr pcallback(callback.release()); + req.callbacks.append(pcallback); + } if (!req.req) _messageDataResolveDelayed->call(); } ApiWrap::MessageIds ApiWrap::collectMessageIds(const MessageDataRequests &requests) { MessageIds result; result.reserve(requests.size()); - for (MessageDataRequests::const_iterator i = requests.cbegin(), e = requests.cend(); i != e; ++i) { + for (auto i = requests.cbegin(), e = requests.cend(); i != e; ++i) { if (i.value().req > 0) continue; result.push_back(MTP_int(i.key())); } @@ -56,7 +58,7 @@ ApiWrap::MessageIds ApiWrap::collectMessageIds(const MessageDataRequests &reques ApiWrap::MessageDataRequests *ApiWrap::messageDataRequests(ChannelData *channel, bool onlyExisting) { if (channel) { - ChannelMessageDataRequests::iterator i = _channelMessageDataRequests.find(channel); + auto i = _channelMessageDataRequests.find(channel); if (i == _channelMessageDataRequests.cend()) { if (onlyExisting) return 0; i = _channelMessageDataRequests.insert(channel, MessageDataRequests()); @@ -72,12 +74,12 @@ void ApiWrap::resolveMessageDatas() { MessageIds ids = collectMessageIds(_messageDataRequests); if (!ids.isEmpty()) { mtpRequestId req = MTP::send(MTPmessages_GetMessages(MTP_vector(ids)), rpcDone(&ApiWrap::gotMessageDatas, (ChannelData*)nullptr), RPCFailHandlerPtr(), 0, 5); - for (MessageDataRequests::iterator i = _messageDataRequests.begin(); i != _messageDataRequests.cend(); ++i) { - if (i.value().req > 0) continue; - i.value().req = req; + for (auto &request : _messageDataRequests) { + if (request.req > 0) continue; + request.req = req; } } - for (ChannelMessageDataRequests::iterator j = _channelMessageDataRequests.begin(); j != _channelMessageDataRequests.cend();) { + for (auto j = _channelMessageDataRequests.begin(); j != _channelMessageDataRequests.cend();) { if (j->isEmpty()) { j = _channelMessageDataRequests.erase(j); continue; @@ -85,9 +87,9 @@ void ApiWrap::resolveMessageDatas() { MessageIds ids = collectMessageIds(j.value()); if (!ids.isEmpty()) { mtpRequestId req = MTP::send(MTPchannels_GetMessages(j.key()->inputChannel, MTP_vector(ids)), rpcDone(&ApiWrap::gotMessageDatas, j.key()), RPCFailHandlerPtr(), 0, 5); - for (MessageDataRequests::iterator i = j->begin(); i != j->cend(); ++i) { - if (i.value().req > 0) continue; - i.value().req = req; + for (auto &request : *j) { + if (request.req > 0) continue; + request.req = req; } } ++j; @@ -128,10 +130,10 @@ void ApiWrap::gotMessageDatas(ChannelData *channel, const MTPmessages_Messages & } MessageDataRequests *requests(messageDataRequests(channel, true)); if (requests) { - for (MessageDataRequests::iterator i = requests->begin(); i != requests->cend();) { + for (auto i = requests->begin(); i != requests->cend();) { if (i.value().req == req) { - for (MessageDataRequest::Callbacks::const_iterator j = i.value().callbacks.cbegin(), e = i.value().callbacks.cend(); j != e; ++j) { - (*j)->call(channel, i.key()); + for_const (auto &callback, i.value().callbacks) { + callback->call(channel, i.key()); } i = requests->erase(i); } else { diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 8845d90bd..606ef7f14 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -29,7 +29,7 @@ public: void init(); typedef SharedCallback RequestMessageDataCallback; - void requestMessageData(ChannelData *channel, MsgId msgId, RequestMessageDataCallback *callback); + void requestMessageData(ChannelData *channel, MsgId msgId, std_::unique_ptr callback); void requestFullPeer(PeerData *peer); void requestPeer(PeerData *peer); diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index 60d53120a..4c130f4a8 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -6843,7 +6843,7 @@ void HistoryMessage::createComponents(const CreateConfig &config) { if (auto reply = Get()) { reply->replyToMsgId = config.replyTo; if (!reply->updateData(this) && App::api()) { - App::api()->requestMessageData(history()->peer->asChannel(), reply->replyToMsgId, new HistoryDependentItemCallback(fullId())); + App::api()->requestMessageData(history()->peer->asChannel(), reply->replyToMsgId, std_::make_unique(fullId())); } } if (auto via = Get()) { @@ -8192,7 +8192,7 @@ HistoryService::HistoryService(History *history, const MTPDmessageService &msg) UpdateComponents(HistoryServicePinned::Bit()); MsgId pinnedMsgId = Get()->msgId = msg.vreply_to_msg_id.v; if (!updatePinned() && App::api()) { - App::api()->requestMessageData(history->peer->asChannel(), pinnedMsgId, new HistoryDependentItemCallback(fullId())); + App::api()->requestMessageData(history->peer->asChannel(), pinnedMsgId, std_::make_unique(fullId())); } } setMessageByAction(msg.vaction); diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 44d58b0ef..1242148f5 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -3703,7 +3703,7 @@ void HistoryWidget::applyDraft(bool parseLinks) { if (_editMsgId || _replyToId) { updateReplyEditTexts(); if (!_replyEditMsg && App::api()) { - App::api()->requestMessageData(_peer->asChannel(), _editMsgId ? _editMsgId : _replyToId, new ReplyEditMessageDataCallback()); + App::api()->requestMessageData(_peer->asChannel(), _editMsgId ? _editMsgId : _replyToId, std_::make_unique()); } } } @@ -7249,7 +7249,7 @@ bool HistoryWidget::pinnedMsgVisibilityUpdated() { update(); } if (!_pinnedBar->msg && App::api()) { - App::api()->requestMessageData(_peer->asChannel(), _pinnedBar->msgId, new ReplyEditMessageDataCallback()); + App::api()->requestMessageData(_peer->asChannel(), _pinnedBar->msgId, std_::make_unique()); } } else if (_pinnedBar) { destroyPinnedBar(); diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 53cf80b79..22e24d2d9 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -3826,6 +3826,107 @@ void MainWidget::updateReceived(const mtpPrime *from, const mtpPrime *end) { update(); } +namespace { + +bool fwdInfoDataLoaded(const MTPMessageFwdHeader &header) { + if (header.type() != mtpc_messageFwdHeader) { + return true; + } + auto &info = header.c_messageFwdHeader(); + if (info.has_channel_id()) { + if (!App::channelLoaded(peerFromChannel(info.vchannel_id))) { + return false; + } + if (info.has_from_id() && !App::user(peerFromUser(info.vfrom_id), PeerData::MinimalLoaded)) { + return false; + } + } else { + if (info.has_from_id() && !App::userLoaded(peerFromUser(info.vfrom_id))) { + return false; + } + } + return true; +} + +bool mentionUsersLoaded(const MTPVector &entities) { + for_const (auto &entity, entities.c_vector().v) { + auto type = entity.type(); + if (type == mtpc_messageEntityMentionName) { + if (!App::userLoaded(peerFromUser(entity.c_messageEntityMentionName().vuser_id))) { + return false; + } + } else if (type == mtpc_inputMessageEntityMentionName) { + auto &inputUser = entity.c_inputMessageEntityMentionName().vuser_id; + if (inputUser.type() == mtpc_inputUser) { + if (!App::userLoaded(peerFromUser(inputUser.c_inputUser().vuser_id))) { + return false; + } + } + } + } + return true; +} + +enum class DataIsLoadedResult { + NotLoaded = 0, + FromNotLoaded = 1, + MentionNotLoaded = 2, + Ok = 3, +}; +DataIsLoadedResult allDataLoadedForMessage(const MTPMessage &msg) { + switch (msg.type()) { + case mtpc_message: { + const MTPDmessage &d(msg.c_message()); + if (!d.is_post() && d.has_from_id()) { + if (!App::userLoaded(peerFromUser(d.vfrom_id))) { + return DataIsLoadedResult::FromNotLoaded; + } + } + if (d.has_via_bot_id()) { + if (!App::userLoaded(peerFromUser(d.vvia_bot_id))) { + return DataIsLoadedResult::NotLoaded; + } + } + if (d.has_fwd_from() && !fwdInfoDataLoaded(d.vfwd_from)) { + return DataIsLoadedResult::NotLoaded; + } + if (d.has_entities() && !mentionUsersLoaded(d.ventities)) { + return DataIsLoadedResult::MentionNotLoaded; + } + } break; + case mtpc_messageService: { + const MTPDmessageService &d(msg.c_messageService()); + if (!d.is_post() && d.has_from_id()) { + if (!App::userLoaded(peerFromUser(d.vfrom_id))) { + return DataIsLoadedResult::FromNotLoaded; + } + } + switch (d.vaction.type()) { + case mtpc_messageActionChatAddUser: { + for_const(const MTPint &userId, d.vaction.c_messageActionChatAddUser().vusers.c_vector().v) { + if (!App::userLoaded(peerFromUser(userId))) { + return DataIsLoadedResult::NotLoaded; + } + } + } break; + case mtpc_messageActionChatJoinedByLink: { + if (!App::userLoaded(peerFromUser(d.vaction.c_messageActionChatJoinedByLink().vinviter_id))) { + return DataIsLoadedResult::NotLoaded; + } + } break; + case mtpc_messageActionChatDeleteUser: { + if (!App::userLoaded(peerFromUser(d.vaction.c_messageActionChatDeleteUser().vuser_id))) { + return DataIsLoadedResult::NotLoaded; + } + } break; + } + } break; + } + return DataIsLoadedResult::Ok; +} + +} // namespace + void MainWidget::feedUpdates(const MTPUpdates &updates, uint64 randomId) { switch (updates.type()) { case mtpc_updates: { @@ -3872,21 +3973,13 @@ void MainWidget::feedUpdates(const MTPUpdates &updates, uint64 randomId) { case mtpc_updateShortMessage: { const auto &d(updates.c_updateShortMessage()); - if (!App::userLoaded(d.vuser_id.v) || (d.has_via_bot_id() && !App::userLoaded(d.vvia_bot_id.v))) { + if (!App::userLoaded(d.vuser_id.v) + || (d.has_via_bot_id() && !App::userLoaded(d.vvia_bot_id.v)) + || (d.has_entities() && !mentionUsersLoaded(d.ventities)) + || (d.has_fwd_from() && !fwdInfoDataLoaded(d.vfwd_from))) { MTP_LOG(0, ("getDifference { good - getting user for updateShortMessage }%1").arg(cTestMode() ? " TESTMODE" : "")); return getDifference(); } - if (d.has_fwd_from() && d.vfwd_from.type() == mtpc_messageFwdHeader) { - const auto &f(d.vfwd_from.c_messageFwdHeader()); - if (f.has_from_id() && !App::userLoaded(f.vfrom_id.v)) { - MTP_LOG(0, ("getDifference { good - getting user for updateShortMessage }%1").arg(cTestMode() ? " TESTMODE" : "")); - return getDifference(); - } - if (f.has_channel_id() && !App::channelLoaded(f.vchannel_id.v)) { - MTP_LOG(0, ("getDifference { good - getting user for updateShortMessage }%1").arg(cTestMode() ? " TESTMODE" : "")); - return getDifference(); - } - } if (!ptsUpdated(d.vpts.v, d.vpts_count.v, updates)) { return; } @@ -3906,22 +3999,15 @@ void MainWidget::feedUpdates(const MTPUpdates &updates, uint64 randomId) { case mtpc_updateShortChatMessage: { const auto &d(updates.c_updateShortChatMessage()); bool noFrom = !App::userLoaded(d.vfrom_id.v); - if (!App::chatLoaded(d.vchat_id.v) || noFrom || (d.has_via_bot_id() && !App::userLoaded(d.vvia_bot_id.v))) { + if (!App::chatLoaded(d.vchat_id.v) + || noFrom + || (d.has_via_bot_id() && !App::userLoaded(d.vvia_bot_id.v)) + || (d.has_entities() && !mentionUsersLoaded(d.ventities)) + || (d.has_fwd_from() && !fwdInfoDataLoaded(d.vfwd_from))) { MTP_LOG(0, ("getDifference { good - getting user for updateShortChatMessage }%1").arg(cTestMode() ? " TESTMODE" : "")); if (noFrom && App::api()) App::api()->requestFullPeer(App::chatLoaded(d.vchat_id.v)); return getDifference(); } - if (d.has_fwd_from() && d.vfwd_from.type() == mtpc_messageFwdHeader) { - const auto &f(d.vfwd_from.c_messageFwdHeader()); - if (f.has_from_id() && !App::userLoaded(f.vfrom_id.v)) { - MTP_LOG(0, ("getDifference { good - getting user for updateShortChatMessage }%1").arg(cTestMode() ? " TESTMODE" : "")); - return getDifference(); - } - if (f.has_channel_id() && !App::channelLoaded(f.vchannel_id.v)) { - MTP_LOG(0, ("getDifference { good - getting user for updateShortChatMessage }%1").arg(cTestMode() ? " TESTMODE" : "")); - return getDifference(); - } - } if (!ptsUpdated(d.vpts.v, d.vpts_count.v, updates)) { return; } @@ -3948,6 +4034,9 @@ void MainWidget::feedUpdates(const MTPUpdates &updates, uint64 randomId) { feedUpdate(MTP_updateMessageID(d.vid, MTP_long(randomId))); // ignore real date if (peerId) { if (auto item = App::histItemById(peerToChannel(peerId), d.vid.v)) { + if (d.has_entities() && !mentionUsersLoaded(d.ventities)) { + api()->requestMessageData(item->history()->peer->asChannel(), item->id, nullptr); + } auto entities = d.has_entities() ? entitiesFromMTP(d.ventities.c_vector().v) : EntitiesInText(); item->setText({ text, entities }); item->updateMedia(d.has_media() ? (&d.vmedia) : nullptr); @@ -3972,73 +4061,6 @@ void MainWidget::feedUpdates(const MTPUpdates &updates, uint64 randomId) { } } -namespace { - -enum class DataIsLoadedResult { - NotLoaded = 0, - FromNotLoaded = 1, - Ok = 2, -}; -DataIsLoadedResult allDataLoadedForMessage(const MTPMessage &msg) { - switch (msg.type()) { - case mtpc_message: { - const MTPDmessage &d(msg.c_message()); - if (!d.is_post() && d.has_from_id()) { - if (!App::userLoaded(peerFromUser(d.vfrom_id))) { - return DataIsLoadedResult::FromNotLoaded; - } - } - if (d.has_via_bot_id()) { - if (!App::userLoaded(peerFromUser(d.vvia_bot_id))) { - return DataIsLoadedResult::NotLoaded; - } - } - if (d.has_fwd_from() && d.vfwd_from.type() == mtpc_messageFwdHeader) { - ChannelId fromChannelId = d.vfwd_from.c_messageFwdHeader().vchannel_id.v; - if (fromChannelId) { - if (!App::channelLoaded(peerFromChannel(fromChannelId))) { - return DataIsLoadedResult::NotLoaded; - } - } else { - if (!App::userLoaded(peerFromUser(d.vfwd_from.c_messageFwdHeader().vfrom_id))) { - return DataIsLoadedResult::NotLoaded; - } - } - } - } break; - case mtpc_messageService: { - const MTPDmessageService &d(msg.c_messageService()); - if (!d.is_post() && d.has_from_id()) { - if (!App::userLoaded(peerFromUser(d.vfrom_id))) { - return DataIsLoadedResult::FromNotLoaded; - } - } - switch (d.vaction.type()) { - case mtpc_messageActionChatAddUser: { - for_const(const MTPint &userId, d.vaction.c_messageActionChatAddUser().vusers.c_vector().v) { - if (!App::userLoaded(peerFromUser(userId))) { - return DataIsLoadedResult::NotLoaded; - } - } - } break; - case mtpc_messageActionChatJoinedByLink: { - if (!App::userLoaded(peerFromUser(d.vaction.c_messageActionChatJoinedByLink().vinviter_id))) { - return DataIsLoadedResult::NotLoaded; - } - } break; - case mtpc_messageActionChatDeleteUser: { - if (!App::userLoaded(peerFromUser(d.vaction.c_messageActionChatDeleteUser().vuser_id))) { - return DataIsLoadedResult::NotLoaded; - } - } break; - } - } break; - } - return DataIsLoadedResult::Ok; -} - -} // namespace - void MainWidget::feedUpdate(const MTPUpdate &update) { if (!MTP::authedId()) return; From 0b2401132eb518659891bd5641ec77491bcbf037 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 9 May 2016 15:03:06 +0300 Subject: [PATCH 10/20] Message edit warning timer (up to 15 minutes). Displaying "edited" info in messages. --- Telegram/Resources/langs/lang.strings | 2 +- Telegram/SourceFiles/dialogswidget.cpp | 4 +- Telegram/SourceFiles/history.cpp | 6 +- Telegram/SourceFiles/historywidget.cpp | 76 ++++++++++++++++++++++---- Telegram/SourceFiles/historywidget.h | 7 ++- 5 files changed, 75 insertions(+), 20 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index f71a41df2..3b54f501d 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -599,7 +599,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_forwarded_channel_via" = "Forwarded from {channel} via {inline_bot}"; "lng_forwarded_signed" = "{channel} ({user})"; "lng_in_reply_to" = "In reply to"; -"lng_edited" = "Edited"; +"lng_edited" = "edited"; "lng_edited_date" = "Edited: {date}"; "lng_cancel_edit_post_sure" = "Cancel editing?"; "lng_cancel_edit_post_yes" = "Yes"; diff --git a/Telegram/SourceFiles/dialogswidget.cpp b/Telegram/SourceFiles/dialogswidget.cpp index b2de4040d..b7f48c84e 100644 --- a/Telegram/SourceFiles/dialogswidget.cpp +++ b/Telegram/SourceFiles/dialogswidget.cpp @@ -1395,8 +1395,8 @@ void DialogsInner::selectSkipPage(int32 pixels, int32 direction) { _sel = *i; } } else { - for (auto i = shownDialogs()->cfind(_sel), b = shownDialogs()->cbegin(); i != b && (toSkip--); --i) { - _sel = *i; + for (auto i = shownDialogs()->cfind(_sel), b = shownDialogs()->cbegin(); i != b && (toSkip--);) { + _sel = *(--i); } if (toSkip && importantDialogs) { _importantSwitchSel = true; diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index 4c130f4a8..2398db550 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -3095,8 +3095,7 @@ void HistoryItem::setId(MsgId newId) { } bool HistoryItem::canEdit(const QDateTime &cur) const { - auto channel = _history->peer->asChannel(); - if (!channel || id < 0 || date.secsTo(cur) >= Global::EditTimeLimit()) return false; + if (id < 0 || date.secsTo(cur) >= Global::EditTimeLimit()) return false; if (auto msg = toHistoryMessage()) { if (msg->Has() || msg->Has()) return false; @@ -3114,6 +3113,7 @@ bool HistoryItem::canEdit(const QDateTime &cur) const { } } if (isPost()) { + auto channel = _history->peer->asChannel(); return (channel->amCreator() || (channel->amEditor() && out())); } return out(); @@ -6484,7 +6484,7 @@ void HistoryMessageEdited::create(const QDateTime &editDate, const QDateTime &da _editDate = editDate; QString time = date.toString(cTimeFormat()); - _edited.setText(st::msgDateFont, time, _textNameOptions); + _edited.setText(st::msgDateFont, lang(lng_edited) + ' ' + time, _textNameOptions); } int HistoryMessageEdited::maxWidth() const { diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 1242148f5..1781e4ced 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -2070,9 +2070,9 @@ QString HistoryInner::tooltipText() const { if (_dragCursorState == HistoryInDateCursorState && _dragAction == NoDrag) { if (App::hoveredItem()) { QString dateText = App::hoveredItem()->date.toString(QLocale::system().dateTimeFormat(QLocale::LongFormat)); - //if (auto edited = App::hoveredItem()->Get()) { - // dateText += '\n' + lng_edited_date(lt_date, edited->_editDate.toString(QLocale::system().dateTimeFormat(QLocale::LongFormat))); - //} + if (auto edited = App::hoveredItem()->Get()) { + dateText += '\n' + lng_edited_date(lt_date, edited->_editDate.toString(QLocale::system().dateTimeFormat(QLocale::LongFormat))); + } return dateText; } } else if (_dragCursorState == HistoryInForwardedCursorState && _dragAction == NoDrag) { @@ -2985,6 +2985,8 @@ HistoryWidget::HistoryWidget(QWidget *parent) : TWidget(parent) connect(&_attachDragDocument, SIGNAL(dropped(const QMimeData*)), this, SLOT(onDocumentDrop(const QMimeData*))); connect(&_attachDragPhoto, SIGNAL(dropped(const QMimeData*)), this, SLOT(onPhotoDrop(const QMimeData*))); + + connect(&_updateEditTimeLeftDisplay, SIGNAL(timeout()), this, SLOT(updateField())); } void HistoryWidget::start() { @@ -7077,9 +7079,10 @@ void HistoryWidget::keyPressEvent(QKeyEvent *e) { if (_field.isEmpty() && !_editMsgId && !_replyToId) { App::contextItem(_history->lastSentMsg); onEditMessage(); + return; } } -// _scroll.keyPressEvent(e); + _scroll.keyPressEvent(e); } } else { e->ignore(); @@ -7474,8 +7477,8 @@ void HistoryWidget::onEditMessage() { _history->setEditDraft(std_::make_unique(editData, to->id, cursor, false)); applyDraft(false); - _previewData = 0; - if (HistoryMedia *media = to->getMedia()) { + _previewData = nullptr; + if (auto media = to->getMedia()) { if (media->type() == MediaTypeWebPage) { _previewData = static_cast(media)->webpage(); updatePreview(); @@ -7491,7 +7494,7 @@ void HistoryWidget::onEditMessage() { updateFieldPlaceholder(); updateMouseTracking(); updateReplyToName(); - resizeEvent(0); + resizeEvent(nullptr); updateField(); _saveDraftText = true; @@ -8089,7 +8092,7 @@ void HistoryWidget::updateField() { update(0, fy, width(), height() - fy); } -void HistoryWidget::drawField(Painter &p) { +void HistoryWidget::drawField(Painter &p, const QRect &rect) { int32 backy = _field.y() - st::sendPadding, backh = _field.height() + 2 * st::sendPadding; Text *from = 0, *text = 0; bool serviceColor = false, hasForward = readyToForward(); @@ -8110,7 +8113,7 @@ void HistoryWidget::drawField(Painter &p) { backh += st::replyHeight; } bool drawPreview = (_previewData && _previewData->pendingTill >= 0) && !_replyForwardPressed; - p.fillRect(0, backy, width(), backh, st::taMsgField.bgColor->b); + p.fillRect(0, backy, width(), backh, st::taMsgField.bgColor); if (_editMsgId || _replyToId || (!hasForward && _kbReplyTo)) { int32 replyLeft = st::replySkip; p.drawSprite(QPoint(st::replyIconPos.x(), backy + st::replyIconPos.y()), _editMsgId ? st::editIcon : st::replyIcon); @@ -8126,8 +8129,7 @@ void HistoryWidget::drawField(Painter &p) { } p.setPen(st::replyColor); if (_editMsgId) { - p.setFont(st::msgServiceNameFont); - p.drawText(replyLeft, backy + st::msgReplyPadding.top() + st::msgServiceNameFont->ascent, lang(lng_edit_message)); + paintEditHeader(p, rect, replyLeft, backy); } else { _replyToName.drawElided(p, replyLeft, backy + st::msgReplyPadding.top(), width() - replyLeft - _fieldBarCancel.width() - st::msgReplyPadding.right()); } @@ -8182,6 +8184,56 @@ void HistoryWidget::drawField(Painter &p) { } } +namespace { + +constexpr int DisplayEditTimeWarningMs = 900 * 1000; +constexpr int FullDayInMs = 86400 * 1000; + +} // namespace + +void HistoryWidget::paintEditHeader(Painter &p, const QRect &rect, int left, int top) const { + if (!rect.intersects(QRect(left, top, width() - left, st::normalFont->height))) { + return; + } + + p.setFont(st::msgServiceNameFont); + p.drawText(left, top + st::msgReplyPadding.top() + st::msgServiceNameFont->ascent, lang(lng_edit_message)); + + if (!_replyEditMsg) return; + + QString editTimeLeftText; + int updateIn = -1; + auto tmp = ::date(unixtime()); + auto timeSinceMessage = _replyEditMsg->date.msecsTo(QDateTime::currentDateTime()); + auto editTimeLeft = (Global::EditTimeLimit() * 1000LL) - timeSinceMessage; + if (editTimeLeft < 2) { + editTimeLeftText = qsl("0:00"); + } else if (editTimeLeft > DisplayEditTimeWarningMs) { + updateIn = static_cast(qMin(editTimeLeft - DisplayEditTimeWarningMs, qint64(FullDayInMs))); + } else { + updateIn = (editTimeLeft % 1000); + if (!updateIn) { + updateIn = 1000; + } + ++updateIn; + + editTimeLeft = (editTimeLeft - 1) / 1000; // seconds + editTimeLeftText = qsl("%1:%2").arg(editTimeLeft / 60).arg(editTimeLeft % 60, 2, 10, QChar('0')); + } + + // Restart timer only if we are sure that we've painted the whole timer. + if (rect.contains(QRect(left, top, width() - left, st::normalFont->height)) && updateIn > 0) { + _updateEditTimeLeftDisplay.start(updateIn); + LOG(("Time since message: %1ms, update in %2ms: %3").arg(timeSinceMessage).arg(updateIn).arg(editTimeLeftText)); + } + + if (!editTimeLeftText.isEmpty()) { + p.setFont(st::normalFont); + p.setPen(st::msgInDateFg); + p.drawText(left + st::msgServiceNameFont->width(lang(lng_edit_message)) + st::normalFont->spacew, top + st::msgReplyPadding.top() + st::msgServiceNameFont->ascent, editTimeLeftText); + } +} + void HistoryWidget::drawRecordButton(Painter &p) { if (a_recordDown.current() < 1) { p.setOpacity(st::btnAttachEmoji.opacity * (1 - a_recordOver.current()) + st::btnAttachEmoji.overOpacity * a_recordOver.current()); @@ -8308,7 +8360,7 @@ void HistoryWidget::paintEvent(QPaintEvent *e) { if (_list) { if (!_field.isHidden() || _recording) { - drawField(p); + drawField(p, r); if (_send.isHidden()) { drawRecordButton(p); if (_recording) drawRecording(p); diff --git a/Telegram/SourceFiles/historywidget.h b/Telegram/SourceFiles/historywidget.h index 89ab558d7..270145286 100644 --- a/Telegram/SourceFiles/historywidget.h +++ b/Telegram/SourceFiles/historywidget.h @@ -791,7 +791,6 @@ public slots: void onDraftSave(bool delayed = false); void updateStickers(); - void updateField(); void onRecordError(); void onRecordDone(QByteArray result, VoiceWaveform waveform, qint32 samples); @@ -809,6 +808,8 @@ private slots: void onMentionInsert(UserData *user); void onInlineBotCancel(); + void updateField(); + private: // Updates position of controls around the message field, @@ -834,6 +835,7 @@ private: HistoryItem *_replyEditMsg = nullptr; Text _replyEditMsgText; + mutable SingleTimer _updateEditTimeLeftDisplay; IconedButton _fieldBarCancel; void updateReplyEditTexts(bool force = false); @@ -861,7 +863,8 @@ private: void sendExistingDocument(DocumentData *doc, const QString &caption); void sendExistingPhoto(PhotoData *photo, const QString &caption); - void drawField(Painter &p); + void drawField(Painter &p, const QRect &rect); + void paintEditHeader(Painter &p, const QRect &rect, int left, int top) const; void drawRecordButton(Painter &p); void drawRecording(Painter &p); void drawPinnedBar(Painter &p); From 4b83c34f0deee6afcfcaf47743bd8bb400f5bc16 Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 9 May 2016 17:42:10 +0300 Subject: [PATCH 11/20] Setting f_views flag for a client-side forwarded message with views. --- Telegram/SourceFiles/history.cpp | 27 ++++++++++++++++++++++++++ Telegram/SourceFiles/history.h | 19 ------------------ Telegram/SourceFiles/historywidget.cpp | 2 +- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index 2398db550..aedd4dd20 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -6752,6 +6752,33 @@ HistoryMessage::HistoryMessage(History *history, const MTPDmessage &msg) setText(textWithEntities); } +namespace { + +MTPDmessage::Flags newForwardedFlags(PeerData *p, int32 from, HistoryMessage *fwd) { + MTPDmessage::Flags result = newMessageFlags(p) | MTPDmessage::Flag::f_fwd_from; + if (from) { + result |= MTPDmessage::Flag::f_from_id; + } + if (fwd->Has()) { + result |= MTPDmessage::Flag::f_via_bot_id; + } + if (!p->isChannel()) { + if (HistoryMedia *media = fwd->getMedia()) { + if (media->type() == MediaTypeVoiceFile) { + result |= MTPDmessage::Flag::f_media_unread; +// } else if (media->type() == MediaTypeVideo) { +// result |= MTPDmessage::flag_media_unread; + } + } + } + if (fwd->hasViews()) { + result |= MTPDmessage::Flag::f_views; + } + return result; +} + +} // namespace + HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *fwd) : HistoryItem(history, id, newForwardedFlags(history->peer, from, fwd) | flags, date, from) { CreateConfig config; diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h index 7a8d8c7cc..50f1fa41b 100644 --- a/Telegram/SourceFiles/history.h +++ b/Telegram/SourceFiles/history.h @@ -2818,25 +2818,6 @@ inline MTPDmessage::Flags newMessageFlags(PeerData *p) { } return result; } -inline MTPDmessage::Flags newForwardedFlags(PeerData *p, int32 from, HistoryMessage *fwd) { - MTPDmessage::Flags result = newMessageFlags(p); - if (from) { - result |= MTPDmessage::Flag::f_from_id; - } - if (fwd->Has()) { - result |= MTPDmessage::Flag::f_via_bot_id; - } - if (!p->isChannel()) { - if (HistoryMedia *media = fwd->getMedia()) { - if (media->type() == MediaTypeVoiceFile) { - result |= MTPDmessage::Flag::f_media_unread; -// } else if (media->type() == MediaTypeVideo) { -// result |= MTPDmessage::flag_media_unread; - } - } - } - return result; -} struct HistoryServicePinned : public BaseComponent { MsgId msgId = 0; diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 1781e4ced..953bc465f 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -8211,7 +8211,7 @@ void HistoryWidget::paintEditHeader(Painter &p, const QRect &rect, int left, int } else if (editTimeLeft > DisplayEditTimeWarningMs) { updateIn = static_cast(qMin(editTimeLeft - DisplayEditTimeWarningMs, qint64(FullDayInMs))); } else { - updateIn = (editTimeLeft % 1000); + updateIn = static_cast(editTimeLeft % 1000); if (!updateIn) { updateIn = 1000; } From f02fc4dd96f2be2a046346f5f906fff63a0fc43f Mon Sep 17 00:00:00 2001 From: John Preston Date: Mon, 9 May 2016 17:42:10 +0300 Subject: [PATCH 12/20] Setting f_views flag for a client-side forwarded message with views. --- Telegram/SourceFiles/history.cpp | 27 ++++++++++++++++++++++++++ Telegram/SourceFiles/history.h | 19 ------------------ Telegram/SourceFiles/historywidget.cpp | 3 +-- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index 2398db550..aedd4dd20 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -6752,6 +6752,33 @@ HistoryMessage::HistoryMessage(History *history, const MTPDmessage &msg) setText(textWithEntities); } +namespace { + +MTPDmessage::Flags newForwardedFlags(PeerData *p, int32 from, HistoryMessage *fwd) { + MTPDmessage::Flags result = newMessageFlags(p) | MTPDmessage::Flag::f_fwd_from; + if (from) { + result |= MTPDmessage::Flag::f_from_id; + } + if (fwd->Has()) { + result |= MTPDmessage::Flag::f_via_bot_id; + } + if (!p->isChannel()) { + if (HistoryMedia *media = fwd->getMedia()) { + if (media->type() == MediaTypeVoiceFile) { + result |= MTPDmessage::Flag::f_media_unread; +// } else if (media->type() == MediaTypeVideo) { +// result |= MTPDmessage::flag_media_unread; + } + } + } + if (fwd->hasViews()) { + result |= MTPDmessage::Flag::f_views; + } + return result; +} + +} // namespace + HistoryMessage::HistoryMessage(History *history, MsgId id, MTPDmessage::Flags flags, QDateTime date, int32 from, HistoryMessage *fwd) : HistoryItem(history, id, newForwardedFlags(history->peer, from, fwd) | flags, date, from) { CreateConfig config; diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h index 7a8d8c7cc..50f1fa41b 100644 --- a/Telegram/SourceFiles/history.h +++ b/Telegram/SourceFiles/history.h @@ -2818,25 +2818,6 @@ inline MTPDmessage::Flags newMessageFlags(PeerData *p) { } return result; } -inline MTPDmessage::Flags newForwardedFlags(PeerData *p, int32 from, HistoryMessage *fwd) { - MTPDmessage::Flags result = newMessageFlags(p); - if (from) { - result |= MTPDmessage::Flag::f_from_id; - } - if (fwd->Has()) { - result |= MTPDmessage::Flag::f_via_bot_id; - } - if (!p->isChannel()) { - if (HistoryMedia *media = fwd->getMedia()) { - if (media->type() == MediaTypeVoiceFile) { - result |= MTPDmessage::Flag::f_media_unread; -// } else if (media->type() == MediaTypeVideo) { -// result |= MTPDmessage::flag_media_unread; - } - } - } - return result; -} struct HistoryServicePinned : public BaseComponent { MsgId msgId = 0; diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 1781e4ced..2e0b015f8 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -8211,7 +8211,7 @@ void HistoryWidget::paintEditHeader(Painter &p, const QRect &rect, int left, int } else if (editTimeLeft > DisplayEditTimeWarningMs) { updateIn = static_cast(qMin(editTimeLeft - DisplayEditTimeWarningMs, qint64(FullDayInMs))); } else { - updateIn = (editTimeLeft % 1000); + updateIn = static_cast(editTimeLeft % 1000); if (!updateIn) { updateIn = 1000; } @@ -8224,7 +8224,6 @@ void HistoryWidget::paintEditHeader(Painter &p, const QRect &rect, int left, int // Restart timer only if we are sure that we've painted the whole timer. if (rect.contains(QRect(left, top, width() - left, st::normalFont->height)) && updateIn > 0) { _updateEditTimeLeftDisplay.start(updateIn); - LOG(("Time since message: %1ms, update in %2ms: %3").arg(timeSinceMessage).arg(updateIn).arg(editTimeLeftText)); } if (!editTimeLeftText.isEmpty()) { From d26a8cbdcc0c940b3dd2bed78dc37e50cf756d4a Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 May 2016 14:42:03 +0300 Subject: [PATCH 13/20] Mentioning a user without username by first name only. Beta 9048001. --- Telegram/Resources/winrc/Telegram.rc | 8 ++++---- Telegram/Resources/winrc/Updater.rc | 8 ++++---- Telegram/SourceFiles/core/version.h | 2 +- Telegram/SourceFiles/historywidget.cpp | 5 ++++- Telegram/build/version | 2 +- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index 6df5730f8..ee19b5322 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -34,8 +34,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 0,9,48,0 - PRODUCTVERSION 0,9,48,0 + FILEVERSION 0,9,48,1 + PRODUCTVERSION 0,9,48,1 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -51,10 +51,10 @@ BEGIN BLOCK "040904b0" BEGIN VALUE "CompanyName", "Telegram Messenger LLP" - VALUE "FileVersion", "0.9.48.0" + VALUE "FileVersion", "0.9.48.1" VALUE "LegalCopyright", "Copyright (C) 2014-2016" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "0.9.48.0" + VALUE "ProductVersion", "0.9.48.1" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 5a58c3a01..6f897a376 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -25,8 +25,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 0,9,48,0 - PRODUCTVERSION 0,9,48,0 + FILEVERSION 0,9,48,1 + PRODUCTVERSION 0,9,48,1 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.48.0" + VALUE "FileVersion", "0.9.48.1" VALUE "LegalCopyright", "Copyright (C) 2014-2016" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "0.9.48.0" + VALUE "ProductVersion", "0.9.48.1" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index f220a893c..31c790162 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,7 +22,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "core/basic_types.h" -#define BETA_VERSION_MACRO (0ULL) +#define BETA_VERSION_MACRO (9048001ULL) constexpr int AppVersion = 9048; constexpr str_const AppVersionStr = "0.9.48"; diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 2e0b015f8..e2ce96b1b 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -3007,7 +3007,10 @@ void HistoryWidget::onStickersUpdated() { void HistoryWidget::onMentionInsert(UserData *user) { QString replacement, entityTag; if (user->username.isEmpty()) { - replacement = App::peerName(user); + replacement = user->firstName; + if (replacement.isEmpty()) { + replacement = App::peerName(user); + } entityTag = qsl("mention://user.") + QString::number(user->bareId()) + '.' + QString::number(user->access); } else { replacement = '@' + user->username; diff --git a/Telegram/build/version b/Telegram/build/version index 0c657fd1c..ecb2c68fb 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -3,4 +3,4 @@ AppVersionStrMajor 0.9 AppVersionStrSmall 0.9.48 AppVersionStr 0.9.48 AlphaChannel 0 -BetaVersion 0 +BetaVersion 9048001 From 6188aea7ed5709a5a795d56a7e15221179481fd3 Mon Sep 17 00:00:00 2001 From: John Preston Date: Tue, 10 May 2016 16:39:42 +0300 Subject: [PATCH 14/20] Beta version 9048001 for Xcode and QtCreator. --- Telegram/SourceFiles/core/basic_types.h | 6 ++-- .../SourceFiles/history/field_autocomplete.h | 2 +- Telegram/SourceFiles/historywidget.cpp | 3 +- Telegram/SourceFiles/pspecific_mac.cpp | 2 +- Telegram/SourceFiles/ui/flattextarea.cpp | 4 +-- Telegram/SourceFiles/ui/flattextarea.h | 2 ++ Telegram/Telegram.pro | 2 ++ Telegram/Telegram.xcodeproj/project.pbxproj | 30 ++++++++++++------- Telegram/Telegram.xcodeproj/qt_preprocess.mak | 6 ++++ 9 files changed, 39 insertions(+), 18 deletions(-) diff --git a/Telegram/SourceFiles/core/basic_types.h b/Telegram/SourceFiles/core/basic_types.h index d828ffd20..def3b712b 100644 --- a/Telegram/SourceFiles/core/basic_types.h +++ b/Telegram/SourceFiles/core/basic_types.h @@ -169,11 +169,11 @@ template char(&ArraySizeHelper(T(&array)[N]))[N]; // 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; +friend Q_DECL_CONSTEXPR QIncompatibleFlag operator|(Flags::enum_type f1, int f2) Q_DECL_NOTHROW; #define Q_DECLARE_FRIEND_OPERATORS_FOR_FLAGS(Flags) \ -friend QFlags operator|(Flags::enum_type f1, Flags::enum_type f2) Q_DECL_NOTHROW; \ -friend QFlags operator|(Flags::enum_type f1, QFlags f2) Q_DECL_NOTHROW; \ +friend Q_DECL_CONSTEXPR QFlags operator|(Flags::enum_type f1, Flags::enum_type f2) Q_DECL_NOTHROW; \ +friend Q_DECL_CONSTEXPR QFlags operator|(Flags::enum_type f1, QFlags 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 diff --git a/Telegram/SourceFiles/history/field_autocomplete.h b/Telegram/SourceFiles/history/field_autocomplete.h index b3ffaf58f..200a720fd 100644 --- a/Telegram/SourceFiles/history/field_autocomplete.h +++ b/Telegram/SourceFiles/history/field_autocomplete.h @@ -57,7 +57,7 @@ public: int32 innerTop(); int32 innerBottom(); - bool eventFilter(QObject *obj, QEvent *e); + bool eventFilter(QObject *obj, QEvent *e) override; enum class ChooseMethod { ByEnter, diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index e2ce96b1b..85a9ac085 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -5665,7 +5665,8 @@ void HistoryWidget::inlineBotResolveDone(const MTPcontacts_ResolvedPeer &result) UserData *resolvedBot = nullptr; if (result.type() == mtpc_contacts_resolvedPeer) { const auto &d(result.c_contacts_resolvedPeer()); - if (resolvedBot = App::feedUsers(d.vusers)) { + resolvedBot = App::feedUsers(d.vusers); + if (resolvedBot) { if (!resolvedBot->botInfo || resolvedBot->botInfo->inlinePlaceholder.isEmpty()) { resolvedBot = nullptr; } diff --git a/Telegram/SourceFiles/pspecific_mac.cpp b/Telegram/SourceFiles/pspecific_mac.cpp index 6ebacc89f..7cac0e6c4 100644 --- a/Telegram/SourceFiles/pspecific_mac.cpp +++ b/Telegram/SourceFiles/pspecific_mac.cpp @@ -431,7 +431,7 @@ void PsMainWindow::psMacUpdateMenu() { canPaste = !Application::clipboard()->text().isEmpty(); } else if (FlatTextarea *edit = qobject_cast(focused)) { canCut = canCopy = canDelete = edit->textCursor().hasSelection(); - canSelectAll = !edit->getLastText().isEmpty(); + canSelectAll = !edit->isEmpty(); canUndo = edit->isUndoAvailable(); canRedo = edit->isRedoAvailable(); canPaste = !Application::clipboard()->text().isEmpty(); diff --git a/Telegram/SourceFiles/ui/flattextarea.cpp b/Telegram/SourceFiles/ui/flattextarea.cpp index e777fe49c..4d9431344 100644 --- a/Telegram/SourceFiles/ui/flattextarea.cpp +++ b/Telegram/SourceFiles/ui/flattextarea.cpp @@ -83,12 +83,12 @@ QString FlatTextarea::tagsMimeType() { } 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) , a_phColor(st.phColor->c) , _a_appearance(animation(this, &FlatTextarea::step_appearance)) +, _lastTextWithTags { v, tags } , _st(st) { setAcceptRichText(false); resize(_st.width, _st.font->height); @@ -821,7 +821,7 @@ void FlatTextarea::parseLinks() { // some code is duplicated in text.cpp! continue; } } - newLinks.push_back({ domainOffset - 1, p - start - domainOffset + 2 }); + newLinks.push_back({ domainOffset - 1, static_cast(p - start - domainOffset + 2) }); offset = matchOffset = p - start; } diff --git a/Telegram/SourceFiles/ui/flattextarea.h b/Telegram/SourceFiles/ui/flattextarea.h index 8315022ea..39f605bee 100644 --- a/Telegram/SourceFiles/ui/flattextarea.h +++ b/Telegram/SourceFiles/ui/flattextarea.h @@ -116,6 +116,8 @@ public: public: virtual QString mimeTagFromTag(const QString &tagId) = 0; virtual QString tagFromMimeTag(const QString &mimeTag) = 0; + virtual ~TagMimeProcessor() { + } }; void setTagMimeProcessor(std_::unique_ptr &&processor); diff --git a/Telegram/Telegram.pro b/Telegram/Telegram.pro index 7973bc234..0094427a1 100644 --- a/Telegram/Telegram.pro +++ b/Telegram/Telegram.pro @@ -125,6 +125,7 @@ SOURCES += \ ./SourceFiles/dialogs/dialogs_indexed_list.cpp \ ./SourceFiles/dialogs/dialogs_layout.cpp \ ./SourceFiles/dialogs/dialogs_list.cpp \ + ./SourceFiles/history/field_autocomplete.cpp \ ./SourceFiles/inline_bots/inline_bot_layout_internal.cpp \ ./SourceFiles/inline_bots/inline_bot_layout_item.cpp \ ./SourceFiles/inline_bots/inline_bot_result.cpp \ @@ -248,6 +249,7 @@ HEADERS += \ ./SourceFiles/dialogs/dialogs_list.h \ ./SourceFiles/dialogs/dialogs_row.h \ ./SourceFiles/history/history_common.h \ + ./SourceFiles/history/field_autocomplete.h \ ./SourceFiles/inline_bots/inline_bot_layout_internal.h \ ./SourceFiles/inline_bots/inline_bot_layout_item.h \ ./SourceFiles/inline_bots/inline_bot_result.h \ diff --git a/Telegram/Telegram.xcodeproj/project.pbxproj b/Telegram/Telegram.xcodeproj/project.pbxproj index 9bcf3780d..95a76cb28 100644 --- a/Telegram/Telegram.xcodeproj/project.pbxproj +++ b/Telegram/Telegram.xcodeproj/project.pbxproj @@ -75,6 +75,8 @@ 076B1C5B1CBFC8F1002C0BC2 /* top_bar_widget.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 076B1C591CBFC8F1002C0BC2 /* top_bar_widget.cpp */; }; 076B1C5F1CBFC98F002C0BC2 /* overview_layout.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 076B1C5D1CBFC98F002C0BC2 /* overview_layout.cpp */; }; 076B1C631CBFCC53002C0BC2 /* moc_top_bar_widget.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 076B1C621CBFCC53002C0BC2 /* moc_top_bar_widget.cpp */; }; + 076C51D41CE205120038F22A /* field_autocomplete.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 076C51D21CE205120038F22A /* field_autocomplete.cpp */; }; + 076C51D71CE2069F0038F22A /* moc_field_autocomplete.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 076C51D61CE2069F0038F22A /* moc_field_autocomplete.cpp */; }; 077A4AF71CA41C38002188D2 /* connection_abstract.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 077A4AEC1CA41C38002188D2 /* connection_abstract.cpp */; }; 077A4AF81CA41C38002188D2 /* connection_auto.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 077A4AEE1CA41C38002188D2 /* connection_auto.cpp */; }; 077A4AF91CA41C38002188D2 /* connection_http.cpp in Compile Sources */ = {isa = PBXBuildFile; fileRef = 077A4AF01CA41C38002188D2 /* connection_http.cpp */; }; @@ -396,6 +398,9 @@ 076B1C5D1CBFC98F002C0BC2 /* overview_layout.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = overview_layout.cpp; path = SourceFiles/overview/overview_layout.cpp; sourceTree = SOURCE_ROOT; }; 076B1C5E1CBFC98F002C0BC2 /* overview_layout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = overview_layout.h; path = SourceFiles/overview/overview_layout.h; sourceTree = SOURCE_ROOT; }; 076B1C621CBFCC53002C0BC2 /* moc_top_bar_widget.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = moc_top_bar_widget.cpp; path = GeneratedFiles/Debug/moc_top_bar_widget.cpp; sourceTree = SOURCE_ROOT; }; + 076C51D21CE205120038F22A /* field_autocomplete.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = field_autocomplete.cpp; path = SourceFiles/history/field_autocomplete.cpp; sourceTree = SOURCE_ROOT; }; + 076C51D31CE205120038F22A /* field_autocomplete.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = field_autocomplete.h; path = SourceFiles/history/field_autocomplete.h; sourceTree = SOURCE_ROOT; }; + 076C51D61CE2069F0038F22A /* moc_field_autocomplete.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = moc_field_autocomplete.cpp; path = GeneratedFiles/Debug/moc_field_autocomplete.cpp; sourceTree = SOURCE_ROOT; }; 0771C4C94B623FC34BF62983 /* introwidget.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = introwidget.cpp; path = SourceFiles/intro/introwidget.cpp; sourceTree = ""; }; 077A4AEC1CA41C38002188D2 /* connection_abstract.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = connection_abstract.cpp; path = SourceFiles/mtproto/connection_abstract.cpp; sourceTree = SOURCE_ROOT; }; 077A4AED1CA41C38002188D2 /* connection_abstract.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = connection_abstract.h; path = SourceFiles/mtproto/connection_abstract.h; sourceTree = SOURCE_ROOT; }; @@ -884,6 +889,8 @@ 076B1C561CBFC8C9002C0BC2 /* history */ = { isa = PBXGroup; children = ( + 076C51D21CE205120038F22A /* field_autocomplete.cpp */, + 076C51D31CE205120038F22A /* field_autocomplete.h */, 076B1C571CBFC8D9002C0BC2 /* history_common.h */, ); name = history; @@ -1207,6 +1214,7 @@ 801973D3334D0FCA849CF485 /* Debug */ = { isa = PBXGroup; children = ( + 076C51D61CE2069F0038F22A /* moc_field_autocomplete.cpp */, 076B1C621CBFCC53002C0BC2 /* moc_top_bar_widget.cpp */, 07C8FE111CB80915007A8702 /* moc_toast_manager.cpp */, 077A4AFF1CA41EE2002188D2 /* moc_connection_abstract.cpp */, @@ -1675,6 +1683,7 @@ 77DA1217B595B799FB72CDDA /* flatinput.cpp in Compile Sources */, DE6A34CA3A5561888FA01AF1 /* flatlabel.cpp in Compile Sources */, 07C8FE041CB66D97007A8702 /* inline_bot_send_data.cpp in Compile Sources */, + 076C51D41CE205120038F22A /* field_autocomplete.cpp in Compile Sources */, 03270F718426CFE84729079E /* flattextarea.cpp in Compile Sources */, E3D7A5CA24541D5DB69D6606 /* images.cpp in Compile Sources */, ADE99904299B99EB6135E8D9 /* scrollarea.cpp in Compile Sources */, @@ -1771,6 +1780,7 @@ 07DE92AD1AA4928B00A18F6F /* moc_passcodebox.cpp in Compile Sources */, FCC949FEA178F9F5D7478027 /* moc_flattextarea.cpp in Compile Sources */, 07DB674D1AD07C9200A51329 /* abstractbox.cpp in Compile Sources */, + 076C51D71CE2069F0038F22A /* moc_field_autocomplete.cpp in Compile Sources */, 3F6EB1F5B98E704960FEA686 /* moc_scrollarea.cpp in Compile Sources */, 60CB4898955209B665E7B07D /* moc_twidget.cpp in Compile Sources */, 077A4B051CA41EE2002188D2 /* moc_connection_http.cpp in Compile Sources */, @@ -1903,11 +1913,13 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = ""; COPY_PHASE_STRIP = NO; + CRASHPAD_PATH = ./../../Libraries/crashpad/crashpad; CURRENT_PROJECT_VERSION = "$(TDESKTOP_VERSION)"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DYLIB_COMPATIBILITY_VERSION = "$(TDESKTOP_MAJOR_VERSION)"; DYLIB_CURRENT_VERSION = "$(TDESKTOP_VERSION)"; ENABLE_STRICT_OBJC_MSGSEND = YES; + FFMPEG_PATH = /usr/local; FRAMEWORK_SEARCH_PATHS = ""; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_LINK_WITH_DYNAMIC_LIBRARIES = NO; @@ -1940,6 +1952,7 @@ "$(CRASHPAD_PATH)", "$(CRASHPAD_PATH)/third_party/mini_chromium/mini_chromium", ); + ICONV_PATH = /usr/local; INFOPLIST_FILE = Telegram.plist; INSTALL_DIR = ./../Mac/Release/; LDPLUSPLUS = "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++"; @@ -1954,6 +1967,7 @@ ); MACOSX_DEPLOYMENT_TARGET = 10.8; OBJROOT = "./../Mac/$(CONFIGURATION)Intermediate"; + OPENAL_PATH = /usr/local; OTHER_CFLAGS = ( "-pipe", "-g", @@ -2015,15 +2029,11 @@ PRODUCT_NAME = Telegram; QT_LIBRARY_SUFFIX = ""; QT_PATH = "/usr/local/tdesktop/Qt-5.6.0"; - ZLIB_PATH = "/usr/local"; - FFMPEG_PATH = "/usr/local"; - ICONV_PATH = "/usr/local"; - CRASHPAD_PATH = "./../../Libraries/crashpad/crashpad"; - OPENAL_PATH = "/usr/local"; SDKROOT = macosx; SYMROOT = ./../Mac; TDESKTOP_MAJOR_VERSION = 0.9; TDESKTOP_VERSION = 0.9.48; + ZLIB_PATH = /usr/local; }; name = Release; }; @@ -2042,12 +2052,14 @@ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = ""; COPY_PHASE_STRIP = NO; + CRASHPAD_PATH = ./../../Libraries/crashpad/crashpad; CURRENT_PROJECT_VERSION = "$(TDESKTOP_VERSION)"; DEBUG_INFORMATION_FORMAT = dwarf; DYLIB_COMPATIBILITY_VERSION = "$(TDESKTOP_MAJOR_VERSION)"; DYLIB_CURRENT_VERSION = "$(TDESKTOP_VERSION)"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + FFMPEG_PATH = /usr/local; FRAMEWORK_SEARCH_PATHS = ""; GCC_GENERATE_DEBUGGING_SYMBOLS = YES; GCC_LINK_WITH_DYNAMIC_LIBRARIES = NO; @@ -2080,6 +2092,7 @@ "$(CRASHPAD_PATH)", "$(CRASHPAD_PATH)/third_party/mini_chromium/mini_chromium", ); + ICONV_PATH = /usr/local; INFOPLIST_FILE = Telegram.plist; INSTALL_DIR = ./../Mac/Debug/; LDPLUSPLUS = "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++"; @@ -2095,6 +2108,7 @@ MACOSX_DEPLOYMENT_TARGET = 10.8; OBJROOT = "./../Mac/$(CONFIGURATION)Intermediate"; ONLY_ACTIVE_ARCH = YES; + OPENAL_PATH = /usr/local; OTHER_CFLAGS = ( "-pipe", "-g", @@ -2156,15 +2170,11 @@ PRODUCT_NAME = Telegram; QT_LIBRARY_SUFFIX = _debug; QT_PATH = "/usr/local/tdesktop/Qt-5.6.0"; - ZLIB_PATH = "/usr/local"; - FFMPEG_PATH = "/usr/local"; - ICONV_PATH = "/usr/local"; - CRASHPAD_PATH = "./../../Libraries/crashpad/crashpad"; - OPENAL_PATH = "/usr/local"; SDKROOT = macosx; SYMROOT = ./../Mac; TDESKTOP_MAJOR_VERSION = 0.9; TDESKTOP_VERSION = 0.9.48; + ZLIB_PATH = /usr/local; }; name = Debug; }; diff --git a/Telegram/Telegram.xcodeproj/qt_preprocess.mak b/Telegram/Telegram.xcodeproj/qt_preprocess.mak index 43332f3cb..8547fc00a 100644 --- a/Telegram/Telegram.xcodeproj/qt_preprocess.mak +++ b/Telegram/Telegram.xcodeproj/qt_preprocess.mak @@ -111,6 +111,7 @@ compilers: GeneratedFiles/qrc_telegram.cpp\ GeneratedFiles/Debug/moc_sessionsbox.cpp\ GeneratedFiles/Debug/moc_stickersetbox.cpp\ GeneratedFiles/Debug/moc_usernamebox.cpp\ + GeneratedFiles/Debug/moc_field_autocomplete.cpp\ GeneratedFiles/Debug/moc_introwidget.cpp\ GeneratedFiles/Debug/moc_introcode.cpp\ GeneratedFiles/Debug/moc_introphone.cpp\ @@ -237,6 +238,7 @@ compiler_moc_header_make_all: GeneratedFiles/Debug/moc_apiwrap.cpp\ GeneratedFiles/Debug/moc_sessionsbox.cpp\ GeneratedFiles/Debug/moc_stickersetbox.cpp\ GeneratedFiles/Debug/moc_usernamebox.cpp\ + GeneratedFiles/Debug/moc_field_autocomplete.cpp\ GeneratedFiles/Debug/moc_introwidget.cpp\ GeneratedFiles/Debug/moc_introcode.cpp\ GeneratedFiles/Debug/moc_introphone.cpp\ @@ -306,6 +308,7 @@ compiler_moc_header_clean: GeneratedFiles/Debug/moc_sessionsbox.cpp\ GeneratedFiles/Debug/moc_stickersetbox.cpp\ GeneratedFiles/Debug/moc_usernamebox.cpp\ + GeneratedFiles/Debug/moc_field_autocomplete.cpp\ GeneratedFiles/Debug/moc_introwidget.cpp\ GeneratedFiles/Debug/moc_introcode.cpp\ GeneratedFiles/Debug/moc_introphone.cpp\ @@ -498,6 +501,9 @@ GeneratedFiles/Debug/moc_stickersetbox.cpp: SourceFiles/boxes/stickersetbox.h GeneratedFiles/Debug/moc_usernamebox.cpp:SourceFiles/boxes/usernamebox.h $(MOC_FILE) SourceFiles/boxes/usernamebox.h -o GeneratedFiles/Debug/moc_usernamebox.cpp +GeneratedFiles/Debug/moc_field_autocomplete.cpp: SourceFiles/history/field_autocomplete.h + $(MOC_FILE) SourceFiles/history/field_autocomplete.h -o GeneratedFiles/Debug/moc_field_autocomplete.cpp + GeneratedFiles/Debug/moc_introwidget.cpp: SourceFiles/intro/introwidget.h $(MOC_FILE) SourceFiles/intro/introwidget.h -o GeneratedFiles/Debug/moc_introwidget.cpp From 8c5493ba44877c9f3375d22ff8980c2c5c68e6d6 Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 12 May 2016 12:07:17 +0300 Subject: [PATCH 15/20] Fixed telegram.qrc dependency on basic.style compilation. --- Telegram/Telegram.vcxproj | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/Telegram/Telegram.vcxproj b/Telegram/Telegram.vcxproj index cc6dc115f..b67575bfe 100644 --- a/Telegram/Telegram.vcxproj +++ b/Telegram/Telegram.vcxproj @@ -2307,7 +2307,7 @@ - %(FullPath);.\Resources\art\icon256.png;%(AdditionalInputs) + %(FullPath);.\Resources\art\icon256.png;.\Resources\art\sprite_125x.png;.\Resources\art\sprite_150x.png;%(AdditionalInputs) Rcc%27ing %(Identity)... .\GeneratedFiles\qrc_%(Filename).cpp;%(Outputs) "$(QTDIR)\bin\rcc.exe" -name "%(Filename)" -no-compress "%(FullPath)" -o .\GeneratedFiles\qrc_%(Filename).cpp @@ -2388,7 +2388,18 @@ true true - + + Document + "$(SolutionDir)$(Platform)\codegen\$(Configuration)\codegen_style.exe" "-I.\Resources" "-I.\SourceFiles" "-o.\GeneratedFiles\styles" %(FullPath) + "$(SolutionDir)$(Platform)\codegen\$(Configuration)\codegen_style.exe" "-I.\Resources" "-I.\SourceFiles" "-o.\GeneratedFiles\styles" %(FullPath) + "$(SolutionDir)$(Platform)\codegen\$(Configuration)\codegen_style.exe" "-I.\Resources" "-I.\SourceFiles" "-o.\GeneratedFiles\styles" %(FullPath) + .\GeneratedFiles\styles\style_%(Filename).h;.\GeneratedFiles\styles\style_%(Filename).cpp;.\Resources\art\sprite_125x.png;.\Resources\art\sprite_150x.png;%(Outputs) + .\GeneratedFiles\styles\style_%(Filename).h;.\GeneratedFiles\styles\style_%(Filename).cpp;.\Resources\art\sprite_125x.png;.\Resources\art\sprite_150x.png;%(Outputs) + .\GeneratedFiles\styles\style_%(Filename).h;.\GeneratedFiles\styles\style_%(Filename).cpp;.\Resources\art\sprite_125x.png;.\Resources\art\sprite_150x.png;%(Outputs) + Compiling style %(Identity)... + Compiling style %(Identity)... + Compiling style %(Identity)... + From cb025b3aa8efb72406927e4e570ac125a76f40cb Mon Sep 17 00:00:00 2001 From: John Preston Date: Thu, 12 May 2016 19:55:45 +0300 Subject: [PATCH 16/20] Fixed access hash storing in EntityInTextMentionName. Editing message timer is shown for the last five minutes. --- Telegram/SourceFiles/historywidget.cpp | 2 +- Telegram/SourceFiles/ui/text/text_entity.cpp | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 85a9ac085..0d1a57f35 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -8190,7 +8190,7 @@ void HistoryWidget::drawField(Painter &p, const QRect &rect) { namespace { -constexpr int DisplayEditTimeWarningMs = 900 * 1000; +constexpr int DisplayEditTimeWarningMs = 300 * 1000; constexpr int FullDayInMs = 86400 * 1000; } // namespace diff --git a/Telegram/SourceFiles/ui/text/text_entity.cpp b/Telegram/SourceFiles/ui/text/text_entity.cpp index d3a79d030..5835f8bbb 100644 --- a/Telegram/SourceFiles/ui/text/text_entity.cpp +++ b/Telegram/SourceFiles/ui/text/text_entity.cpp @@ -1363,7 +1363,14 @@ EntitiesInText entitiesFromMTP(const QVector &entities) { case mtpc_messageEntityEmail: { const auto &d(entity.c_messageEntityEmail()); result.push_back(EntityInText(EntityInTextEmail, d.voffset.v, d.vlength.v)); } break; case mtpc_messageEntityHashtag: { const auto &d(entity.c_messageEntityHashtag()); result.push_back(EntityInText(EntityInTextHashtag, d.voffset.v, d.vlength.v)); } break; case mtpc_messageEntityMention: { const auto &d(entity.c_messageEntityMention()); result.push_back(EntityInText(EntityInTextMention, d.voffset.v, d.vlength.v)); } break; - case mtpc_messageEntityMentionName: { const auto &d(entity.c_messageEntityMentionName()); result.push_back(EntityInText(EntityInTextMentionName, d.voffset.v, d.vlength.v, QString::number(d.vuser_id.v))); } break; + case mtpc_messageEntityMentionName: { + const auto &d(entity.c_messageEntityMentionName()); + auto data = QString::number(d.vuser_id.v); + if (auto user = App::userLoaded(peerFromUser(d.vuser_id))) { + data += '.' + QString::number(user->access); + } + result.push_back(EntityInText(EntityInTextMentionName, d.voffset.v, d.vlength.v, data)); + } break; case mtpc_inputMessageEntityMentionName: { const auto &d(entity.c_inputMessageEntityMentionName()); auto data = ([&d]() -> QString { From 7444a9ad67de9fc803f707aab939ffaaec0f3cfe Mon Sep 17 00:00:00 2001 From: John Preston Date: Sat, 14 May 2016 20:28:35 +0300 Subject: [PATCH 17/20] Fixed scroll down to reply. Show that bot admins see all messages. Fixed date display when messages were at the same day of different months. Shared contact with author signature height increased. --- Telegram/SourceFiles/history.cpp | 3 +++ Telegram/SourceFiles/history.h | 4 ++-- Telegram/SourceFiles/historywidget.cpp | 10 +++++----- Telegram/SourceFiles/profilewidget.cpp | 18 +++++++++--------- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/Telegram/SourceFiles/history.cpp b/Telegram/SourceFiles/history.cpp index aedd4dd20..2a61878b0 100644 --- a/Telegram/SourceFiles/history.cpp +++ b/Telegram/SourceFiles/history.cpp @@ -5356,6 +5356,9 @@ void HistoryContact::initDimensions() { if (_userId) { _minh = st::msgFileThumbPadding.top() + st::msgFileThumbSize + st::msgFileThumbPadding.bottom(); + if (_parent->Has()) { + _minh += st::msgDateFont->height - st::msgDateDelta.y(); + } } else { _minh = st::msgFilePadding.top() + st::msgFileSize + st::msgFilePadding.bottom(); } diff --git a/Telegram/SourceFiles/history.h b/Telegram/SourceFiles/history.h index 50f1fa41b..35695e4bc 100644 --- a/Telegram/SourceFiles/history.h +++ b/Telegram/SourceFiles/history.h @@ -1524,8 +1524,8 @@ protected: // to add required bits to the Composer mask // after that always use Has() bool displayDate() const { - if (HistoryItem *prev = previous()) { - return prev->date.date().day() != date.date().day(); + if (auto prev = previous()) { + return prev->date.date() != date.date(); } return true; } diff --git a/Telegram/SourceFiles/historywidget.cpp b/Telegram/SourceFiles/historywidget.cpp index 0d1a57f35..089dab499 100644 --- a/Telegram/SourceFiles/historywidget.cpp +++ b/Telegram/SourceFiles/historywidget.cpp @@ -3724,11 +3724,6 @@ void HistoryWidget::showHistory(const PeerId &peerId, MsgId showAtMsgId, bool re if (_history) { if (_peer->id == peerId && !reload) { - _history->forgetScrollState(); - if (_migrated) { - _migrated->forgetScrollState(); - } - bool wasOnlyImportant = _history->isChannel() ? _history->asChannelHistory()->onlyImportant() : true; bool canShowNow = _history->isReadyFor(showAtMsgId, _fixedInScrollMsgId, _fixedInScrollMsgTop); @@ -3738,6 +3733,11 @@ void HistoryWidget::showHistory(const PeerId &peerId, MsgId showAtMsgId, bool re if (!canShowNow) { delayedShowAt(showAtMsgId); } else { + _history->forgetScrollState(); + if (_migrated) { + _migrated->forgetScrollState(); + } + if (_history->isChannel() && wasOnlyImportant != _history->asChannelHistory()->onlyImportant()) { clearAllLoadRequests(); } diff --git a/Telegram/SourceFiles/profilewidget.cpp b/Telegram/SourceFiles/profilewidget.cpp index a5ad1cc3a..adfc156a3 100644 --- a/Telegram/SourceFiles/profilewidget.cpp +++ b/Telegram/SourceFiles/profilewidget.cpp @@ -1030,15 +1030,6 @@ void ProfileInner::paintEvent(QPaintEvent *e) { if (!data) { data = _participantsData[cnt] = new ParticipantData(); data->name.setText(st::profileListNameFont, user->name, _textNameOptions); - if (user->botInfo) { - if (user->botInfo->readsAllHistory) { - data->online = lang(lng_status_bot_reads_all); - } else { - data->online = lang(lng_status_bot_not_reads_all); - } - } else { - data->online = App::onlineText(user, l_time); - } if (_peerChat) { data->admin = (peerFromUser(_peerChat->creator) == user->id) || (_peerChat->adminsEnabled() && (_peerChat->admins.constFind(user) != _peerChat->admins.cend())); } else if (_peerChannel) { @@ -1046,6 +1037,15 @@ void ProfileInner::paintEvent(QPaintEvent *e) { } else { data->admin = false; } + if (user->botInfo) { + if (user->botInfo->readsAllHistory || data->admin) { + data->online = lang(lng_status_bot_reads_all); + } else { + data->online = lang(lng_status_bot_not_reads_all); + } + } else { + data->online = App::onlineText(user, l_time); + } if (_amCreator) { data->cankick = (user != App::self()); } else if (_peerChat && _peerChat->amAdmin()) { From fb024e2256fcfa61d55e8cd4db5026f41946c199 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sat, 14 May 2016 21:14:55 +0300 Subject: [PATCH 18/20] Langs updated, changelog for 0.9.49 version added. --- Telegram/Resources/langs/lang.strings | 2 +- Telegram/Resources/langs/lang_de.strings | 12 +++++-- Telegram/Resources/langs/lang_es.strings | 12 +++++-- Telegram/Resources/langs/lang_it.strings | 14 ++++++-- Telegram/Resources/langs/lang_ko.strings | 14 ++++++-- Telegram/Resources/langs/lang_nl.strings | 12 +++++-- Telegram/Resources/langs/lang_pt_BR.strings | 12 +++++-- Telegram/SourceFiles/lang.cpp | 40 ++++++++++++++++++++- Telegram/SourceFiles/lang.h | 5 ++- Telegram/SourceFiles/mainwidget.cpp | 29 +-------------- Telegram/Telegram.xcodeproj/project.pbxproj | 6 +--- 11 files changed, 106 insertions(+), 52 deletions(-) diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 3b54f501d..c712b264f 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -897,7 +897,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_new_version_wrap" = "Telegram Desktop was updated to version {version}\n\n{changes}\n\nFull version history is available here:\n{link}"; "lng_new_version_minor" = "— Bug fixes and other minor improvements"; -"lng_new_version_text" = "BOTS 2.0\n\n— Introducing Bot API 2.0, the biggest update to our bot platform since June 2015.\n— Bots can now update existing messages on the fly as you interact with them.\n— New Inline keyboards with callback, 'open URL' or 'switch to inline mode' buttons help create seamless interfaces.\n— Inline bots can now send all attachments supported by Telegram (videos, music, stickers, locations, etc.).\n— Try out these sample bots to see what's coming your way soon: @music, @sticker, @youtube, @foursquare\n\nMore info: {link}"; +"lng_new_version_text" = "— Edit your messages everywhere within 2 days after posting (press the up arrow button to edit your last message).\n— Mention people in groups by typing @ and selecting them from the list — even if they don't have a username.\n\nMore: {link}"; "lng_menu_insert_unicode" = "Insert Unicode control character"; diff --git a/Telegram/Resources/langs/lang_de.strings b/Telegram/Resources/langs/lang_de.strings index 915c455d7..d9b0e31b3 100644 --- a/Telegram/Resources/langs/lang_de.strings +++ b/Telegram/Resources/langs/lang_de.strings @@ -399,7 +399,6 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_preview_loading" = "Lade Linkvorschau..."; "lng_profile_chat_unaccessible" = "Gruppe nicht verfügbar"; -"lng_topbar_info" = "Info"; "lng_profile_about_section" = "Info"; "lng_profile_description_section" = "Beschreibung"; "lng_profile_settings_section" = "Einstellungen"; @@ -600,6 +599,11 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_forwarded_channel_via" = "Weitergeleitet aus {channel} via {inline_bot}"; "lng_forwarded_signed" = "{channel} ({user})"; "lng_in_reply_to" = "Antwort auf"; +"lng_edited" = "bearbeitet"; +"lng_edited_date" = "Bearbeitet: {date}"; +"lng_cancel_edit_post_sure" = "Bearbeitung abbrechen?"; +"lng_cancel_edit_post_yes" = "Ja"; +"lng_cancel_edit_post_no" = "Nein"; "lng_bot_share_location_unavailable" = "Teilen von Standorten ist derzeit bei Telegram Desktop nicht möglich."; "lng_bot_inline_geo_unavailable" = "Dieser Bot braucht deinen aktuellen Standort. Die Funktion ist bei Telegram Desktop derzeit nicht verfügbar."; @@ -893,12 +897,16 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_new_version_wrap" = "Telegram Desktop wurde aktualisiert auf Version {version}\n\n{changes}\n\nGesamter Versionsverlauf:\n{link}"; "lng_new_version_minor" = "— Fehlerbehebungen und Softwareoptimierungen"; -"lng_new_version_text" = "— Optische Verbesserungen (u.a. runde Profilbilder)"; +"lng_new_version_text" = "— Bearbeite deine Nachrichten nachträglich (innerhalb von 2 Tagen).\n— Erwähne Leute in Gruppen - ohne das diese einen Benutzernamen haben müssen (einfach @ eingeben und aus der Liste auswählen).\n\nMehr: {link}"; "lng_menu_insert_unicode" = "Unicode-Steuerzeichen einfügen"; "lng_full_name" = "{first_name} {last_name}"; +// Not used + +"lng_topbar_info" = "Info"; + // Wnd specific "lng_wnd_choose_program_menu" = "Öffnen mit..."; diff --git a/Telegram/Resources/langs/lang_es.strings b/Telegram/Resources/langs/lang_es.strings index 6734c8537..932ef8681 100644 --- a/Telegram/Resources/langs/lang_es.strings +++ b/Telegram/Resources/langs/lang_es.strings @@ -399,7 +399,6 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_preview_loading" = "Obteniendo enlace..."; "lng_profile_chat_unaccessible" = "El grupo es inaccesible"; -"lng_topbar_info" = "Información"; "lng_profile_about_section" = "Acerca de"; "lng_profile_description_section" = "Descripción"; "lng_profile_settings_section" = "Ajustes"; @@ -600,6 +599,11 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_forwarded_channel_via" = "Reenviado desde {channel} vía {inline_bot}"; "lng_forwarded_signed" = "{channel} ({user})"; "lng_in_reply_to" = "Respondiendo a"; +"lng_edited" = "editado"; +"lng_edited_date" = "Editado: {date}"; +"lng_cancel_edit_post_sure" = "¿Cancelar edición?"; +"lng_cancel_edit_post_yes" = "Sí"; +"lng_cancel_edit_post_no" = "No"; "lng_bot_share_location_unavailable" = "Lo sentimos, compartir la ubicación no está disponible actualmente en Telegram Desktop."; "lng_bot_inline_geo_unavailable" = "Lo sentimos, este bot requiere compartir la ubicación.\nNo está disponible en Telegram Desktop."; @@ -893,12 +897,16 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_new_version_wrap" = "Telegram Desktop ha sido actualizada a la versión {version}\n\n{changes}\n\nEl historial completo está disponible aquí:\n{link}"; "lng_new_version_minor" = "— Corrección de errores y otras mejoras menores"; -"lng_new_version_text" = "— Mejoras visuales"; +"lng_new_version_text" = "— Edita tus mensajes, en todas partes, hasta 2 días después del envío.\n— Menciona a personas en grupos escribiendo @ y eligiéndolas desde la lista, incluso si no tienen un alias.\n\nMás: {link}"; "lng_menu_insert_unicode" = "Insertar caracteres de control Unicode"; "lng_full_name" = "{first_name} {last_name}"; +// Not used + +"lng_topbar_info" = "Información"; + // Wnd specific "lng_wnd_choose_program_menu" = "Elegir programa predeterminado..."; diff --git a/Telegram/Resources/langs/lang_it.strings b/Telegram/Resources/langs/lang_it.strings index be26b49a2..f102d8467 100644 --- a/Telegram/Resources/langs/lang_it.strings +++ b/Telegram/Resources/langs/lang_it.strings @@ -399,7 +399,6 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_preview_loading" = "Recupero le info del link..."; "lng_profile_chat_unaccessible" = "Gruppo non accessibile"; -"lng_topbar_info" = "Info"; "lng_profile_about_section" = "Info"; "lng_profile_description_section" = "Descrizione"; "lng_profile_settings_section" = "Impostazioni"; @@ -600,6 +599,11 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_forwarded_channel_via" = "Inoltrato da {channel} via {inline_bot}"; "lng_forwarded_signed" = "{channel} ({user})"; "lng_in_reply_to" = "In risposta a"; +"lng_edited" = "modificato"; +"lng_edited_date" = "Modificato: {date}"; +"lng_cancel_edit_post_sure" = "Annullare la modifica?"; +"lng_cancel_edit_post_yes" = "Sì"; +"lng_cancel_edit_post_no" = "No"; "lng_bot_share_location_unavailable" = "Spiacenti, la condivisione della posizione non è al momento disponibile su Telegram Desktop."; "lng_bot_inline_geo_unavailable" = "Spiacenti, questo bot richiede la condivisione della posizione. Non è disponibile su Telegram Desktop."; @@ -628,7 +632,7 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_media_auto_photo" = "Download automatico foto"; "lng_media_auto_audio" = "Download automatico messaggi vocali"; "lng_media_auto_gif" = "Download automatico GIF"; -"lng_media_auto_private_chats" = "Chat"; +"lng_media_auto_private_chats" = "Chat private"; "lng_media_auto_groups" = "Gruppi e canali"; "lng_media_auto_play" = "Autoriproduzione"; @@ -893,12 +897,16 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_new_version_wrap" = "Telegram Desktop si è aggiornato alla versione {version}\n\n{changes}\n\nLa cronologia degli aggiornamenti è disponibile qui:\n{link}"; "lng_new_version_minor" = "— Risoluzione di problemi e altri miglioramenti minori"; -"lng_new_version_text" = "— Miglioramenti visivi"; +"lng_new_version_text" = "— Modifica i tuoi messaggi ovunque entro 2 giorni dall'invio.\n— Menziona le persone nei gruppi digitando @ e selezionandole dalla lista — anche se non hanno un username.\n\nPiù informazioni: {link}"; "lng_menu_insert_unicode" = "Inserisci carattere di controllo Unicode"; "lng_full_name" = "{first_name} {last_name}"; +// Not used + +"lng_topbar_info" = "Info"; + // Wnd specific "lng_wnd_choose_program_menu" = "Scegli programma predefinito..."; diff --git a/Telegram/Resources/langs/lang_ko.strings b/Telegram/Resources/langs/lang_ko.strings index fcdcb5133..c98c9df76 100644 --- a/Telegram/Resources/langs/lang_ko.strings +++ b/Telegram/Resources/langs/lang_ko.strings @@ -399,7 +399,6 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_preview_loading" = "링크 정보를 가져오는 중.."; "lng_profile_chat_unaccessible" = "그룹에 접근할 수 없습니다."; -"lng_topbar_info" = "정보"; "lng_profile_about_section" = "정보"; "lng_profile_description_section" = "설명"; "lng_profile_settings_section" = "환경설정"; @@ -594,12 +593,17 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_channel_public_link_copied" = "클립보드에 링크가 복사되었습니다."; -"lng_forwarded" = "{user}로 부터 전달 받음"; +"lng_forwarded" = "{user}님으로 부터 전달 받음"; "lng_forwarded_channel" = "{channel}로 부터 전달 받음"; "lng_forwarded_via" = "{inline_bot}을 {user}로 부터 전달 받음"; "lng_forwarded_channel_via" = "{inline_bot}을 {channel}로 부터 전달 받음"; "lng_forwarded_signed" = "{channel} ({user})"; "lng_in_reply_to" = "다음 유저에게 답장 :"; +"lng_edited" = "수정됨"; +"lng_edited_date" = "수정됨: {date}"; +"lng_cancel_edit_post_sure" = "수정을 취소하겠습니까?"; +"lng_cancel_edit_post_yes" = "네"; +"lng_cancel_edit_post_no" = "아니요"; "lng_bot_share_location_unavailable" = "죄송합니다, 위치 공유는 텔레그램 테스크탑에서는 현재 지원을 하고 있지 않습니다."; "lng_bot_inline_geo_unavailable" = "죄송합니다, 위치 공유는 텔레그램 테스크탑에서는 현재 지원을 하고 있지 않습니다."; @@ -893,12 +897,16 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_new_version_wrap" = "텔레그램 데스크탑은 {version} 버전으로 업데이트 되었습니다.\n\n{changes}\n\n전체 버전 히스토리는 아래에서 확인 가능합니다:\n{link}"; "lng_new_version_minor" = "— 버그 수정 및 일부 기능 향상"; -"lng_new_version_text" = "— 비주얼 향상"; +"lng_new_version_text" = "— 메시지 작성 후 2일내 수정 기능\n— 그룹내에서 @를 입력하고 상대방을 선택하여 멘션 기능 ㅡ 아이디가 없어도 가능\n\n자세한 설명: {link}"; "lng_menu_insert_unicode" = "유니코드 문자를 입력하세요."; "lng_full_name" = "{last_name} {first_name}"; +// Not used + +"lng_topbar_info" = "정보"; + // Wnd specific "lng_wnd_choose_program_menu" = "기본 실행 프로그램을 선택해주세요.."; diff --git a/Telegram/Resources/langs/lang_nl.strings b/Telegram/Resources/langs/lang_nl.strings index 5768cf0c2..71c18f934 100644 --- a/Telegram/Resources/langs/lang_nl.strings +++ b/Telegram/Resources/langs/lang_nl.strings @@ -399,7 +399,6 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_preview_loading" = "Link-preview ophalen..."; "lng_profile_chat_unaccessible" = "Groep is ontoegankelijk"; -"lng_topbar_info" = "Info"; "lng_profile_about_section" = "Over"; "lng_profile_description_section" = "Beschrijving"; "lng_profile_settings_section" = "Instellingen"; @@ -600,6 +599,11 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_forwarded_channel_via" = "Doorgestuurd van {channel} via {inline_bot}"; "lng_forwarded_signed" = "{channel} ({user})"; "lng_in_reply_to" = "Antwoord op"; +"lng_edited" = "bewerkt"; +"lng_edited_date" = "Bewerkt: {date}"; +"lng_cancel_edit_post_sure" = "Bewerken annuleren?"; +"lng_cancel_edit_post_yes" = "Ja"; +"lng_cancel_edit_post_no" = "Nee"; "lng_bot_share_location_unavailable" = "Sorry, locatie delen is nog niet beschikbaar via Telegram Desktop."; "lng_bot_inline_geo_unavailable" = "Deze bot heeft je locatie nodig, dit is\nnog niet beschikbaar via Telegram Desktop."; @@ -893,12 +897,16 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_new_version_wrap" = "Telegram is bijgewerkt naar versie {version}\n\n{changes} \n\nVolledige versiegeschiedenis is hier te vinden:\n{link}"; "lng_new_version_minor" = "— Probleemoplossing en andere kleine verbeteringen"; -"lng_new_version_text" = "— Visuele verbeteringen"; +"lng_new_version_text" = "— 2 dagen om je berichten naderhand te bewerken.\n— Anderen vermelden in groepen via '@', kies daarna de persoon uit de lijst. Ook als ze geen gebruikersnaam hebben!\n\nMeer over deze update: {link}"; "lng_menu_insert_unicode" = "Unicode-besturingsteken invoegen"; "lng_full_name" = "{first_name} {last_name}"; +// Not used + +"lng_topbar_info" = "Info"; + // Wnd specific "lng_wnd_choose_program_menu" = "Standaardprogramma selecteren... "; diff --git a/Telegram/Resources/langs/lang_pt_BR.strings b/Telegram/Resources/langs/lang_pt_BR.strings index a2e156758..76b254907 100644 --- a/Telegram/Resources/langs/lang_pt_BR.strings +++ b/Telegram/Resources/langs/lang_pt_BR.strings @@ -399,7 +399,6 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_preview_loading" = "Obtendo Informações do Link..."; "lng_profile_chat_unaccessible" = "Grupo inacessível"; -"lng_topbar_info" = "Info"; "lng_profile_about_section" = "Sobre"; "lng_profile_description_section" = "Descrição"; "lng_profile_settings_section" = "Configurações"; @@ -600,6 +599,11 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_forwarded_channel_via" = "Encaminhado de {channel} via {inline_bot}"; "lng_forwarded_signed" = "{channel} ({user})"; "lng_in_reply_to" = "Em resposta a"; +"lng_edited" = "editado"; +"lng_edited_date" = "Editado: {date}"; +"lng_cancel_edit_post_sure" = "Cancelar edição?"; +"lng_cancel_edit_post_yes" = "Sim"; +"lng_cancel_edit_post_no" = "Não"; "lng_bot_share_location_unavailable" = "O compartilhamento de localização está atualmente indisponível no Telegram Desktop."; "lng_bot_inline_geo_unavailable" = "Esse bot requer compartilhamento de localização\nIsso não está disponível no Telegram Desktop."; @@ -893,12 +897,16 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org "lng_new_version_wrap" = "Telegram Desktop foi atualizado para a versão {version}\n\n{changes}\n\nHistórico completo de mudanças disponível aqui:\n{link}"; "lng_new_version_minor" = "— Resolução de bugs e outras melhorias menores"; -"lng_new_version_text" = "— Melhorias no visual"; +"lng_new_version_text" = "— Edite suas mensagens em qualquer lugar, até 2 dias após postagem.\n— Mencione pessoas nos grupos digitando @ e selecionando-os através da lista — mesmo que eles não tenham nome de usuário.\n\nMais: {link}"; "lng_menu_insert_unicode" = "Inserir caractere de controle Unicode"; "lng_full_name" = "{first_name} {last_name}"; +// Not used + +"lng_topbar_info" = "Info"; + // Wnd specific "lng_wnd_choose_program_menu" = "Escolher programa padrão..."; diff --git a/Telegram/SourceFiles/lang.cpp b/Telegram/SourceFiles/lang.cpp index b9bf2e9bd..c264bed1e 100644 --- a/Telegram/SourceFiles/lang.cpp +++ b/Telegram/SourceFiles/lang.cpp @@ -19,9 +19,10 @@ Full license: https://github.com/telegramdesktop/tdesktop/blob/master/LICENSE Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org */ #include "stdafx.h" - #include "lang.h" +#include "langloaderplain.h" + LangString langCounted(ushort key0, ushort tag, float64 value) { // current lang dependent int v = qFloor(value); QString sv; @@ -45,6 +46,43 @@ LangString langCounted(ushort key0, ushort tag, float64 value) { // current lang return lang(LangKey(key0)).tag(tag, sv); } +#define NEW_VER_TAG lt_link +#define NEW_VER_TAG_VALUE "https://telegram.org/blog/edit" + +QString langNewVersionText() { +#ifdef NEW_VER_TAG + return lng_new_version_text(NEW_VER_TAG, QString::fromUtf8(NEW_VER_TAG_VALUE)); +#else // NEW_VER_TAG + return lang(lng_new_version_text); +#endif // NEW_VER_TAG +} + +#ifdef NEW_VER_TAG +#define NEW_VER_KEY lng_new_version_text__tagged +#define NEW_VER_POSTFIX .tag(NEW_VER_TAG, QString::fromUtf8(NEW_VER_TAG_VALUE)) +#else // NEW_VER_TAG +#define NEW_VER_KEY lng_new_version_text +#define NEW_VER_POSTFIX +#endif // NEW_VER_TAG + +QString langNewVersionTextForLang(int langId) { + LangLoaderResult result; + if (langId) { + LangLoaderPlain loader(qsl(":/langs/lang_") + LanguageCodes[langId].c_str() + qsl(".strings"), LangLoaderRequest(lng_language_name, NEW_VER_KEY)); + result = loader.found(); + } else { + result.insert(lng_language_name, langOriginal(lng_language_name)); + result.insert(NEW_VER_KEY, langOriginal(NEW_VER_KEY)); + } + return result.value(lng_language_name, LanguageCodes[langId].c_str() + qsl(" language")) + qsl(":\n\n") + LangString(result.value(NEW_VER_KEY, qsl("--none--")))NEW_VER_POSTFIX; +} + +#undef NEW_VER_POSTFIX +#undef NEW_VER_KEY + +#undef NEW_VER_TAG_VALUE +#undef NEW_VER_TAG + const QString &LangLoader::errors() const { if (_errors.isEmpty() && !_err.isEmpty()) { _errors = _err.join('\n'); diff --git a/Telegram/SourceFiles/lang.h b/Telegram/SourceFiles/lang.h index 7394e8559..d3465ddb4 100644 --- a/Telegram/SourceFiles/lang.h +++ b/Telegram/SourceFiles/lang.h @@ -142,9 +142,8 @@ inline LangString langDateTimeFull(const QDateTime &date) { return lng_mediaview_date_time(lt_date, langDayOfMonthFull(date.date()), lt_time, date.time().toString(cTimeFormat())); } -inline LangString langNewVersionText() { - return lng_new_version_text(lt_link, qsl("https://telegram.org/blog/bots-2-0")); -} +QString langNewVersionText(); +QString langNewVersionTextForLang(int langId); class LangLoader { public: diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 22e24d2d9..000fc8024 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -43,7 +43,6 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "localstorage.h" #include "shortcuts.h" #include "audio.h" -#include "langloaderplain.h" MainWidget::MainWidget(MainWindow *window) : TWidget(window) , _a_show(animation(this, &MainWidget::step_show)) @@ -1055,35 +1054,9 @@ void executeParsedCommand(const QString &command) { if (command == qsl("new_version_text")) { App::wnd()->serviceNotification(langNewVersionText()); } else if (command == qsl("all_new_version_texts")) { - -#define NEW_VER_TAG lt_link -#define NEW_VER_TAG_VALUE "https://telegram.org/blog/bots-2-0" - -#ifdef NEW_VER_TAG -#define NEW_VER_KEY lng_new_version_text__tagged -#define NEW_VER_POSTFIX .tag(NEW_VER_TAG, QString::fromUtf8(NEW_VER_TAG_VALUE)) -#else -#define NEW_VER_KEY lng_new_version_text -#define NEW_VER_POSTFIX -#endif - for (int i = 0; i < languageCount; ++i) { - LangLoaderResult result; - if (i) { - LangLoaderPlain loader(qsl(":/langs/lang_") + LanguageCodes[i].c_str() + qsl(".strings"), LangLoaderRequest(lng_language_name, NEW_VER_KEY)); - result = loader.found(); - } else { - result.insert(lng_language_name, langOriginal(lng_language_name)); - result.insert(NEW_VER_KEY, langOriginal(NEW_VER_KEY)); - } - App::wnd()->serviceNotification(result.value(lng_language_name, LanguageCodes[i].c_str() + qsl(" language")) + qsl(":\n\n") + LangString(result.value(NEW_VER_KEY, qsl("--none--")))NEW_VER_POSTFIX); + App::wnd()->serviceNotification(langNewVersionTextForLang(i)); } - -#undef NEW_VER_POSTFIX -#undef NEW_VER_KEY -#undef NEW_VER_TAG_VALUE -#undef NEW_VER_TAG - } } } // namespace diff --git a/Telegram/Telegram.xcodeproj/project.pbxproj b/Telegram/Telegram.xcodeproj/project.pbxproj index 95a76cb28..6d1a1ff41 100644 --- a/Telegram/Telegram.xcodeproj/project.pbxproj +++ b/Telegram/Telegram.xcodeproj/project.pbxproj @@ -457,8 +457,6 @@ 07BE85111A20961F008ACB9F /* moc_localstorage.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = moc_localstorage.cpp; path = GeneratedFiles/Debug/moc_localstorage.cpp; sourceTree = SOURCE_ROOT; }; 07C3AF24194335ED0016CFF1 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Telegram/Images.xcassets; sourceTree = SOURCE_ROOT; }; 07C3AF27194336B90016CFF1 /* pspecific_mac_p.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = pspecific_mac_p.h; path = SourceFiles/pspecific_mac_p.h; sourceTree = SOURCE_ROOT; }; - 07C3AF2919433ABF0016CFF1 /* style_classes.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = style_classes.txt; path = Resources/style_classes.txt; sourceTree = SOURCE_ROOT; }; - 07C3AF2A19433ABF0016CFF1 /* style.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = style.txt; path = Resources/style.txt; sourceTree = SOURCE_ROOT; }; 07C7596D1B1F7E0000662169 /* autoupdater.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = autoupdater.cpp; path = SourceFiles/autoupdater.cpp; sourceTree = SOURCE_ROOT; }; 07C7596E1B1F7E0000662169 /* autoupdater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = autoupdater.h; path = SourceFiles/autoupdater.h; sourceTree = SOURCE_ROOT; }; 07C759711B1F7E2800662169 /* moc_autoupdater.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = moc_autoupdater.cpp; path = GeneratedFiles/Debug/moc_autoupdater.cpp; sourceTree = SOURCE_ROOT; }; @@ -849,6 +847,7 @@ 074968CB1A44D0B800394F46 /* langs */ = { isa = PBXGroup; children = ( + 07080BCB1A4357F300741A51 /* lang.strings */, 07A190511A723E0A004287AE /* lang_ko.strings */, 072E117A1A56EB9400A87ACC /* lang_pt_BR.strings */, 078DD0241A48DD9E00DD14CC /* lang_de.strings */, @@ -918,10 +917,7 @@ isa = PBXGroup; children = ( 074968CB1A44D0B800394F46 /* langs */, - 07080BCB1A4357F300741A51 /* lang.strings */, 0747FF811CC644FF00096FC3 /* numbers.txt */, - 07C3AF2919433ABF0016CFF1 /* style_classes.txt */, - 07C3AF2A19433ABF0016CFF1 /* style.txt */, 07AF95F71AFD03C80060B057 /* telegram_emojis.qrc */, 07AF95F81AFD03C80060B057 /* telegram_mac.qrc */, 1292B92B4848460640F6A391 /* telegram.qrc */, From c2a5ab0c5a9184032319dd6725a822c95e11e0c7 Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 15 May 2016 13:30:47 +0300 Subject: [PATCH 19/20] Version 0.9.49: disabling high dpi scaling in all systems except OS X. --- Telegram/Resources/winrc/Telegram.rc | 8 ++++---- Telegram/Resources/winrc/Updater.rc | 8 ++++---- Telegram/SourceFiles/application.cpp | 4 ++-- Telegram/SourceFiles/core/version.h | 6 +++--- Telegram/SourceFiles/main.cpp | 4 ++++ Telegram/Telegram.xcodeproj/project.pbxproj | 4 ++-- Telegram/build/version | 8 ++++---- 7 files changed, 23 insertions(+), 19 deletions(-) diff --git a/Telegram/Resources/winrc/Telegram.rc b/Telegram/Resources/winrc/Telegram.rc index ee19b5322..65d2ba0f4 100644 --- a/Telegram/Resources/winrc/Telegram.rc +++ b/Telegram/Resources/winrc/Telegram.rc @@ -34,8 +34,8 @@ IDI_ICON1 ICON "..\\art\\icon256.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 0,9,48,1 - PRODUCTVERSION 0,9,48,1 + FILEVERSION 0,9,49,0 + PRODUCTVERSION 0,9,49,0 FILEFLAGSMASK 0x3fL #ifdef _DEBUG FILEFLAGS 0x1L @@ -51,10 +51,10 @@ BEGIN BLOCK "040904b0" BEGIN VALUE "CompanyName", "Telegram Messenger LLP" - VALUE "FileVersion", "0.9.48.1" + VALUE "FileVersion", "0.9.49.0" VALUE "LegalCopyright", "Copyright (C) 2014-2016" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "0.9.48.1" + VALUE "ProductVersion", "0.9.49.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/Resources/winrc/Updater.rc b/Telegram/Resources/winrc/Updater.rc index 6f897a376..9fc56b8bf 100644 --- a/Telegram/Resources/winrc/Updater.rc +++ b/Telegram/Resources/winrc/Updater.rc @@ -25,8 +25,8 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US // VS_VERSION_INFO VERSIONINFO - FILEVERSION 0,9,48,1 - PRODUCTVERSION 0,9,48,1 + FILEVERSION 0,9,49,0 + PRODUCTVERSION 0,9,49,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.48.1" + VALUE "FileVersion", "0.9.49.0" VALUE "LegalCopyright", "Copyright (C) 2014-2016" VALUE "ProductName", "Telegram Desktop" - VALUE "ProductVersion", "0.9.48.1" + VALUE "ProductVersion", "0.9.49.0" END END BLOCK "VarFileInfo" diff --git a/Telegram/SourceFiles/application.cpp b/Telegram/SourceFiles/application.cpp index e86263622..0b4470dbf 100644 --- a/Telegram/SourceFiles/application.cpp +++ b/Telegram/SourceFiles/application.cpp @@ -1031,10 +1031,10 @@ void AppClass::checkMapVersion() { if (Local::oldMapVersion() < AppVersion) { if (Local::oldMapVersion()) { QString versionFeatures; - if ((cAlphaVersion() || cBetaVersion()) && Local::oldMapVersion() < 9041) { + if ((cAlphaVersion() || cBetaVersion()) && Local::oldMapVersion() < 9049) { versionFeatures = QString::fromUtf8("\xe2\x80\x94 Select and copy text in photo / video captions and web page previews\n\xe2\x80\x94 Media player shortcuts are enabled only when player is opened"); // versionFeatures = langNewVersionText(); - } else if (Local::oldMapVersion() < 9041) { + } else if (Local::oldMapVersion() < 9049) { versionFeatures = langNewVersionText(); } else { versionFeatures = lang(lng_new_version_minor).trimmed(); diff --git a/Telegram/SourceFiles/core/version.h b/Telegram/SourceFiles/core/version.h index 31c790162..df192203a 100644 --- a/Telegram/SourceFiles/core/version.h +++ b/Telegram/SourceFiles/core/version.h @@ -22,9 +22,9 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "core/basic_types.h" -#define BETA_VERSION_MACRO (9048001ULL) +#define BETA_VERSION_MACRO (0ULL) -constexpr int AppVersion = 9048; -constexpr str_const AppVersionStr = "0.9.48"; +constexpr int AppVersion = 9049; +constexpr str_const AppVersionStr = "0.9.49"; constexpr bool AppAlphaVersion = false; constexpr uint64 AppBetaVersion = BETA_VERSION_MACRO; diff --git a/Telegram/SourceFiles/main.cpp b/Telegram/SourceFiles/main.cpp index e0300abb6..0b704044b 100644 --- a/Telegram/SourceFiles/main.cpp +++ b/Telegram/SourceFiles/main.cpp @@ -25,6 +25,10 @@ Copyright (c) 2014-2016 John Preston, https://desktop.telegram.org #include "localstorage.h" int main(int argc, char *argv[]) { +#ifndef Q_OS_MAC // Retina display support is working fine, others are not. + QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling, true); +#endif // Q_OS_MAC + settingsParseArgs(argc, argv); if (cLaunchMode() == LaunchModeFixPrevious) { return psFixPrevious(); diff --git a/Telegram/Telegram.xcodeproj/project.pbxproj b/Telegram/Telegram.xcodeproj/project.pbxproj index 6d1a1ff41..fa9500d16 100644 --- a/Telegram/Telegram.xcodeproj/project.pbxproj +++ b/Telegram/Telegram.xcodeproj/project.pbxproj @@ -2028,7 +2028,7 @@ SDKROOT = macosx; SYMROOT = ./../Mac; TDESKTOP_MAJOR_VERSION = 0.9; - TDESKTOP_VERSION = 0.9.48; + TDESKTOP_VERSION = 0.9.49; ZLIB_PATH = /usr/local; }; name = Release; @@ -2169,7 +2169,7 @@ SDKROOT = macosx; SYMROOT = ./../Mac; TDESKTOP_MAJOR_VERSION = 0.9; - TDESKTOP_VERSION = 0.9.48; + TDESKTOP_VERSION = 0.9.49; ZLIB_PATH = /usr/local; }; name = Debug; diff --git a/Telegram/build/version b/Telegram/build/version index ecb2c68fb..648a9fccf 100644 --- a/Telegram/build/version +++ b/Telegram/build/version @@ -1,6 +1,6 @@ -AppVersion 9048 +AppVersion 9049 AppVersionStrMajor 0.9 -AppVersionStrSmall 0.9.48 -AppVersionStr 0.9.48 +AppVersionStrSmall 0.9.49 +AppVersionStr 0.9.49 AlphaChannel 0 -BetaVersion 9048001 +BetaVersion 0 From 895facbcddadcf4f888738b6e3f4dfd46a3fd86f Mon Sep 17 00:00:00 2001 From: John Preston Date: Sun, 15 May 2016 23:14:47 +0300 Subject: [PATCH 20/20] Fixed changelog for alpha and beta in 0.9.49 version. --- Telegram/SourceFiles/application.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Telegram/SourceFiles/application.cpp b/Telegram/SourceFiles/application.cpp index 0b4470dbf..7e9850c5b 100644 --- a/Telegram/SourceFiles/application.cpp +++ b/Telegram/SourceFiles/application.cpp @@ -1032,8 +1032,8 @@ void AppClass::checkMapVersion() { if (Local::oldMapVersion()) { QString versionFeatures; if ((cAlphaVersion() || cBetaVersion()) && Local::oldMapVersion() < 9049) { - versionFeatures = QString::fromUtf8("\xe2\x80\x94 Select and copy text in photo / video captions and web page previews\n\xe2\x80\x94 Media player shortcuts are enabled only when player is opened"); -// versionFeatures = langNewVersionText(); +// versionFeatures = QString::fromUtf8("\xe2\x80\x94 Select and copy text in photo / video captions and web page previews\n\xe2\x80\x94 Media player shortcuts are enabled only when player is opened"); + versionFeatures = langNewVersionText(); } else if (Local::oldMapVersion() < 9049) { versionFeatures = langNewVersionText(); } else {