Commit b4110dc1 authored by JC Brand's avatar JC Brand

Merge branch 'master' into converse-omemo

parents 2884549b 299fa4f3
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
"rules": { "rules": {
"lodash/prefer-lodash-method": [2, { "lodash/prefer-lodash-method": [2, {
"ignoreMethods": [ "ignoreMethods": [
"find", "endsWith", "startsWith", "filter", "reduce", "find", "endsWith", "startsWith", "filter", "reduce", "isArray", "create",
"map", "replace", "toLower", "split", "trim", "forEach", "toUpperCase", "includes" "map", "replace", "toLower", "split", "trim", "forEach", "toUpperCase", "includes"
] ]
}], }],
...@@ -216,10 +216,7 @@ ...@@ -216,10 +216,7 @@
"one-var": "off", "one-var": "off",
"one-var-declaration-per-line": "off", "one-var-declaration-per-line": "off",
"operator-assignment": "off", "operator-assignment": "off",
"operator-linebreak": [ "operator-linebreak": "off",
"error",
"after"
],
"padded-blocks": "off", "padded-blocks": "off",
"prefer-arrow-callback": "off", "prefer-arrow-callback": "off",
"prefer-const": "error", "prefer-const": "error",
......
...@@ -21,8 +21,9 @@ ...@@ -21,8 +21,9 @@
- Add a checkbox to indicate whether a trusted device is being used or not. - 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 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. 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 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 ### Bugfixes
......
This diff is collapsed.
This diff is collapsed.
.. 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 @@ ...@@ -12,11 +12,6 @@
<script src="dist/converse.js"></script> <script src="dist/converse.js"></script>
</head> </head>
<body class="reset"> <body class="reset">
<div class="content">
<div class="inner-content">
<h1 class="brand-heading"><i class="icon-conversejs"></i> Converse</h1>
</div>
</div>
<script> <script>
/* /*
@licstart @licstart
......
...@@ -283,6 +283,10 @@ ...@@ -283,6 +283,10 @@
</div> </div>
</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>
<div class="new-msgs-indicator">▼ You have unread messages ▼</div> <div class="new-msgs-indicator">▼ You have unread messages ▼</div>
<form class="sendXMPPMessage"> <form class="sendXMPPMessage">
......
...@@ -7,13 +7,14 @@ ...@@ -7,13 +7,14 @@
} }
.form-group { .form-group {
.suggestion-box,
.awesomplete { .awesomplete {
width: 100%; width: 100%;
} }
} }
div.awesomplete { .suggestion-box,
display: inline-block; .awesomplete {
position: relative; position: relative;
mark { mark {
background: $lightest-red; background: $lightest-red;
...@@ -23,6 +24,7 @@ ...@@ -23,6 +24,7 @@
display: block; display: block;
} }
.suggestion-box__results,
> ul { > ul {
&:before { &:before {
content: ""; content: "";
...@@ -30,18 +32,19 @@ ...@@ -30,18 +32,19 @@
top: -.43em; top: -.43em;
left: 1em; left: 1em;
width: 0; height: 0; width: 0; height: 0;
padding: .4em;
background: white; background: white;
border: inherit; border: inherit;
border-right: 0; border-right: 0;
border-bottom: 0; border-bottom: 0;
-webkit-transform: rotate(45deg); -webkit-transform: rotate(45deg);
transform: rotate(45deg); transform: rotate(45deg);
z-index: 1;
} }
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
z-index: 1; z-index: 2;
min-width: 100%; min-width: 100%;
box-sizing: border-box; box-sizing: border-box;
list-style: none; list-style: none;
...@@ -49,9 +52,9 @@ ...@@ -49,9 +52,9 @@
border-radius: .3em; border-radius: .3em;
margin: .2em 0 0; margin: .2em 0 0;
background: hsla(0,0%,100%,.9); 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); 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; text-shadow: none;
> li { > li {
...@@ -62,19 +65,45 @@ ...@@ -62,19 +65,45 @@
padding: 1em; 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[hidden],
div.awesomplete > ul:empty { div.awesomplete > ul:empty {
display: none; display: none;
} }
@supports (transform: scale(0)) { @supports (transform: scale(0)) {
.suggestion-box > ul,
div.awesomplete > ul { div.awesomplete > ul {
transition: .3s cubic-bezier(.4,.2,.5,1.4); transition: .3s cubic-bezier(.4,.2,.5,1.4);
transform-origin: 1.43em -.43em; transform-origin: 1.43em -.43em;
} }
.suggestion-box > ul[hidden],
.suggestion-box > ul:empty,
div.awesomplete > ul[hidden], div.awesomplete > ul[hidden],
div.awesomplete > ul:empty { div.awesomplete > ul:empty {
opacity: 0; opacity: 0;
...@@ -84,23 +113,33 @@ ...@@ -84,23 +113,33 @@
} }
} }
div.awesomplete > ul > li:hover { .suggestion-box > ul > li[aria-selected="true"],
background: $red;
color: $inverse-link-color;
}
div.awesomplete > ul > li[aria-selected="true"] { div.awesomplete > ul > li[aria-selected="true"] {
background: hsl(205, 40%, 40%); background: $dark-red;
color: white; color: $inverse-link-color;
} }
.suggestion-box li:hover mark,
div.awesomplete li:hover mark { div.awesomplete li:hover mark {
background: $darkest-red; background: $lightest-red;
color: $inverse-link-color; color: $inverse-link-color;
} }
.suggestion-box li[aria-selected="true"] mark,
div.awesomplete li[aria-selected="true"] mark { div.awesomplete li[aria-selected="true"] mark {
background: hsl(86, 100%, 21%); background: $red;
color: inherit; 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 @@ ...@@ -237,11 +237,18 @@
width: 100%; width: 100%;
} }
.suggestion-box__results {
&:after {
display: none;
}
}
.spoiler-hint { .spoiler-hint {
width: 100%; width: 100%;
} }
.chat-textarea { .chat-textarea {
color: $chat-textarea-color;
border-top-left-radius: 0; border-top-left-radius: 0;
border-top-right-radius: 0; border-top-right-radius: 0;
@include border-bottom-radius($chatbox-border-radius); @include border-bottom-radius($chatbox-border-radius);
...@@ -250,9 +257,13 @@ ...@@ -250,9 +257,13 @@
border: none; border: none;
min-height: $chat-textarea-height; min-height: $chat-textarea-height;
margin-bottom: -4px; // Not clear why this is necessar :( margin-bottom: -4px; // Not clear why this is necessar :(
resize: none;
&.spoiler { &.spoiler {
height: 42px; height: 42px;
} }
&.correcting {
background-color: lighten($chat-head-color, 50%);
}
} }
.send-button { .send-button {
...@@ -271,7 +282,7 @@ ...@@ -271,7 +282,7 @@
margin: 0; margin: 0;
padding: 0.25em; padding: 0.25em;
display: block; display: block;
border-top: 8px solid $chat-head-color; border-top: 4px solid $chat-head-color;
background-color: white; background-color: white;
color: $chat-head-color; color: $chat-head-color;
.fa, .fa:hover { .fa, .fa:hover {
...@@ -437,6 +448,12 @@ ...@@ -437,6 +448,12 @@
/* ******************* Overlay and embedded styles *************************** */ /* ******************* Overlay and embedded styles *************************** */
#conversejs.converse-embedded {
.chat-textarea {
max-height: $fullpage-max-chat-textarea-height;
}
}
#conversejs.converse-embedded, #conversejs.converse-embedded,
#conversejs.converse-overlayed { #conversejs.converse-overlayed {
.chat-head { .chat-head {
......
...@@ -116,9 +116,6 @@ ...@@ -116,9 +116,6 @@
color: $chat-head-text-color; color: $chat-head-text-color;
} }
} }
.mentioned {
font-weight: bold;
}
.disconnect-container { .disconnect-container {
margin: 1em; margin: 1em;
width: 100%; width: 100%;
...@@ -263,7 +260,7 @@ ...@@ -263,7 +260,7 @@
.sendXMPPMessage { .sendXMPPMessage {
.chat-toolbar { .chat-toolbar {
background-color: white; background-color: white;
border-top: 8px solid $chatroom-head-color; border-top: 4px solid $chatroom-head-color;
color: $chatroom-head-color; color: $chatroom-head-color;
.fa, .fa:hover { .fa, .fa:hover {
color: $chatroom-head-color; color: $chatroom-head-color;
...@@ -271,6 +268,9 @@ ...@@ -271,6 +268,9 @@
} }
.chat-textarea { .chat-textarea {
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
&.correcting {
background-color: lighten($chatroom-head-color, 30%);
}
} }
.send-button { .send-button {
background-color: $chatroom-head-color; background-color: $chatroom-head-color;
...@@ -278,6 +278,7 @@ ...@@ -278,6 +278,7 @@
} }
.room-invite { .room-invite {
padding-bottom: 1em;
.invited-contact { .invited-contact {
margin: -1px 0 0 -1px; margin: -1px 0 0 -1px;
width: 100%; width: 100%;
......
#conversejs { #conversejs {
.message { .message {
.mention {
font-weight: bold;
}
.mention--self {
font-weight: normal;
}
&.date-separator { &.date-separator {
height: 2em; height: 2em;
margin: 0; margin: 0;
...@@ -80,7 +86,7 @@ ...@@ -80,7 +86,7 @@
} }
&.correcting { &.correcting {
&.groupchat { &.groupchat {
background-color: lighten($chatroom-head-color, 35%); background-color: lighten($chatroom-head-color, 30%);
} }
&:not(.groupchat) { &:not(.groupchat) {
background-color: lighten($chat-head-color, 50%); background-color: lighten($chat-head-color, 50%);
......
...@@ -56,6 +56,7 @@ $border-color: #CCC !default; ...@@ -56,6 +56,7 @@ $border-color: #CCC !default;
$icon-color: $blue !default; $icon-color: $blue !default;
$save-button-color: $green !default; $save-button-color: $green !default;
$chat-textarea-color: #666 !default;
$chat-textarea-height: 60px !default; $chat-textarea-height: 60px !default;
$send-button-height: 27px !default; $send-button-height: 27px !default;
...@@ -140,7 +141,7 @@ $legend-font-size: 16px !default; ...@@ -140,7 +141,7 @@ $legend-font-size: 16px !default;
$line-height-small: 14px !default; $line-height-small: 14px !default;
$line-height: 16px !default; $line-height: 16px !default;
$line-height-large: 20px !default; $line-height-large: 20px !default;
$line-height-huge: 24px !default; $line-height-huge: 27px !default;
$occupants-padding: 1em; $occupants-padding: 1em;
...@@ -148,7 +149,7 @@ $fullpage-chat-head-height: 62px !default; ...@@ -148,7 +149,7 @@ $fullpage-chat-head-height: 62px !default;
$fullpage-chat-height: 100vh; $fullpage-chat-height: 100vh;
$fullpage-chat-width: 100%; $fullpage-chat-width: 100%;
$fullpage-emoji-picker-height: 150px !default; $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-head-height: 55px !default;
$overlayed-chat-height: 450px !default; $overlayed-chat-height: 450px !default;
......
(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 @@ ...@@ -707,7 +707,7 @@
.then(function () { .then(function () {
var view = _converse.chatboxviews.get(sender_jid); var view = _converse.chatboxviews.get(sender_jid);
// Check that the notification appears inside the chatbox in the DOM // 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.length).toBe(1);
expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing'); expect(events[0].textContent).toEqual(mock.cur_names[1] + ' is typing');
...@@ -1491,7 +1491,7 @@ ...@@ -1491,7 +1491,7 @@
var chatbox = _converse.chatboxes.get(sender_jid); var chatbox = _converse.chatboxes.get(sender_jid);
var chatboxview = _converse.chatboxviews.get(sender_jid); var chatboxview = _converse.chatboxviews.get(sender_jid);
var msgsIndicatorSelector = 'a.open-chat:contains("' + chatbox.get('fullname') + '") .msgs-indicator'; 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 () { var msgFactory = function () {
return test_utils.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read'); return test_utils.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read');
}; };
...@@ -1527,7 +1527,7 @@ ...@@ -1527,7 +1527,7 @@
return test_utils.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read'); 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', 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); chatbox.save('scrolled', true);
...@@ -1559,7 +1559,7 @@ ...@@ -1559,7 +1559,7 @@
return test_utils.createChatMessage(_converse, sender_jid, 'This message will be received as unread, but eventually will be read'); 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', 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); chatbox.save('scrolled', true);
...@@ -1591,7 +1591,7 @@ ...@@ -1591,7 +1591,7 @@
}; };
const selectUnreadMsgCount = function () { const selectUnreadMsgCount = function () {
const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid); 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); const chatbox = _converse.chatboxes.get(sender_jid);
...@@ -1601,9 +1601,9 @@ ...@@ -1601,9 +1601,9 @@
const chatboxview = _converse.chatboxviews.get(sender_jid); const chatboxview = _converse.chatboxviews.get(sender_jid);
chatboxview.minimize(); chatboxview.minimize();
const $unreadMsgCount = selectUnreadMsgCount(); const unread_count = selectUnreadMsgCount();
expect(u.isVisible($unreadMsgCount[0])).toBeTruthy(); expect(u.isVisible(unread_count)).toBeTruthy();
expect($unreadMsgCount.html()).toBe('1'); expect(unread_count.innerHTML).toBe('1');
done(); done();
}); });
})); }));
...@@ -1625,7 +1625,7 @@ ...@@ -1625,7 +1625,7 @@
}; };
const selectUnreadMsgCount = function () { const selectUnreadMsgCount = function () {
const minimizedChatBoxView = _converse.minimized_chats.get(sender_jid); 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); const chatboxview = _converse.chatboxviews.get(sender_jid);
...@@ -1633,9 +1633,9 @@ ...@@ -1633,9 +1633,9 @@
_converse.chatboxes.onMessage(msgFactory()); _converse.chatboxes.onMessage(msgFactory());
const $unreadMsgCount = selectUnreadMsgCount(); const unread_count = selectUnreadMsgCount();
expect(u.isVisible($unreadMsgCount[0])).toBeTruthy(); expect(u.isVisible(unread_count)).toBeTruthy();
expect($unreadMsgCount.html()).toBe('1'); expect(unread_count.innerHTML).toBe('1');
done(); done();
}); });
})); }));
......
This diff is collapsed.
This diff is collapsed.
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
define(["jquery", "jasmine", "mock", "test-utils"], factory); define(["jquery", "jasmine", "mock", "test-utils"], factory);
} (this, function ($, jasmine, mock, test_utils) { } (this, function ($, jasmine, mock, test_utils) {
"use strict"; "use strict";
var _ = converse.env._; const Strophe = converse.env.Strophe,
var $msg = converse.env.$msg; _ = converse.env._,
$msg = converse.env.$msg;
describe("Notifications", function () { describe("Notifications", function () {
// Implement the protocol defined in https://xmpp.org/extensions/xep-0313.html#config // Implement the protocol defined in https://xmpp.org/extensions/xep-0313.html#config
...@@ -74,7 +75,7 @@ ...@@ -74,7 +75,7 @@
delete window.Notification; delete window.Notification;
} }
done(); done();
}); }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
})); }));
it("is shown for headline messages", it("is shown for headline messages",
......
...@@ -224,69 +224,64 @@ ...@@ -224,69 +224,64 @@
})); }));
it("shows unread messages directed at the user", mock.initConverseWithAsync( it("shows unread messages directed at the user", mock.initConverseWithAsync(
{ whitelisted_plugins: ['converse-roomslist'], { whitelisted_plugins: ['converse-roomslist'],
allow_bookmarks: false // Makes testing easier, otherwise we allow_bookmarks: false // Makes testing easier, otherwise we
// have to mock stanza traffic. // have to mock stanza traffic.
}, function (done, _converse) { }, function (done, _converse) {
test_utils.waitUntil(function () { test_utils.waitUntil(() => !_.isUndefined(_converse.rooms_list_view), 500)
return !_.isUndefined(_converse.rooms_list_view) .then(() => test_utils.openAndEnterChatRoom(_converse, 'kitchen', 'conference.shakespeare.lit', 'romeo'))
}, 500) .then(() => {
.then(function () { const room_jid = 'kitchen@conference.shakespeare.lit';
var room_jid = 'kitchen@conference.shakespeare.lit'; const view = _converse.chatboxviews.get(room_jid);
test_utils.openAndEnterChatRoom( view.model.set({'minimized': true});
_converse, 'kitchen', 'conference.shakespeare.lit', 'romeo').then(function () { const contact_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost';
const nick = mock.chatroom_names[0];
var view = _converse.chatboxviews.get(room_jid); view.model.onMessage(
view.model.set({'minimized': true}); $msg({
var contact_jid = mock.cur_names[5].replace(/ /g,'.').toLowerCase() + '@localhost'; from: room_jid+'/'+nick,
var nick = mock.chatroom_names[0]; id: (new Date()).getTime(),
view.model.onMessage( to: 'dummy@localhost',
$msg({ type: 'groupchat'
from: room_jid+'/'+nick, }).c('body').t('foo').tree());
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 // 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( var room_el = _converse.rooms_list_view.el.querySelector(
".available-chatroom" ".available-chatroom"
); );
expect(_.includes(room_el.classList, 'unread-msgs')); expect(_.includes(room_el.classList, 'unread-msgs'));
// If the user is mentioned, the counter also gets updated // If the user is mentioned, the counter also gets updated
view.model.onMessage( view.model.onMessage(
$msg({ $msg({
from: room_jid+'/'+nick, from: room_jid+'/'+nick,
id: (new Date()).getTime(), id: (new Date()).getTime(),
to: 'dummy@localhost', to: 'dummy@localhost',
type: 'groupchat' type: 'groupchat'
}).c('body').t('romeo: Your attention is required').tree() }).c('body').t('romeo: Your attention is required').tree()
); );
var indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator"); var indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
expect(indicator_el.textContent).toBe('1'); expect(indicator_el.textContent).toBe('1');
view.model.onMessage( view.model.onMessage(
$msg({ $msg({
from: room_jid+'/'+nick, from: room_jid+'/'+nick,
id: (new Date()).getTime(), id: (new Date()).getTime(),
to: 'dummy@localhost', to: 'dummy@localhost',
type: 'groupchat' type: 'groupchat'
}).c('body').t('romeo: and another thing...').tree() }).c('body').t('romeo: and another thing...').tree()
); );
indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator"); indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
expect(indicator_el.textContent).toBe('2'); expect(indicator_el.textContent).toBe('2');
// When the chat gets maximized again, the unread indicators are removed // When the chat gets maximized again, the unread indicators are removed
view.model.set({'minimized': false}); view.model.set({'minimized': false});
indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator"); indicator_el = _converse.rooms_list_view.el.querySelector(".msgs-indicator");
expect(_.isNull(indicator_el)); expect(_.isNull(indicator_el));
room_el = _converse.rooms_list_view.el.querySelector(".available-chatroom"); room_el = _converse.rooms_list_view.el.querySelector(".available-chatroom");
expect(_.includes(room_el.classList, 'unread-msgs')).toBeFalsy(); expect(_.includes(room_el.classList, 'unread-msgs')).toBeFalsy();
done(); done();
}); }).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
});
})); }));
}); });
})); }));
This diff is collapsed.
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
const u = converse.env.utils; const u = converse.env.utils;
Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0'); Strophe.addNamespace('MESSAGE_CORRECT', 'urn:xmpp:message-correct:0');
Strophe.addNamespace('REFERENCE', 'urn:xmpp:reference:0');
converse.plugins.add('converse-chatboxes', { converse.plugins.add('converse-chatboxes', {
...@@ -225,7 +226,7 @@ ...@@ -225,7 +226,7 @@
}); });
}; };
xhr.open('PUT', this.get('put'), true); 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')); xhr.send(this.get('file'));
} }
}); });
...@@ -298,6 +299,7 @@ ...@@ -298,6 +299,7 @@
older_versions.push(message.get('message')); older_versions.push(message.get('message'));
message.save({ message.save({
'message': _converse.chatboxes.getMessageBody(stanza), 'message': _converse.chatboxes.getMessageBody(stanza),
'references': this.getReferencesFromStanza(stanza),
'older_versions': older_versions, 'older_versions': older_versions,
'edited': true 'edited': true
}); });
...@@ -323,11 +325,23 @@ ...@@ -323,11 +325,23 @@
if (message.get('is_spoiler')) { if (message.get('is_spoiler')) {
if (message.get('spoiler_hint')) { 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 { } 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')) { if (message.get('file')) {
stanza.c('x', {'xmlns': Strophe.NS.OUTOFBAND}).c('url').t(message.get('message')).up(); stanza.c('x', {'xmlns': Strophe.NS.OUTOFBAND}).c('url').t(message.get('message')).up();
} }
...@@ -384,10 +398,11 @@ ...@@ -384,10 +398,11 @@
const older_versions = message.get('older_versions') || []; const older_versions = message.get('older_versions') || [];
older_versions.push(message.get('message')); older_versions.push(message.get('message'));
message.save({ message.save({
'correcting': false,
'edited': true,
'message': attrs.message, 'message': attrs.message,
'older_versions': older_versions, 'older_versions': older_versions,
'edited': true, 'references': attrs.references
'correcting': false
}); });
} else { } else {
message = this.messages.create(attrs); message = this.messages.create(attrs);
...@@ -444,6 +459,21 @@ ...@@ -444,6 +459,21 @@
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); }).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) { getMessageAttributesFromStanza (stanza, original_stanza) {
/* Parses a passed in message stanza and returns an object /* Parses a passed in message stanza and returns an object
* of attributes. * of attributes.
...@@ -467,12 +497,15 @@ ...@@ -467,12 +497,15 @@
stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE || stanza.getElementsByTagName(_converse.ACTIVE).length && _converse.ACTIVE ||
stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE; stanza.getElementsByTagName(_converse.GONE).length && _converse.GONE;
const attrs = { const attrs = {
'chat_state': chat_state, 'chat_state': chat_state,
'is_archived': !_.isNil(archive), 'is_archived': !_.isNil(archive),
'is_delayed': !_.isNil(delay), 'is_delayed': !_.isNil(delay),
'is_spoiler': !_.isNil(spoiler), 'is_spoiler': !_.isNil(spoiler),
'message': _converse.chatboxes.getMessageBody(stanza) || undefined, 'message': _converse.chatboxes.getMessageBody(stanza) || undefined,
'references': this.getReferencesFromStanza(stanza),
'msgid': stanza.getAttribute('id'), 'msgid': stanza.getAttribute('id'),
'time': delay ? delay.getAttribute('stamp') : moment().format(), 'time': delay ? delay.getAttribute('stamp') : moment().format(),
'type': stanza.getAttribute('type') 'type': stanza.getAttribute('type')
...@@ -533,14 +566,13 @@ ...@@ -533,14 +566,13 @@
_converse.windowState === 'hidden'; _converse.windowState === 'hidden';
}, },
incrementUnreadMsgCounter (stanza) { incrementUnreadMsgCounter (message) {
/* Given a newly received message, update the unread counter if /* Given a newly received message, update the unread counter if
* necessary. * necessary.
*/ */
if (_.isNull(stanza.querySelector('body'))) { if (!message) { return; }
return; // The message has no text if (_.isNil(message.get('message'))) { return; }
} if (utils.isNewMessage(message) && this.isHidden()) {
if (utils.isNewMessage(stanza) && this.isHidden()) {
this.save({'num_unread': this.get('num_unread') + 1}); this.save({'num_unread': this.get('num_unread') + 1});
_converse.incrementMsgCounter(); _converse.incrementMsgCounter();
} }
...@@ -633,8 +665,7 @@ ...@@ -633,8 +665,7 @@
* Parameters: * Parameters:
* (XMLElement) stanza - The incoming message stanza * (XMLElement) stanza - The incoming message stanza
*/ */
let from_jid = stanza.getAttribute('from'), let to_jid = stanza.getAttribute('to');
to_jid = stanza.getAttribute('to');
const to_resource = Strophe.getResourceFromJid(to_jid); const to_resource = Strophe.getResourceFromJid(to_jid);
if (_converse.filter_by_resource && (to_resource && to_resource !== _converse.resource)) { if (_converse.filter_by_resource && (to_resource && to_resource !== _converse.resource)) {
...@@ -648,12 +679,13 @@ ...@@ -648,12 +679,13 @@
// messages, but Prosody sends headline messages with the // messages, but Prosody sends headline messages with the
// wrong type ('chat'), so we need to filter them out here. // wrong type ('chat'), so we need to filter them out here.
_converse.log( _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 Strophe.LogLevel.INFO
); );
return true; return true;
} }
let from_jid = stanza.getAttribute('from');
const forwarded = stanza.querySelector('forwarded'), const forwarded = stanza.querySelector('forwarded'),
original_stanza = stanza; original_stanza = stanza;
...@@ -679,6 +711,12 @@ ...@@ -679,6 +711,12 @@
let contact_jid; let contact_jid;
if (is_me) { if (is_me) {
// I am the sender, so this must be a forwarded message... // 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); contact_jid = Strophe.getBareJidFromJid(to_jid);
} else { } else {
contact_jid = from_bare_jid; contact_jid = from_bare_jid;
...@@ -691,10 +729,8 @@ ...@@ -691,10 +729,8 @@
if (chatbox && !chatbox.handleMessageCorrection(stanza)) { if (chatbox && !chatbox.handleMessageCorrection(stanza)) {
const msgid = stanza.getAttribute('id'), const msgid = stanza.getAttribute('id'),
message = msgid && chatbox.messages.findWhere({msgid}); message = msgid && chatbox.messages.findWhere({msgid});
if (!message) { if (!message) { // Only create the message when we're sure it's not a duplicate
// Only create the message when we're sure it's not a duplicate chatbox.incrementUnreadMsgCounter(chatbox.createMessage(stanza, original_stanza));
chatbox.incrementUnreadMsgCounter(original_stanza);
chatbox.createMessage(stanza, original_stanza);
} }
} }
_converse.emit('message', {'stanza': original_stanza, 'chatbox': chatbox}); _converse.emit('message', {'stanza': original_stanza, 'chatbox': chatbox});
......
...@@ -50,12 +50,6 @@ ...@@ -50,12 +50,6 @@
"use strict"; "use strict";
const { $msg, Backbone, Promise, Strophe, _, b64_sha1, f, sizzle, moment } = converse.env; const { $msg, Backbone, Promise, Strophe, _, b64_sha1, f, sizzle, moment } = converse.env;
const u = converse.env.utils; const u = converse.env.utils;
const KEY = {
ENTER: 13,
UP_ARROW: 38,
DOWN_ARROW: 40,
FORWARD_SLASH: 47
};
converse.plugins.add('converse-chatview', { converse.plugins.add('converse-chatview', {
/* Plugin dependencies are other plugins which might be /* Plugin dependencies are other plugins which might be
...@@ -396,13 +390,13 @@ ...@@ -396,13 +390,13 @@
if (this.model.get('composing_spoiler')) { if (this.model.get('composing_spoiler')) {
placeholder = __('Hidden message'); placeholder = __('Hidden message');
} else { } else {
placeholder = __('Personal message'); placeholder = __('Message');
} }
const form_container = this.el.querySelector('.message-form-container'); const form_container = this.el.querySelector('.message-form-container');
form_container.innerHTML = tpl_chatbox_message_form( form_container.innerHTML = tpl_chatbox_message_form(
_.extend(this.model.toJSON(), { _.extend(this.model.toJSON(), {
'hint_value': _.get(this.el.querySelector('.spoiler-hint'), 'value'), 'hint_value': _.get(this.el.querySelector('.spoiler-hint'), 'value'),
'label_personal_message': placeholder, 'label_message': placeholder,
'label_send': __('Send'), 'label_send': __('Send'),
'label_spoiler_hint': __('Optional hint'), 'label_spoiler_hint': __('Optional hint'),
'message_value': _.get(this.el.querySelector('.chat-textarea'), 'value'), 'message_value': _.get(this.el.querySelector('.chat-textarea'), 'value'),
...@@ -801,7 +795,7 @@ ...@@ -801,7 +795,7 @@
*/ */
this.showMessage(message); this.showMessage(message);
if (message.get('correcting')) { if (message.get('correcting')) {
this.insertIntoTextArea(message.get('message'), true); this.insertIntoTextArea(message.get('message'), true, true);
} }
_converse.emit('messageAdded', { _converse.emit('messageAdded', {
'message': message, 'message': message,
...@@ -898,6 +892,7 @@ ...@@ -898,6 +892,7 @@
hint_el.value = ''; hint_el.value = '';
} }
textarea.value = ''; textarea.value = '';
u.removeClass('correcting', textarea);
textarea.focus(); textarea.focus();
// Trigger input event, so that the textarea resizes // Trigger input event, so that the textarea resizes
const event = document.createEvent('Event'); const event = document.createEvent('Event');
...@@ -912,15 +907,34 @@ ...@@ -912,15 +907,34 @@
keyPressed (ev) { keyPressed (ev) {
/* Event handler for when a key is pressed in a chat box textarea. /* Event handler for when a key is pressed in a chat box textarea.
*/ */
if (ev.shiftKey) { return; } if (ev.ctrlKey) {
// When ctrl is pressed, no chars are entered into the textarea.
if (ev.keyCode === KEY.ENTER) { return;
this.onFormSubmitted(ev); }
} else if (ev.keyCode === KEY.UP_ARROW && !ev.target.selectionEnd) { if (!ev.shiftKey && !ev.altKey) {
this.editEarlierMessage(); if (ev.keyCode === _converse.keycodes.FORWARD_SLASH) {
} else if (ev.keyCode === KEY.DOWN_ARROW && ev.target.selectionEnd === ev.target.value.length) { // Forward slash is used to run commands. Nothing to do here.
this.editLaterMessage(); return;
} else if (ev.keyCode !== KEY.FORWARD_SLASH && this.model.get('chat_state') !== _converse.COMPOSING) { } 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 // Set chat state to composing if keyCode is not a forward-slash
// (which would imply an internal command and not a message). // (which would imply an internal command and not a message).
this.setChatState(_converse.COMPOSING); this.setChatState(_converse.COMPOSING);
...@@ -931,7 +945,19 @@ ...@@ -931,7 +945,19 @@
return f(this.model.messages.filter({'sender': 'me'})); 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) { onMessageEditButtonClicked (ev) {
ev.preventDefault();
const idx = this.model.messages.findLastIndex('correcting'), const idx = this.model.messages.findLastIndex('correcting'),
currently_correcting = idx >=0 ? this.model.messages.at(idx) : null, currently_correcting = idx >=0 ? this.model.messages.at(idx) : null,
message_el = u.ancestor(ev.target, '.chat-msg'), message_el = u.ancestor(ev.target, '.chat-msg'),
...@@ -942,10 +968,10 @@ ...@@ -942,10 +968,10 @@
currently_correcting.save('correcting', false); currently_correcting.save('correcting', false);
} }
message.save('correcting', true); message.save('correcting', true);
this.insertIntoTextArea(message.get('message'), true); this.insertIntoTextArea(u.prefixMentions(message), true, true);
} else { } else {
message.save('correcting', false); message.save('correcting', false);
this.insertIntoTextArea('', true); this.insertIntoTextArea('', true, false);
} }
}, },
...@@ -964,10 +990,10 @@ ...@@ -964,10 +990,10 @@
} }
} }
if (message) { if (message) {
this.insertIntoTextArea(message.get('message'), true); this.insertIntoTextArea(message.get('message'), true, true);
message.save('correcting', true); message.save('correcting', true);
} else { } else {
this.insertIntoTextArea('', true); this.insertIntoTextArea('', true, false);
} }
}, },
...@@ -987,7 +1013,7 @@ ...@@ -987,7 +1013,7 @@
} }
message = message || this.getOwnMessages().findLast((msg) => msg.get('message')); message = message || this.getOwnMessages().findLast((msg) => msg.get('message'));
if (message) { if (message) {
this.insertIntoTextArea(message.get('message'), true); this.insertIntoTextArea(message.get('message'), true, true);
message.save('correcting', true); message.save('correcting', true);
} }
}, },
...@@ -1008,18 +1034,25 @@ ...@@ -1008,18 +1034,25 @@
return this; return this;
}, },
insertIntoTextArea (value, replace=false) { insertIntoTextArea (value, replace=false, correcting=false) {
const textarea = this.el.querySelector('.chat-textarea'); const textarea = this.el.querySelector('.chat-textarea');
if (correcting) {
u.addClass('correcting', textarea);
} else {
u.removeClass('correcting', textarea);
}
if (replace) { if (replace) {
textarea.value = '';
textarea.value = value; textarea.value = value;
} else { } else {
let existing = textarea.value; let existing = textarea.value;
if (existing && (existing[existing.length-1] !== ' ')) { if (existing && (existing[existing.length-1] !== ' ')) {
existing = existing + ' '; existing = existing + ' ';
} }
textarea.value = '';
textarea.value = existing+value+' '; textarea.value = existing+value+' ';
} }
textarea.focus() u.putCurserAtEnd(textarea);
}, },
createEmojiPicker () { createEmojiPicker () {
......
...@@ -67,6 +67,7 @@ ...@@ -67,6 +67,7 @@
// Core plugins are whitelisted automatically // Core plugins are whitelisted automatically
_converse.core_plugins = [ _converse.core_plugins = [
'converse-autocomplete',
'converse-bookmarks', 'converse-bookmarks',
'converse-caps', 'converse-caps',
'converse-chatboxes', 'converse-chatboxes',
...@@ -107,6 +108,22 @@ ...@@ -107,6 +108,22 @@
// Make converse pluggable // Make converse pluggable
pluggable.enable(_converse, '_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 // Module-level constants
_converse.STATUS_WEIGHTS = { _converse.STATUS_WEIGHTS = {
'offline': 6, 'offline': 6,
...@@ -813,7 +830,7 @@ ...@@ -813,7 +830,7 @@
defaults () { defaults () {
return { return {
"jid": _converse.bare_jid, "jid": _converse.bare_jid,
"status": _converse.default_state, "status": _converse.default_state
} }
}, },
...@@ -1173,7 +1190,7 @@ ...@@ -1173,7 +1190,7 @@
_converse.locale, _converse.locale,
_converse.locales, _converse.locales,
u.interpolate(_converse.locales_url, {'locale': _converse.locale})) 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) .then(finishInitialization)
.catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL)); .catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
} }
......
...@@ -324,7 +324,7 @@ ...@@ -324,7 +324,7 @@
message_archiving_timeout: 8000, // Time (in milliseconds) to wait before aborting MAM request 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) { if (iq.querySelectorAll('feature-not-implemented').length) {
_converse.log( _converse.log(
"Message Archive Management (XEP-0313) not supported by this server", "Message Archive Management (XEP-0313) not supported by this server",
......
...@@ -168,6 +168,7 @@ ...@@ -168,6 +168,7 @@
text = xss.filterXSS(text, {'whiteList': {}}); text = xss.filterXSS(text, {'whiteList': {}});
msg_content.innerHTML = _.flow( msg_content.innerHTML = _.flow(
_.partial(u.geoUriToHttp, _, _converse.geouri_replacement), _.partial(u.geoUriToHttp, _, _converse.geouri_replacement),
_.partial(u.addMentionsMarkup, _, this.model.get('references'), this.model.collection.chatbox),
u.addHyperlinks, u.addHyperlinks,
u.renderNewLines, u.renderNewLines,
_.partial(u.addEmoji, _converse, emojione, _) _.partial(u.addEmoji, _converse, emojione, _)
...@@ -260,7 +261,7 @@ ...@@ -260,7 +261,7 @@
getExtraMessageClasses () { getExtraMessageClasses () {
let extra_classes = this.model.get('is_delayed') && 'delayed' || ''; let extra_classes = this.model.get('is_delayed') && 'delayed' || '';
if (this.model.get('type') === 'groupchat' && this.model.get('sender') === 'them') { 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 // Add special class to mark groupchat messages
// in which we are mentioned. // in which we are mentioned.
extra_classes += ' mentioned'; extra_classes += ' mentioned';
......
This diff is collapsed.
This diff is collapsed.
...@@ -97,7 +97,7 @@ ...@@ -97,7 +97,7 @@
.c('value').t(push_app_server.secret); .c('value').t(push_app_server.secret);
} }
_converse.api.sendIQ(stanza) _converse.api.sendIQ(stanza)
.then(() => _converse.session.set('push_enabled', true)) .then(() => _converse.session.save('push_enabled', true))
.catch((e) => { .catch((e) => {
_converse.log(`Could not enable push app server for ${push_app_server.jid}`, Strophe.LogLevel.ERROR); _converse.log(`Could not enable push app server for ${push_app_server.jid}`, Strophe.LogLevel.ERROR);
_converse.log(e, Strophe.LogLevel.ERROR); _converse.log(e, Strophe.LogLevel.ERROR);
......
...@@ -7,6 +7,7 @@ if (typeof define !== 'undefined') { ...@@ -7,6 +7,7 @@ if (typeof define !== 'undefined') {
* -------------------- * --------------------
* Any of the following components may be removed if they're not needed. * Any of the following components may be removed if they're not needed.
*/ */
"converse-autocomplete",
"converse-bookmarks", // XEP-0048 Bookmarks "converse-bookmarks", // XEP-0048 Bookmarks
"converse-caps", // XEP-0115 Entity Capabilities "converse-caps", // XEP-0115 Entity Capabilities
"converse-chatview", // Renders standalone chat boxes for single user chat "converse-chatview", // Renders standalone chat boxes for single user chat
......
...@@ -159,9 +159,10 @@ ...@@ -159,9 +159,10 @@
xhr.onerror(); xhr.onerror();
} }
}; };
xhr.onerror = function () { xhr.onerror = (e) => {
reject(xhr.statusText); const err_message = e ? ` Error: ${e.message}` : '';
}; reject(new Error(`Could not fetch translations. Status: ${xhr.statusText}. ${err_message}`));
}
xhr.send(); xhr.send();
}); });
} }
......
<div class="chat-area col"> <div class="chat-area col">
<div class="chat-content {[ if (o.show_send_button) { ]}chat-content-sendbutton{[ } ]}"></div> <div class="chat-content {[ if (o.show_send_button) { ]}chat-content-sendbutton{[ } ]}"></div>
<div class="new-msgs-indicator hidden">▼ {{{ o.unread_msgs }}} ▼</div> <div class="message-form-container"/>
<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> </div>
...@@ -6,14 +6,20 @@ ...@@ -6,14 +6,20 @@
{[ } ]} {[ } ]}
<input type="text" placeholder="{{o.label_spoiler_hint}}" value="{{ o.hint_value }}" <input type="text" placeholder="{{o.label_spoiler_hint}}" value="{{ o.hint_value }}"
class="{[ if (!o.composing_spoiler) { ]} hidden {[ } ]} spoiler-hint"/> class="{[ if (!o.composing_spoiler) { ]} hidden {[ } ]} spoiler-hint"/>
<textarea
type="text" <div class="suggestion-box">
class="chat-textarea <ul class="suggestion-box__results suggestion-box__results--above" hidden></ul>
{[ if (o.show_send_button) { ]} chat-textarea-send-button {[ } ]} <textarea
{[ if (o.composing_spoiler) { ]} spoiler {[ } ]}" type="text"
placeholder="{{{o.label_personal_message}}}">{{ o.message_value }}</textarea> class="chat-textarea suggestion-box__input
{[ if (o.show_send_button) { ]} {[ if (o.show_send_button) { ]} chat-textarea-send-button {[ } ]}
<button type="submit" class="pure-button send-button">{{{ o.label_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> </form>
</div> </div>
{[ 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 @@ ...@@ -7,3 +7,6 @@
{[ if (o.show_call_button) { ]} {[ if (o.show_call_button) { ]}
<li class="toggle-call fa fa-phone" title="{{{o.label_start_call}}}"></li> <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 @@ ...@@ -98,6 +98,21 @@
var u = {}; 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='*') { u.getNextElement = function (el, selector='*') {
let next_el = el.nextElementSibling; let next_el = el.nextElementSibling;
while (!_.isNull(next_el) && !sizzle.matchesSelector(next_el, selector)) { while (!_.isNull(next_el) && !sizzle.matchesSelector(next_el, selector)) {
...@@ -214,6 +229,38 @@ ...@@ -214,6 +229,38 @@
return encodeURI(decodeURI(url)).replace(/[!'()]/g, escape).replace(/\*/g, "%2A"); 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) { u.addHyperlinks = function (text) {
return URI.withinString(text, function (url) { return URI.withinString(text, function (url) {
var uri = new URI(url); var uri = new URI(url);
...@@ -808,7 +855,26 @@ ...@@ -808,7 +855,26 @@
} else { } else {
model.set(attributes); 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) { u.isVisible = function (el) {
if (u.hasClass('hidden', el)) { if (u.hasClass('hidden', el)) {
...@@ -892,6 +958,19 @@ ...@@ -892,6 +958,19 @@
return Math.floor(Math.random() * Math.floor(max)); 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 () { u.getUniqueId = function () {
return 'xxxxxxxx-xxxx'.replace(/[x]/g, function(c) { return 'xxxxxxxx-xxxx'.replace(/[x]/g, function(c) {
var r = Math.random() * 16 | 0, var r = Math.random() * 16 | 0,
......
...@@ -202,6 +202,7 @@ var specs = [ ...@@ -202,6 +202,7 @@ var specs = [
"spec/user-details-modal", "spec/user-details-modal",
"spec/messages", "spec/messages",
"spec/chatroom", "spec/chatroom",
"spec/autocomplete",
"spec/minchats", "spec/minchats",
"spec/notification", "spec/notification",
"spec/login", "spec/login",
......
...@@ -103,18 +103,18 @@ ...@@ -103,18 +103,18 @@
return utils.waitUntil(() => _converse.chatboxviews.get(jid)); return utils.waitUntil(() => _converse.chatboxviews.get(jid));
}; };
utils.openChatRoomViaModal = function (_converse, jid, nick) { utils.openChatRoomViaModal = function (_converse, jid, nick='') {
// Opens a new chatroom // Opens a new chatroom
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
utils.openControlBox(_converse); 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(); roomspanel.el.querySelector('.show-add-muc-modal').click();
utils.closeControlBox(_converse); utils.closeControlBox(_converse);
const modal = roomspanel.add_room_modal; const modal = roomspanel.add_room_modal;
utils.waitUntil(function () { utils.waitUntil(() => u.isVisible(modal.el), 1000)
return u.isVisible(modal.el); .then(() => {
}, 1000).then(function () {
modal.el.querySelector('input[name="chatroom"]').value = jid; modal.el.querySelector('input[name="chatroom"]').value = jid;
modal.el.querySelector('input[name="nickname"]').value = nick;
modal.el.querySelector('form input[type="submit"]').click(); modal.el.querySelector('form input[type="submit"]').click();
resolve(); resolve();
}).catch(_.partial(console.error, _)); }).catch(_.partial(console.error, _));
...@@ -172,9 +172,9 @@ ...@@ -172,9 +172,9 @@
id: 'DC352437-C019-40EC-B590-AF29E879AF97' id: 'DC352437-C019-40EC-B590-AF29E879AF97'
}).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'}) }).c('x').attrs({xmlns:'http://jabber.org/protocol/muc#user'})
.c('item').attrs({ .c('item').attrs({
affiliation: 'member', affiliation: 'owner',
jid: _converse.bare_jid, jid: _converse.bare_jid,
role: 'participant' role: 'moderator'
}).up() }).up()
.c('status').attrs({code:'110'}); .c('status').attrs({code:'110'});
_converse.connection._dataRecv(utils.createRequest(presence)); _converse.connection._dataRecv(utils.createRequest(presence));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment