DEV: Add implementations for various notification types for the new user menu (#17589)

This commit is a subset of the changes proposed in https://github.com/discourse/discourse/pull/17379.
This commit is contained in:
Osama Sayegh 2022-07-22 05:07:32 +03:00 committed by GitHub
parent 78427e0797
commit 6b8d635943
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 746 additions and 1 deletions

View File

@ -0,0 +1,17 @@
import UserMenuNotificationItem from "discourse/components/user-menu/notification-item";
import I18n from "I18n";
export default class UserMenuBookmarkReminderNotificationItem extends UserMenuNotificationItem {
get linkTitle() {
if (this.notification.data.bookmark_name) {
return I18n.t("notifications.titles.bookmark_reminder_with_name", {
name: this.notification.data.bookmark_name,
});
}
return super.linkTitle;
}
get description() {
return super.description || this.notification.data.title;
}
}

View File

@ -0,0 +1,15 @@
import UserMenuNotificationItem from "discourse/components/user-menu/notification-item";
import I18n from "I18n";
export default class UserMenuCustomNotificationItem extends UserMenuNotificationItem {
get linkTitle() {
if (this.notification.data.title) {
return I18n.t(this.notification.data.title);
}
return super.linkTitle;
}
get icon() {
return `notification.${this.notification.data.message}`;
}
}

View File

@ -0,0 +1,36 @@
import UserMenuNotificationItem from "discourse/components/user-menu/notification-item";
import getURL from "discourse-common/lib/get-url";
import I18n from "I18n";
export default class UserMenuGrantedBadgeNotificationItem extends UserMenuNotificationItem {
get linkHref() {
const badgeId = this.notification.data.badge_id;
if (badgeId) {
let slug = this.notification.data.badge_slug;
if (!slug) {
slug = this.notification.data.badge_name
.replace(/[^A-Za-z0-9_]+/g, "-")
.toLowerCase();
}
let username = this.notification.data.username;
username = username ? `?username=${username.toLowerCase()}` : "";
return getURL(`/badges/${badgeId}/${slug}${username}`);
} else {
return super.url;
}
}
get label() {
return I18n.t("notifications.granted_badge", {
description: this.notification.data.badge_name,
});
}
get wrapLabel() {
return false;
}
get description() {
return null;
}
}

View File

@ -0,0 +1,11 @@
import UserMenuNotificationItem from "discourse/components/user-menu/notification-item";
export default class UserMenuGroupMentionedNotificationItem extends UserMenuNotificationItem {
get label() {
return `${this.username} @${this.notification.data.group_name}`;
}
get labelWrapperClasses() {
return "mention-group notify";
}
}

View File

@ -0,0 +1,23 @@
import UserMenuNotificationItem from "discourse/components/user-menu/notification-item";
import I18n from "I18n";
export default class UserMenuGroupMessageSummaryNotificationItem extends UserMenuNotificationItem {
get inboxCount() {
return this.notification.data.inbox_count;
}
get label() {
return I18n.t("notifications.group_message_summary", {
count: this.inboxCount,
group_name: this.notification.data.group_name,
});
}
get wrapLabel() {
return false;
}
get description() {
return null;
}
}

View File

@ -0,0 +1,13 @@
import UserMenuNotificationItem from "discourse/components/user-menu/notification-item";
import { userPath } from "discourse/lib/url";
import I18n from "I18n";
export default class UserMenuInviteeAcceptedNotificationItem extends UserMenuNotificationItem {
get linkHref() {
return userPath(this.notification.data.display_username);
}
get description() {
return I18n.t("notifications.invitee_accepted_your_invitation");
}
}

View File

@ -0,0 +1,21 @@
import UserMenuNotificationItem from "discourse/components/user-menu/notification-item";
import { userPath } from "discourse/lib/url";
import I18n from "I18n";
export default class UserMenuLikedConsolidatedNotificationItem extends UserMenuNotificationItem {
get linkHref() {
return userPath(
`${
this.notification.username || this.currentUser.username
}/notifications/likes-received?acting_username=${
this.notification.data.username
}`
);
}
get description() {
return I18n.t("notifications.liked_consolidated_description", {
count: this.notification.data.count,
});
}
}

View File

@ -0,0 +1,38 @@
import UserMenuNotificationItem from "discourse/components/user-menu/notification-item";
import { formatUsername } from "discourse/lib/utilities";
import I18n from "I18n";
export default class UserMenuLikedNotificationItem extends UserMenuNotificationItem {
get count() {
return this.notification.data.count;
}
get username2() {
return formatUsername(this.notification.data.username2);
}
get label() {
if (this.count === 2) {
return I18n.t("notifications.liked_by_2_users", {
username: this.username,
username2: this.username2,
});
} else if (this.count > 2) {
return I18n.t("notifications.liked_by_multiple_users", {
username: this.username,
username2: this.username2,
count: this.count - 2,
});
} else {
return super.label;
}
}
get labelWrapperClasses() {
if (this.count === 2) {
return "double-user";
} else if (this.count > 2) {
return "multi-user";
}
}
}

View File

@ -0,0 +1,23 @@
import UserMenuNotificationItem from "discourse/components/user-menu/notification-item";
import { groupPath } from "discourse/lib/url";
import I18n from "I18n";
export default class UserMenuMembershipRequestAcceptedNotificationItem extends UserMenuNotificationItem {
get linkHref() {
return groupPath(this.notification.data.group_name);
}
get label() {
return I18n.t("notifications.membership_request_accepted", {
group_name: this.notification.data.group_name,
});
}
get wrapLabel() {
return false;
}
get description() {
return null;
}
}

View File

@ -0,0 +1,26 @@
import UserMenuNotificationItem from "discourse/components/user-menu/notification-item";
import { userPath } from "discourse/lib/url";
import I18n from "I18n";
export default class UserMenuMembershipRequestConsolidatedNotificationItem extends UserMenuNotificationItem {
get linkHref() {
return userPath(
`${this.notification.username || this.currentUser.username}/messages`
);
}
get label() {
return I18n.t("notifications.membership_request_consolidated", {
group_name: this.notification.data.group_name,
count: this.notification.data.count,
});
}
get wrapLabel() {
return false;
}
get description() {
return null;
}
}

View File

@ -0,0 +1,8 @@
import UserMenuNotificationItem from "discourse/components/user-menu/notification-item";
import I18n from "I18n";
export default class UserMenuMovedPostNotificationItem extends UserMenuNotificationItem {
get label() {
return I18n.t("notifications.user_moved_post", { username: this.username });
}
}

View File

@ -0,0 +1,8 @@
import UserMenuNotificationItem from "discourse/components/user-menu/notification-item";
import I18n from "I18n";
export default class UserMenuWatchingFirstPostNotificationItem extends UserMenuNotificationItem {
get label() {
return I18n.t("notifications.watching_first_post_label");
}
}

View File

@ -2,7 +2,28 @@ import RestModel from "discourse/models/rest";
import { tracked } from "@glimmer/tracking";
const DEFAULT_ITEM = "user-menu/notification-item";
const _componentForType = {};
function defaultComponentForType() {
return {
bookmark_reminder: "user-menu/bookmark-reminder-notification-item",
custom: "user-menu/custom-notification-item",
granted_badge: "user-menu/granted-badge-notification-item",
group_mentioned: "user-menu/group-mentioned-notification-item",
group_message_summary: "user-menu/group-message-summary-notification-item",
invitee_accepted: "user-menu/invitee-accepted-notification-item",
liked: "user-menu/liked-notification-item",
liked_consolidated: "user-menu/liked-consolidated-notification-item",
membership_request_accepted:
"user-menu/membership-request-accepted-notification-item",
membership_request_consolidated:
"user-menu/membership-request-consolidated-notification-item",
moved_post: "user-menu/moved-post-notification-item",
watching_first_post: "user-menu/watching-first-post-notification-item",
};
}
let _componentForType = defaultComponentForType();
// TODO(osama): add plugin API
export default class Notification extends RestModel {
@tracked read;

View File

@ -0,0 +1,107 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import { deepMerge } from "discourse-common/lib/object";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import { render } from "@ember/test-helpers";
import Notification from "discourse/models/notification";
import { hbs } from "ember-cli-htmlbars";
import I18n from "I18n";
function getNotification(overrides = {}) {
return Notification.create(
deepMerge(
{
id: 11,
user_id: 1,
notification_type: NOTIFICATION_TYPES.bookmark_reminder,
read: false,
high_priority: true,
created_at: "2022-07-01T06:00:32.173Z",
post_number: 113,
topic_id: 449,
fancy_title: "This is fancy title <a>!",
slug: "this-is-fancy-title",
data: {
title: "this is unsafe bookmark title <a>!",
display_username: "osama",
bookmark_name: null,
bookmarkable_url: "/t/sometopic/3232",
},
},
overrides
)
);
}
module(
"Integration | Component | user-menu | bookmark-reminder-notification-item",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::BookmarkReminderNotificationItem @item={{this.notification}}/>`;
test("when the bookmark has a name", async function (assert) {
this.set(
"notification",
getNotification({ data: { bookmark_name: "MY BOOKMARK" } })
);
await render(template);
const link = query("li a");
assert.strictEqual(
link.title,
I18n.t("notifications.titles.bookmark_reminder_with_name", {
name: "MY BOOKMARK",
}),
"the notification has a title that includes the bookmark name"
);
});
test("when the bookmark doesn't have a name", async function (assert) {
this.set(
"notification",
getNotification({ data: { bookmark_name: null } })
);
await render(template);
const link = query("li a");
assert.strictEqual(
link.title,
I18n.t("notifications.titles.bookmark_reminder"),
"the notification has a generic title"
);
});
test("when the bookmark reminder doesn't originate from a topic and has a title", async function (assert) {
this.set(
"notification",
getNotification({
post_number: null,
topic_id: null,
fancy_title: null,
data: {
title: "this is unsafe bookmark title <a>!",
bookmarkable_url: "/chat/channel/33",
},
})
);
await render(template);
const description = query("li .notification-description");
assert.strictEqual(
description.textContent.trim(),
"this is unsafe bookmark title <a>!",
"the title is rendered safely as description"
);
});
test("when the bookmark reminder originates from a topic", async function (assert) {
this.set("notification", getNotification());
await render(template);
const description = query("li .notification-description");
assert.strictEqual(
description.textContent.trim(),
"This is fancy title <a>!",
"fancy_title is safe and rendered correctly"
);
});
}
);

View File

@ -0,0 +1,63 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import { deepMerge } from "discourse-common/lib/object";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import { render } from "@ember/test-helpers";
import Notification from "discourse/models/notification";
import { hbs } from "ember-cli-htmlbars";
import I18n from "I18n";
function getNotification(overrides = {}) {
return Notification.create(
deepMerge(
{
id: 11,
user_id: 1,
notification_type: NOTIFICATION_TYPES.granted_badge,
read: false,
high_priority: false,
created_at: "2022-07-01T06:00:32.173Z",
data: {
badge_id: 12,
badge_name: "Tough Guy <a>",
badge_slug: "tough-guy",
username: "ossa",
badge_title: false,
},
},
overrides
)
);
}
module(
"Integration | Component | user-menu | granted-badge-notification-item",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::GrantedBadgeNotificationItem @item={{this.notification}}/>`;
test("links to the badge page and filters by the username", async function (assert) {
this.set("notification", getNotification());
await render(template);
const link = query("li a");
assert.ok(link.href.endsWith("/badges/12/tough-guy?username=ossa"));
});
test("displays the right notification content", async function (assert) {
this.set("notification", getNotification());
await render(template);
const div = query("li div");
assert.strictEqual(
div.textContent.trim(),
I18n.t("notifications.granted_badge", {
description: "Tough Guy <a>",
}),
"label is rendered safely"
);
assert.ok(!exists("li .notification-label"));
assert.ok(!exists("li .notification-description"));
});
}
);

View File

@ -0,0 +1,68 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import { deepMerge } from "discourse-common/lib/object";
import { render } from "@ember/test-helpers";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import Notification from "discourse/models/notification";
import { hbs } from "ember-cli-htmlbars";
function getNotification(overrides = {}) {
return Notification.create(
deepMerge(
{
id: 11,
user_id: 1,
notification_type: NOTIFICATION_TYPES.group_mentioned,
read: false,
high_priority: false,
created_at: "2022-07-01T06:00:32.173Z",
post_number: 113,
topic_id: 449,
fancy_title: "This is fancy title &lt;a&gt;!",
slug: "this-is-fancy-title",
data: {
topic_title: "this is title before it becomes fancy <a>!",
original_post_id: 112,
original_post_type: 1,
original_username: "kolary",
display_username: "osama",
group_id: 333,
group_name: "hikers",
},
},
overrides
)
);
}
module(
"Integration | Component | user-menu | group-mentioned-notification-item",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::GroupMentionedNotificationItem @item={{this.notification}}/>`;
test("notification label displays the user who mentioned and the mentioned group", async function (assert) {
this.set("notification", getNotification());
await render(template);
const label = query("li .notification-label");
assert.strictEqual(label.textContent.trim(), "osama @hikers");
assert.ok(
label.classList.contains("mention-group"),
"label has mention-group class"
);
assert.ok(label.classList.contains("notify"), "label has notify class");
});
test("notification description displays the topic title", async function (assert) {
this.set("notification", getNotification());
await render(template);
const description = query("li .notification-description");
assert.strictEqual(
description.textContent.trim(),
"This is fancy title <a>!"
);
});
}
);

View File

@ -0,0 +1,55 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import { render } from "@ember/test-helpers";
import { deepMerge } from "discourse-common/lib/object";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import Notification from "discourse/models/notification";
import { hbs } from "ember-cli-htmlbars";
import I18n from "I18n";
function getNotification(overrides = {}) {
return Notification.create(
deepMerge(
{
id: 11,
user_id: 1,
notification_type: NOTIFICATION_TYPES.group_message_summary,
read: false,
high_priority: false,
created_at: "2022-07-01T06:00:32.173Z",
data: {
group_id: 321,
group_name: "drummers",
inbox_count: 13,
username: "drummers.boss",
},
},
overrides
)
);
}
module(
"Integration | Component | user-menu | group-message-summary-notification-item",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::GroupMessageSummaryNotificationItem @item={{this.notification}}/>`;
test("the notification displays the right content", async function (assert) {
this.set("notification", getNotification());
await render(template);
const notification = query("li");
assert.strictEqual(
notification.textContent.trim(),
I18n.t("notifications.group_message_summary", {
count: 13,
group_name: "drummers",
})
);
assert.ok(!exists("li .notification-label"));
assert.ok(!exists("li .notification-description"));
});
}
);

View File

@ -0,0 +1,71 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import { render } from "@ember/test-helpers";
import { deepMerge } from "discourse-common/lib/object";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import Notification from "discourse/models/notification";
import { hbs } from "ember-cli-htmlbars";
import I18n from "I18n";
function getNotification(overrides = {}) {
return Notification.create(
deepMerge(
{
id: 11,
user_id: 1,
notification_type: NOTIFICATION_TYPES.liked_consolidated,
read: false,
high_priority: false,
created_at: "2022-07-01T06:00:32.173Z",
data: {
topic_title: "this is some topic and it's irrelevant",
original_post_id: 3294,
original_post_type: 1,
original_username: "liker439",
display_username: "liker439",
username: "liker439",
count: 44,
},
},
overrides
)
);
}
module(
"Integration | Component | user-menu | liked-consolidated-notification-item",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::LikedConsolidatedNotificationItem @item={{this.notification}}/>`;
test("the notification links to the likes received notifications page of the user", async function (assert) {
this.set("notification", getNotification());
await render(template);
const link = query("li a");
assert.ok(
link.href.endsWith(
"/u/eviltrout/notifications/likes-received?acting_username=liker439"
)
);
});
test("the notification label displays the user who liked", async function (assert) {
this.set("notification", getNotification());
await render(template);
const label = query("li .notification-label");
assert.strictEqual(label.textContent.trim(), "liker439");
});
test("the notification description displays the number of likes", async function (assert) {
this.set("notification", getNotification());
await render(template);
const description = query("li .notification-description");
assert.strictEqual(
description.textContent.trim(),
I18n.t("notifications.liked_consolidated_description", { count: 44 })
);
});
}
);

View File

@ -0,0 +1,114 @@
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import { render } from "@ember/test-helpers";
import { deepMerge } from "discourse-common/lib/object";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import Notification from "discourse/models/notification";
import { hbs } from "ember-cli-htmlbars";
import I18n from "I18n";
function getNotification(overrides = {}) {
return Notification.create(
deepMerge(
{
id: 11,
user_id: 1,
notification_type: NOTIFICATION_TYPES.liked,
read: false,
high_priority: false,
created_at: "2022-07-01T06:00:32.173Z",
post_number: 113,
topic_id: 449,
fancy_title: "This is fancy title &lt;a&gt;!",
slug: "this-is-fancy-title",
data: {
topic_title: "this is title before it becomes fancy <a>!",
username: "osama",
display_username: "osama",
username2: "shrek",
count: 2,
},
},
overrides
)
);
}
module(
"Integration | Component | user-menu | liked-notification-item",
function (hooks) {
setupRenderingTest(hooks);
const template = hbs`<UserMenu::LikedNotificationItem @item={{this.notification}}/>`;
test("when the likes count is 2", async function (assert) {
this.set("notification", getNotification({ data: { count: 2 } }));
await render(template);
const label = query("li .notification-label");
const description = query("li .notification-description");
assert.strictEqual(
label.textContent.trim(),
"osama, shrek",
"the label displays both usernames comma-concatenated"
);
assert.ok(
label.classList.contains("double-user"),
"label has double-user class"
);
assert.strictEqual(
description.textContent.trim(),
"This is fancy title <a>!",
"the description displays the topic title"
);
});
test("when the likes count is more than 2", async function (assert) {
this.set("notification", getNotification({ data: { count: 3 } }));
await render(template);
const label = query("li .notification-label");
const description = query("li .notification-description");
assert.strictEqual(
label.textContent.trim(),
I18n.t("notifications.liked_by_multiple_users", {
username: "osama",
username2: "shrek",
count: 1,
}),
"the label displays the first 2 usernames comma-concatenated with the count of remaining users"
);
assert.ok(
label.classList.contains("multi-user"),
"label has multi-user class"
);
assert.strictEqual(
description.textContent.trim(),
"This is fancy title <a>!",
"the description displays the topic title"
);
});
test("when the likes count is 1", async function (assert) {
this.set(
"notification",
getNotification({ data: { count: 1, username2: null } })
);
await render(template);
const label = query("li .notification-label");
const description = query("li .notification-description");
assert.strictEqual(
label.textContent.trim(),
"osama",
"the label displays the username"
);
assert.strictEqual(
description.textContent.trim(),
"This is fancy title <a>!",
"the description displays the topic title"
);
});
}
);

View File

@ -2322,6 +2322,8 @@ en:
empty: "No notifications found."
post_approved: "Your post was approved"
reviewable_items: "items requiring review"
watching_first_post_label: "New Topic"
user_moved_post: "%{username} moved"
mentioned: "<span>%{username}</span> %{description}"
group_mentioned: "<span>%{username}</span> %{description}"
quoted: "<span>%{username}</span> %{description}"
@ -2334,6 +2336,10 @@ en:
liked_many:
one: "<span class='multi-user'>%{username}, %{username2} and %{count} other</span> %{description}"
other: "<span class='multi-user'>%{username}, %{username2} and %{count} others</span> %{description}"
liked_by_2_users: "%{username}, %{username2}"
liked_by_multiple_users:
one: "%{username}, %{username2} and %{count} other"
other: "%{username}, %{username2} and %{count} others"
liked_consolidated_description:
one: "liked %{count} of your posts"
other: "liked %{count} of your posts"
@ -2342,6 +2348,7 @@ en:
invited_to_private_message: "<p><span>%{username}</span> %{description}"
invited_to_topic: "<span>%{username}</span> %{description}"
invitee_accepted: "<span>%{username}</span> accepted your invitation"
invitee_accepted_your_invitation: "accepted your invitation"
moved_post: "<span>%{username}</span> moved %{description}"
linked: "<span>%{username}</span> %{description}"
granted_badge: "Earned '%{description}'"