一部のユーザープロフィールに背景動画を追加しますか?

特定のユーザープロフィールページに動画を追加しようとしています。これにより、すべてのパトロンがプロフィールに特定の背景動画を設定できるようになります。(皆さんが騒ぎ出す前に、これは単なるループアニメーションであり、本格的な動画ではありません。見た目はかなり良く、Steamのプロフィール背景に似ています。)

以下のHTMLとCSSコードは、すべてのユーザーに対して機能しますが、明らかに私たちが求めているものではありません。

// 「ヘッダー」タブに入力します

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

「general」カテゴリのページに画像を追加するために body.category-general を使用するのとは異なり、特定のグループのユーザーや特定のユーザー名のプロフィールページに割り当てられた特定のスラッグは存在しないようです。私たちはこれに慣れておらず、主にCSSの経験しかなく、HTMLを直接操作した経験はほとんどないため、これを希望通りに機能させる簡単な方法があるかどうかはわかりません。

最善のアプローチは、ユーザーグループに基づいて同様のスラッグを追加することだと考えていますが、これをどのように実現し、動画が正しいコンテンツを持つページにのみ表示されるようにするかはわかりません。また、この特定のアプローチに固執しているわけでもありません。もし、より簡単な方法があれば、そちらを採用するつもりです。

例えば、グループごとではなく、ユーザーごとにこれを行うという考えにも前向きです。もしそれが何らかの形で簡単であれば。

ただ、すべてのページに動画をハードコーディングしたくないので、関係する特定のユーザーのページにのみ読み込まれるようにしたいのです。

編集: 変更があるかもしれないので、安定版を使用していることを追記すべきかもしれません。

現在のところ、正規リンクから特定のユーザーのページにいるかどうかを検出できるかどうかを確認し、そうであればビデオを適用するというアプローチをとっています。そのため、以下のコードを使用しています。

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

しかし、これはフルリフレッシュでのみ機能するようです。ページ間を移動しても --currUsername プロパティが更新されず、ユーザーページにランダムな背景色を適用する代わりに、F5 が最後にユーザーページで押された場合はすべてのページにランダムな背景色が適用され、F5 が最後に非ユーザーページで押された場合はどのページにも何も適用されません。

JavaScript にはそれほど詳しくないので、なぜそのようなことが起こるのか分かりません。ページが変更されたときに、関数が実行され(実際には実行されます)、pageURL 変数が更新され、ページを読み込むときに --currUsername プロパティが更新されるはずです。しかし、これはフルリフレッシュでのみ発生し、それ以外の場合は変数が変更されていないようです。

ここで何が間違っているか、何かアイデアはありますか?

これは、og:url プロパティは更新されるのに、正規 URL が更新されないためだと思われます。

唯一の問題は、var pageURL = document.querySelector("meta[property='og:url']").getAttribute("content"); を使用すると、メタタグ自体が更新される 前に 更新されてしまうことです。つまり、このコードは現在のページの URL ではなく、前のページの URL を取得してしまいます。

If you want to add content to a specific page, your best option is a plugin-outlet. In a nutshell, plugin-outlets are spaces reserved in Discourse templates that you can use to add new content.

The first thing you’ll need to find out is if a plugin-outlet exists on the page you want to target. There’s a theme component that you can install that helps you with that.

Plugin outlet locations theme component

Once you install that component, toggle it, head over to the page you want to target and check what you have to work with. In your case, such a plugin-outlet exists (highlighted in green)

So, the one we want is above-user-profile

Suppose that it didn’t exist… what then? The best option, in that case, is to ask for it to be added or send a PR to add it to core. It will get accepted most of the time if your use-case makes sense.

Anyway, as I said, it already exists in this case. So, let’s see how you can add markup to it. You won’t need the component above for the rest of this, so you can disable it now since you already have the name of the plugin outlet.

All you need to do is add something like this in the header tab of your theme.

<script type="text/x-handlebars" data-template-name="/connectors/OUTLET_NAME/SOME_NAME">
  Your markup goes here...
</script>

You need to change OUTLET_NAME to the name of the outlet you want to target. Then change SOME_NAME to the name you want to give this customization. The name can be anything, but try to be descriptive if you can. It’s a good practice. So, we end up with this.

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  Your markup goes here...like
  <h1>Hello World!!</h1>
</script>

Let’s try that and see what happens… remember, the snippet above goes in your theme’s common > header tab.

and…

So far so good, but let’s dig deeper.

You don’t want your videos to show on every profile, and you want them to only show based on some condition. So, how do you do that? Well, you’ll need two things, some data to consume and a bit of Javascript.

Let’s find the data. Remember when I said plugin-outlets are reserved spaces? Well, what’s the point of having them without context? That’s why Discourse passes the relevant bits of context to each plugin outlet… but first, let’s take a step back. When you add this

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  Your markup goes here, like...
  <h1>Hello World!!</h1>
</script>

It looks like HTML - and the script tags are - but what’s inside them is treated as handlebars code.

That means that you can do something like this instead

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

and check the browser console. You’ll see this whenever the outlet is rendered; IE, you’re on a user page.

image

Now, is any of this stuff helpful? Yes… but not at the moment. We’ll come back to this. Let’s take another step back and see how Discourse passes the context to the outlet. If you search for the outlet name on Github - or locally - you’ll get this.

Search · "above-user-profile" · GitHub

Let’s open that file. The first thing you see is this line

Look at the last bit of that line

args=(hash model=model)

You’ll see that Discourse passes model as an argument to the outlet. For all intents and purposes and to keep things simple, model = data

So, one of the arguments for our outlet is model, and that’s where the data we want is going to be. So, let’s go back to our snippet.

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

and let’s change it to this instead

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

Now we get this in the console.

You can browse through that data and see if it has what you need. It should since it contains all the data about the user used in other elements on that page. It’s the “model” for the user page for that particular user.

One of the properties available there is… drumroll :drum: … the groups the user belongs to.

So, if you do

{{log args.model.groups}}

you’ll get all the groups the user belongs to in the console.

user groups logged in the console

Alright, now we have the data we need, so the only thing left is to add some condition(s) based on that.

You might be tempted to think that we can do that in the same snippet, but, unfortunately, we can’t. Handlebars is a templating language. It has very, very basic support for logic - nothing beyond simple true/false conditions and loops. You can’t do comparisons and other stuff like that.

So where exactly can you do that? In a connector class, sounds fancy… I know.

In a nutshell, a connector class is essentially a bit of Javascript attached to the outlet. It’s a lot more nuanced than that, but that’s all you really need to know for now.

So, let’s create one. We do it like this

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

});
</script>

OUTLET_NAME and SOME_NAME here should be the same as the ones we used above. So let’s change them

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

});
</script>

This snippet goes in the common > header tab in your theme as well. So you should now have something that looks like this

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

Inside our connector class, we can do some work… but… we need to be mindful that it’s not just like any javaScript file. For lack of a better description… think of it as an Ember component on a diet. Expanding on this is a bit outside the scope here, so let’s move on.

There are four methods wired up to it by default

actions allows you to define actions like so

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

You can then call that action from within the outlet, like when a button is pressed. We won’t be needing this one here, so let’s move on.

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  shouldRender(args, component) {
    // return true or false here
  }
});

We also won’t be using this one since the outlet only renders on profile pages, and we don’t have any other requirements for now. However, you can use this to add any conditions you want to test before the outlet is rendered. For example, the trust-level of the current user or stuff like that. Moving on…

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

This is the one we want to focus on. Whatever javaScript conditions or variables you want to set go here. Before we dig deeper into that one, let’s cover the last method first for completeness’ sake

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

this fires when the outlet is going to be removed. So, it allows you to do any cleanup required, like remove event listeners and so on.

Ok, let’s go back to setupComponent

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

You can see that it has two things passed to it. First, there’s args and then there’s component

args here is the same stuff we looked at earlier. It’s the context data that Discourse passed to the outlet. So, if you do

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

you’ll see the same information in the browser console that we saw before. The groups that the profile owner belongs to. This is where the fun begins, you now have the data, and you have the correct hook. So you can do whatever you want here. So, if I want the video to only show on the profiles of members that belong to a certain group, I can do this

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

If you try this on a profile page that belongs to a user in the staff group, it will print true in the console. So, now the only thing we have left to do is to pass that to the outlet template. Here’s how you can do that.

component passed to setupComponent here is shared between the connector and the outlet. You can pass things to the outlet by setting them as properties on the component like so

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

Now, if we head back to the template and do something like

{{log showVideo}}

and it will print the same result. So we now put that in a Handlebars condition and add your markup inside it like so

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

then check a profile page for a staff user. You’ll see that it loads the video.

Once you navigate away from the staff member’s profile, the video will go away. The video won’t show on profiles for users who are not in the staff group.

So, let’s put all of this together. This is the same stuff from above.

Here’s the CSS I used. common > css tab

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

// if you want it on mobile too
.mobile-view {
  body[class*="user-"] {
    background: none;
    .user-main,
    .user-content {
      padding: 0.5em;
      background: rgba(var(--secondary-rgb), 0.8);
    }
  }
}

HTML / javaScript / Handlebars. This goes in the common > header tab of your theme

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

Change TARGET_GROUP to the name of the group you want to target and add the src attributes for your videos.

This post was on the longer side… don’t be deterred by that. Once you grasp the concept, everything we did above can be done in less than 3-5 minutes.

The nice thing here is that all the stuff we talked about is pretty much the same for any plugin outlet. The only thing that changes is the name. So, this applies to any plugin-outlet modifications you want to do in the future.

  1. find the outlet name
  2. get the data
  3. process the data in a connector
  4. pass the properties back to the template
「いいね!」 7

それは信じられないほど詳細で、来週時間があるときに必ず確認しますが、要するに、ざっと見たところ、現在の実装よりもはるかに優れているようです(すべてのページに動画を埋め込み、ユーザープロファイルにのみ表示するというもので、アカウント名が特定の文字列である場合にユーザーのページのbodyにタグを追加するスクリプトで実現しました)。詳細な説明をありがとうございます。取り掛かるのが待ちきれません!

「いいね!」 2

わかりました。このアプローチはほとんどの場合うまく機能し、説明も素晴らしいです。本当にご尽力いただきありがとうございます。

ユーザーごとに作業するのは簡単です。次のように、特定のユーザー名を確認するだけです。

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

しかし、CSSで問題が発生しています。これらのユーザーのユーザーページの外観を少し変更したいのです。

現在、以前と同じコードでこれを実現しています。

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

これにより、新しいユーザーごとに「User1」コードをコピーして貼り付けることができますが、ページ読み込み後に50ミリ秒の遅延が発生し、それがエンドユーザーに表示されます(削除すると、何らかの理由で機能しません)。

提供されたコードに、このクラスのbodyへの追加をフックする方法はありますか?これにより、動画のないページと動画のあるページを異なる方法でスタイル設定するために使用できますか?

そして、改めて詳細なご説明をいただき、本当にありがとうございました。

「いいね!」 1