FEATURE: Add user tips for post and topic features (#18964)
* DEV: Add utility to hide all user tips * DEV: Add UserTip Glimmer component * DEV: Add tests for existing user tips * FEATURE: Add user tip for post menu * FEATURE: Add user tip for topic notification level * FEATURE: Add user tip for suggested topics * FEATURE: Hide new popups for existing users
This commit is contained in:
parent
f90a8438e9
commit
ac272c041e
|
@ -2,6 +2,7 @@ import { alias, or } from "@ember/object/computed";
|
|||
import { computed } from "@ember/object";
|
||||
import Component from "@ember/component";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { NotificationLevels } from "discourse/lib/notification-levels";
|
||||
import { getTopicFooterButtons } from "discourse/lib/register-topic-footer-button";
|
||||
import { getTopicFooterDropdowns } from "discourse/lib/register-topic-footer-dropdown";
|
||||
|
||||
|
@ -46,6 +47,11 @@ export default Component.extend({
|
|||
return !isPM || this.canSendPms;
|
||||
},
|
||||
|
||||
@discourseComputed("topic.details.notification_level")
|
||||
showNotificationUserTip(notificationLevel) {
|
||||
return notificationLevel >= NotificationLevels.TRACKING;
|
||||
},
|
||||
|
||||
canSendPms: alias("currentUser.can_send_private_messages"),
|
||||
|
||||
canInviteTo: alias("topic.details.can_invite_to"),
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<span {{did-insert this.showUserTip}}></span>
|
|
@ -0,0 +1,35 @@
|
|||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import Component from "@glimmer/component";
|
||||
import { hideUserTip } from "discourse/lib/user-tips";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default class UserTip extends Component {
|
||||
@service currentUser;
|
||||
|
||||
@action
|
||||
showUserTip(element) {
|
||||
if (!this.currentUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, selector, content, placement } = this.args;
|
||||
this.currentUser.showUserTip({
|
||||
id,
|
||||
|
||||
titleText: I18n.t(`user_tips.${id}.title`),
|
||||
contentText: content || I18n.t(`user_tips.${id}.content`),
|
||||
|
||||
reference: selector
|
||||
? element.parentElement.querySelector(selector) || element.parentElement
|
||||
: element,
|
||||
appendTo: element.parentElement,
|
||||
|
||||
placement: placement || "top",
|
||||
});
|
||||
}
|
||||
|
||||
willDestroy() {
|
||||
hideUserTip(this.args.id);
|
||||
}
|
||||
}
|
|
@ -611,6 +611,10 @@ export default Controller.extend(bufferedProperty("model"), {
|
|||
|
||||
// Post related methods
|
||||
replyToPost(post) {
|
||||
if (this.currentUser) {
|
||||
this.currentUser.hideUserTipForever("post_menu");
|
||||
}
|
||||
|
||||
const composerController = this.composer;
|
||||
const topic = post ? post.get("topic") : this.model;
|
||||
const quoteState = this.quoteState;
|
||||
|
|
|
@ -30,6 +30,7 @@ export function showUserTip(options) {
|
|||
|
||||
arrow: iconHTML("tippy-rounded-arrow"),
|
||||
placement: options.placement,
|
||||
appendTo: options.appendTo,
|
||||
|
||||
// It often happens for the reference element to be rerendered. In this
|
||||
// case, tippy must be rerendered too. Having an animation means that the
|
||||
|
@ -77,6 +78,15 @@ export function hideUserTip(userTipId) {
|
|||
instance.destroy();
|
||||
}
|
||||
delete instances[userTipId];
|
||||
|
||||
const index = queue.findIndex((userTip) => userTip.id === userTipId);
|
||||
if (index > -1) {
|
||||
queue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function hideAllUserTips() {
|
||||
Object.keys(instances).forEach(hideUserTip);
|
||||
}
|
||||
|
||||
function addToQueue(options) {
|
||||
|
|
|
@ -44,6 +44,7 @@ import { cancel } from "@ember/runloop";
|
|||
import discourseLater from "discourse-common/lib/later";
|
||||
import { isTesting } from "discourse-common/config/environment";
|
||||
import {
|
||||
hideAllUserTips,
|
||||
hideUserTip,
|
||||
showNextUserTip,
|
||||
showUserTip,
|
||||
|
@ -1101,8 +1102,10 @@ const User = RestModel.extend({
|
|||
}
|
||||
|
||||
if (!userTips[options.id]) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Cannot show user tip with type =", options.id);
|
||||
if (!isTesting()) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Cannot show user tip with type =", options.id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1142,7 +1145,7 @@ const User = RestModel.extend({
|
|||
seenUserTips.push(userTips[userTipId]);
|
||||
}
|
||||
} else {
|
||||
Object.keys(userTips).forEach(hideUserTip);
|
||||
hideAllUserTips();
|
||||
seenUserTips = [-1];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
<div id="suggested-topics" class="suggested-topics" role="complementary" aria-labelledby="suggested-topics-title">
|
||||
<UserTip @id="suggested_topics" />
|
||||
|
||||
<h3 id="suggested-topics-title" class="suggested-topics-title">
|
||||
{{i18n this.suggestedTitleLabel}}
|
||||
</h3>
|
||||
|
|
|
@ -29,6 +29,10 @@
|
|||
<PinnedButton @pinned={{this.topic.pinned}} @topic={{this.topic}} />
|
||||
|
||||
{{#if this.showNotificationsButton}}
|
||||
{{#if this.showNotificationUserTip}}
|
||||
<UserTip @id="topic_notification_levels" @selector=".notifications-button" />
|
||||
{{/if}}
|
||||
|
||||
<TopicNotificationsButton @notificationLevel={{this.topic.details.notification_level}} @topic={{this.topic}} />
|
||||
{{/if}}
|
||||
|
||||
|
|
|
@ -211,8 +211,9 @@ createWidget("header-notifications", {
|
|||
contentText: I18n.t("user_tips.first_notification.content"),
|
||||
|
||||
reference: document
|
||||
.querySelector(".badge-notification")
|
||||
.querySelector(".d-header .badge-notification")
|
||||
?.parentElement?.querySelector(".avatar"),
|
||||
appendTo: document.querySelector(".d-header .panel"),
|
||||
|
||||
placement: "bottom-end",
|
||||
});
|
||||
|
|
|
@ -711,6 +711,10 @@ export default createWidget("post-menu", {
|
|||
},
|
||||
|
||||
showMoreActions() {
|
||||
if (this.currentUser) {
|
||||
this.currentUser.hideUserTipForever("post_menu");
|
||||
}
|
||||
|
||||
this.state.collapsed = false;
|
||||
const likesPromise = !this.state.likedUsers.length
|
||||
? this.getWhoLiked()
|
||||
|
@ -730,6 +734,8 @@ export default createWidget("post-menu", {
|
|||
keyValueStore &&
|
||||
keyValueStore.set({ key: "likedPostId", value: attrs.id });
|
||||
return this.sendWidgetAction("showLogin");
|
||||
} else {
|
||||
this.currentUser.hideUserTipForever("post_menu");
|
||||
}
|
||||
|
||||
if (this.capabilities.canVibrate && !isTesting()) {
|
||||
|
|
|
@ -25,6 +25,7 @@ import { transformBasicPost } from "discourse/lib/transform-post";
|
|||
import autoGroupFlairForUser from "discourse/lib/avatar-flair";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { nativeShare } from "discourse/lib/pwa-utils";
|
||||
import { hideUserTip } from "discourse/lib/user-tips";
|
||||
|
||||
function transformWithCallbacks(post) {
|
||||
let transformed = transformBasicPost(post);
|
||||
|
@ -593,6 +594,10 @@ createWidget("post-contents", {
|
|||
},
|
||||
|
||||
share() {
|
||||
if (this.currentUser) {
|
||||
this.currentUser.hideUserTipForever("post_menu");
|
||||
}
|
||||
|
||||
const post = this.findAncestorModel();
|
||||
nativeShare(this.capabilities, { url: post.shareUrl }).catch(() => {
|
||||
const topic = post.topic;
|
||||
|
@ -928,4 +933,34 @@ export default createWidget("post", {
|
|||
kvs.set({ key: "lastWarnedLikes", value: Date.now() });
|
||||
}
|
||||
},
|
||||
|
||||
didRenderWidget() {
|
||||
if (!this.currentUser || !this.siteSettings.enable_user_tips) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reference = document.querySelector(
|
||||
".post-controls .actions .show-more-actions"
|
||||
);
|
||||
|
||||
this.currentUser.showUserTip({
|
||||
id: "post_menu",
|
||||
|
||||
titleText: I18n.t("user_tips.post_menu.title"),
|
||||
contentText: I18n.t("user_tips.post_menu.content"),
|
||||
|
||||
reference,
|
||||
appendTo: reference?.closest(".post-controls"),
|
||||
|
||||
placement: "top",
|
||||
});
|
||||
},
|
||||
|
||||
destroy() {
|
||||
hideUserTip("post_menu");
|
||||
},
|
||||
|
||||
willRerenderWidget() {
|
||||
hideUserTip("post_menu");
|
||||
},
|
||||
});
|
||||
|
|
|
@ -612,6 +612,7 @@ export default createWidget("topic-timeline", {
|
|||
contentText: I18n.t("user_tips.topic_timeline.content"),
|
||||
|
||||
reference: document.querySelector("div.timeline-scrollarea-wrapper"),
|
||||
appendTo: document.querySelector("div.topic-timeline"),
|
||||
|
||||
placement: "left",
|
||||
});
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
import { visit } from "@ember/test-helpers";
|
||||
import { hideAllUserTips } from "discourse/lib/user-tips";
|
||||
import { acceptance, query } from "discourse/tests/helpers/qunit-helpers";
|
||||
import I18n from "I18n";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("User Tips - first_notification", function (needs) {
|
||||
needs.user({ unread_high_priority_notifications: 1 });
|
||||
needs.site({ user_tips: { first_notification: 1 } });
|
||||
|
||||
needs.hooks.beforeEach(() => hideAllUserTips());
|
||||
needs.hooks.afterEach(() => hideAllUserTips());
|
||||
|
||||
test("Shows first notification user tip", async function (assert) {
|
||||
this.siteSettings.enable_user_tips = true;
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
assert.equal(
|
||||
query(".user-tip-title").textContent.trim(),
|
||||
I18n.t("user_tips.first_notification.title")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("User Tips - topic_timeline", function (needs) {
|
||||
needs.user();
|
||||
needs.site({ user_tips: { topic_timeline: 2 } });
|
||||
|
||||
needs.hooks.beforeEach(() => hideAllUserTips());
|
||||
needs.hooks.afterEach(() => hideAllUserTips());
|
||||
|
||||
test("Shows topic timeline user tip", async function (assert) {
|
||||
this.siteSettings.enable_user_tips = true;
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
assert.equal(
|
||||
query(".user-tip-title").textContent.trim(),
|
||||
I18n.t("user_tips.topic_timeline.title")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("User Tips - post_menu", function (needs) {
|
||||
needs.user();
|
||||
needs.site({ user_tips: { post_menu: 3 } });
|
||||
|
||||
needs.hooks.beforeEach(() => hideAllUserTips());
|
||||
needs.hooks.afterEach(() => hideAllUserTips());
|
||||
|
||||
test("Shows post menu user tip", async function (assert) {
|
||||
this.siteSettings.enable_user_tips = true;
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
assert.equal(
|
||||
query(".user-tip-title").textContent.trim(),
|
||||
I18n.t("user_tips.post_menu.title")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("User Tips - topic_notification_levels", function (needs) {
|
||||
needs.user();
|
||||
needs.site({ user_tips: { topic_notification_levels: 4 } });
|
||||
|
||||
needs.hooks.beforeEach(() => hideAllUserTips());
|
||||
needs.hooks.afterEach(() => hideAllUserTips());
|
||||
|
||||
test("Shows post menu user tip", async function (assert) {
|
||||
this.siteSettings.enable_user_tips = true;
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
|
||||
assert.equal(
|
||||
query(".user-tip-title").textContent.trim(),
|
||||
I18n.t("user_tips.topic_notification_levels.title")
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("User Tips - suggested_topics", function (needs) {
|
||||
needs.user();
|
||||
needs.site({ user_tips: { suggested_topics: 5 } });
|
||||
|
||||
needs.hooks.beforeEach(() => hideAllUserTips());
|
||||
needs.hooks.afterEach(() => hideAllUserTips());
|
||||
|
||||
test("Shows post menu user tip", async function (assert) {
|
||||
this.siteSettings.enable_user_tips = true;
|
||||
|
||||
await visit("/t/internationalization-localization/280");
|
||||
assert.equal(
|
||||
query(".user-tip-title").textContent.trim(),
|
||||
I18n.t("user_tips.suggested_topics.title")
|
||||
);
|
||||
});
|
||||
});
|
|
@ -23,6 +23,12 @@
|
|||
left: 11px;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="user-tips"][data-placement^="top"]
|
||||
> .tippy-svg-arrow
|
||||
> svg {
|
||||
top: 11px;
|
||||
}
|
||||
|
||||
.tippy-box[data-theme~="user-tips"][data-placement^="bottom"]
|
||||
> .tippy-svg-arrow
|
||||
> svg {
|
||||
|
|
|
@ -288,6 +288,9 @@ class User < ActiveRecord::Base
|
|||
@user_tips ||= Enum.new(
|
||||
first_notification: 1,
|
||||
topic_timeline: 2,
|
||||
post_menu: 3,
|
||||
topic_notification_levels: 4,
|
||||
suggested_topics: 5,
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -1838,6 +1838,18 @@ en:
|
|||
title: "Topic timeline"
|
||||
content: "Scroll quickly through a post using the topic timeline."
|
||||
|
||||
post_menu:
|
||||
title: "Post menu"
|
||||
content: "See how else you can interact with the post by clicking the three dots!"
|
||||
|
||||
topic_notification_levels:
|
||||
title: "You are now following this topic"
|
||||
content: "Look for this bell to adjust your notification preferences for specific topics or whole categories."
|
||||
|
||||
suggested_topics:
|
||||
title: "Keep reading!"
|
||||
content: "Here are some topics we think you might like to read next."
|
||||
|
||||
loading: "Loading..."
|
||||
errors:
|
||||
prev_page: "while trying to load"
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class HideUserTips3To5ForExistingUsers < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
execute "UPDATE user_options SET seen_popups = seen_popups || '{3, 4, 5}'"
|
||||
end
|
||||
|
||||
def down
|
||||
execute "UPDATE user_options SET seen_popups = array_remove(array_remove(array_remove(seen_popups, 3), 4), 5)"
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue