diff --git a/.meteor/packages b/.meteor/packages
index fe41447..8b344b0 100644
--- a/.meteor/packages
+++ b/.meteor/packages
@@ -17,3 +17,4 @@ momentjs
collectionFS
imagemagick
loadpicker
+page-js-ie-support
diff --git a/client/helpers/router.js b/client/helpers/router.js
index e069684..7132d7b 100644
--- a/client/helpers/router.js
+++ b/client/helpers/router.js
@@ -9,6 +9,9 @@ Meteor.Router.add({
}
}
},
+ '/mailbox/compose': 'compose',
+ '/mailbox': 'mailbox',
+ '/settings': 'settings',
'/profile': {
as: 'profile',
to: 'profile',
@@ -20,7 +23,6 @@ Meteor.Router.add({
to: 'profile',
and: function(id){ Session.set('currentUserProfile', id);}
},
- '/settings': 'settings',
'*': 'p404'
});
diff --git a/client/main.js b/client/main.js
index 76e8b4d..f46dc57 100644
--- a/client/main.js
+++ b/client/main.js
@@ -1,13 +1,25 @@
-Meteor.subscribe('questions');
+// All my data
Meteor.subscribe('myData', function(){
Session.set('settings', Meteor.user().settings||{});
});
+// My own picture.
Meteor.subscribe('myPictures');
+// Online friends widget.
Meteor.subscribe('myOnlineFriends');
+// The list of my friends.
+Meteor.subscribe('myFriends');
+
+// My conversations
+Meteor.subscribe('myConversations');
+
+
+// When visiting someone's profile
Deps.autorun(function () {
userProfileHandle = Meteor.subscribe("oneUserProfile", Session.get("currentUserProfile"));
userPictureHandle = Meteor.subscribe("oneUserPictures", Session.get("currentUserProfile"));
});
+// Questions for the profile form.
+Meteor.subscribe('questions');
diff --git a/client/views/includes/header.html b/client/views/includes/header.html
index 5049d27..8ab5354 100644
--- a/client/views/includes/header.html
+++ b/client/views/includes/header.html
@@ -10,6 +10,9 @@
Settings
+
+
+
{{/if}}
diff --git a/client/views/mailbox/compose/compose.html b/client/views/mailbox/compose/compose.html
new file mode 100644
index 0000000..6814d2c
--- /dev/null
+++ b/client/views/mailbox/compose/compose.html
@@ -0,0 +1,16 @@
+
+
+ Compose new email
+
+
+
\ No newline at end of file
diff --git a/client/views/mailbox/compose/compose.js b/client/views/mailbox/compose/compose.js
new file mode 100644
index 0000000..424df9e
--- /dev/null
+++ b/client/views/mailbox/compose/compose.js
@@ -0,0 +1,21 @@
+Template.compose.helpers({
+ friendsList: function(){
+ var me = Meteor.users.findOne(Meteor.userId());
+ return Meteor.users.find({_id : {$in : me.friends}}, {reactive: false});
+ }
+});
+
+Template.compose.events({
+ 'submit form': function(e){
+ e.preventDefault();
+ var values = {
+ to: $('select[name="to"]').val(),
+ body: $('textarea[name="body"]').val()
+ };
+ if(values.to && values.body){
+ Meteor.call('sendMessage', values, function(err, res){
+ console.log(err, res);
+ })
+ }
+ }
+});
\ No newline at end of file
diff --git a/client/views/mailbox/mailbox.html b/client/views/mailbox/mailbox.html
new file mode 100644
index 0000000..9752a0e
--- /dev/null
+++ b/client/views/mailbox/mailbox.html
@@ -0,0 +1,11 @@
+
+
+Compose a new message
+
+
+ {{#each conversations}}
+ - {{lastMessage}} with {{userInfo}}
+ {{/each}}
+
+
+
\ No newline at end of file
diff --git a/client/views/mailbox/mailbox.js b/client/views/mailbox/mailbox.js
new file mode 100644
index 0000000..ab5f2d7
--- /dev/null
+++ b/client/views/mailbox/mailbox.js
@@ -0,0 +1,9 @@
+Template.mailbox.helpers({
+ conversations: function(){
+ return Conversations.find({owner: Meteor.userId()});
+ },
+ userInfo: function(){
+
+ return this.with;
+ }
+});
\ No newline at end of file
diff --git a/lib/collections/activities.js b/lib/collections/activities.js
new file mode 100644
index 0000000..b9e6f01
--- /dev/null
+++ b/lib/collections/activities.js
@@ -0,0 +1,2 @@
+// Activities between users
+Activities = new Meteor.Collection('activities');
\ No newline at end of file
diff --git a/lib/collections/conversations.js b/lib/collections/conversations.js
new file mode 100644
index 0000000..8adf4f6
--- /dev/null
+++ b/lib/collections/conversations.js
@@ -0,0 +1,15 @@
+Conversations = new Meteor.Collection('conversations');
+
+// Record who's talking with who, and the last message.
+// All managed server side with message posting.
+Conversations.allow({
+ insert: function(userId, doc){
+ return false;
+ },
+ update: function(userId, doc){
+ return false;
+ },
+ remove: function(){
+ return false;
+ }
+});
\ No newline at end of file
diff --git a/lib/collections/message.js b/lib/collections/message.js
new file mode 100644
index 0000000..23e466f
--- /dev/null
+++ b/lib/collections/message.js
@@ -0,0 +1,38 @@
+Messages = new Meteor.Collection('messages');
+
+Messages.allow({
+ insert: function(userId, doc){
+ // Check that the message is emitted by userId
+ if(!(doc.from === userId)){
+ return false;
+ }
+
+ // Check that the emitter exists
+ var target ;
+ if(!(target = Meteor.user.findOne(doc.to))){
+ return false;
+ }
+ // Check that the emitter is not blacklisted to the receiver.
+ if(_.contains(target.blacklist, userId)){
+ return false;
+ }
+
+ // check that the message is not empty
+ if(doc.body.length == 0){
+ return false;
+ }
+
+ // All good. Let's go.
+ return true;
+ },
+ update: function(userId, fields){
+ return userId == fields.to; // Can only update message /received/
+ }
+});
+
+Messages.deny({
+ update: function(userId, fields){
+ // only field that can be updated is the 'viewed' field.
+ return (_.without(fields, 'viewed')).length > 0;
+ }
+});
\ No newline at end of file
diff --git a/lib/collections/pictures.js b/lib/collections/pictures.js
new file mode 100644
index 0000000..128b2b5
--- /dev/null
+++ b/lib/collections/pictures.js
@@ -0,0 +1,69 @@
+Pictures = new CollectionFS('pictures', {autopublish: false});
+
+var isValidImage = function(type){
+ return _.contains(['image/png','image/jpeg','image/gif'], type);
+};
+
+Pictures.allow({
+ insert: function(userId, myFile) {
+ var valid = true;
+ // Is a valid image
+ valid = valid && isValidImage(myFile.contentType);
+ // Is not too big.
+ valid = valid && myFile.length < (this.maxFileSize || 1024*1024);
+ // User quota is ok.
+ valid = valid && (!this.maxFilePerUser || (this.maxFilePerUser == -1) || Pictures.find({owner: userId}).count() < this.maxFilePerUser);
+ // User owns the file -- don't know how it could not, but that's in the doc.
+ valid = valid && userId && myFile.owner === userId;
+ return valid;
+ },
+ update: function(userId, file, fields, modifier) {
+ var valid = true, authorized = this.authorizedFields || [];
+ // Fields are authorized for modification
+ if(authorized){
+ valid = valid && _.every(fields, function(f){_.contains(authorized, f)});
+ }
+ // Check that user owns the image...
+ valid = valid && file.owner === userId;
+
+ return valid;
+ },
+ remove: function(userId, files) { return false; }
+});
+
+Pictures.fileHandlers({
+ save: function(options){
+ if (options.fileRecord.length > 5000000 || !isValidImage(options.fileRecord.contentType)){
+ return null;
+ }
+ return { blob: options.blob, fileRecord: options.fileRecord };
+ },
+ thumbnail50x50: function(options){
+ if (isValidImage(options.fileRecord.contentType)){
+ var destination = options.destination();
+ Imagemagick.resize({
+ srcData: options.blob,
+ dstPath: destination.serverFilename,
+ width: 50,
+ height: 50
+ });
+ return destination.fileData;
+ } else {
+ return null;
+ }
+ },
+ thumbnail150x150: function(options){
+ if (isValidImage(options.fileRecord.contentType)){
+ var destination = options.destination();
+ Imagemagick.resize({
+ srcData: options.blob,
+ dstPath: destination.serverFilename,
+ width: 150,
+ height: 150
+ });
+ return destination.fileData;
+ } else {
+ return null;
+ }
+}
+});
\ No newline at end of file
diff --git a/lib/collections/questions.js b/lib/collections/questions.js
new file mode 100644
index 0000000..0907519
--- /dev/null
+++ b/lib/collections/questions.js
@@ -0,0 +1,3 @@
+// Questions for user profile.
+Questions = new Meteor.Collection('questions');
+
diff --git a/main.config.js b/main.config.js
index 4822c7e..0d1fbc0 100644
--- a/main.config.js
+++ b/main.config.js
@@ -10,6 +10,18 @@ if(Meteor.isClient){
filePickerKey = "Av2HCAqJSM2aHdX5yKTZtz";
}
+/*
+ Messages
+ */
+// Duration to measure velocity (default 2 minutes).
+//Messages.velocityCaliber = 60*1000*2;
+Messages.velocityCaliber = 30*1000;
+// If target user is online, how many messages per velocityCaliber millisecond can the emitter send
+Messages.onlineMaxVelocity = 5;
+// If target is offline
+Messages.offlineMaxVelocity = 5;
+// Cooldown penalty (def: 1 minute)
+Messages.cooldownPenalty = 10*1000;
/*
User posted Pictures.
diff --git a/server/fixtures.js b/server/fixtures.js
index c2bb87c..aadc150 100644
--- a/server/fixtures.js
+++ b/server/fixtures.js
@@ -46,4 +46,58 @@ if(Questions.find().count() == 0){
required: true
});
}
-
+if(Meteor.users.find({}).count() <= 2){
+ Meteor.users.insert({
+ "createdAt": 1372216131137,
+ "emails": [
+ {
+ "address": "whatever",
+ "verified": false
+ }
+ ],
+ "friends": [
+ "L7SLCm9mJyetnb3oD"
+ ],
+ "invisible": false,
+ "lastseen": 1373524657763,
+ "online": 1,
+ "profile": {
+ "dob": "08-11-1982",
+ "gender": "Female",
+ "name": "Fake User 1",
+ "online": 0
+ },
+ "services": {
+ },
+ "settings": {
+ "invisible": false
+ },
+ "visible": 1
+ });
+ Meteor.users.insert({
+ "createdAt": 1372216131137,
+ "emails": [
+ {
+ "address": "whatever2",
+ "verified": false
+ }
+ ],
+ "friends": [
+ "L7SLCm9mJyetnb3oD"
+ ],
+ "invisible": false,
+ "lastseen": 1373524657763,
+ "online": 1,
+ "profile": {
+ "dob": "08-11-1992",
+ "gender": "Male",
+ "name": "Fake User 2",
+ "online": 0
+ },
+ "services": { },
+ "settings": {
+ "invisible": false
+ },
+ "visible": 1
+ });
+}
diff --git a/server/messages.js b/server/messages.js
new file mode 100644
index 0000000..a27a64c
--- /dev/null
+++ b/server/messages.js
@@ -0,0 +1,103 @@
+Meteor.methods({
+ sendMessage: function(doc){
+
+ // Check that the emitter exists
+ var target ;
+ if(!(target = Meteor.users.findOne(doc.to))){
+ throw new Meteor.Error(404, 'User not found');
+ }
+ // Check that the emitter is not blacklisted by the receiver.
+ if(_.contains(target.blacklist, Meteor.userId())){
+ throw new Meteor.Error(300, 'Permission Denied');
+ }
+
+ // Sender
+ var user = Meteor.users.findOne(Meteor.userId());
+
+ // Check if the user is under a cooldown penalty.
+ var cooldown = user.cooldown;
+
+ if(cooldown && cooldown > new Date().getTime()){
+ //Add an extra cooldown.
+ Meteor.users.update(Meteor.userId(), { $inc : {cooldown: Messages.cooldownPenalty} });
+ throw new Meteor.Error(300, 'Posting too fast, wait ' + moment.duration(cooldown+Messages.cooldownPenalty-new Date().getTime()).humanize());
+ }
+
+ // Check user message sending velocity.
+ var velocity = target.profile.online ? Messages.onlineMaxVelocity : Messages.offlineMaxVelocity;
+
+ var messages = Messages.find({from: Meteor.userId(), sent: {$gt: new Date().getTime() - Messages.velocityCaliber}});
+
+ if(messages.count() > velocity){ // Posting too fast
+ // Give a cooldown penalty
+ Meteor.users.update(Meteor.userId(), {$set : { cooldown: new Date().getTime()+Messages.cooldownPenalty }});
+ throw new Meteor.Error(300, 'Posting too fast, wait ' + moment.duration(Messages.cooldownPenalty).humanize());
+ }
+
+ // All good, let's send that message
+ // Reset potential user penalty
+ Meteor.users.update(Meteor.userId(), {$set: {cooldown: 0}});
+
+ doc.sent = new Date().getTime();
+ doc.viewed = 0;
+ doc.from = Meteor.userId();
+
+ var message = _.pick(doc, ['body','sent','from','to', 'viewed']);
+
+ var id = Messages.insert(message);
+ // Create the conversations between those two uses if it does not exist already.
+ // apparently upsert is fucked up with Meteor, so that's the best I could find.
+ if(Conversations.findOne({
+ owner: Meteor.userId(),
+ with: target._id
+ })){
+ Conversations.update(
+ {
+ owner: Meteor.userId(),
+ with: target._id
+ },{ $set: {
+ timestamp: new Date().getTime(),
+ lastMessageId: id,
+ lastMessage: doc.body,
+ lastMessageFrom: Meteor.userId()
+ }
+ });
+ } else {
+ Conversations.insert({
+ timestamp: new Date().getTime(),
+ owner: Meteor.userId(),
+ with: target._id,
+ lastMessageId: id,
+ lastMessage: doc.body,
+ lastMessageFrom: Meteor.userId()
+ });
+ }
+ if(Conversations.findOne({
+ with: Meteor.userId(),
+ owner: target._id
+ })){
+ Conversations.update(
+ {
+ with: Meteor.userId(),
+ owner: target._id
+ },{ $set: {
+ timestamp: new Date().getTime(),
+ lastMessageId: id,
+ lastMessage: doc.body,
+ lastMessageFrom: Meteor.userId()
+ }
+ });
+ } else {
+ Conversations.insert({
+ timestamp: new Date().getTime(),
+ with: Meteor.userId(),
+ owner: target._id,
+ lastMessageId: id,
+ lastMessage: doc.body,
+ lastMessageFrom: Meteor.userId()
+ });
+ }
+
+ return true;
+ }
+});
\ No newline at end of file
diff --git a/server/publications.js b/server/publications.js
index 9b4eca1..4d8618c 100644
--- a/server/publications.js
+++ b/server/publications.js
@@ -15,6 +15,61 @@ Meteor.publish("myData", function () {
);
});
+//Meteor.publish("myConversations", function() {
+// Meteor.publishWithRelations({
+// handle: this,
+// collection: Conversations,
+// filter: {owner: this.userId},
+// mappings: [{
+// key: 'with',
+// collection: Meteor.users
+// }]
+// });
+//});
+//
+//Meteor.publish("myConversations", function(limit, skip){
+// // We need basic information about the people we're talking to.
+// var self = this, conversationsHandle = null, profileHandle = [];
+//
+// function publishProfile(conversation){
+//// console.log('conversation', conversation);
+// var profile = Meteor.users.find(conversation.with);
+// profileHandle[conversation._id] = profile.observe({
+// added: function(profile){
+// self.added(profile);
+// }
+// });
+// }
+// conversationsHandle = Conversations.find({}, {limit: limit, skip: skip}).observe({
+//// conversationsHandle = Conversations.find({owner: this.userId}, {limit: limit, skip: skip}).observe({
+// added: function(conversation){
+//// publishProfile(conversation);
+// self.added(conversation);
+// }
+//
+// });
+//
+// self.ready();
+//
+// self.onStop(function(){
+// conversationsHandle.stop();
+// _.each(profileHandle, function(p){ p.stop()});
+// });
+//});
+
+Meteor.publish("myConversations", function(limit, skip){
+ return Conversations.find({owner: this.userId}, {limit: limit, skip: skip});
+});
+
+Meteor.publish("myMessages", function(limit, skip){
+ return Messages.find({ $or: { to: this.userId, from: this.userId}}, {limit: limit, skip: skip});
+});
+
+Meteor.publish("myFriends", function(limit, skip){
+ var me = Meteor.users.findOne(this.userId);
+ return Meteor.users.find({_id : {$in : me.friends}}, {fields: { profile: 1 } });
+});
+
// Also maintains user online/offline status
Meteor.publish("myOnlineFriends", function(){
var friends = Meteor.users.findOne(this.userId).friends || [];
diff --git a/server/users.js b/server/users.js
index 709aa43..8aca32f 100644
--- a/server/users.js
+++ b/server/users.js
@@ -31,7 +31,6 @@ Meteor.methods({
if(!valid){
throw new Meteor.Error(300, 'errors.not_saved');
} else {
-
// Account only visible if at least those two information are made available.
var visible = 0;
if(cleaned.name && cleaned.gender){
diff --git a/smart.json b/smart.json
index 3aa5be8..a7e8be8 100644
--- a/smart.json
+++ b/smart.json
@@ -10,6 +10,7 @@
"momentjs": {},
"collectionFS": {},
"imagemagick": {},
- "loadpicker": {}
+ "loadpicker": {},
+ "publish-with-relations": {}
}
}
diff --git a/smart.lock b/smart.lock
index 98e16a7..f85b0f3 100644
--- a/smart.lock
+++ b/smart.lock
@@ -12,7 +12,8 @@
"momentjs": {},
"collectionFS": {},
"imagemagick": {},
- "loadpicker": {}
+ "loadpicker": {},
+ "publish-with-relations": {}
},
"packages": {
"accounts-ui-bootstrap-dropdown": {
@@ -22,8 +23,8 @@
},
"router": {
"git": "https://github.com/tmeasday/meteor-router.git",
- "tag": "v0.5.1",
- "commit": "32e377c7703bb119acccc859fc7296882903cbe7"
+ "tag": "v0.5.2",
+ "commit": "45af76a97624f141e4a61298d5a049e6adbb631c"
},
"i18n": {
"git": "https://github.com/jerico-dev/meteor-i18n.git",
@@ -50,6 +51,11 @@
"tag": "v1.0.0",
"commit": "9f8aa83427b21e2d6279db3f018900bf09560352"
},
+ "publish-with-relations": {
+ "git": "https://github.com/erundook/meteor-publish-with-relations.git",
+ "tag": "v0.1.5",
+ "commit": "98213e9f056b139597c99fb9ec2884ae6be76d91"
+ },
"page-js-ie-support": {
"git": "https://github.com/tmeasday/meteor-page-js-ie-support.git",
"tag": "v1.3.5",