¿Añadir video de fondo a ciertos perfiles de usuario?

Estoy intentando añadir un vídeo a páginas de perfil de usuario específicas, de modo que todos nuestros mecenas tengan un determinado vídeo de fondo en su perfil. (Antes de que os pongáis todos nerviosos, sería solo una animación en bucle, no un vídeo completo; debería quedar bastante bien, similar a los fondos de perfil de Steam.)

El siguiente código HTML y CSS funciona para todos los usuarios, pero obviamente eso no es lo que buscamos:

// Esto va en la pestaña "Header"

<video playsinline autoplay muted loop id="myVideo" poster="[INSERTAR ENLACE]">
	<source src="[INSERTAR ENLACE]" type="video/webm">
	<source src="[INSERTAR ENLACE]" type="video/mp4">
</video>
#myVideo {
  position: fixed;
  top: 63px;
  min-height: 1080px;
  margin-left: 50vw;
  transform: translate(-50%);
}

.user-content
{
    background: none;
}

.user-main .about.has-background .details {
    padding-bottom: 15px;
}
.user-main .about
{
    margin-bottom: 0px;
}
.user-content-wrapper
{
    background: rgba(var(--secondary-rgb), 0.8);
}

A diferencia de usar body.category-general para añadir una imagen solo a las páginas de la categoría “general”, no parecen existir “slugs” específicos asignados a las páginas de perfil de usuarios de un grupo específico o de un nombre de usuario específico. Somos bastante nuevos en esto y tenemos más experiencia con CSS que con el trabajo directo con HTML, por lo que no estamos seguros de si existe una forma fácil y conveniente de hacer que esto funcione como nos gustaría.

Imaginamos que el mejor enfoque sería añadir un “slug” similar para los perfiles de usuario basado en su grupo, pero no estamos seguros de cómo hacerlo y de cómo hacer que el vídeo solo se muestre en las páginas con el contenido correcto, y tampoco estamos comprometidos a usar específicamente este enfoque si existe otro método más fácil.

Por ejemplo, también estaríamos abiertos a la idea de hacerlo por usuario en lugar de por grupo, si eso es de alguna manera más fácil.

Solo preferiríamos no tener que codificar el vídeo en cada página, para que solo se cargue cuando estés en el usuario o usuarios específicos en cuestión.

Edición: Probablemente debería mencionar que estamos en la rama estable, en caso de que eso cambie algo.

Nuestro enfoque actual es ver si podemos simplemente detectar que estamos en la página de cierto usuario a través del enlace canónico y, si es así, aplicar el video. Como tal, tenemos lo siguiente:

<script type="text/discourse-plugin" version="0.8">
    api.onPageChange(() => {
        determineUser();
    });
    
    function determineUser() {
        var pageURL = document.querySelector("link[rel='canonical']").getAttribute("href");
        var isUserPage = pageURL.includes("https://www.fortressoflies.com/u/");
        document.documentElement.style.setProperty('--currUsername', pageURL);
        if(isUserPage)
        {
            document.documentElement.style.setProperty('--lastUsername', pageURL);
            $('body').css('background-color', '#'+(Math.random()*0xFFFFFF<<0).toString(16));
        }
    }
</script>

Sin embargo, esto solo parece funcionar en una actualización completa; por alguna razón, hacer clic de una página a otra no actualiza la propiedad --currUsername, y en lugar de aplicar un fondo de color aleatorio a las páginas de usuario, aplica un fondo de color aleatorio a todas las páginas si se presionó F5 por última vez en una página de usuario, mientras que no aplica nada a ninguna página si se presionó F5 por última vez en una página que no es de usuario.

Francamente, no tengo suficiente experiencia con JavaScript para saber por qué sería así; me parece que, al cambiar de página, la función debería ejecutarse (lo cual hace), haciendo que la variable pageURL se actualice, y esto debería hacer que la propiedad --currUsername se actualice al cargar una página. Sin embargo, esto solo ocurre en una actualización completa, de lo contrario, las variables no parecen cambiar.

¿Alguna idea de lo que estoy haciendo mal aquí?

Parece que esto se debe a que la URL canónica no se actualiza, mientras que la propiedad “og:url” sí lo hace.

El único problema es que usar var pageURL = document.querySelector("meta[property='og:url']").getAttribute("content"); se actualiza antes de que la etiqueta meta se actualice, es decir, este código me da la URL de la página anterior, no la actual.

Si quieres agregar contenido a una página específica, tu mejor opción es un plugin-outlet. En pocas palabras, los plugin-outlets son espacios reservados en las plantillas de Discourse que puedes usar para añadir nuevo contenido.

Lo primero que necesitas hacer es averiguar si existe un plugin-outlet en la página que quieres modificar. Hay un componente de tema que puedes instalar para ayudarte con eso.

(deprecated) Plugin outlet locations theme component

Una vez que instales ese componente, actívalo, ve a la página que quieres modificar y revisa qué tienes disponible. En tu caso, existe un plugin-outlet (resaltado en verde)

Así que, el que nos interesa es above-user-profile

Supongamos que no existiera… ¿qué hacer? En ese caso, la mejor opción es solicitar que se añada o enviar una PR para incluirlo en el núcleo. La mayoría de las veces será aceptado si tu caso de uso tiene sentido.

De todos modos, como ya mencioné, en este caso ya existe. Así que veamos cómo puedes agregar marcado a él. No necesitarás el componente anterior para el resto de esto, así que puedes desactivarlo ahora que ya tienes el nombre del plugin outlet.

Todo lo que necesitas hacer es agregar algo como esto en la pestaña de encabezado de tu tema.

<script type="text/x-handlebars" data-template-name="/connectors/OUTLET_NAME/SOME_NAME">
  Tu marcado va aquí...
</script>

Debes cambiar OUTLET_NAME por el nombre del outlet que quieres modificar. Luego cambia SOME_NAME por el nombre que quieras darle a esta personalización. El nombre puede ser cualquier cosa, pero intenta ser descriptivo si es posible. Es una buena práctica. Así que terminamos con esto.

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  Tu marcado va aquí... como
  <h1>¡Hola Mundo!!</h1>
</script>

Probémoslo y veamos qué sucede… recuerda, el fragmento anterior va en la pestaña common > header de tu tema.

y…

Hasta aquí todo bien, pero profundicemos.

No quieres que tus videos se muestren en todos los perfiles, sino solo bajo ciertas condiciones. Entonces, ¿cómo lo haces? Necesitarás dos cosas: algunos datos para consumir y un poco de JavaScript.

Encontrémos los datos. ¿Recuerdas cuando dije que los plugin-outlets son espacios reservados? ¿Cuál sería el punto de tenerlos sin contexto? Por eso Discourse pasa los fragmentos relevantes de contexto a cada plugin outlet… pero primero, retrocedamos un paso. Cuando agregas esto

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  Tu marcado va aquí, como...
  <h1>¡Hola Mundo!!</h1>
</script>

Parece HTML —y las etiquetas script lo son—, pero lo que hay dentro se trata como código Handlebars.

Eso significa que puedes hacer algo como esto en su lugar:

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  {{log this}}
</script>

y revisa la consola del navegador. Verás esto cada vez que se renderice el outlet, es decir, cuando estés en una página de usuario.

Ahora, ¿algo de esto es útil? Sí… pero no por el momento. Volvemos a esto más adelante. Retrocedamos otro paso y veamos cómo Discourse pasa el contexto al outlet. Si buscas el nombre del outlet en GitHub —o localmente— obtendrás esto:

Repository search results · GitHub

Abramos ese archivo. Lo primero que verás es esta línea:

Observa la última parte de esa línea:

args=(hash model=model)

Verás que Discourse pasa model como argumento al outlet. Para todos los efectos prácticos y para mantener las cosas simples, model = data.

Así que uno de los argumentos de nuestro outlet es model, y ahí estará la información que buscamos. Volvamos a nuestro fragmento.

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  {{log this}}
</script>

y cambiémoslo por esto:

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
-  {{log this}}
+  {{log args}}
</script>

Ahora obtenemos esto en la consola:

Puedes explorar esos datos y ver si tienen lo que necesitas. Deberían, ya que contienen toda la información sobre el usuario utilizada en otros elementos de esa página. Es el “modelo” para la página de usuario de ese usuario en particular.

Una de las propiedades disponibles allí es… redoble de tambores :drum: … los grupos a los que pertenece el usuario.

Así que, si haces:

{{log args.model.groups}}

obtendrás todos los grupos a los que pertenece el usuario en la consola.

Bien, ahora tenemos los datos que necesitamos, así que lo único que queda es agregar alguna(s) condición(es) basada(s) en ellos.

Podrías pensar que podemos hacer eso en el mismo fragmento, pero, desafortunadamente, no podemos. Handlebars es un lenguaje de plantillas. Tiene soporte muy, muy básico para lógica: nada más allá de condiciones simples verdadero/falso y bucles. No puedes hacer comparaciones ni otras cosas como esa.

Entonces, ¿dónde exactamente puedes hacer eso? En una clase de conector, suena sofisticado… lo sé.

En pocas palabras, una clase de conector es esencialmente un poco de JavaScript adjunto al outlet. Es mucho más matizado que eso, pero eso es todo lo que realmente necesitas saber por ahora.

Así que creémos una. Lo hacemos así:

<script type="text/discourse-plugin" version="0.8">
api.registerConnectorClass('OUTLET_NAME', 'SOME_NAME', {

});
</script>

OUTLET_NAME y SOME_NAME aquí deben ser los mismos que usamos arriba. Así que cambiémoslos:

<script type="text/discourse-plugin" version="0.8">
api.registerConnectorClass('above-user-profile', 'add-profile-videos', {

});
</script>

Este fragmento también va en la pestaña common > header de tu tema. Así que ahora deberías tener algo que se ve así:

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  {{log args.model.groups}}
</script>

<script type="text/discourse-plugin" version="0.8">
  api.registerConnectorClass('above-user-profile', 'add-profile-videos', {

  });
</script>

Dentro de nuestra clase de conector, podemos hacer algo… pero… debemos tener en cuenta que no es como cualquier archivo de JavaScript. Por falta de una mejor descripción… piénsalo como un componente Ember a dieta. Expandir esto está un poco fuera del alcance aquí, así que sigamos adelante.

Hay cuatro métodos conectados por defecto:

actions te permite definir acciones así:

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  actions: {
    myAction() {
      // hacer algo
    }
  }
});

Luego puedes llamar a esa acción desde dentro del outlet, como cuando se presiona un botón. No la necesitaremos aquí, así que sigamos.

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  shouldRender(args, component) {
    // devolver true o false aquí
  }
});

Tampoco usaremos esta, ya que el outlet solo se renderiza en páginas de perfil y por ahora no tenemos otros requisitos. Sin embargo, puedes usarla para agregar cualquier condición que quieras probar antes de que se renderice el outlet. Por ejemplo, el nivel de confianza del usuario actual o cosas así. Sigamos…

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  setupComponent(args, component) {
    // hacer algo
  }
});

Esta es la que queremos enfocarnos. Cualquier condición o variable de JavaScript que quieras establecer va aquí. Antes de profundizar en esta, cubramos el último método primero por completitud:

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  teardownComponent(args, component) {
    // hacer algo
  }
});

Esto se ejecuta cuando el outlet va a ser eliminado. Así que te permite realizar cualquier limpieza necesaria, como eliminar escuchadores de eventos, etc.

Bien, volvamos a setupComponent:

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  setupComponent(args, component) {
    // hacer algo
  }
});

Puedes ver que se le pasan dos cosas. Primero está args y luego component.

args aquí es lo mismo que vimos antes. Son los datos de contexto que Discourse pasó al outlet. Así que, si haces:

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  setupComponent(args, component) {
    console.log(args.model.groups);
  }
});

verás la misma información en la consola del navegador que vimos antes. Los grupos a los que pertenece el propietario del perfil. Aquí es donde comienza la diversión; ahora tienes los datos y el gancho correcto. Así que puedes hacer lo que quieras aquí. Entonces, si quiero que el video solo se muestre en los perfiles de miembros que pertenecen a un grupo determinado, puedo hacer esto:

  api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
    setupComponent(args, component) {
      const inGroup = [...args.model.groups].filter(g => g.name === TARGET_GROUP)
      const showVideo = inGroup.length ? true : false;

      console.log(showVideo);
    }
  });

Si pruebas esto en una página de perfil que pertenece a un usuario del grupo staff, imprimirá true en la consola. Así que ahora lo único que nos queda es pasar eso a la plantilla del outlet. Así es como puedes hacerlo.

component pasado a setupComponent aquí se comparte entre el conector y el outlet. Puedes pasar cosas al outlet estableciéndolas como propiedades en el componente así:

  const TARGET_GROUP = "staff"

  api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
    setupComponent(args, component) {
      const inGroup = [...args.model.groups].filter(g => g.name === TARGET_GROUP)
      const showVideo = inGroup.length ? true : false;
-     console.log(showVideo);
+     component.setProperties({showVideo})
    }
  });

Ahora, si volvemos a la plantilla y hacemos algo como:

{{log showVideo}}

imprimirá el mismo resultado. Así que ahora lo ponemos en una condición de Handlebars y agregamos tu marcado dentro así:

<script
  type="text/x-handlebars"
  data-template-name="/connectors/above-user-profile/add-profile-videos"
>
  {{#if showVideo}}
    <video playsinline autoplay muted loop id="myVideo" poster="[INSERT LINK]">
      <source src="[INSERT LINK]" type="video/webm">
      <source src="[INSERT LINK]" type="video/mp4">
    </video>
  {{/if}}
</script>

luego revisa una página de perfil de un usuario de staff. Verás que carga el video.

Una vez que navegas fuera del perfil del miembro del staff, el video desaparece. El video no se mostrará en los perfiles de usuarios que no estén en el grupo staff.

Así que, pongamos todo esto junto. Esto es lo mismo que vimos arriba.

Aquí está el CSS que usé. Pestaña common > css:

#myVideo {
  position: fixed;
  top: var(--header-offset);
  min-height: 100vh;
  left: 0;
  z-index: -1;
}

.user-content {
  background: none;
}

.user-main {
  padding: 0.5em;
  background: rgba(var(--secondary-rgb), 0.8);
}

// si también lo quieres en móvil
.mobile-view {
  body[class*="user-"] {
    background: none;
    .user-main,
    .user-content {
      padding: 0.5em;
      background: rgba(var(--secondary-rgb), 0.8);
    }
  }
}

HTML / JavaScript / Handlebars. Esto va en la pestaña common > header de tu tema:

<script
  type="text/x-handlebars"
  data-template-name="/connectors/above-user-profile/add-profile-videos"
>
  {{#if showVideo}}
    <video playsinline autoplay muted loop id="myVideo" poster="[INSERT LINK]">
      <source src="[INSERT LINK]" type="video/webm">
      <source src="[INSERT LINK]" type="video/mp4">
    </video>
  {{/if}}
</script>

<script type="text/discourse-plugin" version="0.8">
  const TARGET_GROUP = "staff"

  api.registerConnectorClass('above-user-profile', 'add-profile-videos', {
    setupComponent(args, component) {
      const inGroup = [...args.model.groups].filter(g => g.name === TARGET_GROUP)
      const showVideo = inGroup.length ? true : false;
      component.setProperties({showVideo})
    }
  });
</script>

Cambia TARGET_GROUP por el nombre del grupo que quieras modificar y agrega los atributos src para tus videos.

Esta publicación fue un poco larga… no te desanimes por eso. Una vez que comprendas el concepto, todo lo que hicimos arriba se puede hacer en menos de 3-5 minutos.

Lo bueno aquí es que todo lo que hablamos es prácticamente lo mismo para cualquier plugin outlet. Lo único que cambia es el nombre. Así que esto se aplica a cualquier modificación de plugin outlet que quieras hacer en el futuro.

  1. encuentra el nombre del outlet
  2. obtén los datos
  3. procesa los datos en un conector
  4. pasa las propiedades de vuelta a la plantilla

Eso es increíblemente profundo y me aseguraré de revisarlo cuando tenga tiempo la próxima semana, pero basta decir que, de un vistazo, parece mucho mejor que mi implementación actual (incrustar el video en cada página y solo mostrarlo en el perfil del usuario, lo que logré con un script que agrega una etiqueta al cuerpo de la página de un usuario si el nombre de su cuenta es algo específico). ¡Gracias por la explicación detallada, ¡no puedo esperar a ponerme a ello!

De acuerdo, en su mayor parte, este enfoque funciona muy bien, y la explicación es fantástica. Muchas gracias por ir más allá.

Trabajar a nivel de usuario es sencillo: simplemente comprobamos un nombre de usuario determinado, así:

      const isUser1 = args.model.username == "User1"
      component.setProperties({isUser1})

Sin embargo, nos encontramos con un problema con nuestro CSS: deseamos realizar algunos cambios en la apariencia de la página de usuario solo para estos usuarios.

En este momento, lo estamos logrando a través del mismo código que antes:

<script type="text/discourse-plugin" version="0.8">
    api.onPageChange(() =>{
        window.onload = determineUser();
    });
    
    async function determineUser() {
        await sleep(50);
        var pageURL = document.querySelector("meta[property='og:url']").getAttribute("content");
        var isUserPage = pageURL.includes("https://www.siteurl.com/u/");
        var isUser1 = pageURL.includes("u/User1/");
        document.body.className = document.body.className.replace(" user-page-animated","");

        
        if(isUserPage)
        {
            if(isUser1)
            {
                document.body.className += ' user-page-animated';
            }
        }
    }
    
    function sleep(ms)
    {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
</script>

Esto nos permite simplemente copiar y pegar el código de “User1” para cada nuevo usuario, pero depende de un retraso de 50 ms después de cada carga de página antes de activarse, lo que es visible para el usuario final (y, si se elimina, no funciona por alguna razón).

¿Hay alguna manera de vincular también esta adición de una clase al cuerpo al código que proporcionaste, para que podamos usarla para estilizar páginas con videos de manera diferente a las que no los tienen?

Y, en serio, gracias de nuevo por la detallada explicación.