diff --git a/index.html b/index.html
index bb2e9398..5bc8df50 100644
--- a/index.html
+++ b/index.html
@@ -27,8 +27,12 @@
var HOST = "localhost:6060";
// API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own
var API_KEY = "AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K";
- // Minimum time between two keypress notifications, milliseconds
- var KEYPRESS_NOTIFICATION = 5*1000;
+ // Minimum time between two keypress notifications, milliseconds.
+ 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.
function shortDateFormat(then) {
@@ -274,7 +278,7 @@
this.setState({showErrorText: false});
},
handleLoginRequested: function(login, password) {
- this.props.onLogin(login, password);
+ this.props.onLoginRequest(login, password);
this.setState({showErrorText: true});
},
render: function() {
@@ -370,8 +374,8 @@
},
render: function() {
var title = this.props.public;
- if (title.length > 26) {
- title = title.substring(0, 24) + "...";
+ if (title.length > 32) {
+ title = title.substring(0, 30) + "...";
}
return (
@@ -397,16 +401,18 @@
{showUnreadBadge}
);
}
- })
+ });
/* Online/offline indicator */
Contact.Online = React.createClass({
render: function() {
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;
} else {
- lastOnline = 'online now';
+ lastOnline = "offline";
}
return (
@@ -415,48 +421,66 @@
);
}
- })
+ });
- /* List of contacts/topics */
- var ContactList = React.createClass({
+ /* ContactsView holds all contacts-related stuff */
+ var ContactsView = React.createClass({
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) {
this.props.onTopicSelected(sel);
},
render: function() {
- var contactNodes = null;
- if (this.props.data) {
- contactNodes = Object.keys(this.props.data).map(function(key) {
- var c = this.props.data[key];
- // FIXME(gene): add support for received/read count
- var unread = c.seq;
- return (
-
- );
- }, this);
- }
- return (
-
- );
- }
- });
+ var contactNodes = [];
+ Tinode.getMeTopic().contacts(function(c) {
+ var online = false;
+ var lastSeen = null;
+ if (c.seen) {
+ lastSeen = shortDateFormat(new Date(c.seen.when));
+ // TODO(gene): also handle user agent in c.seen.ua
+ }
+ var unread = c.seq;
+ if (c.read > 0) {
+ unread -= c.read;
+ }
+ contactNodes.push(
+
+ );
+ }, this);
- /* The panel holds all contacts-related stuff */
- var ContactsView = React.createClass({
- render: function() {
return (
);
@@ -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}
];
*/
- var received = {
+ var Received = {
+ UNDEF: -2,
SENDING: -1,
SERVER: 0,
CLIENT: 1,
@@ -520,27 +545,26 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
/* Received/read indicator */
Message.ReceivedMarker = React.createClass({
render: function() {
- var timestamp = (this.props.received === received.SENDING) ?
+ var timestamp = (this.props.received === Received.SENDING) ?
"sending ..." :
shortDateFormat(this.props.timestamp);
var marker = null;
- // These markers make sense for own messages only.
- if (!this.props.response) {
- if (this.props.received === received.SENDING) {
- marker = '\u231A'; // watch face
- } else if (this.props.received === received.SERVER || this.props.received === received.CLIENT) {
- marker = '\u2714'; // checkmark
- if (this.props.received === received.CLIENT) {
- var style = {marginLeft: "-0.5em"};
- marker += ✔;
- }
- } else if (this.props.received === received.READ) {
- marker = '\u1F441'; // open eye
+
+ if (this.props.received === Received.SENDING) {
+ marker = '\u231A'; // watch face
+ } else if (this.props.received === Received.SERVER || this.props.received === Received.CLIENT) {
+ marker = '\u2714'; // checkmark
+ if (this.props.received === Received.CLIENT) {
+ var style = {marginLeft: "-0.5em"};
+ marker += {'\u2714'};
}
+ } else if (this.props.received === Received.READ) {
+ marker = '\uD83D\uDC41'; // open eye
}
+
return (
- {timestamp} {marker}
+ {timestamp}{'\u00a0'}{marker}
);
}
@@ -552,7 +576,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
return {
message: '',
// 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) {
@@ -565,7 +589,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
handleMessageTyping: function(e) {
var newState = {message: e.target.value};
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);
if (topic.isSubscribed()) {
topic.noteKeyPress();
@@ -598,27 +622,52 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
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);
},
componentWillReceiveProps: function(nextProps) {
this.propsChange(nextProps);
},
propsChange: function(props) {
- var msgs = [];
- var topic = Tinode.getTopic(props.topic);
- if (topic.isSubscribed()) {
- this.handleInfoChange(topic);
- topic.messages(function(msg) {
- msgs = msgs.concat(msg);
- });
- } else {
+ if (props.topicSelected != this.state.topic) {
+ var msgs = [];
+ var topic = Tinode.getTopic(props.topicSelected);
+ // Bind the new topic to component.
topic.onData = this.handleNewMessage;
- topic.onInfoChange = this.handleInfoChange;
+ topic.onMetaInfo = this.handleInfoChange;
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) {
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) {
- // FIXME(gene): handleNewMessage is bound to all loaded topics as onData. Unbind it when switching topics.
- // Ignore messages from all other topics but the current one.
- if (this.state.topic === msg.topic) {
- this.setState({messages: this.state.messages.concat(msg)});
- }
+ this.setState({messages: this.state.messages.concat(msg)});
+ // Aknowledge all messages, including own messges.
+ clearTimeout(this.readTimer);
+ this.readTimer = setTimeout(function() {
+ Tinode.getTopic(msg.topic).noteRead(msg.seq);
+ }, READ_DELAY);
+ this.props.onData(msg);
},
render: function() {
var me = Tinode.getCurrentUserID();
var topic = Tinode.getTopic(this.state.topic);
var messageNodes = this.state.messages.map(function(msg) {
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 (
-
+
{this.state.title}
@@ -654,7 +716,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
-
+
);
@@ -681,8 +743,8 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
var AppView = React.createClass({
getInitialState: function() {
return {
- sidePanel: 'login',
- sideTitle: 'not set',
+ sidePanelSelected: 'login',
+ sidePanelTitle: 'not set',
loggedIn: false,
errorText: '',
topicSelected: '',
@@ -692,36 +754,44 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
},
componentDidMount: function() {
Tinode.enableLogging(true);
- // Setup with default transport.
+ Tinode.onDisconnect = this.handleDicsonnect;
+ // Setup with the default transport, usually websocket.
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;
this.setState({loginDisabled: true, errorText: ''});
Tinode.connect().then(function() {
+ // Login
return Tinode.loginBasic(login, password);
}).then(function() {
- instance.setState({loggedIn: true});
+ // Logged in fine, subscribe to 'me' attaching callbacks from the contacts view.
var me = Tinode.getMeTopic();
- me.onInfoChange = instance.handleMeInfoChange;
- me.onSubsChange = instance.handleMeSubsChange;
+ me.onMetaInfo = instance.tnMeMetaInfo;
+ instance.setState({sidePanelSelected: "contacts", loggedIn: true});
// Subscribe, fetch topic info and the list of subscriptions.
- return me.subscribe({get: "info sub"});
- }).then(function() {
- var me = Tinode.getMeTopic();
- instance.setState({sidePanel: "contacts"});
+ me.subscribe({get: "info sub data"});
}).catch(function(err) {
+ // Login failed, report error
instance.setState({loginDisabled: false, errorText: err.message});
});
},
- handleMeInfoChange: function(info) {
- this.setState({sideTitle: info.public});
+ tnMeMetaInfo: function(info) {
+ this.setState({sidePanelTitle: info.public});
},
- handleMeSubsChange: function(sub) {
- var subs = this.state.subscriptions;
- subs[sub.topic] = sub;
- this.setState({subscriptions: subs});
+ // Sending "received" notifications
+ tnData: function(data) {
+ clearTimeout(this.receivedTimer);
+ 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_) {
var instance = this;
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});
});
},
+ // User clicked on a contact in the side panel.
handleTopicSelected: function(topicName) {
this.setState({topicSelected: topicName});
},
+ // User clicked on a message send button.
handleSendMessage: function(msg) {
var topic = Tinode.getTopic(this.state.topicSelected);
topic.publish(msg);
},
+ // User chose a Sign Up menu item.
handleSignUp: function() {
- this.setState({sidePanel: 'register'});
+ this.setState({sidePanelSelected: 'register'});
},
+ // User chose Settings menu item.
handleSettings: function() {
- this.setState({sidePanel: 'settings'});
+ this.setState({sidePanelSelected: 'settings'});
},
+ // User clicked Cancel button in Setting or Sign Up panel.
handleSidepanelCancel: function() {
if (this.state.loggedIn) {
- this.setState({sidePanel: 'contacts'});
+ this.setState({sidePanelSelected: 'contacts'});
} else {
- this.setState({sidePanel: 'login'});
+ this.setState({sidePanelSelected: 'login'});
}
},
render: function() {
@@ -757,14 +832,14 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
@@ -775,7 +850,8 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
{this.state.topicSelected ?
- :
+ :
}