This commit is contained in:
Scott Comer 2014-05-02 15:20:21 -05:00
commit 6be0791adb
18 changed files with 642 additions and 67 deletions

View File

@ -149,4 +149,5 @@ chat_messages.sql
diagnostics.sql
user_mods.sql
connection_stale_expire.sql
rename_chat_messages.sql
fix_connection_fields.sql

View File

@ -0,0 +1,2 @@
-- Rename columns .
ALTER TABLE chat_messages RENAME COLUMN messsage TO message;

View File

@ -141,6 +141,7 @@ require "jam_ruby/models/email_error"
require "jam_ruby/app/mailers/async_mailer"
require "jam_ruby/app/mailers/batch_mailer"
require "jam_ruby/models/affiliate_partner"
require "jam_ruby/models/chat_message"
include Jampb

View File

@ -1,26 +1,55 @@
module JamRuby
class ChatMessage < ActiveRecord::Base
self.table_name = 'chat_messages'
self.primary_key = 'id'
default_scope order('created_at DESC')
belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id"
belongs_to :session, :class_name => "JamRuby::MusicSession", :foreign_key => "session_id"
attr_accessible :user_id, :message, :music_session_id
belongs_to :user
belongs_to :music_session
validates :message, length: {minimum: 1, maximum: 255}, no_profanity: true
def self.send_chat_msg(music_session, chat_msg, user)
msg = @@message_factory.chat_message(
music_session.id,
user.name,
user.id,
chat_msg.message,
chat_msg.id,
chat_msg.created_at
)
class << self
@@mq_router = MQRouter.new
@@message_factory = MessageFactory.new
def index(user, params = {})
limit = params[:limit]
limit ||= 20
limit = limit.to_i
start = params[:start].presence
start = start.to_i || 0
query = ChatMessage.offset(start).limit(limit)
if query.length == 0
[query, nil]
elsif query.length < limit
[query, nil]
else
[query, start + limit]
end
end
def send_chat_msg(music_session, chat_msg, user, client_id)
msg = @@message_factory.chat_message(
music_session.id,
user.name,
user.id,
chat_msg.message,
chat_msg.id,
chat_msg.created_at.utc.iso8601
)
@@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id})
end
@@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => chat_msg.user_id})
end
end

View File

@ -22,6 +22,7 @@ module JamRuby
has_many :fan_invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::FanInvitation"
has_many :invited_fans, :through => :fan_invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver
has_many :recordings, :class_name => "JamRuby::Recording", :inverse_of => :music_session
has_many :chats, :class_name => "JamRuby::ChatMessages", :foreign_key => "session_id"
belongs_to :band, :inverse_of => :music_sessions, :class_name => "JamRuby::Band", :foreign_key => "band_id"
after_create :started_session

View File

@ -61,6 +61,9 @@ module JamRuby
has_many :notifications, :class_name => "JamRuby::Notification", :foreign_key => "target_user_id"
has_many :inverse_notifications, :through => :notifications, :class_name => "JamRuby::User"
# chats
has_many :chats, :class_name => "JamRuby::ChatMessage", :foreign_key => "user_id"
# friends
has_many :friendships, :class_name => "JamRuby::Friendship", :foreign_key => "user_id"
has_many :friends, :through => :friendships, :class_name => "JamRuby::User"

View File

@ -0,0 +1,346 @@
(function(context,$) {
"use strict";
context.JK = context.JK || {};
context.JK.ChatPanel = function(app) {
var logger = context.JK.logger;
var rest = context.JK.Rest();
var $panel = null;
var $contents = null;
var $count = null;
var $chatMessages = null;
var $chatMessagesScroller = null;
var $sendChatMessageBtn = null;
var $chatSender = null;
var $form = null;
var $textBox = null;
var $sessionId = null;
var $errorMsg = null;
var sendingMessage = false;
var showing = false;
var fullyInitialized = false;
var renderQueue = [];
var sidebar = null;
var user = null;
var currentPage = 0;
var LIMIT = 20;
var next = null;
function reset() {
fullyInitialized = false;
renderQueue = [];
sendingMessage = false;
$chatMessages.empty();
$textBox.val('');
}
function buildMessage() {
var message = {};
message['message'] = $textBox.val();
message['music_session'] = $sessionId;
message['client_id'] = context.JK.clientId;
return message;
}
function sendMessage() {
if(!context.JK.JamServer.connected) {
return false;
}
var msg = $textBox.val();
if(!msg || msg == '') {
// don't bother the server with empty messages
return false;
}
if(!sendingMessage) {
sendingMessage = true;
rest.createChatMessage(buildMessage())
.done(function() {
$textBox.val('');
renderMessage(msg, user.id, user.name, new Date().toISOString(), true);
})
.fail(function(jqXHR) {
app.notifyServerError(jqXHR, 'Unable to Send Chat Message');
})
.always(function() {
sendingMessage = false;
})
}
return false;
}
function scrollToBottom(instant) {
$chatMessagesScroller.animate({scrollTop: $chatMessagesScroller[0].scrollHeight}, instant ? 0 : 'slow');
}
function renderMessage(msg, senderId, senderName, sent, append) {
var options = {
msg: msg,
sender: senderId == user.id ? 'me' : senderName,
sent: sent
};
var txt = $(context._.template($('#template-chat-message').html(), options, { variable: 'data' }));
txt.find('.timeago').timeago();
if(append) {
$chatMessages.append(txt);
scrollToBottom();
}
else {
$chatMessages.prepend(txt);
}
}
function drainQueue() {
context._.each(renderQueue, function(msg) {
renderMessage(msg.message, msg.user_id, msg.user_name, msg.sent, true);
});
renderQueue = [];
}
// we handled the notification, meaning the dialog showed this message as a chat message
function handledNotification(payload) {
return showing && payload.description == "TEXT_MESSAGE";
}
function pasteIntoInput(el, text) {
el.focus();
if (typeof el.selectionStart == "number"
&& typeof el.selectionEnd == "number") {
var val = el.value;
var selStart = el.selectionStart;
el.value = val.slice(0, selStart) + text + val.slice(el.selectionEnd);
el.selectionEnd = el.selectionStart = selStart + text.length;
} else if (typeof document.selection != "undefined") {
var textRange = document.selection.createRange();
textRange.text = text;
textRange.collapse(false);
textRange.select();
}
}
function handleEnter(evt) {
if (evt.keyCode == 13 && evt.shiftKey) {
evt.preventDefault();
pasteIntoInput(this, "\n");
}
else if(evt.keyCode == 13 && !evt.shiftKey){
sendMessage();
return false;
}
}
function events(bind) {
if (bind) {
$form.submit(sendMessage);
$textBox.keydown(handleEnter);
$sendChatMessageBtn.click(sendMessage);
registerChatMessage(bind);
}
else {
$form.submit(null);
$textBox.keydown(null);
$sendChatMessageBtn.click(null);
}
}
// called from sidebar when messages come in
function chatMessageReceived(payload) {
if(fullyInitialized) {
if (isChatPanelVisible()) {
renderMessage(payload.msg, payload.sender_id, payload.sender_name, payload.created_at, true);
}
else {
highlightCount();
incrementChatCount();
renderQueue.push({message: payload.msg, user_id: payload.sender_id, user_name: payload.sender_name, sent: payload.created_at});
context.jamClient.UserAttention(true);
}
}
else {
renderQueue.push({message: payload.msg, user_id: payload.sender_id, user_name: payload.sender_name, sent: payload.created_at});
}
}
function registerChatMessage(bind) {
if (bind && bind == true) {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.CHAT_MESSAGE, function(header, payload) {
logger.debug("Handling CHAT_MESSAGE msg " + JSON.stringify(payload));
chatMessageReceived(payload);
handledNotification(payload);
});
}
else {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.CHAT_MESSAGE, null);
}
}
function opened() {
lowlightCount();
setCount(0);
drainQueue();
}
function sessionStarted(e, data) {
$sessionId = data.session.id;
// open chat panel
$chatSender.show();
$chatMessagesScroller.show();
$errorMsg.hide();
$panel.find('.panel-header').trigger('click');
$panel.on('open', opened);
$panel.find('.btn-next-pager').attr('href', '/api/sessions/' + $sessionId + '/chats?page=1');
reset();
// load previous chat messages
rest.getChatMessages(buildQuery())
.done(function (response) {
handleChatResponse(response);
scrollToBottom(true);
showing = true;
fullyInitialized = true;
drainQueue();
})
.fail(function (jqXHR) {
app.notifyServerError(jqXHR, 'Unable to Load Session Conversations')
});
events(true);
}
function sessionStopped(e, data) {
// open chat panel
$chatSender.hide();
$chatMessagesScroller.hide();
$errorMsg.show();
reset();
events(false);
}
function isChatPanelVisible() {
return $contents.is(':visible');
}
function incrementChatCount() {
var count = parseInt($count.text());
setCount(count + 1);
}
function setCount(count) {
$count.text(count);
}
function lowlightCount() {
$count.removeClass('highlighted');
}
function highlightCount() {
$count.addClass('highlighted');
}
function buildQuery() {
var query = {type: 'CHAT_MESSAGE', music_session: $sessionId, limit:LIMIT, page:currentPage};
if(next) {
query.start = next;
}
return query;
}
function renderChats(chats) {
context._.each(chats, function (chatMessage) {
renderMessage(chatMessage.message, chatMessage.user_id, chatMessage.user.name, chatMessage.created_at);
});
}
function handleChatResponse(response) {
next = response.next;
renderChats(response.chats);
if(response.next == null) {
// if we less results than asked for, end searching
$chatMessagesScroller.infinitescroll('pause');
logger.debug("end of chatss");
if(currentPage > 0) {
// there are bugs with infinitescroll not removing the 'loading'.
// it's most noticeable at the end of the list, so whack all such entries
$('.infinite-scroll-loader').remove();
}
}
else {
currentPage++;
buildQuery();
// registerInfiniteScroll();
}
}
function registerInfiniteScroll() {
$chatMessagesScroller.infinitescroll({
behavior: 'local',
navSelector: '.chat-sender .btn-next-pager',
nextSelector: '.chat-sender .btn-next-pager',
binder: $chatMessagesScroller,
dataType: 'json',
appendCallback: false,
prefill: false,
bufferPx: 100,
loading: {
msg: $('<div class="infinite-scroll-loader">Loading ...</div>'),
img: '/assets/shared/spinner-32.gif'
},
path: function(page) {
return '/api/sessions/' + $sessionId + '/chats?' + $.param(buildQuery());
}
},function(json, opts) {
handleChatResponse(json);
});
$chatMessagesScroller.infinitescroll('resume');
}
function initialize(sidebarInstance) {
sidebar = sidebarInstance;
$panel = $('[layout-id="panelChat"]');
$contents = $panel.find('.chatcontents');
$chatMessagesScroller = $panel.find('.chat-list-scroller');
$count = $panel.find('#sidebar-chat-count');
$chatMessages = $panel.find('.previous-chat-list');
$sendChatMessageBtn = $panel.find('.btn-send-chat-message');
$chatSender = $panel.find('.chat-sender');
$form = $panel.find('.chat-message-form');
$textBox = $form.find('textarea');
$errorMsg = $panel.find('.chat-status');
$errorMsg.show();
$chatSender.hide();
$chatMessagesScroller.hide();
app.user()
.done(function (userDetail) {
user = userDetail;
});
}
this.initialize = initialize;
this.sessionStarted = sessionStarted;
this.sessionStopped = sessionStopped;
this.registerChatMessage = registerChatMessage;
};
return this;
})(window,jQuery);

View File

@ -588,6 +588,10 @@
}
function UserAttention(option) {
}
// passed an array of recording objects from the server
function GetLocalRecordingState(recordings) {
var result = { recordings:[]};
@ -754,6 +758,8 @@
this.OnLoggedIn = OnLoggedIn;
this.OnLoggedOut = OnLoggedOut;
this.UserAttention = UserAttention;
// Recording Playback
this.GetLocalRecordingState = GetLocalRecordingState;
this.OpenRecording = OpenRecording;

View File

@ -944,19 +944,21 @@
});
}
function createChatMessage(session_id, options) {
function createChatMessage(options) {
return $.ajax({
type: "POST",
url: '/api/sessions/' + session_id + '/chat?' + $.param(options),
url: '/api/chat?' + $.param(options),
dataType: "json",
contentType: 'application/json'
});
}
function getChatMessages(session_id, options) {
function getChatMessages(options) {
var musciSessionId = options["music_session"];
delete options["music_session"];
return $.ajax({
type: "GET",
url: '/api/sessions/' + session_id + '/chats' + $.param(options),
url: '/api/sessions/' + musciSessionId + '/chats?' + $.param(options),
dataType: "json",
contentType: 'application/json'
});

View File

@ -95,9 +95,24 @@
notificationPanel.initialize(me, textMessageDialog);
}
// session events for chat
function registerSessionEvents() {
$(document).on('jamkazam.session_started', function(e, data){
chatPanel.sessionStarted(e, data);
return false;
});
$(document).on('jamkazam.session_stopped', function(e, data) {
chatPanel.sessionStopped(e, data);
return false;
});
}
function initializeChatPanel() {
// chatPanel = new context.JK.ChatPanel(app);
// chatPanel.initialize(me, textMessageDialog);
chatPanel = new context.JK.ChatPanel(app);
chatPanel.initialize(me);
registerSessionEvents();
}
function search(query) {

View File

@ -172,29 +172,66 @@
font-size: 100%;
}
.chat-fixed {
position:static;
.chatcontents {
position: relative;
}
.chat-select {
font-size:12px;
text-align:center;
.chat-list-scroller {
position: relative;
display: block;
overflow: auto;
margin: 0px 15px;
height: 210px;
}
.btn-send-chat-message {
margin-top: 5px;
margin-right: 30px;
}
#new-chat-message {
width: 90%;
height: 40px;
}
.chat-status {
line-height: 20px;
text-align: center;
padding: 10px;
}
.chat-message {
margin:5px 0;
.chat-message-sender {
font-weight:bold;
margin-right:10px;
color: #020C81;
&:after {
content:':'
}
}
.chat-message-text {
line-height:18px;
white-space:pre-line;
color: #D5E2E4;
}
.chat-message-timestamp {
margin-top:4px;
}
.chat-select select {
color:#AAA;
display:block;
font-size:12px;
color:$ColorSidebarText;
background-color:shade($ColorElementPrimary, 20);
border:none;
}
}
.chat-text {
float:left;
width:180px;
font-size:11px;
margin-top:5px;
margin-bottom:5px;
color:#D5E2E4;
.chat-sender {
width: 100%;
position: absolute;
bottom: 10px;
padding: 0px 15px;
}
em {

View File

@ -7,18 +7,21 @@ class ApiChatsController < ApiController
def create
@chat_msg = ChatMessage.new
@chat_msg.user_id = current_user.id
@chat_msg.session_id = params[:music_session]
@chat_msg.message = params[:msg]
@chat_msg.music_session_id = @music_session.id
@chat_msg.message = params[:message]
if @chat_msg.save
ChatMessage.send_chat_msg @music_session, @chat_msg, current_user
ChatMessage.send_chat_msg @music_session, @chat_msg, current_user, params[:client_id]
end
respond_with_model(@chat_msg)
end
def index
@chat_msgs = ChatMessage.find_by_session_id(params[:music_session])
.paginate(page: params[:page], per_page: pararms[:per_page] || 20)
data = ChatMessage.index(current_user, params)
@chats = data[0]
@next = data[1]
render "api_chats/index", :layout => nil
end
def check_session

View File

@ -0,0 +1,7 @@
node :next do |page|
@next
end
node :chats do |page|
partial "api_chats/show", object: @chats
end

View File

@ -0,0 +1,11 @@
object @chat
attributes :message, :user_id, :session_id, :created_at
node :user do |c|
user_data = {}
if c.user_id
user_data[:name] = c.user.name
end
user_data
end

View File

@ -93,7 +93,6 @@
</div>
<!-- Chat -->
<!--
<div layout="panel" layout-id="panelChat">
<div layout-panel="collapsed">
</div>
@ -101,24 +100,26 @@
<div layout-panel="header" class="panel-header">
<h2>chat<div id="sidebar-chat-count" class="badge">0</div></h2>
</div>
<div layout-panel="contents" class="panelcontents">
<div class="chat-fixed">
<input id="chat-input" type="text" placeholder="enter message" /><br />
<div class="chat-select">Send to:
<select id="sidebar-chat-friend-list">
<option>Everyone</option>
<option>All Musicians</option>
<option>All Fans</option>
</select>
<div layout-panel="contents" class="chatcontents">
<div class="chat-status">
<span>Chat is available during session is connected.</span>
</div>
<div class="chat-list-scroller">
<div class="previous-chat-list">
</div>
</div>
<div class="chat-sender">
<form class="chat-message-form">
<textarea name="chat-message" id="new-chat-message" placeholder="enter message"></textarea>
</form>
<div class="right">
<%= link_to 'NEXT', '#', class: 'btn-next-pager' %>
<%= link_to 'SEND', '#', class: 'button-orange btn-send-chat-message' %>
</div>
</div>
<ul id="sidebar-chat-list">
</ul>
</div>
</div>
</div>
-->
<!-- Notifications -->
<div layout="panel" layout-id="panelNotifications">
@ -220,20 +221,17 @@
<div id="div-actions">
<a id="btn-notification-action" class="button-orange smallbutton right"></a>
</div>
<br/ >
<br/>
<br clear="all" />
</li>
</script>
<!-- Chat panel template -->
<script type="text/template" id="template-chat-panel">
<li>
<div class="avatar-small"><img src="{avatar_url}" /></div>
<div class="chat-text">
<strong>{label}:</strong>&nbsp;{text}
<em>({date})</em>
</div>
<br clear="all" />
</li>
<script type="text/template" id="template-chat-message">
<div class="chat-message">
<span class="chat-message-sender">{{data.sender}}</span>
<span class="chat-message-text">{{data.msg}}</span>
<time class="chat-message-timestamp timeago" datetime="{{data.sent}}">{{data.sent}}</time>
</div>
</script>

View File

@ -260,8 +260,8 @@ SampleApp::Application.routes.draw do
match '/users/:id/share/recording/:provider' => 'api_users#share_recording', :via => :get
# session chat
match '/sessions/:id/chat' => 'api_chats#create', :via => :post
match '/sessions/:id/chats' => 'api_chats#index', :via => :get
match '/chat' => 'api_chats#create', :via => :post
match '/sessions/:music_session/chats' => 'api_chats#index', :via => :get
# user recordings
# match '/users/:id/recordings' => 'api_users#recording_index', :via => :get

View File

@ -0,0 +1,103 @@
require 'spec_helper'
describe "Chat Message", :js => true, :type => :feature, :capybara_feature => true do
before(:all) do
Capybara.default_wait_time = 15
end
let(:user1) { FactoryGirl.create(:user) }
let(:user2) { FactoryGirl.create(:user) }
before(:each) do
UserMailer.deliveries.clear
MusicSession.delete_all
ChatMessage.delete_all
end
# what are all the ways to be in a session?
describe "join session" do
it "on try to send chat before joining session" do
description = "Try to send chat message before joining session!"
create_session(creator: user1, description: description)
in_client(user2) do
sign_in_poltergeist(user1)
find("[layout-id=\"panelChat\"] .panel-header").trigger(:click)
find(".chat-status", text: 'Chat is available during session is connected.')
end
end
it "on join a session" do
description = "Find chat panel expanded when join session"
create_session(creator: user1, description: description)
join_session(user2, description: description)
find(".chatcontents").should be_visible
find("[layout-id=\"panelChat\"] .chat-sender").should be_visible
end
end
describe "sidebar session chat behavior" do
it "send a message" do
description = "Find chat panel expanded when join session"
create_session(creator: user1, description: description)
join_session(user2, description: description)
in_client(user1) do
send_chat_message("Hey, I am #{user1.id}")
find('.chat-message-text', text: "Hey, I am #{user1.id}")
end
in_client(user2) do
find('.chat-message-text', text: "Hey, I am #{user1.id}")
send_chat_message("Received, I am #{user2.id}")
end
in_client(user1) do
find('.chat-message-text', text: "Received, I am #{user2.id}")
end
end
it "shows error with a notify" do
description = "Find chat panel expanded when join session"
create_session(creator: user1, description: description)
join_session(user2, description: description)
in_client(user2) do
chat_max = 256
chat_msg = 'a' * (chat_max + 1)
send_chat_message(chat_msg)
find('#notification').should have_text("Unable to Send Chat Message")
end
end
it "shows badge if not on chat panel" do
description = "Find chat panel expanded when join session"
create_session(creator: user1, description: description)
join_session(user2, description: description)
in_client(user1) do
find("[layout-id=\"panelFriends\"] .panel-header").trigger(:click)
end
in_client(user2) do
send_chat_message("Detect badge information")
end
in_client(user1) do
find('#sidebar-chat-count.badge.highlighted', text: "1")
find("[layout-id=\"panelChat\"] .panel-header").trigger(:click)
find('.chat-message-text', text: "Detect badge information")
end
end
end
end

View File

@ -71,6 +71,16 @@ def send_text_message(msg, options={})
end
end
# sends a chat message during session
def send_chat_message(msg)
find("[layout-id=\"panelChat\"] .chat-sender").should be_visible
within("[layout-id=\"panelChat\"] .chat-sender form.chat-message-form") do
fill_in 'new-chat-message', with: msg
end
find("[layout-id=\"panelChat\"] .chat-sender .btn-send-chat-message").trigger(:click)
end
def open_notifications
find("#{NOTIFICATION_PANEL} .panel-header").trigger(:click)
end