* VRFS-1473 - notifications should be highlighted, wip
This commit is contained in:
parent
d365054237
commit
526f6fe577
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE users ADD COLUMN notification_seen_at TIMESTAMP;
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -934,6 +934,7 @@
|
|||
}
|
||||
|
||||
function getNotifications(options) {
|
||||
if(!options) options = {};
|
||||
var id = getId(options);
|
||||
return $.ajax({
|
||||
type: "GET",
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"/>')
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,10 @@
|
|||
-webkit-border-radius:50%;
|
||||
-moz-border-radius:50%;
|
||||
border-radius:50%;
|
||||
|
||||
&.highlighted {
|
||||
background-color:white;
|
||||
}
|
||||
}
|
||||
|
||||
.expander {
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -311,6 +311,10 @@
|
|||
}
|
||||
|
||||
JK.bindHoverEvents();
|
||||
|
||||
setInterval(function() {
|
||||
console.log("IN FOCUS: " + document.hasFocus());
|
||||
}, 1000)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 );
|
||||
|
|
@ -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|
|
||||
|
|
|
|||
Loading…
Reference in New Issue