Private Message Button on topic authors posts - Code Review Request


(Dean Taylor) #1

I’m adding a “Private Message” button to topic author topic and posts within a specific category to make “Private Message” more accessible as some users are unable to reply in those categories due to permissions requirements.

Could you review the following JavaScript code?

It works and displays the Private Message dialog, with quoting support.

It uses Discourse.__container__.lookup which causes an air of code smell for me.

window.replyAsNewPrivateMessage = function (target) {
    var postId = $(target).closest('.boxed').data('post-id'),
        composerController = Discourse.__container__.lookup('controller:composer'),
        quoteController = Discourse.__container__.lookup('controller:quote-button'),
        quotedText ='post'), quoteController.get('buffer')),
        topicController = Discourse.__container__.lookup('controller:topic'),
        post = topicController.get('postStream.posts').findBy('id', postId);

        action:      Discourse.Composer.PRIVATE_MESSAGE,
        usernames:   post.get('username'),
        archetypeId: 'private_message',
        draftKey:    'new_private_message'
    }).then(function () {
        return Em.isEmpty(quotedText) ? Discourse.Post.loadQuote(post.get('id')) : quotedText;
    }).then(function (q) {
        var postUrl = "" + location.protocol + "//" + + (post.get('url')),
            postLink = "[" + topicController.get('title') + "](" + postUrl + ")";
        composerController.appendText(I18n.t("post.continue_discussion", {postLink: postLink}) + "\n\n" + q);

Any comments welcome - Thanks!

Link to user profile page with "send message" window already open
PM-User to Invite to Convo or Continue in PM
(Jeff Atwood) #2

Sure @eviltrout can you have a look at this?

(Kane York) #3

I think that’s actually the only way to do that from a userscript, unfortunately!

(Joe Seyfried) #4

Sorry for thread-napping this:

    topicController = Discourse.__container__.lookup('controller:topic'),
    post = topicController.get('postStream.posts').findBy('id', postId);

…is exactly what I’m trying to do - but I seem to fail at the topicController.get('postStream.posts'): EmberInspector just coughs up a undefined at this point (the first statement works just fine). Did you get your code working like that, or did you modify something there? Or is EmberInspector screwing with me?

(Dean Taylor) #5

It works for me…

For me I have really developed this in Google Chrome Developer Tools / Console.

To test:

  1. I paste the code above into the Console tab
  2. Inspect and then edit a post HTML (in dev tools) to contain <a onclick="window.replyAsNewPrivateMessage(this)">new private message</a>
  3. Click the link just added.

Thanks for your reply @riking, I can’t bring myself to “like” your post though - this code still makes me feel dirty.

(Joe Seyfried) #6

Thanks Dean, I liked your post anyway. :wink:

The problem was sitting right in front of the monitor - this works only in a topic where there’s a postStream. I was looking for something else - I will post on a separate topic to stop abusing your post. I like the user script very much, I’m going to include this btw…

One more stupid question though:

You wrote:

…which I find a bit hard currently because I cannot seem to find any documentation for stuff like


(_lookup_ - really? Where did you find that and how did you determine what you can look up?)

        post = topicController.get('postStream.posts')

(Same here: where have you got the postStream.posts from?)

I know the documentation project is on its way, but if you could provide me with some pointers where to dig, I’d be glad!

(Kane York) #7

Unfortunately, the answer to those is pretty much only answered by exploring in the console with the Ember Debugger extension and here:

(Robin Ward) #8

You are right that the container is a code smell.

The problem here is you’re trying to trigger an event in the Ember app from outside it. You could eliminate the need for the container lookup if you added your button into the handlebars template.

Where did you say you want it to show up?

(Dean Taylor) #9

I was thinking inside the .post-menu-area .post-controls .actions next to Reply, or to the far-left of that same row.

Design and style style still to be worked on.

(Robin Ward) #10

I see, there’s not yet a simple API for adding a post menu button from an extension. The container should be fine for now, but in the long term we should make that extensible.

(Dean Taylor) #11

Could you give a hint as to how the code might be different?

(Robin Ward) #12

Any {{action}} taken in handlebars will go to a controller. The controller can get a handle on the composer via needs, like this.get('controllers.composer').someMethod() – but the thing is you need to insert the button into the template, which we normally use outlets for. As I said the post menu is not very extensible now, you’d have to really reach into our internals to do it.

(Kane York) #13

Actually, it’s quite possible to add a post menu button… Here’s an example. You also need to add it in the post_menu site setting.

@DeanMarkTaylor is this clear enough to modify for what you need? Note that it’s a toggle instead of an action.

// Show Raw button
  //@Monarch at
  //Button is redelcared and sligthly modified due to new scope. it can be further simplified.
  Button = function(action, label, icon, opts) {
    this.action = action;
    this.label = label;

    if (typeof icon === "object") { this.opts = icon; } else { this.icon = icon;}
    this.opts = this.opts || opts || {};

    this.render = function (buffer){
        var opts = this.opts;
        buffer.push("<button title=\"" + this.label + "\"");
        if (opts.className) { buffer.push(" class=\"" + opts.className + "\""); }
        buffer.push(" data-action=\"" + this.action + "\">");
        if (this.icon) { buffer.push("<i class=\"fa fa-" + this.icon + "\"></i>"); }

          return new Button('raw', 'view raw post', 'code', {className: "raw-button", disabled: false});

          var topicID = post.topic_id,
              postID =  post.post_number,
              postArea = $("article[data-post-id='""'] div.contents"),
              $rawButton = $(this.element).find("button.raw-button"),
              styles = [{backgroundColor: 'transparent', color: '#A7A7A7'},{backgroundColor: '#08C', color: '#FFF'}];

          if (postArea.children('.tdwtf-raw-area').length == 0){
              var postArea_raw_content = $('<pre class="tdwtf-raw-area"></pre>'),
                  cooked = postArea.children('.cooked');


              $.get('/raw/' + topicID + '/' + postID) .done(function (content) {
                  postArea_raw_content.css({"white-space":"pre-wrap", 'border':'2px dashed #E7E7E7','padding':'3px'}) .text(content);
          } else {
              var postArea_raw_content = postArea.children('.tdwtf-raw-area');
              if ( !postArea_raw_content.hasClass("active") ){  //raw no active
              } else{

(Dean Taylor) #14

Thanks @riking

Yeap that pushed me the right direction as well as @eviltrout’s comments regarding internals.

I’ve put my completed working implementation in a Gist, please do review and let me know any thoughts.

I tried to avoid reimplementing Button.

EDIT: Note the implementation is slightly different to the original request, the code adds buttons to all users posts not just the topic authors as originally suggested.

(Robin Ward) #15

I think you missed the keyword of “simple” – I’d hardly call that simple compared to other plugin APIs. Sure, you can do it, but it’s quite complex and very tied to a particular implementation of Discourse.

(Dean Taylor) #16

Based on what I did for the Private Message button…
Here is a quite short example of how to add a post button and have access to post and topic inside the topic controller.

@eviltrout How would you expect (or like) the API to be simpler or less tied - just out of interest?

// Show Custom "Boom!" button in post menu
// NOTE: In order for the button to display you need to add 'boom' to the post_menu site setting.

// Add our custom string for labelling the button
if (!I18n.translations.en.js.boom) {
    I18n.translations.en.js.boom = {};
I18n.translations.en.js.boom.action = 'Boom!';

    buttonForBoom: function (post) {
        var Button = require('discourse/views/post-menu').Button;
        return new Button('boom', 'boom.action', 'asterisk', {
            className: 'boom-button',
            disabled:  false

    clickBoom: function (post) {
        this.get('controller').send('boom', post);

    actions: {
        boom: function (post) {

            // Do some action here where you have access to post and topic (this).
            alert('Topic "' + this.get('title') + '" has just exploded - well done!');

            return false;

(Robin Ward) #17

I don’t know off the top of my head (I’d have to jump into it and try for myself) but I imagine what you wrote but with far fewer lines. Hopefully one or two functions to wire it up rather than all that ceremony of opening/closing classes. To be clear this is something that should be exposed by our code as an API, it’s definitely not your fault for having to do all that to write up a button :smile:

(Arpit Jalan) #18

This feature is now available in Discourse:

(Jeff Atwood) #19

@DeanMarkTaylor did the PM button on posts get implemented to your satisfaction? I have had one customer request this…

(Dean Taylor) #20

Yes thanks, the latest code is in the github link in a previous post.

Please note that it has not been tested with latest Dev version of Discourse so will likely need updating.