user discovery

This commit is contained in:
Gene S
2016-02-29 09:32:38 -08:00
parent 58d02cf4bc
commit ed49b8b10f
2 changed files with 335 additions and 65 deletions
+140 -13
View File
@@ -2,6 +2,8 @@
box-sizing: border-box;
padding: 0;
margin: 0;
font-family: Roboto, Arial, sans-serif;
text-rendering: optimizeLegibility;
}
/* Basic elements defined */
@@ -9,7 +11,6 @@
html, body {
height:100%;
width:100%;
font-family: 'Roboto', Arial, sans-serif;
font-size: 10pt;
color: #666;
overflow: hidden;
@@ -173,6 +174,42 @@ form {
font-size: 1.2rem;
}
/* BEGIN Popup/dropdown menu */
ul.menu {
position: absolute;
left: -1.5rem;
top: 1.5rem;
list-style: none;
width: 12rem;
border-radius: 0.2rem;
padding-top: 0.35rem;
padding-bottom: 0.35rem;
box-shadow: 0 1px 6px rgba(0, 0, 0, .45);
background-color: white;
z-index: 3;
}
ul.menu > li {
padding: 0.75rem 0.75rem 0.75rem 1.25rem;
}
ul.menu > li:hover {
background-color: #f5f5f5;
}
ul.menu > li.separator {
padding: 0;
margin: 2px;
height: 1px;
background-color: #ccc;
}
ul.menu > li.separator:hover {
background-color: #ccc;
}
/* END popup/dropdown menu */
/* END of basic elements */
#mountPoint {
@@ -297,18 +334,22 @@ form {
/* Area which displays an error message */
#alert-box {
padding: 0.5em;
background-color: transparent;
height: 0;
overflow: hidden;
}
#alert-box.error-alert {
padding: 0.5em;
background-color: #ffcdd2;
border-color: #d32f2f;
color: #600;
color: #b71c1c;
height: auto;
}
/* List of contacts */
#contacts-panel {
overflow-y: auto;
margin-top: 1rem;:
margin-bottom: auto;
height: 100%;
}
@@ -317,16 +358,15 @@ form {
list-style: none;
}
#contact-box li {
#contact-box > li {
display: flex;
padding: 0.35rem 0.25rem 0.5rem 0.35rem;
min-height: 3.5rem;
overflow: hidden;
}
#contact-box li:hover {
#contact-box > li:hover {
background-color: #eceff1; /* blue-gray 50 highlight */
}
#contact-box li.selected {
#contact-box > li.selected {
background-color: #cfd8dc; /* blue-gray 100 highlight */
}
@@ -334,6 +374,26 @@ form {
font-size: 1.05rem;
}
/* The down arrow head - trigger for context menu in contacts list */
#contact-box li .menuTrigger {
position: relative;
margin-left: auto;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background-color: transparent;
}
#contact-box li:hover .menuTrigger {
background-color: white;
}
#contact-box li .menuTrigger a {
color: transparent;
}
#contact-box li:hover .menuTrigger a {
color: #757575;
}
/* Avatar and online-offline indicator */
#contact-box .avatar-box {
margin-right: 0.35rem;
@@ -363,26 +423,33 @@ form {
#contact-box .offline {
color: #888;
}
/* END of avatar box */
#add-topic {
position: absolute;
right: 1em;
bottom: 1em;
}
/* END of avatar box */
/* New account panel (signup form) */
#signup-form {
/* Various forms inside a panel (signup form, new topic form) */
.panel-form {
display: flex;
flex-direction: column;
}
#signup-form-one {
.panel-form-one {
display: flex;
justify-content: space-between;
justify-content: flex-start;
align-items: center;
}
.panel-form-two {
display: flex;
flex-direction: column;
width: 100%;
}
/* Styling of avatar upload area in the New Account panel */
.avatar-upload {
position: relative;
@@ -698,9 +765,69 @@ form {
padding: 0.35rem 1.5rem;
}
/* END of modal dialog */
/* BEGIN Tabs */
.tabbar {
padding-top: 1rem;
background: #eeeeee; /* #2196f3; */
}
.tabbar > ul {
list-style:none;
margin:0;
width:100%;
overflow:hidden;
padding:0;
margin-left:1rem;
}
.tabbar > ul > li {
float: left;
}
.tabbar > ul > li > a {
position: relative;
text-decoration: none;
text-transform: uppercase;
display: block;
min-width: 6rem;
text-align: center;
line-height: 2rem;
font-weight: 400;
font-size: 1.1rem;
color: rgba(33,150,243,0.6);
overflow: hidden;
box-shadow: inset 0 -2px 2.5px -1px rgba(0,0,0,0.5);
}
.tabbar li.active > a {
color: #2196f3;
}
.tabbar li.active > a:after {
height:0.15rem;
display:block;
content:"";
bottom:0;
left:0;
position: absolute;
background: #2196f3;
animation: tabbar-border-expand 0.2s cubic-bezier(0.4, 0.0, 0.4, 1) 0s alternate forwards;
}
@keyframes tabbar-border-expand {
0% {
opacity:0;
width:0;
}
100% {
opacity:1;
width:100%;
}
}
/* END Tabs */
/* Miscellaneous */
/* Badge with the number of unread messages */
+195 -52
View File
@@ -27,8 +27,8 @@
var APP_NAME = "TinodeReact/0.6";
// Host name and port 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";
@@ -66,6 +66,68 @@
return photo ? 'data:image/' + photo.type + ';base64,' + photo.data : null;
}
/* BEGIN Popup/dropdown menu */
var Menu = React.createClass({
getInitialState: function() {
return {
visible: this.props.visible
};
},
componentWillUnmount: function() {
document.removeEventListener('mousedown', this.handlePageClick, false);
},
componentWillReceiveProps: function(nextProps) {
this.setState({visible: nextProps.visible});
if (nextProps.visible) {
document.addEventListener('mousedown', this.handlePageClick, false);
} else {
document.removeEventListener('mousedown', this.handlePageClick, false);
}
},
handlePageClick: function(e) {
if (ReactDOM.findDOMNode(this).contains(e.target)) {
console.log("click in menu, exiting");
return;
}
e.stopPropagation();
document.removeEventListener('mousedown', this.handlePageClick);
console.log("click outside of menu");
this.setState({visible: false});
},
handleClick: function(e) {
e.preventDefault();
e.stopPropagation();
this.setState({visible: false});
this.props.onResult(this.props.topic, this.props.items[e.currentTarget.dataset.id]);
},
render: function() {
if (!this.state.visible) {
return null;
}
var count = 0;
var menuItems = [];
var instance = this;
this.props.items.map(function(item) {
if (item === "-") {
menuItems.push(
<li className="separator" key={count} />
);
} else {
menuItems.push(
<li onClick={instance.handleClick} data-id={count} key={count}>{item}</li>
);
}
count++;
});
return (
<ul className="menu">
{menuItems}
</ul>
);
}
});
/* END Popup/dropdown menu */
/* BEGIN Login: a login form */
var LoginView = React.createClass({
getInitialState: function() {
@@ -179,9 +241,9 @@
}
return (
<div>
<form id="signup-form" onSubmit={this.handleSubmit} onCancel={this.props.onCancel}>
<div id="signup-form-one">
<div>
<form className="panel-form" onSubmit={this.handleSubmit} onCancel={this.props.onCancel}>
<div className="panel-form-one">
<div className="panel-form-two">
<input type="text" placeholder="Login"
value={this.state.login} onChange={this.handleLoginChange} required autofocus />
<input type="password" placeholder="Password"
@@ -189,7 +251,7 @@
<input type="password" placeholder="Repeat password"
value={this.state.password2} onChange={this.handlePassword2Change} required />
</div>
<CreateAccountView.AvatarUpload onImageChanged={this.handleImageChanged} />
<AvatarUpload onImageChanged={this.handleImageChanged} />
</div>
<input type="text" placeholder="Full name, e.g. John Doe"
value={this.state.fn} onChange={this.handleFnChange} />
@@ -202,7 +264,7 @@
}
});
CreateAccountView.AvatarUpload = React.createClass({
var AvatarUpload = React.createClass({
getInitialState: function() {
return {
dataUrl: null
@@ -248,6 +310,65 @@
});
/* END Account registration */
/* BEGIN Create new topic and invite users or send an invite */
var NewTopicView = React.createClass({
getInitialState: function() {
return {
tabSelected: "p2p",
fn: '', // full/formatted name
imageDataUrl: null
};
},
handleTabClick: function(e) {
e.preventDefault();
this.setState({tabSelected: e.currentTarget.dataset.id});
},
handleFnChange: function(e) {
this.setState({fn: e.target.value});
},
handleImageChanged: function(img) {
this.setState({imageDataUrl: img});
},
handleGroupSubmit: function(e) {
e.preventDefault();
},
render: function() {
var submitClasses = "blue";
if (this.props.disabled) {
submitClasses += " disabled";
}
return (
<div>
<div className="tabbar">
<ul>
<li className={this.state.tabSelected === "p2p" ? "active" : null}>
<a href="#" data-id="p2p" onClick={this.handleTabClick}>1:1</a>
</li>
<li className={this.state.tabSelected === "grp" ? "active" : null}>
<a href="#" data-id="grp" onClick={this.handleTabClick}>group</a>
</li>
</ul>
</div>
{this.state.tabSelected === "grp" ?
<form className="panel-form" onSubmit={this.handleGroupSubmit} onCancel={this.props.onCancel}>
<div className="panel-form-one">
<div className="panel-form-two">
<input type="text" placeholder="Name of the topic to create"
value={this.state.fn} onChange={this.handleFnChange} autofocus required />
<div className="dialog-buttons">
<button className={submitClasses} type="submit">Create</button>
</div>
</div>
<AvatarUpload onImageChanged={this.handleImageChanged} />
</div>
</form>
: null}
</div>
);
}
});
/* END Create new topic and invite users or send an invite */
/* BEGIN Tinode config panel */
var SettingsView = React.createClass({
getInitialState: function() {
@@ -292,7 +413,7 @@
});
/* END Tinode config panel */
/* BEGIN Manage side panel - handle Login, Account Registration, and Contacts views */
/* BEGIN Manage side panel - handle Login, Account Registration, Contacts, NewTopic views */
var SideNavbar = React.createClass({
render: function() {
var avatar = null;
@@ -307,14 +428,17 @@
<div id="sidepanel-title" className="panel-title">{this.props.title}</div>
{this.props.state === 'login' ?
<SideNavbar.MenuStart onSignUp={this.props.onSignUp} onSettings={this.props.onSettings} /> :
this.props.state === 'settings' ?
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> :
this.props.state === 'edit-account' ?
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> :
this.props.state === 'register' ?
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> :
this.props.state === 'contacts' ?
<SideNavbar.MenuSettings onSettings={this.props.onSettings} /> : null}
this.props.state === 'settings' ?
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> :
this.props.state === 'edit-account' ?
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> :
this.props.state === 'register' ?
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> :
this.props.state === 'contacts' ?
<SideNavbar.MenuContacts onNewTopic={this.props.onNewTopic} onSettings={this.props.onSettings} /> :
this.props.state === 'new-topic' ?
<SideNavbar.MenuCancel onCancel={this.props.onCancel} /> :
null}
</div>
);
}
@@ -340,10 +464,14 @@
}
})
SideNavbar.MenuSettings = React.createClass({
SideNavbar.MenuContacts = React.createClass({
render: function() {
return (
<a href="#" onClick={this.props.onSettings}><i className="material-icons">settings</i></a>
<div>
<a href="#" onClick={this.props.onNewTopic}><i className="material-icons">add_circle</i></a>
&nbsp;
<a href="#" onClick={this.props.onSettings}><i className="material-icons">settings</i></a>
</div>
);
}
})
@@ -376,6 +504,10 @@
case 'contacts':
title = this.props.title;
avatar = this.props.avatar;
break;
case 'new-topic':
title = "Start New Chat";
break;
default:;
};
return (
@@ -384,6 +516,7 @@
title={title} avatar={avatar}
onSignUp={this.props.onSignUp}
onSettings={this.props.onSettings}
onNewTopic={this.props.onNewTopic}
onCancel={this.props.onCancel} />
<SidepanelView.ErrorPanel
@@ -409,8 +542,14 @@
this.props.state === 'contacts' ?
<ContactsView
data={this.props.subscriptions}
onTopicSelected={this.props.onTopicSelected}
onNewTopic={this.props.onNewTopic} /> :
onTopicSelected={this.props.onTopicSelected} /> :
this.props.state === 'new-topic' ?
<NewTopicView
data={this.props.subscriptions}
onContactSelected={this.props.onContactSelected}
onStartNewTopic={this.props.onStartNewTopic} /> :
null}
</div>
);
@@ -446,35 +585,33 @@
/* END EditAccount */
/* BEGIN Contact list (list of topics) */
/* A form for initiating a new topic
var NewTopic = React.createClass({
render: function() {
return (
<div className="input-group" id="newTopicInput">
<label htmlFor="addTopic" className="sr-only">Add user or topic</label>
<input id="addTopic" className="form-control" type="text" placeholder="Add topic" />
<span className="input-group-btn"><button className="btn btn-default" type="button">
<i className="material-icons">add</i>
</button></span>
</div>
);
}
});
*/
/* A single topic */
var contactMenuItems = ["Delete chat", "Mute", "Block"];
var Contact = React.createClass({
handleClick: function() {
this.props.onSelected(this.props.topic);
},
render: function() {
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 = this.props.avatar ?
getInitialState: function() {
return {
menuShownFor: null
};
},
handleClick: function() {
this.props.onSelected(this.props.topic);
},
handleContextClick: function(e) {
e.preventDefault();
e.stopPropagation();
this.setState({menuShownFor: this.props.topic});
},
handleMenuAction: function(topic, action) {
console.log("Menu clicked '" + action + "' for topic " + topic);
this.setState({menuShownFor: null});
},
render: function() {
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 = this.props.avatar ?
<img src={this.props.avatar} className="avatar" /> :
(Tinode.getTopicType(this.props.topic) === "grp") ?
<i className="material-icons">group</i> :
@@ -489,9 +626,18 @@
<span className="contact-title">{title}</span> <Contact.Unread count={this.props.unread} />
<p>{this.props.comment}</p>
</div>
<span className="menuTrigger">
<a href="#" onClick={this.handleContextClick}>
<i className="material-icons">arrow_drop_down</i>
</a>
<Menu items={contactMenuItems}
topic={this.props.topic}
onResult={this.handleMenuAction}
visible={this.state.menuShownFor ? true : false} />
</span>
</li>
);
}
}
});
/* The counter of unread messages in the topic */
@@ -573,9 +719,6 @@
<ul id="contact-box">
{contactNodes}
</ul>
<button className="round blue" id="add-topic" onClick={this.props.onNewTopic}>
<i className="material-icons big">add</i>
</button>
</div>
);
}
@@ -1046,7 +1189,7 @@
}
},
handleNewTopic: function() {
this.setState({dialogSelected: 'new-topic'});
this.setState({sidePanelSelected: 'new-topic'});
},
handleDialogCancel: function() {
this.setState({dialogSelected: null});