Use es2015 modules instead of UMD

This commit is contained in:
JC Brand 2018-10-23 03:41:38 +02:00
parent 3a33d6f99b
commit 6904f9a897
43 changed files with 58085 additions and 58071 deletions

View File

@ -1,6 +1,7 @@
{
"parserOptions": {
"ecmaVersion": 2017
"ecmaVersion": 2017,
"sourceType": "module"
},
"env": {
"browser": true,

View File

@ -1,5 +1,10 @@
# Changelog
## 4.1.0 (Unreleased)
- Use [Lerna](https://lernajs.io/) to create the @converse/headless package
- Use ES2015 modules instead of UMD.
## 4.0.3 (2018-10-22)
- New translations: Arabic, Basque, Czech, French, German, Hungarian, Japanese, Norwegian Bokmål, Polish, Romanian, Spanish

View File

@ -157,12 +157,16 @@ watchcss: dev
$(SASS) --watch -I $(BOURBON) -I $(BOOTSTRAP) sass:css
.PHONY: watchjs
watchjs: dev
watchjs: dev dist/converse-headless.js
./node_modules/.bin/npx webpack --mode=development --watch
.PHONY: watchjsheadless
watchjsheadless: dev
./node_modules/.bin/npx webpack --mode=development --watch --type=headless
.PHONY: watch
watch: dev
make -j 2 watchjs watchcss
make -j 3 watchcss watchjsheadless watchjs
.PHONY: logo
logo: logo/conversejs-transparent16.png \

80256
dist/converse.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -1555,34 +1555,32 @@
it("properly handles notification that a room has been destroyed",
mock.initConverseWithPromises(
null, ['rosterGroupsFetched'], {},
function (done, _converse) {
async function (done, _converse) {
test_utils.openChatRoomViaModal(_converse, 'problematic@muc.localhost', 'dummy')
.then(function () {
const presence = $pres().attrs({
from:'problematic@muc.localhost',
id:'n13mt3l',
to:'dummy@localhost/pda',
type:'error'})
.c('error').attrs({'type':'cancel'})
.c('gone').attrs({'xmlns':'urn:ietf:params:xml:ns:xmpp-stanzas'})
.t('xmpp:other-room@chat.jabberfr.org?join').up()
.c('text').attrs({'xmlns':'urn:ietf:params:xml:ns:xmpp-stanzas'})
.t("We didn't like the name").nodeTree;
await test_utils.openChatRoomViaModal(_converse, 'problematic@muc.localhost', 'dummy')
const presence = $pres().attrs({
from:'problematic@muc.localhost',
id:'n13mt3l',
to:'dummy@localhost/pda',
type:'error'})
.c('error').attrs({'type':'cancel'})
.c('gone').attrs({'xmlns':'urn:ietf:params:xml:ns:xmpp-stanzas'})
.t('xmpp:other-room@chat.jabberfr.org?join').up()
.c('text').attrs({'xmlns':'urn:ietf:params:xml:ns:xmpp-stanzas'})
.t("We didn't like the name").nodeTree;
const view = _converse.chatboxviews.get('problematic@muc.localhost');
spyOn(view, 'showErrorMessage').and.callThrough();
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(view.el.querySelector('.chatroom-body .disconnect-msg').textContent)
.toBe('This room no longer exists');
expect(view.el.querySelector('.chatroom-body .destroyed-reason').textContent)
.toBe(`"We didn't like the name"`);
expect(view.el.querySelector('.chatroom-body .moved-label').textContent.trim())
.toBe('The conversation has moved. Click below to enter.');
expect(view.el.querySelector('.chatroom-body .moved-link').textContent.trim())
.toBe(`other-room@chat.jabberfr.org`);
done();
}).catch(_.partial(console.error, _));
const view = _converse.chatboxviews.get('problematic@muc.localhost');
spyOn(view, 'showErrorMessage').and.callThrough();
_converse.connection._dataRecv(test_utils.createRequest(presence));
expect(view.el.querySelector('.chatroom-body .disconnect-msg').textContent)
.toBe('This room no longer exists');
expect(view.el.querySelector('.chatroom-body .destroyed-reason').textContent)
.toBe(`"We didn't like the name"`);
expect(view.el.querySelector('.chatroom-body .moved-label').textContent.trim())
.toBe('The conversation has moved. Click below to enter.');
expect(view.el.querySelector('.chatroom-body .moved-link').textContent.trim())
.toBe(`other-room@chat.jabberfr.org`);
done();
}));
it("will use the user's reserved nickname, if it exists",

View File

@ -12,46 +12,41 @@
describe("A list of open rooms", function () {
it("is shown in the \"Rooms\" panel", mock.initConverseWithPromises(
null, ['rosterGroupsFetched', 'chatBoxesFetched'],
{ allow_bookmarks: false // Makes testing easier, otherwise we
// have to mock stanza traffic.
},
function (done, _converse) {
test_utils.openControlBox();
const controlbox = _converse.chatboxviews.get('controlbox');
let list = controlbox.el.querySelector('div.rooms-list-container');
expect(_.includes(list.classList, 'hidden')).toBeTruthy();
null, ['rosterGroupsFetched', 'chatBoxesFetched'],
{ allow_bookmarks: false // Makes testing easier, otherwise we
// have to mock stanza traffic.
}, async function (done, _converse) {
let room_els;
test_utils.openControlBox();
const controlbox = _converse.chatboxviews.get('controlbox');
let list = controlbox.el.querySelector('div.rooms-list-container');
expect(_.includes(list.classList, 'hidden')).toBeTruthy();
test_utils.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC')
.then(() => {
expect(_.isUndefined(_converse.rooms_list_view)).toBeFalsy();
room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
expect(room_els.length).toBe(1);
expect(room_els[0].innerText).toBe('room@conference.shakespeare.lit');
return test_utils.openChatRoom(_converse, 'lounge', 'localhost', 'dummy');
}).then(() => {
room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
expect(room_els.length).toBe(2);
await test_utils.openChatRoom(_converse, 'room', 'conference.shakespeare.lit', 'JC');
expect(_.isUndefined(_converse.rooms_list_view)).toBeFalsy();
let room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
expect(room_els.length).toBe(1);
expect(room_els[0].innerText).toBe('room@conference.shakespeare.lit');
await test_utils.openChatRoom(_converse, 'lounge', 'localhost', 'dummy');
room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
expect(room_els.length).toBe(2);
var view = _converse.chatboxviews.get('room@conference.shakespeare.lit');
view.close();
room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
expect(room_els.length).toBe(1);
expect(room_els[0].innerText).toBe('lounge@localhost');
list = controlbox.el.querySelector('div.rooms-list-container');
expect(_.includes(list.classList, 'hidden')).toBeFalsy();
let view = _converse.chatboxviews.get('room@conference.shakespeare.lit');
view.close();
room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
expect(room_els.length).toBe(1);
expect(room_els[0].innerText).toBe('lounge@localhost');
list = controlbox.el.querySelector('div.rooms-list-container');
expect(_.includes(list.classList, 'hidden')).toBeFalsy();
view = _converse.chatboxviews.get('lounge@localhost');
view.close();
room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
expect(room_els.length).toBe(0);
view = _converse.chatboxviews.get('lounge@localhost');
view.close();
room_els = _converse.rooms_list_view.el.querySelectorAll(".open-room");
expect(room_els.length).toBe(0);
list = controlbox.el.querySelector('div.rooms-list-container');
expect(_.includes(list.classList, 'hidden')).toBeTruthy();
done();
}).catch(_.partial(_converse.log, _, Strophe.LogLevel.FATAL));
list = controlbox.el.querySelector('div.rooms-list-container');
expect(_.includes(list.classList, 'hidden')).toBeTruthy();
done();
}
));
});

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -4,60 +4,58 @@
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
(function (root, factory) {
define(["@converse/headless/converse-core"], factory);
}(this, function (converse) {
const { Strophe, $build, _, b64_sha1 } = converse.env;
import converse from "@converse/headless/converse-core";
Strophe.addNamespace('CAPS', "http://jabber.org/protocol/caps");
const { Strophe, $build, _, b64_sha1 } = converse.env;
function propertySort (array, property) {
return array.sort((a, b) => { return a[property] > b[property] ? -1 : 1 });
Strophe.addNamespace('CAPS', "http://jabber.org/protocol/caps");
function propertySort (array, property) {
return array.sort((a, b) => { return a[property] > b[property] ? -1 : 1 });
}
function generateVerificationString (_converse) {
const identities = _converse.api.disco.own.identities.get(),
features = _converse.api.disco.own.features.get();
if (identities.length > 1) {
propertySort(identities, "category");
propertySort(identities, "type");
propertySort(identities, "lang");
}
function generateVerificationString (_converse) {
const identities = _converse.api.disco.own.identities.get(),
features = _converse.api.disco.own.features.get();
let S = _.reduce(
identities,
(result, id) => `${result}${id.category}/${id.type}/${_.get(id, 'lang', '')}/${id.name}<`,
"");
if (identities.length > 1) {
propertySort(identities, "category");
propertySort(identities, "type");
propertySort(identities, "lang");
}
features.sort();
S = _.reduce(features, (result, feature) => `${result}${feature}<`, S);
return b64_sha1(S);
}
let S = _.reduce(
identities,
(result, id) => `${result}${id.category}/${id.type}/${_.get(id, 'lang', '')}/${id.name}<`,
"");
function createCapsNode (_converse) {
return $build("c", {
'xmlns': Strophe.NS.CAPS,
'hash': "sha-1",
'node': "https://conversejs.org",
'ver': generateVerificationString(_converse)
}).nodeTree;
}
features.sort();
S = _.reduce(features, (result, feature) => `${result}${feature}<`, S);
return b64_sha1(S);
}
converse.plugins.add('converse-caps', {
function createCapsNode (_converse) {
return $build("c", {
'xmlns': Strophe.NS.CAPS,
'hash': "sha-1",
'node': "https://conversejs.org",
'ver': generateVerificationString(_converse)
}).nodeTree;
}
converse.plugins.add('converse-caps', {
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
XMPPStatus: {
constructPresence () {
const presence = this.__super__.constructPresence.apply(this, arguments);
presence.root().cnode(createCapsNode(this.__super__._converse));
return presence;
}
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
XMPPStatus: {
constructPresence () {
const presence = this.__super__.constructPresence.apply(this, arguments);
presence.root().cnode(createCapsNode(this.__super__._converse));
return presence;
}
}
});
}));
}
});

View File

@ -4,176 +4,169 @@
// Copyright (c) 2012-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
(function (root, factory) {
define([
"@converse/headless/converse-core",
"templates/chatboxes.html",
"@converse/headless/converse-chatboxes",
"backbone.nativeview",
"backbone.overview"
], factory);
}(this, function (converse, tpl_chatboxes) {
"use strict";
import "@converse/headless/converse-chatboxes";
import "backbone.nativeview";
import "backbone.overview";
import converse from "@converse/headless/converse-core";
import tpl_chatboxes from "templates/chatboxes.html";
const { Backbone, _ } = converse.env;
const { Backbone, _ } = converse.env;
const AvatarMixin = {
const AvatarMixin = {
renderAvatar (el) {
el = el || this.el;
const canvas_el = el.querySelector('canvas');
if (_.isNull(canvas_el)) {
return;
}
const image_type = this.model.vcard.get('image_type'),
image = this.model.vcard.get('image'),
img_src = "data:" + image_type + ";base64," + image,
img = new Image();
return new Promise((resolve, reject) => {
img.onload = () => {
const ctx = canvas_el.getContext('2d'),
ratio = img.width / img.height;
ctx.clearRect(0, 0, canvas_el.width, canvas_el.height);
if (ratio < 1) {
const scaled_img_with = canvas_el.width*ratio,
x = Math.floor((canvas_el.width-scaled_img_with)/2);
ctx.drawImage(img, x, 0, scaled_img_with, canvas_el.height);
} else {
ctx.drawImage(img, 0, 0, canvas_el.width, canvas_el.height*ratio);
}
resolve();
};
img.src = img_src;
});
},
};
converse.plugins.add('converse-chatboxviews', {
dependencies: ["converse-chatboxes"],
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
initStatus: function (reconnecting) {
const { _converse } = this.__super__;
if (!reconnecting) {
_converse.chatboxviews.closeAllChatBoxes();
}
return this.__super__.initStatus.apply(this, arguments);
}
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
_converse.api.promises.add([
'chatBoxViewsInitialized'
]);
_converse.ViewWithAvatar = Backbone.NativeView.extend(AvatarMixin);
_converse.VDOMViewWithAvatar = Backbone.VDOMView.extend(AvatarMixin);
_converse.ChatBoxViews = Backbone.Overview.extend({
_ensureElement () {
/* Override method from backbone.js
* If the #conversejs element doesn't exist, create it.
*/
if (!this.el) {
let el = _converse.root.querySelector('#conversejs');
if (_.isNull(el)) {
el = document.createElement('div');
el.setAttribute('id', 'conversejs');
const body = _converse.root.querySelector('body');
if (body) {
body.appendChild(el);
} else {
// Perhaps inside a web component?
_converse.root.appendChild(el);
}
}
el.innerHTML = '';
this.setElement(el, false);
} else {
this.setElement(_.result(this, 'el'), false);
}
},
initialize () {
this.model.on("destroy", this.removeChat, this);
this.el.classList.add(`converse-${_converse.view_mode}`);
this.render();
},
render () {
try {
this.el.innerHTML = tpl_chatboxes();
} catch (e) {
this._ensureElement();
this.el.innerHTML = tpl_chatboxes();
}
this.row_el = this.el.querySelector('.row');
},
insertRowColumn (el) {
/* Add a new DOM element (likely a chat box) into the
* the row managed by this overview.
*/
this.row_el.insertAdjacentElement('afterBegin', el);
},
removeChat (item) {
this.remove(item.get('id'));
},
closeAllChatBoxes () {
/* This method gets overridden in src/converse-controlbox.js if
* the controlbox plugin is active.
*/
this.each(function (view) { view.close(); });
return this;
},
chatBoxMayBeShown (chatbox) {
return this.model.chatBoxMayBeShown(chatbox);
}
});
/************************ BEGIN Event Handlers ************************/
_converse.api.waitUntil('rosterContactsFetched').then(() => {
_converse.roster.on('add', (contact) => {
/* When a new contact is added, check if we already have a
* chatbox open for it, and if so attach it to the chatbox.
*/
const chatbox = _converse.chatboxes.findWhere({'jid': contact.get('jid')});
if (chatbox) {
chatbox.addRelatedContact(contact);
}
});
});
_converse.api.listen.on('chatBoxesInitialized', () => {
_converse.chatboxviews = new _converse.ChatBoxViews({
'model': _converse.chatboxes
});
_converse.emit('chatBoxViewsInitialized');
});
_converse.api.listen.on('clearSession', () => _converse.chatboxviews.closeAllChatBoxes());
/************************ END Event Handlers ************************/
renderAvatar (el) {
el = el || this.el;
const canvas_el = el.querySelector('canvas');
if (_.isNull(canvas_el)) {
return;
}
});
return converse;
}));
const image_type = this.model.vcard.get('image_type'),
image = this.model.vcard.get('image'),
img_src = "data:" + image_type + ";base64," + image,
img = new Image();
return new Promise((resolve, reject) => {
img.onload = () => {
const ctx = canvas_el.getContext('2d'),
ratio = img.width / img.height;
ctx.clearRect(0, 0, canvas_el.width, canvas_el.height);
if (ratio < 1) {
const scaled_img_with = canvas_el.width*ratio,
x = Math.floor((canvas_el.width-scaled_img_with)/2);
ctx.drawImage(img, x, 0, scaled_img_with, canvas_el.height);
} else {
ctx.drawImage(img, 0, 0, canvas_el.width, canvas_el.height*ratio);
}
resolve();
};
img.src = img_src;
});
},
};
converse.plugins.add('converse-chatboxviews', {
dependencies: ["converse-chatboxes"],
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
initStatus: function (reconnecting) {
const { _converse } = this.__super__;
if (!reconnecting) {
_converse.chatboxviews.closeAllChatBoxes();
}
return this.__super__.initStatus.apply(this, arguments);
}
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
_converse.api.promises.add([
'chatBoxViewsInitialized'
]);
_converse.ViewWithAvatar = Backbone.NativeView.extend(AvatarMixin);
_converse.VDOMViewWithAvatar = Backbone.VDOMView.extend(AvatarMixin);
_converse.ChatBoxViews = Backbone.Overview.extend({
_ensureElement () {
/* Override method from backbone.js
* If the #conversejs element doesn't exist, create it.
*/
if (!this.el) {
let el = _converse.root.querySelector('#conversejs');
if (_.isNull(el)) {
el = document.createElement('div');
el.setAttribute('id', 'conversejs');
const body = _converse.root.querySelector('body');
if (body) {
body.appendChild(el);
} else {
// Perhaps inside a web component?
_converse.root.appendChild(el);
}
}
el.innerHTML = '';
this.setElement(el, false);
} else {
this.setElement(_.result(this, 'el'), false);
}
},
initialize () {
this.model.on("destroy", this.removeChat, this);
this.el.classList.add(`converse-${_converse.view_mode}`);
this.render();
},
render () {
try {
this.el.innerHTML = tpl_chatboxes();
} catch (e) {
this._ensureElement();
this.el.innerHTML = tpl_chatboxes();
}
this.row_el = this.el.querySelector('.row');
},
insertRowColumn (el) {
/* Add a new DOM element (likely a chat box) into the
* the row managed by this overview.
*/
this.row_el.insertAdjacentElement('afterBegin', el);
},
removeChat (item) {
this.remove(item.get('id'));
},
closeAllChatBoxes () {
/* This method gets overridden in src/converse-controlbox.js if
* the controlbox plugin is active.
*/
this.each(function (view) { view.close(); });
return this;
},
chatBoxMayBeShown (chatbox) {
return this.model.chatBoxMayBeShown(chatbox);
}
});
/************************ BEGIN Event Handlers ************************/
_converse.api.waitUntil('rosterContactsFetched').then(() => {
_converse.roster.on('add', (contact) => {
/* When a new contact is added, check if we already have a
* chatbox open for it, and if so attach it to the chatbox.
*/
const chatbox = _converse.chatboxes.findWhere({'jid': contact.get('jid')});
if (chatbox) {
chatbox.addRelatedContact(contact);
}
});
});
_converse.api.listen.on('chatBoxesInitialized', () => {
_converse.chatboxviews = new _converse.ChatBoxViews({
'model': _converse.chatboxes
});
_converse.emit('chatBoxViewsInitialized');
});
_converse.api.listen.on('clearSession', () => _converse.chatboxviews.closeAllChatBoxes());
/************************ END Event Handlers ************************/
}
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6,366 +6,363 @@
//
/*global define, window, document */
(function (root, factory) {
define(["@converse/headless/converse-core",
"templates/dragresize.html",
"converse-chatview",
"converse-controlbox"
], factory);
}(this, function (converse, tpl_dragresize) {
"use strict";
const { _ } = converse.env;
import "converse-chatview";
import "converse-controlbox";
import converse from "@converse/headless/converse-core";
import tpl_dragresize from "templates/dragresize.html";
function renderDragResizeHandles (_converse, view) {
const flyout = view.el.querySelector('.box-flyout');
const div = document.createElement('div');
div.innerHTML = tpl_dragresize();
flyout.insertBefore(
div,
flyout.firstChild
);
}
const { _ } = converse.env;
function renderDragResizeHandles (_converse, view) {
const flyout = view.el.querySelector('.box-flyout');
const div = document.createElement('div');
div.innerHTML = tpl_dragresize();
flyout.insertBefore(
div,
flyout.firstChild
);
}
converse.plugins.add('converse-dragresize', {
/* Plugin dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin.
*
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found. By default it's
* false, which means these plugins are only loaded opportunistically.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-chatview", "converse-headline", "converse-muc-views"],
converse.plugins.add('converse-dragresize', {
/* Plugin dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin.
*
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found. By default it's
* false, which means these plugins are only loaded opportunistically.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-chatview", "converse-headline", "converse-muc-views"],
enabled (_converse) {
return _converse.view_mode == 'overlayed';
enabled (_converse) {
return _converse.view_mode == 'overlayed';
},
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
registerGlobalEventHandlers () {
const that = this;
document.addEventListener('mousemove', function (ev) {
if (!that.resizing || !that.allow_dragresize) { return true; }
ev.preventDefault();
that.resizing.chatbox.resizeChatBox(ev);
});
document.addEventListener('mouseup', function (ev) {
if (!that.resizing || !that.allow_dragresize) { return true; }
ev.preventDefault();
const height = that.applyDragResistance(
that.resizing.chatbox.height,
that.resizing.chatbox.model.get('default_height')
);
const width = that.applyDragResistance(
that.resizing.chatbox.width,
that.resizing.chatbox.model.get('default_width')
);
if (that.connection.connected) {
that.resizing.chatbox.model.save({'height': height});
that.resizing.chatbox.model.save({'width': width});
} else {
that.resizing.chatbox.model.set({'height': height});
that.resizing.chatbox.model.set({'width': width});
}
that.resizing = null;
});
return this.__super__.registerGlobalEventHandlers.apply(this, arguments);
},
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
registerGlobalEventHandlers () {
const that = this;
document.addEventListener('mousemove', function (ev) {
if (!that.resizing || !that.allow_dragresize) { return true; }
ev.preventDefault();
that.resizing.chatbox.resizeChatBox(ev);
ChatBox: {
initialize () {
const { _converse } = this.__super__;
const result = this.__super__.initialize.apply(this, arguments),
height = this.get('height'), width = this.get('width'),
save = this.get('id') === 'controlbox' ? this.set.bind(this) : this.save.bind(this);
save({
'height': _converse.applyDragResistance(height, this.get('default_height')),
'width': _converse.applyDragResistance(width, this.get('default_width')),
});
return result;
}
},
document.addEventListener('mouseup', function (ev) {
if (!that.resizing || !that.allow_dragresize) { return true; }
ev.preventDefault();
const height = that.applyDragResistance(
that.resizing.chatbox.height,
that.resizing.chatbox.model.get('default_height')
);
const width = that.applyDragResistance(
that.resizing.chatbox.width,
that.resizing.chatbox.model.get('default_width')
);
if (that.connection.connected) {
that.resizing.chatbox.model.save({'height': height});
that.resizing.chatbox.model.save({'width': width});
} else {
that.resizing.chatbox.model.set({'height': height});
that.resizing.chatbox.model.set({'width': width});
}
that.resizing = null;
});
return this.__super__.registerGlobalEventHandlers.apply(this, arguments);
ChatBoxView: {
events: {
'mousedown .dragresize-top': 'onStartVerticalResize',
'mousedown .dragresize-left': 'onStartHorizontalResize',
'mousedown .dragresize-topleft': 'onStartDiagonalResize'
},
ChatBox: {
initialize () {
const { _converse } = this.__super__;
const result = this.__super__.initialize.apply(this, arguments),
height = this.get('height'), width = this.get('width'),
save = this.get('id') === 'controlbox' ? this.set.bind(this) : this.save.bind(this);
save({
'height': _converse.applyDragResistance(height, this.get('default_height')),
'width': _converse.applyDragResistance(width, this.get('default_width')),
});
return result;
initialize () {
window.addEventListener('resize', _.debounce(this.setDimensions.bind(this), 100));
this.__super__.initialize.apply(this, arguments);
},
render () {
const result = this.__super__.render.apply(this, arguments);
renderDragResizeHandles(this.__super__._converse, this);
this.setWidth();
return result;
},
setWidth () {
// If a custom width is applied (due to drag-resizing),
// then we need to set the width of the .chatbox element as well.
if (this.model.get('width')) {
this.el.style.width = this.model.get('width');
}
},
ChatBoxView: {
events: {
'mousedown .dragresize-top': 'onStartVerticalResize',
'mousedown .dragresize-left': 'onStartHorizontalResize',
'mousedown .dragresize-topleft': 'onStartDiagonalResize'
},
_show () {
this.initDragResize().setDimensions();
this.__super__._show.apply(this, arguments);
},
initialize () {
window.addEventListener('resize', _.debounce(this.setDimensions.bind(this), 100));
this.__super__.initialize.apply(this, arguments);
},
initDragResize () {
/* Determine and store the default box size.
* We need this information for the drag-resizing feature.
*/
const { _converse } = this.__super__,
flyout = this.el.querySelector('.box-flyout'),
style = window.getComputedStyle(flyout);
render () {
const result = this.__super__.render.apply(this, arguments);
renderDragResizeHandles(this.__super__._converse, this);
this.setWidth();
return result;
},
if (_.isUndefined(this.model.get('height'))) {
const height = parseInt(style.height.replace(/px$/, ''), 10),
width = parseInt(style.width.replace(/px$/, ''), 10);
this.model.set('height', height);
this.model.set('default_height', height);
this.model.set('width', width);
this.model.set('default_width', width);
}
const min_width = style['min-width'];
const min_height = style['min-height'];
this.model.set('min_width', min_width.endsWith('px') ? Number(min_width.replace(/px$/, '')) :0);
this.model.set('min_height', min_height.endsWith('px') ? Number(min_height.replace(/px$/, '')) :0);
// Initialize last known mouse position
this.prev_pageY = 0;
this.prev_pageX = 0;
if (_converse.connection.connected) {
this.height = this.model.get('height');
this.width = this.model.get('width');
}
return this;
},
setWidth () {
// If a custom width is applied (due to drag-resizing),
// then we need to set the width of the .chatbox element as well.
if (this.model.get('width')) {
this.el.style.width = this.model.get('width');
}
},
setDimensions () {
// Make sure the chat box has the right height and width.
this.adjustToViewport();
this.setChatBoxHeight(this.model.get('height'));
this.setChatBoxWidth(this.model.get('width'));
},
_show () {
this.initDragResize().setDimensions();
this.__super__._show.apply(this, arguments);
},
initDragResize () {
/* Determine and store the default box size.
* We need this information for the drag-resizing feature.
*/
const { _converse } = this.__super__,
flyout = this.el.querySelector('.box-flyout'),
style = window.getComputedStyle(flyout);
if (_.isUndefined(this.model.get('height'))) {
const height = parseInt(style.height.replace(/px$/, ''), 10),
width = parseInt(style.width.replace(/px$/, ''), 10);
this.model.set('height', height);
this.model.set('default_height', height);
this.model.set('width', width);
this.model.set('default_width', width);
}
const min_width = style['min-width'];
const min_height = style['min-height'];
this.model.set('min_width', min_width.endsWith('px') ? Number(min_width.replace(/px$/, '')) :0);
this.model.set('min_height', min_height.endsWith('px') ? Number(min_height.replace(/px$/, '')) :0);
// Initialize last known mouse position
this.prev_pageY = 0;
this.prev_pageX = 0;
if (_converse.connection.connected) {
this.height = this.model.get('height');
this.width = this.model.get('width');
}
return this;
},
setDimensions () {
// Make sure the chat box has the right height and width.
this.adjustToViewport();
this.setChatBoxHeight(this.model.get('height'));
this.setChatBoxWidth(this.model.get('width'));
},
setChatBoxHeight (height) {
const { _converse } = this.__super__;
if (height) {
height = _converse.applyDragResistance(height, this.model.get('default_height'))+'px';
} else {
height = "";
}
const flyout_el = this.el.querySelector('.box-flyout');
if (!_.isNull(flyout_el)) {
flyout_el.style.height = height;
}
},
setChatBoxWidth (width) {
const { _converse } = this.__super__;
if (width) {
width = _converse.applyDragResistance(width, this.model.get('default_width'))+'px';
} else {
width = "";
}
this.el.style.width = width;
const flyout_el = this.el.querySelector('.box-flyout');
if (!_.isNull(flyout_el)) {
flyout_el.style.width = width;
}
},
adjustToViewport () {
/* Event handler called when viewport gets resized. We remove
* custom width/height from chat boxes.
*/
const viewport_width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const viewport_height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
if (viewport_width <= 480) {
this.model.set('height', undefined);
this.model.set('width', undefined);
} else if (viewport_width <= this.model.get('width')) {
this.model.set('width', undefined);
} else if (viewport_height <= this.model.get('height')) {
this.model.set('height', undefined);
}
},
onStartVerticalResize (ev) {
const { _converse } = this.__super__;
if (!_converse.allow_dragresize) { return true; }
// Record element attributes for mouseMove().
const flyout = this.el.querySelector('.box-flyout'),
style = window.getComputedStyle(flyout);
this.height = parseInt(style.height.replace(/px$/, ''), 10);
_converse.resizing = {
'chatbox': this,
'direction': 'top'
};
this.prev_pageY = ev.pageY;
},
onStartHorizontalResize (ev) {
const { _converse } = this.__super__;
if (!_converse.allow_dragresize) { return true; }
const flyout = this.el.querySelector('.box-flyout'),
style = window.getComputedStyle(flyout);
this.width = parseInt(style.width.replace(/px$/, ''), 10);
_converse.resizing = {
'chatbox': this,
'direction': 'left'
};
this.prev_pageX = ev.pageX;
},
onStartDiagonalResize (ev) {
const { _converse } = this.__super__;
this.onStartHorizontalResize(ev);
this.onStartVerticalResize(ev);
_converse.resizing.direction = 'topleft';
},
resizeChatBox (ev) {
let diff;
const { _converse } = this.__super__;
if (_converse.resizing.direction.indexOf('top') === 0) {
diff = ev.pageY - this.prev_pageY;
if (diff) {
this.height = ((this.height-diff) > (this.model.get('min_height') || 0)) ? (this.height-diff) : this.model.get('min_height');
this.prev_pageY = ev.pageY;
this.setChatBoxHeight(this.height);
}
}
if (_.includes(_converse.resizing.direction, 'left')) {
diff = this.prev_pageX - ev.pageX;
if (diff) {
this.width = ((this.width+diff) > (this.model.get('min_width') || 0)) ? (this.width+diff) : this.model.get('min_width');
this.prev_pageX = ev.pageX;
this.setChatBoxWidth(this.width);
}
}
setChatBoxHeight (height) {
const { _converse } = this.__super__;
if (height) {
height = _converse.applyDragResistance(height, this.model.get('default_height'))+'px';
} else {
height = "";
}
const flyout_el = this.el.querySelector('.box-flyout');
if (!_.isNull(flyout_el)) {
flyout_el.style.height = height;
}
},
HeadlinesBoxView: {
events: {
'mousedown .dragresize-top': 'onStartVerticalResize',
'mousedown .dragresize-left': 'onStartHorizontalResize',
'mousedown .dragresize-topleft': 'onStartDiagonalResize'
},
initialize () {
window.addEventListener('resize', _.debounce(this.setDimensions.bind(this), 100));
return this.__super__.initialize.apply(this, arguments);
},
render () {
const result = this.__super__.render.apply(this, arguments);
renderDragResizeHandles(this.__super__._converse, this);
this.setWidth();
return result;
setChatBoxWidth (width) {
const { _converse } = this.__super__;
if (width) {
width = _converse.applyDragResistance(width, this.model.get('default_width'))+'px';
} else {
width = "";
}
this.el.style.width = width;
const flyout_el = this.el.querySelector('.box-flyout');
if (!_.isNull(flyout_el)) {
flyout_el.style.width = width;
}
},
ControlBoxView: {
events: {
'mousedown .dragresize-top': 'onStartVerticalResize',
'mousedown .dragresize-left': 'onStartHorizontalResize',
'mousedown .dragresize-topleft': 'onStartDiagonalResize'
},
initialize () {
window.addEventListener('resize', _.debounce(this.setDimensions.bind(this), 100));
this.__super__.initialize.apply(this, arguments);
},
render () {
const result = this.__super__.render.apply(this, arguments);
renderDragResizeHandles(this.__super__._converse, this);
this.setWidth();
return result;
},
renderLoginPanel () {
const result = this.__super__.renderLoginPanel.apply(this, arguments);
this.initDragResize().setDimensions();
return result;
},
renderControlBoxPane () {
const result = this.__super__.renderControlBoxPane.apply(this, arguments);
this.initDragResize().setDimensions();
return result;
adjustToViewport () {
/* Event handler called when viewport gets resized. We remove
* custom width/height from chat boxes.
*/
const viewport_width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const viewport_height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
if (viewport_width <= 480) {
this.model.set('height', undefined);
this.model.set('width', undefined);
} else if (viewport_width <= this.model.get('width')) {
this.model.set('width', undefined);
} else if (viewport_height <= this.model.get('height')) {
this.model.set('height', undefined);
}
},
ChatRoomView: {
events: {
'mousedown .dragresize-top': 'onStartVerticalResize',
'mousedown .dragresize-left': 'onStartHorizontalResize',
'mousedown .dragresize-topleft': 'onStartDiagonalResize'
},
onStartVerticalResize (ev) {
const { _converse } = this.__super__;
if (!_converse.allow_dragresize) { return true; }
// Record element attributes for mouseMove().
const flyout = this.el.querySelector('.box-flyout'),
style = window.getComputedStyle(flyout);
this.height = parseInt(style.height.replace(/px$/, ''), 10);
_converse.resizing = {
'chatbox': this,
'direction': 'top'
};
this.prev_pageY = ev.pageY;
},
initialize () {
window.addEventListener('resize', _.debounce(this.setDimensions.bind(this), 100));
this.__super__.initialize.apply(this, arguments);
},
onStartHorizontalResize (ev) {
const { _converse } = this.__super__;
if (!_converse.allow_dragresize) { return true; }
const flyout = this.el.querySelector('.box-flyout'),
style = window.getComputedStyle(flyout);
this.width = parseInt(style.width.replace(/px$/, ''), 10);
_converse.resizing = {
'chatbox': this,
'direction': 'left'
};
this.prev_pageX = ev.pageX;
},
render () {
const result = this.__super__.render.apply(this, arguments);
renderDragResizeHandles(this.__super__._converse, this);
this.setWidth();
return result;
onStartDiagonalResize (ev) {
const { _converse } = this.__super__;
this.onStartHorizontalResize(ev);
this.onStartVerticalResize(ev);
_converse.resizing.direction = 'topleft';
},
resizeChatBox (ev) {
let diff;
const { _converse } = this.__super__;
if (_converse.resizing.direction.indexOf('top') === 0) {
diff = ev.pageY - this.prev_pageY;
if (diff) {
this.height = ((this.height-diff) > (this.model.get('min_height') || 0)) ? (this.height-diff) : this.model.get('min_height');
this.prev_pageY = ev.pageY;
this.setChatBoxHeight(this.height);
}
}
if (_.includes(_converse.resizing.direction, 'left')) {
diff = this.prev_pageX - ev.pageX;
if (diff) {
this.width = ((this.width+diff) > (this.model.get('min_width') || 0)) ? (this.width+diff) : this.model.get('min_width');
this.prev_pageX = ev.pageX;
this.setChatBoxWidth(this.width);
}
}
}
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this;
HeadlinesBoxView: {
events: {
'mousedown .dragresize-top': 'onStartVerticalResize',
'mousedown .dragresize-left': 'onStartHorizontalResize',
'mousedown .dragresize-topleft': 'onStartDiagonalResize'
},
_converse.api.settings.update({
allow_dragresize: true,
});
initialize () {
window.addEventListener('resize', _.debounce(this.setDimensions.bind(this), 100));
return this.__super__.initialize.apply(this, arguments);
},
_converse.applyDragResistance = function (value, default_value) {
/* This method applies some resistance around the
* default_value. If value is close enough to
* default_value, then default_value is returned instead.
*/
if (_.isUndefined(value)) {
return undefined;
} else if (_.isUndefined(default_value)) {
return value;
}
const resistance = 10;
if ((value !== default_value) &&
(Math.abs(value- default_value) < resistance)) {
return default_value;
}
return value;
};
render () {
const result = this.__super__.render.apply(this, arguments);
renderDragResizeHandles(this.__super__._converse, this);
this.setWidth();
return result;
}
},
ControlBoxView: {
events: {
'mousedown .dragresize-top': 'onStartVerticalResize',
'mousedown .dragresize-left': 'onStartHorizontalResize',
'mousedown .dragresize-topleft': 'onStartDiagonalResize'
},
initialize () {
window.addEventListener('resize', _.debounce(this.setDimensions.bind(this), 100));
this.__super__.initialize.apply(this, arguments);
},
render () {
const result = this.__super__.render.apply(this, arguments);
renderDragResizeHandles(this.__super__._converse, this);
this.setWidth();
return result;
},
renderLoginPanel () {
const result = this.__super__.renderLoginPanel.apply(this, arguments);
this.initDragResize().setDimensions();
return result;
},
renderControlBoxPane () {
const result = this.__super__.renderControlBoxPane.apply(this, arguments);
this.initDragResize().setDimensions();
return result;
}
},
ChatRoomView: {
events: {
'mousedown .dragresize-top': 'onStartVerticalResize',
'mousedown .dragresize-left': 'onStartHorizontalResize',
'mousedown .dragresize-topleft': 'onStartDiagonalResize'
},
initialize () {
window.addEventListener('resize', _.debounce(this.setDimensions.bind(this), 100));
this.__super__.initialize.apply(this, arguments);
},
render () {
const result = this.__super__.render.apply(this, arguments);
renderDragResizeHandles(this.__super__._converse, this);
this.setWidth();
return result;
}
}
});
}));
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this;
_converse.api.settings.update({
allow_dragresize: true,
});
_converse.applyDragResistance = function (value, default_value) {
/* This method applies some resistance around the
* default_value. If value is close enough to
* default_value, then default_value is returned instead.
*/
if (_.isUndefined(value)) {
return undefined;
} else if (_.isUndefined(default_value)) {
return value;
}
const resistance = 10;
if ((value !== default_value) &&
(Math.abs(value- default_value) < resistance)) {
return default_value;
}
return value;
};
}
});

View File

@ -1,40 +1,38 @@
// Converse.js
// http://conversejs.org
//
// Copyright (c) 2012-2018, the Converse.js developers
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
(function (root, factory) {
define(["@converse/headless/converse-core", "@converse/headless/converse-muc"], factory);
}(this, function (converse) {
"use strict";
const { Backbone, _ } = converse.env;
import "@converse/headless/converse-muc";
import converse from "@converse/headless/converse-core";
converse.plugins.add('converse-embedded', {
const { Backbone, _ } = converse.env;
enabled (_converse) {
return _converse.view_mode === 'embedded';
},
converse.plugins.add('converse-embedded', {
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
this._converse.api.settings.update({
'allow_logout': false, // No point in logging out when we have auto_login as true.
'allow_muc_invitations': false, // Doesn't make sense to allow because only
// roster contacts can be invited
'hide_muc_server': true
});
const { _converse } = this;
if (!_.isArray(_converse.auto_join_rooms) && !_.isArray(_converse.auto_join_private_chats)) {
throw new Error("converse-embedded: auto_join_rooms must be an Array");
}
if (_converse.auto_join_rooms.length > 1 && _converse.auto_join_private_chats.length > 1) {
throw new Error("converse-embedded: It doesn't make "+
"sense to have the auto_join_rooms setting more then one, "+
"since only one chat room can be open at any time.");
}
enabled (_converse) {
return _converse.view_mode === 'embedded';
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
this._converse.api.settings.update({
'allow_logout': false, // No point in logging out when we have auto_login as true.
'allow_muc_invitations': false, // Doesn't make sense to allow because only
// roster contacts can be invited
'hide_muc_server': true
});
const { _converse } = this;
if (!_.isArray(_converse.auto_join_rooms) && !_.isArray(_converse.auto_join_private_chats)) {
throw new Error("converse-embedded: auto_join_rooms must be an Array");
}
});
}));
if (_converse.auto_join_rooms.length > 1 && _converse.auto_join_private_chats.length > 1) {
throw new Error("converse-embedded: It doesn't make "+
"sense to have the auto_join_rooms setting more then one, "+
"since only one chat room can be open at any time.");
}
}
});

View File

@ -4,57 +4,52 @@
// Copyright (c) JC Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define */
(function (root, factory) {
define(["@converse/headless/converse-core",
"templates/inverse_brand_heading.html",
"converse-chatview",
"converse-controlbox",
"@converse/headless/converse-muc",
"converse-singleton"
], factory);
}(this, function (converse, tpl_brand_heading) {
"use strict";
const { Strophe, _ } = converse.env;
import "@converse/headless/converse-muc";
import "converse-chatview";
import "converse-controlbox";
import "converse-singleton";
import converse from "@converse/headless/converse-core";
import tpl_brand_heading from "templates/inverse_brand_heading.html";
converse.plugins.add('converse-fullscreen', {
const { Strophe, _ } = converse.env;
enabled (_converse) {
return _.includes(['fullscreen', 'embedded'], _converse.view_mode);
},
converse.plugins.add('converse-fullscreen', {
overrides: {
// overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// new functions which don't exist yet can also be added.
enabled (_converse) {
return _.includes(['fullscreen', 'embedded'], _converse.view_mode);
},
ControlBoxView: {
createBrandHeadingHTML() {
return tpl_brand_heading();
},
overrides: {
// overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// new functions which don't exist yet can also be added.
insertBrandHeading () {
const { _converse } = this.__super__;
const el = _converse.root.getElementById('converse-login-panel');
el.parentNode.insertAdjacentHTML(
'afterbegin',
this.createBrandHeadingHTML()
);
}
ControlBoxView: {
createBrandHeadingHTML() {
return tpl_brand_heading();
},
insertBrandHeading () {
const { _converse } = this.__super__;
const el = _converse.root.getElementById('converse-login-panel');
el.parentNode.insertAdjacentHTML(
'afterbegin',
this.createBrandHeadingHTML()
);
}
},
initialize () {
this._converse.api.settings.update({
chatview_avatar_height: 50,
chatview_avatar_width: 50,
hide_open_bookmarks: true,
show_controlbox_by_default: true,
sticky_controlbox: true
});
}
});
}));
},
initialize () {
this._converse.api.settings.update({
chatview_avatar_height: 50,
chatview_avatar_width: 50,
hide_open_bookmarks: true,
show_controlbox_by_default: true,
sticky_controlbox: true
});
}
});

View File

@ -3,154 +3,148 @@
//
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define */
(function (root, factory) {
define([
"@converse/headless/converse-core",
"templates/chatbox.html",
"converse-chatview",
], factory);
}(this, function (converse, tpl_chatbox) {
"use strict";
const { _, utils } = converse.env;
import "converse-chatview";
import converse from "@converse/headless/converse-core";
import tpl_chatbox from "templates/chatbox.html";
converse.plugins.add('converse-headline', {
/* Plugin dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin.
*
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found. By default it's
* false, which means these plugins are only loaded opportunistically.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-chatview"],
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
ChatBoxes: {
model (attrs, options) {
const { _converse } = this.__super__;
if (attrs.type == _converse.HEADLINES_TYPE) {
return new _converse.HeadlinesBox(attrs, options);
} else {
return this.__super__.model.apply(this, arguments);
}
},
}
},
const { _, utils } = converse.env;
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
converse.plugins.add('converse-headline', {
/* Plugin dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin.
*
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found. By default it's
* false, which means these plugins are only loaded opportunistically.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-chatview"],
_converse.HeadlinesBox = _converse.ChatBox.extend({
defaults: {
'type': _converse.HEADLINES_TYPE,
'bookmarked': false,
'chat_state': undefined,
'num_unread': 0,
'url': ''
},
});
overrides: {
// Overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// New functions which don't exist yet can also be added.
_converse.HeadlinesBoxView = _converse.ChatBoxView.extend({
className: 'chatbox headlines',
events: {
'click .close-chatbox-button': 'close',
'click .toggle-chatbox-button': 'minimize',
'keypress textarea.chat-textarea': 'keyPressed'
},
initialize () {
this.initDebounced();
this.disable_mam = true; // Don't do MAM queries for this box
this.model.messages.on('add', this.onMessageAdded, this);
this.model.on('show', this.show, this);
this.model.on('destroy', this.hide, this);
this.model.on('change:minimized', this.onMinimizedChanged, this);
this.render().insertHeading().fetchMessages().insertIntoDOM().hide();
_converse.emit('chatBoxOpened', this);
_converse.emit('chatBoxInitialized', this);
},
render () {
this.el.setAttribute('id', this.model.get('box_id'))
this.el.innerHTML = tpl_chatbox(
_.extend(this.model.toJSON(), {
info_close: '',
label_personal_message: '',
show_send_button: false,
show_toolbar: false,
unread_msgs: ''
}
));
this.content = this.el.querySelector('.chat-content');
return this;
},
// Override to avoid the methods in converse-chatview.js
'renderMessageForm': _.noop,
'afterShown': _.noop
});
function onHeadlineMessage (message) {
/* Handler method for all incoming messages of type "headline". */
const from_jid = message.getAttribute('from');
if (utils.isHeadlineMessage(_converse, message)) {
if (_.includes(from_jid, '@') &&
!_converse.api.contacts.get(from_jid) &&
!_converse.allow_non_roster_messaging) {
return;
}
if (_.isNull(message.querySelector('body'))) {
// Avoid creating a chat box if we have nothing to show
// inside it.
return;
}
const chatbox = _converse.chatboxes.create({
'id': from_jid,
'jid': from_jid,
'type': _converse.HEADLINES_TYPE,
'from': from_jid
});
chatbox.createMessage(message, message);
_converse.emit('message', {'chatbox': chatbox, 'stanza': message});
ChatBoxes: {
model (attrs, options) {
const { _converse } = this.__super__;
if (attrs.type == _converse.HEADLINES_TYPE) {
return new _converse.HeadlinesBox(attrs, options);
} else {
return this.__super__.model.apply(this, arguments);
}
return true;
}
function registerHeadlineHandler () {
_converse.connection.addHandler(onHeadlineMessage, null, 'message');
}
_converse.on('connected', registerHeadlineHandler);
_converse.on('reconnected', registerHeadlineHandler);
_converse.on('chatBoxViewsInitialized', () => {
const that = _converse.chatboxviews;
_converse.chatboxes.on('add', item => {
if (!that.get(item.get('id')) && item.get('type') === _converse.HEADLINES_TYPE) {
that.add(item.get('id'), new _converse.HeadlinesBoxView({model: item}));
}
});
});
},
}
});
}));
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
_converse.HeadlinesBox = _converse.ChatBox.extend({
defaults: {
'type': _converse.HEADLINES_TYPE,
'bookmarked': false,
'chat_state': undefined,
'num_unread': 0,
'url': ''
},
});
_converse.HeadlinesBoxView = _converse.ChatBoxView.extend({
className: 'chatbox headlines',
events: {
'click .close-chatbox-button': 'close',
'click .toggle-chatbox-button': 'minimize',
'keypress textarea.chat-textarea': 'keyPressed'
},
initialize () {
this.initDebounced();
this.disable_mam = true; // Don't do MAM queries for this box
this.model.messages.on('add', this.onMessageAdded, this);
this.model.on('show', this.show, this);
this.model.on('destroy', this.hide, this);
this.model.on('change:minimized', this.onMinimizedChanged, this);
this.render().insertHeading().fetchMessages().insertIntoDOM().hide();
_converse.emit('chatBoxOpened', this);
_converse.emit('chatBoxInitialized', this);
},
render () {
this.el.setAttribute('id', this.model.get('box_id'))
this.el.innerHTML = tpl_chatbox(
_.extend(this.model.toJSON(), {
info_close: '',
label_personal_message: '',
show_send_button: false,
show_toolbar: false,
unread_msgs: ''
}
));
this.content = this.el.querySelector('.chat-content');
return this;
},
// Override to avoid the methods in converse-chatview.js
'renderMessageForm': _.noop,
'afterShown': _.noop
});
function onHeadlineMessage (message) {
/* Handler method for all incoming messages of type "headline". */
const from_jid = message.getAttribute('from');
if (utils.isHeadlineMessage(_converse, message)) {
if (_.includes(from_jid, '@') &&
!_converse.api.contacts.get(from_jid) &&
!_converse.allow_non_roster_messaging) {
return;
}
if (_.isNull(message.querySelector('body'))) {
// Avoid creating a chat box if we have nothing to show
// inside it.
return;
}
const chatbox = _converse.chatboxes.create({
'id': from_jid,
'jid': from_jid,
'type': _converse.HEADLINES_TYPE,
'from': from_jid
});
chatbox.createMessage(message, message);
_converse.emit('message', {'chatbox': chatbox, 'stanza': message});
}
return true;
}
function registerHeadlineHandler () {
_converse.connection.addHandler(onHeadlineMessage, null, 'message');
}
_converse.on('connected', registerHeadlineHandler);
_converse.on('reconnected', registerHeadlineHandler);
_converse.on('chatBoxViewsInitialized', () => {
const that = _converse.chatboxviews;
_converse.chatboxes.on('add', item => {
if (!that.get(item.get('id')) && item.get('type') === _converse.HEADLINES_TYPE) {
that.add(item.get('id'), new _converse.HeadlinesBoxView({model: item}));
}
});
});
}
});

View File

@ -4,273 +4,256 @@
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
(function (root, factory) {
define([
"./utils/html",
"utils/emoji",
"@converse/headless/converse-core",
"xss",
"filesize",
"templates/csn.html",
"templates/file_progress.html",
"templates/info.html",
"templates/message.html",
"templates/message_versions_modal.html",
], factory);
}(this, function (
html,
u,
converse,
xss,
filesize,
tpl_csn,
tpl_file_progress,
tpl_info,
tpl_message,
tpl_message_versions_modal
) {
"use strict";
const { Backbone, _, moment } = converse.env;
import converse from "@converse/headless/converse-core";
import filesize from "filesize";
import html from "./utils/html";
import tpl_csn from "templates/csn.html";
import tpl_file_progress from "templates/file_progress.html";
import tpl_info from "templates/info.html";
import tpl_message from "templates/message.html";
import tpl_message_versions_modal from "templates/message_versions_modal.html";
import u from "utils/emoji";
import xss from "xss";
const { Backbone, _, moment } = converse.env;
converse.plugins.add('converse-message-view', {
converse.plugins.add('converse-message-view', {
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
_converse.api.settings.update({
'show_images_inline': true
});
_converse.api.settings.update({
'show_images_inline': true
});
_converse.MessageVersionsModal = _converse.BootstrapModal.extend({
toHTML () {
return tpl_message_versions_modal(_.extend(
_converse.MessageVersionsModal = _converse.BootstrapModal.extend({
toHTML () {
return tpl_message_versions_modal(_.extend(
this.model.toJSON(), {
'__': __
}));
}
});
_converse.MessageView = _converse.ViewWithAvatar.extend({
events: {
'click .chat-msg__edit-modal': 'showMessageVersionsModal'
},
initialize () {
if (this.model.vcard) {
this.model.vcard.on('change', this.render, this);
}
this.model.on('change', this.onChanged, this);
this.model.on('destroy', this.remove, this);
},
async render () {
const is_followup = u.hasClass('chat-msg--followup', this.el);
if (this.model.isOnlyChatStateNotification()) {
this.renderChatStateNotification()
} else if (this.model.get('file') && !this.model.get('oob_url')) {
this.renderFileUploadProgresBar();
} else if (this.model.get('type') === 'error') {
this.renderErrorMessage();
} else {
await this.renderChatMessage();
}
if (is_followup) {
u.addClass('chat-msg--followup', this.el);
}
return this.el;
},
async onChanged (item) {
// Jot down whether it was edited because the `changed`
// attr gets removed when this.render() gets called further
// down.
const edited = item.changed.edited;
if (this.model.changed.progress) {
return this.renderFileUploadProgresBar();
}
if (_.filter(['correcting', 'message', 'type', 'upload'],
prop => Object.prototype.hasOwnProperty.call(this.model.changed, prop)).length) {
await this.render();
}
if (edited) {
this.onMessageEdited();
}
},
onMessageEdited () {
if (this.model.get('is_archived')) {
return;
}
this.el.addEventListener('animationend', () => u.removeClass('onload', this.el));
u.addClass('onload', this.el);
},
replaceElement (msg) {
if (!_.isNil(this.el.parentElement)) {
this.el.parentElement.replaceChild(msg, this.el);
}
this.setElement(msg);
return this.el;
},
async renderChatMessage () {
const is_me_message = this.isMeCommand(),
moment_time = moment(this.model.get('time')),
role = this.model.vcard ? this.model.vcard.get('role') : null,
roles = role ? role.split(',') : [];
const msg = u.stringToElement(tpl_message(
_.extend(
this.model.toJSON(), {
'__': __
}));
'__': __,
'is_me_message': is_me_message,
'roles': roles,
'pretty_time': moment_time.format(_converse.time_format),
'time': moment_time.format(),
'extra_classes': this.getExtraMessageClasses(),
'label_show': __('Show more'),
'username': this.model.getDisplayName()
})
));
const url = this.model.get('oob_url');
if (url) {
msg.querySelector('.chat-msg__media').innerHTML = _.flow(
_.partial(u.renderFileURL, _converse),
_.partial(u.renderMovieURL, _converse),
_.partial(u.renderAudioURL, _converse),
_.partial(u.renderImageURL, _converse))(url);
}
});
_converse.MessageView = _converse.ViewWithAvatar.extend({
events: {
'click .chat-msg__edit-modal': 'showMessageVersionsModal'
},
initialize () {
if (this.model.vcard) {
this.model.vcard.on('change', this.render, this);
let text = this.getMessageText();
const msg_content = msg.querySelector('.chat-msg__text');
if (text && text !== url) {
if (is_me_message) {
text = text.replace(/^\/me/, '');
}
this.model.on('change', this.onChanged, this);
this.model.on('destroy', this.remove, this);
},
async render () {
const is_followup = u.hasClass('chat-msg--followup', this.el);
if (this.model.isOnlyChatStateNotification()) {
this.renderChatStateNotification()
} else if (this.model.get('file') && !this.model.get('oob_url')) {
this.renderFileUploadProgresBar();
} else if (this.model.get('type') === 'error') {
this.renderErrorMessage();
} else {
await this.renderChatMessage();
}
if (is_followup) {
u.addClass('chat-msg--followup', this.el);
}
return this.el;
},
async onChanged (item) {
// Jot down whether it was edited because the `changed`
// attr gets removed when this.render() gets called further
// down.
const edited = item.changed.edited;
if (this.model.changed.progress) {
return this.renderFileUploadProgresBar();
}
if (_.filter(['correcting', 'message', 'type', 'upload'],
prop => Object.prototype.hasOwnProperty.call(this.model.changed, prop)).length) {
await this.render();
}
if (edited) {
this.onMessageEdited();
}
},
onMessageEdited () {
if (this.model.get('is_archived')) {
return;
}
this.el.addEventListener('animationend', () => u.removeClass('onload', this.el));
u.addClass('onload', this.el);
},
replaceElement (msg) {
if (!_.isNil(this.el.parentElement)) {
this.el.parentElement.replaceChild(msg, this.el);
}
this.setElement(msg);
return this.el;
},
async renderChatMessage () {
const is_me_message = this.isMeCommand(),
moment_time = moment(this.model.get('time')),
role = this.model.vcard ? this.model.vcard.get('role') : null,
roles = role ? role.split(',') : [];
const msg = u.stringToElement(tpl_message(
_.extend(
this.model.toJSON(), {
'__': __,
'is_me_message': is_me_message,
'roles': roles,
'pretty_time': moment_time.format(_converse.time_format),
'time': moment_time.format(),
'extra_classes': this.getExtraMessageClasses(),
'label_show': __('Show more'),
'username': this.model.getDisplayName()
})
));
const url = this.model.get('oob_url');
if (url) {
msg.querySelector('.chat-msg__media').innerHTML = _.flow(
_.partial(u.renderFileURL, _converse),
_.partial(u.renderMovieURL, _converse),
_.partial(u.renderAudioURL, _converse),
_.partial(u.renderImageURL, _converse))(url);
}
let text = this.getMessageText();
const msg_content = msg.querySelector('.chat-msg__text');
if (text && text !== url) {
if (is_me_message) {
text = text.replace(/^\/me/, '');
}
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, _)
)(text);
}
const promises = [];
promises.push(u.renderImageURLs(_converse, msg_content));
if (this.model.get('type') !== 'headline') {
promises.push(this.renderAvatar(msg));
}
await Promise.all(promises);
this.replaceElement(msg);
this.model.collection.trigger('rendered', this);
},
renderErrorMessage () {
const moment_time = moment(this.model.get('time')),
msg = u.stringToElement(
tpl_info(_.extend(this.model.toJSON(), {
'extra_classes': 'chat-error',
'isodate': moment_time.format()
})));
return this.replaceElement(msg);
},
renderChatStateNotification () {
let text;
const from = this.model.get('from'),
name = this.model.getDisplayName();
if (this.model.get('chat_state') === _converse.COMPOSING) {
if (this.model.get('sender') === 'me') {
text = __('Typing from another device');
} else {
text = __('%1$s is typing', name);
}
} else if (this.model.get('chat_state') === _converse.PAUSED) {
if (this.model.get('sender') === 'me') {
text = __('Stopped typing on the other device');
} else {
text = __('%1$s has stopped typing', name);
}
} else if (this.model.get('chat_state') === _converse.GONE) {
text = __('%1$s has gone away', name);
} else {
return;
}
const isodate = moment().format();
this.replaceElement(
u.stringToElement(
tpl_csn({
'message': text,
'from': from,
'isodate': isodate
})));
},
renderFileUploadProgresBar () {
const msg = u.stringToElement(tpl_file_progress(
_.extend(this.model.toJSON(), {
'filesize': filesize(this.model.get('file').size),
})));
this.replaceElement(msg);
this.renderAvatar();
},
showMessageVersionsModal (ev) {
ev.preventDefault();
if (_.isUndefined(this.model.message_versions_modal)) {
this.model.message_versions_modal = new _converse.MessageVersionsModal({'model': this.model});
}
this.model.message_versions_modal.show(ev);
},
getMessageText () {
if (this.model.get('is_encrypted')) {
return this.model.get('plaintext') ||
(_converse.debug ? __('Unencryptable OMEMO message') : null);
}
return this.model.get('message');
},
isMeCommand () {
const text = this.getMessageText();
if (!text) {
return false;
}
const match = text.match(/^\/(.*?)(?: (.*))?$/);
return match && match[1] === 'me';
},
processMessageText () {
var text = this.get('message');
text = u.geoUriToHttp(text, _converse.geouri_replacement);
},
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)) {
// Add special class to mark groupchat messages
// in which we are mentioned.
extra_classes += ' mentioned';
}
}
if (this.model.get('correcting')) {
extra_classes += ' correcting';
}
return extra_classes;
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, _)
)(text);
}
});
}
});
return converse;
}));
const promises = [];
promises.push(u.renderImageURLs(_converse, msg_content));
if (this.model.get('type') !== 'headline') {
promises.push(this.renderAvatar(msg));
}
await Promise.all(promises);
this.replaceElement(msg);
this.model.collection.trigger('rendered', this);
},
renderErrorMessage () {
const moment_time = moment(this.model.get('time')),
msg = u.stringToElement(
tpl_info(_.extend(this.model.toJSON(), {
'extra_classes': 'chat-error',
'isodate': moment_time.format()
})));
return this.replaceElement(msg);
},
renderChatStateNotification () {
let text;
const from = this.model.get('from'),
name = this.model.getDisplayName();
if (this.model.get('chat_state') === _converse.COMPOSING) {
if (this.model.get('sender') === 'me') {
text = __('Typing from another device');
} else {
text = __('%1$s is typing', name);
}
} else if (this.model.get('chat_state') === _converse.PAUSED) {
if (this.model.get('sender') === 'me') {
text = __('Stopped typing on the other device');
} else {
text = __('%1$s has stopped typing', name);
}
} else if (this.model.get('chat_state') === _converse.GONE) {
text = __('%1$s has gone away', name);
} else {
return;
}
const isodate = moment().format();
this.replaceElement(
u.stringToElement(
tpl_csn({
'message': text,
'from': from,
'isodate': isodate
})));
},
renderFileUploadProgresBar () {
const msg = u.stringToElement(tpl_file_progress(
_.extend(this.model.toJSON(), {
'filesize': filesize(this.model.get('file').size),
})));
this.replaceElement(msg);
this.renderAvatar();
},
showMessageVersionsModal (ev) {
ev.preventDefault();
if (_.isUndefined(this.model.message_versions_modal)) {
this.model.message_versions_modal = new _converse.MessageVersionsModal({'model': this.model});
}
this.model.message_versions_modal.show(ev);
},
getMessageText () {
if (this.model.get('is_encrypted')) {
return this.model.get('plaintext') ||
(_converse.debug ? __('Unencryptable OMEMO message') : null);
}
return this.model.get('message');
},
isMeCommand () {
const text = this.getMessageText();
if (!text) {
return false;
}
const match = text.match(/^\/(.*?)(?: (.*))?$/);
return match && match[1] === 'me';
},
processMessageText () {
var text = this.get('message');
text = u.geoUriToHttp(text, _converse.geouri_replacement);
},
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)) {
// Add special class to mark groupchat messages
// in which we are mentioned.
extra_classes += ' mentioned';
}
}
if (this.model.get('correcting')) {
extra_classes += ' correcting';
}
return extra_classes;
}
});
}
});

File diff suppressed because it is too large Load Diff

View File

@ -1,117 +1,111 @@
// Converse.js
// http://conversejs.org
//
// Copyright (c) 2018, the Converse.js developers
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([
"@converse/headless/converse-core",
"templates/alert_modal.html",
"bootstrap",
"backbone.vdomview"
], factory);
}
}(this, function (converse, tpl_alert_modal, bootstrap) {
"use strict";
import "backbone.vdomview";
import bootstrap from "bootstrap";
import converse from "@converse/headless/converse-core";
import tpl_alert_modal from "templates/alert_modal.html";
const { Strophe, Backbone, _ } = converse.env;
const { Strophe, Backbone, _ } = converse.env;
converse.plugins.add('converse-modal', {
initialize () {
const { _converse } = this;
converse.plugins.add('converse-modal', {
_converse.BootstrapModal = Backbone.VDOMView.extend({
initialize () {
const { _converse } = this;
initialize () {
this.render().insertIntoDOM();
this.modal = new bootstrap.Modal(this.el, {
backdrop: 'static',
keyboard: true
});
this.el.addEventListener('hide.bs.modal', (event) => {
if (!_.isNil(this.trigger_el)) {
this.trigger_el.classList.remove('selected');
}
}, false);
},
_converse.BootstrapModal = Backbone.VDOMView.extend({
insertIntoDOM () {
const container_el = _converse.chatboxviews.el.querySelector("#converse-modals");
container_el.insertAdjacentElement('beforeEnd', this.el);
},
show (ev) {
if (ev) {
ev.preventDefault();
this.trigger_el = ev.target;
this.trigger_el.classList.add('selected');
initialize () {
this.render().insertIntoDOM();
this.modal = new bootstrap.Modal(this.el, {
backdrop: 'static',
keyboard: true
});
this.el.addEventListener('hide.bs.modal', (event) => {
if (!_.isNil(this.trigger_el)) {
this.trigger_el.classList.remove('selected');
}
this.modal.show();
}, false);
},
insertIntoDOM () {
const container_el = _converse.chatboxviews.el.querySelector("#converse-modals");
container_el.insertAdjacentElement('beforeEnd', this.el);
},
show (ev) {
if (ev) {
ev.preventDefault();
this.trigger_el = ev.target;
this.trigger_el.classList.add('selected');
}
});
this.modal.show();
}
});
_converse.Alert = _converse.BootstrapModal.extend({
_converse.Alert = _converse.BootstrapModal.extend({
initialize () {
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
this.model.on('change', this.render, this);
},
initialize () {
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
this.model.on('change', this.render, this);
},
toHTML () {
return tpl_alert_modal(this.model.toJSON());
}
});
toHTML () {
return tpl_alert_modal(this.model.toJSON());
}
});
_converse.api.listen.on('afterTearDown', () => {
if (!_converse.chatboxviews) {
return;
}
const container = _converse.chatboxviews.el.querySelector("#converse-modals");
if (container) {
container.innerHTML = '';
}
});
_converse.api.listen.on('afterTearDown', () => {
if (!_converse.chatboxviews) {
return;
}
const container = _converse.chatboxviews.el.querySelector("#converse-modals");
if (container) {
container.innerHTML = '';
}
});
/************************ BEGIN API ************************/
// We extend the default converse.js API to add methods specific to MUC chat rooms.
let alert
/************************ BEGIN API ************************/
// We extend the default converse.js API to add methods specific to MUC chat rooms.
let alert
_.extend(_converse.api, {
'alert': {
'show' (type, title, messages) {
if (_.isString(messages)) {
messages = [messages];
}
if (type === Strophe.LogLevel.ERROR) {
type = 'alert-danger';
} else if (type === Strophe.LogLevel.INFO) {
type = 'alert-info';
} else if (type === Strophe.LogLevel.WARN) {
type = 'alert-warning';
}
if (_.isUndefined(alert)) {
const model = new Backbone.Model({
'title': title,
'messages': messages,
'type': type
})
alert = new _converse.Alert({'model': model});
} else {
alert.model.set({
'title': title,
'messages': messages,
'type': type
});
}
alert.show();
_.extend(_converse.api, {
'alert': {
'show' (type, title, messages) {
if (_.isString(messages)) {
messages = [messages];
}
if (type === Strophe.LogLevel.ERROR) {
type = 'alert-danger';
} else if (type === Strophe.LogLevel.INFO) {
type = 'alert-info';
} else if (type === Strophe.LogLevel.WARN) {
type = 'alert-warning';
}
if (_.isUndefined(alert)) {
const model = new Backbone.Model({
'title': title,
'messages': messages,
'type': type
})
alert = new _converse.Alert({'model': model});
} else {
alert.model.set({
'title': title,
'messages': messages,
'type': type
});
}
alert.show();
}
});
}
});
}));
}
});
}
});

File diff suppressed because it is too large Load Diff

View File

@ -6,286 +6,285 @@
//
/*global define */
(function (root, factory) {
define(["@converse/headless/converse-core"], factory);
}(this, function (converse) {
"use strict";
const { Strophe, _, sizzle } = converse.env,
u = converse.env.utils;
import converse from "@converse/headless/converse-core";
converse.plugins.add('converse-notification', {
const { Strophe, _, sizzle } = converse.env,
u = converse.env.utils;
dependencies: ["converse-chatboxes"],
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
converse.plugins.add('converse-notification', {
dependencies: ["converse-chatboxes"],
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this;
const { __ } = _converse;
_converse.supports_html5_notification = "Notification" in window;
_converse.api.settings.update({
notify_all_room_messages: false,
show_desktop_notifications: true,
show_chatstate_notifications: false,
chatstate_notification_blacklist: [],
// ^ a list of JIDs to ignore concerning chat state notifications
play_sounds: true,
sounds_path: '/sounds/',
notification_icon: '/logo/conversejs-filled.svg'
});
_converse.isOnlyChatStateNotification = (msg) =>
// See XEP-0085 Chat State Notification
_.isNull(msg.querySelector('body')) && (
_.isNull(msg.querySelector(_converse.ACTIVE)) ||
_.isNull(msg.querySelector(_converse.COMPOSING)) ||
_.isNull(msg.querySelector(_converse.INACTIVE)) ||
_.isNull(msg.querySelector(_converse.PAUSED)) ||
_.isNull(msg.querySelector(_converse.GONE))
)
;
_converse.shouldNotifyOfGroupMessage = function (message) {
/* Is this a group message worthy of notification?
*/
const { _converse } = this;
const { __ } = _converse;
let notify_all = _converse.notify_all_room_messages;
const jid = message.getAttribute('from'),
resource = Strophe.getResourceFromJid(jid),
room_jid = Strophe.getBareJidFromJid(jid),
sender = resource && Strophe.unescapeNode(resource) || '';
if (sender === '' || message.querySelectorAll('delay').length > 0) {
return false;
}
const room = _converse.chatboxes.get(room_jid);
const body = message.querySelector('body');
if (_.isNull(body)) {
return false;
}
const mentioned = (new RegExp(`\\b${room.get('nick')}\\b`)).test(body.textContent);
notify_all = notify_all === true ||
(_.isArray(notify_all) && _.includes(notify_all, room_jid));
if (sender === room.get('nick') || (!notify_all && !mentioned)) {
return false;
}
return true;
};
_converse.supports_html5_notification = "Notification" in window;
_converse.isMessageToHiddenChat = function (message) {
if (_.includes(['mobile', 'fullscreen', 'embedded'], _converse.view_mode)) {
const jid = Strophe.getBareJidFromJid(message.getAttribute('from')),
view = _converse.chatboxviews.get(jid);
_converse.api.settings.update({
notify_all_room_messages: false,
show_desktop_notifications: true,
show_chatstate_notifications: false,
chatstate_notification_blacklist: [],
// ^ a list of JIDs to ignore concerning chat state notifications
play_sounds: true,
sounds_path: '/sounds/',
notification_icon: '/logo/conversejs-filled.svg'
});
_converse.isOnlyChatStateNotification = (msg) =>
// See XEP-0085 Chat State Notification
_.isNull(msg.querySelector('body')) && (
_.isNull(msg.querySelector(_converse.ACTIVE)) ||
_.isNull(msg.querySelector(_converse.COMPOSING)) ||
_.isNull(msg.querySelector(_converse.INACTIVE)) ||
_.isNull(msg.querySelector(_converse.PAUSED)) ||
_.isNull(msg.querySelector(_converse.GONE))
)
;
_converse.shouldNotifyOfGroupMessage = function (message) {
/* Is this a group message worthy of notification?
*/
let notify_all = _converse.notify_all_room_messages;
const jid = message.getAttribute('from'),
resource = Strophe.getResourceFromJid(jid),
room_jid = Strophe.getBareJidFromJid(jid),
sender = resource && Strophe.unescapeNode(resource) || '';
if (sender === '' || message.querySelectorAll('delay').length > 0) {
return false;
}
const room = _converse.chatboxes.get(room_jid);
const body = message.querySelector('body');
if (_.isNull(body)) {
return false;
}
const mentioned = (new RegExp(`\\b${room.get('nick')}\\b`)).test(body.textContent);
notify_all = notify_all === true ||
(_.isArray(notify_all) && _.includes(notify_all, room_jid));
if (sender === room.get('nick') || (!notify_all && !mentioned)) {
return false;
if (!_.isNil(view)) {
return view.model.get('hidden') || _converse.windowState === 'hidden' || !u.isVisible(view.el);
}
return true;
};
_converse.isMessageToHiddenChat = function (message) {
if (_.includes(['mobile', 'fullscreen', 'embedded'], _converse.view_mode)) {
const jid = Strophe.getBareJidFromJid(message.getAttribute('from')),
view = _converse.chatboxviews.get(jid);
if (!_.isNil(view)) {
return view.model.get('hidden') || _converse.windowState === 'hidden' || !u.isVisible(view.el);
}
return true;
}
return _converse.windowState === 'hidden';
}
_converse.shouldNotifyOfMessage = function (message) {
const forwarded = message.querySelector('forwarded');
if (!_.isNull(forwarded)) {
return false;
} else if (message.getAttribute('type') === 'groupchat') {
return _converse.shouldNotifyOfGroupMessage(message);
} else if (u.isHeadlineMessage(_converse, message)) {
// We want to show notifications for headline messages.
return _converse.isMessageToHiddenChat(message);
}
const is_me = Strophe.getBareJidFromJid(
message.getAttribute('from')) === _converse.bare_jid;
return !_converse.isOnlyChatStateNotification(message) &&
!is_me &&
_converse.isMessageToHiddenChat(message);
};
_converse.playSoundNotification = function () {
/* Plays a sound to notify that a new message was recieved.
*/
// XXX Eventually this can be refactored to use Notification's sound
// feature, but no browser currently supports it.
// https://developer.mozilla.org/en-US/docs/Web/API/notification/sound
let audio;
if (_converse.play_sounds && !_.isUndefined(window.Audio)) {
audio = new Audio(_converse.sounds_path+"msg_received.ogg");
if (audio.canPlayType('audio/ogg')) {
audio.play();
} else {
audio = new Audio(_converse.sounds_path+"msg_received.mp3");
if (audio.canPlayType('audio/mp3')) {
audio.play();
}
}
}
};
_converse.areDesktopNotificationsEnabled = function () {
return _converse.supports_html5_notification &&
_converse.show_desktop_notifications &&
Notification.permission === "granted";
};
_converse.showMessageNotification = function (message) {
/* Shows an HTML5 Notification to indicate that a new chat
* message was received.
*/
let title, roster_item;
const full_from_jid = message.getAttribute('from'),
from_jid = Strophe.getBareJidFromJid(full_from_jid);
if (message.getAttribute('type') === 'headline') {
if (!_.includes(from_jid, '@') || _converse.allow_non_roster_messaging) {
title = __("Notification from %1$s", from_jid);
} else {
return;
}
} else if (!_.includes(from_jid, '@')) {
// workaround for Prosody which doesn't give type "headline"
title = __("Notification from %1$s", from_jid);
} else if (message.getAttribute('type') === 'groupchat') {
title = __("%1$s says", Strophe.getResourceFromJid(full_from_jid));
} else {
if (_.isUndefined(_converse.roster)) {
_converse.log(
"Could not send notification, because roster is undefined",
Strophe.LogLevel.ERROR);
return;
}
roster_item = _converse.roster.get(from_jid);
if (!_.isUndefined(roster_item)) {
title = __("%1$s says", roster_item.getDisplayName());
} else {
if (_converse.allow_non_roster_messaging) {
title = __("%1$s says", from_jid);
} else {
return;
}
}
}
// TODO: we should suppress notifications if we cannot decrypt
// the message...
const body = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, message).length ?
__('OMEMO Message received') :
_.get(message.querySelector('body'), 'textContent');
if (!body) {
return;
}
const n = new Notification(title, {
'body': body,
'lang': _converse.locale,
'icon': _converse.notification_icon
});
setTimeout(n.close.bind(n), 5000);
};
_converse.showChatStateNotification = function (contact) {
/* Creates an HTML5 Notification to inform of a change in a
* contact's chat state.
*/
if (_.includes(_converse.chatstate_notification_blacklist, contact.jid)) {
// Don't notify if the user is being ignored.
return;
}
const chat_state = contact.chat_status;
let message = null;
if (chat_state === 'offline') {
message = __('has gone offline');
} else if (chat_state === 'away') {
message = __('has gone away');
} else if ((chat_state === 'dnd')) {
message = __('is busy');
} else if (chat_state === 'online') {
message = __('has come online');
}
if (message === null) {
return;
}
const n = new Notification(contact.getDisplayName(), {
body: message,
lang: _converse.locale,
icon: _converse.notification_icon
});
setTimeout(n.close.bind(n), 5000);
};
_converse.showContactRequestNotification = function (contact) {
const n = new Notification(contact.getDisplayName(), {
body: __('wants to be your contact'),
lang: _converse.locale,
icon: _converse.notification_icon
});
setTimeout(n.close.bind(n), 5000);
};
_converse.showFeedbackNotification = function (data) {
if (data.klass === 'error' || data.klass === 'warn') {
const n = new Notification(data.subject, {
body: data.message,
lang: _converse.locale,
icon: _converse.notification_icon
});
setTimeout(n.close.bind(n), 5000);
}
};
_converse.handleChatStateNotification = function (contact) {
/* Event handler for on('contactPresenceChanged').
* Will show an HTML5 notification to indicate that the chat
* status has changed.
*/
if (_converse.areDesktopNotificationsEnabled() &&
_converse.show_chatstate_notifications) {
_converse.showChatStateNotification(contact);
}
};
_converse.handleMessageNotification = function (data) {
/* Event handler for the on('message') event. Will call methods
* to play sounds and show HTML5 notifications.
*/
const message = data.stanza;
if (!_converse.shouldNotifyOfMessage(message)) {
return false;
}
_converse.playSoundNotification();
if (_converse.areDesktopNotificationsEnabled()) {
_converse.showMessageNotification(message);
}
};
_converse.handleContactRequestNotification = function (contact) {
if (_converse.areDesktopNotificationsEnabled(true)) {
_converse.showContactRequestNotification(contact);
}
};
_converse.handleFeedback = function (data) {
if (_converse.areDesktopNotificationsEnabled(true)) {
_converse.showFeedbackNotification(data);
}
};
_converse.requestPermission = function () {
if (_converse.supports_html5_notification &&
! _.includes(['denied', 'granted'], Notification.permission)) {
// Ask user to enable HTML5 notifications
Notification.requestPermission();
}
};
_converse.on('pluginsInitialized', function () {
// We only register event handlers after all plugins are
// registered, because other plugins might override some of our
// handlers.
_converse.on('contactRequest', _converse.handleContactRequestNotification);
_converse.on('contactPresenceChanged', _converse.handleChatStateNotification);
_converse.on('message', _converse.handleMessageNotification);
_converse.on('feedback', _converse.handleFeedback);
_converse.on('connected', _converse.requestPermission);
});
return _converse.windowState === 'hidden';
}
});
}));
_converse.shouldNotifyOfMessage = function (message) {
const forwarded = message.querySelector('forwarded');
if (!_.isNull(forwarded)) {
return false;
} else if (message.getAttribute('type') === 'groupchat') {
return _converse.shouldNotifyOfGroupMessage(message);
} else if (u.isHeadlineMessage(_converse, message)) {
// We want to show notifications for headline messages.
return _converse.isMessageToHiddenChat(message);
}
const is_me = Strophe.getBareJidFromJid(
message.getAttribute('from')) === _converse.bare_jid;
return !_converse.isOnlyChatStateNotification(message) &&
!is_me &&
_converse.isMessageToHiddenChat(message);
};
_converse.playSoundNotification = function () {
/* Plays a sound to notify that a new message was recieved.
*/
// XXX Eventually this can be refactored to use Notification's sound
// feature, but no browser currently supports it.
// https://developer.mozilla.org/en-US/docs/Web/API/notification/sound
let audio;
if (_converse.play_sounds && !_.isUndefined(window.Audio)) {
audio = new Audio(_converse.sounds_path+"msg_received.ogg");
if (audio.canPlayType('audio/ogg')) {
audio.play();
} else {
audio = new Audio(_converse.sounds_path+"msg_received.mp3");
if (audio.canPlayType('audio/mp3')) {
audio.play();
}
}
}
};
_converse.areDesktopNotificationsEnabled = function () {
return _converse.supports_html5_notification &&
_converse.show_desktop_notifications &&
Notification.permission === "granted";
};
_converse.showMessageNotification = function (message) {
/* Shows an HTML5 Notification to indicate that a new chat
* message was received.
*/
let title, roster_item;
const full_from_jid = message.getAttribute('from'),
from_jid = Strophe.getBareJidFromJid(full_from_jid);
if (message.getAttribute('type') === 'headline') {
if (!_.includes(from_jid, '@') || _converse.allow_non_roster_messaging) {
title = __("Notification from %1$s", from_jid);
} else {
return;
}
} else if (!_.includes(from_jid, '@')) {
// workaround for Prosody which doesn't give type "headline"
title = __("Notification from %1$s", from_jid);
} else if (message.getAttribute('type') === 'groupchat') {
title = __("%1$s says", Strophe.getResourceFromJid(full_from_jid));
} else {
if (_.isUndefined(_converse.roster)) {
_converse.log(
"Could not send notification, because roster is undefined",
Strophe.LogLevel.ERROR);
return;
}
roster_item = _converse.roster.get(from_jid);
if (!_.isUndefined(roster_item)) {
title = __("%1$s says", roster_item.getDisplayName());
} else {
if (_converse.allow_non_roster_messaging) {
title = __("%1$s says", from_jid);
} else {
return;
}
}
}
// TODO: we should suppress notifications if we cannot decrypt
// the message...
const body = sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, message).length ?
__('OMEMO Message received') :
_.get(message.querySelector('body'), 'textContent');
if (!body) {
return;
}
const n = new Notification(title, {
'body': body,
'lang': _converse.locale,
'icon': _converse.notification_icon
});
setTimeout(n.close.bind(n), 5000);
};
_converse.showChatStateNotification = function (contact) {
/* Creates an HTML5 Notification to inform of a change in a
* contact's chat state.
*/
if (_.includes(_converse.chatstate_notification_blacklist, contact.jid)) {
// Don't notify if the user is being ignored.
return;
}
const chat_state = contact.chat_status;
let message = null;
if (chat_state === 'offline') {
message = __('has gone offline');
} else if (chat_state === 'away') {
message = __('has gone away');
} else if ((chat_state === 'dnd')) {
message = __('is busy');
} else if (chat_state === 'online') {
message = __('has come online');
}
if (message === null) {
return;
}
const n = new Notification(contact.getDisplayName(), {
body: message,
lang: _converse.locale,
icon: _converse.notification_icon
});
setTimeout(n.close.bind(n), 5000);
};
_converse.showContactRequestNotification = function (contact) {
const n = new Notification(contact.getDisplayName(), {
body: __('wants to be your contact'),
lang: _converse.locale,
icon: _converse.notification_icon
});
setTimeout(n.close.bind(n), 5000);
};
_converse.showFeedbackNotification = function (data) {
if (data.klass === 'error' || data.klass === 'warn') {
const n = new Notification(data.subject, {
body: data.message,
lang: _converse.locale,
icon: _converse.notification_icon
});
setTimeout(n.close.bind(n), 5000);
}
};
_converse.handleChatStateNotification = function (contact) {
/* Event handler for on('contactPresenceChanged').
* Will show an HTML5 notification to indicate that the chat
* status has changed.
*/
if (_converse.areDesktopNotificationsEnabled() &&
_converse.show_chatstate_notifications) {
_converse.showChatStateNotification(contact);
}
};
_converse.handleMessageNotification = function (data) {
/* Event handler for the on('message') event. Will call methods
* to play sounds and show HTML5 notifications.
*/
const message = data.stanza;
if (!_converse.shouldNotifyOfMessage(message)) {
return false;
}
_converse.playSoundNotification();
if (_converse.areDesktopNotificationsEnabled()) {
_converse.showMessageNotification(message);
}
};
_converse.handleContactRequestNotification = function (contact) {
if (_converse.areDesktopNotificationsEnabled(true)) {
_converse.showContactRequestNotification(contact);
}
};
_converse.handleFeedback = function (data) {
if (_converse.areDesktopNotificationsEnabled(true)) {
_converse.showFeedbackNotification(data);
}
};
_converse.requestPermission = function () {
if (_converse.supports_html5_notification &&
! _.includes(['denied', 'granted'], Notification.permission)) {
// Ask user to enable HTML5 notifications
Notification.requestPermission();
}
};
_converse.on('pluginsInitialized', function () {
// We only register event handlers after all plugins are
// registered, because other plugins might override some of our
// handlers.
_converse.on('contactRequest', _converse.handleContactRequestNotification);
_converse.on('contactPresenceChanged', _converse.handleChatStateNotification);
_converse.on('message', _converse.handleMessageNotification);
_converse.on('feedback', _converse.handleFeedback);
_converse.on('connected', _converse.requestPermission);
});
}
});

View File

@ -4,143 +4,136 @@
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as a module called "myplugin"
define(["@converse/headless/converse-core", "templates/oauth_providers.html", "hellojs"], factory);
} else {
// Browser globals. If you're not using a module loader such as require.js,
// then this line below executes. Make sure that your plugin's <script> tag
// appears after the one from converse.js.
factory(converse);
}
}(this, function (converse, tpl_oauth_providers, hello) {
'use strict';
const _ = converse.env._,
Backbone = converse.env.Backbone,
Strophe = converse.env.Strophe;
import converse from "@converse/headless/converse-core";
import hello from "hellojs";
import tpl_oauth_providers from "templates/oauth_providers.html";
// The following line registers your plugin.
converse.plugins.add("converse-oauth", {
const _ = converse.env._,
Backbone = converse.env.Backbone,
Strophe = converse.env.Strophe;
/* Optional dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin. They are called "optional" because they might not be
* available, in which case any overrides applicable to them will be
* ignored.
*
* NB: These plugins need to have already been loaded via require.js.
*
* It's possible to make optional dependencies non-optional.
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found.
// The following line registers your plugin.
converse.plugins.add("converse-oauth", {
/* Optional dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin. They are called "optional" because they might not be
* available, in which case any overrides applicable to them will be
* ignored.
*
* NB: These plugins need to have already been loaded via require.js.
*
* It's possible to make optional dependencies non-optional.
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found.
*/
'optional_dependencies': ['converse-register'],
/* If you want to override some function or a Backbone model or
* view defined elsewhere in converse.js, then you do that under
* the "overrides" namespace.
*/
'overrides': {
/* For example, the private *_converse* object has a
* method "onConnected". You can override that method as follows:
*/
'optional_dependencies': ['converse-register'],
'LoginPanel': {
/* If you want to override some function or a Backbone model or
* view defined elsewhere in converse.js, then you do that under
* the "overrides" namespace.
*/
'overrides': {
/* For example, the private *_converse* object has a
* method "onConnected". You can override that method as follows:
*/
'LoginPanel': {
insertOAuthProviders () {
const { _converse } = this.__super__;
if (_.isUndefined(this.oauth_providers_view)) {
this.oauth_providers_view =
new _converse.OAuthProvidersView({'model': _converse.oauth_providers});
insertOAuthProviders () {
const { _converse } = this.__super__;
if (_.isUndefined(this.oauth_providers_view)) {
this.oauth_providers_view =
new _converse.OAuthProvidersView({'model': _converse.oauth_providers});
this.oauth_providers_view.render();
this.el.querySelector('.buttons').insertAdjacentElement(
'afterend',
this.oauth_providers_view.el
);
}
this.oauth_providers_view.render();
},
render (cfg) {
const { _converse } = this.__super__;
const result = this.__super__.render.apply(this, arguments);
if (_converse.oauth_providers && !_converse.auto_login) {
this.insertOAuthProviders();
}
return result;
}
}
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
_converse.api.settings.update({
'oauth_providers': {},
});
_converse.OAuthProviders = Backbone.Collection.extend({
'sync': __.noop,
initialize () {
_.each(_converse.user_settings.oauth_providers, (provider) => {
const item = new Backbone.Model(_.extend(provider, {
'login_text': __('Log in with %1$s', provider.name)
}));
this.add(item, {'silent': true});
});
}
});
_converse.oauth_providers = new _converse.OAuthProviders();
_converse.OAuthProvidersView = Backbone.VDOMView.extend({
'events': {
'click .oauth-login': 'oauthLogin'
},
toHTML () {
return tpl_oauth_providers(
_.extend({
'_': _,
'__': _converse.__,
'providers': this.model.toJSON()
}));
},
fetchOAuthProfileDataAndLogin () {
this.oauth_service.api('me').then((profile) => {
const response = this.oauth_service.getAuthResponse();
_converse.api.user.login({
'jid': `${profile.name}@${this.provider.get('host')}`,
'password': response.access_token
});
});
},
oauthLogin (ev) {
ev.preventDefault();
const id = ev.target.getAttribute('data-id');
this.provider = _converse.oauth_providers.get(id);
this.oauth_service = hello(id);
const data = {};
data[id] = this.provider.get('client_id');
hello.init(data, {
'redirect_uri': '/redirect.html'
});
this.oauth_service.login().then(
() => this.fetchOAuthProfileDataAndLogin(),
(error) => _converse.log(error.error_message, Strophe.LogLevel.ERROR)
this.el.querySelector('.buttons').insertAdjacentElement(
'afterend',
this.oauth_providers_view.el
);
}
});
this.oauth_providers_view.render();
},
render (cfg) {
const { _converse } = this.__super__;
const result = this.__super__.render.apply(this, arguments);
if (_converse.oauth_providers && !_converse.auto_login) {
this.insertOAuthProviders();
}
return result;
}
}
});
}));
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
_converse.api.settings.update({
'oauth_providers': {},
});
_converse.OAuthProviders = Backbone.Collection.extend({
'sync': __.noop,
initialize () {
_.each(_converse.user_settings.oauth_providers, (provider) => {
const item = new Backbone.Model(_.extend(provider, {
'login_text': __('Log in with %1$s', provider.name)
}));
this.add(item, {'silent': true});
});
}
});
_converse.oauth_providers = new _converse.OAuthProviders();
_converse.OAuthProvidersView = Backbone.VDOMView.extend({
'events': {
'click .oauth-login': 'oauthLogin'
},
toHTML () {
return tpl_oauth_providers(
_.extend({
'_': _,
'__': _converse.__,
'providers': this.model.toJSON()
}));
},
fetchOAuthProfileDataAndLogin () {
this.oauth_service.api('me').then((profile) => {
const response = this.oauth_service.getAuthResponse();
_converse.api.user.login({
'jid': `${profile.name}@${this.provider.get('host')}`,
'password': response.access_token
});
});
},
oauthLogin (ev) {
ev.preventDefault();
const id = ev.target.getAttribute('data-id');
this.provider = _converse.oauth_providers.get(id);
this.oauth_service = hello(id);
const data = {};
data[id] = this.provider.get('client_id');
hello.init(data, {
'redirect_uri': '/redirect.html'
});
this.oauth_service.login().then(
() => this.fetchOAuthProfileDataAndLogin(),
(error) => _converse.log(error.error_message, Strophe.LogLevel.ERROR)
);
}
});
}
});

File diff suppressed because it is too large Load Diff

View File

@ -6,271 +6,259 @@
//
/*global define */
(function (root, factory) {
define(["@converse/headless/converse-core",
"bootstrap",
"formdata-polyfill",
"templates/alert.html",
"templates/chat_status_modal.html",
"templates/profile_modal.html",
"templates/profile_view.html",
"templates/status_option.html",
"@converse/headless/converse-vcard",
"converse-modal"
], factory);
}(this, function (
converse,
bootstrap,
_FormData,
tpl_alert,
tpl_chat_status_modal,
tpl_profile_modal,
tpl_profile_view,
tpl_status_option
) {
"use strict";
const { Strophe, Backbone, Promise, utils, _, moment } = converse.env;
const u = converse.env.utils;
import "@converse/headless/converse-vcard";
import "converse-modal";
import _FormData from "formdata-polyfill";
import bootstrap from "bootstrap";
import converse from "@converse/headless/converse-core";
import tpl_alert from "templates/alert.html";
import tpl_chat_status_modal from "templates/chat_status_modal.html";
import tpl_profile_modal from "templates/profile_modal.html";
import tpl_profile_view from "templates/profile_view.html";
import tpl_status_option from "templates/status_option.html";
converse.plugins.add('converse-profile', {
dependencies: ["converse-modal", "converse-vcard", "converse-chatboxviews"],
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
const { Strophe, Backbone, Promise, utils, _, moment } = converse.env;
const u = converse.env.utils;
_converse.ProfileModal = _converse.BootstrapModal.extend({
events: {
'click .change-avatar': "openFileSelection",
'change input[type="file"': "updateFilePreview",
'submit .profile-form': 'onFormSubmitted'
},
converse.plugins.add('converse-profile', {
initialize () {
this.model.on('change', this.render, this);
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
_converse.emit('profileModalInitialized', this.model);
},
dependencies: ["converse-modal", "converse-vcard", "converse-chatboxviews"],
toHTML () {
return tpl_profile_modal(_.extend(
this.model.toJSON(),
this.model.vcard.toJSON(), {
'_': _,
'__': __,
'_converse': _converse,
'alt_avatar': __('Your avatar image'),
'heading_profile': __('Your Profile'),
'label_close': __('Close'),
'label_email': __('Email'),
'label_fullname': __('Full Name'),
'label_jid': __('XMPP Address (JID)'),
'label_nickname': __('Nickname'),
'label_role': __('Role'),
'label_role_help': __('Use commas to separate multiple roles. Your roles are shown next to your name on your chat messages.'),
'label_url': __('URL'),
'utils': u,
'view': this
}));
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
afterRender () {
this.tabs = _.map(this.el.querySelectorAll('.nav-item'), (tab) => new bootstrap.Tab(tab));
},
openFileSelection (ev) {
ev.preventDefault();
this.el.querySelector('input[type="file"]').click();
},
_converse.ProfileModal = _converse.BootstrapModal.extend({
events: {
'click .change-avatar': "openFileSelection",
'change input[type="file"': "updateFilePreview",
'submit .profile-form': 'onFormSubmitted'
},
updateFilePreview (ev) {
const file = ev.target.files[0],
reader = new FileReader();
reader.onloadend = () => {
this.el.querySelector('.avatar').setAttribute('src', reader.result);
};
reader.readAsDataURL(file);
},
initialize () {
this.model.on('change', this.render, this);
_converse.BootstrapModal.prototype.initialize.apply(this, arguments);
_converse.emit('profileModalInitialized', this.model);
},
setVCard (data) {
_converse.api.vcard.set(_converse.bare_jid, data)
.then(() => _converse.api.vcard.update(this.model.vcard, true))
.catch((err) => {
_converse.log(err, Strophe.LogLevel.FATAL);
_converse.api.alert.show(
Strophe.LogLevel.ERROR,
__('Error'),
[__("Sorry, an error happened while trying to save your profile data."),
__("You can check your browser's developer console for any error output.")]
)
toHTML () {
return tpl_profile_modal(_.extend(
this.model.toJSON(),
this.model.vcard.toJSON(), {
'_': _,
'__': __,
'_converse': _converse,
'alt_avatar': __('Your avatar image'),
'heading_profile': __('Your Profile'),
'label_close': __('Close'),
'label_email': __('Email'),
'label_fullname': __('Full Name'),
'label_jid': __('XMPP Address (JID)'),
'label_nickname': __('Nickname'),
'label_role': __('Role'),
'label_role_help': __('Use commas to separate multiple roles. Your roles are shown next to your name on your chat messages.'),
'label_url': __('URL'),
'utils': u,
'view': this
}));
},
afterRender () {
this.tabs = _.map(this.el.querySelectorAll('.nav-item'), (tab) => new bootstrap.Tab(tab));
},
openFileSelection (ev) {
ev.preventDefault();
this.el.querySelector('input[type="file"]').click();
},
updateFilePreview (ev) {
const file = ev.target.files[0],
reader = new FileReader();
reader.onloadend = () => {
this.el.querySelector('.avatar').setAttribute('src', reader.result);
};
reader.readAsDataURL(file);
},
setVCard (data) {
_converse.api.vcard.set(_converse.bare_jid, data)
.then(() => _converse.api.vcard.update(this.model.vcard, true))
.catch((err) => {
_converse.log(err, Strophe.LogLevel.FATAL);
_converse.api.alert.show(
Strophe.LogLevel.ERROR,
__('Error'),
[__("Sorry, an error happened while trying to save your profile data."),
__("You can check your browser's developer console for any error output.")]
)
});
this.modal.hide();
},
onFormSubmitted (ev) {
ev.preventDefault();
const reader = new FileReader(),
form_data = new FormData(ev.target),
image_file = form_data.get('image');
const data = {
'fn': form_data.get('fn'),
'nickname': form_data.get('nickname'),
'role': form_data.get('role'),
'email': form_data.get('email'),
'url': form_data.get('url'),
};
if (!image_file.size) {
_.extend(data, {
'image': this.model.vcard.get('image'),
'image_type': this.model.vcard.get('image_type')
});
this.modal.hide();
},
onFormSubmitted (ev) {
ev.preventDefault();
const reader = new FileReader(),
form_data = new FormData(ev.target),
image_file = form_data.get('image');
const data = {
'fn': form_data.get('fn'),
'nickname': form_data.get('nickname'),
'role': form_data.get('role'),
'email': form_data.get('email'),
'url': form_data.get('url'),
};
if (!image_file.size) {
this.setVCard(data);
} else {
reader.onloadend = () => {
_.extend(data, {
'image': this.model.vcard.get('image'),
'image_type': this.model.vcard.get('image_type')
'image': btoa(reader.result),
'image_type': image_file.type
});
this.setVCard(data);
} else {
reader.onloadend = () => {
_.extend(data, {
'image': btoa(reader.result),
'image_type': image_file.type
});
this.setVCard(data);
};
reader.readAsBinaryString(image_file);
}
};
reader.readAsBinaryString(image_file);
}
});
}
});
_converse.ChatStatusModal = _converse.BootstrapModal.extend({
events: {
"submit form#set-xmpp-status": "onFormSubmitted",
"click .clear-input": "clearStatusMessage"
},
_converse.ChatStatusModal = _converse.BootstrapModal.extend({
events: {
"submit form#set-xmpp-status": "onFormSubmitted",
"click .clear-input": "clearStatusMessage"
},
toHTML () {
return tpl_chat_status_modal(
_.extend(
this.model.toJSON(),
this.model.vcard.toJSON(), {
'label_away': __('Away'),
'label_close': __('Close'),
'label_busy': __('Busy'),
'label_cancel': __('Cancel'),
'label_custom_status': __('Custom status'),
'label_offline': __('Offline'),
'label_online': __('Online'),
'label_save': __('Save'),
'label_xa': __('Away for long'),
'modal_title': __('Change chat status'),
'placeholder_status_message': __('Personal status message')
}));
},
afterRender () {
this.el.addEventListener('shown.bs.modal', () => {
this.el.querySelector('input[name="status_message"]').focus();
}, false);
},
clearStatusMessage (ev) {
if (ev && ev.preventDefault) {
ev.preventDefault();
u.hideElement(this.el.querySelector('.clear-input'));
}
const roster_filter = this.el.querySelector('input[name="status_message"]');
roster_filter.value = '';
},
onFormSubmitted (ev) {
ev.preventDefault();
const data = new FormData(ev.target);
this.model.save({
'status_message': data.get('status_message'),
'status': data.get('chat_status')
});
this.modal.hide();
}
});
_converse.XMPPStatusView = _converse.VDOMViewWithAvatar.extend({
tagName: "div",
events: {
"click a.show-profile": "showProfileModal",
"click a.change-status": "showStatusChangeModal",
"click .logout": "logOut"
},
initialize () {
this.model.on("change", this.render, this);
this.model.vcard.on("change", this.render, this);
},
toHTML () {
const chat_status = this.model.get('status') || 'offline';
return tpl_profile_view(_.extend(
toHTML () {
return tpl_chat_status_modal(
_.extend(
this.model.toJSON(),
this.model.vcard.toJSON(), {
'__': __,
'fullname': this.model.vcard.get('fullname') || _converse.bare_jid,
'status_message': this.model.get('status_message') ||
__("I am %1$s", this.getPrettyStatus(chat_status)),
'chat_status': chat_status,
'_converse': _converse,
'title_change_settings': __('Change settings'),
'title_change_status': __('Click to change your chat status'),
'title_log_out': __('Log out'),
'title_your_profile': __('Your profile')
'label_away': __('Away'),
'label_close': __('Close'),
'label_busy': __('Busy'),
'label_cancel': __('Cancel'),
'label_custom_status': __('Custom status'),
'label_offline': __('Offline'),
'label_online': __('Online'),
'label_save': __('Save'),
'label_xa': __('Away for long'),
'modal_title': __('Change chat status'),
'placeholder_status_message': __('Personal status message')
}));
},
},
afterRender () {
this.renderAvatar();
},
afterRender () {
this.el.addEventListener('shown.bs.modal', () => {
this.el.querySelector('input[name="status_message"]').focus();
}, false);
},
showProfileModal (ev) {
if (_.isUndefined(this.profile_modal)) {
this.profile_modal = new _converse.ProfileModal({model: this.model});
}
this.profile_modal.show(ev);
},
showStatusChangeModal (ev) {
if (_.isUndefined(this.status_modal)) {
this.status_modal = new _converse.ChatStatusModal({model: this.model});
}
this.status_modal.show(ev);
},
logOut (ev) {
clearStatusMessage (ev) {
if (ev && ev.preventDefault) {
ev.preventDefault();
const result = confirm(__("Are you sure you want to log out?"));
if (result === true) {
_converse.logOut();
}
},
getPrettyStatus (stat) {
if (stat === 'chat') {
return __('online');
} else if (stat === 'dnd') {
return __('busy');
} else if (stat === 'xa') {
return __('away for long');
} else if (stat === 'away') {
return __('away');
} else if (stat === 'offline') {
return __('offline');
} else {
return __(stat) || __('online');
}
u.hideElement(this.el.querySelector('.clear-input'));
}
});
}
});
}));
const roster_filter = this.el.querySelector('input[name="status_message"]');
roster_filter.value = '';
},
onFormSubmitted (ev) {
ev.preventDefault();
const data = new FormData(ev.target);
this.model.save({
'status_message': data.get('status_message'),
'status': data.get('chat_status')
});
this.modal.hide();
}
});
_converse.XMPPStatusView = _converse.VDOMViewWithAvatar.extend({
tagName: "div",
events: {
"click a.show-profile": "showProfileModal",
"click a.change-status": "showStatusChangeModal",
"click .logout": "logOut"
},
initialize () {
this.model.on("change", this.render, this);
this.model.vcard.on("change", this.render, this);
},
toHTML () {
const chat_status = this.model.get('status') || 'offline';
return tpl_profile_view(_.extend(
this.model.toJSON(),
this.model.vcard.toJSON(), {
'__': __,
'fullname': this.model.vcard.get('fullname') || _converse.bare_jid,
'status_message': this.model.get('status_message') ||
__("I am %1$s", this.getPrettyStatus(chat_status)),
'chat_status': chat_status,
'_converse': _converse,
'title_change_settings': __('Change settings'),
'title_change_status': __('Click to change your chat status'),
'title_log_out': __('Log out'),
'title_your_profile': __('Your profile')
}));
},
afterRender () {
this.renderAvatar();
},
showProfileModal (ev) {
if (_.isUndefined(this.profile_modal)) {
this.profile_modal = new _converse.ProfileModal({model: this.model});
}
this.profile_modal.show(ev);
},
showStatusChangeModal (ev) {
if (_.isUndefined(this.status_modal)) {
this.status_modal = new _converse.ChatStatusModal({model: this.model});
}
this.status_modal.show(ev);
},
logOut (ev) {
ev.preventDefault();
const result = confirm(__("Are you sure you want to log out?"));
if (result === true) {
_converse.logOut();
}
},
getPrettyStatus (stat) {
if (stat === 'chat') {
return __('online');
} else if (stat === 'dnd') {
return __('busy');
} else if (stat === 'xa') {
return __('away for long');
} else if (stat === 'away') {
return __('away');
} else if (stat === 'offline') {
return __('offline');
} else {
return __(stat) || __('online');
}
}
});
}
});

View File

@ -7,129 +7,128 @@
/* This is a Converse.js plugin which add support for registering
* an "App Server" as defined in XEP-0357
*/
(function (root, factory) {
define(["@converse/headless/converse-core"], factory);
}(this, function (converse) {
"use strict";
const { Strophe, $iq, _ } = converse.env;
Strophe.addNamespace('PUSH', 'urn:xmpp:push:0');
import converse from "@converse/headless/converse-core";
const { Strophe, $iq, _ } = converse.env;
Strophe.addNamespace('PUSH', 'urn:xmpp:push:0');
converse.plugins.add('converse-push', {
converse.plugins.add('converse-push', {
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
_converse.api.settings.update({
'push_app_servers': [],
'enable_muc_push': false
_converse.api.settings.update({
'push_app_servers': [],
'enable_muc_push': false
});
async function disablePushAppServer (domain, push_app_server) {
if (!push_app_server.jid) {
return;
}
const result = await _converse.api.disco.supports(Strophe.NS.PUSH, domain || _converse.bare_jid)
if (!result.length) {
return _converse.log(
`Not disabling push app server "${push_app_server.jid}", no disco support from your server.`,
Strophe.LogLevel.WARN
);
}
const stanza = $iq({'type': 'set'});
if (domain !== _converse.bare_jid) {
stanza.attrs({'to': domain});
}
stanza.c('disable', {
'xmlns': Strophe.NS.PUSH,
'jid': push_app_server.jid,
});
async function disablePushAppServer (domain, push_app_server) {
if (!push_app_server.jid) {
return;
}
const result = await _converse.api.disco.supports(Strophe.NS.PUSH, domain || _converse.bare_jid)
if (!result.length) {
return _converse.log(
`Not disabling push app server "${push_app_server.jid}", no disco support from your server.`,
Strophe.LogLevel.WARN
);
}
const stanza = $iq({'type': 'set'});
if (domain !== _converse.bare_jid) {
stanza.attrs({'to': domain});
}
stanza.c('disable', {
'xmlns': Strophe.NS.PUSH,
'jid': push_app_server.jid,
});
if (push_app_server.node) {
stanza.attrs({'node': push_app_server.node});
}
_converse.api.sendIQ(stanza)
.catch(e => {
_converse.log(`Could not disable push app server for ${push_app_server.jid}`, Strophe.LogLevel.ERROR);
_converse.log(e, Strophe.LogLevel.ERROR);
});
if (push_app_server.node) {
stanza.attrs({'node': push_app_server.node});
}
_converse.api.sendIQ(stanza)
.catch(e => {
_converse.log(`Could not disable push app server for ${push_app_server.jid}`, Strophe.LogLevel.ERROR);
_converse.log(e, Strophe.LogLevel.ERROR);
});
}
async function enablePushAppServer (domain, push_app_server) {
if (!push_app_server.jid || !push_app_server.node) {
return;
}
const identity = await _converse.api.disco.getIdentity('pubsub', 'push', push_app_server.jid);
if (!identity) {
return _converse.log(
`Not enabling push the service "${push_app_server.jid}", it doesn't have the right disco identtiy.`,
Strophe.LogLevel.WARN
);
}
const result = await Promise.all([
_converse.api.disco.supports(Strophe.NS.PUSH, push_app_server.jid),
_converse.api.disco.supports(Strophe.NS.PUSH, domain)
]);
if (!result[0].length && !result[1].length) {
return _converse.log(
`Not enabling push app server "${push_app_server.jid}", no disco support from your server.`,
Strophe.LogLevel.WARN
);
}
const stanza = $iq({'type': 'set'});
if (domain !== _converse.bare_jid) {
stanza.attrs({'to': domain});
}
stanza.c('enable', {
'xmlns': Strophe.NS.PUSH,
'jid': push_app_server.jid,
'node': push_app_server.node
});
if (push_app_server.secret) {
stanza.c('x', {'xmlns': Strophe.NS.XFORM, 'type': 'submit'})
.c('field', {'var': 'FORM_TYPE'})
.c('value').t(`${Strophe.NS.PUBSUB}#publish-options`).up().up()
.c('field', {'var': 'secret'})
.c('value').t(push_app_server.secret);
}
return _converse.api.sendIQ(stanza);
async function enablePushAppServer (domain, push_app_server) {
if (!push_app_server.jid || !push_app_server.node) {
return;
}
async function enablePush (domain) {
domain = domain || _converse.bare_jid;
const push_enabled = _converse.session.get('push_enabled') || [];
if (_.includes(push_enabled, domain)) {
return;
}
const enabled_services = _.reject(_converse.push_app_servers, 'disable');
try {
await Promise.all(_.map(enabled_services, _.partial(enablePushAppServer, domain)))
} catch (e) {
_converse.log('Could not enable push App Server', Strophe.LogLevel.ERROR);
if (e) _converse.log(e, Strophe.LogLevel.ERROR);
} finally {
push_enabled.push(domain);
}
const disabled_services = _.filter(_converse.push_app_servers, 'disable');
_.each(disabled_services, _.partial(disablePushAppServer, domain));
_converse.session.save('push_enabled', push_enabled);
const identity = await _converse.api.disco.getIdentity('pubsub', 'push', push_app_server.jid);
if (!identity) {
return _converse.log(
`Not enabling push the service "${push_app_server.jid}", it doesn't have the right disco identtiy.`,
Strophe.LogLevel.WARN
);
}
_converse.api.listen.on('statusInitialized', () => enablePush());
function onChatBoxAdded (model) {
if (model.get('type') == _converse.CHATROOMS_TYPE) {
enablePush(Strophe.getDomainFromJid(model.get('jid')));
}
const result = await Promise.all([
_converse.api.disco.supports(Strophe.NS.PUSH, push_app_server.jid),
_converse.api.disco.supports(Strophe.NS.PUSH, domain)
]);
if (!result[0].length && !result[1].length) {
return _converse.log(
`Not enabling push app server "${push_app_server.jid}", no disco support from your server.`,
Strophe.LogLevel.WARN
);
}
if (_converse.enable_muc_push) {
_converse.api.listen.on('chatBoxesInitialized', () => _converse.chatboxes.on('add', onChatBoxAdded));
const stanza = $iq({'type': 'set'});
if (domain !== _converse.bare_jid) {
stanza.attrs({'to': domain});
}
stanza.c('enable', {
'xmlns': Strophe.NS.PUSH,
'jid': push_app_server.jid,
'node': push_app_server.node
});
if (push_app_server.secret) {
stanza.c('x', {'xmlns': Strophe.NS.XFORM, 'type': 'submit'})
.c('field', {'var': 'FORM_TYPE'})
.c('value').t(`${Strophe.NS.PUBSUB}#publish-options`).up().up()
.c('field', {'var': 'secret'})
.c('value').t(push_app_server.secret);
}
return _converse.api.sendIQ(stanza);
}
async function enablePush (domain) {
domain = domain || _converse.bare_jid;
const push_enabled = _converse.session.get('push_enabled') || [];
if (_.includes(push_enabled, domain)) {
return;
}
const enabled_services = _.reject(_converse.push_app_servers, 'disable');
try {
await Promise.all(_.map(enabled_services, _.partial(enablePushAppServer, domain)))
} catch (e) {
_converse.log('Could not enable push App Server', Strophe.LogLevel.ERROR);
if (e) _converse.log(e, Strophe.LogLevel.ERROR);
} finally {
push_enabled.push(domain);
}
const disabled_services = _.filter(_converse.push_app_servers, 'disable');
_.each(disabled_services, _.partial(disablePushAppServer, domain));
_converse.session.save('push_enabled', push_enabled);
}
_converse.api.listen.on('statusInitialized', () => enablePush());
function onChatBoxAdded (model) {
if (model.get('type') == _converse.CHATROOMS_TYPE) {
enablePush(Strophe.getDomainFromJid(model.get('jid')));
}
}
});
}));
if (_converse.enable_muc_push) {
_converse.api.listen.on('chatBoxesInitialized', () => _converse.chatboxes.on('add', onChatBoxAdded));
}
}
});

File diff suppressed because it is too large Load Diff

View File

@ -1,289 +1,287 @@
// Converse.js (A browser based XMPP chat client)
// http://conversejs.org
//
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
// Copyright (c) 2013-2018, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define */
/* This is a non-core Converse.js plugin which shows a list of currently open
* rooms in the "Rooms Panel" of the ControlBox.
*/
(function (root, factory) {
define(["@converse/headless/converse-core",
"@converse/headless/converse-muc",
"templates/rooms_list.html",
"templates/rooms_list_item.html"
], factory);
}(this, function (converse, muc, tpl_rooms_list, tpl_rooms_list_item) {
const { Backbone, Promise, Strophe, b64_sha1, sizzle, _ } = converse.env;
const u = converse.env.utils;
converse.plugins.add('converse-roomslist', {
import converse from "@converse/headless/converse-core";
import muc from "@converse/headless/converse-muc";
import tpl_rooms_list from "templates/rooms_list.html";
import tpl_rooms_list_item from "templates/rooms_list_item.html"
/* Optional dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin. They are called "optional" because they might not be
* available, in which case any overrides applicable to them will be
* ignored.
*
* It's possible however to make optional dependencies non-optional.
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found.
*
* NB: These plugins need to have already been loaded via require.js.
const { Backbone, Promise, Strophe, b64_sha1, sizzle, _ } = converse.env;
const u = converse.env.utils;
converse.plugins.add('converse-roomslist', {
/* Optional dependencies are other plugins which might be
* overridden or relied upon, and therefore need to be loaded before
* this plugin. They are called "optional" because they might not be
* available, in which case any overrides applicable to them will be
* ignored.
*
* It's possible however to make optional dependencies non-optional.
* If the setting "strict_plugin_dependencies" is set to true,
* an error will be raised if the plugin is not found.
*
* NB: These plugins need to have already been loaded via require.js.
*/
dependencies: ["converse-singleton", "converse-controlbox", "converse-muc", "converse-bookmarks"],
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
dependencies: ["converse-singleton", "converse-controlbox", "@converse/headless/converse-muc", "converse-bookmarks"],
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this,
{ __ } = _converse;
const { _converse } = this,
{ __ } = _converse;
_converse.OpenRooms = Backbone.Collection.extend({
_converse.OpenRooms = Backbone.Collection.extend({
comparator (room) {
if (room.get('bookmarked')) {
const bookmark = _.head(_converse.bookmarksview.model.where({'jid': room.get('jid')}));
return bookmark.get('name');
} else {
return room.get('name');
}
},
comparator (room) {
if (room.get('bookmarked')) {
const bookmark = _.head(_converse.bookmarksview.model.where({'jid': room.get('jid')}));
return bookmark.get('name');
} else {
return room.get('name');
}
},
initialize () {
_converse.chatboxes.on('add', this.onChatBoxAdded, this);
_converse.chatboxes.on('change:hidden', this.onChatBoxChanged, this);
_converse.chatboxes.on('change:bookmarked', this.onChatBoxChanged, this);
_converse.chatboxes.on('change:name', this.onChatBoxChanged, this);
_converse.chatboxes.on('change:num_unread', this.onChatBoxChanged, this);
_converse.chatboxes.on('change:num_unread_general', this.onChatBoxChanged, this);
_converse.chatboxes.on('remove', this.onChatBoxRemoved, this);
this.reset(_.map(_converse.chatboxes.where({'type': 'chatroom'}), 'attributes'));
},
initialize () {
_converse.chatboxes.on('add', this.onChatBoxAdded, this);
_converse.chatboxes.on('change:hidden', this.onChatBoxChanged, this);
_converse.chatboxes.on('change:bookmarked', this.onChatBoxChanged, this);
_converse.chatboxes.on('change:name', this.onChatBoxChanged, this);
_converse.chatboxes.on('change:num_unread', this.onChatBoxChanged, this);
_converse.chatboxes.on('change:num_unread_general', this.onChatBoxChanged, this);
_converse.chatboxes.on('remove', this.onChatBoxRemoved, this);
this.reset(_.map(_converse.chatboxes.where({'type': 'chatroom'}), 'attributes'));
},
onChatBoxAdded (item) {
if (item.get('type') === 'chatroom') {
this.create(item.attributes);
}
},
onChatBoxAdded (item) {
if (item.get('type') === 'chatroom') {
this.create(item.attributes);
}
},
onChatBoxChanged (item) {
if (item.get('type') === 'chatroom') {
const room = this.get(item.get('jid'));
if (!_.isNil(room)) {
room.set(item.attributes);
}
}
},
onChatBoxRemoved (item) {
if (item.get('type') === 'chatroom') {
const room = this.get(item.get('jid'))
this.remove(room);
onChatBoxChanged (item) {
if (item.get('type') === 'chatroom') {
const room = this.get(item.get('jid'));
if (!_.isNil(room)) {
room.set(item.attributes);
}
}
});
},
_converse.RoomsList = Backbone.Model.extend({
defaults: {
"toggle-state": _converse.OPENED
onChatBoxRemoved (item) {
if (item.get('type') === 'chatroom') {
const room = this.get(item.get('jid'))
this.remove(room);
}
});
_converse.RoomsListElementView = Backbone.VDOMView.extend({
events: {
'click .room-info': 'showRoomDetailsModal'
},
initialize () {
this.model.on('destroy', this.remove, this);
this.model.on('remove', this.remove, this);
this.model.on('change:bookmarked', this.render, this);
this.model.on('change:hidden', this.render, this);
this.model.on('change:name', this.render, this);
this.model.on('change:num_unread', this.render, this);
this.model.on('change:num_unread_general', this.render, this);
},
toHTML () {
return tpl_rooms_list_item(
_.extend(this.model.toJSON(), {
// XXX: By the time this renders, the _converse.bookmarks
// collection should already exist if bookmarks are
// supported by the XMPP server. So we can use it
// as a check for support (other ways of checking are async).
'allow_bookmarks': _converse.allow_bookmarks && _converse.bookmarks,
'currently_open': _converse.isSingleton() && !this.model.get('hidden'),
'info_leave_room': __('Leave this groupchat'),
'info_remove_bookmark': __('Unbookmark this groupchat'),
'info_add_bookmark': __('Bookmark this groupchat'),
'info_title': __('Show more information on this groupchat'),
'name': this.getRoomsListElementName(),
'open_title': __('Click to open this groupchat')
}));
},
showRoomDetailsModal (ev) {
const room = _converse.chatboxes.get(this.model.get('jid'));
ev.preventDefault();
if (_.isUndefined(room.room_details_modal)) {
room.room_details_modal = new _converse.RoomDetailsModal({'model': room});
}
room.room_details_modal.show(ev);
},
getRoomsListElementName () {
if (this.model.get('bookmarked') && _converse.bookmarksview) {
const bookmark = _.head(_converse.bookmarksview.model.where({'jid': this.model.get('jid')}));
return bookmark.get('name');
} else {
return this.model.get('name');
}
}
});
_converse.RoomsListView = Backbone.OrderedListView.extend({
tagName: 'div',
className: 'open-rooms-list list-container rooms-list-container',
events: {
'click .add-bookmark': 'addBookmark',
'click .close-room': 'closeRoom',
'click .list-toggle': 'toggleRoomsList',
'click .remove-bookmark': 'removeBookmark',
'click .open-room': 'openRoom',
},
listSelector: '.rooms-list',
ItemView: _converse.RoomsListElementView,
subviewIndex: 'jid',
initialize () {
Backbone.OrderedListView.prototype.initialize.apply(this, arguments);
this.model.on('add', this.showOrHide, this);
this.model.on('remove', this.showOrHide, this);
const storage = _converse.config.get('storage'),
id = b64_sha1(`converse.roomslist${_converse.bare_jid}`);
this.list_model = new _converse.RoomsList({'id': id});
this.list_model.browserStorage = new Backbone.BrowserStorage[storage](id);
this.list_model.fetch();
this.render();
this.sortAndPositionAllItems();
},
render () {
this.el.innerHTML = tpl_rooms_list({
'toggle_state': this.list_model.get('toggle-state'),
'desc_rooms': __('Click to toggle the list of open groupchats'),
'label_rooms': __('Open Groupchats'),
'_converse': _converse
});
if (this.list_model.get('toggle-state') !== _converse.OPENED) {
this.el.querySelector('.open-rooms-list').classList.add('collapsed');
}
this.showOrHide();
this.insertIntoControlBox();
return this;
},
insertIntoControlBox () {
const controlboxview = _converse.chatboxviews.get('controlbox');
if (!_.isUndefined(controlboxview) && !u.rootContains(_converse.root, this.el)) {
const el = controlboxview.el.querySelector('.open-rooms-list');
if (!_.isNull(el)) {
el.parentNode.replaceChild(this.el, el);
}
}
},
hide () {
u.hideElement(this.el);
},
show () {
u.showElement(this.el);
},
openRoom (ev) {
ev.preventDefault();
const name = ev.target.textContent;
const jid = ev.target.getAttribute('data-room-jid');
const data = {
'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)) || jid
}
_converse.api.rooms.open(jid, data);
},
closeRoom (ev) {
ev.preventDefault();
const name = ev.target.getAttribute('data-room-name');
const jid = ev.target.getAttribute('data-room-jid');
if (confirm(__("Are you sure you want to leave the groupchat %1$s?", name))) {
// TODO: replace with API call
_converse.chatboxviews.get(jid).close();
}
},
showOrHide (item) {
if (!this.model.models.length) {
u.hideElement(this.el);
} else {
u.showElement(this.el);
}
},
removeBookmark: _converse.removeBookmarkViaEvent,
addBookmark: _converse.addBookmarkViaEvent,
toggleRoomsList (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
const icon_el = ev.target.querySelector('.fa');
if (icon_el.classList.contains("fa-caret-down")) {
u.slideIn(this.el.querySelector('.open-rooms-list')).then(() => {
this.list_model.save({'toggle-state': _converse.CLOSED});
icon_el.classList.remove("fa-caret-down");
icon_el.classList.add("fa-caret-right");
});
} else {
u.slideOut(this.el.querySelector('.open-rooms-list')).then(() => {
this.list_model.save({'toggle-state': _converse.OPENED});
icon_el.classList.remove("fa-caret-right");
icon_el.classList.add("fa-caret-down");
});
}
}
});
const initRoomsListView = function () {
const storage = _converse.config.get('storage'),
id = b64_sha1(`converse.open-rooms-{_converse.bare_jid}`),
model = new _converse.OpenRooms();
model.browserStorage = new Backbone.BrowserStorage[storage](id);
_converse.rooms_list_view = new _converse.RoomsListView({'model': model});
};
if (_converse.allow_bookmarks) {
u.onMultipleEvents([
{'object': _converse, 'event': 'chatBoxesFetched'},
{'object': _converse, 'event': 'roomsPanelRendered'},
{'object': _converse, 'event': 'bookmarksInitialized'}
], initRoomsListView);
} else {
u.onMultipleEvents([
{'object': _converse, 'event': 'chatBoxesFetched'},
{'object': _converse, 'event': 'roomsPanelRendered'}
], initRoomsListView);
}
});
_converse.api.listen.on('reconnected', initRoomsListView);
_converse.RoomsList = Backbone.Model.extend({
defaults: {
"toggle-state": _converse.OPENED
}
});
_converse.RoomsListElementView = Backbone.VDOMView.extend({
events: {
'click .room-info': 'showRoomDetailsModal'
},
initialize () {
this.model.on('destroy', this.remove, this);
this.model.on('remove', this.remove, this);
this.model.on('change:bookmarked', this.render, this);
this.model.on('change:hidden', this.render, this);
this.model.on('change:name', this.render, this);
this.model.on('change:num_unread', this.render, this);
this.model.on('change:num_unread_general', this.render, this);
},
toHTML () {
return tpl_rooms_list_item(
_.extend(this.model.toJSON(), {
// XXX: By the time this renders, the _converse.bookmarks
// collection should already exist if bookmarks are
// supported by the XMPP server. So we can use it
// as a check for support (other ways of checking are async).
'allow_bookmarks': _converse.allow_bookmarks && _converse.bookmarks,
'currently_open': _converse.isSingleton() && !this.model.get('hidden'),
'info_leave_room': __('Leave this groupchat'),
'info_remove_bookmark': __('Unbookmark this groupchat'),
'info_add_bookmark': __('Bookmark this groupchat'),
'info_title': __('Show more information on this groupchat'),
'name': this.getRoomsListElementName(),
'open_title': __('Click to open this groupchat')
}));
},
showRoomDetailsModal (ev) {
const room = _converse.chatboxes.get(this.model.get('jid'));
ev.preventDefault();
if (_.isUndefined(room.room_details_modal)) {
room.room_details_modal = new _converse.RoomDetailsModal({'model': room});
}
room.room_details_modal.show(ev);
},
getRoomsListElementName () {
if (this.model.get('bookmarked') && _converse.bookmarksview) {
const bookmark = _.head(_converse.bookmarksview.model.where({'jid': this.model.get('jid')}));
return bookmark.get('name');
} else {
return this.model.get('name');
}
}
});
_converse.RoomsListView = Backbone.OrderedListView.extend({
tagName: 'div',
className: 'open-rooms-list list-container rooms-list-container',
events: {
'click .add-bookmark': 'addBookmark',
'click .close-room': 'closeRoom',
'click .list-toggle': 'toggleRoomsList',
'click .remove-bookmark': 'removeBookmark',
'click .open-room': 'openRoom',
},
listSelector: '.rooms-list',
ItemView: _converse.RoomsListElementView,
subviewIndex: 'jid',
initialize () {
Backbone.OrderedListView.prototype.initialize.apply(this, arguments);
this.model.on('add', this.showOrHide, this);
this.model.on('remove', this.showOrHide, this);
const storage = _converse.config.get('storage'),
id = b64_sha1(`converse.roomslist${_converse.bare_jid}`);
this.list_model = new _converse.RoomsList({'id': id});
this.list_model.browserStorage = new Backbone.BrowserStorage[storage](id);
this.list_model.fetch();
this.render();
this.sortAndPositionAllItems();
},
render () {
this.el.innerHTML = tpl_rooms_list({
'toggle_state': this.list_model.get('toggle-state'),
'desc_rooms': __('Click to toggle the list of open groupchats'),
'label_rooms': __('Open Groupchats'),
'_converse': _converse
});
if (this.list_model.get('toggle-state') !== _converse.OPENED) {
this.el.querySelector('.open-rooms-list').classList.add('collapsed');
}
this.showOrHide();
this.insertIntoControlBox();
return this;
},
insertIntoControlBox () {
const controlboxview = _converse.chatboxviews.get('controlbox');
if (!_.isUndefined(controlboxview) && !u.rootContains(_converse.root, this.el)) {
const el = controlboxview.el.querySelector('.open-rooms-list');
if (!_.isNull(el)) {
el.parentNode.replaceChild(this.el, el);
}
}
},
hide () {
u.hideElement(this.el);
},
show () {
u.showElement(this.el);
},
openRoom (ev) {
ev.preventDefault();
const name = ev.target.textContent;
const jid = ev.target.getAttribute('data-room-jid');
const data = {
'name': name || Strophe.unescapeNode(Strophe.getNodeFromJid(jid)) || jid
}
_converse.api.rooms.open(jid, data);
},
closeRoom (ev) {
ev.preventDefault();
const name = ev.target.getAttribute('data-room-name');
const jid = ev.target.getAttribute('data-room-jid');
if (confirm(__("Are you sure you want to leave the groupchat %1$s?", name))) {
// TODO: replace with API call
_converse.chatboxviews.get(jid).close();
}
},
showOrHide (item) {
if (!this.model.models.length) {
u.hideElement(this.el);
} else {
u.showElement(this.el);
}
},
removeBookmark: _converse.removeBookmarkViaEvent,
addBookmark: _converse.addBookmarkViaEvent,
toggleRoomsList (ev) {
if (ev && ev.preventDefault) { ev.preventDefault(); }
const icon_el = ev.target.querySelector('.fa');
if (icon_el.classList.contains("fa-caret-down")) {
u.slideIn(this.el.querySelector('.open-rooms-list')).then(() => {
this.list_model.save({'toggle-state': _converse.CLOSED});
icon_el.classList.remove("fa-caret-down");
icon_el.classList.add("fa-caret-right");
});
} else {
u.slideOut(this.el.querySelector('.open-rooms-list')).then(() => {
this.list_model.save({'toggle-state': _converse.OPENED});
icon_el.classList.remove("fa-caret-right");
icon_el.classList.add("fa-caret-down");
});
}
}
});
const initRoomsListView = function () {
const storage = _converse.config.get('storage'),
id = b64_sha1(`converse.open-rooms-{_converse.bare_jid}`),
model = new _converse.OpenRooms();
model.browserStorage = new Backbone.BrowserStorage[storage](id);
_converse.rooms_list_view = new _converse.RoomsListView({'model': model});
};
if (_converse.allow_bookmarks) {
u.onMultipleEvents([
{'object': _converse, 'event': 'chatBoxesFetched'},
{'object': _converse, 'event': 'roomsPanelRendered'},
{'object': _converse, 'event': 'bookmarksInitialized'}
], initRoomsListView);
} else {
u.onMultipleEvents([
{'object': _converse, 'event': 'chatBoxesFetched'},
{'object': _converse, 'event': 'roomsPanelRendered'}
], initRoomsListView);
}
});
}));
_converse.api.listen.on('reconnected', initRoomsListView);
}
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
// Converse.js
// http://conversejs.org
//
// Copyright (c) 2012-2018, the Converse.js developers
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
/* converse-singleton
@ -13,106 +13,103 @@
*
* This plugin makes sense in mobile or fullscreen chat environments (as
* configured by the `view_mode` setting).
*
*/
(function (root, factory) {
define(
["@converse/headless/converse-core", "converse-chatview"],
factory);
}(this, function (converse) {
"use strict";
const { _, Strophe } = converse.env;
const u = converse.env.utils;
import "converse-chatview";
import converse from "@converse/headless/converse-core";
const { _, Strophe } = converse.env;
const u = converse.env.utils;
function hideChat (view) {
if (view.model.get('id') === 'controlbox') { return; }
u.safeSave(view.model, {'hidden': true});
view.hide();
}
function hideChat (view) {
if (view.model.get('id') === 'controlbox') { return; }
u.safeSave(view.model, {'hidden': true});
view.hide();
}
converse.plugins.add('converse-singleton', {
// It's possible however to make optional dependencies non-optional.
// If the setting "strict_plugin_dependencies" is set to true,
// an error will be raised if the plugin is not found.
converse.plugins.add('converse-singleton', {
// It's possible however to make optional dependencies non-optional.
// If the setting "strict_plugin_dependencies" is set to true,
// an error will be raised if the plugin is not found.
//
// NB: These plugins need to have already been loaded via require.js.
dependencies: ['converse-chatboxes', 'converse-muc', 'converse-muc-views', 'converse-controlbox', 'converse-rosterview'],
overrides: {
// overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// NB: These plugins need to have already been loaded via require.js.
dependencies: ['converse-chatboxes', 'converse-muc', 'converse-muc-views', 'converse-controlbox', 'converse-rosterview'],
// new functions which don't exist yet can also be added.
ChatBoxes: {
overrides: {
// overrides mentioned here will be picked up by converse.js's
// plugin architecture they will replace existing methods on the
// relevant objects or classes.
//
// new functions which don't exist yet can also be added.
ChatBoxes: {
chatBoxMayBeShown (chatbox) {
const { _converse } = this.__super__;
if (chatbox.get('id') === 'controlbox') {
return true;
}
if (_converse.isSingleton()) {
const any_chats_visible = _converse.chatboxes
.filter(cb => cb.get('id') != 'controlbox')
.filter(cb => !cb.get('hidden')).length > 0;
chatBoxMayBeShown (chatbox) {
const { _converse } = this.__super__;
if (chatbox.get('id') === 'controlbox') {
if (any_chats_visible) {
return !chatbox.get('hidden');
} else {
return true;
}
if (_converse.isSingleton()) {
const any_chats_visible = _converse.chatboxes
.filter(cb => cb.get('id') != 'controlbox')
.filter(cb => !cb.get('hidden')).length > 0;
if (any_chats_visible) {
return !chatbox.get('hidden');
} else {
return true;
}
} else {
return this.__super__.chatBoxMayBeShown.apply(this, arguments);
}
},
createChatBox (jid, attrs) {
/* Make sure new chat boxes are hidden by default. */
const { _converse } = this.__super__;
if (_converse.isSingleton()) {
attrs = attrs || {};
attrs.hidden = true;
}
return this.__super__.createChatBox.call(this, jid, attrs);
} else {
return this.__super__.chatBoxMayBeShown.apply(this, arguments);
}
},
ChatBoxView: {
shouldShowOnTextMessage () {
const { _converse } = this.__super__;
if (_converse.isSingleton()) {
return false;
} else {
return this.__super__.shouldShowOnTextMessage.apply(this, arguments);
}
},
createChatBox (jid, attrs) {
/* Make sure new chat boxes are hidden by default. */
const { _converse } = this.__super__;
if (_converse.isSingleton()) {
attrs = attrs || {};
attrs.hidden = true;
}
return this.__super__.createChatBox.call(this, jid, attrs);
}
},
_show (focus) {
/* We only have one chat visible at any one
* time. So before opening a chat, we make sure all other
* chats are hidden.
*/
const { _converse } = this.__super__;
if (_converse.isSingleton()) {
_.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat);
u.safeSave(this.model, {'hidden': false});
}
return this.__super__._show.apply(this, arguments);
ChatBoxView: {
shouldShowOnTextMessage () {
const { _converse } = this.__super__;
if (_converse.isSingleton()) {
return false;
} else {
return this.__super__.shouldShowOnTextMessage.apply(this, arguments);
}
},
ChatRoomView: {
show (focus) {
const { _converse } = this.__super__;
if (_converse.isSingleton()) {
_.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat);
u.safeSave(this.model, {'hidden': false});
}
return this.__super__.show.apply(this, arguments);
_show (focus) {
/* We only have one chat visible at any one
* time. So before opening a chat, we make sure all other
* chats are hidden.
*/
const { _converse } = this.__super__;
if (_converse.isSingleton()) {
_.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat);
u.safeSave(this.model, {'hidden': false});
}
return this.__super__._show.apply(this, arguments);
}
},
ChatRoomView: {
show (focus) {
const { _converse } = this.__super__;
if (_converse.isSingleton()) {
_.each(this.__super__._converse.chatboxviews.xget(this.model.get('id')), hideChat);
u.safeSave(this.model, {'hidden': false});
}
return this.__super__.show.apply(this, arguments);
}
}
});
}));
}
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -7,88 +7,87 @@
/* This is a Converse.js plugin which add support for application-level pings
* as specified in XEP-0199 XMPP Ping.
*/
(function (root, factory) {
define(["./converse-core", "strophejs-plugin-ping"], factory);
}(this, function (converse) {
"use strict";
// Strophe methods for building stanzas
const { Strophe, _ } = converse.env;
converse.plugins.add('converse-ping', {
import "strophejs-plugin-ping";
import converse from "./converse-core";
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this;
// Strophe methods for building stanzas
const { Strophe, _ } = converse.env;
_converse.api.settings.update({
ping_interval: 180 //in seconds
});
converse.plugins.add('converse-ping', {
_converse.ping = function (jid, success, error, timeout) {
// XXX: We could first check here if the server advertised that
// it supports PING.
// However, some servers don't advertise while still keeping the
// connection option due to pings.
//
// var feature = _converse.disco_entities[_converse.domain].features.findWhere({'var': Strophe.NS.PING});
_converse.lastStanzaDate = new Date();
if (_.isNil(jid)) {
jid = Strophe.getDomainFromJid(_converse.bare_jid);
}
if (_.isUndefined(timeout) ) { timeout = null; }
if (_.isUndefined(success) ) { success = null; }
if (_.isUndefined(error) ) { error = null; }
if (_converse.connection) {
_converse.connection.ping.ping(jid, success, error, timeout);
return true;
}
return false;
};
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this;
_converse.pong = function (ping) {
_converse.lastStanzaDate = new Date();
_converse.connection.ping.pong(ping);
_converse.api.settings.update({
ping_interval: 180 //in seconds
});
_converse.ping = function (jid, success, error, timeout) {
// XXX: We could first check here if the server advertised that
// it supports PING.
// However, some servers don't advertise while still keeping the
// connection option due to pings.
//
// var feature = _converse.disco_entities[_converse.domain].features.findWhere({'var': Strophe.NS.PING});
_converse.lastStanzaDate = new Date();
if (_.isNil(jid)) {
jid = Strophe.getDomainFromJid(_converse.bare_jid);
}
if (_.isUndefined(timeout) ) { timeout = null; }
if (_.isUndefined(success) ) { success = null; }
if (_.isUndefined(error) ) { error = null; }
if (_converse.connection) {
_converse.connection.ping.ping(jid, success, error, timeout);
return true;
};
}
return false;
};
_converse.registerPongHandler = function () {
if (!_.isUndefined(_converse.connection.disco)) {
_converse.api.disco.own.features.add(Strophe.NS.PING);
}
_converse.connection.ping.addPingHandler(_converse.pong);
};
_converse.pong = function (ping) {
_converse.lastStanzaDate = new Date();
_converse.connection.ping.pong(ping);
return true;
};
_converse.registerPingHandler = function () {
_converse.registerPongHandler();
if (_converse.ping_interval > 0) {
_converse.connection.addHandler(function () {
/* Handler on each stanza, saves the received date
* in order to ping only when needed.
*/
_converse.lastStanzaDate = new Date();
return true;
});
_converse.connection.addTimedHandler(1000, function () {
const now = new Date();
if (!_converse.lastStanzaDate) {
_converse.lastStanzaDate = now;
}
if ((now - _converse.lastStanzaDate)/1000 > _converse.ping_interval) {
return _converse.ping();
}
return true;
});
}
};
_converse.registerPongHandler = function () {
if (!_.isUndefined(_converse.connection.disco)) {
_converse.api.disco.own.features.add(Strophe.NS.PING);
}
_converse.connection.ping.addPingHandler(_converse.pong);
};
const onConnected = function () {
// Wrapper so that we can spy on registerPingHandler in tests
_converse.registerPingHandler();
};
_converse.on('connected', onConnected);
_converse.on('reconnected', onConnected);
}
});
}));
_converse.registerPingHandler = function () {
_converse.registerPongHandler();
if (_converse.ping_interval > 0) {
_converse.connection.addHandler(function () {
/* Handler on each stanza, saves the received date
* in order to ping only when needed.
*/
_converse.lastStanzaDate = new Date();
return true;
});
_converse.connection.addTimedHandler(1000, function () {
const now = new Date();
if (!_converse.lastStanzaDate) {
_converse.lastStanzaDate = now;
}
if ((now - _converse.lastStanzaDate)/1000 > _converse.ping_interval) {
return _converse.ping();
}
return true;
});
}
};
const onConnected = function () {
// Wrapper so that we can spy on registerPingHandler in tests
_converse.registerPingHandler();
};
_converse.on('connected', onConnected);
_converse.on('reconnected', onConnected);
}
});

View File

@ -4,240 +4,239 @@
// Copyright (c) 2013-2018, the Converse.js developers
// Licensed under the Mozilla Public License (MPLv2)
(function (root, factory) {
define(["./converse-core", "./templates/vcard.html"], factory);
}(this, function (converse, tpl_vcard) {
"use strict";
const { Backbone, Promise, Strophe, _, $iq, $build, b64_sha1, moment, sizzle } = converse.env;
const u = converse.env.utils;
import converse from "./converse-core";
import tpl_vcard from "./templates/vcard.html";
const { Backbone, Promise, Strophe, _, $iq, $build, b64_sha1, moment, sizzle } = converse.env;
const u = converse.env.utils;
converse.plugins.add('converse-vcard', {
converse.plugins.add('converse-vcard', {
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this;
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { _converse } = this;
_converse.VCard = Backbone.Model.extend({
defaults: {
'image': _converse.DEFAULT_IMAGE,
'image_type': _converse.DEFAULT_IMAGE_TYPE
},
_converse.VCard = Backbone.Model.extend({
defaults: {
'image': _converse.DEFAULT_IMAGE,
'image_type': _converse.DEFAULT_IMAGE_TYPE
},
set (key, val, options) {
// Override Backbone.Model.prototype.set to make sure that the
// default `image` and `image_type` values are maintained.
let attrs;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
if (_.has(attrs, 'image') && !attrs['image']) {
attrs['image'] = _converse.DEFAULT_IMAGE;
attrs['image_type'] = _converse.DEFAULT_IMAGE_TYPE;
return Backbone.Model.prototype.set.call(this, attrs, options);
} else {
return Backbone.Model.prototype.set.apply(this, arguments);
}
}
});
_converse.VCards = Backbone.Collection.extend({
model: _converse.VCard,
initialize () {
this.on('add', (vcard) => _converse.api.vcard.update(vcard));
}
});
function onVCardData (jid, iq, callback) {
const vcard = iq.querySelector('vCard');
let result = {};
if (!_.isNull(vcard)) {
result = {
'stanza': iq,
'fullname': _.get(vcard.querySelector('FN'), 'textContent'),
'nickname': _.get(vcard.querySelector('NICKNAME'), 'textContent'),
'image': _.get(vcard.querySelector('PHOTO BINVAL'), 'textContent'),
'image_type': _.get(vcard.querySelector('PHOTO TYPE'), 'textContent'),
'url': _.get(vcard.querySelector('URL'), 'textContent'),
'role': _.get(vcard.querySelector('ROLE'), 'textContent'),
'email': _.get(vcard.querySelector('EMAIL USERID'), 'textContent'),
'vcard_updated': moment().format(),
'vcard_error': undefined
};
}
if (result.image) {
const buffer = u.base64ToArrayBuffer(result['image']);
crypto.subtle.digest('SHA-1', buffer)
.then(ab => {
result['image_hash'] = u.arrayBufferToHex(ab);
if (callback) callback(result);
});
set (key, val, options) {
// Override Backbone.Model.prototype.set to make sure that the
// default `image` and `image_type` values are maintained.
let attrs;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
if (_.has(attrs, 'image') && !attrs['image']) {
attrs['image'] = _converse.DEFAULT_IMAGE;
attrs['image_type'] = _converse.DEFAULT_IMAGE_TYPE;
return Backbone.Model.prototype.set.call(this, attrs, options);
} else {
return Backbone.Model.prototype.set.apply(this, arguments);
}
}
});
_converse.VCards = Backbone.Collection.extend({
model: _converse.VCard,
initialize () {
this.on('add', (vcard) => _converse.api.vcard.update(vcard));
}
});
function onVCardData (jid, iq, callback) {
const vcard = iq.querySelector('vCard');
let result = {};
if (!_.isNull(vcard)) {
result = {
'stanza': iq,
'fullname': _.get(vcard.querySelector('FN'), 'textContent'),
'nickname': _.get(vcard.querySelector('NICKNAME'), 'textContent'),
'image': _.get(vcard.querySelector('PHOTO BINVAL'), 'textContent'),
'image_type': _.get(vcard.querySelector('PHOTO TYPE'), 'textContent'),
'url': _.get(vcard.querySelector('URL'), 'textContent'),
'role': _.get(vcard.querySelector('ROLE'), 'textContent'),
'email': _.get(vcard.querySelector('EMAIL USERID'), 'textContent'),
'vcard_updated': moment().format(),
'vcard_error': undefined
};
}
if (result.image) {
const buffer = u.base64ToArrayBuffer(result['image']);
crypto.subtle.digest('SHA-1', buffer)
.then(ab => {
result['image_hash'] = u.arrayBufferToHex(ab);
if (callback) callback(result);
}
});
} else {
if (callback) callback(result);
}
}
function onVCardError (jid, iq, errback) {
if (errback) {
errback({
'stanza': iq,
'jid': jid,
'vcard_error': moment().format()
});
}
}
function createStanza (type, jid, vcard_el) {
const iq = $iq(jid ? {'type': type, 'to': jid} : {'type': type});
if (!vcard_el) {
iq.c("vCard", {'xmlns': Strophe.NS.VCARD});
} else {
iq.cnode(vcard_el);
}
return iq;
}
function setVCard (jid, data) {
if (!jid) {
throw Error("No jid provided for the VCard data");
}
const vcard_el = Strophe.xmlHtmlNode(tpl_vcard(data)).firstElementChild;
return _converse.api.sendIQ(createStanza("set", jid, vcard_el));
}
function getVCard (_converse, jid) {
/* Request the VCard of another user. Returns a promise.
*
* Parameters:
* (String) jid - The Jabber ID of the user whose VCard
* is being requested.
*/
const to = Strophe.getBareJidFromJid(jid) === _converse.bare_jid ? null : jid;
return new Promise((resolve, reject) => {
_converse.connection.sendIQ(
createStanza("get", to),
_.partial(onVCardData, jid, _, resolve),
_.partial(onVCardError, jid, _, resolve),
_converse.IQ_TIMEOUT
);
function onVCardError (jid, iq, errback) {
if (errback) {
errback({
'stanza': iq,
'jid': jid,
'vcard_error': moment().format()
});
}
}
/* Event handlers */
_converse.initVCardCollection = function () {
_converse.vcards = new _converse.VCards();
const id = b64_sha1(`converse.vcards`);
_converse.vcards.browserStorage = new Backbone.BrowserStorage[_converse.config.get('storage')](id);
_converse.vcards.fetch();
function createStanza (type, jid, vcard_el) {
const iq = $iq(jid ? {'type': type, 'to': jid} : {'type': type});
if (!vcard_el) {
iq.c("vCard", {'xmlns': Strophe.NS.VCARD});
} else {
iq.cnode(vcard_el);
}
_converse.api.listen.on('sessionInitialized', _converse.initVCardCollection);
return iq;
}
function setVCard (jid, data) {
if (!jid) {
throw Error("No jid provided for the VCard data");
}
const vcard_el = Strophe.xmlHtmlNode(tpl_vcard(data)).firstElementChild;
return _converse.api.sendIQ(createStanza("set", jid, vcard_el));
}
_converse.on('addClientFeatures', () => {
_converse.api.disco.own.features.add(Strophe.NS.VCARD);
});
_.extend(_converse.api, {
/**
* The XEP-0054 VCard API
*
* This API lets you access and update user VCards
*
* @namespace _converse.api.vcard
* @memberOf _converse.api
*/
'vcard': {
/**
* Enables setting new values for a VCard.
*
* @method _converse.api.vcard.set
* @param {string} jid The JID for which the VCard should be set
* @param {object} data A map of VCard keys and values
* @example
* _converse.api.vcard.set({
* 'jid': _converse.bare_jid,
* 'fn': 'John Doe',
* 'nickname': 'jdoe'
* }).then(() => {
* // Succes
* }).catch(() => {
* // Failure
* }).
*/
'set' (jid, data) {
return setVCard(jid, data);
},
/**
* @method _converse.api.vcard.get
* @param {Backbone.Model|string} model Either a `Backbone.Model` instance, or a string JID.
* If a `Backbone.Model` instance is passed in, then it must have either a `jid`
* attribute or a `muc_jid` attribute.
* @param {boolean} [force] A boolean indicating whether the vcard should be
* fetched even if it's been fetched before.
* @returns {promise} A Promise which resolves with the VCard data for a particular JID or for
* a `Backbone.Model` instance which represents an entity with a JID (such as a roster contact,
* chat or chatroom occupant).
*
* @example
* _converse.api.waitUntil('rosterContactsFetched').then(() => {
* _converse.api.vcard.get('someone@example.org').then(
* (vcard) => {
* // Do something with the vcard...
* }
* );
* });
*/
'get' (model, force) {
if (_.isString(model)) {
return getVCard(_converse, model);
} else if (force ||
!model.get('vcard_updated') ||
!moment(model.get('vcard_error')).isSame(new Date(), "day")) {
const jid = model.get('jid');
if (!jid) {
throw new Error("No JID to get vcard for!");
}
return getVCard(_converse, jid);
} else {
return Promise.resolve({});
}
},
/**
* Fetches the VCard associated with a particular `Backbone.Model` instance
* (by using its `jid` or `muc_jid` attribute) and then updates the model with the
* returned VCard data.
*
* @method _converse.api.vcard.update
* @param {Backbone.Model} model A `Backbone.Model` instance
* @param {boolean} [force] A boolean indicating whether the vcard should be
* fetched again even if it's been fetched before.
* @returns {promise} A promise which resolves once the update has completed.
* @example
* _converse.api.waitUntil('rosterContactsFetched').then(() => {
* const chatbox = _converse.chatboxes.getChatBox('someone@example.org');
* _converse.api.vcard.update(chatbox);
* });
*/
'update' (model, force) {
return this.get(model, force)
.then(vcard => {
delete vcard['stanza']
model.save(vcard);
});
}
}
function getVCard (_converse, jid) {
/* Request the VCard of another user. Returns a promise.
*
* Parameters:
* (String) jid - The Jabber ID of the user whose VCard
* is being requested.
*/
const to = Strophe.getBareJidFromJid(jid) === _converse.bare_jid ? null : jid;
return new Promise((resolve, reject) => {
_converse.connection.sendIQ(
createStanza("get", to),
_.partial(onVCardData, jid, _, resolve),
_.partial(onVCardError, jid, _, resolve),
_converse.IQ_TIMEOUT
);
});
}
});
}));
/* Event handlers */
_converse.initVCardCollection = function () {
_converse.vcards = new _converse.VCards();
const id = b64_sha1(`converse.vcards`);
_converse.vcards.browserStorage = new Backbone.BrowserStorage[_converse.config.get('storage')](id);
_converse.vcards.fetch();
}
_converse.api.listen.on('sessionInitialized', _converse.initVCardCollection);
_converse.on('addClientFeatures', () => {
_converse.api.disco.own.features.add(Strophe.NS.VCARD);
});
_.extend(_converse.api, {
/**
* The XEP-0054 VCard API
*
* This API lets you access and update user VCards
*
* @namespace _converse.api.vcard
* @memberOf _converse.api
*/
'vcard': {
/**
* Enables setting new values for a VCard.
*
* @method _converse.api.vcard.set
* @param {string} jid The JID for which the VCard should be set
* @param {object} data A map of VCard keys and values
* @example
* _converse.api.vcard.set({
* 'jid': _converse.bare_jid,
* 'fn': 'John Doe',
* 'nickname': 'jdoe'
* }).then(() => {
* // Succes
* }).catch(() => {
* // Failure
* }).
*/
'set' (jid, data) {
return setVCard(jid, data);
},
/**
* @method _converse.api.vcard.get
* @param {Backbone.Model|string} model Either a `Backbone.Model` instance, or a string JID.
* If a `Backbone.Model` instance is passed in, then it must have either a `jid`
* attribute or a `muc_jid` attribute.
* @param {boolean} [force] A boolean indicating whether the vcard should be
* fetched even if it's been fetched before.
* @returns {promise} A Promise which resolves with the VCard data for a particular JID or for
* a `Backbone.Model` instance which represents an entity with a JID (such as a roster contact,
* chat or chatroom occupant).
*
* @example
* _converse.api.waitUntil('rosterContactsFetched').then(() => {
* _converse.api.vcard.get('someone@example.org').then(
* (vcard) => {
* // Do something with the vcard...
* }
* );
* });
*/
'get' (model, force) {
if (_.isString(model)) {
return getVCard(_converse, model);
} else if (force ||
!model.get('vcard_updated') ||
!moment(model.get('vcard_error')).isSame(new Date(), "day")) {
const jid = model.get('jid');
if (!jid) {
throw new Error("No JID to get vcard for!");
}
return getVCard(_converse, jid);
} else {
return Promise.resolve({});
}
},
/**
* Fetches the VCard associated with a particular `Backbone.Model` instance
* (by using its `jid` or `muc_jid` attribute) and then updates the model with the
* returned VCard data.
*
* @method _converse.api.vcard.update
* @param {Backbone.Model} model A `Backbone.Model` instance
* @param {boolean} [force] A boolean indicating whether the vcard should be
* fetched again even if it's been fetched before.
* @returns {promise} A promise which resolves once the update has completed.
* @example
* _converse.api.waitUntil('rosterContactsFetched').then(() => {
* const chatbox = _converse.chatboxes.getChatBox('someone@example.org');
* _converse.api.vcard.update(chatbox);
* });
*/
'update' (model, force) {
return this.get(model, force)
.then(vcard => {
delete vcard['stanza']
model.save(vcard);
});
}
}
});
}
});

View File

@ -3,477 +3,441 @@
//
// This is the utilities module.
//
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
// Copyright (c) 2013-2018, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define, escape, window, Uint8Array */
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define([
"sizzle",
"es6-promise/dist/es6-promise.auto",
"../lodash.noconflict",
"backbone",
"strophe.js",
], factory);
} else {
// Used by the mockups
const Strophe = {
'Strophe': root.Strophe,
'$build': root.$build,
'$iq': root.$iq,
'$msg': root.$msg,
'$pres': root.$pres,
'SHA1': root.SHA1,
'MD5': root.MD5,
'b64_hmac_sha1': root.b64_hmac_sha1,
'b64_sha1': root.b64_sha1,
'str_hmac_sha1': root.str_hmac_sha1,
'str_sha1': root.str_sha1
};
root.converse_utils = factory(
root.sizzle,
root.Promise,
null,
root._,
root.Backbone,
Strophe
);
}
}(this, function (
sizzle,
Promise,
_,
Backbone,
Strophe
) {
"use strict";
Strophe = Strophe.Strophe;
/*global escape, Uint8Array */
const u = {};
import Backbone from "backbone";
import Promise from "es6-promise/dist/es6-promise.auto";
import { Strophe } from "strophe.js";
import _ from "../lodash.noconflict";
import sizzle from "sizzle";
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;
}
const 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;
}
}
return candidates.reduce(reducer, '');
}
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.isValidJID = function (jid) {
return _.compact(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
};
u.isValidMUCJID = function (jid) {
return !jid.startsWith('@') && !jid.endsWith('@');
};
u.isSameBareJID = function (jid1, jid2) {
return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
Strophe.getBareJidFromJid(jid2).toLowerCase();
};
u.getMostRecentMessage = function (model) {
const messages = model.messages.filter('message');
return messages[messages.length-1];
}
u.isNewMessage = function (message) {
/* Given a stanza, determine whether it's a new
* message, i.e. not a MAM archived one.
*/
if (message instanceof Element) {
return !(
sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, message).length &&
sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, message).length
);
} else {
return !(message.get('is_delayed') && message.get('is_archived'));
return accumulator;
}
};
}
return candidates.reduce(reducer, '');
}
u.isOnlyChatStateNotification = function (attrs) {
if (attrs instanceof Backbone.Model) {
attrs = attrs.attributes;
}
return attrs['chat_state'] &&
!attrs['oob_url'] &&
!attrs['file'] &&
!(attrs['is_encrypted'] && attrs['plaintext']) &&
!attrs['message'];
};
u.isHeadlineMessage = function (_converse, message) {
var from_jid = message.getAttribute('from');
if (message.getAttribute('type') === 'headline') {
return true;
}
const chatbox = _converse.chatboxes.get(Strophe.getBareJidFromJid(from_jid));
if (chatbox && chatbox.get('type') === 'chatroom') {
return false;
}
if (message.getAttribute('type') !== 'error' &&
!_.isNil(from_jid) &&
!_.includes(from_jid, '@')) {
// Some servers (I'm looking at you Prosody) don't set the message
// type to "headline" when sending server messages. For now we
// check if an @ signal is included, and if not, we assume it's
// a headline message.
return true;
}
return false;
};
u.merge = function merge (first, second) {
/* Merge the second object into the first one.
*/
for (var k in second) {
if (_.isObject(first[k])) {
merge(first[k], second[k]);
} else {
first[k] = second[k];
}
}
};
u.applyUserSettings = function applyUserSettings (context, settings, user_settings) {
/* Configuration settings might be nested objects. We only want to
* add settings which are whitelisted.
*/
for (var k in settings) {
if (_.isUndefined(user_settings[k])) {
continue;
}
if (_.isObject(settings[k]) && !_.isArray(settings[k])) {
applyUserSettings(context[k], settings[k], user_settings[k]);
} else {
context[k] = user_settings[k];
}
}
};
u.stringToNode = function (s) {
/* Converts an HTML string into a DOM Node.
* Expects that the HTML string has only one top-level element,
* i.e. not multiple ones.
*
* Parameters:
* (String) s - The HTML string
*/
var div = document.createElement('div');
div.innerHTML = s;
return div.firstElementChild;
};
u.getOuterWidth = function (el, include_margin=false) {
var width = el.offsetWidth;
if (!include_margin) {
return width;
}
var style = window.getComputedStyle(el);
width += parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10);
return width;
};
u.stringToElement = function (s) {
/* Converts an HTML string into a DOM element.
* Expects that the HTML string has only one top-level element,
* i.e. not multiple ones.
*
* Parameters:
* (String) s - The HTML string
*/
var div = document.createElement('div');
div.innerHTML = s;
return div.firstElementChild;
};
u.matchesSelector = function (el, selector) {
/* Checks whether the DOM element matches the given selector.
*
* Parameters:
* (DOMElement) el - The DOM element
* (String) selector - The selector
*/
return (
el.matches ||
el.matchesSelector ||
el.msMatchesSelector ||
el.mozMatchesSelector ||
el.webkitMatchesSelector ||
el.oMatchesSelector
).call(el, selector);
};
u.queryChildren = function (el, selector) {
/* Returns a list of children of the DOM element that match the
* selector.
*
* Parameters:
* (DOMElement) el - the DOM element
* (String) selector - the selector they should be matched
* against.
*/
return _.filter(el.childNodes, _.partial(u.matchesSelector, _, selector));
};
u.contains = function (attr, query) {
return function (item) {
if (typeof attr === 'object') {
var value = false;
_.forEach(attr, function (a) {
value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase());
});
return value;
} else if (typeof attr === 'string') {
return _.includes(item.get(attr).toLowerCase(), query.toLowerCase());
} else {
throw new TypeError('contains: wrong attribute type. Must be string or array.');
}
};
};
u.isOfType = function (type, item) {
return item.get('type') == type;
};
u.isInstance = function (type, item) {
return item instanceof type;
};
u.getAttribute = function (key, item) {
return item.get(key);
};
u.contains.not = function (attr, query) {
return function (item) {
return !(u.contains(attr, query)(item));
};
};
u.rootContains = function (root, el) {
// The document element does not have the contains method in IE.
if (root === document && !root.contains) {
return document.head.contains(el) || document.body.contains(el);
}
return root.contains ? root.contains(el) : window.HTMLElement.prototype.contains.call(root, el);
};
u.createFragmentFromText = function (markup) {
/* Returns a DocumentFragment containing DOM nodes based on the
* passed-in markup text.
*/
// http://stackoverflow.com/questions/9334645/create-node-from-markup-string
var frag = document.createDocumentFragment(),
tmp = document.createElement('body'), child;
tmp.innerHTML = markup;
// Append elements in a loop to a DocumentFragment, so that the
// browser does not re-render the document for each node.
while (child = tmp.firstChild) { // eslint-disable-line no-cond-assign
frag.appendChild(child);
}
return frag
};
u.isPersistableModel = function (model) {
return model.collection && model.collection.browserStorage;
};
u.getResolveablePromise = function () {
/* Returns a promise object on which `resolve` or `reject` can be
* called.
*/
const wrapper = {};
const promise = new Promise((resolve, reject) => {
wrapper.resolve = resolve;
wrapper.reject = reject;
})
_.assign(promise, wrapper);
return promise;
};
u.interpolate = function (string, o) {
return string.replace(/{{{([^{}]*)}}}/g,
(a, b) => {
var r = o[b];
return typeof r === 'string' || typeof r === 'number' ? r : a;
});
};
u.onMultipleEvents = function (events=[], callback) {
/* Call the callback once all the events have been triggered
*
* Parameters:
* (Array) events: An array of objects, with keys `object` and
* `event`, representing the event name and the object it's
* triggered upon.
* (Function) callback: The function to call once all events have
* been triggered.
*/
let triggered = [];
function handler (result) {
triggered.push(result)
if (events.length === triggered.length) {
callback(triggered);
triggered = [];
}
}
_.each(events, (map) => map.object.on(map.event, handler));
};
u.safeSave = function (model, attributes) {
if (u.isPersistableModel(model)) {
model.save(attributes);
} 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)) {
return false;
}
// XXX: Taken from jQuery's "visible" implementation
return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
};
u.triggerEvent = function (el, name, type="Event", bubbles=true, cancelable=true) {
const evt = document.createEvent(type);
evt.initEvent(name, bubbles, cancelable);
el.dispatchEvent(evt);
};
u.geoUriToHttp = function(text, geouri_replacement) {
const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g;
return text.replace(regex, geouri_replacement);
};
u.httpToGeoUri = function(text, _converse) {
const replacement = 'geo:$1,$2';
return text.replace(_converse.geouri_regex, replacement);
};
u.getSelectValues = function (select) {
const result = [];
const options = select && select.options;
for (var i=0, iLen=options.length; i<iLen; i++) {
const opt = options[i];
if (opt.selected) {
result.push(opt.value || opt.text);
}
}
return result;
};
u.formatFingerprint = function (fp) {
fp = fp.replace(/^05/, '');
const arr = [];
for (let i=1; i<8; i++) {
const idx = i*8+i-1;
fp = fp.slice(0, idx) + ' ' + fp.slice(idx);
}
return fp;
};
u.appendArrayBuffer = function (buffer1, buffer2) {
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer;
};
u.arrayBufferToHex = function (ab) {
// https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex#40031979
return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
};
u.arrayBufferToString = function (ab) {
return new TextDecoder("utf-8").decode(ab);
};
u.stringToArrayBuffer = function (string) {
const bytes = new TextEncoder("utf-8").encode(string);
return bytes.buffer;
};
u.arrayBufferToBase64 = function (ab) {
return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), ''));
};
u.base64ToArrayBuffer = function (b64) {
const binary_string = window.atob(b64),
len = binary_string.length,
bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i)
}
return bytes.buffer
};
u.getRandomInt = function (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 () {
return 'xxxxxxxx-xxxx'.replace(/[x]/g, function(c) {
var r = Math.random() * 16 | 0,
v = c === 'x' ? r : r & 0x3 | 0x8;
return v.toString(16);
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.isValidJID = function (jid) {
return _.compact(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
};
u.isValidMUCJID = function (jid) {
return !jid.startsWith('@') && !jid.endsWith('@');
};
u.isSameBareJID = function (jid1, jid2) {
return Strophe.getBareJidFromJid(jid1).toLowerCase() ===
Strophe.getBareJidFromJid(jid2).toLowerCase();
};
u.getMostRecentMessage = function (model) {
const messages = model.messages.filter('message');
return messages[messages.length-1];
}
u.isNewMessage = function (message) {
/* Given a stanza, determine whether it's a new
* message, i.e. not a MAM archived one.
*/
if (message instanceof Element) {
return !(
sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, message).length &&
sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, message).length
);
} else {
return !(message.get('is_delayed') && message.get('is_archived'));
}
};
u.isOnlyChatStateNotification = function (attrs) {
if (attrs instanceof Backbone.Model) {
attrs = attrs.attributes;
}
return attrs['chat_state'] &&
!attrs['oob_url'] &&
!attrs['file'] &&
!(attrs['is_encrypted'] && attrs['plaintext']) &&
!attrs['message'];
};
u.isHeadlineMessage = function (_converse, message) {
var from_jid = message.getAttribute('from');
if (message.getAttribute('type') === 'headline') {
return true;
}
const chatbox = _converse.chatboxes.get(Strophe.getBareJidFromJid(from_jid));
if (chatbox && chatbox.get('type') === 'chatroom') {
return false;
}
if (message.getAttribute('type') !== 'error' &&
!_.isNil(from_jid) &&
!_.includes(from_jid, '@')) {
// Some servers (I'm looking at you Prosody) don't set the message
// type to "headline" when sending server messages. For now we
// check if an @ signal is included, and if not, we assume it's
// a headline message.
return true;
}
return false;
};
u.merge = function merge (first, second) {
/* Merge the second object into the first one.
*/
for (var k in second) {
if (_.isObject(first[k])) {
merge(first[k], second[k]);
} else {
first[k] = second[k];
}
}
};
u.applyUserSettings = function applyUserSettings (context, settings, user_settings) {
/* Configuration settings might be nested objects. We only want to
* add settings which are whitelisted.
*/
for (var k in settings) {
if (_.isUndefined(user_settings[k])) {
continue;
}
if (_.isObject(settings[k]) && !_.isArray(settings[k])) {
applyUserSettings(context[k], settings[k], user_settings[k]);
} else {
context[k] = user_settings[k];
}
}
};
u.stringToNode = function (s) {
/* Converts an HTML string into a DOM Node.
* Expects that the HTML string has only one top-level element,
* i.e. not multiple ones.
*
* Parameters:
* (String) s - The HTML string
*/
var div = document.createElement('div');
div.innerHTML = s;
return div.firstElementChild;
};
u.getOuterWidth = function (el, include_margin=false) {
var width = el.offsetWidth;
if (!include_margin) {
return width;
}
var style = window.getComputedStyle(el);
width += parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10);
return width;
};
u.stringToElement = function (s) {
/* Converts an HTML string into a DOM element.
* Expects that the HTML string has only one top-level element,
* i.e. not multiple ones.
*
* Parameters:
* (String) s - The HTML string
*/
var div = document.createElement('div');
div.innerHTML = s;
return div.firstElementChild;
};
u.matchesSelector = function (el, selector) {
/* Checks whether the DOM element matches the given selector.
*
* Parameters:
* (DOMElement) el - The DOM element
* (String) selector - The selector
*/
return (
el.matches ||
el.matchesSelector ||
el.msMatchesSelector ||
el.mozMatchesSelector ||
el.webkitMatchesSelector ||
el.oMatchesSelector
).call(el, selector);
};
u.queryChildren = function (el, selector) {
/* Returns a list of children of the DOM element that match the
* selector.
*
* Parameters:
* (DOMElement) el - the DOM element
* (String) selector - the selector they should be matched
* against.
*/
return _.filter(el.childNodes, _.partial(u.matchesSelector, _, selector));
};
u.contains = function (attr, query) {
return function (item) {
if (typeof attr === 'object') {
var value = false;
_.forEach(attr, function (a) {
value = value || _.includes(item.get(a).toLowerCase(), query.toLowerCase());
});
return value;
} else if (typeof attr === 'string') {
return _.includes(item.get(attr).toLowerCase(), query.toLowerCase());
} else {
throw new TypeError('contains: wrong attribute type. Must be string or array.');
}
};
return u;
}));
};
u.isOfType = function (type, item) {
return item.get('type') == type;
};
u.isInstance = function (type, item) {
return item instanceof type;
};
u.getAttribute = function (key, item) {
return item.get(key);
};
u.contains.not = function (attr, query) {
return function (item) {
return !(u.contains(attr, query)(item));
};
};
u.rootContains = function (root, el) {
// The document element does not have the contains method in IE.
if (root === document && !root.contains) {
return document.head.contains(el) || document.body.contains(el);
}
return root.contains ? root.contains(el) : window.HTMLElement.prototype.contains.call(root, el);
};
u.createFragmentFromText = function (markup) {
/* Returns a DocumentFragment containing DOM nodes based on the
* passed-in markup text.
*/
// http://stackoverflow.com/questions/9334645/create-node-from-markup-string
var frag = document.createDocumentFragment(),
tmp = document.createElement('body'), child;
tmp.innerHTML = markup;
// Append elements in a loop to a DocumentFragment, so that the
// browser does not re-render the document for each node.
while (child = tmp.firstChild) { // eslint-disable-line no-cond-assign
frag.appendChild(child);
}
return frag
};
u.isPersistableModel = function (model) {
return model.collection && model.collection.browserStorage;
};
u.getResolveablePromise = function () {
/* Returns a promise object on which `resolve` or `reject` can be
* called.
*/
const wrapper = {};
const promise = new Promise((resolve, reject) => {
wrapper.resolve = resolve;
wrapper.reject = reject;
})
_.assign(promise, wrapper);
return promise;
};
u.interpolate = function (string, o) {
return string.replace(/{{{([^{}]*)}}}/g,
(a, b) => {
var r = o[b];
return typeof r === 'string' || typeof r === 'number' ? r : a;
});
};
u.onMultipleEvents = function (events=[], callback) {
/* Call the callback once all the events have been triggered
*
* Parameters:
* (Array) events: An array of objects, with keys `object` and
* `event`, representing the event name and the object it's
* triggered upon.
* (Function) callback: The function to call once all events have
* been triggered.
*/
let triggered = [];
function handler (result) {
triggered.push(result)
if (events.length === triggered.length) {
callback(triggered);
triggered = [];
}
}
_.each(events, (map) => map.object.on(map.event, handler));
};
u.safeSave = function (model, attributes) {
if (u.isPersistableModel(model)) {
model.save(attributes);
} 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)) {
return false;
}
// XXX: Taken from jQuery's "visible" implementation
return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
};
u.triggerEvent = function (el, name, type="Event", bubbles=true, cancelable=true) {
const evt = document.createEvent(type);
evt.initEvent(name, bubbles, cancelable);
el.dispatchEvent(evt);
};
u.geoUriToHttp = function(text, geouri_replacement) {
const regex = /geo:([\-0-9.]+),([\-0-9.]+)(?:,([\-0-9.]+))?(?:\?(.*))?/g;
return text.replace(regex, geouri_replacement);
};
u.httpToGeoUri = function(text, _converse) {
const replacement = 'geo:$1,$2';
return text.replace(_converse.geouri_regex, replacement);
};
u.getSelectValues = function (select) {
const result = [];
const options = select && select.options;
for (var i=0, iLen=options.length; i<iLen; i++) {
const opt = options[i];
if (opt.selected) {
result.push(opt.value || opt.text);
}
}
return result;
};
u.formatFingerprint = function (fp) {
fp = fp.replace(/^05/, '');
const arr = [];
for (let i=1; i<8; i++) {
const idx = i*8+i-1;
fp = fp.slice(0, idx) + ' ' + fp.slice(idx);
}
return fp;
};
u.appendArrayBuffer = function (buffer1, buffer2) {
const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp.buffer;
};
u.arrayBufferToHex = function (ab) {
// https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex#40031979
return Array.prototype.map.call(new Uint8Array(ab), x => ('00' + x.toString(16)).slice(-2)).join('');
};
u.arrayBufferToString = function (ab) {
return new TextDecoder("utf-8").decode(ab);
};
u.stringToArrayBuffer = function (string) {
const bytes = new TextEncoder("utf-8").encode(string);
return bytes.buffer;
};
u.arrayBufferToBase64 = function (ab) {
return btoa((new Uint8Array(ab)).reduce((data, byte) => data + String.fromCharCode(byte), ''));
};
u.base64ToArrayBuffer = function (b64) {
const binary_string = window.atob(b64),
len = binary_string.length,
bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i)
}
return bytes.buffer
};
u.getRandomInt = function (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 () {
return 'xxxxxxxx-xxxx'.replace(/[x]/g, function(c) {
var r = Math.random() * 16 | 0,
v = c === 'x' ? r : r & 0x3 | 0x8;
return v.toString(16);
});
};
export default u;

File diff suppressed because one or more lines are too long

View File

@ -5,39 +5,32 @@
//
// Copyright (c) 2013-2018, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define */
(function (root, factory) {
define([
"../lodash.noconflict",
"./core",
"../templates/field.html"
], factory);
}(this, function (_, u, tpl_field) {
"use strict";
u.webForm2xForm = function (field) {
/* Takes an HTML DOM and turns it into an XForm field.
*
* Parameters:
* (DOMElement) field - the field to convert
*/
let value;
if (field.getAttribute('type') === 'checkbox') {
value = field.checked && 1 || 0;
} else if (field.tagName == "TEXTAREA") {
value = _.filter(field.value.split('\n'), _.trim);
} else if (field.tagName == "SELECT") {
value = u.getSelectValues(field);
} else {
value = field.value;
}
return u.stringToNode(
tpl_field({
'name': field.getAttribute('name'),
'value': value
})
);
};
return u;
}));
import _ from "../lodash.noconflict";
import tpl_field from "../templates/field.html";
import u from "./core";
u.webForm2xForm = function (field) {
/* Takes an HTML DOM and turns it into an XForm field.
*
* Parameters:
* (DOMElement) field - the field to convert
*/
let value;
if (field.getAttribute('type') === 'checkbox') {
value = field.checked && 1 || 0;
} else if (field.tagName == "TEXTAREA") {
value = _.filter(field.value.split('\n'), _.trim);
} else if (field.tagName == "SELECT") {
value = u.getSelectValues(field);
} else {
value = field.value;
}
return u.stringToNode(
tpl_field({
'name': field.getAttribute('name'),
'value': value
})
);
};
export default u;

View File

@ -3,101 +3,102 @@
//
// This is the utilities module.
//
// Copyright (c) 2012-2017, Jan-Carel Brand <jc@opkode.com>
// Copyright (c) 2013-2018, Jan-Carel Brand <jc@opkode.com>
// Licensed under the Mozilla Public License (MPLv2)
//
/*global define, escape, Jed */
(function (root, factory) {
define(["../converse-core", "./core"], factory);
}(this, function (converse, u) {
"use strict";
/*global escape, Jed */
const { Strophe, sizzle, _ } = converse.env;
u.computeAffiliationsDelta = function computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) {
/* Given two lists of objects with 'jid', 'affiliation' and
* 'reason' properties, return a new list containing
* those objects that are new, changed or removed
* (depending on the 'remove_absentees' boolean).
*
* The affiliations for new and changed members stay the
* same, for removed members, the affiliation is set to 'none'.
*
* The 'reason' property is not taken into account when
* comparing whether affiliations have been changed.
*
* Parameters:
* (Boolean) exclude_existing: Indicates whether JIDs from
* the new list which are also in the old list
* (regardless of affiliation) should be excluded
* from the delta. One reason to do this
* would be when you want to add a JID only if it
* doesn't have *any* existing affiliation at all.
* (Boolean) remove_absentees: Indicates whether JIDs
* from the old list which are not in the new list
* should be considered removed and therefore be
* included in the delta with affiliation set
* to 'none'.
* (Array) new_list: Array containing the new affiliations
* (Array) old_list: Array containing the old affiliations
*/
const new_jids = _.map(new_list, 'jid');
const old_jids = _.map(old_list, 'jid');
import converse from "@converse/headless/converse-core";
import u from "./core";
// Get the new affiliations
let delta = _.map(
_.difference(new_jids, old_jids),
(jid) => new_list[_.indexOf(new_jids, jid)]
);
if (!exclude_existing) {
// Get the changed affiliations
delta = delta.concat(_.filter(new_list, function (item) {
const idx = _.indexOf(old_jids, item.jid);
if (idx >= 0) {
return item.affiliation !== old_list[idx].affiliation;
}
return false;
}));
}
if (remove_absentees) {
// Get the removed affiliations
delta = delta.concat(
_.map(
_.difference(old_jids, new_jids),
(jid) => ({'jid': jid, 'affiliation': 'none'})
)
);
}
return delta;
};
const { Strophe, sizzle, _ } = converse.env;
u.parseMemberListIQ = function parseMemberListIQ (iq) {
/* Given an IQ stanza with a member list, create an array of member objects.
*/
return _.map(
sizzle(`query[xmlns="${Strophe.NS.MUC_ADMIN}"] item`, iq),
(item) => {
const data = {
'affiliation': item.getAttribute('affiliation'),
}
const jid = item.getAttribute('jid');
if (u.isValidJID(jid)) {
data['jid'] = jid;
} else {
// XXX: Prosody sends nick for the jid attribute value
// Perhaps for anonymous room?
data['nick'] = jid;
}
const nick = item.getAttribute('nick');
if (nick) {
data['nick'] = nick;
}
const role = item.getAttribute('role');
if (role) {
data['role'] = nick;
}
return data;
u.computeAffiliationsDelta = function computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) {
/* Given two lists of objects with 'jid', 'affiliation' and
* 'reason' properties, return a new list containing
* those objects that are new, changed or removed
* (depending on the 'remove_absentees' boolean).
*
* The affiliations for new and changed members stay the
* same, for removed members, the affiliation is set to 'none'.
*
* The 'reason' property is not taken into account when
* comparing whether affiliations have been changed.
*
* Parameters:
* (Boolean) exclude_existing: Indicates whether JIDs from
* the new list which are also in the old list
* (regardless of affiliation) should be excluded
* from the delta. One reason to do this
* would be when you want to add a JID only if it
* doesn't have *any* existing affiliation at all.
* (Boolean) remove_absentees: Indicates whether JIDs
* from the old list which are not in the new list
* should be considered removed and therefore be
* included in the delta with affiliation set
* to 'none'.
* (Array) new_list: Array containing the new affiliations
* (Array) old_list: Array containing the old affiliations
*/
const new_jids = _.map(new_list, 'jid');
const old_jids = _.map(old_list, 'jid');
// Get the new affiliations
let delta = _.map(
_.difference(new_jids, old_jids),
(jid) => new_list[_.indexOf(new_jids, jid)]
);
if (!exclude_existing) {
// Get the changed affiliations
delta = delta.concat(_.filter(new_list, function (item) {
const idx = _.indexOf(old_jids, item.jid);
if (idx >= 0) {
return item.affiliation !== old_list[idx].affiliation;
}
return false;
}));
}
if (remove_absentees) {
// Get the removed affiliations
delta = delta.concat(
_.map(
_.difference(old_jids, new_jids),
(jid) => ({'jid': jid, 'affiliation': 'none'})
)
);
};
}));
}
return delta;
};
u.parseMemberListIQ = function parseMemberListIQ (iq) {
/* Given an IQ stanza with a member list, create an array of member objects.
*/
return _.map(
sizzle(`query[xmlns="${Strophe.NS.MUC_ADMIN}"] item`, iq),
(item) => {
const data = {
'affiliation': item.getAttribute('affiliation'),
}
const jid = item.getAttribute('jid');
if (u.isValidJID(jid)) {
data['jid'] = jid;
} else {
// XXX: Prosody sends nick for the jid attribute value
// Perhaps for anonymous room?
data['nick'] = jid;
}
const nick = item.getAttribute('nick');
if (nick) {
data['nick'] = nick;
}
const role = item.getAttribute('role');
if (role) {
data['role'] = nick;
}
return data;
}
);
};

File diff suppressed because it is too large Load Diff

View File

@ -112,6 +112,8 @@
modal.el.querySelector('input[name="chatroom"]').value = jid;
modal.el.querySelector('input[name="nickname"]').value = nick;
modal.el.querySelector('form input[type="submit"]').click();
await utils.waitUntil(() => _converse.chatboxviews.get(jid), 1000);
return _converse.chatboxviews.get(jid);
};
utils.openChatRoom = function (_converse, room, server, nick) {