FEATURE: Separate notification indicators for new PMs and reviewables (#19201)

This PR adds separate notification indicators for PMs and reviewables that have arrived since the last time the user opened the notifications menu.

The PM indicator is the strongest one of all three indicators followed by the reviewable indicator and then finally the blue indicator. This means that if there's a new PM and a new reviewable, then the PM indicator will be shown.

Meta topic: https://meta.discourse.org/t/no-green-or-red-notification-bubbles/242783?u=osama.

Internal topic: t/82995.
This commit is contained in:
Osama Sayegh 2022-12-01 02:05:32 +03:00 committed by GitHub
parent b8b5cbe463
commit 23bd993164
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 289 additions and 41 deletions

View File

@ -53,6 +53,8 @@ export default {
read_first_notification: data.read_first_notification,
all_unread_notifications_count: data.all_unread_notifications_count,
grouped_unread_notifications: data.grouped_unread_notifications,
new_personal_messages_notifications_count:
data.new_personal_messages_notifications_count,
});
if (

View File

@ -82,25 +82,48 @@ createWidget("header-notifications", {
contents.push(h("div.do-not-disturb-background", iconNode("moon")));
} else {
if (this.currentUser.redesigned_user_menu_enabled) {
const unread = user.all_unread_notifications_count || 0;
const reviewables = user.unseen_reviewable_count || 0;
const count = unread + reviewables;
if (count > 0) {
if (this._shouldHighlightAvatar()) {
contents.push(h("span.ring"));
}
let ringClass = null;
if (user.new_personal_messages_notifications_count) {
ringClass = "personal-messages";
contents.push(
this.attach("link", {
action: attrs.action,
className: "badge-notification with-icon new-pms",
icon: "envelope",
omitSpan: true,
title: "notifications.tooltip.new_message_notification",
titleOptions: {
count: user.new_personal_messages_notifications_count,
},
})
);
} else if (user.unseen_reviewable_count) {
contents.push(
this.attach("link", {
action: attrs.action,
className: "badge-notification with-icon new-reviewables",
icon: "flag",
omitSpan: true,
title: "notifications.tooltip.new_reviewable",
titleOptions: { count: user.unseen_reviewable_count },
})
);
} else if (user.all_unread_notifications_count) {
ringClass = "regular-notifications";
contents.push(
this.attach("link", {
action: attrs.action,
className: "badge-notification unread-notifications",
rawLabel: count,
rawLabel: user.all_unread_notifications_count,
omitSpan: true,
title: "notifications.tooltip.regular",
titleOptions: { count },
titleOptions: { count: user.all_unread_notifications_count },
})
);
}
if (ringClass && this._shouldHighlightAvatar()) {
contents.push(h(`span.ring.revamped.${ringClass}`));
}
} else {
const unreadNotifications = user.unread_notifications;
if (!!unreadNotifications) {

View File

@ -9,6 +9,7 @@ import {
} from "@ember/test-helpers";
import { exists, query } from "discourse/tests/helpers/qunit-helpers";
import { hbs } from "ember-cli-htmlbars";
import I18n from "I18n";
module("Integration | Component | site-header", function (hooks) {
setupRenderingTest(hooks);
@ -18,7 +19,7 @@ module("Integration | Component | site-header", function (hooks) {
this.currentUser.set("read_first_notification", false);
});
test("displaying unread and reviewable notifications count when user's notifications and reviewables count are updated", async function (assert) {
test("unread notifications count rerenders when user's notifications count is updated", async function (assert) {
this.currentUser.set("all_unread_notifications_count", 1);
this.currentUser.set("redesigned_user_menu_enabled", true);
@ -35,14 +36,6 @@ module("Integration | Component | site-header", function (hooks) {
".header-dropdown-toggle.current-user .unread-notifications"
);
assert.strictEqual(unreadBadge.textContent, "5");
this.currentUser.set("unseen_reviewable_count", 3);
await settled();
unreadBadge = query(
".header-dropdown-toggle.current-user .unread-notifications"
);
assert.strictEqual(unreadBadge.textContent, "8");
});
test("hamburger menu icon shows pending reviewables count", async function (assert) {
@ -133,4 +126,121 @@ module("Integration | Component | site-header", function (hooks) {
"the up arrow key moves the focus in the opposite direction"
);
});
test("new personal messages bubble is prioritized over unseen reviewables and regular notifications bubbles", async function (assert) {
this.currentUser.set("redesigned_user_menu_enabled", true);
this.currentUser.set("all_unread_notifications_count", 5);
this.currentUser.set("new_personal_messages_notifications_count", 2);
this.currentUser.set("unseen_reviewable_count", 3);
await render(hbs`<SiteHeader />`);
assert.notOk(
exists(
".header-dropdown-toggle.current-user .badge-notification.unread-notifications"
),
"regular notifications bubble isn't displayed when there are new personal messages notifications"
);
assert.notOk(
exists(
".header-dropdown-toggle.current-user .badge-notification.with-icon.new-reviewables"
),
"reviewables bubble isn't displayed when there are new personal messages notifications"
);
const pmsBubble = query(
".header-dropdown-toggle.current-user .badge-notification.with-icon.new-pms"
);
assert.strictEqual(
pmsBubble.textContent.trim(),
"",
"personal messages bubble has no count"
);
assert.ok(
pmsBubble.querySelector(".d-icon-envelope"),
"personal messages bubble has envelope icon"
);
assert.strictEqual(
pmsBubble.title,
I18n.t("notifications.tooltip.new_message_notification", { count: 2 }),
"personal messages bubble bubble has a title"
);
});
test("unseen reviewables bubble is prioritized over regular notifications", async function (assert) {
this.currentUser.set("redesigned_user_menu_enabled", true);
this.currentUser.set("all_unread_notifications_count", 5);
this.currentUser.set("new_personal_messages_notifications_count", 0);
this.currentUser.set("unseen_reviewable_count", 3);
await render(hbs`<SiteHeader />`);
assert.notOk(
exists(
".header-dropdown-toggle.current-user .badge-notification.unread-notifications"
),
"regular notifications bubble isn't displayed when there are unseen reviewables notifications"
);
const reviewablesBubble = query(
".header-dropdown-toggle.current-user .badge-notification.with-icon.new-reviewables"
);
assert.strictEqual(
reviewablesBubble.textContent.trim(),
"",
"reviewables bubble has no count"
);
assert.ok(
reviewablesBubble.querySelector(".d-icon-flag"),
"reviewables bubble has flag icon"
);
assert.strictEqual(
reviewablesBubble.title,
I18n.t("notifications.tooltip.new_reviewable", { count: 3 }),
"reviewables bubble has a title"
);
assert.notOk(
exists(
".header-dropdown-toggle.current-user .badge-notification.with-icon.new-pms"
),
"personal messages bubble isn't displayed"
);
});
test("regular notifications bubble is shown if there are neither new personal messages nor unseen reviewables", async function (assert) {
this.currentUser.set("redesigned_user_menu_enabled", true);
this.currentUser.set("all_unread_notifications_count", 5);
this.currentUser.set("new_personal_messages_notifications_count", 0);
this.currentUser.set("unseen_reviewable_count", 0);
await render(hbs`<SiteHeader />`);
const regularNotificationsBubble = query(
".header-dropdown-toggle.current-user .badge-notification.unread-notifications"
);
assert.strictEqual(
regularNotificationsBubble.textContent,
"5",
"regular notifications bubble has a count"
);
assert.strictEqual(
regularNotificationsBubble.title,
I18n.t("notifications.tooltip.regular", { count: 5 }),
"regular notifications bubble has a title"
);
assert.notOk(
exists(
".header-dropdown-toggle.current-user .badge-notification.with-icon.new-reviewables"
),
"reviewables bubble isn't displayed"
);
assert.notOk(
exists(
".header-dropdown-toggle.current-user .badge-notification.with-icon.new-pms"
),
"personal messages bubble isn't displayed"
);
});
});

View File

@ -486,21 +486,6 @@ table {
}
}
.ring {
$gradient-start: transparent;
$gradient-end: #090;
background: radial-gradient($gradient-start, $gradient-end);
top: -11px !important;
right: 23.5px !important;
border-radius: 100%;
width: 20px;
height: 20px;
transform-origin: center;
animation-iteration-count: infinite;
animation-duration: 3s;
animation-name: ping;
}
.fade {
opacity: 0;
transition: opacity 0.15s linear;

View File

@ -141,7 +141,7 @@
border-top: 1px solid transparent;
border-left: 1px solid transparent;
border-right: 1px solid transparent;
.d-icon {
> .d-icon {
color: var(--primary-medium);
}
}
@ -158,7 +158,7 @@
border-top: 1px solid var(--primary-low);
border-left: 1px solid var(--primary-low);
border-right: 1px solid var(--primary-low);
.d-icon {
> .d-icon {
color: var(--primary-medium);
}
&:after {
@ -185,14 +185,36 @@
display: inline-block;
color: var(--header_primary-low-mid);
}
.notifications {
position: relative;
}
.ring {
position: absolute;
top: -9px;
top: -11px;
right: 23.5px;
z-index: z("base");
margin-left: 0;
background: radial-gradient(transparent, var(--success));
border-radius: 100%;
width: 20px;
height: 20px;
transform-origin: center;
animation-iteration-count: infinite;
animation-duration: 3s;
animation-name: ping;
&.revamped {
right: -4px;
top: -6px;
&.regular-notifications {
background: radial-gradient(
transparent,
var(--tertiary-med-or-tertiary)
);
}
}
}
.header-dropdown-toggle {
position: relative;
@ -203,10 +225,24 @@
left: 0;
top: -4px;
min-width: 0.6em;
}
.unread-notifications {
left: auto;
right: -3px;
&.with-icon {
&.new-pms {
background-color: var(--success);
}
&.new-reviewables {
background-color: var(--danger);
}
.d-icon {
color: var(--secondary);
font-size: var(--font-down-1);
width: 1em;
}
}
}
.unread-notifications {
background-color: var(--tertiary-med-or-tertiary);
}
.unread-high-priority-notifications,

View File

@ -597,6 +597,23 @@ class User < ActiveRecord::Base
@unread_high_prios ||= unread_notifications_of_priority(high_priority: true)
end
def new_personal_messages_notifications_count
args = {
user_id: self.id,
seen_notification_id: self.seen_notification_id,
private_message: Notification.types[:private_message]
}
DB.query_single(<<~SQL, args).first
SELECT COUNT(*)
FROM notifications
WHERE user_id = :user_id
AND id > :seen_notification_id
AND NOT read
AND notification_type = :private_message
SQL
end
# PERF: This safeguard is in place to avoid situations where
# a user with enormous amounts of unread data can issue extremely
# expensive queries
@ -775,6 +792,7 @@ class User < ActiveRecord::Base
if self.redesigned_user_menu_enabled?
payload[:all_unread_notifications_count] = all_unread_notifications_count
payload[:grouped_unread_notifications] = grouped_unread_notifications
payload[:new_personal_messages_notifications_count] = new_personal_messages_notifications_count
end
MessageBus.publish("/notification/#{id}", payload, user_ids: [id])

View File

@ -46,6 +46,7 @@ class CurrentUserSerializer < BasicUserSerializer
:is_anonymous,
:reviewable_count,
:unseen_reviewable_count,
:new_personal_messages_notifications_count,
:read_faq?,
:automatically_unpin_topics,
:mailing_list_mode,
@ -374,6 +375,10 @@ class CurrentUserSerializer < BasicUserSerializer
redesigned_user_menu_enabled
end
def include_new_personal_messages_notifications_count?
redesigned_user_menu_enabled
end
def redesigned_user_page_nav_enabled
if SiteSetting.enable_new_user_profile_nav_groups.present?
object.in_any_groups?(SiteSetting.enable_new_user_profile_nav_groups_map)

View File

@ -2383,6 +2383,12 @@ en:
high_priority:
one: "%{count} unread high priority notification"
other: "%{count} unread high priority notifications"
new_message_notification:
one: "%{count} new message notification"
other: "%{count} new message notifications"
new_reviewable:
one: "%{count} new reviewable"
other: "%{count} new reviewables"
title: "notifications of @name mentions, replies to your posts and topics, messages, etc"
none: "Unable to load notifications at this time."
empty: "No notifications found."

View File

@ -2176,6 +2176,7 @@ RSpec.describe User do
user.update!(admin: true)
Fabricate(:notification, user: user, notification_type: 1)
Fabricate(:notification, notification_type: 15, high_priority: true, read: false, user: user)
Fabricate(:notification, user: user, notification_type: Notification.types[:private_message], read: false)
messages = MessageBus.track_publish("/notification/#{user.id}") do
user.publish_notifications_state
@ -2185,8 +2186,9 @@ RSpec.describe User do
message = messages.first
expect(message.data[:all_unread_notifications_count]).to eq(2)
expect(message.data[:grouped_unread_notifications]).to eq({ 1 => 1, 15 => 1 })
expect(message.data[:all_unread_notifications_count]).to eq(3)
expect(message.data[:grouped_unread_notifications]).to eq({ 1 => 1, 15 => 1, Notification.types[:private_message] => 1 })
expect(message.data[:new_personal_messages_notifications_count]).to eq(1)
end
end
end
@ -3116,6 +3118,53 @@ RSpec.describe User do
expect(admin.secure_category_ids).not_to include(private_category.id)
end
end
end
describe '#new_personal_messages_notifications_count' do
it "returns count of new and unread private_message notifications of the user" do
another_user = Fabricate(:user)
Fabricate(:notification, user: user, read: false)
last_seen_id = Fabricate(
:notification,
user: user,
read: false,
notification_type: Notification.types[:private_message]
).id
expect(user.new_personal_messages_notifications_count).to eq(1)
Fabricate(
:notification,
user: user,
read: false,
notification_type: Notification.types[:private_message]
)
Fabricate(
:notification,
user: another_user,
read: false,
notification_type: Notification.types[:private_message]
)
Fabricate(
:notification,
user: user,
read: true,
notification_type: Notification.types[:private_message]
)
Fabricate(
:notification,
user: user,
read: false,
notification_type: Notification.types[:replied]
)
user.update!(seen_notification_id: last_seen_id)
expect(user.new_personal_messages_notifications_count).to eq(1)
end
end
end

View File

@ -390,5 +390,19 @@ RSpec.describe CurrentUserSerializer do
end
end
describe "#new_personal_messages_notifications_count" do
fab!(:notification) { Fabricate(:notification, user: user, read: false, notification_type: Notification.types[:private_message]) }
it "isn't included when enable_experimental_sidebar_hamburger is disabled" do
SiteSetting.enable_experimental_sidebar_hamburger = false
expect(serializer.as_json[:new_personal_messages_notifications_count]).to be_nil
end
it "is included when enable_experimental_sidebar_hamburger is enabled" do
SiteSetting.enable_experimental_sidebar_hamburger = true
expect(serializer.as_json[:new_personal_messages_notifications_count]).to eq(1)
end
end
include_examples "#display_sidebar_tags", described_class
end