This commit is contained in:
Gene S
2016-01-20 10:24:30 -08:00
+177 -101
View File
@@ -27,8 +27,12 @@
var HOST = "localhost:6060"; var HOST = "localhost:6060";
// API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own // API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own
var API_KEY = "AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K"; var API_KEY = "AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K";
// Minimum time between two keypress notifications, milliseconds // Minimum time between two keypress notifications, milliseconds.
var KEYPRESS_NOTIFICATION = 5*1000; var KEYPRESS_DELAY = 5*1000;
// Delay before sending a {note} for reciving a message, milliseconds.
var RECEIVED_DELAY = 500;
// Delay before sending a read notification, milliseconds.
var READ_DELAY = 3*1000;
// Short representation of time in the past. // Short representation of time in the past.
function shortDateFormat(then) { function shortDateFormat(then) {
@@ -274,7 +278,7 @@
this.setState({showErrorText: false}); this.setState({showErrorText: false});
}, },
handleLoginRequested: function(login, password) { handleLoginRequested: function(login, password) {
this.props.onLogin(login, password); this.props.onLoginRequest(login, password);
this.setState({showErrorText: true}); this.setState({showErrorText: true});
}, },
render: function() { render: function() {
@@ -370,8 +374,8 @@
}, },
render: function() { render: function() {
var title = this.props.public; var title = this.props.public;
if (title.length > 26) { if (title.length > 32) {
title = title.substring(0, 24) + "..."; title = title.substring(0, 30) + "...";
} }
return ( return (
<li className="media contact" onClick={this.handleClick}> <li className="media contact" onClick={this.handleClick}>
@@ -397,16 +401,18 @@
<span>{showUnreadBadge}</span> <span>{showUnreadBadge}</span>
); );
} }
}) });
/* Online/offline indicator */ /* Online/offline indicator */
Contact.Online = React.createClass({ Contact.Online = React.createClass({
render: function() { render: function() {
var lastOnline = ''; var lastOnline = '';
if (!this.props.online && this.props.when) { if (this.props.now) {
lastOnline = "online now";
} else if (this.props.when) {
lastOnline = this.props.when; lastOnline = this.props.when;
} else { } else {
lastOnline = 'online now'; lastOnline = "offline";
} }
return ( return (
@@ -415,48 +421,66 @@
</span> </span>
); );
} }
}) });
/* List of contacts/topics */ /* ContactsView holds all contacts-related stuff */
var ContactList = React.createClass({ var ContactsView = React.createClass({
getInitialState: function() { getInitialState: function() {
return {selected: false}; return {subscriptions: []};
},
componentWillMount: function() {
var me = Tinode.getMeTopic();
me.onContactUpdate = this.tnMeContactUpdate;
me.onData = this.tnInvite;
me.onSubsUpdated = this.tnMeSubsUpdated;
},
tnMeContactUpdate: function(what, cont) {
console.log("changed: " + what + " in " + cont.topic);
this.forceUpdate();
},
tnInvite: function(invite) {
console.log(invite);
},
tnMeSubsUpdated: function(names) {
/*var subs = [];
Tinode.getMeTopic().contacts(function(cont) {
subs.push(cont);
});
this.setState({subscriptions: subs});
*/
this.forceUpdate();
}, },
onSelectionChange: function(sel) { onSelectionChange: function(sel) {
this.props.onTopicSelected(sel); this.props.onTopicSelected(sel);
}, },
render: function() { render: function() {
var contactNodes = null; var contactNodes = [];
if (this.props.data) { Tinode.getMeTopic().contacts(function(c) {
contactNodes = Object.keys(this.props.data).map(function(key) { var online = false;
var c = this.props.data[key]; var lastSeen = null;
// FIXME(gene): add support for received/read count if (c.seen) {
var unread = c.seq; lastSeen = shortDateFormat(new Date(c.seen.when));
return ( // TODO(gene): also handle user agent in c.seen.ua
<Contact public={c.public} comment={c.private} unread={unread} }
onSelected={this.onSelectionChange} topic={c.topic} key={c.topic} /> var unread = c.seq;
); if (c.read > 0) {
}, this); unread -= c.read;
} }
return ( contactNodes.push(
<div className="contactList"> <Contact public={c.public} comment={c.private} unread={unread} now={online} when={lastSeen}
<ul className="media-list"> onSelected={this.onSelectionChange} topic={c.topic} key={c.topic} />
{contactNodes} );
</ul> }, this);
</div>
);
}
});
/* The panel holds all contacts-related stuff */
var ContactsView = React.createClass({
render: function() {
return ( return (
<div className="contactBox"> <div className="contactBox">
<div className="container-fluid"> <div className="container-fluid">
<NewTopic /> <NewTopic />
<ContactList data={this.props.data} <div className="contactList">
onTopicSelected={this.props.onTopicSelected} /> <ul className="media-list">
{contactNodes}
</ul>
</div>
</div> </div>
</div> </div>
); );
@@ -494,7 +518,8 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
{id: 7, content: "OK!", timestamp: new Date(2016, 0, 3, 18, 19, 20, 567), response: true} {id: 7, content: "OK!", timestamp: new Date(2016, 0, 3, 18, 19, 20, 567), response: true}
]; ];
*/ */
var received = { var Received = {
UNDEF: -2,
SENDING: -1, SENDING: -1,
SERVER: 0, SERVER: 0,
CLIENT: 1, CLIENT: 1,
@@ -520,27 +545,26 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
/* Received/read indicator */ /* Received/read indicator */
Message.ReceivedMarker = React.createClass({ Message.ReceivedMarker = React.createClass({
render: function() { render: function() {
var timestamp = (this.props.received === received.SENDING) ? var timestamp = (this.props.received === Received.SENDING) ?
"sending ..." : "sending ..." :
shortDateFormat(this.props.timestamp); shortDateFormat(this.props.timestamp);
var marker = null; var marker = null;
// These markers make sense for own messages only.
if (!this.props.response) { if (this.props.received === Received.SENDING) {
if (this.props.received === received.SENDING) { marker = '\u231A'; // watch face
marker = '\u231A'; // watch face } else if (this.props.received === Received.SERVER || this.props.received === Received.CLIENT) {
} else if (this.props.received === received.SERVER || this.props.received === received.CLIENT) { marker = '\u2714'; // checkmark
marker = '\u2714'; // checkmark if (this.props.received === Received.CLIENT) {
if (this.props.received === received.CLIENT) { var style = {marginLeft: "-0.5em"};
var style = {marginLeft: "-0.5em"}; marker += <span style={style}>{'\u2714'}</span>;
marker += <span style={style}>&#x2714;</span>;
}
} else if (this.props.received === received.READ) {
marker = '\u1F441'; // open eye
} }
} else if (this.props.received === Received.READ) {
marker = '\uD83D\uDC41'; // open eye
} }
return ( return (
<span className="pull-right timestamp"> <span className="pull-right timestamp">
<small className="text-muted">{timestamp}</small> {marker} <small className="text-muted">{timestamp}{'\u00a0'}{marker}</small>
</span> </span>
); );
} }
@@ -552,7 +576,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
return { return {
message: '', message: '',
// Make initial keypress time as if it happened 5001 milliseconds in the past. // Make initial keypress time as if it happened 5001 milliseconds in the past.
keypressTimestamp: new Date().getTime() - KEYPRESS_NOTIFICATION - 1 keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1
}; };
}, },
handleSend: function(e) { handleSend: function(e) {
@@ -565,7 +589,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
handleMessageTyping: function(e) { handleMessageTyping: function(e) {
var newState = {message: e.target.value}; var newState = {message: e.target.value};
var now = new Date().getTime(); var now = new Date().getTime();
if (now - this.state.keypressTimestamp > KEYPRESS_NOTIFICATION) { if (now - this.state.keypressTimestamp > KEYPRESS_DELAY) {
var topic = Tinode.getTopic(this.props.topic); var topic = Tinode.getTopic(this.props.topic);
if (topic.isSubscribed()) { if (topic.isSubscribed()) {
topic.noteKeyPress(); topic.noteKeyPress();
@@ -598,27 +622,52 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
title: '' title: ''
}; };
}, },
componentDidMount: function() { // Scroll last message into view on component update e.g. on message received.
componentDidUpdate: function() {
//var node = ReactDOM.findDOMNode(this);
//if (node) {
// node.scrollTop = node.scrollHeight;
//}
var node = this.refs.messagesPanel;
node.scrollTop = node.scrollHeight;
},
componentWillMount: function() {
this.propsChange(this.props); this.propsChange(this.props);
}, },
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps: function(nextProps) {
this.propsChange(nextProps); this.propsChange(nextProps);
}, },
propsChange: function(props) { propsChange: function(props) {
var msgs = []; if (props.topicSelected != this.state.topic) {
var topic = Tinode.getTopic(props.topic); var msgs = [];
if (topic.isSubscribed()) { var topic = Tinode.getTopic(props.topicSelected);
this.handleInfoChange(topic); // Bind the new topic to component.
topic.messages(function(msg) {
msgs = msgs.concat(msg);
});
} else {
topic.onData = this.handleNewMessage; topic.onData = this.handleNewMessage;
topic.onInfoChange = this.handleInfoChange; topic.onMetaInfo = this.handleInfoChange;
topic.onSubsUpdated = this.handleSubsUpdated; topic.onSubsUpdated = this.handleSubsUpdated;
topic.subscribe({get: "info sub data"}); // Unbind the previous topic from this component.
if (this.state.topic) {
var oldTopic = Tinode.getTopic(this.state.topic);
oldTopic.onData = undefined;
oldTopic.onMetaInfo = undefined;
oldTopic.onSubsUpdated = undefined;
}
if (topic.isSubscribed()) {
this.handleInfoChange(topic);
topic.messages(function(msg) {
msgs = msgs.concat(msg);
});
} else {
topic.subscribe({get: "info sub data"});
}
// The user switched to the new topic before the timer for
// the previous topic has triggered, kill it.
clearTimeout(this.readTimer);
this.setState({messages: msgs, topic: props.topicSelected});
} }
this.setState({messages: msgs, topic: props.topic});
}, },
handleInfoChange: function(info) { handleInfoChange: function(info) {
console.log("handleInfoChange: " + info); console.log("handleInfoChange: " + info);
@@ -628,23 +677,36 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
}, },
handleNewMessage: function(msg) { handleNewMessage: function(msg) {
// FIXME(gene): handleNewMessage is bound to all loaded topics as onData. Unbind it when switching topics. this.setState({messages: this.state.messages.concat(msg)});
// Ignore messages from all other topics but the current one. // Aknowledge all messages, including own messges.
if (this.state.topic === msg.topic) { clearTimeout(this.readTimer);
this.setState({messages: this.state.messages.concat(msg)}); this.readTimer = setTimeout(function() {
} Tinode.getTopic(msg.topic).noteRead(msg.seq);
}, READ_DELAY);
this.props.onData(msg);
}, },
render: function() { render: function() {
var me = Tinode.getCurrentUserID(); var me = Tinode.getCurrentUserID();
var topic = Tinode.getTopic(this.state.topic); var topic = Tinode.getTopic(this.state.topic);
var messageNodes = this.state.messages.map(function(msg) { var messageNodes = this.state.messages.map(function(msg) {
var isReply = !(msg.from === me); var isReply = !(msg.from === me);
var msgReceived = Received.UNDEF;
if (!isReply) {
if (topic.msgReadCount(msg.seq) > 1) {
msgReceived = Received.READ;
} else if (topic.msgRecvCount(msg.seq) > 1) {
msgReceived = Received.CLIENT;
} else {
msgReceived = Received.SERVER;
}
}
return ( return (
<Message content={msg.content} timestamp={msg.ts} response={isReply} key={msg.seq} /> <Message content={msg.content} timestamp={msg.ts} response={isReply}
received={msgReceived} key={msg.seq} />
); );
}); });
return ( return (
<div className="panel panel-default fullViewPort" id="messagesPanel"> <div className="panel panel-default fullViewPort" id="messagesPanel" ref="messagesPanel">
<div className="panel-heading" id="chatTitle"> <div className="panel-heading" id="chatTitle">
<h3 className="panel-title">{this.state.title}</h3> <h3 className="panel-title">{this.state.title}</h3>
</div> </div>
@@ -654,7 +716,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
</ul> </ul>
</div> </div>
<div className="panel-footer" id="sendMessagePanel"> <div className="panel-footer" id="sendMessagePanel">
<Message.Send topic={this.props.topic} sendMessage={this.props.sendMessage} /> <Message.Send topic={this.props.topicSelected} sendMessage={this.props.sendMessage} />
</div> </div>
</div> </div>
); );
@@ -681,8 +743,8 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
var AppView = React.createClass({ var AppView = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
sidePanel: 'login', sidePanelSelected: 'login',
sideTitle: 'not set', sidePanelTitle: 'not set',
loggedIn: false, loggedIn: false,
errorText: '', errorText: '',
topicSelected: '', topicSelected: '',
@@ -692,36 +754,44 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
}, },
componentDidMount: function() { componentDidMount: function() {
Tinode.enableLogging(true); Tinode.enableLogging(true);
// Setup with default transport. Tinode.onDisconnect = this.handleDicsonnect;
// Setup with the default transport, usually websocket.
Tinode.setup(HOST, API_KEY); Tinode.setup(HOST, API_KEY);
}, },
handleLogin: function(login, password) { handleDicsonnect: function() {
console.log("DISCONNECTED!");
},
// User clicked Login button in the side panel.
handleLoginRequest: function(login, password) {
var instance = this; var instance = this;
this.setState({loginDisabled: true, errorText: ''}); this.setState({loginDisabled: true, errorText: ''});
Tinode.connect().then(function() { Tinode.connect().then(function() {
// Login
return Tinode.loginBasic(login, password); return Tinode.loginBasic(login, password);
}).then(function() { }).then(function() {
instance.setState({loggedIn: true}); // Logged in fine, subscribe to 'me' attaching callbacks from the contacts view.
var me = Tinode.getMeTopic(); var me = Tinode.getMeTopic();
me.onInfoChange = instance.handleMeInfoChange; me.onMetaInfo = instance.tnMeMetaInfo;
me.onSubsChange = instance.handleMeSubsChange; instance.setState({sidePanelSelected: "contacts", loggedIn: true});
// Subscribe, fetch topic info and the list of subscriptions. // Subscribe, fetch topic info and the list of subscriptions.
return me.subscribe({get: "info sub"}); me.subscribe({get: "info sub data"});
}).then(function() {
var me = Tinode.getMeTopic();
instance.setState({sidePanel: "contacts"});
}).catch(function(err) { }).catch(function(err) {
// Login failed, report error
instance.setState({loginDisabled: false, errorText: err.message}); instance.setState({loginDisabled: false, errorText: err.message});
}); });
}, },
handleMeInfoChange: function(info) { tnMeMetaInfo: function(info) {
this.setState({sideTitle: info.public}); this.setState({sidePanelTitle: info.public});
}, },
handleMeSubsChange: function(sub) { // Sending "received" notifications
var subs = this.state.subscriptions; tnData: function(data) {
subs[sub.topic] = sub; clearTimeout(this.receivedTimer);
this.setState({subscriptions: subs}); this.receivedTimer = setTimeout(function() {
this.receivedTimer = undefined;
Tinode.getTopic(data.topic).noteRecv(data.seq);
}, RECEIVED_DELAY);
}, },
// Registration of a new account.
handleNewAccount: function(login_, password_, public_, private_) { handleNewAccount: function(login_, password_, public_, private_) {
var instance = this; var instance = this;
Tinode.connect().then(function(){ Tinode.connect().then(function(){
@@ -731,24 +801,29 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
instance.setState({errorText: err.message}); instance.setState({errorText: err.message});
}); });
}, },
// User clicked on a contact in the side panel.
handleTopicSelected: function(topicName) { handleTopicSelected: function(topicName) {
this.setState({topicSelected: topicName}); this.setState({topicSelected: topicName});
}, },
// User clicked on a message send button.
handleSendMessage: function(msg) { handleSendMessage: function(msg) {
var topic = Tinode.getTopic(this.state.topicSelected); var topic = Tinode.getTopic(this.state.topicSelected);
topic.publish(msg); topic.publish(msg);
}, },
// User chose a Sign Up menu item.
handleSignUp: function() { handleSignUp: function() {
this.setState({sidePanel: 'register'}); this.setState({sidePanelSelected: 'register'});
}, },
// User chose Settings menu item.
handleSettings: function() { handleSettings: function() {
this.setState({sidePanel: 'settings'}); this.setState({sidePanelSelected: 'settings'});
}, },
// User clicked Cancel button in Setting or Sign Up panel.
handleSidepanelCancel: function() { handleSidepanelCancel: function() {
if (this.state.loggedIn) { if (this.state.loggedIn) {
this.setState({sidePanel: 'contacts'}); this.setState({sidePanelSelected: 'contacts'});
} else { } else {
this.setState({sidePanel: 'login'}); this.setState({sidePanelSelected: 'login'});
} }
}, },
render: function() { render: function() {
@@ -757,14 +832,14 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
<div id="sidepanel-wrapper"> <div id="sidepanel-wrapper">
<SidepanelView <SidepanelView
state={this.state.sidePanel} state={this.state.sidePanelSelected}
title={this.state.sideTitle} title={this.state.sidePanelTitle}
subscriptions={this.state.subscriptions} subscriptions={this.state.subscriptions}
loginDisabled={this.state.loginDisabled} loginDisabled={this.state.loginDisabled}
errorText={this.state.errorText} errorText={this.state.errorText}
onSignUp={this.handleSignUp} onSignUp={this.handleSignUp}
onSettings={this.handleSettings} onSettings={this.handleSettings}
onLogin={this.handleLogin} onLoginRequest={this.handleLoginRequest}
onCreateAccount={this.handleNewAccount} onCreateAccount={this.handleNewAccount}
onTopicSelected={this.handleTopicSelected} onTopicSelected={this.handleTopicSelected}
onCancel={this.handleSidepanelCancel} /> onCancel={this.handleSidepanelCancel} />
@@ -775,7 +850,8 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
<div className="row fullHeight"> <div className="row fullHeight">
<div className="col-lg-12 fullHeight"> <div className="col-lg-12 fullHeight">
{this.state.topicSelected ? {this.state.topicSelected ?
<MessagesView topic={this.state.topicSelected} sendMessage={this.handleSendMessage} /> : <MessagesView topicSelected={this.state.topicSelected} onData={this.tnData}
sendMessage={this.handleSendMessage} /> :
<DummyMessagesView />} <DummyMessagesView />}
</div> </div>
</div> </div>