diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb new file mode 100644 index 0000000..d0a2808 --- /dev/null +++ b/app/controllers/notifications_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class NotificationsController < ApplicationController + before_action :set_user + + def index + @notifications = @user.notifications.includes(:user).latest.page(params[:page]).per(10) + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 83cf630..8cd435e 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true class ApplicationMailer < ActionMailer::Base - default from: 'Twitterクローン送信者' + default from: ENV['EMAIL'] layout 'mailer' end diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb new file mode 100644 index 0000000..565b81c --- /dev/null +++ b/app/mailers/notification_mailer.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class NotificationMailer < ApplicationMailer + def complete(notification:) + @notification = notification + mail(to: @notification.user_email, subject: 'Twitterクローン通知') + end +end diff --git a/app/models/comment.rb b/app/models/comment.rb index 53a43a6..57fac1d 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,9 +1,14 @@ # frozen_string_literal: true class Comment < ApplicationRecord + include NotificationCreator + + after_create :create_notification + belongs_to :user belongs_to :post has_many_attached :images + has_one :notification, as: :action, dependent: :destroy validates :content, presence: true diff --git a/app/models/concerns/notification_creator.rb b/app/models/concerns/notification_creator.rb new file mode 100644 index 0000000..811a2a1 --- /dev/null +++ b/app/models/concerns/notification_creator.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module NotificationCreator + def create_notification + notification = Notification.create!(user: post.user, action: self, action_type: self.class.name) + NotificationMailer.complete(notification:).deliver_later + end +end diff --git a/app/models/like.rb b/app/models/like.rb index 251e593..0cd1892 100644 --- a/app/models/like.rb +++ b/app/models/like.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true class Like < ApplicationRecord + include NotificationCreator + + after_create :create_notification + belongs_to :user belongs_to :post + has_one :notification, as: :action, dependent: :destroy validates :user_id, presence: true, uniqueness: { scope: :post_id } end diff --git a/app/models/notification.rb b/app/models/notification.rb new file mode 100644 index 0000000..0764cdf --- /dev/null +++ b/app/models/notification.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class Notification < ApplicationRecord + belongs_to :user + belongs_to :action, polymorphic: true + + scope :latest, -> { order(created_at: :desc) } + + delegate :email, to: :user, prefix: true + + def sender + action.user + end +end diff --git a/app/models/repost.rb b/app/models/repost.rb index 0f99c3c..1be60fc 100644 --- a/app/models/repost.rb +++ b/app/models/repost.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true class Repost < ApplicationRecord + include NotificationCreator + + after_create :create_notification + belongs_to :user belongs_to :post + has_one :notification, as: :action, dependent: :destroy validates :user_id, presence: true, uniqueness: { scope: :post_id } end diff --git a/app/models/user.rb b/app/models/user.rb index 8d73d6a..b53c827 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -40,6 +40,7 @@ class User < ApplicationRecord has_many :rooms, through: :room_members has_many :messages, dependent: :destroy + has_many :notifications, dependent: :destroy with_options presence: true do validates :name diff --git a/app/views/notification_mailer/complete.html.slim b/app/views/notification_mailer/complete.html.slim new file mode 100644 index 0000000..2f15be5 --- /dev/null +++ b/app/views/notification_mailer/complete.html.slim @@ -0,0 +1,7 @@ +h1 Twitterクローン通知 +- if @notification.action_type == 'Like' + = "#{@notification.sender.name}さんがあなたの投稿をいいねしました" +- elsif @notification.action_type == 'Comment' + = "#{@notification.sender.name}さんがあなたの投稿にコメントしました" +- elsif @notification.action_type == 'Repost' + = "#{@notification.sender.name}さんがあなたの投稿をリツイートしました" \ No newline at end of file diff --git a/app/views/notification_mailer/complete.text.slim b/app/views/notification_mailer/complete.text.slim new file mode 100644 index 0000000..8611b7d --- /dev/null +++ b/app/views/notification_mailer/complete.text.slim @@ -0,0 +1 @@ +' Notification#complete diff --git a/app/views/notifications/_notification.html.slim b/app/views/notifications/_notification.html.slim new file mode 100644 index 0000000..3e31e11 --- /dev/null +++ b/app/views/notifications/_notification.html.slim @@ -0,0 +1,42 @@ +- if notification.action_type == 'Like' + .card + .card-body.container-grid + = link_to post_path(notification.action.post), data: { turbo: false}, class: "row post text-dark" + .row + .col-1.text-center.d-flex.align-items-center + i.bi-heart-fill.already-liking[style="font-size: 2rem"] + .col + - if notification.sender.photo.attached? + = image_tag notification.sender.photo, class: "rounded-circle img-fluid", style: " max-width: 7%; height: auto;" + .row + .col + = link_to "#{notification.sender.name}さん", user_path(notification.sender), data: { turbo: false}, class: "text-dark text-decoration-none fw-bold" + = "があなたの投稿をいいねしました" +- elsif notification.action_type == 'Comment' + .card + .card-body.container-grid + = link_to post_path(notification.action.post), data: { turbo: false}, class: "row post text-dark" + .row + .col-1.text-center.d-flex.align-items-center + i.bi-chat.text-muted[style="font-size: 2rem"] + .col + - if notification.sender.photo.attached? + = image_tag notification.sender.photo, class: "rounded-circle img-fluid", style: " max-width: 7%; height: auto;" + .row + .col + = link_to "#{notification.sender.name}さん", user_path(notification.sender), data: { turbo: false}, class: "text-dark text-decoration-none fw-bold" + = "があなたの投稿にコメントしました" +- elsif notification.action_type == 'Repost' + .card + .card-body.container-grid + = link_to post_path(notification.action.post), data: { turbo: false}, class: "row post text-dark" + .row + .col-1.text-center.d-flex.align-items-center + i.bi-repeat.already-reposting[style="font-size: 2rem"] + .col + - if notification.sender.photo.attached? + = image_tag notification.sender.photo, class: "rounded-circle img-fluid", style: " max-width: 7%; height: auto;" + .row + .col + = link_to "#{notification.sender.name}さん", user_path(notification.sender), data: { turbo: false}, class: "text-dark text-decoration-none fw-bold" + = "があなたの投稿をリツイートしました" diff --git a/app/views/notifications/index.html.slim b/app/views/notifications/index.html.slim new file mode 100644 index 0000000..e402532 --- /dev/null +++ b/app/views/notifications/index.html.slim @@ -0,0 +1,11 @@ +.container-fluid + = render 'shared/navigation' + .row.flex-nowrap + = render 'shared/sidebar', :user => @user + .col + = render 'shared/flash' + h3.fw-bold.my-3 + | 通知 + = paginate @notifications + - @notifications.each do |notification| + = render notification diff --git a/app/views/shared/_sidebar.html.slim b/app/views/shared/_sidebar.html.slim index fe11f1e..052aa65 100644 --- a/app/views/shared/_sidebar.html.slim +++ b/app/views/shared/_sidebar.html.slim @@ -13,7 +13,7 @@ span.ms-1.d-none.d-sm-inline[style="font-size:1.5rem"] | 話題を検索 li - a.nav-link.align-middle.text-dark.px-0.d-flex.align-items-center[href="#"] + = link_to notifications_path, data: { turbo: false }, class: "nav-link align-middle text-dark px-0 d-flex align-items-center" i.fs-4.bi-bell.mx-2 span.ms-1.d-none.d-sm-inline[style="font-size:1.5rem"] | 通知 diff --git a/config/routes.rb b/config/routes.rb index 2b70c0a..d5654d5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,6 +22,7 @@ resources :comments, only: %i[create] resources :rooms, only: %i[index show] resources :messages, only: %i[create] + resources :notifications, only: %i[index] root 'posts#index' end diff --git a/db/migrate/20230928081221_create_notifications.rb b/db/migrate/20230928081221_create_notifications.rb new file mode 100644 index 0000000..09ccec5 --- /dev/null +++ b/db/migrate/20230928081221_create_notifications.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class CreateNotifications < ActiveRecord::Migration[7.0] + def change + create_table :notifications do |t| + t.references :user, null: false, foreign_key: true + t.references :action, polymorphic: true, null: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 93e588b..89e78e6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_09_26_121703) do +ActiveRecord::Schema[7.0].define(version: 2023_09_28_081221) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -92,6 +92,16 @@ t.index ["user_id"], name: "index_messages_on_user_id" end + create_table "notifications", force: :cascade do |t| + t.bigint "user_id", null: false + t.string "action_type", null: false + t.bigint "action_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["action_type", "action_id"], name: "index_notifications_on_action" + t.index ["user_id"], name: "index_notifications_on_user_id" + end + create_table "posts", force: :cascade do |t| t.text "content", null: false t.bigint "user_id", null: false @@ -172,6 +182,7 @@ add_foreign_key "likes", "users" add_foreign_key "messages", "rooms" add_foreign_key "messages", "users" + add_foreign_key "notifications", "users" add_foreign_key "posts", "users" add_foreign_key "reposts", "posts" add_foreign_key "reposts", "users" diff --git a/db/seeds.rb b/db/seeds.rb index 286b38e..7c6142b 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -36,7 +36,7 @@ room = Room.create! member_ids.each { |member_id| RoomMember.create!(user_id: member_id, room_id: room.id) } end - rand(0..15).times do |n| + rand(1..5).times do |n| Message.create!( user_id: member_ids.sample, room_id: room.id, @@ -65,7 +65,7 @@ user_id: user_ids.reject { |id| id == post.user_id }.sample, post_id: post.id ) - rand(0..10).times do |m| + rand(0..5).times do |m| Comment.create!( user_id: user_ids.reject { |id| id == post.user_id }.sample, post_id: post.id, diff --git a/spec/fixtures/notification_mailer/complete b/spec/fixtures/notification_mailer/complete new file mode 100644 index 0000000..83c246f --- /dev/null +++ b/spec/fixtures/notification_mailer/complete @@ -0,0 +1,3 @@ +NotificationMailer#complete + +Hi, find me in app/views/notification_mailer/complete diff --git a/spec/mailers/previews/notification_mailer_preview.rb b/spec/mailers/previews/notification_mailer_preview.rb new file mode 100644 index 0000000..1a71347 --- /dev/null +++ b/spec/mailers/previews/notification_mailer_preview.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# Preview all emails at http://localhost:3000/rails/mailers/notification_mailer +class NotificationMailerPreview < ActionMailer::Preview + # Preview this email at http://localhost:3000/rails/mailers/notification_mailer/complete + def complete + NotificationMailer.complete + end +end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb new file mode 100644 index 0000000..7804b5c --- /dev/null +++ b/spec/models/notification_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Notification, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/notifications_spec.rb b/spec/requests/notifications_spec.rb new file mode 100644 index 0000000..3cfb93d --- /dev/null +++ b/spec/requests/notifications_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Notifications', type: :request do + describe 'GET /index' do + it 'returns http success' do + get '/notifications/index' + expect(response).to have_http_status(:success) + end + end +end