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");
}
if (!this.siteSettings.enable_onboarding_popups) {
if (!this.siteSettings.enable_user_tips) {
if (
this.currentUser &&
!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("seen_popups", null);
this.model.set("user_option.skip_new_user_tips", false);

View File

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

View File

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

View File

@ -13,7 +13,7 @@ import { wantsNewWindow } from "discourse/lib/intercept-click";
import { logSearchLinkClick } from "discourse/lib/search";
import RenderGlimmer from "discourse/widgets/render-glimmer";
import { hbs } from "ember-cli-htmlbars";
import { hidePopup } from "discourse/lib/popup";
import { hideUserTip } from "discourse/lib/user-tips";
let _extraHeaderIcons = [];
@ -88,7 +88,7 @@ createWidget("header-notifications", {
const count = unread + reviewables;
if (count > 0) {
if (this._shouldHighlightAvatar()) {
if (this.siteSettings.enable_onboarding_popups) {
if (this.siteSettings.enable_user_tips) {
contents.push(h("span.ring"));
} else {
this._addAvatarHighlight(contents);
@ -124,7 +124,7 @@ createWidget("header-notifications", {
const unreadHighPriority = user.unread_high_priority_notifications;
if (!!unreadHighPriority) {
if (this._shouldHighlightAvatar()) {
if (this.siteSettings.enable_onboarding_popups) {
if (this.siteSettings.enable_user_tips) {
contents.push(h("span.ring"));
} else {
this._addAvatarHighlight(contents);
@ -198,17 +198,17 @@ createWidget("header-notifications", {
didRenderWidget() {
if (
!this.currentUser ||
!this.siteSettings.enable_onboarding_popups ||
!this.siteSettings.enable_user_tips ||
!this._shouldHighlightAvatar()
) {
return;
}
this.currentUser.showPopup({
this.currentUser.showUserTip({
id: "first_notification",
titleText: I18n.t("popup.first_notification.title"),
contentText: I18n.t("popup.first_notification.content"),
titleText: I18n.t("user_tips.first_notification.title"),
contentText: I18n.t("user_tips.first_notification.content"),
reference: document
.querySelector(".badge-notification")
@ -219,11 +219,11 @@ createWidget("header-notifications", {
},
destroy() {
hidePopup("first_notification");
hideUserTip("first_notification");
},
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 renderTags from "discourse/lib/render-tags";
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 LAST_READ_HEIGHT = 20;
@ -601,15 +601,15 @@ export default createWidget("topic-timeline", {
},
didRenderWidget() {
if (!this.currentUser || !this.siteSettings.enable_onboarding_popups) {
if (!this.currentUser || !this.siteSettings.enable_user_tips) {
return;
}
this.currentUser.showPopup({
this.currentUser.showUserTip({
id: "topic_timeline",
titleText: I18n.t("popup.topic_timeline.title"),
contentText: I18n.t("popup.topic_timeline.content"),
titleText: I18n.t("user_tips.topic_timeline.title"),
contentText: I18n.t("user_tips.topic_timeline.content"),
reference: document.querySelector("div.timeline-scrollarea-wrapper"),
@ -618,10 +618,10 @@ export default createWidget("topic-timeline", {
},
destroy() {
hidePopup("topic_timeline");
hideUserTip("topic_timeline");
},
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();
});
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();
site.set("onboarding_popup_types", { first_notification: 1 });
site.set("user_tips", { first_notification: 1 });
await visit("/u/eviltrout/preferences/interface");
assert.ok(
exists(".pref-reset-seen-popups"),
"has reset seen popups button"
exists(".pref-reset-seen-user-tips"),
"has reset seen user tips button"
);
await click(".pref-reset-seen-popups");
await click(".pref-reset-seen-user-tips");
assert.deepEqual(lastUserData, {
seen_popups: "",

View File

@ -12,7 +12,6 @@
@import "crawler_layout";
@import "d-icon";
@import "d-popover";
@import "d-onboarding";
@import "dialog";
@import "directory";
@import "discourse";
@ -59,4 +58,5 @@
@import "topic";
@import "upload";
@import "user-badges";
@import "user-tips";
@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
def self.user_tips
@user_tips ||= Enum.new(
first_notification: 1,
topic_timeline: 2,
)
end
def visible_sidebar_tags(user_guardian = nil)
user_guardian ||= guardian
DiscourseTagging.filter_visible(custom_sidebar_tags, user_guardian)

View File

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

View File

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

View File

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

View File

@ -1161,7 +1161,7 @@ en:
not_first_time: "Not your first time?"
skip_link: "Skip these tips"
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"
color_scheme_default_on_all_devices: "Set default color scheme(s) on all my devices"
color_scheme: "Color Scheme"
@ -1827,7 +1827,7 @@ en:
what_are_you_doing: "What are you doing?"
remove_status: "Remove status"
popup:
user_tips:
primary: "Got it!"
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"
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."

View File

@ -379,7 +379,7 @@ basic:
enable_user_status:
client: true
default: false
enable_onboarding_popups:
enable_user_tips:
client: true
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
end
describe '#onboarding_popup_types' do
it 'is included if enable_onboarding_popups' do
SiteSetting.enable_onboarding_popups = true
describe '#user_tips' do
it 'is included if enable_user_tips' do
SiteSetting.enable_user_tips = true
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
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
expect(serialized[:onboarding_popup_types]).to eq(nil)
expect(serialized[:user_tips]).to eq(nil)
end
end

View File

@ -530,7 +530,7 @@ RSpec.describe UserUpdater do
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.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)