Merge branch 'master' into converse-omemo
This commit is contained in:
commit
b4110dc162
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
126
css/converse.css
126
css/converse.css
|
@ -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
1144
dist/converse.js
vendored
File diff suppressed because it is too large
Load Diff
12
docs/source/api/index.rst
Normal file
12
docs/source/api/index.rst
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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%);
|
||||
|
|
|
@ -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
175
spec/autocomplete.js
Normal 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, _));
|
||||
}));
|
||||
});
|
||||
}));
|
|
@ -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();
|
||||
});
|
||||
}));
|
||||
|
|
385
spec/chatroom.js
385
spec/chatroom.js
|
@ -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);
|
||||
|
|
1191
spec/messages.js
1191
spec/messages.js
File diff suppressed because it is too large
Load Diff
|
@ -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",
|
||||
|
|
|
@ -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));
|
||||
}));
|
||||
});
|
||||
}));
|
||||
|
|
415
src/converse-autocomplete.js
Normal file
415
src/converse-autocomplete.js
Normal 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;
|
||||
}
|
||||
});
|
||||
}));
|
|
@ -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});
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
{[ } ]}
|
|
@ -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>
|
||||
{[ } ]}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -202,6 +202,7 @@ var specs = [
|
|||
"spec/user-details-modal",
|
||||
"spec/messages",
|
||||
"spec/chatroom",
|
||||
"spec/autocomplete",
|
||||
"spec/minchats",
|
||||
"spec/notification",
|
||||
"spec/login",
|
||||
|
|
|
@ -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));
|
||||
|
|
Loading…
Reference in New Issue
Block a user