Нужна помощь в поиске причины, по которой список тем не обновляется после клика на уведомление о новой теме/ответе

На сайте создателя тем я пытался удалить из списка тем темы от игнорируемых пользователей, вот мой код:

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

Это работает как на мобильных устройствах, так и на десктопе именно так, как ожидалось, но я обнаружил, что из-за этого список тем не обновляется после нажатия на уведомление о новом топике или ответе.

У меня есть видеозапись воспроизведения этой ошибки.

В видео я просматривал мобильный вид на своем десктопе и создавал новый ответ на «test new topic alert 3» со своего телефона, затем нажал на уведомление на десктопе. В обычном случае поднятая тема должна отображаться вверху списка, но в данном случае этого не происходит.

Я думал, что метод, который я использовал выше, был вызван ошибочно, но он не вызывается при нажатии на уведомление о теме.

Есть ли у кого-нибудь идеи, почему возникла эта ошибка? Большое спасибо.

Обновление: Я обнаружил, что это также приводит к тому, что автоматическая загрузка новых тем при прокрутке больше не работает в мобильном виде.

Если вы уберете код выше, который не показывает игнорируемых пользователей, проблема решится? Если да, то, скорее всего, ваш код фильтрует темы всех пользователей (или большего числа пользователей, чем нужно), из-за чего они не отображаются.

Я не знаю, откуда берется значение ignored, но возможно, оно заполняется именем пользователя, создавшего новую тему, которая вызвала предупреждение вверху. Может быть, туда попадают пользователи, которых не следует игнорировать? Можете ли вы вывести в лог значения, содержащиеся в ignored?

Я думаю, что это не так, поскольку эта проблема возникла только на мобильных устройствах/в мобильном режиме (не удалось поднять и загрузить больше сообщений).

Я вручную установил const ignored = ['david', 'pekka_gaiser', 'sam'];, чтобы протестировать это на сайте создателя тем, так как предположил, что не достиг уровня доверия, необходимого для использования функции игнорирования, или что она была отключена.

И тема, которую я поднял, — это тема, которую я создал сам.

Странно, если проблема не в этом конкретном коде. Если вы измените код на:

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

проблема сохраняется? Если да, то, скорее всего, причина где-то в другом месте.

Нет, баг исчез, если я изменю блок на то, что вы предоставили. Я думал, что, возможно, функция фильтра нарушает цикл обновления данных темы, но этот баг возникает только на мобильных устройствах. Я не могу понять, что вызывает разницу между десктопом и мобильными устройствами…

Тогда единственное, что я могу предположить, — это то, что переменная ignored заполняется неправильно, как я уже говорил ранее. Возможно, где-то ещё её значение изменяется. Попробуйте изменить код следующим образом:

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)
        );
      }
});

В приведённом коде я поместил переменную ignored в ту же область видимости, в которой применяется фильтр, и изменил имя переменной, чтобы избежать непреднамеренных изменений значений массива (например, если где-то вызывается ignored.push(...), это может изменить его значения в другом месте).

Когда вы делаете что-то вроде этого:

this.topics = something_else

вы переопределяете это свойство на другое значение. Это допустимо, если за этим свойством ничего не следит, но в данном случае за массивом topics следят несколько компонентов.

Например:

Если вы выполните это в установке для разработки, Ember выдаст ошибки в консоли, как показано ниже.

.

Итак, ключевой вывод здесь: вместо этого следует делать что-то вроде этого:

this.set("topics", something_else)

пока декоратор @tracked не будет добавлен в Discourse.

Таким образом, если вы попробуете что-то вроде этого:

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);
  }
});

это должно работать как на десктопе, так и на мобильных устройствах в теме-конструкторе.

Однако…

Если вы попробуете это в установке для разработки, вы увидите другую ошибку.

Это должно кое-что вам подсказать. Массив topics используется во многих разных местах. Поэтому не стоит с ним возиться — особенно учитывая, что ваш случай использования в основном визуальный и не связан с вопросами безопасности.

Вот что я предлагаю: отступите назад и попробуйте другой подход. Вместо того чтобы пытаться изменить массив, сделайте что-то гораздо проще. Если тему создал игнорируемый пользователь, добавьте CSS-класс и скройте её с помощью CSS.

Вы можете сделать это следующим образом — я оставил некоторые комментарии, если вы захотите разобраться, но вы можете удалить их, когда будете готовы использовать код.

Это помещается в файл инициализатора:

import { apiInitializer } from "discourse/lib/api";
import discourseComputed 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 user = api.getCurrentUser();

  // если пользователь не авторизован, выходим
  if (!user) {
    return;
  }

  // получаем список игнорируемых пользователей
  const ignored = user.ignored_users;

  // вспомогательная функция для избежания дублирования кода
  const addIgnoredTopicClass = context => {
    // получаем классы из ядра / других плагинов и тем
    let classList = context._super(...arguments);

    // формируем условие
    const shouldAddClass = ignored.includes(
      context.topic.posters[0].user.username
    );

    // добавляем класс игнорируемого, если условие истинно
    if (shouldAddClass) {
      classList += IGNORED_TOPIC_CLASS_STRING;
    }

    // возвращаем classList вместе с изменениями, если они есть
    return classList;
  };

  // добавляем класс в стандартный список тем, как на странице "Последние"
  api.modifyClass("component:topic-list-item", {
    pluginId: PLUGIN_ID,
    @discourseComputed()
    unboundClassNames() {
      return addIgnoredTopicClass(this);
    }
  });

  // делаем то же самое для списка тем на странице категорий
  api.modifyClass("component:latest-topic-list-item", {
    pluginId: PLUGIN_ID,
    @discourseComputed()
    unboundClassNames() {
      return addIgnoredTopicClass(this);
    }
  });
});

а это помещается в /common/common.css:

// здесь мы не используем display: none, чтобы не нарушать работу load-more
.ignored-op-topic {
  height: 0;
  width: 0;
  position: fixed;
  bottom: 0;
}

Спасибо, второй метод работает.

Можно ли добавить дополнительный класс к дочернему элементу компонента?

Например, я хотел бы скрыть аватар игнорируемого пользователя в списке аватаров на рабочем столе справа от заголовка темы. Если добавить класс к элементу списка тем, сможет ли это добавить класс к компоненту аватара?

Метод unboundClassNames(), который мы использовали выше, отвечает за классы, добавляемые к элементу компонента. Иными словами, это HTML-элементы .topic-list-item или .latest-topic-list-item. Вы можете увидеть это здесь:

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

и здесь:

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

Ember берет любую строку, возвращаемую этим методом, и добавляет её как атрибут class самого элемента.

Если вы хотите добавить классы к дочерним элементам этого элемента, вам придется использовать что-то другое.

У каждого автора в свойстве posters есть свойство под названием extras. Это свойство используется как флаг для добавления дополнительных классов к аватару при его отображении.

Оно устанавливается здесь:

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

и используется здесь:

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

Таким образом, вы можете добавлять классы к аватарам в списке тем на основе условия, если расширите это свойство.

Вы можете использовать декоратор @on, когда компонент получает свои атрибуты, чтобы вызвать метод для этого. Поскольку мы уже изменяем классы этих компонентов, мы можем включить это новое поведение в код выше.

Вот что у нас получается:

в инициализаторе

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);
    },
  });
});

Это должно добавить CSS-класс ignored-op-topic к элементам списка тем, созданным игнорируемым пользователем, и CSS-класс ignored-user-avatar к каждому аватару игнорируемого пользователя в столбце авторов.

У нас уже есть CSS для .ignored-op-topic из предыдущего примера.

// мы не используем display: none; здесь, потому что не хотим нарушать работу load-more
.ignored-op-topic {
  height: 0;
  width: 0;
  position: fixed;
  bottom: 0;
}

Теперь вы хотите скрыть аватары игнорируемых пользователей в столбце авторов.

Не делайте этого. Это вызовет много путаницы.

Что, если игнорируемый пользователь ответит на тему, и она будет поднята, но вы скроете его аватар? Это создаст впечатление, что тему поднял кто-то другой.

Кроме того, на странице категорий рядом с заголовками тем есть только один аватар. Что произойдет, если последний ответ был от игнорируемого пользователя? Никакого аватара?

Вы можете понять, как такие случаи могут создать неприятный опыт для ваших пользователей.

Вместо того чтобы скрывать аватары игнорируемых пользователей, вы можете заменить их на SVG-иконку. У всех игнорируемых пользователей будет один и тот же аватар. Это можно сделать с помощью 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>'
  );
}

и это будет отображаться так:

и то же самое для latest-topic-list-item. Замените SVG на любую иконку, которую вы хотите использовать.

Теперь, когда это сделано…

Я ответил на ваш вопрос, потому что это хорошая возможность поговорить о кастомизации списка тем и о том, как это сделать. Однако у меня есть много сомнений относительно вашего случая использования. Необходимость скрывать аватары игнорируемых пользователей указывает на лежащую в основе проблему. Одно дело сказать:

“этот человек пишет на темы, которые меня не интересуют. Я проигнорирую его, чтобы уменьшить шум.”

но это совершенно другое дело сказать:

“даже вид аватара этого человека вызывает эмоциональную реакцию. Я никогда больше не хочу видеть его аватар.”

Вы знаете свое сообщество лучше всех… но, возможно, стоит разобраться в этом.

Понял. Я пытался максимально приблизиться к функции блокировки, как в Mastodon или Twitter, когда при игнорировании пользователя вы больше никогда не увидите его контент. Я согласен, что большинству сообществ такая функция может никогда не понадобиться. Но поскольку мои пользователи об этом просят, я хотел бы сделать всё возможное.