├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── denshobato.gemspec ├── lib ├── denshobato.rb ├── denshobato │ ├── extenders │ │ └── core.rb │ ├── helpers │ │ ├── controller_helper.rb │ │ ├── core_helper.rb │ │ ├── core_modules │ │ │ ├── blacklist_helper.rb │ │ │ ├── conversation_helper.rb │ │ │ └── message_helper.rb │ │ ├── helper_utils.rb │ │ ├── view_helper.rb │ │ └── view_messaging_helper.rb │ ├── models │ │ ├── blacklist.rb │ │ ├── conversation.rb │ │ ├── message.rb │ │ └── notification.rb │ └── version.rb └── generators │ └── denshobato │ ├── install_generator.rb │ └── migrations │ ├── create_blacklists.rb │ ├── create_conversations.rb │ ├── create_messages.rb │ └── create_notifications.rb └── spec ├── denshobato ├── extenders │ └── core_spec.rb ├── helpers │ ├── view_helper_spec.rb │ └── view_messaging_helper_spec.rb ├── models │ ├── blacklist_spec.rb │ ├── conversation_spec.rb │ ├── message_spec.rb │ └── notification_spec.rb └── user_spec.rb ├── factories ├── admins.rb ├── ducks.rb └── users.rb ├── spec_helper.rb └── spec_helpers └── conversation_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.2 4 | before_install: gem install bundler -v 1.11.2 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in denshobato.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Denshobato - Private messaging between models. 2 | 3 | ![alt text](http://i.imgur.com/NuhMPrg.png "Denshobato") 4 | 5 | [![Gem Version](https://badge.fury.io/rb/denshobato.svg)](https://badge.fury.io/rb/denshobato) 6 | [![Build Status](https://travis-ci.org/ID25/denshobato.svg?branch=master)](https://travis-ci.org/ID25/denshobato) 7 | 8 | Denshobato is a Rails gem that helps models communicate with each other. It gives simple api for creating a complete conversation system. You can create conversation with any model. Denshobato provides api methods for making conversation, messages, blacklists and trash. It also provides Helper methods for controller and view. 9 | *** 10 | 11 | ##### [Install Gem](#install_gem) 12 | ##### [Tutorial](#-tutorial) 13 | ##### [Conversation API](#conversation) 14 | ##### [Message API](#message) 15 | ##### [Trash API](#trash) 16 | ##### [BlackList API](#blacklist) 17 | ##### [Controller Herper API](#controller) 18 | ##### [View Herper API](#view) 19 | ##### [Extensions](#extensions-1) 20 | 21 | ### Requirements 22 | ``` 23 | Rails 4. 24 | 25 | Rails 3 isn't supported. 26 | Not ready for 5, while it is in Beta. 27 | ``` 28 | 29 | ### Install Gem 30 | 31 | ```ruby 32 | gem 'denshobato' 33 | ``` 34 | or 35 | 36 | ```gem install denshobato``` 37 | 38 | Then run installation: 39 | 40 | ```shell 41 | rails g denshobato:install 42 | ``` 43 | 44 | Run migrations 45 | 46 | ```shell 47 | rake db:migrate 48 | ``` 49 | 50 | Add this line to your model. This will make it able to send messages. 51 | ```ruby 52 | denshobato_for :your_class 53 | ``` 54 | 55 | ### Tutorial 56 | #### Create messaging system between reseller and customer. 57 | [Part 1](http://id25.github.io/2016/03/01/make-private-dialog-between-reseller-and-customer-part-1.html) 58 | 59 | [Part 2](http://id25.github.io/2016/03/02/make-private-dialog-between-reseller-and-customer-part-2.html) 60 | 61 | 62 | ### Example: 63 | ```ruby 64 | class User < ActiveRecord::Base 65 | denshobato_for :user 66 | end 67 | 68 | class Customer < ActiveRecord::Base 69 | denshobato_for :customer 70 | end 71 | ``` 72 | 73 | ```ruby 74 | @user.make_conversation_with(@customer).save 75 | 76 | @user.send_message('Hello', @customer) 77 | ``` 78 | 79 | You're ready! 80 | 81 | ### Conversations API 82 | 83 | #####Create conversation with user 84 | 85 | ```ruby 86 | current_user.make_conversation_with(customer) 87 | # => # 89 | 90 | # Example: 91 | # In your view: 92 | 93 | - @users.each do |user| 94 | = link_to user.email, user if current_user != user 95 | = button_to 'Start Conversation', start_conversation_path(id: user.id, class: user.class.name), 96 | class: 'btn btn-success' 97 | 98 | # Create route 99 | # We need specify controller, because by default rails search for denshobato_conversations 100 | post 'start_conversation', to: 'conversations#start_conversation', as: :start_conversation 101 | 102 | # And action 103 | def start_conversation 104 | recipient = params[:class].constantize.find(params[:id]) 105 | conversation = current_account.make_conversation_with(recipient) 106 | 107 | if conversation.save 108 | redirect_to :conversations 109 | end 110 | end 111 | ``` 112 | 113 | Another way to create a conversation. 114 | ```ruby 115 | 116 | # In your view 117 | # @conversation = current_user.hato_conversations.build 118 | 119 | = form_for @conversation, url: :conversations do |form| 120 | = fill_conversation_form(form, user) # => denshobato view helper, for conversation creating 121 | = f.submit 'Start Conversation', class: 'btn btn-primary' 122 | 123 | # Route, simple rest actions, i.e create for this. 124 | resources :denshobato_conversations, as: :conversations, 125 | path: 'conversations', controller: 'conversations' 126 | 127 | def create 128 | @conversation = current_account.hato_conversations.build(conversation_params) 129 | if @conversation.save 130 | redirect_to conversation_path(@conversation) 131 | else 132 | redirect_to :new, notice: 'Something went wrong' 133 | end 134 | end 135 | ``` 136 | 137 | #####Fetch all conversations, where you're present. 138 | 139 | ```ruby 140 | current_user.my_converstions 141 | # => #, 144 | # ]> 147 | ``` 148 | 149 | #####Find conversation with another user 150 | 151 | ```ruby 152 | current_user.find_conversation_with(customer) 153 | # => # 156 | ``` 157 | 158 | #####Return all your trashed conversation 159 | ```ruby 160 | current_user.trashed_conversations 161 | # => #]> 164 | ``` 165 | 166 | ##### Return all messages from conversation 167 | ```ruby 168 | @conversation.messages 169 | # => [#, 171 | # #] 173 | ``` 174 | 175 | ### Messages API 176 | 177 | ```ruby 178 | # This method sends message directly to the recipient 179 | # Takes responsibility to create conversation if it doesn`t exist yet or sends message to an existing conversation 180 | 181 | # Important! 182 | # After each created message, send notification 183 | if @message.save 184 | @message.send_notification(@conversation.id) 185 | end 186 | # See example below 187 | 188 | msg = current_user.send_message(body: 'Hello', recipient) 189 | # => # 191 | msg.save 192 | ``` 193 | 194 | Another way - send message directly to a conversation 195 | ```ruby 196 | current_user.send_message_to(conversation.id, body: 'Hello') 197 | 198 | # Example 199 | # @message_form = current_user.hato_messages.build 200 | 201 | = form_for @message_form, url: :messages do |form| 202 | = form.text_field :body, class: 'form-control' 203 | = fill_message_form(form, current_account, @conversation.id) # => denshobato helper, for message creating 204 | = form.submit 'Send message', class: 'btn btn-primary' 205 | 206 | # Controller 207 | def create 208 | conversation_id = params[:denshobato_message][:conversation_id] 209 | @message = current_account.send_message_to(conversation_id, message_params) 210 | 211 | if @message.save 212 | # Important, send notifications after save message 213 | @message.send_notification(conversation_id) 214 | redirect_to conversation_path(conversation_id) 215 | else 216 | render :new, notice: 'Error' 217 | end 218 | end 219 | ``` 220 | 221 | ### Trash API 222 | 223 | ##### Move conversation to trash and remove it out of there 224 | ```ruby 225 | # @conversation.to_trash 226 | # @conversation.from_tash 227 | 228 | #Example 229 | # In your view 230 | - @conversations.each do |room| 231 | = link_to "Conversation with #{room.recipient.email}", conversation_path(room) 232 | = button_to 'Move to Trash', to_trash_path(id: room), class: 'btn btn-warning', method: :patch 233 | = button_to 'Move from Trash', from_trash_path(id: room), class: 'btn btn-warning', method: :patch 234 | 235 | # Route 236 | patch :to_trash, to: 'conversations#to_trash', as: :to_trash 237 | patch :from_trash, to: 'conversations#from_trash', as: :from_trash 238 | 239 | # In your conversation controller 240 | %w(to_trash from_trash).each do |name| 241 | define_method name do 242 | room = Denshobato::Conversation.find(params[:id]) 243 | room.send(name) 244 | redirect_to :conversations 245 | end 246 | end 247 | ``` 248 | 249 | ### BlackList API 250 | 251 | ```ruby 252 | # current_user.add_to_blacklist(customer) 253 | # current_user.remove_from_blacklist(customer) 254 | 255 | - @users.each do |user| 256 | = link_to user.email, user if current_account != user 257 | - if user_in_black_list?(current_account, user) 258 | p This user in your blacklist 259 | = button_to 'Remove from black list', remove_from_blacklist_path(user: user, 260 | klass: user.class.name), class: 'btn btn-info' 261 | - else 262 | = button_to 'Add to black list', black_list_path(user: user, klass: user.class.name), 263 | class: 'btn btn-danger' 264 | 265 | # Routes 266 | post :black_list, to: 'users#add_to_blacklist', as: :black_list 267 | post :remove_from_blacklist, to: 'users#remove_from_blacklist', as: :remove_from_blacklist 268 | 269 | # Controller 270 | [%w(add_to_blacklist save), %w(remove_from_blacklist destroy)].each do |name, action| 271 | define_method name do 272 | user = params[:klass].constantize.find(params[:user]) 273 | record = current_account.send(name, user) 274 | record.send(action) ? (redirect_to :users) : (redirect_to :root) 275 | end 276 | end 277 | ``` 278 | 279 | ### Controller Helpers 280 | 281 | Check if user is already in conversation 282 | ```ruby 283 | # user_in_conversation?(current_user, room) 284 | 285 | # Example 286 | @conversation = Denshobato::Conversation.find(params[:id]) 287 | unless user_in_conversation?(current_user, @conversation) 288 | redirect_to :conversations, notice: 'You can`t join this conversation' 289 | end 290 | ``` 291 | 292 | Check if sender and recipient already have conversation together. 293 | ```ruby 294 | # conversation_exists?(sender, recipient) 295 | 296 | if conversation_exists?(current_user, @customer) 297 | do_somthing 298 | end 299 | ``` 300 | 301 | Check if user can create conversation with other user 302 | ```ruby 303 | # can_create_conversation?(sender, recipient) 304 | 305 | if can_create_conversation?(current_user, @customer) 306 | @conversation_form = ... 307 | end 308 | ``` 309 | 310 | 311 | ### View Helpers 312 | 313 | Check if conversation exists, return `true` or `false` 314 | ```ruby 315 | # @conversation = current_user.find_conversation(@user) 316 | 317 | - if conversation_exists?(current_user, @user) 318 | = link_to 'Open chat', your_path(@conversation) 319 | ``` 320 | 321 | Check if user can create conversation with another user 322 | ```ruby 323 | # can_create_conversation?(sender, recipient) 324 | 325 | - if can_create_conversation?(current_user, @customer) 326 | = link_to 'Start Conversation', your_path... 327 | ``` 328 | 329 | Check if recipient is in blacklist 330 | ```ruby 331 | # user_in_black_list?(sender, recipient) 332 | 333 | - if user_in_black_list?(current_user, @customer) 334 | = button_to 'Remove from black list', remove_path... 335 | ``` 336 | 337 | Show name of recipient in conversation list 338 | ```ruby 339 | - @conversations.includes(:sender).each do |room| 340 | = link_to "Conversation with: #{interlocutor_name(current_user, room, :first_name, :last_name)}", 341 | conversation_path(room) 342 | 343 | # => Conversation with: John Doe 344 | ``` 345 | 346 | Show avatar for recipient 347 | ```ruby 348 | = interlocutor_avatar(current_user, :user_avatar, @conversation, 'img-responsive') 349 | 350 | # => 351 | ``` 352 | 353 | Show the last message, it's author and his avatar 354 | ```ruby 355 | = "Last message: #{room.messages.last.try(:body)}" 356 | = "#{interlocutor_image(room.messages.last.try(:author), :user_avatar, 'img-circle')}" 357 | = "Last message from: #{message_from(room.messages.last, :first_name, :last_name)}" 358 | ``` 359 | 360 | Same inside of a conversation 361 | ```ruby 362 | - @messages.includes(:author).each do |msg| 363 | p = interlocutor_info(msg.author, :fist_name, :last_name) 364 | = interlocutor_image(msg.author, :user_avatar, 'img-circle') 365 | p = msg.body 366 | hr 367 | ``` 368 | 369 | ### Pagination 370 | If you use Kaminari, or Will Paginate, just follow their guide. 371 | 372 | Example: 373 | ```ruby 374 | @messages = @conversation.messages.page(params[:page]).per(25) # => Kaminari 375 | @messages = @conversation.messages.page(params[:page]).per_page(25) # => Will Paginate 376 | ``` 377 | 378 | And in your view 379 | ```ruby 380 | = paginate @messages # => Kaminari 381 | = will_paginate @messages # => Will Paginate 382 | ``` 383 | 384 | *** 385 | 386 | ### Extensions 387 | ![alt text](http://i.imgur.com/0sUUfDl.jpg "Screen") 388 | Denshobato has addon [denshobato_chat_panel](https://github.com/ID25/denshobato_chat_panel). This is simple chat panel for you. If you don't need any special customization for dialog panel, or if you want to try messaging quickly, you can use chat panel. 389 | 390 | That`s all for now. 391 | 392 | ## Upcoming features 393 | + Conference 394 | + Read/Unread messages 395 | 396 | ## Issues 397 | 398 | If you've found a bug, or have proposal/feature request, create an issue with your thoughts. 399 | [Denshobato Issues](https://github.com/ID25/denshobato/issues) 400 | 401 | ## Contributing 402 | 403 | + Fork it 404 | + Create your feature branch (git checkout -b my-new-feature) 405 | + Write tests for new feature/bug fix 406 | + Make sure that tests pass and everything works like a charm 407 | + Commit your changes (git commit -am 'Added some feature') 408 | + Push to the branch (git push origin my-new-feature) 409 | + Create new Pull Request 410 | 411 | ## The MIT License (MIT) 412 | 413 | #### Denshobato - Private messaging between models. 414 | ![alt text](http://i.imgur.com/bo7kj7d.png "Denshobato") 415 | 416 | Copyright (c) 2016 Eugene Domosedov (ID25) 417 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'bundler/setup' 4 | require 'denshobato' 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require 'irb' 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /denshobato.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'denshobato/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'denshobato' 8 | spec.version = Denshobato::VERSION 9 | spec.authors = ['ID25'] 10 | spec.email = ['xid25x@gmail.com'] 11 | 12 | spec.summary = 'Denshobato - private messaging between models' 13 | spec.description = 'Denshobato - private messaging between models' 14 | spec.homepage = 'https://github.com/ID25/denshobato' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = 'exe' 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ['lib'] 21 | 22 | spec.add_runtime_dependency 'rails', '>= 4.0.0' 23 | 24 | spec.add_development_dependency 'bundler', '~> 1.11' 25 | spec.add_development_dependency 'rake', '~> 10.0' 26 | spec.add_development_dependency 'rspec', '~> 3.0' 27 | spec.add_development_dependency 'activerecord' 28 | spec.add_development_dependency 'sqlite3' 29 | spec.add_development_dependency 'database_cleaner' 30 | spec.add_development_dependency 'factory_girl', '~> 4.0' 31 | spec.add_development_dependency 'shoulda-matchers', '~> 3.1' 32 | end 33 | -------------------------------------------------------------------------------- /lib/denshobato.rb: -------------------------------------------------------------------------------- 1 | require 'denshobato/version' 2 | 3 | # Helpers 4 | Denshobato.autoload :HelperUtils, 'denshobato/helpers/helper_utils' 5 | Denshobato.autoload :ViewHelper, 'denshobato/helpers/view_helper' 6 | Denshobato.autoload :ControllerHelper, 'denshobato/helpers/controller_helper' 7 | 8 | # View Helpers for messaging 9 | Denshobato.autoload :ViewMessagingHelper, 'denshobato/helpers/view_messaging_helper' 10 | 11 | module Denshobato 12 | if defined?(ActiveRecord::Base) 13 | require 'denshobato/extenders/core' # denshobato_for method 14 | 15 | # Active Record Models 16 | Denshobato.autoload :Conversation, 'denshobato/models/conversation' 17 | Denshobato.autoload :Message, 'denshobato/models/message' 18 | Denshobato.autoload :Notification, 'denshobato/models/notification' 19 | Denshobato.autoload :Blacklist, 'denshobato/models/blacklist' 20 | 21 | # Add helper methods to core model 22 | Denshobato.autoload :ConversationHelper, 'denshobato/helpers/core_modules/conversation_helper' 23 | Denshobato.autoload :MessageHelper, 'denshobato/helpers/core_modules/message_helper' 24 | Denshobato.autoload :BlacklistHelper, 'denshobato/helpers/core_modules/blacklist_helper' 25 | Denshobato.autoload :CoreHelper, 'denshobato/helpers/core_helper' 26 | 27 | ActiveRecord::Base.extend Denshobato::Extenders::Core 28 | end 29 | 30 | # Include Helpers 31 | ActionView::Base.include Denshobato::ViewHelper if defined?(ActionView::Base) 32 | ActionController::Base.include Denshobato::ControllerHelper if defined?(ActionController::Base) 33 | end 34 | -------------------------------------------------------------------------------- /lib/denshobato/extenders/core.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | module Extenders 3 | module Core 4 | def denshobato_for(_klass) 5 | # Adds associations and methods to messagable model 6 | 7 | adds_methods_to_model 8 | end 9 | 10 | private 11 | 12 | def adds_methods_to_model 13 | include Denshobato::CoreHelper # Adds helper methods for the core model 14 | 15 | # Adds has_many association for a model, to allow it to create conversations 16 | class_eval do 17 | # Add conversations 18 | has_many :denshobato_conversations, as: :sender, class_name: '::Denshobato::Conversation', dependent: :destroy 19 | 20 | # Add messages 21 | has_many :denshobato_messages, as: :author, class_name: '::Denshobato::Message', dependent: :destroy 22 | 23 | # Add blacklists 24 | has_many :denshobato_blacklists, as: :blocker, class_name: '::Denshobato::Blacklist', dependent: :destroy 25 | 26 | # Added alias for the sake of brevity 27 | alias_method :hato_conversations, :denshobato_conversations 28 | alias_method :hato_messages, :denshobato_messages 29 | alias_method :blacklist, :denshobato_blacklists 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/denshobato/helpers/controller_helper.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | module ControllerHelper 3 | include Denshobato::HelperUtils 4 | 5 | def user_in_conversation?(user, room) 6 | # redirect_to :root, notice: 'You can`t join this conversation unless user_in_conversation?(current_account, @conversation)' 7 | 8 | hato_conversation.where(id: room.id, sender: user).present? || hato_conversation.where(id: room.id, recipient: user).present? 9 | end 10 | 11 | def conversation_exists?(sender, recipient) 12 | # Check if sender and recipient already have conversation together. 13 | 14 | hato_conversation.find_by(sender: sender, recipient: recipient) 15 | end 16 | 17 | def can_create_conversation?(sender, recipient) 18 | # If current sender is current recipient, return false 19 | 20 | sender == recipient ? false : true 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/denshobato/helpers/core_helper.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | module CoreHelper 3 | include Denshobato::HelperUtils # Useful helpers 4 | include Denshobato::ConversationHelper # Methods of Conversation model 5 | include Denshobato::MessageHelper # Methods of Message model 6 | include Denshobato::BlacklistHelper # Methods of BlackList model 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/denshobato/helpers/core_modules/blacklist_helper.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | module BlacklistHelper 3 | def add_to_blacklist(user) 4 | # Add user to blacklist 5 | # User can`t create conversation or send message to a blocked model 6 | 7 | blacklist.build(blocked: user) 8 | end 9 | 10 | def remove_from_blacklist(user) 11 | # Remove user from blacklist 12 | 13 | hato_blacklist.find_by(blocker: self, blocked: user) 14 | end 15 | 16 | def my_blacklist 17 | # Show blocked users 18 | 19 | blacklist.includes(:blocked) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/denshobato/helpers/core_modules/conversation_helper.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | module ConversationHelper 3 | def my_conversations 4 | # Return active user conversations (which is not in trash) 5 | 6 | trashed = block_given? ? yield : false 7 | hato_conversation.my_conversations(self, trashed) 8 | end 9 | 10 | def trashed_conversations 11 | # Return trashed conversations 12 | 13 | my_conversations { true } # => hato_conversation.where trashed: true 14 | end 15 | 16 | def make_conversation_with(recipient) 17 | # Build conversation 18 | # = form_for current_user.make_conversation_with(recipient) do |f| 19 | # = f.submit 'Start Chat', class: 'btn btn-primary' 20 | 21 | hato_conversations.build(recipient: recipient) 22 | end 23 | 24 | def find_conversation_with(user) 25 | # Return an existing conversation between sender and recipient 26 | 27 | hato_conversation.find_by(sender: self, recipient: user) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/denshobato/helpers/core_modules/message_helper.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | module MessageHelper 3 | def send_message(text, recipient) 4 | # This method sends message directly to the recipient 5 | # Takes responsibility to create conversation if it doesn`t exist yet 6 | # sends message to an existing conversation 7 | 8 | # Find conversation. 9 | room = hato_conversation.find_by(sender: self, recipient: recipient) 10 | 11 | # If conversation doesn`t exist, create one. 12 | conversation = room.nil? ? create_conversation(self, recipient) : room 13 | 14 | # Return validation error, if conversation is a String (see create_conversation method) 15 | return errors.add(:blacklist, conversation) if conversation.is_a?(String) 16 | 17 | # Create message for this conversation. 18 | send_message_to(conversation.id, body: text) 19 | end 20 | 21 | def send_message_to(id, params) 22 | # This method sends message directly to conversation 23 | 24 | return errors.add(:message, 'Conversation not present') unless id 25 | 26 | # Expect record id 27 | # If id == active record object, get it`s id 28 | id = id.id if id.is_a?(ActiveRecord::Base) 29 | 30 | room = hato_conversation.find(id) 31 | 32 | # Show validation error if the author of a message is not in conversation 33 | return message_error(id, self) unless user_in_conversation(room, self) 34 | 35 | # If everything is ok, build message 36 | hato_messages.build(params) 37 | end 38 | 39 | private 40 | 41 | def create_conversation(sender, recipient) 42 | room = sender.make_conversation_with(recipient) 43 | 44 | # Get validation error 45 | room.valid? ? room.save && room : room.errors[:blacklist].join('') 46 | end 47 | 48 | def message_error(id, author) 49 | # TODO: Return validation error in the most efficient way 50 | 51 | author.hato_messages.build(conversation_id: id) 52 | end 53 | 54 | def user_in_conversation(room, author) 55 | # Check if user is in conversation as sender or recipient 56 | 57 | hato_conversation.where(id: room.id, sender: author).present? || hato_conversation.where(id: room.id, recipient: author).present? 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/denshobato/helpers/helper_utils.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | module HelperUtils 3 | private 4 | 5 | def class_name(klass) 6 | klass.class.name 7 | end 8 | 9 | def hato_conversation 10 | Denshobato::Conversation 11 | end 12 | 13 | def hato_message 14 | Denshobato::Message 15 | end 16 | 17 | def hato_blacklist 18 | Denshobato::Blacklist 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/denshobato/helpers/view_helper.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | module ViewHelper 3 | include Denshobato::HelperUtils 4 | include Denshobato::ViewMessagingHelper 5 | 6 | def conversation_exists?(sender, recipient) 7 | # Check if sender and recipient already have conversation together. 8 | 9 | hato_conversation.find_by(sender: sender, recipient: recipient) 10 | end 11 | 12 | def can_create_conversation?(sender, recipient) 13 | # If current sender is current recipient, return false 14 | 15 | sender == recipient ? false : true 16 | end 17 | 18 | def user_in_black_list?(blocker, blocked) 19 | hato_blacklist.where(blocker: blocker, blocked: blocked).present? 20 | end 21 | 22 | def devise_url_helper(action, user, controller) 23 | # Polymorphic devise urls 24 | # E.g, you have two models, seller and customer 25 | # You can create helper (like current_account) 26 | # Use this method for url's 27 | 28 | # devise_url_helper(:edit, current_account, :registration) 29 | # => :edit_seller_registration, or :edit_customer_registration 30 | 31 | "#{action}_#{user.class.name.downcase}_#{controller}".to_sym 32 | end 33 | 34 | def fill_conversation_form(form, recipient) 35 | # = form_for @conversation do |form| 36 | ### = fill_conversation_form(form, @conversation) 37 | ### = f.submit 'Start Chating', class: 'btn btn-primary' 38 | 39 | recipient_id = form.hidden_field :recipient_id, value: recipient.id 40 | recipient_type = form.hidden_field :recipient_type, value: recipient.class.name 41 | 42 | recipient_id + recipient_type 43 | end 44 | 45 | def fill_message_form(form, user, room_id) 46 | # @message = current_user.build_conversation_message(@conversation) 47 | # = form_for [@conversation, @message] do |form| 48 | ### = form.text_field :body 49 | ### = fill_message_form(form, @message) 50 | ### = form.submit 51 | 52 | room_id = room_id.id if room_id.is_a?(ActiveRecord::Base) 53 | 54 | sender_id = form.hidden_field :sender_id, value: user.id 55 | sender_class = form.hidden_field :sender_type, value: user.class.name 56 | conversation_id = form.hidden_field :conversation_id, value: room_id 57 | 58 | sender_id + sender_class + conversation_id 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/denshobato/helpers/view_messaging_helper.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | module ViewMessagingHelper 3 | # OPTIMIZE: Metaprogram interlocutors methods. 4 | 5 | def interlocutor_avatar(user, image_column, conversation, css_class) 6 | sender = conversation.sender 7 | recipient = conversation.recipient 8 | 9 | return show_image(sender, image_column, css_class) if user == sender 10 | return show_image(recipient, image_column, css_class) if user == recipient 11 | end 12 | 13 | def interlocutor_name(user, conversation, *fields) 14 | sender = conversation.sender 15 | recipient = conversation.recipient 16 | 17 | return show_filter(sender, fields) if fields.any? && user == sender 18 | return show_filter(recipient, fields) if fields.any? && user == recipient 19 | end 20 | 21 | def message_from(message, *fields) 22 | # Show information about message creator 23 | 24 | return unless message 25 | show_filter(message.author, fields) 26 | end 27 | 28 | def interlocutor_info(klass, *fields) 29 | show_filter(klass, fields) 30 | end 31 | 32 | def interlocutor_image(user, column, css_class) 33 | show_image(user, column, css_class) 34 | end 35 | 36 | private 37 | 38 | def show_image(user, image, css_class) 39 | # Show image_tag with user avatar and css class 40 | 41 | image_tag(user.try(image) || '', class: css_class) 42 | end 43 | 44 | def show_filter(klass, fields) 45 | # Adds fields to View 46 | # h3 = "Conversation with: #{interlocutor_name(user, conversation, :first_name, :last_name)}" 47 | # => Conversation with John Doe 48 | 49 | fields.each_with_object([]) { |field, array| array << klass.send(:try, field) }.join(' ').strip 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/denshobato/models/blacklist.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | class Blacklist < ::ActiveRecord::Base 3 | self.table_name = 'denshobato_blacklists' 4 | 5 | # Set up polymorphic association 6 | belongs_to :blocker, polymorphic: true 7 | belongs_to :blocked, polymorphic: true 8 | 9 | # Validation 10 | validates :blocker_id, :blocker_type, uniqueness: { scope: [:blocked_id, :blocked_type], message: 'User already in your blacklist' } 11 | validates :blocked_id, :blocked_type, :blocker_id, :blocker_type, presence: true 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/denshobato/models/conversation.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | class Conversation < ::ActiveRecord::Base 3 | include Denshobato::HelperUtils 4 | 5 | self.table_name = 'denshobato_conversations' 6 | 7 | # Set-up Polymorphic association 8 | belongs_to :sender, polymorphic: true 9 | belongs_to :recipient, polymorphic: true 10 | 11 | # Has Many association 12 | has_many :denshobato_notifications, class_name: '::Denshobato::Notification', dependent: :destroy 13 | 14 | # Validate fields 15 | validates :sender_id, :sender_type, :recipient_id, :recipient_type, presence: true 16 | validate :conversation_uniqueness, on: :create 17 | before_validation :check_sender # Sender can't create conversation with himself 18 | before_validation :blocked_user # Check if blocked user tries to start conversation 19 | 20 | # Callbacks 21 | after_create :recipient_conversation # Create conversation for recipient, where he is sender. 22 | after_destroy :remove_messages, if: :both_conversation_removed? # Remove messages and notifications 23 | 24 | # Scopes 25 | scope :my_conversations, lambda { |user, bool| 26 | bool ? where(trashed: bool, sender: user).order(updated_at: :desc) : includes(:recipient).where(trashed: bool, sender: user).order(updated_at: :desc) 27 | } 28 | 29 | # Methods 30 | def messages 31 | # Return all messages of conversation 32 | 33 | ids = notifications.pluck(:message_id) 34 | hato_message.where(id: ids) 35 | end 36 | 37 | def to_trash 38 | # Move conversation to trash 39 | 40 | bool = block_given? ? yield : true 41 | update(trashed: bool) 42 | end 43 | 44 | def from_trash 45 | # Move conversation from trash 46 | 47 | to_trash { false } 48 | end 49 | 50 | # Alias 51 | alias notifications denshobato_notifications 52 | 53 | private 54 | 55 | def recipient_conversation 56 | if hato_conversation.where(recipient: sender, sender: recipient).present? 57 | errors.add(:conversation, 'You already have conversation with this user') 58 | else 59 | recipient.make_conversation_with(sender).save 60 | end 61 | end 62 | 63 | def check_sender 64 | errors.add(:conversation, 'You can`t create conversation with yourself') if sender == recipient 65 | end 66 | 67 | def conversation_uniqueness 68 | # Check conversation for uniqueness, when recipient is sender, and vice versa. 69 | 70 | hash = Hash[*columns.flatten] # => { sender_id: 1, sender_type: 'User' ... } 71 | 72 | errors.add(:conversation, 'You already have conversation with this user.') if hato_conversation.where(hash).present? 73 | end 74 | 75 | def remove_messages 76 | # When sender and recipient remove their conversation together 77 | # remove all messages and notifications belonging to this conversation 78 | 79 | hato_message.where(id: messages.map(&:id)).destroy_all 80 | notifications.destroy_all 81 | end 82 | 83 | def both_conversation_removed? 84 | # Check when both conversations are removed 85 | 86 | hato_conversation.where(sender: recipient, recipient: sender).empty? 87 | end 88 | 89 | def blocked_user 90 | if hato_blacklist.where(blocker: recipient, blocked: sender).present? 91 | errors.add(:blacklist, 'You`re in blacklist') 92 | end 93 | 94 | if hato_blacklist.where(blocker: sender, blocked: recipient).present? 95 | errors.add(:blacklist, 'Remove user from blacklist, to start conversation') 96 | end 97 | end 98 | 99 | def columns 100 | [['sender_id', sender_id], ['sender_type', sender_type], ['recipient_id', recipient_id], ['recipient_type', recipient_type]] 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/denshobato/models/message.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | class Message < ::ActiveRecord::Base 3 | attr_accessor :conversation_id 4 | 5 | self.table_name = 'denshobato_messages' 6 | 7 | # Associations 8 | belongs_to :author, polymorphic: true 9 | has_many :denshobato_notifications, class_name: '::Denshobato::Notification' 10 | 11 | # Validations 12 | before_validation :access_to_posting_message 13 | validates :body, :author_id, :author_type, presence: true 14 | 15 | # Callbacks 16 | before_destroy :skip_deleting_messages, if: :message_belongs_to_conversation? 17 | 18 | # Alias 19 | alias notifications denshobato_notifications 20 | 21 | # Methods 22 | def send_notification(id) 23 | # Find current conversation 24 | conversation = hato_conversation.find(id) 25 | 26 | # Create Notifications 27 | create_notifications_for(conversation) 28 | end 29 | 30 | def message_time 31 | # Formatted time for chat panel 32 | 33 | created_at.strftime('%a %b %d | %I:%M %p') 34 | end 35 | 36 | private 37 | 38 | def skip_deleting_messages 39 | errors.add(:base, 'Can`t delete message, as long as it belongs to the conversation') 40 | 41 | # The before_destroy callback needs a true/false value to determine whether or not to proceeed 42 | false 43 | end 44 | 45 | def access_to_posting_message 46 | return unless conversation_id 47 | 48 | room = hato_conversation.find(conversation_id) 49 | 50 | # If author of message is not present in conversation, show error 51 | 52 | errors.add(:message, 'You can`t post to this conversation') unless user_in_conversation(room, author) 53 | end 54 | 55 | def create_notifications_for(conversation) 56 | # Take sender and recipient 57 | sender = conversation.sender 58 | recipient = conversation.recipient 59 | 60 | # Find conversation where sender it's recipient 61 | conversation_2 = recipient.find_conversation_with(sender) 62 | 63 | # If recipient deletes conversation, create it for him 64 | conversation_2 = create_conversation_for_recipient(sender, recipient) if conversation_2.nil? 65 | 66 | # Send notifications for new messages to sender and recipient 67 | [conversation.id, conversation_2.id].each { |id| notifications.create(conversation_id: id) } 68 | end 69 | 70 | def user_in_conversation(room, author) 71 | # Check if user is already in conversation 72 | 73 | hato_conversation.where(id: room.id, sender: author).present? || hato_conversation.where(id: room.id, recipient: author).present? 74 | end 75 | 76 | def create_conversation_for_recipient(sender, recipient) 77 | # Create Conversation for recipient 78 | # Skip callbacks, because conversation for sender exists already 79 | 80 | conv = hato_conversation.new(sender: recipient, recipient: sender) 81 | hato_conversation.skip_callback(:create, :after, :recipient_conversation) 82 | conv.save 83 | conv 84 | end 85 | 86 | def message_belongs_to_conversation? 87 | # Check if message has live notifications for any conversation 88 | 89 | hato_conversation.where(id: notifications.pluck(:conversation_id)).present? 90 | end 91 | 92 | def hato_conversation 93 | Denshobato::Conversation 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/denshobato/models/notification.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | class Notification < ::ActiveRecord::Base 3 | self.table_name = 'denshobato_notifications' 4 | 5 | # Associations 6 | belongs_to :denshobato_message, class_name: 'Denshobato::Message', foreign_key: 'message_id', dependent: :destroy 7 | belongs_to :denshobato_conversation, class_name: 'Denshobato::Conversation', foreign_key: 'conversation_id', touch: true 8 | 9 | # Validations 10 | validates :message_id, :conversation_id, presence: true 11 | validates :message_id, uniqueness: { scope: :conversation_id } 12 | 13 | # Aliases 14 | alias message denshobato_message 15 | alias conversation denshobato_conversation 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/denshobato/version.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | VERSION = '0.0.2'.freeze 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/denshobato/install_generator.rb: -------------------------------------------------------------------------------- 1 | module Denshobato 2 | module Generators 3 | class InstallGenerator < Rails::Generators::Base 4 | source_root File.dirname(__FILE__) 5 | desc 'Add the migrations' 6 | 7 | def copy_conversations 8 | p 'Copying migrations' 9 | 10 | copy_file './migrations/create_conversations.rb', "db/migrate/#{time}_create_denshobato_conversations.rb" 11 | end 12 | 13 | def copy_messages 14 | sleep 1 15 | copy_file './migrations/create_messages.rb', "db/migrate/#{time}_create_denshobato_messages.rb" 16 | end 17 | 18 | def copy_notifications 19 | sleep 1 20 | copy_file './migrations/create_notifications.rb', "db/migrate/#{time}_create_denshobato_notifications.rb" 21 | end 22 | 23 | def copy_blacklists 24 | sleep 1 25 | copy_file './migrations/create_blacklists.rb', "db/migrate/#{time}_create_denshobato_blacklists.rb" 26 | end 27 | 28 | def done 29 | puts 'Denshobato Installed' 30 | puts 'Run rake db:migrate' 31 | end 32 | 33 | private 34 | 35 | def time 36 | DateTime.now.to_s(:number) 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/generators/denshobato/migrations/create_blacklists.rb: -------------------------------------------------------------------------------- 1 | class CreateDenshobatoBlacklists < ActiveRecord::Migration 2 | def change 3 | create_table :denshobato_blacklists do |t| 4 | t.references :blocker, polymorphic: true, index: { name: 'blocker_user' } 5 | t.references :blocked, polymorphic: true, index: { name: 'blocked_user' } 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/generators/denshobato/migrations/create_conversations.rb: -------------------------------------------------------------------------------- 1 | class CreateDenshobatoConversations < ActiveRecord::Migration 2 | def change 3 | create_table :denshobato_conversations do |t| 4 | t.boolean :trashed, default: false 5 | t.references :sender, polymorphic: true, index: { name: 'conversation_polymorphic_sender' } 6 | t.references :recipient, polymorphic: true, index: { name: 'conversation_polymorphic_recipient' } 7 | 8 | t.timestamps null: false 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/generators/denshobato/migrations/create_messages.rb: -------------------------------------------------------------------------------- 1 | class CreateDenshobatoMessages < ActiveRecord::Migration 2 | def change 3 | create_table :denshobato_messages do |t| 4 | t.text :body, default: '' 5 | t.references :author, polymorphic: true, index: { name: 'message_polymorphic_author' } 6 | 7 | t.timestamps null: false 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/generators/denshobato/migrations/create_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateDenshobatoNotifications < ActiveRecord::Migration 2 | def change 3 | create_table :denshobato_notifications do |t| 4 | t.integer :message_id, index: { name: 'notification_for_message' } 5 | t.integer :conversation_id, index: { name: 'notification_for_conversation' } 6 | end 7 | 8 | add_index :denshobato_notifications, [:message_id, :conversation_id], name: 'unique_messages_for_conversations', unique: true 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/denshobato/extenders/core_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'denshobato/extenders/core' 3 | 4 | describe Denshobato::Extenders::Core do 5 | before :each do 6 | @user = create(:user, name: 'Eugene') 7 | @duck = create(:duck, name: 'Donalnd Duck') 8 | @mark = create(:user, name: 'Mark') 9 | end 10 | 11 | describe '#denshobato_for' do 12 | let!(:conversation) { @user.hato_conversations.create(recipient: @duck) } 13 | 14 | it 'user has_many conversations' do 15 | expect(@user.denshobato_conversations.count).to eq 1 16 | expect(@user.denshobato_conversations.class.inspect).to include 'ActiveRecord_Associations_CollectionProxy' 17 | end 18 | 19 | it 'conversation belongs_to user' do 20 | expect(conversation.sender).to eq @user 21 | end 22 | end 23 | 24 | describe '#make_conversation_with' do 25 | it 'create conversations' do 26 | model = @user.make_conversation_with(@duck) 27 | 28 | expect(model.sender).to eq @user 29 | expect(model.recipient).to eq @duck 30 | expect(model.valid?).to be_truthy 31 | end 32 | end 33 | 34 | describe '#conversations' do 35 | it 'return all conversations where user as sender or recipient' do 36 | @mark.make_conversation_with(@user).save 37 | @duck.make_conversation_with(@user).save 38 | 39 | error = @mark.make_conversation_with(@user) 40 | 41 | expect(@user.hato_conversations.count).to eq 2 42 | expect(error.valid?).to be_falsey 43 | expect(error.errors[:conversation].join('')).to eq 'You already have conversation with this user.' 44 | end 45 | end 46 | 47 | describe '#find_conversation_with' do 48 | it 'find conversation with user and duck' do 49 | @user.make_conversation_with(@duck).save 50 | result = @user.find_conversation_with(@duck) 51 | conversation = Denshobato::Conversation.find_by(sender: @user, recipient: @duck) 52 | 53 | expect(result).to eq conversation 54 | end 55 | end 56 | 57 | describe '#send_message_to' do 58 | it 'initialize message' do 59 | @user.make_conversation_with(@duck).save 60 | room = @user.find_conversation_with(@duck) 61 | 62 | msg = @user.send_message_to(room.id, body: 'Hello') 63 | 64 | expect(msg.body).to eq 'Hello' 65 | expect(msg.author).to eq @user 66 | end 67 | 68 | it 'save message and send notifications' do 69 | @user.make_conversation_with(@duck).save 70 | room = @user.find_conversation_with(@duck) 71 | msg = @user.send_message_to(room.id, body: 'Hello') 72 | msg.save 73 | msg.send_notification(room.id) 74 | 75 | expect(Denshobato::Notification.count).to eq 2 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /spec/denshobato/helpers/view_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Denshobato::ViewHelper do 4 | helper = Helper.new 5 | 6 | describe '#conversation_exists?' do 7 | let(:sender) { create(:user, name: 'X') } 8 | let(:recipient) { create(:user, name: 'Y') } 9 | 10 | it 'returns false, conversation not exists yet' do 11 | expect(helper.conversation_exists?(sender, recipient)).to be_falsey 12 | end 13 | 14 | it 'returns true, conversation exist' do 15 | sender.make_conversation_with(recipient).save 16 | 17 | expect(helper.conversation_exists?(sender, recipient)).to be_truthy 18 | end 19 | end 20 | 21 | describe '#can_create_conversation?' do 22 | let(:sender) { create(:user, name: 'X') } 23 | 24 | it 'return true if sender isn`t recipient' do 25 | expect(helper.can_create_conversation?(sender, sender)).to be_falsey 26 | end 27 | end 28 | 29 | describe '#devise_url_helper' do 30 | let(:user) { create(:user) } 31 | let(:duck) { create(:duck) } 32 | 33 | it 'return correct url' do 34 | expect(helper.devise_url_helper(:new, user, :session)).to eq :new_user_session 35 | expect(helper.devise_url_helper(:edit, duck, :registration)). to eq :edit_duck_registration 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/denshobato/helpers/view_messaging_helper_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | Denshobato.autoload :ViewMessagingHelper, 'denshobato/helpers/view_messaging_helper' 3 | 4 | describe Denshobato::ViewMessagingHelper do 5 | class Helper 6 | include Denshobato::ViewMessagingHelper 7 | 8 | def image_tag(image, css) 9 | "" 10 | end 11 | end 12 | 13 | helper = Helper.new 14 | 15 | before :each do 16 | @sender = create(:user, name: 'John Smitt') 17 | @recipient = create(:duck, name: 'Donald', last_name: 'Duck') 18 | end 19 | 20 | describe '#interlocutor_name' do 21 | it 'return name of recipient' do 22 | @sender.make_conversation_with(@recipient).save 23 | conversation = @sender.find_conversation_with(@recipient) 24 | 25 | expect(helper.interlocutor_name(@sender, conversation, :name, :last_name)).to eq 'John Smitt' 26 | end 27 | end 28 | 29 | describe '#interlocutor_avatar' do 30 | it 'return with url and css class' do 31 | @sender.make_conversation_with(@recipient).save 32 | conversation = @sender.find_conversation_with(@recipient) 33 | @recipient[:avatar] = 'cat_image.jpg' 34 | @recipient.save 35 | image = helper.interlocutor_avatar(@sender, :avatar, conversation, 'img-rounded') 36 | 37 | expect(image).to eq "" 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/denshobato/models/blacklist_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | Denshobato.autoload :Conversation, 'denshobato/models/conversation' 3 | Denshobato.autoload :Message, 'denshobato/models/message' 4 | 5 | describe Denshobato::Blacklist, type: :model do 6 | it { should validate_presence_of(:blocker_id) } 7 | it { should validate_presence_of(:blocker_type) } 8 | it { should validate_presence_of(:blocked_id) } 9 | it { should validate_presence_of(:blocked_type) } 10 | it { should belong_to(:blocker) } 11 | it { should belong_to(:blocked) } 12 | 13 | before :each do 14 | @user = create(:user, name: 'Eugene') 15 | @duck = create(:duck, name: 'Duck') 16 | 17 | @user.add_to_blacklist(@duck).save 18 | end 19 | 20 | describe '#add_to_blacklist' do 21 | context 'User blocked duck' do 22 | it 'user block duck' do 23 | klass = @user.add_to_blacklist(@duck) 24 | 25 | expect(@user.blacklist).to include Denshobato::Blacklist.find_by(blocker: @user, blocked: @duck) 26 | expect(klass.valid?).to be_falsey 27 | expect(klass.errors.full_messages).to eq ['Blocker User already in your blacklist', 'Blocker type User already in your blacklist'] 28 | end 29 | end 30 | 31 | context 'duck can`t start conversation with user' do 32 | it 'user block duck, and duck can`t start conversation with user' do 33 | result = @duck.make_conversation_with(@user) 34 | 35 | expect(result.valid?).to be_falsey 36 | expect(result.errors[:blacklist]).to eq ['You`re in blacklist'] 37 | end 38 | end 39 | 40 | context 'duck can`t send message to user' do 41 | it 'user block duck, and duck can`t start conversation with user' do 42 | result = @duck.send_message('Hello, user', @user) 43 | 44 | expect(result).to eq ['You`re in blacklist'] 45 | end 46 | end 47 | 48 | context 'user can`t start conversation with blocked user' do 49 | it 'user block duck, and duck can`t start conversation with user' do 50 | result = @user.make_conversation_with(@duck) 51 | 52 | expect(result.valid?).to be_falsey 53 | expect(result.errors[:blacklist].join('')).to eq 'Remove user from blacklist, to start conversation' 54 | end 55 | end 56 | 57 | context 'user can`t send message to blocked user' do 58 | it 'user block duck, and duck can`t start conversation with user' do 59 | result = @user.send_message('Hello blocked user', @duck) 60 | 61 | expect(result.join('')).to eq 'Remove user from blacklist, to start conversation' 62 | end 63 | end 64 | end 65 | 66 | describe '#remove_from_blacklist' do 67 | it 'remove user from blacklist' do 68 | result = @user.remove_from_blacklist(@duck) 69 | result.destroy 70 | 71 | expect(@user.reload.blacklist).to match_array [] 72 | end 73 | end 74 | 75 | describe '#my_blacklist' do 76 | it 'return collection with blocked users' do 77 | expect(@user.my_blacklist).to include Denshobato::Blacklist.find_by(blocker: @user, blocked: @duck) 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/denshobato/models/conversation_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | Denshobato.autoload :Conversation, 'denshobato/models/conversation' 3 | Denshobato.autoload :Message, 'denshobato/models/message' 4 | 5 | describe Denshobato::Conversation, type: :model do 6 | it { should validate_presence_of(:sender_id) } 7 | it { should validate_presence_of(:sender_type) } 8 | it { should validate_presence_of(:recipient_id) } 9 | it { should validate_presence_of(:recipient_type) } 10 | it { should belong_to(:sender) } 11 | it { should belong_to(:recipient) } 12 | it { should have_many(:denshobato_notifications) } 13 | 14 | before :each do 15 | @user = create(:user, name: 'DHH') 16 | @duck = create(:duck, name: 'Quack') 17 | @wolf = create(:user, name: 'Wolf') 18 | end 19 | 20 | describe 'specific table in database' do 21 | conversation = Denshobato::Conversation 22 | 23 | it 'return correct database table' do 24 | expect(conversation.table_name).to eq 'denshobato_conversations' 25 | end 26 | end 27 | 28 | describe 'valiadtions' do 29 | it 'validate sender_id presence' do 30 | model = @user.hato_conversations.build(recipient: @duck, sender: @user) 31 | model.sender_id = nil 32 | model.save 33 | 34 | expect(model.errors.full_messages.join(', ')).to eq "Sender can't be blank" 35 | end 36 | 37 | it 'validate recipient_id presence' do 38 | model = @user.hato_conversations.build 39 | model.save 40 | 41 | expect(model.errors.full_messages.join(', ')).to eq "Recipient can't be blank, Recipient type can't be blank" 42 | end 43 | end 44 | 45 | describe 'validate uniqueness' do 46 | it 'validate uniqueness' do 47 | @user.hato_conversations.create(recipient: @duck, sender: @user) 48 | model = @duck.hato_conversations.create(recipient: @user, sender: @duck) 49 | 50 | expect(model.errors.messages[:conversation].join('')).to eq 'You already have conversation with this user.' 51 | end 52 | end 53 | 54 | describe 'has_many messages' do 55 | it 'return Associations::CollectionProxy' do 56 | @duck.hato_conversations.create(recipient: @user, sender: @duck) 57 | conversation = @duck.hato_conversations.first 58 | message = @duck.hato_messages.build(body: 'Moon Sonata') 59 | message.save 60 | 61 | message.send_notification(conversation.id) 62 | 63 | expect(conversation.messages).to eq @duck.hato_messages 64 | end 65 | end 66 | 67 | describe 'check sender validation' do 68 | it 'get error, when sender create conversation with yourself' do 69 | result = @user.make_conversation_with(@user) 70 | 71 | expect(result.valid?).to be_falsey 72 | expect(result.errors[:conversation].join('')).to eq 'You can`t create conversation with yourself' 73 | end 74 | end 75 | 76 | describe 'remove messages, if other users remove conversation' do 77 | it 'both users remove their conversation, messages from this conversation will delete' do 78 | create_conversation_and_messages 79 | @user.hato_conversations[0].destroy 80 | @duck.hato_conversations[0].destroy 81 | 82 | expect(@room.messages).to eq [] 83 | end 84 | end 85 | 86 | describe 'conversation member has access to messages, when other member delete conversation' do 87 | it 'user remove conversation, messages still in db' do 88 | create_conversation_and_messages 89 | @user.hato_conversations[0].destroy 90 | room2 = @duck.find_conversation_with(@user) 91 | 92 | expect(room2.messages).to eq [@msg, @msg2] 93 | end 94 | end 95 | 96 | describe 'user create new conversation with same recipient' do 97 | it 'user with new conversation see only new messages' do 98 | create_conversation_and_messages 99 | 100 | @user.hato_conversations.destroy_all 101 | @user.make_conversation_with(@duck).save 102 | room = @user.find_conversation_with(@duck) 103 | room2 = @duck.find_conversation_with(@user) 104 | @msg = @user.send_message('Hello again', @duck) 105 | @msg.save 106 | @msg.send_notification(room.id) 107 | 108 | expect(room.messages).not_to eq room2.messages 109 | end 110 | end 111 | 112 | describe 'remove extra messages, after multiple removing conversation' do 113 | it 'remove messages which not belong to any conversations' do 114 | @user.make_conversation_with(@duck).save 115 | room = @user.find_conversation_with(@duck) 116 | msg = @user.send_message('First user message', @duck) 117 | create_msg(msg, room) 118 | 119 | duck_room = @duck.find_conversation_with(@user) 120 | msg2 = @duck.send_message('First duck message', @user) 121 | create_msg(msg2, duck_room) 122 | 123 | room.destroy 124 | @user.make_conversation_with(@duck).save 125 | 126 | msg3 = @user.send_message('Hello again', @duck) 127 | new_room = @user.find_conversation_with(@duck) 128 | create_msg(msg3, new_room) 129 | 130 | duck_room.destroy 131 | @duck.make_conversation_with(@user).save 132 | new_duck_room = @duck.find_conversation_with(@user) 133 | msg4 = @duck.send_message('In new duck conversation', @user) 134 | create_msg(msg4, new_duck_room) 135 | 136 | expect(new_room.messages).to eq [msg3, msg4] 137 | expect(new_duck_room.messages).to eq [msg4] 138 | expect { msg3.destroy }.to change { msg3.errors.full_messages.join('') } 139 | .from('') 140 | .to('Can`t delete message, as long as it belongs to the conversation') 141 | end 142 | end 143 | 144 | describe '#to_trash' do 145 | it 'move conversation to trash' do 146 | create_conversation(@user, @duck, @wolf) 147 | @conversation.to_trash 148 | 149 | expect(@conversation.trashed).to be_truthy 150 | end 151 | end 152 | 153 | describe '#from_trash' do 154 | it 'move conversation to trash' do 155 | create_conversation(@user, @duck, @wolf) 156 | @conversation.to_trash 157 | @conversation.from_trash 158 | 159 | expect(@conversation.trashed).to be_falsey 160 | end 161 | end 162 | 163 | describe '#my_conversations' do 164 | it 'return active conversations' do 165 | create_conversation(@user, @duck, @wolf) 166 | @conversation.to_trash 167 | 168 | expect(@user.my_conversations).not_to include @conversation 169 | end 170 | end 171 | 172 | describe '#trashed_conversations' do 173 | it 'return trashed conversations' do 174 | create_conversation(@user, @duck, @wolf) 175 | @conversation.to_trash 176 | 177 | expect(@user.trashed_conversations).to include @conversation 178 | expect(@user.trashed_conversations).not_to include @user.my_conversations 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /spec/denshobato/models/message_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | Denshobato.autoload :Conversation, 'denshobato/models/conversation' 3 | 4 | describe Denshobato::Message, type: :model do 5 | it { should validate_presence_of(:body) } 6 | it { should validate_presence_of(:author_type) } 7 | it { should validate_presence_of(:author_id) } 8 | it { should belong_to(:author) } 9 | it { should have_many(:denshobato_notifications) } 10 | 11 | before :each do 12 | @user = create(:user, name: 'Eugene') 13 | @duck = create(:duck, name: 'Donalnd Duck') 14 | @mark = create(:user, name: 'Mark') 15 | end 16 | 17 | describe 'specific table in database' do 18 | it 'return correct database table' do 19 | expect(Denshobato::Message.table_name).to eq 'denshobato_messages' 20 | end 21 | end 22 | 23 | describe '#send_message' do 24 | it 'create message, if conversation not exists, then conversation will created, together with the message.' do 25 | @user.send_message('Hello John', @duck).save 26 | conversation = @user.find_conversation_with(@duck) 27 | @user.hato_messages.first.send_notification(conversation.id) 28 | message = conversation.messages 29 | 30 | expect(@user.hato_messages).to eq message 31 | end 32 | end 33 | 34 | describe 'return error, when create message directly, without conversation' do 35 | it 'get validation error' do 36 | message = @user.send_message_to(nil, body: 'Text') 37 | 38 | expect(message.join(', ')).to eq 'Conversation not present' 39 | end 40 | end 41 | 42 | describe 'update conversation updated_at when message was created' do 43 | it 'update conversation' do 44 | @user.make_conversation_with(@duck).save 45 | conversation = @user.find_conversation_with(@duck) 46 | @user.send_message_to(conversation.id, body: 'lol') 47 | 48 | expect(conversation.updated_at.utc.strftime('%a %b %d | %I:%M %p')).to eq Time.now.utc.strftime('%a %b %d | %I:%M %p') 49 | end 50 | end 51 | 52 | describe 'access_to_posting_message' do 53 | it 'user can`t post to duck and mark conversation' do 54 | @duck.make_conversation_with(@mark).save 55 | room = @duck.find_conversation_with(@mark) 56 | @duck.send_message_to(room.id, body: 'Hi Mark').save 57 | 58 | result = @user.send_message_to(room.id, body: 'Hi there') 59 | 60 | expect(result.valid?).to be_falsey 61 | expect(result.errors[:message].join('')).to eq 'You can`t post to this conversation' 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/denshobato/models/notification_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Denshobato::Notification, type: :model do 4 | it { should validate_presence_of(:message_id) } 5 | it { should validate_presence_of(:conversation_id) } 6 | it { should validate_uniqueness_of(:message_id).scoped_to(:conversation_id) } 7 | it { should belong_to(:denshobato_message) } 8 | it { should belong_to(:denshobato_conversation) } 9 | 10 | describe 'specific table in database' do 11 | it 'return correct database table' do 12 | expect(Denshobato::Notification.table_name).to eq 'denshobato_notifications' 13 | end 14 | end 15 | 16 | describe 'send notifications to users after create message' do 17 | let(:user) { create(:user, name: 'DHH') } 18 | let(:duck) { create(:duck, name: 'Quack') } 19 | 20 | it 'save message and send notifications' do 21 | user.make_conversation_with(duck).save 22 | room = user.find_conversation_with(duck) 23 | msg = user.send_message_to(room.id, body: 'Hello') 24 | msg.save 25 | msg.send_notification(room.id) 26 | 27 | expect(Denshobato::Notification.count).to eq 2 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/denshobato/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | Denshobato.autoload :Conversation, 'denshobato/models/conversation' 3 | 4 | describe Denshobato::Conversation do 5 | describe 'user conversations' do 6 | let(:sender) { create(:user, name: 'Frodo') } 7 | let(:recipient) { create(:user, name: 'Harry Potter') } 8 | let(:another_sender) { create(:user, name: 'Luke') } 9 | 10 | it 'return conversations where current user is present as sender or recipient' do 11 | recipient.hato_conversations.create(recipient: sender) 12 | another_sender.hato_conversations.create(recipient: sender) 13 | 14 | expect(sender.hato_conversations).to eq sender.hato_conversations 15 | end 16 | end 17 | 18 | describe 'alias attribute for short' do 19 | let(:sender) { create(:user, name: 'Frodo') } 20 | 21 | it 'return same association array' do 22 | expect(sender.hato_conversations).to eq sender.denshobato_conversations 23 | end 24 | end 25 | 26 | describe 'chating between two models' do 27 | let(:sender) { create(:user, name: 'Frodo') } 28 | let(:duck) { create(:duck, name: 'Quack') } 29 | 30 | it 'conversations between user and duck' do 31 | duck.hato_conversations.create(recipient: sender) 32 | 33 | conv1 = duck.find_conversation_with(sender) 34 | conv2 = sender.find_conversation_with(duck) 35 | 36 | expect(conv1.sender).to eq conv2.recipient 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/factories/admins.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :admin do 3 | name { 'Admin' } 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/factories/ducks.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :duck do 3 | name { 'Donald' } 4 | last_name { '' } 5 | avatar { '' } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :user do 3 | name { 'John Doe' } 4 | last_name { '' } 5 | avatar { 'cat_image.jpg' } 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'database_cleaner' 3 | require 'shoulda/matchers' 4 | require 'factory_girl' 5 | require 'active_record' 6 | require 'spec_helpers/conversation_helper' 7 | require 'denshobato' 8 | 9 | ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') 10 | 11 | ActiveRecord::Schema.define(version: 1) do 12 | create_table :users do |t| 13 | t.string :name, default: '' 14 | t.string :avatar, default: '' 15 | t.string :last_name, default: '' 16 | end 17 | 18 | create_table :admins do |t| 19 | t.string :name, default: '' 20 | end 21 | 22 | create_table :ducks do |t| 23 | t.string :name, default: '' 24 | t.string :last_name, default: '' 25 | t.string :avatar, default: '' 26 | end 27 | 28 | create_table :denshobato_conversations do |t| 29 | t.boolean :trashed, default: false 30 | t.references :sender, polymorphic: true, index: { name: 'conversation_polymorphic_sender' } 31 | t.references :recipient, polymorphic: true, index: { name: 'conversation_polymorphic_recipient' } 32 | 33 | t.timestamps null: false 34 | end 35 | 36 | create_table :denshobato_messages do |t| 37 | t.text :body, default: '' 38 | t.references :author, polymorphic: true, index: { name: 'message_polymorphic_author' } 39 | 40 | t.timestamps null: false 41 | end 42 | 43 | create_table :denshobato_notifications do |t| 44 | t.integer :message_id, index: { name: 'notification_for_message' } 45 | t.integer :conversation_id, index: { name: 'notification_for_conversation' } 46 | end 47 | 48 | create_table :denshobato_blacklists do |t| 49 | t.references :blocker, polymorphic: true, index: { name: 'blocker_user' } 50 | t.references :blocked, polymorphic: true, index: { name: 'blocked_user' } 51 | end 52 | end 53 | 54 | ActiveRecord::Base.extend Denshobato::Extenders::Core 55 | 56 | class User < ActiveRecord::Base 57 | denshobato_for :user 58 | end 59 | 60 | class Admin < ActiveRecord::Base 61 | denshobato_for :user 62 | end 63 | 64 | class Duck < ActiveRecord::Base 65 | denshobato_for :user 66 | end 67 | 68 | Denshobato.autoload :ViewHelper, 'denshobato/helpers/view_helper' 69 | 70 | class Helper 71 | include Denshobato::ViewHelper 72 | end 73 | 74 | Shoulda::Matchers.configure do |config| 75 | config.integrate do |with| 76 | with.test_framework :rspec 77 | with.library :active_record 78 | end 79 | end 80 | 81 | RSpec.configure do |config| 82 | config.include FactoryGirl::Syntax::Methods 83 | config.include(Shoulda::Matchers::ActiveModel, type: :model) 84 | config.include(Shoulda::Matchers::ActiveRecord, type: :model) 85 | 86 | config.include ConversationHelper 87 | 88 | config.before(:all) do 89 | FactoryGirl.reload 90 | end 91 | 92 | config.before(:suite) do 93 | DatabaseCleaner.strategy = :transaction 94 | DatabaseCleaner.clean_with(:truncation) 95 | end 96 | 97 | config.around(:each) do |example| 98 | DatabaseCleaner.cleaning do 99 | example.run 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/spec_helpers/conversation_helper.rb: -------------------------------------------------------------------------------- 1 | module ConversationHelper 2 | def create_conversation(sender, recipient, other_recipient = nil) 3 | sender.make_conversation_with(recipient).save 4 | sender.make_conversation_with(other_recipient).save if other_recipient 5 | 6 | @conversation = sender.find_conversation_with(other_recipient) 7 | end 8 | 9 | def create_msg(msg, room) 10 | msg.save 11 | msg.send_notification(room.id) 12 | end 13 | 14 | def create_conversation_and_messages 15 | @user.make_conversation_with(@duck).save 16 | @room = @user.find_conversation_with(@duck) 17 | @msg = @user.send_message_to(@room.id, body: 'Hello') 18 | @msg2 = @duck.send_message_to(@room.id, body: 'Hi user') 19 | create_msg(@msg, @room) 20 | create_msg(@msg2, @room) 21 | end 22 | end 23 | --------------------------------------------------------------------------------