Skip to content

Commit

Permalink
Correctly Display Personal Emotes (#199)
Browse files Browse the repository at this point in the history
  • Loading branch information
Nerixyz authored Jan 25, 2024
1 parent fd7ffb9 commit d106922
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 43 deletions.
59 changes: 54 additions & 5 deletions src/providers/seventv/SeventvEventAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,16 @@ void SeventvEventAPI::onEmoteSetUpdate(const Dispatch &dispatch)
// pulled: Array<{ key, old_value }>,
// updated: Array<{ key, value, old_value }>,
// }
for (const auto pushedRef : dispatch.body["pushed"].toArray())
auto pushedArray = dispatch.body["pushed"].toArray();
auto pulledArray = dispatch.body["pulled"].toArray();
auto updatedArray = dispatch.body["updated"].toArray();
qCDebug(chatterinoSeventvEventAPI).nospace()
<< "Update emote set " << dispatch.id
<< " added: " << pushedArray.count()
<< ", removed: " << pulledArray.count()
<< ", updated: " << updatedArray.count();

for (const auto pushedRef : pushedArray)
{
auto pushed = pushedRef.toObject();
if (pushed["key"].toString() != "emotes")
Expand All @@ -276,7 +285,7 @@ void SeventvEventAPI::onEmoteSetUpdate(const Dispatch &dispatch)
<< "Invalid dispatch" << dispatch.body;
}
}
for (const auto updatedRef : dispatch.body["updated"].toArray())
for (const auto updatedRef : updatedArray)
{
auto updated = updatedRef.toObject();
if (updated["key"].toString() != "emotes")
Expand All @@ -298,7 +307,7 @@ void SeventvEventAPI::onEmoteSetUpdate(const Dispatch &dispatch)
<< "Invalid dispatch" << dispatch.body;
}
}
for (const auto pulledRef : dispatch.body["pulled"].toArray())
for (const auto pulledRef : pulledArray)
{
auto pulled = pulledRef.toObject();
if (pulled["key"].toString() != "emotes")
Expand All @@ -319,6 +328,26 @@ void SeventvEventAPI::onEmoteSetUpdate(const Dispatch &dispatch)
<< "Invalid dispatch" << dispatch.body;
}
}

if (!this->lastPersonalEmoteAssignment_)
{
return;
}

if (this->lastPersonalEmoteAssignment_->emoteSetID == dispatch.id)
{
auto emoteSet =
getIApp()->getSeventvPersonalEmotes()->getEmoteSetByID(dispatch.id);
if (emoteSet)
{
qCDebug(chatterinoSeventvEventAPI) << "Flushed last emote set";
this->signals_.personalEmoteSetAdded.invoke({
this->lastPersonalEmoteAssignment_->userName,
*emoteSet,
});
}
}
this->lastPersonalEmoteAssignment_ = std::nullopt;
}

void SeventvEventAPI::onUserUpdate(const Dispatch &dispatch)
Expand Down Expand Up @@ -393,12 +422,30 @@ void SeventvEventAPI::onEntitlementCreate(
}
break;
case CosmeticKind::EmoteSet: {
qCDebug(chatterinoSeventvEventAPI)
<< "Assign user" << entitlement.userID << "to emote set"
<< entitlement.refID;
if (auto set =
getIApp()->getSeventvPersonalEmotes()->assignUserToEmoteSet(
entitlement.refID, entitlement.userID))
{
this->signals_.personalEmoteSetAdded.invoke(
{entitlement.userName, *set});
if ((*set)->empty())
{
qCDebug(chatterinoSeventvEventAPI)
<< "Saving emote set as it's empty to wait for further "
"updates";
this->lastPersonalEmoteAssignment_ =
LastPersonalEmoteAssignment{
.userName = entitlement.userName,
.emoteSetID = entitlement.refID,
};
}
else
{
this->lastPersonalEmoteAssignment_ = std::nullopt;
this->signals_.personalEmoteSetAdded.invoke(
{entitlement.userName, *set});
}
}
}
break;
Expand Down Expand Up @@ -442,6 +489,8 @@ void SeventvEventAPI::onEmoteSetCreate(const Dispatch &dispatch)

if (createDispatch.isPersonal)
{
qCDebug(chatterinoSeventvEventAPI)
<< "Create emote set" << createDispatch.emoteSetID;
getIApp()->getSeventvPersonalEmotes()->createEmoteSet(
createDispatch.emoteSetID);
}
Expand Down
11 changes: 11 additions & 0 deletions src/providers/seventv/SeventvEventAPI.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ class SeventvEventAPI
/** Twitch channel ids */
std::unordered_set<QString> subscribedTwitchChannels_;
std::chrono::milliseconds heartbeatInterval_;

struct LastPersonalEmoteAssignment {
QString userName;
QString emoteSetID;
std::shared_ptr<const EmoteMap> emoteSet;
};

/// This is a workaround for 7TV sending `CreateEntitlement` before
/// `UpdateEmoteSet`. We only upsert emotes when a user gets assigned a
/// new emote set, but in this case, we're upserting after updating as well.
std::optional<LastPersonalEmoteAssignment> lastPersonalEmoteAssignment_;
};

} // namespace chatterino
1 change: 0 additions & 1 deletion src/providers/seventv/SeventvPaints.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ std::vector<PaintDropShadow> parseDropShadows(const QJsonArray &dropShadows)

std::optional<std::shared_ptr<Paint>> parsePaint(const QJsonObject &paintJson)
{
qDebug() << paintJson;
const QString name = paintJson["name"].toString();
const QString id = paintJson["id"].toString();

Expand Down
17 changes: 17 additions & 0 deletions src/providers/seventv/SeventvPersonalEmotes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,21 @@ std::optional<EmotePtr> SeventvPersonalEmotes::getEmoteForUser(
return std::nullopt;
}

std::optional<std::shared_ptr<const EmoteMap>>
SeventvPersonalEmotes::getEmoteSetByID(const QString &emoteSetID) const
{
std::shared_lock<std::shared_mutex> lock(this->mutex_);
if (!this->enabled_)
{
return std::nullopt;
}

auto id = this->emoteSets_.find(emoteSetID);
if (id == this->emoteSets_.end())
{
return std::nullopt;
}
return id->second.get();
}

} // namespace chatterino
3 changes: 3 additions & 0 deletions src/providers/seventv/SeventvPersonalEmotes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ class SeventvPersonalEmotes : public Singleton
std::optional<EmotePtr> getEmoteForUser(const QString &userID,
const EmoteName &emoteName) const;

std::optional<std::shared_ptr<const EmoteMap>> getEmoteSetByID(
const QString &emoteSetID) const;

private:
// emoteSetID => emoteSet
std::unordered_map<QString, Atomic<std::shared_ptr<const EmoteMap>>>
Expand Down
143 changes: 106 additions & 37 deletions src/providers/twitch/TwitchChannel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1792,18 +1792,25 @@ void TwitchChannel::listenSevenTVCosmetics() const
void TwitchChannel::upsertPersonalSeventvEmotes(
const QString &userLogin, const std::shared_ptr<const EmoteMap> &emoteMap)
{
// This is attempting a (kind-of) surgical replacement of the users' last
// sent message. The the last message is essentially re-parsed and newly
// added emotes are inserted where appropriate.

assertInGuiThread();
auto snapshot = this->getMessageSnapshot();
if (snapshot.size() == 0)
{
return;
}

/// Finds the last message of the user (searches the last five messages).
/// If no message is found, `std::nullopt` is returned.
const auto findMessage = [&]() -> std::optional<MessagePtr> {
auto end = std::max<ptrdiff_t>(0, (ptrdiff_t)snapshot.size() - 5);
auto size = static_cast<qsizetype>(snapshot.size());
auto end = std::max<qsizetype>(0, size - 5);

// explicitly using signed integers here to represent '-1'
for (ptrdiff_t i = (ptrdiff_t)snapshot.size() - 1; i >= end; i--)
for (qsizetype i = size - 1; i >= end; i--)
{
const auto &message = snapshot[i];
if (message->loginName == userLogin)
Expand All @@ -1821,11 +1828,105 @@ void TwitchChannel::upsertPersonalSeventvEmotes(
return;
}

using MessageElementVec = std::vector<std::unique_ptr<MessageElement>>;

/// Tries to find words in the @a textElement that are emotes in the @a emoteMap
/// (i.e. newly added emotes) and converts these to an emote element
/// or, if they're zero-width, to a layered emote element.
const auto upsertWords = [&](MessageElementVec &elements,
TextElement *textElement) {
std::vector<TextElement::Word> words;

/// Appends a text element with the pending @a words
/// and clears the vector.
///
/// @pre @a words must not be empty
const auto flush = [&]() {
elements.emplace_back(std::make_unique<TextElement>(
std::move(words), textElement->getFlags(), textElement->color(),
textElement->style()));
words.clear();
};

/// Attempts to insert the emote as a zero-width emote.
/// If there are pending words to be inserted (i.e. @a words is not empty
/// and thus there's no previous emote to merge the @a emote with),
/// or there are no elements in the message yet, the insertion fails.
///
/// @returns `true` iff the insertion succeeded.
const auto tryInsertZeroWidth = [&](const EmotePtr &emote) -> bool {
if (!words.empty() || elements.empty())
{
// either the last element will be a TextElement _or_
// there are no elements.
return false;
}
// [THIS IS LARGELY THE SAME AS IN TwitchMessageBuilder::tryAppendEmote]
// Attempt to merge current zero-width emote into any previous emotes
auto *asEmote = dynamic_cast<EmoteElement *>(elements.back().get());
if (asEmote)
{
// Make sure to access asEmote before taking ownership when releasing
auto baseEmote = asEmote->getEmote();
// Need to remove EmoteElement and replace with LayeredEmoteElement
auto baseEmoteElement = std::move(elements.back());
elements.pop_back();

std::vector<LayeredEmoteElement::Emote> layers{
{baseEmote, baseEmoteElement->getFlags()},
{emote, MessageElementFlag::SevenTVEmote},
};
elements.emplace_back(std::make_unique<LayeredEmoteElement>(
std::move(layers),
baseEmoteElement->getFlags() |
MessageElementFlag::SevenTVEmote,
textElement->color()));
return true;
}

auto *asLayered =
dynamic_cast<LayeredEmoteElement *>(elements.back().get());
if (asLayered)
{
asLayered->addEmoteLayer(
{emote, MessageElementFlag::SevenTVEmote});
asLayered->addFlags(MessageElementFlag::SevenTVEmote);
return true;
}
return false;
};

// Find all words that match a personal emote and replace them with emotes
for (const auto &word : textElement->words())
{
auto emoteIt = emoteMap->find(EmoteName{word.text});
if (emoteIt == emoteMap->end())
{
words.emplace_back(word);
continue;
}

if (emoteIt->second->zeroWidth)
{
if (tryInsertZeroWidth(emoteIt->second))
{
continue;
}
}

flush();

elements.emplace_back(std::make_unique<EmoteElement>(
emoteIt->second, MessageElementFlag::SevenTVEmote));
}
flush();
};

auto cloned = message.value()->cloneWith([&](Message &message) {
// We create a new vector of elements,
// if we encounter a `TextElement` that contains any emote,
// we insert an `EmoteElement` at the position.
std::vector<std::unique_ptr<MessageElement>> elements;
// we insert an `EmoteElement` (or `LayeredEmoteElement`) at the position.
MessageElementVec elements;
elements.reserve(message.elements.size());

std::for_each(
Expand All @@ -1839,39 +1940,7 @@ void TwitchChannel::upsertPersonalSeventvEmotes(
if (textElement != nullptr &&
textElement->getFlags().has(MessageElementFlag::Text))
{
std::vector<TextElement::Word> words;
// Append the text element and clear the vector.
const auto flush = [&]() {
elements.emplace_back(std::make_unique<TextElement>(
std::move(words), textElement->getFlags(),
textElement->color(), textElement->style()));
words.clear();
};

// Search for a word that matches any emote.
for (const auto &word : textElement->words())
{
auto emoteIt = emoteMap->find(EmoteName{word.text});
if (emoteIt != emoteMap->cend())
{
MessageElementFlags emoteFlags(
MessageElementFlag::SevenTVEmote);
// TODO: This doesn't support zero-width emotes.
// To support these emotes, we'd now need to look back at the added elements
// and insert/update a LayeredEmoteElement.
// As of now, this requires too much effort.

flush();
elements.emplace_back(
std::make_unique<EmoteElement>(emoteIt->second,
emoteFlags));
}
else
{
words.emplace_back(word);
}
}
flush();
upsertWords(elements, textElement);
}
else
{
Expand Down

0 comments on commit d106922

Please sign in to comment.