refactored contect menu and updated readme
This commit is contained in:
@@ -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.
|
||||||
|
|
||||||

|
## 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
@@ -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
@@ -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}
|
||||||
|
|||||||
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 |
Binary file not shown.
|
Before Width: | Height: | Size: 150 KiB |
Reference in New Issue
Block a user