Merge branch 'develop' of bitbucket.org:jamkazam/jam-cloud into develop

This commit is contained in:
Brian Smith 2014-04-09 21:18:57 -04:00
commit ff03770256
48 changed files with 1119 additions and 571 deletions

View File

@ -48,7 +48,7 @@ gem 'unf', '0.1.3' #optional fog dependency
gem 'country-select'
gem 'aasm', '3.0.16'
gem 'postgres-copy', '0.6.0'
gem 'aws-sdk', '1.29.1'
gem 'aws-sdk' #, '1.29.1'
gem 'bugsnag'
gem 'gon'
gem 'cocoon'

View File

@ -28,7 +28,7 @@ gem 'amqp', '1.0.2'
gem 'will_paginate'
gem 'actionmailer', '3.2.13'
gem 'sendgrid', '1.2.0'
gem 'aws-sdk', '1.29.1'
gem 'aws-sdk' #, '1.29.1'
gem 'carrierwave', '0.9.0'
gem 'aasm', '3.0.16'
gem 'devise', '>= 1.1.2'

View File

@ -36,7 +36,7 @@ module JamWebEventMachine
end
def self.run_em
def self.run_em(calling_thread)
EM.run do
# this is global because we need to check elsewhere if we are currently connected to amqp before signalling success with some APIs, such as 'create session'
@ -53,6 +53,8 @@ module JamWebEventMachine
MQRouter.user_exchange = exchange
end
end
calling_thread.wakeup
end
end
@ -63,9 +65,12 @@ module JamWebEventMachine
end
def self.run
current = Thread.current
Thread.new do
run_em
run_em(current)
end
Thread.stop
end
def self.start
@ -79,8 +84,9 @@ module JamWebEventMachine
EM.stop
end
@@log.debug("starting EventMachine")
current = Thread.current
Thread.new do
run_em
run_em(current)
end
die_gracefully_on_signal
end

View File

@ -102,7 +102,7 @@ FOO
def send_test_batch
self.perform_event('do_test_run!')
if 'test'==Rails.env
if 'test' == Rails.env
BatchMailer.send_batch_email_test(self.id).deliver!
else
BatchMailer.send_batch_email_test(self.id).deliver

View File

@ -366,6 +366,8 @@ module JamRuby
def send_session_ended(session_id)
return if session_id.nil? # so we don't query every notification in the system with a nil session_id
notifications = Notification.where(:session_id => session_id)
# publish to all users who have a notification for this session

View File

@ -68,6 +68,8 @@ describe RecordedTrack do
describe "aws-based operations", :aws => true do
def put_file_to_aws(signed_data, contents)
begin
RestClient.put( signed_data[:url],
contents,
{
@ -76,6 +78,11 @@ describe RecordedTrack do
:'Content-MD5' => signed_data[:md5],
:Authorization => signed_data[:authorization]
})
rescue => e
puts e.response
raise e
end
end
# create a test file
upload_file='some_file.ogg'

View File

@ -1,3 +1,6 @@
ENV["RAILS_ENV"] = "test"
require 'simplecov'
require 'support/utilities'
require 'active_record'
@ -28,13 +31,14 @@ require 'factories'
include JamRuby
# manually register observers
ActiveRecord::Base.add_observer InvitedUserObserver.instance
ActiveRecord::Base.add_observer UserObserver.instance
ActiveRecord::Base.add_observer FeedbackObserver.instance
ActiveRecord::Base.add_observer RecordedTrackObserver.instance
RecordedTrack.observers.disable :all # only a few tests want this observer active
#RecordedTrack.observers.disable :all # only a few tests want this observer active
# put ActionMailer into test mode
ActionMailer::Base.delivery_method = :test

View File

@ -2,8 +2,9 @@ JAMKAZAM_TESTING_BUCKET = 'jamkazam-testing' # cuz i'm not comfortable using aws
def app_config
klass = Class.new do
def aws_bucket
JAMKAZAM_TESTING_BUCKET
JAMKAZAM_TESTING_BUCKET
end
def aws_access_key_id
@ -14,6 +15,18 @@ def app_config
'h0V0ffr3JOp/UtgaGrRfAk25KHNiO9gm8Pj9m6v3'
end
def aws_region
'us-east-1'
end
def aws_bucket_public
'jamkazam-testing-public'
end
def aws_cache
'315576000'
end
def audiomixer_path
# you can specify full path to audiomixer with AUDIOMIXER_PATH env variable...
# or we check for audiomixer path in the user's workspace
@ -77,30 +90,6 @@ def app_config
"#{external_protocol}#{external_hostname}#{(external_port == 80 || external_port == 443) ? '' : ':' + external_port.to_s}"
end
def aws_access_key_id
'AKIAJESQY24TOT542UHQ'
end
def aws_secret_access_key
'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'
end
def aws_region
'us-east-1'
end
def aws_bucket
'jamkazam-testing'
end
def aws_bucket_public
'jamkazam-testing-public'
end
def aws_cache
'315576000'
end
def max_audio_downloads
100
end
@ -124,7 +113,7 @@ def app_config
end
return klass.new
klass.new
end
def run_tests? type

View File

@ -48,7 +48,7 @@ gem 'fb_graph', '2.5.9'
gem 'sendgrid', '1.2.0'
gem 'recaptcha', '0.3.4'
gem 'filepicker-rails', '0.1.0'
gem 'aws-sdk', '1.29.1'
gem 'aws-sdk' #, '1.29.1'
gem 'aasm', '3.0.16'
gem 'carrierwave', '0.9.0'
gem 'carrierwave_direct'

View File

@ -1,15 +1,47 @@
// The wrapper around the web-socket connection to the server
(function(context, $) {
// manages the connection, heartbeats, and reconnect logic.
// presents itself as a dialog, or in-situ banner (_jamServer.html.haml)
(function (context, $) {
"use strict";
"use strict";
context.JK = context.JK || {};
context.JK = context.JK || {};
var logger = context.JK.logger;
var msg_factory = context.JK.MessageFactory;
var logger = context.JK.logger;
var msg_factory = context.JK.MessageFactory;
// Let socket.io know where WebSocketMain.swf is
context.WEB_SOCKET_SWF_LOCATION = "assets/flash/WebSocketMain.swf";
// Let socket.io know where WebSocketMain.swf is
context.WEB_SOCKET_SWF_LOCATION = "assets/flash/WebSocketMain.swf";
context.JK.JamServer = function (app) {
// heartbeat
var heartbeatInterval = null;
var heartbeatMS = null;
var heartbeatMissedMS = 10000; // if 5 seconds go by and we haven't seen a heartbeat ack, get upset
var lastHeartbeatAckTime = null;
var lastHeartbeatFound = false;
var heartbeatAckCheckInterval = null;
var notificationLastSeenAt = undefined;
var notificationLastSeen = undefined;
// reconnection logic
var connectDeferred = null;
var freezeInteraction = false;
var countdownInterval = null;
var reconnectAttemptLookup = [2, 2, 2, 4, 8, 15, 30];
var reconnectAttempt = 0;
var reconnectingWaitPeriodStart = null;
var reconnectDueTime = null;
var connectTimeout = null;
// elements
var $inSituBanner = null;
var $inSituBannerHolder = null;
var $messageContents = null;
var $dialog = null;
var $templateServerConnection = null;
var $templateDisconnected = null;
var $currentDisplay = null;
var server = {};
server.socket = {};
@ -21,141 +53,459 @@
server.connected = false;
// if activeElementVotes is null, then we are assuming this is the initial connect sequence
function initiateReconnect(activeElementVotes, in_error) {
var initialConnect = !!activeElementVotes;
freezeInteraction = activeElementVotes && ((activeElementVotes.dialog && activeElementVotes.dialog.freezeInteraction === true) || (activeElementVotes.screen && activeElementVotes.screen.freezeInteraction === true));
if(!initialConnect) {
context.JK.CurrentSessionModel.onWebsocketDisconnected(in_error);
}
if(in_error) {
reconnectAttempt = 0;
$currentDisplay = renderDisconnected();
beginReconnectPeriod();
}
}
// handles logic if the websocket connection closes, and if it was in error then also prompt for reconnect
function closedCleanup(in_error) {
if(server.connected) {
server.connected = false;
context.JK.CurrentSessionModel.onWebsocketDisconnected(in_error);
// stop future heartbeats
if (heartbeatInterval != null) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
// notify anyone listening that the socket closed
var len = server.socketClosedListeners.length;
for(var i = 0; i < len; i++) {
try {
server.socketClosedListeners[i](in_error);
} catch (ex) {
logger.warn('exception in callback for websocket closed event:' + ex);
}
}
// stop checking for heartbeat acks
if (heartbeatAckCheckInterval != null) {
clearTimeout(heartbeatAckCheckInterval);
heartbeatAckCheckInterval = null;
}
if (server.connected) {
server.connected = false;
if(app.clientUpdating) {
// we don't want to do a 'cover the whole screen' dialog
// because the client update is already showing.
return;
}
server.reconnecting = true;
var result = app.activeElementEvent('beforeDisconnect');
initiateReconnect(result, in_error);
app.activeElementEvent('afterDisconnect');
// notify anyone listening that the socket closed
var len = server.socketClosedListeners.length;
for (var i = 0; i < len; i++) {
try {
server.socketClosedListeners[i](in_error);
} catch (ex) {
logger.warn('exception in callback for websocket closed event:' + ex);
}
}
}
}
server.registerOnSocketClosed = function(callback) {
server.socketClosedListeners.push(callback);
////////////////////
//// HEARTBEAT /////
////////////////////
function _heartbeatAckCheck() {
// if we've seen an ack to the latest heartbeat, don't bother with checking again
// this makes us resilient to front-end hangs
if (lastHeartbeatFound) {
return;
}
// check if the server is still sending heartbeat acks back down
// this logic equates to 'if we have not received a heartbeat within heartbeatMissedMS, then get upset
if (new Date().getTime() - lastHeartbeatAckTime.getTime() > heartbeatMissedMS) {
logger.error("no heartbeat ack received from server after ", heartbeatMissedMS, " seconds . giving up on socket connection");
context.JK.JamServer.close(true);
}
else {
lastHeartbeatFound = true;
}
}
server.registerMessageCallback = function(messageType, callback) {
if (server.dispatchTable[messageType] === undefined) {
server.dispatchTable[messageType] = [];
}
server.dispatchTable[messageType].push(callback);
};
server.unregisterMessageCallback = function(messageType, callback) {
if (server.dispatchTable[messageType] !== undefined) {
for(var i = server.dispatchTable[messageType].length; i--;) {
if (server.dispatchTable[messageType][i] === callback)
{
server.dispatchTable[messageType].splice(i, 1);
break;
}
}
if (server.dispatchTable[messageType].length === 0) {
delete server.dispatchTable[messageType];
}
}
};
server.connect = function() {
logger.log("server.connect");
var uri = context.JK.websocket_gateway_uri; // Set in index.html.erb.
//var uri = context.gon.websocket_gateway_uri; // Leaving here for now, as we're looking for a better solution.
server.socket = new context.WebSocket(uri);
server.socket.onopen = server.onOpen;
server.socket.onmessage = server.onMessage;
server.socket.onclose = server.onClose;
};
server.close = function(in_error) {
logger.log("closing websocket");
server.socket.close();
closedCleanup(in_error);
function _heartbeat() {
if (app.heartbeatActive) {
var message = context.JK.MessageFactory.heartbeat(notificationLastSeen, notificationLastSeenAt);
notificationLastSeenAt = undefined;
notificationLastSeen = undefined;
context.JK.JamServer.send(message);
lastHeartbeatFound = false;
}
}
server.rememberLogin = function() {
var token, loginMessage;
token = $.cookie("remember_token");
var clientType = context.jamClient.IsNativeClient() ? 'client' : 'browser';
loginMessage = msg_factory.login_with_token(token, null, clientType);
server.send(loginMessage);
};
function loggedIn(header, payload) {
server.onOpen = function() {
logger.log("server.onOpen");
server.rememberLogin();
};
if(!connectTimeout) {
clearTimeout(connectTimeout);
connectTimeout = null;
}
server.onMessage = function(e) {
var message = JSON.parse(e.data),
messageType = message.type.toLowerCase(),
payload = message[messageType],
callbacks = server.dispatchTable[message.type];
app.clientId = payload.client_id;
if(message.type != context.JK.MessageType.HEARTBEAT_ACK && message.type != context.JK.MessageType.PEER_MESSAGE) {
logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload));
// tell the backend that we have logged in
context.jamClient.OnLoggedIn(payload.user_id, payload.token);
$.cookie('client_id', payload.client_id);
heartbeatMS = payload.heartbeat_interval * 1000;
logger.debug("jamkazam.js.loggedIn(): clientId now " + app.clientId + "; Setting up heartbeat every " + heartbeatMS + " MS");
heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS);
heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000);
lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat
connectDeferred.resolve();
app.activeElementEvent('afterConnect', payload);
}
function heartbeatAck(header, payload) {
lastHeartbeatAckTime = new Date();
context.JK.CurrentSessionModel.trackChanges(header, payload);
}
function registerLoginAck() {
logger.debug("register for loggedIn to set clientId");
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.LOGIN_ACK, loggedIn);
}
function registerHeartbeatAck() {
logger.debug("register for heartbeatAck");
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, heartbeatAck);
}
function registerSocketClosed() {
logger.debug("register for socket closed");
context.JK.JamServer.registerOnSocketClosed(socketClosed);
}
/**
* Called whenever the websocket closes; this gives us a chance to cleanup things that should be stopped/cleared
* @param in_error did the socket close abnormally?
*/
function socketClosed(in_error) {
// tell the backend that we have logged out
context.jamClient.OnLoggedOut();
}
///////////////////
/// RECONNECT /////
///////////////////
function internetUp() {
var start = new Date().getTime();
server.connect()
.done(function() {
guardAgainstRapidTransition(start, performReconnect);
})
.fail(function() {
guardAgainstRapidTransition(start, closedOnReconnectAttempt);
});
}
// websocket couldn't connect. let's try again soon
function closedOnReconnectAttempt() {
failedReconnect();
}
function performReconnect() {
if($currentDisplay.is('.no-websocket-connection')) {
$currentDisplay.hide();
// TODO: tell certain elements that we've reconnected
}
else {
context.JK.CurrentSessionModel.leaveCurrentSession()
.always(function() {
window.location.reload();
});
}
server.reconnecting = false;
}
function buildOptions() {
return {};
}
function renderDisconnected() {
var content = null;
if(freezeInteraction) {
var template = $templateDisconnected.html();
var templateHtml = $(context.JK.fillTemplate(template, buildOptions()));
templateHtml.find('.reconnect-countdown').html(formatDelaySecs(reconnectDelaySecs()));
content = context.JK.Banner.show({
html : templateHtml,
type: 'reconnect'
}) ;
}
else {
var $inSituContent = $(context._.template($templateServerConnection.html(), buildOptions(), { variable: 'data' }));
$inSituContent.find('.reconnect-countdown').html(formatDelaySecs(reconnectDelaySecs()));
$messageContents.empty();
$messageContents.append($inSituContent);
$inSituBannerHolder.show();
content = $inSituBannerHolder;
}
return content;
}
function formatDelaySecs(secs) {
return $('<span class="countdown-seconds"><span class="countdown">' + secs + '</span> ' + (secs == 1 ? ' second.<span style="visibility:hidden">s</span>' : 'seconds.') + '</span>');
}
function setCountdown($parent) {
$parent.find('.reconnect-countdown').html(formatDelaySecs(reconnectDelaySecs()));
}
function renderCouldNotReconnect() {
return renderDisconnected();
}
function renderReconnecting() {
$currentDisplay.find('.reconnect-progress-msg').text('Attempting to reconnect...')
if($currentDisplay.is('.no-websocket-connection')) {
$currentDisplay.find('.disconnected-reconnect').removeClass('reconnect-enabled').addClass('reconnect-disabled');
}
else {
$currentDisplay.find('.disconnected-reconnect').removeClass('button-orange').addClass('button-grey');
}
}
function failedReconnect() {
reconnectAttempt += 1;
$currentDisplay = renderCouldNotReconnect();
beginReconnectPeriod();
}
function guardAgainstRapidTransition(start, nextStep) {
var now = new Date().getTime();
if ((now - start) < 1500) {
setTimeout(function() {
nextStep();
}, 1500 - (now - start))
}
else {
nextStep();
}
}
function attemptReconnect() {
var start = new Date().getTime();
renderReconnecting();
rest.serverHealthCheck()
.done(function() {
guardAgainstRapidTransition(start, internetUp);
})
.fail(function(xhr, textStatus, errorThrown) {
if(xhr && xhr.status >= 100) {
// we could connect to the server, and it's alive
guardAgainstRapidTransition(start, internetUp);
}
else {
guardAgainstRapidTransition(start, failedReconnect);
}
});
return false;
}
function clearReconnectTimers() {
if(countdownInterval) {
clearInterval(countdownInterval);
countdownInterval = null;
}
}
function beginReconnectPeriod() {
// allow user to force reconnect
$currentDisplay.find('a.disconnected-reconnect').unbind('click').click(function() {
if($(this).is('.button-orange') || $(this).is('.reconnect-enabled')) {
clearReconnectTimers();
attemptReconnect();
}
return false;
});
if (callbacks !== undefined) {
var len = callbacks.length;
for(var i = 0; i < len; i++) {
try {
callbacks[i](message, payload);
} catch (ex) {
logger.warn('exception in callback for websocket message:' + ex);
throw ex;
}
}
reconnectingWaitPeriodStart = new Date().getTime();
reconnectDueTime = reconnectingWaitPeriodStart + reconnectDelaySecs() * 1000;
// update count down timer periodically
countdownInterval = setInterval(function() {
var now = new Date().getTime();
if(now > reconnectDueTime) {
clearReconnectTimers();
attemptReconnect();
}
else {
logger.log("Unexpected message type %s.", message.type);
var secondsUntilReconnect = Math.ceil((reconnectDueTime - now) / 1000);
$currentDisplay.find('.reconnect-countdown').html(formatDelaySecs(secondsUntilReconnect));
}
}, 333);
}
function reconnectDelaySecs() {
if (reconnectAttempt > reconnectAttemptLookup.length - 1) {
return reconnectAttemptLookup[reconnectAttemptLookup.length - 1];
}
else {
return reconnectAttemptLookup[reconnectAttempt];
}
}
server.registerOnSocketClosed = function (callback) {
server.socketClosedListeners.push(callback);
}
server.registerMessageCallback = function (messageType, callback) {
if (server.dispatchTable[messageType] === undefined) {
server.dispatchTable[messageType] = [];
}
server.dispatchTable[messageType].push(callback);
};
server.onClose = function() {
logger.log("Socket to server closed.");
server.unregisterMessageCallback = function (messageType, callback) {
if (server.dispatchTable[messageType] !== undefined) {
for (var i = server.dispatchTable[messageType].length; i--;) {
if (server.dispatchTable[messageType][i] === callback) {
server.dispatchTable[messageType].splice(i, 1);
break;
}
}
closedCleanup(true);
if (server.dispatchTable[messageType].length === 0) {
delete server.dispatchTable[messageType];
}
}
};
server.send = function(message) {
server.connect = function () {
connectDeferred = new $.Deferred();
logger.log("server.connect");
var uri = context.JK.websocket_gateway_uri; // Set in index.html.erb.
//var uri = context.gon.websocket_gateway_uri; // Leaving here for now, as we're looking for a better solution.
var jsMessage = JSON.stringify(message);
server.socket = new context.WebSocket(uri);
server.socket.onopen = server.onOpen;
server.socket.onmessage = server.onMessage;
server.socket.onclose = server.onClose;
if(message.type != context.JK.MessageType.HEARTBEAT && message.type != context.JK.MessageType.PEER_MESSAGE) {
logger.log("server.send(" + jsMessage + ")");
}
if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) {
server.socket.send(jsMessage);
} else {
logger.log("Dropped message because server connection is closed.");
connectTimeout = setTimeout(function() {
connectTimeout = null;
if(connectDeferred.state() === 'pending') {
connectDeferred.reject();
}
}, 4000);
return connectDeferred;
};
server.loginSession = function(sessionId) {
var loginMessage;
server.close = function (in_error) {
logger.log("closing websocket");
if (!server.signedIn) {
logger.log("Not signed in!");
// TODO: surface the error
return;
server.socket.close();
closedCleanup(in_error);
}
server.rememberLogin = function () {
var token, loginMessage;
token = $.cookie("remember_token");
var clientType = context.jamClient.IsNativeClient() ? 'client' : 'browser';
loginMessage = msg_factory.login_with_token(token, null, clientType);
server.send(loginMessage);
};
server.onOpen = function () {
logger.log("server.onOpen");
server.rememberLogin();
};
server.onMessage = function (e) {
var message = JSON.parse(e.data),
messageType = message.type.toLowerCase(),
payload = message[messageType],
callbacks = server.dispatchTable[message.type];
if (message.type != context.JK.MessageType.HEARTBEAT_ACK && message.type != context.JK.MessageType.PEER_MESSAGE) {
logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload));
}
if (callbacks !== undefined) {
var len = callbacks.length;
for (var i = 0; i < len; i++) {
try {
callbacks[i](message, payload);
} catch (ex) {
logger.warn('exception in callback for websocket message:' + ex);
throw ex;
}
}
}
else {
logger.log("Unexpected message type %s.", message.type);
}
};
loginMessage = msg_factory.login_jam_session(sessionId);
server.send(loginMessage);
server.onClose = function () {
logger.log("Socket to server closed.");
if(connectDeferred.state() === "pending") {
connectDeferred.reject();
}
closedCleanup(true);
};
server.send = function (message) {
var jsMessage = JSON.stringify(message);
if (message.type != context.JK.MessageType.HEARTBEAT && message.type != context.JK.MessageType.PEER_MESSAGE) {
logger.log("server.send(" + jsMessage + ")");
}
if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) {
server.socket.send(jsMessage);
} else {
logger.log("Dropped message because server connection is closed.");
}
};
server.loginSession = function (sessionId) {
var loginMessage;
if (!server.signedIn) {
logger.log("Not signed in!");
// TODO: surface the error
return;
}
loginMessage = msg_factory.login_jam_session(sessionId);
server.send(loginMessage);
};
/** with the advent of the reliable UDP channel, this is no longer how messages are sent from client-to-clent
@ -163,47 +513,88 @@
* @param receiver_id client ID of message to send
* @param message the actual message
*/
server.sendP2PMessage = function(receiver_id, message) {
//logger.log("P2P message from [" + server.clientID + "] to [" + receiver_id + "]: " + message);
//console.time('sendP2PMessage');
var outgoing_msg = msg_factory.client_p2p_message(server.clientID, receiver_id, message);
server.send(outgoing_msg);
//console.timeEnd('sendP2PMessage');
server.sendP2PMessage = function (receiver_id, message) {
//logger.log("P2P message from [" + server.clientID + "] to [" + receiver_id + "]: " + message);
//console.time('sendP2PMessage');
var outgoing_msg = msg_factory.client_p2p_message(server.clientID, receiver_id, message);
server.send(outgoing_msg);
//console.timeEnd('sendP2PMessage');
};
server.updateNotificationSeen = function(notificationId, notificationCreatedAt) {
var time = new Date(notificationCreatedAt);
if(!notificationCreatedAt) {
throw 'invalid value passed to updateNotificationSeen'
}
if(!notificationLastSeenAt) {
notificationLastSeenAt = notificationCreatedAt;
notificationLastSeen = notificationId;
logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
}
else if(time.getTime() > new Date(notificationLastSeenAt).getTime()) {
notificationLastSeenAt = notificationCreatedAt;
notificationLastSeen = notificationId;
logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
}
else {
logger.debug("ignored notificationLastSeenAt for: " + notificationCreatedAt);
}
}
// Message callbacks
server.registerMessageCallback(context.JK.MessageType.LOGIN_ACK, function (header, payload) {
server.signedIn = true;
logger.debug("Handling LOGIN_ACK. Updating client id to " + payload.client_id);
server.clientID = payload.client_id;
server.publicIP = payload.public_ip;
server.connected = true;
if (context.jamClient !== undefined) {
logger.debug("... (handling LOGIN_ACK) Updating backend client, connected to true and clientID to " +
payload.client_id);
context.jamClient.connected = true;
context.jamClient.clientID = server.clientID;
}
});
server.registerMessageCallback(context.JK.MessageType.PEER_MESSAGE, function (header, payload) {
if (context.jamClient !== undefined) {
context.jamClient.P2PMessageReceived(header.from, payload.message);
}
});
context.JK.JamServer = server;
// Message callbacks
server.registerMessageCallback(context.JK.MessageType.LOGIN_ACK, function(header, payload) {
server.signedIn = true;
logger.debug("Handling LOGIN_ACK. Updating client id to " + payload.client_id);
server.clientID = payload.client_id;
server.publicIP = payload.public_ip;
server.connected = true;
if (context.jamClient !== undefined)
{
logger.debug("... (handling LOGIN_ACK) Updating backend client, connected to true and clientID to " +
payload.client_id);
context.jamClient.connected = true;
context.jamClient.clientID = server.clientID;
}
});
server.registerMessageCallback(context.JK.MessageType.PEER_MESSAGE, function(header, payload) {
if (context.jamClient !== undefined)
{
context.jamClient.P2PMessageReceived(header.from, payload.message);
}
});
// Callbacks from jamClient
if (context.jamClient !== undefined)
{
context.jamClient.SendP2PMessage.connect(server.sendP2PMessage);
if (context.jamClient !== undefined) {
context.jamClient.SendP2PMessage.connect(server.sendP2PMessage);
}
function initialize() {
registerLoginAck();
registerHeartbeatAck();
registerSocketClosed();
$inSituBanner = $('.server-connection');
$inSituBannerHolder = $('.no-websocket-connection');
$messageContents = $inSituBannerHolder.find('.message-contents');
$dialog = $('#banner');
$templateServerConnection = $('#template-server-connection');
$templateDisconnected = $('#template-disconnected');
if($inSituBanner.length != 1) { throw "found wrong number of .server-connection: " + $inSituBanner.length; }
if($inSituBannerHolder.length != 1) { throw "found wrong number of .no-websocket-connection: " + $inSituBannerHolder.length; }
if($messageContents.length != 1) { throw "found wrong number of .message-contents: " + $messageContents.length; }
if($dialog.length != 1) { throw "found wrong number of #banner: " + $dialog.length; }
if($templateServerConnection.length != 1) { throw "found wrong number of #template-server-connection: " + $templateServerConnection.length; }
if($templateDisconnected.length != 1) { throw "found wrong number of #template-disconnected: " + $templateDisconnected.length; }
}
this.initialize = initialize;
this.initiateReconnect = initiateReconnect;
return this;
}
})(window, jQuery);

View File

@ -25,7 +25,7 @@
return newContent;
}
$('#banner').show()
$('#banner').attr('data-type', options.type).show()
$('#banner_overlay').show()
// return the core of the banner so that caller can attach event handlers to newly created HTML

View File

@ -123,6 +123,11 @@
return false;
}
if(!context.JK.JamServer.connected) {
app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.');
return false;
}
// If user hasn't completed FTUE - do so now.
if (!(context.JK.hasOneConfiguredDevice())) {
app.afterFtue = function() { submitForm(evt); };

View File

@ -342,6 +342,12 @@
}
function afterShow(data) {
if(!context.JK.JamServer.connected) {
app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.');
window.location = '/client#/home'
return;
}
clearResults();
buildQuery();
refreshDisplay();

View File

@ -20,16 +20,9 @@
var app;
var logger = context.JK.logger;
var rest = context.JK.Rest();
var heartbeatInterval = null;
var heartbeatMS = null;
var heartbeatMissedMS = 10000; // if 5 seconds go by and we haven't seen a heartbeat ack, get upset
var inBadState = false;
var lastHeartbeatAckTime = null;
var lastHeartbeatFound = false;
var heartbeatAckCheckInterval = null;
var userDeferred = null;
var notificationLastSeenAt = undefined;
var notificationLastSeen = undefined;
var opts = {
inClient: true, // specify false if you want the app object but none of the client-oriented features
@ -72,58 +65,6 @@
$(routes.handler);
}
function _heartbeatAckCheck() {
// if we've seen an ack to the latest heartbeat, don't bother with checking again
// this makes us resilient to front-end hangs
if (lastHeartbeatFound) {
return;
}
// check if the server is still sending heartbeat acks back down
// this logic equates to 'if we have not received a heartbeat within heartbeatMissedMS, then get upset
if (new Date().getTime() - lastHeartbeatAckTime.getTime() > heartbeatMissedMS) {
logger.error("no heartbeat ack received from server after ", heartbeatMissedMS, " seconds . giving up on socket connection");
context.JK.JamServer.close(true);
}
else {
lastHeartbeatFound = true;
}
}
function _heartbeat() {
if (app.heartbeatActive) {
var message = context.JK.MessageFactory.heartbeat(notificationLastSeen, notificationLastSeenAt);
notificationLastSeenAt = undefined;
notificationLastSeen = undefined;
context.JK.JamServer.send(message);
lastHeartbeatFound = false;
}
}
function loggedIn(header, payload) {
app.clientId = payload.client_id;
// tell the backend that we have logged in
context.jamClient.OnLoggedIn(payload.user_id, payload.token);
$.cookie('client_id', payload.client_id);
app.initAfterConnect();
heartbeatMS = payload.heartbeat_interval * 1000;
logger.debug("jamkazam.js.loggedIn(): clientId now " + app.clientId + "; Setting up heartbeat every " + heartbeatMS + " MS");
heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS);
heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000);
lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat
}
function heartbeatAck(header, payload) {
lastHeartbeatAckTime = new Date();
context.JK.CurrentSessionModel.trackChanges(header, payload);
}
/**
* This occurs when the websocket gateway loses a connection to the backend messaging system,
* resulting in severe loss of functionality
@ -153,37 +94,6 @@
context.jamClient.OnDownloadAvailable();
}
/**
* Called whenever the websocket closes; this gives us a chance to cleanup things that should be stopped/cleared
* @param in_error did the socket close abnormally?
*/
function socketClosed(in_error) {
// tell the backend that we have logged out
context.jamClient.OnLoggedOut();
// stop future heartbeats
if (heartbeatInterval != null) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
// stop checking for heartbeat acks
if (heartbeatAckCheckInterval != null) {
clearTimeout(heartbeatAckCheckInterval);
heartbeatAckCheckInterval = null;
}
}
function registerLoginAck() {
logger.debug("register for loggedIn to set clientId");
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.LOGIN_ACK, loggedIn);
}
function registerHeartbeatAck() {
logger.debug("register for heartbeatAck");
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, heartbeatAck);
}
function registerBadStateError() {
logger.debug("register for server_bad_state_error");
@ -200,12 +110,6 @@
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.DOWNLOAD_AVAILABLE, downloadAvailable);
}
function registerSocketClosed() {
logger.debug("register for socket closed");
context.JK.JamServer.registerOnSocketClosed(socketClosed);
}
/**
* Generic error handler for Ajax calls.
*/
@ -259,7 +163,7 @@
* being shown or hidden.
* @screen is a string corresponding to the screen's layout-id attribute
* @handler is an object with up to four optional keys:
* beforeHide, afterHide, beforeShow, afterShow, which should all have
* beforeHide, afterHide, beforeShow, afterShow, beforeDisconnect, which should all have
* functions as values. If there is data provided by the screen's route
* it will be provided to these functions.
*/
@ -387,26 +291,12 @@
return userDeferred;
}
this.activeElementEvent = function(evtName, data) {
return this.layout.activeElementEvent(evtName, data);
}
this.updateNotificationSeen = function(notificationId, notificationCreatedAt) {
var time = new Date(notificationCreatedAt);
if(!notificationCreatedAt) {
throw 'invalid value passed to updateNotificationSeen'
}
if(!notificationLastSeenAt) {
notificationLastSeenAt = notificationCreatedAt;
notificationLastSeen = notificationId;
logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
}
else if(time.getTime() > new Date(notificationLastSeenAt).getTime()) {
notificationLastSeenAt = notificationCreatedAt;
notificationLastSeen = notificationId;
logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
}
else {
logger.debug("ignored notificationLastSeenAt for: " + notificationCreatedAt);
}
context.JK.JamServer.updateNotificationSeen(notificationId, notificationCreatedAt);
}
this.unloadFunction = function () {
@ -437,11 +327,8 @@
userDeferred = rest.getUserDetail();
if (opts.inClient) {
registerLoginAck();
registerHeartbeatAck();
registerBadStateRecovered();
registerBadStateError();
registerSocketClosed();
registerDownloadAvailable();
context.JK.FaderHelpers.initialize();
context.window.onunload = this.unloadFunction;

View File

@ -27,6 +27,8 @@
// privates
var logger = context.JK.logger;
var NOT_HANDLED = "not handled";
var me = null; // Reference to this instance for context sanity.
var opts = {
@ -36,6 +38,7 @@
notifyGutter: 10,
collapsedSidebar: 30,
panelHeaderHeight: 36,
alwaysOpenPanelHeaderHeight:78, // for the search bar
gutter: 60, // Margin around the whole UI
screenMargin: 0, // Margin around screens (not headers/sidebar)
gridOuterMargin: 6, // Outer margin on Grids (added to screenMargin if screen)
@ -74,7 +77,7 @@
}
function setInitialExpandedSidebarPanel() {
expandedPanel = $('[layout="panel"]').first().attr("layout-id");
expandedPanel = 'panelFriends';
}
function layout() {
@ -253,10 +256,9 @@
}
var $expandedPanel = $('[layout-id="' + expandedPanel + '"]');
var $expandedPanelContents = $expandedPanel.find('[layout-panel="contents"]');
var combinedHeaderHeight = $('[layout-panel="contents"]').length * opts.panelHeaderHeight;
var searchHeight = $('.sidebar .search').first().height();
var combinedHeaderHeight = ($('[layout-panel="contents"]').length - 1) * opts.panelHeaderHeight + opts.alwaysOpenPanelHeaderHeight;
var expanderHeight = $('[layout-sidebar-expander]').height();
var expandedPanelHeight = sidebarHeight - (combinedHeaderHeight + expanderHeight + searchHeight);
var expandedPanelHeight = sidebarHeight - (combinedHeaderHeight + expanderHeight);
$('[layout-panel="contents"]').hide();
$('[layout-panel="contents"]').css({"height": "1px"});
$expandedPanelContents.show();
@ -315,11 +317,16 @@
// This allows dialogs to use the notification.
$('[layout="notify"]').css({"z-index": "1001", "padding": "20px"});
$('[layout="panel"]').css({position: 'relative'});
$('[layout-panel="expanded"] [layout-panel="header"]').css({
margin: "0px",
padding: "0px",
height: opts.panelHeaderHeight + "px"
});
var $headers = $('[layout-panel="expanded"] [layout-panel="header"]');
context._.each($headers, function($header) {
$header = $($header);
var isAlwaysOpenHeader = $header.is('.always-open');
$header.css({
margin: "0px",
padding: "0px",
height: (isAlwaysOpenHeader ? opts.alwaysOpenPanelHeaderHeight : opts.panelHeaderHeight) + "px"
});
})
$('[layout-grid]').css({
position: "relative"
});
@ -436,18 +443,29 @@
return screenBindings[screen][evtName].call(me, data);
}
}
return NOT_HANDLED;
}
function dialogEvent(dialog, evtName, data) {
if (dialog && dialog in dialogBindings) {
if (evtName in dialogBindings[dialog]) {
var result = dialogBindings[dialog][evtName].call(me, data);
if (result === false) {
return false;
}
return dialogBindings[dialog][evtName].call(me, data);
}
}
return true;
return NOT_HANDLED;
}
function activeElementEvent(evtName, data) {
var result = {};
var currDialog = currentDialog();
if(currDialog) {
result.dialog = dialogEvent(currDialog.attr('layout-id'), evtName, data);
}
if(currentScreen) {
result.screen = screenEvent(currentScreen, evtName, data);
}
return result;
}
function onHashChange(e, postFunction) {
@ -614,7 +632,7 @@
}
function showDialog(dialog, options) {
if (!dialogEvent(dialog, 'beforeShow', options)) {
if (dialogEvent(dialog, 'beforeShow', options) === false) {
return;
}
var $overlay = $('.dialog-overlay')
@ -896,14 +914,30 @@
return isNoisyNotification(payload);
}
this.shouldFreezeAppOnDisconnect = function() {
return shouldFreezeAppOnDisconnect();
}
this.isDialogShowing = function() {
return isDialogShowing();
}
this.activeElementEvent = function(evtName, data) {
return activeElementEvent(evtName, data);
}
this.close = function (evt) {
close(evt);
};
this.beforeDisconnect = function() {
fireEvents();
}
this.afterReconnect = function() {
fireEvents();
}
this.closeDialog = closeDialog;
this.handleDialogState = handleDialogState;

View File

@ -30,6 +30,18 @@
setCount(count + 1);
}
function decrementNotificationCount() {
var count = parseInt($count.text());
if(count > 0) {
count = count - 1;
setCount(count);
if(count == 0) {
lowlightCount();
missedNotificationsWhileAway = false;
}
}
}
// set the element to white, and pulse it down to the un-highlighted value 2x, then set
function pulseToDark() {
logger.debug("pulsing notification badge")
@ -348,6 +360,7 @@
function deleteNotification(notificationId) {
console.trace();
var url = "/api/users/" + context.JK.currentUserId + "/notifications/" + notificationId;
$.ajax({
type: "DELETE",
@ -356,8 +369,11 @@
url: url,
processData: false,
success: function(response) {
$('li[notification-id=' + notificationId + ']').hide();
//decrementNotificationCount();
var notification = $('li[notification-id=' + notificationId + ']');
if(notification.length > 0) {
decrementNotificationCount();
}
notification.remove();
},
error: app.ajaxError
});

View File

@ -77,6 +77,7 @@
}
context.JK.SearchResultScreen.onSearchSuccess = function(response) {
$('#sidebar-search-results').show();
searchResults(response, true);
searchResults(response, false);
context.JK.bindHoverEvents();
@ -183,11 +184,6 @@
if (isSidebar) {
// show header
$('#sidebar-search-header').show();
// hide panels
$('[layout-panel="contents"]').hide();
$('[layout-panel="contents"]').css({"height": "1px"});
// resize search results area
$('#sidebar-search-results').height(context.JK.Sidebar.getHeight() + 'px');
}
else {
$('#result-count').html(resultCount);

View File

@ -139,8 +139,11 @@
shareDialog.initialize(context.JK.FacebookHelperInstance);
}
function beforeDisconnect() {
return { freezeInteraction: true };
}
function alertCallback(type, text) {
function alertCallback(type, text) {
function timeCallback() {
var start = new Date();
@ -275,6 +278,13 @@
function afterShow(data) {
if(!context.JK.JamServer.connected) {
promptLeave = false;
app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.');
window.location = '/client#/home'
return;
}
if (!context.JK.hasOneConfiguredDevice() || context.JK.TrackHelpers.getUserTracks(context.jamClient).length == 0) {
app.afterFtue = function() { initializeSession(); };
app.cancelFtue = function() { promptLeave = false; window.location = '/client#/home' };
@ -1516,7 +1526,8 @@
'beforeShow': beforeShow,
'afterShow': afterShow,
'beforeHide': beforeHide,
'beforeLeave' : beforeLeave
'beforeLeave' : beforeLeave,
'beforeDisconnect' : beforeDisconnect,
};
app.bindScreen('session', screenBindings);
};

View File

@ -151,6 +151,11 @@
var $parentRow = $('tr[id=' + session.id + ']', tbGroup);
$('.join-link', $parentRow).click(function(evt) {
if(!context.JK.JamServer.connected) {
app.notifyAlert("Not Connected", 'To create or join a session, you must be connected to the server.');
return false;
}
// If no FTUE, show that first.
if (!context.JK.hasOneConfiguredDevice() || context.JK.TrackHelpers.getUserTracks(context.jamClient).length == 0) {
app.afterFtue = function() { joinClick(session.id); };

View File

@ -22,7 +22,7 @@
// we track all the clientIDs of all the participants ever seen by this session, so that we can reliably convert a clientId from the backend into a username/avatar
var participantsEverSeen = {};
function id() {
function id() {
return currentSession ? currentSession.id : null;
}
@ -379,67 +379,11 @@
});
}
function reconnect() {
context.JK.CurrentSessionModel.leaveCurrentSession()
.always(function() {
window.location.reload();
});
}
function registerReconnect(content) {
$('a.disconnected-reconnect', content).click(function() {
var template = $('#template-reconnecting').html();
var templateHtml = context.JK.fillTemplate(template, null);
var content = context.JK.Banner.show({
html : template
});
rest.serverHealthCheck()
.done(function() {
reconnect();
})
.fail(function(xhr, textStatus, errorThrown) {
if(xhr && xhr.status >= 100) {
// we could connect to the server, and it's alive
reconnect();
}
else {
var template = $('#template-could-not-reconnect').html();
var templateHtml = context.JK.fillTemplate(template, null);
var content = context.JK.Banner.show({
html : template
});
registerReconnect(content);
}
});
return false;
});
}
function onWebsocketDisconnected(in_error) {
if(app.clientUpdating) {
// we don't want to do a 'cover the whole screen' dialog
// because the client update is already showing.
return;
}
// kill the streaming of the session immediately
logger.debug("calling jamClient.LeaveSession for clientId=" + clientId);
client.LeaveSession({ sessionID: currentSessionId });
if(in_error) {
var template = $('#template-disconnected').html();
var templateHtml = context.JK.fillTemplate(template, null);
var content = context.JK.Banner.show({
html : template
}) ;
registerReconnect(content);
}
}
// returns a deferred object
@ -456,7 +400,6 @@
if(foundParticipant) {
return $.Deferred().resolve(foundParticipant.user).promise();
}
}
// TODO: find it via some REST API if not found?

View File

@ -104,40 +104,14 @@
}
}
context.JK.Sidebar.getHeight = function() {
// TODO: refactor this - copied from layout.js
var sidebarHeight = $(context).height() - 75 - 2 * 60 + $('[layout-sidebar-expander]').height();
var combinedHeaderHeight = $('[layout-panel="contents"]').length * 36;
var searchHeight = $('.sidebar .search').first().height();
var expanderHeight = $('[layout-sidebar-expander]').height();
var expandedPanelHeight = sidebarHeight - (combinedHeaderHeight + expanderHeight + searchHeight);
return expandedPanelHeight;
}
function showFriendsPanel() {
var $expandedPanelContents = $('[layout-id="panelFriends"] [layout-panel="contents"]');
var expandedPanelHeight = context.JK.Sidebar.getHeight();
// hide all other contents
$('[layout-panel="contents"]').hide();
$('[layout-panel="contents"]').css({"height": "1px"});
// show the appropriate contens
$expandedPanelContents.show();
$expandedPanelContents.animate({"height": expandedPanelHeight + "px"}, 400);
}
function hideSearchResults() {
emptySearchResults();
$('#search-input').val('');
$('#sidebar-search-header').hide();
showFriendsPanel();
}
function emptySearchResults() {
$('#sidebar-search-results').empty();
$('#sidebar-search-results').height('0px');
}
var delay = (function(){

View File

@ -10,6 +10,8 @@
var $previousMessagesScroller = null;
var $sendTextMessage = null;
var $form = null;
var $interactionBlocker = null;
var $disconnectedMsg = null;
var $textBox = null;
var userLookup = null;
var otherId = null;
@ -48,10 +50,14 @@
}
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;
return false;
}
if(!sendingMessage) {
@ -136,35 +142,30 @@
$textBox.focus();
}
function beforeShow(args) {
app.layout.closeDialog('text-message') // ensure no others are showing. this is a singleton dialog
function renderDialog() {
app.user()
.done(function(userDetail) {
.done(function (userDetail) {
user = userDetail;
var other = args.d1;
if(!other) throw "other must be specified in TextMessageDialog"
otherId = other;
showing = true;
userLookup[user.id] = user;
rest.getUserDetail({id: otherId})
.done(function(otherUser) {
.done(function (otherUser) {
userLookup[otherUser.id] = otherUser;
$dialog.find('.receiver-name').text(otherUser.name);
$dialog.find('textarea').attr('placeholder', 'enter a message to ' + otherUser.name + '...');
$dialog.find('.offline-tip').text('An email will be sent if ' + otherUser.name + ' is offline');
$dialog.find('.offline-tip').text('An email will be sent if ' + otherUser.name + ' is offline');
if (!context.JK.JamServer.connected) {
renderNotConnected();
}
$sendTextMessage.click(sendMessage);
rest.getNotifications(buildParams())
.done(function(response) {
context._.each(response, function(textMessage) {
.done(function (response) {
context._.each(response, function (textMessage) {
renderMessage(textMessage.message, textMessage.source_user_id, userLookup[textMessage.source_user_id].name, textMessage.created_at);
})
@ -172,21 +173,53 @@
fullyInitialized = true;
drainQueue();
})
.fail(function(jqXHR) {
.fail(function (jqXHR) {
app.notifyServerError(jqXHR, 'Unable to Load Conversation')
})
})
.fail(function(jqXHR) {
.fail(function (jqXHR) {
app.notifyServerError(jqXHR, 'Unable to Load Other User')
})
})
}
function beforeShow(args) {
app.layout.closeDialog('text-message') // ensure no others are showing. this is a singleton dialog
var other = args.d1;
if (!other) throw "other must be specified in TextMessageDialog"
otherId = other;
renderDialog();
}
function afterHide() {
showing = false;
reset();
}
function renderNotConnected() {
$interactionBlocker.addClass('active');
$disconnectedMsg.addClass('active');
}
function renderConnected() {
$interactionBlocker.removeClass('active');
$disconnectedMsg.removeClass('active');
}
function beforeDisconnect() {
renderNotConnected();
}
function afterConnect() {
renderConnected();
reset();
renderDialog();
}
function pasteIntoInput(el, text) {
el.focus();
if (typeof el.selectionStart == "number"
@ -255,24 +288,13 @@
}
}
/**
function showDialog(_other) {
app.layout.closeDialog('text-message') // this dialog is implemented as a singleton, so must enforce this
reset();
if(!_other) throw "other must be specified in TextMessageDialog"
otherId = _other;
app.layout.showDialog('text-message')
}*/
function initialize() {
var dialogBindings = {
'beforeShow' : beforeShow,
'afterShow' : afterShow,
'afterHide': afterHide
'afterHide': afterHide,
'beforeDisconnect': beforeDisconnect,
'afterConnect': afterConnect
};
@ -284,6 +306,8 @@
$sendTextMessage = $dialog.find('.btn-send-text-message');
$form = $dialog.find('form');
$textBox = $form.find('textarea');
$interactionBlocker = $dialog.find('.interaction-blocker');
$disconnectedMsg = $dialog.find('.disconnected-msg');
events();
}

View File

@ -1,8 +1,19 @@
#banner {
display:none;
&[data-type="reconnect"] {
height:240px;
}
h2 {
font-weight:bold;
font-size:x-large;
}
.countdown {
font-weight:bold;
min-width:9px;
display:inline-block;
}
}
#banner h2 {
font-weight:bold;
font-size:x-large;
}

View File

@ -33,6 +33,7 @@
*= require ./account
*= require ./search
*= require ./ftue
*= require ./jamServer
*= require ./gearWizard
*= require ./whatsNextDialog
*= require ./invitationDialog

View File

@ -8,7 +8,7 @@
&.musician-bubble {
width:410px;
width:425px;
}
h2 {

View File

@ -0,0 +1,65 @@
.no-websocket-connection {
display:none;
text-align:center;
width:100%;
position:absolute;
}
.server-connection {
margin:auto;
display:inline-block;
zoom:1;
text-align:center;
padding:10px 20px;
background-color:#404040;
border-color:#ccc;
border-style:solid;
border-width:0 2px 2px;
-webkit-box-shadow: 0px 0px 15px rgba(50, 50, 50, 1);
-moz-box-shadow: 0px 0px 15px rgba(50, 50, 50, 1);
box-shadow: 0px 0px 15px rgba(50, 50, 50, 1);
h2 {
font-size:20px;
vertical-align:baseline;
margin-bottom:10px;
}
img.alert-icon {
top:14px;
height:16px;
width:16px;
&.left-side {
padding-right:20px;
}
&.right-side {
padding-left:20px;
}
}
.reconnect-progress-msg {
margin-bottom:10px;
}
.reconnect-countdown {
}
#reconnect-now {
margin-top:10px;
}
.countdown {
font-weight:bold;
min-width:9px;
display:inline-block;
}
.countdown-seconds {
}
}

View File

@ -58,4 +58,39 @@
text-align:center;
width:50px;
}
.interaction-blocker {
display:none;
position:absolute;
background-color:#333;
text-align:center;
&.active {
display:block;
top:0;
bottom:40px;
left:0;
right:0;
opacity:0.5;
}
}
.disconnected-msg {
color:white;
font-size:25px;
left:0;
margin:auto;
position:absolute;
text-align:center;
top:30%;
width:100%;
display:none;
&.active {
display:inline-block;
}
}
}

View File

@ -9,3 +9,8 @@ body {
height:100%;
margin:0 !important;
}
.wrapper {
width:1280px;
margin:0 auto;
}

View File

@ -1,8 +0,0 @@
class VideosController < ApplicationController
def show_dialog
@video_id = @params[:video_id]
end
end

View File

@ -1,7 +1,7 @@
<!-- generic banner for use by an code -->
<div class="overlay" id="banner_overlay"></div>
<div id="banner" class="dialog-overlay-sm">
<div id="banner" class="dialog-overlay-sm" data-type="">
<!-- dialog header -->
<div class="content-head">

View File

@ -0,0 +1,12 @@
.no-websocket-connection
.server-connection
%h2
= image_tag( "content/icon_alert.png" , :class => "alert-icon left-side" )
%span Disconnected from Server
= image_tag( "content/icon_alert.png" , :class => "alert-icon right-side" )
.message-contents
%script{type: 'text/template', id: 'template-server-connection'}
%div.reconnect-progress-msg
%span.reconnect-before= 'Reconnecting automatically in '
%span.reconnect-countdown= '{{data.countdown}}'
%a.disconnected-reconnect.reconnect-enabled{href:'#'} RECONNECT NOW

View File

@ -11,28 +11,36 @@
</div>
<!-- Search Box -->
<div class="search">
<div layout-panel="header" class="panel-header">
<h2>search</h2>
<div class="search" layout="panel" layout-id="panelSearch">
<div layout-panel="collapsed">
</div>
<div class="searchbox">
<form id="searchForm">
<input id="search-input" autocomplete="off" type="text" name="search" placeholder="enter search terms" />
</form>
<div id="sidebar-search-header" style="margin: 4px 4px 8px 8px">
<div class="left">
<strong><a id="sidebar-search-expand" style="color:#fff; text-decoration:underline">&laquo;&nbsp;EXPAND</a></strong>
</div>
<!-- search filter dropdown -->
<div class="right">
Show:&nbsp;<%= select_tag(Search::SEARCH_TEXT_TYPE_ID, options_for_select(Search::SEARCH_TEXT_TYPES.collect { |ii| [ii.to_s.titleize, ii] })) %>
<div layout-panel="expanded" class="panel expanded">
<div layout-panel="header" class="panel-header always-open">
<h2>search</h2>
<div class="searchbox">
<form id="searchForm">
<input id="search-input" autocomplete="off" type="text" name="search" placeholder="enter search terms" />
</form>
</div>
</div>
</div>
<div style="clear:both;"></div><br />
<!-- border between header and beginning of search results -->
<div class="sidebar-search-result"></div>
<div id="sidebar-search-results" class="results-wrapper">
<!-- border between header and beginning of search results -->
<!--<div class="sidebar-search-result"></div>-->
<div class="panelcontents" layout-panel="contents">
<div id="sidebar-search-header" style="margin: 4px 4px 8px 8px">
<div class="left">
<strong><a id="sidebar-search-expand" style="color:#fff; text-decoration:underline">&laquo;&nbsp;EXPAND</a></strong>
</div>
<!-- search filter dropdown -->
<div class="right">
Show:&nbsp;<%= select_tag(Search::SEARCH_TEXT_TYPE_ID, options_for_select(Search::SEARCH_TEXT_TYPES.collect { |ii| [ii.to_s.titleize, ii] })) %>
</div>
</div>
<div style="clear:both;"></div><br />
<div id="sidebar-search-results" class="results-wrapper"></div>
</div>
</div>
</div>

View File

@ -16,6 +16,9 @@
.right
%a.button-grey.btn-close-dialog{href:'#', 'layout-action' => 'close'} CLOSE
%a.button-orange.btn-send-text-message{href:'#'} SEND
.interaction-blocker
%span.disconnected-msg DISCONNECTED FROM SERVER
%script{type: 'text/template', id: 'template-previous-message'}
.previous-message

View File

@ -4,39 +4,12 @@
<br />
<br />
<div align="center">
You have been disconnected from JamKazam.
You have been disconnected from JamKazam. <br/><br/>
<span class="reconnect-progress-msg">Reconnecting automatically in <span class="reconnect-countdown">{countdown}</span></span>
</div>
<br clear="all" /><br />
<div class="right">
<a href="#" class="button-orange disconnected-reconnect">RECONNECT</a>
<a href="#" class="button-orange disconnected-reconnect">RECONNECT NOW</a>
</div>
</script>
<script type="text/template" id="template-reconnecting">
<h2 align="center">Reconnecting to Server</h2>
<br />
<br />
<br />
<div align="center">
Attempting to reestablish connection to the server...
</div>
<br clear="all" /><br />
</script>
<script type="text/template" id="template-could-not-reconnect">
<h2 align="center">Unable to Reconnect</h2>
<br />
<br />
<br />
<div align="center">
We were not able to reconnect to JamKazam. <br/></br/>
Please check your internet connection, then try again later.
</div>
<br clear="all" /><br />
<div class="right">
<a href="#" class="button-orange disconnected-reconnect">TRY AGAIN TO RECONNECT</a>
</div>
</script>

View File

@ -14,6 +14,7 @@
<%= render "faders" %>
<%= render "vu_meters" %>
<%= render "ftue" %>
<%= render "jamServer" %>
<%= render "clients/gear/gear_wizard" %>
<%= render "terms" %>
<%= render "leaveSessionWarning" %>
@ -118,8 +119,9 @@
window.location.href = '/?redirect-to=' + encodeURIComponent(JK.locationPath());
<% end %>
// Some things can't be initialized until we're connected. Put them here.
function _initAfterConnect() {
function _initAfterConnect(connected) {
if (this.didInitAfterConnect) return;
this.didInitAfterConnect = true
@ -242,12 +244,17 @@
var testBridgeScreen = new JK.TestBridgeScreen(JK.app);
testBridgeScreen.initialize();
if(!connected) {
jamServer.initiateReconnect(null, true);
}
JK.app.initialRouting();
JK.hideCurtain(300);
}
JK.app = JK.JamKazam();
JK.app.initAfterConnect = _initAfterConnect;
var jamServer = new JK.JamServer(JK.app);
jamServer.initialize();
// If no jamClient (when not running in native client)
// create a fake one.
@ -302,25 +309,16 @@
JK.TickDuration('.feed-entry.music-session-history-entry .inprogress .tick-duration');
JK.JamServer.connect(); // singleton here defined in JamServer.js
JK.JamServer.connect() // singleton here defined in JamServer.js
.done(function() {
_initAfterConnect(true);
})
.fail(function() {
_initAfterConnect(false);
});
// this ensures that there is always a CurrentSessionModel, even if it's for a non-active session
JK.CurrentSessionModel = new JK.SessionModel(JK.app, JK.JamServer, window.jamClient);
// Run a check to see if we're logged in yet. Only after that should
// we initialize the other screens.
function testConnected() {
this.numCalls = (this.numCalls || 0) + 1;
if (JK.clientId) {
JK.app.initAfterConnect();
} else {
if (50 <= this.numCalls) { // 5 second max
JK.notifyAlert('Server Error', 'Could not connect to server. Please try again later.')
} else {
window.setTimeout(testConnected, 100);
}
}
}
testConnected();
}
JK.bindHoverEvents();

View File

@ -32,7 +32,9 @@
<body class="jam">
<div id="minimal-container">
<%= javascript_include_tag "minimal/minimal" %>
<%= yield %>
<div class="wrapper">
<%= yield %>
</div>
</div>
<script type="text/javascript">
@ -56,5 +58,4 @@
<%= render "shared/ga" %>
<!-- version info: <%= version %> -->
</body>
</html>
c
</html>

View File

@ -2,8 +2,9 @@ unless $rails_rake_task
JamWebEventMachine.start
if APP_CONFIG.websocket_gateway_enable && !$rails_rake_task
if APP_CONFIG.websocket_gateway_enable && !$rails_rake_task && ENV['NO_WEBSOCKET_GATEWAY'] != '1'
current = Thread.current
Thread.new do
JamWebsockets::Server.new.run(
:port => APP_CONFIG.websocket_gateway_port,
@ -11,8 +12,10 @@ unless $rails_rake_task
:connect_time_stale => APP_CONFIG.websocket_gateway_connect_time_stale,
:connect_time_expire => APP_CONFIG.websocket_gateway_connect_time_expire,
:rabbitmq_host => APP_CONFIG.rabbitmq_host,
:rabbitmq_port => APP_CONFIG.rabbitmq_port)
:rabbitmq_port => APP_CONFIG.rabbitmq_port,
:calling_thread => current)
end
Thread.stop
end
end

View File

@ -63,8 +63,6 @@ SampleApp::Application.routes.draw do
match '/events/:slug', to: 'events#show', :via => :get, :as => 'event'
match '/video/dialog/:id', to: 'videos#show', :via => :get
match '/endorse/:id/:service', to: 'users#endorse', :as => 'endorse'
# temporarily allow for debugging--only allows admini n

View File

@ -4,12 +4,6 @@ describe "Account", :js => true, :type => :feature, :capybara_feature => true do
subject { page }
before(:all) do
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 10
end
let(:user) { FactoryGirl.create(:user) }
before(:each) do
@ -92,7 +86,7 @@ describe "Account", :js => true, :type => :feature, :capybara_feature => true do
before(:each) do
fill_in "first_name", with: "Bobby"
fill_in "last_name", with: "Toes"
find('input[name=subscribe_email]').set(false)
uncheck('subscribe_email')
find("#account-edit-profile-submit").trigger(:click)
end

View File

@ -4,11 +4,6 @@ describe "Notification Highlighter", :js => true, :type => :feature, :capybara_f
subject { page }
before(:all) do
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 10
end
let(:user) { FactoryGirl.create(:user) }
let(:user2) { FactoryGirl.create(:user) }
@ -138,30 +133,34 @@ describe "Notification Highlighter", :js => true, :type => :feature, :capybara_f
end
end
describe "delete notification" do
describe "user no unseen notifications" do
describe "notification occurs in realtime" do
before(:each) do
User.delete_all
end
describe "sidebar is open" do
describe "user can see notifications" do
it "stays deactivated" do
it "while notification panel closed" do
# we should see the count go to 1, but once the notification is accepted, which causes it to delete,
# we should see the count go back down to 0.
end
end
in_client(user) do
sign_in_poltergeist(user)
end
describe "user can not see notifications" do
describe "with dialog open" do
it "temporarily activates" do
in_client(user2) do
sign_in_poltergeist(user2)
find_musician(user)
find(".result-list-button-wrapper[data-musician-id='#{user.id}'] .search-m-friend").trigger(:click)
end
end
end
in_client(user) do
badge = find("#{NOTIFICATION_PANEL} .badge", text: '1')
badge['class'].include?('highlighted').should == true
describe "with document blurred" do
it "temporarily activates" do
find('#notification #ok-button', text: 'ACCEPT').trigger(:click)
end
end
end
badge = find("#{NOTIFICATION_PANEL} .badge", text: '0')
badge['class'].include?('highlighted').should == false
end
end
end

View File

@ -0,0 +1,113 @@
require 'spec_helper'
# tests what happens when the websocket connection goes away
describe "Reconnect", :js => true, :type => :feature, :capybara_feature => true do
subject { page }
let(:user1) { FactoryGirl.create(:user) }
let(:user2) { FactoryGirl.create(:user) }
before(:each) do
emulate_client
end
it "websocket connection is down on initial connection" do
FactoryGirl.create(:friendship, :user => user1, :friend => user2)
FactoryGirl.create(:friendship, :user => user2, :friend => user1)
Rails.application.config.stub(:websocket_gateway_uri).and_return('ws://localhost:99/websocket') # bogus port
sign_in_poltergeist(user1, validate: false)
page.should have_selector('.no-websocket-connection')
find('.homecard.createsession').trigger(:click)
find('h1', text:'create session')
find('#btn-create-session').trigger(:click)
find('#notification h2', text: 'Not Connected') # get notified you can't go to create session
page.evaluate_script('window.history.back()')
find('.homecard.findsession').trigger(:click)
find('#notification h2', text: 'Not Connected') # get notified you can't go to find session
find('h2', text: 'create session') # and be back on home screen
find('.homecard.feed').trigger(:click)
find('h1', text:'feed')
page.evaluate_script('window.history.back()')
find('.homecard.musicians').trigger(:click)
find('h1', text:'musicians')
page.evaluate_script('window.history.back()')
find('.homecard.profile').trigger(:click)
find('h1', text:'profile')
page.evaluate_script('window.history.back()')
find('.homecard.account').trigger(:click)
find('h1', text:'account')
page.evaluate_script('window.history.back()')
initiate_text_dialog user2
find('span.disconnected-msg', text: 'DISCONNECTED FROM SERVER')
end
it "websocket goes down on home page" do
sign_in_poltergeist(user1)
5.times do
close_websocket
# we should see indication that the websocket is down
page.should have_selector('.no-websocket-connection')
# but.. after a few seconds, it should reconnect on it's own
page.should_not have_selector('.no-websocket-connection')
end
# then verify we can create a session
create_join_session(user1, [user2])
formal_leave_by user1
# websocket goes down while chatting
in_client(user1) do
initiate_text_dialog user2
# normal, happy dialog
page.should_not have_selector('span.disconnected-msg', text: 'DISCONNECTED FROM SERVER')
close_websocket
# dialog-specific disconnect should show
page.should have_selector('span.disconnected-msg', text: 'DISCONNECTED FROM SERVER')
# and generic disconnect
page.should have_selector('.no-websocket-connection')
# after a few seconds, the page should reconnect on it's own
page.should_not have_selector('span.disconnected-msg', text: 'DISCONNECTED FROM SERVER')
page.should_not have_selector('.no-websocket-connection')
end
end
it "websocket goes down on session page" do
create_session(creator: user1)
5.times do
close_websocket
# we should see indication that the websocket is down
page.should have_selector('h2', text: 'Disconnected from Server')
# but.. after a few seconds, it should reconnect on it's own
page.should_not have_selector('h2', text: 'Disconnected from Server')
find('h1', text:'session')
end
end
end

View File

@ -4,13 +4,8 @@ describe "Profile Menu", :js => true, :type => :feature, :capybara_feature => tr
subject { page }
before(:all) do
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 10
end
let(:user) { FactoryGirl.create(:user) }
let(:user2) { FactoryGirl.create(:user) }
before(:each) do
UserMailer.deliveries.clear
@ -36,5 +31,19 @@ describe "Profile Menu", :js => true, :type => :feature, :capybara_feature => tr
end
end
describe "panel behavior" do
it "search, then click notifications" do
notification = Notification.send_text_message("text message", user2, user)
notification.errors.any?.should be_false
site_search(user2.name, validate: user2)
open_notifications
find("#sidebar-notification-list li[notification-id='#{notification.id}']")
end
end
end

View File

@ -41,6 +41,7 @@ Thread.new {
end
}
current = Thread.current
Thread.new do
ActiveRecord::Base.connection.disconnect!
ActiveRecord::Base.establish_connection(YAML::load(File.open('config/database.yml'))["test"])
@ -52,12 +53,13 @@ Thread.new do
:connect_time_stale => 2,
:connect_time_expire => 5,
:rabbitmq_host => 'localhost',
:rabbitmq_port => 5672)
:rabbitmq_port => 5672,
:calling_thread => current)
rescue Exception => e
puts "websocket-gateway failed: #{e}"
end
end
Thread.stop
ActiveRecord::Base.connection.disconnect!

View File

@ -9,7 +9,10 @@ def site_search(text, options = {})
fill_in "search-input", with: text
end
find('#sidebar-search-expand').trigger(:click) if options[:expand]
if options[:expand]
page.driver.execute_script("jQuery('#searchForm').submit()")
find('h1', text:'search results')
end
end
# goes to the musician tile, and tries to find a musician
@ -37,6 +40,16 @@ def find_musician(user)
raise "unable to find musician #{user}"
end
def initiate_text_dialog(user)
# verify that the chat window is grayed out
site_search(user.first_name, expand: true)
find("#search-results a[user-id=\"#{user.id}\"][hoveraction=\"musician\"]", text: user.name).hover_intent
find('#musician-hover #btnMessage').trigger(:click)
find('h1', text: 'conversation with ' + user.name)
end
# sends a text message in the chat interface.
def send_text_message(msg, options={})
find('#text-message-dialog') # assert that the dialog is showing already
@ -62,6 +75,7 @@ def open_notifications
find("#{NOTIFICATION_PANEL} .panel-header").trigger(:click)
end
def hover_intent(element)
element.hover
element.hover
@ -95,4 +109,8 @@ end
# simulates focus event on window
def window_focus
page.evaluate_script(%{window.jQuery(window).trigger('focus');})
end
def close_websocket
page.evaluate_script("window.JK.JamServer.close(true)")
end

View File

@ -86,13 +86,19 @@ def set_cookie(k, v)
end
end
def sign_in_poltergeist(user)
def sign_in_poltergeist(user, options = {})
validate = options[:validate]
validate = true if validate.nil?
visit signin_path
fill_in "Email Address:", with: user.email
fill_in "Password:", with: user.password
click_button "SIGN IN"
wait_until_curtain_gone
# presence of this means websocket gateway is not working
page.should have_no_selector('.no-websocket-connection') if validate
end
@ -411,8 +417,8 @@ def wait_for_easydropdown(select)
end
# defaults to enter key (13)
def send_key(keycode = 13)
keypress_script = "var e = $.Event('keydown', { keyCode: #{keycode} }); $('#search-input').trigger(e);"
def send_key(selector, keycode = 13)
keypress_script = "var e = $.Event('keyup', { keyCode: #{keycode} }); jQuery('#{selector}').trigger(e);"
page.driver.execute_script(keypress_script)
end

View File

@ -35,7 +35,7 @@ gem 'aasm', '3.0.16'
gem 'carrierwave'
gem 'devise'
gem 'postgres-copy'
gem 'aws-sdk', '1.29.1'
gem 'aws-sdk' #, '1.29.1'
gem 'bugsnag'
gem 'postgres_ext'
gem 'resque'

View File

@ -12,13 +12,13 @@ module JamWebsockets
end
def run(options={})
host = "0.0.0.0"
port = options[:port]
connect_time_stale = options[:connect_time_stale].to_i
connect_time_expire = options[:connect_time_expire].to_i
rabbitmq_host = options[:rabbitmq_host]
rabbitmq_port = options[:rabbitmq_port].to_i
calling_thread = options[:calling_thread]
@log.info "starting server #{host}:#{port} staleness_time=#{connect_time_stale}; reconnect time = #{connect_time_expire}, rabbitmq=#{rabbitmq_host}:#{rabbitmq_port}"
@ -36,6 +36,7 @@ module JamWebsockets
start_connection_expiration(expire_time)
start_connection_flagger(connect_time_stale)
start_websocket_listener(host, port, options[:emwebsocket_debug])
calling_thread.wakeup if calling_thread
end
# if you don't do this, the app won't exit unless you kill -9
@ -55,6 +56,7 @@ module JamWebsockets
@log.info "new client #{ws}"
@router.new_client(ws)
end
@log.debug("started websocket")
end
def start_connection_expiration(stale_max_time)