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