added topic and account editing

This commit is contained in:
or-else
2017-06-24 21:03:37 +03:00
parent 61fbc69356
commit ac97f1d61d
3 changed files with 523 additions and 148 deletions
+7 -3
View File
@@ -18,9 +18,13 @@ Background pattern from http://subtlepatterns.com/, commercial and non-commercia
### Desktop ### Desktop
<img src="web-desktop.png" alt="Desktop web: full app" width=540 /> <p align="center">
<img src="web-desktop.png" alt="Desktop web: full app" width=540 />
</p>
### Mobile ### Mobile
<img src="web-mobile-contacts.png" alt="Mobile web: contacts" width=401 /> <p align="center" nowrap>
<img src="web-mobile-chat.png" alt="Mobile web: chat" width=485 /> <img src="web-mobile-contacts.png" alt="Mobile web: contacts" width=401 />
<img src="web-mobile-chat.png" alt="Mobile web: chat" width=485 />
</p>
+140 -57
View File
@@ -18,6 +18,15 @@ html, body {
box-shadow: inset 0 10rem #3949AB; /* the deep-plue stripe accross the top */ box-shadow: inset 0 10rem #3949AB; /* the deep-plue stripe accross the top */
} }
tt {
font-family: Roboto Mono, monospace;
}
hr {
height: 1px;
color: #666;
}
a { a {
color: #2196f3; color: #2196f3;
text-decoration: none; text-decoration: none;
@@ -34,11 +43,18 @@ a:focus {
} }
form { form {
font-size: 1.2rem; font-size: 1.1rem;
padding: 0.5rem 0.75rem;
line-height: 1.75;
} }
form label { form label {
font-size: 1rem; font-size: 1.1rem;
}
form label.small {
color: #1e88e5;
font-size: 0.95rem;
} }
img.avatar { img.avatar {
@@ -131,13 +147,12 @@ button.white:focus, button.white:hover, button.blue:active {
textarea, input { textarea, input {
font-family: inherit; font-family: inherit;
font-size: 1.05rem; font-size: 1.1rem;
background: transparent; background: transparent;
border: none; border: none;
margin: 0.25rem 0; margin: 0.25rem 0;
padding: 0 0.125rem; padding: 0 0.125rem;
outline: none; outline: none;
box-sizing: border-box;
color: #666; color: #666;
} }
textarea { textarea {
@@ -148,7 +163,6 @@ input[type="password"],
textarea { textarea {
height: 2.5rem; height: 2.5rem;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
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);
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
width: 100%; width: 100%;
@@ -160,6 +174,12 @@ input[type="password"]:focus {
box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25); box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25);
} }
input[type="text"]::placeholder,
input[type="password"]::placeholder,
textarea::placeholder {
color: #bbb;
}
/* Scrollbar styling for Chrome. Firefox does not support scrollbar styling. */ /* Scrollbar styling for Chrome. Firefox does not support scrollbar styling. */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 0.6rem; width: 0.6rem;
@@ -176,12 +196,6 @@ input[type="password"]:focus {
/* END of input and buttons */ /* END of input and buttons */
form {
padding: 0.5rem 0.75rem;
line-height: 1.75;
font-size: 1.2rem;
}
/* BEGIN Popup/dropdown menu */ /* BEGIN Popup/dropdown menu */
ul.menu { ul.menu {
@@ -254,6 +268,14 @@ ul.menu > li.separator:hover {
height: 100%; height: 100%;
} }
#info-view {
display: flex;
flex-direction: column;
width: 24rem;
min-width: 24rem;
height: 100%;
}
@media (max-width: 960px) { @media (max-width: 960px) {
#mountPoint { #mountPoint {
padding: 0; padding: 0;
@@ -268,6 +290,10 @@ ul.menu > li.separator:hover {
#sidepanel { #sidepanel {
width: 100%; width: 100%;
} }
#info-view {
width: 100%;
}
} }
/* Generic announcement text, centered vertically and horizontally */ /* Generic announcement text, centered vertically and horizontally */
@@ -281,11 +307,38 @@ ul.menu > li.separator:hover {
font-size: 1.4rem; font-size: 1.4rem;
} }
/* Class to alternatively hide sidepanel/topic-view */ /* Class to alternatively hide sidepanel/topic-view/info-view */
.nodisplay { .nodisplay {
display: none!important; display: none!important;
} }
/* BEGIN InPlaceEdit styles */
.in-place-edit {
display: inline-block;
font-family: inherit;
font-size: 1.1rem;
height: 2.5rem;
line-height: 1.5;
margin: 0.25rem 0.125rem;
-x-padding: 0 0.125rem;
width: 100%;
vertical-align: baseline;
border-bottom: 1px dashed #ddd;
cursor: pointer;
overflow: hidden;
}
.in-place-edit.disabled {
border-bottom: none;
cursor: default;
}
.in-place-edit.placeholder {
color: #bbb;
}
/* END of InPlaceEdit styles */
/* END of top-level layout */ /* END of top-level layout */
/* Sidepanel styles */ /* Sidepanel styles */
@@ -293,30 +346,37 @@ ul.menu > li.separator:hover {
font-size: 1.4rem; font-size: 1.4rem;
} }
.dialog-buttons { .caption-panel {
display: flex;
padding: 0.5em;
justify-content: flex-end;
}
#side-caption-panel {
display: flex; display: flex;
align-items: center; align-items: center;
flex: none; flex: none;
padding: 0.5em; padding: 0.5em;
height: 4em; height: 4em;
background-color: #2196f3; /* blue 500 */
color: #bbdefb; /* light-blue 100 */
box-shadow: 0 1px 2px gray; box-shadow: 0 1px 2px gray;
z-index: 1; /* otherwise the shadow becomes invisible */ z-index: 1; /* otherwise the shadow becomes invisible */
} }
.dialog-buttons {
display: flex;
padding: 0.5em;
justify-content: flex-end;
}
.avatar-box {
flex-grow: 0;
flex-shrink: 0;
}
#side-caption-panel {
background-color: #2196f3; /* blue 500 */
color: #bbdefb; /* light-blue 100 */
}
#side-caption-panel a { #side-caption-panel a {
color: #bbdefb; /* blue 100 */ color: #bbdefb; /* blue 100 */
padding-left: 0.5em; padding-left: 0.5rem;
vertical-align: -1em; /* icons are bigger than the declared font size */ line-height: 1;
} }
#side-caption-panel :hover { #side-caption-panel :hover {
@@ -330,11 +390,16 @@ ul.menu > li.separator:hover {
#self-avatar { #self-avatar {
width: 3rem; width: 3rem;
min-width: 3rem;
height: 3rem; height: 3rem;
flex: none; flex: none;
border-radius: 50%; border-radius: 50%;
} }
#self-avatar.avatar-box {
font-size: 1.8rem;
}
/* Application settings */ /* Application settings */
#settings-form ul { #settings-form ul {
list-style: none; list-style: none;
@@ -417,6 +482,7 @@ ul.menu > li.separator:hover {
margin-right: 0.35rem; margin-right: 0.35rem;
position: relative; position: relative;
width: 3rem; width: 3rem;
min-width: 3rem;
height: 3rem; height: 3rem;
font-size: 1.8rem; font-size: 1.8rem;
overflow: hidden; overflow: hidden;
@@ -472,7 +538,7 @@ ul.menu > li.separator:hover {
.panel-form-two { .panel-form-two {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; flex-grow: 1;
} }
@@ -481,36 +547,32 @@ ul.menu > li.separator:hover {
position: relative; position: relative;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
width: 4.5rem; flex-shrink: 0;
height: auto; width: 7rem;
box-sizing: border-box; height: 7rem;
margin-left: 0.5rem; margin-left: 0.5rem;
padding: 0; padding: 0;
border-radius: 50%;
} }
.avatar-upload .blank { .avatar-upload .blank {
text-align: center;
vertical-align: middle;
border: 1px dashed #999; border: 1px dashed #999;
border-radius: 0.75rem;
font-size: 1rem; font-size: 1rem;
width: 4.5rem; width: 7rem;
height: 4.5rem; height: 7rem;
overflow: hidden; overflow: hidden;
color: #999; color: #999;
vertical-align: middle; line-height: 7rem;
line-height: 4.5rem; border-radius: 50%;
box-sizing: border-box;
margin: 0;
padding: 0;
} }
.avatar-upload .preview { .avatar-upload .preview {
border-radius: 0.75rem; width: 7rem;
width: 4.5rem; height: 7rem;
height: 4.5rem;
overflow: hidden; overflow: hidden;
box-sizing: border-box; border-radius: 50%;
margin: 0;
padding: 0;
} }
.avatar-upload .inputfile.hidden { .avatar-upload .inputfile.hidden {
@@ -526,14 +588,14 @@ ul.menu > li.separator:hover {
vertical-align: middle; vertical-align: middle;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
width: 2.5rem; width: 3rem;
height: 2.5rem; height: 3rem;
line-height: 3rem; line-height: 4rem;
border-radius: 50%; border-radius: 50%;
box-shadow: 0 1px 4px rgba(0,0,0,0.4); box-shadow: 0 1px 4px rgba(0,0,0,0.4);
position: absolute; position: absolute;
top: 3.875rem; top: 6rem;
left: 1rem; left: 2rem;
background-color: #fafafa; background-color: #fafafa;
} }
@@ -552,24 +614,23 @@ ul.menu > li.separator:hover {
/* Topic/messages view */ /* Topic/messages view */
#topic-caption-panel { #topic-caption-panel {
display: flex;
align-items:center;
justify-content: flex-start;
flex: none;
padding: 0.5rem;
height: 4rem;
background-color: #b0bec5; background-color: #b0bec5;
box-shadow: 0 1px 2px gray;
z-index: 1; /* otherwise the shadow becomes invisible */
} }
#topic-caption-panel a { #topic-caption-panel a {
color: #757575; color: #757575;
text-decoration: none; text-decoration: none;
} }
#topic-caption-panel :hover {
color: black;
}
#topic-caption-panel .avatar-box { #topic-caption-panel .avatar-box {
width: 3rem; width: 3rem;
min-width: 3rem;
height: 3rem; height: 3rem;
font-size: 1.8rem;
margin-right: 0.35rem; margin-right: 0.35rem;
} }
@@ -730,7 +791,9 @@ ul.menu > li.separator:hover {
.chat-box .avatar-box { .chat-box .avatar-box {
position: relative; position: relative;
width: 1.4rem; width: 1.4rem;
min-width: 1.4rem;
height: 1.4rem; height: 1.4rem;
min-height: 1.4rem;
font-size: 0.8rem; font-size: 0.8rem;
margin-bottom: 0.8rem; margin-bottom: 0.8rem;
margin-right: 0.8rem; margin-right: 0.8rem;
@@ -784,6 +847,24 @@ ul.menu > li.separator:hover {
/* END of talk bubbles */ /* END of talk bubbles */
/* END of topic/mesages view */ /* END of topic/mesages view */
/* InfoView styles */
#info-caption-panel {
background-color: #b0bec5;
}
#info-caption-panel a {
color: #757575;
line-height: 1;
text-decoration: none;
}
#info-caption-panel :hover {
color: black;
}
#info-title {
margin-right: auto;
}
/* END of InfoView */
/* Modal dialog */ /* Modal dialog */
.dialog-overlay { .dialog-overlay {
@@ -915,7 +996,7 @@ ul.tabbar li.active > a:after {
color: #2196f3; /* blue 500 */ color: #2196f3; /* blue 500 */
} }
/* Letter tile base style */ /* Letter tile base style, responsive square box */
.lettertile { .lettertile {
width: 100%; width: 100%;
padding-bottom: 100%; padding-bottom: 100%;
@@ -923,9 +1004,11 @@ ul.tabbar li.active > a:after {
position: relative; position: relative;
} }
/* Responsive square box for text tiles */ /* Div with letter tile content */
.lettertile > div { .lettertile > div {
display: flex; display: flex;
flex-shrink: 0;
flex-grow: 0;
position: absolute; position: absolute;
top: 0; bottom: 0; left: 0; right: 0; top: 0; bottom: 0; left: 0; right: 0;
text-transform: uppercase; text-transform: uppercase;
+376 -88
View File
@@ -67,6 +67,8 @@
// Make a data URL from public.photo // Make a data URL from public.photo
function makeImageUrl(photo) { function makeImageUrl(photo) {
//console.log("makeImageUrl");
//console.log(photo);
return (photo && photo.type && photo.data) ? return (photo && photo.type && photo.data) ?
'data:image/' + photo.type + ';base64,' + photo.data : null; 'data:image/' + photo.type + ';base64,' + photo.data : null;
} }
@@ -94,6 +96,26 @@
return host; return host;
} }
// Create VCard which represents topic 'public' info
function vcard(fn, imageDataUrl) {
var card = null;
if ((fn && fn.trim()) || imageDataUrl) {
card = {};
if (fn) {
card.fn = fn.trim();
}
if (imageDataUrl) {
var dataStart = imageDataUrl.indexOf(",");
card.photo = {
data: imageDataUrl.substring(dataStart+1),
type: "jpg"
};
}
}
return card;
}
/* BEGIN Popup/dropdown menu */ /* BEGIN Popup/dropdown menu */
var ContextMenu = React.createClass({ var ContextMenu = React.createClass({
componentWillUnmount: function() { componentWillUnmount: function() {
@@ -172,6 +194,15 @@
}); });
/* END Popup/dropdown menu */ /* END Popup/dropdown menu */
/* The X menu to be displayed in title bars */
var MenuCancel = React.createClass({
render: function() {
return (
<a href="#" onClick={this.props.onCancel}><i className="material-icons">close</i></a>
);
}
});
/* BEGIN Lettertile: Avatar box: either a bitmap or a letter tile or a stock icon. */ /* BEGIN Lettertile: Avatar box: either a bitmap or a letter tile or a stock icon. */
class LetterTile extends React.PureComponent { class LetterTile extends React.PureComponent {
render() { render() {
@@ -196,6 +227,66 @@
} }
/* END Lettertile */ /* END Lettertile */
/* BEGIN In-place text editor. Shows text with an icon
* which toggles it into an input field */
var InPlaceEdit = React.createClass({
getInitialState: function() {
return {
active: this.props.state,
text: this.props.text || "",
changed: false
};
},
handeTextChange: function(e) {
this.setState({text: e.target.value});
},
handleKeyPress: function(e) {
if (e.keyCode === 27) {
this.setState({text: this.props.text, active: false});
}
},
handleStartEditing: function() {
ReactDOM.findDOMNode(this).focus();
this.setState({active: !this.props.readOnly});
},
handleEditingFinished: function() {
this.setState({active: false});
if (this.state.text !== this.props.text) {
this.props.onFinished(this.state.text);
}
},
render: function() {
var spanText = this.props.type === "password" ?
"••••••••" : this.state.text;
var spanClass = "in-place-edit" +
(this.props.readOnly ? " disabled" : "");
if (!spanText) {
spanText = this.props.placeholder;
spanClass += " placeholder";
}
return (
this.state.active ?
<input type={this.props.type || "text"}
value={this.state.text}
placeholder={this.props.placeholder}
onChange={this.handeTextChange}
onKeyPress={this.handleKeyPress}
onBlur={this.handleEditingFinished}
autoFocus />
:
<span className={spanClass}
onClick={this.handleStartEditing}>{spanText}</span>
);
}
});
/* END InPlaceEdit */
/* BEGIN Combobox for selecting host name */ /* BEGIN Combobox for selecting host name */
class HostSelector extends React.PureComponent { class HostSelector extends React.PureComponent {
constructor(props) { constructor(props) {
@@ -338,22 +429,10 @@
// FIXME(gene): report an error here. // FIXME(gene): report an error here.
} else { } else {
this.setState({errorCleared: false}); this.setState({errorCleared: false});
var vcard = null; this.props.onCreateAccount(
this.state.login.trim(),
if ((this.state.fn && this.state.fn.trim()) || this.state.imageDataUrl) { this.state.password.trim(),
vcard = {}; vcard(this.state.fn, this.state.imageDataUrl));
if (this.state.fn) {
vcard.fn = this.state.fn.trim();
}
if (this.state.imageDataUrl) {
var dataStart = this.state.imageDataUrl.indexOf(",");
vcard.photo = {
data: this.state.imageDataUrl.substring(dataStart+1),
type: "jpg"
};
}
}
this.props.onCreateAccount(this.state.login.trim(), this.state.password.trim(), vcard);
} }
}, },
render: function() { render: function() {
@@ -362,26 +441,24 @@
submitClasses += " disabled"; submitClasses += " disabled";
} }
return ( return (
<div> <form className="panel-form" onSubmit={this.handleSubmit}>
<form className="panel-form" onSubmit={this.handleSubmit}> <div className="panel-form-one">
<div className="panel-form-one"> <div className="panel-form-two">
<div className="panel-form-two"> <input type="text" placeholder="Login"
<input type="text" placeholder="Login" value={this.state.login} onChange={this.handleLoginChange} required autoFocus />
value={this.state.login} onChange={this.handleLoginChange} required autoFocus /> <input type="password" placeholder="Password"
<input type="password" placeholder="Password" value={this.state.password} onChange={this.handlePasswordChange} required />
value={this.state.password} onChange={this.handlePasswordChange} required /> <input type="password" placeholder="Repeat password"
<input type="password" placeholder="Repeat password" value={this.state.password2} onChange={this.handlePassword2Change} required />
value={this.state.password2} onChange={this.handlePassword2Change} required />
</div>
<AvatarUpload onImageChanged={this.handleImageChanged} />
</div> </div>
<input type="text" placeholder="Full name, e.g. John Doe" <AvatarUpload onImageChanged={this.handleImageChanged} />
value={this.state.fn} onChange={this.handleFnChange} /> </div>
<div className="dialog-buttons"> <input type="text" placeholder="Full name, e.g. John Doe"
<button className={submitClasses} type="submit">Sign up</button> value={this.state.fn} onChange={this.handleFnChange} />
</div> <div className="dialog-buttons">
</form> <button className={submitClasses} type="submit">Sign up</button>
</div> </div>
</form>
); );
} }
}); });
@@ -389,7 +466,7 @@
var AvatarUpload = React.createClass({ var AvatarUpload = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
dataUrl: null dataUrl: this.props.avatar
}; };
}, },
handleFileUpload(e) { handleFileUpload(e) {
@@ -398,7 +475,7 @@
img.crossOrigin = 'Anonymous'; img.crossOrigin = 'Anonymous';
img.onload = function() { img.onload = function() {
var canvas = document.createElement('CANVAS'); var canvas = document.createElement('CANVAS');
canvas.width = canvas.height = 64; canvas.width = canvas.height = 128;
var ctx = canvas.getContext('2d'); var ctx = canvas.getContext('2d');
var sx = 0, sy = 0, size; var sx = 0, sy = 0, size;
if (this.height > this.width) { if (this.height > this.width) {
@@ -409,7 +486,7 @@
sx = (this.width - size) / 2; sx = (this.width - size) / 2;
} }
ctx.imageSmoothingEnabled = true; ctx.imageSmoothingEnabled = true;
ctx.drawImage(this, sx, sy, size, size, 0, 0, 64, 64); ctx.drawImage(this, sx, sy, size, size, 0, 0, canvas.width, canvas.height);
var bits = canvas.toDataURL("image/jpg"); var bits = canvas.toDataURL("image/jpg");
canvas = null; canvas = null;
@@ -423,12 +500,14 @@
<div className="avatar-upload"> <div className="avatar-upload">
{this.state.dataUrl ? {this.state.dataUrl ?
<img src={this.state.dataUrl} className="preview" /> : <img src={this.state.dataUrl} className="preview" /> :
<div className="blank">64&times;64</div>} <div className="blank">128&times;128</div>}
<input type="file" id="file-input-avatar" className="inputfile hidden" {this.props.readOnly ? null :
accept="image/*" onChange={this.handleFileUpload} /> <input type="file" id="file-input-avatar" className="inputfile hidden"
accept="image/*" onChange={this.handleFileUpload} />}
{this.props.readOnly ? null :
<label htmlFor="file-input-avatar" className="round"> <label htmlFor="file-input-avatar" className="round">
<i className="material-icons">file_upload</i> <i className="material-icons">file_upload</i>
</label> </label>}
</div> </div>
); );
} }
@@ -492,22 +571,22 @@
var SideNavbar = React.createClass({ var SideNavbar = React.createClass({
render: function() { render: function() {
return ( return (
<div id="side-caption-panel"> <div id="side-caption-panel" className="caption-panel">
<div id="self-avatar"><LetterTile <div id="self-avatar" className="avatar-box"><LetterTile
avatar={this.props.avatar} topic={this.props.myUserId} title={this.props.title} /></div> avatar={this.props.avatar} topic={this.props.myUserId} title={this.props.title} /></div>
<div id="sidepanel-title" className="panel-title">{this.props.title}</div> <div id="sidepanel-title" className="panel-title">{this.props.title}</div>
{this.props.state === 'login' ? {this.props.state === 'login' ?
<SideNavbar.MenuStart onSignUp={this.props.onSignUp} onSettings={this.props.onSettings} /> : <SideNavbar.MenuStart onSignUp={this.props.onSignUp} onSettings={this.props.onSettings} /> :
this.props.state === 'settings' ? this.props.state === 'settings' ?
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> : <MenuCancel onCancel={this.props.onCancel} /> :
this.props.state === 'edit-account' ? this.props.state === 'edit-account' ?
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> : <MenuCancel onCancel={this.props.onCancel} /> :
this.props.state === 'register' ? this.props.state === 'register' ?
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> : <MenuCancel onCancel={this.props.onCancel} /> :
this.props.state === 'contacts' ? this.props.state === 'contacts' ?
<SideNavbar.MenuContacts onNewTopic={this.props.onNewTopic} onSettings={this.props.onSettings} /> : <SideNavbar.MenuContacts onNewTopic={this.props.onNewTopic} onSettings={this.props.onSettings} /> :
this.props.state === 'new-topic' ? this.props.state === 'new-topic' ?
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> : <MenuCancel onCancel={this.props.onCancel} /> :
null} null}
</div> </div>
); );
@@ -526,14 +605,6 @@
} }
}); });
SideNavbar.MenuCancel = React.createClass({
render: function() {
return (
<a href="#" onClick={this.props.onCancel}><i className="material-icons">close</i></a>
);
}
})
SideNavbar.MenuContacts = React.createClass({ SideNavbar.MenuContacts = React.createClass({
render: function() { render: function() {
return ( return (
@@ -614,7 +685,10 @@
/> : /> :
this.props.state === 'edit-account' ? this.props.state === 'edit-account' ?
<EditAccountView onCancel={this.props.onCancel} /> : <EditAccountView
login={this.props.login}
onSubmit={this.props.onUpdateAccount}
onCancel={this.props.onCancel} /> :
this.props.state === 'contacts' ? this.props.state === 'contacts' ?
<ContactsView <ContactsView
@@ -653,10 +727,89 @@
/* EditAccount parameters */ /* EditAccount parameters */
var EditAccountView = React.createClass({ var EditAccountView = React.createClass({
getInitialState: function() {
return {
updated: false,
address: null,
fullName: undefined,
avatar: null,
login: undefined,
password: undefined
};
},
componentWillMount: function() {
var me = Tinode.getMeTopic();
me.onMetaDesc = this.onMetaDesc;
var defacs = me.getDefaultAccess();
this.setState({
fullName: me.public ? me.public.fn : undefined,
avatar: makeImageUrl(me.public ? me.public.photo : null),
address: Tinode.getCurrentUserID(),
login: this.props.login,
auth: defacs.auth,
anon: defacs.anon
});
},
componentWillUnmount: function() {
var me = Tinode.getMeTopic();
me.onMetaDesc = undefined;
},
onMetaDesc: function(desc) {
// Update form with changed values
},
handleFullNameUpdate: function(fn) {
this.setState({updated: true, fullName: fn});
},
handlePasswordUpdate: function(pwd) {
this.setState({updated: true, password: pwd});
},
handleImageChanged: function(img) {
this.setState({updated: true, avatar: img});
},
handleSubmit: function(e) {
e.preventDefault();
if (this.state.updated) {
this.props.onSubmit(this.state.password,
vcard(this.state.fullName, this.state.avatar));
}
},
render: function() { render: function() {
return ( return (
<form id="edit-account"> <form id="edit-account" className="panel-form" onSubmit={this.handleSubmit}>
<p>Not implemented</p> <div className="panel-form-one">
<div className="panel-form-two">
<div><label className="small">Your name</label></div>
<div><InPlaceEdit
placeholder="Full name, e.g. John Doe"
text={this.state.fullName}
onFinished={this.handleFullNameUpdate} /></div>
<div><label className="small">Password</label></div>
<div><InPlaceEdit
placeholder="Unchanged"
type="password"
onFinished={this.handlePasswordUpdate} /></div>
</div>
<AvatarUpload
avatar={this.state.avatar}
onImageChanged={this.handleImageChanged} />
</div>
<br />
<hr />
<br />
<div>
<div>Login: <tt>{this.state.login}</tt></div>
<div>Address: <tt>{this.state.address}</tt></div>
<div>Default access mode:</div>
<div>&nbsp;&nbsp;Auth: <tt>{this.state.auth}</tt></div>
<div>&nbsp;&nbsp;Anon: <tt>{this.state.anon}</tt></div>
</div>
<div className="dialog-buttons">
<button className="blue" type="submit">Update</button>
</div>
</form> </form>
); );
} }
@@ -866,20 +1019,7 @@
} }
}, },
handleGroupSubmit: function(name, dataUrl) { handleGroupSubmit: function(name, dataUrl) {
var vcard = null; this.props.onCreateTopic(undefined, vcard(name, dataUrl));
if (name || dataUrl) {
vcard = {
fn: name,
};
if (dataUrl) {
var dataStart = dataUrl.indexOf(",");
vcard.photo = {
data: dataUrl.substring(dataStart+1),
type: "jpg"
};
}
}
this.props.onCreateTopic(undefined, vcard);
}, },
render: function() { render: function() {
return ( return (
@@ -1026,9 +1166,118 @@
); );
} }
}); });
/* END Create new topic and invite users or send an invite */ /* END Create new topic and invite users or send an invite */
/* BEGIN InfoView: panel with topic/user info */
var InfoView = React.createClass({
getInitialState: function() {
return {
updated: false,
address: null,
fullName: undefined,
avatar: null,
private: null
};
},
componentWillMount: function() {
var topic = Tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
topic.onMetaDesc = this.onMetaDesc;
var defacs = topic.getDefaultAccess() || {};
this.setState({
fullName: topic.public ? topic.public.fn : undefined,
avatar: makeImageUrl(topic.public ? topic.public.photo : null),
private: topic.private,
address: topic.name,
auth: defacs.auth,
anon: defacs.anon
});
},
componentWillUnmount: function() {
var topic = Tinode.getTopic(this.props.topic);
if (!topic) {
return;
}
topic.onMetaDesc = undefined;
},
onMetaDesc: function(desc) {
// Update form with changed values
},
handleFullNameUpdate: function(fn) {
this.setState({updated: true, fullName: fn});
},
handlePrivateUpdate: function(priv) {
this.setState({updated: true, private: priv});
},
handleImageChanged: function(img) {
this.setState({updated: true, avatar: img});
},
handleSubmit: function(e) {
e.preventDefault();
if (this.state.updated) {
this.props.onSubmit(this.props.topic,
vcard(this.state.fullName,this.state.avatar),
this.state.private);
}
},
render: function() {
return (
<div id="info-view">
<div className="caption-panel" id="info-caption-panel">
<div className="panel-title" id="info-title">Info</div>
<div>
<MenuCancel onCancel={this.props.onCancel} />
</div>
</div>
<form id="edit-account" className="panel-form" onSubmit={this.handleSubmit}>
<div className="panel-form-one">
<div className="panel-form-two">
<div><label className="small">Name</label></div>
<div><InPlaceEdit
placeholder="Group name"
text={this.state.fullName}
onFinished={this.handleFullNameUpdate} /></div>
<div><label className="small">Private comment</label></div>
<div><InPlaceEdit
placeholder="Visible to you only"
text={this.state.private}
onFinished={this.handlePrivateUpdate} /></div>
</div>
<AvatarUpload
avatar={this.state.avatar}
onImageChanged={this.handleImageChanged} />
</div>
<hr />
<br />
<div>
<div>Address: <tt>{this.state.address}</tt></div>
{this.state.auth || this.state.anon ?
<div>
<div>Default access mode:</div>
<div>&nbsp;&nbsp;Auth: <tt>{this.state.auth}</tt></div>
<div>&nbsp;&nbsp;Anon: <tt>{this.state.anon}</tt></div>
</div>
:
null
}
</div>
<div className="dialog-buttons">
<button className="blue" type="submit">Update</button>
</div>
</form>
</div>
);
}
});
/* END InfoView */
/* BEGIN Conversation panel */ /* BEGIN Conversation panel */
var Received = { var Received = {
UNDEF: -2, UNDEF: -2,
@@ -1132,6 +1381,7 @@
// 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.leave(false);
oldTopic.onData = undefined; oldTopic.onData = undefined;
oldTopic.onInfo = undefined; oldTopic.onInfo = undefined;
oldTopic.onMetaDesc = undefined; oldTopic.onMetaDesc = undefined;
@@ -1269,9 +1519,9 @@
var avatar = this.state.avatar || true; var avatar = this.state.avatar || true;
component = ( component = (
<div id="topic-view" className={this.props.hideSelf ? 'nodisplay' : null}> <div id="topic-view" className={this.props.hideSelf ? 'nodisplay' : null}>
<div id="topic-caption-panel"> <div id="topic-caption-panel" className="caption-panel">
{this.props.displayMobile ? {this.props.displayMobile ?
<a href="#" id="hide-message-view" onClick={this.props.onHideMessageView}> <a href="#" id="hide-message-view" onClick={this.props.onHideMessagesView}>
<i className="material-icons big">arrow_back</i> <i className="material-icons big">arrow_back</i>
</a> </a>
: :
@@ -1287,7 +1537,7 @@
<div id="topic-title" className="panel-title">{this.state.title}</div> <div id="topic-title" className="panel-title">{this.state.title}</div>
<div id="topic-last-seen">{lastSeen}</div> <div id="topic-last-seen">{lastSeen}</div>
</div> </div>
<div id="topic-users">users online and menu buttons</div> <div id="topic-users"></div>
<div> <div>
<a href="#" onClick={this.handleContextClick}> <a href="#" onClick={this.handleContextClick}>
<i className="material-icons big">more_vert</i> <i className="material-icons big">more_vert</i>
@@ -1438,6 +1688,7 @@
loginDisabled: false, loginDisabled: false,
subscriptions: {}, subscriptions: {},
displayMobile: (window.innerWidth <= MEDIA_BREAKPOINT), displayMobile: (window.innerWidth <= MEDIA_BREAKPOINT),
showInfoPanel: false,
mobilePanel: 'sidepanel', mobilePanel: 'sidepanel',
contextMenuVisible: false, contextMenuVisible: false,
contextMenuBounds: null, contextMenuBounds: null,
@@ -1534,6 +1785,7 @@
topicSelected: '', topicSelected: '',
loginDisabled: false, loginDisabled: false,
subscriptions: {}, subscriptions: {},
showInfoPanel: false,
mobilePanel: 'sidepanel', mobilePanel: 'sidepanel',
contextMenuVisible: false, contextMenuVisible: false,
contextMenuBounds: null, contextMenuBounds: null,
@@ -1561,14 +1813,22 @@
handleTopicSelected: function(topicName) { handleTopicSelected: function(topicName) {
if (topicName) { if (topicName) {
// Contact selected // Contact selected
this.setState({topicSelected: topicName, mobilePanel: 'topic-view'}); this.setState({
topicSelected: topicName,
mobilePanel: 'topic-view',
showInfoPanel: false
});
} else { } else {
// Currently selected contact deleted // Currently selected contact deleted
this.setState({topicSelected: null, mobilePanel: 'sidepanel'}); this.setState({
topicSelected: null,
mobilePanel: 'sidepanel',
showInfoPanel: false
});
} }
}, },
// In mobile view user requested to show sidepanel // In mobile view user requested to show sidepanel
handleHideMessageView: function() { handleHideMessagesView: function() {
this.setState({mobilePanel: 'sidepanel'}); this.setState({mobilePanel: 'sidepanel'});
}, },
// User clicked on a message send button. // User clicked on a message send button.
@@ -1585,8 +1845,8 @@
var instance = this; var instance = this;
Tinode.connect(this.state.serverAddress) Tinode.connect(this.state.serverAddress)
.then(function() { .then(function() {
return Tinode.createUserBasic(login_, password_, return Tinode.createAccountBasic(login_, password_,
{login: "basic", public: public_, private: private_}); {login: true, public: public_, private: private_});
}) })
.then(function() { .then(function() {
instance.handleLoginSuccessful(instance); instance.handleLoginSuccessful(instance);
@@ -1595,6 +1855,15 @@
instance.setState({errorText: err.message}); instance.setState({errorText: err.message});
}); });
}, },
handleUpdateAccountRequest: function(password, pub, priv) {
this.handleSidepanelCancel();
var me = Tinode.getMeTopic();
me.setMeta({desc: {public: pub, private: priv}});
if (password) {
Tinode.updateAccountBasic(null, Tinode.getCurrentLogin(), password);
}
},
// User chose Settings menu item. // User chose Settings menu item.
handleSettings: function() { handleSettings: function() {
this.setState({sidePanelSelected: this.state.myUserId ? 'edit-account' : 'settings'}); this.setState({sidePanelSelected: this.state.myUserId ? 'edit-account' : 'settings'});
@@ -1619,7 +1888,7 @@
this.setState({sidePanelSelected: 'new-topic'}); this.setState({sidePanelSelected: 'new-topic'});
}, },
// Create of a new topic. New P2P topic requires peer's name. // Create of a new topic. New P2P topic requires peer's name.
handleNewTopicRequest: function(peerName, vcard) { handleNewTopicRequest: function(peerName, pub) {
var instance = this; var instance = this;
// Check if user clicked on an existing connection. // Check if user clicked on an existing connection.
@@ -1635,7 +1904,7 @@
var params = {get: {desc: {}, data: {}}}; var params = {get: {desc: {}, data: {}}};
if (!peerName) { if (!peerName) {
params.get.sub = {}; // Fetch subs for grp topics only. Subscriptions for a p2p topic are useless. params.get.sub = {}; // Fetch subs for grp topics only. Subscriptions for a p2p topic are useless.
params.set = {desc: {public: vcard}}; params.set = {desc: {public: pub}};
} }
topic.subscribe(params).then(function() { topic.subscribe(params).then(function() {
instance.setState({ instance.setState({
@@ -1649,6 +1918,11 @@
}); });
} }
}, },
handleTopicUpdateRequest: function(topicName, pub, priv) {
var topic = Tinode.getTopic(topicName);
topic.setMeta({desc: {public: pub, private: priv}});
},
/** Called when the user enters a contact into the contact search field in the NewAccount panel /** Called when the user enters a contact into the contact search field in the NewAccount panel
@param query {Array} is an array of contacts to search for @param query {Array} is an array of contacts to search for
*/ */
@@ -1710,8 +1984,7 @@
switch (action) { switch (action) {
case "Info": case "Info":
// TODO(gene): implement topic info this.handleShowInfoView();
console.log("Topic info not implemented");
break; break;
case "Clear messages": case "Clear messages":
topic.delMessagesAll(false).then(function(){ topic.delMessagesAll(false).then(function(){
@@ -1738,7 +2011,12 @@
break; break;
} }
}, },
handleShowInfoView: function() {
this.setState({showInfoPanel: true});
},
handleHideInfoView: function() {
this.setState({showInfoPanel: false});
},
render: function() { render: function() {
return ( return (
<div id="app-container"> <div id="app-container">
@@ -1776,6 +2054,7 @@
onSettings={this.handleSettings} onSettings={this.handleSettings}
onLoginRequest={this.handleLoginRequest} onLoginRequest={this.handleLoginRequest}
onCreateAccount={this.handleNewAccountRequest} onCreateAccount={this.handleNewAccountRequest}
onUpdateAccount={this.handleUpdateAccountRequest}
onTopicSelected={this.handleTopicSelected} onTopicSelected={this.handleTopicSelected}
onSearchContacts={this.handleSearchContacts} onSearchContacts={this.handleSearchContacts}
onCreateTopic={this.handleNewTopicRequest} onCreateTopic={this.handleNewTopicRequest}
@@ -1785,7 +2064,7 @@
showContextMenu={this.handleShowContextMenu} /> showContextMenu={this.handleShowContextMenu} />
<MessagesView <MessagesView
onHideMessageView={this.handleHideMessageView} onHideMessagesView={this.handleHideMessagesView}
displayMobile={this.state.displayMobile} displayMobile={this.state.displayMobile}
hideSelf={(this.state.displayMobile && this.state.mobilePanel !== 'topic-view')} hideSelf={(this.state.displayMobile && this.state.mobilePanel !== 'topic-view')}
topic={this.state.topicSelected} topic={this.state.topicSelected}
@@ -1796,6 +2075,15 @@
showContextMenu={this.handleShowContextMenu} showContextMenu={this.handleShowContextMenu}
sendMessage={this.handleSendMessage} /> sendMessage={this.handleSendMessage} />
{this.state.showInfoPanel ?
<InfoView
topic={this.state.topicSelected}
onSubmit={this.handleTopicUpdateRequest}
onCancel={this.handleHideInfoView}
/>
:
null
}
</div> </div>
); );
} }