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 and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web). Tinode javascript library has no
external dependencies. 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/ Try it at http://api.tinode.co/x/example-react-js/
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) 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; border-radius: 2px;
box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.25); box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.25);
text-transform: uppercase; text-transform: uppercase;
} }
button[disabled], button[disabled],
@@ -71,18 +70,29 @@ button.round {
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
padding: 0; padding: 0;
line-height: 1; margin: 0;
line-height: 3.5rem;
outline: none; /* otherwise a square outline is shown */ outline: none; /* otherwise a square outline is shown */
box-shadow: 0 2px 0.5rem 0 rgba(0, 0, 0, 0.25); box-shadow: 0 2px 0.5rem 0 rgba(0, 0, 0, 0.25);
z-index: 2; 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 { button:focus, button:hover, button:active {
color: #fff; color: #fff;
} }
button:active { button:active {
box-shadow: inset 0 3px 0.5rem rgba(0, 0, 0, 0.125); 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 { button.blue {
background-color: #1e88e5; /* blue 600 */ background-color: #1e88e5; /* blue 600 */
@@ -92,27 +102,44 @@ button.blue:focus, button.blue:hover, button.blue:active {
background-color: #1976d2; 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 { textarea, input {
font-family: inherit; font-family: inherit;
font-size: 1.05rem;
background: transparent; background: transparent;
border: none; border: none;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
border-radius: 0.2em; border-radius: 0.2rem;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
width: 100%; width: 100%;
margin: 0.25em 0; margin: 0.25rem 0;
padding: 0 0.125em; padding: 0 0.125rem;
outline: none; outline: none;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
box-sizing: border-box; box-sizing: border-box;
color: #666; color: #666;
} }
textarea:focus,input:focus { textarea:focus, input:focus {
border-bottom: 1px solid #2196f3; border-bottom: 1px solid #2196f3;
box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25); box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25);
} }
input { input {
height: 2.5em; height: 2.5rem;
} }
textarea { textarea {
resize: none; resize: none;
@@ -134,7 +161,7 @@ textarea {
/* END of input and buttons */ /* END of input and buttons */
form { form {
padding: 0.5em 1em; padding: 0.5em 0.75em;
line-height: 1.75; line-height: 1.75;
font-size: 1.2rem; font-size: 1.2rem;
} }
@@ -191,7 +218,6 @@ form {
#side-caption-panel { #side-caption-panel {
display: flex; display: flex;
align-items: center; align-items: center;
x-justify-content: space-between;
flex: none; flex: none;
padding: 0.5em; padding: 0.5em;
height: 4em; height: 4em;
@@ -222,6 +248,7 @@ form {
width: 3rem; width: 3rem;
height: auto; height: auto;
flex: none; flex: none;
border-radius: 33%;
} }
/* Area which displays an error message */ /* Area which displays an error message */
@@ -239,6 +266,7 @@ form {
#contacts-panel { #contacts-panel {
overflow-y: auto; overflow-y: auto;
margin-bottom: auto; margin-bottom: auto;
height: 100%;
} }
#contact-box { #contact-box {
@@ -264,7 +292,7 @@ form {
/* Avatar and online-offline indicator */ /* Avatar and online-offline indicator */
#contact-box .avatar-box { #contact-box .avatar-box {
margin-right: 0.5rem; margin-right: 0.35rem;
position: relative; position: relative;
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
@@ -274,6 +302,7 @@ form {
#contact-box .avatar { #contact-box .avatar {
width: 3rem; width: 3rem;
height: auto; height: auto;
border-radius: 33%;
} }
/* Online-offline indicator positioned inside avatar-box */ /* Online-offline indicator positioned inside avatar-box */
@@ -281,8 +310,8 @@ form {
font-size: 1.5em; font-size: 1.5em;
line-height: 1; line-height: 1;
position: absolute; position: absolute;
right: 0; right: -0.1rem;
bottom: 0; bottom: -0.1rem;
} }
#contact-box .online { #contact-box .online {
color: #4caf50; color: #4caf50;
@@ -298,6 +327,90 @@ form {
} }
/* END of avatar box */ /* 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 */ /* END of sidepanel styles */
/* Topic/messages view */ /* Topic/messages view */
@@ -370,16 +483,16 @@ form {
.chat-box .bubble { .chat-box .bubble {
position: relative; position: relative;
max-width: 32em; max-width: 40rem;
min-width: 16em; min-width: 12rem;
height: auto; height: auto;
padding: 0.75em 0.5em 0.75em 1.25em; padding: 0.75rem 0.5rem 0.75rem 1.25rem;
margin: 0; margin: 0;
text-align: left; text-align: left;
border-radius: 0.5em; border-radius: 0.5rem;
-webkit-border-radius: 0.5em; -webkit-border-radius: 0.5rem;
-moz-border-radius: 0.5em; -moz-border-radius: 0.5rem;
/* tested in chrome 47 on mac - performance sucks /* tested in chrome 47 on mac - performance sucks
-webkit-filter: drop-shadow(0px 1px 1px -1px rgba(0,0,0,0.3)); -webkit-filter: drop-shadow(0px 1px 1px -1px rgba(0,0,0,0.3));
@@ -388,23 +501,23 @@ form {
} }
.chat-box li.single { .chat-box li.single {
margin-top: 0.75em; margin-top: 0.75rem;
margin-bottom: 0.75em; margin-bottom: 0.75rem;
} }
.chat-box li.first { .chat-box li.first {
margin-top: 0.75em; margin-top: 0.75rem;
margin-bottom: 0.15em; margin-bottom: 0.15rem;
} }
.chat-box li.middle { .chat-box li.middle {
margin-top: 0.15em; margin-top: 0.15rem;
margin-bottom: 0.15em; margin-bottom: 0.15rem;
} }
.chat-box li.last { .chat-box li.last {
margin-top: 0.15em; margin-top: 0.15rem;
margin-bottom: 0.75em; margin-bottom: 0.75rem;
} }
/* Left triangle placed bottom left flush. */ /* Left triangle placed bottom left flush. */
@@ -428,6 +541,8 @@ form {
.chat-box li.right .tip::before { .chat-box li.right .tip::before {
content: ''; content: '';
position: absolute; position: absolute;
display: flex;
justify-content: space-between;
width: 0; width: 0;
height: 0; height: 0;
left: auto; left: auto;
@@ -441,49 +556,44 @@ form {
border-left-color: transparent; border-left-color: transparent;
} }
.chat-box .avatar { .chat-box .avatar-box {
width: 40px; width: 3rem;
text-align: center; text-align: center;
} }
.chat-box .avatar img { .chat-box .avatar-box img {
width: 32px; width: 2.4rem;
display: block; display: block;
position: relative; position: relative;
left: 4px; left: 0.3rem;
border-radius: 50%; border-radius: 33%;
} }
.chat-box .bubble p { .chat-box .bubble p {
display: flex;
justify-content: space-between;
margin: 0; 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 */ /* Typing notification bubble */
.chat-box .typing { .chat-box .typing {
display: inline-block; display: inline-block;
border-radius: 0.5em; border-radius: 0.5rem;
width: 2em; width: 2rem;
padding: 0.25em 0.35em; padding: 0.25rem 0.35rem;
vertical-align: middle; vertical-align: middle;
text-align: center; text-align: center;
color: #aaa; color: #aaa;
} margin-top: 0.75rem;
.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;
} }
/* END of talk bubbles */ /* END of talk bubbles */
@@ -491,7 +601,7 @@ form {
/* Modal dialog */ /* Modal dialog */
.overlay { .dialog-overlay {
position: absolute; position: absolute;
display: flex; display: flex;
top: 0; top: 0;
@@ -499,12 +609,44 @@ form {
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 10; z-index: 10;
background-color: rgba(0,0,0,0.5); background-color: rgba(0,0,0,0.25);
} }
.modal-dialog { .modal-dialog {
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: white; background-color: white;
min-width: 24rem;
min-height: 16rem;
margin: auto; 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 */ /* END of modal dialog */
/* Miscellaneous */ /* Miscellaneous */
@@ -518,7 +660,7 @@ form {
color: white; color: white;
border-radius: 1em; border-radius: 1em;
padding: 0.25em; padding: 0.25em;
min-width: 1.5em; min-width: 1.75em;
text-align: center; text-align: center;
} }
+12 -20
View File
@@ -17,10 +17,7 @@
width="192" width="192"
height="192" height="192"
viewBox="0 0 192 192" viewBox="0 0 192 192"
sodipodi:docname="Tinode logo-round-colored-tilted.svg" sodipodi:docname="Tinode logo-round-colored.svg"><metadata
inkscape:export-filename="/home/gsokolov/work/icons/logo32x32.png"
inkscape:export-xdpi="15.789473"
inkscape:export-ydpi="15.789473"><metadata
id="metadata8"><rdf:RDF><cc:Work id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type 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 rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
@@ -346,19 +343,19 @@
guidetolerance="10000" guidetolerance="10000"
inkscape:pageopacity="0" inkscape:pageopacity="0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:window-width="1370" inkscape:window-width="1855"
inkscape:window-height="782" inkscape:window-height="1176"
id="namedview4" id="namedview4"
showgrid="true" showgrid="true"
showguides="false" showguides="false"
inkscape:snap-perpendicular="true" inkscape:snap-perpendicular="true"
inkscape:snap-tangential="true" inkscape:snap-tangential="true"
inkscape:zoom="2.7812866" inkscape:zoom="2.7812866"
inkscape:cx="96.95464" inkscape:cx="97.314186"
inkscape:cy="97.415892" inkscape:cy="97.415892"
inkscape:window-x="290" inkscape:window-x="65"
inkscape:window-y="245" inkscape:window-y="24"
inkscape:window-maximized="0" inkscape:window-maximized="1"
inkscape:current-layer="g10" inkscape:current-layer="g10"
fit-margin-top="0" fit-margin-top="0"
fit-margin-left="0" fit-margin-left="0"
@@ -532,7 +529,11 @@
height="24" height="24"
width="128" width="128"
id="rect4982" 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" rx="12"
transform="matrix(0,1,-1,0,0,0)" transform="matrix(0,1,-1,0,0,0)"
ry="12" ry="12"
@@ -550,13 +551,4 @@
height="23.999969" height="23.999969"
width="92" width="92"
id="rect4982-6-9" 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> 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> <body>
<div id="mountPoint"></div> <div id="mountPoint"></div>
<script type="text/babel"> <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 // Host name and posrt to connect to
var HOST = "api.tinode.co"; // var HOST = "api.tinode.co";
//var HOST = "localhost:6060"; var HOST = "localhost:6060";
// API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own // API key. Use https://github.com/tinode/chat/tree/master/keygen to generate your own
var API_KEY = "AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K"; var API_KEY = "AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K";
@@ -58,6 +60,11 @@
{hour12: false, year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'}); {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 */ /* BEGIN Login: a login form */
var LoginView = React.createClass({ var LoginView = React.createClass({
getInitialState: function() { getInitialState: function() {
@@ -115,14 +122,15 @@
/* END Login */ /* END Login */
/* BEGIN Account registration */ /* BEGIN Account registration */
var CreateAccountView = React.createClass({ var CreateAccountView = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
login: '', login: '',
password: '', password: '',
password2: '', password2: '',
public: '', fn: '', // full/formatted name
private: '', imageDataUrl: null,
errorCleared: false errorCleared: false
}; };
}, },
@@ -135,11 +143,11 @@
handlePassword2Change: function(e) { handlePassword2Change: function(e) {
this.setState({password2: e.target.value}); this.setState({password2: e.target.value});
}, },
handlePublicChange: function(e) { handleFnChange: function(e) {
this.setState({public: e.target.value}); this.setState({fn: e.target.value});
}, },
handlePrivateChange: function(e) { handleImageChanged: function(img) {
this.setState({private: e.target.value}); this.setState({imageDataUrl: img});
}, },
handleSubmit: function(e) { handleSubmit: function(e) {
e.preventDefault(); e.preventDefault();
@@ -147,8 +155,20 @@
} else { } else {
this.setState({errorCleared: false}); this.setState({errorCleared: false});
this.props.onCreateAccount(this.state.login.trim(), this.state.password.trim(), var vcard = null;
this.state.public.trim(), this.state.private.trim()); 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() { render: function() {
@@ -159,16 +179,19 @@
return ( return (
<div> <div>
<form id="signup-form" onSubmit={this.handleSubmit} onCancel={this.props.onCancel}> <form id="signup-form" onSubmit={this.handleSubmit} onCancel={this.props.onCancel}>
<input type="text" placeholder="Login" <div id="signup-form-one">
value={this.state.login} onChange={this.handleLoginChange} required autofocus /> <div>
<input type="password" placeholder="Password" <input type="text" placeholder="Login"
value={this.state.password} onChange={this.handlePasswordChange} required /> value={this.state.login} onChange={this.handleLoginChange} required autofocus />
<input type="password" placeholder="Repeat password" <input type="password" placeholder="Password"
value={this.state.password2} onChange={this.handlePassword2Change} required /> value={this.state.password} onChange={this.handlePasswordChange} required />
<textarea rows="3" onChange={this.handlePublicChange} <input type="password" placeholder="Repeat password"
value={this.state.public} placeholder="Public: text or valid JSON"></textarea> value={this.state.password2} onChange={this.handlePassword2Change} required />
<textarea rows="3" onChange={this.handlePrivateChange} </div>
value={this.state.private} placeholder="Private: text or valid JSON"></textarea> <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"> <div className="dialog-buttons">
<button className={submitClassList} type="submit">Sign up</button> <button className={submitClassList} type="submit">Sign up</button>
</div> </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 */ /* END Account registration */
/* BEGIN Tinode config panel */ /* BEGIN Tinode config panel */
@@ -270,7 +341,7 @@
title = "Settings"; break; title = "Settings"; break;
case 'contacts': case 'contacts':
title = this.props.title; title = this.props.title;
avatar = true; avatar = this.props.avatar;
default:; default:;
}; };
return ( return (
@@ -280,17 +351,29 @@
onSignUp={this.props.onSignUp} onSignUp={this.props.onSignUp}
onSettings={this.props.onSettings} onSettings={this.props.onSettings}
onCancel={this.props.onCancel} /> 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' ? {this.props.state === 'login' ?
<LoginView disabled={this.props.loginDisabled} <LoginView disabled={this.props.loginDisabled}
onLogin={this.handleLoginRequested} onLogin={this.handleLoginRequested}
onClearError={this.handleClearErrorText} /> : onClearError={this.handleClearErrorText} /> :
this.props.state === 'register' ? 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' ? this.props.state === 'settings' ?
<SettingsView onCancel={this.props.onCancel} /> : <SettingsView onCancel={this.props.onCancel} /> :
this.props.state === 'contacts' ? 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} null}
</div> </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 */ /* A single topic */
var Contact = React.createClass({ var Contact = React.createClass({
handleClick: function() { handleClick: function() {
this.props.onSelected(this.props.topic); this.props.onSelected(this.props.topic);
}, },
render: function() { render: function() {
var title = this.props.public; var title = this.props.title;
if (title.length > 32) { if (title.length > 30) {
title = title.substring(0, 30) + "..."; title = title.substring(0, 28) + "...";
} }
var selected = (this.props.selected === this.props.topic) ? "selected" : null; var selected = (this.props.selected === this.props.topic) ? "selected" : null;
var avatar = (Tinode.getTopicType(this.props.topic) === "grp") ? <i className="material-icons">group</i> : var avatar = this.props.avatar ?
<i className="material-icons">person</i>; <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 ( return (
<li className={selected} onClick={this.handleClick}> <li className={selected} onClick={this.handleClick}>
<div className="avatar-box"> <div className="avatar-box">
@@ -402,9 +474,12 @@
me.onSubsUpdated = this.tnMeSubsUpdated; me.onSubsUpdated = this.tnMeSubsUpdated;
}, },
tnMeContactUpdate: function(what, cont) { tnMeContactUpdate: function(what, cont) {
console.log("changed: " + what + " in :"); if (what === "on" || what === "off" || what === "read") {
console.log(cont); this.forceUpdate();
this.forceUpdate(); } else {
console.log("changed (not updating for it): " + what + " in :");
console.log(cont);
}
}, },
tnInvite: function(invite) { tnInvite: function(invite) {
console.log(invite); console.log(invite);
@@ -430,8 +505,15 @@
unread -= c.read; unread -= c.read;
} }
contactNodes.push( contactNodes.push(
<Contact public={c.public} comment={c.private} unread={unread} now={c.online} selected={this.state.selected} <Contact
onSelected={this.onSelectionChange} topic={c.topic} key={c.topic} /> 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); }, this);
@@ -440,7 +522,9 @@
<ul id="contact-box"> <ul id="contact-box">
{contactNodes} {contactNodes}
</ul> </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> </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}> <div className={bubbleClass} style={colors}>
<p>{this.props.content} <p>{this.props.content}
<Message.ReceivedMarker response={this.props.response} <Message.ReceivedMarker response={this.props.response}
timestamp={this.props.timestamp} received={this.props.received} /> timestamp={this.props.timestamp} received={this.props.received} />
</p> </p>
</div> </div>
</li> </li>
@@ -554,17 +638,19 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
this.propsChange(nextProps); this.propsChange(nextProps);
}, },
propsChange: function(props) { propsChange: function(props) {
if (props.topicSelected != this.state.topic) { if (props.topicSelected && (props.topicSelected != this.state.topic)) {
var msgs = []; var msgs = [];
var topic = Tinode.getTopic(props.topicSelected); var topic = Tinode.getTopic(props.topicSelected);
// Bind the new topic to component. // Bind the new topic to component.
topic.onData = this.handleNewMessage; topic.onData = this.handleNewMessage;
topic.onInfo = this.handleInfoReceipt;
topic.onMetaInfo = this.handleInfoChange; topic.onMetaInfo = this.handleInfoChange;
topic.onSubsUpdated = this.handleSubsUpdated; topic.onSubsUpdated = this.handleSubsUpdated;
// Unbind the previous topic from this component. // Unbind the previous topic from this component.
if (this.state.topic) { if (this.state.topic) {
var oldTopic = Tinode.getTopic(this.state.topic); var oldTopic = Tinode.getTopic(this.state.topic);
oldTopic.onData = undefined; oldTopic.onData = undefined;
oldTopic.onInfo = undefined;
oldTopic.onMetaInfo = undefined; oldTopic.onMetaInfo = undefined;
oldTopic.onSubsUpdated = 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) { handleInfoChange: function(info) {
this.setState({title: info.public}); this.setState({title: info.public.vcard.fn});
}, },
handleSubsUpdated: function(uids) { handleSubsUpdated: function(uids) {
@@ -600,6 +686,20 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
}, READ_DELAY); }, READ_DELAY);
this.props.onData(msg); 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() { render: function() {
var component = null; var component = null;
if (this.state.topic) { 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 isReply = !(msg.from === myUid);
var msgReceived = Received.UNDEF; var msgReceived = Received.UNDEF;
if (!isReply) { if (!isReply) {
if (topic.msgReadCount(msg.seq) > 1) { if (topic.msgReadCount(msg.seq) > 0) {
msgReceived = Received.READ; msgReceived = Received.READ;
} else if (topic.msgRecvCount(msg.seq) > 1) { } else if (topic.msgRecvCount(msg.seq) > 0) {
msgReceived = Received.CLIENT; msgReceived = Received.CLIENT;
} else { } else {
msgReceived = Received.SERVER; 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 */ /* 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 */ /* The top-level class to hold all fuinctionality together */
var AppView = React.createClass({ var AppView = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
sidePanelSelected: 'login', sidePanelSelected: 'login',
sidePanelTitle: null, sidePanelTitle: null,
sidePanelAvatar: null,
dialogSelected: null,
loggedIn: false, loggedIn: false,
errorText: '', errorText: '',
topicSelected: '', topicSelected: '',
@@ -759,12 +898,14 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
Tinode.enableLogging(true); Tinode.enableLogging(true);
Tinode.onDisconnect = this.handleDicsonnect; Tinode.onDisconnect = this.handleDicsonnect;
// Setup with the default transport, usually websocket. // Setup with the default transport, usually websocket.
Tinode.setup(HOST, API_KEY); Tinode.setup(APP_NAME, HOST, API_KEY);
}, },
handleDicsonnect: function() { handleDicsonnect: function() {
this.setState({ this.setState({
sidePanelSelected: 'login', sidePanelSelected: 'login',
sidePanelTitle: null, sidePanelTitle: null,
sidePanelAvatar: null,
dialogSelected: null,
loggedIn: false, loggedIn: false,
errorText: 'disconnected', errorText: 'disconnected',
topicSelected: '', topicSelected: '',
@@ -792,7 +933,10 @@ dignissim a diam a hendrerit. Nulla vel est ut quam rhoncus pulvinar. In hac hab
}); });
}, },
tnMeMetaInfo: function(info) { tnMeMetaInfo: function(info) {
this.setState({sidePanelTitle: info.public}); this.setState({
sidePanelTitle: info.public.vcard.fn,
sidePanelAvatar: makeImageUrl(info.public.vcard.photo)
});
}, },
// Sending "received" notifications // Sending "received" notifications
tnData: function(data) { 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'}); this.setState({sidePanelSelected: 'login'});
} }
}, },
handleNewTopic: function() {
this.setState({dialogSelected: 'new-topic'});
},
handleDialogCancel: function() {
this.setState({dialogSelected: null});
},
render: function() { render: function() {
return ( return (
<div id="app-container"> <div id="app-container">
<ModalDialogWrapper
dialog={this.state.dialogSelected}
onOk={this.handleDialogCancel}
onCancel={this.handleDialogCancel} />
<SidepanelView <SidepanelView
state={this.state.sidePanelSelected} state={this.state.sidePanelSelected}
title={this.state.sidePanelTitle} title={this.state.sidePanelTitle}
avatar={this.state.sidePanelAvatar}
subscriptions={this.state.subscriptions} subscriptions={this.state.subscriptions}
loginDisabled={this.state.loginDisabled} loginDisabled={this.state.loginDisabled}
errorText={this.state.errorText} 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} onLoginRequest={this.handleLoginRequest}
onCreateAccount={this.handleNewAccount} onCreateAccount={this.handleNewAccount}
onTopicSelected={this.handleTopicSelected} onTopicSelected={this.handleTopicSelected}
onNewTopic={this.handleNewTopic}
onCancel={this.handleSidepanelCancel} /> onCancel={this.handleSidepanelCancel} />
<MessagesView <MessagesView