Building a Chat Client With Ionic / Socket.io / Redis / Node.js
about a 5 minute read
I wanted a fun challenge to push myself and cross a few things off my ever so growing I want to play with this type of lists. I love learning, and there are so many awesome tools / utilities / libraries out there to evaluate its hard to justify incorporating them into every project at work without having some knowledge of the tools.
DISCLAIMER: I may use some tools incorrectly, but the main purpose of this fun little project was to learn and have fun.
I wanted to build a chat client that would have messages that disappear after a certain time, much like SnapChat. The idea also included the ability to create channels that also disappear after a certain time like messages.
In future versions, I’d love to include location to join channels that are near you.
Users can join existing channels, or create their own. All users can see channels, and join any.
Tech details – using Redis / Node.js
At first, I wanted to create messages some how and have them each have expire times. After failing miserably, I got the amazing chance to pair up with Michael Gorsuch to give me some alternative ideas. (Shameless plug – if you need to do some server monitoring, check out his project Canary.io, it’s AWESOME).
The concept is – instead of using separate keys with ezxpire times – use Redis’ sorted sets with scores of the times in UNIX format and the member being a JSON encoded string. I had my channels keys in the format of messages:ChannelName.
Something like:
12
//ZADD key score member [score member ...]
zadd messages:RedisChat 10581098019 '{"name": "Josh", "id": "5"}'
Now, when we want to get all messages for a channel, its simply:
12
//ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
zrangebyscore messages:RedisChat 0 10924019840
Since I was using Node.js – I simply used setInterval to have a function be run that removes all old posts named removeKeys, and looked as such:
123456789101112131415161718192021222324252627
//NOTE: Using Moment.js, as well as having channelWatchList being populatedvarchannelWatchList=['Lobby','RedisChat'];functionremoveKeys(){console.log('We are removing old messages');for(varchannelIndexinchannelWatchList){varchannel=channelWatchList[channelIndex];varmessageChannel='messages:'+channel;console.log('message channel',messageChannel)vartimeToRemove=moment().subtract('m',1).unix();//Remove messages before min agoredisClient.zrangebyscore(messageChannel,0,timeToRemove,function(err,result){if(result&&result.length>0){for(varresultIndexinresult){varmessage=JSON.parse(result[resultIndex]);//NOTE: Using socket.ioio.emit('message:remove:channel:'+channel,{message:message,channel:channel});}}});redisClient.zremrangebyscore(messageChannel,0,timeToRemove,function(err,result){console.log('Removed ',result,' messages');});}}
The client – Ionic
This was by far the easy part. First I just used the Ionic CLI to create a basic app.
I started by modifying the index.html file to include Socket.io. Nothing too fancy: <script src="js/socket.io.js"></script>.
Next, I used some AngularJS services for socket.io:
angular.module('starter.controllers',['services']).controller('AppCtrl',function($scope,$state,$filter,socket,Auth){//Ensure they are authed first.if(Auth.currentUser()==null){$state.go('login');return;}//input models$scope.draft={message:''};$scope.channel={name:''};//App info$scope.channels=[];$scope.listeningChannels=[];$scope.activeChannel=null;$scope.userName=Auth.currentUser().name;$scope.messages=[];////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////Socket.io listeners//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////socket.on('channels',functionchannels(channels){console.log('channels',channels);console.log(channels);$scope.channels=channels;});socket.on('message:received',functionmessageReceived(message){$scope.messages.push(message);});socket.emit('user:joined',{name:Auth.currentUser().name});socket.on('user:joined',function(user){console.log('user:joined');$scope.messages.push(user);});$scope.listenChannel=functionlistenChannel(channel){socket.on('messages:channel:'+channel,functionmessages(messages){console.log('got messages: ',messages);console.log(messages.length);for(vari=0,j=messages.length;i<j;i++){varmessage=messages[i];console.log('message');console.log(message);console.log('apply with function');$scope.messages.push(message);}});socket.on('message:channel:'+channel,functionmessage(message){console.log('got message: '+message);if(channel!=$scope.activeChannel){return;}$scope.messages.push(message);});socket.on('message:remove:channel:'+channel,function(removalInfo){console.log('removalInfo to remove: ',removalInfo);varexpires=removalInfo.message.expires;varexpireMessageIndex=$filter('messageByExpires')($scope.messages,expires);if(expireMessageIndex){$scope.messages.splice(expireMessageIndex,1);}});$scope.listeningChannels.push(channel);}//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// Controller methods//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////$scope.joinChannel=functionjoinChannel(channel){$scope.activeChannel=channel;$scope.messages=[];$scope.channel.name='';//Listen to channel if we dont have it already.if($scope.listeningChannels.indexOf(channel)==-1){$scope.listenChannel(channel);}socket.emit('channel:join',{channel:channel,name:Auth.currentUser().name});}$scope.sendMessage=functionsendMessage(draft){if(!draft.message||draft.message==null||typeofdraft=='undefined'||draft.length==0){return;}socket.emit('message:send',{message:draft.message,name:Auth.currentUser().name,channel:$scope.activeChannel});$scope.draft.message='';};$scope.logout=functionlogout(){Auth.logout();$state.go('login');}//Auto join the lobby$scope.joinChannel('Lobby');})