user discovery
This commit is contained in:
+140
-13
@@ -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
@@ -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>
|
||||
|
||||
<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});
|
||||
|
||||
Reference in New Issue
Block a user