* VRFS-1473 - Notification Highlighter - make obvious to user that they have new notifications complete

* VRFS-1523 - USER: Make "enter" key send message in new user-to-user messaging feature
This commit is contained in:
Seth Call 2014-03-27 18:43:15 +00:00
parent 526f6fe577
commit 53a6941ceb
19 changed files with 1161 additions and 798 deletions

View File

@ -472,8 +472,8 @@ message TestClientMessage {
// sent from client to server periodically to let server track if the client is truly alive and avoid TCP timeout scenarios // sent from client to server periodically to let server track if the client is truly alive and avoid TCP timeout scenarios
// the server will send a HeartbeatAck in response to this // the server will send a HeartbeatAck in response to this
message Heartbeat { message Heartbeat {
optional string notification_seen_at = 1; optional string notification_seen = 1;
optional string notification_seen_at = 2;
} }
// target: client // target: client

View File

@ -294,7 +294,28 @@ module JamRuby
# returns the # of new notifications # returns the # of new notifications
def new_notifications def new_notifications
Notification.select('id').where(target_user_id: id).where('created_at > ?', notification_seen_at).count search = Notification.select('id').where(target_user_id: self.id)
search = search.where('created_at > ?', self.notification_seen_at) if self.notification_seen_at
search.count
end
# the user can pass in a timestamp string, or the keyword 'LATEST'
# if LATEST is specified, we'll use the latest_notification as the timestamp
# if not, just use seen as-is
def update_notification_seen_at seen
new_latest_seen = nil
if seen == 'LATEST'
latest = self.latest_notification
new_latest_seen = latest.created_at if latest
else
new_latest_seen = seen
end
self.notification_seen_at = new_latest_seen
end
def latest_notification
Notification.select('created_at').where(target_user_id: id).limit(1).order('created_at DESC').first
end end
def confirm_email! def confirm_email!

View File

@ -99,9 +99,10 @@
}; };
// Heartbeat message // Heartbeat message
factory.heartbeat = function(notification_last_seen_at) { factory.heartbeat = function(lastNotificationSeen, lastNotificationSeenAt) {
var data = {}; var data = {};
data.notification_last_seen_at = notification_last_seen_at; data.notification_seen = lastNotificationSeen;
data.notification_seen_at = lastNotificationSeenAt;
return client_container(msg.HEARTBEAT, route_to.SERVER, data); return client_container(msg.HEARTBEAT, route_to.SERVER, data);
}; };

View File

@ -29,6 +29,7 @@
var heartbeatAckCheckInterval = null; var heartbeatAckCheckInterval = null;
var userDeferred = null; var userDeferred = null;
var notificationLastSeenAt = undefined; var notificationLastSeenAt = undefined;
var notificationLastSeen = undefined;
var opts = { var opts = {
inClient: true, // specify false if you want the app object but none of the client-oriented features inClient: true, // specify false if you want the app object but none of the client-oriented features
@ -92,9 +93,9 @@
function _heartbeat() { function _heartbeat() {
if (app.heartbeatActive) { if (app.heartbeatActive) {
var message = context.JK.MessageFactory.heartbeat(notificationLastSeen, notificationLastSeenAt);
var message = context.JK.MessageFactory.heartbeat(notificationLastSeenAt);
notificationLastSeenAt = undefined; notificationLastSeenAt = undefined;
notificationLastSeen = undefined;
context.JK.JamServer.send(message); context.JK.JamServer.send(message);
lastHeartbeatFound = false; lastHeartbeatFound = false;
} }
@ -386,7 +387,7 @@
return userDeferred; return userDeferred;
} }
this.updateNotificationSeen = function(notificationCreatedAt) { this.updateNotificationSeen = function(notificationId, notificationCreatedAt) {
var time = new Date(notificationCreatedAt); var time = new Date(notificationCreatedAt);
if(!notificationCreatedAt) { if(!notificationCreatedAt) {
@ -395,10 +396,12 @@
if(!notificationLastSeenAt) { if(!notificationLastSeenAt) {
notificationLastSeenAt = notificationCreatedAt; notificationLastSeenAt = notificationCreatedAt;
notificationLastSeen = notificationId;
logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt); logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
} }
else if(time.getTime() > new Date(notificationLastSeenAt).getTime()) { else if(time.getTime() > new Date(notificationLastSeenAt).getTime()) {
notificationLastSeenAt = notificationCreatedAt; notificationLastSeenAt = notificationCreatedAt;
notificationLastSeen = notificationId;
logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt); logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
} }
else { else {

View File

@ -251,7 +251,8 @@
if (!sidebarVisible) { if (!sidebarVisible) {
return; return;
} }
var $expandedPanelContents = $('[layout-id="' + expandedPanel + '"] [layout-panel="contents"]'); var $expandedPanel = $('[layout-id="' + expandedPanel + '"]');
var $expandedPanelContents = $expandedPanel.find('[layout-panel="contents"]');
var combinedHeaderHeight = $('[layout-panel="contents"]').length * opts.panelHeaderHeight; var combinedHeaderHeight = $('[layout-panel="contents"]').length * opts.panelHeaderHeight;
var searchHeight = $('.sidebar .search').first().height(); var searchHeight = $('.sidebar .search').first().height();
var expanderHeight = $('[layout-sidebar-expander]').height(); var expanderHeight = $('[layout-sidebar-expander]').height();
@ -259,6 +260,7 @@
$('[layout-panel="contents"]').hide(); $('[layout-panel="contents"]').hide();
$('[layout-panel="contents"]').css({"height": "1px"}); $('[layout-panel="contents"]').css({"height": "1px"});
$expandedPanelContents.show(); $expandedPanelContents.show();
$expandedPanel.triggerHandler('open')
$expandedPanelContents.animate({"height": expandedPanelHeight + "px"}, opts.animationDuration); $expandedPanelContents.animate({"height": expandedPanelHeight + "px"}, opts.animationDuration);
} }
@ -531,6 +533,25 @@
return openDialogs.length > 0; return openDialogs.length > 0;
} }
function currentDialog() {
if(openDialogs.length == 0) return null;
return openDialogs[openDialogs.length - 1];
}
// payload is a notification event from websocket gateway
function dialogObscuredNotification(payload) {
var openDialog = currentDialog();
if(!openDialog) return false;
if(typeof openDialog.handledNotification === 'function') {
return !openDialog.handledNotification(payload);
}
else {
return true;
}
}
/** /**
* Responsible for keeping N dialogs in correct stacked order, * Responsible for keeping N dialogs in correct stacked order,
* also moves the .dialog-overlay such that it hides/obscures all dialogs except the highest one * also moves the .dialog-overlay such that it hides/obscures all dialogs except the highest one
@ -854,6 +875,10 @@
showDialog(dialog, options); showDialog(dialog, options);
}; };
this.dialogObscuredNotification = function() {
return dialogObscuredNotification();
}
this.isDialogShowing = function() { this.isDialogShowing = function() {
return isDialogShowing(); return isDialogShowing();
} }

View File

@ -10,34 +10,38 @@
var missedNotificationsWhileAway = false; var missedNotificationsWhileAway = false;
var $panel = null; var $panel = null;
var $expanded = null; var $expanded = null;
var $contents = null;
var $count = null; var $count = null;
var $list = null;
var $notificationTemplate = null;
var darkenedColor = '#0D7B89'; var darkenedColor = '#0D7B89';
var highlightedColor = 'white' var highlightedColor = 'white'
var textMessageDialog = null;
var queuedNotification = null;
// one important limitation; if the user is focused on an iframe, this will be false var queuedNotificationCreatedAt = null;
// however, if they are doing something with Facebook or the photo picker, this may actually still be desirable
function userCanSeeNotifications() {
return document.hasFocus() || app.layout.isDialogShowing();
}
function isNotificationsPanelVisible() { function isNotificationsPanelVisible() {
return $expanded.is(':visible') return $contents.is(':visible')
} }
function incrementNotificationCount() { function incrementNotificationCount() {
var count = parseInt($count.text()); var count = parseInt($count.text());
$count.text(count + 1); setCount(count + 1);
} }
// set the element to white, and pulse it down to the un-highlighted value 2x, then set // set the element to white, and pulse it down to the un-highlighted value 2x, then set
function pulseToDark() { function pulseToDark() {
logger.debug("pulsing notification badge")
lowlightCount(); lowlightCount();
$count.pulse({'background-color' : highlightedColor}, {pulses: 2}, function() { $count.pulse({'background-color' : highlightedColor}, {pulses: 2}, function() {
$count.text('0'); $count.removeAttr('style')
setCount(0);
}) })
} }
function setCount(count) {
$count.text(count);
}
function lowlightCount() { function lowlightCount() {
$count.removeClass('highlighted'); $count.removeClass('highlighted');
@ -47,11 +51,35 @@
$count.addClass('highlighted'); $count.addClass('highlighted');
} }
function queueNotificationSeen(notificationId, notificationCreatedAt) {
var time = new Date(notificationCreatedAt);
if(!notificationCreatedAt) {
throw 'invalid value passed to queuedNotificationCreatedAt'
}
if(!queuedNotificationCreatedAt) {
queuedNotification = notificationId;
queuedNotificationCreatedAt = notificationCreatedAt;
logger.debug("updated queuedNotificationCreatedAt with: " + notificationCreatedAt);
}
else if(time.getTime() > new Date(queuedNotificationCreatedAt).getTime()) {
queuedNotification = notificationId;
queuedNotificationCreatedAt = notificationCreatedAt;
logger.debug("updated queuedNotificationCreatedAt with: " + notificationCreatedAt);
}
else {
logger.debug("ignored queuedNotificationCreatedAt for: " + notificationCreatedAt);
}
}
function onNotificationOccurred(payload) { function onNotificationOccurred(payload) {
if(userCanSeeNotifications()) { if(userCanSeeNotifications(payload)) {
app.updateNotificationSeen(payload.created_at); app.updateNotificationSeen(payload.notification_id, payload.created_at);
} }
else { else {
queueNotificationSeen(payload.notification_id, payload.created_at);
highlightCount(); highlightCount();
incrementNotificationCount(); incrementNotificationCount();
missedNotificationsWhileAway = true; missedNotificationsWhileAway = true;
@ -63,12 +91,28 @@
if(missedNotificationsWhileAway) { if(missedNotificationsWhileAway) {
// catch user's eye, then put count to 0 // catch user's eye, then put count to 0
pulseToDark(); pulseToDark();
if(queuedNotificationCreatedAt) {
app.updateNotificationSeen(queuedNotification, queuedNotificationCreatedAt);
}
} }
} }
queuedNotification = null;
queuedNotificationCreatedAt = null;
missedNotificationsWhileAway = false; missedNotificationsWhileAway = false;
} }
function opened() {
queuedNotification = null;
queuedNotificationCreatedAt = null;
rest.updateUser({notification_seen_at: 'LATEST'})
.done(function(response) {
lowlightCount();
setCount(0);
})
.fail(app.ajaxError)
}
function windowBlurred() { function windowBlurred() {
} }
@ -77,6 +121,42 @@
$(app.layout).on('dialog_closed', function(e, data) {if(data.dialogCount == 0) userCameBack(); }); $(app.layout).on('dialog_closed', function(e, data) {if(data.dialogCount == 0) userCameBack(); });
$(window).focus(userCameBack); $(window).focus(userCameBack);
$(window).blur(windowBlurred); $(window).blur(windowBlurred);
app.user()
.done(function(user) {
setCount(user.new_notifications);
if(user.new_notifications > 0) {
highlightCount();
}
});
$panel.on('open', opened);
// friend notifications
registerFriendRequest();
registerFriendRequestAccepted();
registerNewUserFollower();
registerNewBandFollower();
// session notifications
registerSessionInvitation();
registerSessionEnded();
registerJoinRequest();
registerJoinRequestApproved();
registerJoinRequestRejected();
registerMusicianSessionJoin();
registerBandSessionJoin();
// recording notifications
registerMusicianRecordingSaved();
registerBandRecordingSaved();
registerRecordingMasterMixComplete();
// band notifications
registerBandInvitation();
registerBandInvitationAccepted();
// register text messages
registerTextMessage();
} }
function populate() { function populate() {
@ -88,20 +168,736 @@
.fail(app.ajaxError) .fail(app.ajaxError)
} }
function initialize(sidebar) { function updateNotificationList(response) {
$list.empty();
$.each(response, function(index, val) {
if(val.description == 'TEXT_MESSAGE') {
val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html();
}
// fill in template for Connect pre-click
var template = $notificationTemplate.html();
var notificationHtml = context.JK.fillTemplate(template, {
notificationId: val.notification_id,
sessionId: val.session_id,
avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
text: val.formatted_msg,
date: $.timeago(val.created_at)
});
$list.append(notificationHtml);
// val.description contains the notification record's description value from the DB (i.e., type)
initializeActions(val, val.description);
});
}
function initializeActions(payload, type) {
var $notification = $('li[notification-id=' + payload.notification_id + ']');
var $btnNotificationAction = '#btn-notification-action';
// wire up "x" button to delete notification
$notification.find('#img-delete-notification').click(deleteNotificationHandler);
// customize action buttons based on notification type
if (type === context.JK.MessageType.FRIEND_REQUEST) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('ACCEPT');
$action_btn.click(function() {
acceptFriendRequest(payload);
});
}
else if (type === context.JK.MessageType.FRIEND_REQUEST_ACCEPTED) {
$notification.find('#div-actions').hide();
}
else if (type === context.JK.MessageType.NEW_USER_FOLLOWER || type === context.JK.MessageType.NEW_BAND_FOLLOWER) {
$notification.find('#div-actions').hide();
}
else if (type === context.JK.MessageType.SESSION_INVITATION) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('JOIN');
$action_btn.click(function() {
openTerms(payload);
});
}
else if (type === context.JK.MessageType.JOIN_REQUEST) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('APPROVE');
$action_btn.click(function() {
approveJoinRequest(payload);
});
}
else if (type === context.JK.MessageType.JOIN_REQUEST_APPROVED) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('JOIN');
$action_btn.click(function() {
openTerms(payload);
});
}
else if (type === context.JK.MessageType.JOIN_REQUEST_REJECTED) {
$notification.find('#div-actions').hide();
}
else if (type === context.JK.MessageType.MUSICIAN_SESSION_JOIN || type === context.JK.MessageType.BAND_SESSION_JOIN) {
var actionText = '';
var callback;
if (context.JK.currentUserMusician) {
// user is MUSICIAN; musician_access = TRUE
if (payload.musician_access) {
actionText = "JOIN";
callback = joinSession;
}
// user is MUSICIAN; fan_access = TRUE
else if (payload.fan_access) {
actionText = "LISTEN";
callback = listenToSession;
}
}
else {
// user is FAN; fan_access = TRUE
if (payload.fan_access) {
actionText = "LISTEN";
callback = listenToSession;
}
}
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text(actionText);
$action_btn.click(function() {
callback(payload);
});
}
else if (type === context.JK.MessageType.MUSICIAN_RECORDING_SAVED || type === context.JK.MessageType.BAND_RECORDING_SAVED) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('LISTEN');
$action_btn.click(function() {
listenToRecording(payload);
});
}
else if (type === context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE) {
$notification.find('#div-actions').hide();
context.jamClient.OnDownloadAvailable(); // poke backend, letting it know a download is available
}
else if (type === context.JK.MessageType.BAND_INVITATION) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('ACCEPT');
$action_btn.click(function() {
acceptBandInvitation(payload);
});
}
else if (type === context.JK.MessageType.BAND_INVITATION_ACCEPTED) {
$notification.find('#div-actions').hide();
}
else if (type === context.JK.MessageType.TEXT_MESSAGE) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('REPLY');
$action_btn.click(function() {
var userId = $notification.find('.more-text-available').attr('data-sender-id');
app.layout.showDialog('text-message', { d1: userId });
});
var moreTextLink = $notification.find('.more-text-available');
var textMessage = $notification.find('.text-message');
var clipped_msg = textMessage.attr('data-is-clipped') === 'true';
if(clipped_msg) {
moreTextLink.text('more').show();
moreTextLink.click(function(e) {
var userId = $(this).attr('data-sender-id');
return false;
});
}
else {
moreTextLink.hide();
}
}
}
function acceptBandInvitation(args) {
rest.updateBandInvitation(
args.band_id,
args.band_invitation_id,
true
).done(function(response) {
deleteNotification(args.notification_id); // delete notification corresponding to this friend request
}).error(app.ajaxError);
}
function deleteNotification(notificationId) {
var url = "/api/users/" + context.JK.currentUserId + "/notifications/" + notificationId;
$.ajax({
type: "DELETE",
dataType: "json",
contentType: 'application/json',
url: url,
processData: false,
success: function(response) {
$('li[notification-id=' + notificationId + ']').hide();
//decrementNotificationCount();
},
error: app.ajaxError
});
}
function listenToSession(args) {
deleteNotification(args.notification_id);
context.JK.popExternalLink('/sessions/' + args.session_id);
}
/*********** TODO: THE NEXT 3 FUNCTIONS ARE COPIED FROM sessionList.js. REFACTOR TO COMMON PLACE. *************/
function joinSession(args) {
// NOTE: invited musicians get their own notification, so no need to check if user has invitation here
// like other places because an invited user would never get this notification
if (args.musician_access) {
if (args.approval_required) {
openAlert(args.session_id);
}
else {
openTerms(args);
}
}
deleteNotification(args.notification_id);
}
function registerJoinRequestApproved() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST_APPROVED, function(header, payload) {
logger.debug("Handling JOIN_REQUEST_APPROVED msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Join Request Approved",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "JOIN SESSION",
"ok_callback": openTerms,
"ok_callback_args": { "session_id": payload.session_id, "notification_id": payload.notification_id }
});
});
}
function registerJoinRequestRejected() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST_REJECTED, function(header, payload) {
logger.debug("Handling JOIN_REQUEST_REJECTED msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Join Request Rejected",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
});
});
}
function registerJoinRequest() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST, function(header, payload) {
logger.debug("Handling JOIN_REQUEST msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "New Join Request",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "APPROVE",
"ok_callback": approveJoinRequest,
"ok_callback_args": { "join_request_id": payload.join_request_id, "notification_id": payload.notification_id },
"cancel_text": "REJECT",
"cancel_callback": rejectJoinRequest,
"cancel_callback_args": { "join_request_id": payload.join_request_id, "notification_id": payload.notification_id }
});
});
}
function registerFriendRequestAccepted() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_REQUEST_ACCEPTED, function(header, payload) {
logger.debug("Handling FRIEND_REQUEST_ACCEPTED msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
initializeFriendsPanel();
app.notify({
"title": "Friend Request Accepted",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
});
});
}
function registerNewUserFollower() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.NEW_USER_FOLLOWER, function(header, payload) {
logger.debug("Handling NEW_USER_FOLLOWER msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "New Follower",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
});
});
}
function registerNewBandFollower() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.NEW_BAND_FOLLOWER, function(header, payload) {
logger.debug("Handling NEW_BAND_FOLLOWER msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "New Band Follower",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
});
});
}
function registerFriendRequest() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_REQUEST, function(header, payload) {
logger.debug("Handling FRIEND_REQUEST msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "New Friend Request",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "ACCEPT",
"ok_callback": acceptFriendRequest,
"ok_callback_args": { "friend_request_id": payload.friend_request_id, "notification_id": payload.notification_id }
});
});
}
function acceptFriendRequest(args) {
rest.acceptFriendRequest({
status: 'accept',
friend_request_id: args.friend_request_id
}).done(function(response) {
deleteNotification(args.notification_id); // delete notification corresponding to this friend request
initializeFriendsPanel(); // refresh friends panel when request is accepted
}).error(app.ajaxError);
}
function registerSessionEnded() {
// TODO: this should clean up all notifications related to this session
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_ENDED, function(header, payload) {
logger.debug("Handling SESSION_ENDED msg " + JSON.stringify(payload));
deleteSessionNotifications(payload.session_id);
});
}
// remove all notifications for this session
function deleteSessionNotifications(sessionId) {
$('li[session-id=' + sessionId + ']').hide();
//decrementNotificationCount();
}
function registerSessionInvitation() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_INVITATION, function(header, payload) {
logger.debug("Handling SESSION_INVITATION msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
var participants = [];
rest.getSession(payload.session_id).done(function(response) {
$.each(response.participants, function(index, val) {
participants.push({"photo_url": context.JK.resolveAvatarUrl(val.user.photo_url), "name": val.user.name});
});
var participantHtml = "You have been invited to join a session with: <br/><br/>";
participantHtml += "<table><tbody>";
$.each(participants, function(index, val) {
if (index < 4) {
participantHtml += "<tr><td><img class='avatar-small' src='" + context.JK.resolveAvatarUrl(val.photo_url) + "' /></td><td>" + val.name + "</td></tr>";
}
});
participantHtml += "</tbody></table>";
app.notify({
"title": "Session Invitation",
"text": participantHtml
}, {
"ok_text": "JOIN SESSION",
"ok_callback": openTerms,
"ok_callback_args": { "session_id": payload.session_id, "notification_id": payload.notification_id }
});
}).error(app.ajaxError);
});
}
function registerMusicianSessionJoin() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, function(header, payload) {
logger.debug("Handling MUSICIAN_SESSION_JOIN msg " + JSON.stringify(payload));
var okText = '';
var showNotification = false;
var callback;
if (context.JK.currentUserMusician) {
// user is MUSICIAN; musician_access = TRUE
if (payload.musician_access) {
showNotification = true;
okText = "JOIN";
callback = joinSession;
}
// user is MUSICIAN; fan_access = TRUE
else if (payload.fan_access) {
showNotification = true;
okText = "LISTEN";
callback = listenToSession;
}
}
else {
// user is FAN; fan_access = TRUE
if (payload.fan_access) {
showNotification = true;
okText = "LISTEN";
callback = listenToSession;
}
}
if (showNotification) {
handleNotification(payload, header.type);
app.notify({
"title": "Musician Joined Session",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": okText,
"ok_callback": callback,
"ok_callback_args": {
"session_id": payload.session_id,
"fan_access": payload.fan_access,
"musician_access": payload.musician_access,
"approval_required": payload.approval_required,
"notification_id": payload.notification_id
}
}
);
}
});
}
function registerBandSessionJoin() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_SESSION_JOIN, function(header, payload) {
logger.debug("Handling BAND_SESSION_JOIN msg " + JSON.stringify(payload));
var okText = '';
var showNotification = false;
var callback;
if (context.JK.currentUserMusician) {
// user is MUSICIAN; musician_access = TRUE
if (payload.musician_access) {
showNotification = true;
okText = "JOIN";
callback = joinSession;
}
// user is MUSICIAN; fan_access = TRUE
else if (payload.fan_access) {
showNotification = true;
okText = "LISTEN";
callback = listenToSession;
}
}
else {
// user is FAN; fan_access = TRUE
if (payload.fan_access) {
showNotification = true;
okText = "LISTEN";
callback = listenToSession;
}
}
if (showNotification) {
handleNotification(payload, header.type);
app.notify({
"title": "Band Joined Session",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "LISTEN",
"ok_callback": callback,
"ok_callback_args": {
"session_id": payload.session_id,
"fan_access": payload.fan_access,
"musician_access": payload.musician_access,
"approval_required": payload.approval_required,
"notification_id": payload.notification_id
}
}
);
}
});
}
function registerMusicianRecordingSaved() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_RECORDING_SAVED, function(header, payload) {
logger.debug("Handling MUSICIAN_RECORDING_SAVED msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Musician Recording Saved",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "LISTEN",
"ok_callback": listenToRecording,
"ok_callback_args": {
"recording_id": payload.recording_id,
"notification_id": payload.notification_id
}
});
});
}
function registerBandRecordingSaved() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_RECORDING_SAVED, function(header, payload) {
logger.debug("Handling BAND_RECORDING_SAVED msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Band Recording Saved",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "LISTEN",
"ok_callback": listenToRecording,
"ok_callback_args": {
"recording_id": payload.recording_id,
"notification_id": payload.notification_id
}
});
});
}
function registerRecordingMasterMixComplete() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE, function(header, payload) {
logger.debug("Handling RECORDING_MASTER_MIX_COMPLETE msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Recording Master Mix Complete",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "SHARE",
"ok_callback": shareRecording,
"ok_callback_args": {
"recording_id": payload.recording_id
}
});
});
}
function shareRecording(args) {
var recordingId = args.recording_id;
}
function registerBandInvitation() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION, function(header, payload) {
logger.debug("Handling BAND_INVITATION msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Band Invitation",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "ACCEPT",
"ok_callback": acceptBandInvitation,
"ok_callback_args": {
"band_invitation_id": payload.band_invitation_id,
"band_id": payload.band_id,
"notification_id": payload.notification_id
}
});
});
}
function registerTextMessage() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TEXT_MESSAGE, function(header, payload) {
logger.debug("Handling TEXT_MESSAGE msg " + JSON.stringify(payload));
textMessageDialog.messageReceived(payload);
handleNotification(payload, header.type);
});
}
function registerBandInvitationAccepted() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION_ACCEPTED, function(header, payload) {
logger.debug("Handling BAND_INVITATION_ACCEPTED msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Band Invitation Accepted",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
});
});
}
// one important limitation; if the user is focused on an iframe, this will be false
// however, if they are doing something with Facebook or the photo picker, this may actually still be desirable
function userCanSeeNotifications(payload) {
return document.hasFocus() && !app.layout.dialogObscuredNotification(payload);
}
// default handler for incoming notification
function handleNotification(payload, type) {
// on a load of notifications, it is possible to load a very new notification,
// and get a websocket notification right after for that same notification,
// so we need to protect against such duplicates
if($list.find('li[notification-id="' + payload.notification_id + '"]').length > 0) {
return false;
}
// add notification to sidebar
var template = $notificationTemplate.html();
var notificationHtml = context.JK.fillTemplate(template, {
notificationId: payload.notification_id,
sessionId: payload.session_id,
avatar_url: context.JK.resolveAvatarUrl(payload.photo_url),
text: payload.msg instanceof jQuery ? payload.msg.html() : payload.msg ,
date: $.timeago(payload.created_at)
});
$list.prepend(notificationHtml);
onNotificationOccurred(payload);
initializeActions(payload, type);
return true;
}
function onCreateJoinRequest(sessionId) {
var joinRequest = {};
joinRequest.music_session = sessionId;
joinRequest.user = context.JK.currentUserId;
rest.createJoinRequest(joinRequest)
.done(function(response) {
}).error(context.JK.app.ajaxError);
context.JK.app.layout.closeDialog('alert');
}
function approveJoinRequest(args) {
rest.updateJoinRequest(args.join_request_id, true)
.done(function(response) {
deleteNotification(args.notification_id);
}).error(app.ajaxError);
}
function rejectJoinRequest(args) {
rest.updateJoinRequest(args.join_request_id, false)
.done(function(response) {
deleteNotification(args.notification_id);
}).error(app.ajaxError);
}
function openTerms(args) {
var termsDialog = new context.JK.TermsDialog(app, args, onTermsAccepted);
termsDialog.initialize();
app.layout.showDialog('terms');
}
function onTermsAccepted(args) {
deleteNotification(args.notification_id);
context.location = '/client#/session/' + args.session_id;
}
function openAlert(sessionId) {
var alertDialog = new context.JK.AlertDialog(context.JK.app, "YES",
"You must be approved to join this session. Would you like to send a request to join?",
sessionId, onCreateJoinRequest);
alertDialog.initialize();
context.JK.app.layout.showDialog('alert');
}
function listenToRecording(args) {
deleteNotification(args.notification_id);
context.JK.popExternalLink('/recordings/' + args.recording_id);
}
function deleteNotificationHandler(evt) {
evt.stopPropagation();
var notificationId = $(this).attr('notification-id');
deleteNotification(notificationId);
}
function initialize(sidebar, textMessageDialogInstance) {
textMessageDialog = textMessageDialogInstance;
$panel = $('[layout-id="panelNotifications"]'); $panel = $('[layout-id="panelNotifications"]');
$expanded = $panel.find('.panel.expanded'); $expanded = $panel.find('.panel.expanded');
$contents = $panel.find('.panelcontents');
$count = $panel.find('#sidebar-notification-count'); $count = $panel.find('#sidebar-notification-count');
$list = $panel.find('#sidebar-notification-list');
$notificationTemplate = $('#template-notification-panel');
if($panel.length == 0) throw "notifications panel not found" if($panel.length == 0) throw "notifications panel not found"
if($expanded.length == 0) throw "notifications expanded content not found" if($expanded.length == 0) throw "notifications expanded content not found"
if($contents.length == 0) throw "notifications contents not found"
if($count.length == 0) throw "notifications count element not found"; if($count.length == 0) throw "notifications count element not found";
if($list.length == 0) throw "notification list element not found";
if($notificationTemplate.length == 0) throw "notification template not found";
events(); events();
populate(); populate();
}; };
this.initiliaze = initialize; this.initialize = initialize;
this.onNotificationOccurred = onNotificationOccurred; this.onNotificationOccurred = onNotificationOccurred;
}; };
})(window, jQuery); })(window, jQuery);

View File

@ -10,6 +10,7 @@
var invitationDialog = null; var invitationDialog = null;
var textMessageDialog = null; var textMessageDialog = null;
var notificationPanel = null; var notificationPanel = null;
var me = null;
function initializeSearchPanel() { function initializeSearchPanel() {
$('#search_text_type').change(function() { $('#search_text_type').change(function() {
@ -118,188 +119,7 @@
function initializeNotificationsPanel() { function initializeNotificationsPanel() {
notificationPanel = new context.JK.NotificationPanel(app); notificationPanel = new context.JK.NotificationPanel(app);
notificationPanel.initialize(); notificationPanel.initialize(me, textMessageDialog);
}
function updateNotificationList(response) {
$('#sidebar-notification-list').empty();
$.each(response, function(index, val) {
if(val.description == 'TEXT_MESSAGE') {
val.formatted_msg = textMessageDialog.formatTextMessage(val.message.substring(0, 200), val.source_user_id, val.source_user.name, val.message.length > 200).html();
}
// fill in template for Connect pre-click
var template = $('#template-notification-panel').html();
var notificationHtml = context.JK.fillTemplate(template, {
notificationId: val.notification_id,
sessionId: val.session_id,
avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
text: val.formatted_msg,
date: $.timeago(val.created_at)
});
$('#sidebar-notification-list').append(notificationHtml);
// val.description contains the notification record's description value from the DB (i.e., type)
initializeActions(val, val.description);
});
}
function initializeActions(payload, type) {
var $notification = $('li[notification-id=' + payload.notification_id + ']');
var $btnNotificationAction = '#btn-notification-action';
// wire up "x" button to delete notification
$notification.find('#img-delete-notification').click(deleteNotificationHandler);
// customize action buttons based on notification type
if (type === context.JK.MessageType.FRIEND_REQUEST) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('ACCEPT');
$action_btn.click(function() {
acceptFriendRequest(payload);
});
}
else if (type === context.JK.MessageType.FRIEND_REQUEST_ACCEPTED) {
$notification.find('#div-actions').hide();
}
else if (type === context.JK.MessageType.NEW_USER_FOLLOWER || type === context.JK.MessageType.NEW_BAND_FOLLOWER) {
$notification.find('#div-actions').hide();
}
else if (type === context.JK.MessageType.SESSION_INVITATION) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('JOIN');
$action_btn.click(function() {
openTerms(payload);
});
}
else if (type === context.JK.MessageType.JOIN_REQUEST) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('APPROVE');
$action_btn.click(function() {
approveJoinRequest(payload);
});
}
else if (type === context.JK.MessageType.JOIN_REQUEST_APPROVED) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('JOIN');
$action_btn.click(function() {
openTerms(payload);
});
}
else if (type === context.JK.MessageType.JOIN_REQUEST_REJECTED) {
$notification.find('#div-actions').hide();
}
else if (type === context.JK.MessageType.MUSICIAN_SESSION_JOIN || type === context.JK.MessageType.BAND_SESSION_JOIN) {
var actionText = '';
var callback;
if (context.JK.currentUserMusician) {
// user is MUSICIAN; musician_access = TRUE
if (payload.musician_access) {
actionText = "JOIN";
callback = joinSession;
}
// user is MUSICIAN; fan_access = TRUE
else if (payload.fan_access) {
actionText = "LISTEN";
callback = listenToSession;
}
}
else {
// user is FAN; fan_access = TRUE
if (payload.fan_access) {
actionText = "LISTEN";
callback = listenToSession;
}
}
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text(actionText);
$action_btn.click(function() {
callback(payload);
});
}
else if (type === context.JK.MessageType.MUSICIAN_RECORDING_SAVED || type === context.JK.MessageType.BAND_RECORDING_SAVED) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('LISTEN');
$action_btn.click(function() {
listenToRecording(payload);
});
}
else if (type === context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE) {
$notification.find('#div-actions').hide();
context.jamClient.OnDownloadAvailable(); // poke backend, letting it know a download is available
}
else if (type === context.JK.MessageType.BAND_INVITATION) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('ACCEPT');
$action_btn.click(function() {
acceptBandInvitation(payload);
});
}
else if (type === context.JK.MessageType.BAND_INVITATION_ACCEPTED) {
$notification.find('#div-actions').hide();
}
else if (type === context.JK.MessageType.TEXT_MESSAGE) {
var $action_btn = $notification.find($btnNotificationAction);
$action_btn.text('REPLY');
$action_btn.click(function() {
var userId = $notification.find('.more-text-available').attr('data-sender-id');
app.layout.showDialog('text-message', { d1: userId });
});
var moreTextLink = $notification.find('.more-text-available');
var textMessage = $notification.find('.text-message');
var clipped_msg = textMessage.attr('data-is-clipped') === 'true';
if(clipped_msg) {
moreTextLink.text('more').show();
moreTextLink.click(function(e) {
var userId = $(this).attr('data-sender-id');
return false;
});
}
else {
moreTextLink.hide();
}
}
}
function deleteNotificationHandler(evt) {
evt.stopPropagation();
var notificationId = $(this).attr('notification-id');
deleteNotification(notificationId);
}
function deleteNotification(notificationId) {
var url = "/api/users/" + context.JK.currentUserId + "/notifications/" + notificationId;
$.ajax({
type: "DELETE",
dataType: "json",
contentType: 'application/json',
url: url,
processData: false,
success: function(response) {
$('li[notification-id=' + notificationId + ']').hide();
decrementNotificationCount();
},
error: app.ajaxError
});
} }
function initializeChatPanel() { function initializeChatPanel() {
@ -348,61 +168,6 @@
$('#sidebar-search-results').height('0px'); $('#sidebar-search-results').height('0px');
} }
function decrementNotificationCount() {
/**
var count = parseInt($('#sidebar-notification-count').html());
if (count === 0) {
$('#sidebar-notification-count').html(0);
}
else {
$('#sidebar-notification-count').html(count - 1);
}
*/
}
// one important limitation; if the user is focused on an iframe, this will be false
// however, if they are doing something with Facebook or the photo picker, this may actually still be desirable
function userCanSeeNotifications() {
return document.hasFocus() || app.layout.isDialogShowing();
}
// default handler for incoming notification
function handleNotification(payload, type) {
// on a load of notifications, it is possible to load a very new notification,
// and get a websocket notification right after for that same notification,
// so we need to protect against such duplicates
if($('#sidebar-notification-list').find('li[notification-id="' + payload.notification_id + '"]').length > 0) {
return false;
}
// increment displayed notification count
incrementNotificationCount();
// add notification to sidebar
var template = $("#template-notification-panel").html();
var notificationHtml = context.JK.fillTemplate(template, {
notificationId: payload.notification_id,
sessionId: payload.session_id,
avatar_url: context.JK.resolveAvatarUrl(payload.photo_url),
text: payload.msg instanceof jQuery ? payload.msg.html() : payload.msg ,
date: $.timeago(payload.created_at)
});
$('#sidebar-notification-list').prepend(notificationHtml);
if(userCanSeeNotifications()) {
app.updateNotificationSeen(payload.created_at);
}
else {
}
initializeActions(payload, type);
return true;
}
var delay = (function(){ var delay = (function(){
var timer = 0; var timer = 0;
return function(callback, ms) { return function(callback, ms) {
@ -460,32 +225,14 @@
// friend notifications // friend notifications
registerFriendUpdate(); registerFriendUpdate();
registerFriendRequest();
registerFriendRequestAccepted();
registerNewUserFollower();
registerNewBandFollower();
// session invitations // session invitations
registerSessionInvitation();
registerSessionEnded();
registerJoinRequest();
registerJoinRequestApproved();
registerJoinRequestRejected();
registerSessionJoin(); registerSessionJoin();
registerSessionDepart(); registerSessionDepart();
registerMusicianSessionJoin();
registerBandSessionJoin();
// recording notifications // recording notifications
registerMusicianRecordingSaved();
registerBandRecordingSaved();
registerRecordingStarted(); registerRecordingStarted();
registerRecordingEnded(); registerRecordingEnded();
registerRecordingMasterMixComplete();
// band notifications
registerBandInvitation();
registerBandInvitationAccepted();
// broadcast notifications // broadcast notifications
registerSourceUpRequested(); registerSourceUpRequested();
@ -493,9 +240,6 @@
registerSourceUp(); registerSourceUp();
registerSourceDown(); registerSourceDown();
// register text messages
registerTextMessage();
// watch for Invite More Users events // watch for Invite More Users events
$('#sidebar-div .btn-email-invitation').click(function() { $('#sidebar-div .btn-email-invitation').click(function() {
invitationDialog.showEmailDialog(); invitationDialog.showEmailDialog();
@ -529,210 +273,6 @@
}); });
} }
function registerFriendRequest() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_REQUEST, function(header, payload) {
logger.debug("Handling FRIEND_REQUEST msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "New Friend Request",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "ACCEPT",
"ok_callback": acceptFriendRequest,
"ok_callback_args": { "friend_request_id": payload.friend_request_id, "notification_id": payload.notification_id }
});
});
}
function acceptFriendRequest(args) {
rest.acceptFriendRequest({
status: 'accept',
friend_request_id: args.friend_request_id
}).done(function(response) {
deleteNotification(args.notification_id); // delete notification corresponding to this friend request
initializeFriendsPanel(); // refresh friends panel when request is accepted
}).error(app.ajaxError);
}
function registerFriendRequestAccepted() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_REQUEST_ACCEPTED, function(header, payload) {
logger.debug("Handling FRIEND_REQUEST_ACCEPTED msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
initializeFriendsPanel();
app.notify({
"title": "Friend Request Accepted",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
});
});
}
function registerNewUserFollower() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.NEW_USER_FOLLOWER, function(header, payload) {
logger.debug("Handling NEW_USER_FOLLOWER msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "New Follower",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
});
});
}
function registerNewBandFollower() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.NEW_BAND_FOLLOWER, function(header, payload) {
logger.debug("Handling NEW_BAND_FOLLOWER msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "New Band Follower",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
});
});
}
function registerSessionInvitation() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_INVITATION, function(header, payload) {
logger.debug("Handling SESSION_INVITATION msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
var participants = [];
rest.getSession(payload.session_id).done(function(response) {
$.each(response.participants, function(index, val) {
participants.push({"photo_url": context.JK.resolveAvatarUrl(val.user.photo_url), "name": val.user.name});
});
var participantHtml = "You have been invited to join a session with: <br/><br/>";
participantHtml += "<table><tbody>";
$.each(participants, function(index, val) {
if (index < 4) {
participantHtml += "<tr><td><img class='avatar-small' src='" + context.JK.resolveAvatarUrl(val.photo_url) + "' /></td><td>" + val.name + "</td></tr>";
}
});
participantHtml += "</tbody></table>";
app.notify({
"title": "Session Invitation",
"text": participantHtml
}, {
"ok_text": "JOIN SESSION",
"ok_callback": openTerms,
"ok_callback_args": { "session_id": payload.session_id, "notification_id": payload.notification_id }
});
}).error(app.ajaxError);
});
}
function openTerms(args) {
var termsDialog = new context.JK.TermsDialog(app, args, onTermsAccepted);
termsDialog.initialize();
app.layout.showDialog('terms');
}
function onTermsAccepted(args) {
deleteNotification(args.notification_id);
context.location = '/client#/session/' + args.session_id;
}
function registerSessionEnded() {
// TODO: this should clean up all notifications related to this session
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_ENDED, function(header, payload) {
logger.debug("Handling SESSION_ENDED msg " + JSON.stringify(payload));
deleteSessionNotifications(payload.session_id);
});
}
// remove all notifications for this session
function deleteSessionNotifications(sessionId) {
$('li[session-id=' + sessionId + ']').hide();
decrementNotificationCount();
}
function registerJoinRequest() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST, function(header, payload) {
logger.debug("Handling JOIN_REQUEST msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "New Join Request",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "APPROVE",
"ok_callback": approveJoinRequest,
"ok_callback_args": { "join_request_id": payload.join_request_id, "notification_id": payload.notification_id },
"cancel_text": "REJECT",
"cancel_callback": rejectJoinRequest,
"cancel_callback_args": { "join_request_id": payload.join_request_id, "notification_id": payload.notification_id }
});
});
}
function approveJoinRequest(args) {
rest.updateJoinRequest(args.join_request_id, true)
.done(function(response) {
deleteNotification(args.notification_id);
}).error(app.ajaxError);
}
function rejectJoinRequest(args) {
rest.updateJoinRequest(args.join_request_id, false)
.done(function(response) {
deleteNotification(args.notification_id);
}).error(app.ajaxError);
}
function registerJoinRequestApproved() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST_APPROVED, function(header, payload) {
logger.debug("Handling JOIN_REQUEST_APPROVED msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Join Request Approved",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "JOIN SESSION",
"ok_callback": openTerms,
"ok_callback_args": { "session_id": payload.session_id, "notification_id": payload.notification_id }
});
});
}
function registerJoinRequestRejected() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.JOIN_REQUEST_REJECTED, function(header, payload) {
logger.debug("Handling JOIN_REQUEST_REJECTED msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Join Request Rejected",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
});
});
}
function registerSessionJoin() { function registerSessionJoin() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, function(header, payload) { context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, function(header, payload) {
logger.debug("Handling SESSION_JOIN msg " + JSON.stringify(payload)); logger.debug("Handling SESSION_JOIN msg " + JSON.stringify(payload));
@ -765,201 +305,6 @@
}); });
} }
function registerMusicianSessionJoin() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, function(header, payload) {
logger.debug("Handling MUSICIAN_SESSION_JOIN msg " + JSON.stringify(payload));
var okText = '';
var showNotification = false;
var callback;
if (context.JK.currentUserMusician) {
// user is MUSICIAN; musician_access = TRUE
if (payload.musician_access) {
showNotification = true;
okText = "JOIN";
callback = joinSession;
}
// user is MUSICIAN; fan_access = TRUE
else if (payload.fan_access) {
showNotification = true;
okText = "LISTEN";
callback = listenToSession;
}
}
else {
// user is FAN; fan_access = TRUE
if (payload.fan_access) {
showNotification = true;
okText = "LISTEN";
callback = listenToSession;
}
}
if (showNotification) {
handleNotification(payload, header.type);
app.notify({
"title": "Musician Joined Session",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": okText,
"ok_callback": callback,
"ok_callback_args": {
"session_id": payload.session_id,
"fan_access": payload.fan_access,
"musician_access": payload.musician_access,
"approval_required": payload.approval_required,
"notification_id": payload.notification_id
}
}
);
}
});
}
function registerBandSessionJoin() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_SESSION_JOIN, function(header, payload) {
logger.debug("Handling BAND_SESSION_JOIN msg " + JSON.stringify(payload));
var okText = '';
var showNotification = false;
var callback;
if (context.JK.currentUserMusician) {
// user is MUSICIAN; musician_access = TRUE
if (payload.musician_access) {
showNotification = true;
okText = "JOIN";
callback = joinSession;
}
// user is MUSICIAN; fan_access = TRUE
else if (payload.fan_access) {
showNotification = true;
okText = "LISTEN";
callback = listenToSession;
}
}
else {
// user is FAN; fan_access = TRUE
if (payload.fan_access) {
showNotification = true;
okText = "LISTEN";
callback = listenToSession;
}
}
if (showNotification) {
handleNotification(payload, header.type);
app.notify({
"title": "Band Joined Session",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "LISTEN",
"ok_callback": callback,
"ok_callback_args": {
"session_id": payload.session_id,
"fan_access": payload.fan_access,
"musician_access": payload.musician_access,
"approval_required": payload.approval_required,
"notification_id": payload.notification_id
}
}
);
}
});
}
function listenToSession(args) {
deleteNotification(args.notification_id);
context.JK.popExternalLink('/sessions/' + args.session_id);
}
/*********** TODO: THE NEXT 3 FUNCTIONS ARE COPIED FROM sessionList.js. REFACTOR TO COMMON PLACE. *************/
function joinSession(args) {
// NOTE: invited musicians get their own notification, so no need to check if user has invitation here
// like other places because an invited user would never get this notification
if (args.musician_access) {
if (args.approval_required) {
openAlert(args.session_id);
}
else {
openTerms(args);
}
}
deleteNotification(args.notification_id);
}
function openAlert(sessionId) {
var alertDialog = new context.JK.AlertDialog(context.JK.app, "YES",
"You must be approved to join this session. Would you like to send a request to join?",
sessionId, onCreateJoinRequest);
alertDialog.initialize();
context.JK.app.layout.showDialog('alert');
}
function onCreateJoinRequest(sessionId) {
var joinRequest = {};
joinRequest.music_session = sessionId;
joinRequest.user = context.JK.currentUserId;
rest.createJoinRequest(joinRequest)
.done(function(response) {
}).error(context.JK.app.ajaxError);
context.JK.app.layout.closeDialog('alert');
}
//////////////////////////////////////////////////////////////////////////////////////////
function registerMusicianRecordingSaved() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_RECORDING_SAVED, function(header, payload) {
logger.debug("Handling MUSICIAN_RECORDING_SAVED msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Musician Recording Saved",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "LISTEN",
"ok_callback": listenToRecording,
"ok_callback_args": {
"recording_id": payload.recording_id,
"notification_id": payload.notification_id
}
});
});
}
function registerBandRecordingSaved() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_RECORDING_SAVED, function(header, payload) {
logger.debug("Handling BAND_RECORDING_SAVED msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Band Recording Saved",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "LISTEN",
"ok_callback": listenToRecording,
"ok_callback_args": {
"recording_id": payload.recording_id,
"notification_id": payload.notification_id
}
});
});
}
function listenToRecording(args) {
deleteNotification(args.notification_id);
context.JK.popExternalLink('/recordings/' + args.recording_id);
}
function registerRecordingStarted() { function registerRecordingStarted() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_STARTED, function(header, payload) { context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_STARTED, function(header, payload) {
logger.debug("Handling RECORDING_STARTED msg " + JSON.stringify(payload)); logger.debug("Handling RECORDING_STARTED msg " + JSON.stringify(payload));
@ -984,86 +329,6 @@
}); });
} }
function registerRecordingMasterMixComplete() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_MASTER_MIX_COMPLETE, function(header, payload) {
logger.debug("Handling RECORDING_MASTER_MIX_COMPLETE msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Recording Master Mix Complete",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "SHARE",
"ok_callback": shareRecording,
"ok_callback_args": {
"recording_id": payload.recording_id
}
});
});
}
function shareRecording(args) {
var recordingId = args.recording_id;
}
function registerBandInvitation() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION, function(header, payload) {
logger.debug("Handling BAND_INVITATION msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Band Invitation",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
}, {
"ok_text": "ACCEPT",
"ok_callback": acceptBandInvitation,
"ok_callback_args": {
"band_invitation_id": payload.band_invitation_id,
"band_id": payload.band_id,
"notification_id": payload.notification_id
}
});
});
}
function acceptBandInvitation(args) {
rest.updateBandInvitation(
args.band_id,
args.band_invitation_id,
true
).done(function(response) {
deleteNotification(args.notification_id); // delete notification corresponding to this friend request
}).error(app.ajaxError);
}
function registerTextMessage() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TEXT_MESSAGE, function(header, payload) {
logger.debug("Handling TEXT_MESSAGE msg " + JSON.stringify(payload));
textMessageDialog.messageReceived(payload);
handleNotification(payload, header.type);
});
}
function registerBandInvitationAccepted() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.BAND_INVITATION_ACCEPTED, function(header, payload) {
logger.debug("Handling BAND_INVITATION_ACCEPTED msg " + JSON.stringify(payload));
handleNotification(payload, header.type);
app.notify({
"title": "Band Invitation Accepted",
"text": payload.msg,
"icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
});
});
}
function registerSourceUpRequested() { function registerSourceUpRequested() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SOURCE_UP_REQUESTED, function(header, payload) { context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SOURCE_UP_REQUESTED, function(header, payload) {
@ -1170,13 +435,13 @@
} }
this.initialize = function(invitationDialogInstance, textMessageDialogInstance) { this.initialize = function(invitationDialogInstance, textMessageDialogInstance) {
invitationDialog = invitationDialogInstance;
textMessageDialog = textMessageDialogInstance;
events(); events();
initializeSearchPanel(); initializeSearchPanel();
initializeFriendsPanel(); initializeFriendsPanel();
initializeChatPanel(); initializeChatPanel();
initializeNotificationsPanel(); initializeNotificationsPanel();
invitationDialog = invitationDialogInstance;
textMessageDialog = textMessageDialogInstance;
}; };
} }
})(window,jQuery); })(window,jQuery);

View File

@ -47,8 +47,7 @@
return message; return message;
} }
function sendMessage(e) { function sendMessage() {
var msg = $textBox.val(); var msg = $textBox.val();
if(!msg || msg == '') { if(!msg || msg == '') {
// don't bother the server with empty messages // don't bother the server with empty messages
@ -124,6 +123,15 @@
return markedUpMsg; return markedUpMsg;
} }
// we handled the notification, meaning the dialog showed this message as a chat message
function handledNotification(payload) {
return showing && payload.description == "TEXT_MESSAGE" && payload.sender_id == otherId;
}
function afterShow(args) {
$textBox.focus();
}
function beforeShow(args) { function beforeShow(args) {
app.layout.closeDialog('text-message') // ensure no others are showing. this is a singleton dialog app.layout.closeDialog('text-message') // ensure no others are showing. this is a singleton dialog
@ -175,13 +183,38 @@
reset(); reset();
} }
function postMessage(e) { function pasteIntoInput(el, text) {
el.focus();
if (typeof el.selectionStart == "number"
&& typeof el.selectionEnd == "number") {
var val = el.value;
var selStart = el.selectionStart;
el.value = val.slice(0, selStart) + text + val.slice(el.selectionEnd);
el.selectionEnd = el.selectionStart = selStart + text.length;
} else if (typeof document.selection != "undefined") {
var textRange = document.selection.createRange();
textRange.text = text;
textRange.collapse(false);
textRange.select();
}
}
return false; function handleEnter(evt) {
if (evt.keyCode == 13 && evt.shiftKey) {
pasteIntoInput(this, "\n");
evt.preventDefault();
}
else if(evt.keyCode == 13 && !evt.shiftKey){
sendMessage();
return false;
}
} }
function events() { function events() {
$form.submit(postMessage) $form.submit(sendMessage)
// http://stackoverflow.com/questions/6014702/how-do-i-detect-shiftenter-and-generate-a-new-line-in-textarea
$textBox.keydown(handleEnter);
} }
@ -234,6 +267,7 @@
function initialize() { function initialize() {
var dialogBindings = { var dialogBindings = {
'beforeShow' : beforeShow, 'beforeShow' : beforeShow,
'afterShow' : afterShow,
'afterHide': afterHide 'afterHide': afterHide
}; };
@ -253,6 +287,7 @@
this.initialize = initialize; this.initialize = initialize;
this.messageReceived = messageReceived; this.messageReceived = messageReceived;
this.formatTextMessage = formatTextMessage; this.formatTextMessage = formatTextMessage;
this.handledNotification = handledNotification;
} }
return this; return this;

View File

@ -29,6 +29,7 @@
.previous-message-text { .previous-message-text {
line-height:18px; line-height:18px;
white-space:pre-line;
} }
.previous-message-timestamp { .previous-message-timestamp {
@ -52,4 +53,9 @@
width:100%; width:100%;
height:40px; height:40px;
} }
.btn-send-text-message {
text-align:center;
width:50px;
}
} }

View File

@ -44,6 +44,12 @@ class ApiUsersController < ApiController
@user.show_whats_next = params[:show_whats_next] if params.has_key?(:show_whats_next) @user.show_whats_next = params[:show_whats_next] if params.has_key?(:show_whats_next)
@user.subscribe_email = params[:subscribe_email] if params.has_key?(:subscribe_email) @user.subscribe_email = params[:subscribe_email] if params.has_key?(:subscribe_email)
@user.biography = params[:biography] if params.has_key?(:biography) @user.biography = params[:biography] if params.has_key?(:biography)
# allow keyword of 'LATEST' to mean set the notification_seen_at to the most recent notification for this user
if params.has_key?(:notification_seen_at)
@user.update_notification_seen_at params[:notification_seen_at]
end
@user.save @user.save
if @user.errors.any? if @user.errors.any?

View File

@ -214,11 +214,6 @@ class UsersController < ApplicationController
@promo_latest, start = Feed.index(nil, limit: 10) @promo_latest, start = Feed.index(nil, limit: 10)
end end
# temporary--will go away soon
@jamfest_2014 = Event.find_by_id('80bb6acf-3ddc-4305-9442-75e6ec047c27')
@jamfest_2014 = Event.find_by_id('a2dfbd26-9b17-4446-8c61-b67a542ea6ee') unless @jamfest_2014
# temporary--end
@welcome_page = true @welcome_page = true
render :layout => "web" render :layout => "web"
end end

View File

@ -1,4 +1,4 @@
object @music_session object @music_session
if !current_user if !current_user
# there should be more data returned, but we need to think very carefully about what data is public for a music session # there should be more data returned, but we need to think very carefully about what data is public for a music session

View File

@ -311,10 +311,6 @@
} }
JK.bindHoverEvents(); JK.bindHoverEvents();
setInterval(function() {
console.log("IN FOCUS: " + document.hasFocus());
}, 1000)
}) })
</script> </script>

View File

@ -7,10 +7,6 @@
= link_to "Already have an account?", signin_path, class: "signin", id: "signin" = link_to "Already have an account?", signin_path, class: "signin", id: "signin"
- content_for :after_black_bar do - content_for :after_black_bar do
- if @jamfest_2014
.jamfest{style: 'top:-70px;position:relative'}
%a{ href: event_path(@jamfest_2014.slug), style: 'font-size:24px' }
Join us online March 12 for Virtual Jam Fest!
%div{style: "padding-top:20px;"} %div{style: "padding-top:20px;"}
.right .right
= render :partial => "buzz" = render :partial => "buzz"

View File

@ -8,6 +8,7 @@ describe "Home Screen", :js => true, :type => :feature, :capybara_feature => tru
Capybara.javascript_driver = :poltergeist Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 10 Capybara.default_wait_time = 10
MusicSession.delete_all
end end
let(:user) { FactoryGirl.create(:user) } let(:user) { FactoryGirl.create(:user) }

View File

@ -0,0 +1,168 @@
require 'spec_helper'
describe "Notification Highlighter", :js => true, :type => :feature, :capybara_feature => true do
subject { page }
before(:all) do
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 10
end
let(:user) { FactoryGirl.create(:user) }
let(:user2) { FactoryGirl.create(:user) }
shared_examples_for :notification_badge do |options|
it "in correct state" do
sign_in_poltergeist(user) unless page.has_selector?('h2', 'musicians')
badge = find("#{NOTIFICATION_PANEL} .badge", text:options[:count])
badge['class'].include?('highlighted').should == options[:highlighted]
if options[:action] == :click
badge.trigger(:click)
badge = find("#{NOTIFICATION_PANEL} .badge", text:0)
badge['class'].include?('highlighted').should == false
end
end
end
describe "user with no notifications" do
it_behaves_like :notification_badge, highlighted: false, count:0
describe "and realtime notifications with sidebar closed" do
before(:each) do
sign_in_poltergeist(user)
document_focus
user.reload
end
it_behaves_like :notification_badge, highlighted: false, count:0
describe "sees notification" do
before(:each) do
notification = Notification.send_text_message("text message", user2, user)
notification.errors.any?.should be_false
end
it_behaves_like :notification_badge, highlighted: false, count:0, action: :click
end
describe "document out of focus" do
before(:each) do
document_blur
notification = Notification.send_text_message("text message", user2, user)
notification.errors.any?.should be_false
end
it_behaves_like :notification_badge, highlighted: true, count:1, action: :click
end
end
describe "and realtime notifications with sidebar open" do
before(:each) do
# generate one message so that count = 1 to start
notification = Notification.send_text_message("text message", user2, user)
notification.errors.any?.should be_false
sign_in_poltergeist(user)
document_focus
open_notifications
badge = find("#{NOTIFICATION_PANEL} .badge", text:'0') # wait for the opening of the sidebar to bring count to 0
user.reload
end
it_behaves_like :notification_badge, highlighted: false, count:0
describe "sees notification" do
before(:each) do
notification = Notification.send_text_message("text message", user2, user)
notification.errors.any?.should be_false
find('#notification #ok-button') # wait for notification to show, so that we know the sidebar had a chance to update
end
it_behaves_like :notification_badge, highlighted: false, count:0
end
describe "document out of focus" do
before(:each) do
document_blur
notification = Notification.send_text_message("text message 2", user2, user)
notification.errors.any?.should be_false
find('#notification #ok-button')
end
it_behaves_like :notification_badge, highlighted: true, count:1
describe "user comes back" do
before(:each) do
window_focus
it_behaves_like :notification_badge, highlighted: false, count:1
end
end
end
end
end
describe "user with new notifications" do
before(:each) do
# create a notification
notification = Notification.send_text_message("text message", user2, user)
notification.errors.any?.should be_false
end
it_behaves_like :notification_badge, highlighted:true, count:1, action: :click
describe "user has previously seen notifications" do
before(:each) do
user.update_notification_seen_at 'LATEST'
user.save!
end
it_behaves_like :notification_badge, highlighted: false, count:0, action: :click
describe "user again has unseen notifications" do
before(:each) do
# create a notification
notification = Notification.send_text_message("text message", user2, user)
notification.errors.any?.should be_false
end
it_behaves_like :notification_badge, highlighted:true, count:1, action: :click
end
end
end
describe "user no unseen notifications" do
describe "notification occurs in realtime" do
describe "sidebar is open" do
describe "user can see notifications" do
it "stays deactivated" do
end
end
describe "user can not see notifications" do
describe "with dialog open" do
it "temporarily activates" do
end
end
describe "with document blurred" do
it "temporarily activates" do
end
end
end
end
end
end
end

View File

@ -42,7 +42,7 @@ describe "Text Message", :js => true, :type => :feature, :capybara_feature => tr
notification = Notification.send_text_message("bibbity bobbity boo", @user2, @user1) notification = Notification.send_text_message("bibbity bobbity boo", @user2, @user1)
notification.errors.any?.should be_false notification.errors.any?.should be_false
open_sidebar open_notifications
# find the notification and click REPLY # find the notification and click REPLY
find("[layout-id='panelNotifications'] [notification-id='#{notification.id}'] .button-orange", text:'REPLY').trigger(:click) find("[layout-id='panelNotifications'] [notification-id='#{notification.id}'] .button-orange", text:'REPLY').trigger(:click)

View File

@ -1,6 +1,8 @@
# methods here all assume you are in /client # methods here all assume you are in /client
NOTIFICATION_PANEL = '[layout-id="panelNotifications"]'
# enters text into the search sidebar # enters text into the search sidebar
def site_search(text, options = {}) def site_search(text, options = {})
within('#searchForm') do within('#searchForm') do
@ -56,11 +58,41 @@ def send_text_message(msg, options={})
end end
end end
def open_sidebar def open_notifications
find('[layout-id="panelNotifications"] .panel-header').trigger(:click) find("#{NOTIFICATION_PANEL} .panel-header").trigger(:click)
end end
def hover_intent(element) def hover_intent(element)
element.hover element.hover
element.hover element.hover
end
# forces document.hasFocus() to return false
def document_blur
page.evaluate_script(%{(function() {
// save original
if(!window.documentFocus) { window.documentFocus = window.document.hasFocus; }
window.document.hasFocus = function() {
console.log("document.hasFocus() returns false");
return false;
}
})()})
end
def document_focus
page.evaluate_script(%{(function() {
// save original
if(!window.documentFocus) { window.documentFocus = window.document.hasFocus; }
window.document.hasFocus = function() {
console.log("document.hasFocus() returns true");
return true;
}
})()})
end
# simulates focus event on window
def window_focus
page.evaluate_script(%{window.jQuery(window).trigger('focus');})
end end

View File

@ -575,20 +575,9 @@ module JamWebsockets
connection.touch connection.touch
# update user's notification_seen_at field if the heartbeat indicates it saw one # update user's notification_seen_at field if the heartbeat indicates it saw one
notification_seen_at_parsed = nil # first we try to use the notification id, which should usually exist.
notification_seen_at = heartbeat.notification_seen_at if heartbeat.value_for_tag(1) # if not, then fallback to notification_seen_at, which is approximately the last time we saw a notification
begin update_notification_seen_at(connection, context, heartbeat)
notification_seen_at_parsed = Time.parse(notification_seen_at)
rescue Exception => e
@log.error "unable to parse notification_seen_at in heartbeat from #{context}. notification_seen_at: #{notification_seen_at}"
end
if notification_seen_at_parsed
connection.user.notification_seen_at = notification_seen_at
unless connection.user.save(validate: false)
@log.error "unable to update notification_seen_at for client #{context}. errors: #{connection.user.errors.inspect}"
end
end
end end
ConnectionManager.active_record_transaction do |connection_manager| ConnectionManager.active_record_transaction do |connection_manager|
@ -614,6 +603,34 @@ module JamWebsockets
end end
end end
def update_notification_seen_at(connection, context, heartbeat)
notification_id_field = heartbeat.notification_seen if heartbeat.value_for_tag(1)
if notification_id_field
notification = Notification.find_by_id(notification_id_field)
if notification
connection.user.notification_seen_at = notification.created_at
unless connection.user.save(validate: false)
@log.error "unable to update notification_seen_at via id field for client #{context}. errors: #{connection.user.errors.inspect}"
end
else
notification_seen_at_parsed = nil
notification_seen_at = heartbeat.notification_seen_at if heartbeat.value_for_tag(2)
begin
notification_seen_at_parsed = Time.parse(notification_seen_at) if notification_seen_at && notification_seen_at.length > 0
rescue Exception => e
@log.error "unable to parse notification_seen_at in heartbeat from #{context}. notification_seen_at: #{notification_seen_at}"
end
if notification_seen_at_parsed
connection.user.notification_seen_at = notification_seen_at
unless connection.user.save(validate: false)
@log.error "unable to update notification_seen_at via time field for client #{context}. errors: #{connection.user.errors.inspect}"
end
end
end
end
end
def valid_login(username, password, token, client_id) def valid_login(username, password, token, client_id)
if !token.nil? && token != '' if !token.nil? && token != ''