diff --git a/Linphone/core/setting/SettingsCore.cpp b/Linphone/core/setting/SettingsCore.cpp index f02743db6..99aaa89cf 100644 --- a/Linphone/core/setting/SettingsCore.cpp +++ b/Linphone/core/setting/SettingsCore.cpp @@ -93,6 +93,9 @@ SettingsCore::SettingsCore(QObject *parent) : QObject(parent) { mShowAccountDevices = (accountDomain == mDefaultDomain); } + // Chat + mEmojiFont = settingsModel->getEmojiFont(); + // Ui INIT_CORE_MEMBER(DisableChatFeature, settingsModel) INIT_CORE_MEMBER(DisableMeetingsFeature, settingsModel) diff --git a/Linphone/core/setting/SettingsCore.hpp b/Linphone/core/setting/SettingsCore.hpp index bc57a0d74..1f49823a8 100644 --- a/Linphone/core/setting/SettingsCore.hpp +++ b/Linphone/core/setting/SettingsCore.hpp @@ -233,6 +233,7 @@ public: DECLARE_CORE_GETSET_MEMBER(QString, commandLine, CommandLine) DECLARE_CORE_GETSET_MEMBER(bool, disableCallForward, DisableCallForward) DECLARE_CORE_GETSET_MEMBER(QString, callForwardToAddress, CallForwardToAddress) + DECLARE_CORE_GET_CONSTANT(QFont, emojiFont, EmojiFont) signals: diff --git a/Linphone/model/setting/SettingsModel.cpp b/Linphone/model/setting/SettingsModel.cpp index c7e65c272..e201772ab 100644 --- a/Linphone/model/setting/SettingsModel.cpp +++ b/Linphone/model/setting/SettingsModel.cpp @@ -729,6 +729,17 @@ void SettingsModel::setCallForwardToAddress(QString data) { emit(callForwardToAddressChanged(data)); } +QFont SettingsModel::getEmojiFont() const { + QString family = Utils::coreStringToAppString(mConfig->getString( + UiSection, "emoji_font", Utils::appStringToCoreString(QFont(Constants::DefaultEmojiFont).family()))); + int pointSize = getEmojiFontSize(); + return QFont(family, pointSize); +} + +int SettingsModel::getEmojiFontSize() const { + return mConfig->getInt(UiSection, "emoji_font_size", Constants::DefaultEmojiFontPointSize); +} + // clang-format off void SettingsModel::notifyConfigReady(){ DEFINE_NOTIFY_CONFIG_READY(disableChatFeature, DisableChatFeature) diff --git a/Linphone/model/setting/SettingsModel.hpp b/Linphone/model/setting/SettingsModel.hpp index 5cbdcdeff..8fb1ab208 100644 --- a/Linphone/model/setting/SettingsModel.hpp +++ b/Linphone/model/setting/SettingsModel.hpp @@ -157,6 +157,9 @@ public: static bool clearLocalLdapFriendsUponStartup(const std::shared_ptr &config); + QFont getEmojiFont() const; + int getEmojiFontSize() const; + // UI DECLARE_GETSET(bool, disableChatFeature, DisableChatFeature) DECLARE_GETSET(bool, disableMeetingsFeature, DisableMeetingsFeature) diff --git a/Linphone/tool/AbstractObject.hpp b/Linphone/tool/AbstractObject.hpp index abd112694..0c4104da4 100644 --- a/Linphone/tool/AbstractObject.hpp +++ b/Linphone/tool/AbstractObject.hpp @@ -91,6 +91,11 @@ public: Q_SIGNAL void x##Changed(); \ type m##X; +#define DECLARE_CORE_GET_CONSTANT(type, x, X) \ + Q_PROPERTY(type x MEMBER m##X CONSTANT) \ + type m##X; \ + type get##X() const { return m##X;} + #define DECLARE_GETSET(type, x, X) \ type get##X() const; \ void set##X(type data); \ diff --git a/Linphone/tool/CMakeLists.txt b/Linphone/tool/CMakeLists.txt index 686233d3b..a11b861ef 100644 --- a/Linphone/tool/CMakeLists.txt +++ b/Linphone/tool/CMakeLists.txt @@ -2,6 +2,7 @@ list(APPEND _LINPHONEAPP_SOURCES tool/Constants.cpp tool/EnumsToString.cpp tool/Utils.cpp + tool/UriTools.cpp tool/LinphoneEnums.cpp tool/thread/SafeSharedPointer.hpp diff --git a/Linphone/tool/UriTools.cpp b/Linphone/tool/UriTools.cpp new file mode 100644 index 000000000..ccfdd54be --- /dev/null +++ b/Linphone/tool/UriTools.cpp @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +// ============================================================================= +// Library to deal with IRI and URI. +// See: +// IRI : https://tools.ietf.org/html/rfc3987 +// URI : https://tools.ietf.org/html/rfc3986 +// ============================================================================= + +#include "UriTools.hpp" + +static UriTools gUriTools; + +UriTools::UriTools() { + initRegularExpressions(); +} + +QVector> UriTools::parseIri(const QString &text) { + return parse(text, gUriTools.mIriRegularExpression); +} + +QVector> UriTools::parseUri(const QString &text) { + return parse(text, gUriTools.mUriRegularExpression); +} + +// Parse a text and return all lines where regex is matched or not +QVector> UriTools::parse(const QString &text, const QRegularExpression regex) { + QVector> results; + int currentIndex = 0; + auto match = regex.match(text); + + for (int i = 0; i <= match.lastCapturedIndex(); ++i) { + int startIndex = match.capturedStart(i); + if (currentIndex != startIndex) { + results.push_back({false, text.mid(currentIndex, startIndex - currentIndex)}); + } + results.push_back({true, match.captured(i)}); + currentIndex = startIndex; + } + + if (results.size() == 0) results.push_back({false, text}); + else { + currentIndex += results.back().second.length(); + if (currentIndex < text.size()) results.append(parse(text.mid(currentIndex), regex)); + } + return results; +} + +void UriTools::initRegularExpressions() { + // Level 0. -------------------------------------------------------------------- + QString URI_DEC_OCTET = + QString("(?:") + "25[0-5]" + "|" + "2[0-4]\\d" + "|" + "1\\d{2}" + "|" + "[1-9]\\d" + "|" + "\\d" + ")"; + + QString URI_H16 = "[0-9A-Fa-f]{1,4}"; + QString URI_PCT_ENCODED = "%[A-Fa-f\\d]{2}"; + QString URI_PORT = "\\d*"; + QString URI_SCHEME = "[a-zA-Z][\\w+\\.\\-]*"; + QString URI_SUB_DELIMS = "[!$&\"()*+,;=]"; + QString URI_UNRESERVED = "[\\w\\._~\\-]"; + QString IRI_UCS_CHAR = + QString("(?:") + "[\\x{00A0}-\\x{D7FF}]" + "|" + "[\\x{F900}-\\x{FDCF}]" + "|" + "[\\x{FDF0}-\\x{FFEF}]" + "|" + + "[\\x{10000}-\\x{1FFFD}]" + "|" + "[\\x{20000}-\\x{2FFFD}]" + "|" + "[\\x{30000}-\\x{3FFFD}]" + + //"|" + "[\\x{D800\\x{DC00}-\\x{D83F\\x{DFFD}]" + "|" + "[\\x{D840\\x{DC00}-\\x{D87F\\x{DFFD}]" + "|" + + //"[\\x{D880\\x{DC00}-\\x{D8BF\\x{DFFD}]" + + + "|" + "[\\x{40000}-\\x{4FFFD}]" + "|" + "[\\x{50000}-\\x{5FFFD}]" + "|" + "[\\x{60000}-\\x{6FFFD}]" + + //"|" + "[\\x{D8C0\\x{DC00}-\\x{D8FF\\x{DFFD}]" + "|" + "[\\x{D900\\x{DC00}-\\x{D93F\\x{DFFD}]" + "|" + + //"[\\x{D940\\x{DC00}-\\x{D97F\\x{DFFD}]" + + + "|" + "[\\x{70000}-\\x{7FFFD}]" + "|" + "[\\x{80000}-\\x{8FFFD}]" + "|" + "[\\x{90000}-\\x{9FFFD}]" + + //"|" + "[\\x{D980\\x{DC00}-\\x{D9BF\\x{DFFD}]" + "|" + "[\\x{D9C0\\x{DC00}-\\x{D9FF\\x{DFFD}]" + "|" + + //"[\\x{DA00\\x{DC00}-\\x{DA3F\\x{DFFD}]" + + + "|" + "[\\x{A0000}-\\x{AFFFD}]" + "|" + "[\\x{B0000}-\\x{BFFFD}]" + "|" + "[\\x{C0000}-\\x{CFFFD}]" + + //"|" + "[\\x{DA40\\x{DC00}-\\x{DA7F\\x{DFFD}]" + "|" + "[\\x{DA80\\x{DC00}-\\x{DABF\\x{DFFD}]" + "|" + + //"[\\x{DAC0\\x{DC00}-\\x{DAFF\\x{DFFD}]" + + + "|" + "[\\x{D0000}-\\x{DFFFD}]" + "|" + "[\\x{E1000}-\\x{EFFFD}]" + + //"|" + "[\\x{DB00\\x{DC00}-\\x{DB3F\\x{DFFD}]" + "|" + "[\\x{DB44\\x{DC00}-\\x{DB7F\\x{DFFD}]" + + ")"; + + QString IRI_PRIVATE = + QString("(?:") + "[\\x{E000}-\\x{F8FF}]" + "|" + "[\\x{F0000}-\\x{FFFFD}]" + "|" + "[\\x{100000}-\\x{10FFFD}]" + + //"|" + "[\\x{DBC0\\x{DC00}-\\x{DBFF\\x{DFFD}]" + "|" + "[\\x{DBC0\\x{DC00}-\\x{DBFF\\x{DFFD}]" + + ")"; + + // Level 1. -------------------------------------------------------------------- + QString URI_IPV_FUTURE = QString("v[0-9A-Fa-f]+\\.") + "(?:" + URI_UNRESERVED + URI_SUB_DELIMS + ":" + ")"; + + QString IRI_UNRESERVED = QString("(?:") + "[\\w\\._~\\-]" + "|" + IRI_UCS_CHAR + ")"; + + QString URI_IPV4_ADDRESS = URI_DEC_OCTET + "\\." + URI_DEC_OCTET + "\\." + URI_DEC_OCTET + "\\." + URI_DEC_OCTET; + + QString URI_PCHAR = "(?:" + URI_UNRESERVED + "|" + URI_PCT_ENCODED + "|" + URI_SUB_DELIMS + "|" + "[:@]" + ")"; + + QString URI_REG_NAME = "(?:" + URI_UNRESERVED + "|" + URI_PCT_ENCODED + "|" + URI_SUB_DELIMS + ")*"; + + QString URI_USERINFO = "(?:" + URI_UNRESERVED + "|" + URI_PCT_ENCODED + "|" + URI_SUB_DELIMS + "|" + ":" + ")*"; + + // Level 2. -------------------------------------------------------------------- + + QString URI_FRAGMENT = "(?:" + URI_PCHAR + "|" + "[/?]" + ")*"; + + QString URI_LS32 = "(?:" + URI_H16 + ":" + URI_H16 + "|" + URI_IPV4_ADDRESS + ")"; + + QString URI_QUERY = "(?:" + URI_PCHAR + "|" + "[/?]" + ")*"; + + QString URI_SEGMENT = URI_PCHAR + "*"; + + QString URI_SEGMENT_NZ = URI_PCHAR + "+"; + + QString IRI_PCHAR = "(?:" + IRI_UNRESERVED + "|" + URI_PCT_ENCODED + "|" + URI_SUB_DELIMS + "|" + "[:@]" + ")"; + + QString IRI_REG_NAME = "(?:" + IRI_UNRESERVED + "|" + URI_PCT_ENCODED + "|" + URI_SUB_DELIMS + ")*"; + + QString IRI_USERINFO = "(?:" + IRI_UNRESERVED + "|" + URI_PCT_ENCODED + "|" + URI_SUB_DELIMS + "|" + ":" + ")*"; + + // Level 3. -------------------------------------------------------------------- + + QString URI_IPV6_ADDRESS = QString("(?:") + "(?:" + URI_H16 + ":){6}" + URI_LS32 + "|" + "::(?:" + URI_H16 + + ":){5}" + URI_LS32 + "|" + "\\[" + URI_H16 + "\\]::(?:" + URI_H16 + ":){4}" + URI_LS32 + + "|" + "\\[" + "(?:" + URI_H16 + ":)?" + URI_H16 + "\\]::(?:" + URI_H16 + ":){3}" + + URI_LS32 + "|" + "\\[" + "(?:" + URI_H16 + ":){0,2}" + URI_H16 + "\\]::(?:" + URI_H16 + + ":){2}" + URI_LS32 + "|" + "\\[" + "(?:" + URI_H16 + ":){0,3}" + URI_H16 + + "\\]::" + URI_H16 + ":" + URI_LS32 + "|" + "\\[" + "(?:" + URI_H16 + ":){0,4}" + + URI_H16 + "\\]::" + URI_LS32 + "|" + "\\[" + "(?:" + URI_H16 + ":){0,5}" + URI_H16 + + "\\]::" + URI_H16 + "|" + "\\[" + "(?:" + URI_H16 + ":){0,6}" + URI_H16 + "\\]::" + ")"; + + QString URI_PATH_ABEMPTY = QString("(?:") + "/" + URI_SEGMENT + ")*"; + + QString URI_PATH_ABSOLUTE = QString("/") + "(?:" + URI_SEGMENT_NZ + "(?:" + "/" + URI_SEGMENT + ")*" + ")?"; + + QString URI_PATH_ROOTLESS = URI_SEGMENT_NZ + "(?:" + "/" + URI_SEGMENT + ")*"; + + QString IRI_FRAGMENT = "(?:" + IRI_PCHAR + "|" + "[/?]" + ")*"; + + QString IRI_QUERY = "(?:" + IRI_PCHAR + "|" + IRI_PRIVATE + "|" + "[/?]" + ")*"; + + QString IRI_SEGMENT = IRI_PCHAR + "*"; + QString IRI_SEGMENT_NZ = IRI_PCHAR + "+"; + + // Level 4. -------------------------------------------------------------------- + + QString URI_IP_LITERAL = QString("\\[") + "(?:" + URI_IPV6_ADDRESS + "|" + URI_IPV_FUTURE + ")" + "\\]"; + + QString IRI_PATH_ABEMPTY = QString("(?:") + "/" + IRI_SEGMENT + ")*"; + + QString IRI_PATH_ABSOLUTE = QString("/") + "(?:" + IRI_SEGMENT_NZ + "(?:" + "/" + IRI_SEGMENT + ")*" + ")?"; + + QString IRI_PATH_ROOTLESS = IRI_SEGMENT_NZ + "(?:" + "/" + IRI_SEGMENT + ")*"; + + // Level 5. -------------------------------------------------------------------- + + QString URI_HOST = "(?:" + URI_REG_NAME + "|" + URI_IPV4_ADDRESS + "|" + URI_IP_LITERAL + ")"; + + QString IRI_HOST = "(?:" + IRI_REG_NAME + "|" + URI_IPV4_ADDRESS + "|" + URI_IP_LITERAL + ")"; + + // Level 6. -------------------------------------------------------------------- + + QString URI_AUTHORITY = "(?:" + URI_USERINFO + "@" + ")?" + URI_HOST + "(?:" + ":" + URI_PORT + ")?"; + + QString IRI_AUTHORITY = "(?:" + IRI_USERINFO + "@" + ")?" + IRI_HOST + "(?:" + ":" + URI_PORT + ")?"; + + // Level 7. -------------------------------------------------------------------- + + // `path-empty` not used. + QString URI_HIER_PART = QString("(?:") + "//" + URI_AUTHORITY + URI_PATH_ABEMPTY + "|" + URI_PATH_ABSOLUTE + "|" + + URI_PATH_ROOTLESS + ")"; + QString IRI_HIER_PART = QString("(?:") + "//" + IRI_AUTHORITY + IRI_PATH_ABEMPTY + "|" + IRI_PATH_ABSOLUTE + "|" + + IRI_PATH_ROOTLESS + ")"; + + // Level 8. -------------------------------------------------------------------- + + // Regex to match URI. It respects the RFC 3986. + QString URI = "(?:" + URI_SCHEME + ":" + "|" + "www\\." + ")" + URI_HIER_PART + "(?:" + "\\?" + URI_QUERY + ")?" + + "(?:" + "#" + URI_FRAGMENT + ")?"; + + // Regex to match URI. It respects the RFC 3987. + QString IRI = "(?:" + URI_SCHEME + ":" + "|" + "www\\." + ")" + IRI_HIER_PART + "(?:" + "\\?" + IRI_QUERY + ")?" + + "(?:" + "#" + IRI_FRAGMENT + ")?"; + + mIriRegularExpression = QRegularExpression(IRI, QRegularExpression::CaseInsensitiveOption | + QRegularExpression::UseUnicodePropertiesOption); + mUriRegularExpression = QRegularExpression(URI, QRegularExpression::CaseInsensitiveOption | + QRegularExpression::UseUnicodePropertiesOption); +} diff --git a/Linphone/tool/UriTools.hpp b/Linphone/tool/UriTools.hpp new file mode 100644 index 000000000..2fb29fcf3 --- /dev/null +++ b/Linphone/tool/UriTools.hpp @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-desktop + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +// ============================================================================= +// Library to deal with IRI and URI. +// See: +// IRI : https://tools.ietf.org/html/rfc3987 +// NOTE : Unicodes after \uFFFF are not supported by the QML RegExp (or the right syntax has not been found) : "Invalid +// regular expression" (even with surrogate pairs). Parts have been commented out for latter use. +// URI : https://tools.ietf.org/html/rfc3986 +// ============================================================================= + +#ifndef URI_TOOLS_H +#define URI_TOOLS_H + +#include +#include +#include +#include + +class UriTools { +public: + UriTools(); + bool mSupportUrl = true; + + static QVector> parseIri(const QString &text); + static QVector> parseUri(const QString &text); + static QRegularExpression getRegularExpression(); + +private: + void initRegularExpressions(); + static QVector> parse(const QString &text, const QRegularExpression regex); + + QRegularExpression mIriRegularExpression; // https://tools.ietf.org/html/rfc3987 + QRegularExpression mUriRegularExpression; // https://tools.ietf.org/html/rfc3986 +}; + +#endif \ No newline at end of file diff --git a/Linphone/tool/Utils.cpp b/Linphone/tool/Utils.cpp index b91406e7e..b9044018f 100644 --- a/Linphone/tool/Utils.cpp +++ b/Linphone/tool/Utils.cpp @@ -20,6 +20,7 @@ #include "Utils.hpp" +#include "UriTools.hpp" #include "core/App.hpp" #include "core/call/CallGui.hpp" #include "core/chat/ChatCore.hpp" @@ -1749,3 +1750,94 @@ QString Utils::getPresenceStatus(LinphoneEnums::Presence presence) { } return presenceStatus; } + +QString Utils::encodeTextToQmlRichFormat(const QString &text, const QVariantMap &options) { + /*QString images; + QStringList imageFormat; + for(auto format : QImageReader::supportedImageFormats()) + imageFormat.append(QString::fromLatin1(format).toUpper()); + */ + QStringList formattedText; + bool lastWasUrl = false; + + if (options.contains("noLink") && options["noLink"].toBool()) { + formattedText.append(encodeEmojiToQmlRichFormat(text)); + } else { + auto primaryColor = getDefaultStyleColor("info_500_main"); + auto iriParsed = UriTools::parseIri(text); + + for (int i = 0; i < iriParsed.size(); ++i) { + QString iri = iriParsed[i] + .second.replace('&', "&") + .replace('<', "\u2063<") + .replace('>', "\u2063>") + .replace('"', """) + .replace('\'', "'"); + if (!iriParsed[i].first) { + if (lastWasUrl) { + lastWasUrl = false; + if (iri.front() != ' ') iri.push_front(' '); + } + formattedText.append(encodeEmojiToQmlRichFormat(iri)); + } else { + QString uri = + iriParsed[i].second.left(3) == "www" ? "http://" + iriParsed[i].second : iriParsed[i].second; + /* TODO : preview from link + int extIndex = iriParsed[i].second.lastIndexOf('.'); + QString ext; + if( extIndex >= 0) + ext = iriParsed[i].second.mid(extIndex+1).toUpper(); + if(imageFormat.contains(ext.toLatin1())){// imagesHeight is not used because of bugs on display (blank + image if set without width) images += ""+uri+""; + }else{ + */ + formattedText.append("" + iri + + ""); + lastWasUrl = true; + /*}*/ + } + } + } + if (lastWasUrl && formattedText.last().back() != ' ') { + formattedText.push_back(" "); + } + return "

" + formattedText.join("") + "

"; +} + +QString Utils::encodeEmojiToQmlRichFormat(const QString &body) { + QString fmtBody = ""; + QVector utf32_string = body.toUcs4(); + + bool insideFontBlock = false; + for (auto &code : utf32_string) { + if (Utils::codepointIsEmoji(code)) { + if (!insideFontBlock) { + auto font = App::getInstance()->getSettings()->getEmojiFont().family(); + fmtBody += QString(""); + insideFontBlock = true; + } + } else { + if (insideFontBlock) { + fmtBody += ""; + insideFontBlock = false; + } + } + fmtBody += QString::fromUcs4(reinterpret_cast(&code), 1); + } + if (insideFontBlock) { + fmtBody += ""; + } + return fmtBody; +} + +bool Utils::codepointIsEmoji(uint code) { + return (code >= 0x2600 && code <= 0x27bf) || (code >= 0x2b00 && code <= 0x2bff) || + (code >= 0x1f000 && code <= 0x1faff) || code == 0x200d || code == 0xfe0f; +} diff --git a/Linphone/tool/Utils.hpp b/Linphone/tool/Utils.hpp index ecca7f307..e799aa78b 100644 --- a/Linphone/tool/Utils.hpp +++ b/Linphone/tool/Utils.hpp @@ -149,6 +149,11 @@ public: Q_INVOKABLE static VariantObject *getCurrentCallChat(CallGui *call); Q_INVOKABLE static VariantObject *getChatForAddress(QString address); Q_INVOKABLE static bool isEmptyMessage(QString message); + Q_INVOKABLE static QString encodeTextToQmlRichFormat(const QString &text, + const QVariantMap &options = QVariantMap()); + Q_INVOKABLE static QString encodeEmojiToQmlRichFormat(const QString &body); + static bool codepointIsEmoji(uint code); + // QDir findDirectoryByName(QString startPath, QString name); static QString getApplicationProduct(); diff --git a/Linphone/view/Control/Display/Chat/ChatMessage.qml b/Linphone/view/Control/Display/Chat/ChatMessage.qml index 8a23c51ea..d753af03c 100644 --- a/Linphone/view/Control/Display/Chat/ChatMessage.qml +++ b/Linphone/view/Control/Display/Chat/ChatMessage.qml @@ -20,7 +20,9 @@ Control.Control { property bool isRemoteMessage: chatMessage? chatMessage.core.isRemoteMessage : false property bool isFromChatGroup: chatMessage? chatMessage.core.isFromChatGroup : false property var msgState: chatMessage ? chatMessage.core.messageState : LinphoneEnums.ChatMessageState.StateIdle + property string processedText: UtilsCpp.encodeTextToQmlRichFormat(modelData.core.text) hoverEnabled: true + property bool linkHovered: false signal messageDeletionRequested() @@ -75,6 +77,7 @@ Control.Control { optionsMenu.open() } } + cursorShape: mainItem.linkHovered ? Qt.PointingHandCursor : Qt.IBeamCursor } background: Item { @@ -107,18 +110,30 @@ Control.Control { visible: mainItem.imgUrl != undefined id: contentimage } - Text { - visible: modelData.core.text != undefined - text: modelData.core.text - Layout.fillWidth: true - Layout.fillHeight: true - horizontalAlignment: modelData.core.isRemoteMessage ? Text.AlignLeft : Text.AlignRight - color: DefaultStyle.main2_700 - font { - pixelSize: Typography.p1.pixelSize - weight: Typography.p1.weight - } - } + Text { + id: textElement + visible: mainItem.processedText !== "" + text: mainItem.processedText + textFormat: Text.RichText + wrapMode: Text.Wrap + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: modelData.core.isRemoteMessage ? Text.AlignLeft : Text.AlignRight + color: DefaultStyle.main2_700 + font { + pixelSize: Typography.p1.pixelSize + weight: Typography.p1.weight + } + onLinkActivated: { + if (link.startsWith('sip')) + UtilsCpp.createCall(link) + else + Qt.openUrlExternally(link) + } + onHoveredLinkChanged: { + mainItem.linkHovered = hoveredLink !== "" + } + } RowLayout { Layout.alignment: Qt.AlignRight Text {