Merge branch 'devel' into i18n

This commit is contained in:
Sacha Greif 2015-07-01 15:32:58 +09:00
commit 082b704371
45 changed files with 657 additions and 685 deletions

View file

@ -156,6 +156,6 @@ underscore@1.0.3
url@1.0.4
useraccounts:core@1.8.1
useraccounts:unstyled@1.8.1
utilities:avatar@0.7.11
utilities:avatar@0.7.12
webapp@1.2.0
webapp-hashing@1.0.3

View file

@ -1,8 +1,18 @@
## v0.21 “SlugScope”
* Added URL slugs for posts (i.e. `/posts/xyz/my-post-slug`).
* i18n files clean-up.
* Added post downvote setting.
* Refactored notifications code.
* Added `kadira-debug` package.
* Fixed avatar bug.
* Fixed screen refresh bug on post page.
## v0.20.6 “AutoScope”
* Add Extra CSS field (thanks @johnthepink!)
* Fix security issue with Settings (thanks @jshimko!)
* Add automatic template replacement
* Added Extra CSS field (thanks @johnthepink!).
* Fixed security issue with Settings (thanks @jshimko!).
* Added automatic template replacement.
## v0.20.5 “MinorScope”

View file

@ -1,7 +1,6 @@
// ------------------------------------------------------------------------------------------- //
// ------------------------------------------ Hooks ------------------------------------------ //
// ------------------------------------------------------------------------------------------- //
//////////////////////////////////////////////////////
// Collection Hooks //
//////////////////////////////////////////////////////
Comments.before.insert(function (userId, doc) {
// note: only actually sanitizes on the server
@ -16,10 +15,22 @@ Comments.before.update(function (userId, doc, fieldNames, modifier) {
}
});
/**
* Disallow $rename
*/
Comments.before.update(function (userId, doc, fieldNames, modifier) {
if (!!modifier.$rename) {
throw new Meteor.Error("illegal $rename operator detected!");
}
});
//////////////////////////////////////////////////////
// Callbacks //
//////////////////////////////////////////////////////
function afterCommentOperations (comment) {
var userId = comment.userId,
commentAuthor = Meteor.users.findOne(userId);
var userId = comment.userId;
// increment comment count
Meteor.users.update({_id: userId}, {
@ -33,10 +44,17 @@ function afterCommentOperations (comment) {
$addToSet: {commenters: userId}
});
return comment;
}
Telescope.callbacks.add("commentSubmitAsync", afterCommentOperations);
function upvoteOwnComment (comment) {
var commentAuthor = Meteor.users.findOne(comment.userId);
// upvote comment
Telescope.upvoteItem(Comments, comment, commentAuthor);
return comment;
}
Telescope.callbacks.add("commentSubmitAsync", afterCommentOperations);
Telescope.callbacks.add("commentSubmitAsync", upvoteOwnComment);

View file

@ -29,6 +29,7 @@ AutoForm.hooks({
},
onSuccess: function(formType, comment) {
// TODO: find out why comment is undefined here
comment = this.currentDoc;
Events.track("edit comment", {'commentId': comment._id});
Router.go('post_page', {_id: comment.postId});

View file

@ -1,26 +1,3 @@
//////////////////////////
// Notification Helpers //
//////////////////////////
/**
* Grab common comment properties (for email notifications, only used on server).
* @param {Object} post
*/
Comments.getProperties = function (comment) {
var commentAuthor = Meteor.users.findOne(comment.userId);
var post = Posts.findOne(comment.postId);
var c = {
profileUrl: commentAuthor && commentAuthor.getProfileUrl(true),
postUrl: Posts.getPageUrl(post, true),
authorName : comment.getAuthorName(true),
postTitle: Posts.findOne(comment.postId).title,
htmlBody: comment.htmlBody,
commentUrl: Comments.getPageUrl(comment, true)
};
console.log(c)
return c;
};
//////////////////
// Link Helpers //
//////////////////

View file

@ -39,10 +39,11 @@ Comments.submit = function (comment) {
// --------------------- Server-side Async Callbacks --------------------- //
// run all post submit server callbacks on comment object successively
Telescope.callbacks.runAsync("commentSubmitAsync", comment);
// note: query for comment to get fresh document with collection-hooks effects applied
Telescope.callbacks.runAsync("commentSubmitAsync", Comments.findOne(comment._id));
return comment;
}
};
Comments.edit = function (commentId, modifier, comment) {

View file

@ -28,6 +28,10 @@ Template.layout.rendered = function(){
link.href = Settings.get('faviconUrl', '/img/favicon.ico');
document.getElementsByTagName('head')[0].appendChild(link);
// canonical
var canonicalLink = document.createElement('link');
canonicalLink.rel = 'canonical';
document.getElementsByTagName('head')[0].appendChild(canonicalLink);
};
Template.layout.events({

View file

@ -7,10 +7,6 @@ AutoForm.addInputType("bootstrap-url", {
if (url.substring(0, 7) !== "http://" && url.substring(0, 8) !== "https://") {
url = "http://"+url;
}
// if URL only contains the two "/"" from "http://", then add trailing slash
if (url.match(/\//g).length === 2) {
url = url + "/";
}
return url;
}
}

View file

@ -1,92 +0,0 @@
Meteor.startup(function () {
// New user email
Router.route('/email/new-user/:id?', {
name: 'newUser',
where: 'server',
action: function() {
var html;
var user = Meteor.users.findOne(this.params.id);
var emailProperties = {
profileUrl: Users.getProfileUrl(user),
username: Users.getUserName(user)
};
html = Telescope.email.getTemplate('emailNewUser')(emailProperties);
this.response.write(Telescope.email.buildTemplate(html));
this.response.end();
}
});
// New post email
Router.route('/email/new-post/:id?', {
name: 'newPost',
where: 'server',
action: function() {
var html;
var post = Posts.findOne(this.params.id);
if (!!post) {
html = Telescope.email.getTemplate('emailNewPost')(Posts.getProperties(post));
} else {
html = "<h3>No post found.</h3>"
}
this.response.write(Telescope.email.buildTemplate(html));
this.response.end();
}
});
// Post approved
Router.route('/email/post-approved/:id?', {
name: 'postApproved',
where: 'server',
action: function() {
var html;
var post = Posts.findOne(this.params.id);
if (!!post) {
html = Telescope.email.getTemplate('emailPostApproved')(Posts.getProperties(post));
} else {
html = "<h3>No post found.</h3>"
}
this.response.write(Telescope.email.buildTemplate(html));
this.response.end();
}
});
// New comment email
Router.route('/email/new-comment/:id?', {
name: 'newComment',
where: 'server',
action: function() {
var html;
var comment = Comments.findOne(this.params.id);
if (!!comment) {
html = Telescope.email.getTemplate('emailNewComment')(Comments.getProperties(comment));
} else {
html = "<h3>No post found.</h3>"
}
this.response.write(Telescope.email.buildTemplate(html));
this.response.end();
}
});
// New reply email
Router.route('/email/new-reply/:id?', {
name: 'newReply',
where: 'server',
action: function() {
var html;
var comment = Comments.findOne(this.params.id);
if (!!comment) {
html = Telescope.email.getTemplate('emailNewReply')(Comments.getProperties(comment));
} else {
html = "<h3>No post found.</h3>"
}
this.response.write(Telescope.email.buildTemplate(html));
this.response.end();
}
});
});

View file

@ -25,15 +25,7 @@ Package.onUse(function (api) {
api.addFiles([
'lib/server/email.js',
'lib/server/routes.js',
'lib/server/templates/emailAccountApproved.handlebars',
'lib/server/templates/emailInvite.handlebars',
'lib/server/templates/emailNewComment.handlebars',
'lib/server/templates/emailNewPost.handlebars',
'lib/server/templates/emailNewPendingPost.handlebars',
'lib/server/templates/emailPostApproved.handlebars',
'lib/server/templates/emailNewReply.handlebars',
'lib/server/templates/emailNewUser.handlebars',
'lib/server/templates/emailTest.handlebars',
'lib/server/templates/emailWrapper.handlebars',
], ['server']);

View file

@ -5,4 +5,4 @@
Telescope = {};
Telescope.VERSION = '0.20.6';
Telescope.VERSION = '0.21';

View file

@ -140,9 +140,16 @@ Telescope.utils.getPostCommentUrl = function(postId, commentId) {
};
Telescope.utils.slugify = function (s) {
return getSlug(s, {
var slug = getSlug(s, {
truncate: 60
});
// can't have posts with an "edit" slug
if (slug === "edit") {
slug = "edit-1";
}
return slug;
};
Telescope.utils.getShortUrl = function(post) {

View file

@ -42,7 +42,7 @@ Package.onUse(function (api) {
'momentjs:moment@2.10.3',
'sacha:spin@0.2.4',
'aslagle:reactive-table@0.7.3',
'utilities:avatar@0.7.11',
'utilities:avatar@0.7.12',
'fortawesome:fontawesome@4.3.0',
'ccan:cssreset@1.0.0',
'djedi:sanitize-html@1.6.1',

View file

@ -0,0 +1,103 @@
// ------------------------------------------------------------------------------------------- //
// ----------------------------------------- Posts ------------------------------------------ //
// ------------------------------------------------------------------------------------------- //
// add new post notification callback on post submit
function postSubmitNotification (post) {
var adminIds = _.pluck(Users.find({'isAdmin': true}, {fields: {_id:1}}).fetch(), '_id');
var notifiedUserIds = _.pluck(Users.find({'telescope.notifications.posts': true}, {fields: {_id:1}}).fetch(), '_id');
var notificationData = {
post: _.pick(post, '_id', 'userId', 'title', 'url')
};
// remove post author ID from arrays
adminIds = _.without(adminIds, post.userId);
notifiedUserIds = _.without(notifiedUserIds, post.userId);
if (post.status === Posts.config.STATUS_PENDING && !!adminIds.length) {
// if post is pending, only notify admins
Herald.createNotification(adminIds, {courier: 'newPendingPost', data: notificationData});
} else if (!!notifiedUserIds.length) {
// if post is approved, notify everybody
Herald.createNotification(notifiedUserIds, {courier: 'newPost', data: notificationData});
}
}
Telescope.callbacks.add("postSubmitAsync", postSubmitNotification);
function postApprovedNotification (post) {
var notificationData = {
post: _.pick(post, '_id', 'userId', 'title', 'url')
};
Herald.createNotification(post.userId, {courier: 'postApproved', data: notificationData});
}
Telescope.callbacks.add("postApprovedAsync", postApprovedNotification);
// ------------------------------------------------------------------------------------------- //
// ---------------------------------------- Comments ----------------------------------------- //
// ------------------------------------------------------------------------------------------- //
// add new comment notification callback on comment submit
function commentSubmitNotifications (comment) {
if(Meteor.isServer && !comment.disableNotifications){
var post = Posts.findOne(comment.postId),
postAuthor = Users.findOne(post.userId),
userIdsNotified = [],
notificationData = {
comment: _.pick(comment, '_id', 'userId', 'author', 'htmlBody'),
post: _.pick(post, '_id', 'userId', 'title', 'url')
};
// 1. Notify author of post (if they have new comment notifications turned on)
// but do not notify author of post if they're the ones posting the comment
if (Users.getSetting(postAuthor, "notifications.comments", true) && comment.userId !== postAuthor._id) {
Herald.createNotification(post.userId, {courier: 'newComment', data: notificationData});
userIdsNotified.push(post.userId);
}
// 2. Notify author of comment being replied to
if (!!comment.parentCommentId) {
var parentComment = Comments.findOne(comment.parentCommentId);
// do not notify author of parent comment if they're also post author or comment author
// (someone could be replying to their own comment)
if (parentComment.userId !== post.userId && parentComment.userId !== comment.userId) {
var parentCommentAuthor = Users.findOne(parentComment.userId);
// do not notify parent comment author if they have reply notifications turned off
if (Users.getSetting(parentCommentAuthor, "notifications.replies", true)) {
// add parent comment to notification data
notificationData.parentComment = _.pick(parentComment, '_id', 'userId', 'author', 'htmlBody');
Herald.createNotification(parentComment.userId, {courier: 'newReply', data: notificationData});
userIdsNotified.push(parentComment.userId);
}
}
}
// 3. Notify users subscribed to the thread
// TODO: ideally this would be injected from the telescope-subscribe-to-posts package
if (!!post.subscribers) {
// remove userIds of users that have already been notified
// and of comment author (they could be replying in a thread they're subscribed to)
var subscriberIdsToNotify = _.difference(post.subscribers, userIdsNotified, [comment.userId]);
Herald.createNotification(subscriberIdsToNotify, {courier: 'newCommentSubscribed', data: notificationData});
userIdsNotified = userIdsNotified.concat(subscriberIdsToNotify);
}
}
}
Telescope.callbacks.add("commentSubmitAsync", commentSubmitNotifications);

View file

@ -0,0 +1,82 @@
Settings.addField({
fieldName: 'emailNotifications',
fieldSchema: {
type: Boolean,
optional: true,
defaultValue: true,
autoform: {
group: 'notifications',
instructions: 'Enable email notifications for new posts and new comments (requires restart).'
}
}
});
// make it possible to disable notifications on a per-comment basis
Comments.addField(
{
fieldName: 'disableNotifications',
fieldSchema: {
type: Boolean,
optional: true,
autoform: {
omit: true
}
}
}
);
// Add notifications options to user profile settings
Users.addField([
{
fieldName: 'telescope.notifications.users',
fieldSchema: {
label: 'New users',
type: Boolean,
optional: true,
defaultValue: false,
editableBy: ['admin'],
autoform: {
group: 'Email Notifications'
}
}
},
{
fieldName: 'telescope.notifications.posts',
fieldSchema: {
label: 'New posts',
type: Boolean,
optional: true,
defaultValue: false,
editableBy: ['admin', 'member'],
autoform: {
group: 'Email Notifications'
}
}
},
{
fieldName: 'telescope.notifications.comments',
fieldSchema: {
label: 'Comments on my posts',
type: Boolean,
optional: true,
defaultValue: true,
editableBy: ['admin', 'member'],
autoform: {
group: 'Email Notifications'
}
}
},
{
fieldName: 'telescope.notifications.replies',
fieldSchema: {
label: 'Replies to my comments',
type: Boolean,
optional: true,
defaultValue: true,
editableBy: ['admin', 'member'],
autoform: {
group: 'Email Notifications'
}
}
}
]);

View file

@ -0,0 +1,40 @@
/**
* Use user and post properties to populate post notifications objects.
* @param {Object} post
*/
Posts.getNotificationProperties = function (post) {
var postAuthor = Meteor.users.findOne(post.userId);
var properties = {
postAuthorName : Posts.getAuthorName(post),
postTitle : Telescope.utils.cleanUp(post.title),
profileUrl: Users.getProfileUrl(postAuthor, true),
postUrl: Posts.getPageUrl(post, true),
thumbnailUrl: post.thumbnailUrl,
linkUrl: !!post.url ? Telescope.utils.getOutgoingUrl(post.url) : Posts.getPageUrl(post, true)
};
if(post.url)
properties.url = post.url;
if(post.htmlBody)
properties.htmlBody = post.htmlBody;
return properties;
};
/**
* Use comment, user, and post properties to populate comment notifications objects.
* @param {Object} comment
*/
Comments.getNotificationProperties = function (comment, post) {
var commentAuthor = Meteor.users.findOne(comment.userId);
var properties = {
profileUrl: commentAuthor && commentAuthor.getProfileUrl(true),
postUrl: Posts.getPageUrl(post, true),
authorName : Comments.getAuthorName(comment),
postTitle: post.title,
htmlBody: comment.htmlBody,
commentUrl: Comments.getPageUrl(comment, true)
};
return properties;
};

View file

@ -1,4 +1,3 @@
// send emails every second when in dev environment
if (Meteor.absoluteUrl().indexOf('localhost') !== -1)
Herald.settings.queueTimer = 1000;
@ -14,146 +13,3 @@ Meteor.startup(function () {
Herald.settings.overrides.email = !Settings.get('emailNotifications', true);
});
var commentEmail = function (userToNotify) {
var notification = this;
// put in setTimeout so it doesn't hold up the rest of the method
Meteor.setTimeout(function () {
notificationEmail = buildEmailNotification(notification);
Telescope.email.send(Users.getEmail(userToNotify), notificationEmail.subject, notificationEmail.html);
}, 1);
};
// ------------------------------------------------------------------------------------------- //
// ----------------------------------------- Posts ------------------------------------------ //
// ------------------------------------------------------------------------------------------- //
Herald.addCourier('newPost', {
media: {
email: {
emailRunner: function (user) {
var p = Posts.getProperties(this.data);
var subject = p.postAuthorName+' has created a new post: '+p.postTitle;
var html = Telescope.email.buildTemplate(Telescope.email.getTemplate('emailNewPost')(p));
Telescope.email.send(Users.getEmail(user), subject, html);
}
}
}
// message: function (user) { return 'email template?' }
});
Herald.addCourier('newPendingPost', {
media: {
email: {
emailRunner: function (user) {
var p = Posts.getProperties(this.data);
var subject = p.postAuthorName+' has a new post pending approval: '+p.postTitle;
var html = Telescope.email.buildTemplate(Telescope.email.getTemplate('emailNewPendingPost')(p));
Telescope.email.send(Users.getEmail(user), subject, html);
}
}
}
});
Herald.addCourier('postApproved', {
media: {
onsite: {},
email: {
emailRunner: function (user) {
var p = Posts.getProperties(this.data);
var subject = 'Your post “'+p.postTitle+'” has been approved';
var html = Telescope.email.buildTemplate(Telescope.email.getTemplate('emailPostApproved')(p));
Telescope.email.send(Users.getEmail(user), subject, html);
}
}
},
message: {
default: function () {
return Blaze.toHTML(Blaze.With(this, function () {
return Template.notification_post_approved;
}));
}
},
transform: { // used for on-site notifications
postUrl: function () {
var p = Posts.getProperties(this.data);
return p.postUrl;
},
postTitle: function () {
var p = Posts.getProperties(this.data);
return p.postTitle;
}
}
});
// ------------------------------------------------------------------------------------------- //
// ---------------------------------------- Comments ----------------------------------------- //
// ------------------------------------------------------------------------------------------- //
// specify how to get properties used in template from comment data, used for on-site notifications
var commentCourierTransform = {
profileUrl: function () {
var user = Meteor.users.findOne(this.data.comment.userId);
return user && user.getProfileUrl();
},
authorName: function () {
return Comments.getAuthorName(this.data.comment);
},
commentUrl: function () {
return Posts.getPageUrl({_id: this.data.post._id});
},
postTitle: function () {
return this.data.post.title;
}
};
Herald.addCourier('newComment', {
media: {
onsite: {},
email: {
emailRunner: commentEmail
}
},
message: {
default: function () {
return Blaze.toHTML(Blaze.With(this, function () {
return Template.notification_new_comment;
}));
}
},
transform: commentCourierTransform
});
Herald.addCourier('newReply', {
media: {
onsite: {},
email: {
emailRunner: commentEmail
}
},
message: {
default: function () {
return Blaze.toHTML(Blaze.With(this, function () {
return Template.notification_new_reply;
}));
}
},
transform: commentCourierTransform
});
Herald.addCourier('newCommentSubscribed', {
media: {
onsite: {},
email: {
emailRunner: commentEmail
}
},
message: {
default: function () {
return Blaze.toHTML(Blaze.With(this, function () {
return Template.notification_new_reply;
}));
}
},
transform: commentCourierTransform
});

View file

@ -1,188 +1,97 @@
// add new post notification callback on post submit
function postSubmitNotification (post) {
var notifications = {
var adminIds = _.pluck(Users.find({'isAdmin': true}, {fields: {_id:1}}).fetch(), '_id');
var notifiedUserIds = _.pluck(Users.find({'telescope.notifications.posts': true}, {fields: {_id:1}}).fetch(), '_id');
newPost: {
properties: function () {
return Posts.getNotificationProperties(this.data.post);
},
subject: function () {
return this.postAuthorName+' has created a new post: '+this.postTitle;
},
emailTemplate: "emailNewPost"
},
// remove post author ID from arrays
adminIds = _.without(adminIds, post.userId);
notifiedUserIds = _.without(notifiedUserIds, post.userId);
newPendingPost: {
properties: function () {
return Posts.getNotificationProperties(this.data.post);
},
subject: function () {
return this.postAuthorName+' has a new post pending approval: '+this.postTitle;
},
emailTemplate: "emailNewPendingPost"
},
if (post.status === Posts.config.STATUS_PENDING && !!adminIds.length) {
// if post is pending, only notify admins
Herald.createNotification(adminIds, {courier: 'newPendingPost', data: post});
} else if (!!notifiedUserIds.length) {
// if post is approved, notify everybody
Herald.createNotification(notifiedUserIds, {courier: 'newPost', data: post});
postApproved: {
properties: function () {
return Posts.getNotificationProperties(this.data.post);
},
subject: function () {
return this.postAuthorName+' has a new post pending approval: '+this.postTitle;
},
emailTemplate: "emailPostApproved",
onsiteTemplate: "notification_post_approved"
},
newComment: {
properties: function () {
return Comments.getNotificationProperties(this.data.comment, this.data.post);
},
subject: function () {
return this.authorName+' left a new comment on your post "' + this.postTitle + '"';
},
emailTemplate: "emailNewComment",
onsiteTemplate: "notification_new_comment"
},
newReply: {
properties: function () {
return Comments.getNotificationProperties(this.data.comment, this.data.post);
},
subject: function () {
return this.authorName+' replied to your comment on "'+this.postTitle+'"';
},
emailTemplate: "emailNewReply",
onsiteTemplate: "notification_new_reply"
},
newCommentSubscribed: {
properties: function () {
return Comments.getNotificationProperties(this.data.comment, this.data.post);
},
subject: function () {
return this.authorName+' left a new comment on "' + this.postTitle + '"';
},
emailTemplate: "notification_new_comment",
onsite: "notification_new_comment"
}
return post;
}
Telescope.callbacks.add("postSubmitAsync", postSubmitNotification);
};
function postApprovedNotification (post) {
Herald.createNotification(post.userId, {courier: 'postApproved', data: post});
return post;
}
Telescope.callbacks.add("postApprovedAsync", postApprovedNotification);
// set up couriers
_.each(notifications, function (notification, notificationName) {
// add new comment notification callback on comment submit
function addCommentNotification (comment) {
if(Meteor.isServer && !comment.disableNotifications){
var post = Posts.findOne(comment.postId),
notificationData = {
comment: _.pick(comment, '_id', 'userId', 'author', 'body'),
post: _.pick(post, '_id', 'userId', 'title', 'url')
},
postAuthor = Users.findOne(post.userId),
userIdsNotified = [];
// 1. Notify author of post (if they have new comment notifications turned on)
// but do not notify author of post if they're the ones posting the comment
if (Users.getSetting(postAuthor, "notifications.comments", true) && comment.userId !== postAuthor._id) {
Herald.createNotification(post.userId, {courier: 'newComment', data: notificationData});
userIdsNotified.push(post.userId);
}
// 2. Notify author of comment being replied to
if (!!comment.parentCommentId) {
var parentComment = Comments.findOne(comment.parentCommentId);
// do not notify author of parent comment if they're also post author or comment author
// (someone could be replying to their own comment)
if (parentComment.userId !== post.userId && parentComment.userId !== comment.userId) {
var parentCommentAuthor = Users.findOne(parentComment.userId);
// do not notify parent comment author if they have reply notifications turned off
if (Users.getSetting(parentCommentAuthor, "notifications.replies", true)) {
// add parent comment to notification data
notificationData.parentComment = _.pick(parentComment, '_id', 'userId', 'author');
Herald.createNotification(parentComment.userId, {courier: 'newReply', data: notificationData});
userIdsNotified.push(parentComment.userId);
var courier = {
media: {
email: {
emailRunner: function (user) {
var properties = notification.properties.call(this);
var subject = notification.subject.call(properties);
var html = Telescope.email.buildTemplate(Telescope.email.getTemplate(notification.emailTemplate)(properties));
Telescope.email.send(Users.getEmail(user), subject, html);
}
}
}
// 3. Notify users subscribed to the thread
// TODO: ideally this would be injected from the telescope-subscribe-to-posts package
if (!!post.subscribers) {
// remove userIds of users that have already been notified
// and of comment author (they could be replying in a thread they're subscribed to)
var subscriberIdsToNotify = _.difference(post.subscribers, userIdsNotified, [comment.userId]);
Herald.createNotification(subscriberIdsToNotify, {courier: 'newCommentSubscribed', data: notificationData});
userIdsNotified = userIdsNotified.concat(subscriberIdsToNotify);
}
}
return comment;
}
Telescope.callbacks.add("commentSubmitAsync", addCommentNotification);
var emailNotifications = {
fieldName: 'emailNotifications',
fieldSchema: {
type: Boolean,
optional: true,
defaultValue: true,
autoform: {
group: 'notifications',
instructions: 'Enable email notifications for new posts and new comments (requires restart).'
}
}
};
Settings.addField(emailNotifications);
// make it possible to disable notifications on a per-comment basis
Comments.addField(
{
fieldName: 'disableNotifications',
fieldSchema: {
type: Boolean,
optional: true,
autoform: {
omit: true
}
}
}
);
// Add notifications options to user profile settings
Users.addField([
{
fieldName: 'telescope.notifications.users',
fieldSchema: {
label: 'New users',
type: Boolean,
optional: true,
defaultValue: false,
editableBy: ['admin'],
autoform: {
group: 'Email Notifications'
}
}
},
{
fieldName: 'telescope.notifications.posts',
fieldSchema: {
label: 'New posts',
type: Boolean,
optional: true,
defaultValue: false,
editableBy: ['admin', 'member'],
autoform: {
group: 'Email Notifications'
}
}
},
{
fieldName: 'telescope.notifications.comments',
fieldSchema: {
label: 'Comments on my posts',
type: Boolean,
optional: true,
defaultValue: true,
editableBy: ['admin', 'member'],
autoform: {
group: 'Email Notifications'
}
}
},
{
fieldName: 'telescope.notifications.replies',
fieldSchema: {
label: 'Replies to my comments',
type: Boolean,
optional: true,
defaultValue: true,
editableBy: ['admin', 'member'],
autoform: {
group: 'Email Notifications'
}
}
}
]);
function setNotificationDefaults (user) {
// set notifications default preferences
user.telescope.notifications = {
users: false,
posts: false,
comments: true,
replies: true
};
return user;
}
Telescope.callbacks.add("onCreateUser", setNotificationDefaults);
if (!!notification.onsiteTemplate) {
courier.media.onsite = {};
courier.message = function () {
var properties = notification.properties.call(this);
return Blaze.toHTML(Blaze.With(properties, function () {
return Template[notification.onsiteTemplate];
}));
};
}
Herald.addCourier(notificationName, courier);
});

View file

@ -2,53 +2,6 @@ getUnsubscribeLink = function(user){
return Telescope.utils.getRouteUrl('unsubscribe', {hash: user.telescope.emailHash});
};
// given a notification, return the correct subject and html to send an email
buildEmailNotification = function (notification) {
var subject,
template,
post = notification.data.post,
comment = notification.data.comment;
switch(notification.courier){
case 'newComment':
subject = notification.author()+' left a new comment on your post "' + post.title + '"';
template = 'emailNewComment';
break;
case 'newReply':
subject = notification.author()+' replied to your comment on "'+post.title+'"';
template = 'emailNewReply';
break;
case 'newCommentSubscribed':
subject = notification.author()+' left a new comment on "' + post.title + '"';
template = 'emailNewComment';
break;
default:
break;
}
var emailProperties = _.extend(notification.data, {
body: marked(comment.body),
profileUrl: Users.getProfileUrl({_id: comment.userId}),
postCommentUrl: Telescope.utils.getPostCommentUrl(post._id, comment._id),
postLink: Posts.getLink(post)
});
// console.log(emailProperties)
var notificationHtml = Telescope.email.getTemplate(template)(emailProperties);
var html = Telescope.email.buildTemplate(notificationHtml);
return {
subject: subject,
html: html
};
};
Meteor.methods({
unsubscribeUser : function(hash){
// TO-DO: currently, if you have somebody's email you can unsubscribe them

View file

@ -13,4 +13,95 @@ Meteor.startup(function () {
}
});
});
// New user email
Router.route('/email/new-user/:id?', {
name: 'newUser',
where: 'server',
action: function() {
var html;
var user = Meteor.users.findOne(this.params.id);
var emailProperties = {
profileUrl: Users.getProfileUrl(user),
username: Users.getUserName(user)
};
html = Telescope.email.getTemplate('emailNewUser')(emailProperties);
this.response.write(Telescope.email.buildTemplate(html));
this.response.end();
}
});
// New post email
Router.route('/email/new-post/:id?', {
name: 'newPost',
where: 'server',
action: function() {
var html;
var post = Posts.findOne(this.params.id);
if (!!post) {
html = Telescope.email.getTemplate('emailNewPost')(Posts.getNotificationProperties(post));
} else {
html = "<h3>No post found.</h3>"
}
this.response.write(Telescope.email.buildTemplate(html));
this.response.end();
}
});
// Post approved
Router.route('/email/post-approved/:id?', {
name: 'postApproved',
where: 'server',
action: function() {
var html;
var post = Posts.findOne(this.params.id);
if (!!post) {
html = Telescope.email.getTemplate('emailPostApproved')(Posts.getNotificationProperties(post));
} else {
html = "<h3>No post found.</h3>"
}
this.response.write(Telescope.email.buildTemplate(html));
this.response.end();
}
});
// New comment email
Router.route('/email/new-comment/:id?', {
name: 'newComment',
where: 'server',
action: function() {
var html;
var comment = Comments.findOne(this.params.id);
var post = Posts.findOne(comment.postId);
if (!!comment) {
html = Telescope.email.getTemplate('emailNewComment')(Comments.getNotificationProperties(comment, post));
} else {
html = "<h3>No post found.</h3>"
}
this.response.write(Telescope.email.buildTemplate(html));
this.response.end();
}
});
// New reply email
Router.route('/email/new-reply/:id?', {
name: 'newReply',
where: 'server',
action: function() {
var html;
var comment = Comments.findOne(this.params.id);
var post = Posts.findOne(comment.postId);
if (!!comment) {
html = Telescope.email.getTemplate('emailNewReply')(Comments.getNotificationProperties(comment, post));
} else {
html = "<h3>No post found.</h3>"
}
this.response.write(Telescope.email.buildTemplate(html));
this.response.end();
}
});
});

View file

@ -12,13 +12,15 @@ Package.onUse(function (api) {
api.use([
'telescope:core@0.20.6',
'kestanous:herald@1.3.0',
'kestanous:herald-email@0.5.0',
'cmather:handlebars-server@0.2.0'
'kestanous:herald-email@0.5.0'
]);
api.addFiles([
'lib/notifications.js',
'lib/herald.js',
'lib/helpers.js',
'lib/custom_fields.js',
'lib/notifications.js',
'lib/callbacks.js',
'lib/modules.js',
'package-tap.i18n'
], ['client', 'server']);
@ -39,7 +41,14 @@ Package.onUse(function (api) {
api.addFiles([
'lib/server/notifications-server.js',
'lib/server/routes.js'
'lib/server/routes.js',
'lib/server/templates/emailAccountApproved.handlebars',
'lib/server/templates/emailNewComment.handlebars',
'lib/server/templates/emailNewPost.handlebars',
'lib/server/templates/emailNewPendingPost.handlebars',
'lib/server/templates/emailPostApproved.handlebars',
'lib/server/templates/emailNewReply.handlebars',
'lib/server/templates/emailNewUser.handlebars'
], ['server']);
api.addFiles([

View file

@ -1,12 +1,62 @@
//////////////////////////////////////////////////////
// Collection Hooks //
//////////////////////////////////////////////////////
/**
* Generate HTML body from Markdown on post insert
*/
Posts.before.insert(function (userId, doc) {
if(!!doc.body)
doc.htmlBody = Telescope.utils.sanitize(marked(doc.body));
});
/**
* Generate HTML body from Markdown when post body is updated
*/
Posts.before.update(function (userId, doc, fieldNames, modifier) {
// if body is being modified, update htmlBody too
if (Meteor.isServer && modifier.$set && modifier.$set.body) {
modifier.$set.htmlBody = Telescope.utils.sanitize(marked(modifier.$set.body));
}
});
/**
* Generate slug when post title is updated
*/
Posts.before.update(function (userId, doc, fieldNames, modifier) {
// if title is being modified, update slug too
if (Meteor.isServer && modifier.$set && modifier.$set.title) {
modifier.$set.slug = Telescope.utils.slugify(modifier.$set.title);
}
});
/**
* Disallow $rename
*/
Posts.before.update(function (userId, doc, fieldNames, modifier) {
if (!!modifier.$rename) {
throw new Meteor.Error("illegal $rename operator detected!");
}
});
//////////////////////////////////////////////////////
// Callbacks //
//////////////////////////////////////////////////////
/**
* Increment the user's post count and upvote the post
*/
function afterPostSubmitOperations (post) {
var userId = post.userId,
postAuthor = Meteor.users.findOne(userId);
var userId = post.userId;
Meteor.users.update({_id: userId}, {$inc: {"telescope.postCount": 1}});
Telescope.upvoteItem(Posts, post, postAuthor);
return post;
}
Telescope.callbacks.add("postSubmitAsync", afterPostSubmitOperations);
function upvoteOwnPost (post) {
var postAuthor = Meteor.users.findOne(post.userId);
Telescope.upvoteItem(Posts, post, postAuthor);
return post;
}
Telescope.callbacks.add("postSubmitAsync", upvoteOwnPost);

View file

@ -28,9 +28,8 @@ AutoForm.hooks({
},
onSuccess: function(formType, post) {
post = this.currentDoc;
Events.track("edit post", {'postId': post._id});
Router.go('post_page', {_id: post._id});
Router.go('post_page', post);
},
onError: function(formType, error) {

View file

@ -88,8 +88,6 @@ Template.posts_list_controller.helpers({
// what to do when user clicks "load more"
loadMoreHandler: function (instance) {
event.preventDefault();
// increase limit by 5 and update it
var limit = instance.postsLimit.get();
limit += Settings.get('postsPerPage', 10);

View file

@ -1,31 +1,3 @@
///////////////////////////
// Notifications Helpers //
///////////////////////////
/**
* Grab common post properties (for email notifications, only used on server).
* @param {Object} post
*/
Posts.getProperties = function (post) {
var postAuthor = Meteor.users.findOne(post.userId);
var p = {
postAuthorName : post.getAuthorName(),
postTitle : Telescope.utils.cleanUp(post.title),
profileUrl: Users.getProfileUrl(postAuthor, true),
postUrl: post.getPageUrl(true),
thumbnailUrl: post.thumbnailUrl,
linkUrl: !!post.url ? Telescope.utils.getOutgoingUrl(post.url) : post.getPageUrl(true)
};
if(post.url)
p.url = post.url;
if(post.htmlBody)
p.htmlBody = post.htmlBody;
return p;
};
//////////////////
// Link Helpers //
//////////////////

View file

@ -66,7 +66,8 @@ Posts.submit = function (post) {
// --------------------- Server-Side Async Callbacks --------------------- //
Telescope.callbacks.runAsync("postSubmitAsync", post);
// note: query for post to get fresh document with collection-hooks effects applied
Telescope.callbacks.runAsync("postSubmitAsync", Posts.findOne(post._id));
return post;
};

View file

@ -30,7 +30,7 @@ Posts.getSubParams = function (terms) {
_.extend(parameters.options, {limit: parseInt(terms.limit)});
// limit to "maxLimit" posts at most when limit is undefined, equal to 0, or superior to maxLimit
if(!parameters.options.limit || parameters.options.limit == 0 || parameters.options.limit > maxLimit) {
if(!parameters.options.limit || parameters.options.limit === 0 || parameters.options.limit > maxLimit) {
parameters.options.limit = maxLimit;
}

View file

@ -234,35 +234,3 @@ Posts.allow({
remove: _.partial(Telescope.allowCheck, Posts)
});
//////////////////////////////////////////////////////
// Collection Hooks //
// https://atmospherejs.com/matb33/collection-hooks //
//////////////////////////////////////////////////////
/**
* Generate HTML body from Markdown on post insert
*/
Posts.before.insert(function (userId, doc) {
if(!!doc.body)
doc.htmlBody = Telescope.utils.sanitize(marked(doc.body));
});
/**
* Generate HTML body from Markdown when post body is updated
*/
Posts.before.update(function (userId, doc, fieldNames, modifier) {
// if body is being modified, update htmlBody too
if (Meteor.isServer && modifier.$set && modifier.$set.body) {
modifier.$set.htmlBody = Telescope.utils.sanitize(marked(modifier.$set.body));
}
});
/**
* Generate slug when post title is updated
*/
Posts.before.update(function (userId, doc, fieldNames, modifier) {
// if title is being modified, update slug too
if (Meteor.isServer && modifier.$set && modifier.$set.title) {
modifier.$set.slug = Telescope.utils.slugify(marked(modifier.$set.title));
}
});

View file

@ -115,7 +115,7 @@ Posts.controllers.page = RouteController.extend({
template: 'post_page',
waitOn: function() {
waitOn: function () {
this.postSubscription = coreSubscriptions.subscribe('singlePost', this.params._id);
this.postUsersSubscription = coreSubscriptions.subscribe('postUsers', this.params._id);
this.commentSubscription = coreSubscriptions.subscribe('commentsList', {view: 'postComments', postId: this.params._id});
@ -131,11 +131,9 @@ Posts.controllers.page = RouteController.extend({
},
onBeforeAction: function () {
if (! this.post()) {
if (!this.post()) {
if (this.postSubscription.ready()) {
this.render('not_found');
} else {
this.render('loading');
}
} else {
this.next();
@ -154,8 +152,11 @@ Posts.controllers.page = RouteController.extend({
onAfterAction: function () {
var post = this.post();
if (post && post.slug !== this.params.slug) {
window.history.replaceState({}, "", post.getPageUrl());
if (post) {
if (post.slug !== this.params.slug) {
window.history.replaceState({}, "", post.getPageUrl());
}
$('link[rel="canonical"]').attr("href", post.getPageUrl(true));
}
},
@ -202,33 +203,6 @@ Meteor.startup(function () {
controller: Posts.controllers.scheduled
});
// Post Page
// legacy route
Router.route('/posts/:_id', {
name: 'post_page_id',
onBeforeAction: function () {
var post = {
slug: '_',
_id: this.params._id
};
Router.go("post_page", post);
}
});
Router.route('/p/:_id/:slug?', {
name: 'post_page',
controller: Posts.controllers.page
});
Router.route('/posts/:_id/comment/:commentId', {
name: 'post_page_comment',
controller: Posts.controllers.page,
onAfterAction: function () {
// TODO: scroll to comment position
}
});
// Post Edit
Router.route('/posts/:_id/edit', {
@ -249,6 +223,21 @@ Meteor.startup(function () {
fastRender: true
});
// Post Page
Router.route('/posts/:_id/:slug?', {
name: 'post_page',
controller: Posts.controllers.page
});
Router.route('/posts/:_id/comment/:commentId', {
name: 'post_page_comment',
controller: Posts.controllers.page,
onAfterAction: function () {
// TODO: scroll to comment position
}
});
// Post Submit
Router.route('/submit', {

View file

@ -33,6 +33,7 @@ Meteor.startup(function () {
importRelease('0.20.4');
importRelease('0.20.5');
importRelease('0.20.6');
importRelease('0.21');
// if this is before the first run, mark all release notes as read to avoid showing them
if (!Events.findOne({name: 'firstRun'})) {

View file

@ -54,6 +54,7 @@ Package.onUse(function (api) {
api.addFiles('releases/0.20.4.md', 'server', { isAsset: true });
api.addFiles('releases/0.20.5.md', 'server', { isAsset: true });
api.addFiles('releases/0.20.6.md', 'server', { isAsset: true });
api.addFiles('releases/0.21.md', 'server', { isAsset: true });
// i18n languages (must come last)

View file

@ -0,0 +1,9 @@
### v0.21 “SlugScope”
* Added URL slugs for posts (i.e. `/posts/xyz/my-post-slug`).
* i18n files clean-up.
* Added post downvote setting.
* Refactored notifications code.
* Added `kadira-debug` package.
* Fixed avatar bug.
* Fixed screen refresh bug on post page.

View file

@ -453,10 +453,25 @@ Settings.get = function(setting, defaultValue) {
}
};
// use custom template for checkboxes - not working yet
// if(Meteor.isClient){
// AutoForm.setDefaultTemplateForType('afCheckbox', 'settings');
// }
/**
* Add trailing slash if needed on insert
*/
Settings.before.insert(function (userId, doc) {
if(doc.siteUrl && doc.siteUrl.match(/\//g).length === 2) {
doc.siteUrl = doc.siteUrl + "/";
}
});
/**
* Add trailing slash if needed on update
*/
Settings.before.update(function (userId, doc, fieldNames, modifier) {
if(modifier.$set && modifier.$set.siteUrl && modifier.$set.siteUrl.match(/\//g).length === 2) {
modifier.$set.siteUrl = modifier.$set.siteUrl + "/";
}
});
Meteor.startup(function () {
Settings.allow({

View file

@ -6,7 +6,7 @@ Meteor.startup(function () {
});
},
categoryLink: function(){
return getCategoryUrl(this.slug);
return Categories.getUrl(this.slug);
}
});
});

@ -1 +0,0 @@
Subproject commit 01fbc45251e1e7f9fb9f73beaf131db2bd24a698

View file

@ -1,3 +1,70 @@
//////////////////////////////////////////////////////
// Collection Hooks //
//////////////////////////////////////////////////////
/**
* Generate HTML body from Markdown on user bio insert
*/
Users.after.insert(function (userId, user) {
// run create user async callbacks
Telescope.callbacks.runAsync("onCreateUserAsync", user);
// check if all required fields have been filled in. If so, run profile completion callbacks
if (Users.hasCompletedProfile(user)) {
Telescope.callbacks.runAsync("profileCompletedAsync", user);
}
});
/**
* Generate HTML body from Markdown when user bio is updated
*/
Users.before.update(function (userId, doc, fieldNames, modifier) {
// if bio is being modified, update htmlBio too
if (Meteor.isServer && modifier.$set && modifier.$set["telescope.bio"]) {
modifier.$set["telescope.htmlBio"] = Telescope.utils.sanitize(marked(modifier.$set["telescope.bio"]));
}
});
/**
* Disallow $rename
*/
Users.before.update(function (userId, doc, fieldNames, modifier) {
if (!!modifier.$rename) {
throw new Meteor.Error("illegal $rename operator detected!");
}
});
/**
* If user.telescope.email has changed, check for existing emails and change user.emails if needed
*/
if (Meteor.isServer) {
Users.before.update(function (userId, doc, fieldNames, modifier) {
var user = doc;
// if email is being modified, update user.emails too
if (Meteor.isServer && modifier.$set && modifier.$set["telescope.email"]) {
var newEmail = modifier.$set["telescope.email"];
// check for existing emails and throw error if necessary
var userWithSameEmail = Users.findByEmail(newEmail);
if (userWithSameEmail && userWithSameEmail._id !== doc._id) {
throw new Meteor.Error("email_taken2", i18n.t("this_email_is_already_taken") + " (" + newEmail + ")");
}
// if user.emails exists, change it too
if (!!user.emails) {
user.emails[0].address = newEmail;
modifier.$set.emails = user.emails;
}
}
});
}
//////////////////////////////////////////////////////
// Callbacks //
//////////////////////////////////////////////////////
/**
* Set up user object on creation
* @param {Object} user the user object being iterated on and returned

View file

@ -51,9 +51,10 @@ Users.is.invited = function (userOrUserId) {
Users.is.invitedById = Users.is.invited;
Meteor.users.helpers({
isAdmin: function () {
return Users.is.admin(this);
},
// conflicts with user.isAdmin property
// isAdmin: function () {
// return Users.is.admin(this);
// },
isOwner: function (document) {
return Users.is.owner(this, document);
},

View file

@ -187,7 +187,7 @@ Users.schema = new SimpleSchema({
},
username: {
type: String,
regEx: /^[a-z0-9A-Z_]{3,15}$/,
// regEx: /^[a-z0-9A-Z_]{3,15}$/,
public: true,
optional: true
},
@ -249,58 +249,3 @@ Users.allow({
remove: _.partial(Telescope.allowCheck, Meteor.users)
});
//////////////////////////////////////////////////////
// Collection Hooks //
// https://atmospherejs.com/matb33/collection-hooks //
//////////////////////////////////////////////////////
/**
* Generate HTML body from Markdown on user bio insert
*/
Users.after.insert(function (userId, user) {
// run create user async callbacks
Telescope.callbacks.runAsync("onCreateUserAsync", user);
// check if all required fields have been filled in. If so, run profile completion callbacks
if (Users.hasCompletedProfile(user)) {
Telescope.callbacks.runAsync("profileCompletedAsync", user);
}
});
/**
* Generate HTML body from Markdown when user bio is updated
*/
Users.before.update(function (userId, doc, fieldNames, modifier) {
// if bio is being modified, update htmlBio too
if (Meteor.isServer && modifier.$set && modifier.$set["telescope.bio"]) {
modifier.$set["telescope.htmlBio"] = Telescope.utils.sanitize(marked(modifier.$set["telescope.bio"]));
}
});
/**
* If user.telescope.email has changed, check for existing emails and change user.emails if needed
*/
if (Meteor.isServer) {
Users.before.update(function (userId, doc, fieldNames, modifier) {
var user = doc;
// if email is being modified, update user.emails too
if (Meteor.isServer && modifier.$set && modifier.$set["telescope.email"]) {
var newEmail = modifier.$set["telescope.email"];
// check for existing emails and throw error if necessary
var userWithSameEmail = Users.findByEmail(newEmail);
if (userWithSameEmail && userWithSameEmail._id !== doc._id) {
throw new Meteor.Error("email_taken2", i18n.t("this_email_is_already_taken") + " (" + newEmail + ")");
}
// if user.emails exists, change it too
if (!!user.emails) {
user.emails[0].address = newEmail;
modifier.$set.emails = user.emails;
}
}
});
}