UNPKG

mubot-server

Version:
513 lines (511 loc) 22.1 kB
'use strict'; // Three dependencies. angular.module('myApp', ['angularMoment', 'monospaced.elastic', 'ngDisableScroll']) .config(['$locationProvider', '$compileProvider', ($locationProvider, $compileProvider) => { // optimizations. $compileProvider.commentDirectivesEnabled(false); $compileProvider.cssClassDirectivesEnabled(false); $compileProvider.debugInfoEnabled(false); // ensure html5 is required $locationProvider.html5Mode({ enabled: true, requireBase: false }) }]) // Here we explicitly require injections (array format) such that during minitifcation the implicit calls are not needed. .controller('mainController', ['$scope', '$http', '$location', '$timeout', '$window', 'socket', ($scope, $http, $location, $timeout, $window, socket) => { $scope.pmRightOffSet = function(index) { return $window.innerWidth >= 460 ? ((index) * 310 + 160 + 'px') : '85px' } // Start variable declarations. $scope.usersOnline = {}; $scope.data = {}; $scope.replies = {}; $scope.posts = {}; // The users state which can be a post id, "home", "wallet", or "profile". $scope.state = (document.cookie.split('state=')[1] || 'home').split(';')[0]; // Hack to pad the replies container bellow the selected post. $scope.loaded = {}; // Post id's that are awaiting a marking reason. $scope.messageNeeded = {}; // Withdrawel information. {amount: Number, address: String} $scope.withdraw = {}; // Keep track of whoes typing. {[user.username]: Timer} const TYPING_TIMER = {}; // Keep track of the last time user sent a pm. {[user.username]: Date} const LAST_PM_TIME = {}; // trans = { txid: String, amount: Number, address: String, type: "withdraw/deposit" } socket.emit('transactions', {}, trans => $scope.trans = trans); // Load all posts and create shortcuts. $http.get('/bitmark/api/posts').then(res => { for(let i = 0, l = res.data.length; i < l; ++i) { let post = res.data[i]; $scope.posts[post._id] = post; $scope.replies[post.replyto] ? $scope.replies[post.replyto].push(post) : $scope.replies[post.replyto] = [post] } }); // Load user specific data. $http.get('/bitmark/api/info').then(res => { $scope.pms = res.data.pms || []; delete res.data.pms; // Makes things cleaner. delete res.data._id; delete res.data.__v; // I merge in awaitingPms: {} because mongoose returns nothing upon empty obj. Object.assign($scope.data, {awaitingPms: {}}, res.data); var PM_WINDOWS = $scope.pms.length; var PM_WIDTH = 300, SPACING = 10, USERLIST_WIDTH = 150; for(let i = 0, l = PM_WINDOWS; i < l; ++i) { if(PM_WINDOWS * (PM_WIDTH + SPACING) + USERLIST_WIDTH > $window.innerWidth) { if($scope.pms.length !== 1) // Try to leave just one pm available (or its useless, or needs to be moved to a seperate page) $scope.pms.shift() } } for(let i = 0, l = PM_WINDOWS; i < l; ++i) { let pm = $scope.pms[i]; if(pm.maximized && Object.keys($scope.data.awaitingPms).includes(pm.username)) { $timeout(() => { socket.emit('private message read', pm); delete $scope.data.awaitingPms[pm.username] }, 5000) } // Request pm data, I merge in exists: true here so that the backend knows this window // isn't being opened. Its just being loaded, minimized windows are requested too. socket.emit('private message open', Object.assign($scope.pms[i], {exists: true})) } if($scope.state.length === 24) { // If they are visiting a post then remove notifications on that post. for(let i = 0, l = $scope.data.notifications.length; i < l; ++i) { if($scope.state === $scope.data.notifications[i].id) { $http.get('/bitmark/api/delete_notification/' + $scope.state); $scope.data.notifications.splice(i, 1); break } } } // Define an amount propery on our notifications array, it loops through all the notifications adding up their amount feild. returning the total. Object.defineProperty($scope.data.notifications, 'amount', { get: function(){ var n=0; for(let i=0, l=this.length; i<l; ++i) n+=this[i].amount; return n } }); // Here we use _amount so as to not collide with a potential user named amount. (_ at the start of names is disallowed). Object.defineProperty($scope.data.awaitingPms, '_amount', { get: function(){ var n=0; for(let i=0, k=Object.keys(this), l=k.length; i<l; ++i) n+=this[k[i]]; return n } }); $scope.data.awaitingPms._amount && alert('You have ' + $scope.data.awaitingPms._amount + ' pm(s) awaiting you. \n\nThis message will be replaced by the UI Soon =).') }); // Request the users online. socket.emit('users online', {}, usersOnline => { delete usersOnline[$scope.data.username]; $scope.usersOnline = usersOnline; var amount = 0; for(let i = 0, keys = Object.keys(usersOnline), l = keys.length; i < l; i++) { if(usersOnline[keys[i]].status !== 'offline') amount++ } $scope.amountOnline = amount }); $scope.withdrawMarks = (amount, toAddress) => { if(!$scope.withdraw.amount || !$scope.withdraw.address) return; socket.emit('withdraw', {amount: amount, toAddress: toAddress}); $scope.withdraw.amount = ''; $scope.withdraw.address = '' }; // Create a heart beat monitor to see if users are still really online. var status, recentlyAway, away, gone, offline; $window.onbeforeunload = () => socket.emit('status update', 'offline'); $window.onmousemove = $window.onkeydown = $window.onfocus = _.throttle(()=> { if(status === 'online') return; socket.emit('status update', status = 'online'); $timeout.cancel(recentlyAway); $timeout.cancel(away); $timeout.cancel(gone); $timeout.cancel(offline); recentlyAway = $timeout(()=> socket.emit('status update', status = 'recently-away'), 330000); away = $timeout(()=> socket.emit('status update', status = 'away'), 700000); gone = $timeout(()=> socket.emit('status update', status = 'gone'), 1990000); offline = $timeout(()=> socket.emit('status update', status = 'offline'), 8000000) }, 500); $window.onblur = () => $timeout(() => { if(status === 'away') return; $timeout.cancel(recentlyAway); $timeout.cancel(away); socket.emit("status update", status = 'away') }, 700); socket.on('status update', user => { if(!$scope.usersOnline[user.username]) return; user.status === 'offline' && --$scope.amountOnline; user.status === 'online' && $scope.usersOnline[user.username].status === 'offline' && ++$scope.amountOnline; $scope.usersOnline[user.username].status = user.status }); // If the script has been disconnected for multiple attempts reload the script to ensure no data loss. socket.on('reconnect', attempts => attempts > 1 && ($window.location.href = '/bitmark')); // Process the new post. socket.on('new post', post => { $scope.posts[post._id] = post; $scope.replies[post.replyto] ? $scope.replies[post.replyto].unshift(post) : $scope.replies[post.replyto] = [post]; if(post.ismarking) { $scope.usersOnline[post.replyto_user] && ++$scope.usersOnline[post.replyto_user].balance; post.username !== $scope.data.username && --$scope.usersOnline[post.username].balance; $scope.posts[post.replyto] && ++$scope.posts[post.replyto].marks } // Post is replying to user. if(post.replyto_user === $scope.data.username) { // Post is also marking user if(post.ismarking) ++$scope.data.balance; // no message so assume its a marking a pm. if(post.ismarking && !post.message) { for(let i = 0, l = $scope.pms.length; i < l; ++i) { // Post is marking user and user has pm window open with // the user creating the marking. if($scope.pms[i].username === post.username) { let msgs = $scope.pms[i].history; for(let i = 0, l = msgs.length; i < l; ++i) { // Post is marking users pm. if(post.replyto === msgs[i]._id) { msgs[i].ismarked = true; ++$scope.data.balance; return } } } } } // Loop through user's notifications to check if post already has notifications. for(let i = 0, l = $scope.data.notifications.length; i < l; ++i) { // User already has notifications about this post, increment amount. if(post.replyto === $scope.data.notifications[i].id) return ++$scope.data.notifications[i].amount } // User doesnt have notifications about this post, add it. $scope.data.notifications.push({id: post.replyto, amount: 1, message: post.message}) } }); socket.on('deposit', data => { if(data.username === $scope.data.username) { data.date = new Date; data.type = 'deposit'; delete data.username; $scope.trans.unshift(data); return $scope.data.balance += data.amount } $scope.usersOnline[data.username].balace += data.amount }); socket.on('withdraw', data => { if(data.username === $scope.data.username) { data.date = new Date; data.type = 'withdraw'; delete data.username; $scope.trans.push(data); return $scope.data.balance -= data.amount } $scope.usersOnline[data.username].balace -= data.amount }); socket.on('private message data', populatePmWindow); socket.on('user logged in', user => $scope.usersOnline[user.username] = user && ++$scope.amountOnline); socket.on('user logged out', user => delete $scope.usersOnline[user] && --$scope.amountOnline); socket.on('error', _=>0); $scope.notificationClicked = notification => { $scope.select({'_id' : notification.id }); for(let i = 0, l = $scope.data.notifications.length; i < l; ++i) { if(notification.id === $scope.data.notifications[i].id) { $scope.data.notifications.splice(i, 1); break } } $http.get('/bitmark/api/delete_notification/' + notification.id) }; // If the marking reason got blurred, wait and see if it gets canceled. If not mark it. $scope.reasonBlur = post => { $timeout(() => { $scope.messageNeeded[post._id] && $scope.mark(false, post) }, 200) }; $scope.closePmWindow = pm => { for(let i = 0, l = $scope.pms.length; i < l; ++i) { if(pm === $scope.pms[i]) { $scope.pms.splice(i, 1); break } } socket.emit('private message close', pm) }; // Checks if the pms fit on the screen. $(window).on('resize', _.throttle(() => { //$scope.$apply(() => { const PM_WINDOWS = $scope.pms.length; const PM_WIDTH = 300, SPACING = 10, USERLIST_WIDTH = 150; if(PM_WINDOWS * (PM_WIDTH + SPACING) + USERLIST_WIDTH > $window.innerWidth) { // pms dont fit, remove one. //AxE if($scope.pms.length === 1) { return; // Try to leave just one pm available (or its useless, or needs to be moved to a seperate page) } $scope.pms.shift() } //}) }, 200)); // return true if we need to sip msg. $scope.skipSameMin = () => $scope.skipSameMins && $scope.skipSameMins--; // The getSameMin function groups all the msgs with the same min into 1 block then // it sets skipSameMins to the amount of msgs it grouped, so we can skip displaying them. $scope.getSameMin = (history, index) => { var results = []; var i = index; // While the next history element has the same minnute. So i represents, relative to index, the amount found. while(history[i+1] && history[i].sender === history[i+1].sender && $scope.sameMin(history[i].date, history[i+1].date)) { results.push(history[i++].message + '\n') } if(i > index) { // The above while loop doesnt add the last i. results.push(history[i].message); // Make sure to skip over all the entries with same min // Since we are grouping them up into one next command. $scope.skipSameMins = results.length - 1; return results.join('') } else { // The next message is a diff min. so return msg alone. return history[index].message } }; // Returns true if prev and cur message have same day/min respectivly. $scope.sameDay = (prev, cur) => new Date(prev).getDay() === new Date(cur).getDay(); $scope.sameMin = (prev, cur) => prev - cur < 60000; $scope.togglePmWindow = pm => { // If pm window is open and dontMinimize is true return. (user clicked settings) if(pm.maximized && $scope.data.dontMinimize) return $scope.data.dontMinimize = false; pm.maximized = !pm.maximized; pm.maximized && delete $scope.data.awaitingPms[pm.username]; socket.emit('private message toggle', pm) }; function populatePmWindow(pms) { if(!pms[0]) return; for(let i = 0, l = $scope.pms.length; i < l; ++i) { let pm = $scope.pms[i]; // Loop through current pms and update only the right pm window. if(pms[0].receiver === pm.username || pms[0].sender === pm.username) { pm.history = pms } } } // Throttle here to improve performance if typing is nonstop. $scope.startedTyping = _.throttle(pm => { // pm.username is the person receiving the pm. socket.emit('private_messages typing', {user: pm.username, started: true}) }, 500); socket.on('private_messages typing', data => { for(let i = 0, l = $scope.pms.length; i < l; ++i) { let pm = $scope.pms[i]; if(data.user === pm.username) { // They finished typing. if(data.finished) { pm.istyping = false; $timeout.cancel(TYPING_TIMER[pm.username]) } else { // They started typing. pm.istyping = true; $timeout.cancel(TYPING_TIMER[pm.username]); TYPING_TIMER[pm.username] = $timeout(() => pm.istyping = false, 3750) } return } } }); $scope.openPmWindow = user => { const PM_WIDTH = 300, SPACING = 10, USERLIST_WIDTH = 150; const PM_WINDOWS = $scope.pms.length; for(let i = 0; i < PM_WINDOWS; ++i) { let pm = $scope.pms[i]; if(user.username === pm.username) { // The window exists, and is mazimized, so remove its notifications before we toggle it. pm.maximized && delete $scope.data.awaitingPms[pm.username]; $scope.togglePmWindow(pm); return } } // Check if the new pm window we are adding will fit on the screen. if((PM_WINDOWS+1) * (PM_WIDTH + SPACING) + USERLIST_WIDTH > $window.innerWidth) { $scope.pms.shift() } let pm = { text: '', history: [], maximized: true, istyping: false, username: user.username }; $scope.pms.push(pm); socket.emit('private message open', pm) }; $scope.markPm = pm => { socket.emit('mark private message', pm, marked => marked && (pm.ismarked = true) && --$scope.data.balance) }; $scope.mark = ($event, post) => { $event && $event.stopPropagation(); // Dont allow self marks, and make sure they have available funds. if($scope.data.username === post.username || !$scope.data.balance) return; // Toggle reason textarea. $scope.messageNeeded[post._id] = !$scope.messageNeeded[post._id]; // Return requesting a reason if its toggled on. if($scope.messageNeeded[post._id]) return; // Reason was provided, request the marking. post.marking_msg = $scope.data.marking_msg; $scope.data.marking_msg = ''; $http.post('/bitmark/api/mark/', post).then(()=> --$scope.data.balance) }; $scope.select = post => { if($scope.replyto === post._id) return; for(let i = 0, l = $scope.data.notifications.length; i < l; ++i) { if(post._id === $scope.data.notifications[i].id) { $http.get('/bitmark/api/delete_notification/' + post._id); $scope.data.notifications.splice(i, 1); break } } $scope.replyto = $scope.state = post._id; document.cookie = 'state=' + post._id + '; expires=Thu, 01 Jan 2222 00:00:01 GMT; path=/;'; post.scrollto = document.documentElement.scrollTop || document.body.scrollTop; $scope.updateState('/bitmark/post/', post._id, post.scrollto) }; $scope.updateState = (path, state, scroll) => { scroll = scroll || 0; if(state === 'home' && $scope.replyto) state = $scope.replyto; $location.old = $location.state() || {}; $location.new = { replyto: $scope.replyto, state: state, scrollto: scroll, // This little algo keeps track of the users original scroll position // should the user ever hit the big red X indicating back to home. allscrollto: !$location.old.replyto ? scroll : $location.old.allscrollto }; if($location.new.state === $location.old.state) return; state === 'home' && (state = ''); $location.state($location.new).path(path + state) }; // Watch for location changes so we can apply state accordingly $scope.$on('$locationChangeSuccess', (_, __, ___, ____, oldstate) => { var newstate = $location.state(); // The first request wont have newstate. if(!newstate) return $scope.replyto = $scope.state.length === 24 ? $scope.state : ''; $scope.replyto = newstate.replyto; $scope.state = newstate.state; document.cookie = 'state=' + newstate.state + '; expires=Thu, 01 Jan 2222 00:00:01 GMT; path=/;'; if(!oldstate) oldstate = {}; $timeout(() => $window.scrollTo(0, !newstate.replyto ? newstate.allscrollto : oldstate.scrollto || 0), 0) }); $scope.postGlow = post => { return { 'box-shadow': '0 0 ' + post.marks / 2 + 'px purple' } }; $scope.mergeStyles = styles => { var obj = {}; for(let i = 0, l = styles.length; i < l; ++i) { Object.assign(obj, styles[i]) } return obj }; $scope.back = ($event, post) => { $event.stopPropagation(); document.cookie = 'state=' + post.replyto + '; expires=Thu, 01 Jan 2222 00:00:01 GMT; path=/;'; if (post.replyto) { $scope.state = post.replyto; $scope.replyto = post.replyto; $scope.updateState('/bitmark/post/', post.replyto) } else { $scope.replyto = ''; $scope.updateState('/bitmark/', 'home') } }; socket.on('new private message', pm => { var found = false; for(let i = 0, l = $scope.pms.length; i < l; ++i) { if(pm.sender === $scope.pms[i].username || pm.receiver === $scope.pms[i].username) { $scope.pms[i].history.push(pm); $scope.pms[i].maximized && (found = true) } } if(!found) { let awaiting = $scope.data.awaitingPms; let name = pm.sender; if(!LAST_PM_TIME[name]) { LAST_PM_TIME[name] = new Date; awaiting[name] = awaiting[name] || 0; ++awaiting[name]; return } if(Date.now() - LAST_PM_TIME[name] > 60000) ++awaiting[name]; LAST_PM_TIME[name] = new Date } }); $scope.createPm = ($event, pm) => { $event.preventDefault(); // Used to indicate the pm is new. var sameMin = false; var last; if(last = pm.history[pm.history.length - 1]) { sameMin = Date.now() - last.date > 60000 } socket.emit('new private message', {message: pm.text, username: pm.username, sameMin: sameMin}); pm.text = '' }; var tries = 0; $scope.create = $event => { // In case of error try again. Every once and a while theres // an error here, idk why, but this solves the problem. if(!$scope.data.message.trim()) return; $event.preventDefault(); $scope.data.message = ''; $event.target.blur(); $event.target.rows = 1; $http.post('/bitmark/api/create', { 'message': $scope.data.message, 'replyto': $scope.replyto }).then(res => { // Post created successfully. }).catch(err => { // Error creating, try again. ++tries < 3 && $scope.create($event) }) }; $scope.cancelMarking = ($event, post) => { $event.stopPropagation(); $scope.messageNeeded[post._id] = false; $scope.data.marking_msg = '' }; $scope.logout = () => { socket.emit('log out'); document.cookie = 'login-cookie' + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/;'; document.cookie = 'state' + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/;'; $window.location.href = '/bitmark' }; }]) .filter('reverse', () => { return items => { if(items) return items.slice().reverse() } }) .directive('autoFocus', () => { return { link: { post: (scope, element) => element[0].focus() } } }) .directive('autoscroll', () => { return { restrict: 'A', link: (scope, elem, attr) => { scope.$watch(() => { return elem[0].scrollHeight }, (newVal, oldVal) => { // This directive is just for pms and // 275px is our pm initial size, so skip it. if (!newVal || newVal === '275px') return; elem[0].scrollTop = elem[0].scrollHeight }) } } }) .directive('loaded', () => { var navBarLoaded = false; return { restrict: 'A', link: (scope, elem, attr) => { scope.$watch(() => { try { return document.getElementById(attr.loaded).scrollHeight } catch(e) { return } }, (newVal, oldVal) => { // newVal - oldVal == 42 is a hack to fix bug: (textarea is sending 2 scrollheight updates when its drawn) if(!newVal || newVal - oldVal == 42 || (attr.loaded === 'navbar' && navBarLoaded)) return; if(!navBarLoaded) navBarLoaded = true; elem[0].style.paddingTop = newVal + 10 + 'px' }) } } }) .factory('socket', ['$rootScope', function ($rootScope) { const socket = io('/bitmark'); return { on: function (eventName, callback) { socket.on(eventName, function() { const args = arguments; $rootScope.$apply(() => callback.apply(socket, args)) }) }, emit: function (eventName, data, callback) { socket.emit(eventName, data, function() { const args = arguments; callback && $rootScope.$apply(() => callback.apply(socket, args)) }) } } }]);