Commit b4110dc1 authored by JC Brand's avatar JC Brand

Merge branch 'master' into converse-omemo

parents 2884549b 299fa4f3
......@@ -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
......
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 @@
<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;
background: $dark-red;
color: $inverse-link-color;
}
.suggestion-box li:hover mark,
div.awesomplete li:hover mark {
background: $darkest-red;
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%;
......
#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;
......
(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();
});
}));
......
This diff is collapsed.
This diff is collapsed.
......@@ -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 () {
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());
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());
// 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));
}));
});
}));
This diff is collapsed.
......@@ -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';
......
This diff is collapsed.
This diff is collapsed.
......@@ -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();
});
}
......
<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>
{[ 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));
......
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