Showing with 451 additions and 37 deletions.
  1. +3 −0 app/assets/images/icons/outline/bell.svg
  2. +14 −0 app/components/user_menu/signed_in_component.html.erb
  3. +4 −0 app/components/user_menu/signed_in_component.rb
  4. +12 −1 app/controllers/conversations_controller.rb
  5. +19 −0 app/controllers/notifications_controller.rb
  6. +7 −0 app/controllers/read_notifications_controller.rb
  7. +21 −9 app/mailers/admin_mailer.rb
  8. +5 −3 app/mailers/message_mailer.rb
  9. +0 −1 app/models/business.rb
  10. +2 −0 app/models/conversation.rb
  11. +0 −1 app/models/developer.rb
  12. +8 −0 app/models/feature.rb
  13. +3 −1 app/models/message.rb
  14. +1 −1 app/models/user.rb
  15. +12 −0 app/notifications/new_business_notification.rb
  16. +15 −0 app/notifications/new_conversation_notification.rb
  17. +12 −0 app/notifications/new_developer_profile_notification.rb
  18. +17 −0 app/notifications/new_message_notification.rb
  19. +1 −1 app/views/admin_mailer/new_business.html.erb
  20. +2 −2 app/views/admin_mailer/new_business.text.erb
  21. +1 −1 app/views/admin_mailer/new_conversation.html.erb
  22. +3 −1 app/views/admin_mailer/new_conversation.text.erb
  23. +1 −1 app/views/admin_mailer/new_developer_profile.html.erb
  24. +2 −2 app/views/admin_mailer/new_developer_profile.text.erb
  25. +1 −1 app/views/cold_messages/new.html.erb
  26. +1 −1 app/views/message_mailer/new_message.html.erb
  27. +2 −2 app/views/message_mailer/new_message.text.erb
  28. +5 −0 app/views/notifications/_empty_state.html.erb
  29. +16 −0 app/views/notifications/_notification.html.erb
  30. +18 −0 app/views/notifications/index.html.erb
  31. +5 −0 app/views/read_notifications/_empty_state.html.erb
  32. +10 −0 app/views/read_notifications/index.html.erb
  33. +1 −0 config/i18n-tasks.yml
  34. +21 −0 config/locales/en.yml
  35. +2 −0 config/routes.rb
  36. +7 −1 db/seeds.rb
  37. +21 −0 lib/tasks/message_notifications.rake
  38. +1 −1 test/components/availability_component_test.rb
  39. +48 −0 test/components/user_menu/signed_in_component_test.rb
  40. +14 −0 test/integration/conversations_test.rb
  41. +54 −0 test/integration/notifications_test.rb
  42. +27 −0 test/integration/read_notifications_test.rb
  43. +6 −3 test/mailers/previews/admin_mailer_preview.rb
  44. +2 −1 test/mailers/previews/message_mailer_preview.rb
  45. +5 −2 test/models/message_test.rb
  46. +19 −0 test/models/notification_test.rb
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -1,4 +1,18 @@
<div class="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
<div class="text-white px-2" style="position:relative">
<% if Feature.enabled?(:notifications) %>
<%= link_to notifications_path, class: "flex-shrink-0 text-gray-300 rounded-full hover:text-white group" do %>
<%= inline_svg_tag "icons/outline/bell.svg", class: "mx-auto h-6 w-6" %>
<span class="sr-only"><%= t(".view_notifications") %></span>

<% if unread_notifications? %>
<span class="absolute top-1 right-2 block h-2 w-2 rounded-full ring-2 ring-gray-300 group-hover:ring-white bg-red-400"></span>
<span class="sr-only"><%= t(".new_notifications") %></span>
<% end %>
<% end %>
<% end %>
</div>

<div data-controller="toggle" data-toggle-close-class="hidden" class="ml-3 relative">
<div>
<button type="button" data-action="toggle#toggle" class="bg-gray-800 flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white" id="user-menu-button" aria-expanded="false" aria-haspopup="true">
@@ -28,4 +28,8 @@ def customer?
def admin?
user.admin?
end

def unread_notifications?
user.notifications.unread.any?
end
end
@@ -1,13 +1,24 @@
class ConversationsController < ApplicationController
before_action :authenticate_user!
after_action :mark_notifications_as_read, only: :show, if: -> { Feature.enabled?(:notifications) }

def index
@conversations = current_user.conversations
end

def show
@conversation = Conversation.find(params[:id])
@conversation = conversation
@message = Message.new
authorize @conversation, policy_class: MessagingPolicy
end

private

def conversation
@conversation ||= Conversation.find(params[:id])
end

def mark_notifications_as_read
conversation.notifications_as_conversation.where(recipient: current_user).unread.mark_as_read!
end
end
@@ -0,0 +1,19 @@
class NotificationsController < ApplicationController
before_action :authenticate_user!

def index
@notifications = current_user.notifications.unread
@read_notifications = current_user.notifications.read
end

def show
notification = current_user.notifications.find(params[:id])
notification.mark_as_read!

if (url = notification.to_notification.url)
redirect_to url
else
redirect_to notifications_path, notice: t(".notice")
end
end
end
@@ -0,0 +1,7 @@
class ReadNotificationsController < ApplicationController
before_action :authenticate_user!

def index
@notifications = current_user.notifications.read
end
end
@@ -1,19 +1,31 @@
class AdminMailer < ApplicationMailer
def new_developer_profile
@user = params[:recipient]
@developer = params[:developer]
mail(to: @user.email, subject: "New developer profile added")
@notification = params[:record]
recipient = params[:recipient]

@developer = @notification.to_notification.developer.name

mail(to: recipient.email, subject: "New developer profile added")
end

def new_business
@user = params[:recipient]
@business = params[:business]
mail(to: @user.email, subject: "New business added")
@notification = params[:record]
recipient = params[:recipient]

@business = @notification.to_notification.business.name

mail(to: recipient.email, subject: "New business added")
end

def new_conversation
@user = params[:recipient]
@conversation = params[:conversation]
mail(to: @user.email, subject: "New conversation started")
@notification = params[:record]
recipient = params[:recipient]

instance = @notification.to_notification
@name = instance.conversation.business.name
@company = instance.conversation.business.company
@developer = instance.conversation.developer.name

mail(to: recipient.email, subject: "New conversation started")
end
end
@@ -1,7 +1,9 @@
class MessageMailer < ApplicationMailer
def new_message
@message = params[:message]
@sender = @message.sender
mail(to: @message.recipient.user.email, subject: "#{@sender.name} sent you a message on railsdevs")
@notification = params[:record]
recipient = params[:recipient]
@sender = @notification.to_notification.message.sender.name

mail(to: recipient.email, subject: "#{@sender} sent you a message on railsdevs")
end
end
@@ -3,7 +3,6 @@ class Business < ApplicationRecord

belongs_to :user
has_many :conversations, -> { visible }
has_many :notifications, as: :recipient

validates :name, presence: true
validates :company, presence: true
@@ -4,6 +4,8 @@ class Conversation < ApplicationRecord

has_many :messages, -> { order(:created_at) }, dependent: :destroy

has_noticed_notifications

validates :developer_id, uniqueness: {scope: :business_id}

scope :blocked, -> { where.not(developer_blocked_at: nil).or(Conversation.where.not(business_blocked_at: nil)) }
@@ -10,7 +10,6 @@ class Developer < ApplicationRecord

belongs_to :user
has_many :conversations, -> { visible }
has_many :notifications, as: :recipient
has_one :role_type, dependent: :destroy, autosave: true
has_one_attached :cover_image

@@ -0,0 +1,8 @@
class Feature
def self.enabled?(feature_name)
case feature_name.to_sym
when :notifications
!Rails.env.production?
end
end
end
@@ -4,6 +4,8 @@ class Message < ApplicationRecord
has_one :developer, through: :conversation
has_one :business, through: :conversation

has_noticed_notifications

validates :body, presence: true

after_create :send_recipient_notification
@@ -23,6 +25,6 @@ def recipient
private

def send_recipient_notification
NewMessageNotification.with(message: self).deliver_later(recipient)
NewMessageNotification.with(message: self, conversation: conversation).deliver_later(recipient.user)
end
end
@@ -7,7 +7,7 @@ class User < ApplicationRecord
:validatable
pay_customer

has_many :notifications, as: :recipient
has_many :notifications, as: :recipient, dependent: :destroy
has_one :business, dependent: :destroy
has_one :developer, dependent: :destroy

@@ -3,4 +3,16 @@ class NewBusinessNotification < Noticed::Base
deliver_by :email, mailer: "AdminMailer", method: :new_business

param :business

def title
t "notifications.new_business", business: business.name
end

def url
business_path(business)
end

def business
params[:business]
end
end
@@ -3,4 +3,19 @@ class NewConversationNotification < Noticed::Base
deliver_by :email, mailer: "AdminMailer", method: :new_conversation

param :conversation

def title
t "notifications.new_conversation",
name: conversation.business.name,
company: conversation.business.company,
developer: conversation.developer.name
end

def url
nil
end

def conversation
params[:conversation]
end
end
@@ -3,4 +3,16 @@ class NewDeveloperProfileNotification < Noticed::Base
deliver_by :email, mailer: "AdminMailer", method: :new_developer_profile

param :developer

def title
t "notifications.new_developer_profile", developer: developer.name
end

def url
developer_path(developer)
end

def developer
params[:developer]
end
end
@@ -3,4 +3,21 @@ class NewMessageNotification < Noticed::Base
deliver_by :email, mailer: "MessageMailer", method: :new_message

param :message
param :conversation

def title
t "notifications.new_message", sender: message.sender.name
end

def url
conversation_path(conversation)
end

def message
params[:message]
end

def conversation
params[:conversation]
end
end
@@ -1 +1 @@
<p><b><%= @business.name %></b> added their <%= link_to "business", business_url(@business) %>!</p>
<p><b><%= @business %></b> added their <%= link_to "business", notification_url(@notification) %>!</p>
@@ -1,3 +1,3 @@
<%= @business.name %> added their business!
<%= @business %> added their business!

<%= business_url(@business) %>
<%= notification_url(@notification) %>
@@ -1 +1 @@
<p><b><%= @conversation.business.name %> (<%= @conversation.business.company %>)</b> started a conversation with <%= @conversation.developer.name %>.</p>
<p><b><%= @name %> (<%= @company %>)</b> started <%= link_to "a conversation with #{@developer}", notification_url(@notification) %>.</p>
@@ -1 +1,3 @@
<%= @conversation.business.name %> (<%= @conversation.business.company %>) started a conversation with <%= @conversation.developer.name %>.
<%= @name %> (<%= @company %>) started a conversation with <%= @developer %>.

<%= notification_url(@notification) %>
@@ -1 +1 @@
<p><b><%= @developer.name %></b> added their <%= link_to "developer profile", developer_url(@developer) %>!</p>
<p><b><%= @developer %></b> added their <%= link_to "developer profile", notification_url(@notification) %>!</p>
@@ -1,3 +1,3 @@
<%= @developer.name %> added their developer profile!
<%= @developer %> added their developer profile!

<%= developer_url(@developer) %>
<%= notification_url(@notification) %>
@@ -10,7 +10,7 @@
<div class="block"><%= t(".with") %></div>
<%= link_to developer_path(@message.conversation.developer), class: "flex items-center space-x-2 group" do %>
<%= render AvatarComponent.new(avatarable: @message.conversation.developer, classes: "h-8 w-8") %>
<span class="font-medium text-gray-600 group-hover:text-gray-500"><%= @message.conversation.developer.hero %></span>
<span class="font-medium text-gray-600 group-hover:text-gray-500"><%= @message.conversation.developer.name %></span>
<% end %>
</div>
</div>
@@ -1 +1 @@
<p><%= @sender.name %> sent you <%= link_to "a message", conversation_url(@message.conversation) %>.</p>
<p><%= @sender %> sent you <%= link_to "a message", notification_url(@notification) %>.</p>
@@ -1,3 +1,3 @@
<%= @sender.name %> sent you a message.
<%= @sender %> sent you a message.

<%= conversation_url(@message.conversation) %>
<%= notification_url(@notification) %>
@@ -0,0 +1,5 @@
<div class="text-center">
<%= inline_svg_tag "icons/outline/bell.svg", class: "mx-auto h-6 w-6 text-gray-400" %>
<h3 class="mt-2 text-sm font-medium text-gray-900"><%= t(".title") %></h3>
<p class="mt-1 text-sm text-gray-500"><%= t(".body") %></p>
</div>
@@ -0,0 +1,16 @@
<div role="list" class="w-full max-w-xl mx-auto rounded-lg overflow-hidden border border-gray-200 divide-y divide-gray-200 mt-3">
<%= link_to notification_path(notification), class: "w-full" do %>
<div class="relative bg-white py-5 px-6 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-blue-600">
<div class="flex justify-between space-x-3">
<span class="shrink-0 whitespace-nowrap text-sm text-gray-500">
<%= render TimeComponent.new(notification.created_at) %>
</span>
</div>
<div class="mt-3">
<p class="line-clamp-2 text-sm text-gray-600 text-left">
<%= notification.to_notification.title %>
</p>
</div>
</div>
<% end %>
</div>
@@ -0,0 +1,18 @@
<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<% if @notifications.any? %>
<h1 class="mt-6 text-center text-3xl font-extrabold mb-5"><%= t(".title") %></h1>
<%= render @notifications %>
<% else %>
<div class="mt-8">
<%= render "empty_state" %>
</div>
<% end %>
<div class="mt-6 mx-auto">
<% if @read_notifications.any? %>
<%= link_to read_notifications_path, class: "inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-gray-600 hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500" do %>
<%= inline_svg_tag "icons/solid/search", class: "-ml-1 mr-2 h-5 w-5" %>
<%= t(".cta") %>
<% end %>
<% end %>
</div>
</div>
@@ -0,0 +1,5 @@
<div class="text-center">
<%= inline_svg_tag "icons/outline/bell.svg", class: "mx-auto h-6 w-6 text-gray-400" %>
<h3 class="mt-2 text-sm font-medium text-gray-900"><%= t(".title") %></h3>
<p class="mt-1 text-sm text-gray-500"><%= t(".body") %></p>
</div>
@@ -0,0 +1,10 @@
<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<% if @notifications.any? %>
<h1 class="mt-6 text-center text-3xl font-extrabold mb-5"><%= t(".title") %></h1>
<%= render partial: "notifications/notification", collection: @notifications %>
<% else %>
<div class="mt-8">
<%= render "empty_state" %>
</div>
<% end %>
</div>
@@ -62,6 +62,7 @@ search:
- app/controllers
- app/helpers
- app/mailers
- app/notifications
- app/presenters
- app/views