Chat preview

Chat preview

Chat preview is a feature introduced in Minecraft 1.19 that allows the server to display a preview of a chat message to the client before the client sends their message to the server. This chat preview feature is also compatible with /say and /msg, as well as the ChatArgument and AdventureChatArgument classes.


Enabling chat preview

To use chat preview, your server must have previews-chat set to true in the server.properties file:

...
previews-chat=true
...

For players that want to use chat preview, they must have Chat Preview enabled in Options > Chat Settings...


Specifying a chat preview function

The ChatArgument and AdventureChatArgument classes include a method, withPreview:

public T withPreview(PreviewableFunction preview);

The method withPreview(PreviewableFunction preview) lets you generate a preview to send to the client. This method takes in the PreviewableFunction functional interface, which is a function that takes in a PreviewInfo and returns either a BaseComponent[] (for ChatArgument) or a Component (for AdventureChatArgument):

public T generatePreview(PreviewInfo info) throws WrapperCommandSyntaxException;

The PreviewInfo class is a record containing the following:

public record PreviewInfo<T> {
    Player player();
    String input();
    String fullInput();
    T parsedInput();
}

The following methods are as follows:

Player player();

player() is the player that is currently typing a chat preview.


String input();

input() is the current input for the current ChatArgument or AdventureChatArgument. If a user is typing /mycommand hellowor¦ and the command syntax is /mycommand <ChatArgument>, the result of input() would be "hellowor".


String fullInput();

fullInput() is the full input that the player has typed, including the leading / symbol which is required to start a command. If a user is typing /mycommand hellowor¦, the result of fullInput() would be "/mycommand hellowor".


T parsedInput();

parsedInput() is similar to input(), except it has been parsed by the CommandAPI's argument parser. This is a representation of what the argument in the executor would look like. For a ChatArgument the return type is BaseComponent[], and for AdventureChatArgument the return type is Component.


Using the chat preview function as the argument's value

The ChatArgument and AdventureChatArgument classes also include a method, usePreview:

public T usePreview(boolean usePreview);

The usePreview(boolean usePreview) method lets you specify whether you would like the previewing function to be used as the argument's value during execution. If set to true, when the command's .executes() method is called, the argument value (e.g. arg[0]) will be the same as the content generated by the function provided to withPreview().


Chat preview examples

Example - Using chat preview

Say we wanted to make our own /broadcast command that allowed the user to use & chat colors. We can use chat preview to show users what the result of their /broadcast command would look like before running the command. We'll use the following command syntax:

/broadcast <message>

Because the ChatArgument and AdventureChatArgument can support entity selectors (such as @p), it's best to use the info.parsedInput() method to handle parsed entity selectors. In our code, we use the .withPreview() method and take the parsed input and convert it to plain text. We then convert the plain text with & characters into component text to be displayed to the user.

For execution, we do the same procedure, because the text that the user enters still has & characters that need to be converted into a component.

new CommandAPICommand("broadcast")
    .withArguments(new ChatArgument("message").withPreview(info -> {
        // Convert parsed BaseComponent[] to plain text
        String plainText = BaseComponent.toPlainText(info.parsedInput());

        // Translate the & in plain text and generate a new BaseComponent[]
        return TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText));
    }))
    .executesPlayer((player, args) -> {
        // The user still entered legacy text. We need to properly convert this
        // to a BaseComponent[] by converting to plain text then to BaseComponent[]
        String plainText = BaseComponent.toPlainText((BaseComponent[]) args.get(0));
        Bukkit.spigot().broadcast(TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText)));
    })
    .register();
new CommandAPICommand("broadcast")
    .withArguments(new AdventureChatArgument("message").withPreview(info -> {
        // Convert parsed Component to plain text
        String plainText = PlainTextComponentSerializer.plainText().serialize(info.parsedInput());

        // Translate the & in plain text and generate a new Component
        return LegacyComponentSerializer.legacyAmpersand().deserialize(plainText);
    }))
    .executesPlayer((player, args) -> {
        // The user still entered legacy text. We need to properly convert this
        // to a Component by converting to plain text then to Component
        String plainText = PlainTextComponentSerializer.plainText().serialize((Component) args.get(0));
        Bukkit.broadcast(LegacyComponentSerializer.legacyAmpersand().deserialize(plainText));
    })
    .register();
CommandAPICommand("broadcast")
    .withArguments(ChatArgument("message").withPreview { info ->
        // Convert parsed BaseComponent[] to plain text
        val plainText: String = BaseComponent.toPlainText(*info.parsedInput() as Array<BaseComponent>)

        // Translate the & in plain text and generate a new BaseComponent[]
        TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText))
    } )
    .executesPlayer(PlayerCommandExecutor { _, args ->
        // The user still entered legacy text. We need to properly convert this
        // to a BaseComponent[] by converting to plain text then to BaseComponent[]
        val plainText: String = BaseComponent.toPlainText(*args[0] as Array<BaseComponent>)
        val baseComponents: Array<BaseComponent> = TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText))
        Bukkit.spigot().broadcast(*baseComponents)
    })
    .register()
CommandAPICommand("broadcast")
    .withArguments(AdventureChatArgument("message").withPreview { info ->
        // Convert parsed Component to plain text
        val plainText: String = PlainTextComponentSerializer.plainText().serialize(info.parsedInput() as Component)

        // Translate the & in plain text and generate a new Component
        LegacyComponentSerializer.legacyAmpersand().deserialize(plainText)
    } )
    .executesPlayer(PlayerCommandExecutor { _, args ->
        // The user still entered legacy text. We need to properly convert this
        // to a Component by converting to plain text then to Component
        val plainText: String = PlainTextComponentSerializer.plainText().serialize(args[0] as Component)
        Bukkit.broadcast(LegacyComponentSerializer.legacyAmpersand().deserialize(plainText))
    })
    .register()

Example - Using chat preview with usePreview()

Extending on the example above where we created a /broadcast command with chat preview support, we can simplify the code by using .usePreview(true) to use the preview function as the value of our argument in our executor function. We'll use the same command syntax as the previous example:

/broadcast <message>

By using .usePreview(true), we don't have to re-translate & formatting codes into their corresponding components because that has already been done by the preview function specified in .withPreview() method.

new CommandAPICommand("broadcast")
    .withArguments(new ChatArgument("message").usePreview(true).withPreview(info -> {
        // Convert parsed BaseComponent[] to plain text
        String plainText = BaseComponent.toPlainText(info.parsedInput());

        // Translate the & in plain text and generate a new BaseComponent[]
        return TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText));
    }))
    .executesPlayer((player, args) -> {
        Bukkit.spigot().broadcast((BaseComponent[]) args.get(0));
    })
    .register();
new CommandAPICommand("broadcast")
    .withArguments(new AdventureChatArgument("message").usePreview(true).withPreview(info -> {
        // Convert parsed Component to plain text
        String plainText = PlainTextComponentSerializer.plainText().serialize(info.parsedInput());

        // Translate the & in plain text and generate a new Component
        return LegacyComponentSerializer.legacyAmpersand().deserialize(plainText);
    }))
    .executesPlayer((player, args) -> {
        Bukkit.broadcast((Component) args.get(0));
    })
    .register();
CommandAPICommand("broadcast")
    .withArguments(ChatArgument("message").usePreview(true).withPreview { info ->
        // Convert parsed BaseComponent[] to plain text
        val plainText = BaseComponent.toPlainText(*info.parsedInput() as Array<BaseComponent>)

        // Translate the & in plain text and generate a new BaseComponent[]
        TextComponent.fromLegacyText(ChatColor.translateAlternateColorCodes('&', plainText))
    } )
    .executesPlayer(PlayerCommandExecutor { _, args ->
        Bukkit.spigot().broadcast(*args[0] as Array<BaseComponent>)
    })
    .register()
CommandAPICommand("broadcast")
    .withArguments(AdventureChatArgument("message").usePreview(true).withPreview { info ->
        // Convert parsed Component to plain text
        val plainText = PlainTextComponentSerializer.plainText().serialize(info.parsedInput() as Component)

        // Translate the & in plain text and generate a new Component
        LegacyComponentSerializer.legacyAmpersand().deserialize(plainText)
    } )
    .executesPlayer(PlayerCommandExecutor { _, args ->
        Bukkit.broadcast(args[0] as Component)
    })
    .register()