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

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

特定のページにコンテンツを追加したい場合、最適な方法は plugin-outlet(プラグイン・アウトレット)を使用することです。簡単に言うと、plugin-outlet は Discourse のテンプレートに予約されたスペースで、ここに新しいコンテンツを追加できます。

まず初めに、ターゲットとするページに plugin-outlet が存在するかどうかを確認する必要があります。その際に役立つテーマコンポーネントがあります。

(deprecated) Plugin outlet locations theme component

このコンポーネントをインストールし、有効化したら、ターゲットとするページに移動して利用可能なアウトレットを確認してください。今回の場合、plugin-outlet が存在します(緑色でハイライト)。

したがって、必要なものは above-user-profile です。

もしこれが存在しなかったらどうなるでしょうか?その場合の最善策は、追加を依頼するか、コアに追加するための PR(プルリクエスト)を送ることです。ユースケースが妥当であれば、ほとんどの場合承認されます。

とにかく、今回のケースでは既に存在しています。では、どのようにマークアップを追加するか見ていきましょう。残りの手順では上記のコンポーネントは不要なので、plugin outlet の名前がわかった時点で無効にできます。

必要なことは、テーマのヘッダータブに以下のようなコードを追加することだけです。

<script type="text/x-handlebars" data-template-name="/connectors/OUTLET_NAME/SOME_NAME">
  ここにマークアップを記述...
</script>

OUTLET_NAME をターゲットとする outlet の名前に変更し、SOME_NAME をこのカスタマイズに与えたい名前に変更してください。名前は任意ですが、可能であれば説明的な名前にするのが良い慣習です。これにより、以下のようなコードになります。

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  ここにマークアップを記述...例えば
  <h1>Hello World!!</h1>
</script>

これを試して結果を見てみましょう… 覚えておいてください、上記のスニペットはテーマの common > header タブに記述します。

そして…

ここまでは順調ですが、さらに深く掘り下げてみましょう。

すべてのプロフィールで動画を表示したくはないはずです。ある条件に基づいてのみ表示したいでしょう。では、どうすればよいでしょうか?そのためには、2 つの要素が必要です。消費するデータと、少しの JavaScript です。

データを探しましょう。plugin-outlet が予約されたスペースだと述べたことを思い出してください。では、文脈(コンテキスト)なしにそれらを設ける意味は何でしょうか?そのため、Discourse は関連するコンテキストの断片を各 plugin outlet に渡します… まず一歩引いて考えましょう。以下のようなコードを追加すると

<script type="text/x-handlebars" data-template-name="/connectors/above-user-profile/add-profile-videos">
  ここにマークアップを記述...例えば...
  <h1>Hello World!!</h1>
</script>

これは HTML のように見えます(script タグ自体は HTML ですが)、その中身は handlebars コードとして扱われます。

つまり、以下のような処理も可能です。

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

そしてブラウザのコンソールを確認してください。Outlet がレンダリングされるたびに、つまりユーザーページにいるときに、以下のような出力が見られます。

これらの情報が役立つかというと、現時点ではそうではありません。後で戻りましょう。もう一歩引いて、Discourse がどのようにコンテキストを outlet に渡しているか見てみましょう。GitHub またはローカル環境で outlet 名を検索すると、以下のような結果が得られます。

Repository search results · GitHub

そのファイルを開いてください。最初に表示されるのはこの行です。

その行の最後の部分を見てみましょう

args=(hash model=model)

Discourse が model を引数として outlet に渡していることがわかります。実用上の目的と単純化のため、model = data と考えて問題ありません。

つまり、outlet の引数の一つが model であり、欲しいデータはそこに入っています。では、スニペットに戻りましょう。

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

これを以下のように変更します

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

これでコンソールに以下のような出力が得られます。

そのデータを参照して、必要なものが含まれているか確認してください。そのページ上の他の要素で使用されているユーザーに関するすべてのデータが含まれているため、含まれているはずです。これは、その特定のユーザーのユーザーページの「モデル」です。

利用可能なプロパティの一つは… ドラムロール :drum: … ユーザーが所属するグループです。

したがって、以下のようにすると

{{log args.model.groups}}

コンソールにユーザーが所属するすべてのグループが表示されます。

さて、必要なデータが手に入ったので、残っているのはそれに基づいて条件を追加することだけです。

同じスニペット内で処理できると考えたいかもしれませんが、残念ながらそれはできません。Handlebars はテンプレート言語であり、ロジックのサポートは非常に、非常に 限定的です。単純な真偽条件とループ以上のことはできません。比較処理などは行えません。

では、どこでそれを行えばよいのでしょうか?「connector class(コネクタークラス)」です。少し派手に聞こえますね… 分かります。

簡単に言うと、connector class は outlet に付随する JavaScript の一部です。実際にはもっと複雑ですが、現時点ではそれだけ知っていれば十分です。

では、作成してみましょう。以下のようにします

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

});
</script>

ここで OUTLET_NAMESOME_NAME上記で使用したものと同じである必要があります。したがって、変更しましょう

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

});
</script>

このスニペットもテーマの common > header タブに記述します。これで以下のような状態になっているはずです

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

connector class の内部で処理を行えますが… 注意が必要です。これは単なる JavaScript ファイルとは異なります。より適切な表現が見つからないため… 痩身した Ember コンポーネントだと考えてください。これ以上の詳細は範囲外ですので、先に進みましょう。

デフォルトで 4 つのメソッドが接続されています

actions を使用して、以下のようにアクションを定義できます

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  actions: {
    myAction() {
      // 何かを行う
    }
  }
});

その後、ボタンが押された際などに、outlet 内でそのアクションを呼び出すことができます。今回は必要ありませんので、先に進みましょう。

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  shouldRender(args, component) {
    // ここで true または false を返す
  }
});

これも使用しません。outlet はプロフィールページでのみレンダリングされ、現時点では他の要件がないためです。ただし、outlet がレンダリングされる前にテストしたい条件(現在のユーザーの信頼レベルなど)を追加するために使用できます。次に…

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  setupComponent(args, component) {
    // 何かを行う
  }
});

これが焦点を当てるべきメソッドです。設定したい JavaScript の条件や変数はここに入ります。このメソッドをさらに掘り下げる前に、完全性の観点から最後のメソッドを説明しましょう

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  teardownComponent(args, component) {
    // 何かを行う
  }
});

これは outlet が削除されるときに発火します。イベントリスナーの削除など、必要なクリーンアップを行うことができます。

さて、setupComponent に戻りましょう

api.registerConnectorClass("above-user-profile", "add-profile-videos", {
  setupComponent(args, component) {
    // 何かを行う
  }
});

2 つの引数が渡されていることがわかります。まず args、次に component です。

ここで args は先ほど見たものと同じです。Discourse が outlet に渡したコンテキストデータです。したがって、以下のようにすると

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

先ほどブラウザのコンソールで見たのと同じ情報、つまりプロフィール所有者が所属するグループが表示されます。ここからが本番です。データも正しいフックも手に入りました。したがって、ここで好きな処理を行えます。つまり、特定のグループに所属するメンバーのプロフィールでのみ動画を表示したい場合、以下のようにできます

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

staff グループに所属するユーザーのプロフィールページでこれを試すと、コンソールに true が出力されます。これで残っているのは、それを outlet テンプレートに渡すことです。その方法は以下の通りです。

setupComponent に渡される component は、connector と outlet で共有されます。component のプロパティとして設定することで、outlet に値を渡すことができます

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

これで、テンプレートに戻って以下のような処理を行います

{{log showVideo}}

同じ結果が出力されます。これを Handlebars の条件文に入れ、その中にマークアップを追加します

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

staff ユーザーのプロフィールページを確認してください。動画が読み込まれることが確認できるはずです。

staff メンバーのプロフィールから移動すると、動画は消えます。staff グループに所属しないユーザーのプロフィールでは動画は表示されません。

では、これらすべてをまとめましょう。上記と同じ内容です。

以下は使用した CSS です。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);
}

// モバイル版でも表示させたい場合
.mobile-view {
  body[class*="user-"] {
    background: none;
    .user-main,
    .user-content {
      padding: 0.5em;
      background: rgba(var(--secondary-rgb), 0.8);
    }
  }
}

HTML / JavaScript / Handlebars。これはテーマの common > header タブに記述します

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

TARGET_GROUP をターゲットとするグループ名に変更し、動画の src 属性を追加してください。

この記事は長めになりました… 気にしないでください。概念を理解すれば、上記の作業はすべて 3〜5 分以内に完了します。

良い点は、ここで話したことはほぼすべての plugin outlet に当てはまることです。変わるのは名前だけです。つまり、将来行うあらゆる plugin-outlet の修正に応用できます。

  1. outlet 名を見つける
  2. データを取得する
  3. connector でデータを処理する
  4. プロパティをテンプレートに渡す

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

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

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

      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への追加をフックする方法はありますか?これにより、動画のないページと動画のあるページを異なる方法でスタイル設定するために使用できますか?

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