topic creation and context menu fixes

This commit is contained in:
Gene S
2016-03-03 18:03:11 -08:00
parent 5b89abb646
commit ac86e415e9
2 changed files with 392 additions and 224 deletions
+29 -21
View File
@@ -347,35 +347,35 @@ ul.menu > li.separator:hover {
}
/* List of contacts */
#contacts-panel {
.contacts-panel {
overflow-y: auto;
margin-top: 1rem;:
margin-bottom: auto;
height: 100%;
}
#contact-box {
.contact-box {
list-style: none;
}
#contact-box > li {
.contact-box > li {
display: flex;
padding: 0.35rem 0.25rem 0.5rem 0.35rem;
min-height: 3.5rem;
}
#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 */
}
#contact-box .contact-title {
.contact-box .contact-title {
font-size: 1.05rem;
}
/* The down arrow head - trigger for context menu in contacts list */
#contact-box li .menuTrigger {
.contact-box li .menuTrigger {
position: relative;
margin-left: auto;
width: 1.5rem;
@@ -383,19 +383,19 @@ ul.menu > li.separator:hover {
border-radius: 50%;
background-color: transparent;
}
#contact-box li:hover .menuTrigger {
.contact-box li:hover .menuTrigger {
background-color: white;
}
#contact-box li .menuTrigger a {
.contact-box li .menuTrigger a {
color: transparent;
}
#contact-box li:hover .menuTrigger a {
.contact-box li:hover .menuTrigger a {
color: #757575;
}
/* Avatar and online-offline indicator */
#contact-box .avatar-box {
.contact-box .avatar-box {
margin-right: 0.35rem;
position: relative;
width: 3rem;
@@ -403,25 +403,32 @@ ul.menu > li.separator:hover {
overflow: hidden;
padding: 0;
}
#contact-box .avatar {
.contact-box .avatar {
width: 3rem;
height: auto;
border-radius: 50%;
}
.contact-box .text-box {
display: flex;
flex-direction: column;
justify-content: space-around;
}
/* Online-offline indicator positioned inside avatar-box */
#contact-box .online, .offline {
font-size: 1.5em;
line-height: 1;
.contact-box .online, .offline {
display: inline-block;
width: 0.75rem;
height: 0.75rem;
border-radius: 50%;
position: absolute;
right: -0.1rem;
bottom: -0.1rem;
right: 0;
bottom: 0;
}
#contact-box .online {
color: #4caf50;
.contact-box .online {
background-color: #4caf50;
}
#contact-box .offline {
color: #888;
.contact-box .offline {
background-color: #888;
}
/* END of avatar box */
@@ -840,6 +847,7 @@ ul.menu > li.separator:hover {
border-radius: 1em;
padding: 0.25em;
min-width: 1.75em;
margin-left: 0.5rem;
text-align: center;
}
+363 -203
View File
@@ -66,68 +66,6 @@
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() {
@@ -224,9 +162,9 @@
fn: this.state.name.trim(),
};
if (this.state.imageDataUrl) {
dataStart = this.state.imageDataUrl.indexOf(",");
var dataStart = this.state.imageDataUrl.indexOf(",");
vcard.photo = {
data: this.state.imageDataUrl.substring(dataStart),
data: this.state.imageDataUrl.substring(dataStart+1),
type: "jpg"
};
}
@@ -288,8 +226,11 @@
}
ctx.imageSmoothingEnabled = true;
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;
instance.setState({dataUrl: bits});
instance.props.onImageChanged(bits);
};
img.src = URL.createObjectURL(e.target.files[0]);
},
@@ -310,94 +251,6 @@
});
/* 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 */
var SettingsView = React.createClass({
getInitialState: function() {
@@ -571,13 +424,14 @@
this.props.state === 'contacts' ?
<ContactsView
data={this.props.subscriptions}
showContextMenu={this.props.showContextMenu}
onTopicSelected={this.props.onTopicSelected} /> :
this.props.state === 'new-topic' ?
<NewTopicView
data={this.props.subscriptions}
onContactSelected={this.props.onContactSelected}
onStartNewTopic={this.props.onStartNewTopic} /> :
onCreateTopic={this.props.onCreateTopic} /> :
null}
</div>
@@ -614,29 +468,27 @@
/* END EditAccount */
/* BEGIN Contact list (list of topics) */
/* A single topic */
var contactMenuItems = ["Delete chat", "Mute", "Block"];
/* A single topic */;
var Contact = React.createClass({
getInitialState: function() {
return {
menuShownFor: null
};
},
handleClick: function() {
handleClick: function(e) {
e.preventDefault();
e.stopPropagation();
this.props.onSelected(this.props.topic);
},
handleContextClick: function(e) {
e.preventDefault();
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) {
console.log("Menu clicked '" + action + "' for topic " + topic);
this.setState({menuShownFor: null});
handleMenuAction: function(action) {
console.log("Menu clicked '" + action + "' for topic " + this.props.topic);
},
render: function() {
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) + "...";
}
var selected = (this.props.selected === this.props.topic) ? "selected" : null;
@@ -649,26 +501,31 @@
<li className={selected} onClick={this.handleClick}>
<div className="avatar-box">
{avatar}
<span className={this.props.now ? "online" : "offline"}>&#x25cf;</span>
{this.props.now ? <span className="online"></span> : null}
</div>
<div>
<span className="contact-title">{title}</span> <Contact.Unread count={this.props.unread} />
<p>{this.props.comment}</p>
<div className="text-box">
<div><span className="contact-title">{title}</span>
{this.props.unread > 0 ? <Contact.Unread count={this.props.unread} /> : null}</div>
{this.props.comment ? <div>{this.props.comment}</div> : null}
</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>
{this.props.showContextMenu ?
<span className="menuTrigger">
<a href="#" onClick={this.handleContextClick}>
<i className="material-icons">expand_more</i>
</a>
</span> : null}
</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 */
Contact.Unread = React.createClass({
render: function() {
@@ -697,6 +554,12 @@
me.onData = this.tnInvite;
me.onSubsUpdated = this.tnMeSubsUpdated;
},
componentWillUnmount: function() {
var me = Tinode.getMeTopic();
me.onContactUpdate = undefined;
me.onData = undefined;
me.onSubsUpdated = undefined;
},
tnMeContactUpdate: function(what, cont) {
if (what === "on" || what === "off" || what === "read") {
this.forceUpdate();
@@ -719,7 +582,7 @@
tnMeSubsUpdated: function(names) {
this.forceUpdate();
},
onSelectionChange: function(sel) {
handleSelectionChange: function(sel) {
this.setState({selected: sel});
this.props.onTopicSelected(sel);
},
@@ -732,20 +595,21 @@
}
contactNodes.push(
<Contact
title={c.public.fn}
avatar={makeImageUrl(c.public.photo)}
title={c.public ? c.public.fn : null}
avatar={makeImageUrl(c.public ? c.public.photo : null)}
comment={c.private}
unread={unread} now={c.online}
selected={this.state.selected}
onSelected={this.onSelectionChange}
onSelected={this.handleSelectionChange}
showContextMenu={this.props.showContextMenu}
topic={c.topic}
key={c.topic} />
);
}, this);
return (
<div id="contacts-panel">
<ul id="contact-box">
<div className="contacts-panel">
<ul className="contact-box">
{contactNodes}
</ul>
</div>
@@ -754,6 +618,176 @@
});
/* 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 */
var Received = {
UNDEF: -2,
@@ -886,10 +920,12 @@
}
},
handleDescChange: function(desc) {
this.setState({title: desc.public.fn});
if (desc.public) {
this.setState({title: desc.public.fn});
}
},
handleSubsUpdated: function(uids) {
console.log("topic subscriptions updated");
},
handleNewMessage: function(msg) {
this.setState({messages: this.state.messages.concat(msg)});
@@ -1098,6 +1134,74 @@
});
/* 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 */
var AppView = React.createClass({
getInitialState: function() {
@@ -1106,6 +1210,7 @@
sidePanelTitle: null,
sidePanelAvatar: null,
dialogSelected: null,
contextMenuVisible: false,
loggedIn: false,
errorText: '',
topicSelected: '',
@@ -1142,7 +1247,11 @@
topicSelected: '',
loginDisabled: false,
subscriptions: {},
mobilePanel: 'sidepanel'
mobilePanel: 'sidepanel',
contextMenuVisible: false,
contextMenuPosition: {top: 0, left: 0},
contextMenuItems: null,
handleContextMenuClick: undefined
});
},
// User clicked Login button in the side panel.
@@ -1178,16 +1287,6 @@
Tinode.getTopic(data.topic).noteRecv(data.seq);
}, 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.
handleTopicSelected: function(topicName) {
this.setState({topicSelected: topicName, mobilePanel: 'topic-view'});
@@ -1202,9 +1301,19 @@
topic.publish(msg);
},
// User chose a Sign Up menu item.
handleSignUp: function() {
handleNewAccount: function() {
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.
handleSettings: function() {
this.setState({sidePanelSelected: this.state.loggedIn ? 'edit-account' : 'settings'});
@@ -1217,12 +1326,53 @@
this.setState({sidePanelSelected: 'login'});
}
},
// User clicked a (+) menu item.
handleNewTopic: function() {
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() {
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() {
return (
<div id="app-container">
@@ -1231,6 +1381,13 @@
onOk={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
displayMobile={this.state.displayMobile}
hideSelf={(this.state.displayMobile && this.state.mobilePanel !== 'sidepanel')}
@@ -1240,13 +1397,16 @@
subscriptions={this.state.subscriptions}
loginDisabled={this.state.loginDisabled}
errorText={this.state.errorText}
onSignUp={this.handleSignUp}
onSignUp={this.handleNewAccount}
onSettings={this.handleSettings}
onLoginRequest={this.handleLoginRequest}
onCreateAccount={this.handleNewAccount}
onCreateAccount={this.handleNewAccountRequest}
onTopicSelected={this.handleTopicSelected}
onCreateTopic={this.handleNewTopicRequest}
onNewTopic={this.handleNewTopic}
onCancel={this.handleSidepanelCancel} />
onCancel={this.handleSidepanelCancel}
showContextMenu={this.handleShowContextMenu} />
<MessagesView
onHideMessageView={this.handleHideMessageView}