Refactoring of the headlines plugins

- Move template to relevant plugin
- Turn ElementView into CustomElement
- Use the terminology "Headlines Feed" instead of "Headlines Box"
- Break the `converse-headlines` plugin up into multiple files
- Fix CSS styling for headlines feeds for the Dracula theme
This commit is contained in:
JC Brand 2022-05-08 20:05:30 +02:00
parent 52693bfc0b
commit 858a6051ac
20 changed files with 323 additions and 271 deletions

View File

@ -9,7 +9,7 @@ import "./plugins/caps/index.js"; // XEP-0115 Entity Capabilities
import "./plugins/chat/index.js"; // RFC-6121 Instant messaging
import "./plugins/chatboxes/index.js";
import "./plugins/disco/index.js"; // XEP-0030 Service discovery
import "./plugins/headlines.js"; // Support for headline messages
import "./plugins/headlines/index.js"; // Support for headline messages
import "./plugins/mam/index.js"; // XEP-0313 Message Archive Management
import "./plugins/muc/index.js"; // XEP-0045 Multi-user chat
import "./plugins/ping/index.js"; // XEP-0199 XMPP Ping

View File

@ -1,164 +0,0 @@
/**
* @module converse-headlines
* @copyright 2022, the Converse.js contributors
* @description XEP-0045 Multi-User Chat Views
*/
import { _converse, api, converse } from "@converse/headless/core";
import { isHeadline, isServerMessage } from '@converse/headless/shared/parsers';
import { parseMessage } from '@converse/headless/plugins/chat/parsers';
converse.plugins.add('converse-headlines', {
/* 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-chat"],
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);
}
},
}
},
initialize () {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
/**
* Shows headline messages
* @class
* @namespace _converse.HeadlinesBox
* @memberOf _converse
*/
_converse.HeadlinesBox = _converse.ChatBox.extend({
defaults () {
return {
'bookmarked': false,
'hidden': ['mobile', 'fullscreen'].includes(api.settings.get("view_mode")),
'message_type': 'headline',
'num_unread': 0,
'time_opened': this.get('time_opened') || (new Date()).getTime(),
'type': _converse.HEADLINES_TYPE
}
},
async initialize () {
this.set({'box_id': `box-${this.get('jid')}`});
this.initUI();
this.initMessages();
await this.fetchMessages();
/**
* Triggered once a {@link _converse.HeadlinesBox} has been created and initialized.
* @event _converse#headlinesBoxInitialized
* @type { _converse.HeadlinesBox }
* @example _converse.api.listen.on('headlinesBoxInitialized', model => { ... });
*/
api.trigger('headlinesBoxInitialized', this);
}
});
async function onHeadlineMessage (stanza) {
// Handler method for all incoming messages of type "headline".
if (isHeadline(stanza) || isServerMessage(stanza)) {
const from_jid = stanza.getAttribute('from');
await api.waitUntil('rosterInitialized')
if (from_jid.includes('@') &&
!_converse.roster.get(from_jid) &&
!api.settings.get("allow_non_roster_messaging")) {
return;
}
if (stanza.querySelector('body') === null) {
// 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
});
const attrs = await parseMessage(stanza, _converse);
await chatbox.createMessage(attrs);
api.trigger('message', {chatbox, stanza, attrs});
}
}
/************************ BEGIN Event Handlers ************************/
function registerHeadlineHandler () {
_converse.connection.addHandler(message => (onHeadlineMessage(message) || true), null, 'message');
}
api.listen.on('connected', registerHeadlineHandler);
api.listen.on('reconnected', registerHeadlineHandler);
/************************ END Event Handlers ************************/
/************************ BEGIN API ************************/
Object.assign(api, {
/**
* The "headlines" namespace, which is used for headline-channels
* which are read-only channels containing messages of type
* "headline".
*
* @namespace api.headlines
* @memberOf api
*/
headlines: {
/**
* Retrieves a headline-channel or all headline-channels.
*
* @method api.headlines.get
* @param {String|String[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [create=false] - Whether the chat should be created if it's not found.
* @returns { Promise<_converse.HeadlinesBox> }
*/
async get (jids, attrs={}, create=false) {
async function _get (jid) {
let model = await api.chatboxes.get(jid);
if (!model && create) {
model = await api.chatboxes.create(jid, attrs, _converse.HeadlinesBox);
} else {
model = (model && model.get('type') === _converse.HEADLINES_TYPE) ? model : null;
if (model && Object.keys(attrs).length) {
model.save(attrs);
}
}
return model;
}
if (jids === undefined) {
const chats = await api.chatboxes.get();
return chats.filter(c => (c.get('type') === _converse.HEADLINES_TYPE));
} else if (typeof jids === 'string') {
return _get(jids);
}
return Promise.all(jids.map(jid => _get(jid)));
}
}
});
/************************ END API ************************/
}
});

View File

@ -0,0 +1,44 @@
import { _converse, api } from "@converse/headless/core";
export default {
/**
* The "headlines" namespace, which is used for headline-channels
* which are read-only channels containing messages of type
* "headline".
*
* @namespace api.headlines
* @memberOf api
*/
headlines: {
/**
* Retrieves a headline-channel or all headline-channels.
*
* @method api.headlines.get
* @param {String|String[]} jids - e.g. 'buddy@example.com' or ['buddy1@example.com', 'buddy2@example.com']
* @param {Object} [attrs] - Attributes to be set on the _converse.ChatBox model.
* @param {Boolean} [create=false] - Whether the chat should be created if it's not found.
* @returns { Promise<_converse.HeadlinesFeed> }
*/
async get (jids, attrs={}, create=false) {
async function _get (jid) {
let model = await api.chatboxes.get(jid);
if (!model && create) {
model = await api.chatboxes.create(jid, attrs, _converse.HeadlinesFeed);
} else {
model = (model && model.get('type') === _converse.HEADLINES_TYPE) ? model : null;
if (model && Object.keys(attrs).length) {
model.save(attrs);
}
}
return model;
}
if (jids === undefined) {
const chats = await api.chatboxes.get();
return chats.filter(c => (c.get('type') === _converse.HEADLINES_TYPE));
} else if (typeof jids === 'string') {
return _get(jids);
}
return Promise.all(jids.map(jid => _get(jid)));
}
}
};

View File

@ -0,0 +1,31 @@
import ChatBox from '@converse/headless/plugins/chat/model.js';
import { _converse, api } from '../../core.js';
export default class HeadlinesFeed extends ChatBox {
defaults () {
return {
'bookmarked': false,
'hidden': ['mobile', 'fullscreen'].includes(api.settings.get("view_mode")),
'message_type': 'headline',
'num_unread': 0,
'time_opened': this.get('time_opened') || (new Date()).getTime(),
'type': _converse.HEADLINES_TYPE
}
}
async initialize () {
this.set({'box_id': `box-${this.get('jid')}`});
this.initUI();
this.initMessages();
await this.fetchMessages();
/**
* Triggered once a { @link _converse.HeadlinesFeed } has been created and initialized.
* @event _converse#headlinesFeedInitialized
* @type { _converse.HeadlinesFeed }
* @example _converse.api.listen.on('headlinesFeedInitialized', model => { ... });
*/
api.trigger('headlinesFeedInitialized', this);
}
}

View File

@ -0,0 +1,62 @@
/**
* @module converse-headlines
* @copyright 2022, the Converse.js contributors
* @description XEP-0045 Multi-User Chat Views
*/
import HeadlinesFeed from './feed.js';
import headlines_api from './api.js';
import { _converse, api, converse } from "@converse/headless/core";
import { onHeadlineMessage } from './utils.js';
converse.plugins.add('converse-headlines', {
/* 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-chat"],
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.HeadlinesFeed(attrs, options);
} else {
return this.__super__.model.apply(this, arguments);
}
},
}
},
initialize () {
/**
* Shows headline messages
* @class
* @namespace _converse.HeadlinesFeed
* @memberOf _converse
*/
_converse.HeadlinesFeed = HeadlinesFeed;
function registerHeadlineHandler () {
_converse.connection.addHandler(m => (onHeadlineMessage(m) || true), null, 'message');
}
api.listen.on('connected', registerHeadlineHandler);
api.listen.on('reconnected', registerHeadlineHandler);
Object.assign(api, headlines_api);
}
});

View File

@ -0,0 +1,33 @@
import { _converse, api } from "@converse/headless/core";
import { isHeadline, isServerMessage } from '@converse/headless/shared/parsers';
import { parseMessage } from '@converse/headless/plugins/chat/parsers';
/**
* Handler method for all incoming messages of type "headline".
* @param { XMLElement } stanza
*/
export async function onHeadlineMessage (stanza) {
if (isHeadline(stanza) || isServerMessage(stanza)) {
const from_jid = stanza.getAttribute('from');
await api.waitUntil('rosterInitialized')
if (from_jid.includes('@') &&
!_converse.roster.get(from_jid) &&
!api.settings.get("allow_non_roster_messaging")) {
return;
}
if (stanza.querySelector('body') === null) {
// 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
});
const attrs = await parseMessage(stanza, _converse);
await chatbox.createMessage(attrs);
api.trigger('message', {chatbox, stanza, attrs});
}
}

View File

@ -26,7 +26,7 @@ export default class ChatView extends BaseChatView {
/**
* Triggered once the {@link _converse.ChatBoxView} has been initialized
* @event _converse#chatBoxViewInitialized
* @type { _converse.HeadlinesBoxView }
* @type { _converse.ChatBoxView }
* @example _converse.api.listen.on('chatBoxViewInitialized', view => { ... });
*/
api.trigger('chatBoxViewInitialized', this);

View File

@ -38,7 +38,7 @@ export default (el) => {
${o.connected
? html`
<converse-user-profile></converse-user-profile>
<converse-headlines-panel class="controlbox-section"></converse-headlines-panel>
<converse-headlines-feeds-list class="controlbox-section"></converse-headlines-feeds-list>
<div id="chatrooms" class="controlbox-section">
<converse-rooms-list></converse-rooms-list>
<converse-bookmarks></converse-bookmarks>

View File

@ -0,0 +1,37 @@
import tpl_feeds_list from './templates/feeds-list.js';
import { CustomElement } from 'shared/components/element.js';
import { _converse, api } from '@converse/headless/core';
/**
* Custom element which renders a list of headline feeds
* @class
* @namespace _converse.HeadlinesFeedsList
* @memberOf _converse
*/
export class HeadlinesFeedsList extends CustomElement {
initialize () {
this.model = _converse.chatboxes;
this.listenTo(this.model, 'add', (m) => this.renderIfHeadline(m));
this.listenTo(this.model, 'remove', (m) => this.renderIfHeadline(m));
this.listenTo(this.model, 'destroy', (m) => this.renderIfHeadline(m));
this.requestUpdate();
}
render () {
return tpl_feeds_list(this);
}
renderIfHeadline (model) {
return model?.get('type') === _converse.HEADLINES_TYPE && this.requestUpdate();
}
async openHeadline (ev) { // eslint-disable-line class-methods-use-this
ev.preventDefault();
const jid = ev.target.getAttribute('data-headline-jid');
const feed = await api.headlines.get(jid);
feed.maybeShow(true);
}
}
api.elements.define('converse-headlines-feeds-list', HeadlinesFeedsList);

View File

@ -5,7 +5,7 @@
*/
import '../chatview/index.js';
import './view.js';
import { HeadlinesPanel } from './panel.js';
import { HeadlinesFeedsList } from './feed-list.js';
import { _converse, converse } from '@converse/headless/core';
import './styles/headlines.scss';
@ -25,6 +25,9 @@ converse.plugins.add('converse-headlines-view', {
dependencies: ['converse-headlines', 'converse-chatview'],
initialize () {
_converse.HeadlinesPanel = HeadlinesPanel;
_converse.HeadlinesFeedsList = HeadlinesFeedsList;
// Deprecated
_converse.HeadlinesPanel = HeadlinesFeedsList;
}
});

View File

@ -1,45 +0,0 @@
import tpl_headline_panel from './templates/panel.js';
import { ElementView } from '@converse/skeletor/src/element.js';
import { __ } from 'i18n';
import { _converse, api } from '@converse/headless/core';
/**
* View which renders headlines section of the control box.
* @class
* @namespace _converse.HeadlinesPanel
* @memberOf _converse
*/
export class HeadlinesPanel extends ElementView {
events = {
'click .open-headline': 'openHeadline'
}
initialize () {
this.model = _converse.chatboxes;
this.listenTo(this.model, 'add', this.renderIfHeadline);
this.listenTo(this.model, 'remove', this.renderIfHeadline);
this.listenTo(this.model, 'destroy', this.renderIfHeadline);
this.render();
}
toHTML () {
return tpl_headline_panel({
'heading_headline': __('Announcements'),
'headlineboxes': this.model.filter(m => m.get('type') === _converse.HEADLINES_TYPE),
'open_title': __('Click to open this server message')
});
}
renderIfHeadline (model) {
return model && model.get('type') === _converse.HEADLINES_TYPE && this.render();
}
openHeadline (ev) { // eslint-disable-line class-methods-use-this
ev.preventDefault();
const jid = ev.target.getAttribute('data-headline-jid');
const chat = _converse.chatboxes.get(jid);
chat.maybeShow(true);
}
}
api.elements.define('converse-headlines-panel', HeadlinesPanel);

View File

@ -1,23 +1,50 @@
.conversejs {
.chat-head-headline {
background-color: var(--headline-head-color);
}
.chatbox {
converse-headlines-heading {
&.chat-head {
.chatbox-title__text {
color: var(--headline-head-text-color) !important;
background-color: var(--headline-head-bg-color);
}
.chatbox.headlines {
.chat-head {
&.chat-head-chatbox {
background-color: var(--headline-head-color);
a, a:visited, a:hover, a:not([href]):not([tabindex]) {
&.chatbox-btn {
&.fa,
&.fas,
&.far {
color: var(--headline-head-text-color);
&.button-on:before {
padding: 0.2em;
background-color: var(--headline-head-text-color);
color: var(--headline-head-bg-color);
}
}
}
}
}
}
.chat-body {
background-color: var(--headline-head-color);
.chat-message {
color: var(--headline-message-color);
&.headlines {
.chat-head {
&.chat-head-chatbox {
background-color: var(--headline-head-bg-color);
border-bottom: var(--headline-head-border-bottom);
}
}
.chat-body {
background-color: var(--background);
.chat-message {
color: var(--headline-message-color);
}
hr {
border-bottom: var(--headline-separator-border-bottom);
}
}
.chat-content {
height: 100%;
}
}
.chat-content {
height: 100%;
}
}
.message {
@ -29,22 +56,29 @@
}
}
}
}
.conversejs {
#controlbox {
.controlbox-section {
.controlbox-heading--headline {
color: var(--headline-head-text-color);
}
}
}
converse-chats {
&.converse-fullscreen {
.chatbox.headlines {
.box-flyout {
background-color: var(--headline-head-color);
background-color: var(--headline-head-text-color);
}
.chat-head {
&.chat-head-chatbox {
background-color: var(--headline-head-color);
background-color: var(--headline-head-text-color);
}
}
.flyout {
border-color: var(--headline-head-color);
border-color: var(--headline-head-text-color);
}
}
}

View File

@ -0,0 +1,33 @@
import { __ } from 'i18n';
import { _converse } from '@converse/headless/core';
import { html } from "lit";
const tpls_headlines_feeds_list_item = (el, feed) => {
const open_title = __('Click to open this server message');
return html`
<div class="list-item controlbox-padded d-flex flex-row"
data-headline-jid="${feed.get('jid')}">
<a class="list-item-link open-headline available-room w-100"
data-headline-jid="${feed.get('jid')}"
title="${open_title}"
@click=${ev => el.openHeadline(ev)}
href="#">${feed.get('jid')}</a>
</div>
`;
}
export default (el) => {
const feeds = el.model.filter(m => m.get('type') === _converse.HEADLINES_TYPE);
const heading_headline = __('Announcements');
return html`
<div class="controlbox-section" id="headline">
<div class="d-flex controlbox-padded ${ feeds.length ? '' : 'hidden' }">
<span class="w-100 controlbox-heading controlbox-heading--headline">${heading_headline}</span>
</div>
</div>
<div class="list-container list-container--headline ${ feeds.length ? '' : 'hidden' }">
<div class="items-list rooms-list headline-list">
${ feeds.map(feed => tpls_headlines_feeds_list_item(el, feed)) }
</div>
</div>`
}

View File

@ -1,12 +0,0 @@
import { html } from "lit";
import tpl_headline_list from "templates/headline_list.js";
export default (o) => html`
<div class="controlbox-section" id="headline">
<div class="d-flex controlbox-padded ${ o.headlineboxes.length ? '' : 'hidden' }">
<span class="w-100 controlbox-heading controlbox-heading--headline">${o.heading_headline}</span>
</div>
</div>
${ tpl_headline_list(o) }
`;

View File

@ -68,7 +68,12 @@ describe("A headlines box", function () {
it("will show headline messages in the controlbox", mock.initConverse(
[], {}, async function (_converse) {
await mock.waitForRoster(_converse, 'current', 0);
await mock.waitForRoster(_converse, 'current', 1);
await mock.openControlBox(_converse);
const sender_jid = mock.cur_names[0].replace(/ /g,'.').toLowerCase() + '@montague.lit';
await mock.openChatBoxFor(_converse, sender_jid);
const { u, $msg} = converse.env;
/* <message from='notify.example.com'
* to='romeo@im.example.com'

View File

@ -3,7 +3,7 @@ import tpl_headlines from './templates/headlines.js';
import { _converse, api } from '@converse/headless/core';
class HeadlinesView extends BaseChatView {
class HeadlinesFeedView extends BaseChatView {
async initialize() {
_converse.chatboxviews.add(this.jid, this);
@ -20,9 +20,9 @@ class HeadlinesView extends BaseChatView {
await this.model.messages.fetched;
this.model.maybeShow();
/**
* Triggered once the {@link _converse.HeadlinesBoxView} has been initialized
* Triggered once the { @link _converse.HeadlinesFeedView } has been initialized
* @event _converse#headlinesBoxViewInitialized
* @type { _converse.HeadlinesBoxView }
* @type { _converse.HeadlinesFeedView }
* @example _converse.api.listen.on('headlinesBoxViewInitialized', view => { ... });
*/
api.trigger('headlinesBoxViewInitialized', this);
@ -52,4 +52,4 @@ class HeadlinesView extends BaseChatView {
}
}
api.elements.define('converse-headlines', HeadlinesView);
api.elements.define('converse-headlines', HeadlinesFeedView);

View File

@ -58,7 +58,7 @@ function getBoxesWidth (newchat) {
* to create space.
* @private
* @method _converse.ChatBoxViews#trimChats
* @param { _converse.ChatBoxView|_converse.ChatRoomView|_converse.ControlBoxView|_converse.HeadlinesBoxView } [newchat]
* @param { _converse.ChatBoxView|_converse.ChatRoomView|_converse.ControlBoxView|_converse.HeadlinesFeedView } [newchat]
*/
export function trimChats (newchat) {
if (_converse.isTestEnv() || api.settings.get('no_trimming') || api.settings.get("view_mode") !== 'overlayed') {

View File

@ -131,8 +131,11 @@
--muc-toolbar-btn-color: var(--redder-orange);
--muc-toolbar-btn-disabled-color: gray;
--headline-head-color: var(--orange);
--headlines-color: var(--orange);
--headline-head-text-color: var(--white);
--headline-head-bg-color: var(--headlines-color);
--headline-message-color: #D2842B;
--headline-separator-border-bottom: 2px solid var(--headlines-color);
--chatbox-button-size: 14px;
--fullpage-chatbox-button-size: 16px;

View File

@ -23,6 +23,13 @@
// ---
--headlines-color: var(--pink);
--headline-head-text-color: var(--headlines-color);
--headline-head-bg-color: var(--background);
--headline-message-color: var(--headlines-color);
--headline-separator-border-bottom: 2px solid var(--headlines-color);
--headline-head-border-bottom: 0.15em solid var(--headlines-color);
--icon-hover-color: var(--cyan);
--gray-color: var(--comment);

View File

@ -1,19 +0,0 @@
import { html } from "lit";
const tpl_headline_box = (o) => html`
<div class="list-item controlbox-padded d-flex flex-row"
data-headline-jid="${o.headlinebox.get('jid')}">
<a class="list-item-link open-headline available-room w-100"
data-headline-jid="${o.headlinebox.get('jid')}"
title="${o.open_title}" href="#">${o.headlinebox.get('jid')}</a>
</div>
`;
export default (o) => html`
<div class="list-container list-container--headline ${ o.headlineboxes.length ? '' : 'hidden' }">
<div class="items-list rooms-list headline-list">
${ o.headlineboxes.map(headlinebox => tpl_headline_box(Object.assign({headlinebox}, o))) }
</div>
</div>
`;