From 2b26eb2bd11e75bf7b36d9e8c5b4dc2401e5a611 Mon Sep 17 00:00:00 2001 From: Tanmay Nalawade <91938829+Tanmay-Nalawade@users.noreply.github.com> Date: Thu, 19 Feb 2026 10:09:40 -0700 Subject: [PATCH 01/34] Habit and Daily task counts not displaying for certain filters (#15595) * habits-all fixed * dailies section fixed * to do counter fixed * Remove linting changes and apply logic fix with single quotes * refactor(tasks): simpler badgeCount logic * fix(lint): remove unused import --------- Co-authored-by: Kalista Payne --- .../client/src/components/tasks/column.vue | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/website/client/src/components/tasks/column.vue b/website/client/src/components/tasks/column.vue index 1bf975ba7a..ea06d9072c 100644 --- a/website/client/src/components/tasks/column.vue +++ b/website/client/src/components/tasks/column.vue @@ -348,7 +348,6 @@ import throttle from 'lodash/throttle'; import isEmpty from 'lodash/isEmpty'; import draggable from 'vuedraggable'; -import { shouldDo } from '@/../../common/script/cron'; import inAppRewards from '@/../../common/script/libs/inAppRewards'; import taskDefaults from '@/../../common/script/libs/taskDefaults'; import Task from './task'; @@ -482,25 +481,10 @@ export default { return this.$t('addATask', { type }); }, badgeCount () { - // 0 means the badge will not be shown - // It is shown for the all and due views of dailies - // and for the active and scheduled views of todos. - if (this.type === 'todo' && this.activeFilter.label !== 'complete2') { - return this.taskList.length; - } if (this.type === 'daily') { - if (this.activeFilter.label === 'due') { - return this.taskList.length; - } if (this.activeFilter.label === 'all') { - return this.taskList - .reduce( - (count, t) => (!t.completed - && shouldDo(new Date(), t, this.getUserPreferences) ? count + 1 : count), - 0, - ); - } + if (this.type === 'reward') { + return 0; } - - return 0; + return this.taskList.length; }, }, watch: { From eccc115b7359269275ff5e2a403824d22411f6fa Mon Sep 17 00:00:00 2001 From: Phillip Thelen Date: Thu, 19 Feb 2026 18:11:25 +0100 Subject: [PATCH 02/34] Admin Panel fixes (#15613) * fix profile link to admin panel * fix profile looking broken when no background is equipped --- website/client/src/components/userMenu/profile.vue | 2 +- website/client/src/components/userMenu/profileStats.vue | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/website/client/src/components/userMenu/profile.vue b/website/client/src/components/userMenu/profile.vue index d42713d4a0..811fd920cf 100644 --- a/website/client/src/components/userMenu/profile.vue +++ b/website/client/src/components/userMenu/profile.vue @@ -1340,7 +1340,7 @@ export default { }, openAdminPanel () { - this.$router.push(`/admin-panel/${this.hero._id}`); + this.$router.push(`/admin/panel/${this.hero._id}`); }, }, }; diff --git a/website/client/src/components/userMenu/profileStats.vue b/website/client/src/components/userMenu/profileStats.vue index 548a9e101a..10e78a66a7 100644 --- a/website/client/src/components/userMenu/profileStats.vue +++ b/website/client/src/components/userMenu/profileStats.vue @@ -246,7 +246,9 @@ :class="{white: user.preferences.background}" style="overflow:hidden" > - + {{ statsComputed.gearBonus[stat] !== 0 ? '+' : '' }}{{ - statsComputed.gearBonus[stat] + statsComputed.gearBonus[stat] + statsComputed.classBonus[stat] }} From 3e93911e70800bd8de4caa8bbeaa61feb408f2e7 Mon Sep 17 00:00:00 2001 From: Phillip Thelen Date: Tue, 24 Feb 2026 17:17:21 +0100 Subject: [PATCH 05/34] Phillip/prod perf2 (#15596) * Add new api call for kubernetes startup probe * add hostname as tag for loggly * Only listen to one change * increase vite min chunk size * respond gracefully to shutdown signal * update server readiness according to mongodb and redis connection * make larger vite chunks * fix lint --- website/client/vite.config.mjs | 2 +- website/server/controllers/api-v4/status.js | 39 +++++++++++++++++++ website/server/libs/logger.js | 4 +- website/server/libs/mongoose.js | 8 ++++ website/server/libs/serverStatus.js | 7 ++++ website/server/middlewares/rateLimiter.js | 11 ++++++ .../server/middlewares/requestLogHandler.js | 2 +- website/server/server.js | 13 +++++++ 8 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 website/server/controllers/api-v4/status.js create mode 100644 website/server/libs/serverStatus.js diff --git a/website/client/vite.config.mjs b/website/client/vite.config.mjs index 2b7a6191ab..94701d68d4 100644 --- a/website/client/vite.config.mjs +++ b/website/client/vite.config.mjs @@ -122,7 +122,7 @@ export default defineConfig({ }, rollupOptions: { output: { - experimentalMinChunkSize: 1000 + experimentalMinChunkSize: 20000 } } }, diff --git a/website/server/controllers/api-v4/status.js b/website/server/controllers/api-v4/status.js new file mode 100644 index 0000000000..980a215d1b --- /dev/null +++ b/website/server/controllers/api-v4/status.js @@ -0,0 +1,39 @@ +import { + disableCache, +} from '../../middlewares/cache'; +import SERVER_STATUS from '../../libs/serverStatus'; + +const api = {}; + +/** + * @api {get} /api/v3/ready Get Habitica's Server readiness status + * @apiName GetReady + * @apiGroup Status + * + * @apiSuccess {String} data.status 'ready' if everything is ok + * + * @apiSuccessExample {JSON} Server is Ready + * { + * 'status': 'ready', + * } + */ +api.getReady = { + method: 'GET', + url: '/ready', + // explicitly disable caching so that the server is always checked + middlewares: [disableCache], + async handler (req, res) { + // This allows kubernetes to determine if the server is ready to receive traffic + if (!SERVER_STATUS.MONGODB || !SERVER_STATUS.REDIS || !SERVER_STATUS.EXPRESS) { + res.respond(503, { + status: 'not ready', + }); + } else { + res.respond(200, { + status: 'ready', + }); + } + }, +}; + +export default api; diff --git a/website/server/libs/logger.js b/website/server/libs/logger.js index 424d282bdb..23215a9572 100644 --- a/website/server/libs/logger.js +++ b/website/server/libs/logger.js @@ -3,6 +3,7 @@ import winston from 'winston'; import { Loggly } from 'winston-loggly-bulk'; import nconf from 'nconf'; import _ from 'lodash'; +import os from 'os'; import { CustomError, } from './errors'; @@ -65,9 +66,8 @@ if (IS_PROD) { ), })); } - if (LOGGLY_TOKEN && LOGGLY_SUBDOMAIN) { - const tags = ['Winston-NodeJS']; + const tags = ['Winston-NodeJS', os.hostname()]; if (nconf.get('SERVER_EMOJI')) { tags.push(nconf.get('SERVER_EMOJI')); } diff --git a/website/server/libs/mongoose.js b/website/server/libs/mongoose.js index 661008dd7e..e8884dcbd8 100644 --- a/website/server/libs/mongoose.js +++ b/website/server/libs/mongoose.js @@ -5,6 +5,7 @@ import { getDevelopmentConnectionUrl, getDefaultConnectionOptions, } from './mongodb'; +import SERVER_STATUS from './serverStatus'; const IS_PROD = nconf.get('IS_PROD'); const MAINTENANCE_MODE = nconf.get('MAINTENANCE_MODE'); @@ -24,6 +25,13 @@ const connectionUrl = IS_PROD ? DB_URI : getDevelopmentConnectionUrl(DB_URI); export default async function connectToMongoDB () { // Do not connect to MongoDB when in maintenance mode if (MAINTENANCE_MODE !== 'true') { + mongoose.connection.on('open', () => { + SERVER_STATUS.MONGODB = true; + }); + mongoose.connection.on('disconnected', () => { + SERVER_STATUS.MONGODB = false; + }); + return mongoose.connect(connectionUrl, mongooseOptions).then(() => { logger.info('Connected with Mongoose.'); }); diff --git a/website/server/libs/serverStatus.js b/website/server/libs/serverStatus.js new file mode 100644 index 0000000000..ecfb6423b9 --- /dev/null +++ b/website/server/libs/serverStatus.js @@ -0,0 +1,7 @@ +const SERVER_STATUS = { + MONGODB: false, + REDIS: false, + EXPRESS: false, +}; + +export default SERVER_STATUS; diff --git a/website/server/middlewares/rateLimiter.js b/website/server/middlewares/rateLimiter.js index 97024fa107..b47e9e9eec 100644 --- a/website/server/middlewares/rateLimiter.js +++ b/website/server/middlewares/rateLimiter.js @@ -10,6 +10,7 @@ import { } from '../libs/errors'; import logger from '../libs/logger'; import { apiError } from '../libs/apiError'; +import SERVER_STATUS from '../libs/serverStatus'; // Middleware to rate limit requests to the API @@ -47,6 +48,14 @@ if (RATE_LIMITER_ENABLED) { enable_offline_queue: false, }); + redisClient.on('ready', () => { + SERVER_STATUS.REDIS = true; + }); + + redisClient.on('reconnecting', () => { + SERVER_STATUS.REDIS = false; + }); + redisClient.on('error', error => { logger.error(error, 'Redis Error'); }); @@ -56,6 +65,8 @@ if (RATE_LIMITER_ENABLED) { storeClient: redisClient, }); } +} else { + SERVER_STATUS.REDIS = true; } function setResponseHeaders (res, rateLimiterRes) { diff --git a/website/server/middlewares/requestLogHandler.js b/website/server/middlewares/requestLogHandler.js index c398441103..ab2d4e9582 100644 --- a/website/server/middlewares/requestLogHandler.js +++ b/website/server/middlewares/requestLogHandler.js @@ -41,7 +41,7 @@ export const logRequestData = (req, res, next) => { export const logSlowRequests = (req, res, next) => { req.requestStartTime = Date.now(); - req.on('close', () => { + req.once('close', () => { const requestTime = Date.now() - req.requestStartTime; if (requestTime > SLOW_REQUEST_THRESHOLD) { const data = buildBaseLogData(req); diff --git a/website/server/server.js b/website/server/server.js index 24aec935bd..06326bca90 100644 --- a/website/server/server.js +++ b/website/server/server.js @@ -1,6 +1,8 @@ import nconf from 'nconf'; import express from 'express'; import http from 'http'; +import mongoose from 'mongoose'; +import redis from 'redis'; import logger from './libs/logger'; // Setup translations @@ -18,12 +20,22 @@ import './libs/setupFirebase'; import './models/challenge'; import './models/group'; import './models/user'; +import SERVER_STATUS from './libs/serverStatus'; connectToMongoDB(); const server = http.createServer(); const app = express(); +process.on('SIGTERM', async () => { + console.log('SIGTERM signal received: closing HTTP server'); + server.close(async () => { + await mongoose.disconnect(); + await redis.quit(); + process.exit(0); + }); +}); + app.set('port', nconf.get('PORT')); attachMiddlewares(app, server); @@ -31,6 +43,7 @@ attachMiddlewares(app, server); server.on('request', app); server.listen(app.get('port'), () => { logger.info(`Express server listening on port ${app.get('port')}`); + SERVER_STATUS.EXPRESS = true; }); export default server; From 68bfebcf30b4c10846656853dc17d0acf85a0344 Mon Sep 17 00:00:00 2001 From: Kalista Payne Date: Tue, 24 Feb 2026 10:18:25 -0600 Subject: [PATCH 06/34] 5.45.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 55926bd146..ec22d8f04c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "habitica", - "version": "5.44.3", + "version": "5.45.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "habitica", - "version": "5.44.3", + "version": "5.45.0", "hasInstallScript": true, "dependencies": { "@babel/core": "^7.22.10", diff --git a/package.json b/package.json index de92a06e03..fcb0b1dd81 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "5.44.3", + "version": "5.45.0", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.22.10", From 0ae19d9107147298f011b7d9f1fd2772ad209f10 Mon Sep 17 00:00:00 2001 From: Kalista Payne Date: Tue, 24 Feb 2026 12:02:52 -0600 Subject: [PATCH 07/34] Squashed commit of the following: commit 963b4133ecc04b5e8abbfa3bd0e5387377ab3077 Author: Kalista Payne Date: Thu Feb 19 15:48:41 2026 -0600 fix(text): clean up some gear descriptions commit 53999a5b80fc05b3bd056b07d0ffa8484ff10988 Author: Kalista Payne Date: Wed Feb 4 17:18:07 2026 -0600 fix(content): add seasonal set tokens commit 4510c90e414b0b52999f458ccb753f7b6c98f31e Author: Kalista Payne Date: Wed Feb 4 17:10:24 2026 -0600 feat(content): March-May 2026 --- .../assets/css/sprites/spritesmith-main.css | 185 ++++++++++++++++++ website/common/locales/en/backgrounds.json | 12 ++ website/common/locales/en/gear.json | 60 +++++- website/common/locales/en/limited.json | 44 +++-- website/common/locales/en/subscriber.json | 3 + .../script/content/appearance/backgrounds.js | 9 + .../script/content/constants/releaseDates.js | 3 + .../script/content/constants/seasonalSets.js | 5 + .../script/content/gear/sets/armoire.js | 30 +++ .../script/content/gear/sets/mystery.js | 6 + .../script/content/gear/sets/special/index.js | 46 +++++ 11 files changed, 382 insertions(+), 21 deletions(-) diff --git a/website/client/src/assets/css/sprites/spritesmith-main.css b/website/client/src/assets/css/sprites/spritesmith-main.css index 455fa8a366..13638101c7 100644 --- a/website/client/src/assets/css/sprites/spritesmith-main.css +++ b/website/client/src/assets/css/sprites/spritesmith-main.css @@ -1060,6 +1060,11 @@ width: 141px; height: 147px; } +.background_elven_citadel { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_elven_citadel.png'); + width: 141px; + height: 147px; +} .background_enchanted_music_room { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_enchanted_music_room.png'); width: 141px; @@ -1931,6 +1936,11 @@ width: 141px; height: 147px; } +.background_riding_a_comet { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_riding_a_comet.png'); + width: 141px; + height: 147px; +} .background_rime_ice { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_rime_ice.png'); width: 141px; @@ -2427,6 +2437,11 @@ width: 141px; height: 147px; } +.background_waterfall_with_rainbow { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_waterfall_with_rainbow.png'); + width: 141px; + height: 147px; +} .background_wedding_arch { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_wedding_arch.png'); width: 141px; @@ -29800,6 +29815,11 @@ width: 114px; height: 90px; } +.broad_armor_armoire_handstandOutfit { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_handstandOutfit.png'); + width: 114px; + height: 90px; +} .broad_armor_armoire_hattersSuit { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_hattersSuit.png'); width: 114px; @@ -30075,6 +30095,11 @@ width: 114px; height: 90px; } +.broad_armor_armoire_softYellowSuit { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_softYellowSuit.png'); + width: 114px; + height: 90px; +} .broad_armor_armoire_springPetalYukata { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_armoire_springPetalYukata.png'); width: 114px; @@ -30385,6 +30410,11 @@ width: 114px; height: 90px; } +.head_armoire_floppyYellowHat { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_floppyYellowHat.png'); + width: 114px; + height: 90px; +} .head_armoire_flutteryWig { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_flutteryWig.png'); width: 114px; @@ -30705,6 +30735,11 @@ width: 114px; height: 90px; } +.head_armoire_verdantArmingCap { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_verdantArmingCap.png'); + width: 114px; + height: 90px; +} .head_armoire_vermilionArcherHelm { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_armoire_vermilionArcherHelm.png'); width: 90px; @@ -31120,6 +31155,11 @@ width: 114px; height: 90px; } +.shield_armoire_softYellowPillow { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_softYellowPillow.png'); + width: 114px; + height: 90px; +} .shield_armoire_spanishGuitar { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_spanishGuitar.png'); width: 114px; @@ -31170,6 +31210,11 @@ width: 114px; height: 90px; } +.shield_armoire_verdantBanner { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_verdantBanner.png'); + width: 114px; + height: 90px; +} .shield_armoire_vikingShield { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_armoire_vikingShield.png'); width: 90px; @@ -31440,6 +31485,11 @@ width: 114px; height: 90px; } +.slim_armor_armoire_handstandOutfit { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_handstandOutfit .png'); + width: 114px; + height: 90px; +} .slim_armor_armoire_hattersSuit { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_hattersSuit.png'); width: 114px; @@ -31715,6 +31765,11 @@ width: 114px; height: 90px; } +.slim_armor_armoire_softYellowSuit { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_softYellowSuit.png'); + width: 114px; + height: 90px; +} .slim_armor_armoire_springPetalYukata { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_springPetalYukata.png'); width: 114px; @@ -34125,11 +34180,21 @@ width: 114px; height: 90px; } +.back_mystery_202605 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_202605.png'); + width: 114px; + height: 90px; +} .broad_armor_mystery_202512 { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202512.png'); width: 114px; height: 90px; } +.broad_armor_mystery_202604 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_mystery_202604.png'); + width: 114px; + height: 90px; +} .head_mystery_202512 { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202512.png'); width: 114px; @@ -34140,11 +34205,31 @@ width: 114px; height: 90px; } +.head_mystery_202603 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202603.png'); + width: 114px; + height: 90px; +} +.head_mystery_202604 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_mystery_202604.png'); + width: 114px; + height: 90px; +} +.shield_mystery_202605 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_mystery_202605.png'); + width: 114px; + height: 90px; +} .slim_armor_mystery_202512 { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202512.png'); width: 114px; height: 90px; } +.slim_armor_mystery_202604 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_mystery_202604.png'); + width: 114px; + height: 90px; +} .weapon_mystery_202512 { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202512.png'); width: 114px; @@ -34155,6 +34240,11 @@ width: 114px; height: 90px; } +.weapon_mystery_202603 { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_mystery_202603.png'); + width: 114px; + height: 90px; +} .back_mystery_201402 { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_mystery_201402.png'); width: 90px; @@ -36275,6 +36365,26 @@ width: 114px; height: 90px; } +.broad_armor_special_spring2026Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Healer.png'); + width: 114px; + height: 90px; +} +.broad_armor_special_spring2026Mage { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Mage.png'); + width: 114px; + height: 90px; +} +.broad_armor_special_spring2026Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Rogue.png'); + width: 114px; + height: 90px; +} +.broad_armor_special_spring2026Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_spring2026Warrior.png'); + width: 114px; + height: 90px; +} .broad_armor_special_springHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/broad_armor_special_springHealer.png'); width: 90px; @@ -36595,6 +36705,26 @@ width: 114px; height: 90px; } +.head_special_spring2026Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Healer.png'); + width: 114px; + height: 90px; +} +.head_special_spring2026Mage { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Mage.png'); + width: 114px; + height: 90px; +} +.head_special_spring2026Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Rogue.png'); + width: 114px; + height: 90px; +} +.head_special_spring2026Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_spring2026Warrior.png'); + width: 114px; + height: 90px; +} .head_special_springHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/head_special_springHealer.png'); width: 90px; @@ -36780,6 +36910,21 @@ width: 114px; height: 90px; } +.shield_special_spring2026Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Healer.png'); + width: 114px; + height: 90px; +} +.shield_special_spring2026Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Rogue.png'); + width: 114px; + height: 90px; +} +.shield_special_spring2026Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_spring2026Warrior.png'); + width: 114px; + height: 90px; +} .shield_special_springHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/shield_special_springHealer.png'); width: 90px; @@ -37015,6 +37160,26 @@ width: 114px; height: 90px; } +.slim_armor_special_spring2026Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Healer.png'); + width: 114px; + height: 90px; +} +.slim_armor_special_spring2026Mage { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Mage.png'); + width: 114px; + height: 90px; +} +.slim_armor_special_spring2026Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Rogue.png'); + width: 114px; + height: 90px; +} +.slim_armor_special_spring2026Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_spring2026Warrior.png'); + width: 114px; + height: 90px; +} .slim_armor_special_springHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_special_springHealer.png'); width: 90px; @@ -37255,6 +37420,26 @@ width: 114px; height: 90px; } +.weapon_special_spring2026Healer { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Healer.png'); + width: 114px; + height: 90px; +} +.weapon_special_spring2026Mage { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Mage.png'); + width: 114px; + height: 90px; +} +.weapon_special_spring2026Rogue { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Rogue.png'); + width: 114px; + height: 90px; +} +.weapon_special_spring2026Warrior { + background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_spring2026Warrior.png'); + width: 114px; + height: 90px; +} .weapon_special_springHealer { background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_springHealer.png'); width: 90px; diff --git a/website/common/locales/en/backgrounds.json b/website/common/locales/en/backgrounds.json index f60350b22c..19c76945c4 100644 --- a/website/common/locales/en/backgrounds.json +++ b/website/common/locales/en/backgrounds.json @@ -1063,6 +1063,18 @@ "backgroundElegantPalaceText": "Elegant Palace", "backgroundElegantPalaceNotes": "Admire the colorful halls of an Elegant Palace.", + "backgrounds032026": "SET 142: Released March 2026", + "backgroundWaterfallWithRainbowText": "Waterfall with Rainbow", + "backgroundWaterfallWithRainbowNotes": "Admire the breathtaking beauty of a Waterfall with a Rainbow.", + + "backgrounds042026": "SET 143: Released April 2026", + "backgroundRidingACometText": "Riding a Comet", + "backgroundRidingACometNotes": "Travel through space while Riding a Comet!", + + "backgrounds052026": "SET 144: Released May 2026", + "backgroundElvenCitadelText": "Elven Citadel", + "backgroundElvenCitadelNotes": "Take the scenic journey to an Elven Citadel.", + "timeTravelBackgrounds": "Steampunk Backgrounds", "backgroundAirshipText": "Airship", "backgroundAirshipNotes": "Become a sky sailor on board your very own Airship.", diff --git a/website/common/locales/en/gear.json b/website/common/locales/en/gear.json index 1ba1f07efe..e509d102ce 100644 --- a/website/common/locales/en/gear.json +++ b/website/common/locales/en/gear.json @@ -578,6 +578,15 @@ "weaponSpecialWinter2026MageText": "Candelabra Staff", "weaponSpecialWinter2026MageNotes": "Candelabras help by holding multiple candles at a time—follow its lead the next time you need to multitask. Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition Winter 2025-2026 Gear.", + "weaponSpecialSpring2026WarriorText": "Mighty Froggy Foil", + "weaponSpecialSpring2026WarriorNotes": "An opportunity to duel might present itself at any moment, and with this formidable foil, you will be ready! Increases Strength by <%= str %>. Limited Edition Spring 2026 Gear.", + "weaponSpecialSpring2026RogueText": "Spring Branch", + "weaponSpecialSpring2026RogueNotes": "An opportunity to grow is nearly upon you, and with these budding branches, you will be ready! Increases Strength by <%= str %>. Limited Edition Spring 2026 Gear.", + "weaponSpecialSpring2026HealerText": "Snowdrop Staff", + "weaponSpecialSpring2026HealerNotes": "An opportunity to begin anew with a fresh start is right up ahead, and with this splendid staff, you will be ready! Increases Intelligence by <%= int %>. Limited Edition Spring 2026 Gear.", + "weaponSpecialSpring2026MageText": "Maypole Parasol", + "weaponSpecialSpring2026MageNotes": "An opportunity to celebrate approaches, and with this pretty parasol pole, you will be ready! Increases Intelligence by <%= int %> and Perception by <%= per %>. Limited Edition Spring 2026 Gear.", + "weaponMystery201411Text": "Pitchfork of Feasting", "weaponMystery201411Notes": "Stab your enemies or dig in to your favorite foods - this versatile pitchfork does it all! Confers no benefit. November 2014 Subscriber Item.", "weaponMystery201502Text": "Shimmery Winged Staff of Love and Also Truth", @@ -626,6 +635,8 @@ "weaponMystery202512Notes": "A shining sword cast from sugar, mint, and arcane enchantments. Confers no benefit. December 2025 Subscriber Item.", "weaponMystery202601Text": "Winter's Aegis", "weaponMystery202601Notes": "An icy bubble shield that grants magical protection from opposing elements. Confers no benefit. January 2026 Subscriber Item.", + "weaponMystery202603Text": "Wisteria Wizard Staff", + "weaponMystery202603Notes": "Cast spells to warm the spring air and encourage the blossoms to bud! Confers no benefit. March 2026 Subscriber Item.", "weaponMystery301404Text": "Steampunk Cane", "weaponMystery301404Notes": "Excellent for taking a turn about town. March 3015 Subscriber Item. Confers no benefit.", @@ -1412,6 +1423,15 @@ "armorSpecialWinter2026MageText": "Midwinter Candle Robe", "armorSpecialWinter2026MageNotes": "Glide smoothly along your path like wax on your way to completing your Dailies. Increases Intelligence by <%= int %>. Limited Edition Winter 2025-2026 Gear.", + "armorSpecialSpring2026WarriorText": "Frog Armor", + "armorSpecialSpring2026WarriorNotes": "Spring into action just as soon as the snow begins to thaw. Increases Constitution by <%= con %>. Limited Edition Spring 2026 Gear.", + "armorSpecialSpring2026RogueText": "Birch Bark Armor", + "armorSpecialSpring2026RogueNotes": "Withstand inevitable spring rains as well as light breezes. Increases Perception by <%= per %>. Limited Edition Spring 2026 Gear.", + "armorSpecialSpring2026HealerText": "Snowdrop Gown", + "armorSpecialSpring2026HealerNotes": "Glide gracefully from a cold, dark winter into glorious spring. Increases Constitution by <%= con %>. Limited Edition Spring 2026 Gear.", + "armorSpecialSpring2026MageText": "Maypole Dancer Outfit", + "armorSpecialSpring2026MageNotes": "Arrive ready to dance, picnic, and enjoy the warm weather spring brings. Increases Intelligence by <%= int %>. Limited Edition Spring 2026 Gear.", + "armorMystery201402Text": "Messenger Robes", "armorMystery201402Notes": "Shimmering and strong, these robes have many pockets to carry letters. Confers no benefit. February 2014 Subscriber Item.", "armorMystery201403Text": "Forest Walker Armor", @@ -1550,6 +1570,8 @@ "armorMystery202509Notes": "Bright silks protect you from the weather, hot or cold. Confers no benefit. September 2025 Subscriber Item.", "armorMystery202512Text": "Cookie Champion Armor", "armorMystery202512Notes": "Ready for battle in this plate that is both sweet and strong. Confers no benefit. December 2025 Subscriber Item.", + "armorMystery202604Text": "Audacious Astronaut Spacesuit", + "armorMystery202604Notes": "One small step for your To Do list, one giant leap for your sense of accomplishment! Confers no benefit. April 2026 Subscriber Item.", "armorMystery301404Text": "Steampunk Suit", "armorMystery301404Notes": "Dapper and dashing, wot! Confers no benefit. February 3015 Subscriber Item.", @@ -1800,6 +1822,10 @@ "armorArmoireBlackPartyDressNotes": "You’re strong, smart, hearty, and so fashionable! Increases Strength, Intelligence, and Constitution by <%= attrs %> each. Enchanted Armoire: Black Hairbow Set (Item 2 of 2).", "armorArmoireLoneCowpokeOutfitText": "Lone Cowpoke Outfit", "armorArmoireLoneCowpokeOutfitNotes": "Whoa, there! Want to make a statement when you ride into town as a mysterious stranger ready to be productive? Here’s the perfect outfit, complete with chaps and a shining, silver belt buckle. Increases Constitution by <%= con %>. Enchanted Armoire: Lone Cowpoke Set (Item 2 of 2)", + "armorArmoireSoftYellowSuitText": "Soft Yellow Suit", + "armorArmoireSoftYellowSuitNotes": "Yellow is an energetic color. Wear this to bed, and you will wake up with the sun the next morning ready to tackle a day full of tasks. Increases Constitution and Strength by <%= attrs %> each. Enchanted Armoire: Yellow Loungewear Set (Item 2 of 3).", + "armorArmoireHandstandOutfitText": "Handstand", + "armorArmoireHandstandOutfitNotes": "Things sure do look different when you’re upside-down, don’t they? If you’re feeling stuck, it’s time for a fresh perspective! Increases Perception by <%= per %>. Enchanted Armoire: Handstand Set (Item 1 of 1).", "headgear": "helm", "headgearCapitalized": "Headgear", @@ -2352,6 +2378,15 @@ "headSpecialWinter2026MageText": "Midwinter Candle Hat", "headSpecialWinter2026MageNotes": "Maintain focus and illumination as you set your sights on greater goals this season. Increases Perception by <%= per %>. Limited Edition 2025-2026 Winter Gear.", + "headSpecialSpring2026WarriorText": "Frog Warrior Helm", + "headSpecialSpring2026WarriorNotes": "Frogs are well-known for their resistance to corruption. This helm will grant you their noble qualities! Increases Strength by <%= str %>. Limited Edition Spring 2026 Gear.", + "headSpecialSpring2026RogueText": "Spring Branch Helm", + "headSpecialSpring2026RogueNotes": "Make a striking statement with twigs and buds growing wild in all directions. Increases Perception by <%= per %>. Limited Edition Spring 2026 Gear.", + "headSpecialSpring2026HealerText": "Snowdrop Helm", + "headSpecialSpring2026HealerNotes": "Make a hopeful statement with these beautiful, resilient petals. Increases Intelligence by <%= int %>. Limited Edition Spring 2026 Gear.", + "headSpecialSpring2026MageText": "Mayflower Crown", + "headSpecialSpring2026MageNotes": "Make a joyous statement with bright blooms encircling your head. Increases Perception by <%= per %>. Limited Edition Spring 2026 Gear.", + "headSpecialGaymerxText": "Rainbow Warrior Helm", "headSpecialGaymerxNotes": "In celebration of the GaymerX Conference, this special helmet is decorated with a radiant, colorful rainbow pattern! GaymerX is a game convention celebrating LGTBQ and gaming and is open to everyone.", @@ -2538,7 +2573,11 @@ "headMystery202512Text": "Cookie Champion Helm", "headMystery202512Notes": "Gingerbread forged with ancient magic will protect you as long as you can hold off your urge to try a bite! Confers no benefit. December 2025 Subscriber Item.", "headMystery202602Text": "Sakura Fox Ears", - "headMystery202602Notes": " Your hearing will be sharpened by these ears such that you can hear the buds of blossoms growing on tree branches as spring approaches. Confers no benefit. February 2026 Subscriber Item.", + "headMystery202602Notes": "Your hearing will be sharpened by these ears such that you can hear the buds of blossoms growing on tree branches as spring approaches. Confers no benefit. February 2026 Subscriber Item.", + "headMystery202603Text": "Wisteria Wizard Hat", + "headMystery202603Notes": "This jaunty hat not only enhances your magical ability, it also has a lovely spring scent! Confers no benefit. March 2026 Subscriber Item.", + "headMystery202604Text": "Audacious Astronaut Helmet", + "headMystery202604Notes": "In space, no one can hear you check off your To Do’s. But the real reward is your sense of personal accomplishment! Confers no benefit. April 2026 Subscriber Item.", "headMystery301404Text": "Fancy Top Hat", "headMystery301404Notes": "A fancy top hat for the finest of gentlefolk! January 3015 Subscriber Item. Confers no benefit.", @@ -2769,6 +2808,10 @@ "headArmoireBlacksmithsGogglesNotes": "Shatter and heat-resistant ocular protection is yours when you’re working in a forge. Increases Perception by <%= per %>. Enchanted Armoire: Blacksmith Set (Item 1 of 3).", "headArmoireLoneCowpokeHatText": "Lone Cowpoke Hat", "headArmoireLoneCowpokeHatNotes": "Howdy there, pardner! D’you hate when you’re out on the range, workin’ on tasks, and sun gets in your eyes? Well, good thing you’ve got a hat for that now. Increases Perception by <%= per %>. Enchanted Armoire: Lone Cowpoke Set (Item 1 of 2)", + "headArmoireFloppyYellowHatText": "Yellow Floppy Hat", + "headArmoireFloppyYellowHatNotes": "Many spells have been sewn into this simple hat, giving it a youthful yellow color. Increases all stats by <%= attrs %> each. Enchanted Armoire: Yellow Loungewear Set (Item 1 of 3).", + "headArmoireVerdantArmingCapText": "Verdant Page Arming Cap", + "headArmoireVerdantArmingCapNotes": "This comfy, cushioned coif makes you battle-ready and helps you withstand anything heavy that could come your way. Increases Perception and Constitution by <%= attrs %> each. Enchanted Armoire: Verdant Page Set (Item 1 of 2).", "offhand": "off-hand item", "offHandCapitalized": "Off-Hand Item", @@ -3086,6 +3129,13 @@ "shieldSpecialWinter2026HealerText": "Starburst", "shieldSpecialWinter2026HealerNotes": "Stars help with wayfinding, energy, and illumination—all things that help you better conquer a task list. Increases Constitution by <%= con %>. Limited Edition Winter 2025-2026 Gear.", + "shieldSpecialSpring2026WarriorText": "Frog Warrior Candelabra", + "shieldSpecialSpring2026WarriorNotes": "Not only can this candelabra light your way, you can use it to melt any lingering snow and ice. Increases Constitution by <%= con %>. Limited Edition Spring 2026 Gear.", + "shieldSpecialSpring2026RogueText": "Spring Branch", + "shieldSpecialSpring2026RogueNotes": "Reach out and reach high with these branches. They double as a back scratcher in a pinch. Increases Strength by <%= str %>. Limited Edition Spring 2026 Gear.", + "shieldSpecialSpring2026HealerText": "Snowdrop Leaf", + "shieldSpecialSpring2026HealerNotes": "Create a light breeze with this fan as the days grow warmer. It doubles as a writing utensil in a pinch. Increases Constitution by <%= con %>. Limited Edition Spring 2026 Gear.", + "shieldMystery201601Text": "Resolution Slayer", "shieldMystery201601Notes": "This blade can be used to parry away all distractions. Confers no benefit. January 2016 Subscriber Item.", "shieldMystery201701Text": "Time-Freezer Shield", @@ -3116,6 +3166,8 @@ "shieldMystery202508Notes": "If you thought one spinning blade was cool looking, try two! Confers no benefit. August 2025 Subscriber Item.", "shieldMystery202511Text": "Frost Shield", "shieldMystery202511Notes": "This rugged shield of icy rock protects you from bad Habits but won't freeze your hands. Confers no benefit. November 2025 Subscriber Item.", + "shieldMystery202605Text": "Nightfall Shield", + "shieldMystery202605Notes": "Let the moon’s shining light protect you from dangers in the dark. Confers no benefit. May 2026 Subscriber Item.", "shieldMystery301405Text": "Clock Shield", "shieldMystery301405Notes": "Time is on your side with this towering clock shield! Confers no benefit. June 3015 Subscriber Item.", @@ -3298,6 +3350,10 @@ "shieldArmoireDoubleBassNotes": "Bom doo bom brrrr brr brr brrrr! Gather your party for some grounding or dancing as you listen to music on this deep double bass. Increases Constitution and Strength by <%= attrs %> each. Enchanted Armoire: Musical Instrument Set 2 (Item 3 of 3)", "shieldArmoirePrettyPinkGiftBoxText": "Pretty Pink Present", "shieldArmoirePrettyPinkGiftBoxNotes": "Is this gift from a dear friend? A caring relative? A true love? A secret admirer? Whoever sent it knows you’ll be pleased with what’s inside. Increases all stats by <%= attrs %> each. Enchanted Armoire: Pretty in Pink Set (Item 2 of 2)", + "shieldArmoireSoftYellowPillowText": "Soft Yellow Pillow", + "shieldArmoireSoftYellowPillowNotes": "The experienced warrior packs a pillow for any expedition. Grow and shine as you consolidate all you’ve learned during past adventures… even while you nap. Increases Intelligence and Perception by <%= attrs %> each. Enchanted Armoire: Yellow Loungewear Set (Item 3 of 3).", + "shieldArmoireVerdantBannerText": "Verdant Page Banner", + "shieldArmoireVerdantBannerNotes": "Wave your banner high to signal friends it’s time to rally together! Intelligence by <%= int %>. Enchanted Armoire: Verdant Page Set (Item 2 of 2).", "back": "Back Accessory", "backBase0Text": "No Back Accessory", @@ -3392,6 +3448,8 @@ "backMystery202601Notes": "This mark grants the user control over the elements of the season of cold and frost. Confers no benefit. January 2026 Subscriber Item.", "backMystery202602Text": "Five Tails of Sakura", "backMystery202602Notes": "These fluffy tails are the color of cherry blossoms, a reminder that spring is on the way! Confers no benefit. February 2026 Subscriber Item.", + "backMystery202605Text": "Nightfall Nimbus", + "backMystery202605Notes": "A glowing aureole of moonlight and starlight to illuminate the darkest night. Confers no benefit. May 2026 Subscriber Item.", "backArmoireHarpsichordText": "Harpsichord", "backArmoireHarpsichordNotes": "Pting! Ptiiing! Gather your party for a dinner or picnic and listen to a tinny melody on this harpsichord. Increases Perception and Intelligence by <%= attrs %> each. Enchanted Armoire: Musical Instrument Set 2 (Item 1 of 3)", diff --git a/website/common/locales/en/limited.json b/website/common/locales/en/limited.json index 5141616640..58fcfe3d5a 100644 --- a/website/common/locales/en/limited.json +++ b/website/common/locales/en/limited.json @@ -223,26 +223,30 @@ "fall2024UnderworldSorcerorMageSet": "Underworld Sorceror Set (Mage)", "fall2024SpaceInvaderHealerSet": "Space Invader Set (Healer)", "fall2024BlackCatRogueSet": "Black Cat Set (Rogue)", - "winter2025MooseWarriorSet": "Moose Warrior Set", - "winter2025AuroraMageSet": "Aurora Mage Set", - "winter2025StringLightsHealerSet": "String Lights Healer Set", - "winter2025SnowRogueSet": "Snow Rogue Set", - "spring2025SunshineWarriorSet": "Sunshine Warrior Set", - "spring2025CrystalPointRogueSet": "Crystal Point Rogue Set", - "spring2025PlumeriaHealerSet": "Plumeria Healer Set", - "spring2025MantisMageSet": "Mantis Mage Set", - "summer2025ScallopWarriorSet": "Scallop Warrior Set", - "summer2025SquidRogueSet": "Squid Rogue Set", - "summer2025SeaAngelHealerSet": "Sea Angel Healer Set", - "summer2025FairyWrasseMageSet": "Fairy Wrasse Mage Set", - "fall2025SasquatchWarriorSet": "Sasquatch Warrior Set", - "fall2025SkeletonRogueSet": "Skeleton Rogue Set", - "fall2025KoboldHealerSet": "Kobold Healer Set", - "fall2025MaskedGhostMageSet": "Masked Ghost Mage Set", - "winter2026RimeReaperWarriorSet": "Rime Reaper Warrior Set", - "winter2026SkiRogueSet": "Ski Rogue Set", - "winter2026PolarBearHealerSet": "Polar Bear Healer Set", - "winter2026MidwinterCandleMageSet": "Midwinter Candle Mage Set", + "winter2025MooseWarriorSet": "Moose Set (Warrior)", + "winter2025AuroraMageSet": "Aurora Set (Mage)", + "winter2025StringLightsHealerSet": "String Lights Set (Healer)", + "winter2025SnowRogueSet": "Snow Set (Rogue)", + "spring2025SunshineWarriorSet": "Sunshine Set (Warrior)", + "spring2025CrystalPointRogueSet": "Crystal Point Set (Rogue)", + "spring2025PlumeriaHealerSet": "Plumeria Set (Healer)", + "spring2025MantisMageSet": "Mantis Set (Mage)", + "summer2025ScallopWarriorSet": "Scallop Set (Warrior)", + "summer2025SquidRogueSet": "Squid Set (Rogue)", + "summer2025SeaAngelHealerSet": "Sea Angel Set (Healer)", + "summer2025FairyWrasseMageSet": "Fairy Wrasse Set (Mage)", + "fall2025SasquatchWarriorSet": "Sasquatch Set (Warrior)", + "fall2025SkeletonRogueSet": "Skeleton Set (Rogue)", + "fall2025KoboldHealerSet": "Kobold Set (Healer)", + "fall2025MaskedGhostMageSet": "Masked Ghost Set (Mage)", + "winter2026RimeReaperWarriorSet": "Rime Reaper Set (Warrior)", + "winter2026SkiRogueSet": "Ski Set (Rogue)", + "winter2026PolarBearHealerSet": "Polar Bear Set (Healer)", + "winter2026MidwinterCandleMageSet": "Midwinter Candle Set (Mage)", + "spring2026FrogWarriorSet": "Frog Set (Warrior)", + "spring2026BranchRogueSet": "Spring Branch Set (Rogue)", + "spring2026SnowdropHealerSet": "Snowdrop Set (Healer)", + "spring2026MaypoleMageSet": "Maypole Set (Mage)", "winterPromoGiftHeader": "GIFT A SUBSCRIPTION, GET ONE FREE!", "winterPromoGiftDetails1": "Until January 6th only, when you gift somebody a subscription, you get the same subscription for yourself for free!", "winterPromoGiftDetails2": "Please note that if you or your gift recipient already have a recurring subscription, the gifted subscription will only start after that subscription is cancelled or has expired. Thanks so much for your support! <3", diff --git a/website/common/locales/en/subscriber.json b/website/common/locales/en/subscriber.json index 829f2ba093..cf65bcb05f 100644 --- a/website/common/locales/en/subscriber.json +++ b/website/common/locales/en/subscriber.json @@ -183,6 +183,9 @@ "mysterySet202512": "Cookie Champion Set", "mysterySet202601": "Winter's Aegis Set", "mysterySet202602": "Sakura Fox Set", + "mysterySet202603": "Wisteria Wizard Set", + "mysterySet202604": "Audacious Astronaut Set", + "mysterySet202605": "Nightfall Nimbus Set", "mysterySet301404": "Steampunk Standard Set", "mysterySet301405": "Steampunk Accessories Set", "mysterySet301703": "Peacock Steampunk Set", diff --git a/website/common/script/content/appearance/backgrounds.js b/website/common/script/content/appearance/backgrounds.js index 99390edf9c..d2ccd3ad0b 100644 --- a/website/common/script/content/appearance/backgrounds.js +++ b/website/common/script/content/appearance/backgrounds.js @@ -683,6 +683,15 @@ const backgrounds = { backgrounds022026: { elegant_palace: { }, }, + backgrounds032026: { + waterfall_with_rainbow: { }, + }, + backgrounds042026: { + riding_a_comet: { }, + }, + backgrounds052026: { + elven_citadel: { }, + }, eventBackgrounds: { birthday_bash: { price: 0, diff --git a/website/common/script/content/constants/releaseDates.js b/website/common/script/content/constants/releaseDates.js index 01405c66d3..17c33c3b88 100644 --- a/website/common/script/content/constants/releaseDates.js +++ b/website/common/script/content/constants/releaseDates.js @@ -29,6 +29,9 @@ export const ARMOIRE_RELEASE_DATES = { musicalInstrumentTwo: { year: 2025, month: 12 }, loneCowpoke: { year: 2026, month: 1 }, prettyInPink: { year: 2026, month: 2 }, + yellowLoungewear: { year: 2026, month: 3 }, + handstand: { year: 2026, month: 4 }, + verdantPage: { year: 2026, month: 5 }, }; export const EGGS_RELEASE_DATES = { diff --git a/website/common/script/content/constants/seasonalSets.js b/website/common/script/content/constants/seasonalSets.js index b26df9ff8e..4af20958c9 100644 --- a/website/common/script/content/constants/seasonalSets.js +++ b/website/common/script/content/constants/seasonalSets.js @@ -131,6 +131,11 @@ const SEASONAL_SETS = { 'spring2025CrystalPointRogueSet', 'spring2025PlumeriaHealerSet', 'spring2025MantisMageSet', + + 'spring2026FrogWarriorSet', + 'spring2026BranchRogueSet', + 'spring2026SnowdropHealerSet', + 'spring2026MaypoleMageSet', ], summer: [ diff --git a/website/common/script/content/gear/sets/armoire.js b/website/common/script/content/gear/sets/armoire.js index a3821e97ee..4ab7051adf 100644 --- a/website/common/script/content/gear/sets/armoire.js +++ b/website/common/script/content/gear/sets/armoire.js @@ -561,6 +561,15 @@ const armor = { con: 10, set: 'loneCowpoke', }, + softYellowSuit: { + con: 9, + str: 9, + set: 'yellowLoungewear', + }, + handstandOutfit: { + per: 10, + set: 'handstand', + }, }; const back = { @@ -1156,6 +1165,18 @@ const head = { per: 10, set: 'loneCowpoke', }, + floppyYellowHat: { + con: 3, + int: 3, + per: 3, + str: 3, + set: 'yellowLoungewear', + }, + verdantArmingCap: { + con: 5, + per: 5, + set: 'verdantPage', + }, }; const shield = { @@ -1546,6 +1567,15 @@ const shield = { str: 2, set: 'prettyInPink', }, + softYellowPillow: { + int: 9, + per: 9, + set: 'yellowLoungewear', + }, + verdantBanner: { + int: 10, + set: 'verdantPage', + }, }; const headAccessory = { diff --git a/website/common/script/content/gear/sets/mystery.js b/website/common/script/content/gear/sets/mystery.js index 3c9da34dbd..8c26a6a498 100644 --- a/website/common/script/content/gear/sets/mystery.js +++ b/website/common/script/content/gear/sets/mystery.js @@ -72,6 +72,7 @@ const armor = { 202504: { }, 202509: { }, 202512: { }, + 202604: { }, 301404: { }, 301703: { }, 301704: { }, @@ -122,6 +123,7 @@ const back = { 202510: { }, 202601: { }, 202602: { }, + 202605: { }, }; const body = { @@ -254,6 +256,8 @@ const head = { 202507: { }, 202512: { }, 202602: { }, + 202603: { }, + 202604: { }, 301404: { }, 301405: { }, 301703: { }, @@ -308,6 +312,7 @@ const shield = { 202506: { }, 202508: { }, 202511: { }, + 202605: { }, 301405: { }, 301704: { }, }; @@ -337,6 +342,7 @@ const weapon = { 202511: { }, 202512: { }, 202601: { }, + 202603: { }, 301404: { }, }; diff --git a/website/common/script/content/gear/sets/special/index.js b/website/common/script/content/gear/sets/special/index.js index 9228bc426f..94ccd62d50 100644 --- a/website/common/script/content/gear/sets/special/index.js +++ b/website/common/script/content/gear/sets/special/index.js @@ -839,6 +839,18 @@ const armor = { winter2026Rogue: { set: 'winter2026SkiRogueSet', }, + spring2026Warrior: { + set: 'spring2026FrogWarriorSet', + }, + spring2026Rogue: { + set: 'spring2026BranchRogueSet', + }, + spring2026Healer: { + set: 'spring2026SnowdropHealerSet', + }, + spring2026Mage: { + set: 'spring2026MaypoleMageSet', + }, }; const armorStats = { @@ -1988,6 +2000,18 @@ const head = { winter2026Rogue: { set: 'winter2026SkiRogueSet', }, + spring2026Warrior: { + set: 'spring2026FrogWarriorSet', + }, + spring2026Rogue: { + set: 'spring2026BranchRogueSet', + }, + spring2026Healer: { + set: 'spring2026SnowdropHealerSet', + }, + spring2026Mage: { + set: 'spring2026MaypoleMageSet', + }, }; const headStats = { @@ -2727,6 +2751,16 @@ const shield = { winter2026Rogue: { set: 'winter2026SkiRogueSet', }, + spring2026Warrior: { + set: 'spring2026FrogWarriorSet', + }, + spring2026Rogue: { + set: 'spring2026BranchRogueSet', + notes: t('shieldSpecialSpring2026RogueNotes', { str: 8 }), + }, + spring2026Healer: { + set: 'spring2026SnowdropHealerSet', + }, }; const shieldStats = { @@ -3466,6 +3500,18 @@ const weapon = { winter2026Rogue: { set: 'winter2026SkiRogueSet', }, + spring2026Warrior: { + set: 'spring2026FrogWarriorSet', + }, + spring2026Rogue: { + set: 'spring2026BranchRogueSet', + }, + spring2026Healer: { + set: 'spring2026SnowdropHealerSet', + }, + spring2026Mage: { + set: 'spring2026MaypoleMageSet', + }, }; const weaponStats = { From 40122e5621fb46430371219bcc22fbe85816d75c Mon Sep 17 00:00:00 2001 From: Kalista Payne Date: Thu, 26 Feb 2026 12:08:13 -0600 Subject: [PATCH 08/34] 5.46.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ec22d8f04c..f62f2e3bb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "habitica", - "version": "5.45.0", + "version": "5.46.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "habitica", - "version": "5.45.0", + "version": "5.46.0", "hasInstallScript": true, "dependencies": { "@babel/core": "^7.22.10", diff --git a/package.json b/package.json index fcb0b1dd81..4ff6435850 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "habitica", "description": "A habit tracker app which treats your goals like a Role Playing Game.", - "version": "5.45.0", + "version": "5.46.0", "main": "./website/server/index.js", "dependencies": { "@babel/core": "^7.22.10", From f21e800b0b80b30e9977c402c215568e9e595763 Mon Sep 17 00:00:00 2001 From: Fiz <34069775+Hafizzle@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:02:47 -0600 Subject: [PATCH 09/34] Group Plan Modal (#15588) * 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 --- website/client/src/app.vue | 5 + .../group-plans/groupPlanSelectionModal.vue | 577 ++++++++++++++++++ .../src/components/static/groupPlans.vue | 5 +- .../src/components/ui/selectableCard.vue | 92 +++ website/client/src/pages/user-main.vue | 4 + website/common/locales/en/groups.json | 14 +- website/server/controllers/api-v3/groups.js | 10 +- website/server/libs/inbox/index.js | 2 +- website/server/libs/payments/subscriptions.js | 4 + website/server/models/group.js | 22 +- 10 files changed, 726 insertions(+), 9 deletions(-) create mode 100644 website/client/src/components/group-plans/groupPlanSelectionModal.vue create mode 100644 website/client/src/components/ui/selectableCard.vue diff --git a/website/client/src/app.vue b/website/client/src/app.vue index a49c648541..e454bf53e2 100644 --- a/website/client/src/app.vue +++ b/website/client/src/app.vue @@ -229,6 +229,11 @@ export default { } return Promise.resolve(error); } + if (error.response.status === 404 + && error.response.config.method === 'get' + && error.response.config.url.indexOf('/api/v4/groups/party') !== -1) { + return Promise.reject(error); + } } const errorData = error.response.data; diff --git a/website/client/src/components/group-plans/groupPlanSelectionModal.vue b/website/client/src/components/group-plans/groupPlanSelectionModal.vue new file mode 100644 index 0000000000..118b80bdbc --- /dev/null +++ b/website/client/src/components/group-plans/groupPlanSelectionModal.vue @@ -0,0 +1,577 @@ + + + + + + + diff --git a/website/client/src/components/static/groupPlans.vue b/website/client/src/components/static/groupPlans.vue index 2bcedada89..7bd2d16b9f 100644 --- a/website/client/src/components/static/groupPlans.vue +++ b/website/client/src/components/static/groupPlans.vue @@ -1,5 +1,6 @@ + + + + diff --git a/website/client/src/pages/user-main.vue b/website/client/src/pages/user-main.vue index aa223b7444..8df3031c76 100644 --- a/website/client/src/pages/user-main.vue +++ b/website/client/src/pages/user-main.vue @@ -295,6 +295,10 @@ export default { appState = JSON.parse(appState); if (appState.paymentCompleted) { removeLocalSetting(CONSTANTS.savedAppStateValues.SAVED_APP_STATE); + if (appState.paymentType === 'groupPlan') { + this.$store.state.upgradingGroup = {}; + this.$store.dispatch('guilds:getGroupPlans', true); + } this.$root.$emit('habitica:payment-success', appState); } } diff --git a/website/common/locales/en/groups.json b/website/common/locales/en/groups.json index 9a08a33a12..5cacc54956 100644 --- a/website/common/locales/en/groups.json +++ b/website/common/locales/en/groups.json @@ -428,5 +428,17 @@ "interestedLearningMore": "Interested in Learning More?", "checkGroupPlanFAQ": "Check out the Group Plans FAQ to learn how to get the most out of your shared task experience.", "groupPlanBillingFYI": "Group Plan subscriptions automatically renew unless you cancel at least 24 hours before the end of your current period. You can cancel from the Group Billing tab of your Group Plan. You will be charged within 24 hours before your subscription renews, based on the number of members in your Group Plan at that time. If you add members between payment periods, you'll see an additional prorated charge for their benefits at your next billing cycle.", - "groupPlanBillingFYIShort": "Group Plan subscriptions automatically renew unless you cancel at least 24 hours before the end of your current period. You will be charged within 24 hours before your subscription renews, based on the number of members in your Group Plan at that time. If you add members between payment periods, you'll see an additional prorated charge for their benefits at your next billing cycle." + "groupPlanBillingFYIShort": "Group Plan subscriptions automatically renew unless you cancel at least 24 hours before the end of your current period. You will be charged within 24 hours before your subscription renews, based on the number of members in your Group Plan at that time. If you add members between payment periods, you'll see an additional prorated charge for their benefits at your next billing cycle.", + "chooseAnOption": "Choose an Option", + "upgradeExistingGroup": "Upgrade an Existing Group", + "createNewGroup": "Create a New Group", + "yourParty": "Your Party", + "previouslyUpgradedGroup": "Previously upgraded Group", + "inviteOthersForAdditional": "Invite others to your Group for an additional", + "perMember": "per member", + "additionalMembersProrated": "Additional members invited during the month will be added to the next billing cycle's total as a pro-rated charge.", + "oneMember": "1 member", + "membersCount": "<%= count %> members", + "pendingCount": "(<%= count %> pending)", + "upgradeCancelsPendingInvites": "Upgrading your Party will cancel all pending invites" } diff --git a/website/server/controllers/api-v3/groups.js b/website/server/controllers/api-v3/groups.js index 159adb8466..93c7eb04da 100644 --- a/website/server/controllers/api-v3/groups.js +++ b/website/server/controllers/api-v3/groups.js @@ -334,9 +334,13 @@ api.getGroups = { throw new BadRequest(apiError('guildsOnlyPaginate')); } - const groupFields = basicGroupFields.concat(' description memberCount balance leaderOnly'); + let groupFields = basicGroupFields.concat(' description memberCount balance leaderOnly'); const sort = '-memberCount'; + if (req.query.includeExpiredPlans === 'true') { + groupFields = groupFields.concat(' purchased'); + } + const filters = {}; if (req.query.categories) { const categorySlugs = req.query.categories.split(','); @@ -371,6 +375,10 @@ api.getGroups = { filters.$or.push({ description: searchQuery }); } + if (req.query.includeExpiredPlans === 'true') { + filters.includeExpiredPlans = true; + } + const results = await Group.getGroups({ user, types, diff --git a/website/server/libs/inbox/index.js b/website/server/libs/inbox/index.js index af0d7ae07d..c28400c583 100644 --- a/website/server/libs/inbox/index.js +++ b/website/server/libs/inbox/index.js @@ -1,6 +1,6 @@ import { mapInboxMessage, inboxModel } from '../../models/message'; import { getUserInfo, sendTxn as sendTxnEmail } from '../email'; // eslint-disable-line import/no-cycle -import { sendNotification as sendPushNotification } from '../pushNotifications'; +import { sendNotification as sendPushNotification } from '../pushNotifications'; // eslint-disable-line import/no-cycle export async function sentMessage (sender, receiver, message, translate) { const fakeSending = sender.flags.chatShadowMuted; diff --git a/website/server/libs/payments/subscriptions.js b/website/server/libs/payments/subscriptions.js index 93ff285b41..9a0dd3126c 100644 --- a/website/server/libs/payments/subscriptions.js +++ b/website/server/libs/payments/subscriptions.js @@ -153,6 +153,10 @@ async function prepareSubscriptionValues (data) { groupId = group._id; recipient.purchased.plan.quantity = data.sub.quantity; + if (group.type === 'party') { + await group.removeGroupInvitations(); + } + await addSubscriptionToGroupUsers(group); } diff --git a/website/server/models/group.js b/website/server/models/group.js index 36c678c2cd..f22d378652 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -156,7 +156,16 @@ schema.plugin(baseModel, { noSet: ['_id', 'balance', 'quest', 'memberCount', 'chat', 'bannedWordsAllowed', 'challengeCount', 'tasksOrder', 'purchased', 'managers'], private: ['purchased.plan'], toJSONTransform (plainObj, originalDoc) { - if (plainObj.purchased) plainObj.purchased.active = originalDoc.hasActiveGroupPlan(); + if (plainObj.purchased) { + plainObj.purchased.active = originalDoc.hasActiveGroupPlan(); + const plan = originalDoc.purchased && originalDoc.purchased.plan; + if (plan && plan.customerId) { + plainObj.purchased.wasUpgraded = true; + if (plan.dateTerminated) { + plainObj.purchased.dateTerminated = plan.dateTerminated; + } + } + } }, }); @@ -309,13 +318,16 @@ schema.statics.getGroups = async function getGroups (options = {}) { privacy: 'private', _id: { $in: user.guilds }, 'purchased.plan.customerId': { $exists: true }, - $or: [ + }; + if (!filters.includeExpiredPlans) { + query.$or = [ { 'purchased.plan.dateTerminated': null }, { 'purchased.plan.dateTerminated': { $exists: false } }, { 'purchased.plan.dateTerminated': { $gt: new Date() } }, - ], - }; - _.assign(query, filters); + ]; + } + const filtersWithoutCustom = _.omit(filters, ['includeExpiredPlans']); + _.assign(query, filtersWithoutCustom); const privateGuildsQuery = this.find(query).select(groupFields); if (populateLeader === true) privateGuildsQuery.populate('leader', nameFields); privateGuildsQuery.sort(sort); 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 10/34] 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 @@
    +