From 933ede1220f6c9a2863c6bea49183d09e869426a Mon Sep 17 00:00:00 2001 From: or-else Date: Tue, 27 Jun 2017 20:58:27 +0300 Subject: [PATCH 1/7] separated contact list into a component --- css/base.css | 17 +++++++++- index.html | 91 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 81 insertions(+), 27 deletions(-) diff --git a/css/base.css b/css/base.css index cdcfde3f..c4081f7c 100644 --- a/css/base.css +++ b/css/base.css @@ -461,6 +461,7 @@ ul.menu > li.separator:hover { display: flex; padding: 0.35rem 0.25rem 0.5rem 0.35rem; min-height: 3.5rem; + border-bottom: 1px solid #eee; } .contact-box > li:hover { background-color: #eceff1; /* blue-gray 50 highlight */ @@ -1044,6 +1045,10 @@ ul.tabbar li.active > a:after { color: rgba(250, 250, 250, 0.8); /* default text color #FAFAFA, gray 50 */ } +.lettertile:hover { + color: white; +} + /* Colors of letter tiles */ .lettertile.color0 { background-color: #ef9a9a; @@ -1073,7 +1078,7 @@ ul.tabbar li.active > a:after { background-color: #c5e1a5; } .lettertile.color9 { - background-color: #fff59d; + background-color: #f0e57d; } .lettertile.color10 { background-color: #f48fb1; @@ -1161,3 +1166,13 @@ ul.tabbar li.active > a:after { background-color: #c0c0c0; border-radius: 50%; } + +.chip { + display: inline-block; + background-color: white; + height: 2rem; + line-height: 2; + border-radius: 1rem; + padding: 0 1rem; + background-color: #ccc; +} diff --git a/index.html b/index.html index c5cc4165..3cc5b688 100644 --- a/index.html +++ b/index.html @@ -885,6 +885,11 @@ /* ContactsView holds all contacts-related stuff */ var ContactsView = React.createClass({ + getInitialState: function() { + return { + contactList: this.prepareContactList() + }; + }, componentWillMount: function() { var me = Tinode.getMeTopic(); me.onContactUpdate = this.tnMeContactUpdate; @@ -900,13 +905,13 @@ // Reactions to updates to the contact list. tnMeContactUpdate: function(what, cont) { if (what === "on" || what === "off" || what === "read") { - this.forceUpdate(); + this.resetContactList(); } else if (what === "msg") { // New message received // Skip update if the topic is currently open, otherwise the badge will annoyingly flash. if (this.props.topicSelected !== cont.topic) { - this.forceUpdate(); + this.resetContactList(); } } else if (what === "recv") { // Explicitly ignoring "recv" -- it causes no visible updates to contact list. @@ -918,7 +923,7 @@ this.props.onTopicSelected(null); } // Redraw without the deleted topic. - this.forceUpdate(); + this.resetContactList(); } else { // TODO(gene): handle other types of notifications: // * ua -- user agent changes (may display a pictogram for mobile/desktop) @@ -927,34 +932,68 @@ console.log("changed (not updating for it): " + what + " in: " + cont.topic); } }, + tnInvite: function(invite) { // TODO(gene): implement invite handling console.log("Got invite:"); console.log(invite); }, + tnMeSubsUpdated: function(names) { - this.forceUpdate(); + this.resetContactList(); }, + + prepareContactList: function() { + var contacts = []; + Tinode.getMeTopic().contacts(function(c) { + c.unread = c.seq; + if (c.read > 0) { + c.unread -= c.read; + } + contacts.push(c); + }, this); + + return contacts; + }, + + resetContactList: function() { + this.setState({contactList: this.prepareContactList()}); + }, + + render: function() { + return ( + You have no chats
¯\_(ツ)_/¯} + topicSelected={this.props.topicSelected} + onTopicSelected={this.props.onTopicSelected} + showContextMenu={this.props.showContextMenu} /> + ); + } + }); + /* END Contact list */ + + /* BEGIN ContactList: component for showing a list of contacts, + * such as a list of group members group chat */ + var ContactList = React.createClass({ render: function() { var contactNodes = []; - Tinode.getMeTopic().contacts(function(c) { - var unread = c.seq; - if (c.read > 0) { - unread -= c.read; - } - contactNodes.push( - - ); - }, this); + if (this.props.contacts && this.props.contacts.length > 0) { + this.props.contacts.map(function(c) { + contactNodes.push( + + ); + }, this); + } return (
@@ -963,14 +1002,14 @@ {contactNodes} : -

- You have no chats
¯\_(ツ)_/¯ -

} +
+ {this.props.emptyListMessage} +
}
); } }); - /* END Contact list */ + /* END ContactList */ /* BEGIN Create new topic and invite users or send an invite */ var NewTopicView = React.createClass({ From 45eb91765b5b651d0754eedd0065cabaf9bbff9d Mon Sep 17 00:00:00 2001 From: or-else Date: Thu, 29 Jun 2017 14:02:16 +0300 Subject: [PATCH 2/7] more refactoring of topic and account editing --- css/base.css | 39 ++++++-- index.html | 278 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 219 insertions(+), 98 deletions(-) diff --git a/css/base.css b/css/base.css index c4081f7c..3d00b132 100644 --- a/css/base.css +++ b/css/base.css @@ -40,14 +40,15 @@ a:focus { form { font-size: 1.1rem; padding: 0.5rem 0.75rem; - line-height: 1.75; + line-height: 1.5; } -form label { +label { font-size: 1.1rem; + margin-right: auto; } -form label.small { +label.small { color: #1e88e5; font-size: 0.95rem; } @@ -251,7 +252,7 @@ ul.menu > li.separator:hover { position: relative; display: flex; flex-direction: column; - flex-grow: 0.3; + flex-grow: 0; flex-shrink: 0.3; width: 24rem; min-width: 18rem; @@ -268,7 +269,7 @@ ul.menu > li.separator:hover { #info-view { display: flex; flex-direction: column; - flex-grow: 0.3; + flex-grow: 0; flex-shrink: 0.3; width: 24rem; min-width: 18rem; @@ -461,6 +462,8 @@ ul.menu > li.separator:hover { display: flex; padding: 0.35rem 0.25rem 0.5rem 0.35rem; min-height: 3.5rem; + flex-grow: 0; + flex-shrink: 0; border-bottom: 1px solid #eee; } .contact-box > li:hover { @@ -544,20 +547,35 @@ ul.menu > li.separator:hover { flex-direction: column; } -.panel-form-one { +.panel-form-row { display: flex; justify-content: flex-start; align-items: center; + padding: 0.5rem 0.75rem; + line-height: 1.5; + flex-grow: 0; + flex-shrink: 0; } -.panel-form-two { +.panel-form-column { display: flex; flex-direction: column; - flex-grow: 1; + flex-grow: 0; + flex-shrink: 0; + padding: 0.5rem 0.75rem; + line-height: 1.5; +} +/* Make sure padding is only applied at + the top level panel */ +.panel-form-row > .panel-form-column { + padding: 0!important; + flex-grow: 1; +} +.panel-form-column > .panel-form-row { + padding: 0!important; } - -/* Styling of avatar upload area in the New Account panel */ +/* Styling of avatar upload control */ .avatar-upload { display: flex; flex-direction: column; @@ -565,6 +583,7 @@ ul.menu > li.separator:hover { width: 7rem; height: 9rem; margin-left: 0.5rem; + font-size: 4.5rem; padding: 0; } /* Clearfix */ diff --git a/index.html b/index.html index 3cc5b688..30b2f2ed 100644 --- a/index.html +++ b/index.html @@ -244,9 +244,13 @@ this.setState({text: e.target.value}); }, - handleKeyPress: function(e) { + handleKeyDown: function(e) { if (e.keyCode === 27) { + // Escape pressed this.setState({text: this.props.text, active: false}); + } else if (e.keyCode === 13) { + // Enter pressed + this.handleEditingFinished(); } }, @@ -257,8 +261,9 @@ handleEditingFinished: function() { this.setState({active: false}); - if (this.state.text !== this.props.text) { - this.props.onFinished(this.state.text); + var text = this.state.text.trim(); + if (text !== this.props.text) { + this.props.onFinished(text); } }, @@ -271,8 +276,8 @@ spanText = this.props.placeholder; spanClass += " placeholder"; } - if (spanText.length > 16) { - spanText = spanText.substring(0, 16) + "..."; + if (spanText.length > 20) { + spanText = spanText.substring(0, 19) + "..."; } return ( this.state.active ? @@ -280,7 +285,7 @@ value={this.state.text} placeholder={this.props.placeholder} onChange={this.handeTextChange} - onKeyPress={this.handleKeyPress} + onKeyDown={this.handleKeyDown} onBlur={this.handleEditingFinished} autoFocus /> : @@ -448,8 +453,8 @@ } return (
-
-
+
+
{this.state.dataUrl ? : -
128×128
} + this.props.readOnly && this.props.uid ? + : +
128×128
} {this.props.readOnly ? null : } @@ -726,8 +733,6 @@ var EditAccountView = React.createClass({ getInitialState: function() { return { - updatedDesc: false, - updatedPasswd: false, address: null, fullName: undefined, avatar: null, @@ -743,7 +748,6 @@ fullName: me.public ? me.public.fn : undefined, avatar: makeImageUrl(me.public ? me.public.photo : null), address: Tinode.getCurrentUserID(), - login: this.props.login, auth: defacs.auth, anon: defacs.anon }); @@ -758,29 +762,23 @@ }, handleFullNameUpdate: function(fn) { - this.setState({updatedDesc: true, fullName: fn}); + this.setState({fullName: fn}); + this.props.onSubmit(null, vcard(fn, this.state.avatar)); }, handlePasswordUpdate: function(pwd) { - this.setState({updatedPasswd: true, password: pwd}); + this.setState({password: pwd}); + this.props.onSubmit(pwd, null); }, handleImageChanged: function(img) { - this.setState({updatedDesc: true, avatar: img}); - }, - - handleSubmit: function(e) { - e.preventDefault(); - if (this.state.updatedDesc || this.state.updatedPasswd) { - this.props.onSubmit( - this.state.updatedPasswd ? this.state.password : null, - this.state.updatedDesc ? vcard(this.state.fullName, this.state.avatar) : null); - } + this.setState({avatar: img}); + this.props.onSubmit(null, vcard(this.state.fullName, img)); }, render: function() { return ( - -
-
+
+
+
-
-
Login: {this.state.login}
-
Address: {this.state.address}
- -
+
+
+ + {this.props.login} +
+
+ + {this.state.address} +
+
+ +
Auth: {this.state.auth}
Anon: {this.state.anon}
-
- -
- +
); } }); @@ -845,7 +849,7 @@
- + {this.props.showOnline ? : null}
{title} @@ -966,6 +970,7 @@ contacts={this.state.contactList} emptyListMessage={You have no chats
¯\_(ツ)_/¯
} topicSelected={this.props.topicSelected} + showOnline={true} onTopicSelected={this.props.onTopicSelected} showContextMenu={this.props.showContextMenu} /> ); @@ -980,6 +985,10 @@ var contactNodes = []; 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!"); + } contactNodes.push( + topic={key} + key={key} /> ); }, this); } @@ -1109,8 +1119,8 @@ } return (
-
-
+
+
@@ -1152,7 +1162,7 @@ render: function() { return ( -
+
@@ -1211,11 +1221,14 @@ getInitialState: function() { return { owner: false, - updated: false, address: null, + groupTopic: undefined, fullName: undefined, avatar: null, - private: null + private: null, + selectedContact: null, + muted: undefined, + contactList: [] }; }, componentWillMount: function() { @@ -1225,6 +1238,31 @@ } topic.onMetaDesc = this.onMetaDesc; + topic.onSubsUpdated = this.onSubsUpdated; + this.resetDesc(topic); + this.resetSubs(topic); + }, + + componentWillUnmount: function() { + var topic = Tinode.getTopic(this.props.topic); + if (!topic) { + return; + } + topic.onMetaDesc = undefined; + topic.onSubsUpdated = undefined; + }, + + resetSubs: function(topic) { + var contacts = []; + topic.subscribers(function(sub){ + contacts.push(sub); + }, this); + this.setState({ + contactList: contacts + }); + }, + + resetDesc: function(topic) { var defacs = topic.getDefaultAccess() || {}; var acs = topic.getAccessMode(); this.setState({ @@ -1233,30 +1271,62 @@ avatar: makeImageUrl(topic.public ? topic.public.photo : null), private: topic.private, address: topic.name, + groupTopic: (topic.getType() === "grp"), auth: defacs.auth, - anon: defacs.anon + anon: defacs.anon, + muted: acs && acs.isMuted() }); }, - componentWillUnmount: function() { + + onMetaDesc: function(desc) { var topic = Tinode.getTopic(this.props.topic); if (!topic) { return; } - topic.onMetaDesc = undefined; + this.resetDesc(topic); }, - onMetaDesc: function(desc) { - // Update form with changed values + onSubsUpdated: function() { + var topic = Tinode.getTopic(this.props.topic); + if (!topic) { + return; + } + this.resetSubs(topic); }, handleFullNameUpdate: function(fn) { - this.setState({updated: true, fullName: fn}); + this.setState({fullName: fn}); + this.props.onSubmit(this.props.topic, + vcard(fn, this.state.avatar), + null); }, + handlePrivateUpdate: function(priv) { - this.setState({updated: true, private: priv}); + this.setState({private: priv}); + this.props.onSubmit(this.props.topic, + null, + priv); }, + handleImageChanged: function(img) { - this.setState({updated: true, avatar: img}); + this.setState({avatar: img}); + this.props.onSubmit(this.props.topic, + vcard(this.state.fullName, img), + priv); + }, + + handleMuted: function(e) { + var checked = e.target.checked; + this.setState({muted: checked}); + this.props.onChangePermissions(this.props.topic, checked ? "-P" : "+P"); + }, + + handleMemberSelected: function(uid) { + this.setState({selectedContact: uid}); + }, + + handleContextMenu: function(item) { + console.log("context menu: " + item); }, handleSubmit: function(e) { @@ -1277,43 +1347,63 @@
- -
-
-
-
-
-
-
- +
+
+
+
+
+
-
-
Address: {this.state.address}
- {this.state.auth || this.state.anon ? -
-
-
-
Auth: {this.state.auth}
-
Anon: {this.state.anon}
-
+ +
+
+
+ + {this.state.address} +
+
+ + +
+ {this.state.auth || this.state.anon ? +
+
+
+
Auth: {this.state.auth}
+
Anon: {this.state.anon}
- : - null - } -
-
- -
- +
+ : + null + } +
+ {this.state.groupTopic ? +
+ +
: null} + {this.state.groupTopic ? + + : null}
); } @@ -1897,7 +1987,6 @@ }, handleUpdateAccountRequest: function(password, pub, priv) { - this.handleSidepanelCancel(); if (pub || priv) { Tinode.getMeTopic().setMeta({desc: {public: pub, private: priv}}); } @@ -1964,6 +2053,18 @@ var topic = Tinode.getTopic(topicName); topic.setMeta({desc: {public: pub, private: priv}}); }, + + handleChangePermissions: function(topicName, want) { + var topic = Tinode.getTopic(topicName); + if (!topic) { + return; + } + + var am = topic.getAccessMode(); + am.updateWant(want); + topic.setMeta({sub: {mode: am.getWant()}}); + }, + /** 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 */ @@ -2122,6 +2223,7 @@ topic={this.state.topicSelected} onSubmit={this.handleTopicUpdateRequest} onCancel={this.handleHideInfoView} + onChangePermissions={this.handleChangePermissions} /> : null From e79444e5f7374f65da0b6291740909efe69f7bc5 Mon Sep 17 00:00:00 2001 From: or-else Date: Sun, 2 Jul 2017 16:16:45 +0300 Subject: [PATCH 3/7] lets of bug fixes --- README.md | 6 +- css/base.css | 44 +++++-- index.html | 344 ++++++++++++++++++++++++++++++--------------------- 3 files changed, 239 insertions(+), 155 deletions(-) diff --git a/README.md b/README.md index 1cbd72ae..0bf5baf2 100644 --- a/README.md +++ b/README.md @@ -16,14 +16,14 @@ Background pattern from http://subtlepatterns.com/, commercial and non-commercia ## Missing functionality -* Cannot manage group members. -* Editing chat parameters is incomplete and buggy. +* Cannot manage members of group chats. * Cannot send images or other media. * No typing indicators. -* No indicators of chat status (muted, blocked, etc.). +* No indication of chat status (muted, blocked, etc.). * Long polling as a wire transport does not work. * Browser push notifications are missing. * No encryption. +* Editing chat parameters is incomplete. ## Responsive design diff --git a/css/base.css b/css/base.css index 3d00b132..98ac031c 100644 --- a/css/base.css +++ b/css/base.css @@ -37,6 +37,29 @@ a:focus { outline-offset: -2px; } +a.red:hover, +a.red:focus { + color: #F44336; +} + +a.flat-button { + display: flex; + align-items: center; + margin: 0.15rem 0.35rem; + height: 2em; + line-height: 2em; + font-weight: normal; + text-align: center; + vertical-align: middle; + touch-action: manipulation; + white-space: nowrap; + text-transform: uppercase; +} +a.flat-button:hover, +a.flat-button:focus { + text-decoration: none; +} + form { font-size: 1.1rem; padding: 0.5rem 0.75rem; @@ -226,6 +249,16 @@ ul.menu > li.separator:hover { } /* END popup/dropdown menu */ +/* Generic text colors */ +.blue { + color: #2196f3; /* blue 500 */ +} + +.red { + color: #D32F2F; /* red 700 */ +} +/* END generic text colors */ + /* END of basic elements */ #mountPoint { @@ -447,9 +480,9 @@ ul.menu > li.separator:hover { overflow: hidden; } -.contacts-panel { +.scrollable-panel { overflow-y: auto; - margin-top: 1rem;: + margin-top: 0.25rem; margin-bottom: auto; height: 100%; } @@ -1035,11 +1068,6 @@ ul.tabbar li.active > a:after { vertical-align: middle; } -/* Color of buttons */ -.blue { - color: #2196f3; /* blue 500 */ -} - /* Letter tile base style, responsive square box */ .lettertile { width: 100%; @@ -1178,7 +1206,7 @@ ul.tabbar li.active > a:after { } #self-avatar.material-icons, .avatar-box .material-icons { - font-size: 3rem; + font-size: 2.4rem; } .avatar-box .material-icons { color: rgba(250, 250, 250, 0.8); /* #fafafa, gray 50 */ diff --git a/index.html b/index.html index 30b2f2ed..ade2026f 100644 --- a/index.html +++ b/index.html @@ -262,7 +262,8 @@ handleEditingFinished: function() { this.setState({active: false}); var text = this.state.text.trim(); - if (text !== this.props.text) { + if ((text || this.props.text) && (text !== this.props.text)) { + console.log("'" + text + "' != '" + this.props.text + "'"); this.props.onFinished(text); } }, @@ -452,7 +453,7 @@ submitClasses += " disabled"; } return ( -
+
- -
- +
+
- +
+ +
+
); } }); @@ -841,11 +844,10 @@ } else if (title.length > 30) { title = title.substring(0, 28) + "..."; } - var selected = (this.props.selected === this.props.topic) ? "selected" : null; var online = this.props.now ? "online" : "offline"; var avatar = this.props.avatar ? this.props.avatar : true; return ( -
  • +
  • @@ -983,19 +985,37 @@ var ContactList = React.createClass({ render: function() { var contactNodes = []; + var instance = this; 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!"); + return; } + + // If filter function is provided, filter out the items + // which don't satisfy the condition. + if (instance.props.filterFunc && instance.props.filter { + var content = [key]; + if (c.private) { + content.push(c.private); + } + if (c.public && c.public.fn) { + content.push(c.public.fn); + } + if (!instance.props.filter(instance.props.filter, content)) { + return; + } + } + contactNodes.push( +
    {contactNodes.length > 0 ?
      {contactNodes} @@ -1027,6 +1047,7 @@ return { tabSelected: "p2p", searchQuery: '', + contactList: [], contactSelected: null }; }, @@ -1036,6 +1057,7 @@ fnd.onMetaDesc = this.fndMetaDesc; if (fnd.isSubscribed()) { this.setState({searchQuery: fnd.private ? fnd.private.toString() : ''}); + this.fndSubsUpdated(); } else { fnd.subscribe({get: {sub: {}, desc: {}}}); } @@ -1051,8 +1073,12 @@ } }, fndSubsUpdated: function() { - //console.log(keys); - this.forceUpdate(); + 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) { e.preventDefault(); @@ -1086,7 +1112,13 @@ - +
    }
    ); @@ -1097,19 +1129,24 @@ getInitialState: function() { return { fn: '', // full/formatted name + private: '', imageDataUrl: null }; }, handleFnChange: function(e) { this.setState({fn: e.target.value}); }, + handlePrivateChange: function(e) { + this.setState({private: e.target.value}); + }, handleImageChanged: function(img) { this.setState({imageDataUrl: img}); }, handleSubmit: function(e) { e.preventDefault(); if (this.state.fn && this.state.fn.trim()) { - this.props.onSubmit(this.state.fn.trim(), this.state.imageDataUrl); + this.props.onSubmit(this.state.fn.trim(), + this.state.imageDataUrl, this.state.private.trim()); } }, render: function() { @@ -1118,18 +1155,22 @@ submitClasses += " disabled"; } return ( -
    +
    - Group name + -
    - -
    + +
    - +
    + +
    +
    ); } }); @@ -1161,55 +1202,15 @@ }, render: function() { return ( -
    +
    - - ); - } - }); - - NewTopicView.ContactsList = React.createClass({ - getInitialState: function() { - return { - selectedUser: null - }; - }, - handleSelectionChange: function(sel) { - if (this.props.type === "p2p") { - this.props.onContactSelected(sel); - } else { - console.log(sel); - } - }, - render: function() { - var contactNodes = []; - // Don't attempt to create P2P topics which already exist. Server will reject the duplicates. - var me = Tinode.getMeTopic(); - Tinode.getFndTopic().subscribers(function(s) { - var cont = me ? me.getContact(s.user) : null; - var key = cont ? cont.topic : s.user; - - contactNodes.push( - - ); - }, this); - return ( -
    -
      - {contactNodes} -
    ); } @@ -1228,6 +1229,9 @@ private: null, selectedContact: null, muted: undefined, + access: null, + auth: null, + anon: null, contactList: [] }; }, @@ -1267,14 +1271,17 @@ var acs = topic.getAccessMode(); this.setState({ owner: acs && acs.isOwner(), + sharer: acs && (acs.isAdmin() || acs.isSharer()), + muted: acs && acs.isMuted(), + fullName: topic.public ? topic.public.fn : undefined, avatar: makeImageUrl(topic.public ? topic.public.photo : null), private: topic.private, address: topic.name, groupTopic: (topic.getType() === "grp"), + access: acs.getMode(), auth: defacs.auth, - anon: defacs.anon, - muted: acs && acs.isMuted() + anon: defacs.anon }); }, @@ -1295,24 +1302,28 @@ }, handleFullNameUpdate: function(fn) { - this.setState({fullName: fn}); - this.props.onSubmit(this.props.topic, - vcard(fn, this.state.avatar), - null); + if (this.state.fullName !== fn) { + this.setState({fullName: fn}); + this.props.onSubmit(this.props.topic, + vcard(fn, this.state.avatar), + null); + } }, handlePrivateUpdate: function(priv) { - this.setState({private: priv}); - this.props.onSubmit(this.props.topic, - null, - priv); + if (this.state.priv !== priv) { + this.setState({private: priv}); + this.props.onSubmit(this.props.topic, + null, + priv); + } }, handleImageChanged: function(img) { this.setState({avatar: img}); this.props.onSubmit(this.props.topic, vcard(this.state.fullName, img), - priv); + null); }, handleMuted: function(e) { @@ -1321,6 +1332,14 @@ this.props.onChangePermissions(this.props.topic, checked ? "-P" : "+P"); }, + handleAddMembers: function() { + this.props.onAddMember(this.props.topic); + }, + + handleLeave: function() { + this.props.onLeaveTopic(this.props.topic); + }, + handleMemberSelected: function(uid) { this.setState({selectedContact: uid}); }, @@ -1329,15 +1348,6 @@ console.log("context menu: " + item); }, - handleSubmit: function(e) { - e.preventDefault(); - if (this.state.updated) { - this.props.onSubmit(this.props.topic, - vcard(this.state.fullName,this.state.avatar), - this.state.private); - } - }, - render: function() { return (
    @@ -1347,63 +1357,89 @@
    -
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    -
    -
    -
    -
    -
    - -
    -
    -
    - - {this.state.address} -
    -
    - - -
    - {this.state.auth || this.state.anon ? -
    -
    -
    -
    Auth: {this.state.auth}
    -
    Anon: {this.state.anon}
    +
    + + {this.state.address} +
    +
    + + {this.state.access} +
    +
    + + +
    + {this.state.auth || this.state.anon ? +
    +
    +
    +
    Auth: {this.state.auth}
    +
    Anon: {this.state.anon}
    +
    + : + null + } +
    + {this.state.groupTopic ? +
    +
    + +
    +
    + {this.state.sharer ? + + person_add Add members + + : null} + {!this.state.owner ? + + exit_to_app Leave + + : null} +
    +
    : - null + }
    - {this.state.groupTopic ? -
    - -
    : null} - {this.state.groupTopic ? - - : null}
    ); } @@ -2027,7 +2063,8 @@ errorText: '', topicSelected: peerName, sidePanelSelected: 'contacts', - mobilePanel: 'topic-view' + mobilePanel: 'topic-view', + showInfoPanel: false }); } else { var topic = peerName ? Tinode.newTopicWith(peerName) : Tinode.newTopic(); @@ -2041,7 +2078,8 @@ errorText: '', topicSelected: topic.name, sidePanelSelected: 'contacts', - mobilePanel: 'topic-view' + mobilePanel: 'topic-view', + showInfoPanel: false }); }).catch(function(err) { instance.setState({errorText: err.message}); @@ -2049,20 +2087,36 @@ } }, + handleManageGroupMembers: function(topicName) { + // FIXME(gene): launch group member manager + }, + handleTopicUpdateRequest: function(topicName, pub, priv) { var topic = Tinode.getTopic(topicName); - topic.setMeta({desc: {public: pub, private: priv}}); + if (topic) { + topic.setMeta({desc: {public: pub, private: priv}}); + } }, handleChangePermissions: function(topicName, want) { + var topic = Tinode.getTopic(topicName); + if (topic) { + var am = topic.getAccessMode(); + am.updateWant(want); + topic.setMeta({sub: {mode: am.getWant()}}); + } + }, + + handleLeaveUnsubRequest: function(topicName) { var topic = Tinode.getTopic(topicName); if (!topic) { return; } - - var am = topic.getAccessMode(); - am.updateWant(want); - topic.setMeta({sub: {mode: am.getWant()}}); + var instance = this; + topic.leave(true).then(function(ctrl) { + // Hide MessageView and InfoView panels. + instance.setState({topicSelected: '', showInfoPanel: false}); + }); }, /** Called when the user enters a contact into the contact search field in the NewAccount panel @@ -2070,7 +2124,7 @@ */ handleSearchContacts: function(query) { var find = Tinode.getFndTopic(); - find.setMeta({desc: {private: query}}).then(function() { + find.setMeta({desc: {private: query}}).then(function(ctrl) { find.getMeta({sub: {}, desc: {}}); }); }, @@ -2224,6 +2278,8 @@ onSubmit={this.handleTopicUpdateRequest} onCancel={this.handleHideInfoView} onChangePermissions={this.handleChangePermissions} + onLeaveTopic={this.handleLeaveUnsubRequest} + onAddMember={this.handleManageGroupMembers} /> : null From 58ae90b5744a2196936bba7969468c3520c60667 Mon Sep 17 00:00:00 2001 From: or-else Date: Tue, 4 Jul 2017 19:23:07 +0300 Subject: [PATCH 4/7] added mateerial chips widget --- css/base.css | 111 +++++++++++++++++-- index.html | 303 ++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 320 insertions(+), 94 deletions(-) diff --git a/css/base.css b/css/base.css index 98ac031c..adb69345 100644 --- a/css/base.css +++ b/css/base.css @@ -408,6 +408,7 @@ ul.menu > li.separator:hover { } .avatar-box { + position: relative; flex-grow: 0; flex-shrink: 0; } @@ -489,6 +490,7 @@ ul.menu > li.separator:hover { .contact-box { list-style: none; + overflow-x: hidden; } .contact-box > li { @@ -498,6 +500,8 @@ ul.menu > li.separator:hover { flex-grow: 0; flex-shrink: 0; border-bottom: 1px solid #eee; + border-collapse: collapse; + overflow-x: hidden; } .contact-box > li:hover { background-color: #eceff1; /* blue-gray 50 highlight */ @@ -506,8 +510,19 @@ ul.menu > li.separator:hover { background-color: #cfd8dc; /* blue-gray 100 highlight */ } +.contact-box .text-box { + overflow-x: hidden; +} + .contact-box .contact-title { font-size: 1.05rem; + text-overflow: ellipsis; + overflow: hidden +} + +.contact-box .contact-comment { + text-overflow: ellipsis; + overflow: hidden } /* The down arrow head - trigger for context menu in contacts list */ @@ -558,7 +573,7 @@ ul.menu > li.separator:hover { border-radius: 50%; position: absolute; right: 0; - bottom: 0; + bottom: 0.1rem; } .contact-box .online { background-color: #4caf50; @@ -619,6 +634,13 @@ ul.menu > li.separator:hover { font-size: 4.5rem; padding: 0; } + +.avatar-upload .avatar-box { + position: relative; + width: 7rem; + height: 7rem; +} + /* Clearfix */ .avatar-upload:after { content: "."; @@ -702,6 +724,7 @@ ul.menu > li.separator:hover { #topic-caption-panel .avatar-box { + position: relative; width: 3rem; min-width: 3rem; height: 3rem; @@ -1073,7 +1096,6 @@ ul.tabbar li.active > a:after { width: 100%; padding-bottom: 100%; border-radius: 50%; - position: relative; } /* Div with letter tile content */ @@ -1214,12 +1236,85 @@ ul.tabbar li.active > a:after { border-radius: 50%; } +.chip-input { + display: flex; + flex-wrap: wrap; + cursor: text; + font-size: 1.1rem; + font-weight: normal; + 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; + width: 100%; + margin: 0.25rem 0; + padding: 0 0.125rem 0.35rem 0.125rem; + outline: none; + color: #666; + align-items: center; + justify-content: flex-start; + text-align: center; +} + +.chip-input.focused, +.chip-input:focus { + border-bottom: 1px solid #2196f3; + box-shadow: 0 1px 0 rgba(33, 150, 243, 0.25); +} + +.chip-input > input[type="text"] { + display: inline-block; + width: auto; + border: none; + line-height: 1.8; + box-shadow: none; + min-width: 4rem; + flex: 1 0 auto; + height: 1.8rem; + margin: 0.125rem; +} + .chip { + display: flex; + flex: 0 0 auto; + align-items: center; + height: 1.8rem; + line-height: 1.8; + border-radius: 0.9rem; + margin: 0.125rem; + background-color: #eee; + white-space: nowrap; +} + +.chip > a { display: inline-block; - background-color: white; - height: 2rem; - line-height: 2; - border-radius: 1rem; - padding: 0 1rem; - background-color: #ccc; + color: #ccc; + background-color: #aaa; + border-radius: 50%; + margin: 0.3rem; + width: 1.175rem; + height: 1.175rem; + line-height: 1.15; + text-decoration: none; +} + +.chip > a:focus, +.chip > a:hover { + text-decoration: none; + color: #ddd; + background-color: #bbb; +} + +.chip .avatar-box { + position: relative; + width: 1.8rem; + min-width: 1.8rem; + height: 1.8rem; + min-height: 1.8rem; + font-size: 1rem; + margin-right: 0.2rem; +} + +.chip .avatar-box .material-icons { + font-size: 1.8rem; } diff --git a/index.html b/index.html index ade2026f..64bcac01 100644 --- a/index.html +++ b/index.html @@ -210,7 +210,7 @@ if (this.props.avatar === true) { if (this.props.topic && this.props.title && this.props.title.trim()) { var letter = this.props.title.trim().charAt(0); - var color = "lettertile color" + (Math.abs(stringHash(this.props.topic)) % 16); + var color = "lettertile dark-color" + (Math.abs(stringHash(this.props.topic)) % 16); avatar = (
    {letter}
    ) } else { avatar = (Tinode.getTopicType(this.props.topic) === "grp") ? @@ -296,7 +296,6 @@ ); } }); - /* END InPlaceEdit */ /* BEGIN Combobox for selecting host name */ @@ -344,6 +343,98 @@ } /* END Combobox for selecting host name */ + /* BEGIN ChipInput: group membership widget */ + 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}); + }, + + handleChipCancel: function(item) { + console.log("cancel " + item); + }, + + handleFocusGained: function() { + this.setState({focused: true}); + }, + + handleFocusLost: function() { + this.setState({focused: false}); + }, + + 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}); + } + } + }, + + render: function() { + var chips = []; + var instance = this; + this.state.chips.map(function(item) { + chips.push( + + ); + }); + var className = "chip-input" + (this.state.focused ? " focused" : ""); + return ( +
    + {chips} + +
    + ); + } + }); + + ChipInput.Chip = React.createClass({ + handleCancel: function() { + this.props.onCancel(this.props.title); + }, + + render: function() { + return ( +
    +
    + +
    + {this.props.title} + {this.props.onCancel ? + × + : null} +
    + ); + } + }); + /* END ChipInput */ + /* BEGIN Login: a login form */ var LoginView = React.createClass({ getInitialState: function() { @@ -515,7 +606,10 @@ {this.state.dataUrl ? : this.props.readOnly && this.props.uid ? - : +
    + +
    + :
    128×128
    } {this.props.readOnly ? null :
    {title} {this.props.unread > 0 ? : null}
    - {this.props.comment ?
    {this.props.comment}
    : null} + {this.props.comment ?
    {this.props.comment}
    : null}
    {this.props.showContextMenu ? @@ -996,7 +1090,7 @@ // If filter function is provided, filter out the items // which don't satisfy the condition. - if (instance.props.filterFunc && instance.props.filter { + if (instance.props.filterFunc && instance.props.filter) { var content = [key]; if (c.private) { content.push(c.private); @@ -1041,6 +1135,34 @@ }); /* END ContactList */ + /* BEGIN GroupMembers: controlc for managing a list of group members */ + var GroupManager = React.createClass({ + getInitialState: function() { + return { + members: this.props.members || [], + contacts: this.props.contacts || [], + }; + }, + + handleContactSelected: function(contact) { + + }, + + render: function() { + return ( +
    + + +
    + ); + } + }); + /* END GroupManager */ + /* BEGIN Create new topic and invite users or send an invite */ var NewTopicView = React.createClass({ getInitialState: function() { @@ -1161,6 +1283,7 @@ +
    @@ -1232,9 +1355,11 @@ access: null, auth: null, anon: null, - contactList: [] + contactList: [], + showMemberPanel: false }; }, + componentWillMount: function() { var topic = Tinode.getTopic(this.props.topic); if (!topic) { @@ -1333,7 +1458,8 @@ }, handleAddMembers: function() { - this.props.onAddMember(this.props.topic); + // this.props.onAddMember(this.props.topic); + this.setState({showMemberPanel: true}); }, handleLeave: function() { @@ -1357,89 +1483,94 @@
  • -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    + {this.state.showMemberPanel ? + + : +
    - - {this.state.address} -
    -
    - - {this.state.access} -
    -
    - - -
    - {this.state.auth || this.state.anon ? -
    -
    -
    -
    Auth: {this.state.auth}
    -
    Anon: {this.state.anon}
    -
    +
    +
    +
    +
    +
    + +
    +
    +
    + + {this.state.address} +
    +
    + + {this.state.access} +
    +
    + + +
    + {this.state.auth || this.state.anon ? +
    +
    +
    +
    Auth: {this.state.auth}
    +
    Anon: {this.state.anon}
    +
    +
    : null } +
    + {this.state.groupTopic ? +
    +
    + +
    +
    + {this.state.sharer ? + + person_add Add members + + : null} + {!this.state.owner ? + + exit_to_app Leave + + : null} +
    + +
    + : + + }
    - {this.state.groupTopic ? -
    -
    - -
    -
    - {this.state.sharer ? - - person_add Add members - - : null} - {!this.state.owner ? - - exit_to_app Leave - - : null} -
    - -
    - : - - } -
    + }
    ); } @@ -2087,9 +2218,9 @@ } }, - handleManageGroupMembers: function(topicName) { + //handleManageGroupMembers: function(topicName) { // FIXME(gene): launch group member manager - }, + //onAddMember}, handleTopicUpdateRequest: function(topicName, pub, priv) { var topic = Tinode.getTopic(topicName); From b689d08e712fc71b0cd871f33386b2bd3c9dfe56 Mon Sep 17 00:00:00 2001 From: or-else Date: Thu, 6 Jul 2017 16:15:56 +0300 Subject: [PATCH 5/7] 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 From 8d5f446ccd3d337b5bc126d2bb1dffe63c3df17c Mon Sep 17 00:00:00 2001 From: or-else Date: Sun, 9 Jul 2017 11:22:27 +0300 Subject: [PATCH 6/7] member management is complete now --- README.md | 5 ++- css/base.css | 21 ++++++++++- index.html | 98 ++++++++++++++++++++++++++++++++++++++++++---------- 3 files changed, 102 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 0bf5baf2..b836af10 100644 --- a/README.md +++ b/README.md @@ -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) -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. ## Missing functionality -* Cannot manage members of group chats. * Cannot send images or other media. * No typing indicators. * No indication of chat status (muted, blocked, etc.). * Long polling as a wire transport does not work. * Browser push notifications are missing. * No encryption. -* Editing chat parameters is incomplete. +* Editing chat permissions is incomplete. ## Responsive design diff --git a/css/base.css b/css/base.css index af8956e2..a8229b6a 100644 --- a/css/base.css +++ b/css/base.css @@ -257,6 +257,16 @@ ul.menu > li.separator:hover { .red { 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 of basic elements */ @@ -1274,6 +1284,7 @@ ul.tabbar li.active > a:after { .chip-input { display: flex; flex-wrap: wrap; + flex-grow: 1; cursor: text; font-size: 1.1rem; font-weight: normal; @@ -1282,7 +1293,8 @@ ul.tabbar li.active > a:after { box-shadow: 0 1px 0 rgba(0, 0, 0, 0.1); transition: all 0.1s ease-in-out; width: 100%; - max-height: 8rem; + max-height: 10rem; + min-height: 4rem; overflow-y: scroll; margin: 0.25rem 0rem 0.25rem 0rem; padding: 0 0 0.5rem 0.5rem; @@ -1355,3 +1367,10 @@ ul.tabbar li.active > a:after { .chip .avatar-box .material-icons { font-size: 1.8rem; } + +.chip .spacer { + display: inline-block; + margin: 0.3rem; + width: 0.25rem; + height: 1.175rem; +} diff --git a/index.html b/index.html index 9f2ee3d0..6258fd7a 100644 --- a/index.html +++ b/index.html @@ -347,26 +347,55 @@ getInitialState: function() { return { placeholder: this.props.chips ? "" : this.props.prompt, + sortedChips: this.sortChips(this.props.chips, this.props.required), + chipIndex: this.indexChips(this.props.chips), input: '', focused: false }; }, + componentWillReceiveProps: function(newProps) { 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) { this.setState({input: e.target.value}); - if (this.props.filter) { - this.props.filter(e.target.value); + if (this.props.filterFunc) { + this.props.filterFunc(e.target.value); } }, removeChipAt: function(idx) { - var removed = this.props.chips[idx]; - this.props.onChipRemoved(removed.user, idx); + var removed = this.state.sortedChips[idx]; + this.props.onChipRemoved(removed.user, this.state.chipIndex[removed.user]); }, handleChipCancel: function(item, idx) { @@ -383,25 +412,30 @@ handleKeyDown: function(e) { if (e.key === 'Backspace') { - if (this.state.input.length == 0 && this.props.chips.length > 0) { - this.removeChipAt(this.props.chips.length-1); + if (this.state.input.length == 0 && this.state.sortedChips.length > 0) { + var at = this.state.sortedChips.length - 1; + if (this.state.sortedChips[at].user !== this.props.required) { + this.removeChipAt(at); + } } } }, render: function() { var chips = []; + var instance = this; var count = 0; - this.props.chips.map(function(item) { + this.state.sortedChips.map(function(item) { chips.push( + key={item.user} /> ); count++; }); @@ -437,9 +471,9 @@ title={this.props.title} />
    {this.props.title} - {this.props.onCancel ? + {this.props.onCancel && !this.props.required ? × - : null} + : }
    ); } @@ -906,6 +940,7 @@ title={this.state.fullName} onImageChanged={this.handleImageChanged} />
    +
    @@ -1103,22 +1138,18 @@ 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.error("empty key!"); - return; - } // If filter function is provided, filter out the items // which don't satisfy the condition. if (instance.props.filterFunc && instance.props.filter) { var content = [key]; if (c.private) { - content.push(c.private); + content.push(("" + c.private).toLowerCase()); } 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; } } @@ -1168,6 +1199,8 @@ members: this.props.members, contacts: this.props.contacts, index: this.indexMembers(this.props.members), + contactFilter: '', + noContactsMessage: 'You have no contacts :-(', selectedContacts: this.selectedContacts(this.props.members) }; }, @@ -1231,6 +1264,26 @@ 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= 0) { + return true; + } + } + return false; + } + return true; + }, + handleSubmit: function() { var instance = this; var members = []; @@ -1264,7 +1317,9 @@
    @@ -1272,6 +1327,9 @@
    @@ -1598,6 +1656,7 @@ {this.state.showMemberPanel ? @@ -1624,6 +1683,7 @@ title={this.state.fullName} onImageChanged={this.handleImageChanged} />
    +
    @@ -1650,6 +1710,7 @@ null }
    +
    {this.state.groupTopic ?
    @@ -2580,6 +2641,7 @@ Date: Mon, 10 Jul 2017 19:29:35 +0300 Subject: [PATCH 7/7] topic member management complete --- index.html | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/index.html b/index.html index 6258fd7a..a78beea0 100644 --- a/index.html +++ b/index.html @@ -1514,7 +1514,7 @@ auth: null, anon: null, contactList: [], - showMemberPanel: false + showMemberPanel: this.props.showMemberPanel }; }, @@ -2162,6 +2162,7 @@ subscriptions: {}, displayMobile: (window.innerWidth <= MEDIA_BREAKPOINT), showInfoPanel: false, + infoPanelShowMembers: false, mobilePanel: 'sidepanel', contextMenuVisible: false, contextMenuBounds: null, @@ -2261,6 +2262,7 @@ loginDisabled: false, subscriptions: {}, showInfoPanel: false, + infoPanelShowMembers: false, mobilePanel: 'sidepanel', contextMenuVisible: false, contextMenuBounds: null, @@ -2426,7 +2428,8 @@ topicSelected: topic.name, sidePanelSelected: 'contacts', mobilePanel: 'topic-view', - showInfoPanel: false + showInfoPanel: true, + infoPanelShowMembers: true }); }).catch(function(err) { instance.setState({errorText: err.message}); @@ -2462,7 +2465,11 @@ var instance = this; topic.leave(true).then(function(ctrl) { // Hide MessageView and InfoView panels. - instance.setState({topicSelected: '', showInfoPanel: false}); + instance.setState({ + topicSelected: '', + showInfoPanel: false, + infoPanelShowMembers: false + }); }); }, @@ -2546,10 +2553,10 @@ } }, handleShowInfoView: function() { - this.setState({showInfoPanel: true}); + this.setState({showInfoPanel: true, infoPanelShowMembers: false}); }, handleHideInfoView: function() { - this.setState({showInfoPanel: false}); + this.setState({showInfoPanel: false, infoPanelShowMembers: false}); }, handleMemberUpdateRequest: function(topicName, added, removed) { if (!topicName) { @@ -2642,6 +2649,7 @@ topic={this.state.topicSelected} foundContacts={this.state.foundContacts} myUserId={this.state.myUserId} + showMemberPanel={this.state.infoPanelShowMembers} onSubmit={this.handleTopicUpdateRequest} onCancel={this.handleHideInfoView} onChangePermissions={this.handleChangePermissions}