├── .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 | 
4 |
5 | [](https://badge.fury.io/rb/denshobato)
6 | [](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 | 
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 | 
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 |
--------------------------------------------------------------------------------