Adicionar vídeo de fundo a certos perfis de usuário?

Estou tentando adicionar um vídeo às páginas de perfil de usuários específicos, de modo que todos os nossos patronos tenham um certo vídeo de fundo em seu perfil. (Antes que você se assuste, seria apenas uma animação em loop, não um vídeo completo - deve ficar muito bom, semelhante aos fundos de perfil do Steam.)

O seguinte código HTML e CSS funciona para todos os usuários - mas obviamente não é o que estamos buscando:

// Isso vai na aba "Header"

<video playsinline autoplay muted loop id="myVideo" poster="[INSERIR LINK]">
	<source src="[INSERIR LINK]" type="video/webm">
	<source src="[INSERIR LINK]" 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);
}

Ao contrário de usar body.category-general para adicionar uma imagem apenas às páginas da categoria “geral”, não parecem haver slugs específicos atribuídos às páginas de perfil de usuários de um grupo específico ou de um nome de usuário específico. Somos bem novos nisso e temos mais experiência com CSS do que com trabalho direto em HTML, e por isso não temos certeza se existe uma maneira fácil/conveniente de fazer isso funcionar como gostaríamos.

Imaginamos que a melhor abordagem seria adicionar um slug semelhante para perfis de usuários com base em seu grupo, mas não temos certeza de como fazer isso e como fazer o vídeo aparecer apenas nas páginas com o conteúdo correto, e também não estamos comprometidos em usar especificamente essa abordagem se existir outro método mais fácil.

Por exemplo, também estaríamos abertos à ideia de fazer isso por usuário, em vez de por grupo, se isso for de alguma forma mais fácil.

Apenas preferiríamos não ter que codificar o vídeo em cada página, para que ele só carregue quando você estiver no(s) usuário(s) específico(s) em questão.

Editar: Eu deveria notar que estamos na branch estável, caso isso mude alguma coisa.

Nossa abordagem atual é verificar se podemos simplesmente detectar que estamos na página de um determinado usuário através do link canônico e, se estivermos, aplicar o vídeo. Como tal, temos o seguinte:

<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>

No entanto, isso parece funcionar apenas em uma atualização completa - por algum motivo, clicar de uma página para outra não atualiza a propriedade --currUsername e, em vez de aplicar um fundo de cor aleatória às páginas de usuário, ele aplica um fundo de cor aleatória a todas as páginas se F5 foi pressionado pela última vez em uma página de usuário, enquanto não aplica nada a nenhuma página se F5 foi pressionado pela última vez em uma página que não seja de usuário.

Francamente, não tenho experiência suficiente com JavaScript para saber por que isso aconteceria - parece-me que, em uma mudança de página, a função deveria ser acionada (o que acontece), fazendo com que a variável pageURL seja atualizada, e isso deveria fazer com que a propriedade --currUsername fosse atualizada ao carregar uma página. No entanto, isso só ocorre em uma atualização completa, caso contrário, as variáveis não parecem mudar.

Alguma ideia do que estou errando aqui?

Parece que isso ocorre porque a URL canônica não é atualizada, enquanto a propriedade “og:url” é.

O único problema é que usar var pageURL = document.querySelector("meta[property='og:url']").getAttribute("content"); atualiza antes que a própria meta tag seja atualizada - ou seja, este código me dá a URL da página anterior, não a atual.

Se você deseja adicionar conteúdo a uma página específica, sua melhor opção é um plugin-outlet. Em resumo, plugin-outlets são espaços reservados nos modelos (templates) do Discourse que você pode usar para inserir novo conteúdo.

O primeiro passo é verificar se existe um plugin-outlet na página que você deseja atingir. Há um componente de tema que você pode instalar para ajudar nisso.

(deprecated) Plugin outlet locations theme component

Depois de instalar esse componente, ative-o, vá até a página desejada e verifique o que está disponível para trabalhar. No seu caso, um plugin-outlet existe (destacado em verde)

Portanto, o que queremos é above-user-profile.

Suponha que ele não existisse… o que fazer? Nesse caso, a melhor opção é pedir que ele seja adicionado ou enviar um PR (Pull Request) para incluí-lo no núcleo (core). Na maioria das vezes, ele será aceito se seu caso de uso fizer sentido.

De qualquer forma, como já disse, ele já existe neste caso. Então, vamos ver como você pode adicionar markup a ele. Você não precisará mais do componente mencionado acima para o restante deste guia, então pode desativá-lo agora, já que você já tem o nome do plugin-outlet.

Tudo o que você precisa fazer é adicionar algo assim na aba “header” do seu tema.

<script type="text/x-handlebars" data-template-name="/connectors/NOME_DO_OUTLET/NOME_DA_CUSTOMIZACAO">
  Seu markup vai aqui...
</script>

Você precisa alterar NOME_DO_OUTLET para o nome do outlet que deseja atingir. Em seguida, altere NOME_DA_CUSTOMIZACAO para o nome que deseja dar a essa personalização. O nome pode ser qualquer coisa, mas tente ser descritivo se possível. É uma boa prática. Assim, chegamos a isso:

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  Seu markup vai aqui... como
  <h1>Olá Mundo!!</h1>
</script>

Vamos tentar isso e ver o que acontece… lembre-se, o trecho acima vai na aba common > header do seu tema.

e…

Até agora tudo bem, mas vamos aprofundar.

Você não quer que seus vídeos apareçam em todos os perfis; deseja que eles sejam exibidos apenas sob certa condição. Então, como fazer isso? Bem, você precisará de duas coisas: alguns dados para consumir e um pouco de JavaScript.

Vamos encontrar os dados. Lembra quando eu disse que plugin-outlets são espaços reservados? Bem, qual é o ponto de tê-los sem contexto? É por isso que o Discourse passa as partes relevantes do contexto para cada plugin-outlet… mas, primeiro, vamos dar um passo para trás. Quando você adiciona isso

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  Seu markup vai aqui, como...
  <h1>Olá Mundo!!</h1>
</script>

Parece HTML — e as tags script são —, mas o que está dentro delas é tratado como código Handlebars.

Isso significa que você pode fazer algo assim em vez disso:

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

e verificar o console do navegador. Você verá isso sempre que o outlet for renderizado; ou seja, quando estiver em uma página de usuário.

Agora, alguma dessas informações é útil? Sim… mas não no momento. Voltaremos a isso. Vamos dar outro passo para trás e ver como o Discourse passa o contexto para o outlet. Se você pesquisar pelo nome do outlet no GitHub — ou localmente —, obterá isso:

Repository search results · GitHub

Vamos abrir esse arquivo. A primeira coisa que você vê é esta linha:

Olhe para a última parte dessa linha:

args=(hash model=model)

Você verá que o Discourse passa model como um argumento para o outlet. Para todos os efeitos práticos e para manter as coisas simples, model = data.

Portanto, um dos argumentos do nosso outlet é model, e é lá que estarão os dados que queremos. Então, vamos voltar ao nosso trecho:

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

e vamos alterá-lo para isso:

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

Agora obtemos isso no console:

Você pode navegar por esses dados e ver se eles contêm o que você precisa. Deveriam, já que contêm todos os dados sobre o usuário usados em outros elementos daquela página. É o “modelo” (model) da página do usuário para aquele usuário específico.

Uma das propriedades disponíveis lá é… rolagem de tambor :drum: … os grupos aos quais o usuário pertence.

Então, se você fizer:

{{log args.model.groups}}

você obterá todos os grupos aos quais o usuário pertence no console.

Certo, agora temos os dados de que precisamos, então a única coisa que resta é adicionar alguma condição(s) baseada neles.

Você pode ser tentado a pensar que podemos fazer isso no mesmo trecho, mas, infelizmente, não podemos. Handlebars é uma linguagem de template. Ela tem suporte muito, muito básico para lógica — nada além de condições simples verdadeiro/falso e loops. Você não pode fazer comparações e outras coisas assim.

Então, onde exatamente você pode fazer isso? Em uma classe de conector, soa sofisticado… eu sei.

Em resumo, uma classe de conector é essencialmente um pedaço de JavaScript anexado ao outlet. É muito mais sutil do que isso, mas é tudo o que você realmente precisa saber por enquanto.

Então, vamos criar uma. Fazemos assim:

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

});
</script>

NOME_DO_OUTLET e NOME_DA_CUSTOMIZACAO aqui devem ser os mesmos que usamos acima. Então vamos alterá-los:

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

});
</script>

Esse trecho também vai na aba common > header do seu tema. Então, agora você deve ter algo assim:

<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 da nossa classe de conector, podemos fazer algum trabalho… mas… precisamos ter cuidado, pois não é apenas como qualquer arquivo JavaScript. Por falta de uma descrição melhor… pense nisso como um componente Ember em dieta. Expandir sobre isso está um pouco fora do escopo aqui, então vamos seguir em frente.

Existem quatro métodos conectados a ele por padrão:

actions permite que você defina ações assim:

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

Você pode então chamar essa ação dentro do outlet, como quando um botão é pressionado. Não precisaremos disso aqui, então vamos seguir em frente.

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  shouldRender(args, component) {
    // retorne true ou false aqui
  }
});

Também não usaremos este, pois o outlet só é renderizado em páginas de perfil e não temos outros requisitos por enquanto. No entanto, você pode usá-lo para adicionar quaisquer condições que deseje testar antes que o outlet seja renderizado. Por exemplo, o nível de confiança do usuário atual ou coisas assim. Seguindo em frente…

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

Este é o que queremos focar. Quaisquer condições ou variáveis de JavaScript que você deseja definir vão aqui. Antes de nos aprofundarmos nele, vamos cobrir o último método primeiro, por completude:

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

Isso é acionado quando o outlet está prestes a ser removido. Portanto, permite que você faça qualquer limpeza necessária, como remover ouvintes de eventos e assim por diante.

Ok, vamos voltar a setupComponent:

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

Você pode ver que há duas coisas passadas para ele. Primeiro, há args e depois há component.

args aqui é a mesma coisa que examinamos anteriormente. São os dados de contexto que o Discourse passou para o outlet. Então, se você fizer:

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

você verá as mesmas informações no console do navegador que vimos antes. Os grupos aos quais o proprietário do perfil pertence. É aqui que a diversão começa; agora você tem os dados e o gancho correto. Então você pode fazer o que quiser aqui. Então, se eu quiser que o vídeo apareça apenas nos perfis de membros que pertencem a um determinado grupo, posso fazer isso:

  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);
    }
  });

Se você tentar isso em uma página de perfil que pertence a um usuário do grupo staff, ele imprimirá true no console. Então, agora a única coisa que nos resta fazer é passar isso para o template do outlet. Veja como você pode fazer isso.

component passado para setupComponent aqui é compartilhado entre o conector e o outlet. Você pode passar coisas para o outlet definindo-as como propriedades no componente, assim:

  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})
    }
  });

Agora, se voltarmos ao template e fizermos algo como:

{{log showVideo}}

e ele imprimirá o mesmo resultado. Então, agora colocamos isso em uma condição Handlebars e adicionamos seu markup dentro dela, assim:

<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="[INSERIR LINK]">
  	  <source src="[INSERIR LINK]" type="video/webm">
  	  <source src="[INSERIR LINK]" type="video/mp4">
    </video>
  {{/if}}
</script>

então verifique uma página de perfil de um usuário staff. Você verá que o vídeo é carregado.

Assim que você navegar para fora do perfil do membro da equipe, o vídeo desaparecerá. O vídeo não aparecerá em perfis de usuários que não estão no grupo staff.

Então, vamos juntar tudo isso. Isso é a mesma coisa de acima.

Aqui está o CSS que usei. Aba 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);
}

// se você quiser que funcione no mobile também
.mobile-view {
  body[class*="user-"] {
    background: none;
    .user-main,
    .user-content {
      padding: 0.5em;
      background: rgba(var(--secondary-rgb), 0.8);
    }
  }
}

HTML / JavaScript / Handlebars. Isso vai na aba common > header do seu 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="[INSERIR LINK]">
  	  <source src="[INSERIR LINK]" type="video/webm">
  	  <source src="[INSERIR 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>

Altere TARGET_GROUP para o nome do grupo que deseja atingir e adicione os atributos src para seus vídeos.

Este post foi um pouco longo… não se deixe desencorajar por isso. Uma vez que você compreenda o conceito, tudo o que fizemos acima pode ser feito em menos de 3 a 5 minutos.

A parte legal aqui é que tudo o que discutimos é praticamente o mesmo para qualquer plugin-outlet. A única coisa que muda é o nome. Portanto, isso se aplica a qualquer modificação de plugin-outlet que você queira fazer no futuro.

  1. Encontre o nome do outlet
  2. Obtenha os dados
  3. Processe os dados em um conector
  4. Passe as propriedades de volta para o template

Isso é incrivelmente aprofundado e terei certeza de revisá-lo quando tiver tempo na próxima semana, mas basta dizer que, de uma olhada rápida, parece muito melhor do que minha implementação atual (incorporar o vídeo em todas as páginas e mostrá-lo apenas no perfil do usuário, o que consegui com um script que adiciona uma tag ao corpo da página de um usuário se o nome de sua conta for algo específico). Obrigado pela explicação detalhada, mal posso esperar para começar!

Ok, então, na maior parte, essa abordagem funciona muito bem, e a explicação é fantástica. Muito obrigado por ir além.

Trabalhar em uma base por usuário é simples - nós apenas verificamos um nome de usuário específico como este:

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

No entanto, encontramos um problema com nosso CSS - desejamos fazer algumas alterações na aparência da página do usuário apenas para esses usuários.

No momento, estamos realizando isso através do mesmo código de 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>

Isso nos permite simplesmente copiar e colar o código “User1” para cada novo usuário, mas depende de um atraso de 50ms após cada carregamento de página antes de disparar, o que é visível para o usuário final (e, se removido, não funciona por algum motivo).

Existe alguma maneira de também conectar essa adição de uma classe ao corpo ao código que você forneceu, para que possamos usá-lo para estilizar páginas com vídeos de forma diferente daquelas sem vídeos?

E, sério, obrigado novamente pela explicação detalhada.