Bit of a long post but I'm at a dead end. I am relatively new to Rails but I'm developing an app that allows users to comment and vote on reviews, the review owner should get notified (by the presence of a dot next to the notifications icon in the navbar) when another user votes/comments on their review. I have set up notifications so that the new notifications appear in the notifications tab (index) immediately after a vote or comment is created but I'm struggling to apply the same logic to get the icon in the navbar to update (for a dot to appear) when a new notification arrives in the index. The dot appears after a page refresh but not instantly. Here is the navbar button:
<%= turbo_stream_from "notification_dot_#{current_user.id}" %>
<%= link_to notifications_path, class: "bottom_navbar-link position-relative #{'active' if current_page?(notifications_path)}" do %>
<i class="fas fa-bell"></i>
<% if current_user.notifications.where(read: false).any? %>
<span class="notification-dot"><i class="fa-solid fa-circle"></i></span>
<% end %>
<% end %>
and the notifications index page
<%= turbo_stream_from "notifications_#{current_user.id}" %>
<div class="notifications-list" id="notifications-list">
<% if @notifications.any? %>
<% @notifications.each do |notification| %>
<%= render notification %>
<% end %>
<% else %>
<p>Nothing to see here...</p>
<% end %>
this is the partial used for the dot to appear next to the notifications icon
<% if user.notifications.where(read: false).exists? %>
<span class="notification-dot"><i class="fa-solid fa-circle"></i></span>
<% end %>
and finally the comment/vote models to broadcast the notifications to the index page (working) and to broadcast the dot to appear instantly when there's unread notifications (not working)
class Comment < ApplicationRecord
belongs_to :review
belongs_to :user
has_many :notifications, dependent: :destroy
validates :content, presence: true, length: { minimum: 3, maximum: 140 }
after_create_commit :broadcast_comment, :broadcast_comment_count
after_destroy_commit :broadcast_comment_count
after_create :notify_review_owner
private
def broadcast_comment
broadcast_prepend_to "review_#{review.id}_comments",
target: "comments-#{review.id}",
partial: "comments/comment",
locals: { comment: self, user: self.user }
end
def broadcast_comment_count
broadcast_replace_to "review_#{review.id}_comments",
target: "comment-count-#{review.id}",
partial: "comments/comment_count",
locals: { review: review }
end
def notify_review_owner
return if user == review.user
notification = Notification.create(
user: review.user,
comment: self,
notification_type: 'comment',
content: "#{user.username} commented on your review.",
read: false
)
Rails.logger.info "Notification created: #{notification.inspect}"
Rails.logger.info "Broadcasting notification dot update for User ID: #{review.user.id}"
broadcast_replace_to(
"notification_dot_#{review.user.id}",
target: "notification_dot_#{review.user.id}",
partial: "notifications/notification_dot",
locals: { user: review.user }
)
broadcast_prepend_later_to(
"notifications_#{review.user.id}",
target: "notifications-list",
partial: "notifications/notification",
locals: { notification: notification }
)
end
end
class Vote < ApplicationRecord
belongs_to :review
belongs_to :user
has_many :notifications, dependent: :destroy
validates :user_id, uniqueness: { scope: :review_id, message: "You can only vote once on a review" }
after_create :notify_review_owner
private
def notify_review_owner # rubocop:disable Metrics/MethodLength
return if user == review.user
notification = Notification.create(
user: review.user,
vote_id: self.id,
notification_type: 'vote',
content: "#{user.username} upvoted your review.",
read: false
)
Rails.logger.info "Notification created: #{notification.inspect}"
Rails.logger.info "Broadcasting notification dot update for User ID: #{review.user.id}"
broadcast_replace_to(
"notification_dot_#{review.user.id}",
target: "notification_dot_#{review.user.id}",
partial: "notifications/notification_dot",
locals: { user: review.user }
)
broadcast_prepend_later_to(
"notifications_#{review.user.id}",
target: "notifications-list",
partial: "notifications/notification",
locals: { notification: notification }
)
end
end
I've tried to implement similar logic to what I have working to send notifications to the index page. The server log seems to show that the notification dot broadcast is reaching the client with the broadcast but isn't being rendered on the frontend
You need to subscribe to a stream (action cable kind) and have a target for turbo streams that arrive through that subscription:
# subscribe
<%= turbo_stream_from "notification_dot", current_user %>
<%= link_to notifications_path,
class: ["bottom_navbar-link position-relative", active: current_page?(notifications_path)] do %>
<i class="fas fa-bell"></i> <!--/-->
<%= render "notifications/notification_dot" %>
<% end %>
if you're using replace
action you need to have the whole div you're replacing in the partial:
# app/views/notifications/_notification_dot.html.erb
# target
<%= tag.div id: "notification_dot_#{current_user.id}" do %>
<% if current_user.notifications.where(read: false).any? %>
<span class="notification-dot"><i class="fa-solid fa-circle"></i></span>
<% end %>
<% end %>
and broadcast from models:
broadcast_replace_to(
"notification_dot", review.user, # stream name matching turbo_stream_from
target: "notification_dot_#{review.user.id}", # a div you want to replace
partial: "notifications/notification_dot",
locals: {current_user: review.user}
)