Add support for plugin links (#6386)
Co-authored-by: Nerixyz <nerixdev@outlook.de>
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
- Minor: Badges now link to their home page like emotes in the context menu. (#6437)
|
- Minor: Badges now link to their home page like emotes in the context menu. (#6437)
|
||||||
- Minor: Fixed usercard resizing improperly without recent messages. (#6496)
|
- Minor: Fixed usercard resizing improperly without recent messages. (#6496)
|
||||||
- Minor: Added setting for character limit of deleted messages. (#6491)
|
- Minor: Added setting for character limit of deleted messages. (#6491)
|
||||||
|
- Minor: Added link support to plugin message API. (#6386)
|
||||||
- Dev: Update release documentation. (#6498)
|
- Dev: Update release documentation. (#6498)
|
||||||
- Dev: Make code sanitizers opt in with the `CHATTERINO_SANITIZER_SUPPORT` CMake option. After that's enabled, use the `SANITIZE_*` flag to enable individual sanitizers. (#6493)
|
- Dev: Make code sanitizers opt in with the `CHATTERINO_SANITIZER_SUPPORT` CMake option. After that's enabled, use the `SANITIZE_*` flag to enable individual sanitizers. (#6493)
|
||||||
- Dev: Remove unused QTextCodec includes. (#6487)
|
- Dev: Remove unused QTextCodec includes. (#6487)
|
||||||
|
|||||||
Vendored
+15
@@ -187,6 +187,7 @@ declare namespace c2 {
|
|||||||
interface MessageElementInitBase {
|
interface MessageElementInitBase {
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
trailing_space?: boolean;
|
trailing_space?: boolean;
|
||||||
|
link?: Link;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageColor = "text" | "link" | "system" | string;
|
type MessageColor = "text" | "link" | "system" | string;
|
||||||
@@ -242,6 +243,20 @@ declare namespace c2 {
|
|||||||
type: "reply-curve";
|
type: "reply-curve";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Link {
|
||||||
|
type: LinkType;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LinkType {
|
||||||
|
Url,
|
||||||
|
UserInfo,
|
||||||
|
UserAction,
|
||||||
|
JumpToChannel,
|
||||||
|
CopyToClipboard,
|
||||||
|
JumpToMessage,
|
||||||
|
}
|
||||||
|
|
||||||
enum MessageFlag {
|
enum MessageFlag {
|
||||||
None = 0,
|
None = 0,
|
||||||
System = 0,
|
System = 0,
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ c2.Message = {}
|
|||||||
---@class MessageElementInitBase
|
---@class MessageElementInitBase
|
||||||
---@field tooltip? string Tooltip text
|
---@field tooltip? string Tooltip text
|
||||||
---@field trailing_space? boolean Whether to add a trailing space after the element (default: true)
|
---@field trailing_space? boolean Whether to add a trailing space after the element (default: true)
|
||||||
|
---@field link? c2.Link An action when clicking on this element. Mention and Link elements don't support this. They manage the link themselves.
|
||||||
|
|
||||||
---@alias MessageColor "text"|"link"|"system"|string A color for a text element - "text", "link", and "system" are special values that take the current theme into account
|
---@alias MessageColor "text"|"link"|"system"|string A color for a text element - "text", "link", and "system" are special values that take the current theme into account
|
||||||
|
|
||||||
@@ -344,6 +345,17 @@ c2.Message = {}
|
|||||||
---@param init MessageInit The message initialization table
|
---@param init MessageInit The message initialization table
|
||||||
---@return c2.Message msg The new message
|
---@return c2.Message msg The new message
|
||||||
function c2.Message.new(init) end
|
function c2.Message.new(init) end
|
||||||
|
---@alias c2.Link { type: c2.LinkType, value: string } A link on a message element.
|
||||||
|
---@enum c2.LinkType
|
||||||
|
c2.LinkType = {
|
||||||
|
Url = {}, ---@type c2.LinkType.Url
|
||||||
|
UserInfo = {}, ---@type c2.LinkType.UserInfo
|
||||||
|
UserAction = {}, ---@type c2.LinkType.UserAction
|
||||||
|
JumpToChannel = {}, ---@type c2.LinkType.JumpToChannel
|
||||||
|
CopyToClipboard = {}, ---@type c2.LinkType.CopyToClipboard
|
||||||
|
JumpToMessage = {}, ---@type c2.LinkType.JumpToMessage
|
||||||
|
}
|
||||||
|
|
||||||
-- Begin src/singletons/Fonts.hpp
|
-- Begin src/singletons/Fonts.hpp
|
||||||
|
|
||||||
---@enum c2.FontStyle
|
---@enum c2.FontStyle
|
||||||
|
|||||||
@@ -609,6 +609,19 @@ end)
|
|||||||
|
|
||||||
The full range of options can be found in the typing files ([LuaLS](./plugin-meta.lua), [TypeScript](./chatterino.d.ts)).
|
The full range of options can be found in the typing files ([LuaLS](./plugin-meta.lua), [TypeScript](./chatterino.d.ts)).
|
||||||
|
|
||||||
|
#### `LinkType` enum
|
||||||
|
|
||||||
|
This table describes links available to plugins.
|
||||||
|
|
||||||
|
| `LinkType` | `c2.Link.value` content | Action on click | Example |
|
||||||
|
| ----------------- | ---------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------- |
|
||||||
|
| `Url` | Any URI that makes sense to open | Open Link in browser | `https://example.org` |
|
||||||
|
| `UserInfo` | A Twitch username or `id:TwitchID` | Open a usercard | `mm2pl`, `id:117691339` |
|
||||||
|
| `UserAction` | Command to run or message to send | Send command/message | `/timeout mm2pl 1s test`, `!spoilers` |
|
||||||
|
| `JumpToChannel` | [Channel name](#channelget_name) | Go to already open split with given channel | `#pajlada` |
|
||||||
|
| `CopyToClipboard` | Any Unicode text | Copy value to clipboard | n/a |
|
||||||
|
| `JumpToMessage` | ID of the message | Highlight the message with given ID in current split, do nothing if it was not found | n/a |
|
||||||
|
|
||||||
### Input/Output API
|
### Input/Output API
|
||||||
|
|
||||||
These functions are wrappers for Lua's I/O library. Functions on file pointer
|
These functions are wrappers for Lua's I/O library. Functions on file pointer
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ class Reader:
|
|||||||
for line in line[opener:closer].split(",")
|
for line in line[opener:closer].split(",")
|
||||||
]
|
]
|
||||||
break
|
break
|
||||||
if line.startswith("enum class"):
|
if line.startswith("enum "):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if waiting_for_end:
|
if waiting_for_end:
|
||||||
|
|||||||
@@ -234,6 +234,8 @@ void PluginController::initSol(sol::state_view &lua, Plugin *plugin)
|
|||||||
c2["MessageElementFlag"] = lua::createEnumTable<MessageElementFlag>(lua);
|
c2["MessageElementFlag"] = lua::createEnumTable<MessageElementFlag>(lua);
|
||||||
c2["FontStyle"] = lua::createEnumTable<FontStyle>(lua);
|
c2["FontStyle"] = lua::createEnumTable<FontStyle>(lua);
|
||||||
c2["MessageContext"] = lua::createEnumTable<MessageContext>(lua);
|
c2["MessageContext"] = lua::createEnumTable<MessageContext>(lua);
|
||||||
|
c2["LinkType"] =
|
||||||
|
lua::createEnumTable<lua::api::message::ExposedLinkType>(lua);
|
||||||
|
|
||||||
sol::table io = g["io"];
|
sol::table io = g["io"];
|
||||||
io.set_function(
|
io.set_function(
|
||||||
|
|||||||
@@ -3,8 +3,10 @@
|
|||||||
|
|
||||||
# include "Application.hpp"
|
# include "Application.hpp"
|
||||||
# include "common/QLogging.hpp"
|
# include "common/QLogging.hpp"
|
||||||
|
# include "controllers/plugins/api/Message.hpp"
|
||||||
# include "controllers/plugins/LuaAPI.hpp"
|
# include "controllers/plugins/LuaAPI.hpp"
|
||||||
# include "controllers/plugins/PluginController.hpp"
|
# include "controllers/plugins/PluginController.hpp"
|
||||||
|
# include "messages/Link.hpp"
|
||||||
|
|
||||||
# include <QObject>
|
# include <QObject>
|
||||||
# include <QStringBuilder>
|
# include <QStringBuilder>
|
||||||
@@ -187,6 +189,48 @@ int sol_lua_push(sol::types<chatterino::lua::ThisPluginState>, lua_State *L,
|
|||||||
|
|
||||||
} // namespace chatterino::lua
|
} // namespace chatterino::lua
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
// Link
|
||||||
|
bool sol_lua_check(sol::types<chatterino::Link>, lua_State *L, int index,
|
||||||
|
chatterino::FunctionRef<sol::check_handler_type> handler,
|
||||||
|
sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
return sol::stack::check<sol::table>(L, index, handler, tracking);
|
||||||
|
}
|
||||||
|
|
||||||
|
chatterino::Link sol_lua_get(sol::types<chatterino::Link>, lua_State *L,
|
||||||
|
int index, sol::stack::record &tracking)
|
||||||
|
{
|
||||||
|
sol::table table = sol::stack::get<sol::table>(L, index, tracking);
|
||||||
|
|
||||||
|
auto ty =
|
||||||
|
table.get<sol::optional<lua::api::message::ExposedLinkType>>("type");
|
||||||
|
if (!ty)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Missing 'type' in Link");
|
||||||
|
}
|
||||||
|
auto value = table.get<sol::optional<QString>>("value");
|
||||||
|
if (!value)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("Missing 'value' in Link");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {static_cast<Link::Type>(*ty), *value};
|
||||||
|
}
|
||||||
|
|
||||||
|
int sol_lua_push(sol::types<chatterino::Link>, lua_State *L,
|
||||||
|
const chatterino::Link &value)
|
||||||
|
{
|
||||||
|
sol::table table = sol::table::create(L, 0, 2);
|
||||||
|
table.set("type",
|
||||||
|
static_cast<lua::api::message::ExposedLinkType>(value.type));
|
||||||
|
table.set("value", value.value);
|
||||||
|
return sol::stack::push(L, table);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
// NOLINTEND(readability-named-parameter)
|
// NOLINTEND(readability-named-parameter)
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ constexpr bool IsOptional<std::optional<T>> = true;
|
|||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
|
|
||||||
class Plugin;
|
class Plugin;
|
||||||
|
struct Link;
|
||||||
|
|
||||||
} // namespace chatterino
|
} // namespace chatterino
|
||||||
|
|
||||||
@@ -189,6 +190,12 @@ SOL_STACK_FUNCTIONS(chatterino::lua::ThisPluginState)
|
|||||||
|
|
||||||
} // namespace chatterino::lua
|
} // namespace chatterino::lua
|
||||||
|
|
||||||
|
namespace chatterino {
|
||||||
|
|
||||||
|
SOL_STACK_FUNCTIONS(chatterino::Link)
|
||||||
|
|
||||||
|
} // namespace chatterino
|
||||||
|
|
||||||
SOL_STACK_FUNCTIONS(QString)
|
SOL_STACK_FUNCTIONS(QString)
|
||||||
SOL_STACK_FUNCTIONS(QStringList)
|
SOL_STACK_FUNCTIONS(QStringList)
|
||||||
SOL_STACK_FUNCTIONS(QByteArray)
|
SOL_STACK_FUNCTIONS(QByteArray)
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
#include "controllers/plugins/api/Message.hpp"
|
|
||||||
|
|
||||||
#include "Application.hpp"
|
|
||||||
#include "messages/MessageElement.hpp"
|
|
||||||
|
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "controllers/plugins/api/Message.hpp"
|
||||||
|
|
||||||
|
# include "Application.hpp"
|
||||||
|
# include "controllers/plugins/LuaUtilities.hpp"
|
||||||
# include "controllers/plugins/SolTypes.hpp"
|
# include "controllers/plugins/SolTypes.hpp"
|
||||||
# include "messages/Message.hpp"
|
# include "messages/Message.hpp"
|
||||||
|
# include "messages/MessageElement.hpp"
|
||||||
|
|
||||||
# include <sol/sol.hpp>
|
# include <sol/sol.hpp>
|
||||||
|
|
||||||
@@ -128,6 +127,7 @@ std::unique_ptr<MessageElement> elementFromTable(const sol::table &tbl)
|
|||||||
{
|
{
|
||||||
auto type = requiredGet<QString>(tbl, "type");
|
auto type = requiredGet<QString>(tbl, "type");
|
||||||
std::unique_ptr<MessageElement> el;
|
std::unique_ptr<MessageElement> el;
|
||||||
|
bool linksAllowed = true;
|
||||||
if (type == u"text")
|
if (type == u"text")
|
||||||
{
|
{
|
||||||
el = textElementFromTable(tbl);
|
el = textElementFromTable(tbl);
|
||||||
@@ -139,6 +139,7 @@ std::unique_ptr<MessageElement> elementFromTable(const sol::table &tbl)
|
|||||||
else if (type == u"mention")
|
else if (type == u"mention")
|
||||||
{
|
{
|
||||||
el = mentionElementFromTable(tbl);
|
el = mentionElementFromTable(tbl);
|
||||||
|
linksAllowed = false;
|
||||||
}
|
}
|
||||||
else if (type == u"timestamp")
|
else if (type == u"timestamp")
|
||||||
{
|
{
|
||||||
@@ -155,6 +156,7 @@ std::unique_ptr<MessageElement> elementFromTable(const sol::table &tbl)
|
|||||||
else if (type == u"reply-curve")
|
else if (type == u"reply-curve")
|
||||||
{
|
{
|
||||||
el = replyCurveElementFromTable();
|
el = replyCurveElementFromTable();
|
||||||
|
linksAllowed = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -163,7 +165,54 @@ std::unique_ptr<MessageElement> elementFromTable(const sol::table &tbl)
|
|||||||
assert(el);
|
assert(el);
|
||||||
|
|
||||||
el->setTrailingSpace(tbl.get_or("trailing_space", true));
|
el->setTrailingSpace(tbl.get_or("trailing_space", true));
|
||||||
el->setTooltip(tbl.get_or("tooltip", QString{}));
|
|
||||||
|
auto link = tbl.get<sol::optional<Link>>("link");
|
||||||
|
if (link)
|
||||||
|
{
|
||||||
|
if (!linksAllowed)
|
||||||
|
{
|
||||||
|
throw std::runtime_error("'link' not supported on type='" +
|
||||||
|
type.toStdString() + '\'');
|
||||||
|
}
|
||||||
|
el->setLink(*link);
|
||||||
|
QString tooltip;
|
||||||
|
|
||||||
|
switch (link->type)
|
||||||
|
{
|
||||||
|
case Link::Url:
|
||||||
|
tooltip = QString("<b>URL:</b> %1").arg(link->value);
|
||||||
|
break;
|
||||||
|
case Link::UserAction:
|
||||||
|
tooltip = QString("<b>Command:</b> %1").arg(link->value);
|
||||||
|
break;
|
||||||
|
case Link::CopyToClipboard:
|
||||||
|
tooltip = "<b>Copy to clipboard</b>";
|
||||||
|
break;
|
||||||
|
|
||||||
|
// these links should be safe to click as they don't have any immediate action associated with them
|
||||||
|
case Link::JumpToChannel:
|
||||||
|
case Link::JumpToMessage:
|
||||||
|
case Link::UserInfo:
|
||||||
|
case Link::UserWhisper:
|
||||||
|
case Link::ReplyToMessage:
|
||||||
|
break;
|
||||||
|
|
||||||
|
// these types are not exposed to plugins
|
||||||
|
case Link::None:
|
||||||
|
case Link::AutoModAllow:
|
||||||
|
case Link::AutoModDeny:
|
||||||
|
case Link::InsertText:
|
||||||
|
case Link::OpenAccountsPage:
|
||||||
|
case Link::Reconnect:
|
||||||
|
throw std::runtime_error(
|
||||||
|
"Invalid link type. How'd this happen?");
|
||||||
|
}
|
||||||
|
el->setTooltip(tooltip);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
el->setTooltip(tbl.get_or("tooltip", QString{}));
|
||||||
|
}
|
||||||
|
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||||
|
# include "messages/Link.hpp"
|
||||||
# include "messages/Message.hpp"
|
# include "messages/Message.hpp"
|
||||||
|
|
||||||
# include <sol/forward.hpp>
|
# include <sol/forward.hpp>
|
||||||
|
|
||||||
|
# include <cstdint>
|
||||||
|
|
||||||
namespace chatterino::lua::api::message {
|
namespace chatterino::lua::api::message {
|
||||||
|
|
||||||
/* @lua-fragment
|
/* @lua-fragment
|
||||||
@@ -32,6 +35,7 @@ c2.Message = {}
|
|||||||
---@class MessageElementInitBase
|
---@class MessageElementInitBase
|
||||||
---@field tooltip? string Tooltip text
|
---@field tooltip? string Tooltip text
|
||||||
---@field trailing_space? boolean Whether to add a trailing space after the element (default: true)
|
---@field trailing_space? boolean Whether to add a trailing space after the element (default: true)
|
||||||
|
---@field link? c2.Link An action when clicking on this element. Mention and Link elements don't support this. They manage the link themselves.
|
||||||
|
|
||||||
---@alias MessageColor "text"|"link"|"system"|string A color for a text element - "text", "link", and "system" are special values that take the current theme into account
|
---@alias MessageColor "text"|"link"|"system"|string A color for a text element - "text", "link", and "system" are special values that take the current theme into account
|
||||||
|
|
||||||
@@ -86,6 +90,20 @@ c2.Message = {}
|
|||||||
function c2.Message.new(init) end
|
function c2.Message.new(init) end
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** @lua@alias c2.Link { type: c2.LinkType, value: string } A link on a message element. */
|
||||||
|
|
||||||
|
// We only want certain links to be usable
|
||||||
|
// Note: code dependant on this needs these values to be meaningful `Link::Type`s
|
||||||
|
/** @exposeenum c2.LinkType */
|
||||||
|
enum class ExposedLinkType : std::uint8_t {
|
||||||
|
Url = Link::Type::Url,
|
||||||
|
UserInfo = Link::Type::UserInfo,
|
||||||
|
UserAction = Link::Type::UserAction, // run a command/send message
|
||||||
|
JumpToChannel = Link::Type::JumpToChannel,
|
||||||
|
CopyToClipboard = Link::Type::CopyToClipboard,
|
||||||
|
JumpToMessage = Link::Type::JumpToMessage,
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @includefile singletons/Fonts.hpp
|
* @includefile singletons/Fonts.hpp
|
||||||
* @includefile messages/MessageElement.hpp
|
* @includefile messages/MessageElement.hpp
|
||||||
|
|||||||
@@ -9,13 +9,9 @@ public:
|
|||||||
enum Type {
|
enum Type {
|
||||||
None,
|
None,
|
||||||
Url,
|
Url,
|
||||||
CloseCurrentSplit,
|
|
||||||
UserInfo,
|
UserInfo,
|
||||||
UserTimeout,
|
|
||||||
UserBan,
|
|
||||||
UserWhisper,
|
UserWhisper,
|
||||||
InsertText,
|
InsertText,
|
||||||
ShowMessage,
|
|
||||||
UserAction,
|
UserAction,
|
||||||
AutoModAllow,
|
AutoModAllow,
|
||||||
AutoModDeny,
|
AutoModDeny,
|
||||||
|
|||||||
@@ -1106,6 +1106,7 @@ void TimestampElement::addToContainer(MessageLayoutContainer &container,
|
|||||||
{
|
{
|
||||||
if (ctx.flags.hasAny(this->getFlags()))
|
if (ctx.flags.hasAny(this->getFlags()))
|
||||||
{
|
{
|
||||||
|
this->setTooltip(this->getTooltip());
|
||||||
if (getSettings()->timestampFormat != this->format_)
|
if (getSettings()->timestampFormat != this->format_)
|
||||||
{
|
{
|
||||||
this->format_ = getSettings()->timestampFormat.getValue();
|
this->format_ = getSettings()->timestampFormat.getValue();
|
||||||
@@ -1122,8 +1123,19 @@ TextElement *TimestampElement::formatTime(const QTime &time)
|
|||||||
|
|
||||||
QString format = locale.toString(time, getSettings()->timestampFormat);
|
QString format = locale.toString(time, getSettings()->timestampFormat);
|
||||||
|
|
||||||
return new TextElement(format, MessageElementFlag::Timestamp,
|
auto *text =
|
||||||
MessageColor::System, FontStyle::TimestampMedium);
|
new TextElement(format, MessageElementFlag::Timestamp,
|
||||||
|
MessageColor::System, FontStyle::TimestampMedium);
|
||||||
|
text->setLink(this->getLink());
|
||||||
|
text->setTooltip(this->getTooltip());
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageElement *TimestampElement::setLink(const Link &link)
|
||||||
|
{
|
||||||
|
MessageElement::setLink(link);
|
||||||
|
this->element_->setLink(link);
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject TimestampElement::toJson() const
|
QJsonObject TimestampElement::toJson() const
|
||||||
|
|||||||
@@ -508,6 +508,7 @@ public:
|
|||||||
const MessageLayoutContext &ctx) override;
|
const MessageLayoutContext &ctx) override;
|
||||||
|
|
||||||
TextElement *formatTime(const QTime &time);
|
TextElement *formatTime(const QTime &time);
|
||||||
|
MessageElement *setLink(const Link &link) override;
|
||||||
|
|
||||||
QJsonObject toJson() const override;
|
QJsonObject toJson() const override;
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,11 @@
|
|||||||
" highlight_color = '#12345678',",
|
" highlight_color = '#12345678',",
|
||||||
" channel_name = 'channel',",
|
" channel_name = 'channel',",
|
||||||
" elements = {",
|
" elements = {",
|
||||||
" { type = 'text', text = 'aliens walking' }",
|
" {",
|
||||||
|
" type = 'text',",
|
||||||
|
" text = 'aliens walking',",
|
||||||
|
" link = { type = c2.LinkType.UserInfo, value = 'twitchdev' }",
|
||||||
|
" }",
|
||||||
" }",
|
" }",
|
||||||
"}"
|
"}"
|
||||||
],
|
],
|
||||||
@@ -32,8 +36,8 @@
|
|||||||
"color": "Text",
|
"color": "Text",
|
||||||
"flags": "Text",
|
"flags": "Text",
|
||||||
"link": {
|
"link": {
|
||||||
"type": "None",
|
"type": "UserInfo",
|
||||||
"value": ""
|
"value": "twitchdev"
|
||||||
},
|
},
|
||||||
"style": "ChatMedium",
|
"style": "ChatMedium",
|
||||||
"tooltip": "",
|
"tooltip": "",
|
||||||
|
|||||||
Reference in New Issue
Block a user