diff --git a/CHANGELOG.md b/CHANGELOG.md index 31ef1efcea6..60fd77a86a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - Minor: Added WebSocket API for plugins. (#6076, #6186, #6314, #6315) - Minor: Allow for themes to set transparent values for window background on Linux. (#6137) - Minor: Popup overlay now only draws an outline when being interacted with. (#6140) -- Minor: Added basic message API to plugins. (#5754) +- Minor: Added basic message API to plugins. (#5754, #6386) - Minor: Made filters searchable in the Settings dialog search bar. (#5890) - Minor: Updated emojis to Unicode 16.0. (#6155) - Minor: Allow disabling of double-click tab renaming through setting. (#6163, #6184) diff --git a/docs/chatterino.d.ts b/docs/chatterino.d.ts index 77e47072cbd..ab279a73d6e 100644 --- a/docs/chatterino.d.ts +++ b/docs/chatterino.d.ts @@ -187,6 +187,7 @@ declare namespace c2 { interface MessageElementInitBase { tooltip?: string; trailing_space?: boolean; + link?: Link; } type MessageColor = "text" | "link" | "system" | string; @@ -242,6 +243,20 @@ declare namespace c2 { type: "reply-curve"; } + interface Link { + type: LinkType; + value: string; + } + + enum LinkType { + Url, + UserInfo, + UserAction, + JumpToChannel, + CopyToClipboard, + JumpToMessage, + } + enum MessageFlag { None = 0, System = 0, diff --git a/docs/plugin-meta.lua b/docs/plugin-meta.lua index f6dede6d078..f358ea40ac7 100644 --- a/docs/plugin-meta.lua +++ b/docs/plugin-meta.lua @@ -292,6 +292,7 @@ c2.Message = {} ---@class MessageElementInitBase ---@field tooltip? string Tooltip text ---@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 @@ -344,6 +345,17 @@ c2.Message = {} ---@param init MessageInit The message initialization table ---@return c2.Message msg The new message 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 ---@enum c2.FontStyle diff --git a/docs/wip-plugins.md b/docs/wip-plugins.md index 664ce377c99..d1fc28eb911 100644 --- a/docs/wip-plugins.md +++ b/docs/wip-plugins.md @@ -557,6 +557,19 @@ end) 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 These functions are wrappers for Lua's I/O library. Functions on file pointer diff --git a/scripts/make_luals_meta.py b/scripts/make_luals_meta.py index 14f8a2b2c19..c1d663b54b2 100755 --- a/scripts/make_luals_meta.py +++ b/scripts/make_luals_meta.py @@ -207,7 +207,7 @@ def read_enum_variants(self) -> list[str]: for line in line[opener:closer].split(",") ] break - if line.startswith("enum class"): + if line.startswith("enum "): continue if waiting_for_end: diff --git a/src/controllers/plugins/PluginController.cpp b/src/controllers/plugins/PluginController.cpp index 19f1f6e3cd0..6c315dafc04 100644 --- a/src/controllers/plugins/PluginController.cpp +++ b/src/controllers/plugins/PluginController.cpp @@ -234,6 +234,8 @@ void PluginController::initSol(sol::state_view &lua, Plugin *plugin) c2["MessageElementFlag"] = lua::createEnumTable(lua); c2["FontStyle"] = lua::createEnumTable(lua); c2["MessageContext"] = lua::createEnumTable(lua); + c2["LinkType"] = + lua::createEnumTable(lua); sol::table io = g["io"]; io.set_function( diff --git a/src/controllers/plugins/SolTypes.cpp b/src/controllers/plugins/SolTypes.cpp index 41d8062d1b2..5b0c63c87ea 100644 --- a/src/controllers/plugins/SolTypes.cpp +++ b/src/controllers/plugins/SolTypes.cpp @@ -3,8 +3,10 @@ # include "Application.hpp" # include "common/QLogging.hpp" +# include "controllers/plugins/api/Message.hpp" # include "controllers/plugins/LuaAPI.hpp" # include "controllers/plugins/PluginController.hpp" +# include "messages/Link.hpp" # include # include @@ -140,6 +142,48 @@ int sol_lua_push(sol::types, lua_State *L, } // namespace chatterino::lua +namespace chatterino { + +// Link +bool sol_lua_check(sol::types, lua_State *L, int index, + chatterino::FunctionRef handler, + sol::stack::record &tracking) +{ + return sol::stack::check(L, index, handler, tracking); +} + +chatterino::Link sol_lua_get(sol::types, lua_State *L, + int index, sol::stack::record &tracking) +{ + sol::table table = sol::stack::get(L, index, tracking); + + auto ty = + table.get>("type"); + if (!ty) + { + throw std::runtime_error("Missing 'type' in Link"); + } + auto value = table.get>("value"); + if (!value) + { + throw std::runtime_error("Missing 'value' in Link"); + } + + return {static_cast(*ty), *value}; +} + +int sol_lua_push(sol::types, lua_State *L, + const chatterino::Link &value) +{ + sol::table table = sol::table::create(L, 0, 2); + table.set("type", + static_cast(value.type)); + table.set("value", value.value); + return sol::stack::push(L, table); +} + +} // namespace chatterino + // NOLINTEND(readability-named-parameter) #endif diff --git a/src/controllers/plugins/SolTypes.hpp b/src/controllers/plugins/SolTypes.hpp index d7a05c7b88c..0cb26ed8b8f 100644 --- a/src/controllers/plugins/SolTypes.hpp +++ b/src/controllers/plugins/SolTypes.hpp @@ -25,6 +25,7 @@ constexpr bool IsOptional> = true; namespace chatterino { class Plugin; +struct Link; } // namespace chatterino @@ -188,6 +189,12 @@ SOL_STACK_FUNCTIONS(chatterino::lua::ThisPluginState) } // namespace chatterino::lua +namespace chatterino { + +SOL_STACK_FUNCTIONS(chatterino::Link) + +} // namespace chatterino + SOL_STACK_FUNCTIONS(QString) SOL_STACK_FUNCTIONS(QStringList) SOL_STACK_FUNCTIONS(QByteArray) diff --git a/src/controllers/plugins/api/Message.cpp b/src/controllers/plugins/api/Message.cpp index 3d63f9d344a..e5d035e0656 100644 --- a/src/controllers/plugins/api/Message.cpp +++ b/src/controllers/plugins/api/Message.cpp @@ -1,12 +1,11 @@ -#include "controllers/plugins/api/Message.hpp" - -#include "Application.hpp" -#include "messages/MessageElement.hpp" - #ifdef CHATTERINO_HAVE_PLUGINS +# include "controllers/plugins/api/Message.hpp" +# include "Application.hpp" +# include "controllers/plugins/LuaUtilities.hpp" # include "controllers/plugins/SolTypes.hpp" # include "messages/Message.hpp" +# include "messages/MessageElement.hpp" # include @@ -128,6 +127,7 @@ std::unique_ptr elementFromTable(const sol::table &tbl) { auto type = requiredGet(tbl, "type"); std::unique_ptr el; + bool linksAllowed = true; if (type == u"text") { el = textElementFromTable(tbl); @@ -139,6 +139,7 @@ std::unique_ptr elementFromTable(const sol::table &tbl) else if (type == u"mention") { el = mentionElementFromTable(tbl); + linksAllowed = false; } else if (type == u"timestamp") { @@ -155,6 +156,7 @@ std::unique_ptr elementFromTable(const sol::table &tbl) else if (type == u"reply-curve") { el = replyCurveElementFromTable(); + linksAllowed = false; } else { @@ -163,7 +165,54 @@ std::unique_ptr elementFromTable(const sol::table &tbl) assert(el); el->setTrailingSpace(tbl.get_or("trailing_space", true)); - el->setTooltip(tbl.get_or("tooltip", QString{})); + + auto link = tbl.get>("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("URL: %1").arg(link->value); + break; + case Link::UserAction: + tooltip = QString("Command: %1").arg(link->value); + break; + case Link::CopyToClipboard: + tooltip = "Copy to clipboard"; + 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; } diff --git a/src/controllers/plugins/api/Message.hpp b/src/controllers/plugins/api/Message.hpp index 119a6b5238b..ba66eab97a3 100644 --- a/src/controllers/plugins/api/Message.hpp +++ b/src/controllers/plugins/api/Message.hpp @@ -1,9 +1,12 @@ #pragma once #ifdef CHATTERINO_HAVE_PLUGINS +# include "messages/Link.hpp" # include "messages/Message.hpp" # include +# include + namespace chatterino::lua::api::message { /* @lua-fragment @@ -32,6 +35,7 @@ c2.Message = {} ---@class MessageElementInitBase ---@field tooltip? string Tooltip text ---@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 @@ -86,6 +90,20 @@ c2.Message = {} 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 messages/MessageElement.hpp diff --git a/src/messages/Link.hpp b/src/messages/Link.hpp index 2692ace692e..62480dd30c9 100644 --- a/src/messages/Link.hpp +++ b/src/messages/Link.hpp @@ -9,13 +9,9 @@ struct Link { enum Type { None, Url, - CloseCurrentSplit, UserInfo, - UserTimeout, - UserBan, UserWhisper, InsertText, - ShowMessage, UserAction, AutoModAllow, AutoModDeny, diff --git a/src/messages/MessageElement.cpp b/src/messages/MessageElement.cpp index cb85790e9a2..ef1df33ab2f 100644 --- a/src/messages/MessageElement.cpp +++ b/src/messages/MessageElement.cpp @@ -1106,6 +1106,7 @@ void TimestampElement::addToContainer(MessageLayoutContainer &container, { if (ctx.flags.hasAny(this->getFlags())) { + this->setTooltip(this->getTooltip()); if (getSettings()->timestampFormat != this->format_) { this->format_ = getSettings()->timestampFormat.getValue(); @@ -1122,8 +1123,19 @@ TextElement *TimestampElement::formatTime(const QTime &time) QString format = locale.toString(time, getSettings()->timestampFormat); - return new TextElement(format, MessageElementFlag::Timestamp, - MessageColor::System, FontStyle::TimestampMedium); + auto *text = + 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 diff --git a/src/messages/MessageElement.hpp b/src/messages/MessageElement.hpp index abf0d3ce6c3..909694f6895 100644 --- a/src/messages/MessageElement.hpp +++ b/src/messages/MessageElement.hpp @@ -521,6 +521,7 @@ class TimestampElement : public MessageElement const MessageLayoutContext &ctx) override; TextElement *formatTime(const QTime &time); + MessageElement *setLink(const Link &link) override; QJsonObject toJson() const override; diff --git a/tests/snapshots/PluginMessageCtor/properties.json b/tests/snapshots/PluginMessageCtor/properties.json index 1444dbbdc40..ee74346ebaa 100644 --- a/tests/snapshots/PluginMessageCtor/properties.json +++ b/tests/snapshots/PluginMessageCtor/properties.json @@ -15,7 +15,11 @@ " highlight_color = '#12345678',", " channel_name = 'channel',", " elements = {", - " { type = 'text', text = 'aliens walking' }", + " {", + " type = 'text',", + " text = 'aliens walking',", + " link = { type = c2.LinkType.UserInfo, value = 'twitchdev' }", + " }", " }", "}" ], @@ -32,8 +36,8 @@ "color": "Text", "flags": "Text", "link": { - "type": "None", - "value": "" + "type": "UserInfo", + "value": "twitchdev" }, "style": "ChatMedium", "tooltip": "",