diff --git a/storybook/pages/StatusMessagePage.qml b/storybook/pages/StatusMessagePage.qml index 94de8c6877..985b17b85a 100644 --- a/storybook/pages/StatusMessagePage.qml +++ b/storybook/pages/StatusMessagePage.qml @@ -50,6 +50,16 @@ SplitView { isAReply: true trustIndicator: StatusContactVerificationIcons.TrustedType.None } + ListElement { + timestamp: 1667937930489 + senderId: "zqdeadbeef" + senderDisplayName: "replicator.stateofus.eth" + contentType: StatusMessage.ContentType.Text + message: "Test message with a link https://github.com/. Try to copy the link!" + isContact: true + isAReply: true + trustIndicator: StatusContactVerificationIcons.TrustedType.None + } } readonly property var colorHash: ListModel { ListElement { colorId: 13; segmentLength: 5 } diff --git a/ui/StatusQ/include/StatusQ/statussyntaxhighlighter.h b/ui/StatusQ/include/StatusQ/statussyntaxhighlighter.h index 170c026b06..c4dd68f231 100644 --- a/ui/StatusQ/include/StatusQ/statussyntaxhighlighter.h +++ b/ui/StatusQ/include/StatusQ/statussyntaxhighlighter.h @@ -3,6 +3,7 @@ #include #include #include +#include class QQuickTextDocument; class QTextCharFormat; @@ -26,9 +27,25 @@ class StatusSyntaxHighlighter : public QSyntaxHighlighter, public QQmlParserStat Q_PROPERTY(QString highlightedHyperlink READ highlightedHyperlink WRITE setHighlightedHyperlink NOTIFY highlightedHyperlinkChanged) + Q_PROPERTY(Features features READ features WRITE setFeatures NOTIFY featuresChanged) + Q_INTERFACES(QQmlParserStatus) public: + enum FeatureFlags { + None = 0, + SingleLineBold = 1 << 0, + SingleLineItalic = 1 << 1, + Code = 1 << 2, + CodeBlock = 1 << 3, + SingleLineStrikeThrough = 1 << 4, + Hyperlink = 1 << 5, + HighlightedHyperlink = 1 << 6, + All = SingleLineBold | SingleLineItalic | Code | CodeBlock | SingleLineStrikeThrough | Hyperlink | HighlightedHyperlink + }; + Q_DECLARE_FLAGS(Features, FeatureFlags) + Q_FLAG(Features) + explicit StatusSyntaxHighlighter(QObject* parent = nullptr); QQuickTextDocument* quickTextDocument() const; @@ -47,6 +64,7 @@ signals: void hyperlinkHoverColorChanged(); void hyperlinksChanged(); void highlightedHyperlinkChanged(); + void featuresChanged(); private: QQuickTextDocument* m_quicktextdocument{nullptr}; @@ -80,8 +98,15 @@ private: QStringList getPossibleUrlFormats(const QUrl& url) const; QRegularExpression buildHyperlinkRegex(QStringList hyperlinks) const; + Features features() const; + void setFeatures(Features features); + + void buildRules(); + int findRuleIndex(FeatureFlags flag) const; + struct HighlightingRule { + int id; QRegularExpression pattern; QRegularExpression::MatchType matchType{QRegularExpression::PartialPreferCompleteMatch}; QTextCharFormat format; @@ -94,4 +119,8 @@ private: QTextCharFormat singleLineStrikeThroughFormat; QTextCharFormat hyperlinkFormat; QTextCharFormat highlightedHyperlinkFormat; + + Features m_features{All}; }; + +Q_DECLARE_OPERATORS_FOR_FLAGS(StatusSyntaxHighlighter::Features) \ No newline at end of file diff --git a/ui/StatusQ/src/StatusQ/Components/private/statusMessage/StatusTextMessage.qml b/ui/StatusQ/src/StatusQ/Components/private/statusMessage/StatusTextMessage.qml index a6fd0993b1..6a76561ff4 100644 --- a/ui/StatusQ/src/StatusQ/Components/private/statusMessage/StatusTextMessage.qml +++ b/ui/StatusQ/src/StatusQ/Components/private/statusMessage/StatusTextMessage.qml @@ -1,8 +1,9 @@ -import QtQuick 2.13 +import QtQuick 2.15 import QtGraphicalEffects 1.0 import StatusQ.Components 0.1 import StatusQ.Controls 0.1 +import StatusQ 0.1 import StatusQ.Core.Theme 0.1 import StatusQ.Core.Utils 0.1 @@ -50,7 +51,7 @@ Item { const editedMessage = formattedMessage.slice(0, index) + ` ` + qsTr("(edited)") + `` + formattedMessage.slice(index); - return Utils.getMessageWithStyle(Emoji.parse(editedMessage), d.hoveredLink) + return Utils.getMessageWithStyle(Emoji.parse(editedMessage)) } if (root.convertToSingleLine || isQuote) @@ -66,7 +67,7 @@ Item { // short return not to add styling when no html return formattedMessage - return Utils.getMessageWithStyle(formattedMessage, d.hoveredLink) + return Utils.getMessageWithStyle(formattedMessage) } } @@ -106,8 +107,14 @@ Item { } } - // Horizontal crop mask + StatusSyntaxHighlighter { + quickTextDocument: chatText.textDocument + hyperlinkHoverColor: Theme.palette.primaryColor3 + highlightedHyperlink: d.hoveredLink + features: StatusSyntaxHighlighter.HighlightedHyperlink + } + // Horizontal crop mask Loader { id: horizontalClipMask anchors.fill: chatText diff --git a/ui/StatusQ/src/statussyntaxhighlighter.cpp b/ui/StatusQ/src/statussyntaxhighlighter.cpp index e0e68358a0..bf6579fa8c 100644 --- a/ui/StatusQ/src/statussyntaxhighlighter.cpp +++ b/ui/StatusQ/src/statussyntaxhighlighter.cpp @@ -9,86 +9,133 @@ StatusSyntaxHighlighter::StatusSyntaxHighlighter(QObject* parent) void StatusSyntaxHighlighter::componentComplete() { - HighlightingRule rule; + buildRules(); - //BOLD - singlelineBoldFormat.setFontWeight(QFont::Bold); - rule.pattern = QRegularExpression(QStringLiteral("(\\*\\*(.*?)\\*\\*)|(\\_\\_(.*?)\\_\\_)")); - rule.format = singlelineBoldFormat; - highlightingRules.append(rule); - //BOLD + connect(this, &StatusSyntaxHighlighter::hyperlinksChanged, this, [this](){ + const auto index = findRuleIndex(StatusSyntaxHighlighter::Hyperlink); + if (index == -1) return; - //ITALIC - singleLineItalicFormat.setFontItalic(true); - rule.pattern = QRegularExpression(QStringLiteral("(\\*(.*?)\\*)|(\\_(.*?)\\_)")); - rule.format = singleLineItalicFormat; - highlightingRules.append(rule); - //ITALIC - - //STRIKETHROUGH - singleLineStrikeThroughFormat.setFontStrikeOut(true); - rule.pattern = QRegularExpression(QStringLiteral("\\~\\~(.*?)\\~\\~")); - rule.format = singleLineStrikeThroughFormat; - highlightingRules.append(rule); - //STRIKETHROUGH - - //CODE (`foo`) - codeFormat.setFontFamily(QStringLiteral("Roboto Mono")); - codeFormat.setBackground(m_codeBackgroundColor); - codeFormat.setForeground(m_codeForegroundColor); - rule.pattern = QRegularExpression(QStringLiteral("\\`{1}(.+)\\`{1}"), - // to not match single backtick pair inside a triple backtick block below - QRegularExpression::InvertedGreedinessOption); - rule.format = codeFormat; - highlightingRules.append(rule); - //CODE - - //CODEBLOCK (```\nfoo\nbar```) - rule.pattern = QRegularExpression(QStringLiteral("\\`{3}(.+)\\`{3}")); - rule.format = codeFormat; - highlightingRules.append(rule); - //CODEBLOCK - - //HYPERLINKS - //QRegularExpression to match any hyperlink in m_hyperlinks - hyperlinkFormat.setForeground(m_hyperlinkColor); - rule.pattern = hyperlinksRegularExpression(); - rule.format = hyperlinkFormat; - rule.matchType = QRegularExpression::NormalMatch; - highlightingRules.append(rule); - - const int hyperlinksRuleIndex = highlightingRules.size() - 1; - - //HIGHLIGHTED - highlightedHyperlinkFormat.setForeground(m_hyperlinkColor); - highlightedHyperlinkFormat.setBackground(m_hyperlinkHoverColor); - rule.pattern = highlightedHyperlinkRegularExpression(); - rule.format = highlightedHyperlinkFormat; - rule.matchType = QRegularExpression::NormalMatch; - highlightingRules.append(rule); - - const int highlightedHyperlinkRuleIndex = highlightingRules.size() - 1; - - connect(this, &StatusSyntaxHighlighter::hyperlinksChanged, this, [hyperlinksRuleIndex, this](){ - highlightingRules[hyperlinksRuleIndex].pattern = hyperlinksRegularExpression(); + highlightingRules[index].pattern = hyperlinksRegularExpression(); rehighlight(); }); - connect(this, &StatusSyntaxHighlighter::hyperlinkColorChanged, this, [hyperlinksRuleIndex, this](){ + connect(this, &StatusSyntaxHighlighter::hyperlinkColorChanged, this, [this](){ + const auto index = findRuleIndex(StatusSyntaxHighlighter::Hyperlink); + if (index == -1) return; + hyperlinkFormat.setForeground(m_hyperlinkColor); highlightedHyperlinkFormat.setForeground(m_hyperlinkColor); - highlightingRules[hyperlinksRuleIndex].format = hyperlinkFormat; + highlightingRules[index].format = hyperlinkFormat; rehighlight(); }); - connect(this, &StatusSyntaxHighlighter::highlightedHyperlinkChanged, this, [highlightedHyperlinkRuleIndex, this](){ - highlightingRules[highlightedHyperlinkRuleIndex].pattern = highlightedHyperlinkRegularExpression(); + connect(this, &StatusSyntaxHighlighter::highlightedHyperlinkChanged, this, [this](){ + const auto index = findRuleIndex(StatusSyntaxHighlighter::HighlightedHyperlink); + if (index == -1) return; + + highlightingRules[index].pattern = highlightedHyperlinkRegularExpression(); rehighlight(); }); - connect(this, &StatusSyntaxHighlighter::hyperlinkHoverColorChanged, this, [highlightedHyperlinkRuleIndex, this](){ + connect(this, &StatusSyntaxHighlighter::hyperlinkHoverColorChanged, this, [this](){ + const auto index = findRuleIndex(StatusSyntaxHighlighter::HighlightedHyperlink); + if (index == -1) return; + highlightedHyperlinkFormat.setBackground(m_hyperlinkHoverColor); - highlightingRules[highlightedHyperlinkRuleIndex].format = highlightedHyperlinkFormat; + highlightingRules[index].format = highlightedHyperlinkFormat; rehighlight(); }); + + connect(this, &StatusSyntaxHighlighter::featuresChanged, this, [this](){ + buildRules(); + rehighlight(); + }); +} + +void StatusSyntaxHighlighter::buildRules() +{ + HighlightingRule rule; + highlightingRules.clear(); + + if (m_features & StatusSyntaxHighlighter::SingleLineBold) + { + //BOLD + singlelineBoldFormat.setFontWeight(QFont::Bold); + rule.id = StatusSyntaxHighlighter::SingleLineBold; + rule.pattern = QRegularExpression(QStringLiteral("(\\*\\*(.*?)\\*\\*)|(\\_\\_(.*?)\\_\\_)")); + rule.format = singlelineBoldFormat; + highlightingRules.append(rule); + //BOLD + } + + if (m_features & StatusSyntaxHighlighter::SingleLineItalic) + { + //ITALIC + singleLineItalicFormat.setFontItalic(true); + rule.id = StatusSyntaxHighlighter::SingleLineItalic; + rule.pattern = QRegularExpression(QStringLiteral("(\\*(.*?)\\*)|(\\_(.*?)\\_)")); + rule.format = singleLineItalicFormat; + highlightingRules.append(rule); + //ITALIC + } + + if (m_features & StatusSyntaxHighlighter::SingleLineStrikeThrough) + { + //STRIKETHROUGH + singleLineStrikeThroughFormat.setFontStrikeOut(true); + rule.id = StatusSyntaxHighlighter::SingleLineStrikeThrough; + rule.pattern = QRegularExpression(QStringLiteral("\\~\\~(.*?)\\~\\~")); + rule.format = singleLineStrikeThroughFormat; + highlightingRules.append(rule); + //STRIKETHROUGH + } + + if (m_features & StatusSyntaxHighlighter::Code) + { + //CODE (`foo`) + codeFormat.setFontFamily(QStringLiteral("Roboto Mono")); + codeFormat.setBackground(m_codeBackgroundColor); + codeFormat.setForeground(m_codeForegroundColor); + rule.id = StatusSyntaxHighlighter::Code; + rule.pattern = QRegularExpression(QStringLiteral("\\`{1}(.+)\\`{1}"), + // to not match single backtick pair inside a triple backtick block below + QRegularExpression::InvertedGreedinessOption); + rule.format = codeFormat; + highlightingRules.append(rule); + //CODE + } + + if (m_features & StatusSyntaxHighlighter::CodeBlock) + { + //CODEBLOCK (```\nfoo\nbar```) + rule.id = StatusSyntaxHighlighter::CodeBlock; + rule.pattern = QRegularExpression(QStringLiteral("\\`{3}(.+)\\`{3}")); + rule.format = codeFormat; + highlightingRules.append(rule); + //CODEBLOCK + } + + if (m_features & StatusSyntaxHighlighter::Hyperlink) + { + //HYPERLINKS + hyperlinkFormat.setForeground(m_hyperlinkColor); + rule.id = StatusSyntaxHighlighter::Hyperlink; + rule.pattern = hyperlinksRegularExpression(); + rule.format = hyperlinkFormat; + rule.matchType = QRegularExpression::NormalMatch; + highlightingRules.append(rule); + //HYPERLINKS + } + + if (m_features & StatusSyntaxHighlighter::HighlightedHyperlink) + { + //HIGHLIGHTED + highlightedHyperlinkFormat.setForeground(m_hyperlinkColor); + highlightedHyperlinkFormat.setBackground(m_hyperlinkHoverColor); + rule.id = StatusSyntaxHighlighter::HighlightedHyperlink; + rule.pattern = highlightedHyperlinkRegularExpression(); + rule.format = highlightedHyperlinkFormat; + rule.matchType = QRegularExpression::NormalMatch; + highlightingRules.append(rule); + } } void StatusSyntaxHighlighter::highlightBlock(const QString& text) @@ -217,6 +264,7 @@ QStringList StatusSyntaxHighlighter::getPossibleUrlFormats(const QUrl& url) cons result.append(QRegularExpression::escape(url.toString())); result.append(QRegularExpression::escape(url.toString(QUrl::EncodeUnicode))); result.append(QRegularExpression::escape(url.toString(QUrl::FullyEncoded))); + return result; } @@ -225,9 +273,32 @@ QRegularExpression StatusSyntaxHighlighter::buildHyperlinkRegex(QStringList hype hyperlinks.removeAll(QString()); if(hyperlinks.isEmpty()) - return QRegularExpression("(?!)"); - QString matchHyperlinks = QStringLiteral("(?:^|(?<=\\s))(") + hyperlinks.join("|") + QStringLiteral(")(?:(?=\\s)|$)"); + return QRegularExpression(QStringLiteral("(?!)")); + QString matchHyperlinks = QStringLiteral("(?:^|(?<=\\s))(") + hyperlinks.join('|') + QStringLiteral(")(?:(?=\\s|[[:punct:]])|$)"); auto regex = QRegularExpression(matchHyperlinks, QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption | QRegularExpression::MultilineOption); regex.optimize(); return regex; +} + +StatusSyntaxHighlighter::Features StatusSyntaxHighlighter::features() const +{ + return m_features; +} + +void StatusSyntaxHighlighter::setFeatures(Features features) +{ + if(features == m_features) return; + m_features = features; + emit featuresChanged(); +} + +int StatusSyntaxHighlighter::findRuleIndex(FeatureFlags flag) const +{ + for (int i = 0; i < highlightingRules.size(); ++i) + { + if (highlightingRules[i].id == flag) + return i; + } + + return -1; } \ No newline at end of file