From 42083efb7ef95124cea38798604e1a94a8a286b5 Mon Sep 17 00:00:00 2001 From: Fiz <34069775+Hafizzle@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:36:37 -0600 Subject: [PATCH] Emojis Update (#15620) * Add group plan selection modal for upgrades Allow users to select an existing group to upgrade before creating a new one. * crlf -> lf lint * set selection of group plan Also tiny UI fixes * Update group plan selection to include expired plans * Add includeExpiredPlans option to group fetching * force flag when fetching group plans * Update group plan eligibility check * Fix eslint error in push notification import * replace chaining (?.) w/null check * Remove comment * set initial selected group plan, and fix card rounding * format member count * Show warning for pending party invites when upgrading to paid group plan Show warning for pending party invites when upgrading to paid group plan. If user upgrades from party to group, remove any pending invites * suppress error toasts for group modal, and UI tweaks for group modal suppress error toast for 404 on party fetch for users without a party (for group modal), Increase check SVG size in selectableCard, and show "Previously upgraded" label for parties that were canceled group plans * Clear upgradingGroup state after group plan payment * Update emoji system to native Unicode rendering * Fix line endings in habiticaMarkdown test * fix indented code block detection for markdown-it v14 * update habitica-markdown to include v3 emoji dataset (pointed towards test branch) * size emoji in markdown * emoji autocomplete to chat, messages, tasks, and profile add :emoji shortcode autocomplete dropdown (reusing existing autocomplete mixin w/new helper) * try upping github-action fix * trying another github actions fix * update habitica-markdown package version (v3.0.0 -> v4.0.0) * Fix emoji autocomplete overlapping actual text position dropdown below text * update group-plans info card styles * Support Melior emoji autocomplete & more places for emoji autocomplete Include emoji autocomplete in task checklists, tags, challenge name/summary/description * position emoji autocomplete dropdown below text area * fix: replace nested ternary * Emoji autocomplete fixes Fix emoji autocomplete overlapping checklist text, and add short name emoji autocomplete * Have emoji autocomplete dropdown directly below text, add to task tag * Fix emoji autocomplete starting at beginning/end initially * lint/line length * Add group plan selection modal for upgrades Allow users to select an existing group to upgrade before creating a new one. * crlf -> lf lint * set selection of group plan Also tiny UI fixes * Update group plan selection to include expired plans * Add includeExpiredPlans option to group fetching * force flag when fetching group plans * Update group plan eligibility check * Fix eslint error in push notification import * replace chaining (?.) w/null check * Remove comment * set initial selected group plan, and fix card rounding * format member count * Show warning for pending party invites when upgrading to paid group plan Show warning for pending party invites when upgrading to paid group plan. If user upgrades from party to group, remove any pending invites * suppress error toasts for group modal, and UI tweaks for group modal suppress error toast for 404 on party fetch for users without a party (for group modal), Increase check SVG size in selectableCard, and show "Previously upgraded" label for parties that were canceled group plans * Clear upgradingGroup state after group plan payment * Update emoji system to native Unicode rendering * Fix line endings in habiticaMarkdown test * fix indented code block detection for markdown-it v14 * update habitica-markdown to include v3 emoji dataset (pointed towards test branch) * size emoji in markdown * emoji autocomplete to chat, messages, tasks, and profile add :emoji shortcode autocomplete dropdown (reusing existing autocomplete mixin w/new helper) * try upping github-action fix * trying another github actions fix * update habitica-markdown package version (v3.0.0 -> v4.0.0) * Fix emoji autocomplete overlapping actual text position dropdown below text * update group-plans info card styles * Support Melior emoji autocomplete & more places for emoji autocomplete Include emoji autocomplete in task checklists, tags, challenge name/summary/description * position emoji autocomplete dropdown below text area * fix: replace nested ternary * Emoji autocomplete fixes Fix emoji autocomplete overlapping checklist text, and add short name emoji autocomplete * Have emoji autocomplete dropdown directly below text, add to task tag * Fix emoji autocomplete starting at beginning/end initially * lint/line length * Revert "trying another github actions fix" This reverts commit 72fc7fc20ef1b40119e7103d12b440ab60d07fe2. * Revert "try upping github-action fix" This reverts commit 70e48a57aab2bd7671fefbc5757a18b787c0c3b7. * fix(git): revert ci changes --------- Co-authored-by: Kalista Payne --- package-lock.json | 217 +++++++++----- package.json | 2 +- .../dataexport/GET-export_inbox.html.test.js | 4 +- test/common/libs/habiticaMarkdown.test.js | 67 +++++ website/client/package.json | 2 +- website/client/src/assets/scss/markdown.scss | 5 + .../components/challenges/challengeModal.vue | 89 +++++- .../src/components/chat/emojiAutoComplete.vue | 281 ++++++++++++++++++ website/client/src/components/groups/chat.vue | 10 + .../client/src/components/groups/group.vue | 139 +++++---- .../tasks/modal-controls/checklist.vue | 75 ++++- .../tasks/modal-controls/selectMulti.vue | 60 +++- .../client/src/components/tasks/taskModal.vue | 64 +++- website/client/src/components/tasks/user.vue | 76 ++++- .../src/components/userMenu/profile.vue | 37 ++- .../client/src/mixins/autoCompleteHelper.js | 42 ++- .../src/pages/private-messages/index.vue | 37 ++- website/server/libs/highlightMentions.js | 2 +- 18 files changed, 1037 insertions(+), 172 deletions(-) create mode 100644 test/common/libs/habiticaMarkdown.test.js create mode 100644 website/client/src/components/chat/emojiAutoComplete.vue 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 @@ + + + + + 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 @@
    +