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 6ad832f29..c1f1b078e 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 1d65f5330..c29b007e6 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' @@ -31,13 +34,14 @@ require 'resque_spec/scheduler' 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/hoverBand.js b/web/app/assets/javascripts/hoverBand.js index 2b98e8e62..d6498f91a 100644 --- a/web/app/assets/javascripts/hoverBand.js +++ b/web/app/assets/javascripts/hoverBand.js @@ -2,15 +2,26 @@ "use strict"; context.JK = context.JK || {}; - context.JK.BandHoverBubble = function(bandId, position) { + context.JK.BandHoverBubble = function(bandId, x, y) { var logger = context.JK.logger; var rest = context.JK.Rest(); - var instrumentLogoMap = context.JK.getInstrumentIconMap24(); var hoverSelector = "#band-hover"; this.showBubble = function() { - $(hoverSelector).css({left: position.left-100, top: position.top}); + var mouseLeft = x < (document.body.clientWidth / 2); + var mouseTop = y < (document.body.clientHeight / 2); + var css = {}; + if (mouseLeft) + css.left = x + 10 + 'px'; + else + css.left = x - (7 + $(hoverSelector).width()) + 'px'; + if (mouseTop) + css.top = y + 10 + 'px'; + else + css.top = y - (7 + $(hoverSelector).height()) + 'px'; + + $(hoverSelector).css(css); $(hoverSelector).fadeIn(500); rest.getBand(bandId) @@ -28,7 +39,7 @@ instrumentHtml = '
'; if (val.instruments) { // @FIXME: edge case for Test user that has no instruments? $.each(val.instruments, function(index, instrument) { - instrumentHtml += ' '; + instrumentHtml += ' '; }); } diff --git a/web/app/assets/javascripts/hoverFan.js b/web/app/assets/javascripts/hoverFan.js index 1aa510721..f5b9926d0 100644 --- a/web/app/assets/javascripts/hoverFan.js +++ b/web/app/assets/javascripts/hoverFan.js @@ -2,7 +2,7 @@ "use strict"; context.JK = context.JK || {}; - context.JK.FanHoverBubble = function(userId, position) { + context.JK.FanHoverBubble = function(userId, x, y) { var logger = context.JK.logger; var rest = context.JK.Rest(); @@ -10,7 +10,19 @@ var hoverSelector = "#fan-hover"; this.showBubble = function() { - $(hoverSelector).css({left: position.left-100, top: position.top}); + var mouseLeft = x < (document.body.clientWidth / 2); + var mouseTop = y < (document.body.clientHeight / 2); + var css = {}; + if (mouseLeft) + css.left = x + 10 + 'px'; + else + css.left = x - (7 + $(hoverSelector).width()) + 'px'; + if (mouseTop) + css.top = y + 10 + 'px'; + else + css.top = y - (7 + $(hoverSelector).height()) + 'px'; + + $(hoverSelector).css(css); $(hoverSelector).fadeIn(500); rest.getUserDetail({id: userId}) diff --git a/web/app/assets/javascripts/hoverMusician.js b/web/app/assets/javascripts/hoverMusician.js index 0cee5cb0f..6ce0368a6 100644 --- a/web/app/assets/javascripts/hoverMusician.js +++ b/web/app/assets/javascripts/hoverMusician.js @@ -2,15 +2,27 @@ "use strict"; context.JK = context.JK || {}; - context.JK.MusicianHoverBubble = function(userId, position) { + context.JK.MusicianHoverBubble = function(userId, x, y) { var logger = context.JK.logger; var rest = context.JK.Rest(); - var instrumentLogoMap = context.JK.getInstrumentIconMap24(); var hoverSelector = "#musician-hover"; this.showBubble = function() { - $(hoverSelector).css({left: position.left-100, top: position.top}); + + var mouseLeft = x < (document.body.clientWidth / 2); + var mouseTop = y < (document.body.clientHeight / 2); + var css = {}; + if (mouseLeft) + css.left = x + 10 + 'px'; + else + css.left = x - (7 + $(hoverSelector).width()) + 'px'; + if (mouseTop) + css.top = y + 10 + 'px'; + else + css.top = y - (7 + $(hoverSelector).height()) + 'px'; + + $(hoverSelector).css(css); $(hoverSelector).fadeIn(500); rest.getUserDetail({id: userId}) @@ -20,7 +32,7 @@ // instruments var instrumentHtml = ''; $.each(response.instruments, function(index, val) { - instrumentHtml += '
'; + instrumentHtml += '
'; }); // followings diff --git a/web/app/assets/javascripts/hoverRecording.js b/web/app/assets/javascripts/hoverRecording.js index 6d32b8ffa..3555b626e 100644 --- a/web/app/assets/javascripts/hoverRecording.js +++ b/web/app/assets/javascripts/hoverRecording.js @@ -2,10 +2,9 @@ "use strict"; context.JK = context.JK || {}; - context.JK.RecordingHoverBubble = function(recordingId, position) { + context.JK.RecordingHoverBubble = function(recordingId, x, y) { var logger = context.JK.logger; var rest = context.JK.Rest(); - var instrumentLogoMap = context.JK.getInstrumentIconMap24(); var hoverSelector = "#recording-hover"; function deDupTracks(recordedTracks) { @@ -39,7 +38,19 @@ } this.showBubble = function() { - $(hoverSelector).css({left: position.left-100, top: position.top+20}); + var mouseLeft = x < (document.body.clientWidth / 2); + var mouseTop = y < (document.body.clientHeight / 2); + var css = {}; + if (mouseLeft) + css.left = x + 10 + 'px'; + else + css.left = x - (7 + $(hoverSelector).width()) + 'px'; + if (mouseTop) + css.top = y + 10 + 'px'; + else + css.top = y - (7 + $(hoverSelector).height()) + 'px'; + + $(hoverSelector).css(css); $(hoverSelector).fadeIn(500); rest.getClaimedRecording(recordingId) @@ -61,8 +72,8 @@ instrumentHtml = '
'; $.each(val.instrument_ids, function(index, val) { - instrumentHtml += '  '; - }) + instrumentHtml += '  '; + }); instrumentHtml += '
'; musicianHtml += instrumentHtml; @@ -77,7 +88,7 @@ claimedRecordingId: claimedRecording.id, name: claimedRecording.name, genre: claimedRecording.genre_id.toUpperCase(), - created_at: context.JK.formatDateTime(recording.created_at), + created_at: $.timeago(recording.created_at), description: response.description ? response.description : "", play_count: recording.play_count, comment_count: recording.comment_count, diff --git a/web/app/assets/javascripts/hoverSession.js b/web/app/assets/javascripts/hoverSession.js index 373bd0cc6..f6a9843c5 100644 --- a/web/app/assets/javascripts/hoverSession.js +++ b/web/app/assets/javascripts/hoverSession.js @@ -2,15 +2,26 @@ "use strict"; context.JK = context.JK || {}; - context.JK.SessionHoverBubble = function(sessionId, position) { + context.JK.SessionHoverBubble = function(sessionId, x, y) { var logger = context.JK.logger; var rest = context.JK.Rest(); - var instrumentLogoMap = context.JK.getInstrumentIconMap24(); var hoverSelector = "#session-hover"; this.showBubble = function() { - $(hoverSelector).css({left: position.left-100, top: position.top+10}); + var mouseLeft = x < (document.body.clientWidth / 2); + var mouseTop = y < (document.body.clientHeight / 2); + var css = {}; + if (mouseLeft) + css.left = x + 10 + 'px'; + else + css.left = x - (7 + $(hoverSelector).width()) + 'px'; + if (mouseTop) + css.top = y + 10 + 'px'; + else + css.top = y - (7 + $(hoverSelector).height()) + 'px'; + + $(hoverSelector).css(css); $(hoverSelector).fadeIn(500); rest.getSessionHistory(sessionId) @@ -28,7 +39,7 @@ instrumentHtml = '
'; var instruments = val.instruments.split("|"); $.each(instruments, function(index, instrument) { - instrumentHtml += ' '; + instrumentHtml += ' '; }); instrumentHtml += '
'; @@ -45,7 +56,7 @@ genre: response.genres.toUpperCase(), comment_count: response.comment_count, like_count: response.like_count, - created_at: context.JK.formatDateTime(response.created_at), + created_at: $.timeago(response.created_at), musicians: musicianHtml }); 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/javascripts/utils.js b/web/app/assets/javascripts/utils.js index 657ab1ed8..057ffd3f7 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -154,6 +154,8 @@ } context.JK.bindHoverEvents = function ($parent) { + var timeout = 300; + var fadeoutValue = 100; if (!$parent) { $parent = $('body'); @@ -165,75 +167,87 @@ } function hideBubble($hoverElement) { + var bubbleSelector = $hoverElement.attr("bubble-id"); + $(bubbleSelector).hover( function () { - // do nothing when entering the bubble (this should never happen) + // do nothing when entering the bubble }, function () { - $(this).fadeOut(100); + $(this).fadeOut(fadeoutValue); } ); + + // first check to see if the user isn't hovering over the hover bubble + if (!$(bubbleSelector).is(":hover")) { + $(bubbleSelector).fadeOut(fadeoutValue); + } } // MUSICIAN $("[hoveraction='musician']", $parent).hoverIntent({ - over: function () { - var bubble = new JK.MusicianHoverBubble($(this).attr('user-id'), $(this).offset()); + over: function(e) { + var bubble = new JK.MusicianHoverBubble($(this).attr('user-id'), e.pageX, e.pageY); showBubble(bubble, $(this)); }, out: function () { // this registers for leaving the hoverable element hideBubble($(this)); }, - sensitivity: 1 + sensitivity: 1, + timeout: timeout }); // FAN $("[hoveraction='fan']", $parent).hoverIntent({ - over: function () { - var bubble = new JK.FanHoverBubble($(this).attr('user-id'), $(this).offset()); + over: function(e) { + var bubble = new JK.FanHoverBubble($(this).attr('user-id'), e.pageX, e.pageY); showBubble(bubble, $(this)); }, out: function () { // this registers for leaving the hoverable element hideBubble($(this)); }, - sensitivity: 1 + sensitivity: 1, + timeout: timeout }); // BAND $("[hoveraction='band']", $parent).hoverIntent({ - over: function () { - var bubble = new JK.BandHoverBubble($(this).attr('band-id'), $(this).offset()); + over: function(e) { + var bubble = new JK.BandHoverBubble($(this).attr('band-id'), e.pageX, e.pageY); showBubble(bubble, $(this)); }, out: function () { // this registers for leaving the hoverable element hideBubble($(this)); }, - sensitivity: 1 + sensitivity: 1, + timeout: timeout }); // SESSION $("[hoveraction='session']", $parent).hoverIntent({ - over: function () { - var bubble = new JK.SessionHoverBubble($(this).attr('session-id'), $(this).offset()); + over: function(e) { + var bubble = new JK.SessionHoverBubble($(this).attr('session-id'), e.pageX, e.pageY); showBubble(bubble, $(this)); }, out: function () { // this registers for leaving the hoverable element hideBubble($(this)); }, - sensitivity: 1 + sensitivity: 1, + timeout: timeout }); // RECORDING $("[hoveraction='recording']", $parent).hoverIntent({ - over: function () { - var bubble = new JK.RecordingHoverBubble($(this).attr('recording-id'), $(this).offset()); + over: function(e) { + var bubble = new JK.RecordingHoverBubble($(this).attr('recording-id'), e.pageX, e.pageY); showBubble(bubble, $(this)); }, out: function () { // this registers for leaving the hoverable element hideBubble($(this)); }, - sensitivity: 1 + sensitivity: 1, + timeout: timeout }); } diff --git a/web/app/assets/javascripts/web/recordings.js b/web/app/assets/javascripts/web/recordings.js index 1793b2609..5a901a81e 100644 --- a/web/app/assets/javascripts/web/recordings.js +++ b/web/app/assets/javascripts/web/recordings.js @@ -28,7 +28,6 @@ } function togglePlay() { - if(playing) { stopPlay(); } @@ -51,10 +50,10 @@ function like() { rest.addRecordingLike(recordingId, claimedRecordingId, JK.currentUserId) - .done(function(response) { - $("#spnLikeCount").html(parseInt($("#spnLikeCount").text()) + 1); - $("#btnLike", $scope).unbind("click"); - }); + .done(function(response) { + $("#spnLikeCount").html(parseInt($("#spnLikeCount").text()) + 1); + $("#btnLike", $scope).unbind("click"); + }); } function play() { @@ -68,22 +67,35 @@ var comment = $("#txtRecordingComment").val(); if ($.trim(comment).length > 0) { rest.addRecordingComment(recordingId, JK.currentUserId, comment) - .done(function(response) { - $("#spnCommentCount", $scope).html(parseInt($("#spnCommentCount").text()) + 1); - - var template = $('#template-landing-comment').html(); - var commentHtml = context.JK.fillTemplate(template, { - avatar_url: context.JK.currentUserAvatarUrl, - name: context.JK.currentUserName, - comment: comment, - timeago: $.timeago(Date.now()) - }); - - $(".landing-comment-scroller").prepend(commentHtml); - }); + .done(function(response) { + $("#spnCommentCount", $scope).html(parseInt($("#spnCommentCount").text()) + 1); + renderComment(comment, context.JK.currentUserId, context.JK.currentUserName, + context.JK.currentUserAvatarUrl, $.timeago(Date.now()), context.JK.currentUserMusician, false); + }); } } + function renderComment(comment, userId, userName, userAvatarUrl, timeago, musician, append) { + var template = $('#template-landing-comment').html(); + var commentHtml = context.JK.fillTemplate(template, { + avatar_url: userAvatarUrl, + user_id: userId, + hoverAction: musician ? "musician" : "fan", + name: userName, + comment: comment, + timeago: timeago + }); + + if (append) { + $(".landing-comment-scroller").append(commentHtml); + } + else { + $(".landing-comment-scroller").prepend(commentHtml); + } + + context.JK.bindHoverEvents(); + } + function initialize(_claimedRecordingId, _recordingId) { recordingId = _recordingId; claimedRecordingId = _claimedRecordingId; @@ -116,6 +128,32 @@ $("#btnLike").click(like); $("#btnPlay").click(play); + + $playButton.trigger('click'); + + pollForUpdates(claimedRecordingId); + } + + function pollForUpdates(claimedRecordingId) { + $(".landing-comment-scroller").empty(); + rest.getClaimedRecording(claimedRecordingId) + .done(function(response) { + if (response.recording && response.recording.comments) { + $("#spnPlayCount", $scope).html(response.recording.play_count); + $("#spnCommentCount", $scope).html(response.recording.comment_count); + $("#spnLikeCount", $scope).html(response.recording.like_count); + $.each(response.recording.comments, function(index, val) { + renderComment(val.comment, val.creator.id, val.creator.name, + context.JK.resolveAvatarUrl(val.creator.photo_url), $.timeago(val.created_at), val.creator.musician, true); + }); + setTimeout(function() { + pollForUpdates(claimedRecordingId); + }, 60000); + } + }) + .fail(function(xhr) { + + }); } this.initialize = initialize; diff --git a/web/app/assets/javascripts/web/sessions.js b/web/app/assets/javascripts/web/sessions.js index 070333da5..55a569142 100644 --- a/web/app/assets/javascripts/web/sessions.js +++ b/web/app/assets/javascripts/web/sessions.js @@ -4,38 +4,53 @@ var logger = context.JK.logger; var rest = new JK.Rest(); var sessionId = null; + var $scope = $(".landing-details"); var $controls = null; var $status = null; + var $playButton = $('.play-button'); var playing = false; function like() { rest.addSessionLike(sessionId, JK.currentUserId) - .done(function(response) { - $("#spnLikeCount").html(parseInt($("#spnLikeCount").text()) + 1); - $("#btnLike").unbind("click"); - }); + .done(function(response) { + $("#spnLikeCount").html(parseInt($("#spnLikeCount").text()) + 1); + $("#btnLike").unbind("click"); + }); } function addComment() { var comment = $("#txtSessionComment").val(); if ($.trim(comment).length > 0) { - rest.addSessionComment(sessionId, JK.currentUserId, comment) - .done(function(response) { - $("#spnCommentCount").html(parseInt($("#spnCommentCount").text()) + 1); - - var template = $('#template-landing-comment').html(); - var commentHtml = context.JK.fillTemplate(template, { - avatar_url: context.JK.currentUserAvatarUrl, - name: context.JK.currentUserName, - comment: comment, - timeago: $.timeago(Date.now()) - }); - - $(".landing-comment-scroller").prepend(commentHtml); - }); + rest.addSessionComment(sessionId, JK.currentUserId, comment) + .done(function(response) { + $("#spnCommentCount").html(parseInt($("#spnCommentCount").text()) + 1); + renderComment(comment, context.JK.currentUserId, context.JK.currentUserName, + context.JK.currentUserAvatarUrl, $.timeago(Date.now()), context.JK.currentUserMusician, false); + }); } } + function renderComment(comment, userId, userName, userAvatarUrl, timeago, musician, append) { + var template = $('#template-landing-comment').html(); + var commentHtml = context.JK.fillTemplate(template, { + avatar_url: userAvatarUrl, + user_id: userId, + hoverAction: musician ? "musician" : "fan", + name: userName, + comment: comment, + timeago: timeago + }); + + if (append) { + $(".landing-comment-scroller").append(commentHtml); + } + else { + $(".landing-comment-scroller").prepend(commentHtml); + } + + context.JK.bindHoverEvents(); + } + function stateChange(e, data) { if(data.displayText) { @@ -88,34 +103,57 @@ $controls.bind('statechange.listenBroadcast', stateChange); context.JK.prettyPrintElements($('time.duration').show()); context.JK.TickDuration(null); - $('.play-button').click(togglePlay); - + $playButton.click(togglePlay); sessionId = musicSessionId; if (JK.currentUserId) { - var shareDialog = new JK.ShareDialog(context.JK.app, sessionId, "session"); - shareDialog.initialize(JK.FacebookHelperInstance); + var shareDialog = new JK.ShareDialog(context.JK.app, sessionId, "session"); + shareDialog.initialize(JK.FacebookHelperInstance); - $("#btnShare").click(function(e) { - shareDialog.showDialog(); - }); + $("#btnShare").click(function(e) { + shareDialog.showDialog(); + }); - $("#btnPostComment").click(function(e) { - if ($.trim($("#txtSessionComment").val()).length > 0) { - addComment(); - $("#txtSessionComment").val(''); - $("#txtSessionComment").blur(); - } - }); + $("#btnPostComment").click(function(e) { + if ($.trim($("#txtSessionComment").val()).length > 0) { + addComment(); + $("#txtSessionComment").val(''); + $("#txtSessionComment").blur(); + } + }); } else { - $("#txtSessionComment").attr("disabled", "disabled"); - $("#txtSessionComment").val("You must be logged in to add a comment."); + $("#txtSessionComment").attr("disabled", "disabled"); + $("#txtSessionComment").val("You must be logged in to add a comment."); } $("#btnLike").click(like); + $playButton.trigger('click'); + + pollForUpdates(musicSessionId); + } + + function pollForUpdates(musicSessionId) { + $(".landing-comment-scroller").empty(); + rest.getSessionHistory(musicSessionId) + .done(function(response) { + if (response && response.comments) { + $("#spnCommentCount", $scope).html(response.comment_count); + $("#spnLikeCount", $scope).html(response.like_count); + $.each(response.comments, function(index, val) { + renderComment(val.comment, val.creator.id, val.creator.name, + context.JK.resolveAvatarUrl(val.creator.photo_url), $.timeago(val.created_at), val.creator.musician, true); + }); + setTimeout(function() { + pollForUpdates(musicSessionId); + }, 60000); + } + }) + .fail(function(xhr) { + + }); } this.initialize = initialize; 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..3d2efd754 100644 --- a/web/app/assets/stylesheets/client/hoverBubble.css.scss +++ b/web/app/assets/stylesheets/client/hoverBubble.css.scss @@ -4,11 +4,11 @@ background-color:#242323; border:solid 1px #ed3618; position:absolute; - z-index:999; + z-index:1100; &.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/extras_controller.rb b/web/app/controllers/extras_controller.rb new file mode 100644 index 000000000..131a316d0 --- /dev/null +++ b/web/app/controllers/extras_controller.rb @@ -0,0 +1,9 @@ +class ExtrasController < ApplicationController + + before_filter :signed_in_user + before_filter :admin_user + + def settings + render layout: "web" + end +end diff --git a/web/app/controllers/users_controller.rb b/web/app/controllers/users_controller.rb index cea76d118..d31bb4094 100644 --- a/web/app/controllers/users_controller.rb +++ b/web/app/controllers/users_controller.rb @@ -396,10 +396,6 @@ class UsersController < ApplicationController redirect_to(root_url) unless current_user?(@user) end - def admin_user - redirect_to(root_url) unless current_user.admin? - end - # the User Model expects instruments in a different format than the form submits it # so we have to fix it up. def fixup_instruments(original_instruments) 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/helpers/sessions_helper.rb b/web/app/helpers/sessions_helper.rb index 9966731a6..98b0c9774 100644 --- a/web/app/helpers/sessions_helper.rb +++ b/web/app/helpers/sessions_helper.rb @@ -57,6 +57,10 @@ module SessionsHelper cookies.delete(:remember_token, domain: Rails.application.config.session_cookie_domain) end + def admin_user + redirect_to(root_url) unless current_user.admin? + end + def redirect_back_or(default) redirect_to(session[:return_to] || default) session.delete(:return_to) diff --git a/web/app/views/api_claimed_recordings/show.rabl b/web/app/views/api_claimed_recordings/show.rabl index 683c1bed9..5210fcbc9 100644 --- a/web/app/views/api_claimed_recordings/show.rabl +++ b/web/app/views/api_claimed_recordings/show.rabl @@ -35,7 +35,7 @@ child(:recording => :recording) { attributes :comment, :created_at child(:user => :creator) { - attributes :id, :first_name, :last_name, :photo_url + attributes :id, :first_name, :last_name, :name, :photo_url, :musician } } } diff --git a/web/app/views/api_music_sessions/history_show.rabl b/web/app/views/api_music_sessions/history_show.rabl index 841e427d5..d29de5af3 100644 --- a/web/app/views/api_music_sessions/history_show.rabl +++ b/web/app/views/api_music_sessions/history_show.rabl @@ -22,4 +22,12 @@ child(:music_session_user_histories => :users) { child(:user => :user) { attributes :name, :photo_url } +} + +child(:comments => :comments) { + attributes :comment, :created_at + + child(:user => :creator) { + attributes :id, :first_name, :last_name, :name, :photo_url, :musician + } } \ 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 @@ -