mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
optimize chatter list (#2814)
* optimize chatter list * changelog * Fix tests Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
parent
7659dc27ae
commit
3fddafb867
|
@ -5,6 +5,7 @@
|
|||
- 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: 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)
|
||||
- Bugfix: Fixed FFZ emote links for global emotes (#2807, #2808)
|
||||
|
||||
|
|
|
@ -127,6 +127,7 @@ SOURCES += \
|
|||
src/common/Args.cpp \
|
||||
src/common/Channel.cpp \
|
||||
src/common/ChannelChatters.cpp \
|
||||
src/common/ChatterSet.cpp \
|
||||
src/common/ChatterinoSetting.cpp \
|
||||
src/common/CompletionModel.cpp \
|
||||
src/common/Credentials.cpp \
|
||||
|
@ -139,7 +140,6 @@ SOURCES += \
|
|||
src/common/NetworkPrivate.cpp \
|
||||
src/common/NetworkRequest.cpp \
|
||||
src/common/NetworkResult.cpp \
|
||||
src/common/UsernameSet.cpp \
|
||||
src/common/Version.cpp \
|
||||
src/common/WindowDescriptors.cpp \
|
||||
src/common/QLogging.cpp \
|
||||
|
@ -340,6 +340,7 @@ HEADERS += \
|
|||
src/common/Atomic.hpp \
|
||||
src/common/Channel.hpp \
|
||||
src/common/ChannelChatters.hpp \
|
||||
src/common/ChatterSet.hpp \
|
||||
src/common/ChatterinoSetting.hpp \
|
||||
src/common/Common.hpp \
|
||||
src/common/CompletionModel.hpp \
|
||||
|
@ -363,7 +364,6 @@ HEADERS += \
|
|||
src/common/SignalVectorModel.hpp \
|
||||
src/common/Singleton.hpp \
|
||||
src/common/UniqueAccess.hpp \
|
||||
src/common/UsernameSet.hpp \
|
||||
src/common/Version.hpp \
|
||||
src/common/QLogging.hpp \
|
||||
src/controllers/accounts/Account.hpp \
|
||||
|
|
35
lib/lrucache/.clang-format
Normal file
35
lib/lrucache/.clang-format
Normal 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
|
|
@ -1,72 +1,115 @@
|
|||
/*
|
||||
/*
|
||||
* File: lrucache.hpp
|
||||
* Author: Alexander Ponomarev
|
||||
* Original Author: Alexander Ponomarev
|
||||
*
|
||||
* Created on June 20, 2013, 5:09 PM
|
||||
*/
|
||||
|
||||
#ifndef _LRUCACHE_HPP_INCLUDED_
|
||||
#define _LRUCACHE_HPP_INCLUDED_
|
||||
#define _LRUCACHE_HPP_INCLUDED_
|
||||
|
||||
#include <unordered_map>
|
||||
#include <list>
|
||||
#include <cstddef>
|
||||
#include <list>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace cache {
|
||||
|
||||
template<typename key_t, typename value_t>
|
||||
class lru_cache {
|
||||
template <typename key_t, typename value_t>
|
||||
class lru_cache
|
||||
{
|
||||
public:
|
||||
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::pair<key_t, value_t> key_value_pair_t;
|
||||
typedef typename std::list<key_value_pair_t>::iterator list_iterator_t;
|
||||
|
||||
lru_cache(size_t max_size)
|
||||
: _max_size(max_size)
|
||||
{
|
||||
}
|
||||
|
||||
// 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);
|
||||
_cache_items_list.push_front(key_value_pair_t(key, value));
|
||||
if (it != _cache_items_map.end())
|
||||
{
|
||||
_cache_items_list.erase(it->second);
|
||||
_cache_items_map.erase(it);
|
||||
}
|
||||
_cache_items_map[key] = _cache_items_list.begin();
|
||||
|
||||
if (_cache_items_map.size() > _max_size)
|
||||
{
|
||||
auto last = _cache_items_list.end();
|
||||
last--;
|
||||
_cache_items_map.erase(last->first);
|
||||
_cache_items_list.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
const value_t &get(const key_t &key)
|
||||
{
|
||||
auto it = _cache_items_map.find(key);
|
||||
if (it == _cache_items_map.end())
|
||||
{
|
||||
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);
|
||||
return it->second->second;
|
||||
}
|
||||
}
|
||||
|
||||
bool exists(const key_t &key) const
|
||||
{
|
||||
return _cache_items_map.find(key) != _cache_items_map.end();
|
||||
}
|
||||
|
||||
size_t size() const
|
||||
{
|
||||
return _cache_items_map.size();
|
||||
}
|
||||
|
||||
auto begin() const
|
||||
{
|
||||
return _cache_items_list.begin();
|
||||
}
|
||||
|
||||
auto end() const
|
||||
{
|
||||
return _cache_items_list.end();
|
||||
}
|
||||
|
||||
lru_cache(size_t max_size) :
|
||||
_max_size(max_size) {
|
||||
}
|
||||
|
||||
void put(const key_t& key, const value_t& value) {
|
||||
auto it = _cache_items_map.find(key);
|
||||
_cache_items_list.push_front(key_value_pair_t(key, value));
|
||||
if (it != _cache_items_map.end()) {
|
||||
_cache_items_list.erase(it->second);
|
||||
_cache_items_map.erase(it);
|
||||
}
|
||||
_cache_items_map[key] = _cache_items_list.begin();
|
||||
|
||||
if (_cache_items_map.size() > _max_size) {
|
||||
auto last = _cache_items_list.end();
|
||||
last--;
|
||||
_cache_items_map.erase(last->first);
|
||||
_cache_items_list.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
const value_t& get(const key_t& key) {
|
||||
auto it = _cache_items_map.find(key);
|
||||
if (it == _cache_items_map.end()) {
|
||||
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);
|
||||
return it->second->second;
|
||||
}
|
||||
}
|
||||
|
||||
bool exists(const key_t& key) const {
|
||||
return _cache_items_map.find(key) != _cache_items_map.end();
|
||||
}
|
||||
|
||||
size_t size() const {
|
||||
return _cache_items_map.size();
|
||||
}
|
||||
|
||||
private:
|
||||
std::list<key_value_pair_t> _cache_items_list;
|
||||
std::unordered_map<key_t, list_iterator_t> _cache_items_map;
|
||||
size_t _max_size;
|
||||
std::list<key_value_pair_t> _cache_items_list;
|
||||
std::unordered_map<key_t, list_iterator_t> _cache_items_map;
|
||||
size_t _max_size;
|
||||
};
|
||||
|
||||
} // namespace cache
|
||||
|
||||
#endif /* _LRUCACHE_HPP_INCLUDED_ */
|
||||
} // namespace cache
|
||||
|
||||
#endif /* _LRUCACHE_HPP_INCLUDED_ */
|
||||
|
|
|
@ -18,6 +18,8 @@ set(SOURCE_FILES main.cpp
|
|||
common/ChannelChatters.hpp
|
||||
common/ChatterinoSetting.cpp
|
||||
common/ChatterinoSetting.hpp
|
||||
common/ChatterSet.cpp
|
||||
common/ChatterSet.hpp
|
||||
common/CompletionModel.cpp
|
||||
common/CompletionModel.hpp
|
||||
common/Credentials.cpp
|
||||
|
@ -42,8 +44,6 @@ set(SOURCE_FILES main.cpp
|
|||
common/NetworkResult.hpp
|
||||
common/QLogging.cpp
|
||||
common/QLogging.hpp
|
||||
common/UsernameSet.cpp
|
||||
common/UsernameSet.hpp
|
||||
common/Version.cpp
|
||||
common/Version.hpp
|
||||
common/WindowDescriptors.cpp
|
||||
|
|
|
@ -11,14 +11,15 @@ ChannelChatters::ChannelChatters(Channel &channel)
|
|||
{
|
||||
}
|
||||
|
||||
SharedAccessGuard<const UsernameSet> ChannelChatters::accessChatters() const
|
||||
SharedAccessGuard<const ChatterSet> ChannelChatters::accessChatters() const
|
||||
{
|
||||
return this->chatters_.accessConst();
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -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)
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/ChatterSet.hpp"
|
||||
#include "common/UniqueAccess.hpp"
|
||||
#include "common/UsernameSet.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
|
||||
#include "lrucache/lrucache.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
|
||||
#include <QRgb>
|
||||
|
||||
|
@ -17,14 +16,14 @@ public:
|
|||
ChannelChatters(Channel &channel);
|
||||
virtual ~ChannelChatters() = default; // add vtable
|
||||
|
||||
SharedAccessGuard<const UsernameSet> accessChatters() const;
|
||||
SharedAccessGuard<const ChatterSet> accessChatters() const;
|
||||
|
||||
void addRecentChatter(const QString &user);
|
||||
void addJoinedUser(const QString &user);
|
||||
void addPartedUser(const QString &user);
|
||||
void setChatters(UsernameSet &&set);
|
||||
const QColor getUserColor(const QString &user);
|
||||
void setUserColor(const QString &user, const QColor &color);
|
||||
void updateOnlineChatters(const std::unordered_set<QString> &chatters);
|
||||
|
||||
private:
|
||||
static constexpr int maxChatterColorCount = 5000;
|
||||
|
@ -32,7 +31,7 @@ private:
|
|||
Channel &channel_;
|
||||
|
||||
// maps 2 char prefix to set of names
|
||||
UniqueAccess<UsernameSet> chatters_;
|
||||
UniqueAccess<ChatterSet> chatters_;
|
||||
UniqueAccess<cache::lru_cache<QString, QRgb>> chatterColors_;
|
||||
|
||||
// combines multiple joins/parts into one message
|
||||
|
|
58
src/common/ChatterSet.cpp
Normal file
58
src/common/ChatterSet.cpp
Normal 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
46
src/common/ChatterSet.hpp
Normal 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
|
|
@ -1,8 +1,8 @@
|
|||
#include "common/CompletionModel.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "common/ChatterSet.hpp"
|
||||
#include "common/Common.hpp"
|
||||
#include "common/UsernameSet.hpp"
|
||||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/commands/CommandController.hpp"
|
||||
#include "debug/Benchmark.hpp"
|
||||
|
@ -108,33 +108,31 @@ void CompletionModel::refresh(const QString &prefix, bool isFirstWord)
|
|||
}
|
||||
|
||||
// Usernames
|
||||
if (prefix.length() >= UsernameSet::PrefixLength)
|
||||
QString usernamePostfix =
|
||||
isFirstWord && getSettings()->mentionUsersWithComma ? ","
|
||||
: QString();
|
||||
|
||||
if (prefix.startsWith("@"))
|
||||
{
|
||||
auto usernames = channel->accessChatters();
|
||||
|
||||
QString usernamePrefix = prefix;
|
||||
QString usernamePostfix =
|
||||
isFirstWord && getSettings()->mentionUsersWithComma ? ","
|
||||
: QString();
|
||||
usernamePrefix.remove(0, 1);
|
||||
|
||||
if (usernamePrefix.startsWith("@"))
|
||||
auto chatters =
|
||||
channel->accessChatters()->filterByPrefix(usernamePrefix);
|
||||
|
||||
for (const auto &name : chatters)
|
||||
{
|
||||
usernamePrefix.remove(0, 1);
|
||||
for (const auto &name :
|
||||
usernames->subrange(Prefix(usernamePrefix)))
|
||||
{
|
||||
addString("@" + name + usernamePostfix,
|
||||
TaggedString::Type::Username);
|
||||
}
|
||||
addString("@" + name + usernamePostfix,
|
||||
TaggedString::Type::Username);
|
||||
}
|
||||
else if (!getSettings()->userCompletionOnlyWithAt)
|
||||
}
|
||||
else if (!getSettings()->userCompletionOnlyWithAt)
|
||||
{
|
||||
auto chatters = channel->accessChatters()->filterByPrefix(prefix);
|
||||
|
||||
for (const auto &name : chatters)
|
||||
{
|
||||
for (const auto &name :
|
||||
usernames->subrange(Prefix(usernamePrefix)))
|
||||
{
|
||||
addString(name + usernamePostfix,
|
||||
TaggedString::Type::Username);
|
||||
}
|
||||
addString(name + usernamePostfix, TaggedString::Type::Username);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ public:
|
|||
}
|
||||
|
||||
UniqueAccess(T &&element)
|
||||
: element_(element)
|
||||
: element_(std::move(element))
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@ public:
|
|||
|
||||
UniqueAccess<T> &operator=(T &&element)
|
||||
{
|
||||
this->element_ = element;
|
||||
this->element_ = std::move(element);
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -119,13 +119,14 @@ namespace {
|
|||
|
||||
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",
|
||||
"staff", "admins", "global_mods",
|
||||
"viewers"};
|
||||
|
||||
auto usernames = UsernameSet();
|
||||
auto usernames = std::unordered_set<QString>();
|
||||
|
||||
// parse json
|
||||
QJsonObject jsonCategories = jsonRoot.value("chatters").toObject();
|
||||
|
@ -823,7 +824,7 @@ void TwitchChannel::refreshChatters()
|
|||
auto pair = parseChatters(std::move(data));
|
||||
if (pair.first)
|
||||
{
|
||||
this->setChatters(std::move(pair.second));
|
||||
this->updateOnlineChatters(pair.second);
|
||||
}
|
||||
|
||||
return pair.first;
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
#include "common/Atomic.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "common/ChannelChatters.hpp"
|
||||
#include "common/ChatterSet.hpp"
|
||||
#include "common/Outcome.hpp"
|
||||
#include "common/UniqueAccess.hpp"
|
||||
#include "common/UsernameSet.hpp"
|
||||
#include "providers/twitch/ChannelPointReward.hpp"
|
||||
#include "providers/twitch/TwitchEmotes.hpp"
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
|
|
|
@ -519,11 +519,11 @@ void TwitchMessageBuilder::addTextOrEmoji(const QString &string_)
|
|||
|
||||
if (this->twitchChannel != nullptr && getSettings()->findAllUsernames)
|
||||
{
|
||||
auto chatters = this->twitchChannel->accessChatters();
|
||||
auto match = allUsernamesMentionRegex.match(string);
|
||||
QString username = match.captured(1);
|
||||
|
||||
if (match.hasMatch() && chatters->contains(username))
|
||||
if (match.hasMatch() &&
|
||||
this->twitchChannel->accessChatters()->contains(username))
|
||||
{
|
||||
auto originalTextColor = textColor;
|
||||
|
||||
|
|
|
@ -15,7 +15,9 @@ set(chatterino_SOURCES
|
|||
${CMAKE_SOURCE_DIR}/src/common/NetworkRequest.cpp
|
||||
${CMAKE_SOURCE_DIR}/src/common/NetworkResult.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
|
||||
|
||||
|
@ -39,7 +41,7 @@ set(test_SOURCES
|
|||
${CMAKE_CURRENT_LIST_DIR}/src/AccessGuard.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/NetworkCommon.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/Emojis.cpp
|
||||
)
|
||||
|
@ -68,6 +70,7 @@ target_link_libraries(${PROJECT_NAME} PRIVATE
|
|||
Pajlada::Settings
|
||||
Pajlada::Signals
|
||||
Threads::Threads
|
||||
LRUCache
|
||||
)
|
||||
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||
|
|
84
tests/src/ChatterSet.cpp
Normal file
84
tests/src/ChatterSet.cpp
Normal 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"));
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in a new issue