mirror of
https://github.com/Chatterino/chatterino2.git
synced 2024-11-13 19:49:51 +01:00
Initial backend test for user-based data/customizations (#4144)
Right now only support for colors and no real UX, idea is to test it & allow the idea to grow while figuring out the UX
This commit is contained in:
parent
a9d3c00369
commit
1eabda8668
|
@ -22,6 +22,7 @@ Checks: '-*,
|
|||
-performance-noexcept-move-constructor,
|
||||
-misc-non-private-member-variables-in-classes,
|
||||
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||
-cppcoreguidelines-special-member-functions,
|
||||
-modernize-use-nodiscard,
|
||||
-modernize-use-trailing-return-type,
|
||||
-readability-identifier-length,
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include "controllers/hotkeys/HotkeyController.hpp"
|
||||
#include "controllers/ignores/IgnoreController.hpp"
|
||||
#include "controllers/notifications/NotificationController.hpp"
|
||||
#include "controllers/userdata/UserDataController.hpp"
|
||||
#include "debug/AssertInGuiThread.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "providers/bttv/BttvEmotes.hpp"
|
||||
|
@ -76,6 +77,7 @@ Application::Application(Settings &_settings, Paths &_paths)
|
|||
, chatterinoBadges(&this->emplace<ChatterinoBadges>())
|
||||
, ffzBadges(&this->emplace<FfzBadges>())
|
||||
, seventvBadges(&this->emplace<SeventvBadges>())
|
||||
, userData(&this->emplace<UserDataController>())
|
||||
, logging(&this->emplace<Logging>())
|
||||
{
|
||||
this->instance = this;
|
||||
|
@ -224,6 +226,11 @@ IEmotes *Application::getEmotes()
|
|||
return this->emotes;
|
||||
}
|
||||
|
||||
IUserDataController *Application::getUserData()
|
||||
{
|
||||
return this->userData;
|
||||
}
|
||||
|
||||
void Application::save()
|
||||
{
|
||||
for (auto &singleton : this->singletons_)
|
||||
|
|
|
@ -17,6 +17,8 @@ class AccountController;
|
|||
class NotificationController;
|
||||
class HighlightController;
|
||||
class HotkeyController;
|
||||
class IUserDataController;
|
||||
class UserDataController;
|
||||
|
||||
class Theme;
|
||||
class WindowManager;
|
||||
|
@ -53,6 +55,7 @@ public:
|
|||
virtual TwitchIrcServer *getTwitch() = 0;
|
||||
virtual ChatterinoBadges *getChatterinoBadges() = 0;
|
||||
virtual FfzBadges *getFfzBadges() = 0;
|
||||
virtual IUserDataController *getUserData() = 0;
|
||||
};
|
||||
|
||||
class Application : public IApplication
|
||||
|
@ -89,6 +92,7 @@ public:
|
|||
ChatterinoBadges *const chatterinoBadges{};
|
||||
FfzBadges *const ffzBadges{};
|
||||
SeventvBadges *const seventvBadges{};
|
||||
UserDataController *const userData{};
|
||||
|
||||
/*[[deprecated]]*/ Logging *const logging{};
|
||||
|
||||
|
@ -141,6 +145,7 @@ public:
|
|||
{
|
||||
return this->ffzBadges;
|
||||
}
|
||||
IUserDataController *getUserData() override;
|
||||
|
||||
private:
|
||||
void addSingleton(Singleton *singleton);
|
||||
|
|
|
@ -125,6 +125,10 @@ set(SOURCE_FILES
|
|||
controllers/pings/MutedChannelModel.cpp
|
||||
controllers/pings/MutedChannelModel.hpp
|
||||
|
||||
controllers/userdata/UserDataController.cpp
|
||||
controllers/userdata/UserDataController.hpp
|
||||
controllers/userdata/UserData.hpp
|
||||
|
||||
debug/Benchmark.cpp
|
||||
debug/Benchmark.hpp
|
||||
|
||||
|
@ -364,6 +368,8 @@ set(SOURCE_FILES
|
|||
util/WindowsHelper.cpp
|
||||
util/WindowsHelper.hpp
|
||||
|
||||
util/serialize/Container.hpp
|
||||
|
||||
widgets/AccountSwitchPopup.cpp
|
||||
widgets/AccountSwitchPopup.hpp
|
||||
widgets/AccountSwitchWidget.cpp
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#include "controllers/commands/Command.hpp"
|
||||
#include "controllers/commands/CommandModel.hpp"
|
||||
#include "controllers/commands/builtin/twitch/ChatSettings.hpp"
|
||||
#include "controllers/userdata/UserDataController.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "messages/MessageElement.hpp"
|
||||
|
@ -3037,6 +3038,22 @@ void CommandController::initialize(Settings &, Paths &paths)
|
|||
|
||||
return "";
|
||||
});
|
||||
|
||||
this->registerCommand("/unstable-set-user-color", [](const auto &ctx) {
|
||||
auto userID = ctx.words.at(1);
|
||||
if (ctx.words.size() < 2)
|
||||
{
|
||||
ctx.channel->addMessage(
|
||||
makeSystemMessage(QString("Usage: %1 <TwitchUserID> [color]")
|
||||
.arg(ctx.words.at(0))));
|
||||
}
|
||||
|
||||
auto color = ctx.words.value(2);
|
||||
|
||||
getIApp()->getUserData()->setUserColor(userID, color);
|
||||
|
||||
return "";
|
||||
});
|
||||
}
|
||||
|
||||
void CommandController::save()
|
||||
|
|
71
src/controllers/userdata/UserData.hpp
Normal file
71
src/controllers/userdata/UserData.hpp
Normal file
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
|
||||
#include "util/RapidJsonSerializeQString.hpp"
|
||||
#include "util/RapidjsonHelpers.hpp"
|
||||
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
#include <boost/optional.hpp>
|
||||
#include <pajlada/serialize.hpp>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
// UserData defines a set of data that is defined for a unique user
|
||||
// It can contain things like optional replacement color for the user, a unique alias
|
||||
// or a user note that should be displayed with the user
|
||||
// Replacement fields should be optional, where none denotes that the field should not be updated for the user
|
||||
struct UserData {
|
||||
boost::optional<QColor> color{boost::none};
|
||||
|
||||
// TODO: User note?
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
||||
|
||||
namespace pajlada {
|
||||
|
||||
template <>
|
||||
struct Serialize<chatterino::UserData> {
|
||||
static rapidjson::Value get(const chatterino::UserData &value,
|
||||
rapidjson::Document::AllocatorType &a)
|
||||
{
|
||||
rapidjson::Value obj;
|
||||
obj.SetObject();
|
||||
if (value.color)
|
||||
{
|
||||
const auto &color = *value.color;
|
||||
chatterino::rj::set(obj, "color",
|
||||
color.name().toUtf8().toStdString(), a);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct Deserialize<chatterino::UserData> {
|
||||
static chatterino::UserData get(const rapidjson::Value &value,
|
||||
bool *error = nullptr)
|
||||
{
|
||||
if (!value.IsObject())
|
||||
{
|
||||
PAJLADA_REPORT_ERROR(error)
|
||||
return chatterino::UserData{};
|
||||
}
|
||||
|
||||
chatterino::UserData user;
|
||||
|
||||
QString colorString;
|
||||
if (chatterino::rj::getSafe(value, "color", colorString))
|
||||
{
|
||||
QColor color(colorString);
|
||||
if (color.isValid())
|
||||
{
|
||||
user.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace pajlada
|
100
src/controllers/userdata/UserDataController.cpp
Normal file
100
src/controllers/userdata/UserDataController.cpp
Normal file
|
@ -0,0 +1,100 @@
|
|||
#include "controllers/userdata/UserDataController.hpp"
|
||||
|
||||
#include "singletons/Paths.hpp"
|
||||
#include "util/CombinePath.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
using namespace chatterino;
|
||||
|
||||
std::shared_ptr<pajlada::Settings::SettingManager> initSettingsInstance()
|
||||
{
|
||||
auto sm = std::make_shared<pajlada::Settings::SettingManager>();
|
||||
|
||||
auto *paths = getPaths();
|
||||
|
||||
auto path = combinePath(paths->settingsDirectory, "user-data.json");
|
||||
|
||||
sm->setPath(path.toUtf8().toStdString());
|
||||
|
||||
sm->setBackupEnabled(true);
|
||||
sm->setBackupSlots(9);
|
||||
sm->saveMethod =
|
||||
pajlada::Settings::SettingManager::SaveMethod::SaveAllTheTime;
|
||||
|
||||
return sm;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
UserDataController::UserDataController()
|
||||
: sm(initSettingsInstance())
|
||||
, setting("/users", this->sm)
|
||||
{
|
||||
this->sm->load();
|
||||
this->users = this->setting.getValue();
|
||||
}
|
||||
|
||||
void UserDataController::save()
|
||||
{
|
||||
this->sm->save();
|
||||
}
|
||||
|
||||
boost::optional<UserData> UserDataController::getUser(
|
||||
const QString &userID) const
|
||||
{
|
||||
std::shared_lock lock(this->usersMutex);
|
||||
auto it = this->users.find(userID);
|
||||
|
||||
if (it == this->users.end())
|
||||
{
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
return it->second;
|
||||
}
|
||||
|
||||
std::unordered_map<QString, UserData> UserDataController::getUsers() const
|
||||
{
|
||||
std::shared_lock lock(this->usersMutex);
|
||||
return this->users;
|
||||
}
|
||||
|
||||
void UserDataController::setUserColor(const QString &userID,
|
||||
const QString &colorString)
|
||||
{
|
||||
auto c = this->getUsers();
|
||||
auto it = c.find(userID);
|
||||
boost::optional<QColor> finalColor =
|
||||
boost::make_optional(!colorString.isEmpty(), QColor(colorString));
|
||||
if (it == c.end())
|
||||
{
|
||||
if (!finalColor)
|
||||
{
|
||||
// Early out - user is not configured and will not get a new color
|
||||
return;
|
||||
}
|
||||
|
||||
UserData user;
|
||||
user.color = finalColor;
|
||||
c.insert({userID, user});
|
||||
}
|
||||
else
|
||||
{
|
||||
it->second.color = finalColor;
|
||||
}
|
||||
|
||||
this->update(std::move(c));
|
||||
}
|
||||
|
||||
void UserDataController::update(
|
||||
std::unordered_map<QString, UserData> &&newUsers)
|
||||
{
|
||||
std::unique_lock lock(this->usersMutex);
|
||||
this->users = std::move(newUsers);
|
||||
this->setting.setValue(this->users);
|
||||
}
|
||||
|
||||
} // namespace chatterino
|
60
src/controllers/userdata/UserDataController.hpp
Normal file
60
src/controllers/userdata/UserDataController.hpp
Normal file
|
@ -0,0 +1,60 @@
|
|||
#pragma once
|
||||
|
||||
#include "common/Singleton.hpp"
|
||||
#include "controllers/userdata/UserData.hpp"
|
||||
#include "util/QStringHash.hpp"
|
||||
#include "util/RapidJsonSerializeQString.hpp"
|
||||
#include "util/RapidjsonHelpers.hpp"
|
||||
#include "util/serialize/Container.hpp"
|
||||
|
||||
#include <QColor>
|
||||
#include <QString>
|
||||
#include <boost/optional.hpp>
|
||||
#include <pajlada/settings.hpp>
|
||||
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace chatterino {
|
||||
|
||||
class IUserDataController
|
||||
{
|
||||
public:
|
||||
virtual ~IUserDataController() = default;
|
||||
|
||||
virtual boost::optional<UserData> getUser(const QString &userID) const = 0;
|
||||
|
||||
virtual void setUserColor(const QString &userID,
|
||||
const QString &colorString) = 0;
|
||||
};
|
||||
|
||||
class UserDataController : public IUserDataController, public Singleton
|
||||
{
|
||||
public:
|
||||
UserDataController();
|
||||
|
||||
// Get extra data about a user
|
||||
// If the user does not have any extra data, return none
|
||||
boost::optional<UserData> getUser(const QString &userID) const override;
|
||||
|
||||
// Update or insert extra data for the user's color override
|
||||
void setUserColor(const QString &userID,
|
||||
const QString &colorString) override;
|
||||
|
||||
protected:
|
||||
void save() override;
|
||||
|
||||
private:
|
||||
void update(std::unordered_map<QString, UserData> &&newUsers);
|
||||
|
||||
std::unordered_map<QString, UserData> getUsers() const;
|
||||
|
||||
// Stores a real-time list of users & their customizations
|
||||
std::unordered_map<QString, UserData> users;
|
||||
mutable std::shared_mutex usersMutex;
|
||||
|
||||
std::shared_ptr<pajlada::Settings::SettingManager> sm;
|
||||
pajlada::Settings::Setting<std::unordered_map<QString, UserData>> setting;
|
||||
};
|
||||
|
||||
} // namespace chatterino
|
|
@ -4,6 +4,7 @@
|
|||
#include "controllers/accounts/AccountController.hpp"
|
||||
#include "controllers/ignores/IgnoreController.hpp"
|
||||
#include "controllers/ignores/IgnorePhrase.hpp"
|
||||
#include "controllers/userdata/UserDataController.hpp"
|
||||
#include "messages/Message.hpp"
|
||||
#include "providers/chatterino/ChatterinoBadges.hpp"
|
||||
#include "providers/ffz/FfzBadges.hpp"
|
||||
|
@ -691,6 +692,18 @@ void TwitchMessageBuilder::parseRoomID()
|
|||
|
||||
void TwitchMessageBuilder::parseUsernameColor()
|
||||
{
|
||||
const auto *userData = getIApp()->getUserData();
|
||||
assert(userData != nullptr);
|
||||
|
||||
if (const auto &user = userData->getUser(this->userId_))
|
||||
{
|
||||
if (user->color)
|
||||
{
|
||||
this->usernameColor_ = user->color.value();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const auto iterator = this->tags.find("color");
|
||||
if (iterator != this->tags.end())
|
||||
{
|
||||
|
|
51
src/util/serialize/Container.hpp
Normal file
51
src/util/serialize/Container.hpp
Normal file
|
@ -0,0 +1,51 @@
|
|||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <pajlada/serialize.hpp>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
namespace pajlada {
|
||||
|
||||
template <typename ValueType, typename RJValue>
|
||||
struct Serialize<std::unordered_map<QString, ValueType>, RJValue> {
|
||||
static RJValue get(const std::unordered_map<QString, ValueType> &value,
|
||||
typename RJValue::AllocatorType &a)
|
||||
{
|
||||
RJValue ret(rapidjson::kObjectType);
|
||||
|
||||
for (auto it = value.begin(); it != value.end(); ++it)
|
||||
{
|
||||
detail::AddMember<ValueType, RJValue>(ret, it->first.toUtf8(),
|
||||
it->second, a);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename ValueType, typename RJValue>
|
||||
struct Deserialize<std::unordered_map<QString, ValueType>, RJValue> {
|
||||
static std::unordered_map<QString, ValueType> get(const RJValue &value,
|
||||
bool *error = nullptr)
|
||||
{
|
||||
std::unordered_map<QString, ValueType> ret;
|
||||
|
||||
if (!value.IsObject())
|
||||
{
|
||||
PAJLADA_REPORT_ERROR(error)
|
||||
return ret;
|
||||
}
|
||||
|
||||
for (typename RJValue::ConstMemberIterator it = value.MemberBegin();
|
||||
it != value.MemberEnd(); ++it)
|
||||
{
|
||||
ret.emplace(it->name.GetString(),
|
||||
Deserialize<ValueType, RJValue>::get(it->value, error));
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace pajlada
|
|
@ -1,7 +1,9 @@
|
|||
#include "controllers/highlights/HighlightController.hpp"
|
||||
|
||||
#include "Application.hpp"
|
||||
#include "BaseSettings.hpp"
|
||||
#include "messages/MessageBuilder.hpp" // for MessageParseArgs
|
||||
#include "mocks/UserData.hpp"
|
||||
#include "providers/twitch/TwitchBadge.hpp" // for Badge
|
||||
#include "providers/twitch/api/Helix.hpp"
|
||||
|
||||
|
@ -73,9 +75,14 @@ public:
|
|||
{
|
||||
return nullptr;
|
||||
}
|
||||
IUserDataController *getUserData() override
|
||||
{
|
||||
return &this->userData;
|
||||
}
|
||||
|
||||
AccountController accounts;
|
||||
HighlightController highlights;
|
||||
mock::UserDataController userData;
|
||||
// TODO: Figure this out
|
||||
};
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
#include "Application.hpp"
|
||||
#include "common/Channel.hpp"
|
||||
#include "messages/MessageBuilder.hpp"
|
||||
#include "mocks/UserData.hpp"
|
||||
#include "providers/twitch/TwitchBadge.hpp"
|
||||
#include "singletons/Emotes.hpp"
|
||||
|
||||
|
@ -73,8 +74,13 @@ public:
|
|||
{
|
||||
return nullptr;
|
||||
}
|
||||
IUserDataController *getUserData() override
|
||||
{
|
||||
return &this->userData;
|
||||
}
|
||||
|
||||
Emotes emotes;
|
||||
mock::UserDataController userData;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
|
27
tests/src/mocks/UserData.hpp
Normal file
27
tests/src/mocks/UserData.hpp
Normal file
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include "controllers/userdata/UserDataController.hpp"
|
||||
|
||||
namespace chatterino::mock {
|
||||
|
||||
class UserDataController : public IUserDataController
|
||||
{
|
||||
public:
|
||||
UserDataController() = default;
|
||||
|
||||
// Get extra data about a user
|
||||
// If the user does not have any extra data, return none
|
||||
boost::optional<UserData> getUser(const QString &userID) const override
|
||||
{
|
||||
return boost::none;
|
||||
}
|
||||
|
||||
// Update or insert extra data for the user's color override
|
||||
void setUserColor(const QString &userID,
|
||||
const QString &colorString) override
|
||||
{
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace chatterino::mock
|
Loading…
Reference in a new issue