Skip to content

Commit 82851dd

Browse files
authored
Merge branch 'main' into f3wlvl-codex/prepare-app-for-rails-7.2-and-8-upgrade
2 parents 8d046b3 + c1b7307 commit 82851dd

File tree

7 files changed

+219
-16
lines changed

7 files changed

+219
-16
lines changed

app/javascript/better_together/notifications.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,66 @@ function displayFlashMessage(type, message, onDismiss = null) {
5959
}
6060
}
6161

62+
// Update badge, document title, and favicon with unread notification count
63+
function updateUnreadNotifications(count) {
64+
// Update notification badge
65+
let badge = document.getElementById('person_notification_count');
66+
if (badge) {
67+
if (count > 0) {
68+
badge.textContent = count;
69+
} else {
70+
badge.remove();
71+
badge = null;
72+
}
73+
}
74+
if (!badge && count > 0) {
75+
const icon = document.getElementById('notification-icon');
76+
if (icon) {
77+
badge = document.createElement('span');
78+
badge.id = 'person_notification_count';
79+
badge.className = 'badge bg-primary rounded-pill position-absolute notification-badge';
80+
badge.textContent = count;
81+
icon.appendChild(badge);
82+
}
83+
}
84+
85+
// Update document title
86+
const baseTitle = updateUnreadNotifications.baseTitle ||
87+
(updateUnreadNotifications.baseTitle = document.title.replace(/^\(\d+\)\s*/, ''));
88+
if (count > 0) {
89+
document.title = `(${count}) ${baseTitle}`;
90+
} else {
91+
document.title = baseTitle;
92+
}
93+
94+
// Update favicon with red dot
95+
const link = document.querySelector("link[rel~='icon']");
96+
if (!link) return;
97+
if (!updateUnreadNotifications.originalHref) {
98+
updateUnreadNotifications.originalHref = link.href;
99+
}
100+
if (count > 0) {
101+
const img = document.createElement('img');
102+
img.src = updateUnreadNotifications.originalHref;
103+
img.onload = () => {
104+
const size = 32;
105+
const canvas = document.createElement('canvas');
106+
canvas.width = size;
107+
canvas.height = size;
108+
const ctx = canvas.getContext('2d');
109+
ctx.drawImage(img, 0, 0, size, size);
110+
ctx.fillStyle = '#ff0000';
111+
ctx.beginPath();
112+
ctx.arc(size - 5, 5, 4, 0, 2 * Math.PI);
113+
ctx.fill();
114+
link.href = canvas.toDataURL('image/png');
115+
};
116+
} else {
117+
link.href = updateUnreadNotifications.originalHref;
118+
}
119+
}
120+
62121
export {
63-
displayFlashMessage
122+
displayFlashMessage,
123+
updateUnreadNotifications
64124
}

app/javascript/channels/better_together/notifications_channel.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import consumer from "channels/consumer";
2-
import { displayFlashMessage } from "better_together/notifications";
2+
import { displayFlashMessage, updateUnreadNotifications } from "better_together/notifications";
33
import DevicePermissionsController from "controllers/better_together/device_permissions_controller";
44

55
consumer.subscriptions.create("BetterTogether::NotificationsChannel", {
@@ -18,6 +18,9 @@ consumer.subscriptions.create("BetterTogether::NotificationsChannel", {
1818
messageContent = `<a href="${data["url"]}" target="_blank" rel="noopener" style="color:inherit;">${messageContent}</a>`;
1919
}
2020
displayFlashMessage("info", messageContent);
21+
if (data["unread_count"] !== undefined) {
22+
updateUnreadNotifications(data["unread_count"]);
23+
}
2124
}
2225

2326
if (Notification.permission === "default") {

app/notifiers/better_together/new_message_notifier.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,13 @@ def body
8383
I18n.t('better_together.notifications.new_message.content', content: message.content.to_plain_text.truncate(100))
8484
end
8585

86-
def build_message(_notification)
86+
def build_message(notification)
8787
{
8888
title:,
8989
body:,
9090
identifier:,
91-
url:
91+
url:,
92+
unread_count: notification.recipient.notifications.unread.count
9293
}
9394
end
9495

bin/codex_style_guard

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env bash
2+
bundle exec rubocop --fail-level F
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
# rubocop:disable Metrics/BlockLength
6+
RSpec.describe 'notification badge', type: :feature do
7+
include BetterTogether::DeviseSessionHelpers
8+
9+
before do
10+
configure_host_platform
11+
login_as_platform_manager
12+
end
13+
14+
it 'updates badge and title based on unread count', :js do
15+
visit conversations_path(locale: I18n.default_locale)
16+
original_title = page.title
17+
18+
page.evaluate_async_script(<<~JS)
19+
const done = arguments[0];
20+
import('better_together/notifications').then(m => {
21+
m.updateUnreadNotifications(3);
22+
done();
23+
});
24+
JS
25+
26+
expect(page).to have_css('#person_notification_count', text: '3')
27+
expect(page.title).to eq("(3) #{original_title}")
28+
29+
page.evaluate_async_script(<<~JS)
30+
const done = arguments[0];
31+
import('better_together/notifications').then(m => {
32+
m.updateUnreadNotifications(0);
33+
done();
34+
});
35+
JS
36+
37+
expect(page).to have_no_css('#person_notification_count')
38+
expect(page.title).to eq(original_title)
39+
end
40+
end
41+
# rubocop:enable Metrics/BlockLength

spec/helpers/better_together/notifications_helper_spec.rb

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,64 @@
22

33
require 'rails_helper'
44

5-
# Specs in this file have access to a helper object that includes
6-
# the NotificationsHelper. For example:
7-
#
8-
# describe NotificationsHelper do
9-
# describe "string concat" do
10-
# it "concats two strings with spaces" do
11-
# expect(helper.concat_strings("this","that")).to eq("this that")
12-
# end
13-
# end
14-
# end
155
module BetterTogether
6+
# rubocop:disable Metrics/BlockLength
167
RSpec.describe NotificationsHelper, type: :helper do
17-
it 'exists' do
18-
expect(described_class).to be
8+
let(:unread_count) { 0 }
9+
let(:unread_scope) { instance_double('UnreadScope', size: unread_count) }
10+
let(:notifications) { instance_double('Notifications', unread: unread_scope) }
11+
let(:person) { instance_double('Person', notifications: notifications) }
12+
13+
before do
14+
p = person
15+
helper.singleton_class.define_method(:current_person) { p }
16+
end
17+
18+
describe '#unread_notification_count' do
19+
let(:unread_count) { 1 }
20+
21+
it 'returns number of unread notifications for current person' do
22+
expect(helper.unread_notification_count).to eq(1)
23+
end
24+
25+
it 'returns nil when there is no current person' do
26+
helper.singleton_class.define_method(:current_person) { nil }
27+
expect(helper.unread_notification_count).to be_nil
28+
end
29+
end
30+
31+
describe '#unread_notification_counter' do
32+
let(:unread_count) { 2 }
33+
34+
it 'renders badge html when unread notifications present' do
35+
html = helper.unread_notification_counter
36+
expect(html).to include('span')
37+
expect(html).to include('person_notification_count')
38+
expect(html).to include('badge')
39+
end
40+
41+
it 'returns nil when there are no unread notifications' do
42+
no_unread = instance_double('UnreadScope', size: 0)
43+
no_person = instance_double('Person', notifications: instance_double('Notifications', unread: no_unread))
44+
helper.singleton_class.define_method(:current_person) { no_person }
45+
expect(helper.unread_notification_counter).to be_nil
46+
end
47+
end
48+
49+
describe '#unread_notifications?' do
50+
let(:unread_count) { 3 }
51+
52+
it 'returns true when unread notifications exist' do
53+
expect(helper.unread_notifications?).to be true
54+
end
55+
56+
it 'returns false when there are no unread notifications' do
57+
no_unread = instance_double('UnreadScope', size: 0)
58+
no_person = instance_double('Person', notifications: instance_double('Notifications', unread: no_unread))
59+
helper.singleton_class.define_method(:current_person) { no_person }
60+
expect(helper.unread_notifications?).to be false
61+
end
1962
end
2063
end
64+
# rubocop:enable Metrics/BlockLength
2165
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
module BetterTogether
6+
# rubocop:disable Metrics/BlockLength
7+
RSpec.describe NewMessageNotifier do
8+
let(:recipient) { double('Person') }
9+
let(:conversation) { double('Conversation', id: 1, title: 'Chat') }
10+
let(:sender) { double('Person', name: 'Alice') }
11+
let(:content) { double('Content', to_plain_text: 'hello') }
12+
let(:message_class) do
13+
Class.new do
14+
attr_reader :conversation, :sender, :content
15+
16+
def self.name = 'Message'
17+
def self.has_query_constraints? = false
18+
def self.composite_primary_key? = false
19+
def self.primary_key = 'id'
20+
def self.polymorphic_name = name
21+
22+
def initialize(conversation:, sender:, content:)
23+
@conversation = conversation
24+
@sender = sender
25+
@content = content
26+
end
27+
28+
def _read_attribute(attr)
29+
# rubocop:disable Style/StringConcatenation
30+
instance_variable_get('@' + attr.to_s)
31+
# rubocop:enable Style/StringConcatenation
32+
end
33+
end
34+
end
35+
let(:message) { message_class.new(conversation:, sender:, content:) }
36+
let(:notification) { double('Notification', recipient: recipient) }
37+
38+
subject(:notifier) { described_class.new(record: message) }
39+
40+
before do
41+
stub_const('Message', message_class)
42+
end
43+
44+
it 'includes unread notification count in message' do
45+
unread = double('Unread', count: 2)
46+
allow(recipient).to receive(:notifications).and_return(double('Notifications', unread: unread))
47+
result = notifier.send(:build_message, notification)
48+
expect(result[:unread_count]).to eq(2)
49+
end
50+
end
51+
# rubocop:enable Metrics/BlockLength
52+
end

0 commit comments

Comments
 (0)