Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -233,17 +233,7 @@ eslint: node_modules
check: eslint | src/headless/dist/converse-headless.js dist/converse.js dist/converse.css
npm run types
make check-git-clean
cd src/log && npm test
cd src/headless && npm run test -- --single-run
npm run test -- --single-run

.PHONY: test-headless
test-headless:
cd src/headless && npm run test -- $(ARGS)

.PHONY: test
test:
npm run test -- $(ARGS)
npm run test:all

########################################################################
## Documentation
Expand Down
6 changes: 3 additions & 3 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1697,13 +1697,13 @@ You can set the URL where the sound files are hosted with the `sounds_path`_ opt

Requires the `src/converse-notification.js` plugin.

popular_reactions
-----------------
popular_emojis
--------------

* Default: ``[':thumbsup:', ':heart:', ':joy:', ':open_mouth:']``

An array of emoji shortnames that are displayed as quick-access reaction buttons
in the message reaction picker (see `XEP-0444: Message Reactions <https://xmpp.org/extensions/xep-0444.html>`_).
in the emoji and message reaction pickers (see `XEP-0444: Message Reactions <https://xmpp.org/extensions/xep-0444.html>`_).

These emojis are shown as the first row of buttons in the reaction picker,
allowing users to quickly react to messages without opening the full emoji selector.
Expand Down
5 changes: 4 additions & 1 deletion karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ module.exports = function(config) {
{ pattern: "src/shared/tests/mock.js", type: 'module' },
{ pattern: "src/headless/tests/mock.js", type: 'module' },

// Run earlier, otherwise it fails (times out) with the message avatar not updating for some reason
{ pattern: "src/plugins/chatview/tests/message-avatar.js", type: 'module' },

// Ideally this should go into the headless test runner
{ pattern: "src/headless/plugins/vcard/tests/update.js", type: 'module' },

Expand Down Expand Up @@ -124,7 +127,7 @@ module.exports = function(config) {

// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['Chrome'],
browsers: ['ChromeHeadless'],

// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
Expand Down
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,11 @@
"nodeps": "rspack build --config rspack/rspack.nodeps.js",
"serve": "http-server -c-1",
"serve-tls": "http-server -S -C certs/chat.example.org.crt -K certs/chat.example.org.key",
"test": "karma start karma.conf",
"test:all": "npm run test:headless -- --single-run && npm run test -- --single-run",
"test:headless": "cd src/headless && karma start karma.conf.js",
"test": "karma start karma.conf --single-run",
"test:browser": "karma start karma.conf --browsers Chrome",
"test:all": "npm run test:headless && npm run test",
"test:headless": "cd src/headless && karma start karma.conf.js --single-run",
"test:headless:browser": "cd src/headless && karma start karma.conf.js --browsers Chrome",
"types": "tsc -p ./src/log/tsconfig.json && tsc -p ./src/headless/tsconfig.json && tsc",
"types:check": "tsc --noEmit",
"watch": "concurrently \"npm run watch:headless\" \"npm run watch:main\"",
Expand Down
9 changes: 5 additions & 4 deletions src/headless/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@ module.exports = function (config) {
{ pattern: 'dist/*.js.map', included: false },
'dist/converse-headless.js',
{ pattern: 'tests/*.js', type: 'module' },
{ pattern: 'shared/settings/tests/settings.js', type: 'module' },
{ pattern: 'plugins/blocklist/tests/*.js', type: 'module' },
{ pattern: 'plugins/caps/tests/*.js', type: 'module' },
{ pattern: 'plugins/bookmarks/tests/*.js', type: 'module' },
{ pattern: 'plugins/caps/tests/*.js', type: 'module' },
{ pattern: 'plugins/chat/tests/*.js', type: 'module' },
{ pattern: 'plugins/disco/tests/*.js', type: 'module' },
{ pattern: 'plugins/emoji/tests/*.js', type: 'module' },
{ pattern: 'plugins/mam/tests/*.js', type: 'module' },
{ pattern: 'plugins/muc/tests/*.js', type: 'module' },
{ pattern: 'plugins/ping/tests/*.js', type: 'module' },
{ pattern: 'plugins/pubsub/tests/*.js', type: 'module' },
{ pattern: 'plugins/reactions/tests/*.js', type: 'module' },
{ pattern: 'plugins/roster/tests/*.js', type: 'module' },
{ pattern: 'plugins/smacks/tests/*.js', type: 'module' },
{ pattern: 'plugins/status/tests/*.js', type: 'module' },
{ pattern: 'plugins/reactions/tests/*.js', type: 'module' },
{ pattern: 'shared/settings/tests/settings.js', type: 'module' },
],
proxies: {
'/dist/': '/base/dist/',
Expand All @@ -43,7 +44,7 @@ module.exports = function (config) {
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
browsers: ['ChromeHeadless'],
singleRun: false,
concurrency: Infinity,
});
Expand Down
2 changes: 1 addition & 1 deletion src/headless/plugins/chat/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ class ChatBox extends ModelWithVCard(ModelWithMessages(ModelWithContact(ColorAwa
const is_spoiler = !!this.get('composing_spoiler');
const origin_id = u.getUniqueId();
const text = attrs?.body;
const body = text ? u.shortnamesToUnicode(text) : undefined;
const body = text ? u.emojis.shortnamesToUnicode(text) : undefined;

// Get reply attributes from chatbox model if replying to a message
const reply_to_id = this.get('reply_to_id');
Expand Down
5 changes: 5 additions & 0 deletions src/headless/plugins/emoji/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@

// Regex to detect shortname keys (e.g. ':thumbsup:')
export const SHORTNAME_RE = /^:[a-zA-Z0-9_+*-]+:$/;
// How long to debounce/wait before publishing popular emojis
export const PUBLISH_DEBOUNCE_MILLIS = 30_000;
2 changes: 1 addition & 1 deletion src/headless/plugins/emoji/emoji.json
Original file line number Diff line number Diff line change
Expand Up @@ -2233,7 +2233,7 @@
":grey_exclamation:":{"sn":":grey_exclamation:","cp":"2755","sns":[],"c":"symbols"},
":grey_question:":{"sn":":grey_question:","cp":"2754","sns":[],"c":"symbols"},
":hash:":{"sn":":hash:","cp":"23-fe0f-20e3","sns":[],"c":"symbols"},
":heart:":{"sn":":heart:","cp":"2764","sns":[],"c":"symbols"},
":heart:":{"sn":":heart:","cp":"2764-fe0f","sns":[],"c":"symbols"},
":heart_decoration:":{"sn":":heart_decoration:","cp":"1f49f","sns":[],"c":"symbols"},
":heart_exclamation:":{"sn":":heart_exclamation:","cp":"2763","sns":[":heavy_heart_exclamation_mark_ornament:"],"c":"symbols"},
":heartbeat:":{"sn":":heartbeat:","cp":"1f493","sns":[],"c":"symbols"},
Expand Down
59 changes: 59 additions & 0 deletions src/headless/plugins/emoji/handlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import api from '../../shared/api/index.js';
import converse from '../../shared/api/public.js';
import _converse from '../../shared/_converse.js';
import { getCodePointReferences, getShortnameReferences, isOnlyEmojis } from './utils.js';

const { Strophe } = converse.env;

export function registerPEPPushHandler() {
const bare_jid = _converse.session.get('bare_jid');
api.connection.get().addHandler(
/** @param {Element} stanza */
(stanza) => {
const { popular_emojis } = _converse.state;
popular_emojis?.applyPopularEmojisFromStanza(stanza);
return true;
},
Strophe.NS.REACTIONS_POPULAR,
'message',
'headline',
null,
bare_jid,
);
}

/**
* Given a message, extract emojis from it and use them to update the list of
* popular emojis.
* @param {import('../../plugins/reactions/utils').BaseMessage} message
*/
export async function updatePopularEmojis(message) {
const body = message.get('body');
if (!body) return;

await api.emojis.initialize();

const { popular_emojis } = _converse.state;
if (!popular_emojis) return;

const shortname_refs = new Set(getShortnameReferences(body).map(({ shortname }) => shortname));
const cp_refs = new Set(getCodePointReferences(body).map(({ emoji }) => emoji));
const emojis = [...shortname_refs, ...cp_refs];

if (emojis.length) {
popular_emojis.recordUsage(emojis);
}
}

/**
* @param {import('../../shared/types').MessageAttributes} attrs
* @param {String} text
* @returns {Promise<import('../../shared/types').MessageAttributes & { is_only_emojis: boolean }>}
*/
export async function parseMessage(attrs, text) {
await api.emojis.initialize();
return {
...attrs,
is_only_emojis: text ? isOnlyEmojis(text) : false,
};
}
88 changes: 47 additions & 41 deletions src/headless/plugins/emoji/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,51 +9,59 @@ import api from '../../shared/api/index.js';
import converse from '../../shared/api/public.js';
import EmojiPicker from './picker.js';
import emojis from './api.js';
import { isOnlyEmojis } from './utils.js';
import { parseMessage, registerPEPPushHandler, updatePopularEmojis } from './handlers.js';
import PopularEmojis from './popular-emojis.js';

const { Strophe } = converse.env;

Strophe.addNamespace('REACTIONS_POPULAR', 'urn:xmpp:reactions:popular:0');

converse.emojis = {
initialized: false,
initialized_promise: getOpenPromise(),
};

converse.plugins.add('converse-emoji', {
initialize () {
initialize() {
/* The initialize function gets called as soon as the plugin is
* loaded by converse.js's plugin machinery.
*/
const { ___ } = _converse;

api.settings.extend({
'emoji_image_path': 'https://twemoji.maxcdn.com/v/12.1.6/',
'emoji_categories': {
'smileys': ':grinning:',
'people': ':thumbsup:',
'activity': ':soccer:',
'travel': ':motorcycle:',
'objects': ':bomb:',
'nature': ':rainbow:',
'food': ':hotdog:',
'symbols': ':musical_note:',
'flags': ':flag_ac:',
'custom': null,
emoji_image_path: 'https://twemoji.maxcdn.com/v/12.1.6/',
emoji_categories: {
popular: ':star:',
smileys: ':grinning:',
people: ':thumbsup:',
activity: ':soccer:',
travel: ':motorcycle:',
objects: ':bomb:',
nature: ':rainbow:',
food: ':hotdog:',
symbols: ':musical_note:',
flags: ':flag_ac:',
custom: null,
},
popular_emojis: [':thumbsup:', ':heart:', ':laughing:', ':joy:', ':tada:'],
// We use the triple-underscore method which doesn't actually
// translate but does signify to gettext that these strings should
// go into the POT file. The translation then happens in the
// template. We do this so that users can pass in their own
// strings via converse.initialize, which is before __ is
// available.
'emoji_category_labels': {
'smileys': ___('Smileys and emotions'),
'people': ___('People'),
'activity': ___('Activities'),
'travel': ___('Travel'),
'objects': ___('Objects'),
'nature': ___('Animals and nature'),
'food': ___('Food and drink'),
'symbols': ___('Symbols'),
'flags': ___('Flags'),
'custom': ___('Stickers'),
emoji_category_labels: {
popular: ___('Frequently used'),
smileys: ___('Smileys and emotions'),
people: ___('People'),
activity: ___('Activities'),
travel: ___('Travel'),
objects: ___('Objects'),
nature: ___('Animals and nature'),
food: ___('Food and drink'),
symbols: ___('Symbols'),
flags: ___('Flags'),
custom: ___('Stickers'),
},
});

Expand All @@ -62,24 +70,22 @@ converse.plugins.add('converse-emoji', {
Object.assign(_converse.exports, exports);
Object.assign(api, emojis);

api.listen.on('getOutgoingMessageAttributes', async (_chat, attrs) => {
await api.emojis.initialize();
const { original_text: text } = attrs;
return {
...attrs,
is_only_emojis: text ? isOnlyEmojis(text) : false,
};
api.listen.on('connected', () => {
registerPEPPushHandler();
if (_converse.state.popular_emojis) return;

const popular_emojis = new PopularEmojis();
Object.assign(_converse.state, { popular_emojis });
});

async function parseMessage (_stanza, attrs) {
await api.emojis.initialize();
return {
...attrs,
is_only_emojis: attrs.body ? isOnlyEmojis(attrs.body) : false
}
}
api.listen.on('clearSession', () => {
_converse.state.popular_emojis?.debouncedPublish.flush();
delete _converse.state.popular_emojis;
});

api.listen.on('parseMUCMessage', parseMessage);
api.listen.on('parseMessage', parseMessage);
api.listen.on('getOutgoingMessageAttributes', async (_chat, attrs) => parseMessage(attrs, attrs.original_text));
api.listen.on('parseMUCMessage', (_chat, attrs) => parseMessage(attrs, attrs.body));
api.listen.on('parseMessage', (_chat, attrs) => parseMessage(attrs, attrs.body));
api.listen.on('sendMessage', async ({ message }) => updatePopularEmojis(message));
},
});
Loading
Loading