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

View File

@ -291,7 +291,12 @@ module JamRuby
recordings.concat(msh)
recordings.sort! {|a,b| b.created_at <=> a.created_at}.first(5)
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!
self.email_confirmed = true
end

View File

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

View File

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

View File

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

View File

@ -28,6 +28,7 @@
var lastHeartbeatFound = false;
var heartbeatAckCheckInterval = null;
var userDeferred = null;
var notificationLastSeenAt = undefined;
var opts = {
inClient: true, // specify false if you want the app object but none of the client-oriented features
@ -92,7 +93,8 @@
function _heartbeat() {
if (app.heartbeatActive) {
var message = context.JK.MessageFactory.heartbeat();
var message = context.JK.MessageFactory.heartbeat(notificationLastSeenAt);
notificationLastSeenAt = undefined;
context.JK.JamServer.send(message);
lastHeartbeatFound = false;
}
@ -384,6 +386,26 @@
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 () {
logger.debug("window.unload function called.");

View File

@ -425,6 +425,7 @@
unstackDialogs($overlay);
$dialog.hide();
dialogEvent(dialog, 'afterHide');
$(me).triggerHandler('dialog_closed', {dialogCount: openDialogs.length})
}
function screenEvent(screen, evtName, data) {
@ -526,6 +527,10 @@
}
}
function isDialogShowing() {
return openDialogs.length > 0;
}
/**
* 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
@ -849,6 +854,10 @@
showDialog(dialog, options);
};
this.isDialogShowing = function() {
return isDialogShowing();
}
this.close = function (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 invitationDialog = null;
var textMessageDialog = null;
var notificationPanel = null;
function initializeSearchPanel() {
$('#search_text_type').change(function() {
@ -116,23 +117,8 @@
}
function initializeNotificationsPanel() {
// retrieve pending notifications for this user
var url = "/api/users/" + context.JK.currentUserId + "/notifications"
$.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
});
notificationPanel = new context.JK.NotificationPanel(app);
notificationPanel.initialize();
}
function updateNotificationList(response) {
@ -362,12 +348,8 @@
$('#sidebar-search-results').height('0px');
}
function incrementNotificationCount() {
var count = parseInt($('#sidebar-notification-count').html());
$('#sidebar-notification-count').html(count + 1);
}
function decrementNotificationCount() {
/**
var count = parseInt($('#sidebar-notification-count').html());
if (count === 0) {
$('#sidebar-notification-count').html(0);
@ -375,6 +357,13 @@
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
@ -402,6 +391,13 @@
$('#sidebar-notification-list').prepend(notificationHtml);
if(userCanSeeNotifications()) {
app.updateNotificationSeen(payload.created_at);
}
else {
}
initializeActions(payload, type);
return true;

View File

@ -153,9 +153,9 @@
$element.bt(text, options);
}
context.JK.bindHoverEvents = function ($parent) {
context.JK.bindHoverEvents = function ($parent) {
if(!$parent) {
if (!$parent) {
$parent = $('body');
}
@ -328,9 +328,9 @@
}
// creates an array with entries like [{ id: "drums", description: "Drums"}, ]
context.JK.listInstruments = function() {
context.JK.listInstruments = function () {
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});
});
return instrumentArray;
@ -652,22 +652,22 @@
return hasFlash;
}
context.JK.hasOneConfiguredDevice = function() {
context.JK.hasOneConfiguredDevice = function () {
var result = context.jamClient.FTUEGetGoodConfigurationList();
logger.debug("hasOneConfiguredDevice: ", result);
return result.length > 0;
};
context.JK.getGoodAudioConfigs = function() {
context.JK.getGoodAudioConfigs = function () {
var result = context.jamClient.FTUEGetGoodAudioConfigurations();
logger.debug("goodAudioConfigs=%o", result);
return result;
};
context.JK.getGoodConfigMap = function() {
context.JK.getGoodConfigMap = function () {
var goodConfigMap = [];
var goodConfigs = context.JK.getGoodAudioConfigs();
$.each(goodConfigs, function(index, profileKey) {
$.each(goodConfigs, function (index, profileKey) {
var friendlyName = context.jamClient.FTUEGetConfigurationDevice(profileKey);
goodConfigMap.push({key: profileKey, name: friendlyName});
});
@ -675,12 +675,12 @@
return goodConfigMap;
}
context.JK.getBadAudioConfigs = function() {
context.JK.getBadAudioConfigs = function () {
var badAudioConfigs = [];
var allAudioConfigs = context.jamClient.FTUEGetAllAudioConfigurations();
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) {
badAudioConfigs.push(allAudioConfigs[i]);
}
@ -689,10 +689,10 @@
return badAudioConfigs;
};
context.JK.getBadConfigMap = function() {
context.JK.getBadConfigMap = function () {
var badConfigMap = [];
var badConfigs = context.JK.getBadAudioConfigs();
$.each(badConfigs, function(index, profileKey) {
$.each(badConfigs, function (index, profileKey) {
var friendlyName = context.jamClient.FTUEGetConfigurationDevice(profileKey);
badConfigMap.push({key: profileKey, name: friendlyName});
});
@ -700,7 +700,7 @@
return badConfigMap;
}
context.JK.getFirstGoodDevice = function(preferredDeviceId) {
context.JK.getFirstGoodDevice = function (preferredDeviceId) {
var badConfigs = context.JK.getBadAudioConfigs();
function getGoodDevice() {
@ -713,7 +713,7 @@
}
return deviceId;
}
var deviceId = null;
if (preferredDeviceId) {
@ -724,7 +724,7 @@
}
else {
deviceId = getGoodDevice();
}
}
}
else {
deviceId = getGoodDevice();
@ -733,14 +733,14 @@
}
// returns /client#/home for http://www.jamkazam.com/client#/home
context.JK.locationPath = function() {
context.JK.locationPath = function () {
var bits = context.location.href.split('/');
return '/' + bits.slice(3).join('/');
}
context.JK.nowUTC = function() {
context.JK.nowUTC = function () {
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

View File

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

View File

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

View File

@ -10,7 +10,8 @@ end
# give back more info if the user being fetched is yourself
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
node :is_friend do |uu|

View File

@ -311,6 +311,10 @@
}
JK.bindHoverEvents();
setInterval(function() {
console.log("IN FOCUS: " + document.hasFocus());
}, 1000)
})
</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.'
else
Connection.transaction do
music_session = MusicSession.select(:track_changes_counter).find_by_id(connection.music_session_id) if connection.music_session_id
track_changes_counter = music_session.track_changes_counter if music_session
# send back track_changes_counter if in a 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
# 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
ConnectionManager.active_record_transaction do |connection_manager|