diff --git a/app/assets/javascripts/private/conversations.coffee b/app/assets/javascripts/private/conversations.coffee new file mode 100644 index 0000000..24f83d1 --- /dev/null +++ b/app/assets/javascripts/private/conversations.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/stylesheets/partials/posts/branch_page.scss b/app/assets/stylesheets/partials/posts/branch_page.scss index 76f6bc7..4317f0b 100644 --- a/app/assets/stylesheets/partials/posts/branch_page.scss +++ b/app/assets/stylesheets/partials/posts/branch_page.scss @@ -95,6 +95,30 @@ .infinite-scroll { display: none; } + + .send-message-to-user { + background-color: $navbarColor; + padding: 10px; + color: white; + border-radius: 10px; + margin-top: 10px; + &:hover { + background-color: black; + color: white; + } + } + + .contact-user { + text-align: center; + } + + .contacted-user { + display: inline-block; + border-radius: 10px; + padding: 10px; + background-color: $navbarColor; + color: white; + } #branch-main-content { background: white; diff --git a/app/assets/stylesheets/private/conversations.scss b/app/assets/stylesheets/private/conversations.scss new file mode 100644 index 0000000..aa55a82 --- /dev/null +++ b/app/assets/stylesheets/private/conversations.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the private/conversations controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 146c44c..dbc1f22 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -17,7 +17,10 @@ def create end def show - @post = Post.find(params[:id]) + @post = Post.find(params[:id]) + if user_signed_in? + @message_has_been_sent = conversation_exist? + end end def study @@ -56,4 +59,8 @@ def get_posts }).call end + + def conversation_exist? + Private::Conversation.between_users(current_user.id, @post.user.id).present? + end end diff --git a/app/controllers/private/conversations_controller.rb b/app/controllers/private/conversations_controller.rb new file mode 100644 index 0000000..920570c --- /dev/null +++ b/app/controllers/private/conversations_controller.rb @@ -0,0 +1,33 @@ +class Private::ConversationsController < ApplicationController + def create + recipient_id = Post.find(params[:post_id]).user.id + @conversation = Private::Conversation.new(sender_id: current_user.id, + recipient_id: recipient_id) + if @conversation.save + Private::Message.create(user_id: recipient_id, + conversation_id: @conversation.id, + body: params[:message_body]) + + add_to_conversations unless already_added? + + respond_to do |format| + format.js {render partial: 'posts/show/contact_user/message_form/success'} + end + else + respond_to do |format| + format.js {render partial: 'posts/show/contact_user/message_form/fail'} + end + end + end + + private + + def add_to_conversations + session[:private_conversations] ||= [] + session[:private_conversations] << @conversation.id + end + + def already_added? + session[:private_conversations].include?(@conversation.id) + end +end diff --git a/app/helpers/posts_helper.rb b/app/helpers/posts_helper.rb index 947b9d6..620f78c 100644 --- a/app/helpers/posts_helper.rb +++ b/app/helpers/posts_helper.rb @@ -38,4 +38,20 @@ def update_pagination_partial_path 'posts/posts_pagination_page/remove_pagination' end end + + def contact_user_partial_path + if user_signed_in? + @post.user.id != current_user.id ? 'posts/show/contact_user' : 'shared/empty_partial' + else + 'posts/show/login_required' + end + end + + def leave_message_partial_path + if @message_has_been_sent + 'posts/show/contact_user/already_in_touch' + else + 'posts/show/contact_user/message_form' + end + end end diff --git a/app/helpers/private/conversations_helper.rb b/app/helpers/private/conversations_helper.rb new file mode 100644 index 0000000..88e6f5f --- /dev/null +++ b/app/helpers/private/conversations_helper.rb @@ -0,0 +1,13 @@ +module Private::ConversationsHelper + def private_conv_recipient(conversation) + conversation.opposed_user(current_user) + end + + def load_private_messages(conversation) + if conversation.messages.count > 0 + 'private/conversations/conversation/messages_list/link_to_previous_messages' + else + 'shared/empty_partial' + end + end +end diff --git a/app/models/private/conversation.rb b/app/models/private/conversation.rb new file mode 100644 index 0000000..e3122e1 --- /dev/null +++ b/app/models/private/conversation.rb @@ -0,0 +1,19 @@ +class Private::Conversation < ApplicationRecord + self.table_name = 'private_conversations' + + has_many :messages, + class_name: "Private::Message", + foreign_key: :conversation_id + belongs_to :sender, foreign_key: :sender_id, class_name: 'User' + belongs_to :recipient, foreign_key: :recipient_id, class_name: 'User' + + scope :between_users, -> (user1_id, user2_id) do + where(sender_id: user1_id, recipient_id: user2_id).or( + where(sender_id: user2_id, recipient_id: user1_id) + ) + end + + def opposed_user(user) + user == recipient ? sender : recipient + end +end diff --git a/app/models/private/message.rb b/app/models/private/message.rb new file mode 100644 index 0000000..c9edcf5 --- /dev/null +++ b/app/models/private/message.rb @@ -0,0 +1,8 @@ +class Private::Message < ApplicationRecord + self.table_name = 'private_messages' + + belongs_to :user + belongs_to :conversation, + class_name: "Private::Conversation", + foreign_key: :conversation_id +end diff --git a/app/models/user.rb b/app/models/user.rb index 38100ee..8a0f07c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,4 +5,8 @@ class User < ApplicationRecord :recoverable, :rememberable, :validatable has_many :posts, dependent: :destroy + has_many :private_messages, class_name: "Private::Message" + has_many :private_conversations, + foreign_key: :sender_id, + class_name: "Private::Conversation" end diff --git a/app/views/posts/show.html.erb b/app/views/posts/show.html.erb index bbfccfa..08030dc 100644 --- a/app/views/posts/show.html.erb +++ b/app/views/posts/show.html.erb @@ -4,6 +4,7 @@
Posted by <%= @post.user.name %>

<%= @post.title %>

<%= @post.content %>

+ <%= render contact_user_partial_path %> diff --git a/app/views/posts/show/_contact_user.html.erb b/app/views/posts/show/_contact_user.html.erb new file mode 100644 index 0000000..deb3669 --- /dev/null +++ b/app/views/posts/show/_contact_user.html.erb @@ -0,0 +1,3 @@ +
+ <%= render leave_message_partial_path %> +
\ No newline at end of file diff --git a/app/views/posts/show/_login_required.html.erb b/app/views/posts/show/_login_required.html.erb new file mode 100644 index 0000000..001efe4 --- /dev/null +++ b/app/views/posts/show/_login_required.html.erb @@ -0,0 +1,3 @@ +
+ To contact the user you have to <%= link_to 'Login', login_path %> +
\ No newline at end of file diff --git a/app/views/posts/show/contact_user/_already_in_touch.html.erb b/app/views/posts/show/contact_user/_already_in_touch.html.erb new file mode 100644 index 0000000..2350ff7 --- /dev/null +++ b/app/views/posts/show/contact_user/_already_in_touch.html.erb @@ -0,0 +1,3 @@ +
+ You are already in touch with this user +
\ No newline at end of file diff --git a/app/views/posts/show/contact_user/_message_form.html.erb b/app/views/posts/show/contact_user/_message_form.html.erb new file mode 100644 index 0000000..ee1b51a --- /dev/null +++ b/app/views/posts/show/contact_user/_message_form.html.erb @@ -0,0 +1,12 @@ +<%= form_tag({controller: "private/conversations", action: "create"}, + method: "post", + remote: true) do %> + <%= hidden_field_tag(:post_id, @post.id) %> + <%= text_area_tag(:message_body, + nil, + rows: 3, + class: 'form-control', + placeholder: 'Send a message to the user') %> + <%= submit_tag('Send a message', class: 'btn send-message-to-user') %> +<% end %> + diff --git a/app/views/posts/show/contact_user/message_form/_fail.js b/app/views/posts/show/contact_user/message_form/_fail.js new file mode 100644 index 0000000..25a1df2 --- /dev/null +++ b/app/views/posts/show/contact_user/message_form/_fail.js @@ -0,0 +1 @@ +$('.contact-user').replaceWith('
Message has not been sent
'); \ No newline at end of file diff --git a/app/views/posts/show/contact_user/message_form/_success.js.erb b/app/views/posts/show/contact_user/message_form/_success.js.erb new file mode 100644 index 0000000..d4e007b --- /dev/null +++ b/app/views/posts/show/contact_user/message_form/_success.js.erb @@ -0,0 +1,4 @@ +$('.contact-user').replaceWith('\ +
\ +
Message has been sent
\ +
'); diff --git a/app/views/private/conversations/_conversation.html.erb b/app/views/private/conversations/_conversation.html.erb new file mode 100644 index 0000000..5436324 --- /dev/null +++ b/app/views/private/conversations/_conversation.html.erb @@ -0,0 +1,20 @@ +<% @recipient = private_conv_recipient(conversation) %> +<% @is_messenger = false %> +
  • +
    + <%= render 'private/conversations/conversation/heading', + conversation: conversation %> + + +
    + <%= render 'private/conversations/conversation/messages_list', + conversation: conversation %> + <%= render 'private/conversations/conversation/new_message_form', + conversation: conversation, + user: user %> +
    +
    +
  • \ No newline at end of file diff --git a/app/views/private/conversations/conversation/_heading.html.erb b/app/views/private/conversations/conversation/_heading.html.erb new file mode 100644 index 0000000..6499303 --- /dev/null +++ b/app/views/private/conversations/conversation/_heading.html.erb @@ -0,0 +1,11 @@ +
    + <%= @recipient.name %> +
    + + +<%= link_to "X", + close_private_conversation_path(conversation), + class: 'close-conversation', + title: 'Close', + remote: true, + method: :post %> \ No newline at end of file diff --git a/app/views/private/conversations/conversation/_messages_list.html.erb b/app/views/private/conversations/conversation/_messages_list.html.erb new file mode 100644 index 0000000..3cc3c4e --- /dev/null +++ b/app/views/private/conversations/conversation/_messages_list.html.erb @@ -0,0 +1,9 @@ +
    + <%= render load_private_messages(conversation), conversation: conversation %> +
    + +
    + + +
    \ No newline at end of file diff --git a/app/views/private/conversations/conversation/messages_list/_link_to_previous_messages.html.erb b/app/views/private/conversations/conversation/messages_list/_link_to_previous_messages.html.erb new file mode 100644 index 0000000..22d7af5 --- /dev/null +++ b/app/views/private/conversations/conversation/messages_list/_link_to_previous_messages.html.erb @@ -0,0 +1,6 @@ +<%= link_to "Load messages", + private_messages_path(:conversation_id => conversation.id, + :messages_to_display_offset => @messages_to_display_offset, + :is_messenger => @is_messenger), + class: 'load-more-messages', + remote: true %> \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index fb13bc9..8965c43 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -15,4 +15,13 @@ get 'team' end end + + namespace :private do + resources :conversations, only: [:create] do + member do + post :close + end + end + resources :messages, only: [:index, :create] + end end diff --git a/db/migrate/20211211044212_create_private_conversations.rb b/db/migrate/20211211044212_create_private_conversations.rb new file mode 100644 index 0000000..0afed89 --- /dev/null +++ b/db/migrate/20211211044212_create_private_conversations.rb @@ -0,0 +1,14 @@ +class CreatePrivateConversations < ActiveRecord::Migration[5.1] + def change + create_table :private_conversations do |t| + t.integer :recipient_id + t.integer :sender_id + + t.timestamps + end + + add_index :private_conversations, :recipient_id + add_index :private_conversations, :sender_id + add_index :private_conversations, [:recipient_id, :sender_id], unique: true + end +end diff --git a/db/migrate/20211211044226_create_private_messages.rb b/db/migrate/20211211044226_create_private_messages.rb new file mode 100644 index 0000000..8125e44 --- /dev/null +++ b/db/migrate/20211211044226_create_private_messages.rb @@ -0,0 +1,12 @@ +class CreatePrivateMessages < ActiveRecord::Migration[5.1] + def change + create_table :private_messages do |t| + t.text :body + t.references :user, foreign_key: true + t.belongs_to :conversation, index: true + t.boolean :seen, default: false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 35fa3af..2b7d1c9 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.define(version: 20211111154915) do +ActiveRecord::Schema.define(version: 20211211044226) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -31,6 +31,27 @@ t.index ["user_id"], name: "index_posts_on_user_id" end + create_table "private_conversations", force: :cascade do |t| + t.integer "recipient_id" + t.integer "sender_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["recipient_id", "sender_id"], name: "index_private_conversations_on_recipient_id_and_sender_id", unique: true + t.index ["recipient_id"], name: "index_private_conversations_on_recipient_id" + t.index ["sender_id"], name: "index_private_conversations_on_sender_id" + end + + create_table "private_messages", force: :cascade do |t| + t.text "body" + t.bigint "user_id" + t.bigint "conversation_id" + t.boolean "seen", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["conversation_id"], name: "index_private_messages_on_conversation_id" + t.index ["user_id"], name: "index_private_messages_on_user_id" + end + create_table "users", force: :cascade do |t| t.string "name", default: "", null: false t.string "email", default: "", null: false @@ -44,4 +65,5 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + add_foreign_key "private_messages", "users" end diff --git a/spec/controllers/private/conversations_controller_spec.rb b/spec/controllers/private/conversations_controller_spec.rb new file mode 100644 index 0000000..70d5654 --- /dev/null +++ b/spec/controllers/private/conversations_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Private::ConversationsController, type: :controller do + +end diff --git a/spec/factories/private_conversations.rb b/spec/factories/private_conversations.rb new file mode 100644 index 0000000..1196e98 --- /dev/null +++ b/spec/factories/private_conversations.rb @@ -0,0 +1,17 @@ +FactoryGirl.define do + factory :private_conversation, class: 'Private::Conversation' do + association :recipient, factory: :user + association :sender, factory: :user + + factory :private_conversation_with_messages do + transient do + messages_count 1 + end + + after(:create) do |private_conversation, evaluator| + create_list(:private_message, evaluator.messages_count, + conversation: private_conversation) + end + end + end +end diff --git a/spec/factories/private_messages.rb b/spec/factories/private_messages.rb new file mode 100644 index 0000000..a1433e7 --- /dev/null +++ b/spec/factories/private_messages.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :private_message, class: 'Private::Message' do + body 'a'*20 + association :conversation, factory: :private_conversation + user + end +end diff --git a/spec/features/posts/contact_user_spec.rb b/spec/features/posts/contact_user_spec.rb new file mode 100644 index 0000000..a053414 --- /dev/null +++ b/spec/features/posts/contact_user_spec.rb @@ -0,0 +1,42 @@ +require "rails_helper" + +RSpec.feature "Contact user", :type => :feature do + let(:user) { create(:user) } + let(:category) { create(:category, name: 'Arts', branch: 'hobby') } + let(:post) { create(:post, category_id: category.id) } + + context 'logged in user' do + before(:each) do + sign_in user + end + + scenario "successfully sends a message to a post's author", js: true do + visit post_path(post) + expect(page).to have_selector('.contact-user form') + + fill_in('message_body', with: 'a'*20) + find('form .send-message-to-user').trigger('click') + + expect(page).not_to have_selector('.contact-user form') + expect(page).to have_selector('.contacted-user', + text: 'Message has been sent') + end + + scenario "sees an already contacted mesage", js: true do + create(:private_conversation_with_messages, + recipient_id: post.user.id, + sender_id: user.id) + + visit post_path(post) + expect(page).to have_selector('.contact-user .contacted-user', + text: 'You are already in touch with this user') + end + end + + context "user is not logged in" do + scenario "sees a login is required to contact user" do + visit post_path(post) + expect(page).to have_selector('div', text: 'To contact the user you have to Login') + end + end +end \ No newline at end of file diff --git a/spec/helpers/posts_helper_spec.rb b/spec/helpers/posts_helper_spec.rb index 915d8e9..4e74122 100644 --- a/spec/helpers/posts_helper_spec.rb +++ b/spec/helpers/posts_helper_spec.rb @@ -81,4 +81,44 @@ ) end end + context "#contact_user_partial_path" do + before(:each) do + @current_user = create(:user, id: 1) + helper.stub(:current_user).and_return(@current_user) + end + it "returns a contact user's partial path" do + helper.stub(:user_signed_in?).and_return(true) + assign(:post, create(:post, user_id: create(:user, id: 2).id)) + expect(helper.contact_user_partial_path).to( + eq 'posts/show/contact_user' + ) + end + it "returns an empty partial's path when post is from current user" do + helper.stub(:user_signed_in?).and_return(true) + assign(:post, create(:post, user_id: @current_user.id)) + expect(helper.contact_user_partial_path).to( + eq 'shared/empty_partial' + ) + end + it "returns login required path when no current user logged in" do + helper.stub(:user_signed_in?).and_return(false) + expect(helper.contact_user_partial_path).to( + eq 'posts/show/login_required' + ) + end + end + context "#leave_message_partial_path" do + it "returns an already in touch partial path" do + assign('message_has_been_sent', true) + expect(helper.leave_message_partial_path).to( + eq 'posts/show/contact_user/already_in_touch' + ) + end + it "returns a message form partial path" do + assign('message_has_been_sent', false) + expect(helper.leave_message_partial_path).to( + eq 'posts/show/contact_user/message_form' + ) + end + end end diff --git a/spec/helpers/private/conversations_helper_spec.rb b/spec/helpers/private/conversations_helper_spec.rb new file mode 100644 index 0000000..5cb5e04 --- /dev/null +++ b/spec/helpers/private/conversations_helper_spec.rb @@ -0,0 +1,29 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the Private::ConversationsHelper. For example: +# +# describe Private::ConversationsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe Private::ConversationsHelper, type: :helper do + context "#load_private_messages" do + let(:conversation) { create(:private_conversation) } + + it "returns load_messages partial path" do + create(:private_message, conversation_id: conversation.id) + expect(helper.load_private_messages(conversation)).to eq( + 'private/conversations/conversation/messages_list/link_to_previous_messages' + ) + end + it "returns empty partial when there are no private messages" do + expect(helper.load_private_messages(conversation)).to eq( + 'shared/empty_partial' + ) + end + end +end diff --git a/spec/models/private/conversation_spec.rb b/spec/models/private/conversation_spec.rb new file mode 100644 index 0000000..db0657f --- /dev/null +++ b/spec/models/private/conversation_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +RSpec.describe Private::Conversation, type: :model do + context "Scopes" do + it "gets a conversation between users" do + user1 = create(:user) + user2 = create(:user) + create(:private_conversation, recipient_id: user1.id, sender_id: user2.id) + conversation = Private::Conversation.between_users(user1.id, user2.id) + expect(conversation.count).to eq 1 + end + end + context "Methods" do + it "gets an opposed user of the conversation" do + user1 = create(:user) + user2 = create(:user) + conversation = create(:private_conversation, + recipient_id: user1.id, + sender_id: user2.id) + opposed_user = conversation.opposed_user(user1) + expect(opposed_user).to eq user2 + end + end +end diff --git a/spec/models/private/message_spec.rb b/spec/models/private/message_spec.rb new file mode 100644 index 0000000..fb899c9 --- /dev/null +++ b/spec/models/private/message_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Private::Message, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end