diff --git a/chatterino.pro b/chatterino.pro index d6fc9c888..0660eca3b 100644 --- a/chatterino.pro +++ b/chatterino.pro @@ -117,6 +117,7 @@ SOURCES += \ src/providers/twitch/twitchemotes.cpp \ src/providers/bttv/bttvemotes.cpp \ src/providers/ffz/ffzemotes.cpp \ + src/providers/emoji/emojis.cpp \ src/singletons/commandmanager.cpp \ src/singletons/emotemanager.cpp \ src/singletons/fontmanager.cpp \ @@ -223,7 +224,6 @@ HEADERS += \ src/channel.hpp \ src/const.hpp \ src/debug/log.hpp \ - src/emojis.hpp \ src/messages/image.hpp \ src/messages/layouts/messagelayout.hpp \ src/messages/layouts/messagelayoutcontainer.hpp \ @@ -248,6 +248,7 @@ HEADERS += \ src/providers/twitch/twitchemotes.hpp \ src/providers/bttv/bttvemotes.hpp \ src/providers/ffz/ffzemotes.hpp \ + src/providers/emoji/emojis.hpp \ src/singletons/commandmanager.hpp \ src/singletons/emotemanager.hpp \ src/singletons/fontmanager.hpp \ diff --git a/src/emojis.hpp b/src/emojis.hpp deleted file mode 100644 index 39f2be24c..000000000 --- a/src/emojis.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "util/emotemap.hpp" - -#include - -namespace chatterino { - -struct EmojiData { - // actual byte-representation of the emoji (i.e. \154075\156150 which is :male:) - QString value; - - // what's used in the emoji-one url - QString code; - - // i.e. thinking - QString shortCode; - - util::EmoteData emoteData; -}; - -namespace util { - -using EmojiMap = ConcurrentMap; - -} // namespace util - -} // namespace chatterino diff --git a/src/providers/emoji/emojis.cpp b/src/providers/emoji/emojis.cpp new file mode 100644 index 000000000..2817515fb --- /dev/null +++ b/src/providers/emoji/emojis.cpp @@ -0,0 +1,184 @@ +#include "providers/emoji/emojis.hpp" + +namespace chatterino { +namespace providers { +namespace emoji { + +void Emojis::load() +{ + QFile file(":/emojidata.txt"); + file.open(QFile::ReadOnly); + QTextStream in(&file); + + uint unicodeBytes[4]; + + while (!in.atEnd()) { + // Line example: sunglasses 1f60e + QString line = in.readLine(); + + if (line.at(0) == '#') { + // Ignore lines starting with # (comments) + continue; + } + + QStringList parts = line.split(' '); + if (parts.length() < 2) { + continue; + } + + QString shortCode = parts[0]; + QString code = parts[1]; + + QStringList unicodeCharacters = code.split('-'); + if (unicodeCharacters.length() < 1) { + continue; + } + + int numUnicodeBytes = 0; + + for (const QString &unicodeCharacter : unicodeCharacters) { + unicodeBytes[numUnicodeBytes++] = QString(unicodeCharacter).toUInt(nullptr, 16); + } + + QString unicodeString = QString::fromUcs4(unicodeBytes, numUnicodeBytes); + + QString url = "https://cdnjs.cloudflare.com/ajax/libs/" + "emojione/2.2.6/assets/png/" + + code + ".png"; + + EmojiData emojiData{ + unicodeString, // + code, // + shortCode, // + {new messages::Image(url, 0.35, unicodeString, ":" + shortCode + ":
Emoji")}, + }; + + this->emojiShortCodeToEmoji.insert(shortCode, emojiData); + this->shortCodes.push_back(shortCode.toStdString()); + + this->emojiFirstByte[emojiData.value.at(0)].append(emojiData); + + this->emojis.insert(code, emojiData); + } + + for (auto &p : this->emojiFirstByte) { + std::stable_sort(p.begin(), p.end(), [](const auto &lhs, const auto &rhs) { + return lhs.value.length() > rhs.value.length(); + }); + } +} + +void Emojis::parse(std::vector> &parsedWords, + const QString &text) +{ + int lastParsedEmojiEndIndex = 0; + + for (auto i = 0; i < text.length(); ++i) { + const QChar character = text.at(i); + + if (character.isLowSurrogate()) { + continue; + } + + auto it = this->emojiFirstByte.find(character); + if (it == this->emojiFirstByte.end()) { + // No emoji starts with this character + continue; + } + + const QVector possibleEmojis = it.value(); + + int remainingCharacters = text.length() - i - 1; + + EmojiData matchedEmoji; + + int matchedEmojiLength = 0; + + for (const EmojiData &emoji : possibleEmojis) { + int emojiExtraCharacters = emoji.value.length() - 1; + if (emojiExtraCharacters > remainingCharacters) { + // It cannot be this emoji, there's not enough space for it + continue; + } + + bool match = true; + + for (int j = 1; j < emoji.value.length(); ++j) { + if (text.at(i + j) != emoji.value.at(j)) { + match = false; + + break; + } + } + + if (match) { + matchedEmoji = emoji; + matchedEmojiLength = emoji.value.length(); + + break; + } + } + + if (matchedEmojiLength == 0) { + continue; + } + + int currentParsedEmojiFirstIndex = i; + int currentParsedEmojiEndIndex = i + (matchedEmojiLength); + + int charactersFromLastParsedEmoji = currentParsedEmojiFirstIndex - lastParsedEmojiEndIndex; + + if (charactersFromLastParsedEmoji > 0) { + // Add characters inbetween emojis + parsedWords.emplace_back(util::EmoteData(), text.mid(lastParsedEmojiEndIndex, + charactersFromLastParsedEmoji)); + } + + // Push the emoji as a word to parsedWords + parsedWords.push_back( + std::tuple(matchedEmoji.emoteData, QString())); + + lastParsedEmojiEndIndex = currentParsedEmojiEndIndex; + + i += matchedEmojiLength - 1; + } + + if (lastParsedEmojiEndIndex < text.length()) { + // Add remaining characters + parsedWords.emplace_back(util::EmoteData(), text.mid(lastParsedEmojiEndIndex)); + } +} + +QString Emojis::replaceShortCodes(const QString &text) +{ + QString ret(text); + auto it = this->findShortCodesRegex.globalMatch(text); + + int32_t offset = 0; + + while (it.hasNext()) { + auto match = it.next(); + + auto capturedString = match.captured(); + + QString matchString = capturedString.toLower().mid(1, capturedString.size() - 2); + + auto emojiIt = this->emojiShortCodeToEmoji.constFind(matchString); + + if (emojiIt == this->emojiShortCodeToEmoji.constEnd()) { + continue; + } + + auto emojiData = emojiIt.value(); + + ret.replace(offset + match.capturedStart(), match.capturedLength(), emojiData.value); + + offset += emojiData.value.size() - match.capturedLength(); + } + + return ret; +} + +} // namespace emoji +} // namespace providers +} // namespace chatterino diff --git a/src/providers/emoji/emojis.hpp b/src/providers/emoji/emojis.hpp new file mode 100644 index 000000000..1df9804cf --- /dev/null +++ b/src/providers/emoji/emojis.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "signalvector.hpp" +#include "util/concurrentmap.hpp" +#include "util/emotemap.hpp" + +#include +#include + +#include + +namespace chatterino { +namespace providers { +namespace emoji { + +struct EmojiData { + // actual byte-representation of the emoji (i.e. \154075\156150 which is :male:) + QString value; + + // what's used in the emoji-one url + QString code; + + // i.e. thinking + QString shortCode; + + util::EmoteData emoteData; +}; + +using EmojiMap = util::ConcurrentMap; + +class Emojis +{ +public: + EmojiMap emojis; + + std::vector shortCodes; + + void load(); + QString replaceShortCodes(const QString &text); + + void parse(std::vector> &parsedWords, const QString &text); + +private: + /// Emojis + QRegularExpression findShortCodesRegex{":([-+\\w]+):"}; + + // shortCodeToEmoji maps strings like "sunglasses" to its emoji + QMap emojiShortCodeToEmoji; + + // Maps the first character of the emoji unicode string to a vector of possible emojis + QMap> emojiFirstByte; +}; + +} // namespace emoji +} // namespace providers +} // namespace chatterino diff --git a/src/providers/twitch/twitchchannel.cpp b/src/providers/twitch/twitchchannel.cpp index a5fce4e8f..1d8e2c3d4 100644 --- a/src/providers/twitch/twitchchannel.cpp +++ b/src/providers/twitch/twitchchannel.cpp @@ -151,7 +151,7 @@ void TwitchChannel::sendMessage(const QString &message) debug::Log("[TwitchChannel:{}] Send message: {}", this->name, message); // Do last message processing - QString parsedMessage = app->emotes->replaceShortCodes(message); + QString parsedMessage = app->emotes->emojis.replaceShortCodes(message); parsedMessage = parsedMessage.trimmed(); diff --git a/src/providers/twitch/twitchmessagebuilder.cpp b/src/providers/twitch/twitchmessagebuilder.cpp index 566868745..c30640f73 100644 --- a/src/providers/twitch/twitchmessagebuilder.cpp +++ b/src/providers/twitch/twitchmessagebuilder.cpp @@ -185,7 +185,7 @@ MessagePtr TwitchMessageBuilder::build() std::vector> parsed; // Parse emojis and take all non-emojis and put them in parsed as full text-words - app->emotes->parseEmojis(parsed, split); + app->emotes->emojis.parse(parsed, split); for (const auto &tuple : parsed) { const util::EmoteData &emoteData = std::get<0>(tuple); diff --git a/src/singletons/emotemanager.cpp b/src/singletons/emotemanager.cpp index c94b68191..df2ce56c4 100644 --- a/src/singletons/emotemanager.cpp +++ b/src/singletons/emotemanager.cpp @@ -12,12 +12,6 @@ using namespace chatterino::messages; namespace chatterino { namespace singletons { -EmoteManager::EmoteManager() - : findShortCodesRegex(":([-+\\w]+):") -{ - qDebug() << "init EmoteManager"; -} - void EmoteManager::initialize() { getApp()->accounts->twitch.currentUserChanged.connect([this] { @@ -26,7 +20,7 @@ void EmoteManager::initialize() this->twitch.refresh(currentUser); }); - this->loadEmojis(); + this->emojis.load(); this->bttv.loadGlobalEmotes(); this->ffz.loadGlobalEmotes(); @@ -38,186 +32,6 @@ util::EmoteMap &EmoteManager::getChatterinoEmotes() return _chatterinoEmotes; } -util::EmojiMap &EmoteManager::getEmojis() -{ - return this->emojis; -} - -void EmoteManager::loadEmojis() -{ - QFile file(":/emojidata.txt"); - file.open(QFile::ReadOnly); - QTextStream in(&file); - - uint unicodeBytes[4]; - - while (!in.atEnd()) { - // Line example: sunglasses 1f60e - QString line = in.readLine(); - - if (line.at(0) == '#') { - // Ignore lines starting with # (comments) - continue; - } - - QStringList parts = line.split(' '); - if (parts.length() < 2) { - continue; - } - - QString shortCode = parts[0]; - QString code = parts[1]; - - QStringList unicodeCharacters = code.split('-'); - if (unicodeCharacters.length() < 1) { - continue; - } - - int numUnicodeBytes = 0; - - for (const QString &unicodeCharacter : unicodeCharacters) { - unicodeBytes[numUnicodeBytes++] = QString(unicodeCharacter).toUInt(nullptr, 16); - } - - QString unicodeString = QString::fromUcs4(unicodeBytes, numUnicodeBytes); - - QString url = "https://cdnjs.cloudflare.com/ajax/libs/" - "emojione/2.2.6/assets/png/" + - code + ".png"; - - EmojiData emojiData{ - unicodeString, // - code, // - shortCode, // - {new Image(url, 0.35, unicodeString, ":" + shortCode + ":
Emoji")}, - }; - - this->emojiShortCodeToEmoji.insert(shortCode, emojiData); - this->emojiShortCodes.push_back(shortCode.toStdString()); - - this->emojiFirstByte[emojiData.value.at(0)].append(emojiData); - - this->emojis.insert(code, emojiData); - } - - for (auto &p : this->emojiFirstByte) { - std::stable_sort(p.begin(), p.end(), [](const auto &lhs, const auto &rhs) { - return lhs.value.length() > rhs.value.length(); - }); - } -} - -void EmoteManager::parseEmojis(std::vector> &parsedWords, - const QString &text) -{ - int lastParsedEmojiEndIndex = 0; - - for (auto i = 0; i < text.length(); ++i) { - const QChar character = text.at(i); - - if (character.isLowSurrogate()) { - continue; - } - - auto it = this->emojiFirstByte.find(character); - if (it == this->emojiFirstByte.end()) { - // No emoji starts with this character - continue; - } - - const QVector possibleEmojis = it.value(); - - int remainingCharacters = text.length() - i - 1; - - EmojiData matchedEmoji; - - int matchedEmojiLength = 0; - - for (const EmojiData &emoji : possibleEmojis) { - int emojiExtraCharacters = emoji.value.length() - 1; - if (emojiExtraCharacters > remainingCharacters) { - // It cannot be this emoji, there's not enough space for it - continue; - } - - bool match = true; - - for (int j = 1; j < emoji.value.length(); ++j) { - if (text.at(i + j) != emoji.value.at(j)) { - match = false; - - break; - } - } - - if (match) { - matchedEmoji = emoji; - matchedEmojiLength = emoji.value.length(); - - break; - } - } - - if (matchedEmojiLength == 0) { - continue; - } - - int currentParsedEmojiFirstIndex = i; - int currentParsedEmojiEndIndex = i + (matchedEmojiLength); - - int charactersFromLastParsedEmoji = currentParsedEmojiFirstIndex - lastParsedEmojiEndIndex; - - if (charactersFromLastParsedEmoji > 0) { - // Add characters inbetween emojis - parsedWords.emplace_back(util::EmoteData(), text.mid(lastParsedEmojiEndIndex, - charactersFromLastParsedEmoji)); - } - - // Push the emoji as a word to parsedWords - parsedWords.push_back( - std::tuple(matchedEmoji.emoteData, QString())); - - lastParsedEmojiEndIndex = currentParsedEmojiEndIndex; - - i += matchedEmojiLength - 1; - } - - if (lastParsedEmojiEndIndex < text.length()) { - // Add remaining characters - parsedWords.emplace_back(util::EmoteData(), text.mid(lastParsedEmojiEndIndex)); - } -} - -QString EmoteManager::replaceShortCodes(const QString &text) -{ - QString ret(text); - auto it = this->findShortCodesRegex.globalMatch(text); - - int32_t offset = 0; - - while (it.hasNext()) { - auto match = it.next(); - - auto capturedString = match.captured(); - - QString matchString = capturedString.toLower().mid(1, capturedString.size() - 2); - - auto emojiIt = this->emojiShortCodeToEmoji.constFind(matchString); - - if (emojiIt == this->emojiShortCodeToEmoji.constEnd()) { - continue; - } - - auto emojiData = emojiIt.value(); - - ret.replace(offset + match.capturedStart(), match.capturedLength(), emojiData.value); - - offset += emojiData.value.size() - match.capturedLength(); - } - - return ret; -} - util::EmoteData EmoteManager::getCheerImage(long long amount, bool animated) { // TODO: fix this xD diff --git a/src/singletons/emotemanager.hpp b/src/singletons/emotemanager.hpp index 255a22172..f64ded71c 100644 --- a/src/singletons/emotemanager.hpp +++ b/src/singletons/emotemanager.hpp @@ -2,21 +2,16 @@ #define GIF_FRAME_LENGTH 33 -#include "emojis.hpp" #include "messages/image.hpp" #include "providers/bttv/bttvemotes.hpp" +#include "providers/emoji/emojis.hpp" #include "providers/ffz/ffzemotes.hpp" #include "providers/twitch/twitchemotes.hpp" -#include "signalvector.hpp" #include "singletons/helper/giftimer.hpp" #include "util/concurrentmap.hpp" #include "util/emotemap.hpp" -#include -#include -#include #include -#include namespace chatterino { namespace singletons { @@ -24,20 +19,18 @@ namespace singletons { class EmoteManager { public: - EmoteManager(); - ~EmoteManager() = delete; providers::twitch::TwitchEmotes twitch; providers::bttv::BTTVEmotes bttv; providers::ffz::FFZEmotes ffz; + providers::emoji::Emojis emojis; GIFTimer gifTimer; void initialize(); util::EmoteMap &getChatterinoEmotes(); - util::EmojiMap &getEmojis(); util::EmoteData getCheerImage(long long int amount, bool animated); @@ -46,27 +39,6 @@ public: util::ConcurrentMap miscImageCache; private: - /// Emojis - QRegularExpression findShortCodesRegex; - - // shortCodeToEmoji maps strings like "sunglasses" to its emoji - QMap emojiShortCodeToEmoji; - - // Maps the first character of the emoji unicode string to a vector of possible emojis - QMap> emojiFirstByte; - - util::EmojiMap emojis; - - void loadEmojis(); - -public: - void parseEmojis(std::vector> &parsedWords, - const QString &text); - - QString replaceShortCodes(const QString &text); - - std::vector emojiShortCodes; - /// Chatterino emotes util::EmoteMap _chatterinoEmotes; }; diff --git a/src/util/completionmodel.cpp b/src/util/completionmodel.cpp index 25b30f6c8..b3c0371ab 100644 --- a/src/util/completionmodel.cpp +++ b/src/util/completionmodel.cpp @@ -58,7 +58,7 @@ void CompletionModel::refresh() } // Global: Emojis - const auto &emojiShortCodes = app->emotes->emojiShortCodes; + const auto &emojiShortCodes = app->emotes->emojis.shortCodes; for (const auto &m : emojiShortCodes) { this->addString(":" + m + ":", TaggedString::Type::Emoji); } diff --git a/src/widgets/emotepopup.cpp b/src/widgets/emotepopup.cpp index d521521c0..7ec1fc00c 100644 --- a/src/widgets/emotepopup.cpp +++ b/src/widgets/emotepopup.cpp @@ -127,7 +127,7 @@ void EmotePopup::loadChannel(ChannelPtr _channel) void EmotePopup::loadEmojis() { - auto &emojis = getApp()->emotes->getEmojis(); + auto &emojis = getApp()->emotes->emojis.emojis; ChannelPtr emojiChannel(new Channel("", Channel::None));