mirror of
https://github.com/ENSL/ensl_gathers.git
synced 2024-11-10 15:21:56 +00:00
Implemented basic chat
This commit is contained in:
parent
00339aefaf
commit
4a21328691
14 changed files with 722 additions and 84 deletions
|
@ -23,6 +23,7 @@ npm start
|
|||
- Admin tools
|
||||
- Add a backend datastore to persist gather data and messages
|
||||
- Add sounds
|
||||
- Add WebRTC for internal voice comms
|
||||
|
||||
## License
|
||||
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
"use strict";
|
||||
|
||||
var fs = require("fs");
|
||||
var path = require("path");
|
||||
var chatController = require(path.join(__dirname, "../lib/chat/controller"));
|
||||
|
||||
module.exports = function (io) {
|
||||
var root = io.of("/");
|
||||
var authorised = io.of("/authorised");
|
||||
|
@ -11,7 +15,8 @@ module.exports = function (io) {
|
|||
username: "Chris (" + socket.id.slice(0,5) + ")",
|
||||
steamId: "11111111",
|
||||
email: "cablanchard@gmail.com",
|
||||
bans: []
|
||||
bans: [],
|
||||
avatar: "http://www.ensl.org/local/avatars/6359.jpg"
|
||||
};
|
||||
next();
|
||||
});
|
||||
|
@ -27,6 +32,9 @@ module.exports = function (io) {
|
|||
});
|
||||
};
|
||||
|
||||
// Activate chat controller on root namespace
|
||||
chatController(root);
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
refreshGatherers();
|
||||
|
||||
|
@ -37,7 +45,3 @@ module.exports = function (io) {
|
|||
});
|
||||
});
|
||||
};
|
||||
|
||||
// socket.on('my other event', function (data) {
|
||||
// console.log(data);
|
||||
// });
|
17
lib/chat/Author.js
Normal file
17
lib/chat/Author.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
"use strict";
|
||||
|
||||
// Method which caches a single instance for each author
|
||||
|
||||
var Authors = (function () {
|
||||
var authors = {};
|
||||
|
||||
return function (author) {
|
||||
if (authors[author.id]) {
|
||||
return authors[author.id]
|
||||
} else {
|
||||
return authors[author.id] = author;
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
module.exports = Authors;
|
33
lib/chat/Chatroom.js
Normal file
33
lib/chat/Chatroom.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
"use strict";
|
||||
|
||||
var Message = require("./message");
|
||||
|
||||
function Chatroom (o) {
|
||||
if (!(this instanceof Chatroom)) {
|
||||
return new Chatroom(o);
|
||||
}
|
||||
|
||||
this.messages = [];
|
||||
};
|
||||
|
||||
Chatroom.prototype.createMessage = function (options, callback) {
|
||||
var message = new Message({
|
||||
author: options.author,
|
||||
content: options.content
|
||||
})
|
||||
this.messages.push(message);
|
||||
|
||||
if (callback) {
|
||||
return callback(message);
|
||||
}
|
||||
};
|
||||
|
||||
Chatroom.prototype.retrieveMessages = function (n) {
|
||||
return this.messages
|
||||
.slice(this.messages.length - n, this.messages.length)
|
||||
.map(function (message) {
|
||||
return message.toJson();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = Chatroom;
|
22
lib/chat/Message.js
Normal file
22
lib/chat/Message.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
"use strict";
|
||||
|
||||
var Author = require("./Author");
|
||||
|
||||
function Message (o) {
|
||||
this.author = Author(o.author);
|
||||
this.content = o.content;
|
||||
this.createdAt = new Date();
|
||||
};
|
||||
|
||||
Message.prototype.toJson = function () {
|
||||
return {
|
||||
author: {
|
||||
username: this.author.username,
|
||||
avatar: this.author.avatar
|
||||
},
|
||||
content: this.content,
|
||||
createdAt: this.createdAt
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Message;
|
39
lib/chat/controller.js
Normal file
39
lib/chat/controller.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
"use strict";
|
||||
|
||||
/*
|
||||
* Chatroom Controller
|
||||
*
|
||||
* Server API
|
||||
* message:new - New message needs to be displayed
|
||||
* message:refresh - Reload all messages
|
||||
*
|
||||
* Client API
|
||||
* message:new - New message has been created
|
||||
* message:refresh - Retrieve most recent messages
|
||||
*
|
||||
*/
|
||||
|
||||
var chatroom = require("./chatroom")();
|
||||
|
||||
module.exports = function (namespace) {
|
||||
|
||||
var broadcastUpdate = function (message) {
|
||||
namespace.emit("message:new", message.toJson());
|
||||
};
|
||||
|
||||
namespace.on('connection', function (socket) {
|
||||
|
||||
socket.on('message:new', function (data) {
|
||||
chatroom.createMessage({
|
||||
author: socket._user,
|
||||
content: data.content
|
||||
}, broadcastUpdate);
|
||||
});
|
||||
|
||||
socket.on('message:refresh', function () {
|
||||
socket.emit("message:refresh", {
|
||||
chatHistory: chatroom.retrieveMessages(20)
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -2,7 +2,10 @@ var GatherCounter = React.createClass({
|
|||
render: function () {
|
||||
return (
|
||||
<li>
|
||||
<a href="#"><i className="fa fa-users fa-fw"></i> Gatherers ({this.props.count})</a>
|
||||
<a href="#">
|
||||
<i className="fa fa-users fa-fw"></i> Gatherers
|
||||
<span className="badge add-left"> {this.props.count} </span>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
@ -43,6 +46,146 @@ var GathererMenu = React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
var Chatroom = React.createClass({
|
||||
componentDidMount: function () {
|
||||
var self = this;
|
||||
var TIMER_INTERVAL = 60000; // Every minute
|
||||
|
||||
socket.on("message:new", function (data) {
|
||||
var history = self.props.history;
|
||||
history.push(data);
|
||||
self.setProps({
|
||||
history: history
|
||||
});
|
||||
self.scrollToBottom();
|
||||
});
|
||||
|
||||
// Message History Retrieved
|
||||
socket.on("message:refresh", function (data) {
|
||||
self.setProps({
|
||||
history: data.chatHistory
|
||||
});
|
||||
self.scrollToBottom();
|
||||
});
|
||||
|
||||
socket.emit("message:refresh", {});
|
||||
|
||||
self.timer = setInterval(function () {
|
||||
self.refs.messages.refreshTime();
|
||||
}, TIMER_INTERVAL);
|
||||
},
|
||||
|
||||
componentDidUnmount: function () {
|
||||
clearInterval(this.timer);
|
||||
},
|
||||
sendMessage: function (message) {
|
||||
socket.emit("newMessage", {message: message});
|
||||
},
|
||||
scrollToBottom: function () {
|
||||
var node = React.findDOMNode(this.refs.messageContainer);
|
||||
console.log(node)
|
||||
node.scrollTop = node.scrollHeight;
|
||||
},
|
||||
render: function () {
|
||||
var messages = this.props.history.map(function (message) {
|
||||
return (
|
||||
<ChatMessage
|
||||
avatar={message.author.avatar}
|
||||
username={message.author.username}
|
||||
content={message.content}
|
||||
ref="messages"
|
||||
createdAt={message.createdAt} />
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div className="panel panel-default">
|
||||
<div className="panel-heading">Gather Chat</div>
|
||||
<div className="panel-body">
|
||||
<ul className="chat" id="chatmessages" ref="messageContainer">
|
||||
{messages}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="panel-footer">
|
||||
<MessageBar />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ChatMessage = React.createClass({
|
||||
getInitialState: function () {
|
||||
return {
|
||||
timeAgo: $.timeago(this.props.createdAt)
|
||||
}
|
||||
},
|
||||
refreshTime: function () {
|
||||
var self = this;
|
||||
self.setState({
|
||||
timeAgo: $.timeago(self.props.createdAt)
|
||||
});
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<li className="left clearfix">
|
||||
<span className="chat-img pull-left">
|
||||
<img
|
||||
src={this.props.avatar}
|
||||
alt="User Avatar"
|
||||
height="40"
|
||||
className="img-circle" />
|
||||
</span>
|
||||
<div className="chat-body clearfix">
|
||||
<div className="header">
|
||||
<strong className="primary-font">{this.props.username}</strong>
|
||||
<small className="pull-right text-muted">
|
||||
<i className="fa fa-clock-o fa-fw"></i> {this.state.timeAgo}
|
||||
</small>
|
||||
</div>
|
||||
<p>{this.props.content}</p>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var MessageBar = React.createClass({
|
||||
sendMessage: function (content) {
|
||||
socket.emit("message:new", {
|
||||
content: content
|
||||
});
|
||||
},
|
||||
handleSubmit: function (e) {
|
||||
e.preventDefault();
|
||||
var content = React.findDOMNode(this.refs.content).value.trim();
|
||||
if (!content) return;
|
||||
React.findDOMNode(this.refs.content).value = '';
|
||||
this.sendMessage(content);
|
||||
return;
|
||||
},
|
||||
render: function () {
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit} >
|
||||
<div className="input-group">
|
||||
<input
|
||||
id="btn-input"
|
||||
type="text"
|
||||
className="form-control"
|
||||
ref="content"
|
||||
placeholder="Be polite please..." />
|
||||
<span className="input-group-btn">
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
id="btn-chat"
|
||||
value="Send" />
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
React.render(<GathererMenu count={0} gatherers={[]} />, document.getElementById('side-menu'));
|
||||
React.render(<Chatroom history={[]}/>, document.getElementById('chatroom'));
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"scripts": {
|
||||
"test": "NODE_ENV=test mocha spec/",
|
||||
"start": "node index.js",
|
||||
"compile:dev:react": "node_modules/react-tools/bin/jsx --watch -x jsx lib/react/ public/js/",
|
||||
"compile:dev:react": "node_modules/react-tools/bin/jsx --watch --source-map-inline -x jsx lib/react/ public/js/",
|
||||
"compile:react": "node_modules/react-tools/bin/jsx -x jsx lib/react/ public/js/",
|
||||
"dev": "nodemon index.js"
|
||||
},
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
#chatroom {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.add-left {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
#chatmessages {
|
||||
max-height: 70%;
|
||||
min-height: 70%;
|
||||
overflow-y: scroll;
|
||||
}
|
147
public/js/app.js
147
public/js/app.js
File diff suppressed because one or more lines are too long
221
public/js/timeago.jquery.js
Normal file
221
public/js/timeago.jquery.js
Normal file
|
@ -0,0 +1,221 @@
|
|||
/**
|
||||
* Timeago is a jQuery plugin that makes it easy to support automatically
|
||||
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
|
||||
*
|
||||
* @name timeago
|
||||
* @version 1.4.1
|
||||
* @requires jQuery v1.2.3+
|
||||
* @author Ryan McGeary
|
||||
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
|
||||
*
|
||||
* For usage and examples, visit:
|
||||
* http://timeago.yarp.com/
|
||||
*
|
||||
* Copyright (c) 2008-2015, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
|
||||
*/
|
||||
|
||||
(function (factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(['jquery'], factory);
|
||||
} else {
|
||||
// Browser globals
|
||||
factory(jQuery);
|
||||
}
|
||||
}(function ($) {
|
||||
$.timeago = function(timestamp) {
|
||||
if (timestamp instanceof Date) {
|
||||
return inWords(timestamp);
|
||||
} else if (typeof timestamp === "string") {
|
||||
return inWords($.timeago.parse(timestamp));
|
||||
} else if (typeof timestamp === "number") {
|
||||
return inWords(new Date(timestamp));
|
||||
} else {
|
||||
return inWords($.timeago.datetime(timestamp));
|
||||
}
|
||||
};
|
||||
var $t = $.timeago;
|
||||
|
||||
$.extend($.timeago, {
|
||||
settings: {
|
||||
refreshMillis: 60000,
|
||||
allowPast: true,
|
||||
allowFuture: false,
|
||||
localeTitle: false,
|
||||
cutoff: 0,
|
||||
strings: {
|
||||
prefixAgo: null,
|
||||
prefixFromNow: null,
|
||||
suffixAgo: "ago",
|
||||
suffixFromNow: "from now",
|
||||
inPast: 'any moment now',
|
||||
seconds: "less than a minute",
|
||||
minute: "about a minute",
|
||||
minutes: "%d minutes",
|
||||
hour: "about an hour",
|
||||
hours: "about %d hours",
|
||||
day: "a day",
|
||||
days: "%d days",
|
||||
month: "about a month",
|
||||
months: "%d months",
|
||||
year: "about a year",
|
||||
years: "%d years",
|
||||
wordSeparator: " ",
|
||||
numbers: []
|
||||
}
|
||||
},
|
||||
|
||||
inWords: function(distanceMillis) {
|
||||
if(!this.settings.allowPast && ! this.settings.allowFuture) {
|
||||
throw 'timeago allowPast and allowFuture settings can not both be set to false.';
|
||||
}
|
||||
|
||||
var $l = this.settings.strings;
|
||||
var prefix = $l.prefixAgo;
|
||||
var suffix = $l.suffixAgo;
|
||||
if (this.settings.allowFuture) {
|
||||
if (distanceMillis < 0) {
|
||||
prefix = $l.prefixFromNow;
|
||||
suffix = $l.suffixFromNow;
|
||||
}
|
||||
}
|
||||
|
||||
if(!this.settings.allowPast && distanceMillis >= 0) {
|
||||
return this.settings.strings.inPast;
|
||||
}
|
||||
|
||||
var seconds = Math.abs(distanceMillis) / 1000;
|
||||
var minutes = seconds / 60;
|
||||
var hours = minutes / 60;
|
||||
var days = hours / 24;
|
||||
var years = days / 365;
|
||||
|
||||
function substitute(stringOrFunction, number) {
|
||||
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
|
||||
var value = ($l.numbers && $l.numbers[number]) || number;
|
||||
return string.replace(/%d/i, value);
|
||||
}
|
||||
|
||||
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
|
||||
seconds < 90 && substitute($l.minute, 1) ||
|
||||
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
|
||||
minutes < 90 && substitute($l.hour, 1) ||
|
||||
hours < 24 && substitute($l.hours, Math.round(hours)) ||
|
||||
hours < 42 && substitute($l.day, 1) ||
|
||||
days < 30 && substitute($l.days, Math.round(days)) ||
|
||||
days < 45 && substitute($l.month, 1) ||
|
||||
days < 365 && substitute($l.months, Math.round(days / 30)) ||
|
||||
years < 1.5 && substitute($l.year, 1) ||
|
||||
substitute($l.years, Math.round(years));
|
||||
|
||||
var separator = $l.wordSeparator || "";
|
||||
if ($l.wordSeparator === undefined) { separator = " "; }
|
||||
return $.trim([prefix, words, suffix].join(separator));
|
||||
},
|
||||
|
||||
parse: function(iso8601) {
|
||||
var s = $.trim(iso8601);
|
||||
s = s.replace(/\.\d+/,""); // remove milliseconds
|
||||
s = s.replace(/-/,"/").replace(/-/,"/");
|
||||
s = s.replace(/T/," ").replace(/Z/," UTC");
|
||||
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
|
||||
s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900
|
||||
return new Date(s);
|
||||
},
|
||||
datetime: function(elem) {
|
||||
var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
|
||||
return $t.parse(iso8601);
|
||||
},
|
||||
isTime: function(elem) {
|
||||
// jQuery's `is()` doesn't play well with HTML5 in IE
|
||||
return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
|
||||
}
|
||||
});
|
||||
|
||||
// functions that can be called via $(el).timeago('action')
|
||||
// init is default when no action is given
|
||||
// functions are called with context of a single element
|
||||
var functions = {
|
||||
init: function(){
|
||||
var refresh_el = $.proxy(refresh, this);
|
||||
refresh_el();
|
||||
var $s = $t.settings;
|
||||
if ($s.refreshMillis > 0) {
|
||||
this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis);
|
||||
}
|
||||
},
|
||||
update: function(time){
|
||||
var parsedTime = $t.parse(time);
|
||||
$(this).data('timeago', { datetime: parsedTime });
|
||||
if($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString());
|
||||
refresh.apply(this);
|
||||
},
|
||||
updateFromDOM: function(){
|
||||
$(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) });
|
||||
refresh.apply(this);
|
||||
},
|
||||
dispose: function () {
|
||||
if (this._timeagoInterval) {
|
||||
window.clearInterval(this._timeagoInterval);
|
||||
this._timeagoInterval = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.fn.timeago = function(action, options) {
|
||||
var fn = action ? functions[action] : functions.init;
|
||||
if(!fn){
|
||||
throw new Error("Unknown function name '"+ action +"' for timeago");
|
||||
}
|
||||
// each over objects here and call the requested function
|
||||
this.each(function(){
|
||||
fn.call(this, options);
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
function refresh() {
|
||||
//check if it's still visible
|
||||
if(!$.contains(document.documentElement,this)){
|
||||
//stop if it has been removed
|
||||
$(this).timeago("dispose");
|
||||
return this;
|
||||
}
|
||||
|
||||
var data = prepareData(this);
|
||||
var $s = $t.settings;
|
||||
|
||||
if (!isNaN(data.datetime)) {
|
||||
if ( $s.cutoff == 0 || Math.abs(distance(data.datetime)) < $s.cutoff) {
|
||||
$(this).text(inWords(data.datetime));
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
function prepareData(element) {
|
||||
element = $(element);
|
||||
if (!element.data("timeago")) {
|
||||
element.data("timeago", { datetime: $t.datetime(element) });
|
||||
var text = $.trim(element.text());
|
||||
if ($t.settings.localeTitle) {
|
||||
element.attr("title", element.data('timeago').datetime.toLocaleString());
|
||||
} else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
|
||||
element.attr("title", text);
|
||||
}
|
||||
}
|
||||
return element.data("timeago");
|
||||
}
|
||||
|
||||
function inWords(date) {
|
||||
return $t.inWords(distance(date));
|
||||
}
|
||||
|
||||
function distance(date) {
|
||||
return (new Date().getTime() - date.getTime());
|
||||
}
|
||||
|
||||
// fix for IE6 suckage
|
||||
document.createElement("abbr");
|
||||
document.createElement("time");
|
||||
}));
|
|
@ -8,8 +8,7 @@
|
|||
<div class="col-lg-6">
|
||||
<h3 class="page-header">Current Gather</h3>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<h3 class="page-header">Chat</h3>
|
||||
<div class="col-lg-6" id="chatroom">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<link rel="stylesheet" href="/css/app.css">
|
||||
|
||||
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
|
||||
<script src="/js/timeago.jquery.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
|
||||
<script src="/js/socket.io-1.3.5.js"></script>
|
||||
<script src="/js/theme.js"></script>
|
||||
|
|
Loading…
Reference in a new issue