Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Disabling bugzilla, adding mentions.

  • Loading branch information...
commit ad42a3e1768737076d9d77f98fc4ae95d33e6d2b 1 parent 57a214e
@simonwex simonwex authored
View
2  app/feeds/daemon.js
@@ -5,7 +5,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
require('../../lib/extensions/number');
-require('./bugzilla');
+// require('./bugzilla');
var
maxStoryAge = (1).hours(),
View
71 app/http/controllers/profile.js
@@ -14,7 +14,35 @@ Redis = require('../../../lib/redis');
*/
exports.index = {
get: function(req, res){
- res.render('profile/index', { user: req.user });
+ mysql.query(
+ "SELECT name FROM irc_channels",
+ function(err, rows){
+ if (err){
+ logger.error("Error retrieving IRC Channels in profile.js: " + err);
+ }
+
+ for(var i in rows){
+ rows[i].displayName = rows[i].name.substr(1);
+ }
+
+ mysql.query(
+ "SELECT title, url, verified FROM feeds WHERE user_id = ?",
+ [req.user.id],
+ function(err, feedRows){
+ mysql.query(
+ "SELECT token FROM watched_tokens WHERE user_id = ?",
+ [req.user.id],
+ function(err, tokens){
+ for(var i in tokens){
+ tokens[i] = tokens[i].token;
+ }
+ res.render('profile/index', { user: req.user, channels: rows, feeds: feedRows, watchedTokens: tokens});
+ }
+ );
+ }
+ );
+ }
+ );
},
put: function(req, res){
// Only allow the saving of nick and realName
@@ -32,6 +60,47 @@ exports.index = {
}
};
+exports.watchedTokens = {
+ put: function(req, res){
+ console.log(req.body);
+ var tokens = req.body.tokens;
+ var values = [];
+ var placeholders = [];
+ for (var i in tokens){
+ //Validate
+ if (tokens[i].match(/^\w[\w\-\_]+$/)){
+ values.push(req.user.id)
+ values.push(tokens[i]);
+ placeholders.push('(?, ?)');
+ }
+ else {
+ res.send('"ERROR"', {status: 500});
+ return;
+ }
+ }
+
+ mysql.query(
+ "DELETE FROM watched_tokens WHERE user_id = ?",
+ [req.user.id],
+ function(err, result){
+ if (err)
+ logger.error("Error deleting old tokens");
+
+ mysql.query(
+ "INSERT INTO watched_tokens (user_id, token) VALUES " + placeholders.join(','),
+ values,
+ function(err, result){
+ if (err)
+ logger.error("Error inserting new tokens: " + err);
+
+ res.send('"OK"');
+ }
+ );
+ }
+ );
+ }
+}
+
/*
* POST
*/
View
33 app/http/controllers/site.js
@@ -3,9 +3,10 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
const
-createRedisClient = require('../../../lib/redis'),
-
+redis = require('../../../lib/redis'),
+uuid = require('node-uuid'),
logger = require('../../../lib/logger'),
+crypto = require('crypto'),
config = require('../../../lib/configuration');
/*
@@ -25,8 +26,7 @@ exports.authenticate = function(req, res) {
exports.signout = function(req, res){
if (req.user){
- var redis = createRedisClient();
- redis.publish('user.signout', req.user.id.toString(), function(err){
+ redis.pub.publish('user.signout', req.user.id.toString(), function(err){
if (err)
logger.error(err);
});
@@ -34,3 +34,28 @@ exports.signout = function(req, res){
req.logout();
res.redirect('/');
};
+
+
+exports.driver = function(req, res){
+ var storyId = null;
+
+ if ('story' in req.body){
+ storyId = req.user.email + ":" + uuid.v1()
+ var hash = crypto.createHash('md5').update(req.user.email).digest("hex");
+
+ var story = {
+ id: storyId,
+ people: req.user.email,
+ title: req.body.story.title,
+ href: req.body.story.url,
+ pubdate: new Date(),
+ image: {url: "http://www.gravatar.com/avatar/" + hash + "?s=30"}
+ };
+
+ redis.io.lpush("serializer:stories", JSON.stringify({'userIds': [req.user.id], 'story': story}), function (err, reply) {
+ if (err)
+ logger.error(err);
+ });
+ }
+ res.render('site/driver', {user: req.user, lastId: storyId});
+}
View
16 app/http/controllers/social.js
@@ -25,23 +25,22 @@ exports.manifest = function(req, res){
res.render('social/manifest.json.ejs', { baseUrl: config.get("public_url"), providerSuffix: config.get("social_provider")['name_suffix'], layout: false });
};
-exports.bugs = function(req, res){
- mysql.query('SELECT * FROM stories WHERE user_id = ? AND durable = ? ORDER BY seen_at, published_at', [req.user.id, true], function(err, rows){
+exports.mentions = function(req, res){
+ mysql.query('SELECT * FROM stories WHERE user_id = ? AND durable = ? ORDER BY seen_at, published_at limit 200', [req.user.id, true], function(err, rows){
if (err){
logger.error('Erorr getting stories');
}
- var bugs = [];
+ var mentions = [];
for (var i in rows){
var story = JSON.parse(rows[i].data);
story.seen_at = rows[i].seen_at;
- bugs.push(story);
+ mentions.push(story);
}
- res.render('social/bugs', {user: req.user, bugs: bugs, layout: false});
+ res.render('social/mentions', {user: req.user, mentions: mentions, layout: false});
});
};
-exports.markBugAsViewed = function(req, res){
- console.log(req.body);
+exports.markMentionAsViewed = function(req, res){
if (req.user){
mysql.query(
"UPDATE stories SET seen_at = NOW() WHERE user_id = ? and id = ?",
@@ -50,8 +49,7 @@ exports.markBugAsViewed = function(req, res){
if (err){
logger.error('Error marking story as read: ' + err);
}
- var redis = createRedisClient();
- redis.publish('notifications.bugzilla.read', req.body.id, function(err){
+ redis.publish('notifications.mention.read', req.user.id, function(err){
if (err)
logger.error(err);
});
View
144 app/http/public/javascripts/application.js
@@ -2,9 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
-var DATA = {
- profile: {}
-}
$(function(){
$("#browserid").click(function(){
@@ -26,12 +23,26 @@ $(function(){
$('body').addClass(controller);
$('ul.nav li.' + controller).addClass('active');
+ if (typeof($(".chzn-select").chosen) == 'function'){
+ $(".chzn-select").chosen();
+ }
+
// Page-specific stuff:
switch(document.location.pathname){
case '/profile':
+
+ $('#user_form').submit(function(){
+ saveUser();
+ return false;
+ });
+
var updateLastSaved = function(){
- $('#changes_saved').text("Changes last saved at " + $.format.date(new Date(), 'hh:mm:ssa'));
+
+ $('#changes_saved .timestamp').text($.format.date(new Date(), 'hh:mm:ssa'));
$('#changes_saved').show();
+ setTimeout(function(){
+ $('#changes_saved').fadeOut();
+ }, 2000);
};
var saveUser = function(){
@@ -52,21 +63,34 @@ $(function(){
data: $("#user_form").serialize(),
success: function(data){
$('#error_contacting_nickserv').hide();
- updateLastSaved();
- DATA.profile['realNameFromIRC'] = data['realName'];
+
+ var changeRealName = ($('#user_real_name').val().length == 0);
+
$('#user_real_name').attr('placeholder', 'Real Name');
- if ($('#source_from_irc').is(':checked')){
+ if (changeRealName){
$('#user_real_name').val(data['realName']);
}
- var ul = $('#networks ul');
+ var ul = $('#channels');
ul.empty();
for(var i in data.networks){
var network = data.networks[i];
+ var option = $("#autojoin_channels option[value=" + network + "]");
+
+ if (option.length > 0){
+ option.attr('selected', 'selected');
+ }
+ else{
+ // HACKHACK: This shouldn't be required, but it seems that the full list isn't properly returning from the IRC client...
+ $("#autojoin_channels").append($("<option selected=\"selected\" value=\"" + network + "\">" + network + "</option>"));
+ }
ul.append("<li>" + network + "</li>");
}
- $('#networks').show();
+
+ $('.chzn-select').trigger("liszt:updated");
+
+ $('#channels').show();
},
error: function(err){
$('#error_contacting_nickserv').show();
@@ -79,27 +103,15 @@ $(function(){
return false;
};
- $('#source_from_irc').change(function(){
- if ($('#source_from_irc').is(':checked')){
- if (DATA.profile.realNameFromIRC){
- $('#user_real_name').val(DATA.profile.realNameFromIRC);
- }
- else{
- postNick();
- }
- $('#user_real_name').attr('disabled', true);
- }
- else{
- $('#user_real_name').attr('disabled', false);
- }
- });
- $('#user_real_name').change(function(){
- saveUser();
- });
- $('#user_nick').change(postNick);
+ var nick = $('#user_nick');
+ nick.change(postNick);
+
+ if (nick.val().length > 0){
+ postNick();
+ }
+
+ $('#user_real_name').change(saveUser);
- break;
- case '/feeds':
$('.feeds #new_feed_form').click(function(){
createBlankFeedForm();
return false;
@@ -123,7 +135,6 @@ $(function(){
console.log("Error removing");
}
});
- console.log();
return false;
}
@@ -136,7 +147,7 @@ $(function(){
.removeClass('verified')
.removeClass('error')
.find('.delete').hide();
- console.log(form.serialize());
+
var reqData = form.serialize();
form.find('input[type=hidden]').val($(this).val());
@@ -146,7 +157,6 @@ $(function(){
url: '/feeds/feed',
data: reqData,
success: function(data){
- console.log(data);
if (data.url == ""){
form.remove();
}
@@ -171,18 +181,17 @@ $(function(){
$('.feeds form input[name=url]').change(validateAndSaveUrl);
- // $('input[value="Whatever"]');
- var template = $('#feed_template');
- template.removeAttr('id').remove();
+ var feedTemplate = $('#feed_template');
+ feedTemplate.removeAttr('id').remove();
function createBlankFeedForm(){
var blankInput = $('.feeds input[type=text][value=]');
- if (blankInput.length > 1){
+ if (blankInput.length > 0){
blankInput.focus();
return;
}
- var newFeed = template.clone();
+ var newFeed = feedTemplate.clone();
newFeed.find('input[name=url]').change(validateAndSaveUrl);
newFeed.find('.delete').click(deleteUrl);
newFeed.find('form').submit(function(){
@@ -190,10 +199,71 @@ $(function(){
});
$('ul.feeds').append(newFeed);
newFeed.show();
+ $(newFeed).focus();
}
createBlankFeedForm();
+ function validateAndSaveTokens(){
+ $('.mentionTokens li').removeClass('error');
+ var inputs = $('.mentionTokens li input[type=text]');
+ var tokens = [];
+ for (var i=0; i<inputs.length; i++){
+ if($(inputs[i]).val().match(/^\w[\w\-\_]+$/)){
+ tokens.push($(inputs[i]).val());
+ }
+ else{
+ $(inputs[i]).parent().parent().addClass('error');
+ return false;
+ }
+ }
+
+ tokens;
+ $.ajax({
+ url: '/profile/watchedTokens',
+ type: 'PUT',
+ data: {tokens: tokens},
+ success: updateLastSaved,
+ error: console.log
+ });
+
+ }
+
+ $('#mentions #new_token_input').click(function(){
+ createBlankTokenItem();
+ return false;
+ });
+
+
+ function deleteToken(){
+ $(this).parent().remove();
+ validateAndSaveTokens();
+ return false;
+ }
+
+ var tokenTemplate = $('#mention_token_template');
+ tokenTemplate.removeAttr('id').remove();
+
+ $('#mentions form').submit(function(){return false;})
+
+ function createBlankTokenItem(){
+ var blankInput = $('.mentionTokens input[type=text][value=]');
+ if (blankInput.length > 0){
+ blankInput.focus();
+ return;
+ }
+
+ var newTokenInput = tokenTemplate.clone();
+ newTokenInput.find('input').change(validateAndSaveTokens);
+ newTokenInput.find('.delete').click(deleteToken);
+ $('ul.mentionTokens').append(newTokenInput);
+ newTokenInput.show();
+ newTokenInput.focus();
+ }
+
+ $('ul.mentionTokens .delete').click(deleteToken);
+ $('ul.mentionTokens input[type=text]').change(validateAndSaveTokens);
+
break;
}
View
3  app/http/public/stylesheets/notifications.css
@@ -1,3 +1,6 @@
+h1 {
+ font-size: 2em;
+}
.notifications {
height: 400px;
overflow-y: auto;
View
36 app/http/public/stylesheets/style.css
@@ -44,33 +44,49 @@ body.profile li.search-choice span {
padding-right: 12px;
}
-body.feeds ul.feeds input[type=text]{
+body.profile ul.feeds input[type=text]{
width: 630px;
}
-body.feeds div.feeds {
+body.profile div.feeds {
display: inline;
margin-left: 25px;
}
-body.feeds ul.feeds {
- width: 805px;
+
+body.profile ul {
list-style-type: none;
+}
+
+body.profile ul.mentionTokens li{
+ margin-bottom: 1em;
+}
+
+body.profile ul.feeds {
+ width: 805px;
margin-bottom: 35px;
}
-body.feeds ul.feeds form{
+body.profile .feedcontainer ul form{
padding-left: 59px;
}
-body.feeds ul.feeds form.verified, body.feeds ul.feeds form.error {
+body.profile .feedcontainer ul form.verified, body.profile .feedcontainer ul form.error {
padding-left: 19px;
}
-body.feeds ul.feeds .btn-success, body.feeds ul.feeds .btn-error {
+body.profile .feedcontainer ul .btn-success, body.profile .feedcontainer ul .btn-error {
display: none;
}
-body.feeds ul.feeds .verified .btn-success, body.feeds ul.feeds .error .btn-danger {
+body.profile .feedcontainer ul .verified .btn-success, body.profile .feedcontainer ul .error .btn-danger {
display: inline;
}
-body.feeds .feedcontainer {
+body.profile .mentionTokens .errorMessage{
+ display: none;
+}
+
+body.profile .mentionTokens .error .errorMessage{
+ display: block;
+}
+
+body.profile .feedcontainer {
min-height: 200px;
padding-bottom: 100px;
background-repeat: no-repeat;
@@ -78,6 +94,6 @@ body.feeds .feedcontainer {
background-image: url('/images/rss-bot-montage.jpg');
}
-body.feeds form.well {
+body.profile form.well {
background-color: rgba(245, 245, 245, 0.8);
}
View
14 app/http/server.js
@@ -74,7 +74,10 @@ passport.use(new BrowserID({
var http = express.createServer();
+var redisConfig = config.get('redis');
var sessionStore = new RedisStore({
+ host: redisConfig.host,
+ port: redisConfig.port,
maxAge: (30).days
});
@@ -124,22 +127,23 @@ routes = {
feeds: require('./controllers/feeds')
};
-
-http.get('/', routes.site.index);
+http.get('/', routes.site.index);
http.get('/signout', routes.site.signout);
http.post('/auth/browserid', passport.authenticate('browserid', { failureRedirect: '/login' }), routes.site.authenticate);
+http.get( '/driver', application.authenticate, routes.site.driver);
+http.post('/driver', application.authenticate, routes.site.driver);
http.get('/social/worker.js', routes.social.worker);
http.get('/social/sidebar', routes.social.sidebar);
-http.get('/social/bugs', routes.social.bugs);
-http.post('/social/bug', routes.social.markBugAsViewed);
+http.get('/social/mentions', routes.social.mentions);
+http.post('/social/mention', routes.social.markMentionAsViewed);
http.get('/social/manifest.json', routes.social.manifest);
http.get('/profile', application.authenticate, routes.profile.index.get);
http.put('/profile', application.authenticate, routes.profile.index.put);
http.post('/profile/nick', application.authenticate, routes.profile.nick.post);
-
+http.put('/profile/watchedTokens', application.authenticate, routes.profile.watchedTokens.put);
http.get('/feeds', application.authenticate, routes.feeds.index.get);
http.post('/feeds/feed', application.authenticate, routes.feeds.feed.post);
View
70 app/http/socket.js
@@ -50,24 +50,15 @@ function broadcast(message){
* Story Stuff: This should really get rolled up into a model.
*/
function sendStoryToUser(userId, story){
- var message = {
- data: story
- };
-
- if (story.durable){
- // We're only doing bugzilla right now
- message['topic'] = 'notifications.bugzilla';
- }
- else {
- message['topic'] = 'feed.story';
- }
-
- sendMessageToUser(userId, JSON.stringify(message));
+ sendMessageToUser(userId, JSON.stringify({
+ topic: 'feed.story',
+ data: story
+ }));
}
function subscribeForStories(){
pubsubRedis.on("ready", function(){
- pubsubRedis.subscribe(["feeds.storyForUser", "notifications.bugzilla.read", "user.signout", 'contacts.userStatusUpdate', 'contacts.userOffline', 'contacts.reset']);
+ pubsubRedis.subscribe(["feeds.storyForUser", "notifications.mention.read", "notifications.mention.new", "user.signout", 'contacts.userStatusUpdate', 'contacts.userOffline', 'contacts.reset']);
pubsubRedis.on("message", function(channel, message){
switch(channel){
@@ -76,6 +67,14 @@ function subscribeForStories(){
sendStoryToUser(data.userId, data.story);
break;
+ case "notifications.mention.read":
+ var userId = parseInt(message);
+ sendMessageToUser(userId, '{"topic":"notifications.mention.read"}');
+ break;
+ case "notifications.mention.new":
+ var userId = parseInt(message);
+ sendMessageToUser(userId, '{"topic":"notifications.mention.new"}');
+ break;
case "user.signout":
var userId = parseInt(message);
@@ -101,9 +100,6 @@ function subscribeForStories(){
case "contacts.userOffline":
broadcast(message);
break;
- case "notifications.bugzilla.read":
- broadcast({topic: 'notifications.bugzilla.read', data: {id: message}});
- break;
default:
logger.error("Redis message received in socket.js on unexpected channel: " + channel);
}
@@ -138,6 +134,7 @@ function sendContactListToUser(userId){
if (rows && rows.length){
for (var i in rows){
+ logger.debug('User: ' + rows[i].nick);
sendMessageToUser(userId, JSON.stringify({
topic: 'contacts.userStatusUpdate',
data: {
@@ -154,8 +151,8 @@ function sendContactListToUser(userId){
);
}
-function sendRecentStories(user, durable){
- mysql.query("SELECT * FROM stories WHERE user_id = ? AND durable = ? AND seen_at IS NULL ORDER BY published_at DESC LIMIT 30", [user.id, durable], function(err, rows){
+function sendRecentStories(user){
+ mysql.query("SELECT * FROM stories WHERE user_id = ? AND durable = ? AND seen_at IS NULL ORDER BY published_at DESC LIMIT 30", [user.id, false], function(err, rows){
if (err){
logger.error("Error loading stories for user: " + user.email);
logger.error(err);
@@ -172,18 +169,37 @@ function sendRecentStories(user, durable){
});
}
+function sendNotificationCounts(user){
+ mysql.query(
+ "SELECT COUNT(*) as count FROM stories WHERE user_id = ? and durable = ? AND seen_at IS NULL",
+ [user.id, true],
+ function(err, rows){
+ if (err){
+ logger.error("Error counting notifications for user: " + user.email);
+ logger.error(err);
+ }
+
+ sendMessageToUser(user.id, JSON.stringify({
+ topic: 'notifications.count',
+ mentions: rows[0].count
+ }));
+ }
+ );
+}
+
function userConnected(user){
logger.debug("Loading stories for user. (id: " + user.id + ")");
- sendRecentStories(user, true);
- sendRecentStories(user, false);
+ sendRecentStories(user);
+ sendNotificationCounts(user);
var responseQueue = "irc-resp:" + uuid.v1();
var redis = createRedisClient();
redis.lpush("irc:user-connected", JSON.stringify([user.id, user.nick, responseQueue]));
- // We block for two minutes max.
- redis.brpop(responseQueue, 120, function(err, data){
+ // We block for twenty seconds max.
+ redis.brpop(responseQueue, 20, function(err, data){
+ redis.quit();
if (!data)
logger.error("Timeout exceeded waiting for IRC daemon to update user: " + user.id);
@@ -207,7 +223,7 @@ module.exports = {
socket.on('request', function(request){
if (!request.httpRequest.headers.cookie){
logger.info('Socket request rejected because it lacked cookie data.');
- return request.reject();
+ return request.reject(401);
}
// here we use parseCookie instead of the already parsed cookies because
@@ -217,17 +233,17 @@ module.exports = {
if (!cookies['express.sid']){
logger.info('Socket request rejected because it lacked an express.sid (Session)');
- return request.reject();
+ return request.reject(401);
}
var sessionID = cookies['express.sid'];
store.load(sessionID, function(err, session){
if (err || !session){
logger.error('Socket request rejected due to error loading session: ' + err);
- return request.reject();
+ return request.reject(500);
}
else if ( !session.passport || !session.passport.user){
logger.info('Socket request rejected. -- Passport user empty.');
- return request.reject();
+ return request.reject(401);
}
else{
var connection = request.accept();
View
1  app/http/views/layout.ejs
@@ -55,7 +55,6 @@
</li>
<% } else { %>
<li class="profile" ><a href="/profile" >Profile</a></li>
- <li class="feeds" ><a href="/feeds" >Feeds</a></li>
<li ><a href="/signout" >Sign Out</a></li>
<% } %>
</ul>
View
207 app/http/views/profile/index.ejs
@@ -1,61 +1,170 @@
-<h1>Your Profile <span id="changes_saved" class="label label-success" style="display:none"></span></h1>
-
-<div id="container" class="row">
- <div id="photo" class="span3">
- <div class="thumbnail">
- <img src="<%= user.getGravatarUrl(170) %>" width="170" height="170" />
- <div class="caption">
- <p>
- Image loaded from <a href="http://gravatar/">Gravatar</a>
- <p><a href="http://en.gravatar.com/emails/" target="_NEW" class="btn btn-primary">Manage</a>
- </p>
+<div class="page-header">
+ <h1>Your Profile</h1>
+</div>
+
+<div id="changes_saved" class="span10 alert alert-success" style="display:none">
+ <b>Profile Saved</b> at <span class="timestamp"></span>
+</div>
+
+<form id="user_form" class="form-horizontal">
+ <fieldset>
+ <section id="chat span10">
+ <div class="page-header">
+ <h1>
+ Networking
+ <small>Chat and availability</small>
+ </h1>
</div>
- </div>
- </div>
- <div id="details" class="span8" style="background-color: white">
- <p>
- MoTown uses IRC to update your availability and status. Currently we're
- only supporting this on <a href="irc://irc.mozilla.org">irc.mozilla.org</a>.
- </p>
- <form id="user_form" class="form-horizontal">
- <fieldset>
- <div class="control-group">
- <label class="control-label" for="user_nick">IRC Nick</label>
- <div class="controls">
- <input class="input-xlarge focused" id="user_nick" name="user[nick]" type="text" value="<%= user.nick %>" placeholder="Nick">
- <p class="help-block">We only support irc://irc.mozilla.org</p>
+ <p>
+ MoTown uses IRC to update your availability and chat to other Mozillians. Currently we're
+ only supporting this on <a href="irc://irc.mozilla.org">irc.mozilla.org</a>.
+ </p>
+
+ <div class="control-group">
+ <label class="control-label" for="user_nick">IRC Nickname</label>
+ <div class="controls">
+ <input class="input-xlarge focused" id="user_nick" name="user[nick]" type="text" value="<%= user.nick %>" placeholder="Nick">
+ <p class="help-block">We only support irc://irc.mozilla.org</p>
+ </div>
+ </div>
+ <!-- <div class="control-group">
+ <label class="control-label" for="irc_user">Automatically Connect to IRC For me:</label>
+ <div class="controls">
+ <label class="checkbox">
+ <input type="checkbox" id="irc_user" checked="checked" value="checked">
+ </label>
+ <p class="help-block"></p>
+ </div>
+ </div>
+
+ <div id="networks" class="control-group">
+ <label class="control-label">Auto Join Networks</label>
+ <div class="controls">
+ <select id="autojoin_channels" data-placeholder="Auto Join Networks" multiple="multiple" class="chzn-select">
+ <% for (var i in channels) { %>
+ <option value="<%= channels[i].name %>"><%= channels[i].displayName %></option>
+ <% } %>
+ </select>
+ </div>
+ </div>
+ -->
+ </section>
+ </fieldset>
+
+ <fieldset>
+ <section id="personal">
+ <div class="page-header">
+ <h1>
+ Personal Info
+ <small>your name, photo and the like</small>
+ </h1>
+ </div>
+ <div class="row">
+ <div id="photo" class="span2">
+ <div class="thumbnail">
+ <img src="<%= user.getGravatarUrl(160) %>" width="160" height="160" />
+ <div class="caption">
+ <p>
+ Image loaded from <a href="http://gravatar.com">Gravatar</a>
+ <p><a href="http://en.gravatar.com/emails/" target="_NEW" class="btn btn-primary">Manage</a>
+ </p>
</div>
</div>
- <div class="control-group">
- <label class="control-label" for="source_from_irc">Use IRC for Public Details:</label>
- <div class="controls">
- <label class="checkbox">
- <input type="checkbox" id="source_from_irc" value="checked">
- </label>
- </div>
- </div>
-
+ </div>
+ <div class="span9">
<div class="control-group">
<label class="control-label" for="name">Real Name</label>
<div class="controls">
<input class="input-xlarge" id="user_real_name" name="user[realName]" type="text" value="<%= user.realName %>" placeholder="Real Name">
- <p id="error_contacting_nickserv" style="display:none" class="help-block alert-error">
- Error contacting NickServ. Please set your Real Name manually.
- </p>
</div>
</div>
-
- <div id="networks" class="control-group" style="display: none;">
- <label class="control-label">Networks</label>
- <div class="controls">
- <ul>
- <li>#vancouver</li>
- <li>#b2g</li>
- </ul>
- <span class="help-inline">These are maintained by IRC</span>
+ </div>
+ </div>
+ </section>
+ </fieldset>
+</form>
+
+<section id="feeds">
+ <div class="page-header">
+ <h1>
+ Feeds
+ <small>RSS</small>
+ </h1>
+ <p>
+ MoTown uses RSS feeds to keep you updated. We've filled in a couple of examples for you:
+ </p>
+
+ </div>
+
+ <div class="feedcontainer listcontainer">
+ <ul class="feeds">
+ <% for (var i in feeds){
+ var feed = feeds[i];
+ %>
+ <li>
+ <form class="well form-inline <%= feed.verified ? 'verified' : 'error' %>">
+ <a class="btn btn-success" href="#"><i class="icon-ok-sign icon-white"></i></a>
+ <a class="btn btn-danger btn-error" href="#"><i class="icon-remove-sign icon-white"></i></a>
+ <input type="text" name="url" class="input-xxlarge" placeholder="URL" title="<%= feed.title %>" value="<%= feed.url%>">
+ <input type="hidden" name="previousUrl" value="<%= feed.url%>">
+ <a class="btn btn-danger delete" href="#"><i class="icon-trash icon-white"></i> Delete</a>
+ </form>
+ </li>
+ <% } %>
+ <li id="feed_template" style="display:none">
+ <form class="well form-inline">
+ <a class="btn btn-success btn-error" href="#"><i class="icon-ok-sign icon-white"></i></a>
+ <a class="btn btn-danger btn-error" href="#"><i class="icon-remove-sign icon-white"></i></a>
+ <input type="text" name="url" class="input-xxlarge" placeholder="URL">
+ <input type="hidden" name="previousUrl">
+ <a class="btn btn-danger delete" href="#"><i class="icon-trash icon-white"></i> Delete</a>
+ </form>
+ </li>
+ </ul>
+ <div class="well feeds">
+ <a href="#" id="new_feed_form" class="btn btn-primary"><i class="icon-forward icon-white"></i>New</a>
+ </div>
+ </div>
+
+</section>
+
+<section id="mentions">
+ <div class="page-header">
+ <h1>
+ Mentions
+ <small>When the net screams your name</small>
+ </h1>
+ <p>
+ MoTown checks each story and flags those with mentions of you in the UI.
+ </p>
+
+ </div>
+
+ <div class="listcontainer">
+ <form class="well form-inline <%= feed.verified ? 'verified' : 'error' %>">
+ <ul class="mentionTokens">
+ <% for (var i in watchedTokens){
+ var token = watchedTokens[i];
+ %>
+ <li class="control-group">
+ <div class="input-prepend">
+ <span class="add-on">@</span><input class="input-large" type="text" value="<%= token%>">
</div>
+
+ <a class="btn btn-danger delete" href="#"><i class="icon-trash icon-white"></i> Delete</a>
+ </li>
+ <% } %>
+ <li class="control-group" id="mention_token_template" style="display:none">
+ <div class="input-prepend">
+ <span class="add-on">@</span><input class="input-large" size="16" type="text">
</div>
- </fieldset>
- </form>
+
+ <a class="btn btn-danger delete" href="#"><i class="icon-trash icon-white"></i> Delete</a>
+ <p class="errorMessage help-block">Mentions may only contain alphanumeric characters, dashes and underscores and must begin with an alphanumeric character.</p>
+ </li>
+ </ul>
+ <a href="#" id="new_token_input" class="btn btn-primary"><i class="icon-forward icon-white"></i>New</a>
+ </form>
+
</div>
-</div>
+</section>
View
19 app/http/views/social/bugs.ejs → app/http/views/social/mentions.ejs
@@ -22,21 +22,21 @@
<![endif]-->
<script type="text/javascript">
$(function(){
- $('.bug a').click(function(e){
+ $('.mention a').click(function(e){
var li = $(this).parent();
- var id = li.attr('id').substr(4);
-
+ var id = li.attr('id').substr(8);
+
var self = $(this);
$.ajax({
type: 'POST',
- url: '/social/bug',
+ url: '/social/mention',
data: {'id': id},
success: function(data){
self.removeClass('new');
},
error: function(){
//TODO: Do something intelligent in the UI.
- console.log("Error marking bug as read.");
+ console.log("Error marking mention as read.");
}
});
@@ -49,11 +49,12 @@
<body>
<div id="notif">
+ <h1>Mentions:</h1>
<ul class="notifications">
- <% for (var i in bugs){ %>
- <% var readClass = bugs[i].seen_at ? '' : 'new' %>
- <li id="bug_<%= bugs[i].id%>" class="bug">
- <a target="_blank" href="<%= bugs[i].href %>" class="<%= readClass%>"><%= bugs[i].title%></a>
+ <% for (var i in mentions){ %>
+ <% var readClass = mentions[i].seen_at ? '' : 'new' %>
+ <li id="mention_<%= mentions[i].id%>" class="mention">
+ <a target="_blank" href="<%= mentions[i].href %>" class="<%= readClass%>"><%= mentions[i].title%></a>
</li>
<% } %>
</ul>
View
30 app/http/views/social/worker.js.ejs
@@ -15,6 +15,7 @@
var SPRITES = {
bugIcon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NUFBMzNCQ0JCOThEMTFFMThFRUI5Njk2MEUzNzYyNUQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NUFBMzNCQ0NCOThEMTFFMThFRUI5Njk2MEUzNzYyNUQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo1QUEzM0JDOUI5OEQxMUUxOEVFQjk2OTYwRTM3NjI1RCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo1QUEzM0JDQUI5OEQxMUUxOEVFQjk2OTYwRTM3NjI1RCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Piq9YVIAAB6mSURBVHjaxHsJeFXlue63h0w7e2ciCQkBQhgNQwBxKAKK4FBF76kX5FZubVFr7bH2sdV67lH6VM+ttddTzyk9TsWKh2prUalDbZkagoCEeUggYQpJSCDzvLOTPa217vv+a63tBhGV9nDWk//Z2Wuv9a//e7/ve7/v+/9/OQzDkEt5rFixQnp7eyenpqZOc7vdYxwOR5KmaU2hUOhQMBjc++CDDwbS0tIu2XgcFwsA73vqqaekubn5k84cDnE6neJyuVSDgOrc0qVLZfz48bJ+/fo7161bd//g4OAMCJmVnJysfo9EItLf3x+MRqPHZ86c+cdbb731efze/fTTTwvOia7r6tP+/9wx33PPPYL7Li0AHMjQoUOlo6PjrPMUmudzc3MlJydHvF6vGuDhw4dfPHHixIMpKSly+vRpSUhIUGDZYIbDYXUPQYGFVNx22223A7BGAsxntLe3S1NTk7ru3OOVV16R+++//9IDQK2ePHlSafu5556TwsJCSUpKUkLQjDngNWvWTOru7v7nvr6+b1DbDQ0NEggEYhbDxjGwEZQRI0aIx+Ohtg8AkB/dddddZSNHjqSFCFxE4Crq8+WXX5YtW7aoflatWiXf+ta3LkoO99/iPzZ4FHrJkiVKg/ZRXl6esmHDhscBxg+gad/OnTuVsLQKApWdna0E5TkKBK1La2urVFVVCVxEiouLpwPg0o8//vgPCxcu/Oe5c+c2xj97165dMQD+lsP99yASatvWKi3j1VdfHfnuu++uGjVq1PXU6pgxY5Qb0D1AfkrTNoB2s+8dGBhQja6C7w4As2T58uVfqa6uvufuu+/eSuviQfexree/FQCaPwebnp6uvj/++ONFmzdv/rCkpGQSBSXB0W8TExNjJhxv9vEN0UD9xv5s0qOg6Hs0XOlPjY2Ni3/yk59sJJAEgs/mNZccgK1bt6pPaoqD4MD/8pe/ZIHkpn/wwQfPUnhbwxSY1sH/y8rKlDXQz+2B8zw1T8G7urqkoqJCZs+eHSNJkig/YWXppaWlb+L671955ZXbW1paGngN+6mpqRH8JnPmzFHu+F8OAIW+4oor1CcGPzwjI+Ofnn/++f8FosstKChQArFR8yQvnFca4+B4D89RcNsSeC1BpLA+n08xPi2GmqaQ/OR1cJ8h77///psIpQGA8lf0+Qz4Yg9BJVFSIV8WgIuKAi+88IIPYe1ZMPpoCDkdzVNbW3t0yJAhM0BuDroDBaBgNHuGMQJga8zOF+J5gOdtQQkYwSO3ZGZmqvPsy+/3S09PDwE8it+SQagj8X/p6NGj+/Lz85chLzl+SSwAgi9EaPtHsvjRo0cP19XVLYBmX4WQDg6Ywti+T4EoPAdPoQnC+QCgBVCD9r32dYwOBICN1zBCII/w4Nr5eNYTSIDu4W/Hjx8fQD/fuiQcQA3A5/uBthdA/BSnRuPhN9J0ORgKws/4jJDmTdO3eSM+B7CzOwrO/6lFgsvvFNq+hve3tbXRqkbimYv5bPDRIlzrQ8TZxedcEgBAUjtBbFevXr06HYPbgVPr7TDGQZL0+D/dgIxuNwpFDVKw+INC2kTI6wgQ3YDuw75IpDZnxN37INqLeN7VixcvzrzzzjvLLwYAx98SRydMmEDTm4p/d8OkE+2QaJs3P21LIBg2s9MKCIRt/hSMv9n3UmA22x0Ijg0wwbKtAsdStN++/fbbAgAubR5gDwbH/0RLtM2UwtkFkU1qNhna4ZDnKaydEFFQWg7DIA+b+dl/fAFkf8YpjVL/ls/8b0mEYIwufNxwbmocP2AKQS1Sy0x/hw0bppidANgma5MbCY8+jhivgOQ98f2dIzyPq9GGuRMSmv7LANizZ4/YCHNAjP/0ZWo51+VKaRApiGdznrc/bXMlATJOI0yqiBD/m30vwUA+oVJlAsSKkRZhm/95hOeRiaBXUNPQ0FR55IgYiDr2eNl3UVGRKpNZnV40AMi6znu+6cknZ/xHbe3PnhYZvvYc1xCyO0kN3wkWB+CAS/T09OJMzwULK7uPrKwsZf6M+zYIn+IgWOCvnbKyfcWK//tuV9eaAboVSdIqsK677roLCn9RLsBhNn7ve4uaX3rptRGeRN9PHW65LBKVncwP0NrRwra5Ol0yCH9u6O0TA6xOanQqswZA+HDZLqOggsvEzNx8EpNlnYRqAZBIlYtpcmTeJS6nTEhJnDLiVP07mW/87mc777v3xxFYm8tKs8lDf3cOqPvhD2e3vPTSGxO/fW9y6nVzZRhM9fKaE+Kvq5eulmZp6+qUFmi6PapJDyTp7OuVbqNXBkl25A3IGnI4FZBhgJHIfICps2JShE78kGAJm4Frh+Azy+2UIQAzz+eVobCMrKG5ko6S2jFmrESLxki48ZQYP316WXDlys59DzzwS+NLhMMvBUDrhg1ptcuX/2baokXJ6d/5rtKMY/QYkWuuER/8ztfvl8JumHhnp0g3GL2nW8BsIv4+NL/IYBAVVECiSJiMXr90KMZ3ii+sidML8x2SI5KeJpKEkheaFM4Nsspky8wCEoAjCzbgw3lwhTC0AkzXtKmSB+D6n3zq561lZVvqb7ttP1j17wtAHwSofvjhh4bn5F6WwemnKJid/kYigwsoM3UnmgOFpiQ/H6oOoQVNwYMDsU/3iRMi27ZJ8mAYfWiSADN3FqOAnH45BEsRSfGIJLMlWy1JCSsJSZxzE+UjnBrjsxlKtah4bv6q5H+8PWl4aenP6ktKbgHjfiG5nF/Y9N9/PyNw7NiDBd99QJzo3IGHd8O8X1izRv7jnXekhzFcj1oCD5qN0YNtEC1I4XEuFFYC6vBf+jyFUbEg1aMEMUGyAbPawKB5nn3Dvw18rt74V/n562/IifpTphBwl7RvflPynM6bM7dunRW20u0vDIBdwtpzbmz24cf/p1588bairKyCZNTqOgcCrVWfrJOyfftlM2r4ypO1ZnfUSlRTA1UCRa0WgeDhiNlikxgmAKrxHv4Wsa6NxN0b38AWTe0dsnbPbtlZXS3bKivMqINxJ44bJ0PnzXPkVVUt9Tc1icPiAlseNjvtPssFTp06JTfccINi4Gvgz4zb/P+WW26RdevXz68/fXrsXXv23J33v78hTsRog1rRDZlUOELmTZ0qBtyhZMRwCBA8/6DZwlHlNkK3waepfRMEFQU03fyNWgtjWG6AEHWb97rQ3GD0qEtZUH5ahtw6bbrUgnDnTJykwGJUcTgd4r3xRskrLb39RHn5D34eDFY+8aMfla1cuVIlWjy2b9+u/ucECgooEwBqnbMqPKZCIE5KMG1FfX1dWlbWxkBjozMHGvdcfRUE11SM1zGwjJRUeeh/3G5pN2qaKy1A0+KEj5i/28Ir7bIPIxZWTROMmn4NIZTgEYIQMYV2uc1zLk314UxxydfnzoX/uxQv6OyTcwacegOXZOfmDh3q9/+y/swZ/aGHHpo3ZsyYLfZcxBEkTLQCyhxzgXhfsScfqqqqRufk5Lx98OjRGsfx44+PQBrrRGZlwEwZtkwQIp+YexgdGrpd3pmixczfNu1PLMDAtUYsCUKLxIFlX2e7Aa/UtU/gItiRkMkHuI5jUSTMyZbMDMmdOFHS6+t/d/jIkeMQ/O2KigqW67HyPF7mT5EgkYLwaTjeaWxszOxtaXnoKrfbkzl2rDhhGYZmWgBN1qEhadEsn+biRmBA/lheLs9tWC/v79krOokrqp0tfAwA46zkyrwuYglnNQvg7UeOyi83bpTXNm2WtvZ2846IKbBByyHgDMkcE8g1+bLLZIrD4fO3tDyClDqDslCm85XLZwFAVCorK51AaSWqs8uPHTu2PC0/31PkdC5IhL+oEKSbJm7QFQwOUjcHj3Ord+6Sd/buk8Onm+Stikp561BFnCuEYy5AbdMF7KzPEAvImKtY10KgvXV1smLXLqloPCObjh6Rl8rKJIxcAjfgz1DRSPWlSmTTMt2oO/Jcrpmw2vRjx48vpyyU6fDhw85zQXCek487ULcjvZdF+/fv35rq9TYUZGSMyNA0V8Lw4QplQ7OQ5gMhvBE10Y+CWI41N4kHFpSMzM0DTRxr6zDDF4UJWVoNR2MA6Jb2Yy6gooQVLSwXqG5tU4TLPr3IBc4g3LYx2VImDzLlVDqsgGMiYAan0pH/ZzhdSUU+3/hkr7eesqAAWxQOh59GJHB8JgCowuageHi8vLy8BdawOSc/vzepv78sg9UdEwvL15T/0wIU4ZkCuUFe47KHyCBCZBACBCDIeGZy5AVLs6zWdE5wwBp42qZBQ3Eg6n4CY7uABcIEZIYGtD2I7/0AswB95qDoUZZkEa5uTZAY9phwjTfVk+Dz+/dl5+d3UxbKBPkehzvM+VQmaPsjytHh8JVdYMn1eXl5ezTDKO9vbTVSnc4sJzpV8VNdq6sHO6J8YDSWFS4umSqp0EI9avqxXo8sKBiuNGpYwhhWU4LagqtmKACcBIrlND6dCW6V+FydkyOhSZNkT2eXpIODbps2TZI4Hc++UB8oC0R0MBAqDcMqnDinmJbm0ZuaeiPp6VuhVD/nGKqrq78KEIazwrRldseVnx7U6xOampoehvDN+N7R2NExcPWwYcOSmps9DpS1RpwFOJU7RExXoBVASA8Gf2fJFBEukzFZ6g/gfEgJr+N3w2JtRg/NkLOjgBZVQpHEHAiBesR0B6c7Qa6F+10LEkZtreoEJj0GWZz5AUFwW8mN1eDH4sS1E/Lz03aGQv6hPt9mPOZQJBJZB3Fu7e3t9eD6gRgAnKRAUpCLBOFDmMiB/Px8jYuY2cj/7ygqStFXrkxgLq7Cn2GBQAloftQczRcCOKhBZFtOan0wpEzeNmndIjdl/soCJM4C0BXOORhimcK6zb6YyRkMd0Fzn4FBzVNwuJuhEiR8RzM0txqbmViac4wso79SUpJam5Ehkc7OAcT9BkSEM8hvtKKiotyUlJT6GABMEDo6Ok7D9Osfe+wxtXzFZKEGpe7QlpYwunYa9hQ2HuJUISeq/M1B4Rl/7dDFqWxqMhqJaVsJbjG7HjbBULV/HAjKAgCOM+I0Ex+yNYDg9Soh4nf2wd9o7i4TAFX6ajYvGZzlhWs4VZEEizRuRmbow9g5y9TX16c999xze5ubm9327HKMAxoaGlSCfuDAATlz5oyaVmrr6xvZNTBwz00+X5JYA41fz7MJiJmdkwJHI7GMz2H5u6n1iGnSUZMAdcsC4meDdPgyLcCAgE63CaCT1qLS4bCZGYY/yQ4NfKfwivR0e5b4k+SK+UxNa+stB3ft2p0cjTYwu7USPC7YRM/igPiDKTBzZdYHzZ2dr/UHg/PnOawixw4dnMTg4zRdDdwZNYmNFuC0CE+34j3Dnim8qU1eR3eh/jUrDKmh04UUmTK9dSswTEsAEDHB6RIJygJ0l/Ud4+LMEi2UNYVhkpoC5GRPz71QaOGo7OwbGP/Pt7vkU5mg8h90AJeYg4xo/qkTJ94GO7fp1s0u/M5w1Al+UOammS6gCI5hzhKSwuuWC+gWCSpQ7FAYR4KKBwBk7L6wfW9U5fc8r9GS2Ae/W8kV76Hwfiisu68/th6hcgNoubG2dp/L7Z7f3t4+x154+VwAaDrc2oLjRy1NTQGX17smHIm49P5+KMQt7RD83959T/7lnTVS2digTIhaUJqNmv6t2UJapMc5OrfSsIa0IaIGbmf2MSLULKJES0B0cauoEFbAapaF2bkCz2v43wVCbu3tlV98+Gd56g9/kEP19ZKI8RPkcCDg0pOS/tja3OynLHTrz02FbX+E9ifj4ttbTp8uRVLh8xuGS0fsJAAdfX1ysrlZZWQNHR1gR8MamK0pK8xFTBAc0GZj/4Bs7+6VJlSLTvCFFo58kgZblqBFDcUbvSh3d/b0SXWv34wWEUvrtlupvk3t0wI7oJBa5B1NHA/rBEYGhF+4siOclaW3NzWVUhZYweQvNCXGpWzkzQ8jZHDZrMqdlpYZ0I22aEtLRgiCjikokHtuukl62ttkNnLuQcR5lYeTCxTxhWMpbwK0eLC3R37T0Ch+CJ4BUe9DCTse9h85xwISOIEK4V/xD0otziY4u+Uf8Lzbh+WLpiJAxAyL5AJqGX2HcP2YIdmy9NprpQ+hcXZJCWok8FJ3lwT6+wf0oqJ0aWmpAuvfgfj/MGS7/1w3+FQxhGhQBACW4LMBtUC/y+OpDHpSKkONjcq3SIA3TJ8uC2fOFC8yLg31gEpFLVOlaWoW81PT2zp7xQ9wkMJIF4QsD8fW9axc0LQ6WtIhgHYcZp1iZUfboNVeJFSGxQUx67LyDqbjZOMbpkyWRbNmiw/5jMZVZrhwIKqd1r3eCq/P109ZwA9LUN0WfaYL2EvV4ID72traPMgDDniSk09MmjKlPHHcuEp/TQ2Sm0HFtiEIHKSglhgcCAekaRbhRWweiEq2CwOCnCFugjDMaW7dMGJ5gEqCmIvgezpOuNGCaIP4noVnucgX6CuqAI5a5m9WoTaQIVxD61SzTFyHxFhD6WknCyZO3JSWmlpDWeACzP7uO3dl+lMcgPjfBfPfDStYk52dvfM73/52IPHyyw931daKpmpxhRb+nGbeTpNShYgZEqOayQMcLMPO/JRUuTEpUYbjsq/iWlYiId2IkaCdCXIxZTzOLIYpo/CWK9wuWZTmQ5qPPq0ooCyAIdReK2TY424xlzPG8NrAoPQfOSKhwsJDd997b0dBQcEuykKZUBZ3fSYH2DMmqampqBiDz3o8no2whsD04mJpmz69qn3Fis7wiRNDEvLyTAwYcpCHk3U1xGsNA3dwEsgiLqayUUSAZJSqd6amSBjjc8NnoyydxYgJbk+IRC03mANBrklyixsVH7ONEEBkeszEh1knuYKhmPN/iYl4NsbgQFMTo2ha8xnprjkp0bu+vufGa66RV0eMOHP8+PH3k5KSBkCGeRfkAOT/nAxpxb/bpk6d2o/QYRyqqJCiWbMaOpKTDwzs3Wtmd8y38dAkCH+8s1N+vXefvHCwQvaTQA1zckKFOvh0CIAEWRPgPmo+ohtnpcFnVYU4H1RRRYfgEbhZOGZNupXwNPj75VVkc89jLNuRrDEy2fFfzUodPiyt4VBLxpw5BzgLvH//fqO4uLgfgm/j2g6s2vmZAHBXBjrbNG/evI5Zs2YpiygrLZWxEyYEB2bM2NC1b59EQTBqNRdm14rcYMX2ckFyLZUA4j9P1EgNBuhSM1xmmqy4ASBQ8Kihm6VvXASIb5rFDWRyzQ53Fr9QuD5Y1aqTJ2VHS6sc7uyQlbv3yEHEd26+UC6ILNa/c6e05+R8PP76609VHjqktuZef/31bB2w6k32ZOh5AYD/68uXL+954403jGXLlqml5VWvvy6pAMKzYMHWul5w8v79Ki12w5TOdHdLL4gxFdEgNTFJBjGI+qC5QcIkOssSIIRmFVLqM873DYsEY1UhF0rUpimTXAmkpptm3xaKoIUQfRIlNSFJKaIGOYDTSn/DIL/WykoJzZ37pwmjRkX/8Oabarmdu9op03vvvdfj9Xr1C5Ig9/nFzxDb2+HPuN3FVTk5Sb1bt4oGbUdh1iOysiQTRcYgTHQQA02FJkanZ4pGgoT/KnbXTWGUUFb+b7f4PMA+F1UgSAwEM1FyqD5zvV7J86TKAIQl2C644HhwkkqWoIgBjO0kXLO9uDiJVrhjx47YbjMeebj23GzwU4lQfJggwty08NFHH7k2b9jwfbfXOzCnrs5IP3Ag1XXVVZKPAT00d65sPHhQQnCHazIzZAKsZRAAOaElfdAdixSmsI4Y7dlzgXYtwDE6rN8NpRmHIj8SrZPrgiC8LID97WlTZVNnlwzit5kg6BkjRkgIvh5G6d69Z4/xcW6OnKqo+M5bb721qre3N2pv07FrnC+1OGrv6nzppZdu8nd1zagdHFy1NzNz7PCystnJhYXiwoAmDh0qxdddJzpSUg15+QB8M4GzRxiUkWIWP7G5ezKASn/N0Kk5zD0ASGph4uYCn8NieAcqQmdSgrhSklVESORiKVyNmV/x+Alq9dgF82aUiMD6+j/6SI5FIqdKdf1gblPT115++eUb4e/rPm998IKLoyQXblNBEvEoioqIHgpVbyssrKw9fXogsH07Yu6ABLkdhZUgHwS0E5MxWACQlOpFS5VEq7k8KeLiVBVnfJxmyDLiNkvgpNK4C/HfxZ0e6CPBgz68PkmEsIneVNWv051o3sd1TJi9BqAHyfyICqWjRlV06PqOpsbGMEz/UfvFjIsGgLNCMCG+izK/tbW10ovOeoYM+WDHuHEr2rdtk8DRowoEHYPglBSFS+CaPfw0yYfBp6VJMrfNoiWnZ0givicACF6nEwT5BAgmM25om9ckZ+AeuB7vTYKVJREAApmYDL93mXMRzDSpAESBno0bZV9aWunRYcPeyHA6tebW1kMY+3xof+bnbQO8IABgc8f48eMf4+QIOjqEUyeumjx5R+YDD/x7udu9vQchchAD0DgJyrlAxmqXmRwlpngkmZufIXhm9hDJ4AsSIM0EfKfpitPKJJU1OMWVbAmflSnp2Tm4J1vSMjMlmQDQGhBl3LAOJkvcd8BnhsA1JOVDra3NLYsXP3Xl5ZdvxPNPcqz1KI0nT578GGW46A0S0L4DuUEKaoO9MCXuhdp52803+2fOnetfVlGx7MDrb7x+5ZYtI50Ilw6aLfcHWoTjAAhJAKERwGzt7VPgzITgPq49Ih8IMh6zLDbTSgVKMgBwArSt0GwH7psBoafAmgx8MvPkDLRuLXEz9+9DQlRbVRXcPmfOU9955JGd/tZWbc2bb+5MTk5OgPlPBhgpGLcjfg32SwEADtD57g7SyHWIpwcRT/0fAfFRRUXywJNPfvzLlpbHPH/964sotLNl0iS1I4w8wJjNMNSPHODl2jqp6/er2eRj8NkHQZqJqPA4dW1YCxoscxNgASnQ9rtIxtaB5QlmeZ9f/g+AGasWQsyFFeYFgwCvDxpuOHRY+/OUKT+f+8QTvy8uLNRWI+yVlJS0HDx4cH0gEGjB2G9h3L+QG7jPNyNkH0OHDk2HCZ1IS0vb/6tf/SqwcOFCWbRokdy6YIHs3r1bW/rMMx+sCoc9smXLLyYFg9n6mDHiwYCZIpPwmvo4CTIoadAgo0ADBt7DGdqkZJXC0kpUdCB3sLTGPQTJwzdC0PwQ+hhIeCz3K0B4al1tqITbnamt096bMP4X03/84+f/Yf78wNq1a/lmmjCDzcrK8m/btm0/LGBsbm5uOj5741+zuSAAZE7u4uTR0tKCCKOtRUgJ3HHHHWKvIRwF+TFL3LJlS+iuZ55ZveqJJyKD27f/bNqhQ4VhJFLcYJGK6/IhxAgIVmtttJzkS5N8uEWQ64WW7xusoJxmdel1J0hJWro0ILtjSe3FgMczBwAIAW61RT8BJGYnu7oCH0ws/tcZy5a98M077uim8BBYjZt5zMSJEylsYPPmzWt7enoijY2NsWX/+J0vZwFg7/C86aabYi88IjUeGDdu3IAtPA++8cV1g26kwY8++qi89tprQdczz7zzyrPPtjdv3vzjubW1c8Jg70G0DPj0d2HyO5Aj0LKuGzJEPDRhtX3GmgwhCJpZx7POX4RsLR+gNUPYqbCmURCokwCgjwA+9zkcNWVf+cqz837wg7e+8bWv+dfHCR//ms4kLqft2dPKaX4UQ7Hx04LircBhv7RI1rRfXLB95pFHHmE1pV5nO/cFB3vvjb31taa+3vX8ihXjHR988MANjY13j3U4sjwAwAcgfLAIN4Riyct7+np6pB0MTiCSrOInLzdX0sH6HlhIAtieYY4a7+W2eVzfhEGWZWd/2DBv3q/u/d73ds++4orQ2nOEjz84Lm6N6cRzYAmx8dvK5fYY9VqOnfDwJchzD25uPjd9tF90EGsbrH2MHTVK+39PPnnkd1On/subv//9R5cdOLB0Vk/P/IK+Pm8fX5XjewAwcU6YUDASWViVuAASJB1Axsleg/0BKCEqUW5qYjQIh7W9Hs/u3ZMn/7Z44cI//2Lp0qaUxETjdRRpx44dO6/w8XkMgZgyZcrFhcFz3+/5vCMFD7v/61/vvvnaaz/87erVe3+zdu2copqa26d3d88q7Osb6bWA43Ra0FpocVizQf3QtEbzFHP67IzL1XHI691bNWHCX3LmzSv94ZIltdMnTgzvRca3adMmtXJlv0N43hchrGfZq1ifJcMFAWAndI/q6uovDIJlUdr37733dM/ixW//aePGTX8qK5ugV1fPyG1pmZbX1zc2TddzEwzDh/jkTMJjwhynrvf363pXq89X35KdXRkaM2bfxGuvPfzYggUt40aODHOtAiWt1NXVqVdqSLSfV8eQdwY/Z8foBQGgefFlJ2RUX3ofPrfG33fffRrCZtvi229vqzp2bNeOffu8iNGZJ6uq0ptra31tPT0OgODIhovkFBYGRhUX904qKem+44or/FdOnRoc7O831q1bJ0t+/WsVeb6MNdouwC3zFzr+vwADAPHdagm10bR9AAAAAElFTkSuQmCC",
+ mentionIcon: "data:image/png;base64,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%3D",
}
// Make sure we have the right WebSocket class.
@@ -26,6 +27,7 @@ var baseUrl = '<%= baseUrl%>';
var motown;
var reconnectInterval;
var notificationCount = {
+ mentions: 0,
bugzilla: 0
}
@@ -100,12 +102,12 @@ function initWebSocket(){
function setAmbientNotificationCount(){
apiPort.postMessage({
- topic: 'social.ambient-notification-update',
+ topic: 'social.ambient-notification',
data: {
- name: 'bugzilla',
- iconURL: SPRITES.bugIcon,
- counter: notificationCount.bugzilla,
- contentPanel: baseUrl + '/social/bugs'
+ name: 'mentions',
+ iconURL: SPRITES.mentionIcon,
+ counter: notificationCount.mentions,
+ contentPanel: baseUrl + '/social/mentions'
}
});
}
@@ -116,16 +118,20 @@ var handlers = {
'contacts.userOffline': rawSidebarMessage,
'contacts.reset': rawSidebarMessage,
'contacts.userStatusUpdate': rawSidebarMessage,
- 'notifications.bugzilla.read': function(message){
- notificationCount.bugzilla--;
+ 'notifications.mention.read': function(message){
+ if (notificationCount.mentions > 0)
+ notificationCount.mentions--;
- log(notificationCount.bugzilla, true);
setAmbientNotificationCount();
},
- 'notifications.bugzilla': function(message){
- notificationCount.bugzilla++;
+ 'notifications.mention.new': function(message){
+ notificationCount.mentions++;
setAmbientNotificationCount();
},
+ 'notifications.count': function(message){
+ notificationCount.mentions = message.mentions;
+ setAmbientNotificationCount();
+ },
'social.port-closing': function(data, port){
if (apiPort == port){
apiPort.close();
@@ -133,11 +139,11 @@ var handlers = {
}
},
'social.initialize': function(data, port){
- dump('social.initialize on port ' + JSON.stringify(port) + '\n');
+ log('social.initialize on port ' + JSON.stringify(port) + '\n');
apiPort = port;
apiPort.postMessage({
- topic: 'social.ambient-notification-area',
+ topic: 'social.user-profile',
data: {
userName: user.email,
displayName: user.realName,
View
110 app/serializer.js
@@ -8,13 +8,64 @@
// Module dependencies.
const
-redis = require('../lib/redis')(),
-logger = require('../lib/logger'),
-config = require('../lib/configuration'),
-mysql = require('mysql').createClient(config.get('mysql'));
+redis = require('../lib/redis').new(),
+pubRedis = require('../lib/redis').pub,
+logger = require('../lib/logger'),
+config = require('../lib/configuration'),
+mysql = require('mysql').createClient(config.get('mysql'));
+require('../lib/extensions/array');
+require('../lib/extensions/number');
+
+var watchedTokens = null; // {token: [<user.id>, ...], ...
+
+
+function loadWatchedTokens(callback){
+ mysql.query(
+ "SELECT token, user_id FROM watched_tokens ORDER BY token, user_id",
+ function(err, rows)
+ {
+ if (!err){
+ watchedTokens = {};
+ for (var i in rows){
+
+ var token = "@" + rows[i].token;
+ var userId = rows[i].user_id;
+
+ if (token in watchedTokens){
+ watchedTokens[token].push(userId);
+ }
+ else{
+ watchedTokens[token] = [userId];
+ }
+ }
+ if (typeof(callback) == 'function'){
+ callback();
+ }
+ }
+ else{
+ logger.error("Error loading watched_tokens in Feed Daemon: " + err);
+ }
+ }
+ );
+}
+
+// This method might not look super clear,
+// but it is checking each token found in a story against the
+// complete set of watched tokens. aka {token: [<user.id>, ...], ...}
+// so we return a possibly non-unique list of interested users.
+function checkForMentions(story){
+ var mentionedUsers = [];
+
+ var mentions = story.title.match(/\@\w+/g);
+
+ for (var i in mentions){
+ mentionedUsers = mentionedUsers.concat(watchedTokens[mentions[i]]);
+ }
+
+ return mentionedUsers;
+}
-redis.debug_mode = false;
// {
// "userIds": [ 13, 14 ],
@@ -36,11 +87,13 @@ redis.debug_mode = false;
// }
// }
-
function waitForStory(){
+ logger.debug('Waiting on serializer:stories');
// This is a blocking call only to the network layer (we keep ticking)
redis.brpop('serializer:stories', 0, function(err, data){
- // data == ['stories', '{story json as string}']
+
+ if (err)
+ logger.error("Error dequeueing story: " + err);
//TODO: Error handling including:
// - accepting an error from redis
@@ -53,14 +106,26 @@ function waitForStory(){
var story = JSON.parse(data);
var userIds = story.userIds;
+
story = story.story;
+ var mentionedUsers = [];
+
+ if (!story.durable){
+ mentionedUsers = checkForMentions(story);
+ }
+
+ userIds = Array.uniqueSort(userIds.concat(mentionedUsers));
+
for (var i in userIds){
var userId = userIds[i];
+ var durable = !!(story.durable || mentionedUsers.indexOf(userId) >= 0);
+ logger.debug("Durable? : " + durable);
+
// Persist the story to MySQL
mysql.query(
- 'REPLACE INTO stories SET id = ?, data = ?, user_id = ?, published_at = ?, durable = ?', [story.id, JSON.stringify(story), userId, story.pubdate, !!story.durable],
+ 'REPLACE INTO stories SET id = ?, data = ?, user_id = ?, published_at = ?, durable = ?', [story.id, JSON.stringify(story), userId, story.pubdate, !!durable],
function(err, data){
// TODO: Handle error wisely
if (err)
@@ -68,14 +133,21 @@ function waitForStory(){
// Publish the story data as a pub/sub event for the socket if it's a new record
if (data.affectedRows == 1){
- redis.publish('feeds.storyForUser', JSON.stringify({'userId': userId, 'story': story}), function(err){
- if (err)
- logger.error(err);
- });
+ if (durable){
+ pubRedis.publish('notifications.mention.new', userId.toString(), function(err){
+ if (err)
+ logger.error("Error publishing notifications.mention.new: " + err);
+ });
+ }
+ else {
+ pubRedis.publish('feeds.storyForUser', JSON.stringify({'userId': userId, 'story': story}), function(err){
+ if (err)
+ logger.error(err);
+ });
+ }
}
}
);
-
}
// Rinse and repeat
@@ -83,7 +155,13 @@ function waitForStory(){
});
}
+
+var reloadInterval = setInterval(loadWatchedTokens, (10).minutes());
+
+
redis.on("ready", function(){
- logger.info("Serializer ready.");
- waitForStory();
-});
+ loadWatchedTokens(function(){
+ logger.info("Serializer ready.");
+ waitForStory();
+ });
+});
Please sign in to comment.
Something went wrong with that request. Please try again.