Upcoming post menu changes - How to prepare themes and plugins

As part of our continuous effort to improve the Discourse codebase, we’re removing uses of the legacy “widget” rendering system, and replacing them with Glimmer components.

Recently, we modernized the post menu, and is now available in Discourse behind the glimmer_post_menu_mode setting.

This setting accepts three possible values:

  • disabled: use the legacy “widget” system
  • auto: will detect the compatibility of your current plugins and themes. If any are incompatible, it will use the legacy system; otherwise it will use the new menu.
  • enabled: will use the new menu. If you have any incompatible plugin or theme, your site may be broken.

We already updated our official plugins to be compatible with the new menu, but if you still have any third-party plugin, theme, or theme component incompatible with the new menu, upgrading them will be required.

Warnings will be printed in the browser console identifying the source of the incompatibility.

:timer_clock: Roll-out Timeline

These are rough estimates subject to change

Q4 2024:

  • :white_check_mark: core implementation finished
  • :white_check_mark: official plugins updated
  • :white_check_mark: enabled on Meta
  • :white_check_mark: glimmer_post_menu_mode default to auto; console deprecation messages enabled
  • :white_check_mark: published upgrade advice

Q1 2025:

  • third-party plugins and themes should be updated
  • deprecation messages start, triggering an admin warning banner for any remaining issues
  • enabled the new post menu by default

Q2 2025

  • removal of the feature flag setting and legacy code

:eyes: What does it mean for me?

If your plugin or theme uses any ‘widget’ APIs to customize the post menu, those will need to be updated for compatibility with the new version.

:person_tipping_hand: How do I try the new Post Menu?

In the latest version of Discourse, the new post menu will be enabled if you don’t have any incompatible plugin or theme.

If you do have incompatible extensions installed, as an admin, you can still change the setting to enabled to force using the new menu. Use this with caution as your site may be broken depending on the customizations you have installed.

In the unlikely event that this automatic system does not work as expected, you can temporarily override this ‘automatic feature flag’ using the setting above. If you need to that, please let us know in this topic.

:technologist: Do I need to update my plugin and theme?

You will need to update your plugins or themes if they perform any of the customizations below:

  • Use decorateWidget, changeWidgetSetting, reopenWidget or attachWidgetAction on these widgets:

    • post-menu
    • post-user-tip-shim
    • small-user-list
  • Use any of the API methods below:

    • addPostMenuButton
    • removePostMenuButton
    • replacePostMenuButton

:bulb: In case you have extensions that perform one of the customizations above, a warning will be printed in the console identifying the plugin or component that needs to be upgraded, when you access a topic page.

The deprecation ID is: discourse.post-menu-widget-overrides

:warning: If you use more than one theme in your instance, be sure to check all of them as the warnings will be printed only for the active plugins and currently used themes and theme-components.

What are the replacements?

We introduced the value transformer post-menu-buttons as the new API to customize the post menu.

The value transformer provides a DAG object which allows adding, replacing removing, or reordering the buttons. It also provides context information such as the post associated with the menu, the state of post being displayed and button keys to enable a easier placement of the items.

The DAG APIs expects to receive Ember components if the API needs a new button definition like .add and .replace

Each customization is different, but here is some guidance for the most common use cases:

addPostMenuButton

Before:

withPluginApi("1.34.0", (api) => {
  api.addPostMenuButton("solved", (attrs) => {
    if (attrs.can_accept_answer) {
      const isOp = currentUser?.id === attrs.topicCreatedById;
      return {
        action: "acceptAnswer",
        icon: "far-check-square",
        className: "unaccepted",
        title: "solved.accept_answer",
        label: isOp ? "solved.solution" : null,
        position: attrs.topic_accepted_answer ? "second-last-hidden" : "first",
      };
    }
  });
});

After:

The examples below use Ember’s Template Tag Format (gjs)

// components/solved-accept-answer-button.gjs
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";

export default class SolvedAcceptAnswerButton extends Component {
  // indicates if the button will be prompty displayed or hidden behind the show more button
  static hidden(args) { 
    return args.post.topic_accepted_answer;
  }

  ...

  <template>
    <DButton
      class="post-action-menu__solved-unaccepted unaccepted"
      ...attributes
      @action={{this.acceptAnswer}}
      @icon="far-check-square"
      @label={{if this.showLabel "solved.solution"}}
      @title="solved.accept_answer"
    />
  </template>
}

// initializer.js
import SolvedAcceptAnswerButton from "../components/solved-accept-answer-button";

...
withPluginApi("1.34.0", (api) => {
  api.registerValueTransformer(
    "post-menu-buttons",
    ({
      value: dag, 
      context: {
        post,
        firstButtonKey, // key of the first button
        secondLastHiddenButtonKey, // key of the second last hidden button
        lastHiddenButtonKey, // key of the last hidden button
      },
    }) => {
        dag.add(
          "solved",
          SolvedAcceptAnswerButton,
          post.topic_accepted_answer
            ? {
                before: lastHiddenButtonKey,
                after: secondLastHiddenButtonKey,
              }
            : {
                before: [
                  "assign", // button added by the assign plugin
                  firstButtonKey,
                ],
              }
        );
    }
  );
});

replacePostMenuButton

  • before:
withPluginApi("1.34.0", (api) => {
  api.replacePostMenuButton("like", {
    name: "discourse-reactions-actions",
    buildAttrs: (widget) => {
      return { post: widget.findAncestorModel() };
    },
    shouldRender: (widget) => {
      const post = widget.findAncestorModel();
      return post && !post.deleted_at;
    },
  });
});
  • after:
import ReactionsActionButton from "../components/discourse-reactions-actions-button";

...

withPluginApi("1.34.0", (api) => {
  api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context: { buttonKeys } }) => {
      // ReactionsActionButton is the bnew button component
      dag.replace(buttonKeys.LIKE, ReactionsActionButton);
    }
  );
});

removePostMenuButton

  • before:
withPluginApi("1.34.0", (api) => {
  api.removePostMenuButton('like', (attrs, state, siteSettings, settings, currentUser) => {
    if (attrs.post_number === 1) {
      return true;
    }
  });
});
  • after:
withPluginApi("1.34.0", (api) => {
  api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context: { post, buttonKeys } }) => {
      if (post.post_number === 1) {
        dag.delete(buttonKeys.LIKE);
      }
    }
  );
});

:sos: What about other customizations?

If your customization cannot be achieved using the new API we’ve introduced, please let us know by creating a new dev topic to discuss.

:sparkles: I am a plugin/theme author. How do I update a theme/plugin to support both old and new post menu during the transition?

We’ve used the pattern below to support both the old and new version of the post menu in our plugins:

function customizePostMenu(api) {
  const transformerRegistered = api.registerValueTransformer(
    "post-menu-buttons",
    ({ value: dag, context }) => {
      // new post menu customizations
      ...
    }
  );

  const silencedKey =
    transformerRegistered && "discourse.post-menu-widget-overrides";

  withSilencedDeprecations(silencedKey, () => customizeWidgetPostMenu(api));
}

function customizeWidgetPostMenu(api) {
  // old "widget" code customization here
  ...
}

export default {
  name: "my-plugin",

  initialize(container) {
    withPluginApi("1.34.0", customizePostMenu);
  }
};

:star: More examples

You can check, our official plugins for examples on how to use the new API:

8 Likes