Need help finding why topic list not updating after click new topic/reply alert

On the theme creator site I was trying to remove topics from ignored users from the topic list, here is my code

api.modifyClass("component:topic-list", {
      @on("didReceiveAttrs")
      removeIgnoredUsers() {
        this.topics = this.topics.filter(
          topic => !ignored.includes(topic.posters[0].user.username)
        );
      }
});

It works on both mobile and desktop just as expected, but I find it also cause the topic list not to update after clicking on the new topic/reply alert.

I have a screen record of reproducing this bug.

In the video, I was looking into the mobile view on my desktop and creating a new reply to ‘test new topic alert 3’ from my cell phone, then click the alert on the desktop, in the normal case, the bumped topic should show at the top of topics, but it doesn’t in this case.

I thought the method I used above was triggered mistakenly but it’s not being called when clicking on a topic alert.

Does anyone have any suggestions for why this bug happened? Many thanks.

Update: I found it also cause the autoload more topics while scrolling not to work anymore in mobile view.

2 Likes

If you remove the code above that don’t show ignored users the problem is solved? If it’s solved, my guess is that your code is filtering out the topics of all users (or of more users than it should), which cause them to not show.

I don’t know from where ignored is populated, but it might being populated with the username of the user that created the new topic that showed the alert at the top. Maybe it’s being populated with users that shouldn’t be ignored? Can you log the values present in ignored?

2 Likes

I think it’s not the case since this issue only happened on mobile/mobile view. (not bumped and can’t load more)

I manually set const ignored = ['david', 'pekka_gaiser', 'sam']; in order to test on the theme creator site since I assumed I haven’t reached the trust level that can use ignore feature or it was disabled.

And the topic I bumped is a topic I created.

1 Like

Weird, unless the problem is not in that code specifically. If you change the code to:

api.modifyClass("component:topic-list", {
      @on("didReceiveAttrs")
      removeIgnoredUsers() {
        this.topics = this.topics.filter(() => true);
      }
});

the issue persists? If it persists, the problem is probably somewhere else.

2 Likes

No, the bug is gone if I change the block to what you provide. I thought maybe the filter function breaks the update cycle of topic data, but this bug only happens on mobile, I can’t figure out what causes the difference between desktop vs mobile…

1 Like

Then the only thing I can think of is that the ignored variable is populated wrongly as I said previously. Maybe somewhere else is changing the value. Try to change the code to:

api.modifyClass("component:topic-list", {
      @on("didReceiveAttrs")
      removeIgnoredUsers() {
        const ignored2 = ['david', 'pekka_gaiser', 'sam'];
        this.topics = this.topics.filter(
          topic => !ignored2.includes(topic.posters[0].user.username)
        );
      }
});

In the code above I put the ignored variable in the same scope in which the filter is being applied, and changed the variable name to avoid unintentional changes to the array values (for example, if some place calls ignored.push(...), it could change its values somewhere else).

1 Like

When you do something like this

this.topics = something_else

You’re redefining that property to something else. This is fine if nothing else is watching that property, but in this case, several things are watching the topics array.

For example

If you do this on a development installation, Ember will throw errors in the console like so.

.

So, the key takeaway here is to do something like this instead.

this.set("topics", something_else)

until the @tracked decorator lands in Discourse.

So, if you try something like this

api.modifyClass("component:topic-list", {
  @on("didReceiveAttrs")
  removeIgnoredUsers() {
    const filtered = this.topics.filter(
      topic => !ignored.includes(topic.posters[0].user.username)
    );
    this.set("topics", filtered);
  }
});

It should work on both desktop and mobile on theme creator.

However…

If you try that on development installation, you’ll see another error.

So this should tell you something. The topics array is being used in many different places. So it’s not a good idea to muck around with it - especially since your use case is primarily visual and there are no security implications involved.

Here’s what I suggest: Take a step back and try a different approach. Instead of trying to modify the array, do something much simpler. If an ignored user created the topic, add a CSS class and hide it with CSS.

You do that with something like this - I left some comments if you want to follow along but you can delete them when you’re ready to use it.

This goes in the initilizer file:

import { apiInitializer } from "discourse/lib/api";
import discourseComputed from "discourse-common/utils/decorators";

export default apiInitializer("0.11.1", api => {
  // set an id for your modifications
  const PLUGIN_ID = "hide-ignored-op-topics";

  // The class name you want to add. The space at the start is required
  const IGNORED_TOPIC_CLASS_STRING = " ignored-op-topic";

  // get current user
  const user = api.getCurrentUser();

  // not logged in, bail
  if (!user) {
    return;
  }

  // get a list of ignored users
  const ignored = user.ignored_users;

  // helper function to avoid duplicating code
  const addIgnoredTopicClass = context => {
    // get classes from core / other plugins and themes
    let classList = context._super(...arguments);

    // create your condition
    const shouldAddClass = ignored.includes(
      context.topic.posters[0].user.username
    );

    // add ignored class if condition is true
    if (shouldAddClass) {
      classList += IGNORED_TOPIC_CLASS_STRING;
    }

    // return the classList plus the modifications if any
    return classList;
  };

  // add the class to the default topic list like on the "latest" page
  api.modifyClass("component:topic-list-item", {
    pluginId: PLUGIN_ID,
    @discourseComputed()
    unboundClassNames() {
      return addIgnoredTopicClass(this);
    }
  });

  // do the same for the categories page topic list
  api.modifyClass("component:latest-topic-list-item", {
    pluginId: PLUGIN_ID,
    @discourseComputed()
    unboundClassNames() {
      return addIgnoredTopicClass(this);
    }
  });
});

and this goes in /common/common.css

// we don't use display: none; here because we don't want to mess with load-more
.ignored-op-topic {
  height: 0;
  width: 0;
  position: fixed;
  bottom: 0;
}
2 Likes

Thank you, the 2nd method works.

Is it possible to add an extra class on the child element of the component?

For example, I would like to hide the ignored user’s avatar in the desktop avatar list on the right side of the topic title, as add class to the topic list item, will it be able to add class to an avatar component?

The unboundClassNames() method we used above is responsible for the classes added to the component element. In other words the .topic-list-item or .latest-topic-list-item HTML elements. You can see that here

discourse/app/assets/javascripts/discourse/app/components/topic-list-item.js at ad4faf637c9caf7e6ac60a614e3838a69c928e27 · discourse/discourse · GitHub

and here

discourse/app/assets/javascripts/discourse/app/components/latest-topic-list-item.js at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub

Ember takes whatever string that method returns and adds that as the class attribute on the element itself.

You’ll have to use something else if you want to add classes to children of that element.

Each poster in the posters property has a property called extras. This property is used as a flag for extra classes to be added to the avatar when its rendered.

It’s set here

discourse/app/assets/javascripts/discourse/app/models/topic-list.js at d3a59e3f695c467c22d02db62532194e17ac827b · discourse/discourse · GitHub

and consumed here

discourse/app/assets/javascripts/discourse/app/templates/list/posters-column.hbr at 1472e47aae5bfdfb6fd9abfe89beb186c751f514 · discourse/discourse · GitHub

So, you can add classes to avatars in the topic list based on a condition if you extend that property.

You can use the @on decorator when the component receives its attributes to call a method to do that. Since we’re already modifying those component Classes, we can do incorporate that new behavior into the code above.

Here’s what we end up with

in the initializer

import { apiInitializer } from "discourse/lib/api";
import discourseComputed, { on } from "discourse-common/utils/decorators";

export default apiInitializer("0.11.1", (api) => {
  const PLUGIN_ID = "hide-ignored-op-topics";
  const IGNORED_TOPIC_CLASS_STRING = " ignored-op-topic";
  const IGNORED_AVATAR_CLASS_STRING = " ignored-user-avatar";

  const user = api.getCurrentUser();

  if (!user) {
    return;
  }

  const ignoredUsers = user.ignored_users;

  function isIgnoredUser(poster) {
    return ignoredUsers.includes(poster.user.username);
  }

  function addIgnoredTopicClass() {
    let classList = this._super(...arguments);
    
    const topicCreator = this.topic.posters[0];

    if (isIgnoredUser(topicCreator)) {
      classList += IGNORED_TOPIC_CLASS_STRING;
    }

    return classList;
  }

  function addIgnoredAvatarClass() {
    this.topic.posters.forEach((poster) => {
      if (isIgnoredUser(poster)) {
        // default raw topic-lists
        poster.extras += IGNORED_AVATAR_CLASS_STRING;

        // categories page topic lists
        poster.user.set("extras", IGNORED_AVATAR_CLASS_STRING);
      }
    });
  }

  api.modifyClass("component:topic-list-item", {
    pluginId: PLUGIN_ID,

    @discourseComputed()
    unboundClassNames() {
      return addIgnoredTopicClass.call(this);
    },

    @on("didReceiveAttrs")
    ignoredAvatarClass() {
      addIgnoredAvatarClass.call(this);
    },
  });

  api.modifyClass("component:latest-topic-list-item", {
    pluginId: PLUGIN_ID,

    @discourseComputed()
    unboundClassNames() {
      return addIgnoredTopicClass.call(this);
    },

    @on("didReceiveAttrs")
    ignoredAvatarClass() {
      addIgnoredAvatarClass.call(this);
    },
  });
});

This should give you a ignored-op-topic CSS class on topic list items started by an ignored user and a ignored-user-avatar CSS class on every ignored user avatar in the posters column.

We already have the CSS for the .ignored-op-topic from above.

// we don't use display: none; here because we don't want to mess with load-more
.ignored-op-topic {
  height: 0;
  width: 0;
  position: fixed;
  bottom: 0;
}

Now, you want to hide ignored user avatars in the poster column.

Don’t do that. This will create a lot of confusion.

What if an ignored user replies to a topic, and it gets bumped, but you have their avatar hidden? It would make it look like someone else just bumped the topic.

Also, there’s only one avatar on the categories page next to topic titles. What happens if the last reply is by an ignored user? No avatar?

You can see how such cases would create an unpleasant experience for your users.

Instead of hiding ignored user avatars, you can swap them out with an SVG icon. All ignored users will have that same avatar. You can do that with CSS

.ignored-user-avatar {
  background: white;
  border: 1px solid transparent;
  box-sizing: border-box;
  opacity: 0.5;
  content: svg-uri(
    '<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="m256 0c141.385 0 256 114.615 256 256s-114.615 256-256 256-256-114.615-256-256 114.615-256 256-256zm0 105c-83.262 0-151 67.74-151 151s67.737 151 151 151 151-67.736 151-151-67.74-151-151-151zm-52.816 130.621a22.119 22.119 0 1 0 0-44.237 22.119 22.119 0 0 0 0 44.237zm127.749-22.121a22.116 22.116 0 0 0 -22.12-22.12 22.119 22.119 0 1 0 22.12 22.12zm-40.233 70.79a9.439 9.439 0 0 0 -13.35-13.347l-21.35 21.357-21.352-21.357a9.438 9.438 0 1 0 -13.348 13.347l21.352 21.352-21.352 21.358a9.438 9.438 0 1 0 13.347 13.347l21.353-21.355 21.351 21.351a9.439 9.439 0 0 0 13.349-13.343l-21.352-21.354z"/></svg>'
  );
}

and it will render like so

and the same on latest-topic-list-item. Change the SVG to any icon you want to use.

With that out of the way…

I answered your question because it’s a good opportunity to talk about customizing the topic list and how you would do that. However, I have a lot of reservations about your use case. The need to hide the avatars of ignored users indicates an underlying problem. It’s one thing to say

“this person writes about subjects I’m not interested in. I will ignore them to reduce the noise.”

but it’s an entirely different thing to say

“even seeing this person’s avatar triggers an emotional response. I never want to see their avatar again.”

You know your community more than anyone… but it’s probably something worth looking into.

4 Likes

Understood. I was trying to mimic the block function like mastodon or Twitter as much as possible, that’s when you ignore a user you will never see any content from hir again. I agreed that most communities may never need this kind of function. Since my users are asking that, I would like to try my best.

1 Like