diff --git a/package-lock.json b/package-lock.json
index f62f2e3bb5..f20cdb008a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,7 +44,7 @@
"gulp-filter": "^7.0.0",
"gulp-imagemin": "^7.1.0",
"gulp.spritesmith": "^6.13.0",
- "habitica-markdown": "^3.0.0",
+ "habitica-markdown": "github:HabitRPG/habitica-markdown#fiz/emojis-update",
"heapdump": "^0.3.15",
"helmet": "^4.6.0",
"in-app-purchase": "^1.11.3",
@@ -105,6 +105,9 @@
"npm": "^10"
}
},
+ "../habitica-markdown/habitica-markdown": {
+ "extraneous": true
+ },
"node_modules/@aashutoshrathi/word-wrap": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
@@ -4167,6 +4170,12 @@
"node": ">=14.0.0"
}
},
+ "node_modules/apidoc/node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
"node_modules/apidoc/node_modules/bootstrap": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz",
@@ -4202,6 +4211,15 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/apidoc/node_modules/linkify-it": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
+ "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "uc.micro": "^1.0.1"
+ }
+ },
"node_modules/apidoc/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -4213,6 +4231,22 @@
"node": ">=10"
}
},
+ "node_modules/apidoc/node_modules/markdown-it": {
+ "version": "12.3.2",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
+ "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "~2.1.0",
+ "linkify-it": "^3.0.1",
+ "mdurl": "^1.0.1",
+ "uc.micro": "^1.0.5"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.js"
+ }
+ },
"node_modules/apidoc/node_modules/nodemon": {
"version": "2.0.22",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz",
@@ -12497,50 +12531,14 @@
}
},
"node_modules/habitica-markdown": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-3.0.0.tgz",
- "integrity": "sha512-rw1LJ5Vsjx8sfjNa4e2wFuZf5eqqyb5/kfZXPxqfMMgJCCgIhWStDqY3nIclnpGWpemlKd+qbdh2rLiLgm9kng==",
+ "version": "4.0.0",
+ "resolved": "git+ssh://git@github.com/HabitRPG/habitica-markdown.git#204545c1e028f22b937c0a73c5ef250c8973db16",
+ "license": "GPL-3.0",
"dependencies": {
- "habitica-markdown-emoji": "1.2.4",
- "markdown-it": "10.0.0",
- "markdown-it-link-attributes": "3.0.0",
- "markdown-it-linkify-images": "^1.1.1"
- }
- },
- "node_modules/habitica-markdown-emoji": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/habitica-markdown-emoji/-/habitica-markdown-emoji-1.2.4.tgz",
- "integrity": "sha512-UV0AxpDToldFQULuhTxC1y4sdNTApaIOh7ZuV/92HCPmCGkv3DAlHtYE67OmCqLVfs26HWAGVJaU3+OEnW3gjg==",
- "dependencies": {
- "markdown-it-emoji": "^1.1.1"
- }
- },
- "node_modules/habitica-markdown/node_modules/entities": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
- "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
- },
- "node_modules/habitica-markdown/node_modules/linkify-it": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
- "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
- "dependencies": {
- "uc.micro": "^1.0.1"
- }
- },
- "node_modules/habitica-markdown/node_modules/markdown-it": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz",
- "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==",
- "dependencies": {
- "argparse": "^1.0.7",
- "entities": "~2.0.0",
- "linkify-it": "^2.0.0",
- "mdurl": "^1.0.1",
- "uc.micro": "^1.0.5"
- },
- "bin": {
- "markdown-it": "bin/markdown-it.js"
+ "markdown-it": "^14.0.0",
+ "markdown-it-emoji": "^2.0.2",
+ "markdown-it-link-attributes": "^4.0.1",
+ "markdown-it-linkify-images": "^3.0.0"
}
},
"node_modules/handlebars": {
@@ -14552,13 +14550,20 @@
"integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA=="
},
"node_modules/linkify-it": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz",
- "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+ "license": "MIT",
"dependencies": {
- "uc.micro": "^1.0.1"
+ "uc.micro": "^2.0.0"
}
},
+ "node_modules/linkify-it/node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "license": "MIT"
+ },
"node_modules/load-json-file": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
@@ -14906,59 +14911,79 @@
}
},
"node_modules/markdown-it": {
- "version": "12.3.2",
- "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
- "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==",
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
+ "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+ "license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
- "entities": "~2.1.0",
- "linkify-it": "^3.0.1",
- "mdurl": "^1.0.1",
- "uc.micro": "^1.0.5"
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.0",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
},
"bin": {
- "markdown-it": "bin/markdown-it.js"
+ "markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it-emoji": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz",
- "integrity": "sha512-QCz3Hkd+r5gDYtS2xsFXmBYrgw6KuWcJZLCEkdfAuwzZbShCmCfta+hwAMq4NX/4xPzkSHduMKgMkkPUJxSXNg=="
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-2.0.2.tgz",
+ "integrity": "sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==",
+ "license": "MIT"
},
"node_modules/markdown-it-link-attributes": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/markdown-it-link-attributes/-/markdown-it-link-attributes-3.0.0.tgz",
- "integrity": "sha512-B34ySxVeo6MuEGSPCWyIYryuXINOvngNZL87Mp7YYfKIf6DcD837+lXA8mo6EBbauKsnGz22ZH0zsbOiQRWTNg=="
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.1.tgz",
+ "integrity": "sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==",
+ "license": "MIT"
},
"node_modules/markdown-it-linkify-images": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-1.1.1.tgz",
- "integrity": "sha512-1IEmAaAjIgAwY+tZI0sxDXdy9QKHutj5cN0lH2JBiSZt+2NYKrWRJj0cloQW3OFIfP2MLFA1E+6OLJhXPiLgNw==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-3.0.0.tgz",
+ "integrity": "sha512-Vs5yGJa5MWjFgytzgtn8c1U6RcStj3FZKhhx459U8dYbEE5FTWZ6mMRkYMiDlkFO0j4VCsQT1LT557bY0ETgtg==",
+ "license": "MIT",
"dependencies": {
- "markdown-it": "^8.4.2"
+ "markdown-it": "^13.0.1"
}
},
+ "node_modules/markdown-it-linkify-images/node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
"node_modules/markdown-it-linkify-images/node_modules/entities": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
- "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w=="
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
+ "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
},
"node_modules/markdown-it-linkify-images/node_modules/linkify-it": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz",
- "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
+ "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
+ "license": "MIT",
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/markdown-it-linkify-images/node_modules/markdown-it": {
- "version": "8.4.2",
- "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz",
- "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==",
+ "version": "13.0.2",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz",
+ "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==",
+ "license": "MIT",
"dependencies": {
- "argparse": "^1.0.7",
- "entities": "~1.1.1",
- "linkify-it": "^2.0.0",
+ "argparse": "^2.0.1",
+ "entities": "~3.0.1",
+ "linkify-it": "^4.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
@@ -14969,7 +14994,32 @@
"node_modules/markdown-it/node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
- "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "license": "Python-2.0"
+ },
+ "node_modules/markdown-it/node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/markdown-it/node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+ "license": "MIT"
+ },
+ "node_modules/markdown-it/node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "license": "MIT"
},
"node_modules/matchdep": {
"version": "2.0.0",
@@ -18049,6 +18099,15 @@
"node": ">=6"
}
},
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
diff --git a/package.json b/package.json
index 4ff6435850..19be89e0e8 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
"gulp-filter": "^7.0.0",
"gulp-imagemin": "^7.1.0",
"gulp.spritesmith": "^6.13.0",
- "habitica-markdown": "^3.0.0",
+ "habitica-markdown": "^4.0.0",
"heapdump": "^0.3.15",
"helmet": "^4.6.0",
"in-app-purchase": "^1.11.3",
diff --git a/test/api/v3/integration/dataexport/GET-export_inbox.html.test.js b/test/api/v3/integration/dataexport/GET-export_inbox.html.test.js
index b9f9b96fff..d153828688 100644
--- a/test/api/v3/integration/dataexport/GET-export_inbox.html.test.js
+++ b/test/api/v3/integration/dataexport/GET-export_inbox.html.test.js
@@ -38,7 +38,7 @@ describe('GET /export/inbox.html', () => {
it('renders the markdown messages as html', async () => {
const res = await user.get('/export/inbox.html');
- expect(res).to.include('img class="habitica-emoji"');
+ expect(res).to.include('😄');
expect(res).to.include('
Hello!
');
expect(res).to.include('list 1');
});
@@ -46,7 +46,7 @@ describe('GET /export/inbox.html', () => {
it('sorts messages from newest to oldest', async () => {
const res = await user.get('/export/inbox.html');
- const emojiPosition = res.indexOf('img class="habitica-emoji"');
+ const emojiPosition = res.indexOf('😄');
const headingPosition = res.indexOf('Hello!
');
const listPosition = res.indexOf('list 1');
diff --git a/test/common/libs/habiticaMarkdown.test.js b/test/common/libs/habiticaMarkdown.test.js
new file mode 100644
index 0000000000..2ee91da77b
--- /dev/null
+++ b/test/common/libs/habiticaMarkdown.test.js
@@ -0,0 +1,67 @@
+import md from 'habitica-markdown';
+
+describe('habiticaMarkdown emoji plugin', () => {
+ it('renders standard emoji as Unicode', () => {
+ const result = md.render(':smile:');
+ expect(result).to.include('😄');
+ expect(result).not.to.include('img');
+ });
+
+ it('renders thumbsup emoji as Unicode', () => {
+ const result = md.render(':thumbsup:');
+ expect(result).to.include('👍');
+ });
+
+ it('renders +1 emoji as Unicode', () => {
+ const result = md.render(':+1:');
+ expect(result).to.include('👍');
+ });
+
+ it('renders melior as an img tag', () => {
+ const result = md.render(':melior:');
+ expect(result).to.include('
{
+ const result = md.render('[:smile: link](http://example.com)');
+ expect(result).to.include(':smile: link');
+ expect(result).not.to.include('😄');
+ });
+
+ it('converts emoji outside of links normally', () => {
+ const result = md.render(':smile: [link](http://example.com)');
+ expect(result).to.include('😄');
+ expect(result).to.include('link');
+ });
+
+ it('leaves removed custom emoji (bowtie) as literal text', () => {
+ const result = md.render(':bowtie:');
+ expect(result).to.include(':bowtie:');
+ expect(result).not.to.include('img');
+ });
+
+ it('leaves unknown shortcodes as literal text', () => {
+ const result = md.render(':nonexistent_emoji_xyz:');
+ expect(result).to.include(':nonexistent_emoji_xyz:');
+ });
+
+ it('renders new emoji not in the old dataset', () => {
+ const result = md.render(':yawning_face:');
+ expect(result).to.include('🥱');
+ });
+
+ it('supports unsafeHTMLRender', () => {
+ const result = md.unsafeHTMLRender('bold :smile:');
+ expect(result).to.include('bold');
+ expect(result).to.include('😄');
+ });
+
+ it('supports renderWithMentions', () => {
+ const result = md.renderWithMentions(':smile: @testuser', { userName: 'testuser' });
+ expect(result).to.include('😄');
+ expect(result).to.include('at-text');
+ expect(result).to.include('at-highlight');
+ });
+});
diff --git a/website/client/package.json b/website/client/package.json
index 1cff12c077..0703d588b2 100644
--- a/website/client/package.json
+++ b/website/client/package.json
@@ -28,7 +28,7 @@
"eslint-config-habitrpg": "6.2.0",
"eslint-plugin-mocha": "5.3.0",
"eslint-plugin-vue": "7.20.0",
- "habitica-markdown": "^3.0.0",
+ "habitica-markdown": "^4.0.0",
"hellojs": "^1.20.0",
"intro.js": "^7.2.0",
"jquery": "^3.7.1",
diff --git a/website/client/src/assets/scss/markdown.scss b/website/client/src/assets/scss/markdown.scss
index 19f9e45bc1..de59f1237e 100644
--- a/website/client/src/assets/scss/markdown.scss
+++ b/website/client/src/assets/scss/markdown.scss
@@ -58,6 +58,11 @@ h3.markdown {
img {
max-width: 100%;
}
+
+ .emoji-native {
+ font-size: 0.85em;
+ vertical-align: middle;
+ }
blockquote {
padding: 0 16px;
diff --git a/website/client/src/components/challenges/challengeModal.vue b/website/client/src/components/challenges/challengeModal.vue
index abf98b0b2c..1381771bef 100644
--- a/website/client/src/components/challenges/challengeModal.vue
+++ b/website/client/src/components/challenges/challengeModal.vue
@@ -12,23 +12,39 @@
-
+ @focus="setActiveField('name')"
+ @keydown="onFieldKeydown($event)"
+ @keydown.tab="autoCompleteMixinHandleTab($event)"
+ @keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
+ @keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
+ @keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
+ @keydown.esc="autoCompleteMixinHandleEscape($event)"
+ >
-
+ @focus="setActiveField('shortName')"
+ @keydown="onFieldKeydown($event)"
+ @keydown.tab="autoCompleteMixinHandleTab($event)"
+ @keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
+ @keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
+ @keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
+ @keydown.esc="autoCompleteMixinHandleEscape($event)"
+ >
@@ -55,11 +78,26 @@
class="float-right"
>
+
{
+ if (this.textbox) {
+ this.textbox.setSelectionRange(newCaret, newCaret);
+ this.textbox.focus();
+ }
+ });
+ },
enableSubmit: throttle(function enableSubmit () {
/* Enables the submit button if it was disabled */
if (this.loading) {
diff --git a/website/client/src/components/chat/emojiAutoComplete.vue b/website/client/src/components/chat/emojiAutoComplete.vue
new file mode 100644
index 0000000000..f58aa8d211
--- /dev/null
+++ b/website/client/src/components/chat/emojiAutoComplete.vue
@@ -0,0 +1,281 @@
+
+
+
+
![]()
+
{{ result.emoji }}
+
:{{ result.shortcode }}:
+
+
+
+
+
+
+
diff --git a/website/client/src/components/groups/chat.vue b/website/client/src/components/groups/chat.vue
index d1b0e7ae4d..2d242863bc 100644
--- a/website/client/src/components/groups/chat.vue
+++ b/website/client/src/components/groups/chat.vue
@@ -41,6 +41,14 @@
:chat="group.chat"
@select="selectedAutocomplete"
/>
+
@@ -90,6 +98,7 @@ import { MAX_MESSAGE_LENGTH } from '@/../../common/script/constants';
import externalLinks from '../../mixins/externalLinks';
import autocomplete from '../chat/autoComplete';
+import emojiAutoComplete from '../chat/emojiAutoComplete';
import communityGuidelines from './communityGuidelines';
import chatMessages from '../chat/chatMessages';
import { mapState } from '@/libs/store';
@@ -102,6 +111,7 @@ export default {
},
components: {
autocomplete,
+ emojiAutoComplete,
communityGuidelines,
chatMessages,
},
diff --git a/website/client/src/components/groups/group.vue b/website/client/src/components/groups/group.vue
index a70fe8b90d..044acd7c9d 100644
--- a/website/client/src/components/groups/group.vue
+++ b/website/client/src/components/groups/group.vue
@@ -25,53 +25,61 @@
-
-
-
-
{{ group.memberCount | abbrNum }}
-
- {{ $t('memberList') }}
+
+
+
+
+
+
{{ group.memberCount | abbrNum }}
+
+
+ {{ $t('memberList') }}
+
-
-
{{ group.balance * 4 }}
-
- {{ $t('guildBank') }}
+
+
+
+
{{ group.balance * 4 }}
+
+
+ {{ $t('guildBank') }}
+
@@ -128,35 +136,57 @@
}
.item-with-icon {
+ display: inline-block;
border-radius: 2px;
- background-color: #ffffff;
+ background-color: $white;
box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12);
- padding: 1em;
- text-align: center;
- min-width: 120px;
+ margin-left: 1em;
+ width: 120px;
height: 76px;
- margin-right: 1rem;
+ text-align: center;
+ font-size: 20px;
+ vertical-align: bottom;
+ overflow: hidden;
+ position: relative;
- &:last-of-type {
- margin-left: 0.5rem;
+ .box-content {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ width: 100%;
}
- .svg-icon.shield, .svg-icon.gem {
- width: 28px;
- height: auto;
- margin: 0 auto;
+ .icon-number-row {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 0.1em;
+
+ .number {
+ font-size: 18px;
+ font-weight: normal;
+ margin-left: 0.2em;
+ }
+ }
+
+ .svg-icon {
+ width: 24px;
+ height: 24px;
display: inline-block;
vertical-align: bottom;
- margin-right: 0.5em;
}
- .number {
- font-size: 22px;
- font-weight: bold;
- }
-
- .label {
- margin-top: .5em;
+ .details {
+ font-size: 11px;
+ color: $gray-200;
+ width: 100%;
+ padding: 0 4px;
+ line-height: 1.1;
+ word-break: break-word;
+ max-height: 2.2em;
+ overflow: visible;
}
}
@@ -215,11 +245,6 @@
.icon-row {
margin-top: 1em;
justify-content: flex-end;
-
- .number {
- font-size: 22px;
- font-weight: bold;
- }
}
.chat-row {
diff --git a/website/client/src/components/tasks/modal-controls/checklist.vue b/website/client/src/components/tasks/modal-controls/checklist.vue
index a82a6c9a63..35f5370df2 100644
--- a/website/client/src/components/tasks/modal-controls/checklist.vue
+++ b/website/client/src/components/tasks/modal-controls/checklist.vue
@@ -48,11 +48,19 @@
/>
+
@@ -105,6 +128,8 @@ import chevronIcon from '@/assets/svg/chevron.svg?raw';
import gripIcon from '@/assets/svg/grip.svg?raw';
import checkbox from '@/components/ui/checkbox';
import lockableLabel from './lockableLabel';
+import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
+import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
export default {
name: 'Checklist',
@@ -112,7 +137,9 @@ export default {
checkbox,
draggable,
lockableLabel,
+ emojiAutoComplete,
},
+ mixins: [autoCompleteHelperMixin],
props: {
disabled: {
type: Boolean,
@@ -133,6 +160,8 @@ export default {
showChecklist: true,
hasPossibilityOfIMEConversion: true,
newChecklistItem: null,
+ textbox: null,
+ activeItemIndex: -1,
icons: Object.freeze({
positive: positiveIcon,
destroy: deleteIcon,
@@ -141,6 +170,15 @@ export default {
}),
};
},
+ computed: {
+ activeFieldText () {
+ if (this.activeItemIndex === -1) {
+ return this.newChecklistItem || '';
+ }
+ const item = this.checklist[this.activeItemIndex];
+ return item ? item.text || '' : '';
+ },
+ },
methods: {
summaryClass (item) {
if (!this.disableEdit) return '';
@@ -179,6 +217,40 @@ export default {
this.checklist.splice(i, 1);
this.updateChecklist();
},
+ setActiveItem (index) {
+ this.activeItemIndex = index;
+ if (index === -1) {
+ this.textbox = this.$refs.newChecklistInput;
+ } else {
+ const refArr = this.$refs[`checklistItem-${index}`];
+ this.textbox = refArr ? refArr[0] || refArr : null;
+ }
+ },
+ newChecklistEnterHandler (e) {
+ const ac = this._getActiveAutocomplete();
+ if (ac && ac.selected !== null) {
+ e.preventDefault();
+ ac.makeSelection();
+ } else if (ac) {
+ ac.cancel();
+ this.setHasPossibilityOfIMEConversion(false);
+ } else {
+ this.setHasPossibilityOfIMEConversion(false);
+ }
+ },
+ selectedAutocomplete (newText, newCaret) {
+ if (this.activeItemIndex === -1) {
+ this.newChecklistItem = newText;
+ } else {
+ this.checklist[this.activeItemIndex].text = newText;
+ }
+ this.$nextTick(() => {
+ if (this.textbox) {
+ this.textbox.setSelectionRange(newCaret, newCaret);
+ this.textbox.focus();
+ }
+ });
+ },
},
};
@@ -187,6 +259,7 @@ export default {
@import '@/assets/scss/colors.scss';
.checklist-component {
+ position: relative;
.chevron-flip {
transform: translateY(-5px) rotate(180deg);
diff --git a/website/client/src/components/tasks/modal-controls/selectMulti.vue b/website/client/src/components/tasks/modal-controls/selectMulti.vue
index 922ef7d151..592b28398e 100644
--- a/website/client/src/components/tasks/modal-controls/selectMulti.vue
+++ b/website/client/src/components/tasks/modal-controls/selectMulti.vue
@@ -9,12 +9,27 @@
@toggle="openOrClose($event)"
>
-
+
+
@@ -94,6 +109,10 @@ $itemHeight: 2rem;
}
.select-multi {
+ .search-input-wrapper {
+ position: relative;
+ }
+
.dropdown-toggle {
padding-left: 0.75rem;
}
@@ -185,6 +204,8 @@ $itemHeight: 2rem;
import Vue from 'vue';
import MultiList from '@/components/tasks/modal-controls/multiList';
import markdownDirective from '@/directives/markdown';
+import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
+import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
export default {
directives: {
@@ -192,7 +213,9 @@ export default {
},
components: {
MultiList,
+ emojiAutoComplete,
},
+ mixins: [autoCompleteHelperMixin],
props: {
addNew: {
type: Boolean,
@@ -221,6 +244,7 @@ export default {
wasTagAdded: false,
selected: this.selectedItems,
search: '',
+ textbox: null,
};
},
computed: {
@@ -312,6 +336,38 @@ export default {
this.closeSelectPopup();
}
},
+ setTextbox () {
+ const ref = this.$refs.searchInput;
+ this.textbox = ref ? (ref.$el || ref) : null;
+ },
+ searchEnterHandler (e) {
+ const ac = this._getActiveAutocomplete();
+ if (ac && ac.selected !== null) {
+ e.preventDefault();
+ e.stopPropagation();
+ ac.makeSelection();
+ } else {
+ if (ac) ac.cancel();
+ this.handleSubmit();
+ }
+ },
+ searchEscHandler (e) {
+ const ac = this._getActiveAutocomplete();
+ if (ac && ac.searchActive) {
+ e.preventDefault();
+ e.stopPropagation();
+ ac.cancel();
+ }
+ },
+ selectedAutocomplete (newText, newCaret) {
+ this.search = newText;
+ this.$nextTick(() => {
+ if (this.textbox) {
+ this.textbox.setSelectionRange(newCaret, newCaret);
+ this.textbox.focus();
+ }
+ });
+ },
handleSubmit () {
if (!this.addNew) return;
const { search } = this;
diff --git a/website/client/src/components/tasks/taskModal.vue b/website/client/src/components/tasks/taskModal.vue
index b2f17bf5bb..7117cbf927 100644
--- a/website/client/src/components/tasks/taskModal.vue
+++ b/website/client/src/components/tasks/taskModal.vue
@@ -70,6 +70,13 @@
spellcheck="true"
:disabled="challengeAccessRequired"
:placeholder="$t('addATitle')"
+ @focus="setActiveField('title')"
+ @keydown="autoCompleteMixinUpdateCarretPosition"
+ @keydown.tab="autoCompleteMixinHandleTab($event)"
+ @keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
+ @keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
+ @keypress.enter="titleEnterHandler($event)"
+ @keydown.esc="autoCompleteMixinHandleEscape($event)"
>
+
{
+ this.textbox.setSelectionRange(newCaret, newCaret);
+ this.textbox.focus();
+ });
},
async addTag (name) {
const tagResult = await this.createTag({ name });
diff --git a/website/client/src/components/tasks/user.vue b/website/client/src/components/tasks/user.vue
index 4de6ff3963..3af37856de 100644
--- a/website/client/src/components/tasks/user.vue
+++ b/website/client/src/components/tasks/user.vue
@@ -80,9 +80,17 @@
v-html="icons.drag"
>
@@ -134,6 +149,15 @@
+
{
const data = vm.$store.state.privateMessageOptions;
@@ -751,6 +769,7 @@ export default defineComponent({
/** @type {Record} */
messagesByConversation: {}, // cache {uuid: []}
+ textbox: null,
newMessage: '',
messages: [],
messagesLoading: false,
@@ -963,6 +982,15 @@ export default defineComponent({
}
},
},
+ watch: {
+ shouldShowInputPanel (val) {
+ if (val) {
+ this.$nextTick(() => {
+ this.textbox = this.$refs.textarea;
+ });
+ }
+ },
+ },
async mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('messages'),
@@ -1224,6 +1252,13 @@ export default defineComponent({
triggerStartNewConversationState () {
this.showStartNewConversationInput = true;
},
+ selectedAutocomplete (newText, newCaret) {
+ this.newMessage = newText;
+ this.$nextTick(() => {
+ this.textbox.setSelectionRange(newCaret, newCaret);
+ this.textbox.focus();
+ });
+ },
async startConversationByUsername (targetUserName) {
// check if the target user exists in current conversations, select that conversation
/** @type {PrivateMessages.ConversationSummaryMessageEntry} */
diff --git a/website/server/libs/highlightMentions.js b/website/server/libs/highlightMentions.js
index ae2ca5d293..612267b069 100644
--- a/website/server/libs/highlightMentions.js
+++ b/website/server/libs/highlightMentions.js
@@ -88,7 +88,7 @@ function toSourceMapRegex (token) {
let regexStr = '';
if (type === 'code_block') {
- regexStr = withOptionalIndentation(contentRegex);
+ regexStr = withOptionalIndentation(contentRegex.replace(/\n$/, ''));
} else if (type === 'fence') {
regexStr = `\\s*${markup}.*\n${withOptionalIndentation(contentRegex)}\\s*${markup}`;
} else if (type === 'code_inline') {