optimize chatter list (#2814)

* optimize chatter list

* changelog

* Fix tests

Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
fourtf 2021-05-24 12:13:59 +02:00 committed by GitHub
parent 7659dc27ae
commit 3fddafb867
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 371 additions and 532 deletions

View file

@ -5,6 +5,7 @@
- Minor: Added moderation buttons to search popup when searching in a split with moderation mode enabled. (#2148, #2803) - Minor: Added moderation buttons to search popup when searching in a split with moderation mode enabled. (#2148, #2803)
- Minor: Made "#channel" in `/mentions` tab show in usercards and in the search popup. (#2802) - Minor: Made "#channel" in `/mentions` tab show in usercards and in the search popup. (#2802)
- Minor: Added settings to disable custom FrankerFaceZ VIP/mod badges. (#2693, #2759) - Minor: Added settings to disable custom FrankerFaceZ VIP/mod badges. (#2693, #2759)
- Minor: Limit the number of recent chatters to improve memory usage and reduce freezes. (#2796, #2814)
- Minor: Added `/popout` command. Usage: `/popout [channel]`. It opens browser chat for the provided channel. Can also be used without arguments to open current channels browser chat. (#2556, #2812) - Minor: Added `/popout` command. Usage: `/popout [channel]`. It opens browser chat for the provided channel. Can also be used without arguments to open current channels browser chat. (#2556, #2812)
- Bugfix: Fixed FFZ emote links for global emotes (#2807, #2808) - Bugfix: Fixed FFZ emote links for global emotes (#2807, #2808)

View file

@ -127,6 +127,7 @@ SOURCES += \
src/common/Args.cpp \ src/common/Args.cpp \
src/common/Channel.cpp \ src/common/Channel.cpp \
src/common/ChannelChatters.cpp \ src/common/ChannelChatters.cpp \
src/common/ChatterSet.cpp \
src/common/ChatterinoSetting.cpp \ src/common/ChatterinoSetting.cpp \
src/common/CompletionModel.cpp \ src/common/CompletionModel.cpp \
src/common/Credentials.cpp \ src/common/Credentials.cpp \
@ -139,7 +140,6 @@ SOURCES += \
src/common/NetworkPrivate.cpp \ src/common/NetworkPrivate.cpp \
src/common/NetworkRequest.cpp \ src/common/NetworkRequest.cpp \
src/common/NetworkResult.cpp \ src/common/NetworkResult.cpp \
src/common/UsernameSet.cpp \
src/common/Version.cpp \ src/common/Version.cpp \
src/common/WindowDescriptors.cpp \ src/common/WindowDescriptors.cpp \
src/common/QLogging.cpp \ src/common/QLogging.cpp \
@ -340,6 +340,7 @@ HEADERS += \
src/common/Atomic.hpp \ src/common/Atomic.hpp \
src/common/Channel.hpp \ src/common/Channel.hpp \
src/common/ChannelChatters.hpp \ src/common/ChannelChatters.hpp \
src/common/ChatterSet.hpp \
src/common/ChatterinoSetting.hpp \ src/common/ChatterinoSetting.hpp \
src/common/Common.hpp \ src/common/Common.hpp \
src/common/CompletionModel.hpp \ src/common/CompletionModel.hpp \
@ -363,7 +364,6 @@ HEADERS += \
src/common/SignalVectorModel.hpp \ src/common/SignalVectorModel.hpp \
src/common/Singleton.hpp \ src/common/Singleton.hpp \
src/common/UniqueAccess.hpp \ src/common/UniqueAccess.hpp \
src/common/UsernameSet.hpp \
src/common/Version.hpp \ src/common/Version.hpp \
src/common/QLogging.hpp \ src/common/QLogging.hpp \
src/controllers/accounts/Account.hpp \ src/controllers/accounts/Account.hpp \

View file

@ -0,0 +1,35 @@
Language: Cpp
AccessModifierOffset: -4
AlignEscapedNewlinesLeft: true
AllowShortFunctionsOnASingleLine: false
AllowShortIfStatementsOnASingleLine: false
AllowShortLambdasOnASingleLine: Empty
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterDefinitionReturnType: false
AlwaysBreakBeforeMultilineStrings: false
BasedOnStyle: Google
BraceWrapping: {
AfterClass: 'true'
AfterControlStatement: 'true'
AfterFunction: 'true'
AfterNamespace: 'false'
BeforeCatch: 'true'
BeforeElse: 'true'
}
BreakBeforeBraces: Custom
BreakConstructorInitializersBeforeComma: true
ColumnLimit: 80
ConstructorInitializerAllOnOneLineOrOnePerLine: false
DerivePointerBinding: false
FixNamespaceComments: true
IndentCaseLabels: true
IndentWidth: 4
IndentWrappedFunctionNames: true
IndentPPDirectives: AfterHash
IncludeBlocks: Preserve
NamespaceIndentation: Inner
PointerBindsToType: false
SpacesBeforeTrailingComments: 2
Standard: Auto
ReflowComments: false

View file

@ -1,6 +1,6 @@
/* /*
* File: lrucache.hpp * File: lrucache.hpp
* Author: Alexander Ponomarev * Original Author: Alexander Ponomarev
* *
* Created on June 20, 2013, 5:09 PM * Created on June 20, 2013, 5:09 PM
*/ */
@ -8,33 +8,60 @@
#ifndef _LRUCACHE_HPP_INCLUDED_ #ifndef _LRUCACHE_HPP_INCLUDED_
#define _LRUCACHE_HPP_INCLUDED_ #define _LRUCACHE_HPP_INCLUDED_
#include <unordered_map>
#include <list>
#include <cstddef> #include <cstddef>
#include <list>
#include <stdexcept> #include <stdexcept>
#include <unordered_map>
namespace cache { namespace cache {
template <typename key_t, typename value_t> template <typename key_t, typename value_t>
class lru_cache { class lru_cache
{
public: public:
typedef typename std::pair<key_t, value_t> key_value_pair_t; typedef typename std::pair<key_t, value_t> key_value_pair_t;
typedef typename std::list<key_value_pair_t>::iterator list_iterator_t; typedef typename std::list<key_value_pair_t>::iterator list_iterator_t;
lru_cache(size_t max_size) : lru_cache(size_t max_size)
_max_size(max_size) { : _max_size(max_size)
{
} }
void put(const key_t& key, const value_t& value) { // copy doesn't make sense since we reference the linked list elements directly
lru_cache(lru_cache<key_t, value_t> &) = delete;
lru_cache<key_t, value_t> &operator=(lru_cache<key_t, value_t> &) = delete;
// move
lru_cache(lru_cache<key_t, value_t> &&other)
: _cache_items_list(std::move(other._cache_items_list))
, _cache_items_map(std::move(other._cache_items_map))
{
other._cache_items_list.clear();
other._cache_items_map.clear();
}
lru_cache<key_t, value_t> &operator=(lru_cache<key_t, value_t> &&other)
{
_cache_items_list = std::move(other._cache_items_list);
_cache_items_map = std::move(other._cache_items_map);
other._cache_items_list.clear();
other._cache_items_map.clear();
return *this;
}
void put(const key_t &key, const value_t &value)
{
auto it = _cache_items_map.find(key); auto it = _cache_items_map.find(key);
_cache_items_list.push_front(key_value_pair_t(key, value)); _cache_items_list.push_front(key_value_pair_t(key, value));
if (it != _cache_items_map.end()) { if (it != _cache_items_map.end())
{
_cache_items_list.erase(it->second); _cache_items_list.erase(it->second);
_cache_items_map.erase(it); _cache_items_map.erase(it);
} }
_cache_items_map[key] = _cache_items_list.begin(); _cache_items_map[key] = _cache_items_list.begin();
if (_cache_items_map.size() > _max_size) { if (_cache_items_map.size() > _max_size)
{
auto last = _cache_items_list.end(); auto last = _cache_items_list.end();
last--; last--;
_cache_items_map.erase(last->first); _cache_items_map.erase(last->first);
@ -42,24 +69,41 @@ public:
} }
} }
const value_t& get(const key_t& key) { const value_t &get(const key_t &key)
{
auto it = _cache_items_map.find(key); auto it = _cache_items_map.find(key);
if (it == _cache_items_map.end()) { if (it == _cache_items_map.end())
{
throw std::range_error("There is no such key in cache"); throw std::range_error("There is no such key in cache");
} else { }
_cache_items_list.splice(_cache_items_list.begin(), _cache_items_list, it->second); else
{
_cache_items_list.splice(_cache_items_list.begin(),
_cache_items_list, it->second);
return it->second->second; return it->second->second;
} }
} }
bool exists(const key_t& key) const { bool exists(const key_t &key) const
{
return _cache_items_map.find(key) != _cache_items_map.end(); return _cache_items_map.find(key) != _cache_items_map.end();
} }
size_t size() const { size_t size() const
{
return _cache_items_map.size(); return _cache_items_map.size();
} }
auto begin() const
{
return _cache_items_list.begin();
}
auto end() const
{
return _cache_items_list.end();
}
private: private:
std::list<key_value_pair_t> _cache_items_list; std::list<key_value_pair_t> _cache_items_list;
std::unordered_map<key_t, list_iterator_t> _cache_items_map; std::unordered_map<key_t, list_iterator_t> _cache_items_map;
@ -69,4 +113,3 @@ private:
} // namespace cache } // namespace cache
#endif /* _LRUCACHE_HPP_INCLUDED_ */ #endif /* _LRUCACHE_HPP_INCLUDED_ */

View file

@ -18,6 +18,8 @@ set(SOURCE_FILES main.cpp
common/ChannelChatters.hpp common/ChannelChatters.hpp
common/ChatterinoSetting.cpp common/ChatterinoSetting.cpp
common/ChatterinoSetting.hpp common/ChatterinoSetting.hpp
common/ChatterSet.cpp
common/ChatterSet.hpp
common/CompletionModel.cpp common/CompletionModel.cpp
common/CompletionModel.hpp common/CompletionModel.hpp
common/Credentials.cpp common/Credentials.cpp
@ -42,8 +44,6 @@ set(SOURCE_FILES main.cpp
common/NetworkResult.hpp common/NetworkResult.hpp
common/QLogging.cpp common/QLogging.cpp
common/QLogging.hpp common/QLogging.hpp
common/UsernameSet.cpp
common/UsernameSet.hpp
common/Version.cpp common/Version.cpp
common/Version.hpp common/Version.hpp
common/WindowDescriptors.cpp common/WindowDescriptors.cpp

View file

@ -11,14 +11,15 @@ ChannelChatters::ChannelChatters(Channel &channel)
{ {
} }
SharedAccessGuard<const UsernameSet> ChannelChatters::accessChatters() const SharedAccessGuard<const ChatterSet> ChannelChatters::accessChatters() const
{ {
return this->chatters_.accessConst(); return this->chatters_.accessConst();
} }
void ChannelChatters::addRecentChatter(const QString &user) void ChannelChatters::addRecentChatter(const QString &user)
{ {
this->chatters_.access()->insert(user); auto chatters = this->chatters_.access();
chatters->addRecentChatter(user);
} }
void ChannelChatters::addJoinedUser(const QString &user) void ChannelChatters::addJoinedUser(const QString &user)
@ -66,9 +67,11 @@ void ChannelChatters::addPartedUser(const QString &user)
} }
} }
void ChannelChatters::setChatters(UsernameSet &&set) void ChannelChatters::updateOnlineChatters(
const std::unordered_set<QString> &chatters)
{ {
this->chatters_.access()->merge(std::move(set)); auto chatters_ = this->chatters_.access();
chatters_->updateOnlineChatters(chatters);
} }
const QColor ChannelChatters::getUserColor(const QString &user) const QColor ChannelChatters::getUserColor(const QString &user)

View file

@ -1,11 +1,10 @@
#pragma once #pragma once
#include "common/Channel.hpp" #include "common/Channel.hpp"
#include "common/ChatterSet.hpp"
#include "common/UniqueAccess.hpp" #include "common/UniqueAccess.hpp"
#include "common/UsernameSet.hpp"
#include "util/QStringHash.hpp"
#include "lrucache/lrucache.hpp" #include "lrucache/lrucache.hpp"
#include "util/QStringHash.hpp"
#include <QRgb> #include <QRgb>
@ -17,14 +16,14 @@ public:
ChannelChatters(Channel &channel); ChannelChatters(Channel &channel);
virtual ~ChannelChatters() = default; // add vtable virtual ~ChannelChatters() = default; // add vtable
SharedAccessGuard<const UsernameSet> accessChatters() const; SharedAccessGuard<const ChatterSet> accessChatters() const;
void addRecentChatter(const QString &user); void addRecentChatter(const QString &user);
void addJoinedUser(const QString &user); void addJoinedUser(const QString &user);
void addPartedUser(const QString &user); void addPartedUser(const QString &user);
void setChatters(UsernameSet &&set);
const QColor getUserColor(const QString &user); const QColor getUserColor(const QString &user);
void setUserColor(const QString &user, const QColor &color); void setUserColor(const QString &user, const QColor &color);
void updateOnlineChatters(const std::unordered_set<QString> &chatters);
private: private:
static constexpr int maxChatterColorCount = 5000; static constexpr int maxChatterColorCount = 5000;
@ -32,7 +31,7 @@ private:
Channel &channel_; Channel &channel_;
// maps 2 char prefix to set of names // maps 2 char prefix to set of names
UniqueAccess<UsernameSet> chatters_; UniqueAccess<ChatterSet> chatters_;
UniqueAccess<cache::lru_cache<QString, QRgb>> chatterColors_; UniqueAccess<cache::lru_cache<QString, QRgb>> chatterColors_;
// combines multiple joins/parts into one message // combines multiple joins/parts into one message

58
src/common/ChatterSet.cpp Normal file
View file

@ -0,0 +1,58 @@
#include "common/ChatterSet.hpp"
#include <tuple>
#include "debug/Benchmark.hpp"
namespace chatterino {
ChatterSet::ChatterSet()
: items(chatterLimit)
{
}
void ChatterSet::addRecentChatter(const QString &userName)
{
this->items.put(userName.toLower(), userName);
}
void ChatterSet::updateOnlineChatters(
const std::unordered_set<QString> &lowerCaseUsernames)
{
BenchmarkGuard bench("update online chatters");
// Create a new lru cache without the users that are not present anymore.
cache::lru_cache<QString, QString> tmp(chatterLimit);
for (auto &&chatter : lowerCaseUsernames)
{
if (this->items.exists(chatter))
tmp.put(chatter, this->items.get(chatter));
// Less chatters than the limit => try to preserve as many as possible.
else if (lowerCaseUsernames.size() < chatterLimit)
tmp.put(chatter, chatter);
}
this->items = std::move(tmp);
}
bool ChatterSet::contains(const QString &userName) const
{
return this->items.exists(userName.toLower());
}
std::vector<QString> ChatterSet::filterByPrefix(const QString &prefix) const
{
QString lowerPrefix = prefix.toLower();
std::vector<QString> result;
for (auto &&item : this->items)
{
if (item.first.startsWith(lowerPrefix))
result.push_back(item.second);
}
return result;
}
} // namespace chatterino

46
src/common/ChatterSet.hpp Normal file
View file

@ -0,0 +1,46 @@
#pragma once
#include <QString>
#include <functional>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include "lrucache/lrucache.hpp"
#include "util/QStringHash.hpp"
namespace chatterino {
/// ChatterSet is a limited container that contains a list of recent chatters
/// that can be referenced by name.
class ChatterSet
{
public:
/// The limit of how many chatters can be saved for a channel.
static constexpr size_t chatterLimit = 2000;
ChatterSet();
/// Inserts a user name if it isn't contained. Doesn't replace the original
/// if the casing hasn't changed.
void addRecentChatter(const QString &userName);
/// Removes chatters that aren't online anymore. Adds chatters that aren't
/// in the list yet.
void updateOnlineChatters(
const std::unordered_set<QString> &lowerCaseUsernames);
/// Checks if a username is in the list.
bool contains(const QString &userName) const;
/// Get filtered usernames by a prefix for autocompletion. Contained items
/// are in mixed case if available.
std::vector<QString> filterByPrefix(const QString &prefix) const;
private:
// user name in lower case -> user name in normal case
cache::lru_cache<QString, QString> items;
};
using ChatterSet = ChatterSet;
} // namespace chatterino

View file

@ -1,8 +1,8 @@
#include "common/CompletionModel.hpp" #include "common/CompletionModel.hpp"
#include "Application.hpp" #include "Application.hpp"
#include "common/ChatterSet.hpp"
#include "common/Common.hpp" #include "common/Common.hpp"
#include "common/UsernameSet.hpp"
#include "controllers/accounts/AccountController.hpp" #include "controllers/accounts/AccountController.hpp"
#include "controllers/commands/CommandController.hpp" #include "controllers/commands/CommandController.hpp"
#include "debug/Benchmark.hpp" #include "debug/Benchmark.hpp"
@ -108,20 +108,19 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
} }
// Usernames // Usernames
if (prefix.length() >= UsernameSet::PrefixLength)
{
auto usernames = channel->accessChatters();
QString usernamePrefix = prefix;
QString usernamePostfix = QString usernamePostfix =
isFirstWord && getSettings()->mentionUsersWithComma ? "," isFirstWord && getSettings()->mentionUsersWithComma ? ","
: QString(); : QString();
if (usernamePrefix.startsWith("@")) if (prefix.startsWith("@"))
{ {
QString usernamePrefix = prefix;
usernamePrefix.remove(0, 1); usernamePrefix.remove(0, 1);
for (const auto &name :
usernames->subrange(Prefix(usernamePrefix))) auto chatters =
channel->accessChatters()->filterByPrefix(usernamePrefix);
for (const auto &name : chatters)
{ {
addString("@" + name + usernamePostfix, addString("@" + name + usernamePostfix,
TaggedString::Type::Username); TaggedString::Type::Username);
@ -129,12 +128,11 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
} }
else if (!getSettings()->userCompletionOnlyWithAt) else if (!getSettings()->userCompletionOnlyWithAt)
{ {
for (const auto &name : auto chatters = channel->accessChatters()->filterByPrefix(prefix);
usernames->subrange(Prefix(usernamePrefix)))
for (const auto &name : chatters)
{ {
addString(name + usernamePostfix, addString(name + usernamePostfix, TaggedString::Type::Username);
TaggedString::Type::Username);
}
} }
} }

View file

@ -64,7 +64,7 @@ public:
} }
UniqueAccess(T &&element) UniqueAccess(T &&element)
: element_(element) : element_(std::move(element))
{ {
} }
@ -76,7 +76,7 @@ public:
UniqueAccess<T> &operator=(T &&element) UniqueAccess<T> &operator=(T &&element)
{ {
this->element_ = element; this->element_ = std::move(element);
return *this; return *this;
} }

View file

@ -1,182 +0,0 @@
#include "UsernameSet.hpp"
#include <tuple>
namespace chatterino {
namespace {
std::pair<UsernameSet::Iterator, bool> findOrErase(
std::set<QString, CaseInsensitiveLess> &set, const QString &value)
{
if (!value.isLower())
{
auto iter = set.find(value);
if (iter != set.end())
{
if (QString::compare(*iter, value, Qt::CaseSensitive) != 0)
{
set.erase(iter);
}
else
{
return {iter, false};
}
}
}
return {set.end(), true};
}
} // namespace
//
// UsernameSet
//
UsernameSet::ConstIterator UsernameSet::begin() const
{
return this->items.begin();
}
UsernameSet::ConstIterator UsernameSet::end() const
{
return this->items.end();
}
UsernameSet::Range UsernameSet::subrange(const Prefix &prefix) const
{
auto it = this->firstKeyForPrefix.find(prefix);
if (it != this->firstKeyForPrefix.end())
{
auto start = this->items.find(it->second);
auto end = start;
while (end != this->items.end() && prefix.isStartOf(*end))
{
end++;
}
return {start, end};
}
return {this->items.end(), this->items.end()};
}
std::set<QString>::size_type UsernameSet::size() const
{
return this->items.size();
}
std::pair<UsernameSet::Iterator, bool> UsernameSet::insert(const QString &value)
{
auto pair = findOrErase(this->items, value);
if (!pair.second)
{
return pair;
}
this->insertPrefix(value);
return this->items.insert(value);
}
std::pair<UsernameSet::Iterator, bool> UsernameSet::insert(QString &&value)
{
auto pair = findOrErase(this->items, value);
if (!pair.second)
{
return pair;
}
this->insertPrefix(value);
return this->items.insert(std::move(value));
}
void UsernameSet::insertPrefix(const QString &value)
{
auto &string = this->firstKeyForPrefix[Prefix(value)];
if (string.isNull() || value.compare(string, Qt::CaseInsensitive) < 0)
string = value;
}
bool UsernameSet::contains(const QString &value) const
{
return this->items.count(value) == 1;
}
void UsernameSet::merge(UsernameSet &&set)
{
for (auto it = this->items.begin(); it != this->items.end();)
{
auto iter = set.items.find(*it);
if (iter == set.items.end())
{
it = this->items.erase(it);
}
else
{
++it;
}
}
this->items.merge(set.items);
this->firstKeyForPrefix = set.firstKeyForPrefix;
}
//
// Range
//
UsernameSet::Range::Range(ConstIterator start, ConstIterator end)
: start_(start)
, end_(end)
{
}
UsernameSet::ConstIterator UsernameSet::Range::begin()
{
return this->start_;
}
UsernameSet::ConstIterator UsernameSet::Range::end()
{
return this->end_;
}
//
// Prefix
//
Prefix::Prefix(const QString &string)
: first(string.size() >= 1 ? string[0].toLower() : '\0')
, second(string.size() >= 2 ? string[1].toLower() : '\0')
{
}
bool Prefix::operator==(const Prefix &other) const
{
return std::tie(this->first, this->second) ==
std::tie(other.first, other.second);
}
bool Prefix::operator!=(const Prefix &other) const
{
return !(*this == other);
}
bool Prefix::isStartOf(const QString &string) const
{
if (string.size() == 0)
{
return this->first == QChar('\0') && this->second == QChar('\0');
}
else if (string.size() == 1)
{
return this->first == string[0].toLower() &&
this->second == QChar('\0');
}
else
{
return this->first == string[0].toLower() &&
this->second == string[1].toLower();
}
}
} // namespace chatterino

View file

@ -1,89 +0,0 @@
#pragma once
#include <QString>
#include <functional>
#include <set>
#include <unordered_map>
namespace chatterino {
class Prefix
{
public:
Prefix(const QString &string);
bool operator==(const Prefix &other) const;
bool operator!=(const Prefix &other) const;
bool isStartOf(const QString &string) const;
private:
QChar first;
QChar second;
friend struct std::hash<Prefix>;
};
} // namespace chatterino
namespace std {
template <>
struct hash<chatterino::Prefix> {
size_t operator()(const chatterino::Prefix &prefix) const
{
return (size_t(prefix.first.unicode()) << 16) |
size_t(prefix.second.unicode());
}
};
} // namespace std
namespace chatterino {
struct CaseInsensitiveLess {
bool operator()(const QString &lhs, const QString &rhs) const
{
return lhs.compare(rhs, Qt::CaseInsensitive) < 0;
}
};
class UsernameSet
{
public:
static constexpr int PrefixLength = 2;
using Iterator = std::set<QString>::iterator;
using ConstIterator = std::set<QString>::const_iterator;
class Range
{
public:
Range(ConstIterator start, ConstIterator end);
ConstIterator begin();
ConstIterator end();
private:
ConstIterator start_;
ConstIterator end_;
};
ConstIterator begin() const;
ConstIterator end() const;
Range subrange(const Prefix &prefix) const;
std::set<QString>::size_type size() const;
std::pair<Iterator, bool> insert(const QString &value);
std::pair<Iterator, bool> insert(QString &&value);
bool contains(const QString &value) const;
void merge(UsernameSet &&set);
private:
void insertPrefix(const QString &string);
std::set<QString, CaseInsensitiveLess> items;
std::unordered_map<Prefix, QString> firstKeyForPrefix;
};
} // namespace chatterino

View file

@ -119,13 +119,14 @@ namespace {
return messages; return messages;
} }
std::pair<Outcome, UsernameSet> parseChatters(const QJsonObject &jsonRoot) std::pair<Outcome, std::unordered_set<QString>> parseChatters(
const QJsonObject &jsonRoot)
{ {
static QStringList categories = {"broadcaster", "vips", "moderators", static QStringList categories = {"broadcaster", "vips", "moderators",
"staff", "admins", "global_mods", "staff", "admins", "global_mods",
"viewers"}; "viewers"};
auto usernames = UsernameSet(); auto usernames = std::unordered_set<QString>();
// parse json // parse json
QJsonObject jsonCategories = jsonRoot.value("chatters").toObject(); QJsonObject jsonCategories = jsonRoot.value("chatters").toObject();
@ -823,7 +824,7 @@ void TwitchChannel::refreshChatters()
auto pair = parseChatters(std::move(data)); auto pair = parseChatters(std::move(data));
if (pair.first) if (pair.first)
{ {
this->setChatters(std::move(pair.second)); this->updateOnlineChatters(pair.second);
} }
return pair.first; return pair.first;

View file

@ -4,9 +4,9 @@
#include "common/Atomic.hpp" #include "common/Atomic.hpp"
#include "common/Channel.hpp" #include "common/Channel.hpp"
#include "common/ChannelChatters.hpp" #include "common/ChannelChatters.hpp"
#include "common/ChatterSet.hpp"
#include "common/Outcome.hpp" #include "common/Outcome.hpp"
#include "common/UniqueAccess.hpp" #include "common/UniqueAccess.hpp"
#include "common/UsernameSet.hpp"
#include "providers/twitch/ChannelPointReward.hpp" #include "providers/twitch/ChannelPointReward.hpp"
#include "providers/twitch/TwitchEmotes.hpp" #include "providers/twitch/TwitchEmotes.hpp"
#include "providers/twitch/api/Helix.hpp" #include "providers/twitch/api/Helix.hpp"

View file

@ -519,11 +519,11 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
if (this->twitchChannel != nullptr && getSettings()->findAllUsernames) if (this->twitchChannel != nullptr && getSettings()->findAllUsernames)
{ {
auto chatters = this->twitchChannel->accessChatters();
auto match = allUsernamesMentionRegex.match(string); auto match = allUsernamesMentionRegex.match(string);
QString username = match.captured(1); QString username = match.captured(1);
if (match.hasMatch() && chatters->contains(username)) if (match.hasMatch() &&
this->twitchChannel->accessChatters()->contains(username))
{ {
auto originalTextColor = textColor; auto originalTextColor = textColor;

View file

@ -15,7 +15,9 @@ set(chatterino_SOURCES
${CMAKE_SOURCE_DIR}/src/common/NetworkRequest.cpp ${CMAKE_SOURCE_DIR}/src/common/NetworkRequest.cpp
${CMAKE_SOURCE_DIR}/src/common/NetworkResult.cpp ${CMAKE_SOURCE_DIR}/src/common/NetworkResult.cpp
${CMAKE_SOURCE_DIR}/src/common/QLogging.cpp ${CMAKE_SOURCE_DIR}/src/common/QLogging.cpp
${CMAKE_SOURCE_DIR}/src/common/UsernameSet.cpp ${CMAKE_SOURCE_DIR}/src/common/ChatterSet.cpp
${CMAKE_SOURCE_DIR}/src/debug/Benchmark.cpp
${CMAKE_SOURCE_DIR}/src/controllers/highlights/HighlightPhrase.cpp ${CMAKE_SOURCE_DIR}/src/controllers/highlights/HighlightPhrase.cpp
@ -39,7 +41,7 @@ set(test_SOURCES
${CMAKE_CURRENT_LIST_DIR}/src/AccessGuard.cpp ${CMAKE_CURRENT_LIST_DIR}/src/AccessGuard.cpp
${CMAKE_CURRENT_LIST_DIR}/src/NetworkCommon.cpp ${CMAKE_CURRENT_LIST_DIR}/src/NetworkCommon.cpp
${CMAKE_CURRENT_LIST_DIR}/src/NetworkRequest.cpp ${CMAKE_CURRENT_LIST_DIR}/src/NetworkRequest.cpp
${CMAKE_CURRENT_LIST_DIR}/src/UsernameSet.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ChatterSet.cpp
${CMAKE_CURRENT_LIST_DIR}/src/HighlightPhrase.cpp ${CMAKE_CURRENT_LIST_DIR}/src/HighlightPhrase.cpp
${CMAKE_CURRENT_LIST_DIR}/src/Emojis.cpp ${CMAKE_CURRENT_LIST_DIR}/src/Emojis.cpp
) )
@ -68,6 +70,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE
Pajlada::Settings Pajlada::Settings
Pajlada::Signals Pajlada::Signals
Threads::Threads Threads::Threads
LRUCache
) )
target_compile_definitions(${PROJECT_NAME} PRIVATE target_compile_definitions(${PROJECT_NAME} PRIVATE

84
tests/src/ChatterSet.cpp Normal file
View file

@ -0,0 +1,84 @@
#include "common/ChatterSet.hpp"
#include <gtest/gtest.h>
#include <QStringList>
TEST(ChatterSet, insert)
{
chatterino::ChatterSet set;
EXPECT_FALSE(set.contains("pajlada"));
EXPECT_FALSE(set.contains("Pajlada"));
set.addRecentChatter("pajlada");
EXPECT_TRUE(set.contains("pajlada"));
EXPECT_TRUE(set.contains("Pajlada"));
set.addRecentChatter("pajlada");
EXPECT_TRUE(set.contains("pajlada"));
EXPECT_TRUE(set.contains("Pajlada"));
set.addRecentChatter("PAJLADA");
EXPECT_TRUE(set.contains("pajlada"));
EXPECT_TRUE(set.contains("Pajlada"));
}
TEST(ChatterSet, MaxSize)
{
chatterino::ChatterSet set;
EXPECT_FALSE(set.contains("pajlada"));
EXPECT_FALSE(set.contains("Pajlada"));
set.addRecentChatter("pajlada");
EXPECT_TRUE(set.contains("pajlada"));
EXPECT_TRUE(set.contains("Pajlada"));
// After adding CHATTER_LIMIT-1 additional chatters, pajlada should still be in the set
for (auto i = 0; i < chatterino::ChatterSet::chatterLimit - 1; ++i)
{
set.addRecentChatter(QString("%1").arg(i));
}
EXPECT_TRUE(set.contains("pajlada"));
EXPECT_TRUE(set.contains("Pajlada"));
// But adding one more chatter should bump pajlada out of the set
set.addRecentChatter("notpajlada");
EXPECT_FALSE(set.contains("pajlada"));
EXPECT_FALSE(set.contains("Pajlada"));
}
TEST(ChatterSet, MaxSizeLastUsed)
{
chatterino::ChatterSet set;
EXPECT_FALSE(set.contains("pajlada"));
EXPECT_FALSE(set.contains("Pajlada"));
set.addRecentChatter("pajlada");
EXPECT_TRUE(set.contains("pajlada"));
EXPECT_TRUE(set.contains("Pajlada"));
// After adding CHATTER_LIMIT-1 additional chatters, pajlada should still be in the set
for (auto i = 0; i < chatterino::ChatterSet::chatterLimit - 1; ++i)
{
set.addRecentChatter(QString("%1").arg(i));
}
EXPECT_TRUE(set.contains("pajlada"));
EXPECT_TRUE(set.contains("Pajlada"));
// Bump pajlada as a recent chatter
set.addRecentChatter("pajlada");
// After another CHATTER_LIMIT-1 additional chatters, pajlada should still be there
for (auto i = 0; i < chatterino::ChatterSet::chatterLimit - 1; ++i)
{
set.addRecentChatter(QString("new-%1").arg(i));
}
EXPECT_TRUE(set.contains("pajlada"));
EXPECT_TRUE(set.contains("Pajlada"));
}

View file

@ -1,161 +0,0 @@
#include "common/UsernameSet.hpp"
#include <gtest/gtest.h>
#include <QStringList>
chatterino::Prefix prefix_pajlada(QString("pajlada"));
chatterino::Prefix prefix_Pajlada(QString("Pajlada"));
chatterino::Prefix prefix_randers(QString("randers"));
chatterino::Prefix prefix_Chancu(QString("ch"));
TEST(Prefix, isStartOf)
{
EXPECT_TRUE(prefix_pajlada.isStartOf("pajlada"));
EXPECT_TRUE(prefix_pajlada.isStartOf("Pajlada"));
EXPECT_FALSE(prefix_pajlada.isStartOf("randers"));
EXPECT_TRUE(prefix_Pajlada.isStartOf("pajlada"));
EXPECT_TRUE(prefix_Pajlada.isStartOf("Pajlada"));
EXPECT_TRUE(prefix_Pajlada.isStartOf("pajbot"));
EXPECT_TRUE(prefix_Pajlada.isStartOf("Pajbot"));
EXPECT_FALSE(prefix_Pajlada.isStartOf("randers"));
}
TEST(Prefix, EqualsOperator)
{
EXPECT_EQ(prefix_pajlada, prefix_Pajlada);
EXPECT_NE(prefix_pajlada, prefix_randers);
}
TEST(UsernameSet, insert)
{
std::pair<chatterino::UsernameSet::Iterator, bool> p;
chatterino::UsernameSet set;
EXPECT_EQ(set.size(), 0);
p = set.insert("pajlada");
EXPECT_TRUE(p.second);
EXPECT_EQ(set.size(), 1);
p = set.insert("pajlada");
EXPECT_FALSE(p.second);
EXPECT_EQ(set.size(), 1);
// Non-lowercase variant should override full lowercase variant
p = set.insert("PAJLADA");
EXPECT_TRUE(p.second);
EXPECT_EQ(set.size(), 1);
// Lowercase variant should not override non-lowercase variant
p = set.insert("pajlada");
EXPECT_FALSE(p.second);
EXPECT_EQ(set.size(), 1);
p = set.insert("pajbot");
EXPECT_TRUE(p.second);
EXPECT_EQ(set.size(), 2);
p = set.insert("pajbot");
EXPECT_FALSE(p.second);
EXPECT_EQ(set.size(), 2);
p = set.insert("Pajbot");
EXPECT_TRUE(p.second);
EXPECT_EQ(set.size(), 2);
p = set.insert("PAJBOT");
EXPECT_TRUE(p.second);
EXPECT_EQ(set.size(), 2);
// Same uppercase should not result in a change
p = set.insert("PAJBOT");
EXPECT_FALSE(p.second);
EXPECT_EQ(set.size(), 2);
}
TEST(UsernameSet, CollisionTest)
{
QString s;
chatterino::UsernameSet set;
chatterino::Prefix prefix("not_");
set.insert("pajlada");
set.insert("Chancu");
set.insert("chief_tony");
set.insert("ChodzacyKac");
set.insert("ChatAbuser");
set.insert("Normies_GTFO");
set.insert("not_remzy");
set.insert("Mullo2500");
set.insert("muggedbyapie");
EXPECT_EQ(set.size(), 9);
{
QStringList result;
QStringList expectation{"Normies_GTFO", "not_remzy"};
auto subrange = set.subrange(QString("not_"));
std::copy(subrange.begin(), subrange.end(), std::back_inserter(result));
EXPECT_EQ(expectation, result);
}
{
QStringList result;
QStringList expectation{};
auto subrange = set.subrange(QString("te"));
std::copy(subrange.begin(), subrange.end(), std::back_inserter(result));
EXPECT_EQ(expectation, result);
}
{
QStringList result;
QStringList expectation{"pajlada"};
auto subrange = set.subrange(QString("PA"));
std::copy(subrange.begin(), subrange.end(), std::back_inserter(result));
EXPECT_EQ(expectation, result);
}
{
QStringList result;
QStringList expectation{"pajlada"};
auto subrange = set.subrange(QString("pajlada"));
std::copy(subrange.begin(), subrange.end(), std::back_inserter(result));
EXPECT_EQ(expectation, result);
}
{
QStringList result;
QStringList expectation{"pajlada"};
auto subrange = set.subrange(QString("Pajl"));
std::copy(subrange.begin(), subrange.end(), std::back_inserter(result));
EXPECT_EQ(expectation, result);
}
{
QStringList result;
QStringList expectation{"Chancu", "ChatAbuser", "chief_tony",
"ChodzacyKac"};
auto subrange = set.subrange(QString("chan"));
std::copy(subrange.begin(), subrange.end(), std::back_inserter(result));
EXPECT_EQ(expectation, result);
}
{
QStringList result;
QStringList expectation{"muggedbyapie", "Mullo2500"};
auto subrange = set.subrange(QString("mu"));
std::copy(subrange.begin(), subrange.end(), std::back_inserter(result));
EXPECT_EQ(expectation, result);
}
}