DEV: Render glimmer notification items for user notification list (#24802)

This removes the widget notifications list and renders the glimmer user menu notification items instead.
This commit is contained in:
Mark VanLandingham 2023-12-11 11:04:43 -06:00 committed by GitHub
parent 4904c2f11b
commit 223e413a6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 455 additions and 1005 deletions

View File

@ -0,0 +1,32 @@
import Component from "@glimmer/component";
import { longDate, relativeAge } from "discourse/lib/formatter";
export default class RelativeDate extends Component {
get datetime() {
if (this.memoizedDatetime) {
return this.memoizedDatetime;
}
this.memoizedDatetime = new Date(this.args.date);
return this.memoizedDatetime;
}
get title() {
return longDate(this.datetime);
}
get time() {
return this.datetime.getTime();
}
<template>
<span
class="relative-date"
title={{this.title}}
data-time={{this.time}}
data-format="tiny"
>
{{relativeAge this.datetime}}
</span>
</template>
}

View File

@ -24,5 +24,11 @@
</span>
{{/if}}
</div>
<PluginOutlet @name="menu-item-end" @outletArgs={{hash item=this}}>
{{#if this.endComponent}}
<this.endComponent />
{{/if}}
</PluginOutlet>
</a>
</li>

View File

@ -57,6 +57,10 @@ export default class UserMenuItem extends Component {
return this.#item.iconComponentArgs;
}
get endComponent() {
return this.#item.endComponent;
}
get #item() {
return this.args.item;
}

View File

@ -1,23 +0,0 @@
import MountWidget from "discourse/components/mount-widget";
import { observes } from "discourse-common/utils/decorators";
export default MountWidget.extend({
widget: "user-notifications-large",
notifications: null,
args: null,
init() {
this._super(...arguments);
this.args = { notifications: this.notifications };
},
@observes("notifications.length", "notifications.@each.read")
_triggerRefresh() {
this.set("args", {
notifications: this.notifications,
});
this.queueRerender();
},
});

View File

@ -0,0 +1,107 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import DismissNotificationConfirmationModal from "discourse/components/modal/dismiss-notification-confirmation";
import RelativeDate from "discourse/components/relative-date";
import { ajax } from "discourse/lib/ajax";
import UserMenuNotificationItem from "discourse/lib/user-menu/notification-item";
import getURL from "discourse-common/lib/get-url";
import { iconHTML } from "discourse-common/lib/icon-library";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "discourse-i18n";
export default class UserNotificationsController extends Controller {
@service modal;
@service appEvents;
@service currentUser;
@service site;
@service siteSettings;
queryParams = ["filter"];
filter = "all";
get listContainerClassNames() {
return `user-notifications-list ${
this.siteSettings.show_user_menu_avatars ? "show-avatars" : ""
}`;
}
@discourseComputed("filter")
isFiltered() {
return this.filter && this.filter !== "all";
}
@discourseComputed("model.content.@each")
items() {
return this.model.map((notification) => {
const props = {
appEvents: this.appEvents,
currentUser: this.currentUser,
siteSettings: this.siteSettings,
site: this.site,
notification,
endComponent: <template>
<RelativeDate @date={{notification.created_at}} />
</template>,
};
return new UserMenuNotificationItem(props);
});
}
@discourseComputed("model.content.@each.read")
allNotificationsRead() {
return !this.get("model.content").some(
(notification) => !notification.get("read")
);
}
@discourseComputed("isFiltered", "model.content.length")
doesNotHaveNotifications(isFiltered, contentLength) {
return !isFiltered && contentLength === 0;
}
@discourseComputed("isFiltered", "model.content.length")
nothingFound(isFiltered, contentLength) {
return isFiltered && contentLength === 0;
}
@discourseComputed()
emptyStateBody() {
return htmlSafe(
I18n.t("user.no_notifications_page_body", {
preferencesUrl: getURL("/my/preferences/notifications"),
icon: iconHTML("bell"),
})
);
}
async markRead() {
await ajax("/notifications/mark-read", { type: "PUT" });
this.model.forEach((notification) => notification.set("read", true));
}
@action
async resetNew() {
if (this.currentUser.unread_high_priority_notifications > 0) {
this.modal.show(DismissNotificationConfirmationModal, {
model: {
confirmationMessage: I18n.t(
"notifications.dismiss_confirmation.body.default",
{
count: this.currentUser.unread_high_priority_notifications,
}
),
dismissNotifications: () => this.markRead(),
},
});
} else {
this.markRead();
}
}
@action
loadMore() {
this.model.loadMore();
}
}

View File

@ -1,76 +0,0 @@
import Controller from "@ember/controller";
import { inject as service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import DismissNotificationConfirmationModal from "discourse/components/modal/dismiss-notification-confirmation";
import { ajax } from "discourse/lib/ajax";
import getURL from "discourse-common/lib/get-url";
import { iconHTML } from "discourse-common/lib/icon-library";
import discourseComputed from "discourse-common/utils/decorators";
import I18n from "discourse-i18n";
export default Controller.extend({
modal: service(),
queryParams: ["filter"],
filter: "all",
@discourseComputed("filter")
isFiltered() {
return this.filter && this.filter !== "all";
},
@discourseComputed("model.content.@each.read")
allNotificationsRead() {
return !this.get("model.content").some(
(notification) => !notification.get("read")
);
},
@discourseComputed("isFiltered", "model.content.length")
doesNotHaveNotifications(isFiltered, contentLength) {
return !isFiltered && contentLength === 0;
},
@discourseComputed("isFiltered", "model.content.length")
nothingFound(isFiltered, contentLength) {
return isFiltered && contentLength === 0;
},
@discourseComputed()
emptyStateBody() {
return htmlSafe(
I18n.t("user.no_notifications_page_body", {
preferencesUrl: getURL("/my/preferences/notifications"),
icon: iconHTML("bell"),
})
);
},
async markRead() {
await ajax("/notifications/mark-read", { type: "PUT" });
this.model.forEach((n) => n.set("read", true));
},
actions: {
async resetNew() {
if (this.currentUser.unread_high_priority_notifications > 0) {
this.modal.show(DismissNotificationConfirmationModal, {
model: {
confirmationMessage: I18n.t(
"notifications.dismiss_confirmation.body.default",
{
count: this.currentUser.unread_high_priority_notifications,
}
),
dismissNotifications: () => this.markRead(),
},
});
} else {
this.markRead();
}
},
loadMore() {
this.model.loadMore();
},
},
});

View File

@ -5,11 +5,19 @@ import UserMenuBaseItem from "discourse/lib/user-menu/base-item";
import getURL from "discourse-common/lib/get-url";
export default class UserMenuNotificationItem extends UserMenuBaseItem {
constructor({ notification, appEvents, currentUser, siteSettings, site }) {
constructor({
notification,
endComponent,
appEvents,
currentUser,
siteSettings,
site,
}) {
super(...arguments);
this.appEvents = appEvents;
this.notification = notification;
this.currentUser = currentUser;
this.endComponent = endComponent;
this.notification = notification;
this.siteSettings = siteSettings;
this.site = site;

View File

@ -22,7 +22,11 @@
{{#if this.nothingFound}}
<div class="alert alert-info">{{i18n "notifications.empty"}}</div>
{{else}}
<UserNotificationsLarge @notifications={{this.model}} />
<ConditionalLoadingSpinner @condition={{this.loading}} />
<div class={{this.listContainerClassNames}}>
{{#each this.items as |item|}}
<UserMenu::MenuItem @item={{item}} />
{{/each}}
<ConditionalLoadingSpinner @condition={{this.loading}} />
</div>
{{/if}}
{{/if}}

View File

@ -1,19 +0,0 @@
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { createWidgetFrom } from "discourse/widgets/widget";
import getURL from "discourse-common/lib/get-url";
import { iconNode } from "discourse-common/lib/icon-library";
import I18n from "discourse-i18n";
createWidgetFrom(DefaultNotificationItem, "admin-problems-notification-item", {
text() {
return I18n.t("notifications.admin_problems");
},
url() {
return getURL("/admin");
},
icon() {
return iconNode("gift");
},
});

View File

@ -1,34 +0,0 @@
import { formatUsername } from "discourse/lib/utilities";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { createWidgetFrom } from "discourse/widgets/widget";
import I18n from "discourse-i18n";
createWidgetFrom(
DefaultNotificationItem,
"bookmark-reminder-notification-item",
{
text(notificationName, data) {
const username = formatUsername(data.display_username);
const description = this.description(data);
return I18n.t("notifications.bookmark_reminder", {
description,
username,
});
},
notificationTitle(notificationName, data) {
if (notificationName) {
if (data.bookmark_name) {
return I18n.t(`notifications.titles.${notificationName}_with_name`, {
name: data.bookmark_name,
});
} else {
return I18n.t(`notifications.titles.${notificationName}`);
}
} else {
return "";
}
},
}
);

View File

@ -1,22 +0,0 @@
import { formatUsername } from "discourse/lib/utilities";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { createWidgetFrom } from "discourse/widgets/widget";
import { iconNode } from "discourse-common/lib/icon-library";
import I18n from "discourse-i18n";
createWidgetFrom(DefaultNotificationItem, "custom-notification-item", {
notificationTitle(notificationName, data) {
return data.title ? I18n.t(data.title) : "";
},
text(notificationName, data) {
const username = formatUsername(data.display_username);
const description = this.description(data);
return I18n.t(data.message, { description, username });
},
icon(notificationName, data) {
return iconNode(`notification.${data.message}`);
},
});

View File

@ -1,19 +0,0 @@
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { createWidgetFrom } from "discourse/widgets/widget";
import I18n from "discourse-i18n";
createWidgetFrom(
DefaultNotificationItem,
"group-message-summary-notification-item",
{
text(notificationName, data) {
const count = data.inbox_count;
const group_name = data.group_name;
return I18n.t("notifications.group_message_summary", {
count,
group_name,
});
},
}
);

View File

@ -1,13 +0,0 @@
import { userPath } from "discourse/lib/url";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { createWidgetFrom } from "discourse/widgets/widget";
createWidgetFrom(
DefaultNotificationItem,
"invitee-accepted-notification-item",
{
url(data) {
return userPath(data.display_username);
},
}
);

View File

@ -1,31 +0,0 @@
import { isEmpty } from "@ember/utils";
import { userPath } from "discourse/lib/url";
import { escapeExpression } from "discourse/lib/utilities";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { createWidgetFrom } from "discourse/widgets/widget";
import I18n from "discourse-i18n";
createWidgetFrom(
DefaultNotificationItem,
"liked-consolidated-notification-item",
{
url(data) {
return userPath(
`${
this.attrs.username || this.currentUser.username
}/notifications/likes-received?acting_username=${data.display_username}`
);
},
description(data) {
const description = I18n.t(
"notifications.liked_consolidated_description",
{
count: parseInt(data.count, 10),
}
);
return isEmpty(description) ? "" : escapeExpression(description);
},
}
);

View File

@ -1,32 +0,0 @@
import { formatUsername } from "discourse/lib/utilities";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { createWidgetFrom } from "discourse/widgets/widget";
import I18n from "discourse-i18n";
createWidgetFrom(DefaultNotificationItem, "liked-notification-item", {
text(notificationName, data) {
const username = formatUsername(data.display_username);
const description = this.description(data);
if (data.count > 1) {
const count = data.count - 1;
const username2 = formatUsername(data.username2);
if (count === 0) {
return I18n.t("notifications.liked_2", {
description,
username: `<span class="multi-username">${username}</span>`,
username2: `<span class="multi-username">${username2}</span>`,
});
} else {
return I18n.t("notifications.liked_many", {
description,
username: `<span class="multi-username">${username}</span>`,
count,
});
}
}
return I18n.t("notifications.liked", { description, username });
},
});

View File

@ -1,20 +0,0 @@
import { groupPath } from "discourse/lib/url";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { createWidgetFrom } from "discourse/widgets/widget";
import I18n from "discourse-i18n";
createWidgetFrom(
DefaultNotificationItem,
"membership-request-accepted-notification-item",
{
url(data) {
return groupPath(data.group_name);
},
text(notificationName, data) {
return I18n.t(`notifications.${notificationName}`, {
group_name: data.group_name,
});
},
}
);

View File

@ -1,23 +0,0 @@
import { userPath } from "discourse/lib/url";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { createWidgetFrom } from "discourse/widgets/widget";
import I18n from "discourse-i18n";
createWidgetFrom(
DefaultNotificationItem,
"membership-request-consolidated-notification-item",
{
url() {
return userPath(
`${this.attrs.username || this.currentUser.username}/messages`
);
},
text(notificationName, data) {
return I18n.t("notifications.membership_request_consolidated", {
group_name: data.group_name,
count: parseInt(data.count, 10),
});
},
}
);

View File

@ -1,19 +0,0 @@
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import { createWidgetFrom } from "discourse/widgets/widget";
import getURL from "discourse-common/lib/get-url";
import { iconNode } from "discourse-common/lib/icon-library";
import I18n from "discourse-i18n";
createWidgetFrom(DefaultNotificationItem, "new-features-notification-item", {
text() {
return I18n.t("notifications.new_features");
},
url() {
return getURL("/admin");
},
icon() {
return iconNode("gift");
},
});

View File

@ -1,48 +0,0 @@
import { dasherize } from "@ember/string";
import { h } from "virtual-dom";
import { dateNode } from "discourse/helpers/node";
import { createWidget } from "discourse/widgets/widget";
createWidget("large-notification-item", {
tagName: "li",
buildClasses(attrs) {
const result = ["item", "notification", "large-notification"];
if (!attrs.get("read")) {
result.push("unread");
}
return result;
},
html(attrs) {
const notificationName =
this.site.notificationLookup[attrs.notification_type];
return [
this.attach(
`${dasherize(notificationName)}-notification-item`,
attrs,
{},
{
fallbackWidgetName: "default-notification-item",
tagName: "div",
}
),
h("span.time", dateNode(attrs.created_at)),
];
},
});
export default createWidget("user-notifications-large", {
tagName: "ul.notifications.large-notifications",
html(attrs) {
const notifications = attrs.notifications;
const username = notifications.findArgs.username;
return notifications.map((n) => {
n.username = username;
return this.attach("large-notification-item", n);
});
},
});

View File

@ -1,34 +0,0 @@
import { visit } from "@ember/test-helpers";
import { test } from "qunit";
import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers";
import selectKit from "discourse/tests/helpers/select-kit-helper";
acceptance("Notifications filter", function (needs) {
needs.user();
test("Notifications filter true", async function (assert) {
await visit("/u/eviltrout/notifications");
assert.ok(exists(".large-notification"));
});
test("Notifications filter read", async function (assert) {
await visit("/u/eviltrout/notifications");
const dropdown = selectKit(".notifications-filter");
await dropdown.expand();
await dropdown.selectRowByValue("read");
assert.ok(exists(".large-notification"));
});
test("Notifications filter unread", async function (assert) {
await visit("/u/eviltrout/notifications");
const dropdown = selectKit(".notifications-filter");
await dropdown.expand();
await dropdown.selectRowByValue("unread");
assert.ok(exists(".large-notification"));
});
});

View File

@ -78,12 +78,10 @@ acceptance("User Routes", function (needs) {
"has the body class"
);
const $links = queryAll(".item.notification a");
const $links = queryAll(".notification a");
assert.ok(
$links[2].href.includes(
"/u/eviltrout/notifications/likes-received?acting_username=aquaman"
)
$links[2].href.includes("/u/eviltrout/notifications/likes-received")
);
updateCurrentUser({ moderator: true, admin: false });

View File

@ -163,40 +163,6 @@
justify-content: space-between;
box-sizing: border-box;
min-width: 0; // makes sure menu tabs don't go off screen
.double-user,
.multi-user {
white-space: unset;
}
.item-label {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--primary);
}
li {
background-color: var(--secondary);
&.unread,
&.pending {
background-color: var(--tertiary-low);
}
&:hover {
background-color: var(--d-hover);
outline: none;
}
&:focus-within {
background: var(--d-hover);
a {
// we don't need the link focus because we're styling the parent
outline: 0;
}
}
}
}
#quick-access-profile {
@ -380,185 +346,218 @@
}
}
.user-menu {
.quick-access-panel {
width: 100%;
// Panel / user-notification-list styles. **not** menu panel sizing styles
.user-menu .quick-access-panel,
.user-notifications-list {
width: 100%;
display: flex;
flex-direction: column;
min-height: 0;
max-height: 100%;
border-top: 1px solid var(--primary-low);
padding-top: 0.75em;
margin-top: -1px;
&:focus {
outline: none;
}
.double-user,
.multi-user {
white-space: unset;
}
.item-label {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--primary);
}
h3 {
padding: 0 0.4em;
font-weight: bold;
margin: 0.5em 0;
}
.d-icon,
&:hover .d-icon {
color: var(--primary-medium);
}
.icon {
color: var(--primary-high);
}
.btn-primary {
.d-icon {
color: var(--secondary);
}
}
ul {
display: flex;
flex-direction: column;
min-height: 0;
flex-flow: column wrap;
overflow: hidden;
max-height: 100%;
border-top: 1px solid var(--primary-low);
padding-top: 0.75em;
margin-top: -1px;
&:focus {
}
li {
background-color: var(--secondary);
box-sizing: border-box;
list-style-type: none;
&.unread,
&.pending {
background-color: var(--tertiary-low);
}
&:hover {
background-color: var(--d-hover);
outline: none;
}
h3 {
padding: 0 0.4em;
font-weight: bold;
margin: 0.5em 0;
}
.d-icon,
&:hover .d-icon {
color: var(--primary-medium);
}
.icon {
color: var(--primary-high);
}
.btn-primary {
.d-icon {
color: var(--secondary);
&:focus-within {
background: var(--d-hover);
a {
// we don't need the link focus because we're styling the parent
outline: 0;
}
}
ul {
// This is until other languages remove the HTML from within
// notifications. It can then be removed
div .fa {
display: none;
}
span.double-user,
// e.g., "username, username2"
span.multi-user
// e.g., "username and n others"
{
display: inline;
max-width: 100%;
align-items: baseline;
white-space: nowrap;
}
span.multi-user
// e.g., "username, username2, and n others"
{
span.multi-username:nth-of-type(2) {
// margin between username2 and "and n others"
margin-right: 0.25em;
}
}
// truncate when usernames are very long
span.multi-username {
@include ellipsis;
flex: 0 1 auto;
min-width: 1.2em;
max-width: 10em;
&:nth-of-type(2) {
// margin for comma between username and username2
margin-left: 0.25em;
}
}
&:hover {
background-color: var(--d-hover);
outline: none;
}
&:focus-within {
background: var(--d-hover);
a {
// we don't need the link focus because we're styling the parent
outline: 0;
}
.btn-flat:focus {
// undo default btn-flat style
background: transparent;
}
}
a,
.profile-tab-btn {
display: flex;
flex-flow: column wrap;
margin: 0.25em;
padding: 0em 0.25em;
}
button {
padding: 0.25em 0.5em;
}
a,
button {
> div {
overflow: hidden; // clears the text from wrapping below icons
overflow-wrap: anywhere;
@supports not (overflow-wrap: anywhere) {
word-break: break-word;
}
// Truncate items with more than 2 lines.
@include line-clamp(2);
}
}
p {
margin: 0;
overflow: hidden;
max-height: 100%;
}
li {
background-color: var(--d-selected);
box-sizing: border-box;
list-style-type: none;
// This is until other languages remove the HTML from within
// notifications. It can then be removed
div .fa {
display: none;
}
span.double-user,
// e.g., "username, username2"
span.multi-user
// e.g., "username and n others"
{
display: inline;
max-width: 100%;
align-items: baseline;
white-space: nowrap;
}
span.multi-user
// e.g., "username, username2, and n others"
{
span.multi-username:nth-of-type(2) {
// margin between username2 and "and n others"
margin-right: 0.25em;
}
}
// truncate when usernames are very long
span.multi-username {
@include ellipsis;
flex: 0 1 auto;
min-width: 1.2em;
max-width: 10em;
&:nth-of-type(2) {
// margin for comma between username and username2
margin-left: 0.25em;
}
}
&:hover {
background-color: var(--d-hover);
outline: none;
}
&:focus-within {
background: var(--d-hover);
a {
// we don't need the link focus because we're styling the parent
outline: 0;
}
.btn-flat:focus {
// undo default btn-flat style
background: transparent;
}
}
a,
.profile-tab-btn {
display: flex;
margin: 0.25em;
padding: 0em 0.25em;
}
button {
padding: 0.25em 0.5em;
}
a,
button {
> div {
overflow: hidden; // clears the text from wrapping below icons
overflow-wrap: anywhere;
@supports not (overflow-wrap: anywhere) {
word-break: break-word;
}
// Truncate items with more than 2 lines.
@include line-clamp(2);
}
}
p {
margin: 0;
overflow: hidden;
}
}
li:not(.show-all) {
padding: 0;
align-self: flex-start;
width: 100%;
.d-icon {
padding-top: 0.2em;
margin-right: 0.5em;
}
}
.is-warning {
.d-icon-envelope {
color: var(--danger);
}
}
.read {
background-color: var(--secondary);
}
.none {
padding-top: 5px;
}
.spinner-container {
min-height: 2em;
}
.spinner {
width: 20px;
height: 20px;
border-width: 2px;
margin: 0 auto;
}
.show-all a {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
min-height: 30px;
color: var(--primary-med-or-secondary-high);
background: var(--blend-primary-secondary-5);
&:hover {
color: var(--primary);
background: var(--primary-low);
}
}
/* as a big ol' click target, don't let text inside be selected */
@include unselectable;
}
li:not(.show-all) {
padding: 0;
align-self: flex-start;
width: 100%;
.d-icon {
padding-top: 0.2em;
margin-right: 0.5em;
}
}
.is-warning {
.d-icon-envelope {
color: var(--danger);
}
}
.read {
background-color: var(--secondary);
}
.none {
padding-top: 5px;
}
.spinner-container {
min-height: 2em;
}
.spinner {
width: 20px;
height: 20px;
border-width: 2px;
margin: 0 auto;
}
.show-all a {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
min-height: 30px;
color: var(--primary-med-or-secondary-high);
background: var(--blend-primary-secondary-5);
&:hover {
color: var(--primary);
background: var(--primary-low);
}
}
/* as a big ol' click target, don't let text inside be selected */
@include unselectable;
}
.user-menu.show-avatars {
// Styles to have user avatar positioned and sized correctly
.user-menu.show-avatars,
.user-notifications-list.show-avatars {
li {
a {
.icon-avatar {

View File

@ -765,38 +765,6 @@
}
}
.large-notifications {
margin: 0;
}
.large-notification {
display: flex;
align-items: center;
a {
display: flex;
align-items: center;
.d-icon {
margin-right: 0.5em;
}
span:first-child {
color: var(--primary);
}
// Can remove this once other languages have removed html from i18n values
div {
.fa {
display: none;
}
p {
margin: 0;
}
}
}
}
.second-factor {
.second-factor-item {
width: 100%;

View File

@ -46,7 +46,7 @@
color: var(--primary);
}
.time,
.relative-date,
.delete-info,
.draft-type {
line-height: var(--line-height-small);
@ -69,9 +69,32 @@
}
}
.notification .time {
margin-left: auto;
float: none;
.user-notifications-list {
padding-top: 0;
li.notification {
padding: 0.25em 0;
border-bottom: 1px solid var(--primary-low);
a {
align-items: center;
}
.relative-date {
margin-left: auto;
padding-top: 0;
float: none;
}
}
&:not(.show-avatars) {
li.notification {
padding: 0.75em 0;
.d-icon {
padding-top: 0;
font-size: var(--font-up-2);
}
}
}
}
.expand-item,
@ -102,27 +125,6 @@
float: right;
}
.notification {
li {
display: inline-block;
}
p {
display: inline-block;
span:first-child {
color: var(--primary);
}
}
// common/base/header.scss
.fa,
.icon {
color: var(--primary-med-or-secondary-med);
font-size: var(--font-up-4);
}
}
.excerpt {
margin: 1em 0 0 0;
font-size: var(--font-0);

View File

@ -2,4 +2,3 @@
@import "sidebar/edit-navigation-menu/tags-modal";
@import "user-card";
@import "user-info";
@import "user-stream-item";

View File

@ -1,8 +0,0 @@
// Desktop styles for "user-stream-item" component
.user-stream {
.notification {
&.unread {
background-color: var(--tertiary-low);
}
}
}

View File

@ -4,12 +4,6 @@
vertical-align: middle;
}
.notification {
&.unread {
background-color: var(--tertiary-low);
}
}
.group-member-info {
.name {
vertical-align: inherit;

View File

@ -1,50 +0,0 @@
import { h } from "virtual-dom";
import { formatUsername } from "discourse/lib/utilities";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import RawHtml from "discourse/widgets/raw-html";
import { createWidgetFrom } from "discourse/widgets/widget";
import { iconNode } from "discourse-common/lib/icon-library";
import I18n from "discourse-i18n";
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
createWidgetFrom(DefaultNotificationItem, "chat-invitation-notification-item", {
services: ["chat", "router"],
text(data) {
const username = formatUsername(data.invited_by_username);
return I18n.t("notifications.chat_invitation_html", { username });
},
html(attrs) {
const notificationType = attrs.notification_type;
const lookup = this.site.get("notificationLookup");
const notificationName = lookup[notificationType];
const { data } = attrs;
const text = this.text(data);
const title = this.notificationTitle(notificationName, data);
const html = new RawHtml({ html: `<div>${text}</div>` });
const contents = [iconNode("link"), html];
const href = this.url(data);
return h(
"a",
{ attributes: { title, href, "data-auto-route": true } },
contents
);
},
url(data) {
const slug = slugifyChannel({
title: data.chat_channel_title,
slug: data.chat_channel_slug,
});
let url = `/chat/c/${slug || "-"}/${data.chat_channel_id}`;
if (data.chat_message_id) {
url += `/${data.chat_message_id}`;
}
return url;
},
});

View File

@ -1,72 +0,0 @@
import { h } from "virtual-dom";
import { formatUsername } from "discourse/lib/utilities";
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
import RawHtml from "discourse/widgets/raw-html";
import { createWidgetFrom } from "discourse/widgets/widget";
import { iconNode } from "discourse-common/lib/icon-library";
import I18n from "discourse-i18n";
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
const chatNotificationItem = {
services: ["chat", "router"],
text(notificationName, data) {
const username = formatUsername(data.mentioned_by_username);
const identifier = data.identifier ? `@${data.identifier}` : null;
const i18nPrefix = data.is_direct_message_channel
? "notifications.popup.direct_message_chat_mention"
: "notifications.popup.chat_mention";
const i18nSuffix = identifier ? "other_html" : "direct_html";
return I18n.t(`${i18nPrefix}.${i18nSuffix}`, {
username,
identifier,
channel: data.chat_channel_title,
});
},
html(attrs) {
const notificationType = attrs.notification_type;
const lookup = this.site.get("notificationLookup");
const notificationName = lookup[notificationType];
const { data } = attrs;
const title = this.notificationTitle(notificationName, data);
const text = this.text(notificationName, data);
const html = new RawHtml({ html: `<div>${text}</div>` });
const contents = [iconNode("d-chat"), html];
const href = this.url(data);
return h(
"a",
{ attributes: { title, href, "data-auto-route": true } },
contents
);
},
url(data) {
const slug = slugifyChannel({
title: data.chat_channel_title,
slug: data.chat_channel_slug,
});
let notificationRoute = `/chat/c/${slug || "-"}/${data.chat_channel_id}`;
if (data.chat_thread_id) {
notificationRoute += `/t/${data.chat_thread_id}`;
} else {
notificationRoute += `/${data.chat_message_id}`;
}
return notificationRoute;
},
};
createWidgetFrom(
DefaultNotificationItem,
"chat-mention-notification-item",
chatNotificationItem
);
createWidgetFrom(
DefaultNotificationItem,
"chat-group-mention-notification-item",
chatNotificationItem
);

View File

@ -1,52 +0,0 @@
import { render } from "@ember/test-helpers";
import hbs from "htmlbars-inline-precompile";
import { module, test } from "qunit";
import Notification from "discourse/models/notification";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import { deepMerge } from "discourse-common/lib/object";
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
function getNotification(overrides = {}) {
return Notification.create(
deepMerge(
{
id: 11,
notification_type: NOTIFICATION_TYPES.chat_invitation,
read: false,
data: {
message: "chat.invitation_notification",
invited_by_username: "eviltrout",
chat_channel_id: 9,
chat_message_id: 2,
chat_channel_title: "Site",
},
},
overrides
)
);
}
module(
"Discourse Chat | Widget | chat-invitation-notification-item",
function (hooks) {
setupRenderingTest(hooks);
test("notification url", async function (assert) {
this.set("args", getNotification());
await render(
hbs`<MountWidget @widget="chat-invitation-notification-item" @args={{this.args}} />`
);
const data = this.args.data;
assert.strictEqual(
query(".chat-invitation a").getAttribute("href"),
`/chat/c/${slugifyChannel({
title: data.chat_channel_title,
})}/${data.chat_channel_id}/${data.chat_message_id}`
);
});
}
);

View File

@ -1,139 +0,0 @@
import { render } from "@ember/test-helpers";
import hbs from "htmlbars-inline-precompile";
import { module, test } from "qunit";
import Notification from "discourse/models/notification";
import { NOTIFICATION_TYPES } from "discourse/tests/fixtures/concerns/notification-types";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { query } from "discourse/tests/helpers/qunit-helpers";
import { deepMerge } from "discourse-common/lib/object";
import I18n from "discourse-i18n";
import slugifyChannel from "discourse/plugins/chat/discourse/lib/slugify-channel";
function getNotification(overrides = {}) {
return Notification.create(
deepMerge(
{
id: 11,
notification_type: NOTIFICATION_TYPES.chat_invitation,
read: false,
data: {
message: "chat.mention_notification",
mentioned_by_username: "eviltrout",
chat_channel_id: 9,
chat_message_id: 2,
chat_channel_title: "Site",
},
},
overrides
)
);
}
module(
"Discourse Chat | Widget | chat-mention-notification-item",
function (hooks) {
setupRenderingTest(hooks);
test("generated link", async function (assert) {
this.set("args", getNotification());
const data = this.args.data;
await render(
hbs`<MountWidget @widget="chat-mention-notification-item" @args={{this.args}} />`
);
assert.strictEqual(
query(".chat-invitation a div").innerHTML.trim(),
I18n.t("notifications.popup.chat_mention.direct_html", {
username: "eviltrout",
identifier: null,
channel: "Site",
})
);
assert.strictEqual(
query(".chat-invitation a").getAttribute("href"),
`/chat/c/${slugifyChannel({
title: data.chat_channel_title,
})}/${data.chat_channel_id}/${data.chat_message_id}`
);
});
}
);
module(
"Discourse Chat | Widget | chat-group-mention-notification-item",
function (hooks) {
setupRenderingTest(hooks);
test("generated link", async function (assert) {
this.set(
"args",
getNotification({
data: {
mentioned_by_username: "eviltrout",
identifier: "moderators",
},
})
);
const data = this.args.data;
await render(
hbs`<MountWidget @widget="chat-group-mention-notification-item" @args={{this.args}} />`
);
assert.strictEqual(
query(".chat-invitation a div").innerHTML.trim(),
I18n.t("notifications.popup.chat_mention.other_html", {
username: "eviltrout",
identifier: "@moderators",
channel: "Site",
})
);
assert.strictEqual(
query(".chat-invitation a").getAttribute("href"),
`/chat/c/${slugifyChannel({
title: data.chat_channel_title,
})}/${data.chat_channel_id}/${data.chat_message_id}`
);
});
}
);
module(
"Discourse Chat | Widget | chat-group-mention-notification-item (@all)",
function (hooks) {
setupRenderingTest(hooks);
test("generated link", async function (assert) {
this.set(
"args",
getNotification({
data: {
mentioned_by_username: "eviltrout",
identifier: "all",
},
})
);
const data = this.args.data;
await render(
hbs`<MountWidget @widget="chat-group-mention-notification-item" @args={{this.args}} />`
);
assert.strictEqual(
query(".chat-invitation a div").innerHTML.trim(),
I18n.t("notifications.popup.chat_mention.other_html", {
username: "eviltrout",
identifier: "@all",
channel: "Site",
})
);
assert.strictEqual(
query(".chat-invitation a").getAttribute("href"),
`/chat/c/${slugifyChannel({
title: data.chat_channel_title,
})}/${data.chat_channel_id}/${data.chat_message_id}`
);
});
}
);

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
module PageObjects
module Pages
class UserNotifications < PageObjects::Pages::Base
def visit(user)
page.visit("/u/#{user.username}/notifications")
self
end
def filter_dropdown
PageObjects::Components::SelectKit.new(".notifications-filter")
end
def set_filter_value(value)
filter_dropdown.select_row_by_value(value)
end
def has_selected_filter_value?(value)
expect(filter_dropdown).to have_selected_value(value)
end
def has_notification?(notification)
page.has_css?(".notification a[href='#{notification.url}']")
end
def has_no_notification?(notification)
page.has_no_css?(".notification a[href='#{notification.url}']")
end
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
describe "User notifications", type: :system do
fab!(:user)
let(:user_notifications_page) { PageObjects::Pages::UserNotifications.new }
fab!(:read_notification) { Fabricate(:notification, user: user, read: true) }
fab!(:unread_notification) { Fabricate(:notification, user: user, read: false) }
before { sign_in(user) }
describe "filtering" do
it "saves custom picture and system assigned pictures" do
user_notifications_page.visit(user)
user_notifications_page.filter_dropdown
expect(user_notifications_page).to have_selected_filter_value("all")
expect(user_notifications_page).to have_notification(read_notification)
expect(user_notifications_page).to have_notification(unread_notification)
user_notifications_page.set_filter_value("read")
expect(user_notifications_page).to have_notification(read_notification)
expect(user_notifications_page).to have_no_notification(unread_notification)
user_notifications_page.set_filter_value("unread")
expect(user_notifications_page).to have_no_notification(read_notification)
expect(user_notifications_page).to have_notification(unread_notification)
end
end
end