refactored contect menu and updated readme

This commit is contained in:
or-else
2017-06-22 21:04:12 +03:00
parent 67cd659712
commit 61fbc69356
7 changed files with 361 additions and 207 deletions
+11 -3
View File
@@ -6,8 +6,7 @@ and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-we
This is work in progress. Bugs are expected. The app was tested in the latest Chrome & Firefox only. This is work in progress. Bugs are expected. The app was tested in the latest Chrome & Firefox only.
Try it live at http://api.tinode.co/x/example-react-js/. Login as one of `alice`, `bob`, `carol`, `dave`, `frank`. Try it (possibly older version) live at http://api.tinode.co/x/example-react-js/. Login as one of `alice`, `bob`, `carol`, `dave`, `frank`. Password is `<login>123`, e.g. password for `alice` is `alice123`.
Password is `<login>123`, e.g. password for `alice` is `alice123`.
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0) License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
@@ -15,4 +14,13 @@ Photos from https://www.pexels.com/ under [CC0 license](https://www.pexels.com/p
Background pattern from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution. Background pattern from http://subtlepatterns.com/, commercial and non-commercial use allowed with attribution.
![App screenshot](web-topic.png) ## Responsive design
### Desktop
<img src="web-desktop.png" alt="Desktop web: full app" width=540 />
### Mobile
<img src="web-mobile-contacts.png" alt="Mobile web: contacts" width=401 />
<img src="web-mobile-chat.png" alt="Mobile web: chat" width=485 />
+146 -53
View File
@@ -418,6 +418,7 @@ ul.menu > li.separator:hover {
position: relative; position: relative;
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
font-size: 1.8rem;
overflow: hidden; overflow: hidden;
padding: 0; padding: 0;
} }
@@ -566,9 +567,15 @@ ul.menu > li.separator:hover {
text-decoration: none; text-decoration: none;
} }
#topic-caption-panel .avatar-box {
width: 3rem;
height: 3rem;
margin-right: 0.35rem;
}
#hide-message-view { #hide-message-view {
display: block; display: block;
margin-right: 0.5rem; margin-right: 0.35rem;
} }
#topic-users { #topic-users {
@@ -619,6 +626,11 @@ ul.menu > li.separator:hover {
margin: 0 0.5rem; margin: 0 0.5rem;
} }
/* Group chats have avatars on the left. Make left margin smaller */
.chat-box.group {
margin-left: -0.4rem;
}
.chat-box li { .chat-box li {
display: flex; display: flex;
margin: 0; margin: 0;
@@ -626,7 +638,7 @@ ul.menu > li.separator:hover {
align-items: flex-end; align-items: flex-end;
} }
.chat-box li.right { .chat-box .right {
justify-content: flex-end; justify-content: flex-end;
} }
@@ -650,12 +662,12 @@ ul.menu > li.separator:hover {
} }
.chat-box li.single { .chat-box li.single {
margin-top: 0.75rem; margin-top: 0.25rem;
margin-bottom: 0.75rem; margin-bottom: 0.25rem;
} }
.chat-box li.first { .chat-box li.first {
margin-top: 0.75rem; margin-top: 0.25rem;
margin-bottom: 0.15rem; margin-bottom: 0.15rem;
} }
@@ -666,11 +678,19 @@ ul.menu > li.separator:hover {
.chat-box li.last { .chat-box li.last {
margin-top: 0.15rem; margin-top: 0.15rem;
margin-bottom: 0.75rem; margin-bottom: 0.25rem;
}
.chat-box .left .bubble {
background-color: #c5e1a5;
}
.chat-box .right .bubble {
background-color: #fafafa;
} }
/* Left triangle placed bottom left flush. */ /* Left triangle placed bottom left flush. */
.chat-box li.left .tip::before { .chat-box .left .tip::before {
content: ''; content: '';
position: absolute; position: absolute;
width: 0; width: 0;
@@ -680,6 +700,7 @@ ul.menu > li.separator:hover {
top: auto; top: auto;
bottom: 0; bottom: 0;
border: 10px solid; border: 10px solid;
color: #c5e1a5;
border-top-color: transparent; border-top-color: transparent;
border-right-color: transparent; border-right-color: transparent;
border-bottom-color: inherit; border-bottom-color: inherit;
@@ -687,7 +708,7 @@ ul.menu > li.separator:hover {
} }
/* Right triangle placed bottom right flush. */ /* Right triangle placed bottom right flush. */
.chat-box li.right .tip::before { .chat-box .right .tip::before {
content: ''; content: '';
position: absolute; position: absolute;
display: flex; display: flex;
@@ -699,6 +720,7 @@ ul.menu > li.separator:hover {
top: auto; top: auto;
bottom: 0; bottom: 0;
border: 10px solid; border: 10px solid;
color: #fafafa;
border-top-color: transparent; border-top-color: transparent;
border-right-color: transparent; border-right-color: transparent;
border-bottom-color: inherit; border-bottom-color: inherit;
@@ -706,30 +728,36 @@ ul.menu > li.separator:hover {
} }
.chat-box .avatar-box { .chat-box .avatar-box {
width: 3rem;
text-align: center;
}
.chat-box .avatar-box img {
width: 2.4rem;
display: block;
position: relative; position: relative;
left: 0.3rem; width: 1.4rem;
border-radius: 33%; height: 1.4rem;
font-size: 0.8rem;
margin-bottom: 0.8rem;
margin-right: 0.8rem;
} }
.chat-box .bubble p { .chat-box .bubble p {
display: flex; color: #212121;
justify-content: space-between;
margin: 0; margin: 0;
} }
/* The timestamp is a floating element. Need to ensure p is expanded
* to accomodate it. It's usually called clearfix. */
.chat-box .bubble p:after {
content: ".";
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.chat-box .timestamp { .chat-box .timestamp {
display: inline-block; display: inline-block;
position:relative;
font-size: 80%; font-size: 80%;
color: #777; color: #777;
padding-left: 1.5em; padding-left: 1rem;
padding-top: 0.5em; top: 0.5em;
float: right;
white-space: nowrap; white-space: nowrap;
} }
@@ -745,6 +773,14 @@ ul.menu > li.separator:hover {
margin-top: 0.75rem; margin-top: 0.75rem;
} }
/* The name of the person who sent the message
* (group chat only)
*/
.chat-box .author {
font-size: 80%;
color:#777;
}
/* END of talk bubbles */ /* END of talk bubbles */
/* END of topic/mesages view */ /* END of topic/mesages view */
@@ -881,69 +917,126 @@ ul.tabbar li.active > a:after {
/* Letter tile base style */ /* Letter tile base style */
.lettertile { .lettertile {
display: flex; width: 100%;
color: rgba(250, 250, 250, 0.8); /* #FAFAFA, gray 50 */ padding-bottom: 100%;
font-size: 1.8rem;
line-height: 1;
width: 3rem;
height: 3rem;
border-radius: 50%; border-radius: 50%;
position: relative;
}
/* Responsive square box for text tiles */
.lettertile > div {
display: flex;
position: absolute;
top: 0; bottom: 0; left: 0; right: 0;
text-transform: uppercase; text-transform: uppercase;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center; text-align: center;
border-radius: 50%;
line-height: 1;
color: rgba(250, 250, 250, 0.8); /* default text color #FAFAFA, gray 50 */
} }
/* Colors of letter tiles */ /* Colors of letter tiles */
.lettertile.color0 { .lettertile.color0 {
background-color: #c5e1a5;
}
.lettertile.color1 {
background-color: #ef9a9a; background-color: #ef9a9a;
} }
.lettertile.color2 { .lettertile.color1 {
background-color: #90caf9; background-color: #90caf9;
} }
.lettertile.color3 { .lettertile.color2 {
background-color: #fff59d;
}
.lettertile.color4 {
background-color: #b0bec5; background-color: #b0bec5;
} }
.lettertile.color5 { .lettertile.color3 {
background-color: #f48fb1;
}
.lettertile.color6 {
background-color: #b39ddb; background-color: #b39ddb;
} }
.lettertile.color7 { .lettertile.color4 {
background-color: #9fa8da;
}
.lettertile.color8 {
background-color: #ffab91; background-color: #ffab91;
} }
.lettertile.color9 { .lettertile.color5 {
background-color: #ffe082;
}
.lettertile.color10 {
background-color: #a5d6a7; background-color: #a5d6a7;
} }
.lettertile.color11 { .lettertile.color6 {
background-color: #bcaaa4;
}
.lettertile.color12 {
background-color: #eeeeee; background-color: #eeeeee;
} }
.lettertile.color7 {
background-color: #e6ee9c;
}
.lettertile.color8 {
background-color: #c5e1a5;
}
.lettertile.color9 {
background-color: #fff59d;
}
.lettertile.color10 {
background-color: #f48fb1;
}
.lettertile.color11 {
background-color: #9fa8da;
}
.lettertile.color12 {
background-color: #ffe082;
}
.lettertile.color13 { .lettertile.color13 {
background-color: #80deea; background-color: #bcaaa4;
} }
.lettertile.color14 { .lettertile.color14 {
background-color: #e6ee9c; background-color: #80deea;
} }
.lettertile.color15 { .lettertile.color15 {
background-color: #ce93d8; background-color: #ce93d8;
} }
/* Darker version of letter tile colors */
.lettertile.dark-color0 {
background-color: #C62828;
}
.lettertile.dark-color1 {
background-color: #AD1457;
}
.lettertile.dark-color2 {
background-color: #6A1B9A;
}
.lettertile.dark-color3 {
background-color: #4527A0;
}
.lettertile.dark-color4 {
background-color: #283593;
}
.lettertile.dark-color5 {
background-color: #1565C0;
}
.lettertile.dark-color6 {
background-color: #0277BD;
}
.lettertile.dark-color7 {
background-color: #00838F;
}
.lettertile.dark-color8 {
background-color: #00695C;
}
.lettertile.dark-color9 {
background-color: #2E7D32;
}
.lettertile.dark-color10 {
background-color: #558B2F;
}
.lettertile.dark-color11 {
background-color: #9E9D24;
}
.lettertile.dark-color12 {
background-color: #F9A825;
}
.lettertile.dark-color13 {
background-color: #FF8F00;
}
.lettertile.dark-color14 {
background-color: #EF6C00;
}
.lettertile.dark-color15 {
background-color: #D84315;
}
/* Material icons resized from the default 24pt */ /* Material icons resized from the default 24pt */
.material-icons { .material-icons {
font-size: 1.4rem; font-size: 1.4rem;
+204 -151
View File
@@ -51,16 +51,17 @@
// Short representation of time in the past. // Short representation of time in the past.
function shortDateFormat(then) { function shortDateFormat(then) {
var locale = window.navigator.userLanguage || window.navigator.language;
var now = new Date(); var now = new Date();
if (then.getFullYear() == now.getFullYear()) { if (then.getFullYear() == now.getFullYear()) {
if (then.getMonth() == now.getMonth() && then.getDate() == now.getDate()) { if (then.getMonth() == now.getMonth() && then.getDate() == now.getDate()) {
return then.toLocaleTimeString(undefined, {hour12: false, hour: '2-digit', minute: '2-digit'}); return then.toLocaleTimeString(locale, {hour12: false, hour: '2-digit', minute: '2-digit'});
} else { } else {
return then.toLocaleDateString(undefined, return then.toLocaleDateString(locale,
{hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'}); {hour12: false, month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
} }
} }
return then.toLocaleDateString(undefined, return then.toLocaleDateString(locale,
{hour12: false, year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'}); {hour12: false, year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'});
} }
@@ -127,103 +128,59 @@
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.props.hide(); this.props.hide();
this.props.onResult(this.props.items[e.currentTarget.dataset.id]); this.props.onAction(this.props.topic, this.props.items[e.currentTarget.dataset.id]);
}, },
render: function() { render: function() {
if (!this.props.visible) { if (!this.props.visible) {
return null; return null;
} }
// Ensure that menu is inside the app-container.
var hSize = 12 * REM_SIZE;
var vSize = REM_SIZE * (0.7 + this.props.items.length * 2.5);
var left = (this.props.bounds.right - this.props.clickAt.x < hSize) ?
(this.props.clickAt.x - this.props.bounds.left - hSize) :
(this.props.clickAt.x - this.props.bounds.left);
var top = (this.props.bounds.bottom - this.props.clickAt.y < vSize) ?
(this.props.clickAt.y - this.props.bounds.top - vSize) :
(this.props.clickAt.y - this.props.bounds.top);
var position = { var position = {
left: this.props.position.left + 'px', left: left + "px",
top: this.props.position.top + 'px' top: top + "px"
}; };
var count = 0; var count = 0;
var menuItems = [];
var instance = this; var instance = this;
var menu = [];
this.props.items.map(function(item) { this.props.items.map(function(item) {
if (item === "-") { menu.push(
menuItems.push( item === "-" ?
<li className="separator" key={count} /> <li className="separator" key={count} />
); :
} else {
menuItems.push(
<li onClick={instance.handleClick} data-id={count} key={count}>{item}</li> <li onClick={instance.handleClick} data-id={count} key={count}>{item}</li>
); );
}
count++; count++;
}); });
return ( return (
<ul className="menu" style={position}> <ul className="menu" style={position}>
{menuItems} {menu}
</ul> </ul>
); );
} }
}); });
var ContextMenuMixin = {
handleContextClick: function(e) {
e.preventDefault();
e.stopPropagation();
this.props.showContextMenu(e.pageY, e.pageX, this.getMenuItems(), this.handleMenuAction);
},
handleMenuAction: function(action) {
var topic = Tinode.getTopic(this.props.topic);
var metaParam = function(p) {
var am = topic.getAccessMode();
am.updateWant(p);
topic.setMeta({sub: {mode: am.getWant()}});
}
switch (action) {
case "Edit...":
// TODO(gene): implement topic editing
console.log("Topic editing not implemented");
break;
case "Clear messages":
topic.delMessagesAll(false).then(function(){
console.log("refresh message list");
});
break;
case "Mute":
metaParam('-P');
break;
case "Unmute":
metaParam('+P');
break;
case "Block":
metaParam('-J');
break;
case "Unblock":
metaParam('+J');
break;
case "Delete":
topic.delTopic();
break;
default: break;
}
},
getMenuItems: function() {
var acs = Tinode.getMeTopic().getAccessMode(this.props.topic);
if (Tinode.getTopicType(this.props.topic) === "grp") {
return ["Edit...", "Clear messages", acs.isMuted() ? "Unmute" : "Mute",
!acs.isJoiner() ? "Unblock" : "Block", "Delete"];
} else {
return ["Clear messages", acs.isMuted() ? "Unmute" : "Mute",
!acs.isJoiner() ? "Unblock" : "Block", "Delete"];
}
}
};
/* END Popup/dropdown menu */ /* END Popup/dropdown menu */
/* BEGIN Lettertile: Avatar box: either a bitmap or a letter tile or a stock icon. */ /* BEGIN Lettertile: Avatar box: either a bitmap or a letter tile or a stock icon. */
var LetterTile = React.createClass({ class LetterTile extends React.PureComponent {
render: function() { render() {
var avatar = null; var avatar;
if (this.props.avatar === true) { if (this.props.avatar === true) {
if (this.props.topic && this.props.title && this.props.title.trim()) { if (this.props.topic && this.props.title && this.props.title.trim()) {
var letter = this.props.title.trim().charAt(0); var letter = this.props.title.trim().charAt(0);
var color = "lettertile color" + (Math.abs(stringHash(this.props.topic)) % 16); var color = "lettertile color" + (Math.abs(stringHash(this.props.topic)) % 16);
avatar = <div className={color}>{letter}</div> avatar = (<div className={color}><div>{letter}</div></div>)
} else { } else {
avatar = (Tinode.getTopicType(this.props.topic) === "grp") ? avatar = (Tinode.getTopicType(this.props.topic) === "grp") ?
<i className="material-icons">group</i> : <i className="material-icons">group</i> :
@@ -231,30 +188,38 @@
} }
} else if (this.props.avatar) { } else if (this.props.avatar) {
avatar = <img className="avatar" alt="avatar" src={this.props.avatar} />; avatar = <img className="avatar" alt="avatar" src={this.props.avatar} />;
} else {
avatar = null;
} }
return (<div>{avatar}</div>); return avatar;
} }
}); }
/* END Lettertile */ /* END Lettertile */
/* BEGIN Combobox for selecting host name */ /* BEGIN Combobox for selecting host name */
var HostSelector = React.createClass({ class HostSelector extends React.PureComponent {
getInitialState: function() { constructor(props) {
return { super(props);
this.state = {
hostName: this.props.serverAddress, hostName: this.props.serverAddress,
changed: false changed: false
}; };
}, this.handleHostNameChange = this.handleHostNameChange.bind(this);
handleHostNameChange: function(e) { this.handleEditingFinished = this.handleEditingFinished.bind(this);
}
handleHostNameChange(e) {
this.setState({hostName: e.target.value, changed: true}); this.setState({hostName: e.target.value, changed: true});
}, }
handleEditingFinished: function() {
handleEditingFinished() {
if (this.state.changed) { if (this.state.changed) {
this.setState({changed: false}); this.setState({changed: false});
this.props.onServerAddressChange(this.state.hostName.trim()); this.props.onServerAddressChange(this.state.hostName.trim());
} }
}, }
render: function() {
render() {
var hostOptions = []; var hostOptions = [];
for (var key in KNOWN_HOSTS) { for (var key in KNOWN_HOSTS) {
var item = KNOWN_HOSTS[key]; var item = KNOWN_HOSTS[key];
@@ -273,7 +238,7 @@
</datalist> </datalist>
</div>); </div>);
} }
}); }
/* END Combobox for selecting host name */ /* END Combobox for selecting host name */
/* BEGIN Login: a login form */ /* BEGIN Login: a login form */
@@ -701,12 +666,18 @@
/* BEGIN Contact list (list of topics) */ /* BEGIN Contact list (list of topics) */
/* A single topic */; /* A single topic */;
var Contact = React.createClass({ var Contact = React.createClass({
mixins: [ContextMenuMixin],
handleClick: function(e) { handleClick: function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.props.onSelected(this.props.topic); this.props.onSelected(this.props.topic);
}, },
handleContextClick: function(e) {
e.preventDefault();
e.stopPropagation();
this.props.showContextMenu(this.props.topic, e.pageY, e.pageX);
},
render: function() { render: function() {
var title = this.props.title; var title = this.props.title;
if (!title) { if (!title) {
@@ -741,9 +712,10 @@
}); });
/* /*
<ContextMenu items={this.props.contactMenuItems} <ContextMenu
items={this.props.MenuItems}
topic={this.props.topic} topic={this.props.topic}
onResult={this.handleMenuAction} onAction={this.handleMenuAction}
visible={this.state.menuShownFor ? true : false} /> visible={this.state.menuShownFor ? true : false} />
*/ */
@@ -755,9 +727,7 @@
var count = this.props.count > 9 ? "9+" : this.props.count; var count = this.props.count > 9 ? "9+" : this.props.count;
showUnreadBadge = <span className="badge">{count}</span>; showUnreadBadge = <span className="badge">{count}</span>;
} }
return ( return showUnreadBadge;
<span>{showUnreadBadge}</span>
);
} }
}); });
@@ -1068,41 +1038,32 @@
READ: 2 READ: 2
}; };
// Material colors, shade #200; 16 colors.
var messageColors = [
{bgColor: "#c5e1a5", color: "#212121"}, {bgColor: "#ef9a9a", color: "#212121"},
{bgColor: "#90caf9", color: "#212121"}, {bgColor: "#fff59d", color: "#212121"},
{bgColor: "#b0bec5", color: "#212121"}, {bgColor: "#f48fb1", color: "#212121"},
{bgColor: "#b39ddb", color: "#212121"}, {bgColor: "#9fa8da", color: "#212121"},
{bgColor: "#ffab91", color: "#212121"}, {bgColor: "#ffe082", color: "#212121"},
{bgColor: "#a5d6a7", color: "#212121"}, {bgColor: "#bcaaa4", color: "#212121"},
{bgColor: "#eeeeee", color: "#212121"}, {bgColor: "#80deea", color: "#212121"},
{bgColor: "#e6ee9c", color: "#212121"}, {bgColor: "#ce93d8", color: "#212121"}
];
// Colors for user's own messages
var selfColor = {bgColor: "#fafafa", color: "#212121"};
var Message = React.createClass({ var Message = React.createClass({
render: function() { render: function() {
var liClass = (this.props.response ? "left" : "right") + " " + this.props.sequence; var sideClass = this.props.sequence + " " + (this.props.response ? "left" : "right");
var colors;
colors = this.props.response ? {
backgroundColor: messageColors[this.props.userIndex].bgColor,
borderColor: messageColors[this.props.userIndex].bgColor,
color: messageColors[this.props.userIndex].color
} : {
backgroundColor: selfColor.bgColor,
borderColor: selfColor.bgColor,
color: selfColor.color
};
var bubbleClass = (this.props.sequence === "single" || this.props.sequence === "last") ? var bubbleClass = (this.props.sequence === "single" || this.props.sequence === "last") ?
"bubble tip" : "bubble"; "bubble tip" : "bubble";
var avatar = this.props.userAvatar || true;
var fullDisplay = (this.props.userFrom && this.props.response &&
(this.props.sequence === "single" || this.props.sequence === "last"));
return ( return (
<li className={liClass}> <li className={sideClass}>
<div className={bubbleClass} style={colors}> {this.props.userFrom && this.props.response ?
<p>{this.props.content} <div className="avatar-box">
<Message.ReceivedMarker response={this.props.response} {fullDisplay ?
timestamp={this.props.timestamp} received={this.props.received} /> <LetterTile topic={this.props.userFrom}
</p> title={this.props.userName} avatar={avatar} /> :
null}
</div> :
null}
<div>
<div className={bubbleClass}>
<p>{this.props.content}
<Message.ReceivedMarker response={this.props.response}
timestamp={this.props.timestamp} received={this.props.received} />
</p>
</div>
{fullDisplay ? <div className="author">{this.props.userName}</div> : null}
</div> </div>
</li> </li>
); );
@@ -1128,7 +1089,7 @@
} }
return ( return (
<span className="pull-right timestamp"> <span className="timestamp">
{timestamp}{'\u00a0'}{marker} {timestamp}{'\u00a0'}{marker}
</span> </span>
); );
@@ -1136,12 +1097,12 @@
}); });
var MessagesView = React.createClass({ var MessagesView = React.createClass({
mixins: [ContextMenuMixin],
getInitialState: function() { getInitialState: function() {
return { return {
messages: [], messages: [],
topic: '', topic: '',
title: '' title: '',
avatar: null
}; };
}, },
// Scroll last message into view on component update e.g. on message received. // Scroll last message into view on component update e.g. on message received.
@@ -1195,7 +1156,10 @@
}, },
handleDescChange: function(desc) { handleDescChange: function(desc) {
if (desc.public) { if (desc.public) {
this.setState({title: desc.public.fn}); this.setState({
title: desc.public.fn,
avatar: makeImageUrl(desc.public.photo)
});
} }
}, },
handleSubsUpdated: function(uids) { handleSubsUpdated: function(uids) {
@@ -1234,11 +1198,18 @@
console.log("other change in topic: " + info.what); console.log("other change in topic: " + info.what);
} }
}, },
handleContextClick: function(e) {
e.preventDefault();
e.stopPropagation();
this.props.showContextMenu(this.state.topic, e.pageY, e.pageX);
},
render: function() { render: function() {
var component = null; var component = null;
if (this.state.topic) { if (this.state.topic) {
var messageNodes = []; var messageNodes = [];
var topic = Tinode.getTopic(this.state.topic); var topic = Tinode.getTopic(this.state.topic);
var groupTopic = topic.getType() === "grp";
var previousFrom = null; var previousFrom = null;
for (var i=0; i<this.state.messages.length; i++) { for (var i=0; i<this.state.messages.length; i++) {
var msg = this.state.messages[i]; var msg = this.state.messages[i];
@@ -1266,9 +1237,22 @@
msgReceived = Received.SERVER; msgReceived = Received.SERVER;
} }
} }
var userName, userAvatar, userFrom, chatBoxClass;
if (groupTopic) {
var user = topic.userDesc(msg.from);
if (user && user.public) {
userName = user.public.fn;
userAvatar = makeImageUrl(user.public.photo);
}
userFrom = msg.from;
chatBoxClass="chat-box group";
} else {
chatBoxClass="chat-box";
}
messageNodes.push( messageNodes.push(
<Message content={msg.content} timestamp={msg.ts} response={isReply} <Message content={msg.content} timestamp={msg.ts} response={isReply}
userIndex={msg.$userIndex} sequence={sequence} received={msgReceived} key={msg.seq} /> userFrom={userFrom} userName={userName} userAvatar={userAvatar}
sequence={sequence} received={msgReceived} key={msg.seq} />
); );
} }
@@ -1282,15 +1266,23 @@
// TODO(gene): also handle user agent in c.seen.ua // TODO(gene): also handle user agent in c.seen.ua
} }
} }
var avatar = this.state.avatar || true;
component = ( component = (
<div id="topic-view" className={this.props.hideSelf ? 'nodisplay' : null}> <div id="topic-view" className={this.props.hideSelf ? 'nodisplay' : null}>
<div id="topic-caption-panel"> <div id="topic-caption-panel">
{this.props.displayMobile ? {this.props.displayMobile ?
<a href="#" id="hide-message-view" onClick={this.props.onHideMessageView}> <a href="#" id="hide-message-view" onClick={this.props.onHideMessageView}>
<i className="material-icons big">arrow_back</i> <i className="material-icons big">arrow_back</i>
</a> : </a>
:
null} null}
{this.props.displayMobile ?
<div className="avatar-box">
<LetterTile avatar={avatar}
topic={this.state.topic} title={this.state.title} />
<span className="online" />
</div>
: null}
<div id="topic-title-group"> <div id="topic-title-group">
<div id="topic-title" className="panel-title">{this.state.title}</div> <div id="topic-title" className="panel-title">{this.state.title}</div>
<div id="topic-last-seen">{lastSeen}</div> <div id="topic-last-seen">{lastSeen}</div>
@@ -1303,7 +1295,7 @@
</div> </div>
</div> </div>
<div id="messages-panel" ref="messagesPanel"> <div id="messages-panel" ref="messagesPanel">
<ul className="chat-box"> <ul className={chatBoxClass}>
{messageNodes} {messageNodes}
</ul> </ul>
</div> </div>
@@ -1447,6 +1439,11 @@
subscriptions: {}, subscriptions: {},
displayMobile: (window.innerWidth <= MEDIA_BREAKPOINT), displayMobile: (window.innerWidth <= MEDIA_BREAKPOINT),
mobilePanel: 'sidepanel', mobilePanel: 'sidepanel',
contextMenuVisible: false,
contextMenuBounds: null,
contextMenuClickAt: null,
contextMenuTopic: '',
contextMenuItems: [],
serverVersion: 'no connection' serverVersion: 'no connection'
}; };
}, },
@@ -1539,9 +1536,10 @@
subscriptions: {}, subscriptions: {},
mobilePanel: 'sidepanel', mobilePanel: 'sidepanel',
contextMenuVisible: false, contextMenuVisible: false,
contextMenuPosition: {top: 0, left: 0}, contextMenuBounds: null,
contextMenuItems: null, contextMenuClickAt: null,
handleContextMenuClick: undefined, contextMenuTopic: '',
contextMenuItems: [],
serverVersion: 'no connection' serverVersion: 'no connection'
}); });
}, },
@@ -1663,31 +1661,84 @@
handleDialogCancel: function() { handleDialogCancel: function() {
this.setState({dialogSelected: null}); this.setState({dialogSelected: null});
}, },
handleShowContextMenu: function(y, x, items, callback) { handleShowContextMenu: function(topicName, y, x) {
var bounds = ReactDOM.findDOMNode(this).getBoundingClientRect(); var muted = false, blocked = false;
// Ensure that menu is inside the app-container. var topic = Tinode.getTopic(topicName);
var hSize = 12 * REM_SIZE; if (topic) {
var vSize = REM_SIZE * (0.7 + items.length * 2.5); var acs = topic.getAccessMode();
var pos = { muted = acs && acs.isMuted();
left: (bounds.right - x < hSize) ? (x - bounds.left - hSize) : (x - bounds.left), blocked = acs && !acs.isJoiner();
top: (bounds.bottom - y < vSize) ? (y - bounds.top - vSize) : (y - bounds.top) }
};
this.setState({ this.setState({
contextMenuVisible: true, contextMenuVisible: true,
contextMenuPosition: pos, contextMenuClickAt: {x: x, y: y},
contextMenuItems: items, contextMenuTopic: topicName,
handleContextMenuClick: callback contextMenuItems: [
"Info",
"Clear messages",
muted ? "Unmute" : "Mute",
blocked ? "Unblock" : "Block",
"Delete"
],
contextMenuBounds: ReactDOM.findDOMNode(this).getBoundingClientRect()
}); });
}, },
handleHideContextMenu: function() { handleHideContextMenu: function() {
this.setState({ this.setState({
contextMenuVisible: false, contextMenuVisible: false,
contextMenuPosition: {top: 0, left: 0}, contextMenuClickAt: null,
contextMenuItems: null, contextMenuTopic: '',
handleContextMenuClick: undefined contextMenuBounds: null
}); });
}, },
// Menu click.
handleContextMenuAction: function(topicName, action) {
if (!topicName || !action) {
return;
}
var topic = Tinode.getTopic(topicName);
if (!topic) {
return;
}
var metaParam = function(want) {
var am = topic.getAccessMode();
am.updateWant(want);
topic.setMeta({sub: {mode: am.getWant()}});
}
switch (action) {
case "Info":
// TODO(gene): implement topic info
console.log("Topic info not implemented");
break;
case "Clear messages":
topic.delMessagesAll(false).then(function(){
console.log("refresh message list");
});
break;
case "Mute":
metaParam('-P');
break;
case "Unmute":
metaParam('+P');
break;
case "Block":
metaParam('-J');
break;
case "Unblock":
metaParam('+J');
break;
case "Delete":
topic.delTopic();
break;
default:
console.log("Unknown menu action '" + action + "'");
break;
}
},
render: function() { render: function() {
return ( return (
<div id="app-container"> <div id="app-container">
@@ -1697,11 +1748,13 @@
onCancel={this.handleDialogCancel} /> onCancel={this.handleDialogCancel} />
<ContextMenu <ContextMenu
position={this.state.contextMenuPosition} bounds={this.state.contextMenuBounds}
items={this.state.contextMenuItems} clickAt={this.state.contextMenuClickAt}
onResult={this.state.handleContextMenuClick}
visible={this.state.contextMenuVisible} visible={this.state.contextMenuVisible}
hide={this.handleHideContextMenu} /> topic={this.state.contextMenuTopic}
items={this.state.contextMenuItems}
hide={this.handleHideContextMenu}
onAction={this.handleContextMenuAction} />
<SidepanelView <SidepanelView
displayMobile={this.state.displayMobile} displayMobile={this.state.displayMobile}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB