lets of bug fixes

This commit is contained in:
or-else
2017-07-02 16:16:45 +03:00
parent 45eb91765b
commit e79444e5f7
3 changed files with 239 additions and 155 deletions
+3 -3
View File
@@ -16,14 +16,14 @@ Background pattern from http://subtlepatterns.com/, commercial and non-commercia
## Missing functionality ## Missing functionality
* Cannot manage group members. * Cannot manage members of group chats.
* Editing chat parameters is incomplete and buggy.
* Cannot send images or other media. * Cannot send images or other media.
* No typing indicators. * 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. * 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.
## Responsive design ## Responsive design
+36 -8
View File
@@ -37,6 +37,29 @@ a:focus {
outline-offset: -2px; 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 { form {
font-size: 1.1rem; font-size: 1.1rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
@@ -226,6 +249,16 @@ ul.menu > li.separator:hover {
} }
/* END popup/dropdown menu */ /* 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 */ /* END of basic elements */
#mountPoint { #mountPoint {
@@ -447,9 +480,9 @@ ul.menu > li.separator:hover {
overflow: hidden; overflow: hidden;
} }
.contacts-panel { .scrollable-panel {
overflow-y: auto; overflow-y: auto;
margin-top: 1rem;: margin-top: 0.25rem;
margin-bottom: auto; margin-bottom: auto;
height: 100%; height: 100%;
} }
@@ -1035,11 +1068,6 @@ ul.tabbar li.active > a:after {
vertical-align: middle; vertical-align: middle;
} }
/* Color of buttons */
.blue {
color: #2196f3; /* blue 500 */
}
/* Letter tile base style, responsive square box */ /* Letter tile base style, responsive square box */
.lettertile { .lettertile {
width: 100%; width: 100%;
@@ -1178,7 +1206,7 @@ ul.tabbar li.active > a:after {
} }
#self-avatar.material-icons, .avatar-box .material-icons { #self-avatar.material-icons, .avatar-box .material-icons {
font-size: 3rem; font-size: 2.4rem;
} }
.avatar-box .material-icons { .avatar-box .material-icons {
color: rgba(250, 250, 250, 0.8); /* #fafafa, gray 50 */ color: rgba(250, 250, 250, 0.8); /* #fafafa, gray 50 */
+200 -144
View File
@@ -262,7 +262,8 @@
handleEditingFinished: function() { handleEditingFinished: function() {
this.setState({active: false}); this.setState({active: false});
var text = this.state.text.trim(); 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); this.props.onFinished(text);
} }
}, },
@@ -452,7 +453,7 @@
submitClasses += " disabled"; submitClasses += " disabled";
} }
return ( return (
<form className="panel-form" onSubmit={this.handleSubmit}> <div className="panel-form">
<div className="panel-form-row"> <div className="panel-form-row">
<div className="panel-form-column"> <div className="panel-form-column">
<input type="text" placeholder="Login" <input type="text" placeholder="Login"
@@ -464,12 +465,14 @@
</div> </div>
<AvatarUpload onImageChanged={this.handleImageChanged} /> <AvatarUpload onImageChanged={this.handleImageChanged} />
</div> </div>
<input type="text" placeholder="Full name, e.g. John Doe" <div className="panel-form-row">
value={this.state.fn} onChange={this.handleFnChange} /> <input type="text" placeholder="Full name, e.g. John Doe"
<div className="dialog-buttons"> value={this.state.fn} onChange={this.handleFnChange} />
<button className={submitClasses} type="submit">Sign up</button>
</div> </div>
</form> <div className="dialog-buttons">
<button className={submitClasses} onClick={this.handleSubmit}>Sign up</button>
</div>
</div>
); );
} }
}); });
@@ -841,11 +844,10 @@
} else if (title.length > 30) { } else if (title.length > 30) {
title = title.substring(0, 28) + "..."; title = title.substring(0, 28) + "...";
} }
var selected = (this.props.selected === this.props.topic) ? "selected" : null;
var online = this.props.now ? "online" : "offline"; var online = this.props.now ? "online" : "offline";
var avatar = this.props.avatar ? this.props.avatar : true; var avatar = this.props.avatar ? this.props.avatar : true;
return ( return (
<li className={selected} onClick={this.handleClick}> <li className={this.props.selected ? "selected" : null} onClick={this.handleClick}>
<div className="avatar-box"> <div className="avatar-box">
<LetterTile <LetterTile
avatar={avatar} title={this.props.title} topic={this.props.topic} /> avatar={avatar} title={this.props.title} topic={this.props.topic} />
@@ -983,19 +985,37 @@
var ContactList = React.createClass({ var ContactList = React.createClass({
render: function() { render: function() {
var contactNodes = []; var contactNodes = [];
var instance = this;
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) { if (!key) {
console.log("empty 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.push(
<Contact <Contact
title={c.public ? c.public.fn : null} title={c.public ? c.public.fn : null}
avatar={makeImageUrl(c.public ? c.public.photo : null)} avatar={makeImageUrl(c.public ? c.public.photo : null)}
comment={c.private} comment={c.private}
unread={c.unread} now={c.online} unread={c.unread} now={c.online}
selected={this.props.topicSelected} selected={this.props.topicSelected === key}
showOnline={this.props.showOnline} showOnline={this.props.showOnline}
onSelected={this.props.onTopicSelected} onSelected={this.props.onTopicSelected}
showContextMenu={this.props.showContextMenu} showContextMenu={this.props.showContextMenu}
@@ -1006,7 +1026,7 @@
} }
return ( return (
<div className="contacts-panel"> <div className={this.props.noScroll ? null : "scrollable-panel"}>
{contactNodes.length > 0 ? {contactNodes.length > 0 ?
<ul className="contact-box"> <ul className="contact-box">
{contactNodes} {contactNodes}
@@ -1027,6 +1047,7 @@
return { return {
tabSelected: "p2p", tabSelected: "p2p",
searchQuery: '', searchQuery: '',
contactList: [],
contactSelected: null contactSelected: null
}; };
}, },
@@ -1036,6 +1057,7 @@
fnd.onMetaDesc = this.fndMetaDesc; fnd.onMetaDesc = this.fndMetaDesc;
if (fnd.isSubscribed()) { if (fnd.isSubscribed()) {
this.setState({searchQuery: fnd.private ? fnd.private.toString() : ''}); this.setState({searchQuery: fnd.private ? fnd.private.toString() : ''});
this.fndSubsUpdated();
} else { } else {
fnd.subscribe({get: {sub: {}, desc: {}}}); fnd.subscribe({get: {sub: {}, desc: {}}});
} }
@@ -1051,8 +1073,12 @@
} }
}, },
fndSubsUpdated: function() { fndSubsUpdated: function() {
//console.log(keys); var contacts = [];
this.forceUpdate(); // 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) { handleTabClick: function(e) {
e.preventDefault(); e.preventDefault();
@@ -1086,7 +1112,13 @@
<NewTopicView.SearchContacts type="p2p" <NewTopicView.SearchContacts type="p2p"
searchQuery={this.state.searchQuery} searchQuery={this.state.searchQuery}
onSearchContacts={this.props.onSearchContacts} /> onSearchContacts={this.props.onSearchContacts} />
<NewTopicView.ContactsList onContactSelected={this.handleContactSelected} type="p2p" /> <ContactList
contacts={this.state.contactList}
emptyListMessage="Use search to find contacts"
topicSelected={this.state.selectedContact}
showOnline={false}
showContextMenu={false}
onTopicSelected={this.handleContactSelected} />
</div>} </div>}
</div> </div>
); );
@@ -1097,19 +1129,24 @@
getInitialState: function() { getInitialState: function() {
return { return {
fn: '', // full/formatted name fn: '', // full/formatted name
private: '',
imageDataUrl: null imageDataUrl: null
}; };
}, },
handleFnChange: function(e) { handleFnChange: function(e) {
this.setState({fn: e.target.value}); this.setState({fn: e.target.value});
}, },
handlePrivateChange: function(e) {
this.setState({private: e.target.value});
},
handleImageChanged: function(img) { handleImageChanged: function(img) {
this.setState({imageDataUrl: img}); this.setState({imageDataUrl: img});
}, },
handleSubmit: function(e) { handleSubmit: function(e) {
e.preventDefault(); e.preventDefault();
if (this.state.fn && this.state.fn.trim()) { 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() { render: function() {
@@ -1118,18 +1155,22 @@
submitClasses += " disabled"; submitClasses += " disabled";
} }
return ( return (
<form className="panel-form" onSubmit={this.handleSubmit}> <div className="panel-form">
<div className="panel-form-row"> <div className="panel-form-row">
<div className="panel-form-column"> <div className="panel-form-column">
<input type="text" placeholder="Name of the topic to create" <label className="small" htmlFor="new-topic-fn">Group name</label>
<input type="text" id="new-topic-fn" placeholder="Freeform name of the group"
value={this.state.fn} onChange={this.handleFnChange} autoFocus required /> value={this.state.fn} onChange={this.handleFnChange} autoFocus required />
<div className="dialog-buttons"> <label className="small" htmlFor="new-topic-priv">Private comment</label>
<button className={submitClasses} type="submit">Create</button> <input type="text" id="new-topic-priv" placeholder="Visible to you only"
</div> value={this.state.private} onChange={this.handlePrivateChange} />
</div> </div>
<AvatarUpload onImageChanged={this.handleImageChanged} /> <AvatarUpload onImageChanged={this.handleImageChanged} />
</div> </div>
</form> <div className="dialog-buttons">
<button className={submitClasses} onClick={this.handleSubmit}>Create</button>
</div>
</div>
); );
} }
}); });
@@ -1161,55 +1202,15 @@
}, },
render: function() { render: function() {
return ( return (
<form className="panel-form" onSubmit={this.handleFindContacts}> <div className="panel-form">
<div className="panel-form-row"> <div className="panel-form-row">
<input type="text" placeholder="Comma separated list: email:alice@example.com, tel:17025550003..." <input type="text" placeholder="List like email:alice@example.com, tel:17025550003..."
value={this.state.search} onChange={this.handleSearchChange} onKeyPress={this.handleKeyPress} required /> value={this.state.search} onChange={this.handleSearchChange}
onKeyPress={this.handleKeyPress} required />
<a href="#" onClick={this.handleSearch}> <a href="#" onClick={this.handleSearch}>
<i className="material-icons">search</i> <i className="material-icons">search</i>
</a> </a>
</div> </div>
</form>
);
}
});
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(
<Contact
title={((s.public && s.public.fn) || s.private)}
avatar={makeImageUrl(s.public ? s.public.photo : null)}
selected={this.state.selectedUser}
onSelected={this.handleSelectionChange}
topic={key}
key={key} />
);
}, this);
return (
<div className="contacts-panel">
<ul className="contact-box">
{contactNodes}
</ul>
</div> </div>
); );
} }
@@ -1228,6 +1229,9 @@
private: null, private: null,
selectedContact: null, selectedContact: null,
muted: undefined, muted: undefined,
access: null,
auth: null,
anon: null,
contactList: [] contactList: []
}; };
}, },
@@ -1267,14 +1271,17 @@
var acs = topic.getAccessMode(); var acs = topic.getAccessMode();
this.setState({ this.setState({
owner: acs && acs.isOwner(), owner: acs && acs.isOwner(),
sharer: acs && (acs.isAdmin() || acs.isSharer()),
muted: acs && acs.isMuted(),
fullName: topic.public ? topic.public.fn : undefined, fullName: topic.public ? topic.public.fn : undefined,
avatar: makeImageUrl(topic.public ? topic.public.photo : null), avatar: makeImageUrl(topic.public ? topic.public.photo : null),
private: topic.private, private: topic.private,
address: topic.name, address: topic.name,
groupTopic: (topic.getType() === "grp"), groupTopic: (topic.getType() === "grp"),
access: acs.getMode(),
auth: defacs.auth, auth: defacs.auth,
anon: defacs.anon, anon: defacs.anon
muted: acs && acs.isMuted()
}); });
}, },
@@ -1295,24 +1302,28 @@
}, },
handleFullNameUpdate: function(fn) { handleFullNameUpdate: function(fn) {
this.setState({fullName: fn}); if (this.state.fullName !== fn) {
this.props.onSubmit(this.props.topic, this.setState({fullName: fn});
vcard(fn, this.state.avatar), this.props.onSubmit(this.props.topic,
null); vcard(fn, this.state.avatar),
null);
}
}, },
handlePrivateUpdate: function(priv) { handlePrivateUpdate: function(priv) {
this.setState({private: priv}); if (this.state.priv !== priv) {
this.props.onSubmit(this.props.topic, this.setState({private: priv});
null, this.props.onSubmit(this.props.topic,
priv); null,
priv);
}
}, },
handleImageChanged: function(img) { handleImageChanged: function(img) {
this.setState({avatar: img}); this.setState({avatar: img});
this.props.onSubmit(this.props.topic, this.props.onSubmit(this.props.topic,
vcard(this.state.fullName, img), vcard(this.state.fullName, img),
priv); null);
}, },
handleMuted: function(e) { handleMuted: function(e) {
@@ -1321,6 +1332,14 @@
this.props.onChangePermissions(this.props.topic, checked ? "-P" : "+P"); 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) { handleMemberSelected: function(uid) {
this.setState({selectedContact: uid}); this.setState({selectedContact: uid});
}, },
@@ -1329,15 +1348,6 @@
console.log("context menu: " + item); 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() { render: function() {
return ( return (
<div id="info-view"> <div id="info-view">
@@ -1347,63 +1357,89 @@
<MenuCancel onCancel={this.props.onCancel} /> <MenuCancel onCancel={this.props.onCancel} />
</div> </div>
</div> </div>
<div className="panel-form-row"> <div id="info-view-content" className="scrollable-panel">
<div className="panel-form-row">
<div className="panel-form-column">
<div><label className="small">Name</label></div>
<div><InPlaceEdit
placeholder="Group name"
readOnly={!this.state.owner}
text={this.state.fullName}
onFinished={this.handleFullNameUpdate} /></div>
<div><label className="small">Private comment</label></div>
<div><InPlaceEdit
placeholder="Visible to you only"
text={this.state.private}
onFinished={this.handlePrivateUpdate} /></div>
</div>
<AvatarUpload
avatar={this.state.avatar}
readOnly={!this.state.owner}
uid={this.props.topic}
title={this.state.fullName}
onImageChanged={this.handleImageChanged} />
</div>
<div className="panel-form-column"> <div className="panel-form-column">
<div><label className="small">Name</label></div> <div className="panel-form-row">
<div><InPlaceEdit <label>Address:</label>
placeholder="Group name" <tt>{this.state.address}</tt>
readOnly={!this.state.owner} </div>
text={this.state.fullName} <div className="panel-form-row">
onFinished={this.handleFullNameUpdate} /></div> <label>Permissions:</label>
<div><label className="small">Private comment</label></div> <tt>{this.state.access}</tt>
<div><InPlaceEdit </div>
placeholder="Visible to you only" <div className="panel-form-row">
text={this.state.private} <label htmlFor="info-muted">Muted:</label>
onFinished={this.handlePrivateUpdate} /></div> <input id="info-muted" type="checkbox" checked={this.state.muted}
</div> onChange={this.handleMuted} />
<AvatarUpload </div>
avatar={this.state.avatar} {this.state.auth || this.state.anon ?
readOnly={!this.state.owner} <div>
uid={this.props.topic} <div><label className="small">Default access mode:</label></div>
title={this.state.fullName} <div className="quoted">
onImageChanged={this.handleImageChanged} /> <div>Auth: <tt>{this.state.auth}</tt></div>
</div> <div>Anon: <tt>{this.state.anon}</tt></div>
<div className="panel-form-column"> </div>
<div className="panel-form-row">
<label>Address:</label>
<tt>{this.state.address}</tt>
</div>
<div className="panel-form-row">
<label htmlFor="info-muted">Muted:</label>
<input id="info-muted" type="checkbox" checked={this.state.muted}
onChange={this.handleMuted} />
</div>
{this.state.auth || this.state.anon ?
<div>
<div><label className="small">Default access mode:</label></div>
<div className="quoted">
<div>Auth: <tt>{this.state.auth}</tt></div>
<div>Anon: <tt>{this.state.anon}</tt></div>
</div> </div>
:
null
}
</div>
{this.state.groupTopic ?
<div className="panel-form-column">
<div className="panel-form-row">
<label className="small">Group members:</label>
</div>
<div className="panel-form-row">
{this.state.sharer ?
<a href="#" className="flat-button" onClick={this.handleAddMembers}>
<i className="material-icons">person_add</i> Add members
</a>
: null}
{!this.state.owner ?
<a href="#" className="red flat-button" onClick={this.handleLeave}>
<i className="material-icons">exit_to_app</i> Leave
</a>
: null}
</div>
<ContactList
contacts={this.state.contactList}
emptyListMessage="No members"
topicSelected={this.state.selectedContact}
showOnline={false}
noScroll={true}
onTopicSelected={this.handleMemberSelected}
showContextMenu={this.handleContextMenu}
/>
</div> </div>
: :
null <div className="panel-form-row">
<a href="#" className="red flat-button" onClick={this.handleLeave}>
<i className="material-icons">exit_to_app</i> Leave
</a>
</div>
} }
</div> </div>
{this.state.groupTopic ?
<div className="panel-form-row">
<label className="small">Group members:</label>
</div> : null}
{this.state.groupTopic ?
<ContactList
contacts={this.state.contactList}
emptyListMessage="No members"
topicSelected={this.state.selectedContact}
showOnline={false}
onTopicSelected={this.handleMemberSelected}
showContextMenu={this.handleContextMenu}
/>
: null}
</div> </div>
); );
} }
@@ -2027,7 +2063,8 @@
errorText: '', errorText: '',
topicSelected: peerName, topicSelected: peerName,
sidePanelSelected: 'contacts', sidePanelSelected: 'contacts',
mobilePanel: 'topic-view' mobilePanel: 'topic-view',
showInfoPanel: false
}); });
} else { } else {
var topic = peerName ? Tinode.newTopicWith(peerName) : Tinode.newTopic(); var topic = peerName ? Tinode.newTopicWith(peerName) : Tinode.newTopic();
@@ -2041,7 +2078,8 @@
errorText: '', errorText: '',
topicSelected: topic.name, topicSelected: topic.name,
sidePanelSelected: 'contacts', sidePanelSelected: 'contacts',
mobilePanel: 'topic-view' mobilePanel: 'topic-view',
showInfoPanel: false
}); });
}).catch(function(err) { }).catch(function(err) {
instance.setState({errorText: err.message}); instance.setState({errorText: err.message});
@@ -2049,20 +2087,36 @@
} }
}, },
handleManageGroupMembers: function(topicName) {
// FIXME(gene): launch group member manager
},
handleTopicUpdateRequest: function(topicName, pub, priv) { handleTopicUpdateRequest: function(topicName, pub, priv) {
var topic = Tinode.getTopic(topicName); 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) { 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); var topic = Tinode.getTopic(topicName);
if (!topic) { if (!topic) {
return; return;
} }
var instance = this;
var am = topic.getAccessMode(); topic.leave(true).then(function(ctrl) {
am.updateWant(want); // Hide MessageView and InfoView panels.
topic.setMeta({sub: {mode: am.getWant()}}); instance.setState({topicSelected: '', showInfoPanel: false});
});
}, },
/** Called when the user enters a contact into the contact search field in the NewAccount panel /** Called when the user enters a contact into the contact search field in the NewAccount panel
@@ -2070,7 +2124,7 @@
*/ */
handleSearchContacts: function(query) { handleSearchContacts: function(query) {
var find = Tinode.getFndTopic(); var find = Tinode.getFndTopic();
find.setMeta({desc: {private: query}}).then(function() { find.setMeta({desc: {private: query}}).then(function(ctrl) {
find.getMeta({sub: {}, desc: {}}); find.getMeta({sub: {}, desc: {}});
}); });
}, },
@@ -2224,6 +2278,8 @@
onSubmit={this.handleTopicUpdateRequest} onSubmit={this.handleTopicUpdateRequest}
onCancel={this.handleHideInfoView} onCancel={this.handleHideInfoView}
onChangePermissions={this.handleChangePermissions} onChangePermissions={this.handleChangePermissions}
onLeaveTopic={this.handleLeaveUnsubRequest}
onAddMember={this.handleManageGroupMembers}
/> />
: :
null null