Merge branch 'master' of bitbucket.org:jamkazam/jam-web

This commit is contained in:
Seth Call 2013-03-08 00:45:11 -06:00
commit 6bf0f499bd
37 changed files with 933 additions and 332 deletions

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -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);
};

View File

@ -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)

View File

@ -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"

View File

@ -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);
}

View File

@ -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

View File

@ -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 += '<img src="' + inst + '" width="24" height="24" />&nbsp;';
}
}
}
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);
}

View File

@ -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 = {

View File

@ -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,

View File

@ -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);

View File

@ -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);

View File

@ -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.

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -1,3 +1,3 @@
object @user.friends
attributes :id, :first_name, :last_name, :city, :state, :country, :email, :online, :photo_url
attributes :id, :first_name, :last_name, :name, :location, :city, :state, :country, :email, :online, :photo_url

View File

@ -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

View File

@ -100,7 +100,7 @@
<!-- friend invitation box -->
<div class="friendbox">
<div id="selected-friends"></div>
<input id="friend-input" type="text" value="Type a friend's name" />
<input id="friend-input" type="text" placeholder="Type a friend's name" />
</div>
<div class="mt35 mb15">

View File

@ -38,7 +38,7 @@
<!-- keyword filter -->
<div class="search-box">
<input id="session-keyword-srch" type="text" name="search" value="Search by Keyword" />
<input id="session-keyword-srch" type="text" name="search" placeholder="Search by Keyword" />
</div>
<div class="right mr10">
<a id="btn-refresh" href="#/findSession" style="text-decoration:none;" class="button-grey">REFRESH</a>

View File

@ -1,19 +1,49 @@
<!-- Search Results Screen -->
<div layout="screen" layout-id="searchResults" layout-arg="query" class="screen secondary">
<div class="content-head">
<!--
<div class="content-icon">
<%= image_tag "shared/icon_session.png", {:height => 19, :width => 19} %>
<a href="#"><%= image_tag "content/icon_search.png", :size => "19x19" %></a>
</div>
-->
<h1>search results</h1>
<%= render "screen_navigation" %>
</div>
<p>Brian - fill this in from Jeff's mockups. Probably need templates for individual blocks</p>
<!-- TODO remove me... just an example -->
<p id="show-query"></p>
<div class="search-result-header">
<div class="right mr30"><span id="result-count"></span> for "<span id="query"></span>"</div>
<div class="left ml35">
<a id="musician-search" href="#" class="search-nav active">MUSICIANS</a>
<a id="band-search" href="#" class="search-nav">BANDS</a>
<a id="fand-search" href="#" class="search-nav">FANS</a>
<a id="recording-search" href="#" class="search-nav">RECORDINGS</a>
</div>
</div>
<div class="content-scroller">
<div id="search-results" class="content-wrapper" style="padding-left:50px;">
</div>
</div>
</div>
<script type="text/template" id="template-searchResult">
<!-- fill in for a template for one search result with curly braces for replacement vars -->
<script type="text/template" id="template-search-result">
<div user-id="{userId}" class="search-result">
<a href="#" class="avatar-small"><img src="{avatar_url}" /></a>
<div class="search-result-name">{userName}<br />
<span class="search-result-location">{location}</span>
</div>
<br clear="left" />
<div id="instruments" class="left ml10 nowrap">
{instruments}
</div>
<a id="btn-connect-friend" class="button-orange smallbutton right">CONNECT</a>
</div>
</script>
<script type="text/template" id="template-invitation-sent">
<div user-id="{userId}" class="search-connected">
<div style="margin-top:10px;">
<br />
<img src="/assets/content/icon_goodquality.png" width="16" height="16" />
<br />
<b>Invitation Sent!</b><br />
<a href="{profile_url}">View {first_name}&quot;s Profile</a>
</div>
</div>
</script>

View File

@ -26,67 +26,10 @@
</div>
<div layout-panel="expanded" class="panel expanded">
<div layout-panel="header" class="panel-header">
<h2>friends<div class="badge">4</div></h2>
<h2>friends<div id='sidebar-friend-count' class="badge"></div></h2>
</div>
<div layout-panel="contents" class="panelcontents">
<ul>
<li>
<div class="avatar-small">
<%= image_tag "avatars/avatar_david.jpg" %>
</div>
<div class="friend-name">
David Wilson<br/>
<span class="friend-status">
<a href="#">In Session</a>, started at 12:34 pm
</span>
</div>
<div class="friend-icon">
<%= image_tag "shared/icon_session.png", :width=>24, :height=>24 %>
</div>
<br clear="all" />
</li>
<li>
<div class="avatar-small">
<%= image_tag "shared/avatar_creepyeye.jpg" %>
</div>
<div class="friend-name">
Brian Smith<br/>
<span class="friend-status">Available</span>
</div>
<div class="friend-icon"></div>
<br clear="all" />
</li>
<li>
<div class="avatar-small">
<%= image_tag "shared/avatar_silverfox.jpg" %>
</div>
<div class="friend-name">
Peter Walker<br/>
<span class="friend-status">
Recording: <a href="#">Seven Trails</a>
</span>
</div>
<div class="friend-icon">
<%= image_tag "sidebar/icon_recording.png", :width=>24, :height=>24 %>
</div>
<br clear="all" />
</li>
<li class="offline">
<div class="avatar-small">
<%= image_tag "shared/avatar_saltnpepper.jpg" %>
</div>
<div class="friend-name">
Seth Call<br/>
<span class="friend-status">
Offline
</span>
</div>
<div class="friend-icon"></div>
<br clear="all" />
</li>
<ul id="sidebar-friend-list">
</ul>
</div>
</div>
@ -222,3 +165,17 @@
</div>
</div>
</div>
<script type="text/template" id="template-friend-panel">
<li class="{cssClass}">
<div class="avatar-small"><img src="{avatar_url}" /></div>
<div class="friend-name">
{userName}<br/>
<span class="friend-status">
{status} {extra_info}
</span>
</div>
<div class="friend-icon">{info_image_url}</div>
<br clear="all" />
</li>
</script>

View File

@ -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();

View File

@ -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 %>

View File

@ -0,0 +1,136 @@
<% if Rails.env == "test" || Rails.env == "development" %>
<script type='text/javascript'>
$(document).ready(function() {
JK = JK || {};
JK.websocket_gateway_uri = '<%= Rails.application.config.websocket_gateway_uri %>'.replace(/localhost/,location.hostname);
if (!(window.jamClient)) { window.jamClient = new JK.FakeJamClient(); }
// override the onOpen to manage login; do this before connect()
JK.JamServer.onOpen = function() {
JK.logger.log("<%=@prefix%> connection_state: onOpen: logging in: "+'<%=params[:user]%> '+ ' <%=params[:password]%>');
if ($.cookie("remember_token")) {
JK.JamServer.rememberLogin();
<% if params[:user] && params[:password] %>
} else {
JK.JamServer.send(JK.MessageFactory.login_with_user_pass('<%=params[:user]%>', '<%=params[:password]%>'));
<% end %>
}
}
// setup server and app
JK.JamServer.connect();
JK.app = JK.JamKazam();
JK.app.initialize();
// disable normal heartbeat as we need to test without
JK.app.heartbeatActive = false;
// cache of the music_session_id created in the test
var music_session_id;
// checks all participants of a music_session for one with our client_id
function musicSessionContainsClientID(music_session) {
var pp = music_session['participants'];
for (ii=0; ii < pp.length; ii++) {
if (JK.app.client_id == pp[ii]['client_id']) return true;
}
return false;
}
// checks to see if the music_session has been deleted, as it should be after expire duration delay
function isExpired() {
// do a GET call and check for participant; confirm the participant is not there (based on client_id)
$.ajax({
type: 'GET',
dataType: 'json',
contentType: 'application/json',
url: '/api/sessions/'+music_session_id,
processData: false,
success: function(music_session) {
if (musicSessionContainsClientID(music_session)) {
// FIXME: report error; connection should be deleted
JK.logger.log("<%=@prefix%> connection_state: isExpired: ERROR: connection NOT deleted");
} else {
JK.logger.log("<%=@prefix%> connection_state: isExpired: SUCCESS: connection deleted");
}
},
error:function (xhr, ajaxOptions, thrownError){
if(xhr.status==404) {
JK.logger.log("<%=@prefix%> connection_state: isExpired: SUCCESS: 404");
}
}
});
}
// after waiting for the stale duration, is the connection still available? if ok, run isExpire after delay
function isStale() {
// do a GET call and check for participant; confirm the participant is there (based on client_id)
$.ajax({
type: 'GET',
dataType: 'json',
contentType: 'application/json',
url: '/api/sessions/'+music_session_id,
processData: false,
success: function(data) {
if (musicSessionContainsClientID(data)) {
JK.logger.log("<%=@prefix%> connection_state: isStale: connection was found as expected");
window.setTimeout(isExpired,
<%= (Rails.application.config.websocket_gateway_connect_time_expire + 1) * 1000 %>);
} else {
// FIXME: report error; connection should not be deleted
JK.logger.log("<%=@prefix%> connection_state: isStale: ERROR: connection was NOT FOUND");
}
},
error:function (xhr, ajaxOptions, thrownError){
if(xhr.status==404) {
// FIXME: report error
JK.logger.log("<%=@prefix%> connection_state: isStale: ERROR: 404");
}
}
});
}
function createMusicSession() {
var data = {
client_id: JK.app.client_id,
description: 'asdf',
as_musician: true,
legal_terms: true,
genres: ['classical'],
musician_access: true,
fan_chat: true,
fan_access: true,
approval_required: true,
tracks: [{instrument_id: 'electric guitar', sound: "mono"}]
}
JK.logger.log("<%=@prefix%> connection_state: createMusicSession: ... ");
$.ajax({
type: 'POST',
dataType: 'json',
contentType: 'application/json',
url: '/api/sessions/',
data: JSON.stringify(data),
processData: false,
success: function(music_session) {
music_session_id = music_session['id'];
JK.logger.log("<%=@prefix%> connection_state: createMusicSession: music_session_id = "+music_session_id);
window.setTimeout(isStale,
<%= (Rails.application.config.websocket_gateway_connect_time_stale + 1) * 1000 %>);
},
error:function (xhr, ajaxOptions, thrownError){
JK.logger.log("<%=@prefix%> connection_state: createMusicSession: ERROR: "+thrownError);
}
});
}
function myLoggedIn(header, payload) {
JK.logger.log("<%=@prefix%> connection_state: myLoggedIn: "+payload.client_id);
JK.app.client_id = payload.client_id;
$.cookie('client_id', payload.client_id);
JK.logger.log("<%=@prefix%> connection_state: cookie: "+$.cookie('client_id'));
$.cookie('remember_token', payload.token);
createMusicSession();
}
JK.JamServer.registerMessageCallback(JK.MessageType.LOGIN_ACK, myLoggedIn);
});
</script>
<% end %> <!-- Rails.env=='test'||'dev' -->

View File

@ -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

View File

@ -42,4 +42,6 @@ SampleApp::Application.configure do
config.show_log_configuration = true
config.websocket_gateway_enable = true
TEST_CONNECT_STATES = false
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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