是否可以通过 Theme 组件监听用户登录事件

大家好,

我想在用户首次登录时,使用 Theme 组件显示弹窗。

1. 我尝试通过 Theme 组件覆盖登录控制器的登录操作,以便在用户成功登录时显示弹窗。
在登录操作中,我在 hiddenLoginForm.submit(); 之前添加了 bootbox.alert("Test Alert"); 代码。

但弹窗是在 hiddenLoginForm 提交并跳转到主页之后才显示的。

我的需求是:弹窗应在成功登录后跳转到主页之后显示。

2. 为了检查用户是否为首次登录,我尝试检查当前用户的 previous_visit_at 属性值。
如果 previous_visit_atnull(即用户首次登录)。
但测试失败,因为第二次登录时,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 仅适用于简单的对话框或确认提示。如果你需要更复杂的功能,那么使用 showModal() 创建模态框可能是更好的选择。

谢谢 @Johani 的建议。我会避免在登录操作中修改,并尝试按你提到的方式实现。

嗨,@Johani,我在弹窗中添加了一个简单的超链接(例如“/new-topic”),该链接会在新标签页中打开。
点击该链接后,它确实在新标签页中打开了,但系统又显示了一个弹窗,因为我们的代码检查了以下两个条件:

!user.read_first_notification && !user.enforcedSecondFactor

在我的场景中,访问该链接后不应再显示弹窗。

我们能否在弹窗首次显示后更新这些属性的值?如果可以,该如何实现?

恐怕这是不可能的。

这些属性没有 setter;即使有,您的更改也只会临时应用于第一个窗口。一旦用户访问第二个标签页,数据将基于数据库中存储的内容。主题无法访问后端;它们只能更改前端。

您可以做的是在链接中添加一个哈希值,并像这样进行检查。

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 并仅在主页显示弹出窗口应该可行。

我们可以在一段时间后(例如 2 分钟)向新用户展示来自 Discobot 的欢迎介绍:

你好,@Johani

我们能否创建一个如下所示的部件,并在其中调用 bootbox()?
调用 bootbox 后,我们能否再调用 “skipNewUserTips()” 方法?该方法也被 Discourse 在 header-notifications 部件中使用。

<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 部件的 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">您已成为会员。欢迎加入!</h2>
                      <hr>
                      <div class="first-login-bootbox-body">
                        现在您可以:
                        <br>
                        <ol class="user-suggestions">
                          <li>
                            <a href="/new-topic" target="_blank"
                              >提问或发起讨论</a
                            >
                          </li>
                          <li>
                            <a href="/my/preferences/tags" target="_blank"
                              >设置您的通知偏好</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” 设置为其相反值。并在欢迎弹窗小部件中调用 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;

/* 为标题小部件添加新方法,该方法将在显示欢迎弹窗后调用 */
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>

还有一件事,我们通过在主题组件中包含以下脚本来使用 Bootbox 5.4.1 版本,因为目前 Discourse 尚不支持多参数的 bootbox 方法。这样是否可行?

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

我在本地测试过,没有发现任何问题。我做了一些小修改。

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

  /* 向头部小部件添加新方法,该方法将在显示欢迎弹窗后调用 */
  api.reopenWidget("header", {
    closeFirstNotificationMask() {
      this.state.ringBackdrop = false;
      this.state.userVisible = !this.state.userVisible;
    }
  });

  /* 创建新的小部件 "welcome-popup",该小部件仅在用户首次登录时渲染 */
  api.createWidget("welcome-popup", {
    tagName: "div.welcome-popup",
    html(attrs) {
      // 调用头部小部件的 closeFirstNotificationMask 动作方法
      this.sendWidgetAction("closeFirstNotificationMask", attrs);

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

  /* 以下代码将在用户首次登录且渲染 "header-notifications" 小部件后渲染 "welcome-popup" 小部件 */
  api.decorateWidget("header-notifications:before", helper => {
    if (!helper.attrs.active && helper.attrs.ringBackdrop) {
      return helper.attach("welcome-popup", helper.attrs);
    }
  });
</script>

是的,没问题。Discourse 会将我们使用的 bootbox 版本作为垫片(shim)加载,因此在主题组件中加载不同版本不会改变核心功能。新版本仅在你的主题组件中使用。唯一的缺点是它会增加一次额外的请求,并使初始页面加载增加约 4kb。

你好 @Johani,由于你在 header-notifications 组件之前加载了 welcome-popup 组件,Discobot 通知似乎未被加载。此外,点击 welcome-popup 中的链接后,welcome-popup 会再次显示给用户。

我测试过在 header-notifications 组件之后加载 welcome-popup 组件,效果正常。因此,我能否将其改为 “header-notifications:after”?

这很奇怪 :thinking:

我刚刚用三个不同的新用户再次测试了相同的代码,每次都得到了预期的结果。我在首次页面加载时看到了弹窗,Discobot 的问候消息也已发送。随后的页面加载则不再显示弹窗。

当然,如果对你来说可行,那么装饰器的位置应该无关紧要。

感谢 @Johani,我们将使用 after 装饰器。