874 lines
33 KiB
HTML
874 lines
33 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Tinode example: chat web application in react.js</title>
|
|
<link rel="shortcut icon" href="img/logo32x32.png" />
|
|
<!-- Customized Bootstrap style (material design). -->
|
|
<link rel="stylesheet" href="css/bootstrap.css" />
|
|
<!-- Styles for local elements -->
|
|
<link rel="stylesheet" href="css/base.css" />
|
|
<!-- ReactJs scripts -->
|
|
<script src="https://fb.me/react-0.14.6.js"></script>
|
|
<script src="https://fb.me/react-dom-0.14.6.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
|
|
<!-- Some scripts to make bootstrap more dynamic -->
|
|
<!--script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script-->
|
|
<!--script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script-->
|
|
<script src="tinode-0.5.1.js"></script>
|
|
</head>
|
|
</head>
|
|
<body>
|
|
<div id="mountPoint"></div>
|
|
<script type="text/babel">
|
|
// Host name and posrt to connect to
|
|
var HOST = "api.tinode.co";
|
|
// 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_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) {
|
|
var now = new Date();
|
|
if (then.getFullYear() == now.getFullYear()) {
|
|
if (then.getMonth() == now.getMonth() && then.getDate() == now.getDate()) {
|
|
return then.toLocaleTimeString(undefined, {hour12: false, hour: '2-digit', minute: '2-digit'});
|
|
} else {
|
|
return then.toLocaleDateString(undefined,
|
|
{hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
|
|
}
|
|
}
|
|
return then.toLocaleDateString(undefined,
|
|
{hour12: false, year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
|
|
}
|
|
|
|
/* BEGIN Login: a login form */
|
|
var LoginView = React.createClass({
|
|
getInitialState: function() {
|
|
return {
|
|
login: '',
|
|
password: '',
|
|
errorCleared: false
|
|
};
|
|
},
|
|
handleLoginChange: function(e) {
|
|
if (!this.state.errorCleared) {
|
|
this.props.onClearError();
|
|
this.setState({errorCleared: true});
|
|
}
|
|
this.setState({login: e.target.value});
|
|
},
|
|
handlePasswordChange: function(e) {
|
|
if (!this.state.errorCleared) {
|
|
this.props.onClearError();
|
|
this.setState({errorCleared: true});
|
|
}
|
|
this.setState({password: e.target.value});
|
|
},
|
|
handleSubmit: function(e) {
|
|
e.preventDefault();
|
|
this.setState({errorCleared: false});
|
|
this.props.onLogin(this.state.login.trim(), this.state.password.trim());
|
|
},
|
|
render: function() {
|
|
var submitClassList = "btn btn-primary";
|
|
if (this.props.disabled) {
|
|
submitClassList += " disabled";
|
|
}
|
|
return (
|
|
<div className="container-fluid">
|
|
<form className="form-signin" onSubmit={this.handleSubmit}>
|
|
<label htmlFor="inputLogin" className="sr-only">Login</label>
|
|
<input type="text" id="inputLogin" className="form-control"
|
|
placeholder="Login (alice, bob, carol, dave, frank)"
|
|
value={this.state.login}
|
|
onChange={this.handleLoginChange}
|
|
required autofocus />
|
|
<label htmlFor="inputPassword" className="sr-only">Password</label>
|
|
<input type="password" id="inputPassword" className="form-control"
|
|
placeholder="Password (alice123, bob123, ...)"
|
|
value={this.state.password}
|
|
onChange={this.handlePasswordChange}
|
|
required />
|
|
<br />
|
|
<button className={submitClassList} type="submit">Sign in</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|
|
})
|
|
/* END Login */
|
|
|
|
/* BEGIN Account registration */
|
|
var CreateAccountView = React.createClass({
|
|
getInitialState: function() {
|
|
return {
|
|
login: '',
|
|
password: '',
|
|
password2: '',
|
|
public: '',
|
|
private: '',
|
|
errorCleared: false
|
|
};
|
|
},
|
|
handleLoginChange: function(e) {
|
|
this.setState({login: e.target.value});
|
|
},
|
|
handlePasswordChange: function(e) {
|
|
this.setState({password: e.target.value});
|
|
},
|
|
handlePassword2Change: function(e) {
|
|
this.setState({password2: e.target.value});
|
|
},
|
|
handlePublicChange: function(e) {
|
|
this.setState({public: e.target.value});
|
|
},
|
|
handlePrivateChange: function(e) {
|
|
this.setState({private: e.target.value});
|
|
},
|
|
handleSubmit: function(e) {
|
|
e.preventDefault();
|
|
if (this.state.password.trim() != this.state.password2.trim()) {
|
|
|
|
} else {
|
|
this.setState({errorCleared: false});
|
|
this.props.onCreateAccount(this.state.login.trim(), this.state.password.trim(),
|
|
this.state.public.trim(), this.state.private.trim());
|
|
}
|
|
},
|
|
render: function() {
|
|
var submitClassList = "btn btn-primary";
|
|
if (this.props.disabled) {
|
|
submitClassList += " disabled";
|
|
}
|
|
return (
|
|
<div className="container-fluid">
|
|
<form onSubmit={this.handleSubmit} onCancel={this.props.onCancel}>
|
|
<label htmlFor="signupLogin" className="sr-only">Login</label>
|
|
<input type="text" id="signupLogin" className="form-control" placeholder="Login"
|
|
value={this.state.login} onChange={this.handleLoginChange} required autofocus />
|
|
<label htmlFor="signupPassword" className="sr-only">Password</label>
|
|
<input type="password" id="signupPassword" className="form-control" placeholder="Password"
|
|
value={this.state.password} onChange={this.handlePasswordChange} required />
|
|
<label htmlFor="signupPassword2" className="sr-only">Repeat password</label>
|
|
<input type="password" id="signupPassword" className="form-control" placeholder="Repeat password"
|
|
value={this.state.password2} onChange={this.handlePassword2Change} required />
|
|
<label htmlFor="signupPublic" className="sr-only">Account public data</label>
|
|
<textarea id="signupPublic" className="form-control" rows="3" onChange={this.handlePublicChange}
|
|
value={this.state.public} placeholder="Public: text or valid JSON"></textarea>
|
|
<label htmlFor="signupPrivate" className="sr-only">Account private data</label>
|
|
<textarea id="signupPrivate" className="form-control" rows="3" onChange={this.handlePrivateChange}
|
|
value={this.state.private} placeholder="Private: text or valid JSON"></textarea>
|
|
<br />
|
|
<button className={submitClassList} type="submit">Sign up</button>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
/* END Account registration */
|
|
|
|
/* BEGIN Tinode config panel */
|
|
var SettingsView = React.createClass({
|
|
render: function() {
|
|
return (
|
|
<div className="container-fluid">
|
|
<h3>Not implemented</h3>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
/* END Tinode config panel */
|
|
|
|
/* BEGIN Manage side panel - handle Login, Account Registration, and Contacts views */
|
|
var SideNavbar = React.createClass({
|
|
render: function() {
|
|
var avatar = null;
|
|
if (this.props.avatar) {
|
|
var src = this.props.avatar === true ? "img/avatar.svg" : this.props.avatar;
|
|
avatar = <img id="self-avatar" className="avatar pull-left" alt="avatar" src={src} />
|
|
}
|
|
return (
|
|
<nav className="navbar navbar-inverse">
|
|
<div className="container-fluid">
|
|
<div className="navbar-header">
|
|
{avatar}
|
|
<span className="navbar-brand">{this.props.title}</span>
|
|
</div>
|
|
<div>
|
|
<ul className="nav navbar-nav navbar-right">
|
|
{this.props.state === 'login' ?
|
|
<SideNavbar.MenuStart onSignUp={this.props.onSignUp} onSettings={this.props.onSettings} /> :
|
|
this.props.state === 'settings' ?
|
|
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> :
|
|
this.props.state === 'register' ?
|
|
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> :
|
|
this.props.state === 'contacts' ?
|
|
<SideNavbar.MenuSettings onSettings={this.props.onSettings} /> : null}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|
|
})
|
|
/*
|
|
SideNavbar.MenuStart = React.createClass({
|
|
render: function() {
|
|
return (
|
|
<li className="dropdown">
|
|
<a href="#" className="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
|
|
aria-expanded="false">
|
|
<big><span className="glyphicon glyphicon-option-vertical"></span></big>
|
|
</a>
|
|
<ul className="dropdown-menu">
|
|
<li><a href="#" onClick={this.props.onSignUp}><span className="glyphicon glyphicon-user"></span> Sign Up</a></li>
|
|
<li><a href="#" onClick={this.props.onSettings}><span className="glyphicon glyphicon-cog"></span> Settings</a></li>
|
|
</ul>
|
|
</li>
|
|
);
|
|
}
|
|
});
|
|
*/
|
|
SideNavbar.MenuStart = React.createClass({
|
|
render: function() {
|
|
return (
|
|
<li>
|
|
<ul className="nav navbar-nav">
|
|
<li><a href="#"
|
|
onClick={this.props.onSignUp}><big><span className="glyphicon glyphicon-user"></span></big></a></li>
|
|
<li><a href="#"
|
|
onClick={this.props.onSettings}><big><span className="glyphicon glyphicon-cog"></span></big></a></li>
|
|
</ul>
|
|
</li>
|
|
);
|
|
}
|
|
});
|
|
|
|
SideNavbar.MenuCancel = React.createClass({
|
|
render: function() {
|
|
return (
|
|
<li><a href="#" onClick={this.props.onCancel}><big><span className="glyphicon glyphicon-remove"></span></big></a></li>
|
|
);
|
|
}
|
|
})
|
|
|
|
SideNavbar.MenuSettings = React.createClass({
|
|
render: function() {
|
|
return (
|
|
<li><a href="#" onClick={this.props.onSettings}><big><span className="glyphicon glyphicon-cog"></span></big></a></li>
|
|
);
|
|
}
|
|
})
|
|
|
|
var SidepanelView = React.createClass({
|
|
getInitialState: function() {
|
|
return {
|
|
showErrorText: true,
|
|
};
|
|
},
|
|
handleClearErrorText: function() {
|
|
this.setState({showErrorText: false});
|
|
},
|
|
handleLoginRequested: function(login, password) {
|
|
this.props.onLoginRequest(login, password);
|
|
this.setState({showErrorText: true});
|
|
},
|
|
render: function() {
|
|
var title = null;
|
|
var avatar = false;
|
|
switch (this.props.state) {
|
|
case 'login':
|
|
title = "Sign In"; break;
|
|
case 'register':
|
|
title = "Create Account"; break;
|
|
case 'settings':
|
|
title = "Settings"; break;
|
|
case 'contacts':
|
|
title = this.props.title;
|
|
avatar = true;
|
|
default:;
|
|
};
|
|
return (
|
|
<div className="sidepanel-nav">
|
|
<SideNavbar state={this.props.state}
|
|
title={title} avatar={avatar}
|
|
onSignUp={this.props.onSignUp}
|
|
onSettings={this.props.onSettings}
|
|
onCancel={this.props.onCancel} />
|
|
<SidepanelView.ErrorPanel showError={this.state.showErrorText} errorText={this.props.errorText} />
|
|
{this.props.state === 'login' ?
|
|
<LoginView disabled={this.props.loginDisabled}
|
|
onLogin={this.handleLoginRequested}
|
|
onClearError={this.handleClearErrorText} /> :
|
|
this.props.state === 'register' ?
|
|
<CreateAccountView onCreateAccount={this.props.onCreateAccount} onCancel={this.props.onCancel} /> :
|
|
this.props.state === 'settings' ?
|
|
<SettingsView onCancel={this.props.onCancel} /> :
|
|
this.props.state === 'contacts' ?
|
|
<ContactsView data={this.props.subscriptions} onTopicSelected={this.props.onTopicSelected} /> :
|
|
null}
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
SidepanelView.ErrorPanel = React.createClass({
|
|
render: function() {
|
|
var classList = "alert alert-block";
|
|
var errorText = null;
|
|
if (this.props.errorText && this.props.showError) {
|
|
classList += " alert-error";
|
|
errorText = this.props.errorText;
|
|
}
|
|
return (
|
|
<div className={classList}>{errorText}</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
/* END Side panel */
|
|
|
|
/* BEGIN Contact list (list of topics) */
|
|
|
|
/* A form for initiating a new topic */
|
|
var NewTopic = React.createClass({
|
|
render: function() {
|
|
return (
|
|
<div className="input-group" id="newTopicInput">
|
|
<label htmlFor="addTopic" className="sr-only">Add user or topic</label>
|
|
<input id="addTopic" className="form-control" type="text" placeholder="Add topic" />
|
|
<span className="input-group-btn"><button className="btn btn-default" type="button">
|
|
<i className="glyphicon glyphicon-plus-sign"></i>
|
|
</button></span>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
/*
|
|
var contactsData = [
|
|
{topic: "usrBOB", public: "Bob Smith", private: "This is another comment", online: {now: true}, unread: 0},
|
|
{topic: "usrCAROL", public: "Carol Xmas", private: "No comments", online: {now: false, when: "1 hour ago"}, unread: 2},
|
|
{topic: "usrDAVE", public: "Dave Goliaphsson", private: "This is a comment", online: {now: false, when: "30 min ago"}, unread: 0},
|
|
{topic: "usrBOB2", public: "Bob Smith", private: "This is another comment", online: {now: true}, unread: 0},
|
|
{topic: "usrCAROL2", public: "Carol Xmas", private: "No comments", online: {now: false, when: "1 hour ago"}, unread: 2},
|
|
{topic: "usrDAVE2", public: "Dave Goliaphsson", private: "This is a comment", online: {now: false, when: "30 min ago"}, unread: 0},
|
|
{topic: "usrBOB3", public: "Bob Smith", private: "This is another comment", online: {now: true}, unread: 0},
|
|
{topic: "usrCAROL3", public: "Carol Xmas", private: "No comments", online: {now: false, when: "1 hour ago"}, unread: 2},
|
|
{topic: "usrDAVE3", public: "Dave Goliaphsson", private: "This is a comment", online: {now: false, when: "30 min ago"}, unread: 0}
|
|
];
|
|
*/
|
|
|
|
/* A single topic */
|
|
var Contact = React.createClass({
|
|
handleClick: function() {
|
|
this.props.onSelected(this.props.topic);
|
|
},
|
|
render: function() {
|
|
var title = this.props.public;
|
|
if (title.length > 32) {
|
|
title = title.substring(0, 30) + "...";
|
|
}
|
|
return (
|
|
<li className="media contact" onClick={this.handleClick}>
|
|
<a href="#" className="pull-left"><img src="img/avatar.svg" className="avatar img-circle" alt="" /></a>
|
|
<div className="media-body">
|
|
<Contact.Online when={this.props.when} now={this.props.now} />
|
|
<strong>{title}</strong> <Contact.Unread count={this.props.unread} />
|
|
<p>{this.props.comment}</p>
|
|
</div>
|
|
</li>
|
|
);
|
|
}
|
|
});
|
|
|
|
/* The counter of unread messages in the topic */
|
|
Contact.Unread = React.createClass({
|
|
render: function() {
|
|
var showUnreadBadge = null;
|
|
if (this.props.count > 0) {
|
|
showUnreadBadge = <span className="label label-primary label-as-badge">{this.props.count}</span>;
|
|
}
|
|
return (
|
|
<span>{showUnreadBadge}</span>
|
|
);
|
|
}
|
|
});
|
|
|
|
/* Online/offline indicator */
|
|
Contact.Online = React.createClass({
|
|
render: function() {
|
|
var lastOnline = '';
|
|
if (this.props.now) {
|
|
lastOnline = "online now";
|
|
} else if (this.props.when) {
|
|
lastOnline = this.props.when;
|
|
} else {
|
|
lastOnline = "offline";
|
|
}
|
|
|
|
return (
|
|
<span className="text-muted pull-right">
|
|
<span className={this.props.now ? "online" : "offline"}>●</span> <small className="text-muted"> {lastOnline}</small>
|
|
</span>
|
|
);
|
|
}
|
|
});
|
|
|
|
/* ContactsView holds all contacts-related stuff */
|
|
var ContactsView = React.createClass({
|
|
getInitialState: function() {
|
|
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 = [];
|
|
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);
|
|
|
|
return (
|
|
<div className="contactBox">
|
|
<div className="container-fluid">
|
|
<NewTopic />
|
|
<div className="contactList">
|
|
<ul className="media-list">
|
|
{contactNodes}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
/* END Contact list */
|
|
|
|
/* BEGIN Conversation panel */
|
|
/* Don't delete it for now: maybe needed for debugging
|
|
var messagesList = [
|
|
{
|
|
id: 1, content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet tellus interdum. \
|
|
Integer sodales accumsan sem, ut malesuada velit egestas vel. Aenean consequat consectetur justo eget venenatis. Morbi \
|
|
eleifend fringilla urna euismod tristique. Nullam at magna vel mauris rutrum finibus. Duis eu dignissim magna. Donec \
|
|
lobortis suscipit vulputate. Cras luctus elit ex, vitae lobortis magna lobortis bibendum.",
|
|
timestamp: new Date(2015, 11, 31, 22, 23, 24, 567), response: false
|
|
},
|
|
{
|
|
id: 2, content: "Duis porttitor est nec diam lobortis, non aliquam risus aliquam. \
|
|
Nulla vitae est aliquam, egestas nunc dapibus, dapibus risus.\
|
|
Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. \
|
|
Nullam vitae condimentum lectus, quis maximus elit.\
|
|
\
|
|
Nunc ac nisi risus. Integer eget augue vel risus commodo blandit. Nulla faucibus convallis neque sed eleifend. Vestibulum\
|
|
dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac habitasse platea dictumst.",
|
|
timestamp: new Date(2016, 0, 1, 2, 3, 4, 567),
|
|
response: true
|
|
},
|
|
{id: 3, content: "Ooga chaka ooga ooga!", timestamp: new Date(2016, 0, 2, 16, 17, 18, 567), response: false},
|
|
{id: 4, content: "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. \
|
|
Nullam vitae condimentum lectus, quis maximus elit.", timestamp: new Date(2016, 0, 3, 1, 2, 3, 567), response: true},
|
|
{id: 5, content: "Integer sodales accumsan sem, ut malesuada velit egestas vel. Aenean consequat consectetur justo eget venenatis.",
|
|
timestamp: new Date(2016, 0, 3, 4, 5, 6, 567), response: true},
|
|
{id: 6, content: "No way.", timestamp: new Date(2016, 0, 3, 15, 16, 17, 567), response: false},
|
|
{id: 7, content: "OK!", timestamp: new Date(2016, 0, 3, 18, 19, 20, 567), response: true}
|
|
];
|
|
*/
|
|
var Received = {
|
|
UNDEF: -2,
|
|
SENDING: -1,
|
|
SERVER: 0,
|
|
CLIENT: 1,
|
|
READ: 2
|
|
};
|
|
var Message = React.createClass({
|
|
render: function() {
|
|
return (
|
|
<li className={this.props.response ? 'chatBox-inverted' : null}>
|
|
<div className="chatBox-panel">
|
|
<div className="chatBox-body">
|
|
<p>{this.props.content}
|
|
<Message.ReceivedMarker response={this.props.response}
|
|
timestamp={this.props.timestamp} received={this.props.received} />
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
);
|
|
}
|
|
});
|
|
|
|
/* Received/read indicator */
|
|
Message.ReceivedMarker = React.createClass({
|
|
render: function() {
|
|
var timestamp = (this.props.received === Received.SENDING) ?
|
|
"sending ..." :
|
|
shortDateFormat(this.props.timestamp);
|
|
var marker = null;
|
|
|
|
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}{'\u00a0'}{marker}</small>
|
|
</span>
|
|
);
|
|
}
|
|
});
|
|
|
|
/* Send message form */
|
|
Message.Send = React.createClass({
|
|
getInitialState:function() {
|
|
return {
|
|
message: '',
|
|
// Make initial keypress time as if it happened 5001 milliseconds in the past.
|
|
keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1
|
|
};
|
|
},
|
|
handleSend: function(e) {
|
|
var message = this.state.message.trim();
|
|
if (message) {
|
|
this.props.sendMessage(this.state.message.trim());
|
|
this.setState({message: ''});
|
|
}
|
|
},
|
|
handleMessageTyping: function(e) {
|
|
var newState = {message: e.target.value};
|
|
var now = new Date().getTime();
|
|
if (now - this.state.keypressTimestamp > KEYPRESS_DELAY) {
|
|
var topic = Tinode.getTopic(this.props.topic);
|
|
if (topic.isSubscribed()) {
|
|
topic.noteKeyPress();
|
|
}
|
|
newState.keypressTimestamp = now;
|
|
}
|
|
this.setState(newState);
|
|
},
|
|
render: function() {
|
|
return (
|
|
<div className="input-group">
|
|
<label htmlFor="sendMessage" className="sr-only">Add topic</label>
|
|
<input id="sendMessage" className="form-control" type="text" placeholder="New message"
|
|
value={this.state.message} onChange={this.handleMessageTyping} />
|
|
<span className= "input-group-btn">
|
|
<button className= "btn btn-default" type="button" onClick={this.handleSend}>
|
|
<i className="glyphicon glyphicon-send"></i>
|
|
</button>
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
var MessagesView = React.createClass({
|
|
getInitialState: function() {
|
|
return {
|
|
messages: [],
|
|
topic: '',
|
|
title: ''
|
|
};
|
|
},
|
|
// 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) {
|
|
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.onMetaInfo = this.handleInfoChange;
|
|
topic.onSubsUpdated = this.handleSubsUpdated;
|
|
// 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});
|
|
}
|
|
},
|
|
handleInfoChange: function(info) {
|
|
console.log("handleInfoChange: " + info);
|
|
this.setState({title: info.public});
|
|
},
|
|
handleSubsUpdated: function(uids) {
|
|
|
|
},
|
|
handleNewMessage: function(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}
|
|
received={msgReceived} key={msg.seq} />
|
|
);
|
|
});
|
|
return (
|
|
<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>
|
|
<div className="panel-body" id="messagesView">
|
|
<ul className="chatBox">
|
|
{messageNodes}
|
|
</ul>
|
|
</div>
|
|
<div className="panel-footer" id="sendMessagePanel">
|
|
<Message.Send topic={this.props.topicSelected} sendMessage={this.props.sendMessage} />
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
/* This is just a static page to show when no conversation is selected */
|
|
var DummyMessagesView = React.createClass({
|
|
render: function() {
|
|
return (
|
|
<div className="panel panel-default fullHeight">
|
|
<div className="panel-body text-center center-block fullHeight" id="dummyView">
|
|
<div><a href="https://github.com/tinode/chat/"><img id="logo" alt="logo" src="img/logo.svg" />
|
|
<h3>Tinode Demo Chat</h3></a></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
/* END Conversation panel */
|
|
|
|
/* The top-level class to hold all fuinctionality together */
|
|
var AppView = React.createClass({
|
|
getInitialState: function() {
|
|
return {
|
|
sidePanelSelected: 'login',
|
|
sidePanelTitle: 'not set',
|
|
loggedIn: false,
|
|
errorText: '',
|
|
topicSelected: '',
|
|
loginDisabled: false,
|
|
subscriptions: {}
|
|
};
|
|
},
|
|
componentDidMount: function() {
|
|
Tinode.enableLogging(true);
|
|
Tinode.onDisconnect = this.handleDicsonnect;
|
|
// Setup with the default transport, usually websocket.
|
|
Tinode.setup(HOST, API_KEY);
|
|
},
|
|
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() {
|
|
// Logged in fine, subscribe to 'me' attaching callbacks from the contacts view.
|
|
var me = Tinode.getMeTopic();
|
|
me.onMetaInfo = instance.tnMeMetaInfo;
|
|
instance.setState({sidePanelSelected: "contacts", loggedIn: true});
|
|
// Subscribe, fetch topic info and the list of subscriptions.
|
|
me.subscribe({get: "info sub data"});
|
|
}).catch(function(err) {
|
|
// Login failed, report error
|
|
instance.setState({loginDisabled: false, errorText: err.message});
|
|
});
|
|
},
|
|
tnMeMetaInfo: function(info) {
|
|
this.setState({sidePanelTitle: info.public});
|
|
},
|
|
// 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(){
|
|
return Tinode.createUserBasic(login_, password_,
|
|
{login: "basic", public: public_, private: private_});
|
|
}).catch(function(err) {
|
|
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({sidePanelSelected: 'register'});
|
|
},
|
|
// User chose Settings menu item.
|
|
handleSettings: function() {
|
|
this.setState({sidePanelSelected: 'settings'});
|
|
},
|
|
// User clicked Cancel button in Setting or Sign Up panel.
|
|
handleSidepanelCancel: function() {
|
|
if (this.state.loggedIn) {
|
|
this.setState({sidePanelSelected: 'contacts'});
|
|
} else {
|
|
this.setState({sidePanelSelected: 'login'});
|
|
}
|
|
},
|
|
render: function() {
|
|
return (
|
|
<div className="fullScreen">
|
|
|
|
<div id="sidepanel-wrapper">
|
|
<SidepanelView
|
|
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}
|
|
onLoginRequest={this.handleLoginRequest}
|
|
onCreateAccount={this.handleNewAccount}
|
|
onTopicSelected={this.handleTopicSelected}
|
|
onCancel={this.handleSidepanelCancel} />
|
|
</div>
|
|
|
|
<div id="page-content-wrapper">
|
|
<div className="container-fluid fullHeight">
|
|
<div className="row fullHeight">
|
|
<div className="col-lg-12 fullHeight">
|
|
{this.state.topicSelected ?
|
|
<MessagesView topicSelected={this.state.topicSelected} onData={this.tnData}
|
|
sendMessage={this.handleSendMessage} /> :
|
|
<DummyMessagesView />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
);
|
|
}
|
|
})
|
|
|
|
ReactDOM.render(
|
|
<AppView />,
|
|
document.getElementById('mountPoint')
|
|
);
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|