Icônes utilisateur de l'en-tête du thème central

Salut, j’essaie de recréer la séparation du menu d’en-tête avec l’utilisateur et les notifications, similaire au thème central sur meta, j’ai demandé sur le fil du thème mais je n’ai pas eu de réponse.

Si je voulais faire la même chose, quels fichiers devrais-je modifier, et est-il préférable de placer le fichier dans mon thème ou de le recréer avec un composant de thème ?

1 « J'aime »

Si vous inspectez une page et consultez l’onglet Sources, vous pouvez voir comment elle a été créée.

Essentiellement :

  • Le nouveau menu utilisateur est créé avec un widget, ajouté avec api.addToHeaderIcons
  • Le menu des notifications :
    • L’avatar est remplacé par une icône de cloche en remplaçant son contenu à partir du widget header-notifications.
    • L’icône d’accès rapide du menu utilisateur est masquée avec du CSS.
5 « J'aime »

Merci pour votre aide habituelle, je vais créer un environnement de staging pour y travailler. Merci encore.

1 « J'aime »

Bonjour @digitaldominica ! Désolé de ne pas avoir consulté le fil de discussion depuis un moment, car je suis sur le point de publier la prochaine mise à jour. Tout le monde a des commentaires très précieux, mais si je ne m’en détourne pas, j’aurai tendance à les ajouter à ma liste actuelle plutôt que de les sauvegarder pour la mise à jour suivante. :sweat_smile:

@Arkshine a raison. C’est une méthode assez bricolée qui fonctionne dans les contraintes d’un thème (une grande partie de Central n’est que du prototypage car je suis d’abord designer, et développeur ensuite).

Je vais fournir un guide étape par étape sur la façon dont j’ai réalisé le prototype brut dans Central. Mais attention, cette solution pourrait ne pas être pérenne si nous apportons une mise à jour fondamentale au menu utilisateur, et il pourrait y avoir une manière plus optimale de créer le composant.


Étape 1 : Créer un composant de menu utilisateur personnalisé

Créez ces fichiers :

:page_facing_up: /javascripts/discourse/components/header-user-new.js
:page_facing_up: /javascripts/discourse/components/header-user-new.hbs

import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";

export default class HeaderUserNew extends Component {
  @service currentUser;
  @tracked isActive = false;

  constructor() {
    super(...arguments);
    this.handleDocumentClick = this.handleDocumentClick.bind(this);
  }

  @action
  toggleDropdown() {
    this.isActive = !this.isActive;

    if (this.isActive) {
      setTimeout(() => {
        document.addEventListener("click", this.handleDocumentClick);
      }, 0);
    } else {
      document.removeEventListener("click", this.handleDocumentClick);
    }
  }

  handleDocumentClick(event) {
    const dropdown = document.querySelector(".header-user-new__menu");
    const isClickInside = dropdown.contains(event.target);

    if (!isClickInside) {
      this.isActive = false;
      document.removeEventListener("click", this.handleDocumentClick);
    }
  }

  willDestroy() {
    super.willDestroy();
    document.removeEventListener("click", this.handleDocumentClick);
  }
}
<div class={{concatClass "header-user-new" (if isActive "active")}}>
  <button class="header-user-new__button" type="button" {{on "click" this.toggleDropdown}}>
    {{avatar currentUser}}
  </button>

  <div class="header-user-new__menu">
    <LinkTo class="header-user-new__profile" @route="user.summary" @model={{this.currentUser}}>
      {{avatar currentUser}}
      <div class="header-user-new__profile-info">
        <span class="header-user-new__profile-name">
          {{#if currentUser.name}}
            {{currentUser.name}}
          {{else}}
            {{currentUser.username}}
          {{/if}}
        </span>
        <span class="header-user-new__profile-view">
          View Profile
        </span>
      </div>
    </LinkTo>

    <ul>
      <li>
        <LinkTo @route="userActivity.bookmarks" @model={{this.currentUser}}>
          {{d-icon "bookmark"}}
          <span>
            {{i18n "js.user.bookmarks"}}
          </span>
        </LinkTo>
      </li>
      <li>
        <LinkTo @route="preferences" @model={{this.currentUser}}>
          {{d-icon "cog"}}
          <span>
            {{i18n "user.preferences"}}
          </span>
        </LinkTo>
      </li>
      <li>
        <DButton @action={{route-action "logout"}}>
          {{d-icon "sign-out-alt"}}
          <span>
            {{i18n "user.log_out"}}
          </span>
        </DButton>
      </li>
    </ul>
  </div>
</div>

Pour l’instant, je vous laisse le style CSS, mais il est important d’inclure ceci pour l’effet de bascule :

.header-user-new {
  &.active {
    .header-user-new__menu {
      display: flex;
    }
  }
  .header-user-new__menu {
    display: none;
  }
}

Étape 2 : Enregistrer le composant comme widget

Créez ce fichier :

:page_facing_up: /javascripts/discourse/widgets/header-user-new.js

import { hbs } from "ember-cli-htmlbars";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import { createWidget } from "discourse/widgets/widget";

export default createWidget("header-user-new", {
  tagName: "li.header-dropdown-toggle.header-user-new",

  html() {
    return [new RenderGlimmer(this, "div", hbs`<HeaderUserNew />`)];
  },
});

Étape 3 : Ajouter le widget à l’en-tête, remplacer l’icône utilisateur existante par une icône de cloche de notification

Créez ce fichier :

:page_facing_up: /javascripts/discourse/initializers/header-edit.js

import { h } from "virtual-dom";
import { withPluginApi } from "discourse/lib/plugin-api";
import { iconNode } from "discourse-common/lib/icon-library";
import I18n from "discourse-i18n";

export default {
  initialize() {
    withPluginApi("0.8", (api) => {
      api.reopenWidget("header-notifications", {
        html(attrs) {
          const { user } = attrs;

          let avatarAttrs = {
            template: user.get("avatar_template"),
            username: user.get("username"),
          };

          if (this.siteSettings.enable_names) {
            avatarAttrs.name = user.get("name");
          }

          const contents = [h("div", iconNode("bell"))];

          if (this.currentUser.status) {
            contents.push(this.attach("user-status-bubble", this.currentUser.status));
          }

          if (user.isInDoNotDisturb()) {
            contents.push(h("div.do-not-disturb-background", iconNode("moon")));
          } else {
            if (user.new_personal_messages_notifications_count) {
              contents.push(
                this.attach("link", {
                  action: attrs.action,
                  className: "badge-notification with-icon new-pms",
                  icon: "envelope",
                  omitSpan: true,
                  title: "notifications.tooltip.new_message_notification",
                  titleOptions: {
                    count: user.new_personal_messages_notifications_count,
                  },
                  attributes: {
                    "aria-label": I18n.t(
                      "notifications.tooltip.new_message_notification",
                      {
                        count: user.new_personal_messages_notifications_count,
                      }
                    ),
                  },
                })
              );
            } else if (user.unseen_reviewable_count) {
              contents.push(
                this.attach("link", {
                  action: attrs.action,
                  className: "badge-notification with-icon new-reviewables",
                  icon: "flag",
                  omitSpan: true,
                  title: "notifications.tooltip.new_reviewable",
                  titleOptions: { count: user.unseen_reviewable_count },
                  attributes: {
                    "aria-label": I18n.t(
                      "notifications.tooltip.new_reviewable",
                      {
                        count: user.unseen_reviewable_count,
                      }
                    ),
                  },
                })
              );
            } else if (user.all_unread_notifications_count) {
              contents.push(
                this.attach("link", {
                  action: attrs.action,
                  className: "badge-notification unread-notifications",
                  rawLabel: user.all_unread_notifications_count,
                  omitSpan: true,
                  title: "notifications.tooltip.regular",
                  titleOptions: { count: user.all_unread_notifications_count },
                  attributes: {
                    "aria-label": I18n.t("user.notifications"),
                  },
                })
              );
            }
          }
          return contents;
        },
      });

      const currentUser = api.container.lookup("service:current-user");
      if (currentUser !== null) {
        api.addToHeaderIcons("header-user-new");
      }
    });
  },
};

Le changement principal a été de remplacer avatarImg(…) par iconNode pour utiliser l’icône de notification plutôt que l’avatar de l’utilisateur en modifiant (ou en “rouvrant”) le widget header-notifications existant.

Le code original de header-notifications pour référence : discourse/app/assets/javascripts/discourse/app/widgets/header.js at 9bc78625af1d54693bc4f1bad3eaa9161ae030b6 · discourse/discourse · GitHub

Et comme @Arkshine l’a mentionné, j’ai caché la section utilisateur dans le menu des notifications avec CSS.

.d-header .panel {
  .user-menu.revamped .bottom-tabs, #user-menu-button-profile {
    display: none;
  }
}
7 « J'aime »

Salut, j’ai réussi à remplacer l’avatarImg de l’utilisateur par l’icône de cloche, mais la commande suivante :

api.addToHeaderIcons("new-user-icon");

Supprime l’intégralité de l’en-tête au lieu d’afficher l’avatar de l’utilisateur avec un menu déroulant.

2 « J'aime »

C’est mon erreur, il y avait une faute de frappe dans ce code, il devrait être—

api.addToHeaderIcons(“header-user-new”);

Car le widget est enregistré sous le nom de header-user-new à l’étape 2.

3 « J'aime »

Merci, cela a fonctionné.

Si je peux poser une autre question sur quelque chose de séparé ;

Comment le modèle de liste de sujets centraux alterne-t-il entre
posté par l’utilisateur et répondu par l’utilisateur
?

J’ai essayé {{#if topic.replies}} et {{#if topic.posters.[0]}}
mais cela ne fonctionne pas.

Pas de problème ! topic.posters.1.user est ce que vous cherchez (la première réponse). S’il existe, utilisez-le, sinon utilisez l’OP par défaut.

Voici comment il est actuellement structuré dans Central —

{{#if topic.posters.1.user}}
  <span class="tli__last-reply">
    {{d-icon "reply"}}
    <a href="{{topic.lastPoster.user.userPath}}" data-user-card="{{topic.lastPoster.user.username}}">
      {{~topic.lastPoster.user.username~}}
    </a>
    {{theme-i18n "topic_list_item.replied"}}
    <a href={{topic.lastPostUrl}}>
      {{format-date topic.bumpedAt format="medium" leaveAgo="true"}}
    </a>
  </span>
{{else}}
  <span class="tli__last-reply">
    {{d-icon "m-post_add"}}
    <a href="{{topic.posters.0.user.userPath}}" data-user-card="{{topic.posters.0.user.username}}">
      {{~topic.posters.0.user.username~}}
    </a>
    posted
    <a href={{topic.lastPostUrl}}>
      {{format-date topic.bumpedAt format="medium" leaveAgo="true"}}
    </a>
  </span>
{{/if}}
1 « J'aime »

Alors topic.posters.1.user sert d’auteur du message ?

Oh non, topic.posters.0.user est l’auteur, topic.posters.1.user est le premier utilisateur à répondre (s’il existe). :laughing:

3 « J'aime »

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.