From b689d08e712fc71b0cd871f33386b2bd3c9dfe56 Mon Sep 17 00:00:00 2001 From: or-else Date: Thu, 6 Jul 2017 16:15:56 +0300 Subject: [PATCH] subscriber management is somewhat functional --- css/base.css | 59 +++++++-- index.html | 338 +++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 305 insertions(+), 92 deletions(-) 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