Предстоящие изменения в потоке постов: как подготовить темы и плагины

Мы отказываемся от устаревшей системы рендеринга «виджетов» в пользу современных компонентов Glimmer.

Недавно мы модернизировали поток постов, используя компоненты Glimmer. Это руководство поможет вам перенести ваши плагины и темы со старой системы на основе виджетов на новую реализацию Glimmer.

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

График

Это приблизительные сроки, которые могут быть изменены

II квартал 2025 года:

  • :white_check_mark: Завершена базовая реализация
  • :white_check_mark: Начато обновление официальных плагинов и компонентов тем
  • :white_check_mark: Включено на Meta
  • :white_check_mark: Опубликованы рекомендации по обновлению (данное руководство)

III квартал 2025 года:

  • :white_check_mark: Завершено обновление официальных плагинов и компонентов тем
  • :white_check_mark: Установлено значение по умолчанию для glimmer_post_stream_mode равным auto, включены сообщения об устаревании в консоли
  • :white_check_mark: Включены сообщения об устаревании с предупреждающим баннером для администраторов (планируется на июль 2025 года)
  • Плагины и темы сторонних разработчиков должны быть обновлены

IV квартал 2025 года:

  • :white_check_mark: Новый поток постов включен по умолчанию
  • :white_check_mark: Удалена настройка флага функции и устаревший код

Что это значит для меня?

Если ваши плагины или темы используют какие-либо API «виджетов» для кастомизации потока постов, вам необходимо обновить их для работы с новой версией.

Как попробовать новый поток постов?

Чтобы попробовать новый поток постов, просто измените настройку glimmer_post_stream_mode на auto в настройках вашего сайта. Это включит новый поток постов, если у вас нет несовместимых плагинов или тем.

Когда glimmer_post_stream_mode установлен в auto, Discourse автоматически обнаруживает несовместимые плагины и темы. Если они найдены, в консоли браузера появятся полезные предупреждающие сообщения, которые точно укажут, какие плагины или темы требуют обновления, а также предоставят трассировку стека для помощи в поиске соответствующего кода.

Эти сообщения помогут вам точно определить, какие части ваших плагинов или тем необходимо обновить для совместимости с новым потоком постов Glimmer.

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

У меня установлены кастомные плагины или темы. Нужно ли мне их обновлять?

Вам необходимо обновить ваши плагины или темы, если они выполняют любые из следующих кастомизаций:

  • Используют decorateWidget, changeWidgetSetting, reopenWidget или attachWidgetAction для следующих виджетов:

    • actions-summary
    • avatar-flair
    • embedded-post
    • expand-hidden
    • expand-post-button
    • filter-jump-to-post
    • filter-show-all
    • post-article
    • post-avatar-user-info
    • post-avatar
    • post-body
    • post-contents
    • post-date
    • post-edits-indicator
    • post-email-indicator
    • post-gap
    • post-group-request
    • post-links
    • post-locked-indicator
    • post-meta-data
    • post-notice
    • post-placeholder
    • post-stream
    • post
    • poster-name
    • poster-name-title
    • posts-filtered-notice
    • reply-to-tab
    • select-post
    • topic-post-visited-line
  • Используют один из следующих методов API:

    • addPostTransformCallback
    • includePostAttributes

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

ID устаревания: discourse.post-stream-widget-overrides

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

Чем заменить?

Новый поток постов Glimmer предоставляет несколько способов кастомизации отображения постов:

  1. Plugin Outlets (Слоты плагинов): Для добавления контента в определенных точках потока постов.
  2. Transformers (Трансформеры): Для кастомизации элементов, изменения структур данных или поведения компонентов.

Замена includePostAttributes на addTrackedPostProperties

Если ваш плагин использует includePostAttributes для добавления свойств в модель поста, вам необходимо обновить его и использовать addTrackedPostProperties.

До:

api.includePostAttributes('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');

После:

api.addTrackedPostProperties('can_accept_answer', 'accepted_answer', 'topic_accepted_answer');

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

Распространенные паттерны миграции

Использование Plugin Outlets

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

Они являются ключом к отказу от декораций виджетов.

Поток постов Glimmer оборачивает часто кастомизируемый контент в слоты. Используйте функции API плагинов renderBeforeWrapperOutlet и renderAfterWrapperOutlet, чтобы вставить контент до или после них.

1. Замена декораций виджетов на Plugin Outlets

Самая распространенная кастомизация — добавление контента к постам. В системе виджетов вы использовали decorateWidget. С Glimmer вы будете использовать слоты плагинов.

До:

// Часть инициализатора в плагине

import { withPluginApi } from "discourse/lib/plugin-api";
// ... другие импорты

function customizeWidgetPost(api) {
  api.decorateWidget("post-contents:after-cooked", (helper) => {
    const post = helper.getModel();
    if (post.post_number === 1 && post.topic.accepted_answer) {
      return helper.attach("solved-accepted-answer", { post });
    }
  });
}

export default {
  name: "extend-for-solved-button",
  initialize() {
    withPluginApi((api) => {
      // ... другие кастомизации
      customizeWidgetPost(api);
    });
  }
};

После:

// Часть инициализатора .gjs в плагине

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
// ... другие импорты

function customizePost(api) {
  api.renderAfterWrapperOutlet(
    "post-content-cooked-html",
    class extends Component {
      static shouldRender(args) {
        return args.post?.post_number === 1 && args.post?.topic?.accepted_answer;
      }

      <template>
        <SolvedAcceptedAnswer 
          @post={{@post}} 
        />
      </template>
    }
  );
}

export default {
  name: "extend-for-solved-button",
  initialize() {
    withPluginApi((api) => {
      // ... другие кастомизации
      customizePost(api);
    });
  }
};

2. Добавление контента после имени автора

Если ваш плагин добавляет контент после имени автора, используйте API renderAfterWrapperOutlet со слотом post-meta-data-poster-name.

До:

// Часть инициализатора в плагине

import { withPluginApi } from "discourse/lib/plugin-api";
// ... другие импорты

function customizeWidgetPost(api) {
  api.decorateWidget(`poster-name:after`, (dec) => {
    if (!isGPTBot(dec.attrs.user)) {
      return;
    }

    return dec.widget.attach("persona-flair", {
      personaName: dec.model?.topic?.ai_persona_name,
    });
  });
}

export default {
  name: "ai-bot-replies",
  initialize() {
    withPluginApi((api) => {
      // ... другие кастомизации
      customizeWidgetPost(api);
    });
  }
};

После:

// Часть инициализатора .gjs в плагине

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... другие импорты

function customizePost(api) {
  api.renderAfterWrapperOutlet(
    "post-meta-data-poster-name", 
    class extends Component {
      static shouldRender(args) {
        return isGPTBot(args.post?.user);
      }

      <template>
        <span class="persona-flair">{{@post.topic.ai_persona_name}}</span>
      </template>
    }
  );
}

export default {
  name: "ai-bot-replies",
  initialize() {
    withPluginApi((api) => {
      // ... другие кастомизации
      customizePost(api);
    });
  }
};

3. Добавление контента перед содержимым поста

// Часть инициализатора .gjs в теме

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... другие импорты

function customizePost(api) {
  api.renderBeforeWrapperOutlet(
    "post-article",
    class extends Component {
      static shouldRender(args) {
        return args.post?.topic?.pinned;
      }

      <template>
        <div class="pinned-post-notice">
          Это закрепленная тема
        </div>
      </template>
    }
  );
}

export default {
  name: "pinned-topic-notice",
  initialize() {
    withPluginApi((api) => {
      // ... другие кастомизации
      customizePost(api);
    });
  }
};

4. Добавление контента после содержимого поста

// Часть инициализатора .gjs в теме

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... другие импорты

function customizePost(api) {
  api.renderAfterWrapperOutlet(
    "post-article",
    class extends Component {
      static shouldRender(args) {
        return args.post?.wiki;
      }

      // В реальном компоненте вы бы использовали шаблон так:
      <template>
        <div class="wiki-post-notice">
          Этот пост является вики-статьей
        </div>
      </template>
    }
  );
}

export default {
  name: "wiki-post-notice",
  initialize() {
    withPluginApi((api) => {
      customizePost(api);
      // ... другие кастомизации
    });
  }
};

Использование Transformers

Трансформеры — это мощный способ кастомизации компонентов Discourse. Они позволяют изменять данные или поведение компонентов без необходимости переопределять целые компоненты.

Вот некоторые из наиболее актуальных трансформеров значений для кастомизации потока постов:

Название трансформера Описание Контекст
post-class Кастомизация CSS-классов, применяемых к основному элементу поста. { post }
post-meta-data-infos Кастомизация списка компонентов метаданных, отображаемых для поста. Это позволяет добавлять, удалять или менять порядок таких элементов, как дата поста, индикатор редактирования и т.д. { post, metaDataInfoKeys }
post-meta-data-poster-name-suppress-similar-name Решение о том, следует ли скрывать полное имя пользователя, если оно похоже на его имя пользователя. Верните true, чтобы скрыть. { post, name }
post-notice-component Кастомизация или замена компонента, используемого для отображения уведомления о посте. { post, type }
post-show-topic-map Управление видимостью компонента карты темы в первом посте. { post, isPM, isRegular, showWithoutReplies }
post-small-action-class Добавление пользовательских CSS-классов к посту с небольшим действием. { post, actionCode }
post-small-action-custom-component Замена стандартного поста с небольшим действием на пользовательский компонент Glimmer. { post, actionCode }
post-small-action-icon Кастомизация иконки, используемой для поста с небольшим действием. { post, actionCode }
poster-name-class Добавление пользовательских CSS-классов к контейнеру имени автора. { user }

1. Добавление пользовательского класса к постам

// Часть инициализатора в плагине
import { withPluginApi } from "discourse/lib/plugin-api";

function customizePostClasses(api) {
  api.registerValueTransformer(
    "post-class",
    ({ value, context }) => {
      const { post } = context;

      // Добавление пользовательского класса к постам от определенного пользователя
      if (post.user_id === 1) {
        return [...value, "special-user-post"];
      }

      return value;
    }
  );
}

export default {
  name: "custom-post-classes",
  initialize() {
    withPluginApi((api) => {
      // ... другие кастомизации
      customizePostClasses(api);
    });
  }
};

2. Добавление пользовательских метаданных поста

Трансформер post-meta-data-infos позволяет добавлять пользовательские компоненты в секцию метаданных поста.

// Часть инициализатора в плагине
import { withPluginApi } from "discourse/lib/plugin-api";

function customizePostMetadata(api) {
  // Определение компонента, который будет использоваться в секции метаданных
  // Компонент должен быть создан вне обратного вызова трансформера,
  // иначе это может вызвать проблемы с памятью.
  const CustomMetadataComponent = <template>...</template>;

  api.registerValueTransformer(
    "post-meta-data-infos",
    ({ value: metadata, context: { post, metaDataInfoKeys } }) => {
      // Добавление компонента только для определенных постов
      if (post.some_custom_property) {
        metadata.add(
          "custom-metadata-key",
          CustomMetadataComponent,
          {
            // Разместить его перед датой
            before: metaDataInfoKeys.DATE,
            // и после вкладки ответа
            after: metaDataInfoKeys.REPLY_TO_TAB,
          }
        );
      }
    }
  );
}

export default {
  name: "custom-post-metadata",
  initialize() {
    withPluginApi((api) => {
      // ... другие кастомизации
      customizePostMetadata(api);
    });
  }
};

Вот реальный пример из плагина discourse-activity-pub:

// Часть инициализатора в плагине discourse-activity-pub
import { withPluginApi } from "discourse/lib/plugin-api";
import ActivityPubPostStatus from "../components/activity-pub-post-status";
import {
  activityPubPostStatus,
  showStatusToUser,
} from "../lib/activity-pub-utilities";

function customizePost(api, container) {
  const currentUser = api.getCurrentUser();

  const PostMetadataActivityPubStatus = <template>
    <div class="post-info activity-pub">
      <ActivityPubPostStatus @post={{@post}} />
    </div>
  </template>;

  api.registerValueTransformer(
    "post-meta-data-infos",
    ({ value: metadata, context: { post, metaDataInfoKeys } }) => {
      const site = container.lookup("service:site");
      const siteSettings = container.lookup("service:site-settings");

      if (
        site.activity_pub_enabled &&
        post.activity_pub_enabled &&
        post.post_number !== 1 &&
        showStatusToUser(currentUser, siteSettings)
      ) {
        const status = activityPubPostStatus(post);

        if (status) {
          metadata.add(
            "activity-pub-indicator",
            PostMetadataActivityPubStatus,
            {
              before: metaDataInfoKeys.DATE,
              after: metaDataInfoKeys.REPLY_TO_TAB,
            }
          );
        }
      }
    }
  );
}

export default {
  name: "activity-pub",
  initialize(container) {
    withPluginApi((api) => {
      customizePost(api, container);
      // ... другие кастомизации
    });
  }
};

5. Вставка контента до или после обработанного содержимого поста

Если ваш плагин добавляет контент после текста в посте, используйте API renderAfterWrapperOutlet со слотом post-content-cooked-html.

До:

// Часть инициализатора в плагине
import { withPluginApi } from "discourse/lib/plugin-api";

function customizeCooked(api) {
  api.decorateWidget("post-contents:after-cooked", (helper) => {
    const post = helper.getModel();
    if (post.wiki) {
      const banner = document.createElement("div");
      banner.classList.add("wiki-footer");
      banner.textContent = "Этот пост является вики-статьей";
      element.prepend(banner);
    }
  });
}

export default {
  name: "wiki-footer",
  initialize() {
    withPluginApi((api) => {
      // ... другие кастомизации
      customizeCooked(api);
    });
  }
};

После:

// Часть инициализатора в плагине (.gjs)
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";

// Определение компонента, который будет использоваться в слоте плагина
class WikiBanner extends Component {
  static shouldRender(args) {
    return args.post.wiki;
  }

  <template>
    <div class="wiki-footer">Этот пост является вики-статьей</div>
  </template>
}

function customizePost(api) {
  // Используйте renderBeforeWrapperOutlet для добавления контента перед содержимым поста
  api.renderAfterWrapperOutlet(
    "post-content-cooked-html",
    WikiBanner
  );
}

export default {
  name: "wiki-footer",
  initialize() {
    withPluginApi((api) => {
      customizePost(api);
      // ... другие кастомизации
    });
  }
};

Поддержка обеих систем во время перехода

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

Вот паттерн, используемый многими официальными плагинами:

// solved-button.js
import Component from "@glimmer/component";
import { withSilencedDeprecations } from "discourse/lib/deprecated";
import { withPluginApi } from "discourse/lib/plugin-api";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";

function customizePost(api) {
  // кастомизации потока постов glimmer
  api.renderAfterWrapperOutlet(
    "post-content-cooked-html",
    class extends Component {
      static shouldRender(args) {
        return args.post?.post_number === 1 && args.post?.topic?.accepted_answer;
      }

      <template>
        <SolvedAcceptedAnswer 
          @post={{@post}} 
          @decoratorState={{@decoratorState}} 
        />
      </template>
    }
  );

  // ...

  // оборачиваем старый код виджетов, отключая предупреждения об устаревании
  withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
    customizeWidgetPost(api)
  );
}

// старый код виджетов
function customizeWidgetPost(api) {
  api.decorateWidget("post-contents:after-cooked", (helper) => {
    let post = helper.getModel();

    if (helper.attrs.post_number === 1 && post?.topic?.accepted_answer) {
      // Используйте RenderGlimmer для рендеринга компонента Glimmer в системе виджетов
      return new RenderGlimmer(
        helper.widget,
        "div",
        <template><SolvedAcceptedAnswer @post={{@data.post}} /></template>
        null,
        { post }
      );
    }
  });
}

export default {
  name: "extend-for-solved-button",
  initialize(container) {
    const siteSettings = container.lookup("service:site-settings");

    if (siteSettings.solved_enabled) {
      withPluginApi((api) => {
        customizePost(api);
        // ... другие кастомизации
      });
    }
  },
};

Реальные примеры

Вот ссылки на реальные pull-реквесты миграции из наших официальных плагинов. Они показывают, как мы обновляли каждый тип кастомизации:

Устранение неполадок

Мой сайт сломан после включения нового потока постов

  • Верните glimmer_post_stream_mode в disabled
  • Проверьте консоль на наличие конкретных сообщений об ошибках
  • Обновите проблемные плагины/темы перед повторной попыткой

Я не вижу никаких предупреждений, но мои кастомизации не работают

  • Убедитесь, что ваши кастомизации нацелены на виджеты, перечисленные выше
  • Убедитесь, что вы тестируете на странице темы с темами, где ваши кастомизации видны

Нужна помощь?

Если вы нашли ошибку или ваша кастомизация не может быть реализована с помощью новых API, которые мы представили, пожалуйста, сообщите нам об этом ниже.

Поток постов Glimmer включен в Meta.

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

При обновлении моих компонентов я столкнулся со странным кодом:

Во-первых, я несколько озадачен конструкцией @user={{@post}}. Не является ли это опечаткой?

Во-вторых, почему PluginOutlet с именем post-avatar-flair и компонент UserAvatarFlair представляют собой отдельные элементы? Кроме того, почему post-avatar-flair не является обёрткой, как другие близлежащие outlets?

Это работает, поэтому я не думаю, что это опечатка. Возможно, в этом месте объект поста содержит все атрибуты, которые компонент UserAvatarFlair ожидает найти в аргументе @user? Согласен, выглядит это странно!

Согласно описанию PR, похоже, что эти решения были приняты ради согласованности с другими аналогичными «avatar-flair» outlets (которые, вероятно, были введены до появления «wrapper plugin outlets»).

Мы только что объединили запрос на слияние, который включает Glimmer Post Stream по умолчанию.

После следующего обновления совместимые сайты автоматически будут использовать новый Post Stream. Сайты, использующие несовместимые расширения, вернутся к старой версии и будут отображать предупреждения в консоли браузера.

На данный момент вы можете вручную принудительно включить использование старого Post Stream, установив glimmer_post_stream_mode в значение disabled.

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

Я думаю, что получаю предупреждение из-за этого кода, который затем ссылается на эту тему:

api.changeWidgetSetting('post-avatar', 'size', '120');

Как мне обновить это для новой системы?

Что-то вроде этого:

api.registerValueTransformer("post-avatar-size", () => {
    return "120";
});

Отлично, спасибо.

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

Привет, @Boost. :smiley:

Что вы имеете в виду, когда говорите, что переопределения пока не рекомендуется использовать? Если ваш сайт готов, он автоматически переключится на Glimmer Post Stream.

При следующем обновлении вашей установки Discourse Glimmer Post Stream будет включен по умолчанию даже для сайтов, у которых все еще есть несовместимые кастомизации.

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

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

  • glimmer_post_stream_mode
  • deactivate_widgets_rendering

Чтобы снова включить поток постов с виджетами, необходимо изменить обе настройки.

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

Параметр в разделе экспериментальных настроек довольно явно предупреждает об использовании его:

Включить новую реализацию потока постов «glimmer» в режиме «auto» для указанных групп пользователей. Эта реализация находится в активной разработке и не предназначена для использования в продакшене. Не разрабатывайте темы/плагины под неё до тех пор, пока реализация не будет завершена и не будет объявлено об этом.

Таким образом, формулировка довольно категорична: «не предназначена для использования в продакшене. Не разрабатывайте темы/плагины под неё».

Это объяснение для параметра Glimmer post stream mode auto groups.

Хорошо подмечено. Мы упустили это.

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

PR, удаляющий поток постов виджета, был принят.