I am currently working on a chat app in meteor. When first entering the room you get 25 messages initially. Now as new messages come into the page, that value should go up accordingly.
Now I so far I have a tried a couple different things, all not giving the desired result.
I have tried setting up a session variable on the client side that reactively re-subscribes to a given publish as the message count goes up. The problem with this route is this gives an adverse effect as new messages come in where all of the messages on the page need to reload because of the subscribe.
I have recently tried using the reactive-publish package with little luck in that the package has some various adverse effects of it's own.
What might be the best way to tackle this kind of problem? I am hoping there is a solution where I can set up some kind of publish that just streams in messages based on a numerical value that I in the database for each user.
EDIT: To add context
I am thinking something along the lines of
Meteor.publish 'messages', (roomId) ->
dl = // Some value that I pull from the database, which gets updated as new messages come into a room
Messages.find({room: roomId, type: "user_message"}, {sort: {time: -1}, limit: dl, fields: {_id: 1, name: 1, message: 1, room: 1, time: 1, type: 1}})
A huge amount of flexibility in Pub/Sub flexibility is achievable by using the low-level publications API - so much so that I've just written a blog post about it. It should make it pretty clear how you update variables when new documents appear in a query set.
It seems like you want each user to have a unique subscription based on the time that they enter the chat room, i.e. Meteor.subscribe("messages", roomId, new Date), that includes up to 25 messages from before they entered the room. Here's one option:
Meteor.publish("messages", function (roomId, time) {
var lastRecent = _.last(Messages.find({
room: roomId, type: "user_message"
}, {sort: {time: -1}, limit: 25}).fetch());
var cutoffTime = lastRecent && lastRecent.time || time;
return Messages.find({
room: roomId, type: "user_message", time: {$gt: cutoffTime}
});
});
If you want to successively add e.g. 25 old messages at a time whenever a user scrolls to the top of the chat window, consider that you may not actually need to "subscribe" to those older messages. You may be able to set up a method call like Meteor.call("getNOlderMessages", {roomId: roomId, N: 25, priorTo: oldestShownTime}) to fetch them, insert them into a local collection on the client (e.g. OlderMessages = new Meteor.Collection(null);), and then do something like:
<template="messages">
{{#each olderMessages}} {{> message}} {{/each}}
{{#each messages}} {{> message}} {{/each}}
</template>
Related
I have a method that checks for all unread messages belonging to a user. When the app loads, this number appears next to the "Messages" drop down. In Meteor, how would I update this count or variable for when a new message comes in or when the user reads an unread message? Pretty much I needs the method to send down the new count anytime a message status changes without refreshing the app itself.
I'm familiar with the Tracker.autorun functionality but I don't think it'll help with this situation. What's the best practice for approaching this?
Use Publish/Subscribe. It is always reactive. If you do not want to have all unread messages sent to the client straight away and counted there, you create a custom collection that justs count the number of unread messages and publishes that count. Look at the example a bit down in the linked page that starts with
// server: publish the current size of a collection
This is exactly your use case.
I have exactly this setup for new messages. In my header I have:
<li>Messages <span class="counter">{{Messages.count}}</span></li>
And then I have a helper that returns the cursor:
Template.header.helpers({
Messages: function(){ return Messages.find(); }
});
In the old days, before David Weldon set me straight I used to have a helper to return the count, now I just refer to the count directly in the blaze html template.
Now, in this approach I'm subscribing to the Messages collection so that new messages are transmitted to the client and can then be counted locally. This is on the assumption that they are going to be read soon. If you want to avoid this step then you should probably publish a Stats collection or include a stats key in the user object so that just the count itself can be synced via pub-sub.
You can just have a field like read, and update like:
Method for marking one message as read:
markRead: function(messageId){
Messages.update(messageId, {
$set: {
read: true //this needs to be set to false when its inserted
}
})
}
Bulk update method (assuming all messages have receiverId saved):
markAllRead: function(){
Messages.update({receiver: Meteor.userId(), read:false}, {
$set: {
read: true
}
}, {multi: true})
}
You can count read:false ones to retrieve count and you don't have to write anything else
Helper:
count: function(){
//even if your publish/subscribe is correct, the count we want is from messages that are not read and the receiver is current user.
return Messages.find({receiver: Meteor.userId(), read: false }).count();
}
Event:
'click .elementClass': function(){
//both users see the messages and they can both click. We want to update the right message for the right user. Otherwise, the other user can mark the message as read when the receiver is the other user which they shouldn't be able to do. You can do a simple check on the client side, and another check in the method if necessary.
if(this.receiver === Meteor.userId()){
Meteor.call('markAsRead', this._id)
}
}
Let me know if it solves your problem/answers all your questions.
I'm working on a simple app where a User can follow other users. Users can star posts. And a user's feed is composed of posts that have been starred by users they follow. Pretty simple actually. However, this all gets complicated in Mongo and Meteor...
There are basically two way of modeling this that I can think of:
A user has a property, following, which is an array of userIds that the user follows. Also, a post has a property, starrers, which is an array of userIds that have starred this post. The good thing about this method is that publications are relatively simple:
Meteor.publish 'feed', (limit) ->
Posts.find({starrers: {$in: Meteor.users.findOne(#userId).following}}, {sort: {date: -1}, limit:limit})
We aren't reactively listening to who the user is following, but thats not too bad for now. The main problem with this approach is that (1) the individual documents will become large and inefficient if 1000000 people star a post. Another problem is that (2) it would be pain to keep track of information like when a user started following another user or when a user starred a post.
The other way of doing this is having two more collections, Stars and Follows. If a user stars a post, then we create a document with properties userId and postId. If a user follows another user, then we create a document with properties userId and followId. This gives us the advantage of smaller document sizes for Users and Posts, but complicated things when it comes to querying, especially because Mongo doesn't handle joins!
Now, I did some research and people seem to agree that the second choice is the right way to go. Now the problem I'm having is efficiently querying and publishing. Based on the Discover Meteor chapter about Advanced Publications, I created a publication that publishes the posts that are starred by user's followers -- sorted, and limited.
# a helper to handle stopping observeChanges
observer = (sub, func) ->
handle = null
sub.onStop ->
handle?.stop?()
() ->
handle?.stop?()
handle = func()
Meteor.publish 'feed', (limit) ->
sub = this
userId = #userId
followIds = null
eventIds = null
publishFollows = observer sub, () ->
followIds = {}
Follows.find({userId:userId}).observeChanges
added: (id, doc) ->
followIds[id] = doc.followId
sub.added('follows', id, doc)
publishStars()
removed: (id) ->
delete followIds[id]
sub.removed('follows', id)
publishStars()
publishStars = observer sub, () ->
eventIds = {}
Stars.find({userId: {$in: _.keys(followIds)}).observeChanges
added: (id, doc) ->
eventIds[id] = null
sub.added('stars', id, doc)
publishEvents()
removed: (id) ->
delete eventIds[id]
sub.removed('stars', id)
publishEvents()
publishEvents = observer sub, () ->
Events.find({_id: {$in: _.keys(eventIds)}}, {sort: {name:1, date:-1}, limit:limit}).observeChanges
added: (id, doc) ->
sub.added('events', id, doc)
changed: (id, fields) ->
sub.changed('events', id, fields)
removed: (id) ->
sub.removed('events', id)
While this works, it seems very limited at scale. Particularly, we have to compile a list of every starred post by every follower. The size of this list will grow very quickly. Then we do a huge $in query against all posts.
Another annoyance is querying for the feed on the client after we subscribe:
Meteor.subscribe("feed", 20)
posts = null
Tracker.autorun ->
followers = _.pluck(Follows.find({userId: Meteor.userId()}).fetch(), "followId")
starredPostIds = _.pluck(Stars.find({userId: {$in: followers}}).fetch(), "postId")
posts = Posts.find({_id: {$in: starredPostIds}}, {sort: {date: -1}, limit: 20}).fetch()
Its like we're doing all this work twice. First we do all the work on the server to publish the feed. Then we need to go through the exact same logic again on the client to get those posts...
My question here is a matter of design over everything. How can I efficiently design this feed based on followers staring posts? What collection / collection schemas should I use? How should I create the appropriate publication? How can I query for the feed on the client?
So it turns out that Mongo and "non-relational" databases simply aren't designed for relational data. Thus, there is no solution here with Mongo. I've ended up using Neo4j, but SQL would work fine as well.
meteor add reywood:publish-composite
Meteor.publishComposite('tweets', function(username) {
return {
find: function() {
// Find the current user's following users
return Relationships.find({ follower: username });
},
children: [{
find: function(relationship) {
// Find tweets from followed users
return Tweets.find({user: relationship.following});
}
}]
}
});
Meteor.publish('ownTweets', function(username) {
return Tweets.find({user: username});
});
I have an app where you can choose (or add if they don't exist!) a superhero/villain character from a certain universe on the first page; then outfit him with weapons, clothes, and gadgets on the second page (build).
I have this route defined:
Router.route('/build/:character', {
name: 'build'
waitOn: Meteor.subscribe('characters', {name: this.params.character})
//and a few other subscriptions and sessions as well for the items
//and stuff, but those don't matter here.
}
The link from the specific character, though, passes along a query as well:
<a href="{{pathFor 'build' query=this.universe}}">
So the final link could look something like this:
/build/Aquaman?DCComics
Now the page you are on will display a list of weapons and gadgets where you could also add other stuff if you so wish. Then you are supposed to drag the items you want to include onto your version of this hero.
Problem is, at this point the app doesn't know you even want to create your own hero. Maybe the user is just looking through them for fun. There's a button that the user has to click first to initialize the creating process, and that's when the actual _id is created, something like this:
Meteor.methods({
buildHero: function(heroCharacterName, heroUniverse) {
var heroToAdd = {}
heroToAdd['characterName'] = heroCharacterName
heroToAdd['universe'] = heroUniverse
heroToAdd['_createdAt'] = new Date()
CreatedHeroes.insert(heroToAdd, function() {
if (! error)
//Update the subscription somehow...
})
}
})
So, the _id that is created here in the new Collection must be passed along to a subscription somehow, because I don't want the user to see other personal heroes that have been created, only his own newly created one.
The solution I have in mind is adding the _id onto the URL in form of a hastag, and use this.params.hash in the subscription like so:
Router.route('/build/:character', {
name: 'build'
waitOn: [Meteor.subscribe('characters', {name: this.params.character}),
Meteor.subscribe('createdheroes', this.params.hash)]
}
First of all, is this a valid approach? If so, how do I accomplish it; how do I actually update the URL to include this hash?
If not, what would be a better approach?
I think you have to handle this logic in the data context or in a template helper and not in the way of subscribing/publishing.
If I was you I would besure that the newly created item is being published and subscribed by the client and modify your search query just that it only adds the newly created item.
I am not sure if I understand your question well but what I got, you will know the last _id which was used on your insert.
Instead of letting done this automatically by meteor, just use the meteor method to create / get that _id value >> see Meteor Documentation
var new_id = new Mongo.ObjectID()
col1.insert({ _id: new_id, ... });
col2.insert({ ..., ref_col1_id: new_id, ... });
If I have a subscription that does
Meteor.publish("mypublish", function (status) {
var foundOnServer = MyCollection.find({"status": status}, {fields: {_id: 1, status: 1, "name": 1}});
return foundOnServer;
});
and I'm displaying those records in a table, then I want to let the user click on one of them to bring up the full detail of the record, and now I want to display some more fields that weren't previously published to the client, like maybe address, city, state, etc... how do I code things so that I can get the new fields picked up and put into the local minimongo collection, considering the record is already in the client. Doing a Find just returns the document already in the client, without the extra fields I want.
EDIT: I was not aware of the DDP limitation in that once you subscribe to a field in a nested structure, you are unable to then add more fields from that level of the nested structure. https://github.com/meteor/meteor/issues/998
Write another publish function for the appropriate subset and return more/all of the the fields. For example:
Meteor.publish('singlePost', function(postId) {
check(postId, String);
return Posts.find(postId);
});
When you click on a post, you can subscribe to singlePost which will send all of the fields for that post. The key insight is the documents will be merged on the client.
So if the client had {_id: 'x', message: 'hello'} already in minimongo, and you later publish {_id: 'x', color: 'blue'}, the client will then have {_id: 'x', message: 'hello', color: 'blue'} in its database.
I have a meteor collection like this:
Cases = new Meteor.Collection('cases');
As well i have registered users (max 10). I now want to be able to "give" a single case to a registered user and be sure, that no other user is getting that specific case.
The User is working with the case (updating fields, deleting fields) and then sends it in some kind of archive after submitting the user should get a new case that is in the collection.
My thought was to have field called "locked" which initially is set to false and in the moment it is displayed at the user "locked" gets true and is not returned anymore:
return Cases.find({locked: false, done: false}, {limit: 1});
Any ideas how to do that in meteor?
Thanks
You just need to attach an owner field (or similar) to the case. That would allow you to do things like:
Only publish the case to the user who is also the owner using something like:
Meteor.publish('cases/unassigned', function() {
return Cases.find({owner: {$exists: false}});
});
Meteor.publish('cases/mine', function() {
return Cases.find({owner: this.userId});
});
Not allow a user to update or delete a case if it's not assigned to them:
Cases.allow({
update: function(userId, fieldNames, doc, modifier) {
return userId === doc.owner;
},
delete: function(userId, doc) {
return userId === doc.owner;
}
});
Obviously, these would need amending for stuff like super-users and you probably need some methods defined to allow users to take cases, but that's the general idea.
There are concurrency issues to deal with, to reliably allocate a case to only one person.
We need to solve two things:
1. Reliably assign the case to a user
2. Fetch the cases assigned to a user
Number 2. is easy, but depends on 1.
To solve 1., this should work:
var updated = Cases.update(
{_id: <case-to-assign>, version: "ab92c91"},
{assignedTo: Meteor.userId(), version: Meteor.Collection.ObjectID()._str});
if (updated) {
// Successfully assigned
} else {
// Failed to assign, probably because the record was changed first
}
Using this you can query for all of a users cases:
var cases = Cases.find({assignedTo: Meteor.userId()});
If 10 people try get a case at the same time, it should have a pre-set version field, and the MongoDB will only let the .update work once. As soon as the version field changes (due to an .update succeeding) the remaining updates will fail as the version field could no longer match.
Now that the allocation has taken place reliably, fetching is very simple.
As suggested by #Kyll, the filtering of cases should be done inside a Meteor publication.
It would also make sense to perform the case-assignment inside a Meteor method.
UPDATE:
#richsilv's solution is simpler than this one, and works fine.
This solution is useful if you need to know who won immediately, without making further requests to the server.