Merge branch 'master' into converse-omemo

This commit is contained in:
JC Brand 2018-08-18 10:24:55 +02:00
commit b4110dc162
36 changed files with 3252 additions and 1114 deletions

View File

@ -20,7 +20,7 @@
"rules": {
"lodash/prefer-lodash-method": [2, {
"ignoreMethods": [
"find", "endsWith", "startsWith", "filter", "reduce",
"find", "endsWith", "startsWith", "filter", "reduce", "isArray", "create",
"map", "replace", "toLower", "split", "trim", "forEach", "toUpperCase", "includes"
]
}],
@ -216,10 +216,7 @@
"one-var": "off",
"one-var-declaration-per-line": "off",
"operator-assignment": "off",
"operator-linebreak": [
"error",
"after"
],
"operator-linebreak": "off",
"padded-blocks": "off",
"prefer-arrow-callback": "off",
"prefer-const": "error",

View File

@ -21,8 +21,9 @@
- Add a checkbox to indicate whether a trusted device is being used or not.
If the device is not trusted, sessionStorage is used and all user data is deleted from the browser cache upon logout.
If the device is trusted, localStorage is used and user data is cached indefinitely.
- Initial support for XEP-0357 Push Notifications, specifically registering an "App Server".
- Initial support for [XEP-0357 Push Notifications](https://xmpp.org/extensions/xep-0357.html), specifically registering an "App Server".
- Add support for logging in via OAuth (see the [oauth_providers](https://conversejs.org/docs/html/configurations.html#oauth-providers) setting)
- Add support for [XEP-0372 References](https://xmpp.org/extensions/xep-0372.html), specifically section "3.2 Mentions".
### Bugfixes

View File

@ -7490,6 +7490,8 @@ body.reset {
@media screen and (max-width: 480px) {
#conversejs .chatbox .sendXMPPMessage {
width: 100%; } }
#conversejs .chatbox .sendXMPPMessage .suggestion-box__results:after {
display: none; }
#conversejs .chatbox .sendXMPPMessage .spoiler-hint {
width: 100%; }
#conversejs .chatbox .sendXMPPMessage .chat-textarea {
@ -7501,9 +7503,12 @@ body.reset {
width: 100%;
border: none;
min-height: 60px;
margin-bottom: -4px; }
margin-bottom: -4px;
resize: none; }
#conversejs .chatbox .sendXMPPMessage .chat-textarea.spoiler {
height: 42px; }
#conversejs .chatbox .sendXMPPMessage .chat-textarea.correcting {
background-color: #e7f7ee; }
#conversejs .chatbox .sendXMPPMessage .send-button {
position: absolute;
left: 3px;
@ -7519,7 +7524,7 @@ body.reset {
margin: 0;
padding: 0.25em;
display: block;
border-top: 8px solid #3AA569;
border-top: 4px solid #3AA569;
background-color: white;
color: #3AA569; }
#conversejs .chatbox .sendXMPPMessage .chat-toolbar .fa, #conversejs .chatbox .sendXMPPMessage .chat-toolbar .fa:hover {
@ -7632,6 +7637,9 @@ body.reset {
left: 0; }
/* ******************* Overlay and embedded styles *************************** */
#conversejs.converse-embedded .chat-textarea {
max-height: 15em; }
#conversejs.converse-embedded .chat-head,
#conversejs.converse-overlayed .chat-head {
border-top-left-radius: 4px;
@ -7713,7 +7721,7 @@ body.reset {
flex: 0 0 16.6666666667%;
max-width: 16.6666666667%; }
#conversejs.converse-fullscreen .chat-textarea {
max-height: 400px; }
max-height: 15em; }
#conversejs.converse-fullscreen .emoji-picker {
height: 150px; }
#conversejs.converse-fullscreen .chatbox {
@ -7753,7 +7761,7 @@ body.reset {
border-top-right-radius: 4px; }
#conversejs.converse-fullscreen .chatbox .chat-title {
font-size: 20px;
line-height: 24px; }
line-height: 27px; }
#conversejs.converse-fullscreen .chatbox .sendXMPPMessage ul {
width: 100%; }
#conversejs.converse-fullscreen .chatbox .sendXMPPMessage .toggle-smiley ul.emoji-toolbar .emoji-category-picker {
@ -8120,7 +8128,7 @@ body.reset {
padding: 3em 2em 3em; }
#conversejs.converse-fullscreen #controlbox .toggle-register-login,
#conversejs.converse-mobile #controlbox .toggle-register-login {
line-height: 24px; }
line-height: 27px; }
#conversejs.converse-fullscreen #controlbox .brand-heading-container,
#conversejs.converse-mobile #controlbox .brand-heading-container {
flex: 0 0 100%;
@ -8545,9 +8553,6 @@ body.reset {
#conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .chat-info.badge,
#conversejs .chatroom .box-flyout .chatroom-body .chat-info.badge {
color: white; }
#conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .mentioned,
#conversejs .chatroom .box-flyout .chatroom-body .mentioned {
font-weight: bold; }
#conversejs.converse-embedded .chatroom .box-flyout .chatroom-body .disconnect-container,
#conversejs .chatroom .box-flyout .chatroom-body .disconnect-container {
margin: 1em;
@ -8681,7 +8686,7 @@ body.reset {
#conversejs.converse-embedded .chatroom .sendXMPPMessage .chat-toolbar,
#conversejs .chatroom .sendXMPPMessage .chat-toolbar {
background-color: white;
border-top: 8px solid #E77051;
border-top: 4px solid #E77051;
color: #E77051; }
#conversejs.converse-embedded .chatroom .sendXMPPMessage .chat-toolbar .fa, #conversejs.converse-embedded .chatroom .sendXMPPMessage .chat-toolbar .fa:hover,
#conversejs .chatroom .sendXMPPMessage .chat-toolbar .fa,
@ -8690,14 +8695,20 @@ body.reset {
#conversejs.converse-embedded .chatroom .sendXMPPMessage .chat-textarea,
#conversejs .chatroom .sendXMPPMessage .chat-textarea {
border-bottom-right-radius: 0; }
#conversejs.converse-embedded .chatroom .sendXMPPMessage .chat-textarea.correcting,
#conversejs .chatroom .sendXMPPMessage .chat-textarea.correcting {
background-color: #fadfd7; }
#conversejs.converse-embedded .chatroom .sendXMPPMessage .send-button,
#conversejs .chatroom .sendXMPPMessage .send-button {
background-color: #E77051; }
#conversejs.converse-embedded .chatroom .room-invite .invited-contact,
#conversejs .chatroom .room-invite .invited-contact {
margin: -1px 0 0 -1px;
width: 100%;
border: 1px solid #999; }
#conversejs.converse-embedded .chatroom .room-invite,
#conversejs .chatroom .room-invite {
padding-bottom: 1em; }
#conversejs.converse-embedded .chatroom .room-invite .invited-contact,
#conversejs .chatroom .room-invite .invited-contact {
margin: -1px 0 0 -1px;
width: 100%;
border: 1px solid #999; }
/* ******************* Overlay styles *************************** */
#conversejs.converse-overlayed .chatbox.chatroom {
@ -8798,6 +8809,10 @@ body.reset {
border: 1.2em solid #E7A151;
border-top: 0.8em solid #E7A151; }
#conversejs .message .mention {
font-weight: bold; }
#conversejs .message .mention--self {
font-weight: normal; }
#conversejs .message.date-separator {
height: 2em;
margin: 0;
@ -8854,7 +8869,7 @@ body.reset {
#conversejs .message.chat-msg:hover .chat-msg__actions .chat-msg__action {
opacity: 1; }
#conversejs .message.chat-msg.correcting.groupchat {
background-color: #fdf1ee; }
background-color: #fadfd7; }
#conversejs .message.chat-msg.correcting:not(.groupchat) {
background-color: #e7f7ee; }
#conversejs .message.chat-msg .spoiler {
@ -9044,20 +9059,26 @@ body.reset {
#conversejs .visually-hidden {
position: absolute;
clip: rect(0, 0, 0, 0); }
#conversejs .form-group .suggestion-box,
#conversejs .form-group .awesomplete {
width: 100%; }
#conversejs div.awesomplete {
display: inline-block;
#conversejs .suggestion-box,
#conversejs .awesomplete {
position: relative; }
#conversejs div.awesomplete mark {
#conversejs .suggestion-box mark,
#conversejs .awesomplete mark {
background: #FFB9A7; }
#conversejs div.awesomplete > input {
#conversejs .suggestion-box > input,
#conversejs .awesomplete > input {
display: block; }
#conversejs div.awesomplete > ul {
#conversejs .suggestion-box .suggestion-box__results,
#conversejs .suggestion-box > ul,
#conversejs .awesomplete .suggestion-box__results,
#conversejs .awesomplete > ul {
position: absolute;
left: 0;
right: 0;
z-index: 1;
z-index: 2;
min-width: 100%;
box-sizing: border-box;
list-style: none;
@ -9065,55 +9086,96 @@ body.reset {
border-radius: .3em;
margin: .2em 0 0;
background: rgba(255, 255, 255, 0.9);
background: linear-gradient(to bottom right, white, rgba(255, 255, 255, 0.8));
background: linear-gradient(to bottom right, white, rgba(255, 255, 255, 0.9));
border: 1px solid rgba(0, 0, 0, 0.3);
box-shadow: 0.05em 0.2em 0.6em rgba(0, 0, 0, 0.2);
box-shadow: 0.05em 0.2em 0.6em rgba(0, 0, 0, 0.1);
text-shadow: none; }
#conversejs div.awesomplete > ul:before {
#conversejs .suggestion-box .suggestion-box__results:before,
#conversejs .suggestion-box > ul:before,
#conversejs .awesomplete .suggestion-box__results:before,
#conversejs .awesomplete > ul:before {
content: "";
position: absolute;
top: -.43em;
left: 1em;
width: 0;
height: 0;
padding: .4em;
background: white;
border: inherit;
border-right: 0;
border-bottom: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg); }
#conversejs div.awesomplete > ul > li {
transform: rotate(45deg);
z-index: 1; }
#conversejs .suggestion-box .suggestion-box__results > li,
#conversejs .suggestion-box > ul > li,
#conversejs .awesomplete .suggestion-box__results > li,
#conversejs .awesomplete > ul > li {
text-overflow: ellipsis;
overflow-x: hidden;
position: relative;
cursor: pointer;
padding: 1em; }
#conversejs .suggestion-box .suggestion-box__results--above,
#conversejs .awesomplete .suggestion-box__results--above {
bottom: 4.5em; }
#conversejs .suggestion-box .suggestion-box__results--above:before,
#conversejs .awesomplete .suggestion-box__results--above:before {
display: none; }
#conversejs .suggestion-box .suggestion-box__results--above:after,
#conversejs .awesomplete .suggestion-box__results--above:after {
z-index: 1;
content: "";
position: absolute;
bottom: -.43em;
left: 1em;
width: 0;
height: 0;
padding: .4em;
background: white;
border: inherit;
border-left: 0;
border-top: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg); }
#conversejs .suggestion-box > ul[hidden],
#conversejs .suggestion-box > ul:empty,
#conversejs div.awesomplete > ul[hidden],
#conversejs div.awesomplete > ul:empty {
display: none; }
@supports (transform: scale(0)) {
#conversejs .suggestion-box > ul,
#conversejs div.awesomplete > ul {
transition: 0.3s cubic-bezier(0.4, 0.2, 0.5, 1.4);
transform-origin: 1.43em -.43em; }
#conversejs .suggestion-box > ul[hidden],
#conversejs .suggestion-box > ul:empty,
#conversejs div.awesomplete > ul[hidden],
#conversejs div.awesomplete > ul:empty {
opacity: 0;
transform: scale(0);
display: block;
transition-timing-function: ease; } }
#conversejs div.awesomplete > ul > li:hover {
background: #E77051;
color: white; }
#conversejs .suggestion-box > ul > li[aria-selected="true"],
#conversejs div.awesomplete > ul > li[aria-selected="true"] {
background: #3d6d8f;
background: #D24E2B;
color: white; }
#conversejs .suggestion-box li:hover mark,
#conversejs div.awesomplete li:hover mark {
background: #A53214;
background: #FFB9A7;
color: white; }
#conversejs .suggestion-box li[aria-selected="true"] mark,
#conversejs div.awesomplete li[aria-selected="true"] mark {
background: #3d6b00;
background: #E77051;
color: inherit; }
#conversejs.converse-fullscreen .suggestion-box__results--above {
bottom: 4.5em; }
#conversejs.converse-overlayed .suggestion-box__results--above {
bottom: 5.5em; }
#conversejs.converse-embedded {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;

1144
dist/converse.js vendored

File diff suppressed because it is too large Load Diff

12
docs/source/api/index.rst Normal file
View File

@ -0,0 +1,12 @@
.. raw:: html
<div id="banner"><a href="https://github.com/jcbrand/converse.js/blob/master/docs/source/theming.rst">Edit me on GitHub</a></div>
=========================
The new API documentation
=========================
This document is a stub. It shouldn't show at all, instead it's a hack in order
to link to the JSDoc output.
See https://stackoverflow.com/questions/27979803/external-relative-link-in-sphinx-toctree-directive

View File

@ -12,11 +12,6 @@
<script src="dist/converse.js"></script>
</head>
<body class="reset">
<div class="content">
<div class="inner-content">
<h1 class="brand-heading"><i class="icon-conversejs"></i> Converse</h1>
</div>
</div>
<script>
/*
@licstart

View File

@ -283,6 +283,10 @@
</div>
</div>
<div class="message chat-info chat-state-notification"
data-isodate="2018-04-36T18:21:36+02:00"
data-csn="romeo@capulet.lit">Romeo Montague is typing</div>
</div>
<div class="new-msgs-indicator">▼ You have unread messages ▼</div>
<form class="sendXMPPMessage">

View File

@ -7,13 +7,14 @@
}
.form-group {
.suggestion-box,
.awesomplete {
width: 100%;
}
}
div.awesomplete {
display: inline-block;
.suggestion-box,
.awesomplete {
position: relative;
mark {
background: $lightest-red;
@ -23,6 +24,7 @@
display: block;
}
.suggestion-box__results,
> ul {
&:before {
content: "";
@ -30,18 +32,19 @@
top: -.43em;
left: 1em;
width: 0; height: 0;
padding: .4em;
background: white;
border: inherit;
border-right: 0;
border-bottom: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
z-index: 1;
}
position: absolute;
left: 0;
right: 0;
z-index: 1;
z-index: 2;
min-width: 100%;
box-sizing: border-box;
list-style: none;
@ -49,9 +52,9 @@
border-radius: .3em;
margin: .2em 0 0;
background: hsla(0,0%,100%,.9);
background: linear-gradient(to bottom right, white, hsla(0,0%,100%,.8));
background: linear-gradient(to bottom right, white, hsla(0,0%,100%,.9));
border: 1px solid rgba(0,0,0,.3);
box-shadow: .05em .2em .6em rgba(0,0,0,.2);
box-shadow: .05em .2em .6em rgba(0,0,0,.1);
text-shadow: none;
> li {
@ -62,19 +65,45 @@
padding: 1em;
}
}
.suggestion-box__results--above {
bottom: 4.5em;
&:before {
display: none;
}
&:after {
z-index: 1;
content: "";
position: absolute;
bottom: -.43em;
left: 1em;
width: 0; height: 0;
padding: .4em;
background: white;
border: inherit;
border-left: 0;
border-top: 0;
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
}
}
.suggestion-box > ul[hidden],
.suggestion-box > ul:empty,
div.awesomplete > ul[hidden],
div.awesomplete > ul:empty {
display: none;
}
@supports (transform: scale(0)) {
.suggestion-box > ul,
div.awesomplete > ul {
transition: .3s cubic-bezier(.4,.2,.5,1.4);
transform-origin: 1.43em -.43em;
}
.suggestion-box > ul[hidden],
.suggestion-box > ul:empty,
div.awesomplete > ul[hidden],
div.awesomplete > ul:empty {
opacity: 0;
@ -84,23 +113,33 @@
}
}
div.awesomplete > ul > li:hover {
background: $red;
color: $inverse-link-color;
}
.suggestion-box > ul > li[aria-selected="true"],
div.awesomplete > ul > li[aria-selected="true"] {
background: hsl(205, 40%, 40%);
color: white;
}
div.awesomplete li:hover mark {
background: $darkest-red;
background: $dark-red;
color: $inverse-link-color;
}
.suggestion-box li:hover mark,
div.awesomplete li:hover mark {
background: $lightest-red;
color: $inverse-link-color;
}
.suggestion-box li[aria-selected="true"] mark,
div.awesomplete li[aria-selected="true"] mark {
background: hsl(86, 100%, 21%);
background: $red;
color: inherit;
}
}
#conversejs.converse-fullscreen {
.suggestion-box__results--above {
bottom: 4.5em;
}
}
#conversejs.converse-overlayed {
.suggestion-box__results--above {
bottom: 5.5em;
}
}

View File

@ -237,11 +237,18 @@
width: 100%;
}
.suggestion-box__results {
&:after {
display: none;
}
}
.spoiler-hint {
width: 100%;
}
.chat-textarea {
color: $chat-textarea-color;
border-top-left-radius: 0;
border-top-right-radius: 0;
@include border-bottom-radius($chatbox-border-radius);
@ -250,9 +257,13 @@
border: none;
min-height: $chat-textarea-height;
margin-bottom: -4px; // Not clear why this is necessar :(
resize: none;
&.spoiler {
height: 42px;
}
&.correcting {
background-color: lighten($chat-head-color, 50%);
}
}
.send-button {
@ -271,7 +282,7 @@
margin: 0;
padding: 0.25em;
display: block;
border-top: 8px solid $chat-head-color;
border-top: 4px solid $chat-head-color;
background-color: white;
color: $chat-head-color;
.fa, .fa:hover {
@ -437,6 +448,12 @@
/* ******************* Overlay and embedded styles *************************** */
#conversejs.converse-embedded {
.chat-textarea {
max-height: $fullpage-max-chat-textarea-height;
}
}
#conversejs.converse-embedded,
#conversejs.converse-overlayed {
.chat-head {

View File

@ -116,9 +116,6 @@
color: $chat-head-text-color;
}
}
.mentioned {
font-weight: bold;
}
.disconnect-container {
margin: 1em;
width: 100%;
@ -263,7 +260,7 @@
.sendXMPPMessage {
.chat-toolbar {
background-color: white;
border-top: 8px solid $chatroom-head-color;
border-top: 4px solid $chatroom-head-color;
color: $chatroom-head-color;
.fa, .fa:hover {
color: $chatroom-head-color;
@ -271,6 +268,9 @@
}
.chat-textarea {
border-bottom-right-radius: 0;
&.correcting {
background-color: lighten($chatroom-head-color, 30%);
}
}
.send-button {
background-color: $chatroom-head-color;
@ -278,6 +278,7 @@
}
.room-invite {
padding-bottom: 1em;
.invited-contact {
margin: -1px 0 0 -1px;
width: 100%;

View File

@ -1,6 +1,12 @@
#conversejs {
.message {
.mention {
font-weight: bold;
}
.mention--self {
font-weight: normal;
}
&.date-separator {
height: 2em;
margin: 0;
@ -80,7 +86,7 @@
}
&.correcting {
&.groupchat {
background-color: lighten($chatroom-head-color, 35%);
background-color: lighten($chatroom-head-color, 30%);
}
&:not(.groupchat) {
background-color: lighten($chat-head-color, 50%);

View File

@ -56,6 +56,7 @@ $border-color: #CCC !default;
$icon-color: $blue !default;
$save-button-color: $green !default;
$chat-textarea-color: #666 !default;
$chat-textarea-height: 60px !default;
$send-button-height: 27px !default;
@ -140,7 +141,7 @@ $legend-font-size: 16px !default;
$line-height-small: 14px !default;
$line-height: 16px !default;
$line-height-large: 20px !default;
$line-height-huge: 24px !default;
$line-height-huge: 27px !default;
$occupants-padding: 1em;
@ -148,7 +149,7 @@ $fullpage-chat-head-height: 62px !default;
$fullpage-chat-height: 100vh;
$fullpage-chat-width: 100%;
$fullpage-emoji-picker-height: 150px !default;
$fullpage-max-chat-textarea-height: 400px !default;
$fullpage-max-chat-textarea-height: 15em!default;
$overlayed-chat-head-height: 55px !default;
$overlayed-chat-height: 450px !default;

175
spec/autocomplete.js Normal file
View File

@ -0,0 +1,175 @@
(function (root, factory) {
define([
"jasmine",
"mock",
"test-utils"
], factory);
} (this, function (jasmine, mock, test_utils) {
"use strict";
const _ = converse.env._;
const $iq = converse.env.$iq;
const $msg = converse.env.$msg;
const $pres = converse.env.$pres;
const Strophe = converse.env.Strophe;
const u = converse.env.utils;
describe("The nickname autocomplete feature", function () {
it("shows all autocompletion options when the user presses @",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'tom')
.then(() => {
const view = _converse.chatboxviews.get('lounge@localhost');
['dick', 'harry'].forEach((nick) => {
_converse.connection._dataRecv(test_utils.createRequest(
$pres({
'to': 'tom@localhost/resource',
'from': `lounge@localhost/${nick}`
})
.c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': `${nick}@localhost/resource`,
'role': 'participant'
})));
});
// Test that pressing @ brings up all options
const textarea = view.el.querySelector('textarea.chat-textarea');
const at_event = {
'target': textarea,
'preventDefault': _.noop,
'stopPropagation': _.noop,
'keyCode': 50
};
view.keyPressed(at_event);
textarea.value = '@';
view.keyUp(at_event);
expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(3);
expect(view.el.querySelector('.suggestion-box__results li[aria-selected="true"]').textContent).toBe('tom');
expect(view.el.querySelector('.suggestion-box__results li:first-child').textContent).toBe('tom');
expect(view.el.querySelector('.suggestion-box__results li:nth-child(2)').textContent).toBe('dick');
expect(view.el.querySelector('.suggestion-box__results li:nth-child(3)').textContent).toBe('harry');
done();
}).catch(_.partial(console.error, _));
}));
it("autocompletes when the user presses tab",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy')
.then(() => {
const view = _converse.chatboxviews.get('lounge@localhost');
expect(view.model.occupants.length).toBe(1);
let presence = $pres({
'to': 'dummy@localhost/resource',
'from': 'lounge@localhost/some1'
})
.c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': 'some1@localhost/resource',
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(view.model.occupants.length).toBe(2);
const textarea = view.el.querySelector('textarea.chat-textarea');
textarea.value = "hello som";
// Press tab
const tab_event = {
'target': textarea,
'preventDefault': _.noop,
'stopPropagation': _.noop,
'keyCode': 9
}
view.keyPressed(tab_event);
view.keyUp(tab_event);
expect(view.el.querySelector('.suggestion-box__results').hidden).toBeFalsy();
expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(1);
expect(view.el.querySelector('.suggestion-box__results li').textContent).toBe('some1');
const backspace_event = {
'target': textarea,
'preventDefault': _.noop,
'keyCode': 8
}
for (var i=0; i<3; i++) {
// Press backspace 3 times to remove "som"
view.keyPressed(backspace_event);
textarea.value = textarea.value.slice(0, textarea.value.length-1)
view.keyUp(backspace_event);
}
expect(view.el.querySelector('.suggestion-box__results').hidden).toBeTruthy();
presence = $pres({
'to': 'dummy@localhost/resource',
'from': 'lounge@localhost/some2'
})
.c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': 'some2@localhost/resource',
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
textarea.value = "hello s s";
view.keyPressed(tab_event);
view.keyUp(tab_event);
expect(view.el.querySelector('.suggestion-box__results').hidden).toBeFalsy();
expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(2);
const up_arrow_event = {
'target': textarea,
'preventDefault': () => (up_arrow_event.defaultPrevented = true),
'stopPropagation': _.noop,
'keyCode': 38
}
view.keyPressed(up_arrow_event);
view.keyUp(up_arrow_event);
expect(view.el.querySelectorAll('.suggestion-box__results li').length).toBe(2);
expect(view.el.querySelector('.suggestion-box__results li[aria-selected="false"]').textContent).toBe('some1');
expect(view.el.querySelector('.suggestion-box__results li[aria-selected="true"]').textContent).toBe('some2');
view.keyPressed({
'target': textarea,
'preventDefault': _.noop,
'stopPropagation': _.noop,
'keyCode': 13 // Enter
});
expect(textarea.value).toBe('hello s @some2 ');
// Test that pressing tab twice selects
presence = $pres({
'to': 'dummy@localhost/resource',
'from': 'lounge@localhost/z3r0'
})
.c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': 'z3r0@localhost/resource',
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
textarea.value = "hello z";
view.keyPressed(tab_event);
view.keyUp(tab_event);
view.keyPressed(tab_event);
view.keyUp(tab_event);
expect(textarea.value).toBe('hello @z3r0 ');
done();
}).catch(_.partial(console.error, _));
}));
});
}));

View File

@ -707,7 +707,7 @@
.then(function () {
var view = _converse.chatboxviews.get(sender_jid);
// Check that the notification appears inside the chatbox in the DOM
var events = view.el.querySelectorAll('.chat-state-notification');
let events = view.el.querySelectorAll('.chat-state-notification');
expect(events.length).toBe(1);
expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
@ -1491,7 +1491,7 @@
var chatbox = _converse.chatboxes.get(sender_jid);
var chatboxview = _converse.chatboxviews.get(sender_jid);
var msgsIndicatorSelector = 'a.open-chat:contains("' + chatbox.get('fullname') + '") .msgs-indicator';
var selectMsgsIndicator = function () { return $($(_converse.rosterview.el).find(msgsIndicatorSelector)); };
var selectMsgsIndicator = () => $(_converse.rosterview.el).find(msgsIndicatorSelector);
var msgFactory = function () {
return test_utils.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
};
@ -1527,7 +1527,7 @@
return test_utils.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
};
var msgsIndicatorSelector = 'a.open-chat:contains("' + chatbox.get('fullname') + '") .msgs-indicator',
selectMsgsIndicator = function () { return $($(_converse.rosterview.el).find(msgsIndicatorSelector)); };
selectMsgsIndicator = () => $(_converse.rosterview.el).find(msgsIndicatorSelector);
chatbox.save('scrolled', true);
@ -1559,7 +1559,7 @@
return test_utils.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
};
var msgsIndicatorSelector = 'a.open-chat:contains("' + chatbox.get('fullname') + '") .msgs-indicator',
selectMsgsIndicator = function () { return $($(_converse.rosterview.el).find(msgsIndicatorSelector)); };
selectMsgsIndicator = () => $(_converse.rosterview.el).find(msgsIndicatorSelector);
chatbox.save('scrolled', true);
@ -1591,7 +1591,7 @@
};
const selectUnreadMsgCount = function () {
const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid);
return $(minimizedChatBoxView.el).find('.message-count');
return minimizedChatBoxView.el.querySelector('.message-count');
};
const chatbox = _converse.chatboxes.get(sender_jid);
@ -1601,9 +1601,9 @@
const chatboxview = _converse.chatboxviews.get(sender_jid);
chatboxview.minimize();
const $unreadMsgCount = selectUnreadMsgCount();
expect(u.isVisible($unreadMsgCount[0])).toBeTruthy();
expect($unreadMsgCount.html()).toBe('1');
const unread_count = selectUnreadMsgCount();
expect(u.isVisible(unread_count)).toBeTruthy();
expect(unread_count.innerHTML).toBe('1');
done();
});
}));
@ -1625,7 +1625,7 @@
};
const selectUnreadMsgCount = function () {
const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid);
return $(minimizedChatBoxView.el).find('.message-count');
return minimizedChatBoxView.el.querySelector('.message-count');
};
const chatboxview = _converse.chatboxviews.get(sender_jid);
@ -1633,9 +1633,9 @@
_converse.chatboxes.onMessage(msgFactory());
const $unreadMsgCount = selectUnreadMsgCount();
expect(u.isVisible($unreadMsgCount[0])).toBeTruthy();
expect($unreadMsgCount.html()).toBe('1');
const unread_count = selectUnreadMsgCount();
expect(u.isVisible(unread_count)).toBeTruthy();
expect(unread_count.innerHTML).toBe('1');
done();
});
}));

View File

@ -60,49 +60,49 @@
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
let jid, room, chatroomview;
test_utils.createContacts(_converse, 'current');
test_utils.waitUntil(function () {
return $(_converse.rosterview.el).find('.roster-group .group-toggle').length;
}, 300).then(function () {
test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy').then(function () {
var jid = 'lounge@localhost';
var room = _converse.api.rooms.get(jid);
expect(room instanceof Object).toBeTruthy();
test_utils.waitUntil(() => _converse.rosterview.el.querySelectorAll('.roster-group .group-toggle').length, 300)
.then(() => test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy'))
.then(() => {
jid = 'lounge@localhost';
room = _converse.api.rooms.get(jid);
expect(room instanceof Object).toBeTruthy();
var chatroomview = _converse.chatboxviews.get(jid);
expect(chatroomview.is_chatroom).toBeTruthy();
chatroomview = _converse.chatboxviews.get(jid);
expect(chatroomview.is_chatroom).toBeTruthy();
expect(u.isVisible(chatroomview.el)).toBeTruthy();
chatroomview.close();
expect(u.isVisible(chatroomview.el)).toBeTruthy();
chatroomview.close();
// Test with mixed case
test_utils.openAndEnterChatRoom(_converse, 'Leisure', 'localhost', 'dummy').then(function () {
jid = 'Leisure@localhost';
room = _converse.api.rooms.get(jid);
expect(room instanceof Object).toBeTruthy();
chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
expect(u.isVisible(chatroomview.el)).toBeTruthy();
// Test with mixed case
return test_utils.openAndEnterChatRoom(_converse, 'Leisure', 'localhost', 'dummy');
}).then(() => {
jid = 'Leisure@localhost';
room = _converse.api.rooms.get(jid);
expect(room instanceof Object).toBeTruthy();
chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
expect(u.isVisible(chatroomview.el)).toBeTruthy();
jid = 'leisure@localhost';
room = _converse.api.rooms.get(jid);
expect(room instanceof Object).toBeTruthy();
chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
expect(u.isVisible(chatroomview.el)).toBeTruthy();
jid = 'leisure@localhost';
room = _converse.api.rooms.get(jid);
expect(room instanceof Object).toBeTruthy();
chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
expect(u.isVisible(chatroomview.el)).toBeTruthy();
jid = 'leiSure@localhost';
room = _converse.api.rooms.get(jid);
expect(room instanceof Object).toBeTruthy();
chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
expect(u.isVisible(chatroomview.el)).toBeTruthy();
chatroomview.close();
jid = 'leiSure@localhost';
room = _converse.api.rooms.get(jid);
expect(room instanceof Object).toBeTruthy();
chatroomview = _converse.chatboxviews.get(jid.toLowerCase());
expect(u.isVisible(chatroomview.el)).toBeTruthy();
chatroomview.close();
// Non-existing room
jid = 'lounge2@localhost';
room = _converse.api.rooms.get(jid);
expect(typeof room === 'undefined').toBeTruthy();
done();
});
});
// Non-existing room
jid = 'lounge2@localhost';
room = _converse.api.rooms.get(jid);
expect(typeof room === 'undefined').toBeTruthy();
done();
});
}));
@ -1240,8 +1240,9 @@
var occupants = view.el.querySelector('.occupant-list').querySelectorAll('li');
expect(occupants.length).toBe(1);
expect($(occupants).first().find('.occupant-nick').text().trim()).toBe("dummy");
expect($(occupants).first().find('.badge').length).toBe(1);
expect($(occupants).first().find('.badge').first().text()).toBe('Member');
expect($(occupants).first().find('.badge').length).toBe(2);
expect($(occupants).first().find('.badge').first().text()).toBe('Owner');
expect($(occupants).first().find('.badge').last().text()).toBe('Moderator');
var presence = $pres({
to:'dummy@localhost/pda',
@ -1255,15 +1256,15 @@
.c('status').attrs({code:'110'}).nodeTree;
_converse.connection._dataRecv(test_utils.createRequest(presence));
occupants = view.el.querySelector('.occupant-list').querySelectorAll('li');
occupants = view.el.querySelectorAll('.occupant-list li');
expect(occupants.length).toBe(2);
expect($(occupants).first().find('.occupant-nick').text().trim()).toBe("moderatorman");
expect($(occupants).last().find('.occupant-nick').text().trim()).toBe("dummy");
expect($(occupants).first().find('.badge').length).toBe(2);
expect($(occupants).first().find('.badge').first().text()).toBe('Admin');
expect($(occupants).first().find('.badge').last().text()).toBe('Moderator');
expect($(occupants).first().find('.occupant-nick').text().trim()).toBe("dummy");
expect($(occupants).last().find('.occupant-nick').text().trim()).toBe("moderatorman");
expect($(occupants).last().find('.badge').length).toBe(2);
expect($(occupants).last().find('.badge').first().text()).toBe('Admin');
expect($(occupants).last().find('.badge').last().text()).toBe('Moderator');
expect($(occupants).first().attr('title')).toBe(
expect($(occupants).last().attr('title')).toBe(
contact_jid + ' This user is a moderator. Click to mention moderatorman in your message.'
);
@ -1706,10 +1707,10 @@
})
.c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
.c('item').attrs({
affiliation: 'member',
affiliation: 'owner',
jid: 'dummy@localhost/pda',
nick: 'newnick',
role: 'participant'
role: 'moderator'
}).up()
.c('status').attrs({code:'303'}).up()
.c('status').attrs({code:'110'}).nodeTree;
@ -1730,9 +1731,9 @@
})
.c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
.c('item').attrs({
affiliation: 'member',
affiliation: 'owner',
jid: 'dummy@localhost/pda',
role: 'participant'
role: 'moderator'
}).up()
.c('status').attrs({code:'110'}).nodeTree;
@ -2047,7 +2048,7 @@
expect(view.model.get('minimized')).toBeFalsy();
expect(_converse.emit.calls.count(), 3);
done();
});
}));
@ -2075,7 +2076,7 @@
describe("Each chat groupchat can take special commands", function () {
it("/help to show the available commands",
it("takes /help to show the available commands",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
@ -2111,10 +2112,176 @@
expect(info_messages.pop().textContent).toBe('/ban: Ban user from groupchat');
expect(info_messages.pop().textContent).toBe('/admin: Change user\'s affiliation to admin');
done();
});
}).catch(_.partial(console.error, _));
}));
it("/topic to set the groupchat topic",
it("takes /member to make an occupant a member",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
let iq_stanza, view;
test_utils.openAndEnterChatRoom(_converse, 'lounge', 'muc.localhost', 'dummy')
.then(() => {
view = _converse.chatboxviews.get('lounge@muc.localhost');
/* We don't show join/leave messages for existing occupants. We
* know about them because we receive their presences before we
* receive our own.
*/
const presence = $pres({
to: 'dummy@localhost/resource',
from: 'lounge@muc.localhost/marc'
}).c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'none',
'jid': 'marc@localhost/_converse.js-290929789',
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(view.model.occupants.length).toBe(2);
const textarea = view.el.querySelector('.chat-textarea');
let sent_stanza;
spyOn(_converse.connection, 'send').and.callFake((stanza) => {
sent_stanza = stanza;
});
// First check that an error message appears when a
// non-existent nick is used.
textarea.value = '/member chris Welcome to the club!';
view.keyPressed({
target: textarea,
preventDefault: _.noop,
keyCode: 13
});
expect(_converse.connection.send).not.toHaveBeenCalled();
expect(view.el.querySelectorAll('.chat-error').length).toBe(1);
expect(view.el.querySelector('.chat-error').textContent.trim())
.toBe(`Error: Can't find a groupchat participant with the nickname "chris"`)
// Now test with an existing nick
textarea.value = '/member marc Welcome to the club!';
view.keyPressed({
target: textarea,
preventDefault: _.noop,
keyCode: 13
});
expect(_converse.connection.send).toHaveBeenCalled();
expect(sent_stanza.outerHTML).toBe(
`<iq to="lounge@muc.localhost" type="set" xmlns="jabber:client" id="${sent_stanza.getAttribute('id')}">`+
`<query xmlns="http://jabber.org/protocol/muc#admin">`+
`<item affiliation="member" jid="marc@localhost">`+
`<reason>Welcome to the club!</reason>`+
`</item>`+
`</query>`+
`</iq>`);
const result = $iq({
"xmlns": "jabber:client",
"type": "result",
"to": "dummy@localhost/resource",
"from": "lounge@muc.localhost",
"id": sent_stanza.getAttribute('id')
});
_converse.connection.IQ_stanzas = [];
_converse.connection._dataRecv(test_utils.createRequest(result));
return test_utils.waitUntil(() => {
return _.filter(
_converse.connection.IQ_stanzas,
(iq) => {
const node = iq.nodeTree.querySelector('iq[to="lounge@muc.localhost"][type="get"] item[affiliation="member"]');
if (node) { iq_stanza = iq.nodeTree;}
return node;
}).length;
});
}).then(() => {
expect(iq_stanza.outerHTML).toBe(
`<iq to="lounge@muc.localhost" type="get" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}">`+
`<query xmlns="http://jabber.org/protocol/muc#admin">`+
`<item affiliation="member"/>`+
`</query>`+
`</iq>`)
expect(view.model.occupants.length).toBe(2);
const result = $iq({
"xmlns": "jabber:client",
"type": "result",
"to": "dummy@localhost/resource",
"from": "lounge@muc.localhost",
"id": iq_stanza.getAttribute("id")
}).c("query", {"xmlns": "http://jabber.org/protocol/muc#admin"})
.c("item", {"jid": "marc", "affiliation": "member"});
_converse.connection._dataRecv(test_utils.createRequest(result));
expect(view.model.occupants.length).toBe(2);
return test_utils.waitUntil(() => {
return _.filter(
_converse.connection.IQ_stanzas,
(iq) => {
const node = iq.nodeTree.querySelector('iq[to="lounge@muc.localhost"][type="get"] item[affiliation="owner"]');
if (node) { iq_stanza = iq.nodeTree;}
return node;
}).length;
});
}).then(() => {
expect(iq_stanza.outerHTML).toBe(
`<iq to="lounge@muc.localhost" type="get" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}">`+
`<query xmlns="http://jabber.org/protocol/muc#admin">`+
`<item affiliation="owner"/>`+
`</query>`+
`</iq>`)
expect(view.model.occupants.length).toBe(2);
const result = $iq({
"xmlns": "jabber:client",
"type": "result",
"to": "dummy@localhost/resource",
"from": "lounge@muc.localhost",
"id": iq_stanza.getAttribute("id")
}).c("query", {"xmlns": "http://jabber.org/protocol/muc#admin"})
.c("item", {"jid": "dummy@localhost", "affiliation": "owner"});
_converse.connection._dataRecv(test_utils.createRequest(result));
expect(view.model.occupants.length).toBe(2);
return test_utils.waitUntil(() => {
return _.filter(
_converse.connection.IQ_stanzas,
(iq) => {
const node = iq.nodeTree.querySelector('iq[to="lounge@muc.localhost"][type="get"] item[affiliation="admin"]');
if (node) { iq_stanza = iq.nodeTree;}
return node;
}).length;
});
}).then(() => {
expect(iq_stanza.outerHTML).toBe(
`<iq to="lounge@muc.localhost" type="get" xmlns="jabber:client" id="${iq_stanza.getAttribute('id')}">`+
`<query xmlns="http://jabber.org/protocol/muc#admin">`+
`<item affiliation="admin"/>`+
`</query>`+
`</iq>`)
expect(view.model.occupants.length).toBe(2);
const result = $iq({
"xmlns": "jabber:client",
"type": "result",
"to": "dummy@localhost/resource",
"from": "lounge@muc.localhost",
"id": iq_stanza.getAttribute("id")
}).c("query", {"xmlns": "http://jabber.org/protocol/muc#admin"})
_converse.connection._dataRecv(test_utils.createRequest(result));
return test_utils.waitUntil(() => view.el.querySelectorAll('.badge').length > 1);
}).then(() => {
expect(view.model.occupants.length).toBe(2);
expect(view.el.querySelectorAll('.occupant').length).toBe(2);
done();
}).catch(_.partial(console.error, _));
}));
it("takes /topic to set the groupchat topic",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
@ -2169,7 +2336,7 @@
}).catch(_.partial(console.error, _));
}));
it("/clear to clear messages",
it("takes /clear to clear messages",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
@ -2192,7 +2359,7 @@
}).catch(_.partial(console.error, _));
}));
it("/owner to make a user an owner",
it("takes /owner to make a user an owner",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
@ -2219,9 +2386,7 @@
expect(view.onMessageSubmitted).toHaveBeenCalled();
expect(view.validateRoleChangeCommand).toHaveBeenCalled();
expect(view.showErrorMessage).toHaveBeenCalledWith(
"Error: the \"owner\" command takes two arguments, the user's nickname and optionally a reason.",
true
);
"Error: the \"owner\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.model.setAffiliation).not.toHaveBeenCalled();
// Call now with the correct amount of arguments.
@ -2245,7 +2410,7 @@
}).catch(_.partial(console.error, _));
}));
it("/ban to ban a user",
it("takes /ban to ban a user",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
@ -2272,9 +2437,7 @@
expect(view.onMessageSubmitted).toHaveBeenCalled();
expect(view.validateRoleChangeCommand).toHaveBeenCalled();
expect(view.showErrorMessage).toHaveBeenCalledWith(
"Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason.",
true
);
"Error: the \"ban\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.model.setAffiliation).not.toHaveBeenCalled();
// Call now with the correct amount of arguments.
// XXX: Calling onMessageSubmitted directly, trying
@ -2297,7 +2460,7 @@
}).catch(_.partial(console.error, _));
}));
it("/kick to kick a user",
it("takes /kick to kick a user",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
@ -2325,9 +2488,7 @@
expect(view.onMessageSubmitted).toHaveBeenCalled();
expect(view.validateRoleChangeCommand).toHaveBeenCalled();
expect(view.showErrorMessage).toHaveBeenCalledWith(
"Error: the \"kick\" command takes two arguments, the user's nickname and optionally a reason.",
true
);
"Error: the \"kick\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.modifyRole).not.toHaveBeenCalled();
// Call now with the correct amount of arguments.
// XXX: Calling onMessageSubmitted directly, trying
@ -2376,7 +2537,7 @@
}));
it("/op and /deop to make a user a moderator or not",
it("takes /op and /deop to make a user a moderator or not",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
@ -2431,9 +2592,7 @@
expect(view.onMessageSubmitted).toHaveBeenCalled();
expect(view.validateRoleChangeCommand).toHaveBeenCalled();
expect(view.showErrorMessage).toHaveBeenCalledWith(
"Error: the \"op\" command takes two arguments, the user's nickname and optionally a reason.",
true
);
"Error: the \"op\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.modifyRole).not.toHaveBeenCalled();
// Call now with the correct amount of arguments.
@ -2516,12 +2675,13 @@
}).catch(_.partial(console.error, _));
}));
it("/mute and /voice to mute and unmute a user",
it("takes /mute and /voice to mute and unmute a user",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy').then(function () {
test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'dummy')
.then(() => {
var sent_IQ, IQ_id;
var sendIQ = _converse.connection.sendIQ;
spyOn(_converse.connection, 'sendIQ').and.callFake(function (iq, callback, errback) {
@ -2571,9 +2731,7 @@
expect(view.onMessageSubmitted).toHaveBeenCalled();
expect(view.validateRoleChangeCommand).toHaveBeenCalled();
expect(view.showErrorMessage).toHaveBeenCalledWith(
"Error: the \"mute\" command takes two arguments, the user's nickname and optionally a reason.",
true
);
"Error: the \"mute\" command takes two arguments, the user's nickname and optionally a reason.");
expect(view.modifyRole).not.toHaveBeenCalled();
// Call now with the correct amount of arguments.
// XXX: Calling onMessageSubmitted directly, trying
@ -2659,6 +2817,19 @@
describe("When attempting to enter a groupchat", function () {
it("will use the nickname set in the global settings if the user doesn't have a VCard nickname",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {'nickname': 'Benedict-Cucumberpatch'},
function (done, _converse) {
test_utils.openChatRoomViaModal(_converse, 'problematic@muc.localhost')
.then(function () {
const view = _converse.chatboxviews.get('problematic@muc.localhost');
expect(view.model.get('nick')).toBe('Benedict-Cucumberpatch');
done();
}).catch(_.partial(console.error, _));
}));
it("will show an error message if the groupchat requires a password",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched', 'chatBoxesFetched'], {},
@ -3324,30 +3495,9 @@
var view = _converse.chatboxviews.get('coven@chat.shakespeare.lit');
var $chat_content = $(view.el).find('.chat-content');
/* <presence to="dummy@localhost/_converse.js-29092160"
* from="coven@chat.shakespeare.lit/some1">
* <x xmlns="http://jabber.org/protocol/muc#user">
* <item affiliation="owner" jid="dummy@localhost/_converse.js-29092160" role="moderator"/>
* <status code="110"/>
* </x>
* </presence></body>
*/
var presence = $pres({
to: 'dummy@localhost/_converse.js-29092160',
from: 'coven@chat.shakespeare.lit/some1'
}).c('x', {xmlns: Strophe.NS.MUC_USER})
.c('item', {
'affiliation': 'owner',
'jid': 'dummy@localhost/_converse.js-29092160',
'role': 'moderator'
}).up()
.c('status', {code: '110'});
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect($chat_content[0].querySelectorAll('div.chat-info').length).toBe(2);
expect($chat_content.find('div.chat-info:first').html()).toBe("some1 has entered the groupchat");
expect($chat_content.find('div.chat-info:last').html()).toBe("some1 is now a moderator");
presence = $pres({
let presence = $pres({
to: 'dummy@localhost/_converse.js-29092160',
from: 'coven@chat.shakespeare.lit/newguy'
})
@ -3358,7 +3508,7 @@
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect($chat_content[0].querySelectorAll('div.chat-info').length).toBe(3);
expect($chat_content[0].querySelectorAll('div.chat-info').length).toBe(2);
expect($chat_content.find('div.chat-info:last').html()).toBe("newguy has entered the groupchat");
presence = $pres({
@ -3372,7 +3522,7 @@
'role': 'participant'
});
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect($chat_content[0].querySelectorAll('div.chat-info').length).toBe(4);
expect($chat_content[0].querySelectorAll('div.chat-info').length).toBe(3);
expect($chat_content.find('div.chat-info:last').html()).toBe("nomorenicks has entered the groupchat");
// See XEP-0085 http://xmpp.org/extensions/xep-0085.html#definitions
@ -3389,11 +3539,10 @@
// Check that the notification appears inside the chatbox in the DOM
var events = view.el.querySelectorAll('.chat-event');
expect(events.length).toBe(4);
expect(events.length).toBe(3);
expect(events[0].textContent).toEqual('some1 has entered the groupchat');
expect(events[1].textContent).toEqual('some1 is now a moderator');
expect(events[2].textContent).toEqual('newguy has entered the groupchat');
expect(events[3].textContent).toEqual('nomorenicks has entered the groupchat');
expect(events[1].textContent).toEqual('newguy has entered the groupchat');
expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
var notifications = view.el.querySelectorAll('.chat-state-notification');
expect(notifications.length).toBe(1);
@ -3414,11 +3563,10 @@
view.model.onMessage(msg);
events = view.el.querySelectorAll('.chat-event');
expect(events.length).toBe(4);
expect(events.length).toBe(3);
expect(events[0].textContent).toEqual('some1 has entered the groupchat');
expect(events[1].textContent).toEqual('some1 is now a moderator');
expect(events[2].textContent).toEqual('newguy has entered the groupchat');
expect(events[3].textContent).toEqual('nomorenicks has entered the groupchat');
expect(events[1].textContent).toEqual('newguy has entered the groupchat');
expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
notifications = view.el.querySelectorAll('.chat-state-notification');
expect(notifications.length).toBe(1);
@ -3435,11 +3583,10 @@
}).c('body').c('composing', {'xmlns': Strophe.NS.CHATSTATES}).tree();
view.model.onMessage(msg);
events = view.el.querySelectorAll('.chat-event');
expect(events.length).toBe(4);
expect(events.length).toBe(3);
expect(events[0].textContent).toEqual('some1 has entered the groupchat');
expect(events[1].textContent).toEqual('some1 is now a moderator');
expect(events[2].textContent).toEqual('newguy has entered the groupchat');
expect(events[3].textContent).toEqual('nomorenicks has entered the groupchat');
expect(events[1].textContent).toEqual('newguy has entered the groupchat');
expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
notifications = view.el.querySelectorAll('.chat-state-notification');
expect(notifications.length).toBe(2);
@ -3458,7 +3605,7 @@
view.model.onMessage(msg);
var messages = view.el.querySelectorAll('.message');
expect(messages.length).toBe(8);
expect(messages.length).toBe(7);
expect(view.el.querySelectorAll('.chat-msg').length).toBe(1);
expect(view.el.querySelector('.chat-msg .chat-msg__text').textContent).toBe('hello world');
@ -3466,11 +3613,10 @@
// via timeout.
timeout_functions[0]();
events = view.el.querySelectorAll('.chat-event');
expect(events.length).toBe(4);
expect(events.length).toBe(3);
expect(events[0].textContent).toEqual('some1 has entered the groupchat');
expect(events[1].textContent).toEqual('some1 is now a moderator');
expect(events[2].textContent).toEqual('newguy has entered the groupchat');
expect(events[3].textContent).toEqual('nomorenicks has entered the groupchat');
expect(events[1].textContent).toEqual('newguy has entered the groupchat');
expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
notifications = view.el.querySelectorAll('.chat-state-notification');
expect(notifications.length).toBe(1);
@ -3478,11 +3624,10 @@
timeout_functions[1]();
events = view.el.querySelectorAll('.chat-event');
expect(events.length).toBe(4);
expect(events.length).toBe(3);
expect(events[0].textContent).toEqual('some1 has entered the groupchat');
expect(events[1].textContent).toEqual('some1 is now a moderator');
expect(events[2].textContent).toEqual('newguy has entered the groupchat');
expect(events[3].textContent).toEqual('nomorenicks has entered the groupchat');
expect(events[1].textContent).toEqual('newguy has entered the groupchat');
expect(events[2].textContent).toEqual('nomorenicks has entered the groupchat');
notifications = view.el.querySelectorAll('.chat-state-notification');
expect(notifications.length).toBe(0);

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,9 @@
define(["jquery", "jasmine", "mock", "test-utils"], factory);
} (this, function ($, jasmine, mock, test_utils) {
"use strict";
var _ = converse.env._;
var $msg = converse.env.$msg;
const Strophe = converse.env.Strophe,
_ = converse.env._,
$msg = converse.env.$msg;
describe("Notifications", function () {
// Implement the protocol defined in https://xmpp.org/extensions/xep-0313.html#config
@ -74,7 +75,7 @@
delete window.Notification;
}
done();
});
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
}));
it("is shown for headline messages",

View File

@ -224,69 +224,64 @@
}));
it("shows unread messages directed at the user", mock.initConverseWithAsync(
{ whitelisted_plugins: ['converse-roomslist'],
allow_bookmarks: false // Makes testing easier, otherwise we
// have to mock stanza traffic.
}, function (done, _converse) {
{ whitelisted_plugins: ['converse-roomslist'],
allow_bookmarks: false // Makes testing easier, otherwise we
// have to mock stanza traffic.
}, function (done, _converse) {
test_utils.waitUntil(function () {
return !_.isUndefined(_converse.rooms_list_view)
}, 500)
.then(function () {
var room_jid = 'kitchen@conference.shakespeare.lit';
test_utils.openAndEnterChatRoom(
_converse, 'kitchen', 'conference.shakespeare.lit', 'romeo').then(function () {
test_utils.waitUntil(() => !_.isUndefined(_converse.rooms_list_view), 500)
.then(() => test_utils.openAndEnterChatRoom(_converse, 'kitchen', 'conference.shakespeare.lit', 'romeo'))
.then(() => {
const room_jid = 'kitchen@conference.shakespeare.lit';
const view = _converse.chatboxviews.get(room_jid);
view.model.set({'minimized': true});
const contact_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
const nick = mock.chatroom_names[0];
view.model.onMessage(
$msg({
from: room_jid+'/'+nick,
id: (new Date()).getTime(),
to: 'dummy@localhost',
type: 'groupchat'
}).c('body').t('foo').tree());
var view = _converse.chatboxviews.get(room_jid);
view.model.set({'minimized': true});
var contact_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
var nick = mock.chatroom_names[0];
view.model.onMessage(
$msg({
from: room_jid+'/'+nick,
id: (new Date()).getTime(),
to: 'dummy@localhost',
type: 'groupchat'
}).c('body').t('foo').tree());
// If the user isn't mentioned, the counter doesn't get incremented, but the text of the groupchat is bold
var room_el = _converse.rooms_list_view.el.querySelector(
".available-chatroom"
);
expect(_.includes(room_el.classList, 'unread-msgs'));
// If the user isn't mentioned, the counter doesn't get incremented, but the text of the groupchat is bold
var room_el = _converse.rooms_list_view.el.querySelector(
".available-chatroom"
);
expect(_.includes(room_el.classList, 'unread-msgs'));
// If the user is mentioned, the counter also gets updated
view.model.onMessage(
$msg({
from: room_jid+'/'+nick,
id: (new Date()).getTime(),
to: 'dummy@localhost',
type: 'groupchat'
}).c('body').t('romeo: Your attention is required').tree()
);
var indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
expect(indicator_el.textContent).toBe('1');
// If the user is mentioned, the counter also gets updated
view.model.onMessage(
$msg({
from: room_jid+'/'+nick,
id: (new Date()).getTime(),
to: 'dummy@localhost',
type: 'groupchat'
}).c('body').t('romeo: Your attention is required').tree()
);
var indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
expect(indicator_el.textContent).toBe('1');
view.model.onMessage(
$msg({
from: room_jid+'/'+nick,
id: (new Date()).getTime(),
to: 'dummy@localhost',
type: 'groupchat'
}).c('body').t('romeo: and another thing...').tree()
);
indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
expect(indicator_el.textContent).toBe('2');
view.model.onMessage(
$msg({
from: room_jid+'/'+nick,
id: (new Date()).getTime(),
to: 'dummy@localhost',
type: 'groupchat'
}).c('body').t('romeo: and another thing...').tree()
);
indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
expect(indicator_el.textContent).toBe('2');
// When the chat gets maximized again, the unread indicators are removed
view.model.set({'minimized': false});
indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
expect(_.isNull(indicator_el));
room_el = _converse.rooms_list_view.el.querySelector(".available-chatroom");
expect(_.includes(room_el.classList, 'unread-msgs')).toBeFalsy();
done();
});
});
// When the chat gets maximized again, the unread indicators are removed
view.model.set({'minimized': false});
indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
expect(_.isNull(indicator_el));
room_el = _converse.rooms_list_view.el.querySelector(".available-chatroom");
expect(_.includes(room_el.classList, 'unread-msgs')).toBeFalsy();
done();
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
}));
});
}));

View File

@ -0,0 +1,415 @@
// Converse.js
// http://conversejs.org
//
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
// This plugin started as a fork of Lea Verou's Awesomplete
// https://leaverou.github.io/awesomplete/
(function (root, factory) {
define(["converse-core"], factory);
}(this, function (converse) {
const { _, Backbone } = converse.env,
u = converse.env.utils;
converse.plugins.add("converse-autocomplete", {
initialize () {
const { _converse } = this;
_converse.FILTER_CONTAINS = function (text, input) {
return RegExp(helpers.regExpEscape(input.trim()), "i").test(text);
};
_converse.FILTER_STARTSWITH = function (text, input) {
return RegExp("^" + helpers.regExpEscape(input.trim()), "i").test(text);
};
const SORT_BYLENGTH = function (a, b) {
if (a.length !== b.length) {
return a.length - b.length;
}
return a < b? -1 : 1;
};
const ITEM = (text, input) => {
input = input.trim();
const element = document.createElement("li");
element.setAttribute("aria-selected", "false");
const regex = new RegExp("("+input+")", "ig");
const parts = input ? text.split(regex) : [text];
parts.forEach((txt) => {
if (input && txt.match(regex)) {
const match = document.createElement("mark");
match.textContent = txt;
element.appendChild(match);
} else {
element.appendChild(document.createTextNode(txt));
}
});
return element;
};
class AutoComplete {
constructor (el, config={}) {
this.is_opened = false;
if (u.hasClass('.suggestion-box', el)) {
this.container = el;
} else {
this.container = el.querySelector('.suggestion-box');
}
this.input = this.container.querySelector('.suggestion-box__input');
this.input.setAttribute("autocomplete", "off");
this.input.setAttribute("aria-autocomplete", "list");
this.ul = this.container.querySelector('.suggestion-box__results');
this.status = this.container.querySelector('.suggestion-box__additions');
_.assignIn(this, {
'match_current_word': false, // Match only the current word, otherwise all input is matched
'match_on_tab': false, // Whether matching should only start when tab's pressed
'trigger_on_at': false, // Whether @ should trigger autocomplete
'min_chars': 2,
'max_items': 10,
'auto_evaluate': true,
'auto_first': false,
'data': _.identity,
'filter': _converse.FILTER_CONTAINS,
'sort': config.sort === false ? false : SORT_BYLENGTH,
'item': ITEM
}, config);
this.index = -1;
this.bindEvents()
if (this.input.hasAttribute("list")) {
this.list = "#" + this.input.getAttribute("list");
this.input.removeAttribute("list");
} else {
this.list = this.input.getAttribute("data-list") || config.list || [];
}
}
bindEvents () {
// Bind events
const input = {
"blur": () => this.close({'reason': 'blur'})
}
if (this.auto_evaluate) {
input["input"] = () => this.evaluate();
}
this._events = {
'input': input,
'form': {
"submit": () => this.close({'reason': 'submit'})
},
'ul': {
"mousedown": (ev) => this.onMouseDown(ev),
"mouseover": (ev) => this.onMouseOver(ev)
}
};
helpers.bind(this.input, this._events.input);
helpers.bind(this.input.form, this._events.form);
helpers.bind(this.ul, this._events.ul);
}
set list (list) {
if (Array.isArray(list) || typeof list === "function") {
this._list = list;
} else if (typeof list === "string" && _.includes(list, ",")) {
this._list = list.split(/\s*,\s*/);
} else { // Element or CSS selector
list = helpers.getElement(list);
if (list && list.children) {
const items = [];
slice.apply(list.children).forEach(function (el) {
if (!el.disabled) {
const text = el.textContent.trim(),
value = el.value || text,
label = el.label || text;
if (value !== "") {
items.push({ label: label, value: value });
}
}
});
this._list = items;
}
}
if (document.activeElement === this.input) {
this.evaluate();
}
}
get selected () {
return this.index > -1;
}
get opened () {
return this.is_opened;
}
close (o) {
if (!this.opened) {
return;
}
this.ul.setAttribute("hidden", "");
this.is_opened = false;
this.index = -1;
this.trigger("suggestion-box-close", o || {});
}
insertValue (suggestion) {
let value;
if (this.match_current_word) {
u.replaceCurrentWord(this.input, suggestion.value);
} else {
this.input.value = suggestion.value;
}
}
open () {
this.ul.removeAttribute("hidden");
this.is_opened = true;
if (this.auto_first && this.index === -1) {
this.goto(0);
}
this.trigger("suggestion-box-open");
}
destroy () {
//remove events from the input and its form
helpers.unbind(this.input, this._events.input);
helpers.unbind(this.input.form, this._events.form);
//move the input out of the suggestion-box container and remove the container and its children
const parentNode = this.container.parentNode;
parentNode.insertBefore(this.input, this.container);
parentNode.removeChild(this.container);
//remove autocomplete and aria-autocomplete attributes
this.input.removeAttribute("autocomplete");
this.input.removeAttribute("aria-autocomplete");
}
next () {
const count = this.ul.children.length;
this.goto(this.index < count - 1 ? this.index + 1 : (count ? 0 : -1) );
}
previous () {
const count = this.ul.children.length,
pos = this.index - 1;
this.goto(this.selected && pos !== -1 ? pos : count - 1);
}
goto (i) {
// Should not be used directly, highlights specific item without any checks!
const list = this.ul.children;
if (this.selected) {
list[this.index].setAttribute("aria-selected", "false");
}
this.index = i;
if (i > -1 && list.length > 0) {
list[i].setAttribute("aria-selected", "true");
list[i].focus();
this.status.textContent = list[i].textContent;
// scroll to highlighted element in case parent's height is fixed
this.ul.scrollTop = list[i].offsetTop - this.ul.clientHeight + list[i].clientHeight;
this.trigger("suggestion-box-highlight", {'text': this.suggestions[this.index]});
}
}
select (selected, origin) {
if (selected) {
this.index = u.siblingIndex(selected);
} else {
selected = this.ul.children[this.index];
}
if (selected) {
const suggestion = this.suggestions[this.index];
this.insertValue(suggestion);
this.close({'reason': 'select'});
this.auto_completing = false;
this.trigger("suggestion-box-selectcomplete", {'text': suggestion});
}
}
onMouseOver (ev) {
const li = u.ancestor(ev.target, 'li');
if (li) {
this.goto(Array.prototype.slice.call(this.ul.children).indexOf(li))
}
}
onMouseDown (ev) {
if (ev.button !== 0) {
return; // Only select on left click
}
const li = u.ancestor(ev.target, 'li');
if (li) {
ev.preventDefault();
this.select(li, ev.target);
}
}
keyPressed (ev) {
if (this.opened) {
if (_.includes([_converse.keycodes.ENTER, _converse.keycodes.TAB], ev.keyCode) && this.selected) {
ev.preventDefault();
ev.stopPropagation();
this.select();
return true;
} else if (ev.keyCode === _converse.keycodes.ESCAPE) {
this.close({'reason': 'esc'});
return true;
} else if (_.includes([_converse.keycodes.UP_ARROW, _converse.keycodes.DOWN_ARROW], ev.keyCode)) {
ev.preventDefault();
ev.stopPropagation();
this[ev.keyCode === _converse.keycodes.UP_ARROW ? "previous" : "next"]();
return true;
}
}
if (_.includes([
_converse.keycodes.SHIFT,
_converse.keycodes.META,
_converse.keycodes.META_RIGHT,
_converse.keycodes.ESCAPE,
_converse.keycodes.ALT]
, ev.keyCode)) {
return;
}
if (this.match_on_tab && ev.keyCode === _converse.keycodes.TAB) {
ev.preventDefault();
this.auto_completing = true;
} else if (this.trigger_on_at && ev.keyCode === _converse.keycodes.AT) {
this.auto_completing = true;
}
}
evaluate (ev) {
const arrow_pressed = (
ev.keyCode === _converse.keycodes.UP_ARROW ||
ev.keyCode === _converse.keycodes.DOWN_ARROW
);
if (!this.auto_completing || (this.selected && arrow_pressed)) {
return;
}
const list = typeof this._list === "function" ? this._list() : this._list;
if (list.length === 0) {
return;
}
let value = this.match_current_word ? u.getCurrentWord(this.input) : this.input.value;
let ignore_min_chars = false;
if (this.trigger_on_at && value.startsWith('@')) {
ignore_min_chars = true;
value = value.slice('1');
}
if ((value.length >= this.min_chars) || ignore_min_chars) {
this.index = -1;
// Populate list with options that match
this.ul.innerHTML = "";
this.suggestions = list
.map(item => new Suggestion(this.data(item, value)))
.filter(item => this.filter(item, value));
if (this.sort !== false) {
this.suggestions = this.suggestions.sort(this.sort);
}
this.suggestions = this.suggestions.slice(0, this.max_items);
this.suggestions.forEach((text) => this.ul.appendChild(this.item(text, value)));
if (this.ul.children.length === 0) {
this.close({'reason': 'nomatches'});
} else {
this.open();
}
} else {
this.close({'reason': 'nomatches'});
this.auto_completing = false;
}
}
}
// Make it an event emitter
_.extend(AutoComplete.prototype, Backbone.Events);
// Private functions
function Suggestion(data) {
const o = Array.isArray(data)
? { label: data[0], value: data[1] }
: typeof data === "object" && "label" in data && "value" in data ? data : { label: data, value: data };
this.label = o.label || o.value;
this.value = o.value;
}
Object.defineProperty(Suggestion.prototype = Object.create(String.prototype), "length", {
get: function() { return this.label.length; }
});
Suggestion.prototype.toString = Suggestion.prototype.valueOf = function () {
return "" + this.label;
};
// Helpers
var slice = Array.prototype.slice;
const helpers = {
getElement (expr, el) {
return typeof expr === "string"? (el || document).querySelector(expr) : expr || null;
},
bind (element, o) {
if (element) {
for (var event in o) {
if (!Object.prototype.hasOwnProperty.call(o, event)) {
continue;
}
const callback = o[event];
event.split(/\s+/).forEach(event => element.addEventListener(event, callback));
}
}
},
unbind (element, o) {
if (element) {
for (var event in o) {
if (!Object.prototype.hasOwnProperty.call(o, event)) {
continue;
}
const callback = o[event];
event.split(/\s+/).forEach(event => element.removeEventListener(event, callback));
}
}
},
regExpEscape (s) {
return s.replace(/[-\\^$*+?.()|[\]{}]/g, "\\$&");
}
}
_converse.AutoComplete = AutoComplete;
}
});
}));

View File

@ -20,6 +20,7 @@
const u = converse.env.utils;
Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
Strophe.addNamespace('REFERENCE', 'urn:xmpp:reference:0');
converse.plugins.add('converse-chatboxes', {
@ -225,7 +226,7 @@
});
};
xhr.open('PUT', this.get('put'), true);
xhr.setRequestHeader("Content-type", 'application/octet-stream');
xhr.setRequestHeader("Content-type", this.get('file').type);
xhr.send(this.get('file'));
}
});
@ -298,6 +299,7 @@
older_versions.push(message.get('message'));
message.save({
'message': _converse.chatboxes.getMessageBody(stanza),
'references': this.getReferencesFromStanza(stanza),
'older_versions': older_versions,
'edited': true
});
@ -323,11 +325,23 @@
if (message.get('is_spoiler')) {
if (message.get('spoiler_hint')) {
stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER }, message.get('spoiler_hint')).up();
stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER}, message.get('spoiler_hint')).up();
} else {
stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER }).up();
stanza.c('spoiler', {'xmlns': Strophe.NS.SPOILER}).up();
}
}
(message.get('references') || []).forEach(reference => {
const attrs = {
'xmlns': Strophe.NS.REFERENCE,
'begin': reference.begin,
'end': reference.end,
'type': reference.type,
}
if (reference.uri) {
attrs.uri = reference.uri;
}
stanza.c('reference', attrs).up();
});
if (message.get('file')) {
stanza.c('x', {'xmlns': Strophe.NS.OUTOFBAND}).c('url').t(message.get('message')).up();
}
@ -384,10 +398,11 @@
const older_versions = message.get('older_versions') || [];
older_versions.push(message.get('message'));
message.save({
'correcting': false,
'edited': true,
'message': attrs.message,
'older_versions': older_versions,
'edited': true,
'correcting': false
'references': attrs.references
});
} else {
message = this.messages.create(attrs);
@ -444,6 +459,21 @@
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
},
getReferencesFromStanza (stanza) {
const text = _.propertyOf(stanza.querySelector('body'))('textContent');
return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => {
const begin = ref.getAttribute('begin'),
end = ref.getAttribute('end');
return {
'begin': begin,
'end': end,
'type': ref.getAttribute('type'),
'value': text.slice(begin, end),
'uri': ref.getAttribute('uri')
};
});
},
getMessageAttributesFromStanza (stanza, original_stanza) {
/* Parses a passed in message stanza and returns an object
* of attributes.
@ -467,12 +497,15 @@
stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE ||
stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE;
const attrs = {
'chat_state': chat_state,
'is_archived': !_.isNil(archive),
'is_delayed': !_.isNil(delay),
'is_spoiler': !_.isNil(spoiler),
'message': _converse.chatboxes.getMessageBody(stanza) || undefined,
'references': this.getReferencesFromStanza(stanza),
'msgid': stanza.getAttribute('id'),
'time': delay ? delay.getAttribute('stamp') : moment().format(),
'type': stanza.getAttribute('type')
@ -533,14 +566,13 @@
_converse.windowState === 'hidden';
},
incrementUnreadMsgCounter (stanza) {
incrementUnreadMsgCounter (message) {
/* Given a newly received message, update the unread counter if
* necessary.
*/
if (_.isNull(stanza.querySelector('body'))) {
return; // The message has no text
}
if (utils.isNewMessage(stanza) && this.isHidden()) {
if (!message) { return; }
if (_.isNil(message.get('message'))) { return; }
if (utils.isNewMessage(message) && this.isHidden()) {
this.save({'num_unread': this.get('num_unread') + 1});
_converse.incrementMsgCounter();
}
@ -633,8 +665,7 @@
* Parameters:
* (XMLElement) stanza - The incoming message stanza
*/
let from_jid = stanza.getAttribute('from'),
to_jid = stanza.getAttribute('to');
let to_jid = stanza.getAttribute('to');
const to_resource = Strophe.getResourceFromJid(to_jid);
if (_converse.filter_by_resource && (to_resource && to_resource !== _converse.resource)) {
@ -648,12 +679,13 @@
// messages, but Prosody sends headline messages with the
// wrong type ('chat'), so we need to filter them out here.
_converse.log(
`onMessage: Ignoring incoming headline message sent with type 'chat' from JID: ${from_jid}`,
`onMessage: Ignoring incoming headline message sent with type 'chat' from JID: ${stanza.getAttribute('from')}`,
Strophe.LogLevel.INFO
);
return true;
}
let from_jid = stanza.getAttribute('from');
const forwarded = stanza.querySelector('forwarded'),
original_stanza = stanza;
@ -679,6 +711,12 @@
let contact_jid;
if (is_me) {
// I am the sender, so this must be a forwarded message...
if (_.isNull(to_jid)) {
return _converse.log(
`Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`,
Strophe.LogLevel.ERROR
);
}
contact_jid = Strophe.getBareJidFromJid(to_jid);
} else {
contact_jid = from_bare_jid;
@ -691,10 +729,8 @@
if (chatbox && !chatbox.handleMessageCorrection(stanza)) {
const msgid = stanza.getAttribute('id'),
message = msgid && chatbox.messages.findWhere({msgid});
if (!message) {
// Only create the message when we're sure it's not a duplicate
chatbox.incrementUnreadMsgCounter(original_stanza);
chatbox.createMessage(stanza, original_stanza);
if (!message) { // Only create the message when we're sure it's not a duplicate
chatbox.incrementUnreadMsgCounter(chatbox.createMessage(stanza, original_stanza));
}
}
_converse.emit('message', {'stanza': original_stanza, 'chatbox': chatbox});

View File

@ -50,12 +50,6 @@
"use strict";
const { $msg, Backbone, Promise, Strophe, _, b64_sha1, f, sizzle, moment } = converse.env;
const u = converse.env.utils;
const KEY = {
ENTER: 13,
UP_ARROW: 38,
DOWN_ARROW: 40,
FORWARD_SLASH: 47
};
converse.plugins.add('converse-chatview', {
/* Plugin dependencies are other plugins which might be
@ -396,13 +390,13 @@
if (this.model.get('composing_spoiler')) {
placeholder = __('Hidden message');
} else {
placeholder = __('Personal message');
placeholder = __('Message');
}
const form_container = this.el.querySelector('.message-form-container');
form_container.innerHTML = tpl_chatbox_message_form(
_.extend(this.model.toJSON(), {
'hint_value': _.get(this.el.querySelector('.spoiler-hint'), 'value'),
'label_personal_message': placeholder,
'label_message': placeholder,
'label_send': __('Send'),
'label_spoiler_hint': __('Optional hint'),
'message_value': _.get(this.el.querySelector('.chat-textarea'), 'value'),
@ -801,7 +795,7 @@
*/
this.showMessage(message);
if (message.get('correcting')) {
this.insertIntoTextArea(message.get('message'), true);
this.insertIntoTextArea(message.get('message'), true, true);
}
_converse.emit('messageAdded', {
'message': message,
@ -898,6 +892,7 @@
hint_el.value = '';
}
textarea.value = '';
u.removeClass('correcting', textarea);
textarea.focus();
// Trigger input event, so that the textarea resizes
const event = document.createEvent('Event');
@ -912,15 +907,34 @@
keyPressed (ev) {
/* Event handler for when a key is pressed in a chat box textarea.
*/
if (ev.shiftKey) { return; }
if (ev.keyCode === KEY.ENTER) {
this.onFormSubmitted(ev);
} else if (ev.keyCode === KEY.UP_ARROW && !ev.target.selectionEnd) {
this.editEarlierMessage();
} else if (ev.keyCode === KEY.DOWN_ARROW && ev.target.selectionEnd === ev.target.value.length) {
this.editLaterMessage();
} else if (ev.keyCode !== KEY.FORWARD_SLASH && this.model.get('chat_state') !== _converse.COMPOSING) {
if (ev.ctrlKey) {
// When ctrl is pressed, no chars are entered into the textarea.
return;
}
if (!ev.shiftKey && !ev.altKey) {
if (ev.keyCode === _converse.keycodes.FORWARD_SLASH) {
// Forward slash is used to run commands. Nothing to do here.
return;
} else if (ev.keyCode === _converse.keycodes.ESCAPE) {
return this.onEscapePressed(ev);
} else if (ev.keyCode === _converse.keycodes.ENTER) {
return this.onFormSubmitted(ev);
} else if (ev.keyCode === _converse.keycodes.UP_ARROW && !ev.target.selectionEnd) {
return this.editEarlierMessage();
} else if (ev.keyCode === _converse.keycodes.DOWN_ARROW && ev.target.selectionEnd === ev.target.value.length) {
return this.editLaterMessage();
}
}
if (_.includes([
_converse.keycodes.SHIFT,
_converse.keycodes.META,
_converse.keycodes.META_RIGHT,
_converse.keycodes.ESCAPE,
_converse.keycodes.ALT]
, ev.keyCode)) {
return;
}
if (this.model.get('chat_state') !== _converse.COMPOSING) {
// Set chat state to composing if keyCode is not a forward-slash
// (which would imply an internal command and not a message).
this.setChatState(_converse.COMPOSING);
@ -931,7 +945,19 @@
return f(this.model.messages.filter({'sender': 'me'}));
},
onEscapePressed (ev) {
ev.preventDefault();
const idx = this.model.messages.findLastIndex('correcting'),
message = idx >=0 ? this.model.messages.at(idx) : null;
if (message) {
message.save('correcting', false);
}
this.insertIntoTextArea('', true, false);
},
onMessageEditButtonClicked (ev) {
ev.preventDefault();
const idx = this.model.messages.findLastIndex('correcting'),
currently_correcting = idx >=0 ? this.model.messages.at(idx) : null,
message_el = u.ancestor(ev.target, '.chat-msg'),
@ -942,10 +968,10 @@
currently_correcting.save('correcting', false);
}
message.save('correcting', true);
this.insertIntoTextArea(message.get('message'), true);
this.insertIntoTextArea(u.prefixMentions(message), true, true);
} else {
message.save('correcting', false);
this.insertIntoTextArea('', true);
this.insertIntoTextArea('', true, false);
}
},
@ -964,10 +990,10 @@
}
}
if (message) {
this.insertIntoTextArea(message.get('message'), true);
this.insertIntoTextArea(message.get('message'), true, true);
message.save('correcting', true);
} else {
this.insertIntoTextArea('', true);
this.insertIntoTextArea('', true, false);
}
},
@ -987,7 +1013,7 @@
}
message = message || this.getOwnMessages().findLast((msg) => msg.get('message'));
if (message) {
this.insertIntoTextArea(message.get('message'), true);
this.insertIntoTextArea(message.get('message'), true, true);
message.save('correcting', true);
}
},
@ -1008,18 +1034,25 @@
return this;
},
insertIntoTextArea (value, replace=false) {
insertIntoTextArea (value, replace=false, correcting=false) {
const textarea = this.el.querySelector('.chat-textarea');
if (correcting) {
u.addClass('correcting', textarea);
} else {
u.removeClass('correcting', textarea);
}
if (replace) {
textarea.value = '';
textarea.value = value;
} else {
let existing = textarea.value;
if (existing && (existing[existing.length-1] !== ' ')) {
existing = existing + ' ';
}
textarea.value = '';
textarea.value = existing+value+' ';
}
textarea.focus()
u.putCurserAtEnd(textarea);
},
createEmojiPicker () {

View File

@ -67,6 +67,7 @@
// Core plugins are whitelisted automatically
_converse.core_plugins = [
'converse-autocomplete',
'converse-bookmarks',
'converse-caps',
'converse-chatboxes',
@ -107,6 +108,22 @@
// Make converse pluggable
pluggable.enable(_converse, '_converse', 'pluggable');
_converse.keycodes = {
TAB: 9,
ENTER: 13,
SHIFT: 16,
CTRL: 17,
ALT: 18,
ESCAPE: 27,
UP_ARROW: 38,
DOWN_ARROW: 40,
FORWARD_SLASH: 47,
AT: 50,
META: 91,
META_RIGHT: 93
};
// Module-level constants
_converse.STATUS_WEIGHTS = {
'offline': 6,
@ -813,7 +830,7 @@
defaults () {
return {
"jid": _converse.bare_jid,
"status": _converse.default_state,
"status": _converse.default_state
}
},
@ -1173,7 +1190,7 @@
_converse.locale,
_converse.locales,
u.interpolate(_converse.locales_url, {'locale': _converse.locale}))
.catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL))
.catch(e => _converse.log(e.message, Strophe.LogLevel.FATAL))
.then(finishInitialization)
.catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
}

View File

@ -324,7 +324,7 @@
message_archiving_timeout: 8000, // Time (in milliseconds) to wait before aborting MAM request
});
_converse.onMAMError = function (iq) {
_converse.onMAMError = function (model, iq) {
if (iq.querySelectorAll('feature-not-implemented').length) {
_converse.log(
"Message Archive Management (XEP-0313) not supported by this server",

View File

@ -168,6 +168,7 @@
text = xss.filterXSS(text, {'whiteList': {}});
msg_content.innerHTML = _.flow(
_.partial(u.geoUriToHttp, _, _converse.geouri_replacement),
_.partial(u.addMentionsMarkup, _, this.model.get('references'), this.model.collection.chatbox),
u.addHyperlinks,
u.renderNewLines,
_.partial(u.addEmoji, _converse, emojione, _)
@ -260,7 +261,7 @@
getExtraMessageClasses () {
let extra_classes = this.model.get('is_delayed') && 'delayed' || '';
if (this.model.get('type') === 'groupchat' && this.model.get('sender') === 'them') {
if (this.model.collection.chatbox.isUserMentioned(this.model.get('message'))) {
if (this.model.collection.chatbox.isUserMentioned(this.model)) {
// Add special class to mark groupchat messages
// in which we are mentioned.
extra_classes += ' mentioned';

View File

@ -1,7 +1,7 @@
// Converse.js
// http://conversejs.org
//
// Copyright (c) 2012-2018, the Converse.js developers
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
(function (root, factory) {
@ -21,7 +21,6 @@
"templates/chatroom_nickname_form.html",
"templates/chatroom_password_form.html",
"templates/chatroom_sidebar.html",
"templates/chatroom_toolbar.html",
"templates/info.html",
"templates/list_chatrooms_modal.html",
"templates/occupant.html",
@ -49,7 +48,6 @@
tpl_chatroom_nickname_form,
tpl_chatroom_password_form,
tpl_chatroom_sidebar,
tpl_chatroom_toolbar,
tpl_info,
tpl_list_chatrooms_modal,
tpl_occupant,
@ -93,7 +91,7 @@
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found.
*/
dependencies: ["converse-modal", "converse-controlbox", "converse-chatview"],
dependencies: ["converse-autocomplete", "converse-modal", "converse-controlbox", "converse-chatview"],
overrides: {
@ -154,10 +152,10 @@
// Refer to docs/source/configuration.rst for explanations of these
// configuration settings.
_converse.api.settings.update({
auto_list_rooms: false,
hide_muc_server: false, // TODO: no longer implemented...
muc_disable_moderator_commands: false,
visible_toolbar_buttons: {
'auto_list_rooms': false,
'hide_muc_server': false, // TODO: no longer implemented...
'muc_disable_moderator_commands': false,
'visible_toolbar_buttons': {
'toggle_occupants': true
}
});
@ -215,7 +213,7 @@
307: __('You have been kicked from this groupchat'),
321: __("You have been removed from this groupchat because of an affiliation change"),
322: __("You have been removed from this groupchat because the groupchat has changed to members-only and you're not a member"),
332: __("You have been removed from this groupchat because the MUC (Multi-user chat) service is being shut down")
332: __("You have been removed from this groupchat because the service hosting it is being shut down")
},
action_info_messages: {
@ -477,6 +475,10 @@
openChatRoom (ev) {
ev.preventDefault();
const data = this.parseRoomDataFromEvent(ev.target);
if (data.nick === "") {
// Make sure defaults apply if no nick is provided.
data.nick = undefined;
}
_converse.api.rooms.open(data.jid, data);
this.modal.hide();
ev.target.reset();
@ -516,6 +518,7 @@
is_chatroom: true,
events: {
'change input.fileupload': 'onFileSelection',
'click .chat-msg__action-edit': 'onMessageEditButtonClicked',
'click .chatbox-navback': 'showControlBox',
'click .close-chatbox-button': 'close',
'click .configure-chatroom-button': 'getAndRenderConfigurationForm',
@ -530,6 +533,7 @@
'click .toggle-smiley': 'toggleEmojiMenu',
'click .upload-file': 'toggleFileUpload',
'keydown .chat-textarea': 'keyPressed',
'keyup .chat-textarea': 'keyUp',
'input .chat-textarea': 'inputChanged'
},
@ -579,6 +583,8 @@
this.el.innerHTML = tpl_chatroom();
this.renderHeading();
this.renderChatArea();
this.renderMessageForm();
this.initAutoComplete();
if (this.model.get('connection_status') !== converse.ROOMSTATUS.ENTERED) {
this.showSpinner();
}
@ -596,20 +602,40 @@
if (_.isNull(this.el.querySelector('.chat-area'))) {
const container_el = this.el.querySelector('.chatroom-body');
container_el.insertAdjacentHTML('beforeend', tpl_chatarea({
'label_message': __('Message'),
'label_send': __('Send'),
'show_send_button': _converse.show_send_button,
'show_toolbar': _converse.show_toolbar,
'unread_msgs': __('You have unread messages')
'show_send_button': _converse.show_send_button
}));
container_el.insertAdjacentElement('beforeend', this.occupantsview.el);
this.renderToolbar(tpl_chatroom_toolbar);
this.content = this.el.querySelector('.chat-content');
this.toggleOccupants(null, true);
}
return this;
},
initAutoComplete () {
this.auto_complete = new _converse.AutoComplete(this.el, {
'auto_first': true,
'auto_evaluate': false,
'min_chars': 1,
'match_current_word': true,
'match_on_tab': true,
'list': () => this.model.occupants.map(o => ({'label': o.getDisplayName(), 'value': `@${o.getDisplayName()}`})),
'filter': _converse.FILTER_STARTSWITH,
'trigger_on_at': true
});
this.auto_complete.on('suggestion-box-selectcomplete', () => (this.auto_completing = false));
},
keyPressed (ev) {
if (this.auto_complete.keyPressed(ev)) {
return;
}
return _converse.ChatBoxView.prototype.keyPressed.apply(this, arguments);
},
keyUp (ev) {
this.auto_complete.evaluate(ev);
},
showRoomDetailsModal (ev) {
ev.preventDefault();
if (_.isUndefined(this.model.room_details_modal)) {
@ -702,8 +728,8 @@
return _.extend(
_converse.ChatBoxView.prototype.getToolbarOptions.apply(this, arguments),
{
label_hide_occupants: __('Hide the list of participants'),
show_occupants_toggle: this.is_chatroom && _converse.visible_toolbar_buttons.toggle_occupants
'label_hide_occupants': __('Hide the list of participants'),
'show_occupants_toggle': this.is_chatroom && _converse.visible_toolbar_buttons.toggle_occupants
}
);
},
@ -789,13 +815,31 @@
}
},
modifyRole(groupchat, nick, role, reason, onSuccess, onError) {
modifyRole (groupchat, nick, role, reason, onSuccess, onError) {
const item = $build("item", {nick, role});
const iq = $iq({to: groupchat, type: "set"}).c("query", {xmlns: Strophe.NS.MUC_ADMIN}).cnode(item.node);
if (reason !== null) { iq.c("reason", reason); }
return _converse.connection.sendIQ(iq, onSuccess, onError);
},
verifyRoles (roles) {
const me = this.model.occupants.findWhere({'jid': _converse.bare_jid});
if (!_.includes(roles, me.get('role'))) {
this.showErrorMessage(__(`Forbidden: you do not have the necessary role in order to do that.`))
return false;
}
return true;
},
verifyAffiliations (affiliations) {
const me = this.model.occupants.findWhere({'jid': _converse.bare_jid});
if (!_.includes(affiliations, me.get('affiliation'))) {
this.showErrorMessage(__(`Forbidden: you do not have the necessary affiliation in order to do that.`))
return false;
}
return true;
},
validateRoleChangeCommand (command, args) {
/* Check that a command to change a groupchat user's role or
* affiliation has anough arguments.
@ -803,9 +847,7 @@
// TODO check if first argument is valid
if (args.length < 1 || args.length > 2) {
this.showErrorMessage(
__('Error: the "%1$s" command takes two arguments, the user\'s nickname and optionally a reason.',
command),
true
__('Error: the "%1$s" command takes two arguments, the user\'s nickname and optionally a reason.', command)
);
return false;
}
@ -814,15 +856,11 @@
onCommandError (err) {
_converse.log(err, Strophe.LogLevel.FATAL);
this.showErrorMessage(
__("Sorry, an error happened while running the command. Check your browser's developer console for details."),
true
);
this.showErrorMessage(__("Sorry, an error happened while running the command. Check your browser's developer console for details."));
},
parseMessageForCommands (text) {
const _super_ = _converse.ChatBoxView.prototype;
if (_super_.parseMessageForCommands.apply(this, arguments)) {
if (_converse.ChatBoxView.prototype.parseMessageForCommands.apply(this, arguments)) {
return true;
}
if (_converse.muc_disable_moderator_commands) {
@ -833,7 +871,9 @@
command = match[1].toLowerCase();
switch (command) {
case 'admin':
if (!this.validateRoleChangeCommand(command, args)) { break; }
if (!this.verifyAffiliations(['owner']) || !this.validateRoleChangeCommand(command, args)) {
break;
}
this.model.setAffiliation('admin',
[{ 'jid': args[0],
'reason': args[1]
@ -843,7 +883,9 @@
);
break;
case 'ban':
if (!this.validateRoleChangeCommand(command, args)) { break; }
if (!this.verifyAffiliations(['owner', 'admin']) || !this.validateRoleChangeCommand(command, args)) {
break;
}
this.model.setAffiliation('outcast',
[{ 'jid': args[0],
'reason': args[1]
@ -853,7 +895,9 @@
);
break;
case 'deop':
if (!this.validateRoleChangeCommand(command, args)) { break; }
if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
break;
}
this.modifyRole(
this.model.get('jid'), args[0], 'participant', args[1],
undefined, this.onCommandError.bind(this));
@ -879,28 +923,42 @@
]);
break;
case 'kick':
if (!this.validateRoleChangeCommand(command, args)) { break; }
if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
break;
}
this.modifyRole(
this.model.get('jid'), args[0], 'none', args[1],
undefined, this.onCommandError.bind(this));
break;
case 'mute':
if (!this.validateRoleChangeCommand(command, args)) { break; }
if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
break;
}
this.modifyRole(
this.model.get('jid'), args[0], 'visitor', args[1],
undefined, this.onCommandError.bind(this));
break;
case 'member':
if (!this.validateRoleChangeCommand(command, args)) { break; }
case 'member': {
if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
break;
}
const occupant = this.model.occupants.findWhere({'nick': args[0]});
if (!occupant) {
this.showErrorMessage(__(`Error: Can't find a groupchat participant with the nickname "${args[0]}"`));
break;
}
this.model.setAffiliation('member',
[{ 'jid': args[0],
[{ 'jid': occupant.get('jid'),
'reason': args[1]
}]).then(
() => this.model.occupants.fetchMembers(),
(err) => this.onCommandError(err)
);
break;
case 'nick':
} case 'nick':
if (!this.verifyRoles(['visitor', 'participant', 'moderator'])) {
break;
}
_converse.connection.send($pres({
from: _converse.connection.jid,
to: this.model.getRoomJIDAndNick(match[2]),
@ -908,7 +966,9 @@
}).tree());
break;
case 'owner':
if (!this.validateRoleChangeCommand(command, args)) { break; }
if (!this.verifyAffiliations(['owner']) || !this.validateRoleChangeCommand(command, args)) {
break;
}
this.model.setAffiliation('owner',
[{ 'jid': args[0],
'reason': args[1]
@ -918,13 +978,17 @@
);
break;
case 'op':
if (!this.validateRoleChangeCommand(command, args)) { break; }
if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
break;
}
this.modifyRole(
this.model.get('jid'), args[0], 'moderator', args[1],
undefined, this.onCommandError.bind(this));
break;
case 'revoke':
if (!this.validateRoleChangeCommand(command, args)) { break; }
if (!this.verifyAffiliations(['admin', 'owner']) || !this.validateRoleChangeCommand(command, args)) {
break;
}
this.model.setAffiliation('none',
[{ 'jid': args[0],
'reason': args[1]
@ -944,7 +1008,9 @@
);
break;
case 'voice':
if (!this.validateRoleChangeCommand(command, args)) { break; }
if (!this.verifyRoles(['moderator']) || !this.validateRoleChangeCommand(command, args)) {
break;
}
this.modifyRole(
this.model.get('jid'), args[0], 'participant', args[1],
undefined, this.onCommandError.bind(this));

View File

@ -25,7 +25,7 @@
'none': 2,
};
const { Strophe, Backbone, Promise, $iq, $build, $msg, $pres, b64_sha1, sizzle, _, moment } = converse.env;
const { Strophe, Backbone, Promise, $iq, $build, $msg, $pres, b64_sha1, sizzle, f, moment, _ } = converse.env;
// Add Strophe Namespaces
Strophe.addNamespace('MUC_ADMIN', Strophe.NS.MUC + "#admin");
@ -171,7 +171,7 @@
'affiliation': null,
'connection_status': converse.ROOMSTATUS.DISCONNECTED,
'name': '',
'nick': _converse.xmppstatus.get('nickname'),
'nick': _converse.xmppstatus.get('nickname') || _converse.nickname,
'description': '',
'features_fetched': false,
'roomconfig': {},
@ -308,14 +308,80 @@
_converse.connection.sendPresence(presence);
},
getReferenceForMention (mention, index) {
const longest_match = u.getLongestSubstring(
mention,
this.occupants.map(o => o.getDisplayName())
);
if (!longest_match) {
return null;
}
if ((mention[longest_match.length] || '').match(/[A-Za-zäëïöüâêîôûáéíóúàèìòùÄËÏÖÜÂÊÎÔÛÁÉÍÓÚÀÈÌÒÙ]/i)) {
// avoid false positives, i.e. mentions that have
// further alphabetical characters than our longest
// match.
return null;
}
const occupant = this.occupants.findOccupant({'nick': longest_match}) ||
this.occupants.findOccupant({'jid': longest_match});
if (!occupant) {
return null;
}
const obj = {
'begin': index,
'end': index + longest_match.length,
'value': longest_match,
'type': 'mention'
};
if (occupant.get('jid')) {
obj.uri = `xmpp:${occupant.get('jid')}`
}
return obj;
},
extractReference (text, index) {
for (let i=index; i<text.length; i++) {
if (text[i] !== '@') {
continue
} else {
const match = text.slice(i+1),
ref = this.getReferenceForMention(match, i);
if (ref) {
return [text.slice(0, i) + match, ref, i]
}
}
}
return;
},
parseTextForReferences (text) {
const refs = [];
let index = 0;
while (index < (text || '').length) {
const result = this.extractReference(text, index);
if (result) {
text = result[0]; // @ gets filtered out
refs.push(result[1]);
index = result[2];
} else {
break;
}
}
return [text, refs];
},
getOutgoingMessageAttributes (text, spoiler_hint) {
const is_spoiler = this.get('composing_spoiler');
var references;
[text, references] = this.parseTextForReferences(text);
return {
'nick': this.get('nick'),
'from': `${this.get('jid')}/${this.get('nick')}`,
'fullname': this.get('nick'),
'is_spoiler': is_spoiler,
'message': text ? u.httpToGeoUri(emojione.shortnameToUnicode(text), _converse) : undefined,
'nick': this.get('nick'),
'references': references,
'sender': 'me',
'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
'type': 'groupchat'
@ -471,13 +537,11 @@
* A promise which resolves once the list has been
* retrieved.
*/
return new Promise((resolve, reject) => {
affiliation = affiliation || 'member';
const iq = $iq({to: this.get('jid'), type: "get"})
.c("query", {xmlns: Strophe.NS.MUC_ADMIN})
.c("item", {'affiliation': affiliation});
_converse.connection.sendIQ(iq, resolve, reject);
});
affiliation = affiliation || 'member';
const iq = $iq({to: this.get('jid'), type: "get"})
.c("query", {xmlns: Strophe.NS.MUC_ADMIN})
.c("item", {'affiliation': affiliation});
return _converse.api.sendIQ(iq);
},
setAffiliation (affiliation, members) {
@ -678,16 +742,14 @@
if (_.isString(affiliations)) {
affiliations = [affiliations];
}
return new Promise((resolve, reject) => {
const promises = _.map(
affiliations,
_.partial(this.requestMemberList.bind(this))
);
Promise.all(promises).then(
_.flow(u.marshallAffiliationIQs, resolve),
_.flow(u.marshallAffiliationIQs, resolve)
);
});
const promises = _.map(
affiliations,
_.partial(this.requestMemberList.bind(this))
);
return Promise.all(promises).then(
(iq) => u.marshallAffiliationIQs(iq),
(iq) => u.marshallAffiliationIQs(iq)
);
},
updateMemberLists (members, affiliations, deltaFunc) {
@ -864,8 +926,7 @@
if (sender === '') {
return;
}
this.incrementUnreadMsgCounter(original_stanza);
this.createMessage(stanza, original_stanza);
this.incrementUnreadMsgCounter(this.createMessage(stanza, original_stanza));
}
if (sender !== this.get('nick')) {
// We only emit an event if it's not our own message
@ -944,23 +1005,28 @@
* Parameters:
* (String): The text message
*/
return (new RegExp(`\\b${this.get('nick')}\\b`)).test(message);
const nick = this.get('nick');
if (message.get('references').length) {
const mentions = message.get('references').filter(ref => (ref.type === 'mention')).map(ref => ref.value);
return _.includes(mentions, nick);
} else {
return (new RegExp(`\\b${nick}\\b`)).test(message.get('message'));
}
},
incrementUnreadMsgCounter (stanza) {
incrementUnreadMsgCounter (message) {
/* Given a newly received message, update the unread counter if
* necessary.
*
* Parameters:
* (XMLElement): The <messsage> stanza
*/
const body = stanza.querySelector('body');
if (_.isNull(body)) {
return; // The message has no text
}
if (u.isNewMessage(stanza) && this.isHidden()) {
if (!message) { return; }
const body = message.get('message');
if (_.isNil(body)) { return; }
if (u.isNewMessage(message) && this.isHidden()) {
const settings = {'num_unread_general': this.get('num_unread_general') + 1};
if (this.isUserMentioned(body.textContent)) {
if (this.isUserMentioned(message)) {
settings.num_unread = this.get('num_unread') + 1;
_converse.incrementMsgCounter();
}
@ -1032,26 +1098,29 @@
},
fetchMembers () {
const old_jids = _.uniq(_.concat(
_.map(this.where({'affiliation': 'admin'}), (item) => item.get('jid')),
_.map(this.where({'affiliation': 'member'}), (item) => item.get('jid')),
_.map(this.where({'affiliation': 'owner'}), (item) => item.get('jid'))
));
this.chatroom.getJidsWithAffiliations(['member', 'owner', 'admin'])
.then((jids) => {
_.each(_.difference(old_jids, jids), (removed_jid) => {
// Remove absent occupants who've been removed from
// the members lists.
if (removed_jid === _converse.bare_jid) { return; }
const occupant = this.findOccupant({'jid': removed_jid});
if (!occupant) { return; }
.then((new_members) => {
const new_jids = new_members.map(m => m.jid).filter(m => !_.isUndefined(m)),
new_nicks = new_members.map(m => !m.jid && m.nick || undefined).filter(m => !_.isUndefined(m)),
removed_members = this.filter(m => {
return f.includes(m.get('affiliation'), ['admin', 'member', 'owner']) &&
!f.includes(m.get('nick'), new_nicks) &&
!f.includes(m.get('jid'), new_jids);
});
_.each(removed_members, (occupant) => {
if (occupant.get('jid') === _converse.bare_jid) { return; }
if (occupant.get('show') === 'offline') {
occupant.destroy();
}
});
_.each(jids, (attrs) => {
const occupant = this.findOccupant({'jid': attrs.jid});
_.each(new_members, (attrs) => {
let occupant;
if (attrs.jid) {
occupant = this.findOccupant({'jid': attrs.jid});
} else {
occupant = this.findOccupant({'nick': attrs.nick});
}
if (occupant) {
occupant.save(attrs);
} else {

View File

@ -97,7 +97,7 @@
.c('value').t(push_app_server.secret);
}
_converse.api.sendIQ(stanza)
.then(() => _converse.session.set('push_enabled', true))
.then(() => _converse.session.save('push_enabled', true))
.catch((e) => {
_converse.log(`Could not enable push app server for ${push_app_server.jid}`, Strophe.LogLevel.ERROR);
_converse.log(e, Strophe.LogLevel.ERROR);

View File

@ -7,6 +7,7 @@ if (typeof define !== 'undefined') {
* --------------------
* Any of the following components may be removed if they're not needed.
*/
"converse-autocomplete",
"converse-bookmarks", // XEP-0048 Bookmarks
"converse-caps", // XEP-0115 Entity Capabilities
"converse-chatview", // Renders standalone chat boxes for single user chat

View File

@ -159,9 +159,10 @@
xhr.onerror();
}
};
xhr.onerror = function () {
reject(xhr.statusText);
};
xhr.onerror = (e) => {
const err_message = e ? ` Error: ${e.message}` : '';
reject(new Error(`Could not fetch translations. Status: ${xhr.statusText}. ${err_message}`));
}
xhr.send();
});
}

View File

@ -1,14 +1,4 @@
<div class="chat-area col">
<div class="chat-content {[ if (o.show_send_button) { ]}chat-content-sendbutton{[ } ]}"></div>
<div class="new-msgs-indicator hidden">▼ {{{ o.unread_msgs }}} ▼</div>
<form class="sendXMPPMessage">
{[ if (o.show_toolbar) { ]}
<ul class="chat-toolbar no-text-select"></ul>
{[ } ]}
<textarea type="text" class="chat-textarea {[ if (o.show_send_button) { ]}chat-textarea-send-button{[ } ]}"
placeholder="{{{o.label_message}}}"></textarea>
{[ if (o.show_send_button) { ]}
<button type="submit" class="pure-button send-button">{{{ o.label_send }}}</button>
{[ } ]}
</form>
<div class="message-form-container"/>
</div>

View File

@ -6,14 +6,20 @@
{[ } ]}
<input type="text" placeholder="{{o.label_spoiler_hint}}" value="{{ o.hint_value }}"
class="{[ if (!o.composing_spoiler) { ]} hidden {[ } ]} spoiler-hint"/>
<textarea
type="text"
class="chat-textarea
{[ if (o.show_send_button) { ]} chat-textarea-send-button {[ } ]}
{[ if (o.composing_spoiler) { ]} spoiler {[ } ]}"
placeholder="{{{o.label_personal_message}}}">{{ o.message_value }}</textarea>
{[ if (o.show_send_button) { ]}
<button type="submit" class="pure-button send-button">{{{ o.label_send }}}</button>
{[ } ]}
<div class="suggestion-box">
<ul class="suggestion-box__results suggestion-box__results--above" hidden></ul>
<textarea
type="text"
class="chat-textarea suggestion-box__input
{[ if (o.show_send_button) { ]} chat-textarea-send-button {[ } ]}
{[ if (o.composing_spoiler) { ]} spoiler {[ } ]}"
placeholder="{{{o.label_message}}}">{{ o.message_value }}</textarea>
<span class="suggestion-box__additions visually-hidden" role="status" aria-live="assertive" aria-relevant="additions"></span>
{[ if (o.show_send_button) { ]}
<button type="submit" class="pure-button send-button">{{{ o.label_send }}}</button>
{[ } ]}
</div>
</form>
</div>

View File

@ -1,12 +0,0 @@
{[ if (o.use_emoji) { ]}
<li class="toggle-toolbar-menu toggle-smiley dropup">
<a class="toggle-smiley fa fa-smile-o" title="{{{o.label_insert_smiley}}}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></a>
<div class="emoji-picker dropdown-menu toolbar-menu"></div>
</li>
{[ } ]}
{[ if (o.show_call_button) { ]}
<li class="toggle-call fa fa-phone" title="{{{o.label_start_call}}}"></li>
{[ } ]}
{[ if (o.show_occupants_toggle) { ]}
<li class="toggle-occupants fa fa-angle-double-right" title="{{{o.label_hide_occupants}}}"></li>
{[ } ]}

View File

@ -7,3 +7,6 @@
{[ if (o.show_call_button) { ]}
<li class="toggle-call fa fa-phone" title="{{{o.label_start_call}}}"></li>
{[ } ]}
{[ if (o.show_occupants_toggle) { ]}
<li class="toggle-occupants fa fa-angle-double-right" title="{{{o.label_hide_occupants}}}"></li>
{[ } ]}

View File

@ -98,6 +98,21 @@
var u = {};
u.getLongestSubstring = function (string, candidates) {
function reducer (accumulator, current_value) {
if (string.startsWith(current_value)) {
if (current_value.length > accumulator.length) {
return current_value;
} else {
return accumulator;
}
} else {
return accumulator;
}
}
return candidates.reduce(reducer, '');
}
u.getNextElement = function (el, selector='*') {
let next_el = el.nextElementSibling;
while (!_.isNull(next_el) && !sizzle.matchesSelector(next_el, selector)) {
@ -214,6 +229,38 @@
return encodeURI(decodeURI(url)).replace(/[!'()]/g, escape).replace(/\*/g, "%2A");
};
u.prefixMentions = function (message) {
/* Given a message object, return its text with @ chars
* inserted before the mentioned nicknames.
*/
let text = message.get('message');
(message.get('references') || [])
.sort((a, b) => b.begin - a.begin)
.forEach(ref => {
text = `${text.slice(0, ref.begin)}@${text.slice(ref.begin)}`
});
return text;
};
u.addMentionsMarkup = function (text, references, chatbox) {
if (chatbox.get('message_type') !== 'groupchat') {
return text;
}
const nick = chatbox.get('nick');
references
.sort((a, b) => b.begin - a.begin)
.forEach(ref => {
const mention = text.slice(ref.begin, ref.end)
chatbox;
if (mention === nick) {
text = text.slice(0, ref.begin) + `<span class="mention mention--self badge badge-info">${mention}</span>` + text.slice(ref.end);
} else {
text = text.slice(0, ref.begin) + `<span class="mention">${mention}</span>` + text.slice(ref.end);
}
});
return text;
};
u.addHyperlinks = function (text) {
return URI.withinString(text, function (url) {
var uri = new URI(url);
@ -808,7 +855,26 @@
} else {
model.set(attributes);
}
}
};
u.siblingIndex = function (el) {
/* eslint-disable no-cond-assign */
for (var i = 0; el = el.previousElementSibling; i++);
return i;
};
u.getCurrentWord = function (input) {
const cursor = input.selectionEnd || undefined;
return _.last(input.value.slice(0, cursor).split(' '));
};
u.replaceCurrentWord = function (input, new_value) {
const cursor = input.selectionEnd || undefined,
current_word = _.last(input.value.slice(0, cursor).split(' ')),
value = input.value;
input.value = value.slice(0, cursor - current_word.length) + `${new_value} ` + value.slice(cursor);
input.selectionEnd = cursor - current_word.length + new_value.length + 1;
};
u.isVisible = function (el) {
if (u.hasClass('hidden', el)) {
@ -892,6 +958,19 @@
return Math.floor(Math.random() * Math.floor(max));
};
u.putCurserAtEnd = function (textarea) {
if (textarea !== document.activeElement) {
textarea.focus();
}
// Double the length because Opera is inconsistent about whether a carriage return is one character or two.
const len = textarea.value.length * 2;
// Timeout seems to be required for Blink
setTimeout(() => textarea.setSelectionRange(len, len), 1);
// Scroll to the bottom, in case we're in a tall textarea
// (Necessary for Firefox and Chrome)
this.scrollTop = 999999;
};
u.getUniqueId = function () {
return 'xxxxxxxx-xxxx'.replace(/[x]/g, function(c) {
var r = Math.random() * 16 | 0,

View File

@ -202,6 +202,7 @@ var specs = [
"spec/user-details-modal",
"spec/messages",
"spec/chatroom",
"spec/autocomplete",
"spec/minchats",
"spec/notification",
"spec/login",

View File

@ -103,18 +103,18 @@
return utils.waitUntil(() => _converse.chatboxviews.get(jid));
};
utils.openChatRoomViaModal = function (_converse, jid, nick) {
utils.openChatRoomViaModal = function (_converse, jid, nick='') {
// Opens a new chatroom
return new Promise(function (resolve, reject) {
utils.openControlBox(_converse);
var roomspanel = _converse.chatboxviews.get('controlbox').roomspanel;
const roomspanel = _converse.chatboxviews.get('controlbox').roomspanel;
roomspanel.el.querySelector('.show-add-muc-modal').click();
utils.closeControlBox(_converse);
const modal = roomspanel.add_room_modal;
utils.waitUntil(function () {
return u.isVisible(modal.el);
}, 1000).then(function () {
utils.waitUntil(() => u.isVisible(modal.el), 1000)
.then(() => {
modal.el.querySelector('input[name="chatroom"]').value = jid;
modal.el.querySelector('input[name="nickname"]').value = nick;
modal.el.querySelector('form input[type="submit"]').click();
resolve();
}).catch(_.partial(console.error, _));
@ -172,9 +172,9 @@
id: 'DC352437-C019-40EC-B590-AF29E879AF97'
}).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
.c('item').attrs({
affiliation: 'member',
affiliation: 'owner',
jid: _converse.bare_jid,
role: 'participant'
role: 'moderator'
}).up()
.c('status').attrs({code:'110'});
_converse.connection._dataRecv(utils.createRequest(presence));