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;
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 {
+257 -81
View File
@@ -8,7 +8,7 @@
<!-- Browser styling for Chrome, FF, Opera -->
<meta name="theme-color" content="#3949AB">
<!-- 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" />
<!-- Google's material design icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
@@ -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(
<ChipInput.Chip
onCancel={instance.handleChipCancel}
avatar={makeImageUrl(item.public ? item.public.photo : null)}
title={item.public ? item.public.fn : undefined}
topic={item.user}
key={item.user} />
index={count}
key={count} />
);
count++;
});
var className = "chip-input" + (this.state.focused ? " focused" : "");
return (
<div className={className} tabIndex="0">
{chips}
<input type="text"
placeholder={this.state.placeholder}
onChange={this.handleTextInput}
onFocus={this.handleFocusGained}
onBlur={this.handleFocusLost}
onKeyDown={this.handleKeyDown}
value={this.state.input}
autoFocus />
<div className={className}>
{chips}
<input type="text"
placeholder={this.state.placeholder}
onChange={this.handleTextInput}
onFocus={this.handleFocusGained}
onBlur={this.handleFocusLost}
onKeyDown={this.handleKeyDown}
value={this.state.input}
autoFocus />
</div>
);
}
@@ -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' ?
<NewTopicView
data={this.props.subscriptions}
contactsSearchQuery={this.props.contactsSearchQuery}
foundContacts={this.props.foundContacts}
onInitFind={this.props.onInitFind}
onSearchContacts={this.props.onSearchContacts}
onCreateTopic={this.props.onCreateTopic} /> :
@@ -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 (
<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">
<LetterTile
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 className="text-box">
<div><span className="contact-title">{title}</span>
@@ -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(
<Contact
title={c.public ? c.public.fn : null}
avatar={makeImageUrl(c.public ? c.public.photo : null)}
comment={c.private}
unread={c.unread} now={c.online}
selected={this.props.topicSelected === key}
showCheckmark={showCheckmark}
selected={selected}
showOnline={this.props.showOnline}
onSelected={this.props.onTopicSelected}
showContextMenu={this.props.showContextMenu}
topic={key}
index={contactNodes.length}
key={key} />
);
}, 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 (
<div className="panel-form-column">
<div id="group-manager">
<div className="panel-form-row">
<label className="small">Group members</label>
</div>
<ChipInput
chips={this.state.members}
prompt="add members" />
chips={this.state.members}
prompt="add members"
onChipRemoved={this.handleMemberRemoved} />
<div className="panel-form-row">
<label className="small">All contacts</label>
</div>
<ContactList
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>
);
}
@@ -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 @@
</div>
{this.state.showMemberPanel ?
<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 className="panel-form-row">
@@ -1542,7 +1657,7 @@
</div>
<div className="panel-form-row">
{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
</a>
: 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 (
<div id="app-container">
@@ -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} />
<MessagesView
@@ -2406,11 +2579,14 @@
{this.state.showInfoPanel ?
<InfoView
topic={this.state.topicSelected}
foundContacts={this.state.foundContacts}
onSubmit={this.handleTopicUpdateRequest}
onCancel={this.handleHideInfoView}
onChangePermissions={this.handleChangePermissions}
onMemberUpdateRequest={this.handleMemberUpdateRequest}
onLeaveTopic={this.handleLeaveUnsubRequest}
onAddMember={this.handleManageGroupMembers}
onInitFind={this.tnInitFind}
/>
:
null