テーマコンポーネントを使用してユーザーログインイベントをリッスンする方法はありますか

こんにちは、皆様。

初めてログインしたユーザーに対して、Theme コンポーネントを使用してポップアップボックスを表示したいと考えています。

1. ログイン成功時にポップアップボックスを表示するため、Theme コンポーネントを使用してログインコントローラーのログインアクションをオーバーライドしようとしました。
ログインアクション内で、hiddenLoginForm.submit(); の直前に bootbox.alert("Test Alert"); コードを追加しました。

しかし、ポップアップは hiddenLoginForm が送信され、ホームページにリダイレクトされるまで表示されません。

私の要件は、ログイン成功後にホームページへリダイレクトされた後にポップアップボックスを表示することです。

2. ユーザーが初めてログインしているか確認するために、現在のユーザーの previous_visit_at プロパティの値をチェックしようとしました。
previous_visit_atnull の場合(つまり、ユーザーが初めてログインしている場合)です。
しかし、このテストも失敗しました。2 回目にログインした際も、previous_visit_at プロパティの値が null だったためです。
previous_visit_at プロパティの値が正確にいつ更新されるのか知りたいです。

上記の要件を実現するためのご協力をよろしくお願いいたします。

ありがとうございました。
Saurabh Khandelwal

@Saurabh_Khandelwal さん、こんにちは :wave:

この方法は推奨しません。ログインは最も重要なアクションの一つであり、このアクションをオーバーライドすると脆弱になり、コア部分で変更を加えた際にすぐに破綻する可能性があります。

ご記憶かと思いますが、初めてこのサイトに登録された際、以下のような画面が表示されました。

これを表示するための条件は、私たちが使用しているもの と同じです。

!user.read_first_notification && !user.enforcedSecondFactor

したがって、イニシャライザーで同じ条件を使用すれば、ログイン後のユーザーの最初のページ表示時に Bootbox をレンダリングできます。

テーマコンポーネントで以下のようなコードを試してみてください。

import { withPluginApi } from "discourse/lib/plugin-api";
import bootbox from "bootbox";

export default {
  name: "first-login-bootbox",
  initialize() {
    withPluginApi("0.8", api => {
      const user = api.getCurrentUser();
      if (!user) return;

      if (!user.read_first_notification && !user.enforcedSecondFactor) {
        const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor`;
        bootbox.alert(text);
      }
    });
  }
};

これにより、以下のような表示になります。

必要に応じて Bootbox のテキストに HTML を使用することも可能です。ユーザーが Bootbox を閉じて、丸で囲まれたアバターをクリックすると、二度と Bootbox は表示されなくなります。

なお、Bootbox はシンプルなダイアログや確認用のものであり、それよりも少し複雑な機能が必要な場合は、showModal() を使用してモーダルを作成する方が良いでしょう。

@Johani さん、アドバイスありがとうございます。ログインアクションの変更は避け、ご指摘の通り実装を試みます。

こんにちは、@Johani さん。ポップアップボックス内に新しいタブで開く単純なハイパーリンク(例:“/new-topic”)を追加しました。
そのリンクをクリックすると新しいタブが開きますが、以下の条件を確認しているため、再度ポップアップボックスが表示されてしまいます。

!user.read_first_notification && !user.enforcedSecondFactor

私のケースでは、リンクを訪れた後はポップアップボックスが表示されないようにすべきです。

ポップアップボックスが一度表示された後に、これらのプロパティの値を更新することは可能でしょうか?可能であれば、その方法を教えてください。

残念ながら、それは不可能です。

これらのプロパティにはセッターが存在しません。仮に存在したとしても、変更は最初のウィンドウで一時的にのみ適用されます。ユーザーが 2 番目のタブに移動すると、データはデータベースに保存されている内容に基づいて表示されます。テーマはバックエンドにアクセスできず、フロントエンドの変更のみ可能です。

代わりに、リンクにハッシュを追加し、以下のようにチェックする方法があります。

import { withPluginApi } from "discourse/lib/plugin-api";
import bootbox from "bootbox";

export default {
  name: "first-login-bootbox",
  initialize() {
    withPluginApi("0.8", api => {
      const user = api.getCurrentUser();
      if (!user) return;

      if (
        !user.read_first_notification &&
        !user.enforcedSecondFactor &&
        !window.location.hash
      ) {
        const text = `Lorem ipsum dolor sit amet <a href="http://localhost:3000/new-topic#some-hash" target="_blank">Link</a>, consectetur adipiscing elit, sed do eiusmod tempor`;
        bootbox.alert(text);
      }
    });
  }
};

投稿内の /new-topic へのリンクが単なる例なのか、それとも実際に実行したい操作なのかはわかりません。もしそれが望ましい結果である場合、別の問題が発生します。ハッシュ付きのページで bootbox が表示されなくても、ユーザーは以下のような状態を目にすることになります…

…そして、コンポーザーは開きません。これは、ユーザーが最初のページビューで即座にトピックを入力し始めるのは非常に予想外であるため、当然の結果です。

ここで何を実現しようとしているのでしょうか?ユーザーに何かを伝えたいのでしょうか?

他のサイトで行われている一般的な方法は、ウェルカムメッセージを編集することですが、それが可能であれば代替案もあります。

以下に提案します。

  1. 必要な情報をすべて含んだトピックを作成する
  2. そのトピックを公開する
  3. bootbox 内にそのトピックへのリンクを貼り付け、新しいタブで開く

これにより、ユーザーがリンクをクリックすると、以下のような画面(オーバーレイなし)が表示されます。

そのページを見終えたら、最初のタブに戻り、bootbox を閉じて最初の通知を読み、その後サイトを利用を続行できます。

この方法であれば、ハッシュの追加やチェックすら不要です。以下にその例を示します。

import { withPluginApi } from "discourse/lib/plugin-api";
import bootbox from "bootbox";

export default {
  name: "first-login-bootbox",
  initialize() {
    withPluginApi("0.8", api => {
      const user = api.getCurrentUser();
      if (!user) return;

      if (!user.read_first_notification && !user.enforcedSecondFactor) {
        const text = `Lorem ipsum dolor sit amet <a href="http://my.site.com/pub/bentley-flying-spur-s-production-milestone" target="_blank">Link</a>, consectetur adipiscing elit, sed do eiusmod tempor`;
        bootbox.alert(text);
      }
    });
  }
};

「/new-topic」や「/my/preferences」のようなリンクをポップアップ内に表示したいと考えています。

それ以外のアプローチとして、現在の URL を確認してポップアップをホームページでのみ表示し、他のページでは表示しないようにすることも考えています。

「/new-topic」リンクをクリックした後にホームページの URL を確認しても、上記の問題は発生します。

こんにちは、Saurabh さん、
はい、現在の URL を確認し、ポップアップをホームページでのみ表示するようにすれば機能すると思います。

Discobot の新しいユーザー向け紹介を、数分(例:2 分)後に表示することも可能です:

こんにちは、@Johani

以下の通りウィジェットを作成し、その中で bootbox() を呼び出すことは可能でしょうか。
さらに bootbox() を呼び出した後、header-notifications widget でも使用されている skipNewUserTips() メソッドを呼び出すことは可能でしょうか。

<script type="text/discourse-plugin" version="0.8">
 const { h } = require('virtual-dom');
 const { ajax } = require("discourse/lib/ajax").default;
 const DiscourseURL =  require("discourse/lib/url");
 const { userPath } = require("discourse/lib/url");

api.createWidget("welcome-popup", {
  tagName: "div.welcome-popup",
  html(attrs) {
      let user = this.currentUser;
      if (!user) return;
      if (
        !user.get("read_first_notification") &&
        !user.get("enforcedSecondFactor")
      ) {
          const title = `<div class="first-login-bootbox-title">You're a member. Welcome aboard!</div>`;
          const body = `<div class="first-login-bootbox-body">Now you can: </br> <ol class="user-suggestions"><li><a href="/new-topic" target="_blank" >Ask a question or start a discussion</a></li> <li><a href="/my/preferences/tags" target="_blank">Set up your notification settings</a></li></ol></div>`;
          bootbox.alert({
                        title: title,
                        message: body
                    });
            this.skipNewUserTips();        
      }
      return null;
  },

  skipNewUserTips() {

    ajax(userPath(this.currentUser.username_lower), {
      type: "PUT",
      data: {
        skip_new_user_tips: true,
      },
    }).then(() => {
      this.currentUser.set("skip_new_user_tips", true);
    });
  },
});
</script>

<script
  type="text/x-handlebars"
  data-template-name="/connectors/below-site-header/welcome-popup"
>
  {{mount-widget widget="welcome-popup"}}
</script> 

header-notifications widget の Discourse コード:
https://github.com/discourse/discourse/blob/master/app/assets/javascripts/discourse/app/widgets/header.js#L101

これにより問題が発生するでしょうか?

これはすべての新規ユーザーに対して、このオーバーレイを完全にスキップします。つまり、彼らがこれを見ることはありません。

それで問題なければ、はい、機能するはずです。

ただし、bootbox の呼び出しを変更する必要があります。そうしないと、以下のような表示になります。

という処理をする代わりに、単一の引数のみを渡す必要があります。

これらのオプションは Bootbox のより新しいバージョンで追加されたものであり、現在 Discourse で使用しているバージョンでは機能しません(社内でこの件について話し合っていますが、当面はこれらを使用できません)。

また、PUT リクエストを作成しているため、以下の処理もスキップできます。

this.currentUser.set("skip_new_user_tips", true);

したがって、以下のような実装になるかもしれません。

  api.createWidget("welcome-popup", {
    tagName: "div.welcome-popup",
    html(attrs) {
      let user = this.currentUser;
      if (!user) return;
      if (
        !user.get("read_first_notification") &&
        !user.get("enforcedSecondFactor")
      ) {
        const body = `<h2 class="first-login-bootbox-title">You're a member. Welcome aboard!</h2>
                      <hr>
                      <div class="first-login-bootbox-body">
                        Now you can:
                        <br>
                        <ol class="user-suggestions">
                          <li>
                            <a href="/new-topic" target="_blank"
                              >Ask a question or start a discussion</a
                            >
                          </li>
                          <li>
                            <a href="/my/preferences/tags" target="_blank"
                              >Set up your notification settings</a
                            >
                          </li>
                        </ol>
                      </div>`;
        bootbox.alert(body);
        this.skipNewUserTips();
      }
      return null;
    },

    skipNewUserTips() {
      ajax(userPath(this.currentUser.username_lower), {
        type: "PUT",
        data: {
          skip_new_user_tips: true
        }
      });
    }
  });

こんにちは @Johani さん、

skip_new_user_tips を true に設定しているため、Discobot の挨拶通知もスキップされてしまいます。最初の通知オーバーレイマスクが表示されないのは問題ありませんが、Discobot の挨拶通知は表示させたいと考えています。

Discobot 通知:

このため、すべての新規ユーザーに対して Discobot の挨拶通知が読み込まれるようにコードを変更しました。
現在は、ヘッダーウィジェットに新しいメソッド「closeFirstNotificationMask()」を追加しています。このメソッドでは「ringBackdrop」を false に、「userVisible」をその逆値に設定し、welcome-popup ウィジェット内で bootbox メソッドを呼び出した後にこのメソッドを呼び出しています。

参考にしたのは、ユーザーアイコンがクリックされたときに呼び出されるヘッダーウィジェットの「toggleUserMenu」メソッドです。

<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/5.4.1/bootbox.min.js" integrity="sha512-eoo3vw71DUo5NRvDXP/26LFXjSFE1n5GQ+jZJhHz+oOTR4Bwt7QBCjsgGvuVMQUMMMqeEvKrQrNEI4xQMXp3uA==" crossorigin="anonymous"></script>

<script type="text/discourse-plugin" version="0.8">
 const { h } = require('virtual-dom');
 const { ajax } = require("discourse/lib/ajax").default;

/*  Welcome-popup 表示後に呼び出されるヘッダーウィジェットへの新しいメソッドの追加 */
api.reopenWidget("header",{
    closeFirstNotificationMask() {
        this.state.ringBackdrop = false;
        this.state.userVisible = !this.state.userVisible;
        this.toggleBodyScrolling(this.state.userVisible);
    }
});

/* 新しいウィジェット「welcome-popup」の作成。これはユーザーが初めてログインしたときのみレンダリングされます */
api.createWidget("welcome-popup", {
  tagName: "div.welcome-popup",
  html(attrs) {
      let user = this.currentUser;
      if (!user) return;
      if ( !user.get("read_first_notification") && !user.get("enforcedSecondFactor") ) {
          const title = `<div class="first-login-bootbox-title">You're a member. Welcome aboard!</div>`;
          const body = `<div class="first-login-bootbox-body">Now you can: </br> <ol class="user-suggestions"><li><a href="/new-topic" target="_blank ">Ask a question or start a discussion</a></li> <li><a href="/my/preferences/tags" target="_blank">Set up your notification settings</a></li></ol></div>`;
          
          bootbox.alert({
                        title: title,
                        message: body
                    });
            // ヘッダーウィジェットの closeFirstNotificationMask アクションメソッドの呼び出し
            this.sendWidgetAction("closeFirstNotificationMask", this.attrs);
        }
      return null;
  },
  
});

/* 以下のコードは、ユーザーが初めてログインした際に「header-notifications」ウィジェットをレンダリングした後に「welcome-popup」ウィジェットをレンダリングします */
api.decorateWidget("header-notifications:after", helper => { 
    if(!helper.attrs.active && helper.attrs.ringBackdrop){
        return helper.attach("welcome-popup", helper.attrs);     
    }else{
        return null;    
    }
});
</script>

もう一点、現在 Discourse がマルチパラメータの bootbox メソッドをサポートしていないため、テーマコンポーネントに以下のスクリプトを含めて Bootbox 5.4.1 バージョンを使用しています。これで問題ありませんでしょうか?

<script src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/5.4.1/bootbox.min.js" integrity="sha512-eoo3vw71DUo5NRvDXP/26LFXjSFE1n5GQ+jZJhHz+oOTR4Bwt7QBCjsgGvuVMQUMMMqeEvKrQrNEI4xQMXp3uA==" crossorigin="anonymous"></script>

ご意見をお聞かせください。よろしくお願いいたします!

これは良さそうです :+1:

ローカルで試してみましたが、問題は見つかりませんでした。いくつかの minor な変更を加えました。

<script
  src="https://cdnjs.cloudflare.com/ajax/libs/bootbox.js/5.4.1/bootbox.min.js"
  integrity="sha512-eoo3vw71DUo5NRvDXP/26LFXjSFE1n5GQ+jZJhHz+oOTR4Bwt7QBCjsgGvuVMQUMMMqeEvKrQrNEI4xQMXp3uA=="
  crossorigin="anonymous"
></script>

<script type="text/discourse-plugin" version="0.8">
  const user = api.getCurrentUser();
  if (!user || user.read_first_notification || user.enforcedSecondFactor) {
    return;
  }

  const bootboxTitle = `<div class="first-login-bootbox-title">You're a member. Welcome aboard!</div>`;
  const bootboxBody = `<div class="first-login-bootbox-body">Now you can: </br> <ol class="user-suggestions"><li><a href="/new-topic" target="_blank">Ask a question or start a discussion</a></li> <li><a href="/my/preferences/tags" target="_blank">Set up your notification settings</a></li></ol></div>`;

  /* Adding new Method to header Widget which will call after displaying Welcome-popup*/
  api.reopenWidget("header", {
    closeFirstNotificationMask() {
      this.state.ringBackdrop = false;
      this.state.userVisible = !this.state.userVisible;
    }
  });

  /* Created new widget "welcome-popup", this widget will be render only when user login for the first time */
  api.createWidget("welcome-popup", {
    tagName: "div.welcome-popup",
    html(attrs) {
      //Calling closeFirstNotificationMask action method of header widget
      this.sendWidgetAction("closeFirstNotificationMask", attrs);

      bootbox.alert({
        title: bootboxTitle,
        message: bootboxBody
      });
    }
  });

  /* Below code will render "welcome-popup" widget after rendering "header-notifications" widget when User login first time*/
  api.decorateWidget("header-notifications:before", helper => {
    if (!helper.attrs.active && helper.attrs.ringBackdrop) {
      return helper.attach("welcome-popup", helper.attrs);
    }
  });
</script>

はい、問題ありません。Discourse は私たちが使用する bootbox のバージョンをシャムとして読み込むため、テーマコンポーネントで異なるバージョンを読み込んでもコア部分には何も影響しません。新しいバージョンはテーマコンポーネント内でのみ使用されます。唯一の欠点は、初期ページの読み込み時に 1 つの追加リクエストと約 4kb のオーバーヘッドが生じる点です。

@Johani さん、welcome-popup ウィジェットを header-notifications ウィジェットより先に読み込んだため、Discobot の通知が読み込まれていないようです。また、welcome-popup 内のリンクをクリックしても、再度 welcome-popup がユーザーに表示されてしまいます。
私は welcome-popup ウィジェットを header-notifications ウィジェットより後に読み込むようにテストしたところ、問題なく動作しました。そのため、これを「header-notifications:after」に変更してもよろしいでしょうか。

それは不思議ですね :thinking:

さっき、3人の新しいユーザーで同じコードを再度試しましたが、毎回期待通りの結果が得られました。最初のページ表示時にポップアップが表示され、Discobot の挨拶メッセージが送信されます。その後のページ表示ではポップアップは表示されません。

もちろん、あなたにとって動作するのであれば、デコレーターの位置は問題ないはずです。

@Johani さん、ありがとうございます。after デコレータを使用します。