Skip to content

Commit 3f3a529

Browse files
committed
Add support for animated emojis
1 parent d1fc4cc commit 3f3a529

421 files changed

Lines changed: 540 additions & 162 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 24 additions & 1 deletion

build/commands/update-emojis.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const sharp = require('sharp');
4+
const fetch = require('node-fetch');
5+
6+
const utils = require('../utils');
7+
8+
const emojisDir = utils.pathResolve('src/res/animated_emojis/');
9+
10+
if (!fs.existsSync(emojisDir)) {
11+
fs.mkdirSync(emojisDir);
12+
}
13+
14+
async function getGoogleEmojiList() {
15+
const response = await fetch('https://googlefonts.github.io/noto-emoji-animation/data/api.json');
16+
if (!response.ok) {
17+
throw new Error(`Failed to fetch data: ${response.statusText}`);
18+
}
19+
const data = await response.json();
20+
21+
return data.icons.map((icon) => ({
22+
codepoint: icon.codepoint,
23+
name: icon.tags[0].slice(1, -1),
24+
}));
25+
}
26+
27+
async function downloadEmoji(codepoint, gifPath) {
28+
const url = `https://fonts.gstatic.com/s/e/notoemoji/latest/${codepoint}/512.gif`;
29+
30+
const response = await fetch(url);
31+
if (!response.ok) {
32+
throw new Error(`Failed to fetch emoji: ${response.statusText}`);
33+
}
34+
35+
const gifBuffer = await response.buffer();
36+
37+
// Resize the GIF to 64px and save it to the emojis directory
38+
await sharp(gifBuffer, { animated: true })
39+
.resize(64)
40+
.toFile(gifPath);
41+
}
42+
43+
(async () => {
44+
try {
45+
const localDataPath = path.join(emojisDir, 'data.json');
46+
const localOut = {};
47+
const local = {};
48+
49+
try {
50+
const localRaw = await fs.promises.readFile(localDataPath);
51+
Object.assign(local, JSON.parse(localRaw));
52+
} catch (error) {
53+
console.error(`Could not read local data.json: ${error.message}`);
54+
}
55+
56+
const emojis = await getGoogleEmojiList();
57+
58+
for (let i = 0; i < emojis.length; i++) {
59+
const { codepoint, name } = emojis[i];
60+
const unified = codepoint.toUpperCase().replace(/_/g, '-');
61+
const gifPath = path.join(emojisDir, `${codepoint}.gif`);
62+
63+
if (local[unified] && fs.existsSync(gifPath)) {
64+
localOut[unified] = 1;
65+
continue;
66+
}
67+
68+
if (localOut[unified]) {
69+
continue;
70+
}
71+
72+
console.log(`Downloading ${name} (${codepoint})`);
73+
try {
74+
// eslint-disable-next-line no-await-in-loop
75+
await downloadEmoji(codepoint, gifPath);
76+
} catch (error) {
77+
console.error(`Could not get emoji ${name} (${codepoint}): ${error.message}`);
78+
}
79+
80+
localOut[unified] = 1;
81+
}
82+
83+
await fs.promises.writeFile(localDataPath, JSON.stringify(localOut));
84+
} catch (error) {
85+
console.error(`Failed to update emojis: ${error.message}`);
86+
}
87+
})();

build/configs/base.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const { merge } = require('webpack-merge');
33
const ESLintPlugin = require('eslint-webpack-plugin');
44
const ESLintFormatter = require('eslint-formatter-friendly');
55
const { VueLoaderPlugin } = require('vue-loader');
6+
const CopyPlugin = require('copy-webpack-plugin');
67
const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin');
78
const FriendlyErrorsWebpackPlugin = require('@soda/friendly-errors-webpack-plugin');
89

@@ -58,6 +59,19 @@ module.exports = (env, argv, config) => {
5859
formatter: ESLintFormatter,
5960
}),
6061
new VueLoaderPlugin(),
62+
new CopyPlugin({
63+
patterns: [
64+
{
65+
from: utils.pathResolve('src/res/animated_emojis'),
66+
to: utils.pathResolve('dist/plugin-emojis/animated'),
67+
toType: 'dir',
68+
filter: async (file) => /\.gif$/.test(file),
69+
globOptions: {
70+
ignore: ['.*'],
71+
},
72+
},
73+
],
74+
}),
6175
new CaseSensitivePathsPlugin(),
6276
new FriendlyErrorsWebpackPlugin(),
6377
],

package.json

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"license": "Apache-2.0",
55
"private": true,
66
"scripts": {
7+
"update": "node build/commands/update-emojis.js",
78
"dev": "node build/commands/dev.js",
89
"build": "node build/commands/build.js",
910
"stats": "node build/commands/build.js --stats",
@@ -12,15 +13,15 @@
1213
"lint:style": "stylelint --allow-empty-input \"./src/**/*.{vue,html,css,less,scss,sass}\""
1314
},
1415
"dependencies": {
15-
"emoji-mart-vue-fast": "^15.0.2",
16+
"emoji-mart-vue-fast": "git+https://github.com/ItsOnlyBinary/emoji-mart-vue.git#b53683dc9f3c3bdceacf18ed4dd2dd80ea9323f3",
1617
"grapheme-splitter": "^1.0.4",
1718
"platform": "^1.3.6"
1819
},
1920
"devDependencies": {
2021
"@babel/core": "^7.25.2",
2122
"@babel/eslint-parser": "^7.25.1",
22-
"@babel/plugin-transform-runtime": "^7.24.7",
23-
"@babel/preset-env": "^7.25.3",
23+
"@babel/plugin-transform-runtime": "^7.25.4",
24+
"@babel/preset-env": "^7.25.4",
2425
"@kiwiirc/eslint-plugin": "file:./build/plugins/eslint-rules/",
2526
"@soda/friendly-errors-webpack-plugin": "^1.8.1",
2627
"@stylistic/stylelint-plugin": "^2.1.3",
@@ -51,6 +52,7 @@
5152
"less-loader": "^12.2.0",
5253
"mini-css-extract-plugin": "^2.9.1",
5354
"murmurhash3js": "^3.0.1",
55+
"node-fetch": "^2.7.0",
5456
"npm-run-all": "^4.1.5",
5557
"ora": "^5.4.1",
5658
"portfinder": "^1.0.32",
@@ -65,21 +67,23 @@
6567
"rimraf": "^5.0.10",
6668
"sass": "^1.77.8",
6769
"sass-loader": "^16.0.1",
70+
"sharp": "^0.33.5",
6871
"style-loader": "^4.0.0",
6972
"stylelint": "^16.8.2",
70-
"stylelint-config-recess-order": "^5.0.1",
73+
"stylelint-config-recess-order": "^5.1.0",
7174
"stylelint-config-recommended": "^14.0.1",
7275
"stylelint-config-recommended-scss": "^14.1.0",
7376
"stylelint-config-recommended-vue": "^1.5.0",
7477
"stylelint-config-standard": "^36.0.1",
7578
"stylelint-config-standard-scss": "^13.1.0",
7679
"stylelint-order": "^6.0.4",
7780
"stylelint-webpack-plugin": "5.0.1",
81+
"vue": "^2.7.16",
7882
"vue-eslint-parser": "^9.4.3",
7983
"vue-loader": "^15.11.1",
8084
"vue-style-loader": "^4.1.3",
8185
"vue-template-compiler": "^2.7.16",
82-
"webpack": "^5.93.0",
86+
"webpack": "^5.94.0",
8387
"webpack-bundle-analyzer": "^4.10.2",
8488
"webpack-cli": "^5.1.4",
8589
"webpack-dev-server": "^5.0.4",

src/components/EmojiPicker.vue

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
v-bind="pickerProps"
44
:set="emojiSet"
55
:data="emojiIndex"
6+
:external-enabled="externalEnabled"
7+
:external-picker="externalPicker"
68
class="kiwi-emoji-mart"
79
@select="onEmojiSelected"
810
/>
@@ -29,6 +31,12 @@ export default {
2931
emojiSet() {
3032
return config.setting('emojiSet');
3133
},
34+
externalEnabled() {
35+
return config.setting('externalEnabled');
36+
},
37+
externalPicker() {
38+
return config.setting('externalPicker');
39+
},
3240
},
3341
methods: {
3442
getBestAscii(emoji) {
@@ -56,14 +64,18 @@ export default {
5664
return emoji.colons;
5765
},
5866
onEmojiSelected(emoji) {
59-
if (emoji.imageUrl) {
67+
/* eslint-disable no-underscore-dangle */
68+
if ((emoji._data.has_img_external && config.setting('externalEnabled')) || emoji.imageUrl) {
6069
// custom emojis
6170
this.ircinput.addImg(
6271
this.getBestAscii(emoji),
63-
emoji.imageUrl,
72+
(emoji._data.has_img_external && config.setting('externalEnabled')
73+
? emoji._data.externalUrl
74+
: emoji.imageUrl),
6475
);
6576
return;
6677
}
78+
/* eslint-enable no-underscore-dangle */
6779
6880
this.ircinput.addImg(
6981
this.getBestAscii(emoji),

src/config.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/* global kiwi:true */
22

3-
let configBase = 'plugin-emojis';
4-
let defaultConfig = {
3+
export const basePath = getBasePath();
4+
export const configBase = 'plugin-emojis';
5+
6+
export const defaultConfig = {
57
sendNativeEmojis: true,
68
parseEmoticons: true,
79
parseColons: true,
@@ -27,6 +29,9 @@ let defaultConfig = {
2729
imageUrl: 'static/favicon.png',
2830
},
2931
],
32+
externalEnabled: false,
33+
externalPicker: 'none', // 'all', 'hover', 'none'
34+
externalUrl: basePath + configBase + '/animated/%CODEPOINT%.gif',
3035
};
3136

3237
export function setDefaults(kiwi) {
@@ -44,3 +49,9 @@ export function getSetting(name) {
4449
export function setSetting(name, value) {
4550
return kiwi.state.setSetting(['settings', configBase, name].join('.'), value);
4651
}
52+
53+
function getBasePath() {
54+
const scripts = document.getElementsByTagName('script');
55+
const scriptPath = scripts[scripts.length - 1].src;
56+
return scriptPath.substring(0, scriptPath.lastIndexOf('/') + 1);
57+
}

src/libs/EmojiProvider.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,21 +120,26 @@ export function getEmojis(word) {
120120
function makeEmojiObj(emojiRaw, match, index) {
121121
const emojiObj = {
122122
ascii: match,
123-
url: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7',
124-
imgProps: {
125-
style: `background-position: ${emojiRaw.getPosition()}; height: 1.2em; vertical-align: -0.3em;`,
126-
className: `emoji-set-${config.setting('emojiSet')} emoji-type-image`,
127-
},
128123
mart: emojiRaw,
129124
matchDetail: {
130125
index,
131126
match,
132127
},
133128
};
134129

135-
if (emojiRaw.imageUrl) {
136-
emojiObj.url = emojiRaw.imageUrl;
130+
/* eslint-disable no-underscore-dangle */
131+
if ((emojiRaw._data.has_img_external && config.setting('externalEnabled')) || emojiRaw.imageUrl) {
132+
emojiObj.url = emojiRaw._data.has_img_external
133+
? emojiRaw._data.externalUrl
134+
: emojiRaw.imageUrl;
135+
137136
emojiObj.imgProps = {};
137+
} else {
138+
emojiObj.url = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
139+
emojiObj.imgProps = {
140+
style: `background-position: ${emojiRaw.getPosition()}; height: 1.2em; vertical-align: -0.3em;`,
141+
className: `emoji-set-${config.setting('emojiSet')} emoji-type-image`,
142+
};
138143
}
139144

140145
return emojiObj;

src/plugin.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { EmojiIndex } from 'emoji-mart-vue-fast/src';
44
import 'emoji-mart-vue-fast/css/emoji-mart.css';
55
import EmojiData from 'emoji-mart-vue-fast/data/all.json';
6+
import AnimatedEmojiData from '@/res/animated_emojis/data.json';
67
import EmojiPicker from '@/components/EmojiPicker.vue';
78
import * as config from '@/config.js';
89
import * as EmojiProvider from '@/libs/EmojiProvider.js';
@@ -17,6 +18,8 @@ kiwi.plugin('emojis', (kiwi) => {
1718
custom: config.setting('customEmojis'),
1819
recent: config.setting('frequentlyUsedList'),
1920
recentLength: config.setting('frequentlyUsedLength'),
21+
externalEmojis: config.setting('externalEmojis') || AnimatedEmojiData,
22+
externalUrl: config.setting('externalUrl'),
2023
});
2124
kiwi['plugin-emojis'] = Object.create(null);
2225
kiwi['plugin-emojis'].emojiIndex = emojiIndex;
@@ -39,4 +42,20 @@ kiwi.plugin('emojis', (kiwi) => {
3942
});
4043
});
4144
});
45+
46+
kiwi.Vue.watch(
47+
() => config.setting('externalEnabled'),
48+
() => {
49+
kiwi.state.networks.forEach((network) => {
50+
// Re-render messages with user colours
51+
Object.values(network.buffers).forEach((buffer) => {
52+
buffer.getMessages().forEach((msg) => {
53+
if (msg.html.indexOf('kiwi-messagelist-emoji') > -1) {
54+
msg.hasRendered = false;
55+
}
56+
});
57+
});
58+
});
59+
}
60+
);
4261
});

src/res/animated_emojis/1f192.gif

31.1 KB

src/res/animated_emojis/1f193.gif

29.3 KB

0 commit comments

Comments
 (0)