topic creation and context menu fixes
This commit is contained in:
+29
-21
@@ -347,35 +347,35 @@ ul.menu > li.separator:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* List of contacts */
|
/* List of contacts */
|
||||||
#contacts-panel {
|
.contacts-panel {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin-top: 1rem;:
|
margin-top: 1rem;:
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#contact-box {
|
.contact-box {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#contact-box > li {
|
.contact-box > li {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.35rem 0.25rem 0.5rem 0.35rem;
|
padding: 0.35rem 0.25rem 0.5rem 0.35rem;
|
||||||
min-height: 3.5rem;
|
min-height: 3.5rem;
|
||||||
}
|
}
|
||||||
#contact-box > li:hover {
|
.contact-box > li:hover {
|
||||||
background-color: #eceff1; /* blue-gray 50 highlight */
|
background-color: #eceff1; /* blue-gray 50 highlight */
|
||||||
}
|
}
|
||||||
#contact-box > li.selected {
|
.contact-box > li.selected {
|
||||||
background-color: #cfd8dc; /* blue-gray 100 highlight */
|
background-color: #cfd8dc; /* blue-gray 100 highlight */
|
||||||
}
|
}
|
||||||
|
|
||||||
#contact-box .contact-title {
|
.contact-box .contact-title {
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The down arrow head - trigger for context menu in contacts list */
|
/* The down arrow head - trigger for context menu in contacts list */
|
||||||
#contact-box li .menuTrigger {
|
.contact-box li .menuTrigger {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
@@ -383,19 +383,19 @@ ul.menu > li.separator:hover {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
#contact-box li:hover .menuTrigger {
|
.contact-box li:hover .menuTrigger {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
#contact-box li .menuTrigger a {
|
.contact-box li .menuTrigger a {
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
#contact-box li:hover .menuTrigger a {
|
.contact-box li:hover .menuTrigger a {
|
||||||
color: #757575;
|
color: #757575;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Avatar and online-offline indicator */
|
/* Avatar and online-offline indicator */
|
||||||
#contact-box .avatar-box {
|
.contact-box .avatar-box {
|
||||||
margin-right: 0.35rem;
|
margin-right: 0.35rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
@@ -403,25 +403,32 @@ ul.menu > li.separator:hover {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
#contact-box .avatar {
|
.contact-box .avatar {
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
.contact-box .text-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
}
|
||||||
|
|
||||||
/* Online-offline indicator positioned inside avatar-box */
|
/* Online-offline indicator positioned inside avatar-box */
|
||||||
#contact-box .online, .offline {
|
.contact-box .online, .offline {
|
||||||
font-size: 1.5em;
|
display: inline-block;
|
||||||
line-height: 1;
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
border-radius: 50%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -0.1rem;
|
right: 0;
|
||||||
bottom: -0.1rem;
|
bottom: 0;
|
||||||
}
|
}
|
||||||
#contact-box .online {
|
.contact-box .online {
|
||||||
color: #4caf50;
|
background-color: #4caf50;
|
||||||
}
|
}
|
||||||
#contact-box .offline {
|
.contact-box .offline {
|
||||||
color: #888;
|
background-color: #888;
|
||||||
}
|
}
|
||||||
/* END of avatar box */
|
/* END of avatar box */
|
||||||
|
|
||||||
@@ -840,6 +847,7 @@ ul.menu > li.separator:hover {
|
|||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
min-width: 1.75em;
|
min-width: 1.75em;
|
||||||
|
margin-left: 0.5rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+363
-203
@@ -66,68 +66,6 @@
|
|||||||
return photo ? 'data:image/' + photo.type + ';base64,' + photo.data : null;
|
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 */
|
/* BEGIN Login: a login form */
|
||||||
var LoginView = React.createClass({
|
var LoginView = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
@@ -224,9 +162,9 @@
|
|||||||
fn: this.state.name.trim(),
|
fn: this.state.name.trim(),
|
||||||
};
|
};
|
||||||
if (this.state.imageDataUrl) {
|
if (this.state.imageDataUrl) {
|
||||||
dataStart = this.state.imageDataUrl.indexOf(",");
|
var dataStart = this.state.imageDataUrl.indexOf(",");
|
||||||
vcard.photo = {
|
vcard.photo = {
|
||||||
data: this.state.imageDataUrl.substring(dataStart),
|
data: this.state.imageDataUrl.substring(dataStart+1),
|
||||||
type: "jpg"
|
type: "jpg"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -288,8 +226,11 @@
|
|||||||
}
|
}
|
||||||
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, 64, 64);
|
||||||
instance.setState({dataUrl: canvas.toDataURL("image/jpg")});
|
var bits = canvas.toDataURL("image/jpg");
|
||||||
canvas = null;
|
canvas = null;
|
||||||
|
|
||||||
|
instance.setState({dataUrl: bits});
|
||||||
|
instance.props.onImageChanged(bits);
|
||||||
};
|
};
|
||||||
img.src = URL.createObjectURL(e.target.files[0]);
|
img.src = URL.createObjectURL(e.target.files[0]);
|
||||||
},
|
},
|
||||||
@@ -310,94 +251,6 @@
|
|||||||
});
|
});
|
||||||
/* END Account registration */
|
/* 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
|
|
||||||
search: null, // search string
|
|
||||||
imageDataUrl: null
|
|
||||||
};
|
|
||||||
},
|
|
||||||
componentWillMount: function() {
|
|
||||||
var fnd = Tinode.getFndTopic();
|
|
||||||
fnd.onSubsUpdated = this.fndSubsUpdated;
|
|
||||||
fnd.subscribe()
|
|
||||||
.then(function() {
|
|
||||||
fnd.setMeta({desc: {private: ["tel:17025550005"]}});
|
|
||||||
})
|
|
||||||
.then(function() {
|
|
||||||
fnd.getMeta({sub: {}});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
var fnd = Tinode.getFndTopic();
|
|
||||||
fnd.onSubsUpdated = undefined;
|
|
||||||
},
|
|
||||||
fndSubsUpdated: function(keys) {
|
|
||||||
console.log(keys);
|
|
||||||
//this.forceUpdate();
|
|
||||||
},
|
|
||||||
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}>
|
|
||||||
<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}
|
|
||||||
<form className="panel-form" onSubmit={this.handleFindContacts}>
|
|
||||||
<div className="panel-form-one">
|
|
||||||
<input type="text" placeholder="Find people by email or phone" value={this.state.search}
|
|
||||||
onChange={this.handleSearchChange} required />
|
|
||||||
<a href="#" onClick={this.handleSearch}>
|
|
||||||
<i className="material-icons">search</i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
/* END Create new topic and invite users or send an invite */
|
|
||||||
|
|
||||||
/* BEGIN Tinode config panel */
|
/* BEGIN Tinode config panel */
|
||||||
var SettingsView = React.createClass({
|
var SettingsView = React.createClass({
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
@@ -571,13 +424,14 @@
|
|||||||
this.props.state === 'contacts' ?
|
this.props.state === 'contacts' ?
|
||||||
<ContactsView
|
<ContactsView
|
||||||
data={this.props.subscriptions}
|
data={this.props.subscriptions}
|
||||||
|
showContextMenu={this.props.showContextMenu}
|
||||||
onTopicSelected={this.props.onTopicSelected} /> :
|
onTopicSelected={this.props.onTopicSelected} /> :
|
||||||
|
|
||||||
this.props.state === 'new-topic' ?
|
this.props.state === 'new-topic' ?
|
||||||
<NewTopicView
|
<NewTopicView
|
||||||
data={this.props.subscriptions}
|
data={this.props.subscriptions}
|
||||||
onContactSelected={this.props.onContactSelected}
|
onContactSelected={this.props.onContactSelected}
|
||||||
onStartNewTopic={this.props.onStartNewTopic} /> :
|
onCreateTopic={this.props.onCreateTopic} /> :
|
||||||
|
|
||||||
null}
|
null}
|
||||||
</div>
|
</div>
|
||||||
@@ -614,29 +468,27 @@
|
|||||||
/* END EditAccount */
|
/* END EditAccount */
|
||||||
|
|
||||||
/* BEGIN Contact list (list of topics) */
|
/* BEGIN Contact list (list of topics) */
|
||||||
/* A single topic */
|
/* A single topic */;
|
||||||
var contactMenuItems = ["Delete chat", "Mute", "Block"];
|
|
||||||
var Contact = React.createClass({
|
var Contact = React.createClass({
|
||||||
getInitialState: function() {
|
handleClick: function(e) {
|
||||||
return {
|
e.preventDefault();
|
||||||
menuShownFor: null
|
e.stopPropagation();
|
||||||
};
|
|
||||||
},
|
|
||||||
handleClick: function() {
|
|
||||||
this.props.onSelected(this.props.topic);
|
this.props.onSelected(this.props.topic);
|
||||||
},
|
},
|
||||||
handleContextClick: function(e) {
|
handleContextClick: function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this.setState({menuShownFor: this.props.topic});
|
console.log("pageX=" + e.pageX + ", pageY=" + e.pageY);
|
||||||
|
this.props.showContextMenu(e.pageY, e.pageX, ["Delete chat", "Mute", "Block"], this.handleMenuAction);
|
||||||
},
|
},
|
||||||
handleMenuAction: function(topic, action) {
|
handleMenuAction: function(action) {
|
||||||
console.log("Menu clicked '" + action + "' for topic " + topic);
|
console.log("Menu clicked '" + action + "' for topic " + this.props.topic);
|
||||||
this.setState({menuShownFor: null});
|
|
||||||
},
|
},
|
||||||
render: function() {
|
render: function() {
|
||||||
var title = this.props.title;
|
var title = this.props.title;
|
||||||
if (title.length > 30) {
|
if (!title) {
|
||||||
|
title = <i>unknown</i>;
|
||||||
|
} else if (title.length > 30) {
|
||||||
title = title.substring(0, 28) + "...";
|
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;
|
||||||
@@ -649,26 +501,31 @@
|
|||||||
<li className={selected} onClick={this.handleClick}>
|
<li className={selected} onClick={this.handleClick}>
|
||||||
<div className="avatar-box">
|
<div className="avatar-box">
|
||||||
{avatar}
|
{avatar}
|
||||||
<span className={this.props.now ? "online" : "offline"}>●</span>
|
{this.props.now ? <span className="online"></span> : null}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="text-box">
|
||||||
<span className="contact-title">{title}</span> <Contact.Unread count={this.props.unread} />
|
<div><span className="contact-title">{title}</span>
|
||||||
<p>{this.props.comment}</p>
|
{this.props.unread > 0 ? <Contact.Unread count={this.props.unread} /> : null}</div>
|
||||||
|
{this.props.comment ? <div>{this.props.comment}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
<span className="menuTrigger">
|
{this.props.showContextMenu ?
|
||||||
<a href="#" onClick={this.handleContextClick}>
|
<span className="menuTrigger">
|
||||||
<i className="material-icons">arrow_drop_down</i>
|
<a href="#" onClick={this.handleContextClick}>
|
||||||
</a>
|
<i className="material-icons">expand_more</i>
|
||||||
<Menu items={contactMenuItems}
|
</a>
|
||||||
topic={this.props.topic}
|
</span> : null}
|
||||||
onResult={this.handleMenuAction}
|
|
||||||
visible={this.state.menuShownFor ? true : false} />
|
|
||||||
</span>
|
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
<ContextMenu items={this.props.contactMenuItems}
|
||||||
|
topic={this.props.topic}
|
||||||
|
onResult={this.handleMenuAction}
|
||||||
|
visible={this.state.menuShownFor ? true : false} />
|
||||||
|
*/
|
||||||
|
|
||||||
/* The counter of unread messages in the topic */
|
/* The counter of unread messages in the topic */
|
||||||
Contact.Unread = React.createClass({
|
Contact.Unread = React.createClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
@@ -697,6 +554,12 @@
|
|||||||
me.onData = this.tnInvite;
|
me.onData = this.tnInvite;
|
||||||
me.onSubsUpdated = this.tnMeSubsUpdated;
|
me.onSubsUpdated = this.tnMeSubsUpdated;
|
||||||
},
|
},
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
var me = Tinode.getMeTopic();
|
||||||
|
me.onContactUpdate = undefined;
|
||||||
|
me.onData = undefined;
|
||||||
|
me.onSubsUpdated = undefined;
|
||||||
|
},
|
||||||
tnMeContactUpdate: function(what, cont) {
|
tnMeContactUpdate: function(what, cont) {
|
||||||
if (what === "on" || what === "off" || what === "read") {
|
if (what === "on" || what === "off" || what === "read") {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
@@ -719,7 +582,7 @@
|
|||||||
tnMeSubsUpdated: function(names) {
|
tnMeSubsUpdated: function(names) {
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
},
|
},
|
||||||
onSelectionChange: function(sel) {
|
handleSelectionChange: function(sel) {
|
||||||
this.setState({selected: sel});
|
this.setState({selected: sel});
|
||||||
this.props.onTopicSelected(sel);
|
this.props.onTopicSelected(sel);
|
||||||
},
|
},
|
||||||
@@ -732,20 +595,21 @@
|
|||||||
}
|
}
|
||||||
contactNodes.push(
|
contactNodes.push(
|
||||||
<Contact
|
<Contact
|
||||||
title={c.public.fn}
|
title={c.public ? c.public.fn : null}
|
||||||
avatar={makeImageUrl(c.public.photo)}
|
avatar={makeImageUrl(c.public ? c.public.photo : null)}
|
||||||
comment={c.private}
|
comment={c.private}
|
||||||
unread={unread} now={c.online}
|
unread={unread} now={c.online}
|
||||||
selected={this.state.selected}
|
selected={this.state.selected}
|
||||||
onSelected={this.onSelectionChange}
|
onSelected={this.handleSelectionChange}
|
||||||
|
showContextMenu={this.props.showContextMenu}
|
||||||
topic={c.topic}
|
topic={c.topic}
|
||||||
key={c.topic} />
|
key={c.topic} />
|
||||||
);
|
);
|
||||||
}, this);
|
}, this);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="contacts-panel">
|
<div className="contacts-panel">
|
||||||
<ul id="contact-box">
|
<ul className="contact-box">
|
||||||
{contactNodes}
|
{contactNodes}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -754,6 +618,176 @@
|
|||||||
});
|
});
|
||||||
/* END Contact list */
|
/* END Contact list */
|
||||||
|
|
||||||
|
/* BEGIN Create new topic and invite users or send an invite */
|
||||||
|
var NewTopicView = React.createClass({
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
tabSelected: "p2p",
|
||||||
|
contactSelected: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
componentWillMount: function() {
|
||||||
|
var fnd = Tinode.getFndTopic();
|
||||||
|
fnd.onSubsUpdated = this.fndSubsUpdated;
|
||||||
|
fnd.subscribe()
|
||||||
|
.then(function() {
|
||||||
|
fnd.getMeta({sub: {}});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
var fnd = Tinode.getFndTopic();
|
||||||
|
fnd.onSubsUpdated = undefined;
|
||||||
|
},
|
||||||
|
fndSubsUpdated: function() {
|
||||||
|
//console.log(keys);
|
||||||
|
this.forceUpdate();
|
||||||
|
},
|
||||||
|
handleTabClick: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.setState({tabSelected: e.currentTarget.dataset.id, contactSelected: null});
|
||||||
|
},
|
||||||
|
handleContactSelected: function(sel) {
|
||||||
|
if (this.state.tabSelected === "p2p") {
|
||||||
|
this.props.onCreateTopic(sel, undefined);
|
||||||
|
} else {
|
||||||
|
console.log("handle group invite " + sel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleGroupSubmit: function(name, dataUrl) {
|
||||||
|
//this.setState({errorCleared: false});
|
||||||
|
var vcard = null;
|
||||||
|
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() {
|
||||||
|
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" ?
|
||||||
|
<NewTopicView.Group onSubmit={this.handleGroupSubmit} /> :
|
||||||
|
<NewTopicView.SelectContacts onContactSelected={this.handleContactSelected} type="p2p" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
NewTopicView.Group = React.createClass({
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
fn: '', // full/formatted name
|
||||||
|
imageDataUrl: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleFnChange: function(e) {
|
||||||
|
this.setState({fn: e.target.value});
|
||||||
|
},
|
||||||
|
handleImageChanged: function(img) {
|
||||||
|
console.log(img);
|
||||||
|
this.setState({imageDataUrl: img});
|
||||||
|
},
|
||||||
|
handleSubmit: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onSubmit(this.state.fn.trim(), this.state.imageDataUrl);
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
var submitClasses = "blue";
|
||||||
|
if (this.props.disabled) {
|
||||||
|
submitClasses += " disabled";
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<form className="panel-form" onSubmit={this.handleSubmit}>
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
NewTopicView.SelectContacts = React.createClass({
|
||||||
|
getInitialState: function() {
|
||||||
|
return {
|
||||||
|
search: null,
|
||||||
|
selectedUser: null
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleSelectionChange: function(sel) {
|
||||||
|
if (this.props.type === "p2p") {
|
||||||
|
this.props.onContactSelected(sel);
|
||||||
|
} else {
|
||||||
|
console.log(sel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSearchChange: function(e) {
|
||||||
|
this.setState({search: e.target.value});
|
||||||
|
},
|
||||||
|
handleSearch: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
console.log("will search for '" + this.state.search + "'");
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
var contactNodes = [];
|
||||||
|
Tinode.getFndTopic().subscribers(function(s) {
|
||||||
|
contactNodes.push(
|
||||||
|
<Contact
|
||||||
|
title={((s.public && s.public.fn) || s.private)}
|
||||||
|
avatar={makeImageUrl(s.public ? s.public.photo : null)}
|
||||||
|
selected={this.state.selectedUser}
|
||||||
|
onSelected={this.handleSelectionChange}
|
||||||
|
topic={s.user}
|
||||||
|
key={s.user} />
|
||||||
|
);
|
||||||
|
}, this);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form className="panel-form" onSubmit={this.handleFindContacts}>
|
||||||
|
<div className="panel-form-one">
|
||||||
|
<input type="text" placeholder="Emails or phones to find" value={this.state.search}
|
||||||
|
onChange={this.handleSearchChange} required />
|
||||||
|
<a href="#" onClick={this.handleSearch}>
|
||||||
|
<i className="material-icons">search</i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div className="contacts-panel">
|
||||||
|
<ul className="contact-box">
|
||||||
|
{contactNodes}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* END Create new topic and invite users or send an invite */
|
||||||
|
|
||||||
/* BEGIN Conversation panel */
|
/* BEGIN Conversation panel */
|
||||||
var Received = {
|
var Received = {
|
||||||
UNDEF: -2,
|
UNDEF: -2,
|
||||||
@@ -886,10 +920,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleDescChange: function(desc) {
|
handleDescChange: function(desc) {
|
||||||
this.setState({title: desc.public.fn});
|
if (desc.public) {
|
||||||
|
this.setState({title: desc.public.fn});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
handleSubsUpdated: function(uids) {
|
handleSubsUpdated: function(uids) {
|
||||||
|
console.log("topic subscriptions updated");
|
||||||
},
|
},
|
||||||
handleNewMessage: function(msg) {
|
handleNewMessage: function(msg) {
|
||||||
this.setState({messages: this.state.messages.concat(msg)});
|
this.setState({messages: this.state.messages.concat(msg)});
|
||||||
@@ -1098,6 +1134,74 @@
|
|||||||
});
|
});
|
||||||
/* End of modal dialog wrapper */
|
/* End of modal dialog wrapper */
|
||||||
|
|
||||||
|
/* BEGIN Popup/dropdown menu */
|
||||||
|
var ContextMenu = React.createClass({
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this.toggle(false);
|
||||||
|
},
|
||||||
|
componentWillReceiveProps: function(nextProps) {
|
||||||
|
this.toggle(nextProps.visible);
|
||||||
|
},
|
||||||
|
toggle: function(visible) {
|
||||||
|
if (visible) {
|
||||||
|
document.addEventListener('mousedown', this.handlePageClick, false);
|
||||||
|
document.addEventListener('keyup', this.handleEscapeKey, false);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mousedown', this.handlePageClick, false);
|
||||||
|
document.removeEventListener('keyup', this.handleEscapeKey, false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handlePageClick: function(e) {
|
||||||
|
if (ReactDOM.findDOMNode(this).contains(e.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.props.hide();
|
||||||
|
},
|
||||||
|
handleEscapeKey: function(e) {
|
||||||
|
if (e.keyCode === 27) {
|
||||||
|
this.props.hide();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleClick: function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.props.hide();
|
||||||
|
this.props.onResult(this.props.items[e.currentTarget.dataset.id]);
|
||||||
|
},
|
||||||
|
render: function() {
|
||||||
|
if (!this.props.visible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var position = {
|
||||||
|
left: this.props.position.left + 'px',
|
||||||
|
top: this.props.position.top + 'px'
|
||||||
|
};
|
||||||
|
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" style={position}>
|
||||||
|
{menuItems}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/* END Popup/dropdown menu */
|
||||||
|
|
||||||
/* 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() {
|
||||||
@@ -1106,6 +1210,7 @@
|
|||||||
sidePanelTitle: null,
|
sidePanelTitle: null,
|
||||||
sidePanelAvatar: null,
|
sidePanelAvatar: null,
|
||||||
dialogSelected: null,
|
dialogSelected: null,
|
||||||
|
contextMenuVisible: false,
|
||||||
loggedIn: false,
|
loggedIn: false,
|
||||||
errorText: '',
|
errorText: '',
|
||||||
topicSelected: '',
|
topicSelected: '',
|
||||||
@@ -1142,7 +1247,11 @@
|
|||||||
topicSelected: '',
|
topicSelected: '',
|
||||||
loginDisabled: false,
|
loginDisabled: false,
|
||||||
subscriptions: {},
|
subscriptions: {},
|
||||||
mobilePanel: 'sidepanel'
|
mobilePanel: 'sidepanel',
|
||||||
|
contextMenuVisible: false,
|
||||||
|
contextMenuPosition: {top: 0, left: 0},
|
||||||
|
contextMenuItems: null,
|
||||||
|
handleContextMenuClick: undefined
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
// User clicked Login button in the side panel.
|
// User clicked Login button in the side panel.
|
||||||
@@ -1178,16 +1287,6 @@
|
|||||||
Tinode.getTopic(data.topic).noteRecv(data.seq);
|
Tinode.getTopic(data.topic).noteRecv(data.seq);
|
||||||
}, RECEIVED_DELAY);
|
}, RECEIVED_DELAY);
|
||||||
},
|
},
|
||||||
// Registration of a new account.
|
|
||||||
handleNewAccount: function(login_, password_, public_, private_) {
|
|
||||||
var instance = this;
|
|
||||||
Tinode.connect().then(function() {
|
|
||||||
return Tinode.createUserBasic(login_, password_,
|
|
||||||
{login: "basic", public: public_, private: private_});
|
|
||||||
}).catch(function(err) {
|
|
||||||
instance.setState({errorText: err.message});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
// User clicked on a contact in the side panel.
|
// User clicked on a contact in the side panel.
|
||||||
handleTopicSelected: function(topicName) {
|
handleTopicSelected: function(topicName) {
|
||||||
this.setState({topicSelected: topicName, mobilePanel: 'topic-view'});
|
this.setState({topicSelected: topicName, mobilePanel: 'topic-view'});
|
||||||
@@ -1202,9 +1301,19 @@
|
|||||||
topic.publish(msg);
|
topic.publish(msg);
|
||||||
},
|
},
|
||||||
// User chose a Sign Up menu item.
|
// User chose a Sign Up menu item.
|
||||||
handleSignUp: function() {
|
handleNewAccount: function() {
|
||||||
this.setState({sidePanelSelected: 'register'});
|
this.setState({sidePanelSelected: 'register'});
|
||||||
},
|
},
|
||||||
|
// Actual registration of a new account.
|
||||||
|
handleNewAccountRequest: function(login_, password_, public_, private_) {
|
||||||
|
var instance = this;
|
||||||
|
Tinode.connect().then(function() {
|
||||||
|
return Tinode.createUserBasic(login_, password_,
|
||||||
|
{login: "basic", public: public_, private: private_});
|
||||||
|
}).catch(function(err) {
|
||||||
|
instance.setState({errorText: err.message});
|
||||||
|
});
|
||||||
|
},
|
||||||
// User chose Settings menu item.
|
// User chose Settings menu item.
|
||||||
handleSettings: function() {
|
handleSettings: function() {
|
||||||
this.setState({sidePanelSelected: this.state.loggedIn ? 'edit-account' : 'settings'});
|
this.setState({sidePanelSelected: this.state.loggedIn ? 'edit-account' : 'settings'});
|
||||||
@@ -1217,12 +1326,53 @@
|
|||||||
this.setState({sidePanelSelected: 'login'});
|
this.setState({sidePanelSelected: 'login'});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// User clicked a (+) menu item.
|
||||||
handleNewTopic: function() {
|
handleNewTopic: function() {
|
||||||
this.setState({sidePanelSelected: 'new-topic'});
|
this.setState({sidePanelSelected: 'new-topic'});
|
||||||
},
|
},
|
||||||
|
// Create of a new topic. New P2P topic requires peer's name.
|
||||||
|
handleNewTopicRequest: function(peerName, vcard) {
|
||||||
|
var instance = this;
|
||||||
|
var topic = peerName ? Tinode.getTopic(peerName) : Tinode.newTopic();
|
||||||
|
var params = {get: {desc: {}, sub: {}, data: {}}};
|
||||||
|
params.set = peerName ? undefined : {set: {desc: {public: vcard}}};
|
||||||
|
topic.subscribe(params).then(function() {
|
||||||
|
if (peerName) {
|
||||||
|
instance.setState({
|
||||||
|
errorText: '',
|
||||||
|
topicSelected: topic.name,
|
||||||
|
sidePanelSelected: 'contacts',
|
||||||
|
mobilePanel: 'topic-view'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("new group topic created: " + topic.name);
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
instance.setState({errorText: err.message});
|
||||||
|
});
|
||||||
|
},
|
||||||
handleDialogCancel: function() {
|
handleDialogCancel: function() {
|
||||||
this.setState({dialogSelected: null});
|
this.setState({dialogSelected: null});
|
||||||
},
|
},
|
||||||
|
handleShowContextMenu: function(y, x, items, callback) {
|
||||||
|
console.log("SHOW ContextMenu called");
|
||||||
|
var bounds = ReactDOM.findDOMNode(this).getBoundingClientRect();
|
||||||
|
this.setState({
|
||||||
|
contextMenuVisible: true,
|
||||||
|
contextMenuPosition: {top: (y - bounds.top), left: (x - bounds.left)},
|
||||||
|
contextMenuItems: items,
|
||||||
|
handleContextMenuClick: callback
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleHideContextMenu: function() {
|
||||||
|
console.log("HIDE ContextMenu called");
|
||||||
|
this.setState({
|
||||||
|
contextMenuVisible: false,
|
||||||
|
contextMenuPosition: {top: 0, left: 0},
|
||||||
|
contextMenuItems: null,
|
||||||
|
handleContextMenuClick: undefined
|
||||||
|
});
|
||||||
|
},
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div id="app-container">
|
<div id="app-container">
|
||||||
@@ -1231,6 +1381,13 @@
|
|||||||
onOk={this.handleDialogCancel}
|
onOk={this.handleDialogCancel}
|
||||||
onCancel={this.handleDialogCancel} />
|
onCancel={this.handleDialogCancel} />
|
||||||
|
|
||||||
|
<ContextMenu
|
||||||
|
position={this.state.contextMenuPosition}
|
||||||
|
items={this.state.contextMenuItems}
|
||||||
|
onResult={this.state.handleContextMenuClick}
|
||||||
|
visible={this.state.contextMenuVisible}
|
||||||
|
hide={this.handleHideContextMenu} />
|
||||||
|
|
||||||
<SidepanelView
|
<SidepanelView
|
||||||
displayMobile={this.state.displayMobile}
|
displayMobile={this.state.displayMobile}
|
||||||
hideSelf={(this.state.displayMobile && this.state.mobilePanel !== 'sidepanel')}
|
hideSelf={(this.state.displayMobile && this.state.mobilePanel !== 'sidepanel')}
|
||||||
@@ -1240,13 +1397,16 @@
|
|||||||
subscriptions={this.state.subscriptions}
|
subscriptions={this.state.subscriptions}
|
||||||
loginDisabled={this.state.loginDisabled}
|
loginDisabled={this.state.loginDisabled}
|
||||||
errorText={this.state.errorText}
|
errorText={this.state.errorText}
|
||||||
onSignUp={this.handleSignUp}
|
|
||||||
|
onSignUp={this.handleNewAccount}
|
||||||
onSettings={this.handleSettings}
|
onSettings={this.handleSettings}
|
||||||
onLoginRequest={this.handleLoginRequest}
|
onLoginRequest={this.handleLoginRequest}
|
||||||
onCreateAccount={this.handleNewAccount}
|
onCreateAccount={this.handleNewAccountRequest}
|
||||||
onTopicSelected={this.handleTopicSelected}
|
onTopicSelected={this.handleTopicSelected}
|
||||||
|
onCreateTopic={this.handleNewTopicRequest}
|
||||||
onNewTopic={this.handleNewTopic}
|
onNewTopic={this.handleNewTopic}
|
||||||
onCancel={this.handleSidepanelCancel} />
|
onCancel={this.handleSidepanelCancel}
|
||||||
|
showContextMenu={this.handleShowContextMenu} />
|
||||||
|
|
||||||
<MessagesView
|
<MessagesView
|
||||||
onHideMessageView={this.handleHideMessageView}
|
onHideMessageView={this.handleHideMessageView}
|
||||||
|
|||||||
Reference in New Issue
Block a user