diff --git a/Gemfile b/Gemfile index 1e0e868f4..d14e02d67 100644 --- a/Gemfile +++ b/Gemfile @@ -78,6 +78,7 @@ group :test, :cucumber do gem 'rack-test' # gem 'rb-fsevent', '0.9.1', :require => false # gem 'growl', '1.0.3' + gem 'poltergeist' end group :production do diff --git a/app/assets/images/content/bkg_home_account_x.jpg b/app/assets/images/content/bkg_home_account_x.jpg new file mode 100644 index 000000000..43cc67ff6 Binary files /dev/null and b/app/assets/images/content/bkg_home_account_x.jpg differ diff --git a/app/assets/images/content/bkg_home_create_x.jpg b/app/assets/images/content/bkg_home_create_x.jpg new file mode 100644 index 000000000..4e05cbda8 Binary files /dev/null and b/app/assets/images/content/bkg_home_create_x.jpg differ diff --git a/app/assets/images/content/bkg_home_feed_x.jpg b/app/assets/images/content/bkg_home_feed_x.jpg new file mode 100644 index 000000000..2535c1bc4 Binary files /dev/null and b/app/assets/images/content/bkg_home_feed_x.jpg differ diff --git a/app/assets/images/content/bkg_home_find_x.jpg b/app/assets/images/content/bkg_home_find_x.jpg new file mode 100644 index 000000000..fd93fbc4c Binary files /dev/null and b/app/assets/images/content/bkg_home_find_x.jpg differ diff --git a/app/assets/images/content/bkg_home_profile_x.jpg b/app/assets/images/content/bkg_home_profile_x.jpg new file mode 100644 index 000000000..6d4fe73c7 Binary files /dev/null and b/app/assets/images/content/bkg_home_profile_x.jpg differ diff --git a/app/assets/javascripts/AAB_message_factory.js b/app/assets/javascripts/AAB_message_factory.js index 5c31c0a79..76f08b556 100644 --- a/app/assets/javascripts/AAB_message_factory.js +++ b/app/assets/javascripts/AAB_message_factory.js @@ -69,7 +69,10 @@ // create a login message using token (a cookie or similiar) factory.login_with_token = function(token) { - var login = { token : token }; + //context.JK.logger.debug("*** login_with_token: client_id = "+$.cookie("client_id")); + var login = { token : token, + client_id : $.cookie("client_id") + }; return client_container(msg.LOGIN, route_to.SERVER, login); }; diff --git a/app/assets/javascripts/JamServer.js b/app/assets/javascripts/JamServer.js index b21c5d526..e9d9b21fc 100644 --- a/app/assets/javascripts/JamServer.js +++ b/app/assets/javascripts/JamServer.js @@ -1,7 +1,3 @@ -// TODO: Rename? This is really only the websocket/messaging -// part of the server (none of the REST calls go through this). -// perhaps something like RealTimeMessages or something... -// // The wrapper around the web-socket connection to the server (function(context, $) { @@ -32,7 +28,7 @@ server.unregisterMessageCallback = function(messageType, callback) { if (server.dispatchTable[messageType] !== undefined) { - for(var i = server.dispatchTable.length; i--;) { + for(var i = server.dispatchTable[messageType].length; i--;) { if (server.dispatchTable[messageType][i] === callback) { server.dispatchTable[messageType].splice(i, 1); @@ -56,12 +52,15 @@ server.socket.onclose = server.onClose; }; - server.onOpen = function() { - logger.log("server.onOpen"); + server.rememberLogin = function() { var token, loginMessage; token = $.cookie("remember_token"); loginMessage = msg_factory.login_with_token(token); server.send(loginMessage); + } + server.onOpen = function() { + logger.log("server.onOpen"); + server.rememberLogin(); }; server.onMessage = function(e) { @@ -153,18 +152,6 @@ } }); - server.registerMessageCallback(context.JK.MessageType.USER_JOINED_MUSIC_SESSION, function(header, payload) { - context.JK.refreshMusicSession(payload.session_id); - }); - - // TODO: not used - server.registerMessageCallback(context.JK.MessageType.LOGIN_MUSIC_SESSION_ACK, function(header, payload) { - if (context.jamClient !== undefined) - { - // TODO: modify the LOGIN_MUSIC_SESSION_ACK message to include session_id - context.jamClient.JoinSession({ sessionID : payload.session_id}); - } - }); // Callbacks from jamClient if (context.jamClient !== undefined) diff --git a/app/assets/javascripts/createSession.js b/app/assets/javascripts/createSession.js index 8768ce4b7..b1b4336cb 100644 --- a/app/assets/javascripts/createSession.js +++ b/app/assets/javascripts/createSession.js @@ -128,12 +128,13 @@ } // FIXME TODO: + // This code is duplicated in sessionModel.js -- refactor // 1. If no previous session data, a single stereo track with the // top instrument in the user's profile. // 2. Otherwise, use the tracks from the last created session. // Defaulting to 1st instrument in profile always at the moment. var track = { instrument_id: "electric guitar", sound: "stereo" }; - if (context.JK.userMe.instruments.length) { + if (context.JK.userMe.instruments && context.JK.userMe.instruments.length) { track = { instrument_id: context.JK.userMe.instruments[0].instrument_id, sound: "stereo" diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index b6466ad45..0e7f45135 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -11,7 +11,7 @@ context.JK.Header = function(app) { var logger = context.JK.logger; - var searcher; // Will hold an instance to a JK.Searcher (search.js) + var searcher; // Will hold an instance to a JK.Searcher (search.js) var userMe = null; var instrumentAutoComplete; var instrumentIds = []; @@ -156,10 +156,7 @@ function updateHeader() { $('#user').html(userMe.name); - var photoUrl = userMe.photo_url; - if (!(photoUrl)) { - photoUrl = "/assets/shared/avatar_default.jpg"; - } + var photoUrl = context.JK.resolveAvatarUrl(userMe.photo_url); $('#header-avatar').attr('src', photoUrl); } diff --git a/app/assets/javascripts/jamkazam.js b/app/assets/javascripts/jamkazam.js index 520f20d8a..453919d57 100644 --- a/app/assets/javascripts/jamkazam.js +++ b/app/assets/javascripts/jamkazam.js @@ -7,7 +7,7 @@ var JamKazam = context.JK.JamKazam = function() { var app; var logger = context.JK.logger; - var heartbeatInterval = null; + var heartbeatInterval=null; var opts = { layoutOpts: {} @@ -42,20 +42,21 @@ $(routes.handler); } - function _handleLoginAck(header, payload) { - var heartbeatMS = payload.heartbeat_interval * 1000; - logger.debug("Login ACK. Setting up heartbeat every " + heartbeatMS + " MS"); - heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); - } - function _heartbeat() { - var message = context.JK.MessageFactory.heartbeat(); - context.JK.JamServer.send(message); + if (app.heartbeatActive) { + var message = context.JK.MessageFactory.heartbeat(); + context.JK.JamServer.send(message); + } } function loggedIn(header, payload) { app.clientId = payload.client_id; - logger.debug("jamkazam.js: loggedIn. clientId now " + app.clientId); + $.cookie('client_id', payload.client_id); + $.cookie('remember_token', payload.token); + + var 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); } function registerLoginAck() { @@ -143,6 +144,9 @@ events(); }; + // enable temporary suspension of heartbeat for fine-grained control + this.heartbeatActive = true; + /** * Expose clientId as a public variable. * Will be set upon LOGIN_ACK diff --git a/app/assets/javascripts/searchResults.js b/app/assets/javascripts/searchResults.js index ccdb50477..860a7e92d 100644 --- a/app/assets/javascripts/searchResults.js +++ b/app/assets/javascripts/searchResults.js @@ -7,27 +7,116 @@ var logger = context.JK.logger; var query; + var instrument_logo_map = context.JK.getInstrumentIconMap24(); + function beforeShow(data) { query = data.query; } function afterShow(data) { - // TODO remove me - just showing that you should - // have access to the query variable from the search - // box. - $('#show-query').text('Query is ' + query); } function search(evt) { evt.stopPropagation(); + + $('#search-results').empty(); var query = $('#search-input').val(); context.location = '#/searchResults/:' + query; + + $('#query').html(query); + + var url = "/api/search?query=" + query; + $.ajax({ + type: "GET", + dataType: "json", + contentType: 'application/json', + url: url, + processData: false, + success: function(response) { + $.each(response.musicians, function(index, val) { + + // fill in template for Connect pre-click + var template = $('#template-search-result').html(); + var searchResultHtml = context.JK.fillTemplate(template, { + userId: val.id, + avatar_url: context.JK.resolveAvatarUrl(val.photo_url), + userName: val.name, + location: val.location, + instruments: getInstrumentHtml(val.instruments) + }); + + $('#search-results').append(searchResultHtml); + + // fill in template for Connect post-click + template = $('#template-invitation-sent').html(); + var invitationSentHtml = context.JK.fillTemplate(template, { + userId: val.id, + first_name: val.first_name, + profile_url: "/users/" + val.id + }); + + $('#search-results').append(invitationSentHtml); + + // initialize visibility of the divs + $('div[user-id=' + val.id + '].search-connected').hide(); + $('div[user-id=' + val.id + '].search-result').show(); + + // wire up button click handler + $('div[user-id=' + val.id + ']').find('#btn-connect-friend').click(sendFriendRequest); + }); + var resultCount = response.musicians.length; + $('#result-count').html(resultCount); + if (resultCount === 1) { + $('#result-count').append(" Result "); + } + else { + $('#result-count').append(" Results "); + } + }, + error: app.ajaxError + }); + return false; } + function sendFriendRequest(evt) { + evt.stopPropagation(); + var userId = $(this).parent().attr('user-id'); + + //$(this).parent().empty(); + + var url = "/api/users/" + context.JK.currentUserId + "/friend_requests"; + $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: url, + data: '{"friend_id":"' + userId + '"}', + processData: false, + success: function(response) { + // toggle the pre-click and post-click divs + $('div[user-id=' + userId + '].search-connected').show(); + $('div[user-id=' + userId + '].search-result').hide(); + }, + error: app.ajaxError + }); + } + + function getInstrumentHtml(instruments) { + var instrumentLogoHtml = ''; + if (instruments !== undefined) { + for (var i=0; i < instruments.length; i++) { + var inst = '../assets/content/icon_instrument_default24.png'; + if (instruments[i].instrument_id in instrument_logo_map) { + inst = instrument_logo_map[instruments[i].instrument_id]; + instrumentLogoHtml += ' '; + } + } + } + return instrumentLogoHtml; + } + function events() { - // not sure it should go here long-term, but wiring - // up the event handler for the search box in the sidebar. $('#searchForm').submit(search); } diff --git a/app/assets/javascripts/session.js b/app/assets/javascripts/session.js index 207a2b62e..3a6e0077b 100644 --- a/app/assets/javascripts/session.js +++ b/app/assets/javascripts/session.js @@ -5,11 +5,11 @@ context.JK = context.JK || {}; context.JK.SessionScreen = function(app) { var logger = context.JK.logger; + var sessionModel = null; var sessionId; - var session = null; - var users = {}; // Light cache of user info for session users. var tracks = {}; var mixers = []; + // TODO Consolidate dragged controls and handles var $draggingFaderHandle = null; var $draggingFader = null; @@ -36,14 +36,7 @@ } }; - var instrumentIcons = { - "keyboard": "content/icon_instrument_keyboard45.png", - "electric guitar": "content/icon_instrument_guitar45.png", - "bass guitar": "content/icon_instrument_guitar45.png", - "voice": "content/icon_instrument_vocal45.png", - "saxophone": "content/icon_instrument_saxophone45.png" - }; - + var instrumentIcons = context.JK.getInstrumentIconMap45(); // Recreate ChannelGroupIDs ENUM from C++ var ChannelGroupIds = { @@ -86,80 +79,23 @@ } function afterCurrentUserLoaded() { - // Set a flag indicating we're still joining. - // This flag is set to true in refreshSession.js - // after various asynch calls complete. - context.JK.sessionJoined = false; - context.JK.joinMusicSession(sessionId, app); - - function whenSessionJoined() { - if (!(context.JK.sessionJoined)) { - context.setTimeout(whenSessionJoined, 100); - } else { - // ready to proceed. - $.ajax({ - type: "GET", - url: "/api/sessions/" + sessionId, - success: updateSession, - error: fetchSessionError - }); - } - } - whenSessionJoined(); - + sessionModel = new context.JK.SessionModel( + context.JK.JamServer, + context.jamClient, + context.JK.userMe + ); + sessionModel.subscribe('sessionScreen', sessionChanged); + sessionModel.joinSession(sessionId); } function beforeHide(data) { - // Move joinSession function to this file for ease of finding it? - context.JK.leaveMusicSession(sessionId); + sessionModel.leaveCurrentSession(sessionId); // 'unregister' for callbacks context.jamClient.SessionRegisterCallback(""); } - function fetchSessionError() { - context.location = '#/home'; - } - - function updateSession(sessionData) { - session = sessionData; - updateParticipants(function() { renderSession(); }); - } - - /** - * Make sure that for each participant in the session, we have user info. - */ - function updateParticipants(onComplete) { - var callCount = 0; - $.each(session.participants, function(index, value) { - if (!(this.user.id in users)) { - var userInfoUrl = "/api/users/" + this.user.id; - callCount += 1; - $.ajax({ - type: "GET", - url: userInfoUrl, - success: function(user) { - callCount -= 1; - users[user.id] = user; - }, - error: function(jqXHR, textStatus, errorThrown) { - callCount -= 1; - logger.error('Error getting user info from ' + userInfoUrl); - } - }); - } - }); - if (!(onComplete)) { - return; - } - // TODO: generalize this pattern. Likely needed elsewhere. - function checker() { - if (callCount === 0) { - onComplete(); - } else { - context.setTimeout(checker, 10); - } - } - checker(); + function sessionChanged() { + renderSession(); } function renderSession() { @@ -178,17 +114,6 @@ mixers = holder.mixers; } - function _participantForClientId(clientId) { - var foundParticipant = null; - $.each(session.participants, function(index, participant) { - if (participant.client_id === clientId) { - foundParticipant = participant; - return false; - } - }); - return foundParticipant; - } - function _mixerForClientId(clientId, groupIds) { var foundMixer = null; $.each(mixers, function(index, mixer) { @@ -254,17 +179,14 @@ // Draw tracks from participants, then setup timers to look for the // mixers that go with those participants, if they're missing. lookingForMixersCount = 0; - $.each(session.participants, function(index, participant) { + $.each(sessionModel.participants(), function(index, participant) { var name = participant.user.name; if (!(name)) { name = participant.user.first_name + ' ' + participant.user.last_name; } var instrumentIcon = _instrumentIconFromId(participant.tracks[0].instrument_id); - var photoUrl = participant.user.photo_url; - if (!(photoUrl)) { - photoUrl = "/assets/shared/avatar_default.jpg"; - } + var photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url); var myTrack = false; @@ -405,25 +327,6 @@ tracks[trackData.clientId] = new context.JK.SessionTrack(trackData.clientId); } - function _userJoinedSession(header, payload) { - reloadAndUpdateSession(); - } - - function _userLeftSession(header, payload) { - reloadAndUpdateSession(); - } - - function reloadAndUpdateSession() { - $.ajax({ - type: "GET", - url: "/api/sessions/" + sessionId, - success: function(response) { updateSession(response); }, - error: function(jqXHR, textStatus, errorThrown) { - logger.error("Error loading session " + sessionId); - } - }); - } - function handleBridgeCallback() { var eventName = null; var mixerId = null; @@ -771,13 +674,6 @@ } this.initialize = function() { - context.JK.JamServer.registerMessageCallback( - context.JK.MessageType.USER_JOINED_MUSIC_SESSION, - _userJoinedSession); - context.JK.JamServer.registerMessageCallback( - context.JK.MessageType.USER_LEFT_MUSIC_SESSION, - _userLeftSession); - context.jamClient.SetVURefreshRate(150); events(); var screenBindings = { diff --git a/app/assets/javascripts/sessionList.js b/app/assets/javascripts/sessionList.js index df1b17275..cf98590e6 100644 --- a/app/assets/javascripts/sessionList.js +++ b/app/assets/javascripts/sessionList.js @@ -16,16 +16,7 @@ MUSICIANS_ONLY:"Musicians Only" }; - // JW: TODO - I'm building a similar map in session.js - // At somepoint, we should refactor to somewhere we can share. - var instrument_logo_map = { - "acoustic guitar": '../assets/content/icon_instrument_guitar24.png', - "bass guitar": '../assets/content/icon_instrument_guitar24.png', - "electric guitar": '../assets/content/icon_instrument_guitar24.png', - "keyboard": '../assets/content/icon_instrument_keyboard24.png', - "saxophone": '/assets/content/icon_instrument_saxophone24.png', - "voice": '../assets/content/icon_instrument_vocal24.png' - }; + var instrument_logo_map = context.JK.getInstrumentIconMap24(); var _logger = context.JK.logger; @@ -82,7 +73,7 @@ var id = participant.user.id; var name = participant.user.name; - var photoUrl = participant.user.photo_url ? participant.user.photo_url : "/assets/shared/avatar_default.jpg"; + var photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url); var musicianVals = { avatar_url: photoUrl, profile_url: "users/" + id, diff --git a/app/assets/javascripts/sessionModel.js b/app/assets/javascripts/sessionModel.js index 37f974727..043a10289 100644 --- a/app/assets/javascripts/sessionModel.js +++ b/app/assets/javascripts/sessionModel.js @@ -7,60 +7,216 @@ context.JK = context.JK || {}; var logger = context.JK.logger; - var sessions = {}; + context.JK.SessionModel = function(server, client, currentUser) { + var clientId = client.clientID; + var currentSessionId = null; // Set on join, prior to setting currentSession. + var currentSession = null; + var subscribers = {}; + var users = {}; // User info for session participants - sessions.instances = {}; - - sessions.JoinSession = function(session_id) { - if (sessions.instances[session_id] !== undefined) { - logger.error("ERROR: Joined a session twice: " + session_id); - return; + function id() { + return currentSession.id; } - sessions.instances[session_id] = { - id: session_id, - participants: {} - }; - }; - - sessions.LeaveSession = function(session_id) { - if (sessions.instances[session_id] !== undefined) { - delete sessions.instances[session_id]; - } - }; - - sessions.UpdateSessionParticipants = function(session_id, participants) { - var added = []; - var removed = []; - var session = sessions.instances[session_id]; - - if (session === undefined) { - logger.error("ERROR: Unknown session: " + session_id); - return; - } - - var toBeRemoved = $.extend({}, session.participants); - - $.each(participants, function (index, participant) { - if (session.participants[participant.client_id] === undefined) - { - session.participants[participant.client_id] = participant; - added.push(participant); + function participants() { + if (currentSession) { + return currentSession.participants; + } else { + return []; } - else - { - delete toBeRemoved[participant.client_id]; + } + + /** + * Join the session specified by the provided id. + */ + function joinSession(sessionId) { + currentSessionId = sessionId; + logger.debug("SessionModel.joinSession(" + sessionId + ")"); + joinSessionRest(sessionId, function() { + refreshCurrentSession(); + }); + server.registerMessageCallback( + context.JK.MessageType.USER_JOINED_MUSIC_SESSION, + refreshCurrentSession); + server.registerMessageCallback( + context.JK.MessageType.USER_LEFT_MUSIC_SESSION, + refreshCurrentSession); + } + + /** + * Leave the current session + */ + function leaveCurrentSession() { + logger.debug("SessionModel.leaveCurrentSession()"); + // TODO - sessionChanged will be called with currentSession = null + server.unregisterMessageCallback( + context.JK.MessageType.USER_JOINED_MUSIC_SESSION, + refreshCurrentSession); + server.unregisterMessageCallback( + context.JK.MessageType.USER_LEFT_MUSIC_SESSION, + refreshCurrentSession); + leaveSessionRest(currentSessionId, sessionChanged); + currentSession = null; + currentSessionId = null; + } + + /** + * Refresh the current session, and participants. + */ + function refreshCurrentSession() { + logger.debug("SessionModel.refreshCurrentSession()"); + refreshCurrentSessionRest(function() { + refreshCurrentSessionParticipantsRest(sessionChanged); + }); + } + + /** + * Subscribe for sessionChanged events. Provide a subscriberId + * and a callback to be invoked on session changes. + */ + function subscribe(subscriberId, sessionChangedCallback) { + logger.debug("SessionModel.subscribe(" + subscriberId + ", [callback])"); + subscribers[subscriberId] = sessionChangedCallback; + } + + /** + * Notify subscribers that the current session has changed. + */ + function sessionChanged() { + logger.debug("SessionModel.sessionChanged()"); + for (var subscriberId in subscribers) { + subscribers[subscriberId](); } - }); + } - $.each(toBeRemoved, function(client_id, participant) { - delete session.participants[client_id]; - removed.push(participant); - }); + /** + * Reload the session data from the REST server, calling + * the provided callback when complete. + */ + function refreshCurrentSessionRest(callback) { + var url = "/api/sessions/" + currentSessionId; + $.ajax({ + type: "GET", + url: url, + success: function(response) { + currentSession = response; + callback(); + }, + error: ajaxError + }); + } + + /** + * Ensure that we have user info for all current participants. + */ + function refreshCurrentSessionParticipantsRest(callback) { + var callCount = 0; + $.each(participants(), function(index, value) { + if (!(this.user.id in users)) { + var userInfoUrl = "/api/users/" + this.user.id; + callCount += 1; + $.ajax({ + type: "GET", + url: userInfoUrl, + success: function(user) { + callCount -= 1; + users[user.id] = user; + }, + error: function(jqXHR, textStatus, errorThrown) { + callCount -= 1; + logger.error('Error getting user info from ' + userInfoUrl); + } + }); + } + }); + if (!(callback)) { + return; + } + context.JK.joinCalls( + function() { return callCount === 0; }, callback, 10); + } + + function participantForClientId(clientId) { + var foundParticipant = null; + $.each(currentSession.participants, function(index, participant) { + if (participant.client_id === clientId) { + foundParticipant = participant; + return false; + } + }); + return foundParticipant; + } + + /** + * Make the server calls to join the current user to + * the session provided. + */ + function joinSessionRest(sessionId, callback) { + var tracks = getUserTracks(); + var data = { + client_id: clientId, + ip_address: server.publicIP, + as_musician: true, + tracks: tracks + }; + var url = "/api/sessions/" + sessionId + "/participants"; + $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: url, + data: JSON.stringify(data), + processData:false, + success: function(response) { + client.JoinSession({ sessionID: sessionId }); + callback(); + }, + error: ajaxError + }); + } + + function leaveSessionRest(sessionId, callback) { + var url = "/api/participants/" + clientId; + $.ajax({ + type: "DELETE", + url: url, + success: function (response) { + client.LeaveSession({ sessionID: sessionId }); + callback(); + }, + error: ajaxError + }); + } + + function ajaxError(jqXHR, textStatus, errorMessage) { + logger.error("Unexpected ajax error: " + textStatus); + } + + function getUserTracks() { + // FIXME. Setting tracks for join session. Only looking at profile + // for now. Needs to check jamClient instruments and if set, use those + // as first preference. Also need to support jamClient possibly having + // multiple tracks. + // TODO: Defaulting to electric guitar... + var track = { instrument_id: "electric guitar", sound: "stereo" }; + if (currentUser.instruments && currentUser.instruments.length) { + track = { + instrument_id: currentUser.instruments[0].instrument_id, + sound: "stereo" + }; + } + return [track]; + } + + // Public interface + this.id = id; + this.participants = participants; + this.joinSession = joinSession; + this.leaveCurrentSession = leaveCurrentSession; + this.refreshCurrentSession = refreshCurrentSession; + this.subscribe = subscribe; + this.participantForClientId = participantForClientId; - return { added: added, removed: removed }; }; - context.JK.Sessions = sessions; - })(window,jQuery); \ No newline at end of file diff --git a/app/assets/javascripts/sidebar.js b/app/assets/javascripts/sidebar.js new file mode 100644 index 000000000..e1ca1e9a0 --- /dev/null +++ b/app/assets/javascripts/sidebar.js @@ -0,0 +1,80 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.Sidebar = function(app) { + var logger = context.JK.logger; + + function populateFriendsPanel() { + + var url = "/api/users/" + context.JK.currentUserId + "/friends" + $.ajax({ + type: "GET", + dataType: "json", + contentType: 'application/json', + url: url, + processData: false, + success: function(response) { + $.each(response, function(index, val) { + + var css = val.online ? '' : 'offline'; + + // fill in template for Connect pre-click + var template = $('#template-friend-panel').html(); + var searchResultHtml = context.JK.fillTemplate(template, { + userId: val.id, + cssClass: css, + avatar_url: context.JK.resolveAvatarUrl(val.photo_url), + userName: val.name, + status: val.online ? 'Available' : 'Offline', + extra_info: '', + info_image_url: '' + }); + + $('#sidebar-friend-list').append(searchResultHtml); + }); + + // set friend count + $('#sidebar-friend-count').html(response.length); + }, + error: app.ajaxError + }); + + return false; + } + + function sendFriendRequest(evt) { + evt.stopPropagation(); + var userId = $(this).parent().attr('user-id'); + + //$(this).parent().empty(); + + var url = "/api/users/" + context.JK.currentUserId + "/friend_requests"; + $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: url, + data: '{"friend_id":"' + userId + '"}', + processData: false, + success: function(response) { + // toggle the pre-click and post-click divs + $('div[user-id=' + userId + '].search-connected').show(); + $('div[user-id=' + userId + '].search-result').hide(); + }, + error: app.ajaxError + }); + } + + function events() { + populateFriendsPanel(); + } + + this.initialize = function() { + events(); + }; + + }; + +})(window,jQuery); \ No newline at end of file diff --git a/app/assets/javascripts/utils.js b/app/assets/javascripts/utils.js index 3faad3af4..afb214297 100644 --- a/app/assets/javascripts/utils.js +++ b/app/assets/javascripts/utils.js @@ -7,6 +7,24 @@ context.JK = context.JK || {}; + var instrumentIconMap24 = { + "acoustic guitar": '../assets/content/icon_instrument_guitar24.png', + "bass guitar": '../assets/content/icon_instrument_guitar24.png', + "electric guitar": '../assets/content/icon_instrument_guitar24.png', + "keyboard": '../assets/content/icon_instrument_keyboard24.png', + "saxophone": '../assets/content/icon_instrument_saxophone24.png', + "voice": '../assets/content/icon_instrument_vocal24.png' + }; + + var instrumentIconMap45 = { + "acoustic guitar": '../assets/content/icon_instrument_guitar45.png', + "bass guitar": "../assets/content/icon_instrument_guitar45.png", + "electric guitar": "../assets/content/icon_instrument_guitar45.png", + "keyboard": "../assets/content/icon_instrument_keyboard45.png", + "saxophone": "../assets/content/icon_instrument_saxophone45.png", + "voice": "../assets/content/icon_instrument_vocal45.png" + }; + // Uber-simple templating // var template = "Hey {name}"; // var vals = { name: "Jon" }; @@ -18,6 +36,18 @@ return template; }; + context.JK.resolveAvatarUrl = function(photo_url) { + return photo_url ? photo_url : "/assets/shared/avatar_default.jpg"; + } + + context.JK.getInstrumentIconMap24 = function() { + return instrumentIconMap24; + } + + context.JK.getInstrumentIconMap45 = function() { + return instrumentIconMap45; + } + /* * Get the length of a dictionary */ @@ -31,6 +61,25 @@ return count; }; + /** + * Way to verify that a number of parallel tasks have all completed. + * Provide a function to evaluate completion, and a callback to + * invoke when that function evaluates to true. + * NOTE: this does not pause execution, it simply ensures that + * when the test function evaluates to true, the callback will + * be invoked. + */ + context.JK.joinCalls = function(completionTestFunction, callback, interval) { + function doneYet() { + if (completionTestFunction()) { + callback(); + } else { + context.setTimeout(doneYet, interval); + } + } + doneYet(); + }; + /* * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * Digest Algorithm, as defined in RFC 1321. diff --git a/app/assets/stylesheets/client/home.css.scss b/app/assets/stylesheets/client/home.css.scss index 2438666ba..dd1e7ebb9 100644 --- a/app/assets/stylesheets/client/home.css.scss +++ b/app/assets/stylesheets/client/home.css.scss @@ -43,5 +43,21 @@ .homecard.hover { border: 1px solid $translucent2 !important; + background-color: #b32712; +} +.homecard.createsession.hover { + background-image: url(/assets/content/bkg_home_create_x.jpg); +} +.homecard.findsession.hover { + background-image: url(/assets/content/bkg_home_find_x.jpg); +} +.homecard.profile.hover { + background-image: url(/assets/content/bkg_home_profile_x.jpg); +} +.homecard.feed.hover { + background-image: url(/assets/content/bkg_home_feed_x.jpg); +} +.homecard.account.hover { + background-image: url(/assets/content/bkg_home_account_x.jpg); } diff --git a/app/assets/stylesheets/client/searchResults.css.scss b/app/assets/stylesheets/client/searchResults.css.scss new file mode 100644 index 000000000..6734ec71b --- /dev/null +++ b/app/assets/stylesheets/client/searchResults.css.scss @@ -0,0 +1,94 @@ +.search-result-header { + width:100%; + padding:11px 0px 11px 0px; + background-color:#4c4c4c; + min-height:20px; + overflow-x:hidden; +} + +a.search-nav { + font-size:13px; + color:#fff; + text-decoration:none; + margin-right:40px; + float:left; + font-weight:200; + padding-bottom:4px; +} + +a.search-nav.active, a.search-nav.active:hover { + font-weight:700; + border-bottom:solid 3px #ed3618; +} + +a.search-nav:hover { + border-bottom: dotted 2px #ed3618; +} + +.search-result { + width:193px; + min-height:85px; + background-color:#242323; + position:relative; + float:left; + margin:10px 20px 10px 0px; + padding-bottom:5px; +} + +.search-connected { + background-color:#4c4c4c; + width:193px; + min-height:85px; + position:relative; + float:left; + margin:10px 20px 10px 0px; + padding-bottom:5px; + font-size:11px; + text-align:center; + vertical-align:middle; +} + +.search-connected a { + color:#B3DD15; +} + +.search-band-genres { + float:left; + width:40%; + font-size:10px; + margin-left:10px; + padding-right:5px; +} + +.search-result-name { + float:left; + font-size:12px; + margin-top:12px; + font-weight:bold; +} + +.search-result-location { + font-size:11px; + color:#D5E2E4; + font-weight:200; +} + +.avatar-small { + float:left; + padding:1px; + width:36px; + height:36px; + background-color:#ed3618; + margin:10px; + -webkit-border-radius:18px; + -moz-border-radius:18px; + border-radius:18px; +} + +.avatar-small img { + width: 36px; + height: 36px; + -webkit-border-radius:18px; + -moz-border-radius:18px; + border-radius:18px; +} \ No newline at end of file diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 4607e508a..301858051 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -76,4 +76,13 @@ class SessionsController < ApplicationController def failure end + + def connection_state + if (defined?(TEST_CONNECT_STATES) && TEST_CONNECT_STATES) || 'development'==Rails.env + @prefix = defined?(TEST_CONNECT_STATE_JS_LOG_PREFIX) ? TEST_CONNECT_STATE_JS_LOG_PREFIX : '*** ' + render('connection_state', :layout => 'client') && return + end + render :nothing => true, :status => 404 + end + end diff --git a/app/views/api_search/index.rabl b/app/views/api_search/index.rabl index ba13dce45..1bcb83003 100644 --- a/app/views/api_search/index.rabl +++ b/app/views/api_search/index.rabl @@ -8,13 +8,25 @@ end unless @search.musicians.nil? || @search.musicians.size == 0 child(:musicians => :musicians) { - attributes :id, :first_name, :last_name, :location, :photo_url + attributes :id, :first_name, :last_name, :name, :location, :photo_url + + node :is_friend do |musician| + musician.friends?(current_user) + end + + child :musician_instruments => :instruments do + attributes :instrument_id, :description, :proficiency_level, :priority + end } end unless @search.fans.nil? || @search.fans.size == 0 child(:fans => :fans) { - attributes :id, :first_name, :last_name, :location, :photo_url + attributes :id, :first_name, :last_name, :name, :location, :photo_url + + node :is_friend do |fan| + fan.friends?(current_user) + end } end @@ -26,6 +38,6 @@ end unless @search.friends.nil? || @search.friends.size == 0 child(:friends => :friends) { - attributes :id, :first_name, :last_name, :city, :state, :country, :email, :online, :photo_url, :musician + attributes :id, :first_name, :last_name, :name, :location, :email, :online, :photo_url, :musician } end diff --git a/app/views/api_users/friend_index.rabl b/app/views/api_users/friend_index.rabl index 511a0d127..40eb8fd7c 100644 --- a/app/views/api_users/friend_index.rabl +++ b/app/views/api_users/friend_index.rabl @@ -1,3 +1,3 @@ object @user.friends -attributes :id, :first_name, :last_name, :city, :state, :country, :email, :online, :photo_url \ No newline at end of file +attributes :id, :first_name, :last_name, :name, :location, :city, :state, :country, :email, :online, :photo_url \ No newline at end of file diff --git a/app/views/api_users/show.rabl b/app/views/api_users/show.rabl index 8d0831668..fdee4f67f 100644 --- a/app/views/api_users/show.rabl +++ b/app/views/api_users/show.rabl @@ -21,9 +21,6 @@ end unless @user.instruments.nil? || @user.instruments.size == 0 child :musician_instruments => :instruments do - attributes :description, :proficiency_level, :priority - node :instrument_id do |instrument| - instrument.instrument_id - end + attributes :description, :proficiency_level, :priority, :instrument_id end end \ No newline at end of file diff --git a/app/views/clients/_createSession.html.erb b/app/views/clients/_createSession.html.erb index 755253602..98f22b3aa 100644 --- a/app/views/clients/_createSession.html.erb +++ b/app/views/clients/_createSession.html.erb @@ -100,7 +100,7 @@
- +
diff --git a/app/views/clients/_findSession.html.erb b/app/views/clients/_findSession.html.erb index 37b149adb..754746fa0 100644 --- a/app/views/clients/_findSession.html.erb +++ b/app/views/clients/_findSession.html.erb @@ -38,7 +38,7 @@
REFRESH diff --git a/app/views/clients/_searchResults.html.erb b/app/views/clients/_searchResults.html.erb index bb5b3a10d..b58933925 100644 --- a/app/views/clients/_searchResults.html.erb +++ b/app/views/clients/_searchResults.html.erb @@ -1,19 +1,49 @@
-

search results

<%= render "screen_navigation" %>
-

Brian - fill this in from Jeff's mockups. Probably need templates for individual blocks

- -

+
+
for ""
+ +
+
+
+
+
- + + diff --git a/app/views/clients/_sidebar.html.erb b/app/views/clients/_sidebar.html.erb index 896a10565..6d4ed5f7b 100644 --- a/app/views/clients/_sidebar.html.erb +++ b/app/views/clients/_sidebar.html.erb @@ -26,67 +26,10 @@
-

friends
4

+

friends

-
    -
  • -
    - <%= image_tag "avatars/avatar_david.jpg" %> -
    -
    - David Wilson
    - - In Session, started at 12:34 pm - -
    -
    - <%= image_tag "shared/icon_session.png", :width=>24, :height=>24 %> -
    -
    -
  • - -
  • -
    - <%= image_tag "shared/avatar_creepyeye.jpg" %> -
    -
    - Brian Smith
    - Available -
    -
    -
    -
  • - -
  • -
    - <%= image_tag "shared/avatar_silverfox.jpg" %> -
    -
    - Peter Walker
    - - Recording: Seven Trails - -
    -
    - <%= image_tag "sidebar/icon_recording.png", :width=>24, :height=>24 %> -
    -
    -
  • - -
  • -
    - <%= image_tag "shared/avatar_saltnpepper.jpg" %> -
    -
    - Seth Call
    - - Offline - -
    -
    -
    -
  • +
@@ -222,3 +165,17 @@
+ + diff --git a/app/views/clients/index.html.erb b/app/views/clients/index.html.erb index 695b9ebd4..6f12e48d6 100644 --- a/app/views/clients/index.html.erb +++ b/app/views/clients/index.html.erb @@ -72,6 +72,9 @@ var header = new JK.Header(JK.app); header.initialize(); + var sidebar = new JK.Sidebar(JK.app); + sidebar.initialize(); + var homeScreen = new JK.HomeScreen(JK.app); homeScreen.initialize(); diff --git a/app/views/layouts/client.html.erb b/app/views/layouts/client.html.erb index 5a375af5b..cfbb629f8 100644 --- a/app/views/layouts/client.html.erb +++ b/app/views/layouts/client.html.erb @@ -23,6 +23,7 @@ <%= stylesheet_link_tag "client/createSession", media: "all" %> <%= stylesheet_link_tag "client/genreSelector", media: "all" %> <%= stylesheet_link_tag "client/sessionList", media: "all" %> + <%= stylesheet_link_tag "client/searchResults", media: "all" %> <%= include_gon %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> diff --git a/app/views/sessions/connection_state.html.erb b/app/views/sessions/connection_state.html.erb new file mode 100644 index 000000000..f3cfa8783 --- /dev/null +++ b/app/views/sessions/connection_state.html.erb @@ -0,0 +1,136 @@ +<% if Rails.env == "test" || Rails.env == "development" %> + +<% end %> diff --git a/config/application.rb b/config/application.rb index 87d524939..1e4c43294 100644 --- a/config/application.rb +++ b/config/application.rb @@ -83,15 +83,19 @@ module SampleApp # websocket-gateway configs - # Runs the websocket gateway within the web app - config.websocket_gateway_uri = "ws://localhost:6767/websocket" - # Websocket-gateway embedded configs config.websocket_gateway_enable = false - config.websocket_gateway_connect_time_stale = 30 - config.websocket_gateway_connect_time_expire = 180 + if Rails.env=='test' + config.websocket_gateway_connect_time_stale = 2 + config.websocket_gateway_connect_time_expire = 5 + else + config.websocket_gateway_connect_time_stale = 30 + config.websocket_gateway_connect_time_expire = 180 + end config.websocket_gateway_internal_debug = false config.websocket_gateway_port = 6767 + # Runs the websocket gateway within the web app + config.websocket_gateway_uri = "ws://localhost:#{config.websocket_gateway_port}/websocket" # set this to false if you want to disable signups (lock down public user creation) config.signup_enabled = true diff --git a/config/environments/development.rb b/config/environments/development.rb index 89da243ed..09f4a44d3 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -42,4 +42,6 @@ SampleApp::Application.configure do config.show_log_configuration = true config.websocket_gateway_enable = true + + TEST_CONNECT_STATES = false end diff --git a/config/environments/test.rb b/config/environments/test.rb index 76b10e199..2f64022c5 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -43,6 +43,13 @@ SampleApp::Application.configure do # For testing omniauth OmniAuth.config.test_mode = true - config.websocket_gateway_enable = false + TEST_CONNECT_STATES = false + if defined?(TEST_CONNECT_STATES) && TEST_CONNECT_STATES + TEST_CONNECT_STATE_JS_LOG_PREFIX = '*** ASSERT' + TEST_CONNECT_STATE_JS_CONSOLE = '/tmp/jam_connect_js.out' + config.websocket_gateway_enable = true + else + config.websocket_gateway_enable = false + end end diff --git a/config/routes.rb b/config/routes.rb index f8071feb3..d36bb7bb4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -33,6 +33,8 @@ SampleApp::Application.routes.draw do match '/confirm/:signup_token', to: 'users#signup_confirm', as: 'signup_confirm' + match '/test_connection', to: 'sessions#connection_state', :as => :connection_state + # password reset match '/request_reset_password' => 'users#request_reset_password', :via => :get match '/reset_password' => 'users#reset_password', :via => :post diff --git a/spec/features/connection_states_spec.rb b/spec/features/connection_states_spec.rb new file mode 100644 index 000000000..c8e0a468e --- /dev/null +++ b/spec/features/connection_states_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +if defined?(TEST_CONNECT_STATES) && TEST_CONNECT_STATES + describe "ConnectionStates", :js => true, :type => :feature, :capybara_feature => true do + + before(:all) do + Capybara.javascript_driver = :poltergeist + Capybara.current_driver = Capybara.javascript_driver + @user = FactoryGirl.create(:user) + end + + it "visits the connection_state test page and let it run its cycle", :js => true do + visit "/test_connection?user=#{@user.email}&password=foobar" + page.status_code.should be(200) + + # sleep for the duration of stale+expire delay to give browser time to run through the JS + sleep_dur = Rails.application.config.websocket_gateway_connect_time_stale + + Rails.application.config.websocket_gateway_connect_time_expire + # add 1 second each for stale and expire dur used in test_connection; plus 10% buffer + sleep_dur = (sleep_dur + 2) * 1.1 + $stdout.puts("*** sleeping for: #{sleep_dur} seconds to allow browser JS to run") + sleep(sleep_dur) + + # FIXME: The next step is to process the JS console output and raise assertions + # as appropriate; there is currently a database problem wherein inserted Connection records + # are not found after login; it's prolly an issue with db transactions, but will require more + # debugging to determine the cause. The connection row is created properly in the login process + # but when creating music_session, the connection is not found. + + File.exists?(TEST_CONNECT_STATE_JS_CONSOLE).should be_true + TEST_CONNECT_STATE_JS_CONSOLE_IO.flush + + jsfunctions = %W{ myLoggedIn createMusicSession isStale isExpired } + jsconsole = File.read(TEST_CONNECT_STATE_JS_CONSOLE) + jsconsole.split("\n").each do |line| + next unless line =~ /^#{Regexp.escape(TEST_CONNECT_STATE_JS_LOG_PREFIX)}/ + # $stdout.puts("*** console line = #{line}") + /ERROR/.match(line).should be_nil + + # FIXME: do more validation of console output here... + jsfunctions.delete_if { |fcn| line =~ /#{fcn}/ } + end + jsfunctions.count.should == 0 + end + + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 53aaff1fd..4a99c97a1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,7 +7,7 @@ require 'active_record' require 'action_mailer' require 'jam_db' require 'jam_ruby' -require 'spec_db' +require "#{File.dirname(__FILE__)}/spec_db" include JamRuby @@ -22,41 +22,57 @@ Spork.prefork do # Loading more in this block will cause your tests to run faster. However, # if you change any configuration or code from libraries loaded here, you'll # need to restart spork for it take effect. -# This file is copied to spec/ when you run 'rails generate rspec:install' -ENV["RAILS_ENV"] ||= 'test' -require File.expand_path("../../config/environment", __FILE__) -require 'rspec/rails' -require 'rspec/autorun' + # This file is copied to spec/ when you run 'rails generate rspec:install' + ENV["RAILS_ENV"] ||= 'test' + require File.expand_path("../../config/environment", __FILE__) + require 'rspec/rails' + require 'rspec/autorun' -# Requires supporting ruby files with custom matchers and macros, etc, -# in spec/support/ and its subdirectories. -Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} + require 'capybara' + require 'capybara/rspec' -ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base) + if defined?(TEST_CONNECT_STATES) && TEST_CONNECT_STATES + require 'capybara/poltergeist' + TEST_CONNECT_STATE_JS_CONSOLE_IO = File.open(TEST_CONNECT_STATE_JS_CONSOLE, 'w') + Capybara.register_driver :poltergeist do |app| + Capybara::Poltergeist::Driver.new(app, { phantomjs_logger: TEST_CONNECT_STATE_JS_CONSOLE_IO }) + end + Capybara.javascript_driver = :poltergeist + end -RSpec.configure do |config| - # ## Mock Framework - # - # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: - # - # config.mock_with :mocha - # config.mock_with :flexmock - # config.mock_with :rr - config.mock_with :rspec + # Requires supporting ruby files with custom matchers and macros, etc, + # in spec/support/ and its subdirectories. + Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} - # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures - config.fixture_path = "#{::Rails.root}/spec/fixtures" + ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base) - # If you're not using ActiveRecord, or you'd prefer not to run each of your - # examples within a transaction, remove the following line or assign false - # instead of true. - config.use_transactional_fixtures = true + RSpec.configure do |config| + # ## Mock Framework + # + # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line: + # + # config.mock_with :mocha + # config.mock_with :flexmock + # config.mock_with :rr + config.mock_with :rspec - # If true, the base class of anonymous controllers will be inferred - # automatically. This will be the default behavior in future versions of - # rspec-rails. - config.infer_base_class_for_anonymous_controllers = false -end + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + if defined?(TEST_CONNECT_STATES) && TEST_CONNECT_STATES + config.use_transactional_fixtures = false + else + config.use_transactional_fixtures = true + end + + # If true, the base class of anonymous controllers will be inferred + # automatically. This will be the default behavior in future versions of + # rspec-rails. + config.infer_base_class_for_anonymous_controllers = false + end end Spork.each_run do diff --git a/spec/support/shared_db_connection.rb b/spec/support/shared_db_connection.rb new file mode 100644 index 000000000..36494332e --- /dev/null +++ b/spec/support/shared_db_connection.rb @@ -0,0 +1,14 @@ +if defined?(TEST_CONNECT_STATES) && TEST_CONNECT_STATES + class ActiveRecord::Base + mattr_accessor :shared_connection + @@shared_connection = nil + + def self.connection + @@shared_connection || retrieve_connection + end + end + + # Forces all threads to share the same connection. This works on + # Capybara because it starts the web server in a thread. + ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection +end