1 /*
2 This file is part of Telegram Desktop,
3 the official desktop application for the Telegram messaging service.
4
5 For license and copyright information please follow this link:
6 https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
7 */
8 #include "chat_helpers/message_field.h"
9
10 #include "history/history_widget.h"
11 #include "history/history.h" // History::session
12 #include "history/history_item.h" // HistoryItem::originalText
13 #include "base/qthelp_regex.h"
14 #include "base/qthelp_url.h"
15 #include "base/event_filter.h"
16 #include "boxes/abstract_box.h"
17 #include "core/shortcuts.h"
18 #include "core/application.h"
19 #include "core/core_settings.h"
20 #include "ui/wrap/vertical_layout.h"
21 #include "ui/widgets/popup_menu.h"
22 #include "ui/ui_utility.h"
23 #include "data/data_session.h"
24 #include "data/data_user.h"
25 #include "chat_helpers/emoji_suggestions_widget.h"
26 #include "window/window_session_controller.h"
27 #include "lang/lang_keys.h"
28 #include "mainwindow.h"
29 #include "main/main_session.h"
30 #include "styles/style_layers.h"
31 #include "styles/style_boxes.h"
32 #include "styles/style_chat.h"
33 #include "base/qt_adapters.h"
34
35 #include <QtCore/QMimeData>
36 #include <QtCore/QStack>
37 #include <QtGui/QGuiApplication>
38 #include <QtGui/QTextBlock>
39 #include <QtGui/QClipboard>
40 #include <QtWidgets/QApplication>
41
42 namespace {
43
44 using namespace Ui::Text;
45
46 using EditLinkAction = Ui::InputField::EditLinkAction;
47 using EditLinkSelection = Ui::InputField::EditLinkSelection;
48
49 constexpr auto kParseLinksTimeout = crl::time(1000);
50
51 // For mention tags save and validate userId, ignore tags for different userId.
52 class FieldTagMimeProcessor : public Ui::InputField::TagMimeProcessor {
53 public:
54 explicit FieldTagMimeProcessor(
55 not_null<Window::SessionController*> controller);
56
57 QString tagFromMimeTag(const QString &mimeTag) override;
58
59 private:
60 const not_null<Window::SessionController*> _controller;
61
62 };
63
64 class EditLinkBox : public Ui::BoxContent {
65 public:
66 EditLinkBox(
67 QWidget*,
68 not_null<Window::SessionController*> controller,
69 const QString &text,
70 const QString &link,
71 Fn<void(QString, QString)> callback);
72
73 void setInnerFocus() override;
74
75 protected:
76 void prepare() override;
77
78 private:
79 const not_null<Window::SessionController*> _controller;
80 QString _startText;
81 QString _startLink;
82 Fn<void(QString, QString)> _callback;
83 Fn<void()> _setInnerFocus;
84
85 };
86
FieldTagMimeProcessor(not_null<Window::SessionController * > controller)87 FieldTagMimeProcessor::FieldTagMimeProcessor(
88 not_null<Window::SessionController*> controller)
89 : _controller(controller) {
90 }
91
tagFromMimeTag(const QString & mimeTag)92 QString FieldTagMimeProcessor::tagFromMimeTag(const QString &mimeTag) {
93 if (TextUtilities::IsMentionLink(mimeTag)) {
94 const auto userId = _controller->session().userId();
95 auto match = QRegularExpression(":(\\d+)$").match(mimeTag);
96 if (!match.hasMatch()
97 || match.capturedView(1).toULongLong() != userId.bare) {
98 return QString();
99 }
100 return mimeTag.mid(0, mimeTag.size() - match.capturedLength());
101 }
102 return mimeTag;
103 }
104
105 //bool ValidateUrl(const QString &value) {
106 // const auto match = qthelp::RegExpDomain().match(value);
107 // if (!match.hasMatch() || match.capturedStart() != 0) {
108 // return false;
109 // }
110 // const auto protocolMatch = RegExpProtocol().match(value);
111 // return protocolMatch.hasMatch()
112 // && IsGoodProtocol(protocolMatch.captured(1));
113 //}
114
EditLinkBox(QWidget *,not_null<Window::SessionController * > controller,const QString & text,const QString & link,Fn<void (QString,QString)> callback)115 EditLinkBox::EditLinkBox(
116 QWidget*,
117 not_null<Window::SessionController*> controller,
118 const QString &text,
119 const QString &link,
120 Fn<void(QString, QString)> callback)
121 : _controller(controller)
122 , _startText(text)
123 , _startLink(link)
124 , _callback(std::move(callback)) {
125 Expects(_callback != nullptr);
126 }
127
setInnerFocus()128 void EditLinkBox::setInnerFocus() {
129 Expects(_setInnerFocus != nullptr);
130
131 _setInnerFocus();
132 }
133
prepare()134 void EditLinkBox::prepare() {
135 const auto content = Ui::CreateChild<Ui::VerticalLayout>(this);
136
137 const auto session = &_controller->session();
138 const auto text = content->add(
139 object_ptr<Ui::InputField>(
140 content,
141 st::defaultInputField,
142 tr::lng_formatting_link_text(),
143 _startText),
144 st::markdownLinkFieldPadding);
145 text->setInstantReplaces(Ui::InstantReplaces::Default());
146 text->setInstantReplacesEnabled(
147 Core::App().settings().replaceEmojiValue());
148 Ui::Emoji::SuggestionsController::Init(
149 getDelegate()->outerContainer(),
150 text,
151 session);
152 InitSpellchecker(_controller, text);
153
154 const auto placeholder = content->add(
155 object_ptr<Ui::RpWidget>(content),
156 st::markdownLinkFieldPadding);
157 placeholder->setAttribute(Qt::WA_TransparentForMouseEvents);
158 const auto url = Ui::AttachParentChild(
159 content,
160 object_ptr<Ui::MaskedInputField>(
161 content,
162 st::defaultInputField,
163 tr::lng_formatting_link_url(),
164 _startLink.trimmed()));
165 url->heightValue(
166 ) | rpl::start_with_next([placeholder](int height) {
167 placeholder->resize(placeholder->width(), height);
168 }, placeholder->lifetime());
169 placeholder->widthValue(
170 ) | rpl::start_with_next([=](int width) {
171 url->resize(width, url->height());
172 }, placeholder->lifetime());
173 url->move(placeholder->pos());
174
175 const auto submit = [=] {
176 const auto linkText = text->getLastText();
177 const auto linkUrl = qthelp::validate_url(url->getLastText());
178 if (linkText.isEmpty()) {
179 text->showError();
180 return;
181 } else if (linkUrl.isEmpty()) {
182 url->showError();
183 return;
184 }
185 const auto weak = Ui::MakeWeak(this);
186 _callback(linkText, linkUrl);
187 if (weak) {
188 closeBox();
189 }
190 };
191
192 connect(text, &Ui::InputField::submitted, [=] {
193 url->setFocusFast();
194 });
195 connect(url, &Ui::MaskedInputField::submitted, [=] {
196 if (text->getLastText().isEmpty()) {
197 text->setFocusFast();
198 } else {
199 submit();
200 }
201 });
202
203 setTitle(url->getLastText().isEmpty()
204 ? tr::lng_formatting_link_create_title()
205 : tr::lng_formatting_link_edit_title());
206
207 addButton(tr::lng_formatting_link_create(), submit);
208 addButton(tr::lng_cancel(), [=] { closeBox(); });
209
210 content->resizeToWidth(st::boxWidth);
211 content->moveToLeft(0, 0);
212 setDimensions(st::boxWidth, content->height());
213
214 _setInnerFocus = [=] {
215 if (_startText.isEmpty()) {
216 text->setFocusFast();
217 } else {
218 url->setFocusFast();
219 }
220 };
221 }
222
StripSupportHashtag(TextWithEntities && text)223 TextWithEntities StripSupportHashtag(TextWithEntities &&text) {
224 static const auto expression = QRegularExpression(
225 qsl("\\n?#tsf[a-z0-9_-]*[\\s#a-z0-9_-]*$"),
226 QRegularExpression::CaseInsensitiveOption);
227 const auto match = expression.match(text.text);
228 if (!match.hasMatch()) {
229 return std::move(text);
230 }
231 text.text.chop(match.capturedLength());
232 const auto length = text.text.size();
233 if (!length) {
234 return TextWithEntities();
235 }
236 for (auto i = text.entities.begin(); i != text.entities.end();) {
237 auto &entity = *i;
238 if (entity.offset() >= length) {
239 i = text.entities.erase(i);
240 continue;
241 } else if (entity.offset() + entity.length() > length) {
242 entity.shrinkFromRight(length - entity.offset());
243 }
244 ++i;
245 }
246 return std::move(text);
247 }
248
249 } // namespace
250
PrepareMentionTag(not_null<UserData * > user)251 QString PrepareMentionTag(not_null<UserData*> user) {
252 return TextUtilities::kMentionTagStart
253 + QString::number(user->id.value)
254 + '.'
255 + QString::number(user->accessHash());
256 }
257
PrepareEditText(not_null<HistoryItem * > item)258 TextWithTags PrepareEditText(not_null<HistoryItem*> item) {
259 const auto original = item->history()->session().supportMode()
260 ? StripSupportHashtag(item->originalText())
261 : item->originalText();
262 return TextWithTags{
263 original.text,
264 TextUtilities::ConvertEntitiesToTextTags(original.entities)
265 };
266 }
267
268 Fn<bool(
269 Ui::InputField::EditLinkSelection selection,
270 QString text,
271 QString link,
DefaultEditLinkCallback(not_null<Window::SessionController * > controller,not_null<Ui::InputField * > field)272 EditLinkAction action)> DefaultEditLinkCallback(
273 not_null<Window::SessionController*> controller,
274 not_null<Ui::InputField*> field) {
275 const auto weak = Ui::MakeWeak(field);
276 return [=](
277 EditLinkSelection selection,
278 QString text,
279 QString link,
280 EditLinkAction action) {
281 if (action == EditLinkAction::Check) {
282 return Ui::InputField::IsValidMarkdownLink(link)
283 && !TextUtilities::IsMentionLink(link);
284 }
285 controller->show(Box<EditLinkBox>(controller, text, link, [=](
286 const QString &text,
287 const QString &link) {
288 if (const auto strong = weak.data()) {
289 strong->commitMarkdownLinkEdit(selection, text, link);
290 }
291 }), Ui::LayerOption::KeepOther);
292 return true;
293 };
294 }
295
InitMessageField(not_null<Window::SessionController * > controller,not_null<Ui::InputField * > field)296 void InitMessageField(
297 not_null<Window::SessionController*> controller,
298 not_null<Ui::InputField*> field) {
299 field->setMinHeight(st::historySendSize.height() - 2 * st::historySendPadding);
300 field->setMaxHeight(st::historyComposeFieldMaxHeight);
301
302 field->setTagMimeProcessor(
303 std::make_unique<FieldTagMimeProcessor>(controller));
304
305 field->document()->setDocumentMargin(4.);
306 field->setAdditionalMargin(style::ConvertScale(4) - 4);
307
308 field->customTab(true);
309 field->setInstantReplaces(Ui::InstantReplaces::Default());
310 field->setInstantReplacesEnabled(
311 Core::App().settings().replaceEmojiValue());
312 field->setMarkdownReplacesEnabled(rpl::single(true));
313 field->setEditLinkCallback(DefaultEditLinkCallback(controller, field));
314 }
315
InitSpellchecker(not_null<Window::SessionController * > controller,not_null<Ui::InputField * > field)316 void InitSpellchecker(
317 not_null<Window::SessionController*> controller,
318 not_null<Ui::InputField*> field) {
319 #ifndef TDESKTOP_DISABLE_SPELLCHECK
320 const auto s = Ui::CreateChild<Spellchecker::SpellingHighlighter>(
321 field.get(),
322 Core::App().settings().spellcheckerEnabledValue(),
323 Spellchecker::SpellingHighlighter::CustomContextMenuItem{
324 tr::lng_settings_manage_dictionaries(tr::now),
325 [=] {
326 controller->show(Box<Ui::ManageDictionariesBox>(controller));
327 }
328 });
329 field->setExtendedContextMenu(s->contextMenuCreated());
330 #endif // TDESKTOP_DISABLE_SPELLCHECK
331 }
332
HasSendText(not_null<const Ui::InputField * > field)333 bool HasSendText(not_null<const Ui::InputField*> field) {
334 const auto &text = field->getTextWithTags().text;
335 for (const auto &ch : text) {
336 const auto code = ch.unicode();
337 if (code != ' '
338 && code != '\n'
339 && code != '\r'
340 && !IsReplacedBySpace(code)) {
341 return true;
342 }
343 }
344 return false;
345 }
346
ParseInlineBotQuery(not_null<Main::Session * > session,not_null<const Ui::InputField * > field)347 InlineBotQuery ParseInlineBotQuery(
348 not_null<Main::Session*> session,
349 not_null<const Ui::InputField*> field) {
350 auto result = InlineBotQuery();
351
352 const auto &full = field->getTextWithTags();
353 const auto &text = full.text;
354 const auto textLength = text.size();
355
356 auto inlineUsernameStart = 1;
357 auto inlineUsernameLength = 0;
358 if (textLength > 2 && text[0] == '@' && text[1].isLetter()) {
359 inlineUsernameLength = 1;
360 for (auto i = inlineUsernameStart + 1; i != textLength; ++i) {
361 const auto ch = text[i];
362 if (ch.isLetterOrNumber() || ch.unicode() == '_') {
363 ++inlineUsernameLength;
364 continue;
365 } else if (!ch.isSpace()) {
366 inlineUsernameLength = 0;
367 }
368 break;
369 }
370 auto inlineUsernameEnd = inlineUsernameStart + inlineUsernameLength;
371 auto inlineUsernameEqualsText = (inlineUsernameEnd == textLength);
372 auto validInlineUsername = false;
373 if (inlineUsernameEqualsText) {
374 validInlineUsername = text.endsWith(qstr("bot"));
375 } else if (inlineUsernameEnd < textLength && inlineUsernameLength) {
376 validInlineUsername = text[inlineUsernameEnd].isSpace();
377 }
378 if (validInlineUsername) {
379 if (!full.tags.isEmpty()
380 && (full.tags.front().offset
381 < inlineUsernameStart + inlineUsernameLength)) {
382 return InlineBotQuery();
383 }
384 auto username = base::StringViewMid(text, inlineUsernameStart, inlineUsernameLength);
385 if (username != result.username) {
386 result.username = username.toString();
387 if (const auto peer = session->data().peerByUsername(result.username)) {
388 if (const auto user = peer->asUser()) {
389 result.bot = peer->asUser();
390 } else {
391 result.bot = nullptr;
392 }
393 result.lookingUpBot = false;
394 } else {
395 result.bot = nullptr;
396 result.lookingUpBot = true;
397 }
398 }
399 if (result.lookingUpBot) {
400 result.query = QString();
401 return result;
402 } else if (result.bot
403 && (!result.bot->isBot()
404 || result.bot->botInfo->inlinePlaceholder.isEmpty())) {
405 result.bot = nullptr;
406 } else {
407 result.query = inlineUsernameEqualsText
408 ? QString()
409 : text.mid(inlineUsernameEnd + 1);
410 return result;
411 }
412 } else {
413 inlineUsernameLength = 0;
414 }
415 }
416 if (inlineUsernameLength < 3) {
417 result.bot = nullptr;
418 result.username = QString();
419 }
420 result.query = QString();
421 return result;
422 }
423
ParseMentionHashtagBotCommandQuery(not_null<const Ui::InputField * > field)424 AutocompleteQuery ParseMentionHashtagBotCommandQuery(
425 not_null<const Ui::InputField*> field) {
426 auto result = AutocompleteQuery();
427
428 const auto cursor = field->textCursor();
429 if (cursor.hasSelection()) {
430 return result;
431 }
432
433 const auto position = cursor.position();
434 const auto document = field->document();
435 const auto block = document->findBlock(position);
436 for (auto item = block.begin(); !item.atEnd(); ++item) {
437 const auto fragment = item.fragment();
438 if (!fragment.isValid()) {
439 continue;
440 }
441
442 const auto fragmentPosition = fragment.position();
443 const auto fragmentEnd = fragmentPosition + fragment.length();
444 if (fragmentPosition >= position || fragmentEnd < position) {
445 continue;
446 }
447
448 const auto format = fragment.charFormat();
449 if (format.isImageFormat()) {
450 continue;
451 }
452
453 bool mentionInCommand = false;
454 const auto text = fragment.text();
455 for (auto i = position - fragmentPosition; i != 0; --i) {
456 if (text[i - 1] == '@') {
457 if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_'))) {
458 result.fromStart = (i == 1) && (fragmentPosition == 0);
459 result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
460 } else if ((position - fragmentPosition - i < 1 || text[i].isLetter()) && i > 2 && (text[i - 2].isLetterOrNumber() || text[i - 2] == '_') && !mentionInCommand) {
461 mentionInCommand = true;
462 --i;
463 continue;
464 }
465 return result;
466 } else if (text[i - 1] == '#') {
467 if (i < 2 || !(text[i - 2].isLetterOrNumber() || text[i - 2] == '_')) {
468 result.fromStart = (i == 1) && (fragmentPosition == 0);
469 result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
470 }
471 return result;
472 } else if (text[i - 1] == '/') {
473 if (i < 2) {
474 result.fromStart = (i == 1) && (fragmentPosition == 0);
475 result.query = text.mid(i - 1, position - fragmentPosition - i + 1);
476 }
477 return result;
478 }
479 if (position - fragmentPosition - i > 127 || (!mentionInCommand && (position - fragmentPosition - i > 63))) {
480 break;
481 }
482 if (!text[i - 1].isLetterOrNumber() && text[i - 1] != '_') {
483 break;
484 }
485 }
486 break;
487 }
488 return result;
489 }
490
MessageLinksParser(not_null<Ui::InputField * > field)491 MessageLinksParser::MessageLinksParser(not_null<Ui::InputField*> field)
492 : _field(field)
493 , _timer([=] { parse(); }) {
__anon394992770d02null494 _connection = QObject::connect(_field, &Ui::InputField::changed, [=] {
495 const auto length = _field->getTextWithTags().text.size();
496 const auto timeout = (std::abs(length - _lastLength) > 2)
497 ? 0
498 : kParseLinksTimeout;
499 if (!_timer.isActive() || timeout < _timer.remainingTime()) {
500 _timer.callOnce(timeout);
501 }
502 _lastLength = length;
503 });
504 _field->installEventFilter(this);
505 }
506
parseNow()507 void MessageLinksParser::parseNow() {
508 _timer.cancel();
509 parse();
510 }
511
eventFilter(QObject * object,QEvent * event)512 bool MessageLinksParser::eventFilter(QObject *object, QEvent *event) {
513 if (object == _field) {
514 if (event->type() == QEvent::KeyPress) {
515 const auto text = static_cast<QKeyEvent*>(event)->text();
516 if (!text.isEmpty() && text.size() < 3) {
517 const auto ch = text[0];
518 if (false
519 || ch == '\n'
520 || ch == '\r'
521 || ch.isSpace()
522 || ch == QChar::LineSeparator) {
523 _timer.callOnce(0);
524 }
525 }
526 } else if (event->type() == QEvent::Drop) {
527 _timer.callOnce(0);
528 }
529 }
530 return QObject::eventFilter(object, event);
531 }
532
list() const533 const rpl::variable<QStringList> &MessageLinksParser::list() const {
534 return _list;
535 }
536
parse()537 void MessageLinksParser::parse() {
538 const auto &textWithTags = _field->getTextWithTags();
539 const auto &text = textWithTags.text;
540 const auto &tags = textWithTags.tags;
541 const auto &markdownTags = _field->getMarkdownTags();
542 if (text.isEmpty()) {
543 _list = QStringList();
544 return;
545 }
546 const auto tagCanIntersectWithLink = [](const QString &tag) {
547 return (tag == Ui::InputField::kTagBold)
548 || (tag == Ui::InputField::kTagItalic)
549 || (tag == Ui::InputField::kTagUnderline)
550 || (tag == Ui::InputField::kTagStrikeOut);
551 };
552
553 auto ranges = QVector<LinkRange>();
554
555 auto tag = tags.begin();
556 const auto tagsEnd = tags.end();
557 const auto processTag = [&] {
558 Expects(tag != tagsEnd);
559
560 if (Ui::InputField::IsValidMarkdownLink(tag->id)
561 && !TextUtilities::IsMentionLink(tag->id)) {
562 ranges.push_back({ tag->offset, tag->length, tag->id });
563 }
564 ++tag;
565 };
566 const auto processTagsBefore = [&](int offset) {
567 while (tag != tagsEnd
568 && (tag->offset + tag->length <= offset
569 || tagCanIntersectWithLink(tag->id))) {
570 processTag();
571 }
572 };
573 const auto hasTagsIntersection = [&](int till) {
574 if (tag == tagsEnd || tag->offset >= till) {
575 return false;
576 }
577 while (tag != tagsEnd && tag->offset < till) {
578 processTag();
579 }
580 return true;
581 };
582
583 auto markdownTag = markdownTags.begin();
584 const auto markdownTagsEnd = markdownTags.end();
585 const auto markdownTagsAllow = [&](int from, int length) {
586 while (markdownTag != markdownTagsEnd
587 && (markdownTag->adjustedStart
588 + markdownTag->adjustedLength <= from
589 || !markdownTag->closed
590 || tagCanIntersectWithLink(markdownTag->tag))) {
591 ++markdownTag;
592 }
593 if (markdownTag == markdownTagsEnd
594 || markdownTag->adjustedStart >= from + length) {
595 return true;
596 }
597 // Ignore http-links that are completely inside some tags.
598 // This will allow sending http://test.com/__test__/test correctly.
599 return (markdownTag->adjustedStart > from)
600 || (markdownTag->adjustedStart
601 + markdownTag->adjustedLength < from + length);
602 };
603
604 const auto len = text.size();
605 const QChar *start = text.unicode(), *end = start + text.size();
606 for (auto offset = 0, matchOffset = offset; offset < len;) {
607 auto m = qthelp::RegExpDomain().match(text, matchOffset);
608 if (!m.hasMatch()) break;
609
610 auto domainOffset = m.capturedStart();
611
612 auto protocol = m.captured(1).toLower();
613 auto topDomain = m.captured(3).toLower();
614 auto isProtocolValid = protocol.isEmpty() || TextUtilities::IsValidProtocol(protocol);
615 auto isTopDomainValid = !protocol.isEmpty() || TextUtilities::IsValidTopDomain(topDomain);
616
617 if (protocol.isEmpty() && domainOffset > offset + 1 && *(start + domainOffset - 1) == QChar('@')) {
618 auto forMailName = text.mid(offset, domainOffset - offset - 1);
619 auto mMailName = TextUtilities::RegExpMailNameAtEnd().match(forMailName);
620 if (mMailName.hasMatch()) {
621 offset = matchOffset = m.capturedEnd();
622 continue;
623 }
624 }
625 if (!isProtocolValid || !isTopDomainValid) {
626 offset = matchOffset = m.capturedEnd();
627 continue;
628 }
629
630 QStack<const QChar*> parenth;
631 const QChar *domainEnd = start + m.capturedEnd(), *p = domainEnd;
632 for (; p < end; ++p) {
633 QChar ch(*p);
634 if (IsLinkEnd(ch)) {
635 break; // link finished
636 } else if (IsAlmostLinkEnd(ch)) {
637 const QChar *endTest = p + 1;
638 while (endTest < end && IsAlmostLinkEnd(*endTest)) {
639 ++endTest;
640 }
641 if (endTest >= end || IsLinkEnd(*endTest)) {
642 break; // link finished at p
643 }
644 p = endTest;
645 ch = *p;
646 }
647 if (ch == '(' || ch == '[' || ch == '{' || ch == '<') {
648 parenth.push(p);
649 } else if (ch == ')' || ch == ']' || ch == '}' || ch == '>') {
650 if (parenth.isEmpty()) break;
651 const QChar *q = parenth.pop(), open(*q);
652 if ((ch == ')' && open != '(') || (ch == ']' && open != '[') || (ch == '}' && open != '{') || (ch == '>' && open != '<')) {
653 p = q;
654 break;
655 }
656 }
657 }
658 if (p > domainEnd) { // check, that domain ended
659 if (domainEnd->unicode() != '/' && domainEnd->unicode() != '?') {
660 matchOffset = domainEnd - start;
661 continue;
662 }
663 }
664 const auto range = LinkRange {
665 int(domainOffset),
666 static_cast<int>(p - start - domainOffset),
667 QString()
668 };
669 processTagsBefore(domainOffset);
670 if (!hasTagsIntersection(range.start + range.length)) {
671 if (markdownTagsAllow(range.start, range.length)) {
672 ranges.push_back(range);
673 }
674 }
675 offset = matchOffset = p - start;
676 }
677 processTagsBefore(QFIXED_MAX);
678
679 apply(text, ranges);
680 }
681
apply(const QString & text,const QVector<LinkRange> & ranges)682 void MessageLinksParser::apply(
683 const QString &text,
684 const QVector<LinkRange> &ranges) {
685 const auto count = int(ranges.size());
686 const auto current = _list.current();
687 const auto computeLink = [&](const LinkRange &range) {
688 return range.custom.isEmpty()
689 ? base::StringViewMid(text, range.start, range.length)
690 : QStringView(range.custom);
691 };
692 const auto changed = [&] {
693 if (current.size() != count) {
694 return true;
695 }
696 for (auto i = 0; i != count; ++i) {
697 if (computeLink(ranges[i]) != current[i]) {
698 return true;
699 }
700 }
701 return false;
702 }();
703 if (!changed) {
704 return;
705 }
706 auto parsed = QStringList();
707 parsed.reserve(count);
708 for (const auto &range : ranges) {
709 parsed.push_back(computeLink(range).toString());
710 }
711 _list = std::move(parsed);
712 }
713