帖子流即将变更 - 如何准备主题和插件

我们正逐步弃用旧的“小部件”(widget)渲染系统,转而采用现代的 Glimmer 组件。

我们最近已使用 Glimmer 组件对帖子流进行了现代化改造。本指南将引导您将插件和主题从旧的基于小部件的系统迁移到新的 Glimmer 实现。

请放心——这次迁移比乍一看要简单得多。我们设计的新系统比旧的小部件系统更直观、更强大,本指南将帮助您完成整个过程。

时间线

这些为预估时间,可能会有变动

2025 年第二季度:

  • :white_check_mark: 核心实现已完成
  • :white_check_mark: 开始升级官方插件和主题组件
  • :white_check_mark: 已在 Meta 站点启用
  • :white_check_mark: 已发布升级建议(即本指南)

2025 年第三季度:

  • :white_check_mark: 完成官方插件和主题组件的升级
  • :white_check_mark:glimmer_post_stream_mode 默认值设置为 auto,并启用控制台弃用消息
  • :white_check_mark: 启用带有管理员警告横幅的弃用消息(计划于 2025 年 7 月实施)
  • 第三方插件和主题应进行更新

2025 年第四季度:

  • :white_check_mark: 默认启用新的帖子流
  • :white_check_mark: 移除功能标志设置和遗留代码

这对我意味着什么?

如果您的任何插件或主题使用了任何“小部件”API 来自定义帖子流,则需要更新它们以适配新版本。

我如何尝试新的帖子流?

要尝试新的帖子流,只需在站点设置中将 glimmer_post_stream_mode 设置更改为 auto。如果您没有安装不兼容的插件或主题,这将启用新的帖子流。

glimmer_post_stream_mode 设置为 auto 时,Discourse 会自动检测不兼容的插件和主题。如果发现任何不兼容项,您将在浏览器控制台中看到有帮助的警告消息,明确指出哪些插件或主题需要更新,并附带堆栈跟踪以帮助您定位相关代码。

这些消息将帮助您准确识别插件或主题中需要更新以兼容新的 Glimmer 帖子流的部分。

如果您在使用新帖子流时遇到任何问题,请不用担心。目前,您仍可以将该设置改为 disabled 以返回旧系统。如果您安装了不兼容的扩展但仍想尝试,作为管理员,您可以将选项设置为 enabled 以强制启用新帖子流。但请务必谨慎使用——根据您的自定义内容,您的站点可能无法正常工作。

我安装了自定义插件或主题。我需要更新它们吗?

如果您的插件或主题执行了以下任何自定义操作,则需要进行更新:

  • 在以下小部件上使用 decorateWidgetchangeWidgetSettingreopenWidgetattachWidgetAction

    • 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):用于自定义项目、修改数据结构或更改组件行为。

addTrackedPostProperties 替换 includePostAttributes

如果您的插件使用 includePostAttributes 向帖子模型添加属性,则需要更新为使用 addTrackedPostProperties

之前:

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

之后:

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

addTrackedPostProperties 函数将属性标记为跟踪属性,以便在帖子更新时生效。如果您的插件向帖子添加属性并在渲染时使用它们,这一点至关重要。它确保 UI 在这些属性更改时自动更新。

常见迁移模式

使用插件出口

插件出口是您在帖子流特定点添加内容的主要工具。可以将它们想象为您可以注入自定义内容的指定位置。

它们是迁移脱离小部件装饰的关键。

Glimmer 帖子流将经常自定义的内容封装在出口中。使用插件 API 函数 renderBeforeWrapperOutletrenderAfterWrapperOutlet 可以在它们之前或之后插入内容。

1. 用插件出口替换小部件装饰

最常见的自定义是向帖子添加内容。在小部件系统中,您会使用 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. 在发布者名称后添加内容

如果您的插件在发布者名称后添加内容,您将使用 renderAfterWrapperOutlet API 和 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);
      // ... 其他自定义
    });
  }
};

使用转换器

转换器是自定义 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. 在帖子渲染内容前后插入内容

如果您的插件在帖子文本后添加内容,您将使用 renderAfterWrapperOutlet API 和 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>
    }
  );

  // ...

  // 包装旧的 widget 代码并静默弃用警告
  withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
    customizeWidgetPost(api)
  );
}

// 旧的 widget 代码
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 在 widget 系统中渲染 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);
        // ... 其他自定义
      });
    }
  },
};

真实示例

以下是来自我们官方插件的实际迁移拉取请求链接。这些展示了我们如何更新每种类型的自定义:

故障排除

启用新帖子流后我的站点显示异常

  • glimmer_post_stream_mode 改回 disabled
  • 检查控制台中的具体错误消息
  • 在再次尝试之前更新有问题的插件/主题

我没有看到任何警告,但我的自定义不起作用

  • 检查您的自定义是否针对上述列出的小部件
  • 确保您在自定义可见的主题页面上进行测试

需要帮助?

如果您发现了错误,或者无法使用我们引入的新 API 实现您的自定义,请在下方告诉我们。

Glimmer Post Stream 已在 Meta 上启用。

如果您发现任何问题,请在下方发布。

在升级我的组件时,我遇到了一些奇怪的代码:

首先,我对 @user={{@post}} 感到有些困惑。这会不会是印刷错误?

其次,为什么 PluginOutlet 被命名为 post-avatar-flairUserAvatarFlair 是单独的元素?另外,为什么 post-avatar-flair 不像其他附近的 outlet 那样是包装器?

它能正常工作,所以我不认为这是笔误。也许在这个位置,post 对象拥有 UserAvatarFlair 组件期望在 @user 参数上找到的所有属性?我同意它看起来很奇怪!

根据 PR 描述,看起来这些是为了与其他类似的“头像-装饰” outlet(可能是在“包装器插件 outlet”出现之前引入的)保持一致性而做的。

我们刚刚合并了一个默认启用 Glimmer Post Stream 的拉取请求。

下次更新后,兼容的网站将自动使用新的 Post Stream。使用不兼容扩展的网站将回退到旧版本,并在浏览器控制台中显示警告。

目前,您可以通过将 glimmer_post_stream_mode 设置为 disabled 来手动强制使用旧的 Post Stream。

如果您遇到任何问题,请在下方报告。

我认为我收到了一条警告,原因是我使用了这段代码,然后它链接到了这个帖子:

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”帖子流实现。此实现正在积极开发中,无意用于生产环境。在实现最终确定并宣布之前,请勿基于此开发主题/插件。

因此,该措辞非常强烈地表明“无意用于生产环境。请勿基于此开发主题/插件”。

这就是 Glimmer post stream mode auto groups 选项的解释。

抓得好。我们忽略了这一点。

Glimmer Post Stream 现在是默认设置,而小部件版本被视为旧版,并已安排移除。我将更新设置说明。

移除了流后小部件的拉取请求(PR)已被合并。