Mudanças futuras no stream de posts - Como preparar temas e plugins

Estamos migrando do sistema de renderização “widget” legado, substituindo-o por componentes Glimmer modernos.

Recentemente, modernizamos o fluxo de publicações (post stream) usando componentes Glimmer. Este guia o acompanhará na migração de seus plugins e temas do antigo sistema baseado em widgets para a nova implementação Glimmer.

Não se preocupe — essa migração é mais simples do que pode parecer à primeira vista. Projetamos o novo sistema para ser mais intuitivo e poderoso que o antigo sistema de widgets, e este guia o ajudará no processo.

Cronograma

Estas são estimativas sujeitas a alterações

Q2 2025:

  • :white_check_mark: Implementação principal concluída
  • :white_check_mark: Início da atualização dos componentes oficiais de plugins e temas
  • :white_check_mark: Habilitado no Meta
  • :white_check_mark: Publicação de orientações de atualização (este guia)

Q3 2025:

  • :white_check_mark: Conclusão da atualização dos componentes oficiais de plugins e temas
  • :white_check_mark: Definir glimmer_post_stream_mode como padrão auto e habilitar mensagens de descontinuação no console
  • :white_check_mark: Habilitar mensagens de descontinuação com um banner de aviso de administrador (planejado para julho de 2025)
  • Plugins e temas de terceiros devem ser atualizados

Q4 2025:

  • :white_check_mark: Habilitar o novo fluxo de publicações por padrão
  • :white_check_mark: Remover a configuração de feature flag e o código legado

O que isso significa para mim?

Se algum dos seus plugins ou temas utiliza alguma API de “widget” para personalizar o fluxo de publicações, você precisará atualizá-los para funcionar com a nova versão.

Como posso testar o novo fluxo de publicações?

Para testar o novo fluxo de publicações, basta alterar a configuração glimmer_post_stream_mode para auto nas configurações do seu site. Isso habilitará o novo fluxo de publicações, desde que você não tenha plugins ou temas incompatíveis.

Quando glimmer_post_stream_mode estiver definido como auto, o Discourse detectará automaticamente plugins e temas incompatíveis. Se encontrar algum, você verá mensagens de aviso úteis no console do navegador que identificam exatamente quais plugins ou temas precisam ser atualizados, juntamente com rastreamentos de pilha (stack traces) para ajudá-lo a localizar o código relevante.

Essas mensagens ajudarão você a identificar exatamente quais partes dos seus plugins ou temas precisam ser atualizadas para serem compatíveis com o novo fluxo de publicações Glimmer.

Se encontrar problemas ao usar o novo fluxo de publicações, não se preocupe: por enquanto, você ainda pode definir a configuração como disabled para retornar ao sistema antigo. Se tiver extensões incompatíveis instaladas, mas ainda quiser testar, como administrador, você pode definir a opção como enabled para forçar o novo fluxo de publicações. Use isso com cautela — seu site pode não funcionar corretamente, dependendo das personalizações que você possui.

Tenho plugins ou temas personalizados instalados. Preciso atualizá-los?

Você precisará atualizar seus plugins ou temas se realizarem alguma das seguintes personalizações:

  • Usar decorateWidget, changeWidgetSetting, reopenWidget ou attachWidgetAction nestes widgets:

    • 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
  • Usar um dos métodos de API abaixo:

    • addPostTransformCallback
    • includePostAttributes

:light_bulb: Se você tiver extensões que usam alguma das personalizações acima, verá um aviso no console identificando qual plugin ou componente precisa ser atualizado quando visitar uma página de tópico.

O ID de descontinuação é: discourse.post-stream-widget-overrides

:warning: Se você usa mais de um tema em sua instância, verifique todos eles, pois os avisos aparecerão apenas para plugins ativos e temas e componentes de tema atualmente em uso.

Quais são as substituições?

O novo fluxo de publicações Glimmer oferece várias maneiras de personalizar como as publicações aparecem:

  1. Plugin Outlets: Para adicionar conteúdo em pontos específicos do fluxo de publicações.
  2. Transformers: Para personalizar itens, modificar estruturas de dados ou alterar o comportamento de componentes.

Substituindo includePostAttributes por addTrackedPostProperties

Se o seu plugin usa includePostAttributes para adicionar propriedades ao modelo de publicação, você precisará atualizá-lo para usar addTrackedPostProperties.

Antes:

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

Depois:

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

A função addTrackedPostProperties marca propriedades como rastreadas para atualizações de publicação. Isso é importante se o seu plugin adiciona propriedades a uma publicação e as usa durante a renderização. Isso garante que a interface do usuário seja atualizada automaticamente quando essas propriedades mudarem.

Padrões Comuns de Migração

Usando Plugin Outlets

Os Plugin Outlets são sua principal ferramenta para adicionar conteúdo em pontos específicos do fluxo de publicações. Pense neles como locais designados onde você pode injetar seu conteúdo personalizado.

Eles são a chave para migrar das decorações de widgets.

O Fluxo de Publicações Glimmer envolve conteúdo frequentemente personalizado em outlets. Use as funções da API de plugin renderBeforeWrapperOutlet e renderAfterWrapperOutlet para inserir conteúdo antes ou depois deles.

1. Substituindo decorações de widget por Plugin Outlets

A personalização mais comum é adicionar conteúdo às publicações. No sistema de widgets, você usaria decorateWidget. Com Glimmer, você usará Plugin Outlets.

Antes:

// Parte de um inicializador em um plugin

import { withPluginApi } from "discourse/lib/plugin-api";
// ... outras importações

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) => {
      // ... outras personalizações
      customizeWidgetPost(api);
    });
  }
};

Depois:

// Parte de um inicializador .gjs em um plugin

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
// ... outras importações

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) => {
      // ... outras personalizações
      customizePost(api);
    });
  }
};

2. Adicionando conteúdo após o nome do autor

Se o seu plugin adiciona conteúdo após o nome do autor, você usará a API renderAfterWrapperOutlet com o outlet post-meta-data-poster-name.

Antes:

// Parte de um inicializador em um plugin

import { withPluginApi } from "discourse/lib/plugin-api";
// ... outras importações

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) => {
      // ... outras personalizações
      customizeWidgetPost(api);
    });
  }
};

Depois:

// Parte de um inicializador .gjs em um plugin

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... outras importações

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) => {
      // ... outras personalizações
      customizePost(api);
    });
  }
};

3. Adicionando conteúdo antes do conteúdo da publicação

// Parte de um inicializador .gjs em um tema

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... outras importações

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

      <template>
        <div class="pinned-post-notice">
          Este é um tópico fixado
        </div>
      </template>
    }
  );
}

export default {
  name: "pinned-topic-notice",
  initialize() {
    withPluginApi((api) => {
      // ... outras personalizações
      customizePost(api);
    });
  }
};

4. Adicionando conteúdo após o conteúdo da publicação

// Parte de um inicializador .gjs em um tema

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... outras importações

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

      // Em um componente real, você usaria um template assim:
      <template>
        <div class="wiki-post-notice">
          Esta publicação é um wiki
        </div>
      </template>
    }
  );
}

export default {
  name: "wiki-post-notice",
  initialize() {
    withPluginApi((api) => {
      customizePost(api);
      // ... outras personalizações
    });
  }
};

Usando Transformers

Transformers são uma maneira poderosa de personalizar componentes do Discourse. Eles permitem modificar dados ou o comportamento de componentes sem precisar substituir componentes inteiros.

Aqui estão alguns dos transformers de valor mais relevantes para personalização do fluxo de publicações:

Nome do Transformer Descrição Contexto
post-class Personalizar classes CSS aplicadas ao elemento principal da publicação. { post }
post-meta-data-infos Personalizar a lista de componentes de metadados exibidos para uma publicação. Isso permite adicionar, remover ou reordenar itens como a data da publicação, indicador de edição, etc. { post, metaDataInfoKeys }
post-meta-data-poster-name-suppress-similar-name Decidir se deve suprimir a exibição do nome completo do usuário quando for semelhante ao nome de usuário. Retorne true para suprimir. { post, name }
post-notice-component Personalizar ou substituir o componente usado para renderizar um aviso de publicação. { post, type }
post-show-topic-map Controlar a visibilidade do componente de mapa de tópicos na primeira publicação. { post, isPM, isRegular, showWithoutReplies }
post-small-action-class Adicionar classes CSS personalizadas a uma ação pequena de publicação. { post, actionCode }
post-small-action-custom-component Substituir uma ação pequena de publicação padrão por um componente Glimmer personalizado. { post, actionCode }
post-small-action-icon Personalizar o ícone usado para uma ação pequena de publicação. { post, actionCode }
poster-name-class Adicionar classes CSS personalizadas ao contêiner do nome do autor. { user }

1. Adicionando uma classe personalizada às publicações

// Parte de um inicializador em um plugin
import { withPluginApi } from "discourse/lib/plugin-api";

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

      // Adicionar uma classe personalizada a publicações de um usuário específico
      if (post.user_id === 1) {
        return [...value, "special-user-post"];
      }

      return value;
    }
  );
}

export default {
  name: "custom-post-classes",
  initialize() {
    withPluginApi((api) => {
      // ... outras personalizações
      customizePostClasses(api);
    });
  }
};

2. Adicionando metadados personalizados à publicação

O transformer post-meta-data-infos permite adicionar componentes personalizados à seção de metadados da publicação.

// Parte de um inicializador em um plugin
import { withPluginApi } from "discourse/lib/plugin-api";

function customizePostMetadata(api) {
  // Definir um componente a ser usado na seção de metadados
  // O componente deve ser criado fora do callback do transformer,
  // caso contrário, pode causar problemas de memória.
  const CustomMetadataComponent = <template>...</template>;

  api.registerValueTransformer(
    "post-meta-data-infos",
    ({ value: metadata, context: { post, metaDataInfoKeys } }) => {
      // Adicionar o componente apenas para publicações específicas
      if (post.some_custom_property) {
        metadata.add(
          "custom-metadata-key",
          CustomMetadataComponent,
          {
            // Posicioná-lo antes da data
            before: metaDataInfoKeys.DATE,
            // e após a aba de resposta
            after: metaDataInfoKeys.REPLY_TO_TAB,
          }
        );
      }
    }
  );
}

export default {
  name: "custom-post-metadata",
  initialize() {
    withPluginApi((api) => {
      // ... outras personalizações
      customizePostMetadata(api);
    });
  }
};

Aqui está um exemplo do mundo real do plugin discourse-activity-pub:

// Parte de um inicializador no plugin 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);
      // ... outras personalizações
    });
  }
};

5. Inserindo conteúdo antes ou depois do conteúdo formatado da publicação

Se o seu plugin adiciona conteúdo após o texto na publicação, você usará a API renderAfterWrapperOutlet com o outlet post-content-cooked-html.

Antes:

// Parte de um inicializador em um plugin
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 = "Esta publicação é um wiki";
      element.prepend(banner);
    }
  });
}

export default {
  name: "wiki-footer",
  initialize() {
    withPluginApi((api) => {
      // ... outras personalizações
      customizeCooked(api);
    });
  }
};

Depois:

// Parte de um inicializador em um plugin (.gjs)
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";

// Definir um componente a ser usado no plugin outlet
class WikiBanner extends Component {
  static shouldRender(args) {
    return args.post.wiki;
  }

  <template>
    <div class="wiki-footer">Esta publicação é um wiki</div>
  </template>
}

function customizePost(api) {
  // Usar renderBeforeWrapperOutlet para adicionar conteúdo antes do conteúdo da publicação
  api.renderAfterWrapperOutlet(
    "post-content-cooked-html",
    WikiBanner
  );
}

export default {
  name: "wiki-footer",
  initialize() {
    withPluginApi((api) => {
      customizePost(api);
      // ... outras personalizações
    });
  }
};

Suportando Ambos os Sistemas Antigo e Novo Durante a Transição

Como autor de plugins ou temas, você pode querer suportar ambos os sistemas durante a transição para garantir que suas extensões funcionem com os fluxos de publicações antigo e novo.

Aqui está um padrão usado por muitos plugins oficiais:

// 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) {
  // personalizações do fluxo de publicações 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>
    }
  );

  // ...

  // envolver o código antigo de widget silenciando os avisos de descontinuação
  withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
    customizeWidgetPost(api)
  );
}

// código antigo de 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) {
      // Usar RenderGlimmer para renderizar um componente Glimmer no sistema de widgets
      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);
        // ... outras personalizações
      });
    }
  },
};

Exemplos do Mundo Real

Aqui estão links para pull requests reais de migração de nossos plugins oficiais. Eles mostram como atualizamos cada tipo de personalização:

Solução de Problemas

Meu site parece quebrado após habilitar o novo fluxo de publicações

  • Defina glimmer_post_stream_mode de volta para disabled
  • Verifique o console em busca de mensagens de erro específicas
  • Atualize os plugins/temas problemáticos antes de tentar novamente

Não vejo nenhum aviso, mas minhas personalizações não estão funcionando

  • Verifique se suas personalizações têm como alvo os widgets listados acima
  • Certifique-se de estar testando na página de tópico com tópicos onde suas personalizações são visíveis

Precisa de Ajuda?

Se você encontrou um bug ou sua personalização não pode ser alcançada usando as novas APIs que introduzimos, por favor, informe-nos abaixo.

O Glimmer Post Stream está habilitado na Meta.

Se você encontrar algum problema, por favor, poste-o abaixo.

Ao atualizar meus componentes, encontrei um código estranho:

Primeiramente, estou um tanto perplexo com @user={{@post}}. Isso poderia ser um erro de digitação?

Em segundo lugar, por que o PluginOutlet chamado post-avatar-flair e o UserAvatarFlair são elementos separados? Além disso, por que post-avatar-flair não é um wrapper como outros outlets próximos?

Está funcionando, então não acho que seja um erro de digitação. Talvez neste local, o objeto post tenha todos os atributos que o componente UserAvatarFlair espera encontrar no argumento @user? Concordo que parece estranho!

Com base na descrição do PR, parece que essas coisas foram feitas para consistência com outros ‘outlets de flair de avatar’ semelhantes (que provavelmente foram introduzidos antes que os ‘outlets de plugin wrapper’ fossem uma coisa).

Acabamos de mesclar uma pull request que habilita o Glimmer Post Stream por padrão.

Após a próxima atualização, sites compatíveis usarão automaticamente o novo Post Stream. Sites que usam extensões incompatíveis voltarão à versão antiga e exibirão avisos no console do navegador.

Por enquanto, você pode forçar manualmente o uso do Post Stream antigo definindo glimmer_post_stream_mode como disabled.

Se encontrar algum problema, por favor, relate-o abaixo.

Acho que recebo um aviso devido a este código que depois leva a este tópico:

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

Como eu atualizaria isso para o novo sistema?

Algo como isto:

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

Legal, obrigado.

Acho que isso funciona. Coloquei esse código e depois tentei definir temporariamente todas as configurações de força para o modo brilho e pareceu que o tamanho do avatar e a lista de postagens funcionaram bem no meu site depois disso. Após o teste, desativei as substituições, pois parece que elas não são recomendadas para uso ainda. Mas meu site agora deve estar pronto para a mudança.

Olá @Boost. :smiley:

O que você quer dizer com “as substituições ainda não são recomendadas para uso”? Se o seu site estiver pronto, ele será automaticamente alterado para o Glimmer Post Stream.

Quando você atualizar sua instalação do Discourse na próxima vez, o Glimmer Post Stream agora estará habilitado por padrão, mesmo para sites que ainda possuem personalizações incompatíveis.

Na verdade, todos os sistemas de renderização de widgets foram desativados, portanto, qualquer personalização baseada em widgets não será mais renderizada.

Por enquanto, em sites incompatíveis, o administrador pode reativar o comportamento antigo modificando os valores das seguintes configurações:

  • glimmer_post_stream_mode
  • deactivate_widgets_rendering

Para reativar o Glimmer Post Stream, ambas as configurações precisam ser alteradas.

Esta é a fase final antes de remover o código antigo da base de código do Discourse, o que está previsto para acontecer em cerca de um mês. Posteriormente, não será mais possível reativar os widgets.

A opção nas configurações experimentais adverte claramente contra o uso dela:

Habilita a nova implementação de fluxo de postagens ‘glimmer’ no modo ‘auto’ para os grupos de usuários especificados. Esta implementação está em desenvolvimento ativo e não se destina ao uso em produção. Não desenvolva temas/plugins contra ela até que a implementação seja finalizada e anunciada.

Portanto, a redação é bastante forte ao dizer que “não se destina ao uso em produção. Não desenvolva temas/plugins contra ela”.

Essa é a explicação para a opção Glimmer post stream mode auto groups.

Boa observação. Nós perdemos isso.

O Glimmer Post Stream agora é o padrão, e a versão do widget é considerada legada e já está programada para ser removida. Atualizarei a descrição da configuração.

O PR que remove o widget pós-stream foi mesclado.