diff --git a/admin/Gemfile b/admin/Gemfile index b121b5991..c8b1d1772 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -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' diff --git a/ruby/Gemfile b/ruby/Gemfile index f81545af0..5ed51d3d0 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -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' diff --git a/ruby/lib/jam_ruby/lib/em_helper.rb b/ruby/lib/jam_ruby/lib/em_helper.rb index 2e8c46c55..c310cf2bf 100644 --- a/ruby/lib/jam_ruby/lib/em_helper.rb +++ b/ruby/lib/jam_ruby/lib/em_helper.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/email_batch.rb b/ruby/lib/jam_ruby/models/email_batch.rb index bdd3f2e7d..309b63305 100644 --- a/ruby/lib/jam_ruby/models/email_batch.rb +++ b/ruby/lib/jam_ruby/models/email_batch.rb @@ -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 diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 37180190d..4318619d2 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -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 diff --git a/ruby/spec/jam_ruby/models/recorded_track_spec.rb b/ruby/spec/jam_ruby/models/recorded_track_spec.rb index 7fb0add63..42c197bc9 100644 --- a/ruby/spec/jam_ruby/models/recorded_track_spec.rb +++ b/ruby/spec/jam_ruby/models/recorded_track_spec.rb @@ -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' diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index bb26870cd..afe08c312 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -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 diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 564ec7b34..0f78e56c4 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -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 diff --git a/web/Gemfile b/web/Gemfile index 04415dab0..2595343ce 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -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' diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 0544f4c7c..d1126adf1 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -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 $('' + secs + ' ' + (secs == 1 ? ' second.s' : 'seconds.') + ''); + } + + 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); \ No newline at end of file diff --git a/web/app/assets/javascripts/banner.js b/web/app/assets/javascripts/banner.js index d0aa78aa7..535d841c3 100644 --- a/web/app/assets/javascripts/banner.js +++ b/web/app/assets/javascripts/banner.js @@ -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 diff --git a/web/app/assets/javascripts/createSession.js.erb b/web/app/assets/javascripts/createSession.js.erb index b572c2ca7..e5744cbfe 100644 --- a/web/app/assets/javascripts/createSession.js.erb +++ b/web/app/assets/javascripts/createSession.js.erb @@ -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); }; diff --git a/web/app/assets/javascripts/findSession.js b/web/app/assets/javascripts/findSession.js index 11001f3e3..e9b2dd964 100644 --- a/web/app/assets/javascripts/findSession.js +++ b/web/app/assets/javascripts/findSession.js @@ -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(); diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index f00e9b02d..4891c3509 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -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; diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index 0d4ec3dbe..a32df7763 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -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; diff --git a/web/app/assets/javascripts/notificationPanel.js b/web/app/assets/javascripts/notificationPanel.js index 9e183324b..454c11b04 100644 --- a/web/app/assets/javascripts/notificationPanel.js +++ b/web/app/assets/javascripts/notificationPanel.js @@ -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 }); diff --git a/web/app/assets/javascripts/searchResults.js b/web/app/assets/javascripts/searchResults.js index 7cb7a0f9d..ee6f34981 100644 --- a/web/app/assets/javascripts/searchResults.js +++ b/web/app/assets/javascripts/searchResults.js @@ -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); diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index aef2240f7..e667e27c0 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -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); }; diff --git a/web/app/assets/javascripts/sessionList.js b/web/app/assets/javascripts/sessionList.js index 37539b3b0..368b56ffe 100644 --- a/web/app/assets/javascripts/sessionList.js +++ b/web/app/assets/javascripts/sessionList.js @@ -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); }; diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js index 31a057e14..4cecf82bb 100644 --- a/web/app/assets/javascripts/sessionModel.js +++ b/web/app/assets/javascripts/sessionModel.js @@ -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? diff --git a/web/app/assets/javascripts/sidebar.js b/web/app/assets/javascripts/sidebar.js index bac8a80cf..57253d053 100644 --- a/web/app/assets/javascripts/sidebar.js +++ b/web/app/assets/javascripts/sidebar.js @@ -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(){ diff --git a/web/app/assets/javascripts/textMessageDialog.js b/web/app/assets/javascripts/textMessageDialog.js index 3e3049880..8099da112 100644 --- a/web/app/assets/javascripts/textMessageDialog.js +++ b/web/app/assets/javascripts/textMessageDialog.js @@ -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(); } diff --git a/web/app/assets/stylesheets/client/banner.css.scss b/web/app/assets/stylesheets/client/banner.css.scss index e3b8de209..c036c5dbd 100644 --- a/web/app/assets/stylesheets/client/banner.css.scss +++ b/web/app/assets/stylesheets/client/banner.css.scss @@ -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; -} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index acdbd914a..8dfded0bf 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -33,6 +33,7 @@ *= require ./account *= require ./search *= require ./ftue + *= require ./jamServer *= require ./gearWizard *= require ./whatsNextDialog *= require ./invitationDialog diff --git a/web/app/assets/stylesheets/client/hoverBubble.css.scss b/web/app/assets/stylesheets/client/hoverBubble.css.scss index 5016310ac..42faa0190 100644 --- a/web/app/assets/stylesheets/client/hoverBubble.css.scss +++ b/web/app/assets/stylesheets/client/hoverBubble.css.scss @@ -8,7 +8,7 @@ &.musician-bubble { - width:410px; + width:425px; } h2 { diff --git a/web/app/assets/stylesheets/client/jamServer.css.scss b/web/app/assets/stylesheets/client/jamServer.css.scss new file mode 100644 index 000000000..95e1ecf34 --- /dev/null +++ b/web/app/assets/stylesheets/client/jamServer.css.scss @@ -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 { + + } +} + diff --git a/web/app/assets/stylesheets/client/textMessageDialog.css.scss b/web/app/assets/stylesheets/client/textMessageDialog.css.scss index 5605adfce..4a60bb78d 100644 --- a/web/app/assets/stylesheets/client/textMessageDialog.css.scss +++ b/web/app/assets/stylesheets/client/textMessageDialog.css.scss @@ -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; + } + } + + } \ No newline at end of file diff --git a/web/app/assets/stylesheets/minimal/minimal_main.css.scss b/web/app/assets/stylesheets/minimal/minimal_main.css.scss index 5a170e128..50feaf50f 100644 --- a/web/app/assets/stylesheets/minimal/minimal_main.css.scss +++ b/web/app/assets/stylesheets/minimal/minimal_main.css.scss @@ -9,3 +9,8 @@ body { height:100%; margin:0 !important; } + +.wrapper { + width:1280px; + margin:0 auto; +} \ No newline at end of file diff --git a/web/app/controllers/videos_controller.rb b/web/app/controllers/videos_controller.rb deleted file mode 100644 index a63343776..000000000 --- a/web/app/controllers/videos_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ - -class VideosController < ApplicationController - - def show_dialog - @video_id = @params[:video_id] - end - -end \ No newline at end of file diff --git a/web/app/views/clients/_banner.html.erb b/web/app/views/clients/_banner.html.erb index 9a932e3a9..4a7cd1d6e 100644 --- a/web/app/views/clients/_banner.html.erb +++ b/web/app/views/clients/_banner.html.erb @@ -1,7 +1,7 @@ -