Files
im_webapp/index.html
T

683 lines
26 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">
var HOST = "api.tinode.co";
var API_KEY = "AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K";
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, submitDisabled: true});
this.props.onLogin(this.state.login, this.state.password);
},
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: ''};
},
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 != this.state.password2) {
}
this.props.onCreateAccount(this.state.login, this.state.password, this.state.public, this.state.private);
},
render: function() {
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="btn btn-primary" 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.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.onLogin(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() {
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>{this.props.public}</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.online && this.props.when) {
lastOnline = this.props.when;
} else {
lastOnline = 'online now';
}
return (
<span className="text-muted pull-right">
<span className={this.props.now ? "online" : "offline"}>&#x25cf;</span> <small className="text-muted">&nbsp;{lastOnline}</small>
</span>
);
}
})
/* List of contacts/topics */
var ContactList = React.createClass({
getInitialState: function() {
return {selected: false};
},
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];
return (
<Contact public={c.public} comment={c.private} unread={c.unread}
onSelected={this.onSelectionChange} topic={c.topic} key={c.topic} />
);
}, this);
}
return (
<div className="contactList">
<ul className="media-list">
{contactNodes}
</ul>
</div>
);
}
});
/* 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} />
</div>
</div>
);
}
});
/* END Contact list */
/* BEGIN Conversation panel */
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 Message = React.createClass({
getInitialState: function() {
return {received: 0};
},
render: function() {
var timestamp = shortDateFormat(this.props.timestamp);
return (
<li className={this.props.response ? 'chatBox-inverted' : null}>
<div className="chatBox-panel">
<div className="chatBox-body">
<p>{this.props.content}
<span className="pull-right timestamp">
<small className="text-muted">{timestamp}</small> <Message.ReceivedMarker />
</span>
</p>
</div>
</div>
</li>
);
}
});
/* Received/read indicator */
Message.ReceivedMarker = React.createClass({
getInitialState: function() {
return {received: 0};
},
render: function() {
var checkMark = null;
if (this.state.received > 0) {
checkMark = '&#x2714;';
if (this.state.received > 1) {
checkMark += <span style="margin-left:-0.5em;">&#x2714;</span>;
}
}
return (
<span>{checkMark}</span>
);
}
});
/* Send message form */
Message.Send = React.createClass({
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" />
<span className= "input-group-btn">
<button className= "btn btn-default" type = "button">
<i className="glyphicon glyphicon-send"></i>
</button>
</span>
</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>
);
}
});
var MessagesView = React.createClass({
getInitialState: function() {
return {messages: []};
},
componentDidMount: function() {
this.setState({messages: messagesList});
},
render: function() {
var messageNodes = this.state.messages.map(function(msg) {
return (
<Message content={msg.content} timestamp={msg.timestamp} response={msg.response} key={msg.id} />
);
});
return (
<div className="panel panel-default fullViewPort" id="messagesPanel">
<div className="panel-heading" id="chatTitle">
<h3 className="panel-title">{this.props.topic}</h3>
</div>
<div className="panel-body" id="messagesView">
<ul className="chatBox">
{messageNodes}
</ul>
</div>
<div className="panel-footer" id="sendMessagePanel">
<Message.Send />
</div>
</div>
);
}
});
/* END Conversation panel */
/* The top-level class to hold all fuinctionality together */
var AppView = React.createClass({
getInitialState: function() {
return {
sidePanel: 'login',
sideTitle: 'not set',
loggedIn: false,
errorText: '',
selectedTopicName: '',
loginDisabled: false,
subscriptions: {}
};
},
componentDidMount: function() {
Tinode.enableLogging(true);
// Setup with default transport.
Tinode.setup(HOST, API_KEY);
},
handleLogin: function(login, password) {
var instance = this;
this.setState({loginDisabled: true, errorText: ''});
Tinode.connect().then(function() {
return Tinode.loginBasic(login, password);
}).then(function() {
instance.setState({loggedIn: true});
var me = Tinode.getMeTopic();
me.onInfoChange = instance.handleMeInfoChange;
me.onSubsChange = instance.handleMeSubsChange;
// 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"});
}).catch(function(err) {
instance.setState({loginDisabled: false, errorText: err.message});
});
},
handleInfoChange: function(info) {
console.log(info);
this.setState({selectedTopicName: info.public});
},
handleMeInfoChange: function(info) {
this.setState({sideTitle: info.public});
},
handleSubsUpdated: function(uids) {
console.log(uids);
},
handleMeSubsChange: function(sub) {
var subs = this.state.subscriptions;
subs[sub.topic] = sub;
this.setState({subscriptions: subs});
},
handleNewAccount: function(login_, password_, public_, private_) {
},
handleNewMessage: function(msg) {
console.log(msg);
},
handleTopicSelected: function(topicName) {
var topic = Tinode.getTopic(topicName);
if (topic.isSubscribed()) {
this.handleInfoChange(topic);
} else {
topic.onData = this.handleNewMessage;
topic.onInfoChange = this.handleInfoChange;
topic.onSubsUpdated = this.handleSubsUpdated;
topic.subscribe({get: "info sub data"});
}
},
handleSignUp: function() {
this.setState({sidePanel: 'register'});
},
handleSettings: function() {
this.setState({sidePanel: 'settings'});
},
handleSidepanelCancel: function() {
if (this.state.loggedIn) {
this.setState({sidePanel: 'contacts'});
} else {
this.setState({sidePanel: 'login'});
}
},
render: function() {
return (
<div className="fullScreen">
<div id="sidepanel-wrapper">
<SidepanelView
state={this.state.sidePanel}
title={this.state.sideTitle}
subscriptions={this.state.subscriptions}
loginDisabled={this.state.loginDisabled}
errorText={this.state.errorText}
onSignUp={this.handleSignUp}
onSettings={this.handleSettings}
onLogin={this.handleLogin}
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.selectedTopicName ?
<MessagesView topic={this.state.selectedTopicName} /> :
<DummyMessagesView />}
</div>
</div>
</div>
</div>
</div>
);
}
})
ReactDOM.render(
<AppView />,
document.getElementById('mountPoint')
);
</script>
</body>
</html>