File indexing completed on 2024-09-15 12:31:26
0001 /* 0002 SPDX-FileCopyrightText: 2018-2024 Laurent Montel <montel@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "emoticons/emojimanager.h" 0008 #include "rocketchataccount.h" 0009 #include "ruqola_debug.h" 0010 #include <TextEmoticonsCore/UnicodeEmoticonManager> 0011 0012 #include <QJsonArray> 0013 #include <QJsonObject> 0014 #include <QRegularExpressionMatch> 0015 #include <QTextStream> 0016 0017 EmojiManager::EmojiManager(RocketChatAccount *account, QObject *parent) 0018 : QObject(parent) 0019 , mRocketChatAccount(account) 0020 { 0021 } 0022 0023 EmojiManager::~EmojiManager() = default; 0024 0025 QList<TextEmoticonsCore::UnicodeEmoticon> EmojiManager::unicodeEmojiList() const 0026 { 0027 return TextEmoticonsCore::UnicodeEmoticonManager::self()->unicodeEmojiList(); 0028 } 0029 0030 QList<TextEmoticonsCore::EmoticonCategory> EmojiManager::categories() const 0031 { 0032 return TextEmoticonsCore::UnicodeEmoticonManager::self()->categories(); 0033 } 0034 0035 QList<TextEmoticonsCore::UnicodeEmoticon> EmojiManager::emojisForCategory(const QString &category) const 0036 { 0037 return TextEmoticonsCore::UnicodeEmoticonManager::self()->emojisForCategory(category); 0038 } 0039 0040 void EmojiManager::addUpdateEmojiCustomList(const QJsonArray &arrayEmojiCustomArray) 0041 { 0042 bool newEmoji = true; 0043 for (int i = 0; i < arrayEmojiCustomArray.count(); ++i) { 0044 const QJsonObject obj = arrayEmojiCustomArray.at(i).toObject(); 0045 const QJsonObject customEmojiObj = obj.value(QLatin1String("emojiData")).toObject(); 0046 if (!customEmojiObj.isEmpty()) { 0047 if (customEmojiObj.contains(QLatin1String("_id"))) { 0048 const QString identifier = customEmojiObj.value(QLatin1String("_id")).toString(); 0049 for (auto emoji : std::as_const(mCustomEmojiList)) { 0050 if (emoji.identifier() == identifier) { 0051 mCustomEmojiList.removeAll(emoji); 0052 emoji.parseEmoji(customEmojiObj, true); 0053 mCustomEmojiList.append(emoji); 0054 newEmoji = false; 0055 break; 0056 } 0057 } 0058 } 0059 // if (!found) { 0060 // // Parse 0061 // CustomEmoji newEmoji; 0062 // newEmoji.parseEmoji(customEmojiObj, true); 0063 // if (newEmoji.isValid()) { 0064 // mCustomEmojiList.append(newEmoji); 0065 // } 0066 // } 0067 } else { 0068 qCWarning(RUQOLA_LOG) << "addUpdateEmojiCustomList invalid QJsonObject" << customEmojiObj; 0069 } 0070 } 0071 0072 // New QJsonArray([{"emojiData":{"_id":"HdN28k4PQ6J9xLkZ8","_updatedAt":{"$date":1631885946222},"aliases":["roo"],"extension":"png","name":"ruqola"}}]) 0073 // Update 0074 // QJsonArray([{"emojiData":{"_id":"vxE6eG5FrZCvbgM3t","aliases":["rooss"],"extension":"png","name":"xxx","newFile":true,"previousExtension":"png","previousName":"ruqolas"}} 0075 Q_EMIT customEmojiChanged(newEmoji); 0076 } 0077 0078 void EmojiManager::deleteEmojiCustom(const QJsonArray &arrayEmojiCustomArray) 0079 { 0080 // ([{"emojiData":{"_id":"PpawhZMaseBcEuGCG","_updatedAt":{"$date":1631858916014},"aliases":[],"extension":"png","name":"ruqolaff"}}]) 0081 const auto count{arrayEmojiCustomArray.count()}; 0082 for (auto i = 0; i < count; ++i) { 0083 const QJsonObject obj = arrayEmojiCustomArray.at(i).toObject(); 0084 const QJsonObject emojiData = obj.value(QStringLiteral("emojiData")).toObject(); 0085 const QString identifier = emojiData.value(QStringLiteral("_id")).toString(); 0086 if (!identifier.isEmpty()) { 0087 auto it = std::find_if(mCustomEmojiList.cbegin(), mCustomEmojiList.cend(), [identifier](const auto &emoji) { 0088 return emoji.identifier() == identifier; 0089 }); 0090 if (it != mCustomEmojiList.cend()) { 0091 mCustomEmojiList.removeAll(*it); 0092 } 0093 } 0094 } 0095 Q_EMIT customEmojiChanged(false); 0096 } 0097 0098 void EmojiManager::loadCustomEmoji(const QJsonObject &obj) 0099 { 0100 mCustomEmojiList.clear(); 0101 const QJsonObject result = obj.value(QLatin1String("emojis")).toObject(); 0102 const QJsonArray array = result.value(QLatin1String("update")).toArray(); 0103 // TODO add support for remove when we store it in local 0104 for (int i = 0, total = array.size(); i < total; ++i) { 0105 const QJsonObject emojiJson = array.at(i).toObject(); 0106 CustomEmoji emoji; 0107 emoji.parseEmoji(emojiJson); 0108 if (emoji.isValid()) { 0109 mCustomEmojiList.append(std::move(emoji)); 0110 } 0111 } 0112 0113 // clear cache 0114 mReplacePatternDirty = true; 0115 } 0116 0117 int EmojiManager::count() const 0118 { 0119 return mCustomEmojiList.count() + TextEmoticonsCore::UnicodeEmoticonManager::self()->count(); 0120 } 0121 0122 bool EmojiManager::isAnimatedImage(const QString &emojiIdentifier) const 0123 { 0124 if (emojiIdentifier.startsWith(QLatin1Char(':')) && emojiIdentifier.endsWith(QLatin1Char(':'))) { 0125 for (int i = 0, total = mCustomEmojiList.size(); i < total; ++i) { 0126 const CustomEmoji emoji = mCustomEmojiList.at(i); 0127 if (emoji.hasEmoji(emojiIdentifier)) { 0128 return emoji.isAnimatedImage(); 0129 } 0130 } 0131 } 0132 return false; 0133 } 0134 0135 TextEmoticonsCore::UnicodeEmoticon EmojiManager::unicodeEmoticonForEmoji(const QString &emojiIdentifier) const 0136 { 0137 return TextEmoticonsCore::UnicodeEmoticonManager::self()->unicodeEmoticonForEmoji(emojiIdentifier); 0138 } 0139 0140 QString EmojiManager::customEmojiFileNameFromIdentifier(const QString &emojiIdentifier) const 0141 { 0142 for (const CustomEmoji &customEmoji : mCustomEmojiList) { 0143 if (customEmoji.identifier() == emojiIdentifier) { 0144 return customEmoji.emojiFileName(); 0145 } 0146 } 0147 return {}; 0148 } 0149 0150 QString EmojiManager::customEmojiFileName(const QString &emojiIdentifier) const 0151 { 0152 for (const CustomEmoji &customEmoji : mCustomEmojiList) { 0153 if (customEmoji.hasEmoji(emojiIdentifier)) { 0154 return customEmoji.emojiFileName(); 0155 } 0156 } 0157 return {}; 0158 } 0159 0160 QString EmojiManager::normalizedReactionEmoji(const QString &emojiIdentifier) const 0161 { 0162 for (const auto &customEmoji : mCustomEmojiList) { 0163 if (customEmoji.hasEmoji(emojiIdentifier)) { 0164 return customEmoji.emojiIdentifier(); 0165 } 0166 } 0167 const auto unicodeEmojis = unicodeEmojiList(); 0168 for (const auto &unicodeEmoji : unicodeEmojis) { 0169 if (unicodeEmoji.hasEmoji(emojiIdentifier)) { 0170 return unicodeEmoji.identifier(); 0171 } 0172 } 0173 return emojiIdentifier; 0174 } 0175 0176 QString EmojiManager::replaceEmojiIdentifier(const QString &emojiIdentifier, bool isReaction) 0177 { 0178 if (mServerUrl.isEmpty()) { 0179 qCWarning(RUQOLA_LOG) << "Server Url not defined"; 0180 return emojiIdentifier; 0181 } 0182 if (mRocketChatAccount && !mRocketChatAccount->ownUserPreferences().convertAsciiEmoji()) { 0183 return emojiIdentifier; 0184 } 0185 if (emojiIdentifier.startsWith(QLatin1Char(':')) && emojiIdentifier.endsWith(QLatin1Char(':'))) { 0186 for (const CustomEmoji &emoji : std::as_const(mCustomEmojiList)) { 0187 if (emoji.hasEmoji(emojiIdentifier)) { 0188 QString cachedHtml = emoji.cachedHtml(); 0189 if (cachedHtml.isEmpty()) { 0190 // For the moment we can't support animated image as emoticon in text. Only as Reaction. 0191 if (emoji.isAnimatedImage() && isReaction) { 0192 cachedHtml = emoji.generateAnimatedUrlFromCustomEmoji(mServerUrl); 0193 } else { 0194 const QString fileName = customEmojiFileName(emojiIdentifier); 0195 if (!fileName.isEmpty() && mRocketChatAccount) { 0196 const QUrl emojiUrl = mRocketChatAccount->attachmentUrlFromLocalCache(fileName); 0197 if (emojiUrl.isEmpty()) { 0198 // The download is happening, this will all be updated again later 0199 } else { 0200 cachedHtml = emoji.generateHtmlFromCustomEmojiLocalPath(emojiUrl.path()); 0201 } 0202 } else { 0203 qCDebug(RUQOLA_LOG) << " Impossible to find custom emoji " << emojiIdentifier; 0204 } 0205 } 0206 } 0207 return cachedHtml; 0208 } 0209 } 0210 } 0211 0212 const TextEmoticonsCore::UnicodeEmoticon unicodeEmoticon = unicodeEmoticonForEmoji(emojiIdentifier); 0213 if (unicodeEmoticon.isValid()) { 0214 return unicodeEmoticon.unicodeDisplay(); 0215 } 0216 0217 return emojiIdentifier; 0218 } 0219 0220 void EmojiManager::replaceEmojis(QString *str) 0221 { 0222 Q_ASSERT(str); 0223 if (mReplacePatternDirty) { 0224 // build a regexp pattern for all the possible emoticons we want to replace 0225 // i.e. this is going to build a pattern like this: 0226 // \:smiley\:|\:\-\)|... 0227 // to optimize it a bit, we use a common pattern that matches most 0228 // emojis and then we only need to add the other special (ascii) ones 0229 // otherwise the pattern could become extremely long 0230 // 0231 // furthermore, we don't want to replace emojis (esp. non-colon escaped ones) in the 0232 // middle of another string, such as within a URL or such. at the same time, multiple 0233 // smileys may come after another... 0234 const auto commonPattern = QLatin1String(":[\\w\\-]+:"); 0235 // TODO: use QRegularExpression::anchoredPattern once ruqola depends on Qt 5.15 0236 static const QRegularExpression common(QLatin1Char('^') + commonPattern + QLatin1Char('$')); 0237 0238 QString pattern; 0239 QTextStream stream(&pattern); 0240 // prevent replacements within other strings, use a negative-lookbehind to rule out 0241 // that we are within some word or link or such 0242 stream << "(?<![\\w\\-:])"; 0243 // put all other patterns in a non-capturing group 0244 stream << "(?:"; 0245 stream << commonPattern; 0246 0247 auto addEmoji = [&](const QString &string) { 0248 if (common.match(string).hasMatch()) { 0249 return; 0250 } 0251 stream << '|'; 0252 stream << QRegularExpression::escape(string); 0253 }; 0254 auto addEmojis = [&](const auto &emojis) { 0255 for (const auto &emoji : emojis) { 0256 addEmoji(emoji.identifier()); 0257 const auto aliases = emoji.aliases(); 0258 for (const auto &alias : aliases) { 0259 addEmoji(alias); 0260 } 0261 } 0262 }; 0263 0264 addEmojis(mCustomEmojiList); 0265 addEmojis(unicodeEmojiList()); 0266 // close non-capturing group 0267 stream << ")"; 0268 stream.flush(); 0269 0270 mReplacePattern.setPattern(pattern); 0271 mReplacePattern.optimize(); 0272 mReplacePatternDirty = false; 0273 } 0274 0275 if (mReplacePattern.pattern().isEmpty() || !mReplacePattern.isValid()) { 0276 qCWarning(RUQOLA_LOG) << "invalid emoji replace pattern" << mReplacePattern.pattern() << mReplacePattern.errorString(); 0277 return; 0278 } 0279 0280 int offset = 0; 0281 while (offset < str->size()) { 0282 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 0283 const auto match = mReplacePattern.match(str, offset); 0284 #else 0285 const auto match = mReplacePattern.matchView(QStringView(*str), offset); 0286 #endif 0287 if (!match.hasMatch()) { 0288 break; 0289 } 0290 const auto word = match.captured(); 0291 const auto replaceWord = replaceEmojiIdentifier(word); 0292 str->replace(match.capturedStart(), word.size(), replaceWord); 0293 offset = match.capturedStart() + replaceWord.size(); 0294 } 0295 } 0296 0297 QString EmojiManager::serverUrl() const 0298 { 0299 return mServerUrl; 0300 } 0301 0302 void EmojiManager::setServerUrl(const QString &serverUrl) 0303 { 0304 if (mServerUrl != serverUrl) { 0305 mServerUrl = serverUrl; 0306 clearCustomEmojiCachedHtml(); 0307 } 0308 } 0309 0310 void EmojiManager::clearCustomEmojiCachedHtml() 0311 { 0312 for (int i = 0, total = mCustomEmojiList.size(); i < total; ++i) { 0313 mCustomEmojiList[i].clearCachedHtml(); 0314 } 0315 } 0316 0317 const QVector<CustomEmoji> &EmojiManager::customEmojiList() const 0318 { 0319 return mCustomEmojiList; 0320 } 0321 0322 #include "moc_emojimanager.cpp"