Sticky see new or updated topic notification

Yeah, adding a URL will cause navigation since there’s no logic to intercept the event.

The link is calling an Ember action. All actions in Discourse are extendable. So, they act like customization hooks in a way. So how do you modify an action? Well, let’s check what it does first.

Search on Github or locally for the name of the action. Actions are always defined in JS files and referenced in handlebars. We want to see the definition, so we narrow down the search to JS files.

Repository search results · GitHub

You get four files. Which one should you look at? You want to customize the

discovery/topics.hbs

template. So

discovery/topics.js

is what you want to look at.

Is any of this code helpful? No, but now we know where the action is defined. So, let’s modify it.

discovery/topics.js is an Ember class. You can modify Ember classes with a method in the plugin-api called… modifyClass :stuck_out_tongue:

https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/plugin-api.js#L166-L195

We already know the Class we want to modify. It’s discovery/topics. We need to know what kind of Class it is. So, let’s check the file directory.

discourse/app/controllers/discovery/topics.js

It’s a controller.

We also know that we want to modify the showInserted action in that Class. So, we start with this.

api.modifyClass("controller:discovery/topics", {
  pluginId: 'sticky-new-topics-banner',
  actions: {
    showInserted() {
      // let's do some work
    }
  }
});

You can then add whatever code you want to scroll the window when the user clicks on the “new topics” banner. I went with something like this.

const listControls = document.querySelector(".list-controls");
listControls.scrollIntoView();

You can read more about scrollIntoView() here.

Then you add that to the plugin-api method like so.

api.modifyClass("controller:discovery/topics", {
  pluginId: 'sticky-new-topics-banner',
  actions: {
    showInserted() {
+     const listControls = document.querySelector(".list-controls");
+     listControls.scrollIntoView();
    }
  }
});

So, are we done? No. this breaks Discourse because you’re fully overriding the action. Clicking the link will now scroll to the list-controls element, but it won’t load the new topics. Why? Because the code in core is no longer being applied. I mean, this stuff

So, how do you fix that? With this simple line

this._super(...arguments);

You don’t have to copy any code from core if you just want to add to it. Do your work, then add that line. All it does is that it makes sure that the code from core applies.

api.modifyClass("controller:discovery/topics", {
  pluginId: 'sticky-new-topics-banner',
  actions: {
    showInserted() {
     const listControls = document.querySelector(".list-controls");
     listControls.scrollIntoView();
+
+    this._super(...arguments);
    }
  }
});

If you test this, you’ll see that almost everything works great, except… the header is overlapping with the list-controls. Why? Because the header is set to sticky.

You can fix this in several ways in JS - calculate the height, get the offset, import a helper from Discourse… etc. I won’t get into those.

The easiest way is CSS with scroll-margin-top, which you can read about here.

So, we add this

.list-controls {
  scroll-margin-top: calc(var(--header-offset) * 2);
}

In English: When the link is clicked, scroll to the top of list-controls - 2 * the header height, so it doesn’t overlap and has a bit of space below it.

So, let’s put all of this together.

common header tab

<script type="text/discourse-plugin" version="0.8">
api.modifyClass("controller:discovery/topics", {
  pluginId: "sticky-new-topics-banner",
  actions: {
    showInserted() {
      const listControls = document.querySelector(".list-controls");
      listControls.scrollIntoView();
      this._super(...arguments);
    }
  }
});
</script>

common CSS

#list-area {
  // mobile has a different layout
  .alert-info,
  .show-more.has-topics {
    position: sticky;
    // safari is sometimes fussy without the prefix
    position: -webkit-sticky;
    top: var(--header-offset);
    // banner should be on top of content
    z-index: z("header");
  }
}

.list-controls {
  scroll-margin-top: calc(var(--header-offset) * 2);
}
5 Likes