Merge pull request #454 from floatinghotpot/notification

Add notifications, allow watch boards / lists / cards
This commit is contained in:
Maxime Quandalle 2016-01-06 12:49:58 +01:00
commit 1e8368dea5
24 changed files with 585 additions and 16 deletions

View file

@ -0,0 +1,41 @@
// buffer each user's email text in a queue, then flush them in single email
Meteor.startup(() => {
Notifications.subscribe('email', (user, title, description, params) => {
// add quote to make titles easier to read in email text
const quoteParams = _.clone(params);
['card', 'list', 'oldList', 'board', 'comment'].forEach((key) => {
if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`;
});
const text = `${params.user} ${TAPi18n.__(description, quoteParams, user.getLanguage())}\n${params.url}`;
user.addEmailBuffer(text);
// unlike setTimeout(func, delay, args),
// Meteor.setTimeout(func, delay) does not accept args :-(
// so we pass userId with closure
const userId = user._id;
Meteor.setTimeout(() => {
const user = Users.findOne(userId);
// for each user, in the timed period, only the first call will get the cached content,
// other calls will get nothing
const texts = user.getEmailBuffer();
if (texts.length === 0) return;
// merge the cached content into single email and flush
const text = texts.join('\n\n');
user.clearEmailBuffer();
try {
Email.send({
to: user.emails[0].address,
from: Accounts.emailTemplates.from,
subject: TAPi18n.__('act-activity-notify', {}, user.getLanguage()),
text,
});
} catch (e) {
return;
}
}, 30000);
});
});

View file

@ -0,0 +1,48 @@
// a map of notification service, like email, web, IM, qq, etc.
// serviceName -> callback(user, title, description, params)
// expected arguments to callback:
// - user: Meteor user object
// - title: String, TAPi18n key
// - description, String, TAPi18n key
// - params: Object, values extracted from context, to used for above two TAPi18n keys
// see example call to Notifications.notify() in models/activities.js
const notifyServices = {};
Notifications = {
subscribe: (serviceName, callback) => {
notifyServices[serviceName] = callback;
},
unsubscribe: (serviceName) => {
if (typeof notifyServices[serviceName] === 'function')
delete notifyServices[serviceName];
},
// filter recipients according to user settings for notification
getUsers: (participants, watchers) => {
const userMap = {};
participants.forEach((userId) => {
if (userMap[userId]) return;
const user = Users.findOne(userId);
if (user && user.hasTag('notify-participate')) {
userMap[userId] = user;
}
});
watchers.forEach((userId) => {
if (userMap[userId]) return;
const user = Users.findOne(userId);
if (user && user.hasTag('notify-watch')) {
userMap[userId] = user;
}
});
return _.map(userMap, (v) => v);
},
notify: (user, title, description, params) => {
for(const k in notifyServices) {
const notifyImpl = notifyServices[k];
if (notifyImpl && typeof notifyImpl === 'function') notifyImpl(user, title, description, params);
}
},
};

View file

@ -0,0 +1,9 @@
Meteor.startup(() => {
// XXX: add activity id to profile.notifications,
// it can be displayed and rendered on web or mobile UI
// will uncomment the following code once UI implemented
//
// Notifications.subscribe('profile', (user, title, description, params) => {
// user.addNotification(params.activityId);
// });
});

View file

@ -0,0 +1,36 @@
Meteor.methods({
watch(watchableType, id, level) {
check(watchableType, String);
check(id, String);
check(level, Match.OneOf(String, null));
const userId = Meteor.userId();
let watchableObj = null;
let board = null;
if (watchableType === 'board') {
watchableObj = Boards.findOne(id);
if (!watchableObj) throw new Meteor.Error('error-board-doesNotExist');
board = watchableObj;
} else if (watchableType === 'list') {
watchableObj = Lists.findOne(id);
if (!watchableObj) throw new Meteor.Error('error-list-doesNotExist');
board = watchableObj.board();
} else if (watchableType === 'card') {
watchableObj = Cards.findOne(id);
if (!watchableObj) throw new Meteor.Error('error-card-doesNotExist');
board = watchableObj.board();
} else {
throw new Meteor.Error('error-json-schema');
}
if ((board.permission === 'private') && !board.hasMember(userId))
throw new Meteor.Error('error-board-notAMember');
watchableObj.setWatcher(userId, level);
return true;
},
});