diff --git a/css/base.css b/css/base.css
index adb69345..af8956e2 100644
--- a/css/base.css
+++ b/css/base.css
@@ -183,7 +183,7 @@ textarea {
height: 2.5rem;
border-bottom: 1px solid #ccc;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
- transition: all 0.2s ease-in-out;
+ transition: all 0.1s ease-in-out;
width: 100%;
}
textarea:focus,
@@ -581,6 +581,22 @@ ul.menu > li.separator:hover {
.contact-box .offline {
background-color: #C0C0C0;
}
+
+.contact-box .checkmark.material-icons {
+ display: inline-block;
+ width: 1.5rem;
+ height: 1.5rem;
+ font-size: 1.5rem;
+ line-height: 1.5rem;
+ vertical-align: middle;
+ color: #0097A7;
+ position: absolute;
+ right: 0.05rem;
+ bottom: 0.05rem;
+ background-color: white;
+ overflow: visible;
+ border: 0.0px solid transparent;
+}
/* END of avatar box */
#add-topic {
@@ -756,10 +772,11 @@ ul.menu > li.separator:hover {
display: flex;
align-items: center;
flex: none;
- padding: 0.5rem;
+ padding: 0.4rem;
background-color: #eceff1;
box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
z-index: 2;
+ height: 4rem;
}
/* A static panel shown when no topic is selected */
@@ -962,6 +979,24 @@ ul.menu > li.separator:hover {
margin-right: auto;
}
+#group-manager {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+#group-manager-buttons {
+ display: flex;
+ background-color: #eceff1;
+ box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
+ z-index: 2;
+ justify-content: space-around;
+ padding: 0.4rem;
+ height: 4rem;
+ border-left: 1px solid #ccc;
+}
+
/* END of InfoView */
/* Modal dialog */
@@ -1043,7 +1078,7 @@ ul.tabbar > li > a {
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);
+ box-shadow: inset 0 -2px 2.5px -1px rgba(0,0,0,0.25);
}
ul.tabbar li.active > a {
@@ -1083,10 +1118,10 @@ ul.tabbar li.active > a:after {
background-color: #0097A7;
color: white;
border-radius: 50%;
- padding: 0.25rem;
- min-width: 1.4rem;
+ width: 1.35rem;
+ height: 1.35rem;
margin-left: 0.5rem;
- line-height: 1;
+ line-height: 1.35rem;
text-align: center;
vertical-align: middle;
}
@@ -1245,10 +1280,12 @@ ul.tabbar li.active > a:after {
background: transparent;
border-bottom: 1px solid #ccc;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1);
- transition: all 0.2s ease-in-out;
+ transition: all 0.1s ease-in-out;
width: 100%;
- margin: 0.25rem 0;
- padding: 0 0.125rem 0.35rem 0.125rem;
+ max-height: 8rem;
+ overflow-y: scroll;
+ margin: 0.25rem 0rem 0.25rem 0rem;
+ padding: 0 0 0.5rem 0.5rem;
outline: none;
color: #666;
align-items: center;
@@ -1256,8 +1293,7 @@ ul.tabbar li.active > a:after {
text-align: center;
}
-.chip-input.focused,
-.chip-input:focus {
+.chip-input.focused {
border-bottom: 1px solid #2196f3;
box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25);
}
@@ -1284,6 +1320,7 @@ ul.tabbar li.active > a:after {
margin: 0.125rem;
background-color: #eee;
white-space: nowrap;
+ font-size: 90%;
}
.chip > a {
diff --git a/index.html b/index.html
index 64bcac01..9f2ee3d0 100644
--- a/index.html
+++ b/index.html
@@ -8,7 +8,7 @@
-
@@ -263,7 +263,6 @@
this.setState({active: false});
var text = this.state.text.trim();
if ((text || this.props.text) && (text !== this.props.text)) {
- console.log("'" + text + "' != '" + this.props.text + "'");
this.props.onFinished(text);
}
},
@@ -347,20 +346,31 @@
var ChipInput = React.createClass({
getInitialState: function() {
return {
- chips: this.props.chips,
placeholder: this.props.chips ? "" : this.props.prompt,
input: '',
focused: false
};
},
-
- handleTextInput: function(e) {
- console.log(e.target.value);
- this.setState({input: e.target.value});
+ componentWillReceiveProps: function(newProps) {
+ this.setState({
+ input: ''
+ });
},
- handleChipCancel: function(item) {
- console.log("cancel " + item);
+ handleTextInput: function(e) {
+ this.setState({input: e.target.value});
+ if (this.props.filter) {
+ this.props.filter(e.target.value);
+ }
+ },
+
+ removeChipAt: function(idx) {
+ var removed = this.props.chips[idx];
+ this.props.onChipRemoved(removed.user, idx);
+ },
+
+ handleChipCancel: function(item, idx) {
+ this.removeChipAt(idx);
},
handleFocusGained: function() {
@@ -373,10 +383,8 @@
handleKeyDown: function(e) {
if (e.key === 'Backspace') {
- if (this.state.input.length == 0 && this.state.chips.length > 0) {
- var updated = this.state.chips.slice();
- updated.splice(-1);
- this.setState({chips: updated});
+ if (this.state.input.length == 0 && this.props.chips.length > 0) {
+ this.removeChipAt(this.props.chips.length-1);
}
}
},
@@ -384,28 +392,31 @@
render: function() {
var chips = [];
var instance = this;
- this.state.chips.map(function(item) {
+ var count = 0;
+ this.props.chips.map(function(item) {
chips.push(
+ index={count}
+ key={count} />
);
+ count++;
});
var className = "chip-input" + (this.state.focused ? " focused" : "");
return (
-
- {chips}
-
+
+ {chips}
+
);
}
@@ -413,7 +424,7 @@
ChipInput.Chip = React.createClass({
handleCancel: function() {
- this.props.onCancel(this.props.title);
+ this.props.onCancel(this.props.topic, this.props.index);
},
render: function() {
@@ -800,7 +811,9 @@
this.props.state === 'new-topic' ?
:
@@ -922,7 +935,7 @@
handleClick: function(e) {
e.preventDefault();
e.stopPropagation();
- this.props.onSelected(this.props.topic);
+ this.props.onSelected(this.props.topic, this.props.index);
},
handleContextClick: function(e) {
@@ -941,11 +954,16 @@
var online = this.props.now ? "online" : "offline";
var avatar = this.props.avatar ? this.props.avatar : true;
return (
-
+
- {this.props.showOnline ? : null}
+
+ {this.props.showOnline ? :
+ (this.props.showCheckmark && this.props.selected ?
+ check_circle
+ : null)}
{title}
@@ -1075,16 +1093,18 @@
/* END Contact list */
/* BEGIN ContactList: component for showing a list of contacts,
- * such as a list of group members group chat */
+ * such as a list of group members in a group chat */
var ContactList = React.createClass({
render: function() {
var contactNodes = [];
var instance = this;
+ var showCheckmark = Array.isArray(this.props.topicSelected);
+
if (this.props.contacts && this.props.contacts.length > 0) {
this.props.contacts.map(function(c) {
var key = c.topic ? c.topic : c.user;
if (!key) {
- console.log("empty key!");
+ console.error("empty key!");
return;
}
@@ -1103,17 +1123,23 @@
}
}
+ var selected = showCheckmark ?
+ (this.props.topicSelected.indexOf(key) > -1) :
+ (this.props.topicSelected === key);
+
contactNodes.push(
);
}, this);
@@ -1139,24 +1165,118 @@
var GroupManager = React.createClass({
getInitialState: function() {
return {
- members: this.props.members || [],
- contacts: this.props.contacts || [],
+ members: this.props.members,
+ contacts: this.props.contacts,
+ index: this.indexMembers(this.props.members),
+ selectedContacts: this.selectedContacts(this.props.members)
};
},
- handleContactSelected: function(contact) {
+ indexMembers: function(members) {
+ var index = {};
+ members.map(function(m) {
+ index[m.user] = {delta: 0, present: true}; // Delta: 0 unchanged, +1 added, -1 removed
+ });
+ return index;
+ },
+ selectedContacts: function(members) {
+ var sel = [];
+ members.map(function(m) {
+ sel.push(m.user);
+ });
+ return sel;
+ },
+
+ handleContactSelected: function(userId, index) {
+ var status = this.state.index[userId];
+ if (status) {
+ if (status.present) {
+ // Prevent duplicate members
+ return;
+ }
+ status.delta += 1;
+ status.present = true;
+ } else {
+ status = {delta: 1, present: true};
+ }
+
+ var m = this.state.members.slice();
+ m.push(this.props.contacts[index]);
+
+ var sel = this.selectedContacts(m);
+
+ var i = this.state.index;
+ i[userId] = status;
+
+ this.setState({members: m, index: i, selectedContacts: sel});
+ },
+
+ handleMemberRemoved: function(userId, index) {
+ var status = this.state.index[userId];
+ if (!status || !status.present) {
+ return;
+ }
+ status.present = false;
+ status.delta -= 1;
+
+ var m = this.state.members.slice();
+ m.splice(index, 1);
+
+ var sel = this.selectedContacts(m);
+
+ var i = this.state.index;
+ i[userId] = status;
+
+ this.setState({members: m, index: i, selectedContacts: sel});
+ },
+
+ handleSubmit: function() {
+ var instance = this;
+ var members = [];
+ var added = [];
+ var removed = [];
+
+ var keys = Object.keys(this.state.index);
+ keys.map(function(k) {
+ if (instance.state.index[k].present) {
+ members.push(k);
+ }
+
+ if (instance.state.index[k].delta > 0) {
+ added.push(k);
+ } else if (instance.state.index[k].delta < 0) {
+ removed.push(k);
+ }
+ });
+ this.props.onSubmit(members, added, removed);
+ },
+
+ handleCancel: function() {
+ this.props.onCancel();
},
render: function() {
return (
-
+
+
+
+
+ chips={this.state.members}
+ prompt="add members"
+ onChipRemoved={this.handleMemberRemoved} />
+
+
+
+ topicSelected={this.state.selectedContacts}
+ onTopicSelected={this.handleContactSelected} />
+
+
+
+
);
}
@@ -1168,39 +1288,19 @@
getInitialState: function() {
return {
tabSelected: "p2p",
- searchQuery: '',
- contactList: [],
+ searchQuery: this.props.contactsSearchQuery,
+ contactList: this.props.foundContacts,
contactSelected: null
};
},
componentWillMount: function() {
- var fnd = Tinode.getFndTopic();
- fnd.onSubsUpdated = this.fndSubsUpdated;
- fnd.onMetaDesc = this.fndMetaDesc;
- if (fnd.isSubscribed()) {
- this.setState({searchQuery: fnd.private ? fnd.private.toString() : ''});
- this.fndSubsUpdated();
- } else {
- fnd.subscribe({get: {sub: {}, desc: {}}});
- }
+ this.props.onInitFind();
},
- componentWillUnmount: function() {
- var fnd = Tinode.getFndTopic();
- fnd.onSubsUpdated = undefined;
- fnd.onMetaDesc = undefined;
- },
- fndMetaDesc: function(desc) {
- if (desc.private) {
- this.setState({searchQuery: desc.private.toString()});
- }
- },
- fndSubsUpdated: function() {
- var contacts = [];
- // Don't attempt to create P2P topics which already exist. Server will reject the duplicates.
- Tinode.getFndTopic().subscribers(function(s) {
- contacts.push(s);
+ componentWillReceiveProps: function(nextProps) {
+ this.setState({
+ searchQuery: nextProps.contactsSearchQuery,
+ contactList: nextProps.foundContacts
});
- this.setState({contactList: contacts});
},
handleTabClick: function(e) {
e.preventDefault();
@@ -1370,6 +1470,8 @@
topic.onSubsUpdated = this.onSubsUpdated;
this.resetDesc(topic);
this.resetSubs(topic);
+
+ this.props.onInitFind();
},
componentWillUnmount: function() {
@@ -1457,11 +1559,21 @@
this.props.onChangePermissions(this.props.topic, checked ? "-P" : "+P");
},
- handleAddMembers: function() {
+ handleShowAddMembers: function() {
// this.props.onAddMember(this.props.topic);
this.setState({showMemberPanel: true});
},
+ handleHideAddMembers: function() {
+ // this.props.onAddMember(this.props.topic);
+ this.setState({showMemberPanel: false});
+ },
+
+ handleMemberUpdateRequest: function(members, added, removed) {
+ this.props.onMemberUpdateRequest(this.props.topic, added, removed);
+ this.setState({showMemberPanel: false});
+ },
+
handleLeave: function() {
this.props.onLeaveTopic(this.props.topic);
},
@@ -1485,7 +1597,10 @@
{this.state.showMemberPanel ?
+ members={this.state.contactList}
+ contacts={this.props.foundContacts}
+ onCancel={this.handleHideAddMembers}
+ onSubmit={this.handleMemberUpdateRequest} />
:
@@ -1542,7 +1657,7 @@
{this.state.sharer ?
-
+
person_add Add members
: null}
@@ -1992,7 +2107,9 @@
contextMenuClickAt: null,
contextMenuTopic: '',
contextMenuItems: [],
- serverVersion: 'no connection'
+ serverVersion: 'no connection',
+ contactsSearchQuery: '',
+ foundContacts: []
};
},
componentDidMount: function() {
@@ -2106,6 +2223,44 @@
Tinode.getTopic(data.topic).noteRecv(data.seq);
}, RECEIVED_DELAY);
},
+
+ /* Fnd topic: find contacts by tokens */
+ tnInitFind: function() {
+ var fnd = Tinode.getFndTopic();
+ fnd.onSubsUpdated = this.tnFndSubsUpdated;
+ fnd.onMetaDesc = this.tnFndMetaDesc;
+ if (fnd.isSubscribed()) {
+ this.setState({contactsSearchQuery: fnd.private ? fnd.private.toString() : ''});
+ this.tnFndSubsUpdated();
+ } else {
+ fnd.subscribe({get: {sub: {}, desc: {}}});
+ }
+ },
+
+ tnFndMetaDesc: function(desc) {
+ if (desc.private) {
+ this.setState({contactsSearchQuery: desc.private.toString()});
+ }
+ },
+
+ tnFndSubsUpdated: function() {
+ var contacts = [];
+ // Don't attempt to create P2P topics which already exist. Server will reject the duplicates.
+ Tinode.getFndTopic().subscribers(function(s) {
+ contacts.push(s);
+ });
+ this.setState({foundContacts: contacts});
+ },
+ /** 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
+ */
+ handleSearchContacts: function(query) {
+ var find = Tinode.getFndTopic();
+ find.setMeta({desc: {private: query}}).then(function(ctrl) {
+ find.getMeta({sub: {}, desc: {}});
+ });
+ },
+
// User clicked on a contact in the side panel or deleted a contact.
handleTopicSelected: function(topicName) {
if (topicName) {
@@ -2250,15 +2405,6 @@
});
},
- /** 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
- */
- handleSearchContacts: function(query) {
- var find = Tinode.getFndTopic();
- find.setMeta({desc: {private: query}}).then(function(ctrl) {
- find.getMeta({sub: {}, desc: {}});
- });
- },
handleDialogCancel: function() {
this.setState({dialogSelected: null});
},
@@ -2344,6 +2490,29 @@
handleHideInfoView: function() {
this.setState({showInfoPanel: false});
},
+ handleMemberUpdateRequest: function(topicName, added, removed) {
+ if (!topicName) {
+ return;
+ }
+
+ var topic = Tinode.getTopic(topicName);
+ if (!topic) {
+ return;
+ }
+
+ if (added && added.length > 0) {
+ added.map(function(uid) {
+ topic.invite(uid, null, "Join my group!");
+ });
+ }
+
+ if (removed && removed.length > 0) {
+ removed.map(function(uid) {
+ topic.delSubscription(uid);
+ });
+ }
+ },
+
render: function() {
return (
@@ -2383,11 +2552,15 @@
onCreateAccount={this.handleNewAccountRequest}
onUpdateAccount={this.handleUpdateAccountRequest}
onTopicSelected={this.handleTopicSelected}
- onSearchContacts={this.handleSearchContacts}
onCreateTopic={this.handleNewTopicRequest}
onNewTopic={this.handleNewTopic}
onCancel={this.handleSidepanelCancel}
+ onInitFind={this.tnInitFind}
+ contactsSearchQuery={this.state.contactsSearchQuery}
+ foundContacts={this.state.foundContacts}
+ onSearchContacts={this.handleSearchContacts}
+
showContextMenu={this.handleShowContextMenu} />
:
null