Unsubscription & choose to receive a mail onStart / onEnd

- The subscription panel is placed in the "mysettings" menu (not anymmore a popup)
- A Unsubscribe button were added. It removes the email if found
- The 2 options to receive a mail when someone is starting to edit and finished to edit can be chosen for each user (at least one of the two is compulsory)
- First time you go on the email notification panel in the mysettings menu, a call is made to get the email and options already subscribed for this userId
  * If found, the form is pre-filled with the datas
  * If not, the form is set with defaults values
It's just informative. Users can still subscribe another mail for this userId.
- Removed the integration of this plugin in the "Share this pad" menu. First becaue, imo, it's not the place, and second, because it meant 2 forms with the same id.
This commit is contained in:
etherpad 2013-03-26 20:44:34 +01:00
parent 34dfd008ff
commit 53c1df8248
7 changed files with 359 additions and 109 deletions

View file

@ -1,11 +1,25 @@
var eejs = require("ep_etherpad-lite/node/eejs"); var eejs = require("ep_etherpad-lite/node/eejs");
var settings = require('ep_etherpad-lite/node/utils/Settings');
var checked_state = '';
exports.eejsBlock_scripts = function (hook_name, args, cb) { exports.eejsBlock_scripts = function (hook_name, args, cb) {
args.content = args.content + eejs.require("ep_email_notifications/templates/scripts.html", {}, module); args.content = args.content + eejs.require("ep_email_notifications/templates/scripts.html", {}, module);
return cb(); return cb();
}; };
/*
exports.eejsBlock_embedPopup = function (hook_name, args, cb) { exports.eejsBlock_embedPopup = function (hook_name, args, cb) {
args.content = args.content + eejs.require("ep_email_notifications/templates/embedFrame.html", {}, module); args.content = args.content + eejs.require("ep_email_notifications/templates/embedFrame.html", {}, module);
return cb(); return cb();
}; };
*/
exports.eejsBlock_mySettings = function (hook_name, args, cb) {
args.content = args.content + eejs.require('ep_email_notifications/templates/email_notifications_settings.ejs', {checked : checked_state});
return cb();
};
exports.eejsBlock_styles = function (hook_name, args, cb) {
args.content = args.content + '<link href="../static/plugins/ep_email_notifications/static/css/email_notifications.css" rel="stylesheet">';
};

View file

@ -6,11 +6,14 @@
"padUpdate": "ep_email_notifications/update", "padUpdate": "ep_email_notifications/update",
"handleMessage": "ep_email_notifications/handleMessage", "handleMessage": "ep_email_notifications/handleMessage",
"eejsBlock_scripts": "ep_email_notifications/client", "eejsBlock_scripts": "ep_email_notifications/client",
"eejsBlock_embedPopup": "ep_email_notifications/client:eejsBlock_embedPopup" "eejsBlock_mySettings": "ep_email_notifications/client:eejsBlock_mySettings",
"eejsBlock_styles": "ep_email_notifications/client:eejsBlock_styles"
}, },
"client_hooks": { "client_hooks": {
"postAceInit":"ep_email_notifications/static/js/ep_email:postAceInit", "postAceInit":"ep_email_notifications/static/js/ep_email:postAceInit",
"handleClientMessage_emailSubscriptionSuccess":"ep_email_notifications/static/js/ep_email" "handleClientMessage_emailSubscriptionSuccess":"ep_email_notifications/static/js/ep_email",
"handleClientMessage_emailUnsubscriptionSuccess":"ep_email_notifications/static/js/ep_email",
"handleClientMessage_emailNotificationGetUserInfo":"ep_email_notifications/static/js/ep_email"
} }
} }
] ]

View file

@ -13,24 +13,135 @@ exports.handleMessage = function(hook_name, context, callback){
if (context.message.data.userInfo){ if (context.message.data.userInfo){
if(context.message.data.userInfo.email){ // it contains email if(context.message.data.userInfo.email){ // it contains email
console.debug(context.message); console.debug(context.message);
console.info("ep_mail: " + context.message.data.userInfo.email + " / " + context.message.data.userInfo.email_option);
console.info(context.message.data.userInfo);
// does email Subscription already exist for this email address? // does email Subscription already exist for this email address?
db.get("emailSubscription:"+context.message.data.padId, function(err, userIds){ db.get("emailSubscription:"+context.message.data.padId, function(err, userIds){
var alreadyExists = false; var alreadyExists = false;
console.info(userIds);
if(userIds){ if(userIds){
async.forEach(Object.keys(userIds), function(user, cb){ async.forEach(Object.keys(userIds), function(user, cb){
console.debug("UserIds subscribed by email to this pad:", userIds); console.info("UserIds subscribed by email to this pad:", userIds);
if(user == context.message.data.userInfo.email){ // If we already have this email registered for this pad if(user == context.message.data.userInfo.email){ // If we already have this email registered for this pad
// This user ID is already assigned to this padId so don't do anything except tell the user they are already subscribed somehow.. // This user ID is already assigned to this padId so don't do anything except tell the user they are already subscribed somehow..
alreadyExists = true; alreadyExists = true;
console.debug("email ", user, "already subscribed to ", context.message.data.padId, " so sending message to client"); }
cb();
},
function(err){
// There should be something in here!
}); // end async for each
}
if(context.message.data.userInfo.email_option == 'subscribe' && alreadyExists == true){
// SUbscription
console.info("email ", context.message.data.userInfo.email, "already subscribed to ", context.message.data.padId, " so sending message to client");
context.client.json.send({ type: "COLLABROOM",
data:{
type: "emailSubscriptionSuccess",
payload: false
}
});
} else if(context.message.data.userInfo.email_option == 'subscribe' && alreadyExists == false){
// SUbscription
var validatesAsEmail = check(context.message.data.userInfo.email).isEmail();
if(!validatesAsEmail){ // send validation failed if it's malformed.. y'know in general fuck em!
console.warn("Dropped email subscription due to malformed email address");
context.client.json.send({ type: "COLLABROOM",
data:{
type: "emailSubscriptionSuccess",
payload: false
}
});
} else {
console.info ("Subscription: Wrote to the database and sent client a positive response ",context.message.data.userInfo.email);
exports.setAuthorEmail(
context.message.data.userInfo.userId,
context.message.data.userInfo,
callback
);
exports.setAuthorEmailRegistered(
context.message.data.userInfo,
context.message.data.userInfo.userId,
context.message.data.padId
);
context.client.json.send({ type: "COLLABROOM",
data:{
type: "emailSubscriptionSuccess",
payload: true
}
});
}
} else if(context.message.data.userInfo.email_option == 'unsubscribe' && alreadyExists == true) {
// Unsubscription
console.info ("Unsubscription: Remove from the database and sent client a positive response ",context.message.data.userInfo.email);
exports.unsetAuthorEmail(
context.message.data.userInfo.userId,
context.message.data.userInfo,
callback
);
exports.unsetAuthorEmailRegistered(
context.message.data.userInfo,
context.message.data.userInfo.userId,
context.message.data.padId
);
context.client.json.send({ type: "COLLABROOM",
data:{
type: "emailUnsubscriptionSuccess",
payload: true
}
});
} else if(context.message.data.userInfo.email_option == 'unsubscribe' && alreadyExists == false) {
// Unsubscription
console.info ("Unsubscription: Send client a negative response ",context.message.data.userInfo.email);
context.client.json.send({ type: "COLLABROOM",
data:{
type: "emailUnsubscriptionSuccess",
payload: false
}
});
}
}); // close db get
callback([null]); // don't run onto passing colorId or anything else to the message handler
}
}
} else if (context.message.data.type == 'USERINFO_GET' ) { // A request to find datas for a username
if (context.message.data.userInfo){
if(context.message.data.userInfo.userId){ // it contains the userId
console.debug(context.message);
var userIdFound = false;
// does email Subscription already exist for this name and padID?
db.get("emailSubscription:"+context.message.data.padId, function(err, userIds){
if(userIds){
async.forEach(Object.keys(userIds), function(user, cb){
if(userIds[user].authorId == context.message.data.userInfo.userId){ // if we find the same Id in the Db as the one used by the user
console.info("Options for this pad ", userIds[user].authorId, " found in the Db");
userIdFound = true;
// We send back the options set for this user
context.client.json.send({ type: "COLLABROOM", context.client.json.send({ type: "COLLABROOM",
data:{ data:{
type: "emailSubscriptionSuccess", type: "emailNotificationGetUserInfo",
payload: false payload: {
email: user,
onStart: userIds[user].onStart && typeof userIds[user].onStart === 'boolean'?userIds[user].onStart:true,
onEnd: userIds[user].onEnd && typeof userIds[user].onEnd === 'boolean'?userIds[user].onEnd:false,
success:true
}
} }
}); });
} }
@ -41,41 +152,19 @@ exports.handleMessage = function(hook_name, context, callback){
// There should be something in here! // There should be something in here!
}); // end async for each }); // end async for each
} }
var validatesAsEmail = check(context.message.data.userInfo.email).isEmail(); });
if(!validatesAsEmail){ // send validation failed if it's malformed.. y'know in general fuck em!
console.warn("Dropped email subscription due to malformed email address");
context.client.json.send({ type: "COLLABROOM",
data:{
type: "emailSubscriptionSuccess",
payload: false
}
});
}
if(alreadyExists == false && validatesAsEmail){
console.debug ("Wrote to the database and sent client a positive response ",context.message.data.userInfo.email);
exports.setAuthorEmail(
context.message.data.userInfo.userId,
context.message.data.userInfo.email, callback
);
exports.setAuthorEmailRegistered(
context.message.data.userInfo.email,
context.message.data.userInfo.userId,
context.message.data.padId
);
context.client.json.send({ type: "COLLABROOM",
data:{
type: "emailSubscriptionSuccess",
payload: true
}
});
}
}); // close db get
callback([null]); // don't run onto passing colorId or anything else to the message handler
if (!userIdFound) {
// We send back the options set for this user
context.client.json.send({ type: "COLLABROOM",
data:{
type: "emailNotificationGetUserInfo",
payload: {
success:false
}
}
});
}
} }
} }
} }
@ -84,25 +173,46 @@ exports.handleMessage = function(hook_name, context, callback){
} }
// Updates the database with the email record // Updates the database with the email record
exports.setAuthorEmail = function (author, email, callback){ exports.setAuthorEmail = function (author, datas, callback){
db.setSub("globalAuthor:" + author, ["email"], email, callback); db.setSub("globalAuthor:" + author, ["email"], datas.email, callback);
} }
// Write email and padId to the database // Write email and padId to the database
exports.setAuthorEmailRegistered = function(email, authorId, padId){ exports.setAuthorEmailRegistered = function(datas, authorId, padId){
var timestamp = new Date().getTime(); var timestamp = new Date().getTime();
var registered = { var registered = {
authorId: authorId, authorId: authorId,
onStart: datas.email_onStart,
onEnd: datas.email_onEnd,
timestamp: timestamp timestamp: timestamp
}; };
console.debug("registered", registered, " to ", padId); console.debug("registered", registered, " to ", padId);
// Here we have to basically hack a new value into the database, this isn't clean or polite. // Here we have to basically hack a new value into the database, this isn't clean or polite.
db.get("emailSubscription:" + padId, function(err, value){ // get the current value db.get("emailSubscription:" + padId, function(err, value){ // get the current value
if(!value){value = {};} // if an emailSubscription doesnt exist yet for this padId don't panic if(!value){value = {};} // if an emailSubscription doesnt exist yet for this padId don't panic
value[email] = registered; // add the registered values to the object value[datas.email] = registered; // add the registered values to the object
console.warn("written to database"); console.warn("written to database");
db.set("emailSubscription:" + padId, value); // stick it in the database db.set("emailSubscription:" + padId, value); // stick it in the database
}); });
} }
// Updates the database by removing the email record for that AuthorId
exports.unsetAuthorEmail = function (author, datas, callback){
db.get("globalAuthor:" + author, function(err, value){ // get the current value
delete value['email'];
db.set("globalAuthor:" + author, value);
});
}
// Remove email and padId from the database
exports.unsetAuthorEmailRegistered = function(datas, authorId, padId){
console.debug("unregistered", datas.email, " to ", padId);
// Here we have to basically hack a new value into the database, this isn't clean or polite.
db.get("emailSubscription:" + padId, function(err, value){ // get the current value
delete value[datas.email]; // remove the registered values to the object
console.warn("written to database");
db.set("emailSubscription:" + padId, value); // stick it in the database
});
}

View file

@ -0,0 +1,10 @@
.ep_email_settings {
display: none;
padding-left: 1.5em;
padding: .2em;
}
.ep_email_input {
padding:.2em;
width:177px;
}

View file

@ -1,19 +1,44 @@
var cookie = require('ep_etherpad-lite/static/js/pad_cookie').padcookie; var cookie = require('ep_etherpad-lite/static/js/pad_cookie').padcookie;
var firstRun = true;
if(typeof exports == 'undefined'){ if(typeof exports == 'undefined'){
var exports = this['mymodule'] = {}; var exports = this['mymodule'] = {};
} }
exports.postAceInit = function(hook, context){ exports.postAceInit = function(hook, context){
// after 10 seconds if we dont already have an email for this author then prompt them // Uncheck the checkbox
setTimeout(function(){init()},10000); $('#options-emailNotifications').attr('checked', false);
setDefaultOptions();
/* on click */
$('#options-emailNotifications').on('click', function() {
if (firstRun) {
getDatasForUserId();
firstRun = false;
} else {
$('.ep_email_settings').slideToggle();
}
});
// Prepare subscription before submit form
$('#ep_email_subscribe').on('click', function() {
$('#ep_email_option').val('subscribe');
checkAndSend();
});
// Prepare unsubscription before submit form
$('#ep_email_unsubscribe').on('click', function() {
$('#ep_email_option').val('unsubscribe');
checkAndSend();
});
// subscribe by email can be active.. // subscribe by email can be active..
$('.ep_email_form').submit(function(){ $('.ep_email_form').submit(function(){
sendEmailToServer(); sendEmailToServer();
$('.ep_email_settings').slideToggle();
$('#options-emailNotifications').attr('checked', false);
return false; return false;
}); });
} }
exports.handleClientMessage_emailSubscriptionSuccess = function(hook, context){ // was subscribing to the email a big win or fail? exports.handleClientMessage_emailSubscriptionSuccess = function(hook, context){ // was subscribing to the email a big win or fail?
@ -24,21 +49,108 @@ exports.handleClientMessage_emailSubscriptionSuccess = function(hook, context){
} }
} }
function init(){ exports.handleClientMessage_emailUnsubscriptionSuccess = function(hook, context){ // was subscribing to the email a big win or fail?
var popUpIsAlreadyVisible = $('.ep_email_form').is(":visible"); if(context.payload == false){
if(!popUpIsAlreadyVisible){ // if the popup isn't already visible showWasNotRegistered();
if(clientHasAlreadyRegistered()){ // if the client has already registered for emails on this pad. }else{
// showAlreadyRegistered(); // client has already registered, let em know.. showUnregistrationSuccess();
}else{
var cookieVal = pad.getPadId() + "email";
if(cookie.getPref(cookieVal) !== "true"){ // if this user hasn't already subscribed
askClientToEnterEmail(); // ask the client to register TODO uncomment me for a pop up
}
}
} }
} }
function showRegistrationSuccess(){ // show a successful registration message exports.handleClientMessage_emailNotificationGetUserInfo = function (hook, context) { // return the existing options for this userId
var datas = context.payload;
if(datas.success == true){ // If datas were found, set the options with them
if (datas.email) $('#ep_email').val(datas.email);
if (datas.onStart && typeof datas.onStart === 'boolean') $('#ep_email_onStart').attr('checked', datas.onStart);
if (datas.onEnd && typeof datas.onEnd === 'boolean') $('#ep_email_onEnd').attr('checked', datas.onEnd);
} else { // No datas were found, set the options to default values
setDefaultOptions();
}
$('.ep_email_settings').slideToggle();
}
/**
* Set the options in the frame to a default value
*/
function setDefaultOptions() {
$('#ep_email_onStart').attr('checked', true);
$('#ep_email_onEnd').attr('checked', false);
}
/**
* Control options before submitting the form
*/
function checkAndSend() {
var email = getEmail();
if (email && $('#ep_email_option').val() == 'subscribe' && !$('#ep_email_onStart').is(':checked') && !$('#ep_email_onEnd').is(':checked')) {
$.gritter.add({
// (string | mandatory) the heading of the notification
title: "Email subscription error",
// (string | mandatory) the text inside the notification
text: "You need to check at least one of the two options from 'Send a mail when someone..'"
});
} else if (email) {
$('.ep_email_form').submit();
}
return false;
}
/**
* Return the email from the user
*/
function getEmail() {
var email = $('#ep_email').val();
if(!email){ // if we're not using the top value use the notification value
email = $('#ep_email_notification').val();
}
return email;
}
/**
* Ask the server to register the email
*/
function sendEmailToServer(){
var email = getEmail();
var userId = pad.getUserId();
var message = {};
message.type = 'USERINFO_UPDATE';
message.userInfo = {};
message.padId = pad.getPadId();
message.userInfo.email = email;
message.userInfo.email_option = $('#ep_email_option').val();
message.userInfo.email_onStart = $('#ep_email_onStart').is(':checked');
message.userInfo.email_onEnd = $('#ep_email_onEnd').is(':checked');
message.userInfo.userId = userId;
if(email){
pad.collabClient.sendMessage(message);
cookie.setPref(message.padId+"email", "true");
}
}
/**
* Thanks to the userId, we can get back from the Db the options set for this user
* and fill the fields with them
*/
function getDatasForUserId() {
var userId = pad.getUserId();
var message = {};
message.type = 'USERINFO_GET';
message.padId = pad.getPadId();
message.userInfo = {};
message.userInfo.userId = userId;
pad.collabClient.sendMessage(message);
}
/*************************************
Manage return msgs from server
*************************************/
/**
* Show a successful registration message
*/
function showRegistrationSuccess(){
$.gritter.add({ $.gritter.add({
// (string | mandatory) the heading of the notification // (string | mandatory) the heading of the notification
title: "Email subscribed", title: "Email subscribed",
@ -47,7 +159,10 @@ function showRegistrationSuccess(){ // show a successful registration message
}); });
} }
function showAlreadyRegistered(){ // the client already registered for emails on this pad so notify the UI /**
* The client already registered for emails on this pad so notify the UI
*/
function showAlreadyRegistered(){
$.gritter.add({ $.gritter.add({
// (string | mandatory) the heading of the notification // (string | mandatory) the heading of the notification
title: "Email subscription", title: "Email subscription",
@ -59,54 +174,29 @@ function showAlreadyRegistered(){ // the client already registered for emails on
} }
function clientHasAlreadyRegistered(){ // Has the client already registered for emails on this? /**
// Given a specific AuthorID do we have an email address in the database? * Show a successful unregistration message
// Given that email address is it registered to this pad? */
// need to pass the server a message to check function showUnregistrationSuccess(){
var userId = pad.getUserId();
var message = {};
message.type = 'USERINFO_AUTHOR_EMAIL_IS_REGISTERED_TO_PAD';
message.userInfo = {};
message.userInfo.userId = userId;
pad.collabClient.sendMessage(message);
}
function askClientToEnterEmail(){
$.gritter.add({ $.gritter.add({
// (string | mandatory) the heading of the notification // (string | mandatory) the heading of the notification
title: "Enter your email to receive an email when someone modifies this pad", title: "Email unsubscribed",
// (string | mandatory) the text inside the notification // (string | mandatory) the text inside the notification
text: "<form class='ep_email_form'><label for='ep_email'><input id='ep_email_notification' placeholder='your@email.com' style='padding:5px;width:180px;' type=email><input type=submit value=subscribe style='padding:5px;'></form>", text: "You won't receive anymore email when someone changes this pad."
// (bool | optional) if you want it to fade out on its own or just sit there
sticky: true,
// (int | optional) the time you want it to be alive for before fading out
time: '2000',
// the function to bind to the form
after_open: function(e){
$('.ep_email_form').submit(function(){
$(e).hide();
sendEmailToServer();
return false;
});
}
}); });
} }
function sendEmailToServer(){ /**
var email = $('#ep_email').val(); * The client wasn't registered for emails
if(!email){ // if we're not using the top value use the notification value */
email = $('#ep_email_notification').val(); function showWasNotRegistered(){
} $.gritter.add({
var userId = pad.getUserId(); // (string | mandatory) the heading of the notification
var message = {}; title: "Email unsubscription",
message.type = 'USERINFO_UPDATE'; // (string | mandatory) the text inside the notification
message.userInfo = {}; text: "This email address is not registered for this pad",
message.padId = pad.getPadId(); // (bool | optional) if you want it to fade out on its own or just sit there
message.userInfo.email = email; sticky: false
message.userInfo.userId = userId; });
if(email){
pad.collabClient.sendMessage(message);
cookie.setPref(message.padId+"email", "true");
}
}
}

View file

@ -0,0 +1,20 @@
<p>
<input type="checkbox" id="options-emailNotifications"></input>
<label for="options-emailNotifications">Email Notifications</label>
<div class="ep_email_settings">
<form class='ep_email_form'>
<input id='ep_email' class='ep_email_input' placeholder='your@email.com' type=email>
<label>Send a mail when someone..</label>
<br />
<input type="checkbox" style="margin-left:0.3em;" id="ep_email_onStart"></input>
<label for="ep_email_onStart">starts editing the pad</label>
<br />
<input type="checkbox" style="margin-left:0.3em;" id="ep_email_onEnd"></input>
<label for="ep_email_onEnd">finish editing the pad</label>
<input id='ep_email_option'type=hidden >
</form>
<br />
<input style="padding:5px;" id='ep_email_subscribe' type=button value=subscribe>
<input style="padding:5px;" id='ep_email_unsubscribe'type=button value=unsubscribe>
</div>
</p>

View file

@ -45,10 +45,11 @@ exports.notifyBegin = function(padId){
async.forEach(Object.keys(recipients), function(recipient, cb){ async.forEach(Object.keys(recipients), function(recipient, cb){
// Is this recipient already on the pad? // Is this recipient already on the pad?
exports.isUserEditingPad(padId, recipients[recipient].authorId, function(err,userIsOnPad){ // is the user already on the pad? exports.isUserEditingPad(padId, recipients[recipient].authorId, function(err,userIsOnPad){ // is the user already on the pad?
if(!userIsOnPad){ var onStart = typeof(recipients[recipient].onStart) == "undefined" || recipients[recipient].onStart?true:false; // In case onStart wasn't defined we set it to true
if(!userIsOnPad && onStart){
console.debug("Emailing "+recipient +" about a new begin update"); console.debug("Emailing "+recipient +" about a new begin update");
server.send({ server.send({
text: "Your pad at "+urlToPads+padId +" is being edited, we're just emailing you let you know :)\n\n -- This plugin is in alpha state, can you help fund it's development? https://github.com/johnmclear/ep_email_notifications", text: "Your pad at "+urlToPads+padId +" is being edited, we're just emailing you let you know :)",
from: fromName+ "<"+fromEmail+">", from: fromName+ "<"+fromEmail+">",
to: recipient, to: recipient,
subject: "Someone started editing "+padId subject: "Someone started editing "+padId
@ -76,7 +77,9 @@ exports.notifyEnd = function(padId){
async.forEach(Object.keys(recipients), function(recipient, cb){ async.forEach(Object.keys(recipients), function(recipient, cb){
// Is this recipient already on the pad? // Is this recipient already on the pad?
exports.isUserEditingPad(padId, recipients[recipient].authorId, function(err,userIsOnPad){ // is the user already on the$ exports.isUserEditingPad(padId, recipients[recipient].authorId, function(err,userIsOnPad){ // is the user already on the$
if(!userIsOnPad){ var onEnd = typeof(recipients[recipient].onEnd) == "undefined" || recipients[recipient].onEnd?true:false; // In case onEnd wasn't defined we set it to true
if(!userIsOnPad && onEnd){
console.debug("Emailing "+recipient +" about a pad finished being updated"); console.debug("Emailing "+recipient +" about a pad finished being updated");
server.send({ server.send({
text: "Your pad at "+urlToPads+padId +" has finished being edited, we're just emailing you let you know :) \n\n The changes look like this: \n" + changesToPad, text: "Your pad at "+urlToPads+padId +" has finished being edited, we're just emailing you let you know :) \n\n The changes look like this: \n" + changesToPad,