* 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
// the server will send a HeartbeatAck in response to this
message Heartbeat {
optional string notification_seen_at = 1;
optional string notification_seen = 1;
optional string notification_seen_at = 2;
}
// target: client

View File

@ -294,7 +294,28 @@ module JamRuby
# returns the # of 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
def confirm_email!

View File

@ -99,9 +99,10 @@
};
// Heartbeat message
factory.heartbeat = function(notification_last_seen_at) {
factory.heartbeat = function(lastNotificationSeen, lastNotificationSeenAt) {
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);
};

View File

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

View File

@ -251,7 +251,8 @@
if (!sidebarVisible) {
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 searchHeight = $('.sidebar .search').first().height();
var expanderHeight = $('[layout-sidebar-expander]').height();
@ -259,6 +260,7 @@
$('[layout-panel="contents"]').hide();
$('[layout-panel="contents"]').css({"height": "1px"});
$expandedPanelContents.show();
$expandedPanel.triggerHandler('open')
$expandedPanelContents.animate({"height": expandedPanelHeight + "px"}, opts.animationDuration);
}
@ -531,6 +533,25 @@
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,
* also moves the .dialog-overlay such that it hides/obscures all dialogs except the highest one
@ -854,6 +875,10 @@
showDialog(dialog, options);
};
this.dialogObscuredNotification = function() {
return dialogObscuredNotification();
}
this.isDialogShowing = function() {
return isDialogShowing();
}

View File

@ -10,34 +10,38 @@
var missedNotificationsWhileAway = false;
var $panel = null;
var $expanded = null;
var $contents = null;
var $count = null;
var $list = null;
var $notificationTemplate = null;
var darkenedColor = '#0D7B89';
var highlightedColor = 'white'
// 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();
}
var textMessageDialog = null;
var queuedNotification = null;
var queuedNotificationCreatedAt = null;
function isNotificationsPanelVisible() {
return $expanded.is(':visible')
return $contents.is(':visible')
}
function incrementNotificationCount() {
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
function pulseToDark() {
logger.debug("pulsing notification badge")
lowlightCount();
$count.pulse({'background-color' : highlightedColor}, {pulses: 2}, function() {
$count.text('0');
$count.removeAttr('style')
setCount(0);
})
}
function setCount(count) {
$count.text(count);
}
function lowlightCount() {
$count.removeClass('highlighted');
@ -47,11 +51,35 @@
$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) {
if(userCanSeeNotifications()) {
app.updateNotificationSeen(payload.created_at);
if(userCanSeeNotifications(payload)) {
app.updateNotificationSeen(payload.notification_id, payload.created_at);
}
else {
queueNotificationSeen(payload.notification_id, payload.created_at);
highlightCount();
incrementNotificationCount();
missedNotificationsWhileAway = true;
@ -63,12 +91,28 @@
if(missedNotificationsWhileAway) {
// catch user's eye, then put count to 0
pulseToDark();
if(queuedNotificationCreatedAt) {
app.updateNotificationSeen(queuedNotification, queuedNotificationCreatedAt);
}
}
}
queuedNotification = null;
queuedNotificationCreatedAt = null;
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() {
}
@ -77,6 +121,42 @@
$(app.layout).on('dialog_closed', function(e, data) {if(data.dialogCount == 0) userCameBack(); });
$(window).focus(userCameBack);
$(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() {
@ -88,20 +168,736 @@
.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"]');
$expanded = $panel.find('.panel.expanded');
$contents = $panel.find('.panelcontents');
$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($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($list.length == 0) throw "notification list element not found";
if($notificationTemplate.length == 0) throw "notification template not found";
events();
populate();
};
this.initiliaze = initialize;
this.initialize = initialize;
this.onNotificationOccurred = onNotificationOccurred;
};
})(window, jQuery);

View File

@ -10,6 +10,7 @@
var invitationDialog = null;
var textMessageDialog = null;
var notificationPanel = null;
var me = null;
function initializeSearchPanel() {
$('#search_text_type').change(function() {
@ -118,188 +119,7 @@
function initializeNotificationsPanel() {
notificationPanel = new context.JK.NotificationPanel(app);
notificationPanel.initialize();
}
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
});
notificationPanel.initialize(me, textMessageDialog);
}
function initializeChatPanel() {
@ -348,61 +168,6 @@
$('#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 timer = 0;
return function(callback, ms) {
@ -460,32 +225,14 @@
// friend notifications
registerFriendUpdate();
registerFriendRequest();
registerFriendRequestAccepted();
registerNewUserFollower();
registerNewBandFollower();
// session invitations
registerSessionInvitation();
registerSessionEnded();
registerJoinRequest();
registerJoinRequestApproved();
registerJoinRequestRejected();
registerSessionJoin();
registerSessionDepart();
registerMusicianSessionJoin();
registerBandSessionJoin();
// recording notifications
registerMusicianRecordingSaved();
registerBandRecordingSaved();
registerRecordingStarted();
registerRecordingEnded();
registerRecordingMasterMixComplete();
// band notifications
registerBandInvitation();
registerBandInvitationAccepted();
// broadcast notifications
registerSourceUpRequested();
@ -493,9 +240,6 @@
registerSourceUp();
registerSourceDown();
// register text messages
registerTextMessage();
// watch for Invite More Users events
$('#sidebar-div .btn-email-invitation').click(function() {
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() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, function(header, 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() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.RECORDING_STARTED, function(header, 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() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SOURCE_UP_REQUESTED, function(header, payload) {
@ -1170,13 +435,13 @@
}
this.initialize = function(invitationDialogInstance, textMessageDialogInstance) {
invitationDialog = invitationDialogInstance;
textMessageDialog = textMessageDialogInstance;
events();
initializeSearchPanel();
initializeFriendsPanel();
initializeChatPanel();
initializeNotificationsPanel();
invitationDialog = invitationDialogInstance;
textMessageDialog = textMessageDialogInstance;
};
}
})(window,jQuery);

View File

@ -47,8 +47,7 @@
return message;
}
function sendMessage(e) {
function sendMessage() {
var msg = $textBox.val();
if(!msg || msg == '') {
// don't bother the server with empty messages
@ -124,6 +123,15 @@
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) {
app.layout.closeDialog('text-message') // ensure no others are showing. this is a singleton dialog
@ -175,13 +183,38 @@
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() {
$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() {
var dialogBindings = {
'beforeShow' : beforeShow,
'afterShow' : afterShow,
'afterHide': afterHide
};
@ -253,6 +287,7 @@
this.initialize = initialize;
this.messageReceived = messageReceived;
this.formatTextMessage = formatTextMessage;
this.handledNotification = handledNotification;
}
return this;

View File

@ -29,6 +29,7 @@
.previous-message-text {
line-height:18px;
white-space:pre-line;
}
.previous-message-timestamp {
@ -52,4 +53,9 @@
width:100%;
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.subscribe_email = params[:subscribe_email] if params.has_key?(:subscribe_email)
@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
if @user.errors.any?

View File

@ -214,11 +214,6 @@ class UsersController < ApplicationController
@promo_latest, start = Feed.index(nil, limit: 10)
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
render :layout => "web"
end

View File

@ -1,4 +1,4 @@
object @music_session
object @music_session
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

View File

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

View File

@ -7,10 +7,6 @@
= link_to "Already have an account?", signin_path, class: "signin", id: "signin"
- 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;"}
.right
= render :partial => "buzz"

View File

@ -8,6 +8,7 @@ describe "Home Screen", :js => true, :type => :feature, :capybara_feature => tru
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver
Capybara.default_wait_time = 10
MusicSession.delete_all
end
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.errors.any?.should be_false
open_sidebar
open_notifications
# find the notification and click REPLY
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
NOTIFICATION_PANEL = '[layout-id="panelNotifications"]'
# enters text into the search sidebar
def site_search(text, options = {})
within('#searchForm') do
@ -56,11 +58,41 @@ def send_text_message(msg, options={})
end
end
def open_sidebar
find('[layout-id="panelNotifications"] .panel-header').trigger(:click)
def open_notifications
find("#{NOTIFICATION_PANEL} .panel-header").trigger(:click)
end
def hover_intent(element)
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

View File

@ -575,20 +575,9 @@ module JamWebsockets
connection.touch
# update user's notification_seen_at field if the heartbeat indicates it saw one
notification_seen_at_parsed = nil
notification_seen_at = heartbeat.notification_seen_at if heartbeat.value_for_tag(1)
begin
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
# first we try to use the notification id, which should usually exist.
# if not, then fallback to notification_seen_at, which is approximately the last time we saw a notification
update_notification_seen_at(connection, context, heartbeat)
end
ConnectionManager.active_record_transaction do |connection_manager|
@ -614,6 +603,34 @@ module JamWebsockets
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)
if !token.nil? && token != ''