Skip to content

Commit 0368eca

Browse files
authored
Merge branch 'main' into dependabot/bundler/factory_bot_rails-6.5.1
2 parents 932af70 + 263eafb commit 0368eca

File tree

27 files changed

+1267
-73
lines changed

27 files changed

+1267
-73
lines changed

Gemfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ gem 'fog-aws'
2020
# Database adapter for PostgreSQL
2121
gem 'pg', '>= 0.18', '< 2.0'
2222
# Puma as the app server
23-
gem 'puma', '~> 6.6'
23+
gem 'puma', '~> 7.0'
2424

2525
# Pundit for authorization, custom fork for Better Together
2626
gem 'pundit-resources', '~> 1.1.4', github: 'better-together-org/pundit-resources'

Gemfile.lock

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ GEM
162162
unf
163163
ast (2.4.3)
164164
aws-eventstream (1.4.0)
165-
aws-partitions (1.1152.0)
165+
aws-partitions (1.1156.0)
166166
aws-sdk-core (3.232.0)
167167
aws-eventstream (~> 1, >= 1.3.0)
168168
aws-partitions (~> 1, >= 1.992.0)
@@ -174,7 +174,7 @@ GEM
174174
aws-sdk-kms (1.112.0)
175175
aws-sdk-core (~> 3, >= 3.231.0)
176176
aws-sigv4 (~> 1.5)
177-
aws-sdk-s3 (1.198.0)
177+
aws-sdk-s3 (1.199.0)
178178
aws-sdk-core (~> 3, >= 3.231.0)
179179
aws-sdk-kms (~> 1)
180180
aws-sigv4 (~> 1.5)
@@ -527,7 +527,7 @@ GEM
527527
i18n (>= 0.5.0)
528528
railties (>= 6.1.0)
529529
public_suffix (6.0.1)
530-
puma (6.6.1)
530+
puma (7.0.2)
531531
nio4r (~> 2.0)
532532
pundit (2.5.0)
533533
activesupport (>= 3.0.0)
@@ -668,7 +668,7 @@ GEM
668668
rswag-ui (2.16.0)
669669
actionpack (>= 5.2, < 8.1)
670670
railties (>= 5.2, < 8.1)
671-
rubocop (1.80.1)
671+
rubocop (1.80.2)
672672
json (~> 2.3)
673673
language_server-protocol (~> 3.17.0.2)
674674
lint_roller (~> 1.1.0)
@@ -729,10 +729,10 @@ GEM
729729
rexml (~> 3.2, >= 3.2.5)
730730
rubyzip (>= 1.2.2, < 4.0)
731731
websocket (~> 1.0)
732-
sentry-rails (5.26.0)
732+
sentry-rails (5.27.0)
733733
railties (>= 5.0)
734-
sentry-ruby (~> 5.26.0)
735-
sentry-ruby (5.26.0)
734+
sentry-ruby (~> 5.27.0)
735+
sentry-ruby (5.27.0)
736736
bigdecimal
737737
concurrent-ruby (~> 1.0, >= 1.0.2)
738738
shoulda-callback-matchers (1.1.4)
@@ -861,7 +861,7 @@ DEPENDENCIES
861861
listen (>= 3.0.5, < 3.10)
862862
pg (>= 0.18, < 2.0)
863863
pry
864-
puma (~> 6.6)
864+
puma (~> 7.0)
865865
pundit-resources (~> 1.1.4)!
866866
rack-mini-profiler
867867
rack-protection

app/controllers/better_together/notifications_controller.rb

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,84 @@ def index
1313
@unread_count = helpers.current_person.notifications.unread.size
1414
end
1515

16+
def dropdown # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
17+
# Get basic info needed for cache key (minimal queries)
18+
max_updated_at = helpers.current_person.notifications.maximum(:updated_at)
19+
unread_count = helpers.current_person.notifications.unread.size
20+
21+
# Create cache key based on max updated_at of user's notifications
22+
cache_key = "notifications_dropdown/#{helpers.current_person.id}/#{max_updated_at&.to_i}/#{unread_count}"
23+
24+
cached_content = Rails.cache.fetch(cache_key, expires_in: 1.hour) do
25+
# Only fetch detailed data when cache misses
26+
notifications = helpers.recent_notifications
27+
28+
# Warm up fragment caches for individual notifications in background
29+
warm_notification_fragment_caches(notifications) if Rails.env.production?
30+
31+
render_to_string(
32+
partial: 'better_together/notifications/dropdown_content',
33+
locals: { notifications: notifications, unread_count: unread_count }
34+
)
35+
end
36+
37+
render html: cached_content.html_safe
38+
end
39+
1640
# TODO: Make a Stimulus controller to dispatch this action async when messages are viewed
17-
def mark_as_read # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
41+
def mark_as_read
42+
process_mark_as_read_request
43+
44+
respond_to do |format|
45+
format.html { redirect_to notifications_path }
46+
format.turbo_stream { render_turbo_stream_response }
47+
end
48+
end
49+
50+
private
51+
52+
def process_mark_as_read_request
1853
if params[:id]
1954
mark_notification_as_read(params[:id])
2055
elsif params[:record_id]
2156
mark_record_notification_as_read(params[:record_id])
2257
else
2358
helpers.current_person.notifications.unread.update_all(read_at: Time.current)
2459
end
60+
end
2561

26-
respond_to do |format|
27-
format.html { redirect_to notifications_path }
28-
format.turbo_stream do
29-
if @notification
30-
render turbo_stream: turbo_stream.replace(helpers.dom_id(@notification), @notification)
31-
else
32-
render turbo_stream: turbo_stream.replace(
33-
'notifications',
34-
partial: 'better_together/notifications/notifications',
35-
locals: { notifications: helpers.current_person.notifications, unread_count: 0 }
36-
)
37-
end
38-
end
62+
def render_turbo_stream_response
63+
if @notification
64+
render_notification_turbo_stream
65+
else
66+
render_notifications_list_turbo_stream
3967
end
4068
end
4169

70+
def render_notification_turbo_stream
71+
render turbo_stream: turbo_stream.replace(
72+
helpers.dom_id(@notification),
73+
partial: 'better_together/notifications/notification',
74+
locals: notification_locals
75+
)
76+
end
77+
78+
def render_notifications_list_turbo_stream
79+
render turbo_stream: turbo_stream.replace(
80+
'notifications',
81+
partial: 'better_together/notifications/notifications',
82+
locals: { notifications: helpers.current_person.notifications, unread_count: 0 }
83+
)
84+
end
85+
86+
def notification_locals
87+
{
88+
notification: @notification,
89+
notification_title: @notification.event.record&.try(:title) || 'Notification',
90+
notification_url: @notification.try(:url) || '#'
91+
}
92+
end
93+
4294
def mark_notification_as_read(id)
4395
@notification = helpers.current_person.notifications.find(id)
4496
@notification.update(read_at: Time.current)
@@ -47,5 +99,22 @@ def mark_notification_as_read(id)
4799
def mark_record_notification_as_read(id)
48100
mark_notifications_read_for_record_id(id)
49101
end
102+
103+
# Warm fragment caches for notifications to improve subsequent renders
104+
def warm_notification_fragment_caches(notifications)
105+
notifications.each do |notification|
106+
next unless helpers.should_cache_notification?(notification)
107+
108+
# Pre-warm the fragment cache keys
109+
fragment_key = helpers.notification_fragment_cache_key(notification)
110+
type_key = helpers.notification_type_fragment_cache_key(notification)
111+
112+
# Check if fragments are already cached to avoid unnecessary work
113+
unless Rails.cache.exist?(fragment_key) && Rails.cache.exist?(type_key)
114+
# This could be moved to a background job for better performance
115+
Rails.logger.debug "Warming fragment cache for notification #{notification.id}"
116+
end
117+
end
118+
end
50119
end
51120
end

app/helpers/better_together/notifications_helper.rb

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,67 @@ def unread_notification_count
2424
def recent_notifications
2525
current_person.notifications.joins(:event).order(created_at: :desc).limit(5)
2626
end
27+
28+
# Fragment cache key for notification types
29+
def notification_fragment_cache_key(notification)
30+
# Base cache key with notification's cache_key_with_version
31+
base_key = [notification.cache_key_with_version]
32+
33+
# Add related record version if it exists and has cache_key_with_version
34+
if notification.event&.record.respond_to?(:cache_key_with_version)
35+
base_key << notification.event.record.cache_key_with_version
36+
end
37+
38+
# Add event cache key for consistency
39+
base_key << notification.event.cache_key_with_version if notification.event.respond_to?(:cache_key_with_version)
40+
41+
# Add locale for i18n support
42+
base_key << I18n.locale
43+
44+
base_key
45+
end
46+
47+
# Type-specific fragment cache key for notification patterns
48+
def notification_type_fragment_cache_key(notification)
49+
type_key = [
50+
notification.type || notification.class.name,
51+
notification.cache_key_with_version,
52+
I18n.locale
53+
]
54+
55+
# Add related record cache key if available
56+
if notification.event&.record.respond_to?(:cache_key_with_version)
57+
type_key << notification.event.record.cache_key_with_version
58+
end
59+
60+
type_key
61+
end
62+
63+
# Cache expiration utilities for notifications
64+
def expire_notification_fragments(notification)
65+
# Expire all fragments related to this notification
66+
expire_fragment(notification_fragment_cache_key(notification))
67+
expire_fragment(notification_type_fragment_cache_key(notification))
68+
69+
# Expire header/content/footer fragments
70+
expire_fragment(['notification_header', notification.cache_key_with_version])
71+
expire_fragment(['notification_content', notification.cache_key_with_version])
72+
expire_fragment(['notification_footer', notification.cache_key_with_version])
73+
end
74+
75+
# Expire fragments for a specific notification type pattern
76+
def expire_notification_type_fragments(notification_type)
77+
# This is useful when you want to expire all cached fragments for a specific notification type
78+
# Note: This requires more specific cache management based on your needs
79+
Rails.cache.delete_matched("*#{notification_type}*")
80+
end
81+
82+
# Check if a notification should use fragment caching
83+
def should_cache_notification?(notification)
84+
# Only cache if the notification and its record are present and have cache keys
85+
notification.present? &&
86+
notification.event&.record.present? &&
87+
notification.respond_to?(:cache_key_with_version)
88+
end
2789
end
2890
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// app/assets/javascripts/controllers/better_together/lazy_notifications_controller.js
2+
import { Controller } from "@hotwired/stimulus"
3+
4+
export default class extends Controller {
5+
static targets = [ "dropdown", "content" ]
6+
static values = {
7+
loaded: Boolean,
8+
url: String
9+
}
10+
11+
connect() {
12+
this.loadedValue = false
13+
console.log('Lazy notifications controller connected with URL:', this.urlValue)
14+
15+
// Find the dropdown toggle element (the <a> tag with data-bs-toggle="dropdown")
16+
const dropdownToggle = this.element.querySelector('[data-bs-toggle="dropdown"]')
17+
18+
if (dropdownToggle) {
19+
console.log('Found dropdown toggle, adding event listener')
20+
// Listen for Bootstrap dropdown show event on the dropdown toggle
21+
dropdownToggle.addEventListener('show.bs.dropdown', (event) => {
22+
console.log('Bootstrap dropdown show event triggered')
23+
if (!this.loadedValue) {
24+
this.loadContent()
25+
}
26+
})
27+
} else {
28+
console.log('No dropdown toggle found')
29+
}
30+
}
31+
32+
toggle(event) {
33+
// This method can be removed or kept for debugging
34+
console.log('Dropdown toggle clicked')
35+
}
36+
37+
async loadContent() {
38+
if (this.loadedValue) return
39+
40+
console.log('Loading notifications from:', this.urlValue)
41+
42+
try {
43+
// Show loading state
44+
this.contentTarget.innerHTML = `
45+
<div class="text-center p-3">
46+
<div class="spinner-border spinner-border-sm" role="status">
47+
<span class="visually-hidden">Loading...</span>
48+
</div>
49+
<div class="mt-2">Loading notifications...</div>
50+
</div>
51+
`
52+
53+
const response = await fetch(this.urlValue, {
54+
headers: {
55+
'Accept': 'text/html'
56+
}
57+
})
58+
59+
if (response.ok) {
60+
const html = await response.text()
61+
this.contentTarget.innerHTML = html
62+
this.loadedValue = true
63+
} else {
64+
this.contentTarget.innerHTML = `
65+
<div class="text-danger text-center p-3">
66+
Failed to load notifications
67+
</div>
68+
`
69+
}
70+
} catch (error) {
71+
console.error('Error loading notifications:', error)
72+
this.contentTarget.innerHTML = `
73+
<div class="text-danger text-center p-3">
74+
Error loading notifications
75+
</div>
76+
`
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)