adding avatar support

This commit is contained in:
Gene S
2016-01-29 19:23:02 -08:00
parent 9a855a7288
commit d1265c21ee
5 changed files with 425 additions and 135 deletions
+3 -3
View File
@@ -5,12 +5,12 @@ Example using Tinode to build a single-page chat application. The app uses boots
and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web). Tinode javascript library has no
external dependencies.
This is work in progress. Bugs are expected. The work was tested in the latest Chrome only.
This is work in progress. Bugs are expected. The work was tested in the latest Chrome & Firefox only.
Try it at http://api.tinode.co/x/example-react-js/
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
Avatar photos from [https://www.pexels.com/] under [CC0 license](https://www.pexels.com/photo-license/).
Photos from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
Background pattern from [http://subtlepatterns.com/], commercial and non-commercial use allowed with attribution.
Background pattern from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution.
+197 -55
View File
@@ -56,7 +56,6 @@ button {
border-radius: 2px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.25);
text-transform: uppercase;
}
button[disabled],
@@ -71,18 +70,29 @@ button.round {
width: 3rem;
height: 3rem;
padding: 0;
line-height: 1;
margin: 0;
line-height: 3.5rem;
outline: none; /* otherwise a square outline is shown */
box-shadow: 0 2px 0.5rem 0 rgba(0, 0, 0, 0.25);
z-index: 2;
}
button.round.small {
width: 2rem;
height: 2rem;
line-height: 2rem;
box-shadow: 0 2px 0.35rem 0 rgba(0, 0, 0, 0.25);
}
button:focus, button:hover, button:active {
color: #fff;
}
button:active {
box-shadow: inset 0 3px 0.5rem rgba(0, 0, 0, 0.125);
}
button:active.round.small {
box-shadow: inset 0 2px 0.35rem rgba(0, 0, 0, 0.125);
}
button.blue {
background-color: #1e88e5; /* blue 600 */
@@ -92,27 +102,44 @@ button.blue:focus, button.blue:hover, button.blue:active {
background-color: #1976d2;
}
button.blue {
background-color: #1e88e5; /* blue 600 */
color: white;
}
button.blue:focus, button.blue:hover, button.blue:active {
background-color: #1976d2;
}
button.white {
background-color: #FFF;
color: #2196f3; /* blue 500 */
}
button.white:focus, button.white:hover, button.blue:active {
background-color: #e3f2fd;
}
textarea, input {
font-family: inherit;
font-size: 1.05rem;
background: transparent;
border: none;
border-bottom: 1px solid #ccc;
border-radius: 0.2em;
border-radius: 0.2rem;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
width: 100%;
margin: 0.25em 0;
padding: 0 0.125em;
margin: 0.25rem 0;
padding: 0 0.125rem;
outline: none;
transition: all 0.2s ease-in-out;
box-sizing: border-box;
color: #666;
}
textarea:focus,input:focus {
textarea:focus, input:focus {
border-bottom: 1px solid #2196f3;
box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25);
}
input {
height: 2.5em;
height: 2.5rem;
}
textarea {
resize: none;
@@ -134,7 +161,7 @@ textarea {
/* END of input and buttons */
form {
padding: 0.5em 1em;
padding: 0.5em 0.75em;
line-height: 1.75;
font-size: 1.2rem;
}
@@ -191,7 +218,6 @@ form {
#side-caption-panel {
display: flex;
align-items: center;
x-justify-content: space-between;
flex: none;
padding: 0.5em;
height: 4em;
@@ -222,6 +248,7 @@ form {
width: 3rem;
height: auto;
flex: none;
border-radius: 33%;
}
/* Area which displays an error message */
@@ -239,6 +266,7 @@ form {
#contacts-panel {
overflow-y: auto;
margin-bottom: auto;
height: 100%;
}
#contact-box {
@@ -264,7 +292,7 @@ form {
/* Avatar and online-offline indicator */
#contact-box .avatar-box {
margin-right: 0.5rem;
margin-right: 0.35rem;
position: relative;
width: 3rem;
height: 3rem;
@@ -274,6 +302,7 @@ form {
#contact-box .avatar {
width: 3rem;
height: auto;
border-radius: 33%;
}
/* Online-offline indicator positioned inside avatar-box */
@@ -281,8 +310,8 @@ form {
font-size: 1.5em;
line-height: 1;
position: absolute;
right: 0;
bottom: 0;
right: -0.1rem;
bottom: -0.1rem;
}
#contact-box .online {
color: #4caf50;
@@ -298,6 +327,90 @@ form {
}
/* END of avatar box */
/* New account panel (signup form) */
#signup-form {
display: flex;
flex-direction: column;
}
#signup-form-one {
display: flex;
justify-content: space-between;
align-items: center;
}
/* Styling of avatar upload area in the New Account panel */
.avatar-upload {
position: relative;
text-align: center;
vertical-align: middle;
width: 4.5rem;
height: auto;
box-sizing: border-box;
margin: 0 0.5rem;
padding: 0;
}
.avatar-upload .blank {
border: 1px dashed #999;
border-radius: 0.75rem;
font-size: 1rem;
width: 4.5rem;
height: 4.5rem;
overflow: hidden;
color: #999;
vertical-align: middle;
line-height: 4.5rem;
box-sizing: border-box;
margin: 0;
padding: 0;
}
.avatar-upload .preview {
border-radius: 0.75rem;
width: 4.5rem;
height: 4.5rem;
overflow: hidden;
box-sizing: border-box;
margin: 0;
padding: 0;
}
.avatar-upload .inputfile.hidden {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
}
.avatar-upload .inputfile + label.round {
display: inline-block;
vertical-align: middle;
text-align: center;
cursor: pointer;
width: 2.5rem;
height: 2.5rem;
line-height: 3rem;
border-radius: 50%;
box-shadow: 0 1px 4px rgba(0,0,0,0.4);
position: absolute;
top: 3.875rem;
left: 1rem;
background-color: #fafafa;
}
.avatar-upload .inputfile:focus + label.round {
outline: none;
}
.avatar-upload .inputfile:focus + label,
.avatar-upload .inputfile + label:hover {
background-color: #e3f2fd;
color: #42a5f5;
}
/* END of avatar upload */
/* END of new account panel */
/* END of sidepanel styles */
/* Topic/messages view */
@@ -370,16 +483,16 @@ form {
.chat-box .bubble {
position: relative;
max-width: 32em;
min-width: 16em;
max-width: 40rem;
min-width: 12rem;
height: auto;
padding: 0.75em 0.5em 0.75em 1.25em;
padding: 0.75rem 0.5rem 0.75rem 1.25rem;
margin: 0;
text-align: left;
border-radius: 0.5em;
-webkit-border-radius: 0.5em;
-moz-border-radius: 0.5em;
border-radius: 0.5rem;
-webkit-border-radius: 0.5rem;
-moz-border-radius: 0.5rem;
/* tested in chrome 47 on mac - performance sucks
-webkit-filter: drop-shadow(0px 1px 1px -1px rgba(0,0,0,0.3));
@@ -388,23 +501,23 @@ form {
}
.chat-box li.single {
margin-top: 0.75em;
margin-bottom: 0.75em;
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.chat-box li.first {
margin-top: 0.75em;
margin-bottom: 0.15em;
margin-top: 0.75rem;
margin-bottom: 0.15rem;
}
.chat-box li.middle {
margin-top: 0.15em;
margin-bottom: 0.15em;
margin-top: 0.15rem;
margin-bottom: 0.15rem;
}
.chat-box li.last {
margin-top: 0.15em;
margin-bottom: 0.75em;
margin-top: 0.15rem;
margin-bottom: 0.75rem;
}
/* Left triangle placed bottom left flush. */
@@ -428,6 +541,8 @@ form {
.chat-box li.right .tip::before {
content: '';
position: absolute;
display: flex;
justify-content: space-between;
width: 0;
height: 0;
left: auto;
@@ -441,49 +556,44 @@ form {
border-left-color: transparent;
}
.chat-box .avatar {
width: 40px;
.chat-box .avatar-box {
width: 3rem;
text-align: center;
}
.chat-box .avatar img {
width: 32px;
.chat-box .avatar-box img {
width: 2.4rem;
display: block;
position: relative;
left: 4px;
border-radius: 50%;
left: 0.3rem;
border-radius: 33%;
}
.chat-box .bubble p {
display: flex;
justify-content: space-between;
margin: 0;
}
.chat-box .timestamp {
display: inline-block;
font-size: 80%;
color: #777;
padding-left: 1.5em;
padding-top: 0.5em;
white-space: nowrap;
}
/* Typing notification bubble */
.chat-box .typing {
display: inline-block;
border-radius: 0.5em;
width: 2em;
padding: 0.25em 0.35em;
border-radius: 0.5rem;
width: 2rem;
padding: 0.25rem 0.35rem;
vertical-align: middle;
text-align: center;
color: #aaa;
}
.chat-box li.single .typing {
margin-top: 0.75em;
margin-bottom: 0.75em;
}
.chat-box li.last .typing {
margin-top: 0.15em;
margin-bottom: 0.75em;
}
.chat-box .timestamp {
font-size: 80%;
color: #777;
padding-left: 1.5em;
padding-top: 0.5em;
margin-top: 0.75rem;
}
/* END of talk bubbles */
@@ -491,7 +601,7 @@ form {
/* Modal dialog */
.overlay {
.dialog-overlay {
position: absolute;
display: flex;
top: 0;
@@ -499,12 +609,44 @@ form {
width: 100%;
height: 100%;
z-index: 10;
background-color: rgba(0,0,0,0.5);
background-color: rgba(0,0,0,0.25);
}
.modal-dialog {
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: white;
min-width: 24rem;
min-height: 16rem;
margin: auto;
border-radius: 0.3rem;
box-shadow: 0 0.5rem 1rem 0 rgba(0,0,0, 0.25);
}
.modal-dialog .title-panel {
display: flex;
justify-content: space-between;
font-size: 1.4rem;
padding: 1rem;
}
.modal-dialog .body-panel {
margin: auto 0.5rem auto 0.5rem;
}
.modal-dialog .dialog-buttons {
border-top: 1px solid #f5f5f5;
background-color: #fafafa
}
.modal-dialog .dialog-buttons a {
display: inline-block;
text-transform: uppercase;
text-decoration: none;
font-size: 1.1rem;
padding: 0.35rem 1.5rem;
}
/* END of modal dialog */
/* Miscellaneous */
@@ -518,7 +660,7 @@ form {
color: white;
border-radius: 1em;
padding: 0.25em;
min-width: 1.5em;
min-width: 1.75em;
text-align: center;
}
+12 -20
View File
@@ -17,10 +17,7 @@
width="192"
height="192"
viewBox="0 0 192 192"
sodipodi:docname="Tinode logo-round-colored-tilted.svg"
inkscape:export-filename="/home/gsokolov/work/icons/logo32x32.png"
inkscape:export-xdpi="15.789473"
inkscape:export-ydpi="15.789473"><metadata
sodipodi:docname="Tinode logo-round-colored.svg"><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
@@ -346,19 +343,19 @@
guidetolerance="10000"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1370"
inkscape:window-height="782"
inkscape:window-width="1855"
inkscape:window-height="1176"
id="namedview4"
showgrid="true"
showguides="false"
inkscape:snap-perpendicular="true"
inkscape:snap-tangential="true"
inkscape:zoom="2.7812866"
inkscape:cx="96.95464"
inkscape:cx="97.314186"
inkscape:cy="97.415892"
inkscape:window-x="290"
inkscape:window-y="245"
inkscape:window-maximized="0"
inkscape:window-x="65"
inkscape:window-y="24"
inkscape:window-maximized="1"
inkscape:current-layer="g10"
fit-margin-top="0"
fit-margin-left="0"
@@ -532,7 +529,11 @@
height="24"
width="128"
id="rect4982"
style="fill:#4caf50;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><rect
style="fill:#4caf50;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><path
style="fill:#37474f;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 71.095703 70.748047 L 71.095703 104.68945 L 123.41602 157.00977 C 128.11686 161.71061 135.68393 161.71061 140.38477 157.00977 C 145.08561 152.30893 145.08561 144.73991 140.38477 140.03906 L 71.095703 70.748047 z "
transform="translate(31.999996,24.000002)"
id="rect4982-6-9-1" /><rect
rx="12"
transform="matrix(0,1,-1,0,0,0)"
ry="12"
@@ -550,13 +551,4 @@
height="23.999969"
width="92"
id="rect4982-6-9"
style="fill:#37474f;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /><rect
rx="11.762711"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
ry="11.999985"
y="-6.3071899"
x="173.76053"
height="23.999969"
width="76"
id="rect4982-6-9-8"
style="fill:#37474f;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /></g></svg>

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 119 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

+213 -57
View File
@@ -27,9 +27,11 @@
<body>
<div id="mountPoint"></div>
<script type="text/babel">
// Name of this application, used in the User-Agent
var APP_NAME = "TinodeReact/1.0";
// Host name and posrt to connect to
var HOST = "api.tinode.co";
//var HOST = "localhost:6060";
// 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";
@@ -58,6 +60,11 @@
{hour12: false, year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
}
// Make a data URL from vcard.photo
function makeImageUrl(photo) {
return photo ? 'data:image/' + photo.type + ';base64,' + photo.data : null;
}
/* BEGIN Login: a login form */
var LoginView = React.createClass({
getInitialState: function() {
@@ -115,14 +122,15 @@
/* END Login */
/* BEGIN Account registration */
var CreateAccountView = React.createClass({
getInitialState: function() {
return {
login: '',
password: '',
password2: '',
public: '',
private: '',
fn: '', // full/formatted name
imageDataUrl: null,
errorCleared: false
};
},
@@ -135,11 +143,11 @@
handlePassword2Change: function(e) {
this.setState({password2: e.target.value});
},
handlePublicChange: function(e) {
this.setState({public: e.target.value});
handleFnChange: function(e) {
this.setState({fn: e.target.value});
},
handlePrivateChange: function(e) {
this.setState({private: e.target.value});
handleImageChanged: function(img) {
this.setState({imageDataUrl: img});
},
handleSubmit: function(e) {
e.preventDefault();
@@ -147,8 +155,20 @@
} else {
this.setState({errorCleared: false});
this.props.onCreateAccount(this.state.login.trim(), this.state.password.trim(),
this.state.public.trim(), this.state.private.trim());
var vcard = null;
if (this.state.name.trim() || this.state.imageDataUrl) {
vcard = {
fn: this.state.name.trim(),
};
if (this.state.imageDataUrl) {
dataStart = this.state.imageDataUrl.indexOf(",");
vcard.photo = {
data: this.state.imageDataUrl.substring(dataStart),
type: "jpg"
};
}
}
this.props.onCreateAccount(this.state.login.trim(), this.state.password.trim(), vcard);
}
},
render: function() {
@@ -159,16 +179,19 @@
return (
<div>
<form id="signup-form" onSubmit={this.handleSubmit} onCancel={this.props.onCancel}>
<input type="text" placeholder="Login"
value={this.state.login} onChange={this.handleLoginChange} required autofocus />
<input type="password" placeholder="Password"
value={this.state.password} onChange={this.handlePasswordChange} required />
<input type="password" placeholder="Repeat password"
value={this.state.password2} onChange={this.handlePassword2Change} required />
<textarea rows="3" onChange={this.handlePublicChange}
value={this.state.public} placeholder="Public: text or valid JSON"></textarea>
<textarea rows="3" onChange={this.handlePrivateChange}
value={this.state.private} placeholder="Private: text or valid JSON"></textarea>
<div id="signup-form-one">
<div>
<input type="text" placeholder="Login"
value={this.state.login} onChange={this.handleLoginChange} required autofocus />
<input type="password" placeholder="Password"
value={this.state.password} onChange={this.handlePasswordChange} required />
<input type="password" placeholder="Repeat password"
value={this.state.password2} onChange={this.handlePassword2Change} required />
</div>
<CreateAccountView.AvatarUpload onImageChanged={this.handleImageChanged} />
</div>
<input type="text" placeholder="Full name, e.g. John Doe"
value={this.state.fn} onChange={this.handleFnChange} required />
<div className="dialog-buttons">
<button className={submitClassList} type="submit">Sign up</button>
</div>
@@ -177,6 +200,54 @@
);
}
});
CreateAccountView.AvatarUpload = React.createClass({
getInitialState: function() {
return {
dataUrl: null
};
},
handleFileUpload(e) {
console.log("handleFileUpload");
var instance = this;
var img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = function() {
var canvas = document.createElement('CANVAS');
canvas.width = canvas.height = 64;
var ctx = canvas.getContext('2d');
var sx = 0, sy = 0, size;
if (this.height > this.width) {
size = this.width;
sy = (this.height - size) / 2;
} else {
size = this.height;
sx = (this.width - size) / 2;
}
ctx.imageSmoothingEnabled = true;
ctx.drawImage(this, sx, sy, size, size, 0, 0, 64, 64);
instance.setState({dataUrl: canvas.toDataURL("image/jpg")});
canvas = null;
console.log("handleFileUpload.onLoad: done");
};
console.log(e);
img.src = URL.createObjectURL(e.target.files[0]);
},
render: function() {
return (
<div className="avatar-upload">
{this.state.dataUrl ?
<img src={this.state.dataUrl} className="preview" /> :
<div className="blank">48&times;48</div>}
<input type="file" id="file-input-avatar" className="inputfile hidden"
accept="image/*" onChange={this.handleFileUpload} />
<label htmlFor="file-input-avatar" className="round">
<i className="material-icons">file_upload</i>
</label>
</div>
);
}
});
/* END Account registration */
/* BEGIN Tinode config panel */
@@ -270,7 +341,7 @@
title = "Settings"; break;
case 'contacts':
title = this.props.title;
avatar = true;
avatar = this.props.avatar;
default:;
};
return (
@@ -280,17 +351,29 @@
onSignUp={this.props.onSignUp}
onSettings={this.props.onSettings}
onCancel={this.props.onCancel} />
<SidepanelView.ErrorPanel showError={this.state.showErrorText} errorText={this.props.errorText} />
<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} /> :
<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} /> :
<ContactsView
data={this.props.subscriptions}
onTopicSelected={this.props.onTopicSelected}
onNewTopic={this.props.onNewTopic} /> :
null}
</div>
);
@@ -331,33 +414,22 @@
});
*/
/*
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) + "...";
var title = this.props.title;
if (title.length > 30) {
title = title.substring(0, 28) + "...";
}
var selected = (this.props.selected === this.props.topic) ? "selected" : null;
var avatar = (Tinode.getTopicType(this.props.topic) === "grp") ? <i className="material-icons">group</i> :
<i className="material-icons">person</i>;
var avatar = this.props.avatar ?
<img src={this.props.avatar} className="avatar" /> :
(Tinode.getTopicType(this.props.topic) === "grp") ?
<i className="material-icons">group</i> :
<i className="material-icons">person</i>;
return (
<li className={selected} onClick={this.handleClick}>
<div className="avatar-box">
@@ -402,9 +474,12 @@
me.onSubsUpdated = this.tnMeSubsUpdated;
},
tnMeContactUpdate: function(what, cont) {
console.log("changed: " + what + " in :");
console.log(cont);
this.forceUpdate();
if (what === "on" || what === "off" || what === "read") {
this.forceUpdate();
} else {
console.log("changed (not updating for it): " + what + " in :");
console.log(cont);
}
},
tnInvite: function(invite) {
console.log(invite);
@@ -430,8 +505,15 @@
unread -= c.read;
}
contactNodes.push(
<Contact public={c.public} comment={c.private} unread={unread} now={c.online} selected={this.state.selected}
onSelected={this.onSelectionChange} topic={c.topic} key={c.topic} />
<Contact
title={c.public.vcard.fn}
avatar={makeImageUrl(c.public.vcard.photo)}
comment={c.private}
unread={unread} now={c.online}
selected={this.state.selected}
onSelected={this.onSelectionChange}
topic={c.topic}
key={c.topic} />
);
}, this);
@@ -440,7 +522,9 @@
<ul id="contact-box">
{contactNodes}
</ul>
<button className="round blue" id="add-topic"><i className="material-icons big">add</i></button>
<button className="round blue" id="add-topic" onClick={this.props.onNewTopic}>
<i className="material-icons big">add</i>
</button>
</div>
);
}
@@ -498,7 +582,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
<div className={bubbleClass} style={colors}>
<p>{this.props.content}
<Message.ReceivedMarker response={this.props.response}
timestamp={this.props.timestamp} received={this.props.received} />
timestamp={this.props.timestamp} received={this.props.received} />
</p>
</div>
</li>
@@ -554,17 +638,19 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
this.propsChange(nextProps);
},
propsChange: function(props) {
if (props.topicSelected != this.state.topic) {
if (props.topicSelected && (props.topicSelected != this.state.topic)) {
var msgs = [];
var topic = Tinode.getTopic(props.topicSelected);
// Bind the new topic to component.
topic.onData = this.handleNewMessage;
topic.onInfo = this.handleInfoReceipt;
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.onInfo = undefined;
oldTopic.onMetaInfo = undefined;
oldTopic.onSubsUpdated = undefined;
}
@@ -586,7 +672,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
}
},
handleInfoChange: function(info) {
this.setState({title: info.public});
this.setState({title: info.public.vcard.fn});
},
handleSubsUpdated: function(uids) {
@@ -600,6 +686,20 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
}, READ_DELAY);
this.props.onData(msg);
},
handleInfoReceipt: function(info) {
switch (info.what) {
case "kp":
console.log("show typing notification");
break;
case "read":
case "recv":
// Redraw due to changed recv/read status.
this.forceUpdate();
break;
default:
console.log("other change in topic: " + info.what);
}
},
render: function() {
var component = null;
if (this.state.topic) {
@@ -625,9 +725,9 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
var isReply = !(msg.from === myUid);
var msgReceived = Received.UNDEF;
if (!isReply) {
if (topic.msgReadCount(msg.seq) > 1) {
if (topic.msgReadCount(msg.seq) > 0) {
msgReceived = Received.READ;
} else if (topic.msgRecvCount(msg.seq) > 1) {
} else if (topic.msgRecvCount(msg.seq) > 0) {
msgReceived = Received.CLIENT;
} else {
msgReceived = Received.SERVER;
@@ -739,15 +839,54 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
);
}
});
/* END Conversation panel */
/* This is a div which covers the entire AppView and shows selected modal dialog box */
var ModalDialogWrapper = React.createClass({
render: function() {
var dialog = null;
switch (this.props.dialog) {
case "new-topic":
dialog = <ModalDialogWrapper.NewTopic
onOk={this.props.onOk}
onCancel={this.props.onCancel} />;
break;
default:
break;
}
if (dialog) {
dialog = <div className="dialog-overlay">{dialog}</div>;
}
return dialog;
}
});
ModalDialogWrapper.NewTopic = React.createClass({
render: function() {
return (
<div className="modal-dialog">
<div className="title-panel">
New topic
</div>
<div className="body-panel">Not implemented</div>
<div className="dialog-buttons">
<a href="#" onClick={this.props.onOk}>Yes</a>
<a href="#" onClick={this.props.onCancel}>OK</a>
</div>
</div>
);
}
});
/* End of modal dialog wrapper */
/* The top-level class to hold all fuinctionality together */
var AppView = React.createClass({
getInitialState: function() {
return {
sidePanelSelected: 'login',
sidePanelTitle: null,
sidePanelAvatar: null,
dialogSelected: null,
loggedIn: false,
errorText: '',
topicSelected: '',
@@ -759,12 +898,14 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
Tinode.enableLogging(true);
Tinode.onDisconnect = this.handleDicsonnect;
// Setup with the default transport, usually websocket.
Tinode.setup(HOST, API_KEY);
Tinode.setup(APP_NAME, HOST, API_KEY);
},
handleDicsonnect: function() {
this.setState({
sidePanelSelected: 'login',
sidePanelTitle: null,
sidePanelAvatar: null,
dialogSelected: null,
loggedIn: false,
errorText: 'disconnected',
topicSelected: '',
@@ -792,7 +933,10 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
});
},
tnMeMetaInfo: function(info) {
this.setState({sidePanelTitle: info.public});
this.setState({
sidePanelTitle: info.public.vcard.fn,
sidePanelAvatar: makeImageUrl(info.public.vcard.photo)
});
},
// Sending "received" notifications
tnData: function(data) {
@@ -837,13 +981,24 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
this.setState({sidePanelSelected: 'login'});
}
},
handleNewTopic: function() {
this.setState({dialogSelected: 'new-topic'});
},
handleDialogCancel: function() {
this.setState({dialogSelected: null});
},
render: function() {
return (
<div id="app-container">
<ModalDialogWrapper
dialog={this.state.dialogSelected}
onOk={this.handleDialogCancel}
onCancel={this.handleDialogCancel} />
<SidepanelView
state={this.state.sidePanelSelected}
title={this.state.sidePanelTitle}
avatar={this.state.sidePanelAvatar}
subscriptions={this.state.subscriptions}
loginDisabled={this.state.loginDisabled}
errorText={this.state.errorText}
@@ -852,6 +1007,7 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
onLoginRequest={this.handleLoginRequest}
onCreateAccount={this.handleNewAccount}
onTopicSelected={this.handleTopicSelected}
onNewTopic={this.handleNewTopic}
onCancel={this.handleSidepanelCancel} />
<MessagesView