diff --git a/pb/src/client_container.proto b/pb/src/client_container.proto index 04e172870..1df3bebf8 100644 --- a/pb/src/client_container.proto +++ b/pb/src/client_container.proto @@ -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 diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 440613536..66f2ff37d 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -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! diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js index 0b86c7900..04bd51344 100644 --- a/web/app/assets/javascripts/AAB_message_factory.js +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -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); }; diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index d78922ea7..f00e9b02d 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -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 { diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index dc130dc78..555cc0d6d 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -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(); } diff --git a/web/app/assets/javascripts/notificationPanel.js b/web/app/assets/javascripts/notificationPanel.js index a5649a634..f3e46bfc2 100644 --- a/web/app/assets/javascripts/notificationPanel.js +++ b/web/app/assets/javascripts/notificationPanel.js @@ -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:

"; + participantHtml += ""; + + $.each(participants, function(index, val) { + if (index < 4) { + participantHtml += ""; + } + }); + + participantHtml += "
" + val.name + "
"; + + 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); diff --git a/web/app/assets/javascripts/sidebar.js b/web/app/assets/javascripts/sidebar.js index 2e90b7c5e..f4a4a2624 100644 --- a/web/app/assets/javascripts/sidebar.js +++ b/web/app/assets/javascripts/sidebar.js @@ -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:

"; - participantHtml += ""; - - $.each(participants, function(index, val) { - if (index < 4) { - participantHtml += ""; - } - }); - - participantHtml += "
" + val.name + "
"; - - 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); \ No newline at end of file diff --git a/web/app/assets/javascripts/textMessageDialog.js b/web/app/assets/javascripts/textMessageDialog.js index e76d413ba..3bcc2c5c2 100644 --- a/web/app/assets/javascripts/textMessageDialog.js +++ b/web/app/assets/javascripts/textMessageDialog.js @@ -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; diff --git a/web/app/assets/stylesheets/client/textMessageDialog.css.scss b/web/app/assets/stylesheets/client/textMessageDialog.css.scss index 04f837e53..5605adfce 100644 --- a/web/app/assets/stylesheets/client/textMessageDialog.css.scss +++ b/web/app/assets/stylesheets/client/textMessageDialog.css.scss @@ -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; + } } \ No newline at end of file diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index ea3d28c47..5dc55cf9c 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -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? diff --git a/web/app/controllers/users_controller.rb b/web/app/controllers/users_controller.rb index 348cf7469..6eb930565 100644 --- a/web/app/controllers/users_controller.rb +++ b/web/app/controllers/users_controller.rb @@ -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 diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl index c42d940b6..0b1c30540 100644 --- a/web/app/views/api_music_sessions/show.rabl +++ b/web/app/views/api_music_sessions/show.rabl @@ -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 diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 02e1a0176..d28fd6ae5 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -311,10 +311,6 @@ } JK.bindHoverEvents(); - - setInterval(function() { - console.log("IN FOCUS: " + document.hasFocus()); - }, 1000) }) diff --git a/web/app/views/users/welcome.html.haml b/web/app/views/users/welcome.html.haml index f2d987044..823f9d23c 100644 --- a/web/app/views/users/welcome.html.haml +++ b/web/app/views/users/welcome.html.haml @@ -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" diff --git a/web/spec/features/home_spec.rb b/web/spec/features/home_spec.rb index 1585f9c73..7c206be4b 100644 --- a/web/spec/features/home_spec.rb +++ b/web/spec/features/home_spec.rb @@ -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) } diff --git a/web/spec/features/notification_highlighter_spec.rb b/web/spec/features/notification_highlighter_spec.rb new file mode 100644 index 000000000..d75946605 --- /dev/null +++ b/web/spec/features/notification_highlighter_spec.rb @@ -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 diff --git a/web/spec/features/text_message_spec.rb b/web/spec/features/text_message_spec.rb index 601eb7c93..517b135da 100644 --- a/web/spec/features/text_message_spec.rb +++ b/web/spec/features/text_message_spec.rb @@ -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) diff --git a/web/spec/support/client_interactions.rb b/web/spec/support/client_interactions.rb index 8b4bc2121..a02c12026 100644 --- a/web/spec/support/client_interactions.rb +++ b/web/spec/support/client_interactions.rb @@ -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 \ No newline at end of file diff --git a/websocket-gateway/lib/jam_websockets/router.rb b/websocket-gateway/lib/jam_websockets/router.rb index 866429bfb..0f3c8b0e0 100644 --- a/websocket-gateway/lib/jam_websockets/router.rb +++ b/websocket-gateway/lib/jam_websockets/router.rb @@ -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 != ''