member management is complete now
This commit is contained in:
@@ -10,20 +10,19 @@ Try it (possibly older version) live at http://api.tinode.co/x/example-react-js/
|
|||||||
|
|
||||||
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
||||||
Photos from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
|
Sample avatar photos from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/photo-license/).
|
||||||
|
|
||||||
Background pattern from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution.
|
Background pattern from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution.
|
||||||
|
|
||||||
## Missing functionality
|
## Missing functionality
|
||||||
|
|
||||||
* Cannot manage members of group chats.
|
|
||||||
* Cannot send images or other media.
|
* Cannot send images or other media.
|
||||||
* No typing indicators.
|
* No typing indicators.
|
||||||
* No indication of chat status (muted, blocked, etc.).
|
* No indication of chat status (muted, blocked, etc.).
|
||||||
* Long polling as a wire transport does not work.
|
* Long polling as a wire transport does not work.
|
||||||
* Browser push notifications are missing.
|
* Browser push notifications are missing.
|
||||||
* No encryption.
|
* No encryption.
|
||||||
* Editing chat parameters is incomplete.
|
* Editing chat permissions is incomplete.
|
||||||
|
|
||||||
## Responsive design
|
## Responsive design
|
||||||
|
|
||||||
|
|||||||
+20
-1
@@ -257,6 +257,16 @@ ul.menu > li.separator:hover {
|
|||||||
.red {
|
.red {
|
||||||
color: #D32F2F; /* red 700 */
|
color: #D32F2F; /* red 700 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hr {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 0.6rem;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
box-shadow: inset 0 0.25rem 0.25rem -0.25rem rgba(0,0,0,0.15),
|
||||||
|
inset 0 -0.25rem 0.25rem -0.25rem rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
/* END generic text colors */
|
/* END generic text colors */
|
||||||
|
|
||||||
/* END of basic elements */
|
/* END of basic elements */
|
||||||
@@ -1274,6 +1284,7 @@ ul.tabbar li.active > a:after {
|
|||||||
.chip-input {
|
.chip-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
flex-grow: 1;
|
||||||
cursor: text;
|
cursor: text;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@@ -1282,7 +1293,8 @@ ul.tabbar li.active > a:after {
|
|||||||
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.1s ease-in-out;
|
transition: all 0.1s ease-in-out;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 8rem;
|
max-height: 10rem;
|
||||||
|
min-height: 4rem;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
margin: 0.25rem 0rem 0.25rem 0rem;
|
margin: 0.25rem 0rem 0.25rem 0rem;
|
||||||
padding: 0 0 0.5rem 0.5rem;
|
padding: 0 0 0.5rem 0.5rem;
|
||||||
@@ -1355,3 +1367,10 @@ ul.tabbar li.active > a:after {
|
|||||||
.chip .avatar-box .material-icons {
|
.chip .avatar-box .material-icons {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chip .spacer {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0.3rem;
|
||||||
|
width: 0.25rem;
|
||||||
|
height: 1.175rem;
|
||||||
|
}
|
||||||
|
|||||||
+80
-18
@@ -347,26 +347,55 @@
|
|||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
return {
|
return {
|
||||||
placeholder: this.props.chips ? "" : this.props.prompt,
|
placeholder: this.props.chips ? "" : this.props.prompt,
|
||||||
|
sortedChips: this.sortChips(this.props.chips, this.props.required),
|
||||||
|
chipIndex: this.indexChips(this.props.chips),
|
||||||
input: '',
|
input: '',
|
||||||
focused: false
|
focused: false
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillReceiveProps: function(newProps) {
|
componentWillReceiveProps: function(newProps) {
|
||||||
this.setState({
|
this.setState({
|
||||||
input: ''
|
sortedChips: this.sortChips(newProps.chips, newProps.required),
|
||||||
|
chipIndex: this.indexChips(newProps.chips)
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Map chip index to user name
|
||||||
|
indexChips: function(chips) {
|
||||||
|
var index = {};
|
||||||
|
var count = 0;
|
||||||
|
chips.map(function(item) {
|
||||||
|
index[item.user] = count;
|
||||||
|
count ++;
|
||||||
|
});
|
||||||
|
return index;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Have non-removable chips appear before all other chips.
|
||||||
|
sortChips: function(chips, keep) {
|
||||||
|
var required = [];
|
||||||
|
var normal = [];
|
||||||
|
chips.map(function(item) {
|
||||||
|
if (item.user === keep) {
|
||||||
|
required.push(item);
|
||||||
|
} else {
|
||||||
|
normal.push(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return required.concat(normal);
|
||||||
|
},
|
||||||
|
|
||||||
handleTextInput: function(e) {
|
handleTextInput: function(e) {
|
||||||
this.setState({input: e.target.value});
|
this.setState({input: e.target.value});
|
||||||
if (this.props.filter) {
|
if (this.props.filterFunc) {
|
||||||
this.props.filter(e.target.value);
|
this.props.filterFunc(e.target.value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
removeChipAt: function(idx) {
|
removeChipAt: function(idx) {
|
||||||
var removed = this.props.chips[idx];
|
var removed = this.state.sortedChips[idx];
|
||||||
this.props.onChipRemoved(removed.user, idx);
|
this.props.onChipRemoved(removed.user, this.state.chipIndex[removed.user]);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleChipCancel: function(item, idx) {
|
handleChipCancel: function(item, idx) {
|
||||||
@@ -383,25 +412,30 @@
|
|||||||
|
|
||||||
handleKeyDown: function(e) {
|
handleKeyDown: function(e) {
|
||||||
if (e.key === 'Backspace') {
|
if (e.key === 'Backspace') {
|
||||||
if (this.state.input.length == 0 && this.props.chips.length > 0) {
|
if (this.state.input.length == 0 && this.state.sortedChips.length > 0) {
|
||||||
this.removeChipAt(this.props.chips.length-1);
|
var at = this.state.sortedChips.length - 1;
|
||||||
|
if (this.state.sortedChips[at].user !== this.props.required) {
|
||||||
|
this.removeChipAt(at);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
var chips = [];
|
var chips = [];
|
||||||
|
|
||||||
var instance = this;
|
var instance = this;
|
||||||
var count = 0;
|
var count = 0;
|
||||||
this.props.chips.map(function(item) {
|
this.state.sortedChips.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}
|
||||||
|
required={item.user === instance.props.required}
|
||||||
index={count}
|
index={count}
|
||||||
key={count} />
|
key={item.user} />
|
||||||
);
|
);
|
||||||
count++;
|
count++;
|
||||||
});
|
});
|
||||||
@@ -437,9 +471,9 @@
|
|||||||
title={this.props.title} />
|
title={this.props.title} />
|
||||||
</div>
|
</div>
|
||||||
<span>{this.props.title}</span>
|
<span>{this.props.title}</span>
|
||||||
{this.props.onCancel ?
|
{this.props.onCancel && !this.props.required ?
|
||||||
<a href="#" onClick={this.handleCancel} >×</a>
|
<a href="#" onClick={this.handleCancel} >×</a>
|
||||||
: null}
|
: <span className="spacer" />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -906,6 +940,7 @@
|
|||||||
title={this.state.fullName}
|
title={this.state.fullName}
|
||||||
onImageChanged={this.handleImageChanged} />
|
onImageChanged={this.handleImageChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="hr" />
|
||||||
<div className="panel-form-column">
|
<div className="panel-form-column">
|
||||||
<div className="panel-form-row">
|
<div className="panel-form-row">
|
||||||
<label>Login:</label>
|
<label>Login:</label>
|
||||||
@@ -1103,22 +1138,18 @@
|
|||||||
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) {
|
|
||||||
console.error("empty key!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If filter function is provided, filter out the items
|
// If filter function is provided, filter out the items
|
||||||
// which don't satisfy the condition.
|
// which don't satisfy the condition.
|
||||||
if (instance.props.filterFunc && instance.props.filter) {
|
if (instance.props.filterFunc && instance.props.filter) {
|
||||||
var content = [key];
|
var content = [key];
|
||||||
if (c.private) {
|
if (c.private) {
|
||||||
content.push(c.private);
|
content.push(("" + c.private).toLowerCase());
|
||||||
}
|
}
|
||||||
if (c.public && c.public.fn) {
|
if (c.public && c.public.fn) {
|
||||||
content.push(c.public.fn);
|
content.push(("" + c.public.fn).toLowerCase());
|
||||||
}
|
}
|
||||||
if (!instance.props.filter(instance.props.filter, content)) {
|
if (!instance.props.filterFunc(instance.props.filter, content)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1168,6 +1199,8 @@
|
|||||||
members: this.props.members,
|
members: this.props.members,
|
||||||
contacts: this.props.contacts,
|
contacts: this.props.contacts,
|
||||||
index: this.indexMembers(this.props.members),
|
index: this.indexMembers(this.props.members),
|
||||||
|
contactFilter: '',
|
||||||
|
noContactsMessage: 'You have no contacts :-(',
|
||||||
selectedContacts: this.selectedContacts(this.props.members)
|
selectedContacts: this.selectedContacts(this.props.members)
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -1231,6 +1264,26 @@
|
|||||||
this.setState({members: m, index: i, selectedContacts: sel});
|
this.setState({members: m, index: i, selectedContacts: sel});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleContactFilter: function(val) {
|
||||||
|
var msg = !val ?
|
||||||
|
"You have no contacts :-(" :
|
||||||
|
"No contacts match '" + val + "'";
|
||||||
|
|
||||||
|
this.setState({contactFilter: val, noContactsMessage: msg});
|
||||||
|
},
|
||||||
|
|
||||||
|
doContactFiltering: function(filter, values) {
|
||||||
|
if (filter) {
|
||||||
|
for (var i=0; i<values.length; i++) {
|
||||||
|
if (values[i].indexOf(filter) >= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
handleSubmit: function() {
|
handleSubmit: function() {
|
||||||
var instance = this;
|
var instance = this;
|
||||||
var members = [];
|
var members = [];
|
||||||
@@ -1264,7 +1317,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<ChipInput
|
<ChipInput
|
||||||
chips={this.state.members}
|
chips={this.state.members}
|
||||||
|
required={this.props.requiredMember}
|
||||||
prompt="add members"
|
prompt="add members"
|
||||||
|
filterFunc={this.handleContactFilter}
|
||||||
onChipRemoved={this.handleMemberRemoved} />
|
onChipRemoved={this.handleMemberRemoved} />
|
||||||
<div className="panel-form-row">
|
<div className="panel-form-row">
|
||||||
<label className="small">All contacts</label>
|
<label className="small">All contacts</label>
|
||||||
@@ -1272,6 +1327,9 @@
|
|||||||
<ContactList
|
<ContactList
|
||||||
contacts={this.props.contacts}
|
contacts={this.props.contacts}
|
||||||
topicSelected={this.state.selectedContacts}
|
topicSelected={this.state.selectedContacts}
|
||||||
|
filter={this.state.contactFilter}
|
||||||
|
filterFunc={this.doContactFiltering}
|
||||||
|
emptyListMessage={this.state.noContactsMessage}
|
||||||
onTopicSelected={this.handleContactSelected} />
|
onTopicSelected={this.handleContactSelected} />
|
||||||
<div id="group-manager-buttons" className="panel-form-row">
|
<div id="group-manager-buttons" className="panel-form-row">
|
||||||
<button className="blue" onClick={this.handleSubmit}>OK</button>
|
<button className="blue" onClick={this.handleSubmit}>OK</button>
|
||||||
@@ -1598,6 +1656,7 @@
|
|||||||
{this.state.showMemberPanel ?
|
{this.state.showMemberPanel ?
|
||||||
<GroupManager
|
<GroupManager
|
||||||
members={this.state.contactList}
|
members={this.state.contactList}
|
||||||
|
requiredMember={this.props.myUserId}
|
||||||
contacts={this.props.foundContacts}
|
contacts={this.props.foundContacts}
|
||||||
onCancel={this.handleHideAddMembers}
|
onCancel={this.handleHideAddMembers}
|
||||||
onSubmit={this.handleMemberUpdateRequest} />
|
onSubmit={this.handleMemberUpdateRequest} />
|
||||||
@@ -1624,6 +1683,7 @@
|
|||||||
title={this.state.fullName}
|
title={this.state.fullName}
|
||||||
onImageChanged={this.handleImageChanged} />
|
onImageChanged={this.handleImageChanged} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="hr" />
|
||||||
<div className="panel-form-column">
|
<div className="panel-form-column">
|
||||||
<div className="panel-form-row">
|
<div className="panel-form-row">
|
||||||
<label>Address:</label>
|
<label>Address:</label>
|
||||||
@@ -1650,6 +1710,7 @@
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="hr" />
|
||||||
{this.state.groupTopic ?
|
{this.state.groupTopic ?
|
||||||
<div className="panel-form-column">
|
<div className="panel-form-column">
|
||||||
<div className="panel-form-row">
|
<div className="panel-form-row">
|
||||||
@@ -2580,6 +2641,7 @@
|
|||||||
<InfoView
|
<InfoView
|
||||||
topic={this.state.topicSelected}
|
topic={this.state.topicSelected}
|
||||||
foundContacts={this.state.foundContacts}
|
foundContacts={this.state.foundContacts}
|
||||||
|
myUserId={this.state.myUserId}
|
||||||
onSubmit={this.handleTopicUpdateRequest}
|
onSubmit={this.handleTopicUpdateRequest}
|
||||||
onCancel={this.handleHideInfoView}
|
onCancel={this.handleHideInfoView}
|
||||||
onChangePermissions={this.handleChangePermissions}
|
onChangePermissions={this.handleChangePermissions}
|
||||||
|
|||||||
Reference in New Issue
Block a user