diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 0ff5442f..d38458bb 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,4 +1,21 @@ +# frozen_string_literal: true + module ApplicationCable class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_verified_user + end + + private + + def find_verified_user + if (verified_user = env['warden'].user) + verified_user + else + reject_unauthorized_connection + end + end end end diff --git a/app/channels/notifications_channel.rb b/app/channels/notifications_channel.rb new file mode 100644 index 00000000..122aa84c --- /dev/null +++ b/app/channels/notifications_channel.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class NotificationsChannel < ApplicationCable::Channel + def subscribed + stream_for current_user + end +end diff --git a/app/javascript/application.js b/app/javascript/application.js index 5eb22b9e..79fb0782 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -8,3 +8,4 @@ import "leaflet" import "leaflet-providers" import "chartkick" import "Chart.bundle" +import "./channels" diff --git a/app/javascript/channels/consumer.js b/app/javascript/channels/consumer.js new file mode 100644 index 00000000..8ec3aad3 --- /dev/null +++ b/app/javascript/channels/consumer.js @@ -0,0 +1,6 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `bin/rails generate channel` command. + +import { createConsumer } from "@rails/actioncable" + +export default createConsumer() diff --git a/app/javascript/channels/index.js b/app/javascript/channels/index.js new file mode 100644 index 00000000..79ce4754 --- /dev/null +++ b/app/javascript/channels/index.js @@ -0,0 +1,2 @@ +// Import all the channels to be used by Action Cable +import "notifications_channel" diff --git a/app/javascript/channels/notifications_channel.js b/app/javascript/channels/notifications_channel.js new file mode 100644 index 00000000..49875762 --- /dev/null +++ b/app/javascript/channels/notifications_channel.js @@ -0,0 +1,15 @@ +import consumer from "./consumer" + +consumer.subscriptions.create("NotificationsChannel", { + connected() { + console.log("Connected to the notifications channel!"); + }, + + disconnected() { + // Called when the subscription has been terminated by the server + }, + + received(data) { + // Called when there's incoming data on the websocket for this channel + } +}); diff --git a/app/javascript/controllers/notifications_controller.js b/app/javascript/controllers/notifications_controller.js new file mode 100644 index 00000000..54310bc7 --- /dev/null +++ b/app/javascript/controllers/notifications_controller.js @@ -0,0 +1,47 @@ +import { Controller } from "@hotwired/stimulus" +import consumer from "../channels/consumer" + +export default class extends Controller { + static targets = ["container"] + static values = { userId: Number } + + connect() { + console.log("Controller connecting...") + // Ensure we clean up any existing subscription + if (this.subscription) { + console.log("Cleaning up existing subscription") + this.subscription.unsubscribe() + } + + this.subscription = consumer.subscriptions.create("NotificationsChannel", { + connected: () => { + console.log("Connected to NotificationsChannel", this.subscription) + }, + disconnected: () => { + console.log("Disconnected from NotificationsChannel") + }, + received: (data) => { + console.log("Received notification:", data, "Subscription:", this.subscription) + this.displayNotification(data) + } + }) + } + + disconnect() { + console.log("Controller disconnecting...") + if (this.subscription) { + this.subscription.unsubscribe() + this.subscription = null + } + } + + displayNotification(data) { + console.log("Notification received:", data) // For debugging + const notification = document.createElement("div") + notification.classList.add("notification", `notification-${data.kind}`) + notification.innerHTML = `${data.title}: ${data.content}` + + this.containerTarget.appendChild(notification) + setTimeout(() => notification.remove(), 5000) // Auto-hide after 5 seconds + } +} diff --git a/app/models/notification.rb b/app/models/notification.rb index b4a49263..e778e6cc 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Notification < ApplicationRecord + after_create_commit :broadcast_notification + belongs_to :user validates :title, :content, :kind, presence: true @@ -12,4 +14,18 @@ class Notification < ApplicationRecord def read? read_at.present? end + + private + + def broadcast_notification + Rails.logger.debug "Broadcasting notification to #{user.id}" + NotificationsChannel.broadcast_to( + user, + { + title: title, + content: content, + kind: kind + } + ) + end end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 376e2ee5..cf6c175a 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -3,6 +3,7 @@ <%= full_title(yield(:title)) %> + <%= action_cable_meta_tag %> <%= csrf_meta_tags %> <%= csp_meta_tag %> @@ -20,6 +21,9 @@
<%= render 'shared/navbar' %> <%= render 'shared/flash' %> +
+
+
<%= yield %>
diff --git a/config/importmap.rb b/config/importmap.rb index 2a346074..44a10e2d 100644 --- a/config/importmap.rb +++ b/config/importmap.rb @@ -16,4 +16,7 @@ pin 'leaflet-providers' # @2.0.0 pin 'chartkick', to: 'chartkick.js' pin 'Chart.bundle', to: 'Chart.bundle.js' pin 'leaflet.heat' # @0.2.0 -pin "leaflet-draw" # @1.0.4 +pin 'leaflet-draw' # @1.0.4 +pin '@rails/actioncable', to: 'actioncable.esm.js' +pin_all_from 'app/javascript/channels', under: 'channels' +pin 'notifications_channel', to: 'channels/notifications_channel.js' diff --git a/config/routes.rb b/config/routes.rb index 16d7fdb2..e1acae92 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -3,6 +3,7 @@ require 'sidekiq/web' Rails.application.routes.draw do + mount ActionCable.server => '/cable' mount Rswag::Api::Engine => '/api-docs' mount Rswag::Ui::Engine => '/api-docs' authenticate :user, ->(u) { u.admin? } do diff --git a/spec/channels/notifications_channel_spec.rb b/spec/channels/notifications_channel_spec.rb new file mode 100644 index 00000000..7e3543d4 --- /dev/null +++ b/spec/channels/notifications_channel_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe NotificationsChannel, type: :channel do + pending "add some examples to (or delete) #{__FILE__}" +end