Merge branch 'master' of https://github.com/tinode/example-react-js
This commit is contained in:
+177
-101
@@ -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 (
|
||||
<li className="media contact" onClick={this.handleClick}>
|
||||
@@ -397,16 +401,18 @@
|
||||
<span>{showUnreadBadge}</span>
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
/* 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 @@
|
||||
</span>
|
||||
);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
/* 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 (
|
||||
<Contact public={c.public} comment={c.private} unread={unread}
|
||||
onSelected={this.onSelectionChange} topic={c.topic} key={c.topic} />
|
||||
);
|
||||
}, this);
|
||||
}
|
||||
return (
|
||||
<div className="contactList">
|
||||
<ul className="media-list">
|
||||
{contactNodes}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
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(
|
||||
<Contact public={c.public} comment={c.private} unread={unread} now={online} when={lastSeen}
|
||||
onSelected={this.onSelectionChange} topic={c.topic} key={c.topic} />
|
||||
);
|
||||
}, this);
|
||||
|
||||
/* The panel holds all contacts-related stuff */
|
||||
var ContactsView = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div className="contactBox">
|
||||
<div className="container-fluid">
|
||||
<NewTopic />
|
||||
<ContactList data={this.props.data}
|
||||
onTopicSelected={this.props.onTopicSelected} />
|
||||
<NewTopic />
|
||||
<div className="contactList">
|
||||
<ul className="media-list">
|
||||
{contactNodes}
|
||||
</ul>
|
||||
</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}
|
||||
];
|
||||
*/
|
||||
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 += <span style={style}>✔</span>;
|
||||
}
|
||||
} 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 += <span style={style}>{'\u2714'}</span>;
|
||||
}
|
||||
} else if (this.props.received === Received.READ) {
|
||||
marker = '\uD83D\uDC41'; // open eye
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="pull-right timestamp">
|
||||
<small className="text-muted">{timestamp}</small> {marker}
|
||||
<small className="text-muted">{timestamp}{'\u00a0'}{marker}</small>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<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 (
|
||||
<div className="panel panel-default fullViewPort" id="messagesPanel">
|
||||
<div className="panel panel-default fullViewPort" id="messagesPanel" ref="messagesPanel">
|
||||
<div className="panel-heading" id="chatTitle">
|
||||
<h3 className="panel-title">{this.state.title}</h3>
|
||||
</div>
|
||||
@@ -654,7 +716,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
|
||||
</ul>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
@@ -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
|
||||
|
||||
<div id="sidepanel-wrapper">
|
||||
<SidepanelView
|
||||
state={this.state.sidePanel}
|
||||
title={this.state.sideTitle}
|
||||
state={this.state.sidePanelSelected}
|
||||
title={this.state.sidePanelTitle}
|
||||
subscriptions={this.state.subscriptions}
|
||||
loginDisabled={this.state.loginDisabled}
|
||||
errorText={this.state.errorText}
|
||||
onSignUp={this.handleSignUp}
|
||||
onSettings={this.handleSettings}
|
||||
onLogin={this.handleLogin}
|
||||
onLoginRequest={this.handleLoginRequest}
|
||||
onCreateAccount={this.handleNewAccount}
|
||||
onTopicSelected={this.handleTopicSelected}
|
||||
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="col-lg-12 fullHeight">
|
||||
{this.state.topicSelected ?
|
||||
<MessagesView topic={this.state.topicSelected} sendMessage={this.handleSendMessage} /> :
|
||||
<MessagesView topicSelected={this.state.topicSelected} onData={this.tnData}
|
||||
sendMessage={this.handleSendMessage} /> :
|
||||
<DummyMessagesView />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user