Changements à venir dans le flux de publication - Comment préparer les thèmes et les plugins

Nous abandonnons progressivement l’ancien système de rendu des « widgets » pour le remplacer par des composants Glimmer modernes.

Nous avons récemment modernisé le flux de publications en utilisant des composants Glimmer. Ce guide vous accompagnera dans la migration de vos plugins et thèmes de l’ancien système basé sur les widgets vers la nouvelle implémentation Glimmer.

Ne vous inquiétez pas : cette migration est plus simple qu’il n’y paraît à première vue. Nous avons conçu le nouveau système pour qu’il soit plus intuitif et puissant que l’ancien système de widgets, et ce guide vous aidera tout au long du processus.

Calendrier

Ces estimations sont susceptibles d’évoluer

T2 2025 :

  • :white_check_mark: Implémentation de base terminée
  • :white_check_mark: Début de la mise à niveau des plugins et des composants de thème officiels
  • :white_check_mark: Activé sur Meta
  • :white_check_mark: Publication des conseils de mise à niveau (ce guide)

T3 2025 :

  • :white_check_mark: Fin de la mise à niveau des plugins et des composants de thème officiels
  • :white_check_mark: Définition de glimmer_post_stream_mode par défaut sur auto et activation des messages de dépréciation dans la console
  • :white_check_mark: Activation des messages de dépréciation avec une bannière d’avertissement pour les administrateurs (prévue pour juillet 2025)
  • Les plugins et thèmes tiers doivent être mis à jour

T4 2025 :

  • :white_check_mark: Activation du nouveau flux de publications par défaut
  • :white_check_mark: Suppression du paramètre de drapeau de fonctionnalité et du code hérité

Qu’est-ce que cela signifie pour moi ?

Si l’un de vos plugins ou thèmes utilise des API « widget » pour personnaliser le flux de publications, vous devrez les mettre à jour pour qu’ils fonctionnent avec la nouvelle version.

Comment puis-je essayer le nouveau flux de publications ?

Pour essayer le nouveau flux de publications, modifiez simplement le paramètre glimmer_post_stream_mode en auto dans les paramètres de votre site. Cela activera le nouveau flux de publications si vous n’avez aucun plugin ou thème incompatible.

Lorsque glimmer_post_stream_mode est défini sur auto, Discourse détecte automatiquement les plugins et thèmes incompatibles. S’il en trouve, vous verrez des messages d’avertissement utiles dans la console du navigateur qui identifient exactement quels plugins ou thèmes doivent être mis à jour, ainsi que des traces de pile pour vous aider à localiser le code concerné.

Ces messages vous aideront à identifier exactement quelles parties de vos plugins ou thèmes doivent être mises à jour pour être compatibles avec le nouveau flux de publications Glimmer.

Si vous rencontrez des problèmes avec le nouveau flux de publications, ne vous inquiétez pas : pour l’instant, vous pouvez toujours définir le paramètre sur disabled pour revenir à l’ancien système. Si vous avez des extensions incompatibles installées mais que vous souhaitez tout de même essayer, en tant qu’administrateur, vous pouvez définir l’option sur enabled pour forcer le nouveau flux de publications. Utilisez cette option avec précaution : votre site pourrait ne pas fonctionner correctement selon les personnalisations que vous avez mises en place.

J’ai des plugins ou des thèmes personnalisés installés. Dois-je les mettre à jour ?

Vous devrez mettre à jour vos plugins ou thèmes s’ils effectuent l’une des personnalisations suivantes :

  • Utiliser decorateWidget, changeWidgetSetting, reopenWidget ou attachWidgetAction sur les widgets suivants :

    • 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
  • Utiliser l’une des méthodes API ci-dessous :

    • addPostTransformCallback
    • includePostAttributes

:light_bulb: Si vous avez des extensions utilisant l’une des personnalisations ci-dessus, vous verrez un avertissement dans la console indiquant quel plugin ou composant doit être mis à niveau lorsque vous visitez une page de sujet.

L’ID de dépréciation est : discourse.post-stream-widget-overrides

:warning: Si vous utilisez plus d’un thème dans votre instance, assurez-vous de vérifier tous les thèmes, car les avertissements n’apparaîtront que pour les plugins actifs et les thèmes et composants de thème actuellement utilisés.

Quelles sont les alternatives ?

Le nouveau flux de publications Glimmer vous offre plusieurs façons de personnaliser l’apparence des publications :

  1. Plugin Outlets (Sorties de plugin) : Pour ajouter du contenu à des points spécifiques du flux de publications.
  2. Transformers (Transformateurs) : Pour personnaliser des éléments, modifier des structures de données ou changer le comportement des composants.

Remplacer includePostAttributes par addTrackedPostProperties

Si votre plugin utilise includePostAttributes pour ajouter des propriétés au modèle de publication, vous devrez le mettre à jour pour utiliser addTrackedPostProperties à la place.

Avant :

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

Après :

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

La fonction addTrackedPostProperties marque les propriétés comme suivies pour les mises à jour des publications. C’est important si votre plugin ajoute des propriétés à une publication et les utilise lors du rendu. Cela garantit que l’interface utilisateur se met à jour automatiquement lorsque ces propriétés changent.

Modèles courants de migration

Utilisation des Plugin Outlets

Les Plugin Outlets sont votre outil principal pour ajouter du contenu à des points spécifiques du flux de publications. Considérez-les comme des emplacements désignés où vous pouvez injecter votre contenu personnalisé.

Ils sont la clé pour migrer loin des décorations de widgets.

Le flux de publications Glimmer enveloppe souvent le contenu personnalisé dans des sorties. Utilisez les fonctions de l’API de plugin renderBeforeWrapperOutlet et renderAfterWrapperOutlet pour insérer du contenu avant ou après ces sorties.

1. Remplacer les décorations de widgets par des Plugin Outlets

La personnalisation la plus courante consiste à ajouter du contenu aux publications. Dans le système de widgets, vous utilisiez decorateWidget. Avec Glimmer, vous utiliserez des Plugin Outlets à la place.

Avant :

// Partie d'un initialiseur dans un plugin

import { withPluginApi } from "discourse/lib/plugin-api";
// ... autres imports

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) => {
      // ... autres personnalisations
      customizeWidgetPost(api);
    });
  }
};

Après :

// Partie d'un initialiseur .gjs dans un plugin

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
import SolvedAcceptedAnswer from "../components/solved-accepted-answer";
// ... autres imports

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) => {
      // ... autres personnalisations
      customizePost(api);
    });
  }
};

2. Ajouter du contenu après le nom de l’auteur

Si votre plugin ajoute du contenu après le nom de l’auteur, vous utiliserez l’API renderAfterWrapperOutlet avec la sortie post-meta-data-poster-name.

Avant :

// Partie d'un initialiseur dans un plugin

import { withPluginApi } from "discourse/lib/plugin-api";
// ... autres imports

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) => {
      // ... autres personnalisations
      customizeWidgetPost(api);
    });
  }
};

Après :

// Partie d'un initialiseur .gjs dans un plugin

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... autres imports

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) => {
      // ... autres personnalisations
      customizePost(api);
    });
  }
};

3. Ajouter du contenu avant le contenu de la publication

// Partie d'un initialiseur .gjs dans un thème

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... autres imports

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

      <template>
        <div class="pinned-post-notice">
          Ceci est un sujet épinglé
        </div>
      </template>
    }
  );
}

export default {
  name: "pinned-topic-notice",
  initialize() {
    withPluginApi((api) => {
      // ... autres personnalisations
      customizePost(api);
    });
  }
};

4. Ajouter du contenu après le contenu de la publication

// Partie d'un initialiseur .gjs dans un thème

import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";
// ... autres imports

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

      // Dans un vrai composant, vous utiliseriez un template comme ceci :
      <template>
        <div class="wiki-post-notice">
          Cette publication est un wiki
        </div>
      </template>
    }
  );
}

export default {
  name: "wiki-post-notice",
  initialize() {
    withPluginApi((api) => {
      customizePost(api);
      // ... autres personnalisations
    });
  }
};

Utilisation des Transformers

Les Transformers sont un moyen puissant de personnaliser les composants Discourse. Ils vous permettent de modifier des données ou le comportement des composants sans avoir à remplacer des composants entiers.

Voici certains des transformateurs de valeurs les plus pertinents pour la personnalisation du flux de publications :

Nom du Transformateur Description Contexte
post-class Personnaliser les classes CSS appliquées à l’élément de publication principal. { post }
post-meta-data-infos Personnaliser la liste des composants de métadonnées affichés pour une publication. Cela permet d’ajouter, supprimer ou réorganiser des éléments comme la date de publication, l’indicateur de modification, etc. { post, metaDataInfoKeys }
post-meta-data-poster-name-suppress-similar-name Décider s’il faut supprimer l’affichage du nom complet de l’utilisateur lorsqu’il est similaire à son nom d’utilisateur. Retourner true pour supprimer. { post, name }
post-notice-component Personnaliser ou remplacer le composant utilisé pour afficher un avis de publication. { post, type }
post-show-topic-map Contrôler la visibilité du composant de carte de sujet sur la première publication. { post, isPM, isRegular, showWithoutReplies }
post-small-action-class Ajouter des classes CSS personnalisées à une petite action de publication. { post, actionCode }
post-small-action-custom-component Remplacer une petite action de publication standard par un composant Glimmer personnalisé. { post, actionCode }
post-small-action-icon Personnaliser l’icône utilisée pour une petite action de publication. { post, actionCode }
poster-name-class Ajouter des classes CSS personnalisées au conteneur du nom de l’auteur. { user }

1. Ajouter une classe personnalisée aux publications

// Partie d'un initialiseur dans un plugin
import { withPluginApi } from "discourse/lib/plugin-api";

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

      // Ajouter une classe personnalisée aux publications d'un utilisateur spécifique
      if (post.user_id === 1) {
        return [...value, "special-user-post"];
      }

      return value;
    }
  );
}

export default {
  name: "custom-post-classes",
  initialize() {
    withPluginApi((api) => {
      // ... autres personnalisations
      customizePostClasses(api);
    });
  }
};

2. Ajouter des métadonnées de publication personnalisées

Le transformateur post-meta-data-infos vous permet d’ajouter des composants personnalisés à la section des métadonnées de la publication.

// Partie d'un initialiseur dans un plugin
import { withPluginApi } from "discourse/lib/plugin-api";

function customizePostMetadata(api) {
  // Définir un composant à utiliser dans la section des métadonnées
  // Le composant doit être créé en dehors de la fonction de rappel du transformateur,
  // sinon cela peut causer des problèmes de mémoire.
  const CustomMetadataComponent = <template>...</template>;

  api.registerValueTransformer(
    "post-meta-data-infos",
    ({ value: metadata, context: { post, metaDataInfoKeys } }) => {
      // Ajouter le composant uniquement pour certaines publications
      if (post.some_custom_property) {
        metadata.add(
          "custom-metadata-key",
          CustomMetadataComponent,
          {
            // Le placer avant la date
            before: metaDataInfoKeys.DATE,
            // et après l'onglet de réponse
            after: metaDataInfoKeys.REPLY_TO_TAB,
          }
        );
      }
    }
  );
}

export default {
  name: "custom-post-metadata",
  initialize() {
    withPluginApi((api) => {
      // ... autres personnalisations
      customizePostMetadata(api);
    });
  }
};

Voici un exemple concret tiré du plugin discourse-activity-pub :

// Partie d'un initialiseur dans le 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);
      // ... autres personnalisations
    });
  }
};

5. Insérer du contenu avant ou après le contenu cuisiné de la publication

Si votre plugin ajoute du contenu après le texte de la publication, vous utiliserez l’API renderAfterWrapperOutlet avec la sortie post-content-cooked-html.

Avant :

// Partie d'un initialiseur dans un 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 = "Cette publication est un wiki";
      element.prepend(banner);
    }
  });
}

export default {
  name: "wiki-footer",
  initialize() {
    withPluginApi((api) => {
      // ... autres personnalisations
      customizeCooked(api);
    });
  }
};

Après :

// Partie d'un initialiseur dans un plugin (.gjs)
import Component from "@glimmer/component";
import { withPluginApi } from "discourse/lib/plugin-api";

// Définir un composant à utiliser dans la sortie du plugin
class WikiBanner extends Component {
  static shouldRender(args) {
    return args.post.wiki;
  }

  <template>
    <div class="wiki-footer">Cette publication est un wiki</div>
  </template>
}

function customizePost(api) {
  // Utiliser renderBeforeWrapperOutlet pour ajouter du contenu avant le contenu de la publication
  api.renderAfterWrapperOutlet(
    "post-content-cooked-html",
    WikiBanner
  );
}

export default {
  name: "wiki-footer",
  initialize() {
    withPluginApi((api) => {
      customizePost(api);
      // ... autres personnalisations
    });
  }
};

Prise en charge des deux systèmes (ancien et nouveau) pendant la transition

En tant qu’auteur de plugin ou de thème, vous pouvez souhaiter prendre en charge les deux systèmes pendant la transition pour vous assurer que vos extensions fonctionnent avec les anciens et nouveaux flux de publications.

Voici un modèle utilisé par de nombreux plugins officiels :

// 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) {
  // personnalisations du flux de publications 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>
    }
  );

  // ...

  // envelopper l'ancien code de widget en silence les avertissements de dépréciation
  withSilencedDeprecations("discourse.post-stream-widget-overrides", () =>
    customizeWidgetPost(api)
  );
}

// ancien code 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) {
      // Utiliser RenderGlimmer pour afficher un composant Glimmer dans le système 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);
        // ... autres personnalisations
      });
    }
  },
};

Exemples concrets

Voici des liens vers des demandes de tirage (pull requests) réelles de migration de nos plugins officiels. Elles montrent comment nous avons mis à jour chaque type de personnalisation :

Dépannage

Mon site semble cassé après avoir activé le nouveau flux de publications

  • Réglez glimmer_post_stream_mode sur disabled
  • Vérifiez la console pour des messages d’erreur spécifiques
  • Mettez à jour les plugins/thèmes problématiques avant de réessayer

Je ne vois aucun avertissement, mais mes personnalisations ne fonctionnent pas

  • Vérifiez que vos personnalisations ciblent les widgets listés ci-dessus
  • Assurez-vous de tester sur la page de sujet avec des sujets où vos personnalisations sont visibles

Besoin d’aide ?

Si vous avez trouvé un bug ou si votre personnalisation ne peut pas être réalisée avec les nouvelles API que nous avons introduites, veuillez nous en informer ci-dessous.

Le flux Glimmer Post est activé sur Meta.

Si vous rencontrez des problèmes, veuillez les publier ci-dessous.

Lors de la mise à niveau de mes composants, j’ai rencontré du code étrange :

Premièrement, je suis quelque peu perplexe quant à @user={{@post}}. Cela pourrait-il être une erreur typographique ?

Deuxièmement, pourquoi le PluginOutlet nommé post-avatar-flair et le UserAvatarFlair sont-ils des éléments distincts ? De plus, pourquoi post-avatar-flair n’est-il pas un wrapper comme les autres outlets voisins ?

Ça fonctionne, donc je ne pense pas que ce soit une faute de frappe. Peut-être qu’à cet endroit, l’objet post possède tous les attributs que le composant UserAvatarFlair s’attend à trouver sur l’argument @user ? Je suis d’accord que ça a l’air bizarre !

D’après la description de la PR, il semble que ces choses aient été faites pour la cohérence avec d’autres ‘avatar-flair’ similaires (qui ont probablement été introduits avant que les ‘wrapper plugin outlets’ n’existent).

Nous venons de fusionner une pull request qui active le flux de publication Glimmer par défaut.

Après la prochaine mise à jour, les sites compatibles utiliseront automatiquement le nouveau flux de publication. Les sites utilisant des extensions incompatibles reviendront à l’ancienne version et afficheront des avertissements dans la console du navigateur.

Pour l’instant, vous pouvez forcer manuellement l’utilisation de l’ancien flux de publication en définissant glimmer_post_stream_mode sur disabled.

Si vous rencontrez des problèmes, veuillez les signaler ci-dessous.

Je pense que j’obtiens un avertissement à cause de ce code qui mène ensuite à ce fil de discussion :

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

Comment mettrais-je cela à jour pour le nouveau système ?

Quelque chose comme ça :

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

Bien, merci.

Je pense que ça fonctionne. J’ai mis ce code, puis j’ai essayé de régler temporairement tous les paramètres de force pour le mode scintillement et il semblait que la taille de l’avatar et la liste des publications fonctionnaient bien sur mon site après cela. Après les tests, j’ai désactivé les remplacements car il semble qu’ils ne soient pas encore recommandés. Mais mon site devrait maintenant être prêt pour le passage.

Salut @Boost. :smiley:

Qu’entends-tu par le fait que les remplacements ne sont pas encore recommandés ? Si ton site est prêt, il basculera automatiquement vers le flux d’articles Glimmer.

Lors de votre prochaine mise à jour de votre installation Discourse, le Glimmer Post Stream sera désormais activé par défaut, même pour les sites qui ont encore des personnalisations incompatibles.

En fait, tous les systèmes de rendu de widgets sont maintenant désactivés, donc toute personnalisation basée sur des widgets ne sera plus rendue.

Pour l’instant, sur les sites incompatibles, l’administrateur peut réactiver l’ancien comportement en modifiant les valeurs des paramètres suivants :

  • glimmer_post_stream_mode
  • deactivate_widgets_rendering

Pour réactiver le flux de messages de widgets, les deux paramètres doivent être modifiés.

C’est la phase finale avant la suppression de l’ancien code de la base de code de Discourse, ce qui est prévu dans environ un mois. Par la suite, il ne sera plus possible de réactiver les widgets.

L’option dans les paramètres expérimentaux met clairement en garde contre son utilisation :

Activez la nouvelle implémentation du flux de publication ‘glimmer’ en mode ‘auto’ pour les groupes d’utilisateurs spécifiés. Cette implémentation est en cours de développement actif et n’est pas destinée à une utilisation en production. Ne développez pas de thèmes/plugins contre elle tant que l’implémentation ne sera pas finalisée et annoncée.

Ainsi, la formulation est assez forte en disant qu’elle “n’est pas destinée à une utilisation en production. Ne développez pas de thèmes/plugins contre elle”.

C’est l’explication de l’option Glimmer post stream mode auto groups.

Bien vu. Nous avions manqué cela.

Le flux Glimmer Post est maintenant le flux par défaut, et la version widget est considérée comme obsolète et déjà programmée pour être supprimée. Je vais mettre à jour la description du paramètre.

Le PR supprimant le widget après le flux a été fusionné.