Manual Moderation – multiple users

This is the final post on the manual moderation feature, where we will cover how to deal with the case of multiple users logged in moderating or viewing the same wall. The problem was that whenever two pages of the wall were open at the same time, there was no way to tell if another page was polling for tweets, so there would be duplicate tweets added to the database, which would then show up on both walls. This had to be solved as it is expected to have multiple displays or moderators for each wall.

To solve this, we needed to be able to store the user-wall id that was currently polling from the server. Not only did an open page have to check if there was another page polling, but also when the page was closed, the store would have to remove the user-wall id, so that when another open page checked it could start polling and store it’s user-wall id.

This connecting and disconnecting behavior can be detected through websocket events, and socket.io has a convenient way for us to listen to these events on the server:

[code language=”javascript”]
var server = express();
var s = http.createServer(server);
var io = require(‘socket.io’)(s);
var pollingWalls = {};
var clientIds = {};

io.on(‘connection’, function (socket) {

// Create and join UserwallId room when user opens page
socket.on(‘create’, function(userWallId) {
socket.join(userWallId);
});

// When close window, use the socket.id to remove from maps so another poll can pass
socket.on(‘disconnect’, function() {
var clientWallPolls = clientIds[socket.id];
clientWallPolls.forEach(function(wallId){
pollingWalls[wallId] = null;
})
delete clientIds[socket.id];
})

// Check duplicate, start if no one else polling
socket.on(‘checkDup’, function(data){
var clients_in_the_room = io.sockets.adapter.rooms[data.userWallId];
var isNoOneElsePolling = pollingWalls[data.userWallId] === socket.id || !pollingWalls[data.userWallId];
if(clients_in_the_room){
var result = clients_in_the_room.length === 1 || isNoOneElsePolling;
var responseEmit = ‘checkDupSuccess’+ data.userWallId+ data.socketId;
socket.emit(responseEmit, result);
}
})

// Start polling and mark poller
// Pre-cond: no one else polling / previous poller leaves
socket.on(‘addPollingWalls’, function(userWallId){
pollingWalls[userWallId] = socket.id;
var clientWalls = clientIds[socket.id];
if(clientWalls.indexOf(userWallId) === -1){
clientWalls.push(userWallId);
}
})
[/code]

I have pasted the server events(above) and client events(below) so that it is easier to view and explain:

[code language=”javascript”]
var init = function() {
// … other config
socket.emit(‘create’, $stateParams.user + $stateParams.id);
}

// Timeout that checks for multiple users on the same wall
vm.update2 = function(refreshTime) {
return $timeout(function() {
socket.emit(‘checkDup’, {userWallId:userWallId, socketId:socketId});
}, refreshTime);
};

// Event listener that polls if there are no duplicates
socket.on(‘checkDupSuccess’+userWallId+socketId, function(result){
if(result){
SearchService.initData(searchParams).then(successCb, errorCb);
socket.emit(‘addPollingWalls’, userWallId);
}
})
[/code]

In server.js, we use two objects – ‘pollingWalls‘ to map the walls that are currently polling to the userId, and ‘clientIds‘ to map each user to the walls that has the user has opened, so we do not have to traverse the whole object.

We also use socket.io rooms to check if there is no one else polling on the same wall. socket.join allows the connected user to join an existing room or create one if it does not exist. Each room is marked by their user-wall id. In the client code, (in wallDisplay.js the controller for the wall display page), the ‘create‘ event is emitted when a user opens a wall.

The other 2 event listeners on the server are to check if there are duplicate users.

Before each polling interval, in the timeout function below, the ‘checkDup‘ event is emitted, carrying with it the userwallId and the socketId. The event listener on server (as seen above) checks if there are no other users in the room with ‘io.sockets.adapter.rooms[data.userWallId]’ or nobody polling ‘var isNoOneElsePolling = pollingWalls[data.userWallId] === socket.id || !pollingWalls[data.userWallId];’. It then emits an event ‘checkDupSuccess’ carrying the boolean result to the specific ‘socket.id’ of the client that requested the check.

If the check passes then the angular ‘SearchService’ is called, and another event ‘addPollingWalls‘ is emitted to store the ‘userWallId’ and associate it with the client’s socket.id in the ‘pollingWalls‘ and ‘clientIds‘ object.

Create your own walls at: loklak-wall.herokuapp.com

 

Manual Moderation – multiple users

Loklak Walls Manual Moderation – approving tweets

This is a continuation from the previous post – Loklak walls manual moderation – tweet storage. In this section, I will show the changes I made to enable the user to approve or reject a tweet, and make that change happen across all walls that are opened.

First, I had to examine how loklak.net displays it’s tweets previously. On each wall page, a timeout would be called every interval to retrieve new tweets from loklak_server, these tweets would then be stored on the client’s browser window, in the view model, or $scope in angular.

[code language=”javascript”]

vm.update2 = function(refreshTime) {
return $timeout(function() {
SearchService.initData(searchParams).then(function(data) {

vm.statuses = data.statuses.splice(0, searchParams.count);

}, refreshTime);
};
[/code]

 

Having this kind of storage for each browser makes the data inconsistent across the same wall open in different browser windows, as they start querying loklak_server at different times and intervals. This also makes it tough to implement manual moderation previously as the tweets on each open page could not be controlled from the dashboard, since they all lived in their own page.

Now that we have shifted the storage of tweets to mongoDB, we are now able to control how tweets are displayed for all open walls! First, I shifted the calls to loklak_server to the dashboard page instead of the walls page as it made more sense to control the interval from the dashboard rather than the display pages themselves.

Next, I needed a way to sync changes in the database across the dashboard as well as the display pages. At first, I tried to use the same method of http calls, but I soon found them too complicated to sync, having 3 components with interconnected actions. Actions from the dashboard and new entries from the database would have to affect the display, and new entries from the database would have to affect the dashboard and display. Also having an interval for updating the wall after changes were made to the database made it seem very unresponsive and resulted in a bad user experience.

The solution to this was: WebSockets! This allows us to listen for new events like addition of new tweets. When first initialized, the display pages and the dashboard just had to load the existing tweets in the database, when new tweets are loaded, they’ll be added into the database AND the displays and dashboard, making it update in real time.

websocket-small

I chose socket.io as it made integrating WebSockets into the MEAN stack relatively easy. After the http request for new tweets from loklak_server is returned, the app then sends a POST request to the node server, which then emits an event to update the display and the dashboard. Below is the route controller, which posts the tweet array received from loklak_server.

[code language=”javascript”]
module.exports.storeTweet = function (req, res) {
req.body.tweetArr.forEach(function(tweet){
var newTweet = new Tweet(tweet);
newTweet.save(function(err,tweet){
// EMIT DASHBOARD EVENT
io.emit("addNewTweet", tweet);
// EMIT WALL DISPLAY EVENT
io.emit("addNewTweet"+req.body.userWallId, tweet);
}
})
});

[/code]

On the wall display page controller, it listens for the emitted event and adds the data to the display.

[code language=”javascript”]
socket.on(‘addNewTweets’ + $stateParams.user + $stateParams.id, function(tweet){
vm.statuses.splice(0,0, tweet);
})
[/code]

The toggle events are similar in that instead of POST requests,  now we are sending PUT requests from the dashboard to update the tweet in mongoDB, and then changing the data attribute on the wall display. Using AngularJS’s ng-hide we can show/hide the tweet depending on it’s approval field.

Inside the angular directive on the dashboard we attach a toggle function to the click:

[code language=”javascript”]
$scope.toggle = function(){
$scope.data.approval = !$scope.data.approval;
$http.put(‘/api/tweets/’+$scope.data._id, $scope.data);
}
[/code]

[code language=”html”]
<div ng-show="data.approval" ng-attr-id="{{data.id_str}}" class="linear linear-simple" /&amp;gt;
[/code]

Similarly on the server we emit an event:

[code language=”javascript”]

module.exports.updateTweet = function (req, res) {
Tweet
.findById(req.params.tweetId)
.exec(function(err, tweet) {
tweet.approval = !tweet.approval;
tweet.save(function(err) {
res.json({tweet: tweet});
});
});

// EMIT TOGGLE EVENT
io.emit("toggle", req.params.tweetId);
}
}
[/code]

And on the wallDisplay controller we can listen to that toggle event:

[code language=”javascript”]

socket.on(‘toggle’,function(tweetId){
var tweetIdx = vm.statuses.findIndex(function(tweet){
return tweet._id === tweetId;
});
vm.statuses[tweetIdx].approval = !vm.statuses[tweetIdx].approval;
});

[/code]

The end result is manual moderation from the dashboard!

icLfu4KZE9

 

 

Loklak Walls Manual Moderation – approving tweets