FEATURE: Rename onboarding popups to user tips (#18826)

This commit also hides the new user tips for existing users.
This commit is contained in:
Bianca Nenciu 2022-11-09 20:20:34 +02:00 committed by GitHub
parent 3d376c71b6
commit 4dad7816b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 171 additions and 143 deletions

View File

@ -238,7 +238,7 @@ const SiteHeaderComponent = MountWidget.extend(
this.currentUser.on("status-changed", this, "queueRerender"); this.currentUser.on("status-changed", this, "queueRerender");
} }
if (!this.siteSettings.enable_onboarding_popups) { if (!this.siteSettings.enable_user_tips) {
if ( if (
this.currentUser && this.currentUser &&
!this.get("currentUser.read_first_notification") !this.get("currentUser.read_first_notification")

View File

@ -420,7 +420,7 @@ export default Controller.extend({
} }
}, },
resetSeenPopups() { resetSeenUserTips() {
this.model.set("skip_new_user_tips", false); this.model.set("skip_new_user_tips", false);
this.model.set("seen_popups", null); this.model.set("seen_popups", null);
this.model.set("user_option.skip_new_user_tips", false); this.model.set("user_option.skip_new_user_tips", false);

View File

@ -6,8 +6,8 @@ import tippy from "tippy.js";
const instances = {}; const instances = {};
const queue = []; const queue = [];
export function showPopup(options) { export function showUserTip(options) {
hidePopup(options.id); hideUserTip(options.id);
if (!options.reference) { if (!options.reference) {
return; return;
@ -23,7 +23,7 @@ export function showPopup(options) {
showOnCreate: true, showOnCreate: true,
hideOnClick: false, hideOnClick: false,
trigger: "manual", trigger: "manual",
theme: "d-onboarding", theme: "user-tips",
// It must be interactive to make buttons work. // It must be interactive to make buttons work.
interactive: true, interactive: true,
@ -40,17 +40,15 @@ export function showPopup(options) {
allowHTML: true, allowHTML: true,
content: ` content: `
<div class='onboarding-popup-container'> <div class='user-tip-container'>
<div class='onboarding-popup-title'>${escape(options.titleText)}</div> <div class='user-tip-title'>${escape(options.titleText)}</div>
<div class='onboarding-popup-content'>${escape( <div class='user-tip-content'>${escape(options.contentText)}</div>
options.contentText <div class='user-tip-buttons'>
)}</div>
<div class='onboarding-popup-buttons'>
<button class="btn btn-primary btn-dismiss">${escape( <button class="btn btn-primary btn-dismiss">${escape(
options.primaryBtnText || I18n.t("popup.primary") options.primaryBtnText || I18n.t("user_tips.primary")
)}</button> )}</button>
<button class="btn btn-flat btn-text btn-dismiss-all">${escape( <button class="btn btn-flat btn-text btn-dismiss-all">${escape(
options.secondaryBtnText || I18n.t("popup.secondary") options.secondaryBtnText || I18n.t("user_tips.secondary")
)}</button> )}</button>
</div> </div>
</div>`, </div>`,
@ -73,12 +71,12 @@ export function showPopup(options) {
}); });
} }
export function hidePopup(popupId) { export function hideUserTip(userTipId) {
const instance = instances[popupId]; const instance = instances[userTipId];
if (instance && !instance.state.isDestroyed) { if (instance && !instance.state.isDestroyed) {
instance.destroy(); instance.destroy();
} }
delete instances[popupId]; delete instances[userTipId];
} }
function addToQueue(options) { function addToQueue(options) {
@ -92,9 +90,9 @@ function addToQueue(options) {
queue.push(options); queue.push(options);
} }
export function showNextPopup() { export function showNextUserTip() {
const options = queue.shift(); const options = queue.shift();
if (options) { if (options) {
showPopup(options); showUserTip(options);
} }
} }

View File

@ -43,7 +43,11 @@ import Evented from "@ember/object/evented";
import { cancel } from "@ember/runloop"; import { cancel } from "@ember/runloop";
import discourseLater from "discourse-common/lib/later"; import discourseLater from "discourse-common/lib/later";
import { isTesting } from "discourse-common/config/environment"; import { isTesting } from "discourse-common/config/environment";
import { hidePopup, showNextPopup, showPopup } from "discourse/lib/popup"; import {
hideUserTip,
showNextUserTip,
showUserTip,
} from "discourse/lib/user-tips";
export const SECOND_FACTOR_METHODS = { export const SECOND_FACTOR_METHODS = {
TOTP: 1, TOTP: 1,
@ -1090,57 +1094,68 @@ const User = RestModel.extend({
return [...trackedTags, ...watchedTags, ...watchingFirstPostTags]; return [...trackedTags, ...watchedTags, ...watchingFirstPostTags];
}, },
showPopup(options) { showUserTip(options) {
const popupTypes = Site.currentProp("onboarding_popup_types"); const userTips = Site.currentProp("user_tips");
if (!popupTypes[options.id]) { if (!userTips || this.skip_new_user_tips) {
return;
}
if (!userTips[options.id]) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn("Cannot display popup with type =", options.id); console.warn("Cannot show user tip with type =", options.id);
return; return;
} }
const seenPopups = this.seen_popups || []; const seenUserTips = this.seen_popups || [];
if (seenPopups.includes(popupTypes[options.id])) { if (
seenUserTips.includes(-1) ||
seenUserTips.includes(userTips[options.id])
) {
return; return;
} }
showPopup({ showUserTip({
...options, ...options,
onDismiss: () => this.hidePopupForever(options.id), onDismiss: () => this.hideUserTipForever(options.id),
onDismissAll: () => this.hidePopupForever(), onDismissAll: () => this.hideUserTipForever(),
}); });
}, },
hidePopupForever(popupId) { hideUserTipForever(userTipId) {
// Empty popupId means all popups. const userTips = Site.currentProp("user_tips");
const popupTypes = Site.currentProp("onboarding_popup_types"); if (!userTips || this.skip_new_user_tips) {
if (popupId && !popupTypes[popupId]) {
// eslint-disable-next-line no-console
console.warn("Cannot hide popup with type =", popupId);
return; return;
} }
// Hide any shown popups. // Empty userTipId means all user tips.
let seenPopups = this.seen_popups || []; if (userTipId && !userTips[userTipId]) {
if (popupId) { // eslint-disable-next-line no-console
hidePopup(popupId); console.warn("Cannot hide user tip with type =", userTipId);
if (!seenPopups.includes(popupTypes[popupId])) { return;
seenPopups.push(popupTypes[popupId]); }
// Hide any shown user tips.
let seenUserTips = this.seen_popups || [];
if (userTipId) {
hideUserTip(userTipId);
if (!seenUserTips.includes(userTips[userTipId])) {
seenUserTips.push(userTips[userTipId]);
} }
} else { } else {
Object.keys(popupTypes).forEach(hidePopup); Object.keys(userTips).forEach(hideUserTip);
seenPopups = Object.values(popupTypes); seenUserTips = [-1];
} }
// Show next popup in queue. // Show next user tip in queue.
showNextPopup(); showNextUserTip();
// Save seen popups on the server. // Save seen user tips on the server.
if (!this.user_option) { if (!this.user_option) {
this.set("user_option", {}); this.set("user_option", {});
} }
this.set("seen_popups", seenPopups); this.set("seen_popups", seenUserTips);
this.set("user_option.seen_popups", seenPopups); this.set("user_option.seen_popups", seenUserTips);
if (popupId) { if (userTipId) {
return this.save(["seen_popups"]); return this.save(["seen_popups"]);
} else { } else {
this.set("skip_new_user_tips", true); this.set("skip_new_user_tips", true);

View File

@ -117,8 +117,8 @@
<ComboBox @valueProperty="value" @content={{this.titleCountModes}} @value={{this.model.user_option.title_count_mode}} @id="user-title-count-mode" @onChange={{action (mut this.model.user_option.title_count_mode)}} /> <ComboBox @valueProperty="value" @content={{this.titleCountModes}} @value={{this.model.user_option.title_count_mode}} @id="user-title-count-mode" @onChange={{action (mut this.model.user_option.title_count_mode)}} />
</div> </div>
<PreferenceCheckbox @labelKey="user.skip_new_user_tips.description" @checked={{this.model.user_option.skip_new_user_tips}} @class="pref-new-user-tips" /> <PreferenceCheckbox @labelKey="user.skip_new_user_tips.description" @checked={{this.model.user_option.skip_new_user_tips}} @class="pref-new-user-tips" />
{{#if this.site.onboarding_popup_types}} {{#if this.site.user_tips}}
<DButton @class="pref-reset-seen-popups" @action={{action "resetSeenPopups"}}>{{i18n "user.reset_seen_popups"}}</DButton> <DButton @class="pref-reset-seen-user-tips" @action={{action "resetSeenUserTips"}}>{{i18n "user.reset_seen_user_tips"}}</DButton>
{{/if}} {{/if}}
</fieldset> </fieldset>

View File

@ -13,7 +13,7 @@ import { wantsNewWindow } from "discourse/lib/intercept-click";
import { logSearchLinkClick } from "discourse/lib/search"; import { logSearchLinkClick } from "discourse/lib/search";
import RenderGlimmer from "discourse/widgets/render-glimmer"; import RenderGlimmer from "discourse/widgets/render-glimmer";
import { hbs } from "ember-cli-htmlbars"; import { hbs } from "ember-cli-htmlbars";
import { hidePopup } from "discourse/lib/popup"; import { hideUserTip } from "discourse/lib/user-tips";
let _extraHeaderIcons = []; let _extraHeaderIcons = [];
@ -88,7 +88,7 @@ createWidget("header-notifications", {
const count = unread + reviewables; const count = unread + reviewables;
if (count > 0) { if (count > 0) {
if (this._shouldHighlightAvatar()) { if (this._shouldHighlightAvatar()) {
if (this.siteSettings.enable_onboarding_popups) { if (this.siteSettings.enable_user_tips) {
contents.push(h("span.ring")); contents.push(h("span.ring"));
} else { } else {
this._addAvatarHighlight(contents); this._addAvatarHighlight(contents);
@ -124,7 +124,7 @@ createWidget("header-notifications", {
const unreadHighPriority = user.unread_high_priority_notifications; const unreadHighPriority = user.unread_high_priority_notifications;
if (!!unreadHighPriority) { if (!!unreadHighPriority) {
if (this._shouldHighlightAvatar()) { if (this._shouldHighlightAvatar()) {
if (this.siteSettings.enable_onboarding_popups) { if (this.siteSettings.enable_user_tips) {
contents.push(h("span.ring")); contents.push(h("span.ring"));
} else { } else {
this._addAvatarHighlight(contents); this._addAvatarHighlight(contents);
@ -198,17 +198,17 @@ createWidget("header-notifications", {
didRenderWidget() { didRenderWidget() {
if ( if (
!this.currentUser || !this.currentUser ||
!this.siteSettings.enable_onboarding_popups || !this.siteSettings.enable_user_tips ||
!this._shouldHighlightAvatar() !this._shouldHighlightAvatar()
) { ) {
return; return;
} }
this.currentUser.showPopup({ this.currentUser.showUserTip({
id: "first_notification", id: "first_notification",
titleText: I18n.t("popup.first_notification.title"), titleText: I18n.t("user_tips.first_notification.title"),
contentText: I18n.t("popup.first_notification.content"), contentText: I18n.t("user_tips.first_notification.content"),
reference: document reference: document
.querySelector(".badge-notification") .querySelector(".badge-notification")
@ -219,11 +219,11 @@ createWidget("header-notifications", {
}, },
destroy() { destroy() {
hidePopup("first_notification"); hideUserTip("first_notification");
}, },
willRerenderWidget() { willRerenderWidget() {
hidePopup("first_notification"); hideUserTip("first_notification");
}, },
}); });

View File

@ -9,7 +9,7 @@ import discourseLater from "discourse-common/lib/later";
import { relativeAge } from "discourse/lib/formatter"; import { relativeAge } from "discourse/lib/formatter";
import renderTags from "discourse/lib/render-tags"; import renderTags from "discourse/lib/render-tags";
import renderTopicFeaturedLink from "discourse/lib/render-topic-featured-link"; import renderTopicFeaturedLink from "discourse/lib/render-topic-featured-link";
import { hidePopup } from "discourse/lib/popup"; import { hideUserTip } from "discourse/lib/user-tips";
const SCROLLER_HEIGHT = 50; const SCROLLER_HEIGHT = 50;
const LAST_READ_HEIGHT = 20; const LAST_READ_HEIGHT = 20;
@ -601,15 +601,15 @@ export default createWidget("topic-timeline", {
}, },
didRenderWidget() { didRenderWidget() {
if (!this.currentUser || !this.siteSettings.enable_onboarding_popups) { if (!this.currentUser || !this.siteSettings.enable_user_tips) {
return; return;
} }
this.currentUser.showPopup({ this.currentUser.showUserTip({
id: "topic_timeline", id: "topic_timeline",
titleText: I18n.t("popup.topic_timeline.title"), titleText: I18n.t("user_tips.topic_timeline.title"),
contentText: I18n.t("popup.topic_timeline.content"), contentText: I18n.t("user_tips.topic_timeline.content"),
reference: document.querySelector("div.timeline-scrollarea-wrapper"), reference: document.querySelector("div.timeline-scrollarea-wrapper"),
@ -618,10 +618,10 @@ export default createWidget("topic-timeline", {
}, },
destroy() { destroy() {
hidePopup("topic_timeline"); hideUserTip("topic_timeline");
}, },
willRerenderWidget() { willRerenderWidget() {
hidePopup("topic_timeline"); hideUserTip("topic_timeline");
}, },
}); });

View File

@ -144,18 +144,18 @@ acceptance("User Preferences - Interface", function (needs) {
document.querySelector("meta[name='discourse_theme_id']").remove(); document.querySelector("meta[name='discourse_theme_id']").remove();
}); });
test("shows reset seen onboarding popups button", async function (assert) { test("shows reset seen user tips popups button", async function (assert) {
let site = Site.current(); let site = Site.current();
site.set("onboarding_popup_types", { first_notification: 1 }); site.set("user_tips", { first_notification: 1 });
await visit("/u/eviltrout/preferences/interface"); await visit("/u/eviltrout/preferences/interface");
assert.ok( assert.ok(
exists(".pref-reset-seen-popups"), exists(".pref-reset-seen-user-tips"),
"has reset seen popups button" "has reset seen user tips button"
); );
await click(".pref-reset-seen-popups"); await click(".pref-reset-seen-user-tips");
assert.deepEqual(lastUserData, { assert.deepEqual(lastUserData, {
seen_popups: "", seen_popups: "",

View File

@ -12,7 +12,6 @@
@import "crawler_layout"; @import "crawler_layout";
@import "d-icon"; @import "d-icon";
@import "d-popover"; @import "d-popover";
@import "d-onboarding";
@import "dialog"; @import "dialog";
@import "directory"; @import "directory";
@import "discourse"; @import "discourse";
@ -59,4 +58,5 @@
@import "topic"; @import "topic";
@import "upload"; @import "upload";
@import "user-badges"; @import "user-badges";
@import "user-tips";
@import "user"; @import "user";

View File

@ -1,37 +0,0 @@
.onboarding-popup-container {
min-width: 300px;
padding: 0.5em;
text-align: left;
.onboarding-popup-title {
font-size: $font-up-2;
font-weight: bold;
}
.onboarding-popup-content {
margin-top: 0.25em;
}
.onboarding-popup-buttons {
margin-top: 1em;
}
}
.tippy-box[data-theme~="d-onboarding"][data-placement^="left"]
> .tippy-svg-arrow
> svg {
left: 11px;
}
.tippy-box[data-theme~="d-onboarding"][data-placement^="bottom"]
> .tippy-svg-arrow
> svg {
top: -13px;
left: -1px;
}
.tippy-box[data-theme~="d-onboarding"] > .tippy-svg-arrow:after,
.tippy-box[data-theme~="d-onboarding"] > .tippy-svg-arrow > svg {
width: 18px;
height: 18px;
}

View File

@ -0,0 +1,37 @@
.user-tip-container {
min-width: 300px;
padding: 0.5em;
text-align: left;
.user-tip-title {
font-size: $font-up-2;
font-weight: bold;
}
.user-tip-content {
margin-top: 0.25em;
}
.user-tip-buttons {
margin-top: 1em;
}
}
.tippy-box[data-theme~="user-tips"][data-placement^="left"]
> .tippy-svg-arrow
> svg {
left: 11px;
}
.tippy-box[data-theme~="user-tips"][data-placement^="bottom"]
> .tippy-svg-arrow
> svg {
top: -13px;
left: -1px;
}
.tippy-box[data-theme~="user-tips"] > .tippy-svg-arrow:after,
.tippy-box[data-theme~="user-tips"] > .tippy-svg-arrow > svg {
width: 18px;
height: 18px;
}

View File

@ -1,10 +0,0 @@
# frozen_string_literal: true
class OnboardingPopup
def self.types
@types ||= Enum.new(
first_notification: 1,
topic_timeline: 2,
)
end
end

View File

@ -284,6 +284,13 @@ class User < ActiveRecord::Base
MAX_STAFF_DELETE_POST_COUNT ||= 5 MAX_STAFF_DELETE_POST_COUNT ||= 5
def self.user_tips
@user_tips ||= Enum.new(
first_notification: 1,
topic_timeline: 2,
)
end
def visible_sidebar_tags(user_guardian = nil) def visible_sidebar_tags(user_guardian = nil)
user_guardian ||= guardian user_guardian ||= guardian
DiscourseTagging.filter_visible(custom_sidebar_tags, user_guardian) DiscourseTagging.filter_visible(custom_sidebar_tags, user_guardian)

View File

@ -293,7 +293,7 @@ class CurrentUserSerializer < BasicUserSerializer
end end
def include_seen_popups? def include_seen_popups?
SiteSetting.enable_onboarding_popups SiteSetting.enable_user_tips
end end
def include_primary_group_id? def include_primary_group_id?

View File

@ -6,7 +6,7 @@ class SiteSerializer < ApplicationSerializer
:default_archetype, :default_archetype,
:notification_types, :notification_types,
:post_types, :post_types,
:onboarding_popup_types, :user_tips,
:trust_levels, :trust_levels,
:groups, :groups,
:filters, :filters,
@ -104,12 +104,12 @@ class SiteSerializer < ApplicationSerializer
Post.types Post.types
end end
def onboarding_popup_types def user_tips
OnboardingPopup.types User.user_tips
end end
def include_onboarding_popup_types? def include_user_tips?
SiteSetting.enable_onboarding_popups SiteSetting.enable_user_tips
end end
def filters def filters

View File

@ -181,11 +181,7 @@ class UserUpdater
end end
if attributes.key?(:skip_new_user_tips) if attributes.key?(:skip_new_user_tips)
user.user_option.seen_popups = if user.user_option.skip_new_user_tips user.user_option.seen_popups = user.user_option.skip_new_user_tips ? [-1] : nil
OnboardingPopup.types.values
else
nil
end
end end
# automatically disable digests when mailing_list_mode is enabled # automatically disable digests when mailing_list_mode is enabled

View File

@ -1161,7 +1161,7 @@ en:
not_first_time: "Not your first time?" not_first_time: "Not your first time?"
skip_link: "Skip these tips" skip_link: "Skip these tips"
read_later: "I'll read it later." read_later: "I'll read it later."
reset_seen_popups: "Show onboarding tips again" reset_seen_user_tips: "Show user tips again"
theme_default_on_all_devices: "Make this the default theme on all my devices" theme_default_on_all_devices: "Make this the default theme on all my devices"
color_scheme_default_on_all_devices: "Set default color scheme(s) on all my devices" color_scheme_default_on_all_devices: "Set default color scheme(s) on all my devices"
color_scheme: "Color Scheme" color_scheme: "Color Scheme"
@ -1827,7 +1827,7 @@ en:
what_are_you_doing: "What are you doing?" what_are_you_doing: "What are you doing?"
remove_status: "Remove status" remove_status: "Remove status"
popup: user_tips:
primary: "Got it!" primary: "Got it!"
secondary: "don't show me these tips" secondary: "don't show me these tips"

View File

@ -2354,7 +2354,7 @@ en:
sitemap_page_size: "Number of URLs to include in each sitemap page. Max 50.000" sitemap_page_size: "Number of URLs to include in each sitemap page. Max 50.000"
enable_user_status: "(experimental) Allow users to set custom status message (emoji + description)." enable_user_status: "(experimental) Allow users to set custom status message (emoji + description)."
enable_onboarding_popups: "(experimental) Enable educational popups that describe key features to users" enable_user_tips: "(experimental) Enable new user tips that describe key features to users"
short_title: "The short title will be used on the user's home screen, launcher, or other places where space may be limited. It should be limited to 12 characters." short_title: "The short title will be used on the user's home screen, launcher, or other places where space may be limited. It should be limited to 12 characters."

View File

@ -379,7 +379,7 @@ basic:
enable_user_status: enable_user_status:
client: true client: true
default: false default: false
enable_onboarding_popups: enable_user_tips:
client: true client: true
default: false default: false

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class RenameOnboardingPopupsSiteSetting < ActiveRecord::Migration[7.0]
def up
execute "UPDATE site_settings SET name = 'enable_user_tips' WHERE name = 'enable_onboarding_popups'"
end
def down
execute "UPDATE site_settings SET name = 'enable_onboarding_popups' WHERE name = 'enable_user_tips'"
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class HideAllUserTipsForExistentUsers < ActiveRecord::Migration[7.0]
def up
execute "UPDATE user_options SET seen_popups = '{1, 2}'"
end
def down
execute "UPDATE user_options SET seen_popups = '{}'"
end
end

View File

@ -8,17 +8,17 @@ RSpec.describe SiteSerializer do
Site.clear_cache Site.clear_cache
end end
describe '#onboarding_popup_types' do describe '#user_tips' do
it 'is included if enable_onboarding_popups' do it 'is included if enable_user_tips' do
SiteSetting.enable_onboarding_popups = true SiteSetting.enable_user_tips = true
serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
expect(serialized[:onboarding_popup_types]).to eq(OnboardingPopup.types) expect(serialized[:user_tips]).to eq(User.user_tips)
end end
it 'is not included if enable_onboarding_popups is disabled' do it 'is not included if enable_user_tips is disabled' do
serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json serialized = described_class.new(Site.new(guardian), scope: guardian, root: false).as_json
expect(serialized[:onboarding_popup_types]).to eq(nil) expect(serialized[:user_tips]).to eq(nil)
end end
end end

View File

@ -530,7 +530,7 @@ RSpec.describe UserUpdater do
UserUpdater.new(Discourse.system_user, user).update(skip_new_user_tips: true) UserUpdater.new(Discourse.system_user, user).update(skip_new_user_tips: true)
expect(user.user_option.skip_new_user_tips).to eq(true) expect(user.user_option.skip_new_user_tips).to eq(true)
expect(user.user_option.seen_popups).to eq(OnboardingPopup.types.values) expect(user.user_option.seen_popups).to eq([-1])
UserUpdater.new(Discourse.system_user, user).update(skip_new_user_tips: false) UserUpdater.new(Discourse.system_user, user).update(skip_new_user_tips: false)