chat 0.2.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +11 -8
- data/Rakefile +2 -0
- data/app/assets/javascripts/chat/channels/message.coffee +33 -43
- data/app/assets/javascripts/chat/channels/status.coffee +2 -2
- data/app/assets/javascripts/chat/messages.coffee +12 -11
- data/app/assets/stylesheets/chat/avatar_grid.sass +40 -0
- data/app/assets/stylesheets/chat/chat.sass +9 -4
- data/app/assets/stylesheets/chat/checkbox.sass +3 -3
- data/app/assets/stylesheets/chat/emojionearea.min.css +1 -1
- data/app/assets/stylesheets/chat/header.sass +25 -8
- data/app/assets/stylesheets/chat/launch.sass +39 -3
- data/app/assets/stylesheets/chat/list.sass +56 -23
- data/app/assets/stylesheets/chat/message_form.sass +7 -4
- data/app/assets/stylesheets/chat/new.sass +27 -22
- data/app/assets/stylesheets/chat/transcript.sass +60 -64
- data/app/channels/chat/messages_channel.rb +3 -2
- data/app/channels/chat/notification_channel.rb +2 -0
- data/app/channels/chat/status_channel.rb +12 -10
- data/app/controllers/chat/application_controller.rb +5 -1
- data/app/controllers/chat/conversations_controller.rb +24 -2
- data/app/controllers/chat/messages_controller.rb +8 -2
- data/app/helpers/chat/application_helper.rb +35 -24
- data/app/jobs/chat/application_job.rb +9 -1
- data/app/jobs/chat/message_relay_job.rb +5 -13
- data/app/jobs/chat/notification_relay_job.rb +10 -4
- data/app/jobs/chat/status_relay_job.rb +3 -3
- data/app/mailers/chat/application_mailer.rb +2 -0
- data/app/models/chat/application_record.rb +2 -0
- data/app/models/chat/conversation.rb +12 -0
- data/app/models/chat/dot_command.rb +2 -0
- data/app/models/chat/dot_command/gif.rb +2 -0
- data/app/models/chat/dot_command/shrug.rb +3 -1
- data/app/models/chat/dot_command/validator.rb +3 -1
- data/app/models/chat/message.rb +10 -2
- data/app/models/chat/session.rb +3 -3
- data/app/views/chat/_chat.html.haml +10 -6
- data/app/views/chat/conversations/_conversation.html.haml +23 -0
- data/app/views/chat/conversations/_index.html.haml +1 -6
- data/app/views/chat/conversations/_new.html.haml +2 -4
- data/app/views/chat/conversations/_show.html.haml +8 -7
- data/app/views/chat/conversations/create.js.erb +14 -10
- data/app/views/chat/conversations/show.js.erb +8 -5
- data/app/views/chat/messages/_message.haml +9 -12
- data/config/routes.rb +4 -2
- data/lib/chat.rb +5 -1
- data/lib/chat/engine.rb +2 -0
- data/lib/chat/user.rb +7 -12
- data/lib/chat/version.rb +3 -1
- data/lib/generators/chat/install/install_generator.rb +2 -0
- data/lib/generators/chat/install/templates/add_chat_to_users.rb +3 -1
- data/lib/generators/chat/install/templates/chat.rb +2 -0
- data/lib/generators/chat/install/templates/create_chat.rb +19 -7
- data/lib/tasks/chat_tasks.rake +1 -0
- metadata +15 -48
- data/app/models/chat/dot_command/leave.rb +0 -16
@@ -1,5 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chat
|
2
|
-
class ApplicationJob <
|
4
|
+
class ApplicationJob < ::ApplicationJob
|
3
5
|
queue_as :default
|
6
|
+
|
7
|
+
private
|
8
|
+
|
9
|
+
def broadcast(channel, payload)
|
10
|
+
ActionCable.server.broadcast(channel, payload)
|
11
|
+
end
|
4
12
|
end
|
5
13
|
end
|
@@ -1,31 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chat
|
2
4
|
class MessageRelayJob < ApplicationJob
|
3
5
|
def perform(message_id)
|
4
6
|
message = Chat::Message.find(message_id)
|
5
|
-
|
7
|
+
broadcast(
|
6
8
|
"chats::#{message.conversation_id}::messages", render_message(message)
|
7
9
|
)
|
8
10
|
end
|
9
11
|
|
10
12
|
def render_message(message)
|
11
13
|
{
|
12
|
-
user: message.user_id,
|
13
|
-
avatar: avatar(message.user)
|
14
|
+
user: message.user_id, avatar: renderer.chat_avatar(message.user).to_s
|
14
15
|
}.merge(content(message))
|
15
16
|
end
|
16
17
|
|
17
|
-
def avatar(user)
|
18
|
-
renderer.chat_avatar(user).to_s
|
19
|
-
end
|
20
|
-
|
21
18
|
def content(message)
|
22
19
|
if message.image?
|
23
|
-
{
|
24
|
-
message: renderer.image_tag(
|
25
|
-
message.image.url, class: "chat__message-image"
|
26
|
-
),
|
27
|
-
image: true
|
28
|
-
}
|
20
|
+
{ message: renderer.image_tag(message.image.url), image: true }
|
29
21
|
else
|
30
22
|
{ message: message.text, image: false }
|
31
23
|
end
|
@@ -1,11 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chat
|
2
4
|
class NotificationRelayJob < ApplicationJob
|
3
5
|
def perform(message)
|
4
|
-
(message
|
5
|
-
|
6
|
-
"users::#{user_id}::chats", chat_id: message.conversation_id
|
7
|
-
)
|
6
|
+
user_ids(message).each do |user_id|
|
7
|
+
broadcast("users::#{user_id}::chats", chat_id: message.conversation_id)
|
8
8
|
end
|
9
9
|
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def user_ids(message)
|
14
|
+
message.conversation.users.where.not(id: message.user_id).ids
|
15
|
+
end
|
10
16
|
end
|
11
17
|
end
|
@@ -1,9 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Chat
|
2
4
|
class StatusRelayJob < ApplicationJob
|
3
5
|
def perform(user)
|
4
|
-
|
5
|
-
"chat::status", user_id: user.id, status: user.chat_status
|
6
|
-
)
|
6
|
+
broadcast("chat::status", user_id: user.id, status: user.chat_status)
|
7
7
|
end
|
8
8
|
end
|
9
9
|
end
|
@@ -1,9 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Chat::Conversation < ApplicationRecord
|
2
4
|
has_many :messages
|
3
5
|
has_many :sessions, dependent: :destroy
|
4
6
|
has_many :users, through: :sessions, class_name: "::User"
|
5
7
|
|
8
|
+
has_one :last_message, -> { order(created_at: :desc) },
|
9
|
+
class_name: "Chat::Message"
|
10
|
+
|
6
11
|
validates :sessions, presence: {
|
7
12
|
message: "At least one user must be selected"
|
8
13
|
}
|
14
|
+
validate do
|
15
|
+
next unless Chat::Conversation.joins(:users).where.not(id: id).where(
|
16
|
+
users: { id: user_ids }
|
17
|
+
).group(:id).having("COUNT(DISTINCT users.id) = ?", user_ids.count).exists?
|
18
|
+
|
19
|
+
errors.add(:conversation, "already taking place")
|
20
|
+
end
|
9
21
|
end
|
data/app/models/chat/message.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Chat::Message < ApplicationRecord
|
2
4
|
has_attached_file :image
|
3
5
|
validates_attachment :image, content_type: {
|
@@ -6,13 +8,15 @@ class Chat::Message < ApplicationRecord
|
|
6
8
|
|
7
9
|
belongs_to :user, class_name: "::User"
|
8
10
|
belongs_to :conversation
|
11
|
+
belongs_to :session
|
9
12
|
|
10
13
|
delegate :name, to: :user
|
14
|
+
before_save :remove_extra_new_line
|
11
15
|
before_save :execute_dot_command
|
12
16
|
|
13
17
|
after_create_commit do
|
14
|
-
Chat::MessageRelayJob.
|
15
|
-
Chat::NotificationRelayJob.
|
18
|
+
Chat::MessageRelayJob.send(Chat.perform_method.to_sym, id)
|
19
|
+
Chat::NotificationRelayJob.send(Chat.perform_method.to_sym, self)
|
16
20
|
end
|
17
21
|
|
18
22
|
private
|
@@ -26,4 +30,8 @@ class Chat::Message < ApplicationRecord
|
|
26
30
|
def dot_command
|
27
31
|
@dot_command ||= Chat::DotCommand.new(self, text)
|
28
32
|
end
|
33
|
+
|
34
|
+
def remove_extra_new_line
|
35
|
+
self.text = text[0..-16]
|
36
|
+
end
|
29
37
|
end
|
data/app/models/chat/session.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Chat::Session < ApplicationRecord
|
2
4
|
belongs_to :conversation
|
3
5
|
belongs_to :user, class_name: "::User"
|
4
6
|
|
5
|
-
has_many :messages,
|
6
|
-
primary_key: [:user_id, :conversation_id],
|
7
|
-
dependent: :destroy
|
7
|
+
has_many :messages, dependent: :destroy
|
8
8
|
|
9
9
|
validates :user_id, uniqueness: { scope: :conversation_id }
|
10
10
|
end
|
@@ -1,14 +1,18 @@
|
|
1
|
-
= launch_chat_fab
|
1
|
+
= launch_chat_fab
|
2
2
|
|
3
3
|
#chat
|
4
|
+
#chat__background
|
4
5
|
.chat__header
|
5
|
-
|
6
|
+
.chat__settings
|
7
|
+
%div
|
8
|
+
%div
|
6
9
|
.chat__flex
|
7
10
|
%h6 Chat
|
8
11
|
.chat__flex
|
9
|
-
= material_icon.close.css_class(
|
10
|
-
|
11
|
-
= render 'chat/conversations/new'
|
12
|
+
= material_icon.close.css_class("chat__close")
|
13
|
+
= render "chat/conversations/new"
|
12
14
|
.chat__list
|
13
|
-
= render
|
15
|
+
= render "chat/conversations/index"
|
16
|
+
= link_to nil, class: "chat__start-new" do
|
17
|
+
= material_icon.add
|
14
18
|
.current_chat
|
@@ -0,0 +1,23 @@
|
|
1
|
+
= link_to [chat, conversation], remote: true, id: "chat__show-#{conversation.id}" do
|
2
|
+
.avatar_container{ class: chat_avatar_count(conversation) }
|
3
|
+
- if chat_avatar_count(conversation) == "count_default"
|
4
|
+
= chat_avatars(conversation).sample.html_safe
|
5
|
+
.chat_count
|
6
|
+
%span
|
7
|
+
= conversation_user_count(conversation)
|
8
|
+
- else
|
9
|
+
= chat_avatars(conversation).join("").html_safe
|
10
|
+
.chat__glance
|
11
|
+
.chat__glance-name-date
|
12
|
+
.name
|
13
|
+
- if conversation.users.to_a.size > 2
|
14
|
+
Group Chat
|
15
|
+
- else
|
16
|
+
= conversation.users.where.not(id: current_user.id).first.first_name
|
17
|
+
.flex
|
18
|
+
.date
|
19
|
+
- if conversation.last_message.present?
|
20
|
+
= time_ago_in_words conversation.last_message&.created_at
|
21
|
+
ago
|
22
|
+
.chat__glance-last-message
|
23
|
+
= raw conversation.last_message&.text
|
@@ -1,11 +1,6 @@
|
|
1
1
|
- if chat_list.present?
|
2
|
-
|
3
|
-
- chat_list.each do |conversation|
|
4
|
-
= link_to [chat, conversation], remote: true, id: "chat__show-#{conversation.id}" do
|
5
|
-
= conversation.names.gsub(", #{current_user.name}", '').gsub("#{current_user.name}, ", '')
|
6
|
-
.chat__flex
|
2
|
+
= render chat_list
|
7
3
|
- else
|
8
4
|
.chat__none-available
|
9
5
|
= material_icon.chat_bubble_outline
|
10
6
|
%h5 No Chats
|
11
|
-
= link_to 'Start a Chat', nil, class: 'chat__start-new'
|
@@ -1,9 +1,7 @@
|
|
1
1
|
= form_for [chat, Chat::Conversation.new], remote: true do |f|
|
2
2
|
- if chatable_users.present?
|
3
|
-
|
4
|
-
|
5
|
-
.chat__flex
|
6
|
-
= f.submit 'Start Chatting'
|
3
|
+
= chatable_user_check_boxes(f)
|
4
|
+
.chat__submit-container= f.submit "Start Chatting"
|
7
5
|
- else
|
8
6
|
.chat__none-available
|
9
7
|
= material_icon.people
|
@@ -1,9 +1,10 @@
|
|
1
1
|
.chat__transcript{ data: { channel: 'messages', current: { chat: @conversation.id, user: current_user.id }}}
|
2
|
-
|
3
|
-
|
2
|
+
- @conversation.messages.each_with_index do |msg, i|
|
3
|
+
= render "chat/messages/message", message: msg, previous: (i > 0 ? @conversation.messages[i-1] : nil)
|
4
4
|
.chat__flex
|
5
|
-
|
6
|
-
=
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
%div
|
6
|
+
= form_for [chat, @conversation, Chat::Message.new], remote: true do |f|
|
7
|
+
= material_icon.insert_photo.css_class("chat__insert-image")
|
8
|
+
= f.text_area :text, placeholder: 'Send message...', autocomplete: 'off'
|
9
|
+
%span.bar
|
10
|
+
= f.file_field :image
|
@@ -1,21 +1,25 @@
|
|
1
|
-
<% if @conversation.
|
1
|
+
<% if @conversation.errors.messages[:sessions].blank? %>
|
2
2
|
$('#chat .chat__list').html("<%= j render 'chat/conversations/index' %>");
|
3
|
-
$('#chat .chat__list').css('display', 'none');
|
4
3
|
$('.new_conversation').replaceWith("<%= j render 'chat/conversations/new' %>");
|
5
4
|
$('.current_chat').html("<%= j render 'chat/conversations/show' %>");
|
6
|
-
$('.
|
7
|
-
$('.chat__transcript').scrollTop($('.chat__transcript').prop('scrollHeight'));
|
5
|
+
$('.new_message textarea').emojioneArea();
|
8
6
|
App.chat_message.followCurrentChat();
|
9
|
-
|
7
|
+
$("#new_message").fileupload();
|
8
|
+
if($("#chat__show-<%= @conversation.id %>").hasClass('chat__has_new_message')) {
|
9
|
+
App.chat_notification.decrement();
|
10
|
+
$("#chat__show-<%= @conversation.id %>").removeClass('chat__has_new_message')
|
11
|
+
}
|
12
|
+
$('.current_chat').toggleClass("slide_left");
|
13
|
+
$('.chat__list').addClass("slide_left");
|
14
|
+
$('.chat__start-new').addClass("chat__disappear");
|
15
|
+
$('.current_chat').scrollTop($('.current_chat').prop('scrollHeight'));
|
16
|
+
<% else %>
|
10
17
|
error_msg =
|
11
18
|
`<div class='chat__errors'>
|
12
19
|
<%= j @conversation.errors.values.flatten.join(', ') %>
|
13
20
|
</div>`;
|
14
21
|
if($('.chat__errors').length == 0) {
|
15
|
-
$('.
|
22
|
+
$('.new_conversation').scrollTop(0);
|
23
|
+
$('.new_conversation').prepend(error_msg);
|
16
24
|
}
|
17
|
-
<% else %>
|
18
|
-
<% id = Chat.joins(:chat_users).find_by(chat_users: { user_id: @conversation.user_ids }).id %>
|
19
|
-
$('.new_conversation').replaceWith("<%= j render 'chats/new' %>");
|
20
|
-
$("a[href='/chats/<%= id %>']").click();
|
21
25
|
<% end %>
|
@@ -1,12 +1,15 @@
|
|
1
|
+
$('.new_conversation').replaceWith("<%= j render 'chat/conversations/new' %>");
|
1
2
|
$('.current_chat').html("<%= j render 'chat/conversations/show' %>");
|
2
|
-
$('.new_message textarea').emojioneArea()
|
3
|
-
$('.chat__list').css('display', 'none');
|
4
|
-
$('.new_conversation').css('display', 'none');
|
5
|
-
$('.current_chat').css('display', 'flex');
|
3
|
+
$('.new_message textarea').emojioneArea();
|
6
4
|
App.chat_message.followCurrentChat();
|
7
5
|
$("#new_message").fileupload();
|
8
6
|
if($("#chat__show-<%= @conversation.id %>").hasClass('chat__has_new_message')) {
|
9
7
|
App.chat_notification.decrement();
|
10
8
|
$("#chat__show-<%= @conversation.id %>").removeClass('chat__has_new_message')
|
11
9
|
}
|
12
|
-
$('.
|
10
|
+
$('.current_chat').toggleClass("slide_left");
|
11
|
+
$('.chat__list').removeClass("slide_right");
|
12
|
+
$('.chat__list').addClass("slide_left");
|
13
|
+
$('.chat__start-new').addClass("chat__disappear");
|
14
|
+
$('.current_chat').scrollTop($('.current_chat').prop('scrollHeight'));
|
15
|
+
$(".new_conversation").removeClass("slide_right");
|
@@ -1,13 +1,10 @@
|
|
1
|
-
-
|
2
|
-
|
3
|
-
|
4
|
-
- else
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
= chat_avatar(message.user)
|
1
|
+
.message-container{ class: message.user == current_user ? "right" : "left" }
|
2
|
+
- if message.user == previous&.user
|
3
|
+
.transcript_placeholder_avatar
|
4
|
+
- else
|
5
|
+
= chat_avatar(message.user)
|
6
|
+
.message{ class: ("image" if message.image?).to_s }
|
7
|
+
- if message.image?
|
8
|
+
= image_tag message.image.url
|
10
9
|
- else
|
11
|
-
=
|
12
|
-
.text.left= raw(message.text)
|
13
|
-
.chat__flex
|
10
|
+
.content= raw(message.text)
|
data/config/routes.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
Chat::Engine.routes.draw do
|
2
|
-
resources :conversations, only:
|
3
|
-
resources :messages, only:
|
4
|
+
resources :conversations, only: %i(show create) do
|
5
|
+
resources :messages, only: %i(create destroy)
|
4
6
|
end
|
5
7
|
end
|
data/lib/chat.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "chat/engine"
|
2
4
|
require "chat/version"
|
3
5
|
require "chat/user"
|
@@ -6,7 +8,6 @@ require "paperclip"
|
|
6
8
|
require "material_icons"
|
7
9
|
require "coffee-rails"
|
8
10
|
require "jquery-rails"
|
9
|
-
require "composite_primary_keys"
|
10
11
|
require "jquery-fileupload-rails"
|
11
12
|
require "giphy"
|
12
13
|
require "sidekiq"
|
@@ -21,6 +22,9 @@ module Chat
|
|
21
22
|
# The field that contains the users avatar
|
22
23
|
mattr_accessor :user_avatar
|
23
24
|
|
25
|
+
# Can be perform_now or perform_later. Called on jobs
|
26
|
+
mattr_accessor :perform_method
|
27
|
+
|
24
28
|
def self.table_name_prefix
|
25
29
|
"chat_"
|
26
30
|
end
|
data/lib/chat/engine.rb
CHANGED
data/lib/chat/user.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "active_support/concern"
|
2
4
|
|
3
5
|
module Chat
|
@@ -27,25 +29,18 @@ module Chat
|
|
27
29
|
update(chat_status: "offline")
|
28
30
|
end
|
29
31
|
|
30
|
-
def
|
31
|
-
|
32
|
-
session.destroy
|
32
|
+
def chat_avatar
|
33
|
+
send(Chat.user_avatar)
|
33
34
|
end
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
"first_name"
|
38
|
-
end
|
39
|
-
|
40
|
-
def last_name
|
41
|
-
"last_name"
|
42
|
-
end
|
36
|
+
def chat_avatar?
|
37
|
+
send("#{Chat.user_avatar}?")
|
43
38
|
end
|
44
39
|
|
45
40
|
private
|
46
41
|
|
47
42
|
def broadcast_status
|
48
|
-
Chat::StatusRelayJob.
|
43
|
+
Chat::StatusRelayJob.send(Chat.perform_method, self)
|
49
44
|
end
|
50
45
|
end
|
51
46
|
end
|