// Converse.js (A browser based XMPP chat client) // https://conversejs.org // // Copyright (c) 2019, Jan-Carel Brand // Licensed under the Mozilla Public License (MPLv2) // /** * @module converse-bookmark-views * @description * Converse.js plugin which adds views for XEP-0048 bookmarks */ import "backbone.nativeview"; import BrowserStorage from "backbone.browserStorage"; import { OrderedListView } from "backbone.overview"; import converse from "@converse/headless/converse-core"; import muc from "@converse/headless/converse-muc"; import tpl_bookmark from "templates/bookmark.html"; import tpl_bookmarks_list from "templates/bookmarks_list.html" import tpl_chatroom_bookmark_form from "templates/chatroom_bookmark_form.html"; import tpl_chatroom_bookmark_toggle from "templates/chatroom_bookmark_toggle.html"; const { Backbone, Promise, Strophe, $iq, sizzle, _ } = converse.env; const u = converse.env.utils; converse.plugins.add('converse-bookmark-views', { /* 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-chatboxes", "converse-muc", "converse-muc-views"], 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. ChatRoomView: { events: { 'click .toggle-bookmark': 'toggleBookmark' }, async renderHeading () { this.__super__.renderHeading.apply(this, arguments); const { _converse } = this.__super__; if (_converse.allow_bookmarks) { const supported = await _converse.checkBookmarksSupport(); if (supported) { this.renderBookmarkToggle(); } } } } }, initialize () { /* The initialize function gets called as soon as the plugin is * loaded by converse.js's plugin machinery. */ const { _converse } = this, { __ } = _converse; // Configuration values for this plugin // ==================================== // Refer to docs/source/configuration.rst for explanations of these // configuration settings. _converse.api.settings.update({ hide_open_bookmarks: true, muc_respect_autojoin: true }); Object.assign(_converse, { removeBookmarkViaEvent (ev) { /* Remove a bookmark as determined by the passed in * event. */ ev.preventDefault(); const name = ev.target.getAttribute('data-bookmark-name'); const jid = ev.target.getAttribute('data-room-jid'); if (confirm(__("Are you sure you want to remove the bookmark \"%1$s\"?", name))) { _.invokeMap(_converse.bookmarks.where({'jid': jid}), Backbone.Model.prototype.destroy); } }, addBookmarkViaEvent (ev) { /* Add a bookmark as determined by the passed in * event. */ ev.preventDefault(); const jid = ev.target.getAttribute('data-room-jid'); const chatroom = _converse.api.rooms.open(jid, {'bring_to_foreground': true}); _converse.chatboxviews.get(jid).renderBookmarkForm(); }, }); const bookmarkableChatRoomView = { renderBookmarkToggle () { if (this.el.querySelector('.chat-head .toggle-bookmark')) { return; } const { _converse } = this.__super__, { __ } = _converse; const bookmark_button = tpl_chatroom_bookmark_toggle( _.assignIn(this.model.toJSON(), { 'info_toggle_bookmark': this.model.get('bookmarked') ? __('Unbookmark this groupchat') : __('Bookmark this groupchat'), 'bookmarked': this.model.get('bookmarked') })); const buttons_row = this.el.querySelector('.chatbox-buttons') const close_button = buttons_row.querySelector('.close-chatbox-button'); if (close_button) { close_button.insertAdjacentHTML('afterend', bookmark_button); } else { buttons_row.insertAdjacentHTML('beforeEnd', bookmark_button); } }, setBookmarkState () { /* Set whether the groupchat is bookmarked or not. */ if (!_.isUndefined(_converse.bookmarks)) { const models = _converse.bookmarks.where({'jid': this.model.get('jid')}); if (!models.length) { this.model.save('bookmarked', false); } else { this.model.save('bookmarked', true); } } }, renderBookmarkForm () { this.hideChatRoomContents(); if (!this.bookmark_form) { this.bookmark_form = new _converse.MUCBookmarkForm({ 'model': this.model, 'chatroomview': this }); const container_el = this.el.querySelector('.chatroom-body'); container_el.insertAdjacentElement('beforeend', this.bookmark_form.el); } u.showElement(this.bookmark_form.el); }, toggleBookmark (ev) { if (ev) { ev.preventDefault(); ev.stopPropagation(); } const models = _converse.bookmarks.where({'jid': this.model.get('jid')}); if (!models.length) { this.renderBookmarkForm(); } else { models.forEach(model => model.destroy()); } } } Object.assign(_converse.ChatRoomView.prototype, bookmarkableChatRoomView); _converse.MUCBookmarkForm = Backbone.VDOMView.extend({ className: 'muc-bookmark-form', events: { 'submit form': 'onBookmarkFormSubmitted', 'click .button-cancel': 'closeBookmarkForm' }, initialize (attrs) { this.chatroomview = attrs.chatroomview; this.render(); }, toHTML () { return tpl_chatroom_bookmark_form({ 'default_nick': this.model.get('nick'), 'heading': __('Bookmark this groupchat'), 'label_autojoin': __('Would you like this groupchat to be automatically joined upon startup?'), 'label_cancel': __('Cancel'), 'label_name': __('The name for this bookmark:'), 'label_nick': __('What should your nickname for this groupchat be?'), 'label_submit': __('Save'), 'name': this.model.get('name') }); }, onBookmarkFormSubmitted (ev) { ev.preventDefault(); _converse.bookmarks.createBookmark({ 'jid': this.model.get('jid'), 'autojoin': _.get(ev.target.querySelector('input[name="autojoin"]'), 'checked') || false, 'name': _.get(ev.target.querySelector('input[name=name]'), 'value'), 'nick': _.get(ev.target.querySelector('input[name=nick]'), 'value') }); this.closeBookmarkForm(ev); }, closeBookmarkForm (ev) { ev.preventDefault(); this.chatroomview.closeForm(); } }); _converse.BookmarkView = Backbone.VDOMView.extend({ toHTML () { return tpl_bookmark({ 'hidden': _converse.hide_open_bookmarks && _converse.chatboxes.where({'jid': this.model.get('jid')}).length, 'bookmarked': true, 'info_leave_room': __('Leave this groupchat'), 'info_remove': __('Remove this bookmark'), 'info_remove_bookmark': __('Unbookmark this groupchat'), 'info_title': __('Show more information on this groupchat'), 'jid': this.model.get('jid'), 'name': Strophe.xmlunescape(this.model.get('name')), 'open_title': __('Click to open this groupchat') }); } }); _converse.BookmarksView = OrderedListView.extend({ tagName: 'div', className: 'bookmarks-list list-container rooms-list-container', events: { 'click .add-bookmark': 'addBookmark', 'click .bookmarks-toggle': 'toggleBookmarksList', 'click .remove-bookmark': 'removeBookmark', 'click .open-room': 'openRoom', }, listSelector: '.rooms-list', ItemView: _converse.BookmarkView, subviewIndex: 'jid', initialize () { OrderedListView.prototype.initialize.apply(this, arguments); this.model.on('add', this.showOrHide, this); this.model.on('remove', this.showOrHide, this); _converse.chatboxes.on('add', this.renderBookmarkListElement, this); _converse.chatboxes.on('remove', this.renderBookmarkListElement, this); const storage = _converse.config.get('storage'), id = `converse.room-bookmarks${_converse.bare_jid}-list-model`; this.list_model = new _converse.BookmarksList({'id': id}); this.list_model.browserStorage = new BrowserStorage[storage](id); const render = () => { this.render(); this.sortAndPositionAllItems(); } this.list_model.fetch({'success': render, 'error': render}); }, render () { this.el.innerHTML = tpl_bookmarks_list({ 'toggle_state': this.list_model.get('toggle-state'), 'desc_bookmarks': __('Click to toggle the bookmarks list'), 'label_bookmarks': __('Bookmarks'), '_converse': _converse }); 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('.bookmarks-list'); if (!_.isNull(el)) { el.parentNode.replaceChild(this.el, 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, true); }, removeBookmark: _converse.removeBookmarkViaEvent, addBookmark: _converse.addBookmarkViaEvent, renderBookmarkListElement (chatbox) { const bookmarkview = this.get(chatbox.get('jid')); if (_.isNil(bookmarkview)) { // A chat box has been closed, but we don't have a // bookmark for it, so nothing further to do here. return; } bookmarkview.render(); this.showOrHide(); }, showOrHide (item) { if (_converse.hide_open_bookmarks) { const bookmarks = this.model.filter((bookmark) => !_converse.chatboxes.get(bookmark.get('jid'))); if (!bookmarks.length) { u.hideElement(this.el); return; } } if (this.model.models.length) { u.showElement(this.el); } }, toggleBookmarksList (ev) { if (ev && ev.preventDefault) { ev.preventDefault(); } const icon_el = ev.target.matches('.fa') ? ev.target : ev.target.querySelector('.fa'); if (u.hasClass('fa-caret-down', icon_el)) { u.slideIn(this.el.querySelector('.bookmarks')); this.list_model.save({'toggle-state': _converse.CLOSED}); icon_el.classList.remove("fa-caret-down"); icon_el.classList.add("fa-caret-right"); } else { icon_el.classList.remove("fa-caret-right"); icon_el.classList.add("fa-caret-down"); u.slideOut(this.el.querySelector('.bookmarks')); this.list_model.save({'toggle-state': _converse.OPENED}); } } }); /************************ BEGIN Event Handlers ************************/ const initBookmarkViews = async function () { await _converse.api.waitUntil('roomsPanelRendered'); _converse.bookmarksview = new _converse.BookmarksView({'model': _converse.bookmarks}); /** * Triggered once the _converse.Bookmarks collection and _converse.BookmarksView view * has been created and cached bookmarks have been fetched. * @event _converse#bookmarkViewsInitialized * @example _converse.api.listen.on('bookmarkViewsInitialized', () => { ... }); */ _converse.api.trigger('bookmarkViewsInitialized'); } _converse.api.listen.on('bookmarksInitialized', initBookmarkViews); _converse.api.listen.on('chatRoomOpened', view => view.setBookmarkState()); /************************ END Event Handlers ************************/ } });