subscriber management is somewhat functional

This commit is contained in:
or-else
2017-07-06 16:15:56 +03:00
parent 58ae90b574
commit b689d08e71
2 changed files with 305 additions and 92 deletions
+48 -11
View File
@@ -183,7 +183,7 @@ textarea {
height: 2.5rem; height: 2.5rem;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); 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%; width: 100%;
} }
textarea:focus, textarea:focus,
@@ -581,6 +581,22 @@ ul.menu > li.separator:hover {
.contact-box .offline { .contact-box .offline {
background-color: #C0C0C0; 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 */ /* END of avatar box */
#add-topic { #add-topic {
@@ -756,10 +772,11 @@ ul.menu > li.separator:hover {
display: flex; display: flex;
align-items: center; align-items: center;
flex: none; flex: none;
padding: 0.5rem; padding: 0.4rem;
background-color: #eceff1; background-color: #eceff1;
box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */ box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
z-index: 2; z-index: 2;
height: 4rem;
} }
/* A static panel shown when no topic is selected */ /* A static panel shown when no topic is selected */
@@ -962,6 +979,24 @@ ul.menu > li.separator:hover {
margin-right: auto; 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 */ /* END of InfoView */
/* Modal dialog */ /* Modal dialog */
@@ -1043,7 +1078,7 @@ ul.tabbar > li > a {
font-size: 1.1rem; font-size: 1.1rem;
color: rgba(33,150,243,0.6); color: rgba(33,150,243,0.6);
overflow: hidden; 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 { ul.tabbar li.active > a {
@@ -1083,10 +1118,10 @@ ul.tabbar li.active > a:after {
background-color: #0097A7; background-color: #0097A7;
color: white; color: white;
border-radius: 50%; border-radius: 50%;
padding: 0.25rem; width: 1.35rem;
min-width: 1.4rem; height: 1.35rem;
margin-left: 0.5rem; margin-left: 0.5rem;
line-height: 1; line-height: 1.35rem;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
} }
@@ -1245,10 +1280,12 @@ ul.tabbar li.active > a:after {
background: transparent; background: transparent;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); 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%; width: 100%;
margin: 0.25rem 0; max-height: 8rem;
padding: 0 0.125rem 0.35rem 0.125rem; overflow-y: scroll;
margin: 0.25rem 0rem 0.25rem 0rem;
padding: 0 0 0.5rem 0.5rem;
outline: none; outline: none;
color: #666; color: #666;
align-items: center; align-items: center;
@@ -1256,8 +1293,7 @@ ul.tabbar li.active > a:after {
text-align: center; text-align: center;
} }
.chip-input.focused, .chip-input.focused {
.chip-input:focus {
border-bottom: 1px solid #2196f3; border-bottom: 1px solid #2196f3;
box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25); box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25);
} }
@@ -1284,6 +1320,7 @@ ul.tabbar li.active > a:after {
margin: 0.125rem; margin: 0.125rem;
background-color: #eee; background-color: #eee;
white-space: nowrap; white-space: nowrap;
font-size: 90%;
} }
.chip > a { .chip > a {
+257 -81
View File
@@ -8,7 +8,7 @@
<!-- Browser styling for Chrome, FF, Opera --> <!-- Browser styling for Chrome, FF, Opera -->
<meta name="theme-color" content="#3949AB"> <meta name="theme-color" content="#3949AB">
<!-- Google's Roboto font --> <!-- Google's Roboto font -->
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,400italic,700&subset=latin,cyrillic" <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,400italic,500,700&subset=latin,cyrillic"
rel="stylesheet" type="text/css" /> rel="stylesheet" type="text/css" />
<!-- Google's material design icons --> <!-- Google's material design icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" /> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
@@ -263,7 +263,6 @@
this.setState({active: false}); this.setState({active: false});
var text = this.state.text.trim(); var text = this.state.text.trim();
if ((text || this.props.text) && (text !== this.props.text)) { if ((text || this.props.text) && (text !== this.props.text)) {
console.log("'" + text + "' != '" + this.props.text + "'");
this.props.onFinished(text); this.props.onFinished(text);
} }
}, },
@@ -347,20 +346,31 @@
var ChipInput = React.createClass({ var ChipInput = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
chips: this.props.chips,
placeholder: this.props.chips ? "" : this.props.prompt, placeholder: this.props.chips ? "" : this.props.prompt,
input: '', input: '',
focused: false focused: false
}; };
}, },
componentWillReceiveProps: function(newProps) {
handleTextInput: function(e) { this.setState({
console.log(e.target.value); input: ''
this.setState({input: e.target.value}); });
}, },
handleChipCancel: function(item) { handleTextInput: function(e) {
console.log("cancel " + item); 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() { handleFocusGained: function() {
@@ -373,10 +383,8 @@
handleKeyDown: function(e) { handleKeyDown: function(e) {
if (e.key === 'Backspace') { if (e.key === 'Backspace') {
if (this.state.input.length == 0 && this.state.chips.length > 0) { if (this.state.input.length == 0 && this.props.chips.length > 0) {
var updated = this.state.chips.slice(); this.removeChipAt(this.props.chips.length-1);
updated.splice(-1);
this.setState({chips: updated});
} }
} }
}, },
@@ -384,28 +392,31 @@
render: function() { render: function() {
var chips = []; var chips = [];
var instance = this; var instance = this;
this.state.chips.map(function(item) { var count = 0;
this.props.chips.map(function(item) {
chips.push( chips.push(
<ChipInput.Chip <ChipInput.Chip
onCancel={instance.handleChipCancel} onCancel={instance.handleChipCancel}
avatar={makeImageUrl(item.public ? item.public.photo : null)} avatar={makeImageUrl(item.public ? item.public.photo : null)}
title={item.public ? item.public.fn : undefined} title={item.public ? item.public.fn : undefined}
topic={item.user} topic={item.user}
key={item.user} /> index={count}
key={count} />
); );
count++;
}); });
var className = "chip-input" + (this.state.focused ? " focused" : ""); var className = "chip-input" + (this.state.focused ? " focused" : "");
return ( return (
<div className={className} tabIndex="0"> <div className={className}>
{chips} {chips}
<input type="text" <input type="text"
placeholder={this.state.placeholder} placeholder={this.state.placeholder}
onChange={this.handleTextInput} onChange={this.handleTextInput}
onFocus={this.handleFocusGained} onFocus={this.handleFocusGained}
onBlur={this.handleFocusLost} onBlur={this.handleFocusLost}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
value={this.state.input} value={this.state.input}
autoFocus /> autoFocus />
</div> </div>
); );
} }
@@ -413,7 +424,7 @@
ChipInput.Chip = React.createClass({ ChipInput.Chip = React.createClass({
handleCancel: function() { handleCancel: function() {
this.props.onCancel(this.props.title); this.props.onCancel(this.props.topic, this.props.index);
}, },
render: function() { render: function() {
@@ -800,7 +811,9 @@
this.props.state === 'new-topic' ? this.props.state === 'new-topic' ?
<NewTopicView <NewTopicView
data={this.props.subscriptions} contactsSearchQuery={this.props.contactsSearchQuery}
foundContacts={this.props.foundContacts}
onInitFind={this.props.onInitFind}
onSearchContacts={this.props.onSearchContacts} onSearchContacts={this.props.onSearchContacts}
onCreateTopic={this.props.onCreateTopic} /> : onCreateTopic={this.props.onCreateTopic} /> :
@@ -922,7 +935,7 @@
handleClick: function(e) { handleClick: function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.props.onSelected(this.props.topic); this.props.onSelected(this.props.topic, this.props.index);
}, },
handleContextClick: function(e) { handleContextClick: function(e) {
@@ -941,11 +954,16 @@
var online = this.props.now ? "online" : "offline"; var online = this.props.now ? "online" : "offline";
var avatar = this.props.avatar ? this.props.avatar : true; var avatar = this.props.avatar ? this.props.avatar : true;
return ( return (
<li className={this.props.selected ? "selected" : null} onClick={this.handleClick}> <li className={!this.props.showCheckmark && this.props.selected ? "selected" : null}
onClick={this.handleClick}>
<div className="avatar-box"> <div className="avatar-box">
<LetterTile <LetterTile
avatar={avatar} title={this.props.title} topic={this.props.topic} /> avatar={avatar} title={this.props.title} topic={this.props.topic} />
{this.props.showOnline ? <span className={online} /> : null}
{this.props.showOnline ? <span className={online} /> :
(this.props.showCheckmark && this.props.selected ?
<i className="checkmark material-icons">check_circle</i>
: null)}
</div> </div>
<div className="text-box"> <div className="text-box">
<div><span className="contact-title">{title}</span> <div><span className="contact-title">{title}</span>
@@ -1075,16 +1093,18 @@
/* END Contact list */ /* END Contact list */
/* BEGIN ContactList: component for showing a list of contacts, /* 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({ var ContactList = React.createClass({
render: function() { render: function() {
var contactNodes = []; var contactNodes = [];
var instance = this; var instance = this;
var showCheckmark = Array.isArray(this.props.topicSelected);
if (this.props.contacts && this.props.contacts.length > 0) { if (this.props.contacts && this.props.contacts.length > 0) {
this.props.contacts.map(function(c) { this.props.contacts.map(function(c) {
var key = c.topic ? c.topic : c.user; var key = c.topic ? c.topic : c.user;
if (!key) { if (!key) {
console.log("empty key!"); console.error("empty key!");
return; return;
} }
@@ -1103,17 +1123,23 @@
} }
} }
var selected = showCheckmark ?
(this.props.topicSelected.indexOf(key) > -1) :
(this.props.topicSelected === key);
contactNodes.push( contactNodes.push(
<Contact <Contact
title={c.public ? c.public.fn : null} title={c.public ? c.public.fn : null}
avatar={makeImageUrl(c.public ? c.public.photo : null)} avatar={makeImageUrl(c.public ? c.public.photo : null)}
comment={c.private} comment={c.private}
unread={c.unread} now={c.online} unread={c.unread} now={c.online}
selected={this.props.topicSelected === key} showCheckmark={showCheckmark}
selected={selected}
showOnline={this.props.showOnline} showOnline={this.props.showOnline}
onSelected={this.props.onTopicSelected} onSelected={this.props.onTopicSelected}
showContextMenu={this.props.showContextMenu} showContextMenu={this.props.showContextMenu}
topic={key} topic={key}
index={contactNodes.length}
key={key} /> key={key} />
); );
}, this); }, this);
@@ -1139,24 +1165,118 @@
var GroupManager = React.createClass({ var GroupManager = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
members: this.props.members || [], members: this.props.members,
contacts: this.props.contacts || [], 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() { render: function() {
return ( return (
<div className="panel-form-column"> <div id="group-manager">
<div className="panel-form-row">
<label className="small">Group members</label>
</div>
<ChipInput <ChipInput
chips={this.state.members} chips={this.state.members}
prompt="add members" /> prompt="add members"
onChipRemoved={this.handleMemberRemoved} />
<div className="panel-form-row">
<label className="small">All contacts</label>
</div>
<ContactList <ContactList
contacts={this.props.contacts} contacts={this.props.contacts}
onTopicSelected={this.handleContactSelected}/> topicSelected={this.state.selectedContacts}
onTopicSelected={this.handleContactSelected} />
<div id="group-manager-buttons" className="panel-form-row">
<button className="blue" onClick={this.handleSubmit}>OK</button>
<button className="white" onClick={this.handleCancel}>Cancel</button>
</div>
</div> </div>
); );
} }
@@ -1168,39 +1288,19 @@
getInitialState: function() { getInitialState: function() {
return { return {
tabSelected: "p2p", tabSelected: "p2p",
searchQuery: '', searchQuery: this.props.contactsSearchQuery,
contactList: [], contactList: this.props.foundContacts,
contactSelected: null contactSelected: null
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
var fnd = Tinode.getFndTopic(); this.props.onInitFind();
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: {}}});
}
}, },
componentWillUnmount: function() { componentWillReceiveProps: function(nextProps) {
var fnd = Tinode.getFndTopic(); this.setState({
fnd.onSubsUpdated = undefined; searchQuery: nextProps.contactsSearchQuery,
fnd.onMetaDesc = undefined; contactList: nextProps.foundContacts
},
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);
}); });
this.setState({contactList: contacts});
}, },
handleTabClick: function(e) { handleTabClick: function(e) {
e.preventDefault(); e.preventDefault();
@@ -1370,6 +1470,8 @@
topic.onSubsUpdated = this.onSubsUpdated; topic.onSubsUpdated = this.onSubsUpdated;
this.resetDesc(topic); this.resetDesc(topic);
this.resetSubs(topic); this.resetSubs(topic);
this.props.onInitFind();
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
@@ -1457,11 +1559,21 @@
this.props.onChangePermissions(this.props.topic, checked ? "-P" : "+P"); this.props.onChangePermissions(this.props.topic, checked ? "-P" : "+P");
}, },
handleAddMembers: function() { handleShowAddMembers: function() {
// this.props.onAddMember(this.props.topic); // this.props.onAddMember(this.props.topic);
this.setState({showMemberPanel: true}); 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() { handleLeave: function() {
this.props.onLeaveTopic(this.props.topic); this.props.onLeaveTopic(this.props.topic);
}, },
@@ -1485,7 +1597,10 @@
</div> </div>
{this.state.showMemberPanel ? {this.state.showMemberPanel ?
<GroupManager <GroupManager
members={this.state.contactList} /> members={this.state.contactList}
contacts={this.props.foundContacts}
onCancel={this.handleHideAddMembers}
onSubmit={this.handleMemberUpdateRequest} />
: :
<div id="info-view-content" className="scrollable-panel"> <div id="info-view-content" className="scrollable-panel">
<div className="panel-form-row"> <div className="panel-form-row">
@@ -1542,7 +1657,7 @@
</div> </div>
<div className="panel-form-row"> <div className="panel-form-row">
{this.state.sharer ? {this.state.sharer ?
<a href="#" className="flat-button" onClick={this.handleAddMembers}> <a href="#" className="flat-button" onClick={this.handleShowAddMembers}>
<i className="material-icons">person_add</i> Add members <i className="material-icons">person_add</i> Add members
</a> </a>
: null} : null}
@@ -1992,7 +2107,9 @@
contextMenuClickAt: null, contextMenuClickAt: null,
contextMenuTopic: '', contextMenuTopic: '',
contextMenuItems: [], contextMenuItems: [],
serverVersion: 'no connection' serverVersion: 'no connection',
contactsSearchQuery: '',
foundContacts: []
}; };
}, },
componentDidMount: function() { componentDidMount: function() {
@@ -2106,6 +2223,44 @@
Tinode.getTopic(data.topic).noteRecv(data.seq); Tinode.getTopic(data.topic).noteRecv(data.seq);
}, RECEIVED_DELAY); }, 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. // User clicked on a contact in the side panel or deleted a contact.
handleTopicSelected: function(topicName) { handleTopicSelected: function(topicName) {
if (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() { handleDialogCancel: function() {
this.setState({dialogSelected: null}); this.setState({dialogSelected: null});
}, },
@@ -2344,6 +2490,29 @@
handleHideInfoView: function() { handleHideInfoView: function() {
this.setState({showInfoPanel: false}); 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() { render: function() {
return ( return (
<div id="app-container"> <div id="app-container">
@@ -2383,11 +2552,15 @@
onCreateAccount={this.handleNewAccountRequest} onCreateAccount={this.handleNewAccountRequest}
onUpdateAccount={this.handleUpdateAccountRequest} onUpdateAccount={this.handleUpdateAccountRequest}
onTopicSelected={this.handleTopicSelected} onTopicSelected={this.handleTopicSelected}
onSearchContacts={this.handleSearchContacts}
onCreateTopic={this.handleNewTopicRequest} onCreateTopic={this.handleNewTopicRequest}
onNewTopic={this.handleNewTopic} onNewTopic={this.handleNewTopic}
onCancel={this.handleSidepanelCancel} onCancel={this.handleSidepanelCancel}
onInitFind={this.tnInitFind}
contactsSearchQuery={this.state.contactsSearchQuery}
foundContacts={this.state.foundContacts}
onSearchContacts={this.handleSearchContacts}
showContextMenu={this.handleShowContextMenu} /> showContextMenu={this.handleShowContextMenu} />
<MessagesView <MessagesView
@@ -2406,11 +2579,14 @@
{this.state.showInfoPanel ? {this.state.showInfoPanel ?
<InfoView <InfoView
topic={this.state.topicSelected} topic={this.state.topicSelected}
foundContacts={this.state.foundContacts}
onSubmit={this.handleTopicUpdateRequest} onSubmit={this.handleTopicUpdateRequest}
onCancel={this.handleHideInfoView} onCancel={this.handleHideInfoView}
onChangePermissions={this.handleChangePermissions} onChangePermissions={this.handleChangePermissions}
onMemberUpdateRequest={this.handleMemberUpdateRequest}
onLeaveTopic={this.handleLeaveUnsubRequest} onLeaveTopic={this.handleLeaveUnsubRequest}
onAddMember={this.handleManageGroupMembers} onAddMember={this.handleManageGroupMembers}
onInitFind={this.tnInitFind}
/> />
: :
null null