Facebook like load new posts in meteor - meteor

I'm in the process of learning meteor. I followed the tutorial to create microscope. If some one submits a post meteor will re render the template for all users. This could be very annoying if there are hundreds of posts then the user will come back to the top of the page and loose track of where he was. I want to implement something similar to what facebook has. When a new post is submitted template isn't rendered rather, a button or link will appear. Clicking it will cause the template to re-render and show the new posts.
I was thinking of using observeChanges on the collection to detect any changes and it does stop the page from showing new posts but only way to show them is to reload the page.
Meteor.publish('posts', function(options) {
var self = this, postHandle = null;
var initializing = true;
postHandle = Posts.find({}, options).observeChanges({
added: function(id, post) {
if (initializing){
self.added('posts', id, post);
}
},
changed: function(id, fields) {
self.changed('posts', id, fields);
}
});
self.ready();
initializing = false;
self.onStop(function() { postHandle.stop(); });
});
Is this the right path to take? If yes, how do I alert the user of new posts? Else, what would be a better way to implement this?
Thank you

This is a tricky question but also valuable as it pertains to a design pattern that is applicable in many instances. One of the key aspects is wanting to know that there is new data but not wanting to show it (yet) to the user. We can also assume that when the user does want to see the data, they probably don't want to wait for it to be loaded into the client (just like Facebook). This means that the client still needs to cache the data as it arrives, just not display it immediately.
Therefore, you probably don't want to restrict the data displayed in the publication - because this won't send the data to the client. Rather, you want to send all the (relevant) data to the client and cache it there until it is ready.
The easiest way involves having a timestamp in your data to work from. You can then couple this with a Reactive Variable to only add new documents to your displayed set when that Reactive Variable changes. Something like this (code will probably be in different files):
// Within the template where you want to show your data
Template.myTemplate.onCreated(function() {
var self = this;
var options = null; // Define non-time options
// Subscribe to the data so everything is loaded into the client
// Include relevant options to limit data but exclude timestamps
self.subscribe("posts", options);
// Create and initialise a reactive variable with the current date
self.loadedTime = new ReactiveVar(new Date());
// Create a reactive variable to see when new data is available
// Create an autorun for whenever the subscription changes ready() state
// Ignore the first run as ready() should be false
// Subsequent false values indicate new data is arriving
self.newData = new ReactiveVar(false);
self.autorun(function(computation) {
if(!computation.firstRun) {
if(!self.subscriptionsReady()) {
self.newData.set(true);
}
}
});
});
// Fetch the relevant data from that subscribed (cached) within the client
// Assume this will be within the template helper
// Use the value (get()) of the Reactive Variable
Template.myTemplate.helpers({
displayedPosts = function() {
return Posts.find({timestamp: {$lt: Template.instance().loadedTime.get()}});
},
// Second helper to determine whether or not new data is available
// Can be used in the template to notify the user
newData = function() {
return Template.instance().newData.get();
});
// Update the Reactive Variable to the current time
// Assume this takes place within the template helper
// Assume you have button (or similar) with a "reload" class
Template.myTemplate.events({
'click .reLoad' = function(event, template) {
template.loadedTime.set(new Date());
}
});
I think this is the simplest pattern to cover all of the points you raise. It gets more complicated if you don't have a timestamp, you have multiple subscriptions (then need to use the subscription handles) etc. Hope this helps!

As Duncan said in his answer, ReactiveVar is the way to go. I've actually implemented a simple facebook feed page with meteor where I display the public posts from a certain page. I use infinite scroll to keep adding posts to the bottom of the page and store them in a ReactiveVar. Check the sources on github here and the live demo here. Hope it helps!

Related

Re-create template while switching routes

How can we re-create template while switching routes?
For example, i have subscriber template. It detects when user scrolls down to a display and subscribes to more data. It takes several parameters.
Example:
amazing_page.html
{{#each}}
{{amazing_topic}}
{{/each}}
{{>subscriber name='topics' count=5}}
subscriber.js
//rough sample code
Template.subscriber.onCreated(function() {
var self = this;
var type = Template.currentData().name;
var count = Template.currentData().count;
var user = Template.currentData().user;
var skipCount = 0;
self.autorun(function(c){
self.subscribe(type, skipCount, user);
var block = true;
$(window).scroll(function() {
if (($(window).scrollTop() + $(window).height()) >= ($(document).height()) && block) {
block = false;
skipCount = skipCount + count;
console.log(type);
console.log(skipCount);
self.subscribe(type, skipCount, user, {
onReady: function() {
block = true;
},
onStop: function() {
console.log('stopped');
}
});
}
});
})
});
I use this template with different parameters in different routes.
The problem is if user switches some routes, and scrolls down in one page, all subscribers he gets in another pages will actualy work in this page. More, they will store increased values for them variables, and will do all included logic.
I found a bad decision when we use Route.getName (for example) comparing and name parameter of subscriber. It is not a best option. Can someone help me to find a good practice for that?:)
Simple Example:
We have 3 different routes:
1)News
2)Videos
3)Topics
These routes templates have included special subscriber-templates. And subscribtion works fine on scroll.
Ok, now let's visit all of them: News, Videos, Topics.
Good, now scroll down and... I have three instance of subscriber template what will subscribe on them own publications, because they not destroyed when we switch routes.
And, as a result - when user scrolling Topics page, he will call subscribtion for News and Videos too, and he will take data from these collections too;)
And - this is a problem:)
UPD:
Looks like we find a decision. If i use Template.instance (autorun/subscribe) it will start working expected, except some strange cases:)
First of all, when i go in another route in next iteration (scroll down) it returns me data from old, destroyed template + error. Next time (next iteration) it will start to subscribe to a correct data. Hmm...it looks like i have mistake in autorun section...or not?
Attached print screen from console
this
It sounds like you have multiple subscriptions to the same collection and that therefore the list of documents shown in various contexts can change in unexpected ways. Meteor manages multiple subscriptions on the same collection by synchronizing the union of the selected documents.
The simplest way to manage each of your views is to make sure that the data context for a particular view uses a .find() with the query you need. This will typically be the same query that your publication is using.
A different but less efficient approach is to .stop() the subscription when you leave a view.

Use Tracker.autorun to monitor limit changes but not new posts

I want to use
// client
var limit = Session.get('postsLimit');
Tracker.autorun(function (c) {
Meteor.subcribe('posts', limit);
c.stop();
});
// server
Meteor.publish('posts', function (limit) {
return Posts.find({}, {limit: limit});
});
because I don't want the insertion of new posts to affect a user's experience while viewing the posts that were loaded when a user first visits the page. But at the same time, I want the tracker to still recognize limit changes so that it can load posts as the user scrolls. So in other words, I want the tracker to stop subscribing to brand new post after the initial load but still load posts that have already been created.
How can I do this with Tracker or perhaps Tracker isn't the best option?
You could create a client only collection, insert the posts into that and make your helpers return the local collection.
Use observeChanges to watch the for newly added posts, when there are new posts, show the user an appropriate message. When the user wants to view the new posts, add them to the client only collection.
How about this:
//client.js
var now = new Date(); //so the now never change
Tracker.autorun(function (c) {
Meteor.subscribe('posts', now, Session.get('postsLimit'));
});
//server.js
Meteor.publish('posts',function(time,limit){
return Posts.find({savedAt: {$lt: time}},{limit:limit});
});
So when each post is saved into the database, a savedAt field is also appended and the query only searches for those saved before the current time.
Instead of using var now = new Date(); you can also use Session.get('now') then you can change it later by Session.set('now',new Date) when a user decides to fetch everything new.
However have to be aware of the time differences between client and server, may need to synchronise them?

Refreshing page with meteor iron router

Here is the problem :
I am currently programming a chatapp based on what i found on github (https://github.com/sasikanth513/chatDemo)
I am refactoring it with iron-router.
When I go to the page (clicking on the link) I get an existing chatroom (that's what I want)
When I refresh the page (F5) I get a new created chatroom ! (what i want is getting the existing chatroom ...)
Here is the code in ironrouter :
Router.route('/chatroom', {
name: 'chatroom',
data: function() {
var currentId = Session.get('currentId'); //id of the other person
var res=ChatRooms.findOne({chatIds:{$all:[currentId,Meteor.userId()]}});
console.log(res);
if(res){
Session.set("roomid",res._id);
}
else{
var newRoom= ChatRooms.insert({chatIds:[currentId, Meteor.userId()],messages:[]});
Session.set('roomid',newRoom);
}
}
});
You can find my github repo with the whole project : https://github.com/balibou/textr
Thanx a lot !
Your route data depends on Session variables which will be erased after a refresh. You have a few options but the easiest would be to put the room id directly into the route: '/chatroom/:_id'. Then you can use this.params._id to fetch the appropriate ChatRooms document. Note that you could still keep '/chatroom' for cases where the room doesn't exist, however you'd need to redirect to '/chatroom/:_id' after the insert.
In meteor, the Session object is empty when the client starts, and loading/refreshing the page via HTTP "restarts" the client. To deal with this issue, you could persist the user's correspondent id in a Meteor.user attribute, so that you could easily do:
Router.route('/chatroom', {
name: 'chatroom',
data: function() {
var currentId = Meteor.user().profile.correspondentId;
var res=ChatRooms.findOne({chatIds:{$all:[currentId,Meteor.userId()]}});
console.log(res);
if(res){
Session.set("roomid",res._id);
}
else{
var newRoom= ChatRooms.insert({chatIds:[currentId, Meteor.userId()],messages:[]});
Session.set('roomid',newRoom);
}
}
});
This would work, with the proper permissions, but I would recommend not allowing the direct update of that value on the client (I don't know if you want users to be able to override their correspondentId). So if you want to secure this process, replace all that code with a server method call, where your updates are safer.
Another (and more common case) solution was given by David Weldon, if you don't mind having ids in your URL (and therefore not a single url)

Can't put data from a Meteor collection into an array

I'm learning Meteor and I was trying to pass the result of a Collection.find() into and array (using a variable) and the simpler code I have is (in a file that is in the root):
CalEvents = new Mongo.Collection('calevents'); //creating a collection
/*------------------------- Populating the database with dummy data-------*/
if (Meteor.isServer) {
Meteor.startup(function () {
if (CalEvents.find().count() === 0) {
CalEvents.insert({
title: "Initial room",
start: '2010-02-02'
});
}
});
}
/*--------------- Creating an array from the collection-----------------*/
events = [];
calEvents = CalEvents.find({});
calEvents.forEach(function(evt){
events.push({
title: evt.title,
start: evt.start,
})
});
The page has nothing to show but using the console I can see (CalEvents.find().fetch()) that I have data in my database but the "events" variable is empty...
I can't understand why because I tried several other things such as changing file names and moving code to guarantee the proper order.
And I already tried to use CalEvents.find().fetch() to create an array an put the result into a variable but I'm not able to do it...
Does anyone know what's so simple that I'm missing?...
Do you use autosubscribe?
You probably need to make sure the sbscription is ready. See Meteor: How can I tell when the database is ready? and Displaying loader while meteor collection loads.
The reason you do see CalEvents.find().fetch() returning items in the console is that by the time you make that call, the subscription is ready. But in your events = []; ... code (which I assume is in a file under the client directory, you might have assumed that the subscription data has arrived when in fact it has not.
A useful debugging tool is Chrome's device mode ("phone" icon near the search icon in DevTools), which lets you simulate slow networks (e.g. GPRS, with 500ms delay for every request).

How to retrieve only new data?

I'm trying to create simple notification system for my site admin, and I need to send only real-time messages to every admin user. But when I use firebase it loads old data on every page, and user see all messages from database. If I set limit(1) user will see last notification on every page reloading:
var eventsList = new Firebase('https://*****-messages.firebaseio.com/');
eventsList.on('child_added', function(message) {
var message = message.val();
$.notification(message.message);
});
How I can load only new messages, without old notification history?
This is by design, in a real-time system there is no concept of the "latest" data because it's always changing. However, if you want to only display items added to the list after the page has loaded, you can do the following:
var newItems = false;
var eventsList = new Firebase('https://*****-messages.firebaseio.com/');
eventsList.on('child_added', function(message) {
if (!newItems) return;
var message = message.val();
$.notification(message.message);
});
eventsList.once('value', function(messages) {
newItems = true;
});
I would comment on the above, but due to reputation I cannot, so hoping this is adequate and this is to address the last comment by Gruff McGruff.
Once fires after because you want to break out of the loop of grabbing all child items. Once that loops is broken, you'll set the newItems variable to true, and then it will be able to get all new children after that.
If you fired it before, it would defeat the purpose and grab all child items regardless because you'll set the newItems variable immediately.
Also, I've used this approach and it works well.

Resources