* VRFS-1473 - notifications should be highlighted, wip

This commit is contained in:
Seth Call 2014-03-26 17:09:48 +00:00
parent d365054237
commit 526f6fe577
18 changed files with 294 additions and 48 deletions

View File

@ -137,4 +137,4 @@ cascading_delete_constraints_for_release.sql
events_social_description.sql events_social_description.sql
fix_broken_cities.sql fix_broken_cities.sql
notifications_with_text.sql notifications_with_text.sql
notification_seen_at.sql

View File

@ -0,0 +1 @@
ALTER TABLE users ADD COLUMN notification_seen_at TIMESTAMP;

View File

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

View File

@ -291,7 +291,12 @@ module JamRuby
recordings.concat(msh) recordings.concat(msh)
recordings.sort! {|a,b| b.created_at <=> a.created_at}.first(5) recordings.sort! {|a,b| b.created_at <=> a.created_at}.first(5)
end end
# returns the # of new notifications
def new_notifications
Notification.select('id').where(target_user_id: id).where('created_at > ?', notification_seen_at).count
end
def confirm_email! def confirm_email!
self.email_confirmed = true self.email_confirmed = true
end end

View File

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

View File

@ -28,6 +28,7 @@
//= require jquery.infinitescroll //= require jquery.infinitescroll
//= require jquery.hoverIntent //= require jquery.hoverIntent
//= require jquery.dotdotdot //= require jquery.dotdotdot
//= require jquery.pulse
//= require AAA_Log //= require AAA_Log
//= require globals //= require globals
//= require AAB_message_factory //= require AAB_message_factory

View File

@ -934,6 +934,7 @@
} }
function getNotifications(options) { function getNotifications(options) {
if(!options) options = {};
var id = getId(options); var id = getId(options);
return $.ajax({ return $.ajax({
type: "GET", type: "GET",

View File

@ -28,6 +28,7 @@
var lastHeartbeatFound = false; var lastHeartbeatFound = false;
var heartbeatAckCheckInterval = null; var heartbeatAckCheckInterval = null;
var userDeferred = null; var userDeferred = null;
var notificationLastSeenAt = undefined;
var opts = { var opts = {
inClient: true, // specify false if you want the app object but none of the client-oriented features inClient: true, // specify false if you want the app object but none of the client-oriented features
@ -92,7 +93,8 @@
function _heartbeat() { function _heartbeat() {
if (app.heartbeatActive) { if (app.heartbeatActive) {
var message = context.JK.MessageFactory.heartbeat(); var message = context.JK.MessageFactory.heartbeat(notificationLastSeenAt);
notificationLastSeenAt = undefined;
context.JK.JamServer.send(message); context.JK.JamServer.send(message);
lastHeartbeatFound = false; lastHeartbeatFound = false;
} }
@ -384,6 +386,26 @@
return userDeferred; return userDeferred;
} }
this.updateNotificationSeen = function(notificationCreatedAt) {
var time = new Date(notificationCreatedAt);
if(!notificationCreatedAt) {
throw 'invalid value passed to updateNotificationSeen'
}
if(!notificationLastSeenAt) {
notificationLastSeenAt = notificationCreatedAt;
logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
}
else if(time.getTime() > new Date(notificationLastSeenAt).getTime()) {
notificationLastSeenAt = notificationCreatedAt;
logger.debug("updated notificationLastSeenAt with: " + notificationCreatedAt);
}
else {
logger.debug("ignored notificationLastSeenAt for: " + notificationCreatedAt);
}
}
this.unloadFunction = function () { this.unloadFunction = function () {
logger.debug("window.unload function called."); logger.debug("window.unload function called.");

View File

@ -425,6 +425,7 @@
unstackDialogs($overlay); unstackDialogs($overlay);
$dialog.hide(); $dialog.hide();
dialogEvent(dialog, 'afterHide'); dialogEvent(dialog, 'afterHide');
$(me).triggerHandler('dialog_closed', {dialogCount: openDialogs.length})
} }
function screenEvent(screen, evtName, data) { function screenEvent(screen, evtName, data) {
@ -526,6 +527,10 @@
} }
} }
function isDialogShowing() {
return openDialogs.length > 0;
}
/** /**
* Responsible for keeping N dialogs in correct stacked order, * Responsible for keeping N dialogs in correct stacked order,
* also moves the .dialog-overlay such that it hides/obscures all dialogs except the highest one * also moves the .dialog-overlay such that it hides/obscures all dialogs except the highest one
@ -849,6 +854,10 @@
showDialog(dialog, options); showDialog(dialog, options);
}; };
this.isDialogShowing = function() {
return isDialogShowing();
}
this.close = function (evt) { this.close = function (evt) {
close(evt); close(evt);
}; };

View File

@ -0,0 +1,107 @@
(function(context,$) {
"use strict";
context.JK = context.JK || {};
context.JK.NotificationPanel = function(app) {
var logger = context.JK.logger;
var friends = [];
var rest = context.JK.Rest();
var missedNotificationsWhileAway = false;
var $panel = null;
var $expanded = null;
var $count = 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();
}
function isNotificationsPanelVisible() {
return $expanded.is(':visible')
}
function incrementNotificationCount() {
var count = parseInt($count.text());
$count.text(count + 1);
}
// set the element to white, and pulse it down to the un-highlighted value 2x, then set
function pulseToDark() {
lowlightCount();
$count.pulse({'background-color' : highlightedColor}, {pulses: 2}, function() {
$count.text('0');
})
}
function lowlightCount() {
$count.removeClass('highlighted');
}
function highlightCount() {
$count.addClass('highlighted');
}
function onNotificationOccurred(payload) {
if(userCanSeeNotifications()) {
app.updateNotificationSeen(payload.created_at);
}
else {
highlightCount();
incrementNotificationCount();
missedNotificationsWhileAway = true;
}
}
function userCameBack() {
if(isNotificationsPanelVisible()) {
if(missedNotificationsWhileAway) {
// catch user's eye, then put count to 0
pulseToDark();
}
}
missedNotificationsWhileAway = false;
}
function windowBlurred() {
}
function events() {
$(app.layout).on('dialog_closed', function(e, data) {if(data.dialogCount == 0) userCameBack(); });
$(window).focus(userCameBack);
$(window).blur(windowBlurred);
}
function populate() {
// retrieve pending notifications for this user
rest.getNotifications()
.done(function(response) {
updateNotificationList(response);
})
.fail(app.ajaxError)
}
function initialize(sidebar) {
$panel = $('[layout-id="panelNotifications"]');
$expanded = $panel.find('.panel.expanded');
$count = $panel.find('#sidebar-notification-count');
if($panel.length == 0) throw "notifications panel not found"
if($expanded.length == 0) throw "notifications expanded content not found"
if($count.length == 0) throw "notifications count element not found";
events();
populate();
};
this.initiliaze = initialize;
this.onNotificationOccurred = onNotificationOccurred;
};
})(window, jQuery);

View File

@ -9,6 +9,7 @@
var rest = context.JK.Rest(); var rest = context.JK.Rest();
var invitationDialog = null; var invitationDialog = null;
var textMessageDialog = null; var textMessageDialog = null;
var notificationPanel = null;
function initializeSearchPanel() { function initializeSearchPanel() {
$('#search_text_type').change(function() { $('#search_text_type').change(function() {
@ -116,23 +117,8 @@
} }
function initializeNotificationsPanel() { function initializeNotificationsPanel() {
// retrieve pending notifications for this user notificationPanel = new context.JK.NotificationPanel(app);
var url = "/api/users/" + context.JK.currentUserId + "/notifications" notificationPanel.initialize();
$.ajax({
type: "GET",
dataType: "json",
contentType: 'application/json',
url: url,
processData: false,
success: function(response) {
updateNotificationList(response);
// set notification count
$('#sidebar-notification-count').html(response.length);
},
error: app.ajaxError
});
} }
function updateNotificationList(response) { function updateNotificationList(response) {
@ -362,12 +348,8 @@
$('#sidebar-search-results').height('0px'); $('#sidebar-search-results').height('0px');
} }
function incrementNotificationCount() {
var count = parseInt($('#sidebar-notification-count').html());
$('#sidebar-notification-count').html(count + 1);
}
function decrementNotificationCount() { function decrementNotificationCount() {
/**
var count = parseInt($('#sidebar-notification-count').html()); var count = parseInt($('#sidebar-notification-count').html());
if (count === 0) { if (count === 0) {
$('#sidebar-notification-count').html(0); $('#sidebar-notification-count').html(0);
@ -375,6 +357,13 @@
else { else {
$('#sidebar-notification-count').html(count - 1); $('#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 // default handler for incoming notification
@ -402,6 +391,13 @@
$('#sidebar-notification-list').prepend(notificationHtml); $('#sidebar-notification-list').prepend(notificationHtml);
if(userCanSeeNotifications()) {
app.updateNotificationSeen(payload.created_at);
}
else {
}
initializeActions(payload, type); initializeActions(payload, type);
return true; return true;

View File

@ -153,9 +153,9 @@
$element.bt(text, options); $element.bt(text, options);
} }
context.JK.bindHoverEvents = function ($parent) { context.JK.bindHoverEvents = function ($parent) {
if(!$parent) { if (!$parent) {
$parent = $('body'); $parent = $('body');
} }
@ -328,9 +328,9 @@
} }
// creates an array with entries like [{ id: "drums", description: "Drums"}, ] // creates an array with entries like [{ id: "drums", description: "Drums"}, ]
context.JK.listInstruments = function() { context.JK.listInstruments = function () {
var instrumentArray = []; var instrumentArray = [];
$.each(context.JK.server_to_client_instrument_map, function(key, val) { $.each(context.JK.server_to_client_instrument_map, function (key, val) {
instrumentArray.push({"id": context.JK.server_to_client_instrument_map[key].client_id, "description": key}); instrumentArray.push({"id": context.JK.server_to_client_instrument_map[key].client_id, "description": key});
}); });
return instrumentArray; return instrumentArray;
@ -652,22 +652,22 @@
return hasFlash; return hasFlash;
} }
context.JK.hasOneConfiguredDevice = function() { context.JK.hasOneConfiguredDevice = function () {
var result = context.jamClient.FTUEGetGoodConfigurationList(); var result = context.jamClient.FTUEGetGoodConfigurationList();
logger.debug("hasOneConfiguredDevice: ", result); logger.debug("hasOneConfiguredDevice: ", result);
return result.length > 0; return result.length > 0;
}; };
context.JK.getGoodAudioConfigs = function() { context.JK.getGoodAudioConfigs = function () {
var result = context.jamClient.FTUEGetGoodAudioConfigurations(); var result = context.jamClient.FTUEGetGoodAudioConfigurations();
logger.debug("goodAudioConfigs=%o", result); logger.debug("goodAudioConfigs=%o", result);
return result; return result;
}; };
context.JK.getGoodConfigMap = function() { context.JK.getGoodConfigMap = function () {
var goodConfigMap = []; var goodConfigMap = [];
var goodConfigs = context.JK.getGoodAudioConfigs(); var goodConfigs = context.JK.getGoodAudioConfigs();
$.each(goodConfigs, function(index, profileKey) { $.each(goodConfigs, function (index, profileKey) {
var friendlyName = context.jamClient.FTUEGetConfigurationDevice(profileKey); var friendlyName = context.jamClient.FTUEGetConfigurationDevice(profileKey);
goodConfigMap.push({key: profileKey, name: friendlyName}); goodConfigMap.push({key: profileKey, name: friendlyName});
}); });
@ -675,12 +675,12 @@
return goodConfigMap; return goodConfigMap;
} }
context.JK.getBadAudioConfigs = function() { context.JK.getBadAudioConfigs = function () {
var badAudioConfigs = []; var badAudioConfigs = [];
var allAudioConfigs = context.jamClient.FTUEGetAllAudioConfigurations(); var allAudioConfigs = context.jamClient.FTUEGetAllAudioConfigurations();
var goodAudioConfigs = context.JK.getGoodAudioConfigs(); var goodAudioConfigs = context.JK.getGoodAudioConfigs();
for (var i=0; i < allAudioConfigs.length; i++) { for (var i = 0; i < allAudioConfigs.length; i++) {
if ($.inArray(allAudioConfigs[i], goodAudioConfigs) === -1) { if ($.inArray(allAudioConfigs[i], goodAudioConfigs) === -1) {
badAudioConfigs.push(allAudioConfigs[i]); badAudioConfigs.push(allAudioConfigs[i]);
} }
@ -689,10 +689,10 @@
return badAudioConfigs; return badAudioConfigs;
}; };
context.JK.getBadConfigMap = function() { context.JK.getBadConfigMap = function () {
var badConfigMap = []; var badConfigMap = [];
var badConfigs = context.JK.getBadAudioConfigs(); var badConfigs = context.JK.getBadAudioConfigs();
$.each(badConfigs, function(index, profileKey) { $.each(badConfigs, function (index, profileKey) {
var friendlyName = context.jamClient.FTUEGetConfigurationDevice(profileKey); var friendlyName = context.jamClient.FTUEGetConfigurationDevice(profileKey);
badConfigMap.push({key: profileKey, name: friendlyName}); badConfigMap.push({key: profileKey, name: friendlyName});
}); });
@ -700,7 +700,7 @@
return badConfigMap; return badConfigMap;
} }
context.JK.getFirstGoodDevice = function(preferredDeviceId) { context.JK.getFirstGoodDevice = function (preferredDeviceId) {
var badConfigs = context.JK.getBadAudioConfigs(); var badConfigs = context.JK.getBadAudioConfigs();
function getGoodDevice() { function getGoodDevice() {
@ -713,7 +713,7 @@
} }
return deviceId; return deviceId;
} }
var deviceId = null; var deviceId = null;
if (preferredDeviceId) { if (preferredDeviceId) {
@ -724,7 +724,7 @@
} }
else { else {
deviceId = getGoodDevice(); deviceId = getGoodDevice();
} }
} }
else { else {
deviceId = getGoodDevice(); deviceId = getGoodDevice();
@ -733,14 +733,14 @@
} }
// returns /client#/home for http://www.jamkazam.com/client#/home // returns /client#/home for http://www.jamkazam.com/client#/home
context.JK.locationPath = function() { context.JK.locationPath = function () {
var bits = context.location.href.split('/'); var bits = context.location.href.split('/');
return '/' + bits.slice(3).join('/'); return '/' + bits.slice(3).join('/');
} }
context.JK.nowUTC = function() { context.JK.nowUTC = function () {
var d = new Date(); var d = new Date();
return new Date( d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds() ); return new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds());
} }
/* /*
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message

View File

@ -78,8 +78,8 @@
var clicked = $(this); var clicked = $(this);
var href = clicked.attr('href'); var href = clicked.attr('href');
if(href != "#") { if(href != "#") {
context.JK.GA.trackDownload(clicked.attr('data-platform'));
rest.userDownloadedClient().always(function() { rest.userDownloadedClient().always(function() {
context.JK.GA.trackDownload(clicked.attr('data-platform'));
$('body').append('<iframe class="downloading" src="' + clicked.attr('href') + '" style="display:none"/>') $('body').append('<iframe class="downloading" src="' + clicked.attr('href') + '" style="display:none"/>')
}); });
} }

View File

@ -42,6 +42,10 @@
-webkit-border-radius:50%; -webkit-border-radius:50%;
-moz-border-radius:50%; -moz-border-radius:50%;
border-radius:50%; border-radius:50%;
&.highlighted {
background-color:white;
}
} }
.expander { .expander {

View File

@ -10,7 +10,8 @@ end
# give back more info if the user being fetched is yourself # give back more info if the user being fetched is yourself
if @user == current_user if @user == current_user
attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :subscribe_email, :auth_twitter attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings, :show_whats_next, :subscribe_email, :auth_twitter, :new_notifications
elsif current_user elsif current_user
node :is_friend do |uu| node :is_friend do |uu|

View File

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

View File

@ -0,0 +1,72 @@
/*global jQuery*/
/*jshint curly:false*/
;(function ( $, window) {
"use strict";
var defaults = {
pulses : 1,
interval : 0,
returnDelay : 0,
duration : 500
};
$.fn.pulse = function(properties, options, callback) {
// $(...).pulse('destroy');
var stop = properties === 'destroy';
if (typeof options === 'function') {
callback = options;
options = {};
}
options = $.extend({}, defaults, options);
if (!(options.interval >= 0)) options.interval = 0;
if (!(options.returnDelay >= 0)) options.returnDelay = 0;
if (!(options.duration >= 0)) options.duration = 500;
if (!(options.pulses >= -1)) options.pulses = 1;
if (typeof callback !== 'function') callback = function(){};
return this.each(function () {
var el = $(this),
property,
original = {};
var data = el.data('pulse') || {};
data.stop = stop;
el.data('pulse', data);
for (property in properties) {
if (properties.hasOwnProperty(property)) original[property] = el.css(property);
}
var timesPulsed = 0;
function animate() {
if (typeof el.data('pulse') === 'undefined') return;
if (el.data('pulse').stop) return;
if (options.pulses > -1 && ++timesPulsed > options.pulses) return callback.apply(el);
el.animate(
properties,
{
duration : options.duration / 2,
complete : function(){
window.setTimeout(function(){
el.animate(original, {
duration : options.duration / 2,
complete : function() {
window.setTimeout(animate, options.interval);
}
});
},options.returnDelay);
}
}
);
}
animate();
});
};
})( jQuery, window, document );

View File

@ -565,9 +565,30 @@ module JamWebsockets
raise SessionError, 'connection state is gone. please reconnect.' raise SessionError, 'connection state is gone. please reconnect.'
else else
Connection.transaction do Connection.transaction do
music_session = MusicSession.select(:track_changes_counter).find_by_id(connection.music_session_id) if connection.music_session_id # send back track_changes_counter if in a session
track_changes_counter = music_session.track_changes_counter if music_session if connection.music_session_id
music_session = MusicSession.select(:track_changes_counter).find_by_id(connection.music_session_id)
track_changes_counter = music_session.track_changes_counter if music_session
end
# update connection updated_at
connection.touch 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
end end
ConnectionManager.active_record_transaction do |connection_manager| ConnectionManager.active_record_transaction do |connection_manager|