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