Compare commits

..

88 Commits

Author SHA1 Message Date
Kalista Payne 61585b2549 5.46.3 2026-03-12 16:51:19 -05:00
Corban Villa 07275bd522 fix(security): don't reuse IV for cryptography 2026-03-12 14:53:30 -05:00
Fiz 74fc543ef2 Orb of Rebirth Updates (Count usage) (#15629)
* Update orb of rebirth to count every usage

* Orb of rebirth modal UI updates

* Fix rebirth modal showing again after page refresh

* remove unused MAX_LEVEL

* scale orb of rebirth on modal

* UI & wording updates for Orb of Rebirth

* UI & wording updates for Orb of Rebirth cont.

* Orb of rebirth UI tweaks

* Orb of rebirth UI tweaks

* Orb of Rebirth modal UI tweak

* Extend modal waves

* Continued Orb of Rebirth UI Updates

* Orb of rebirth margin tweak

* rebirth-orb asset
2026-03-12 14:51:48 -05:00
Kalista Payne 8c90e5472b 5.46.2 2026-03-10 12:02:09 -05:00
Kalista Payne c8d9ba6c8e chore(git): update submodule 2026-03-10 12:02:06 -05:00
Weblate 1675c2749b Merge branch 'origin/develop' into Weblate. 2026-03-10 17:58:10 +01:00
Rohithgowda K e85a2bae14 fix: prevent duplicate streak achievement notifications (#13325) (#15550)
* fix: prevent duplicate streak achievement notifications (#13325)

* fix(lint): whitespace
also remove unnecessary file and comments

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2026-03-10 10:03:28 -05:00
Weblate 3355500fba Translated using Weblate (French)
Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (277 of 279 strings)

Translated using Weblate (Czech)

Currently translated at 78.5% (739 of 941 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (278 of 279 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (Portuguese)

Currently translated at 68.7% (189 of 275 strings)

Translated using Weblate (Portuguese)

Currently translated at 86.6% (383 of 442 strings)

Translated using Weblate (Portuguese)

Currently translated at 59.5% (2115 of 3551 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Turkish)

Currently translated at 49.5% (1732 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.1% (269 of 292 strings)

Translated using Weblate (Portuguese)

Currently translated at 91.4% (267 of 292 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.0% (438 of 442 strings)

Translated using Weblate (Portuguese)

Currently translated at 59.4% (2112 of 3551 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (Korean)

Currently translated at 99.2% (934 of 941 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Portuguese)

Currently translated at 63.0% (184 of 292 strings)

Translated using Weblate (Portuguese)

Currently translated at 46.2% (117 of 253 strings)

Translated using Weblate (Turkish)

Currently translated at 99.0% (932 of 941 strings)

Co-authored-by: Anderson Ferreira <freiitas.dev@gmail.com>
Co-authored-by: Begümay Çınar <begumay@proton.me>
Co-authored-by: Duggu Ghosh <duggu52d@gmail.com>
Co-authored-by: Isabela de França <ifranceg@gmail.com>
Co-authored-by: Kim Sihyung <kimsihyung@u.nus.edu>
Co-authored-by: Matej Boura <B.Matej@email.cz>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Translatlantic Translation <translatlanticom@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translation: Habitica/Backgrounds
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2026-03-10 12:48:18 +01:00
Kalista Payne 486f15df0f fix(news): allow X dismiss for everyone 2026-03-09 15:00:47 -05:00
Kalista Payne f0b6b5611c feat(news): allow quick dismiss for admins 2026-03-06 19:07:48 -06:00
Kalista Payne 7e45c79714 chore(npm): update lockfiles 2026-03-06 11:58:02 -06:00
Kalista Payne 8da6065355 fix(pins): don't erase pinned quest potions 2026-03-06 11:22:29 -06:00
Kalista Payne a212363bda 5.46.1 2026-03-05 14:59:58 -06:00
Weblate 2e19e73b9e Translated using Weblate (Dutch)
Currently translated at 72.7% (2585 of 3551 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (3466 of 3551 strings)

Translated using Weblate (Czech)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Czech)

Currently translated at 13.0% (33 of 253 strings)

Translated using Weblate (Czech)

Currently translated at 98.2% (164 of 167 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.4% (3497 of 3551 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 50.6% (148 of 292 strings)

Translated using Weblate (German)

Currently translated at 98.1% (434 of 442 strings)

Translated using Weblate (Turkish)

Currently translated at 49.5% (1732 of 3497 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3551 of 3551 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 99.8% (940 of 941 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.7% (441 of 442 strings)

Translated using Weblate (Turkish)

Currently translated at 49.5% (1732 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 49.5% (1732 of 3497 strings)

Translated using Weblate (Russian)

Currently translated at 74.7% (189 of 253 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 82.1% (708 of 862 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 82.1% (708 of 862 strings)

Translated using Weblate (German)

Currently translated at 92.8% (271 of 292 strings)

Translated using Weblate (French)

Currently translated at 100.0% (442 of 442 strings)

Translated using Weblate (German)

Currently translated at 99.5% (937 of 941 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (French)

Currently translated at 100.0% (292 of 292 strings)

Translated using Weblate (French)

Currently translated at 100.0% (941 of 941 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Ukrainian)

Currently translated at 80.4% (152 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 54.5% (138 of 253 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Turkish)

Currently translated at 39.1% (99 of 253 strings)

Translated using Weblate (Turkish)

Currently translated at 35.5% (90 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 95.0% (192 of 202 strings)

Translated using Weblate (French)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Turkish)

Currently translated at 75.7% (653 of 862 strings)

Translated using Weblate (Turkish)

Currently translated at 75.7% (653 of 862 strings)

Translated using Weblate (Turkish)

Currently translated at 33.9% (86 of 253 strings)

Translated using Weblate (Turkish)

Currently translated at 98.8% (425 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 95.3% (410 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 94.8% (408 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Turkish)

Currently translated at 94.6% (407 of 430 strings)

Translated using Weblate (Dutch)

Currently translated at 73.4% (2567 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 82.3% (354 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 49.5% (1732 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 49.5% (1732 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.3% (111 of 114 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 67.1% (2349 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 49.4% (1731 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 49.4% (1731 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 86.2% (238 of 276 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.3% (111 of 114 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 67.1% (2349 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Dutch)

Currently translated at 88.8% (382 of 430 strings)

Translated using Weblate (Dutch)

Currently translated at 88.6% (381 of 430 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Dutch)

Currently translated at 88.3% (380 of 430 strings)

Translated using Weblate (Dutch)

Currently translated at 73.1% (2557 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Dutch)

Currently translated at 58.8% (149 of 253 strings)

Translated using Weblate (Japanese)

Currently translated at 98.6% (143 of 145 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Dutch)

Currently translated at 96.9% (127 of 131 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Dutch)

Currently translated at 76.0% (210 of 276 strings)

Translated using Weblate (Dutch)

Currently translated at 56.5% (143 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 85.5% (368 of 430 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (German)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Portuguese)

Currently translated at 79.3% (150 of 189 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 96.4% (110 of 114 strings)

Translated using Weblate (Dutch)

Currently translated at 95.9% (894 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (201 of 202 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Hungarian)

Currently translated at 96.8% (183 of 189 strings)

Translated using Weblate (Hungarian)

Currently translated at 96.8% (245 of 253 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Hungarian)

Currently translated at 95.5% (193 of 202 strings)

Translated using Weblate (Hungarian)

Currently translated at 95.6% (109 of 114 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 98.6% (143 of 145 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 49.0% (1717 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 76.4% (2672 of 3497 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 51.6% (47 of 91 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 61.4% (169 of 275 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Swedish)

Currently translated at 59.6% (164 of 275 strings)

Translated using Weblate (Swedish)

Currently translated at 87.7% (115 of 131 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Swedish)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Swedish)

Currently translated at 92.9% (106 of 114 strings)

Translated using Weblate (Swedish)

Currently translated at 91.4% (86 of 94 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Swedish)

Currently translated at 91.4% (86 of 94 strings)

Translated using Weblate (Japanese)

Currently translated at 98.1% (3434 of 3497 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (251 of 253 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Swedish)

Currently translated at 92.4% (134 of 145 strings)

Translated using Weblate (Swedish)

Currently translated at 52.1% (144 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 63.5% (183 of 288 strings)

Translated using Weblate (Swedish)

Currently translated at 48.2% (139 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 47.2% (1653 of 3497 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 69.9% (2447 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Swedish)

Currently translated at 4.7% (12 of 253 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Swedish)

Currently translated at 91.0% (152 of 167 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 54.1% (137 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 93.5% (872 of 932 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Dutch)

Currently translated at 92.5% (187 of 202 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Dutch)

Currently translated at 52.5% (133 of 253 strings)

Translated using Weblate (German)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Dutch)

Currently translated at 51.7% (131 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Dutch)

Currently translated at 91.8% (856 of 932 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Dutch)

Currently translated at 91.6% (854 of 932 strings)

Translated using Weblate (Portuguese)

Currently translated at 52.7% (1844 of 3497 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Swedish)

Currently translated at 51.0% (141 of 276 strings)

Translated using Weblate (Swedish)

Currently translated at 77.6% (73 of 94 strings)

Translated using Weblate (Swedish)

Currently translated at 89.9% (170 of 189 strings)

Translated using Weblate (Swedish)

Currently translated at 4.7% (12 of 253 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Swedish)

Currently translated at 58.5% (546 of 932 strings)

Translated using Weblate (Swedish)

Currently translated at 86.2% (163 of 189 strings)

Translated using Weblate (Swedish)

Currently translated at 85.7% (162 of 189 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Swedish)

Currently translated at 70.3% (133 of 189 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (German)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Dutch)

Currently translated at 86.1% (248 of 288 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Swedish)

Currently translated at 59.2% (163 of 275 strings)

Translated using Weblate (Czech)

Currently translated at 9.0% (23 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Dutch)

Currently translated at 73.0% (2556 of 3497 strings)

Translated using Weblate (French)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Czech)

Currently translated at 8.6% (22 of 253 strings)

Translated using Weblate (Spanish)

Currently translated at 99.0% (200 of 202 strings)

Translated using Weblate (Dutch)

Currently translated at 79.8% (230 of 288 strings)

Translated using Weblate (German)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Dutch)

Currently translated at 90.4% (843 of 932 strings)

Translated using Weblate (German)

Currently translated at 99.6% (3485 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (German)

Currently translated at 99.5% (3480 of 3497 strings)

Translated using Weblate (German)

Currently translated at 99.3% (3476 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.0% (198 of 202 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Japanese)

Currently translated at 98.2% (112 of 114 strings)

Translated using Weblate (Dutch)

Currently translated at 96.4% (110 of 114 strings)

Translated using Weblate (Dutch)

Currently translated at 85.9% (801 of 932 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 99.3% (3475 of 3497 strings)

Translated using Weblate (German)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (German)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (German)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (German)

Currently translated at 99.6% (252 of 253 strings)

Translated using Weblate (German)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (German)

Currently translated at 98.8% (250 of 253 strings)

Translated using Weblate (German)

Currently translated at 98.8% (250 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (German)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Dutch)

Currently translated at 84.0% (783 of 932 strings)

Translated using Weblate (German)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Dutch)

Currently translated at 49.4% (125 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 96.7% (88 of 91 strings)

Translated using Weblate (German)

Currently translated at 99.3% (3473 of 3497 strings)

Translated using Weblate (German)

Currently translated at 99.3% (3473 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.6% (3451 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 47.0% (119 of 253 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.3% (3440 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 72.5% (2536 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.6% (3380 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 72.2% (2528 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 99.1% (243 of 245 strings)

Translated using Weblate (Dutch)

Currently translated at 99.4% (188 of 189 strings)

Translated using Weblate (Dutch)

Currently translated at 47.0% (119 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 47.0% (119 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 47.0% (119 of 253 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (German)

Currently translated at 99.2% (3470 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.2% (112 of 114 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Portuguese)

Currently translated at 24.1% (61 of 253 strings)

Translated using Weblate (German)

Currently translated at 96.5% (140 of 145 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 82.1% (708 of 862 strings)

Translated using Weblate (German)

Currently translated at 99.1% (3468 of 3497 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 99.1% (3466 of 3497 strings)

Translated using Weblate (Swedish)

Currently translated at 94.6% (53 of 56 strings)

Translated using Weblate (German)

Currently translated at 99.0% (3464 of 3497 strings)

Translated using Weblate (Portuguese)

Currently translated at 93.3% (14 of 15 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese)

Currently translated at 68.5% (591 of 862 strings)

Translated using Weblate (Portuguese)

Currently translated at 91.0% (184 of 202 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.9% (1885 of 3497 strings)

Translated using Weblate (Ukrainian)

Currently translated at 95.0% (192 of 202 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.5% (928 of 932 strings)

Co-authored-by: Adam Rimanek <adamrimanek.imsp@gmail.com>
Co-authored-by: Alison Alex <spamkari@hotmail.com>
Co-authored-by: Andrés Leiva Barco <andresleibar@gmail.com>
Co-authored-by: Artemis <circlegohard@gmail.com>
Co-authored-by: BMA <simpintis@gmail.com>
Co-authored-by: Begümay Çınar <begumay@proton.me>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Daisy Hsu <HsuD@stmaryscambridge.co.uk>
Co-authored-by: Deleted User <noreply+908@weblate.org>
Co-authored-by: Duggu Ghosh <duggu52d@gmail.com>
Co-authored-by: DumbDump <Schernova13@yandex.ru>
Co-authored-by: Fake Name <alpasalp220@gmail.com>
Co-authored-by: Gizem <gizem100296@gmail.com>
Co-authored-by: Harry Erickson <harry3rickson@gmail.com>
Co-authored-by: Jamie Herbert <pth23tpg@bangor.ac.uk>
Co-authored-by: Jan Freihöfer <jan.stauch.is@gmail.com>
Co-authored-by: Jeremia Hölscher <holscherjury@gmail.com>
Co-authored-by: Jildau Bras <jildaubras@gmail.com>
Co-authored-by: Julius Eikmans <jcs.e@icloud.com>
Co-authored-by: Lawan Fathullah <fathullahlawan@gmail.com>
Co-authored-by: Lizard <li07369427zard@gmail.com>
Co-authored-by: Lucas Vitor de Farias <Lucasvi90robloxbr@gmail.com>
Co-authored-by: Luidson Alejandro Froz Paiva <Luidpah1@gmail.com>
Co-authored-by: Léa <ambredf@outlook.fr>
Co-authored-by: Maria Morant <luisa.morant@yahoo.com>
Co-authored-by: Mausam <mausam_b@protonmail.com>
Co-authored-by: Mia S <miveliina@gmail.com>
Co-authored-by: Nora <yenyisun@gmail.com>
Co-authored-by: Prayaag Thakkar <prayaag13@outlook.com>
Co-authored-by: Rafael Hoyos Arango <rafa131842@gmail.com>
Co-authored-by: SY <smartyeti@proton.me>
Co-authored-by: Sebastian Ramorino <sebarc2005@gmail.com>
Co-authored-by: Sergey <sergejepihin323@gmail.com>
Co-authored-by: Serhii <serzh.photograf@gmail.com>
Co-authored-by: Sonia <sophishport@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Translatlantic Translation <translatlanticom@gmail.com>
Co-authored-by: Uwe B <hbtca@tunixgut.de>
Co-authored-by: Viktor Révész <rviktor@ivankapal.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yaezch <dkrovel@gmail.com>
Co-authored-by: Yorick Steffens <yorick.steffens@gmail.com>
Co-authored-by: Zarah Lundberg <sar_lun@hotmail.com>
Co-authored-by: donkie <lllllllovinllllll@gmail.com>
Co-authored-by: kim ham <kim@pfts.se>
Co-authored-by: viyu viyu <gnaremoob@binotuz.com>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Co-authored-by: ? <importantdata78@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/es/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/character/de/
Translate-URL: https://translate.habitica.com/projects/habitica/character/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/character/es/
Translate-URL: https://translate.habitica.com/projects/habitica/character/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/character/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/content/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/content/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/death/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/death/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/death/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/death/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/death/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/de/
Translate-URL: https://translate.habitica.com/projects/habitica/front/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/front/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/front/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/de/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/es/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/de/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/de/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/es/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/uk/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2026-03-05 11:27:29 +01:00
Hafiz 1047b0e03b up habitica-markdown -> 4.1.0 & defensive check 2026-03-02 14:32:58 -06:00
Kalista Payne 159f850bd1 fix(avatar): correct margin override in buy modal 2026-02-27 15:09:11 -06:00
Fiz 42083efb7e 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 72fc7fc20e.

* Revert "try upping github-action fix"

This reverts commit 70e48a57aa.

* fix(git): revert ci changes

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2026-02-26 16:36:37 -06:00
Fiz f21e800b0b 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
2026-02-26 16:02:47 -06:00
Kalista Payne 40122e5621 5.46.0 2026-02-26 12:08:13 -06:00
Kalista Payne 0ae19d9107 Squashed commit of the following:
commit 963b4133ec
Author: Kalista Payne <kalista@habitica.com>
Date:   Thu Feb 19 15:48:41 2026 -0600

    fix(text): clean up some gear descriptions

commit 53999a5b80
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Feb 4 17:18:07 2026 -0600

    fix(content): add seasonal set tokens

commit 4510c90e41
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Feb 4 17:10:24 2026 -0600

    feat(content): March-May 2026
2026-02-24 12:02:52 -06:00
Kalista Payne 68bfebcf30 5.45.0 2026-02-24 10:18:25 -06:00
Phillip Thelen 3e93911e70 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
2026-02-24 10:17:21 -06:00
Kalista Payne 4ea8636f03 fix(profile): correct stat display for class gear 2026-02-20 13:19:56 -06:00
Kalista Payne 9f97a09b8c fix(export): remove deprecated routes 2026-02-19 15:45:38 -06:00
Phillip Thelen eccc115b73 Admin Panel fixes (#15613)
* fix profile link to admin panel

* fix profile looking broken when no background is equipped
2026-02-19 11:11:25 -06:00
Tanmay Nalawade 2b26eb2bd1 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 <kalista@habitica.com>
2026-02-19 11:09:40 -06:00
Kalista Payne 8e042cabc4 5.44.3 2026-02-10 17:37:48 -06:00
Kalista Payne 8abe167848 chore(subproj): update habitica-images 2026-02-10 17:37:45 -06:00
Kalista Payne 3414f962e2 fix(lint): string style and whitespace 2026-02-10 17:13:02 -06:00
Kalista Payne 1b68e6d4d3 fix(event): show Spring Seasonal Shop on Valentines 2026-02-10 17:07:47 -06:00
Kalista Payne 5dd9711413 Update local dev MongoDB versions (#15334)
* chore(mongodb): update local dev MongoDB versions

* fix(github): update mongodb-github-action

* test(api): attempt to gather more detail on failures

* Revert "test(api): attempt to gather more detail on failures"

This reverts commit 215e768e90.

* WIP mongodb-memory-serve

* fix(mongo): start replica set

* run mongo as gh action service

* remove matrix for mongo

* try npm -> docker instead of services

* try "docker compose"

* disable mongo bootstrap from build

* try gh action again

* try newer action version

* working mongo docker compose 🎉

* fix(lint): leave out unused imports

* update lock

* cleanup previous workflow changes

* remove previous code, dont share mongo data folders on runtype (rs and docker)

* mongo docker for testing; align mongodb directory naming

* remove run-rs, add docker:aio script call, use healthcheck to initiate again

* merge docker-compose.yml, fix client port listening

* fix oudated healthcheck param

* chore(mongodb): update local dev MongoDB versions

* fix(github): update mongodb-github-action

* test(api): attempt to gather more detail on failures

* Revert "test(api): attempt to gather more detail on failures"

This reverts commit 215e768e90.

* WIP mongodb-memory-serve

* run mongo as gh action service

* fix(mongo): start replica set

* remove matrix for mongo

* try npm -> docker instead of services

* try "docker compose"

* disable mongo bootstrap from build

* try gh action again

* try newer action version

* working mongo docker compose 🎉

* fix(lint): leave out unused imports

* update lock

* cleanup previous workflow changes

* remove previous code, dont share mongo data folders on runtype (rs and docker)

* mongo docker for testing; align mongodb directory naming

* remove run-rs, add docker:aio script call, use healthcheck to initiate again

* merge docker-compose.yml, fix client port listening

* fix oudated healthcheck param

* fix(config): remove dup keys

* using npx vite during docker aio run

---------

Co-authored-by: negue <eugen.bolz@gmail.com>
2026-01-30 17:19:35 -06:00
Kalista Payne a542277a41 5.44.2 2026-01-26 11:46:03 -06:00
Phillip Thelen cdf8556fd6 Improve performance in production setup (#15594)
* build cached content files for mobile during gulp build

* load already cached content files during startup

* add option for mongoose to define minPoolSize

* cache client index.html for 10 minutes. Improves initial load times

* add option to auth to use lean version of user doc

* add a way to produce a heapdump from the command line

* fix lint
2026-01-26 11:45:44 -06:00
Kalista Payne 3d93390a7a fix(languages): update template characters 2026-01-23 13:09:14 -06:00
Kalista Payne 59f9cfa0f4 5.44.1 2026-01-23 13:03:30 -06:00
Kalista Payne 80d7804f69 fix(strings): use consistent template characters 2026-01-23 12:53:29 -06:00
Kalista Payne 4e5efe09a3 5.44.0 2026-01-22 11:53:20 -06:00
Fiz d42a597672 Merge pull request #15590 from HabitRPG/kalista/paypal-debug
PayPal debug
2026-01-21 16:11:41 -06:00
Phillip Thelen ea17b2e9c7 Rework how strings are localized (#15589)
* replace lodash template usage with micromustache

* remove function brackets from translations

* add newline

* remove old test

* split core translations from content translations

* fix directory not existing

* fix lint
2026-01-21 14:34:25 -06:00
Phillip Thelen f56708cd88 fix most customizations not being pinnable (#15578)
* fix most customizations not being pinnable

* set correct pinTypes

* fix(pinning): correct purchase types for base hair and mustaches

* automatically unpin purchased customizations

* ability to pin customization items

* Fix pin not showing on buy modal

* Pin on buy modal tweak

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
Co-authored-by: Hafiz <hafizbhamidi@gmail.com>
2026-01-21 13:57:42 -06:00
Kalista Payne 005d14f6e8 5.43.4 2026-01-20 14:36:36 -06:00
Weblate c05a96ce6c Translated using Weblate (Polish)
Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Portuguese)

Currently translated at 23.7% (60 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 23.7% (60 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 23.7% (60 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 90.5% (183 of 202 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.1% (113 of 114 strings)

Translated using Weblate (Portuguese)

Currently translated at 27.6% (70 of 253 strings)

Translated using Weblate (Portuguese)

Currently translated at 92.9% (106 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.8% (3318 of 3497 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.6% (143 of 145 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.8% (3317 of 3497 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 96.8% (245 of 253 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 94.0% (190 of 202 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Portuguese)

Currently translated at 50.1% (1753 of 3497 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Indonesian)

Currently translated at 97.1% (238 of 245 strings)

Translated using Weblate (Ukrainian)

Currently translated at 79.3% (150 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.5% (928 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Co-authored-by: Ashlynn <ashlynn.samuella@gmail.com>
Co-authored-by: Cezary Polakowski <arutha86@gmail.com>
Co-authored-by: Daniel Costa Carvalho <danielcostacarvalho@gmail.com>
Co-authored-by: Illana Beatriz Rocha de Oliveira <dev.illanabeatriz@gmail.com>
Co-authored-by: Lu dG <ludgs@outlook.fr>
Co-authored-by: Lyam Santos Peres <karinesanper@gmail.com>
Co-authored-by: Maria Morant <luisa.morant@yahoo.com>
Co-authored-by: Nina Łapaj <ninapaj@gmail.com>
Co-authored-by: Ri Vargas <goldenhaitang@gmail.com>
Co-authored-by: Sonia <sophishport@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/id/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/uk/
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Npc
Translation: Habitica/Tasks
2026-01-20 21:33:14 +01:00
Kalista Payne 8fdbfb9dc6 fix(lint): import order 2026-01-16 17:47:05 -06:00
Kalista Payne 057a642baa fix(logger): path 2026-01-16 17:43:54 -06:00
Kalista Payne 6c522157a7 feat(payments): log some anonymous data to chase a bug 2026-01-16 17:38:16 -06:00
Fiz ba9a1ab2a9 Merge pull request #15583 from HabitRPG/kalista/username-sanitize
Sanitize usernames come from social auth
2026-01-15 12:00:56 -06:00
Kalista Payne 4767461c4f 5.43.3 2026-01-14 14:19:54 -06:00
Weblate 847c97dc8f Translated using Weblate (Portuguese (Brazil))
Currently translated at 94.7% (3314 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.7% (3314 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Indonesian)

Currently translated at 93.0% (228 of 245 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Polish)

Currently translated at 98.9% (272 of 275 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Danish)

Currently translated at 88.1% (178 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (58 of 58 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Danish)

Currently translated at 66.5% (183 of 275 strings)

Translated using Weblate (German)

Currently translated at 98.9% (3461 of 3497 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Danish)

Currently translated at 92.5% (175 of 189 strings)

Translated using Weblate (Danish)

Currently translated at 9.0% (23 of 253 strings)

Translated using Weblate (German)

Currently translated at 99.0% (200 of 202 strings)

Translated using Weblate (German)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Danish)

Currently translated at 62.5% (172 of 275 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Japanese)

Currently translated at 96.5% (195 of 202 strings)

Translated using Weblate (Japanese)

Currently translated at 98.4% (249 of 253 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (German)

Currently translated at 98.3% (3440 of 3497 strings)

Translated using Weblate (French)

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (French)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.7% (3314 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (253 of 253 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (202 of 202 strings)

Co-authored-by: Ashlynn <ashlynn.samuella@gmail.com>
Co-authored-by: Douglas Meneghetti <douglasrizzom@gmail.com>
Co-authored-by: Gabriela <gabisouzars5@gmail.com>
Co-authored-by: Helene Bæck Christensen <helene.baeck@gmail.com>
Co-authored-by: Henrique Ferreira <pedroferreira217.ph@gmail.com>
Co-authored-by: Illana Beatriz Rocha de Oliveira <dev.illanabeatriz@gmail.com>
Co-authored-by: Isabela de Carvalho <isabela.c.escritora@gmail.com>
Co-authored-by: Joanna K <joanna.kociolek0@gmail.com>
Co-authored-by: Kenvinn <kevinsavio514@gmail.com>
Co-authored-by: Lucas Rafaldini <lucas.rafaldini@gmail.com>
Co-authored-by: Luã Fhelyp Guimarães <fhelypg@gmail.com>
Co-authored-by: Lyam Santos Peres <kaka1213spaenrteoss@gmail.com>
Co-authored-by: Lyam Santos Peres <karinesanper@gmail.com>
Co-authored-by: Maycon Douglas Silva Santos <maycondss@live.com>
Co-authored-by: Ri Vargas <goldenhaitang@gmail.com>
Co-authored-by: Rodrigo Gonçalves Braga <rgbraga@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Thiago Braga <thibraga06@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: purea <g@agaric.eu>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/de/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/id/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/character/da/
Translate-URL: https://translate.habitica.com/projects/habitica/character/de/
Translate-URL: https://translate.habitica.com/projects/habitica/character/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/id/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/death/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/da/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/front/da/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/id/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/da/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt_BR/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2026-01-14 21:18:58 +01:00
Kalista Payne 215b26acac fix(auth): run the actual verify check 2026-01-13 17:01:09 -06:00
Phillip Thelen e223e7821a add config to disable ssl and base_url enforcement (#15587) 2026-01-13 16:44:23 -06:00
Fiz 8134fa7c00 Chat optimization (#15545)
* fix(content): textual tweaks and updates

* fix(link): direct to FAQ instead of wiki

* fix(faq): correct Markdown

* Show orb of rebirth confirmation modal after use (window refresh)

* Set and check rebirth confirmation modal from localstorage

Set and check rebirth confirmation modal from localstorage after window reload

* Don't show orb of rebirth confirmation modal until page reloads

* message effective limit optimization

* Keep max limit for web (400 recent messages)

* Fix amount of messages initially being shown

* PM_PER_PAGE set to 50

* Increases number of messages in inbox test

* Increases number of messages for inbox pagination test

* Set and check rebirth confirmation modal from localstorage

Set and check rebirth confirmation modal from localstorage after window reload

* Don't show orb of rebirth confirmation modal until page reloads

* message effective limit optimization

* Keep max limit for web (400 recent messages)

* Add UUID validation for 'before' query parameter

* add party message stress test tool in admin panel

* lint

* add MAX_PM_COUNT of 400, admin tool for stress testing messages

* comment

* update stress test inbox message tool to use logged in user

* comment

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2026-01-13 14:43:09 -06:00
Kalista Payne 84208f612e 5.43.2 2026-01-09 10:43:36 -06:00
Hafiz 57e06334c0 Update allocation option label 2026-01-09 09:05:00 -06:00
Kalista Payne be695d25b3 5.43.1 2026-01-08 16:08:52 -06:00
Kalista Payne 77ee83f467 fix(privacy): ensure opt-in for analytics in modal 2026-01-08 15:40:09 -06:00
Kalista Payne 86556e346b 5.43.0 2026-01-08 14:22:27 -06:00
Weblate 2007a872c6 Merge branch 'origin/develop' into Weblate. 2026-01-08 21:20:01 +01:00
Weblate a8348038de Translated using Weblate (Italian)
Currently translated at 80.8% (2829 of 3497 strings)

Translated using Weblate (Italian)

Currently translated at 80.4% (2813 of 3497 strings)

Translated using Weblate (French)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Italian)

Currently translated at 79.5% (2781 of 3497 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Indonesian)

Currently translated at 96.4% (110 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 94.6% (3310 of 3497 strings)

Translated using Weblate (German)

Currently translated at 98.3% (3438 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (French)

Currently translated at 100.0% (202 of 202 strings)

Translated using Weblate (French)

Currently translated at 100.0% (145 of 145 strings)

Translated using Weblate (French)

Currently translated at 100.0% (245 of 245 strings)

Translated using Weblate (French)

Currently translated at 93.0% (188 of 202 strings)

Translated using Weblate (French)

Currently translated at 100.0% (114 of 114 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Italian)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.4% (3268 of 3497 strings)

Translated using Weblate (Korean)

Currently translated at 99.7% (930 of 932 strings)

Translated using Weblate (Korean)

Currently translated at 72.1% (622 of 862 strings)

Translated using Weblate (Korean)

Currently translated at 75.8% (326 of 430 strings)

Translated using Weblate (Korean)

Currently translated at 97.7% (129 of 132 strings)

Translated using Weblate (Korean)

Currently translated at 37.2% (92 of 247 strings)

Translated using Weblate (Korean)

Currently translated at 93.6% (104 of 111 strings)

Translated using Weblate (Korean)

Currently translated at 97.5% (909 of 932 strings)

Translated using Weblate (Korean)

Currently translated at 91.4% (86 of 94 strings)

Translated using Weblate (Korean)

Currently translated at 60.9% (173 of 284 strings)

Translated using Weblate (Korean)

Currently translated at 72.5% (312 of 430 strings)

Translated using Weblate (Korean)

Currently translated at 49.7% (1739 of 3497 strings)

Translated using Weblate (Korean)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Korean)

Currently translated at 63.7% (58 of 91 strings)

Translated using Weblate (Turkish)

Currently translated at 55.5% (160 of 288 strings)

Translated using Weblate (Turkish)

Currently translated at 34.8% (86 of 247 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 76.1% (188 of 247 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.7% (3243 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.6% (3240 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.4% (3233 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 72.7% (627 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.3% (3229 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 92.3% (3229 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.5% (3165 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 90.5% (3165 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.6% (284 of 288 strings)

Translated using Weblate (Korean)

Currently translated at 70.7% (610 of 862 strings)

Translated using Weblate (Korean)

Currently translated at 71.3% (307 of 430 strings)

Translated using Weblate (Korean)

Currently translated at 49.6% (1737 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 71.6% (618 of 862 strings)

Translated using Weblate (Romanian)

Currently translated at 56.5% (156 of 276 strings)

Translated using Weblate (Romanian)

Currently translated at 91.2% (104 of 114 strings)

Translated using Weblate (Romanian)

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Japanese)

Currently translated at 98.1% (3432 of 3497 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (114 of 114 strings)

Translated using Weblate (Turkish)

Currently translated at 63.9% (551 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 97.9% (3427 of 3497 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Japanese)

Currently translated at 97.8% (3423 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.2% (57 of 58 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 89.3% (218 of 244 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 77.2% (146 of 189 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 82.1% (708 of 862 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 96.7% (88 of 91 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 95.4% (106 of 111 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 71.6% (197 of 275 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Spanish)

Currently translated at 99.9% (3495 of 3497 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Turkish)

Currently translated at 87.3% (241 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 69.9% (2446 of 3497 strings)

Translated using Weblate (German)

Currently translated at 98.2% (3436 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 74.6% (206 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 74.6% (206 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 69.5% (192 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 65.9% (182 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (193 of 193 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Turkish)

Currently translated at 85.5% (797 of 932 strings)

Co-authored-by: Ashlynn <ashlynn.samuella@gmail.com>
Co-authored-by: Begümay Çınar <begumay@proton.me>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Douglas Meneghetti <douglasrizzom@gmail.com>
Co-authored-by: Frodo <Zatriel@outlook.com.br>
Co-authored-by: Israel Silva Araújo <yisrael.araujo@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: John B <habitica.quarters574@passmail.net>
Co-authored-by: Lucas Lo <lucas1020.rabbit@gmail.com>
Co-authored-by: Luizo <t.czj2019@gmail.com>
Co-authored-by: Lyam Santos Peres <kaka1213spaenrteoss@gmail.com>
Co-authored-by: Márcio Ramos Corrêa <marcio.ramos.correa@gmail.com>
Co-authored-by: Omar Bertolla <scaram@icloud.com>
Co-authored-by: Sefa Uğurlu <ugurlusefa2@gmail.com>
Co-authored-by: Seohee <ka011215@naver.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Trần Thu Trúc <ngw.nah@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: ‌ <e0gla0f@gmail.com>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Co-authored-by: 張鈞崴 <qoo94230@gmail.com>
Co-authored-by: 강아름 <sick_kar@naver.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/id/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/character/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/character/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/id/
Translate-URL: https://translate.habitica.com/projects/habitica/death/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/id/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/id/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ro/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2026-01-08 20:28:08 +01:00
Hafiz 87bcd69979 stat allocation wording 2026-01-08 12:35:31 -06:00
Hafiz 8f8e84d0c7 Stat allocation selection lowercase/capitalization 2026-01-08 11:51:21 -06:00
Hafiz 2c18cb00cc capitalization 2026-01-08 11:27:35 -06:00
Hafiz daa0fd18c0 Remove "points" from task allocation dropdown item title 2026-01-07 19:30:20 -06:00
Kalista Payne 5c555cbf88 fix(auth): revert attempted perf tweak 2026-01-07 17:52:33 -06:00
Kalista Payne 7379c7b230 fix(auth): handle potential collisions on username 2026-01-07 17:26:42 -06:00
Kalista Payne c055537c38 fix(lint): whitespace 2026-01-07 16:49:22 -06:00
Kalista Payne 7559feec8e Revert "fix(lint): whitespace; also revert username fix for further QA"
This reverts commit dcd15a58ebf41d9e44c89c675419ecb7d36b946d.
2026-01-07 16:49:22 -06:00
Kalista Payne 43808696a8 fix(lint): whitespace; also revert username fix for further QA 2026-01-07 16:49:22 -06:00
Kalista Payne 72fb41c7e0 fix(lint): missing variable declaration 2026-01-07 16:45:19 -06:00
Kalista Payne 3bf18e09ed fix(auth): strip invalid characters during social reg 2026-01-07 16:45:19 -06:00
Kalista Payne 407a901883 feat(faq): add detail on stats and allocation 2026-01-07 15:30:37 -06:00
Kalista Payne 81a008906b fix(lint): max-len 2026-01-07 14:36:51 -06:00
Kalista Payne 992a978923 Squashed commit of the following:
commit dcd15a58ebf41d9e44c89c675419ecb7d36b946d
Author: Kalista Payne <kalista@habitica.com>
Date:   Wed Jan 7 14:11:29 2026 -0600

    fix(lint): whitespace; also revert username fix for further QA

commit 8af109f3c358418a778d4dc8387524840343f046
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Jan 5 15:04:30 2026 -0600

    fix(footer): better hover looks

commit 87518d335967a90d3049cfde9160b341cbf55417
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 30 15:10:14 2025 -0600

    fix(footer): change to flex layout for better alignment

commit 45ab33c1afaa5b34c56fb09cdcd0f4c12fee0893
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 30 13:03:56 2025 -0600

    fix(lint): whitespace

commit c9d20e86d29d89b881813a2579ec1ba61acc82cc
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 30 12:54:04 2025 -0600

    fix(lint): unused var and whitespace

commit 05dc11fd11a530b0b460a951a8be32fe6f4e190e
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 30 12:48:26 2025 -0600

    fix(footer): rearrange social block, clean up unused donate css

commit 131cda6bb99b4eb53fdadf551bfddc3f712e81e8
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Dec 29 16:19:18 2025 -0600

    fix(login): move password flows into static wrapper

commit 563ed9fa02a34bbad53421795415e43c955813c1
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Dec 29 16:02:41 2025 -0600

    fix(login): resetPassword not reset-password route name

commit d23b64a03ec48b5634f05b4500153ed54a0ba6e7
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Dec 29 15:48:51 2025 -0600

    fix(login): add missing footer, remove dono button

commit 55e841252fbaafdccf75828b7ce87946c185792a
Author: Kalista Payne <kalista@habitica.com>
Date:   Fri Dec 19 15:58:52 2025 -0600

    fix(passwords): standardize styling for reset pass form

commit 7e84a54f104191d6e6d2e86f25070847e1c1c674
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 16 16:21:50 2025 -0600

    fix(lint): missing variable declaration

commit 777f8f13d7b3b889b5637102b8258b20020c6c6d
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 16 16:17:19 2025 -0600

    fix(auth): strip invalid characters during social reg
2026-01-07 14:13:47 -06:00
Fiz a8062ad615 Implement stat allocation & auto stat allocation (#15551)
* fix(responsive): adjustments for small screens by @Hafizzle

* Fixed cut off modal issues

- Fixed limited time banner being cut off on smaller devices
- Fixed Pin on purchase quest modal being cut off

* Update order of views on Stats tab

* Update styling of stat boxes & add stat allocation info

* Live stat allocation & auto allocation implemented

- Update stat allocation UI/UX cont.
- Update stats live (versus pressing save)
- Dynamically show the distribute dropdown based on if auto allocate switch is enabled

* Remove duplicate string value, convert line endings

* Fix auto allocation toggle alignment

* lint fixes

* Auto allocate UI fixes

- remove info icon in each dropdown option, and replace info below each option
- disable auto allocate dropdown instead of hiding it
- removed radio buttons drop auto allocate options dropdown
- Points available & auto allocate text share same baseline now

* Stat allocation updates

* Assigned Stat

* lint fixes

* stat feedback UI updates

* tweak

* stat allocation UI tweaks

refactor allocation mode selection with selectList component & other UI tweaks

* Fix hover dropdown padding & stat allocation chip always showing

* Update stat allocation dropdown styling

* Remove bold text on hover

* Stat allocation by task dropdown

* remove empty lines

* show selected attribute on component itself

* task stat selection UI updates

* fix(cr): remove weird line endings

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2026-01-06 16:08:34 -06:00
Fiz 781a904583 Replace browser confirmations with confirmation modals (#15544)
* Set and check rebirth confirmation modal from localstorage

Set and check rebirth confirmation modal from localstorage after window reload

* Don't show orb of rebirth confirmation modal until page reloads

* Confirmation prompts (replace browser confirmation prompts)

* lint

* lint

* Fix confirmation modal not showing for items w/gems

* Updated delete and confirmation modals

* lint

* Fix top bar on color on confirmation prompt clipping

* Show currency amount on purchase confirmation modals

* confirmation modals using existing components

* lint

* Broken task confirmation modal

* Replace class change confirmation with modal

* fix(style): correct indentation

* Replace warning svg w/ existing alert svg, and reset language strings

* fix(json): json doesn't like comma dangles

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
2026-01-06 15:19:30 -06:00
Kalista Payne d87946d912 5.42.4 2025-12-19 14:11:46 -06:00
Fiz 7456ff2def Fix g1g1 notification not staying dismissed (#15580)
- use localStorage instead of sessionStorage so dismissal persists across browser sessions
- Add .stop modifier to close button click
2025-12-19 13:57:35 -06:00
Kalista Payne e0af620b40 5.42.3 2025-12-18 12:24:15 -06:00
Weblate bb295551b5 Merge branch 'origin/develop' into Weblate. 2025-12-18 19:22:25 +01:00
Kalista Payne fce400f323 Squashed commit of the following:
commit 6d4accc43d20bb89b0496cec81e3b7d2a80c51b2
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 16 17:00:37 2025 -0600

    fix(front): add some spacing to account for removed pieces

commit 800d8c20c9705b39438866dca360d2d2bbbbf8de
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 16 16:21:50 2025 -0600

    fix(lint): missing variable declaration

commit c16ff32d49fc11cf1fc737c242522485ecf3ef5d
Author: Kalista Payne <kalista@habitica.com>
Date:   Mon Dec 15 17:21:00 2025 -0600

    fix(vue): markdownContainer TypeError

commit 5b4cf689b80c92e7163bcfd12b2c09f582d895f6
Author: Kalista Payne <kalista@habitica.com>
Date:   Tue Dec 16 15:07:43 2025 -0600

    fix(auth): debounce password reset request
2025-12-18 10:32:08 -06:00
Weblate c0ffb8b968 Translated using Weblate (Chinese (Traditional))
Currently translated at 85.8% (237 of 276 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 69.9% (2446 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 76.1% (188 of 247 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (3488 of 3497 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (3419 of 3497 strings)

Translated using Weblate (German)

Currently translated at 98.2% (3435 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 47.3% (117 of 247 strings)

Translated using Weblate (Italian)

Currently translated at 29.9% (74 of 247 strings)

Translated using Weblate (Polish)

Currently translated at 50.5% (1767 of 3497 strings)

Translated using Weblate (Polish)

Currently translated at 94.7% (273 of 288 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Turkish)

Currently translated at 79.0% (340 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 79.0% (340 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 84.0% (783 of 932 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Turkish)

Currently translated at 68.3% (294 of 430 strings)

Translated using Weblate (Turkish)

Currently translated at 80.7% (753 of 932 strings)

Translated using Weblate (Turkish)

Currently translated at 75.7% (706 of 932 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (3417 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 63.7% (176 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 32.7% (81 of 247 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Turkish)

Currently translated at 32.7% (81 of 247 strings)

Translated using Weblate (Turkish)

Currently translated at 75.2% (701 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Turkish)

Currently translated at 75.8% (69 of 91 strings)

Translated using Weblate (Turkish)

Currently translated at 73.1% (682 of 932 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Russian)

Currently translated at 98.6% (919 of 932 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (German)

Currently translated at 98.1% (3434 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 45.7% (113 of 247 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 94.7% (883 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 98.9% (272 of 275 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (3416 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Turkish)

Currently translated at 68.1% (62 of 91 strings)

Translated using Weblate (Spanish)

Currently translated at 99.0% (3463 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (244 of 244 strings)

Translated using Weblate (Turkish)

Currently translated at 92.0% (174 of 189 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Turkish)

Currently translated at 66.3% (618 of 932 strings)

Translated using Weblate (German)

Currently translated at 100.0% (862 of 862 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (German)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Turkish)

Currently translated at 50.0% (138 of 276 strings)

Translated using Weblate (Turkish)

Currently translated at 87.8% (166 of 189 strings)

Translated using Weblate (Turkish)

Currently translated at 32.7% (81 of 247 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (111 of 111 strings)

Translated using Weblate (Turkish)

Currently translated at 62.2% (580 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 97.4% (3409 of 3497 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (247 of 247 strings)

Translated using Weblate (German)

Currently translated at 98.9% (273 of 276 strings)

Translated using Weblate (German)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 82.9% (239 of 288 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 66.8% (2338 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 66.4% (2323 of 3497 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 82.0% (707 of 862 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 89.4% (834 of 932 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 81.9% (706 of 862 strings)

Translated using Weblate (Japanese)

Currently translated at 97.4% (3408 of 3497 strings)

Translated using Weblate (Italian)

Currently translated at 86.2% (163 of 189 strings)

Translated using Weblate (Italian)

Currently translated at 99.4% (927 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (275 of 276 strings)

Translated using Weblate (French)

Currently translated at 100.0% (276 of 276 strings)

Translated using Weblate (Japanese)

Currently translated at 97.0% (3395 of 3497 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Spanish)

Currently translated at 98.9% (3462 of 3497 strings)

Translated using Weblate (Japanese)

Currently translated at 97.0% (3395 of 3497 strings)

Translated using Weblate (German)

Currently translated at 98.1% (3432 of 3497 strings)

Translated using Weblate (German)

Currently translated at 98.0% (845 of 862 strings)

Translated using Weblate (Portuguese)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Portuguese)

Currently translated at 38.4% (95 of 247 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (German)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (German)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (932 of 932 strings)

Translated using Weblate (Japanese)

Currently translated at 96.6% (3380 of 3497 strings)

Translated using Weblate (Dutch)

Currently translated at 46.1% (114 of 247 strings)

Translated using Weblate (Dutch)

Currently translated at 95.6% (87 of 91 strings)

Translated using Weblate (French)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (French)

Currently translated at 100.0% (288 of 288 strings)

Translated using Weblate (French)

Currently translated at 100.0% (430 of 430 strings)

Translated using Weblate (French)

Currently translated at 100.0% (3497 of 3497 strings)

Translated using Weblate (Spanish)

Currently translated at 99.6% (274 of 275 strings)

Translated using Weblate (Spanish)

Currently translated at 98.1% (3431 of 3497 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (274 of 275 strings)

Co-authored-by: Aahzmandia <aahzmandia@gmail.com>
Co-authored-by: Ashlynn <ashlynn.samuella@gmail.com>
Co-authored-by: Azuthrax <azuthrax@gmail.com>
Co-authored-by: Begümay Çınar <begumay@proton.me>
Co-authored-by: Deleted User <noreply+908@weblate.org>
Co-authored-by: Eduardo Ariel Santos da Silva <are3380@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Jakub <jakubrymuza@gmail.com>
Co-authored-by: Jan Freihöfer <jan.stauch.is@gmail.com>
Co-authored-by: Kaakaa <misaka.hibiki@gmail.com>
Co-authored-by: Kubo Mizuki <m.kubo.0916@gmail.com>
Co-authored-by: Kwix26 <damiandamian2133@gmail.com>
Co-authored-by: Manuel <zappyccino@proton.me>
Co-authored-by: Michael <mikosanjp@gmail.com>
Co-authored-by: Riccardo DD <emvadraen@hotmail.com>
Co-authored-by: Sam Zhong <sam.zhongzishen@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: lhh <7012605@gmail.com>
Co-authored-by: nina van dijck laurijssen <n.vandijcklaurijssen@sintmartinusscholen.be>
Co-authored-by: いんこ <ayakabooker@gmail.com>
Co-authored-by: ? <importantdata78@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/id/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/death/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/death/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/death/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/front/it/
Translate-URL: https://translate.habitica.com/projects/habitica/front/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/es/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/de/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hant/
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Death
Translation: Habitica/Defaulttasks
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Overview
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2025-12-18 03:50:38 +01:00
Kalista Payne 72539f9ba3 5.42.2 2025-12-10 14:16:53 -06:00
Kalista Payne dabd466719 Revert "Chat optimization (#15545)"
This reverts commit 2917955ef0.
2025-12-10 14:16:48 -06:00
Kalista Payne 8bf2304330 chore(event): G1G1 date tweaks 2025-12-10 14:15:48 -06:00
Kalista Payne 6937dc4e4e fix(subscription): couple more layout tweaks 2025-12-08 16:37:04 -06:00
Fiz 2917955ef0 Chat optimization (#15545)
* fix(content): textual tweaks and updates

* fix(link): direct to FAQ instead of wiki

* fix(faq): correct Markdown

* Show orb of rebirth confirmation modal after use (window refresh)

* Set and check rebirth confirmation modal from localstorage

Set and check rebirth confirmation modal from localstorage after window reload

* Don't show orb of rebirth confirmation modal until page reloads

* message effective limit optimization

* Keep max limit for web (400 recent messages)

* Fix amount of messages initially being shown

* PM_PER_PAGE set to 50

* Increases number of messages in inbox test

* Increases number of messages for inbox pagination test

* Set and check rebirth confirmation modal from localstorage

Set and check rebirth confirmation modal from localstorage after window reload

* Don't show orb of rebirth confirmation modal until page reloads

* message effective limit optimization

* Keep max limit for web (400 recent messages)

* Add UUID validation for 'before' query parameter

* add party message stress test tool in admin panel

* lint

* add MAX_PM_COUNT of 400, admin tool for stress testing messages

* comment

* update stress test inbox message tool to use logged in user

* comment

---------

Co-authored-by: Kalista Payne <kalista@habitica.com>
2025-12-05 16:12:23 -06:00
Kalista Payne 55d13e44d4 fix(subs): strings and alignments 2025-12-03 17:12:08 -06:00
Kalista Payne 90096f995f 5.42.1 2025-12-03 16:42:22 -06:00
Kalista Payne 5c74c2b914 fix(typo): gooals 2025-12-03 15:36:04 -06:00
506 changed files with 17574 additions and 7369 deletions
+18 -8
View File
@@ -82,7 +82,7 @@ jobs:
CI: true
NODE_ENV: test
- run: npm run test:sanity
common:
runs-on: ubuntu-latest
strategy:
@@ -129,13 +129,13 @@ jobs:
CI: true
NODE_ENV: test
- run: npm run test:content
api-unit:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
mongodb-version: [4.2]
mongodb-version: [7.0]
steps:
- uses: actions/checkout@v4
with:
@@ -144,11 +144,13 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
uses: supercharge/mongodb-github-action@1.3.0
uses: supercharge/mongodb-github-action@1.11.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
@@ -158,15 +160,17 @@ jobs:
env:
CI: true
NODE_ENV: test
- run: npm run test:api:unit
env:
REQUIRES_SERVER=true: true
api-v3-integration:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
mongodb-version: [4.2]
mongodb-version: [7.0]
steps:
- uses: actions/checkout@v4
with:
@@ -176,10 +180,11 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
uses: supercharge/mongodb-github-action@1.3.0
uses: supercharge/mongodb-github-action@1.11.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
@@ -189,15 +194,18 @@ jobs:
env:
CI: true
NODE_ENV: test
- run: npm run test:api-v3:integration
env:
REQUIRES_SERVER=true: true
api-v4-integration:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21.x]
mongodb-version: [4.2]
mongodb-version: [7.0]
steps:
- uses: actions/checkout@v4
with:
@@ -207,10 +215,11 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- name: Start MongoDB ${{ matrix.mongodb-version }} Replica Set
uses: supercharge/mongodb-github-action@1.3.0
uses: supercharge/mongodb-github-action@1.11.0
with:
mongodb-version: ${{ matrix.mongodb-version }}
mongodb-replica-set: rs
- run: sudo apt update
- run: sudo apt -y install libkrb5-dev
- run: cp config.json.example config.json
@@ -220,6 +229,7 @@ jobs:
env:
CI: true
NODE_ENV: test
- run: npm run test:api-v4:integration
env:
REQUIRES_SERVER=true: true
+1 -1
View File
@@ -47,5 +47,5 @@ webpack.webstorm.config
# mongodb replica set for local dev
mongodb-*.tgz
/mongodb-data*
/mongodb-*
/.nyc_output
+2 -3
View File
@@ -46,7 +46,7 @@
"MAINTENANCE_MODE": "false",
"MONGODB_POOL_SIZE": "10",
"MONGODB_SOCKET_TIMEOUT": "20000",
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs",
"NODE_DB_URI": "mongodb://localhost:27017/habitica-dev?replicaSet=rs&directConnection=true&readPreference=secondary",
"NODE_ENV": "development",
"PATH": "bin:node_modules/.bin:/usr/local/bin:/usr/bin:/bin",
"PAYPAL_BILLING_PLANS_basic_12mo": "basic_12mo",
@@ -75,7 +75,6 @@
"S3_ACCESS_KEY_ID": "accessKeyId",
"S3_BUCKET": "bucket",
"S3_SECRET_ACCESS_KEY": "secretAccessKey",
"SESSION_SECRET_IV": "12345678912345678912345678912345",
"SESSION_SECRET_KEY": "1234567891234567891234567891234567891234567891234567891234567891",
"SESSION_SECRET": "YOUR SECRET HERE",
"SITE_HTTP_AUTH_ENABLED": "false",
@@ -90,7 +89,7 @@
"STRIPE_API_KEY": "aaaabbbbccccddddeeeeffff00001111",
"STRIPE_PUB_KEY": "22223333444455556666777788889999",
"STRIPE_WEBHOOKS_ENDPOINT_SECRET": "111111",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs",
"TEST_DB_URI": "mongodb://localhost:27017/habitica-test?replicaSet=rs&directConnection=true&readPreference=secondary",
"TIME_TRAVEL_ENABLED": "false",
"TRUSTED_DOMAINS": "localhost,https://habitica.com",
"WEB_CONCURRENCY": 1
-53
View File
@@ -1,53 +0,0 @@
services:
client:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "run", "client:dev"]
depends_on:
- server
environment:
- BASE_URL=http://server:3000
networks:
- habitica
ports:
- "8080:8080"
volumes:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
- /usr/src/habitica/website/client/node_modules
server:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "start"]
depends_on:
mongo:
condition: service_healthy
environment:
- NODE_DB_URI=mongodb://mongo/habitrpg
networks:
- habitica
ports:
- "3000:3000"
volumes:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
mongo:
image: mongo:5.0.23
restart: unless-stopped
command: ["--replSet", "rs", "--bind_ip_all", "--port", "27017"]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
start_interval: 1s
retries: 30
networks:
- habitica
ports:
- "27017:27017"
networks:
habitica:
driver: bridge
+23
View File
@@ -0,0 +1,23 @@
networks:
mongodb-network:
name: "mongodb-network"
driver: bridge
services:
mongodb:
image: "mongo:7.0"
container_name: "habitica-mongodb-only"
networks:
- mongodb-network
hostname: "mongodb"
ports:
- "27017:27017"
restart: "unless-stopped"
volumes:
- "./mongodb-data-docker:/data/db"
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
retries: 30
+23
View File
@@ -0,0 +1,23 @@
networks:
mongodb-network:
name: "mongodb-network"
driver: bridge
services:
mongodb:
image: "mongo:7.0"
container_name: "habitica-mongodb-test"
networks:
- mongodb-network
hostname: "mongodb"
ports:
- "27017:27017"
restart: "unless-stopped"
volumes:
- "./mongodb-data-docker-testing:/data/db"
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
retries: 30
+43 -22
View File
@@ -1,35 +1,56 @@
version: "3"
services:
client:
build: .
networks:
- habitica
environment:
- BASE_URL=http://server:3000
ports:
- "8080:8080"
command: ["npm", "run", "client:dev"]
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "run", "client:dev:docker"]
depends_on:
- server
server:
build: .
ports:
- "3000:3000"
environment:
- BASE_URL=http://server:3000
networks:
- habitica
ports:
- "5173:5173"
volumes:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
- /usr/src/habitica/website/client/node_modules
server:
build:
context: .
dockerfile: ./Dockerfile-Dev
command: ["npm", "start"]
depends_on:
mongo:
condition: service_healthy
environment:
- NODE_DB_URI=mongodb://mongo/habitrpg
depends_on:
- mongo
mongo:
image: mongo:3.6
ports:
- "27017:27017"
networks:
- habitica
ports:
- "3000:3000"
volumes:
- .:/usr/src/habitica
- /usr/src/habitica/node_modules
mongo:
image: "mongo:7.0"
container_name: "habitica-mongodb"
networks:
- habitica
hostname: "mongodb"
ports:
- "27017:27017"
restart: "unless-stopped"
volumes:
- "./mongodb-data-docker:/data/db"
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs" ]
healthcheck:
test: echo "try { rs.status() } catch (err) { rs.initiate() }" | mongosh --port 27017 --quiet
interval: 10s
timeout: 30s
start_period: 0s
retries: 30
networks:
habitica:
+12 -9
View File
@@ -5,7 +5,7 @@ import path from 'path';
import babel from 'gulp-babel';
import os from 'os';
import fs from 'fs';
import spawn from 'cross-spawn'; // eslint-disable-line import/no-extraneous-dependencies
import spawn from 'cross-spawn';
import clean from 'rimraf';
gulp.task('build:babel:server', () => gulp.src('website/server/**/*.js')
@@ -35,7 +35,7 @@ gulp.task('build:prod', gulp.series(
// When used on windows `run-rs` must first be run without the `--keep` option
// in order to be setup correctly, afterwards it can be used.
const MONGO_PATH = path.join(__dirname, '/../mongodb-data/');
const MONGO_PATH = path.join(__dirname, '/../mongodb-data-docker/');
gulp.task('build:prepare-mongo', async () => {
if (fs.existsSync(MONGO_PATH)) {
@@ -51,29 +51,32 @@ gulp.task('build:prepare-mongo', async () => {
console.log('MongoDB data folder is missing, setting up.'); // eslint-disable-line no-console
// use run-rs without --keep, kill it as soon as the replica set starts
const runRsProcess = spawn('run-rs', ['-v', '4.1.1', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']);
const dockerMongoProcess = spawn('npm', ['run', 'docker:mongo:dev']);
for await (const chunk of runRsProcess.stdout) {
let manuallyStopped = false;
for await (const chunk of dockerMongoProcess.stdout) {
const stringChunk = chunk.toString();
console.log(stringChunk); // eslint-disable-line no-console
// kills the process after the replica set is setup
if (stringChunk.includes('Started replica set')) {
if (stringChunk.includes('mongod startup complete')) {
console.log('MongoDB setup correctly.'); // eslint-disable-line no-console
runRsProcess.kill();
dockerMongoProcess.kill();
manuallyStopped = true;
}
}
let error = '';
for await (const chunk of runRsProcess.stderr) {
for await (const chunk of dockerMongoProcess.stderr) {
const stringChunk = chunk.toString();
error += stringChunk;
}
const exitCode = await new Promise(resolve => {
runRsProcess.on('close', resolve);
dockerMongoProcess.on('close', resolve);
});
if (exitCode || error.length > 0) {
if (!manuallyStopped && (exitCode || error.length > 0)) {
// remove any leftover files
clean.sync(MONGO_PATH);
+61 -26
View File
@@ -6,9 +6,21 @@ gulp.task('cache:content', done => {
// Requiring at runtime because these files access `common`
// code which in production works only if transpiled so after
// gulp build:babel:common has run
const { CONTENT_CACHE_PATH, getLocalizedContentResponse } = require('../website/server/libs/content'); // eslint-disable-line global-require
const {
CONTENT_CACHE_PATH,
getLocalizedContentResponse,
IOS_FILTER,
ANDROID_FILTER,
buildFilterObject,
hashForFilter,
} = require('../website/server/libs/content'); // eslint-disable-line global-require
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
const iosHash = hashForFilter(IOS_FILTER);
const iosFilterObj = buildFilterObject(IOS_FILTER);
const androidHash = hashForFilter(ANDROID_FILTER);
const androidFilterObj = buildFilterObject(ANDROID_FILTER);
try {
// create the cache folder (if it doesn't exist)
try {
@@ -26,33 +38,56 @@ gulp.task('cache:content', done => {
getLocalizedContentResponse(langCode),
'utf8',
);
});
done();
} catch (err) {
done(err);
}
});
gulp.task('cache:i18n', done => {
// Requiring at runtime because these files access `common`
// code which in production works only if transpiled so after
// gulp build:babel:common has run
const { BROWSER_SCRIPT_CACHE_PATH, geti18nBrowserScript } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
try {
// create the cache folder (if it doesn't exist)
try {
fs.mkdirSync(BROWSER_SCRIPT_CACHE_PATH);
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}
// create and save the i18n browser script for each language
langCodes.forEach(languageCode => {
fs.writeFileSync(
`${BROWSER_SCRIPT_CACHE_PATH}${languageCode}.js`,
geti18nBrowserScript(languageCode),
`${CONTENT_CACHE_PATH}${langCode}${iosHash}.json`,
getLocalizedContentResponse(langCode, iosFilterObj),
'utf8',
);
fs.writeFileSync(
`${CONTENT_CACHE_PATH}${langCode}${androidHash}.json`,
getLocalizedContentResponse(langCode, androidFilterObj),
'utf8',
);
});
done();
} catch (err) {
done(err);
}
});
function safeMkdir (path) {
try {
fs.mkdirSync(path);
} catch (err) {
if (err.code !== 'EEXIST') throw err;
}
}
gulp.task('cache:i18n', done => {
// Requiring at runtime because these files access `common`
// code which in production works only if transpiled so after
// gulp build:babel:common has run
const { BROWSER_SCRIPT_CACHE_PATH, geti18nCoreBrowserScript, geti18nContentBrowserScript } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
const { langCodes } = require('../website/server/libs/i18n'); // eslint-disable-line global-require
try {
// create the cache folders (if they doesn't exist)
safeMkdir(BROWSER_SCRIPT_CACHE_PATH);
safeMkdir(`${BROWSER_SCRIPT_CACHE_PATH}core/`);
safeMkdir(`${BROWSER_SCRIPT_CACHE_PATH}content/`);
// create and save the i18n browser script for each language
langCodes.forEach(languageCode => {
fs.writeFileSync(
`${BROWSER_SCRIPT_CACHE_PATH}core/${languageCode}.js`,
geti18nCoreBrowserScript(languageCode),
'utf8',
);
fs.writeFileSync(
`${BROWSER_SCRIPT_CACHE_PATH}content/${languageCode}.js`,
geti18nContentBrowserScript(languageCode),
'utf8',
);
});
+5
View File
@@ -53,6 +53,11 @@ gulp.task('test:prepare:mongo', cb => {
const mongooseOptions = getDefaultConnectionOptions();
const connectionUrl = getDevelopmentConnectionUrl(TEST_DB_URI);
console.info({
mongooseOptions,
connectionUrl,
});
mongoose.connect(connectionUrl, mongooseOptions)
.then(() => mongoose.connection.dropDatabase())
.then(() => mongoose.connection.close()).then(() => {
+565 -684
View File
File diff suppressed because it is too large Load Diff
+11 -6
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.42.0",
"version": "5.46.3",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -39,7 +39,8 @@
"gulp-filter": "^7.0.0",
"gulp-imagemin": "^7.1.0",
"gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0",
"habitica-markdown": "^4.1.0",
"heapdump": "^0.3.15",
"helmet": "^4.6.0",
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0",
@@ -48,10 +49,12 @@
"lodash": "^4.17.21",
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",
"micromustache": "^8.0.3",
"moment": "^2.29.4",
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
"mongoose": "^8.9.5",
"morgan": "^1.10.1",
"nan": "^2.25.0",
"nconf": "^0.12.1",
"node-gcm": "^1.0.5",
"on-headers": "^1.1.0",
@@ -100,13 +103,16 @@
"coverage": "nyc report --reporter=html --report-dir coverage/results; open coverage/results/index.html",
"sprites": "gulp sprites:compile",
"client:dev": "cd website/client && npm run serve",
"client:dev:docker": "cd website/client && npm run serve:docker",
"client:build": "cd website/client && npm run build",
"client:unit": "cd website/client && npm run test:unit",
"start": "node --watch ./website/server/index.js",
"start:simple": "node ./website/server/index.js",
"debug": "node --watch --inspect ./website/server/index.js",
"mongo:dev": "run-rs -v 7.0.23 -l ubuntu2214 --keep --dbpath mongodb-data --number 1 --quiet",
"mongo:test": "run-rs -v 7.0.23 -l ubuntu2214 --keep --dbpath mongodb-data-testing --number 1 --quiet",
"docker:aio": "docker compose -f docker-compose.yml up",
"docker:mongo:dev": "docker compose -f docker-compose.mongo-only.yml up",
"docker:mongo:dev:down": "docker compose -f docker-compose.mongo-only.yml down",
"docker:mongo:test": "docker compose -f docker-compose.mongo-test-local.yml up",
"mongo:test": "node scripts/start-local-mongo.mjs --test-db",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc",
"heroku-postbuild": ".heroku/report_deploy.sh"
@@ -122,7 +128,6 @@
"monk": "^7.3.4",
"nyc": "^15.1.0",
"require-again": "^2.0.0",
"run-rs": "^0.7.7",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
}
@@ -50,5 +50,59 @@ describe('UserNotification Model', () => {
expect(safeNotifications[0].type).to.equal('NEW_CHAT_MESSAGE');
expect(safeNotifications[0].id).to.equal('123');
});
it('removes duplicate STREAK_ACHIEVEMENT notifications', () => {
// Fixes issue #13325 - Users receiving duplicate streak achievement notifications
const notifications = [
new UserNotification({
type: 'STREAK_ACHIEVEMENT',
id: 123,
data: {},
}),
new UserNotification({
type: 'STREAK_ACHIEVEMENT',
id: 456,
data: {},
}),
new UserNotification({
type: 'CRON',
id: 789,
data: {},
}), // different type, should be kept
];
const safeNotifications = UserNotification.cleanupCorruptData(notifications);
expect(safeNotifications.length).to.equal(2);
expect(safeNotifications[0].type).to.equal('STREAK_ACHIEVEMENT');
expect(safeNotifications[0].id).to.equal('123');
expect(safeNotifications[1].type).to.equal('CRON');
expect(safeNotifications[1].id).to.equal('789');
});
it('handles multiple STREAK_ACHIEVEMENT duplicates correctly', () => {
// Test case: 3 duplicate STREAK_ACHIEVEMENT notifications
const notifications = [
new UserNotification({
type: 'STREAK_ACHIEVEMENT',
id: 111,
data: {},
}),
new UserNotification({
type: 'STREAK_ACHIEVEMENT',
id: 222,
data: {},
}),
new UserNotification({
type: 'STREAK_ACHIEVEMENT',
id: 333,
data: {},
}),
];
const safeNotifications = UserNotification.cleanupCorruptData(notifications);
expect(safeNotifications.length).to.equal(1);
expect(safeNotifications[0].type).to.equal('STREAK_ACHIEVEMENT');
expect(safeNotifications[0].id).to.equal('111'); // Keep first one
});
});
});
@@ -1,35 +0,0 @@
import { v4 as generateUUID } from 'uuid';
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
xdescribe('GET /export/avatar-:memberId.html', () => {
let user;
before(async () => {
user = await generateUser();
});
it('validates req.params.memberId', async () => {
await expect(user.get('/export/avatar-:memberId.html')).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('handles non-existing members', async () => {
const dummyId = generateUUID();
await expect(user.get(`/export/avatar-${dummyId}.html`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', { userId: dummyId }),
});
});
it('returns an html page', async () => {
const res = await user.get(`/export/avatar-${user._id}.html`);
expect(res.substring(0, 100).indexOf('<!DOCTYPE html>')).to.equal(0);
});
});
@@ -1,3 +0,0 @@
// TODO how to test this route since it points to a file on AWS s3?
describe('GET /export/avatar-:memberId.png', () => {});
@@ -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('<h1>Hello!</h1>');
expect(res).to.include('<li>list 1</li>');
});
@@ -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('<h1>Hello!</h1>');
const listPosition = res.indexOf('<li>list 1</li>');
@@ -47,7 +47,7 @@ describe('GET /inbox/messages', () => {
it('returns four messages when using page-query ', async () => {
const promises = [];
for (let i = 0; i < 10; i += 1) {
for (let i = 0; i < 50; i += 1) {
promises.push(user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
@@ -65,6 +65,52 @@ describe('POST /user/auth/social', () => {
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
});
it('includes sanitized version of provided username', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
username: 'Google User Name',
});
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.equal('GoogleUserName');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.equal('googleusername');
});
it('generates a random username if provided username contains only disallowed characters', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
username: 'Áîüè',
});
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-');
});
it('generates a random username if provided username contains a disallowed word', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
username: 'i am a TESTPLACEHOLDERSLURWORDHERE',
});
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-');
});
it('generates a random username if sanitized username conflicts with an extant user', async () => {
user = await generateUser({ 'auth.local.username': 'GoogleUserName' });
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
username: 'Google User Name',
});
await expect(getProperty('users', response.id, 'auth.local.username')).to.eventually.contain('hb-');
await expect(getProperty('users', response.id, 'auth.local.lowerCaseUsername')).to.eventually.contain('hb-');
});
it('fails if allowRegister is false and user does not exist', async () => {
await expect(api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -66,7 +66,7 @@ describe('GET /inbox/conversations', () => {
it('returns five messages when using page-query ', async () => {
const promises = [];
for (let i = 0; i < 10; i += 1) {
for (let i = 0; i < 50; i += 1) {
promises.push(user.post('/members/send-private-message', {
toUserId: user.id,
message: 'fourth',
+67
View File
@@ -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('<img class="habitica-emoji"');
expect(result).to.include('src="https://s3.amazonaws.com/habitica-assets/cdn/emoji/melior.png"');
expect(result).to.include('alt="melior"');
});
it('does NOT convert emoji inside markdown links', () => {
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('<b>bold</b> :smile:');
expect(result).to.include('<b>bold</b>');
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');
});
});
+17 -8
View File
@@ -211,22 +211,32 @@ describe('shared.ops.rebirth', () => {
expect(user.achievements.rebirthLevel).to.equal(2);
});
it('does not increment rebirth achievements when level is lower than previous', async () => {
it('increments rebirth achievements even when level is lower than previous', async () => {
user.stats.lvl = 2;
user.achievements.rebirths = 1;
user.achievements.rebirthLevel = 3;
await rebirth(user);
expect(user.achievements.rebirths).to.equal(1);
expect(user.achievements.rebirths).to.equal(2);
expect(user.achievements.rebirthLevel).to.equal(3);
});
it('always increments rebirth achievements when level is MAX_LEVEL', async () => {
it('updates rebirthLevel when current level is higher than previous', async () => {
user.stats.lvl = 5;
user.achievements.rebirths = 1;
user.achievements.rebirthLevel = 3;
await rebirth(user);
expect(user.achievements.rebirths).to.equal(2);
expect(user.achievements.rebirthLevel).to.equal(5);
});
it('increments rebirth achievements when level is MAX_LEVEL', async () => {
user.stats.lvl = MAX_LEVEL;
user.achievements.rebirths = 1;
// this value is not actually possible (actually capped at MAX_LEVEL) but makes a good test
user.achievements.rebirthLevel = MAX_LEVEL + 1;
user.achievements.rebirthLevel = MAX_LEVEL;
await rebirth(user);
@@ -234,11 +244,10 @@ describe('shared.ops.rebirth', () => {
expect(user.achievements.rebirthLevel).to.equal(MAX_LEVEL);
});
it('always increments rebirth achievements when level is greater than MAX_LEVEL', async () => {
it('increments rebirth achievements when level is greater than MAX_LEVEL', async () => {
user.stats.lvl = MAX_LEVEL + 1;
user.achievements.rebirths = 1;
// this value is not actually possible (actually capped at MAX_LEVEL) but makes a good test
user.achievements.rebirthLevel = MAX_LEVEL + 2;
user.achievements.rebirthLevel = MAX_LEVEL;
await rebirth(user);
+9 -1
View File
@@ -20,6 +20,9 @@ describe('shared.ops.unlock', () => {
beforeEach(() => {
user = generateUser();
user.balance = usersStartingGems;
user.pinnedItems.push({ type: 'background', path: 'backgrounds.backgrounds042016.giant_florals' });
user.pinnedItems.push({ type: 'haircolor', path: 'hair.color.rainbow' });
user.pinnedItems.push({ type: 'shirt', path: 'shirt.convict' });
clock = sandbox.useFakeTimers(new Date('2024-04-10'));
});
@@ -272,6 +275,7 @@ describe('shared.ops.unlock', () => {
});
it('unlocks an item (appearance)', async () => {
expect(user.pinnedItems.findIndex(item => item.type === 'shirt')).to.not.equal(-1);
const path = unlockPath.split(',')[0];
const initialShirts = Object.keys(user.purchased.shirt).length;
const [, message] = await unlock(user, { query: { path } });
@@ -282,11 +286,12 @@ describe('shared.ops.unlock', () => {
);
expect(get(user.purchased, path)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 0.5);
expect(user.pinnedItems.findIndex(item => item.type === 'shirt')).to.equal(-1);
});
it('unlocks an item (hair color)', async () => {
user.purchased.hair.color = {};
expect(user.pinnedItems.findIndex(item => item.type === 'haircolor')).to.not.equal(-1);
const path = hairUnlockPath.split(',')[0];
const initialColorHair = Object.keys(user.purchased.hair.color).length;
const [, message] = await unlock(user, { query: { path } });
@@ -297,6 +302,7 @@ describe('shared.ops.unlock', () => {
);
expect(get(user.purchased, path)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 0.5);
expect(user.pinnedItems.findIndex(item => item.type === 'haircolor')).to.equal(-1);
});
it('unlocks an item (facial hair)', async () => {
@@ -334,6 +340,7 @@ describe('shared.ops.unlock', () => {
it('unlocks an item (background)', async () => {
const initialBackgrounds = Object.keys(user.purchased.background).length;
expect(user.pinnedItems.findIndex(item => item.type === 'background')).to.not.equal(-1);
const [, message] = await unlock(user, {
query: { path: backgroundUnlockPath },
});
@@ -344,6 +351,7 @@ describe('shared.ops.unlock', () => {
);
expect(get(user.purchased, backgroundUnlockPath)).to.be.true;
expect(user.balance).to.equal(usersStartingGems - 1.75);
expect(user.pinnedItems.findIndex(item => item.type === 'background')).to.equal(-1);
});
it('handles an invalid hair path gracefully', async () => {
+1 -6
View File
@@ -1,12 +1,7 @@
import { STRING_ERROR_MSG, STRING_DOES_NOT_EXIST_MSG } from '../helpers/content.helper';
import { STRING_DOES_NOT_EXIST_MSG } from '../helpers/content.helper';
import translator from '../../website/common/script/content/translation';
describe('Translator', () => {
it('returns error message if string is not properly formatted', () => {
const improperlyFormattedString = translator('petName', { attr: 0 })();
expect(improperlyFormattedString).to.match(STRING_ERROR_MSG);
});
it('returns an error message if string does not exist', () => {
const stringDoesNotExist = translator('stringDoesNotExist')();
expect(stringDoesNotExist).to.match(STRING_DOES_NOT_EXIST_MSG);
+2 -2
View File
@@ -1,8 +1,8 @@
import i18n from '../../website/common/script/i18n';
import './globals.helper';
import { translations } from '../../website/server/libs/i18n';
import { contentTranslations } from '../../website/server/libs/i18n';
i18n.translations = translations;
i18n.translations = contentTranslations;
export const STRING_ERROR_MSG = /^Error processing the string ".*". Please see Help > Report a Bug.$/;
export const STRING_DOES_NOT_EXIST_MSG = /^String '.*' not found.$/;
+1
View File
@@ -21,6 +21,7 @@ export async function getProperty (collectionName, id, path) {
// Specifically helpful for the GET /groups tests,
// resets the db to an empty state and creates a tavern document
export async function resetHabiticaDB () {
console.info('Resetting Habitica DB');
const groups = mongoose.connection.db.collection('groups');
const users = mongoose.connection.db.collection('users');
return mongoose.connection.dropDatabase()
+1 -1
View File
@@ -32,6 +32,6 @@
<script type="text/javascript" src="//cloudfront.loggly.com/js/loggly.tracker-latest.min.js" async></script>
<!-- Translations -->
<script type='text/javascript' src='/api/v4/i18n/browser-script' vite-ignore></script>
<script type='text/javascript' src='/api/v4/i18n/core' vite-ignore></script>
</body>
</html>
+4979 -2777
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -4,6 +4,7 @@
"private": true,
"scripts": {
"serve": "vite",
"serve:docker": "npx vite --host 0.0.0.0",
"build": "vite build",
"preview": "vite preview",
"test:unit": "vitest run",
@@ -27,12 +28,13 @@
"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",
"lodash": "^4.17.21",
"markdown-it": "^14.0.0",
"micromustache": "^8.0.3",
"moment": "^2.29.4",
"nconf": "^0.12.1",
"sass": "^1.63.4",
+5
View File
@@ -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;
@@ -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;
Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

@@ -58,6 +58,11 @@ h3.markdown {
img {
max-width: 100%;
}
.emoji-native {
font-size: 0.85em;
vertical-align: middle;
}
blockquote {
padding: 0 16px;
@@ -0,0 +1,5 @@
<svg width="330" height="80" viewBox="0 0 330 80" preserveAspectRatio="none" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M159.797 60.5502C165.534 61.8466 171.631 62.7536 178.221 63.1767C208.94 65.1492 233.733 56.6838 260.68 47.483C282.33 40.091 305.37 32.2243 333.99 28.9144L336 16.3018C260.81 7.08865 233.373 23.1672 205.362 39.5825C192.037 47.3908 178.583 55.2753 159.797 60.5502Z" fill="#BDA8FF"/>
<path d="M0 80L331.948 79.9998V29.1594C268.976 36.9871 233.03 66.6959 178.221 63.1767C112.951 58.9858 95.9516 7.31934 0.000104656 0L0 80Z" fill="#925CF3"/>
<path d="M203.54 40.6496C166.339 36.8525 141.531 39.6251 122.334 45.4666C133.94 51.8989 145.792 57.3851 159.797 60.5502C177.727 55.5155 190.801 48.1036 203.54 40.6496Z" fill="#D5C8FF"/>
</svg>

After

Width:  |  Height:  |  Size: 803 B

@@ -1,53 +1,228 @@
<template>
<b-modal
id="rebirth"
:title="$t('modalAchievement')"
size="md"
:hide-footer="true"
size="sm"
:hide-header="true"
>
<div class="modal-body">
<div class="col-12">
<!-- @TODO: +achievementAvatar('sun',0)--><achievement-avatar class="avatar" />
</div><div class="col-6 offset-3 text-center">
<div v-if="user.achievements.rebirthLevel < 100">
{{ $t('rebirthAchievement', {
number: user.achievements.rebirths,
level: user.achievements.rebirthLevel}) }}
</div><div v-if="user.achievements.rebirthLevel >= 100">
{{ $t('rebirthAchievement100', {number: user.achievements.rebirths}) }}
</div><br><button
class="btn btn-primary"
@click="close()"
>
{{ $t('huzzah') }}
</button>
<div
class="close-x"
@click.stop="close()"
>
<div
class="svg-icon svg-close"
v-html="icons.close"
></div>
</div>
<div class="content text-center">
<h2
v-once
class="header"
>
{{ $t('rebirthNewAchievement') }}
</h2>
<div class="d-flex align-items-center justify-content-center icon-area">
<div
v-once
class="svg-icon sparkles mirror"
v-html="icons.starGroup"
></div>
<Sprite
class="achievement-icon"
image-name="achievement-sun2x"
/>
<div
v-once
class="svg-icon sparkles"
v-html="icons.starGroup"
></div>
</div>
</div><achievement-footer />
<p class="subtitle">
{{ $t('rebirthNewAdventure') }}
</p>
<p
class="description"
v-html="achievementText"
></p>
<p
v-once
class="stack-info"
>
{{ $t('rebirthStackInfo') }}
</p>
<button
v-once
class="btn btn-primary"
@click="close()"
>
{{ $t('onwards') }}
</button>
</div>
<div
slot="modal-footer"
class="footer-wave"
v-html="icons.purpleWaves"
></div>
</b-modal>
</template>
<style scoped>
.avatar {
width: 140px;
margin: 0 auto;
margin-bottom: 1.5em;
margin-top: 1.5em;
<style lang="scss">
@import '@/assets/scss/colors.scss';
#rebirth {
.modal-dialog {
width: 330px;
}
.modal-content {
border: none;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
}
.modal-body {
padding: 0;
}
.modal-footer {
padding: 0;
border-top: none;
border-radius: 0;
margin: 0;
line-height: 0;
}
}
</style>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.content {
padding: 24px 24px 0;
}
.header {
font-size: 1.25rem;
line-height: 1.4;
color: $purple-200;
margin-top: 8px;
margin-bottom: 16px;
}
.icon-area {
margin-bottom: 16px;
}
.sparkles {
width: 40px;
height: 64px;
&.mirror {
transform: scaleX(-1);
}
}
.close-x {
position: absolute;
right: 16px;
top: 16px;
cursor: pointer;
z-index: 2;
&:hover .svg-close {
opacity: 0.75;
}
.svg-close {
width: 16px;
height: 16px;
opacity: 0.5;
transition: opacity 0.2s ease;
pointer-events: none;
}
}
.achievement-icon {
margin: 0 24px;
}
.subtitle {
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-style: normal;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
margin-bottom: 12px;
color: $gray-10;
}
.description {
font-size: 0.875rem;
line-height: 1.71;
margin-bottom: 12px;
color: $gray-50;
}
.stack-info {
font-size: 0.875rem;
line-height: 1.71;
color: $gray-50;
margin-bottom: 24px;
}
.btn-primary {
margin-bottom: 8px;
}
.footer-wave {
width: 100%;
::v-deep svg {
display: block;
width: calc(100% + 8px);
height: auto;
margin: 0 -4px -4px;
}
}
</style>
<script>
import achievementFooter from './achievementFooter';
import achievementAvatar from './achievementAvatar';
import closeIcon from '@/assets/svg/close.svg?raw';
import Sprite from '@/components/ui/sprite';
import starGroup from '@/assets/svg/star-group.svg?raw';
import purpleWaves from '@/assets/svg/purple-waves.svg?raw';
import { mapState } from '@/libs/store';
export default {
components: {
achievementFooter,
achievementAvatar,
Sprite,
},
data () {
return {
icons: Object.freeze({
starGroup,
purpleWaves,
close: closeIcon,
}),
};
},
computed: {
...mapState({ user: 'user.data' }),
achievementText () {
const rebirths = this.user.achievements.rebirths || 0;
const level = this.user.achievements.rebirthLevel || 0;
if (level >= 100) {
return this.$t('rebirthAchievement100', { number: rebirths, level });
}
if (rebirths === 1) {
return this.$t('rebirthAchievement', { number: rebirths, level });
}
return this.$t('rebirthAchievementPlural', { number: rebirths, level });
},
},
methods: {
close () {
@@ -1,41 +1,186 @@
<template>
<b-modal
id="rebirth-enabled"
:title="$t('rebirthNew')"
size="md"
:hide-footer="true"
size="sm"
:hide-header="true"
>
<div class="modal-body">
<div class="col-12">
<div class="rebirth_orb"></div>
<p>
<span>{{ $t('rebirthUnlock') }}</span>
</p>
</div>
<div
class="close-x"
@click.stop="close()"
>
<div
class="svg-icon svg-close"
v-html="icons.close"
></div>
</div>
<div class="modal-footer">
<div class="col-12 text-center">
<button
class="btn btn-primary"
@click="close()"
<div class="content text-center">
<h2
v-once
class="header"
>
{{ $t('rebirthUnlockedNewItem') }}
</h2>
<div class="d-flex align-items-center justify-content-center icon-area">
<div
v-once
class="svg-icon sparkles mirror"
v-html="icons.starGroup"
></div>
<img
class="orb-icon"
src="@/assets/images/rebirth-orb.png"
alt="Orb of Rebirth"
>
{{ $t('close') }}
</button>
<div
v-once
class="svg-icon sparkles"
v-html="icons.starGroup"
></div>
</div>
<p
v-once
class="subtitle"
>
{{ $t('rebirthUnlockedOrb') }}
</p>
<p
v-once
class="description"
>
{{ $t('rebirthUnlockedDesc') }}
</p>
<button
v-once
class="btn btn-primary"
@click="close()"
>
{{ $t('onwards') }}
</button>
</div>
<div
slot="modal-footer"
class="clearfix"
></div>
</b-modal>
</template>
<style scoped>
.rebirth_orb {
margin: 0 auto;
<style lang="scss">
@import '@/assets/scss/colors.scss';
#rebirth-enabled {
.modal-dialog {
width: 330px;
}
.modal-content {
border-radius: 8px;
overflow: hidden;
box-shadow: 0 14px 28px 0 rgba($black, 0.24), 0 10px 10px 0 rgba($black, 0.28);
}
.modal-body {
padding: 0;
}
.modal-footer {
padding: 0;
border-top: none;
}
}
</style>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.content {
padding: 24px 24px 0;
}
.header {
font-size: 1.25rem;
line-height: 1.4;
color: $purple-200;
margin-top: 8px;
margin-bottom: 12px;
}
.icon-area {
margin-bottom: 12px;
}
.sparkles {
width: 40px;
height: 64px;
&.mirror {
transform: scaleX(-1);
}
}
.close-x {
position: absolute;
right: 16px;
top: 16px;
cursor: pointer;
z-index: 2;
&:hover .svg-close {
opacity: 0.75;
}
.svg-close {
width: 16px;
height: 16px;
opacity: 0.5;
transition: opacity 0.2s ease;
pointer-events: none;
}
}
.orb-icon {
width: 62px;
height: 62px;
margin: 0 24px;
image-rendering: pixelated;
}
.subtitle {
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-style: normal;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
margin-bottom: 12px;
color: $gray-50;
}
.description {
font-size: 0.875rem;
line-height: 1.71;
margin-bottom: 24px;
color: $gray-100;
}
.btn-primary {
margin-bottom: 24px;
}
</style>
<script>
import closeIcon from '@/assets/svg/close.svg?raw';
import starGroup from '@/assets/svg/star-group.svg?raw';
import { mapState } from '@/libs/store';
export default {
data () {
return {
icons: Object.freeze({
starGroup,
close: closeIcon,
}),
};
},
computed: {
...mapState({ user: 'user.data' }),
},
File diff suppressed because it is too large Load Diff
@@ -311,7 +311,7 @@
<input
id="passwordInput"
v-model="password"
class="form-control input-with-error"
class="form-control dark input-with-error"
type="password"
:placeholder="$t('password')"
:class="{'input-invalid': passwordInvalid, 'input-valid': passwordValid}"
@@ -323,7 +323,7 @@
{{ $t('minPasswordLength') }}
</div>
</div>
<div class="form-group">
<div class="form-group mb-4">
<label
v-once
for="confirmPasswordInput"
@@ -331,7 +331,7 @@
<input
id="confirmPasswordInput"
v-model="passwordConfirm"
class="form-control input-with-error"
class="form-control dark input-with-error"
type="password"
:placeholder="$t('confirmPasswordPlaceholder')"
:class="{'input-invalid': passwordConfirmInvalid, 'input-valid': passwordConfirmValid}"
@@ -344,13 +344,14 @@
</div>
</div>
<div class="text-center">
<div
class="btn btn-info"
:enabled="!resetPasswordSetNewOneData.hasError"
<button
class="btn btn-info w-100"
:disabled="!password || !passwordConfirm
|| password !== passwordConfirm || resetPasswordSetNewOneData.hasError"
@click="resetPasswordSetNewOneLink()"
>
{{ $t('setNewPass') }}
</div>
</button>
</div>
</form>
</div>
@@ -672,7 +673,7 @@ export default {
this.login();
},
async forgotPasswordLink () {
forgotPasswordLink: debounce(async function forgotPassLink () {
if (!this.username) {
window.alert(this.$t('missingEmail')); // eslint-disable-line no-alert
return;
@@ -683,7 +684,7 @@ export default {
});
window.alert(this.$t('newPassSent')); // eslint-disable-line no-alert
},
}, 500),
async resetPasswordSetNewOneLink () {
if (!this.password) {
window.alert(this.$t('missingNewPassword')); // eslint-disable-line no-alert
@@ -12,23 +12,39 @@
<label>
<strong v-once>{{ $t('name') }} *</strong>
</label>
<b-form-input
<input
ref="nameInput"
v-model="workingChallenge.name"
class="form-control"
type="text"
:placeholder="$t('challengeNamePlaceholder')"
@keydown="enableSubmit"
/>
@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)"
>
</div>
<div class="form-group">
<label>
<strong v-once>{{ $t('shortName') }} *</strong>
</label>
<b-form-input
<input
ref="shortNameInput"
v-model="workingChallenge.shortName"
class="form-control"
type="text"
:placeholder="$t('shortNamePlaceholder')"
@keydown="enableSubmit"
/>
@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)"
>
</div>
<div class="form-group">
<label>
@@ -40,10 +56,17 @@
{{ $t('charactersRemaining', {characters: charactersRemaining}) }}
</div>
<textarea
ref="summaryTextarea"
v-model="workingChallenge.summary"
class="summary-textarea form-control"
:placeholder="$t('challengeSummaryPlaceholder')"
@keydown="enableSubmit"
@focus="setActiveField('summary')"
@keydown="onFieldKeydown($event)"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
></textarea>
</div>
<div class="form-group">
@@ -55,11 +78,26 @@
class="float-right"
></a>
<textarea
ref="descriptionTextarea"
v-model="workingChallenge.description"
class="description-textarea form-control"
:placeholder="$t('challengeDescriptionPlaceholder')"
@keydown="enableSubmit"
@focus="setActiveField('description')"
@keydown="onFieldKeydown($event)"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
></textarea>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="activeFieldText"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
</div>
<div
v-if="creating"
@@ -280,12 +318,17 @@ import { TAVERN_ID, MIN_SHORTNAME_SIZE_FOR_CHALLENGES, MAX_SUMMARY_SIZE_FOR_CHAL
import CategoryOptions from '@/../../common/script/content/categoryOptions';
import markdownDirective from '@/directives/markdown';
import { userStateMixin } from '../../mixins/userState';
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
export default {
components: {
emojiAutoComplete,
},
directives: {
markdown: markdownDirective,
},
mixins: [userStateMixin],
mixins: [userStateMixin, autoCompleteHelperMixin],
props: ['groupId'],
data () {
const categoryOptions = CategoryOptions;
@@ -319,9 +362,14 @@ export default {
categoriesHashByKey,
loading: false,
groups: [],
textbox: null,
activeField: 'name',
};
},
computed: {
activeFieldText () {
return this.workingChallenge[this.activeField] || '';
},
creating () {
return !this.workingChallenge.id;
},
@@ -589,6 +637,29 @@ export default {
toggleCategorySelect () {
this.showCategorySelect = !this.showCategorySelect;
},
setActiveField (field) {
this.activeField = field;
const refMap = {
name: 'nameInput',
shortName: 'shortNameInput',
summary: 'summaryTextarea',
description: 'descriptionTextarea',
};
this.textbox = this.$refs[refMap[field]] || null;
},
onFieldKeydown (e) {
this.enableSubmit();
this.autoCompleteMixinUpdateCarretPosition(e);
},
selectedAutocomplete (newText, newCaret) {
this.workingChallenge[this.activeField] = newText;
this.$nextTick(() => {
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) {
@@ -0,0 +1,282 @@
<template>
<div
v-if="searchResults.length > 0"
class="autocomplete-selection"
:style="autocompleteStyle"
>
<div
v-for="result in searchResults"
:key="result.shortcode"
class="autocomplete-results d-flex align-items-center"
:class="{'hover-background': result.hover}"
@click="select(result)"
@mouseenter="setHover(result)"
@mouseleave="resetSelection()"
>
<img
v-if="result.imageUrl"
class="emoji-img"
:src="result.imageUrl"
:alt="result.shortcode"
>
<span
v-else
class="emoji-char"
>{{ result.emoji }}</span>
<span
class="shortcode ml-2"
:class="{'hover-foreground': result.hover}"
>:{{ result.shortcode }}:</span>
</div>
</div>
</template>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.autocomplete-results {
padding: .5em;
}
.autocomplete-selection {
box-shadow: 1px 1px 1px #efefef;
}
.hover-background {
background-color: rgba(213, 200, 255, 0.32);
cursor: pointer;
}
.hover-foreground {
color: $purple-300 !important;
}
.emoji-char {
font-size: 20px;
line-height: 1;
}
.emoji-img {
height: 20px;
width: 20px;
}
.shortcode {
color: $gray-200;
font-size: 14px;
}
</style>
<script>
import habiticaMarkdown from 'habitica-markdown';
export default {
props: ['text', 'caretPosition', 'coords', 'textbox'],
data () {
return {
colonRegex: /:([a-zA-Z0-9_+]*)$/,
currentSearch: '',
searchActive: false,
searchResults: [],
selected: null,
emojiList: [],
renderTick: 0,
internalCoords: { TOP: 0, LEFT: 0 },
};
},
computed: {
autocompleteStyle () {
// eslint-disable-next-line no-unused-vars
const _tick = this.renderTick;
const isTextarea = this.textbox.tagName === 'TEXTAREA';
const dropdownPA = (this.$el && this.$el.nodeType === 1) ? this.$el.offsetParent : null;
const textboxOP = this.textbox.offsetParent;
const needsRectCalc = dropdownPA && textboxOP && dropdownPA !== textboxOP;
let top;
let left;
const caretLeft = this.internalCoords.LEFT - (this.textbox.scrollLeft || 0);
if (needsRectCalc) {
const textboxRect = this.textbox.getBoundingClientRect();
const parentRect = dropdownPA.getBoundingClientRect();
const parentScrollTop = dropdownPA.scrollTop || 0;
if (isTextarea) {
const computedStyle = window.getComputedStyle(this.textbox);
const lineHeight = parseFloat(computedStyle.lineHeight)
|| (parseFloat(computedStyle.fontSize) * 1.4);
const caretTopInTextbox = this.internalCoords.TOP
- (this.textbox.scrollTop || 0) + lineHeight;
const clamped = Math.min(Math.max(caretTopInTextbox, 0), this.textbox.offsetHeight);
top = (textboxRect.top - parentRect.top) + parentScrollTop + clamped + 2;
} else {
top = (textboxRect.bottom - parentRect.top) + parentScrollTop + 2;
}
left = (textboxRect.left - parentRect.left) + caretLeft;
} else {
if (isTextarea) {
const computedStyle = window.getComputedStyle(this.textbox);
const lineHeight = parseFloat(computedStyle.lineHeight)
|| (parseFloat(computedStyle.fontSize) * 1.4);
const caretTopInTextbox = this.internalCoords.TOP
- (this.textbox.scrollTop || 0) + lineHeight;
const clamped = Math.min(Math.max(caretTopInTextbox, 0), this.textbox.offsetHeight);
top = this.textbox.offsetTop + clamped + 2;
} else {
top = this.textbox.offsetTop + this.textbox.offsetHeight + 2;
}
left = this.textbox.offsetLeft + caretLeft;
}
return {
top: `${top}px`,
left: `${left}px`,
position: 'absolute',
minWidth: '150px',
zIndex: 100,
backgroundColor: 'white',
};
},
},
watch: {
searchResults (results, oldResults) {
if (results.length > 0 && (!oldResults || oldResults.length === 0)) {
this.$nextTick(() => {
this.renderTick += 1;
});
}
},
text (newText, prevText) {
if (!this.textbox) return;
this._measureCaretCoords();
const delCharsBool = prevText.length > newText.length;
const caretPosition = this.textbox.selectionEnd;
const lastFocusChar = delCharsBool ? prevText[caretPosition] : newText[caretPosition - 1];
if (
newText.length === 0
|| (lastFocusChar === ':' && delCharsBool)
) {
this.cancel();
} else {
if (lastFocusChar === ':') this.searchActive = true;
if (this.searchActive) {
this.searchResults = this.solveSearchResults(newText.substring(0, caretPosition));
}
}
},
},
created () {
const defs = habiticaMarkdown.emojiDefs;
if (!defs) return;
const customEmojis = habiticaMarkdown.customEmojis || {};
const list = [];
const keys = Object.keys(defs);
keys.sort();
for (const key of keys) {
const entry = { shortcode: key, emoji: defs[key], hover: false };
if (customEmojis[key]) {
entry.imageUrl = customEmojis[key];
}
list.push(entry);
}
this.emojiList = list;
},
methods: {
solveSearchResults (textFocus) {
const regexRes = this.colonRegex.exec(textFocus);
if (!regexRes) {
this.cancel();
return [];
}
this.currentSearch = regexRes[1];
if (this.currentSearch.length === 0) return [];
const lowerSearch = this.currentSearch.toLowerCase();
return this.emojiList
.filter(entry => entry.shortcode.startsWith(lowerSearch))
.slice(0, 6)
.map(entry => ({ ...entry, hover: false }));
},
select (result) {
const { text } = this;
const targetName = `${result.shortcode}: `;
const oldCaret = this.caretPosition;
const escapedSearch = this.currentSearch.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
let newText = text.substring(0, this.caretPosition)
.replace(new RegExp(`${escapedSearch}$`), targetName);
const newCaret = newText.length;
newText += text.substring(oldCaret, text.length);
this.$emit('select', newText, newCaret);
this.cancel();
},
setHover (result) {
this.resetSelection();
result.hover = true;
},
clearHover () {
for (const selection of this.searchResults) {
selection.hover = false;
}
},
resetSelection () {
this.clearHover();
this.selected = null;
},
selectNext () {
if (this.searchResults.length > 0) {
this.clearHover();
this.selected = this.selected === null
? 0
: (this.selected + 1) % this.searchResults.length;
this.searchResults[this.selected].hover = true;
}
},
selectPrevious () {
if (this.searchResults.length > 0) {
this.clearHover();
this.selected = this.selected === null
? this.searchResults.length - 1
: (this.selected - 1 + this.searchResults.length) % this.searchResults.length;
this.searchResults[this.selected].hover = true;
}
},
makeSelection () {
if (this.searchResults.length > 0 && this.selected !== null) {
const result = this.searchResults[this.selected];
this.select(result);
}
},
_measureCaretCoords () {
const el = this.textbox;
const caretPosition = el.selectionEnd;
const div = document.createElement('div');
const span = document.createElement('span');
const copyStyle = getComputedStyle(el);
[].forEach.call(copyStyle, prop => {
div.style[prop] = copyStyle[prop];
});
div.style.position = 'absolute';
div.style.visibility = 'hidden';
document.body.appendChild(div);
div.textContent = el.value.substr(0, caretPosition);
span.textContent = el.value.substr(caretPosition) || '.';
div.appendChild(span);
this.internalCoords = {
TOP: span.offsetTop,
LEFT: span.offsetLeft,
};
document.body.removeChild(div);
},
cancel () {
this.searchActive = false;
this.searchResults = [];
this.resetSelection();
},
},
};
</script>
@@ -52,7 +52,7 @@
<div
v-if="!group.purchased.plan.dateTerminated
&& group.purchased.plan.paymentMethod === 'Stripe'"
class="btn btn-primary"
class="btn btn-primary mb-3"
@click="redirectToStripeEdit({groupId: group.id})"
>
{{ $t('subUpdateCard') }}
@@ -0,0 +1,577 @@
<template>
<b-modal
id="group-plan-selection"
:hide-footer="true"
:hide-header="true"
size="md"
@show="loadData"
@hide="onHide"
>
<div class="selection-modal">
<div class="modal-header-row">
<h2 class="title">
{{ $t('chooseAnOption') }}
</h2>
<div class="header-actions">
<span
class="cancel-text"
@click="close"
>
{{ $t('cancel') }}
</span>
<button
class="btn btn-primary next-button"
:class="{ disabled: !selectedOption }"
:disabled="!selectedOption"
@click="continueFlow"
>
{{ $t('next') }}
</button>
</div>
</div>
<div
v-if="loading"
class="loading-container"
>
<div class="spinner-border text-secondary"></div>
</div>
<template v-else>
<div
v-if="hasUpgradeableGroups"
class="section-header"
>
{{ $t('upgradeExistingGroup') }}
</div>
<selectable-card
v-for="group in upgradeableGuilds"
:key="group._id"
class="option-card"
:selected="isSelected(group)"
@click="selectOption(group)"
>
<div class="option-content">
<div class="option-info">
<div class="option-name">
{{ group.name }}
</div>
<div class="option-members">
{{ formatMemberCount(group.memberCount) }}
</div>
<div class="option-label previously-upgraded">
<div
class="svg-icon sparkle-icon"
v-html="icons.sparkles"
></div>
{{ $t('previouslyUpgradedGroup') }}
</div>
</div>
<div class="option-price">
${{ calculatePrice(group.memberCount) }}.00/mo
</div>
</div>
</selectable-card>
<selectable-card
v-if="upgradeableParty"
class="option-card"
:class="{ 'has-pending-warning': partyPendingInviteCount > 0 }"
:selected="isSelected(upgradeableParty)"
@click="selectOption(upgradeableParty)"
>
<div class="option-content">
<div class="option-info">
<div class="option-name">
{{ upgradeableParty.name }}
</div>
<div class="option-members">
{{ formatMemberCount(upgradeableParty.memberCount) }}
<span
v-if="partyPendingInviteCount > 0"
class="pending-count"
>
{{ $t('pendingCount', { count: partyPendingInviteCount }) }}
</span>
</div>
<div
v-if="isPartyPreviouslyUpgraded"
class="option-label previously-upgraded"
>
<div
class="svg-icon sparkle-icon"
v-html="icons.sparkles"
></div>
{{ $t('previouslyUpgradedGroup') }}
</div>
<div
v-else
class="option-label your-party"
>
<div
class="svg-icon member-icon"
v-html="icons.member"
></div>
{{ $t('yourParty') }}
</div>
</div>
<div class="option-price">
${{ calculatePrice(upgradeableParty.memberCount) }}.00/mo
</div>
</div>
<div
v-if="partyPendingInviteCount > 0"
class="pending-warning-banner"
>
<div
class="svg-icon alert-icon"
v-html="icons.alert"
></div>
<span class="warning-text">{{ $t('upgradeCancelsPendingInvites') }}</span>
</div>
</selectable-card>
<div
v-if="hasUpgradeableGroups"
class="or-divider"
>
<div class="divider-line"></div>
<span class="or-text">{{ $t('or') }}</span>
<div class="divider-line"></div>
</div>
<selectable-card
class="option-card create-new"
:selected="selectedOption === 'new'"
@click="selectOption('new')"
>
<div class="option-content">
<div class="option-info">
<div class="option-name">
{{ $t('createNewGroup') }}
</div>
<div class="option-description">
{{ $t('inviteOthersForAdditional') }}
<span class="price-highlight">${{ perMemberPrice }}.00</span>
{{ $t('perMember') }}.
</div>
</div>
<div class="option-price">
${{ basePrice }}.00/mo
</div>
</div>
</selectable-card>
<div class="footer-note">
{{ $t('additionalMembersProrated') }}
</div>
</template>
</div>
</b-modal>
</template>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.selection-modal {
padding: 24px;
}
.modal-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.title {
font-family: 'Roboto Condensed', sans-serif;
font-weight: 700;
font-size: 20px;
line-height: 28px;
color: $purple-200;
margin: 0;
}
.header-actions {
display: flex;
align-items: center;
}
.cancel-text {
color: $blue-10;
font-size: 0.875rem;
margin-right: 16px;
cursor: pointer;
}
.next-button {
min-width: 64px;
&.disabled {
background-color: $gray-300;
border-color: $gray-300;
cursor: not-allowed;
}
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.section-header {
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-size: 14px;
line-height: 24px;
color: $gray-10;
margin-bottom: 12px;
}
.option-card {
margin-bottom: 12px;
::v-deep .option-name {
color: $gray-50;
}
&.selected ::v-deep .option-name {
color: $purple-200;
}
}
.pending-warning-banner {
display: flex;
align-items: center;
justify-content: center;
height: 32px;
background-color: $yellow-50;
border-radius: 0 0 6px 6px;
margin: 16px -16px 0 -16px;
gap: 4px;
.selected & {
margin: 15px -15px 0 -15px;
}
.alert-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
::v-deep path {
fill: $gray-10;
}
}
.warning-text {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: $gray-10;
}
}
.option-content {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding-left: 32px;
padding-right: 8px;
}
.option-info {
flex: 1;
}
.option-name {
font-family: 'Roboto', sans-serif;
font-size: 14px;
font-weight: 700;
line-height: 24px;
margin-bottom: 4px;
}
.option-members {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: $gray-100;
margin-bottom: 8px;
.pending-count {
font-weight: 700;
color: $yellow-5;
}
}
.option-label {
display: flex;
align-items: center;
font-family: 'Roboto', sans-serif;
font-size: 12px;
line-height: 16px;
gap: 4px;
&.previously-upgraded {
font-weight: 700;
color: $blue-10;
}
&.your-party {
font-weight: 700;
color: $gray-100;
}
.svg-icon {
width: 14px;
height: 14px;
}
.sparkle-icon {
color: $blue-10;
}
.member-icon {
color: $gray-100;
::v-deep path {
fill: $gray-100;
stroke: $gray-100;
stroke-width: 0.5px;
}
}
}
.option-description {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: $gray-100;
.price-highlight {
font-weight: 700;
}
}
.option-price {
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-size: 20px;
line-height: 24px;
color: $purple-200;
white-space: nowrap;
}
.or-divider {
display: flex;
align-items: center;
margin: 20px 0;
.divider-line {
flex: 1;
height: 1px;
background-color: $gray-500;
}
.or-text {
padding: 0 16px;
font-family: 'Roboto', sans-serif;
font-weight: 700;
font-size: 12px;
line-height: 16px;
letter-spacing: 0.2em;
text-transform: uppercase;
color: $gray-100;
}
}
.create-new {
.option-name {
margin-bottom: 8px;
}
}
.footer-note {
font-family: 'Roboto', sans-serif;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: $gray-100;
text-align: center;
margin-top: 16px;
margin-left: 24px;
margin-right: 24px;
}
</style>
<style lang="scss">
#group-plan-selection {
.modal-dialog {
max-width: 504px;
}
.modal-content {
border-radius: 8px;
box-shadow: 0 14px 28px 0 rgba(26, 24, 29, 0.24), 0 10px 10px 0 rgba(26, 24, 29, 0.28);
}
.modal-body {
padding: 0;
}
.option-card.has-pending-warning.selectable-card {
padding-bottom: 0;
}
}
</style>
<script>
import axios from 'axios';
import paymentsMixin from '@/mixins/payments';
import { mapState } from '@/libs/store';
import SelectableCard from '@/components/ui/selectableCard.vue';
import svgSparkles from '@/assets/svg/sparkles.svg?raw';
import svgMember from '@/assets/svg/member-icon.svg?raw';
import svgAlert from '@/assets/svg/for-css/alert.svg?raw';
export default {
components: {
SelectableCard,
},
mixins: [paymentsMixin],
data () {
return {
selectedOption: null,
userGuilds: [],
userParty: null,
activeGroupPlanIds: [],
loading: true,
basePrice: 9,
perMemberPrice: 3,
icons: Object.freeze({
sparkles: svgSparkles,
member: svgMember,
alert: svgAlert,
}),
partyPendingInviteCount: 0,
};
},
computed: {
...mapState({ user: 'user.data' }),
upgradeableGuilds () {
return this.userGuilds.filter(group => {
const leaderId = group.leader?._id || group.leader;
if (leaderId !== this.user._id) return false;
const purchased = group.purchased;
if (!purchased?.wasUpgraded) return false;
if (this.activeGroupPlanIds.includes(group._id)) return false;
if (!purchased.dateTerminated) return false;
return new Date(purchased.dateTerminated) < new Date();
});
},
upgradeableParty () {
if (!this.userParty) return null;
const leaderId = this.userParty.leader?._id || this.userParty.leader;
if (leaderId !== this.user._id) return null;
if (this.activeGroupPlanIds.includes(this.userParty._id)) return null;
return this.userParty;
},
hasUpgradeableGroups () {
return this.upgradeableGuilds.length > 0 || this.upgradeableParty !== null;
},
isPartyPreviouslyUpgraded () {
if (!this.userParty) return false;
const purchased = this.userParty.purchased;
if (!purchased?.wasUpgraded) return false;
if (!purchased.dateTerminated) return false;
return new Date(purchased.dateTerminated) < new Date();
},
},
methods: {
async loadData () {
this.loading = true;
this.selectedOption = null;
this.partyPendingInviteCount = 0;
try {
const [guildsResponse, partyResponse] = await Promise.all([
axios.get('/api/v4/groups', { params: { type: 'guilds', includeExpiredPlans: 'true' } }),
axios.get('/api/v4/groups/party').catch(() => ({ data: { data: null } })),
]);
this.userGuilds = guildsResponse.data.data || [];
this.userParty = partyResponse.data.data;
if (this.userParty) {
try {
const invitesResponse = await axios.get(`/api/v4/groups/${this.userParty._id}/invites`);
this.partyPendingInviteCount = invitesResponse.data.data?.length || 0;
} catch (e) {
this.partyPendingInviteCount = 0;
}
}
await this.$store.dispatch('guilds:getGroupPlans', true);
const groupPlans = this.$store.state.groupPlans?.data || [];
this.activeGroupPlanIds = groupPlans.map(g => g._id);
} catch (e) {
console.error('Error loading group data:', e);
}
this.loading = false;
this.$nextTick(() => {
if (this.upgradeableGuilds.length > 0) {
this.selectedOption = this.upgradeableGuilds[0];
} else if (this.upgradeableParty) {
this.selectedOption = this.upgradeableParty;
} else {
this.selectedOption = 'new';
}
});
},
selectOption (option) {
this.selectedOption = option;
},
isSelected (group) {
if (!this.selectedOption || this.selectedOption === 'new') return false;
return this.selectedOption._id === group._id;
},
calculatePrice (memberCount) {
return this.basePrice + (this.perMemberPrice * (memberCount - 1));
},
formatMemberCount (count) {
return count === 1 ? this.$t('oneMember') : this.$t('membersCount', { count });
},
continueFlow () {
if (!this.selectedOption) return;
const selection = this.selectedOption;
this.close();
if (selection === 'new') {
this.$root.$emit('bv::show::modal', 'create-group');
} else {
this.stripeGroup({ group: selection, upgrade: true });
}
},
close () {
this.$root.$emit('bv::hide::modal', 'group-plan-selection');
},
onHide () {
this.selectedOption = null;
},
},
};
</script>
@@ -41,6 +41,14 @@
:chat="group.chat"
@select="selectedAutocomplete"
/>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="newMessage"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
</div>
<community-guidelines />
<div class="row chat-actions">
@@ -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,
},
+82 -57
View File
@@ -25,53 +25,61 @@
<div class="col-12 col-md-6">
<div class="row icon-row">
<div
class="item-with-icon"
class="item-with-icon p-2"
tabindex="0"
role="button"
@keyup.enter="showMemberModal()"
@click="showMemberModal()"
>
<div
v-if="group.memberCount > 1000"
class="svg-icon shield"
v-html="icons.goldGuildBadgeIcon"
></div>
<div
v-if="group.memberCount > 100 && group.memberCount < 999"
class="svg-icon shield"
v-html="icons.silverGuildBadgeIcon"
></div>
<div
v-if="group.memberCount < 100"
class="svg-icon shield"
v-html="icons.bronzeGuildBadgeIcon"
></div>
<span class="number">{{ group.memberCount | abbrNum }}</span>
<div
v-once
class="member-list label"
>
{{ $t('memberList') }}
<div class="box-content">
<div class="icon-number-row">
<div
v-if="group.memberCount > 1000"
class="svg-icon shield"
v-html="icons.goldGuildBadgeIcon"
></div>
<div
v-if="group.memberCount > 100 && group.memberCount < 999"
class="svg-icon shield"
v-html="icons.silverGuildBadgeIcon"
></div>
<div
v-if="group.memberCount < 100"
class="svg-icon shield"
v-html="icons.bronzeGuildBadgeIcon"
></div>
<span class="number">{{ group.memberCount | abbrNum }}</span>
</div>
<div
v-once
class="details"
>
{{ $t('memberList') }}
</div>
</div>
</div>
<div v-if="!isParty">
<div
class="item-with-icon"
class="item-with-icon p-2"
tabindex="0"
role="button"
@keyup.enter="showGroupGems()"
@click="showGroupGems()"
>
<div
class="svg-icon gem"
v-html="icons.gem"
></div>
<span class="number">{{ group.balance * 4 }}</span>
<div
v-once
class="label"
>
{{ $t('guildBank') }}
<div class="box-content">
<div class="icon-number-row">
<div
class="svg-icon gem"
v-html="icons.gem"
></div>
<span class="number">{{ group.balance * 4 }}</span>
</div>
<div
v-once
class="details"
>
{{ $t('guildBank') }}
</div>
</div>
</div>
</div>
@@ -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 {
@@ -1,8 +1,8 @@
<template>
<div
class="banner d-flex align-items-center justify-content-between py-3 px-4"
id="privacy-banner"
v-if="!hidden"
id="privacy-banner"
class="banner d-flex align-items-center justify-content-between py-3 px-4"
>
<p
class="mr-3 mb-0"
@@ -34,7 +34,7 @@
>
<div
class="close-x"
@click="remove()"
@click.stop="remove()"
>
<div
class="svg-icon svg-close"
@@ -140,7 +140,7 @@ export default {
methods: {
remove () {
if (this.eventKey) {
window.sessionStorage.setItem(`hide-g1g1-${this.eventKey}`, 'true');
window.localStorage.setItem(`hide-g1g1-${this.eventKey}`, 'true');
}
this.$emit('notification-removed');
},
@@ -318,7 +318,7 @@ export default {
shouldShowG1g1 () {
if (!this.currentG1g1Event) return false;
const eventKey = this.g1g1EventKey;
if (eventKey && window.sessionStorage.getItem(`hide-g1g1-${eventKey}`) === 'true') {
if (eventKey && window.localStorage.getItem(`hide-g1g1-${eventKey}`) === 'true') {
return false;
}
return !this.g1g1Hidden;
@@ -491,6 +491,9 @@ export default {
},
methods: {
mapProfileLinksToModal () {
if (!this.$refs?.markdownContainer) {
return;
}
const links = this.$refs.markdownContainer.getElementsByTagName('a');
for (let i = 0; i < links.length; i += 1) {
let link = links[i].pathname;
@@ -10,6 +10,9 @@
>
<div class="modal-body">
<news-content ref="newsContent" />
<close-x
@close="dismissAlert()"
/>
</div>
<div class="modal-footer d-flex align-items-center pb-0">
@@ -30,12 +33,18 @@
</template>
<script>
import { mapState } from '@/libs/store';
import newsContent from './newsContent';
import closeX from '../ui/closeX.vue';
export default {
components: {
closeX,
newsContent,
},
computed: {
...mapState({ user: 'user.data' }),
},
methods: {
async onShow () {
this.$refs.newsContent.getPosts();
@@ -330,6 +330,7 @@ export default {
handledNotifications,
isInitialLoadComplete: false,
pendingRebirthNotification: null,
lastShownStreakCount: null, // Track last shown streak to prevent duplicates
};
},
computed: {
@@ -726,17 +727,24 @@ export default {
this.$root.$emit('habitica:won-challenge', notification);
break;
case 'REBIRTH_ACHIEVEMENT':
if (localStorage.getItem('show-rebirth-confirmation') === 'true') {
markAsRead = false;
} else if (!this.isInitialLoadComplete) {
this.pendingRebirthNotification = notification;
markAsRead = false;
} else {
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
if (localStorage.getItem('show-rebirth-confirmation') !== 'true') {
if (!this.isInitialLoadComplete) {
this.pendingRebirthNotification = notification;
markAsRead = false;
} else {
this.playSound('Achievement_Unlocked');
this.$root.$emit('bv::show::modal', 'rebirth');
}
}
break;
case 'STREAK_ACHIEVEMENT':
// Client-side deduplication: prevent showing duplicate streak achievements
if (this.lastShownStreakCount === this.user.achievements.streak) {
// Same streak already shown, skip this notification
break;
}
this.lastShownStreakCount = this.user.achievements.streak;
this.text(`${this.$t('streaks')}: ${this.user.achievements.streak}`, () => {
this.$root.$emit('bv::show::modal', 'streak');
}, this.user.preferences.suppressModals.streak);
@@ -105,7 +105,7 @@ export default {
},
data () {
return {
privacyConsent: true,
privacyConsent: false,
};
},
methods: {
@@ -189,6 +189,7 @@
>
</p>
<div
v-if="paymentMethodLogo.icon"
class="svg svg-icon mb-4"
:class="paymentMethodLogo.class"
v-html="paymentMethodLogo.icon"
@@ -205,6 +206,13 @@
<div>{{ $t('subUpdateCard') }}</div>
</button>
</div>
<div
v-once
v-if="!hasGroupPlan"
class="small text-center mb-4"
>
{{ $t('subscriptionBillingFYIShort') }}
</div>
<div
v-if="purchasedPlanExtraMonthsDetails.months > 0"
class="extra-months green-10 py-2 px-3 mb-4"
@@ -409,6 +417,7 @@
<div class="d-flex flex-column align-items-center mt-3">
<div
v-once
v-if="!hasSubscription"
class="small gray-100 w-50 text-center mb-5"
>
{{ $t('subscriptionBillingFYI') }}
@@ -42,7 +42,7 @@
:hide-class-badge="true"
:with-background="true"
:override-avatar-gear="getAvatarOverrides(item)"
:sprites-margin="'0px auto 0px -24px'"
:sprites-margin="'0px auto 0px -2px'"
/>
</div>
<item
@@ -281,6 +281,11 @@
.badge-dialog {
left: -8px;
top: -8px;
.badge-pin {
width: 32px;
height: 32px;
}
}
.avatar {
@@ -851,10 +856,17 @@ export default {
- ownedMounts
- ownedItems;
if (
petsRemaining < 0
&& !window.confirm(this.$t('purchasePetItemConfirm', { itemText: this.item.text })) // eslint-disable-line no-alert
) return;
if (petsRemaining < 0) {
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:purchase-confirm', {
message: this.$t('purchasePetItemConfirm', { itemText: this.item.text }),
currency: this.item.currency,
cost: this.item.value * this.selectedAmountToBuy,
resolve,
});
});
if (!confirmed) return;
}
}
if (this.item.purchaseType === 'customization') {
@@ -866,11 +878,14 @@ export default {
this.purchased(this.item.text);
} else {
const shouldConfirmPurchase = this.item.currency === 'gems' || this.item.currency === 'hourglasses';
if (
shouldConfirmPurchase
&& !this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)
) {
return;
if (shouldConfirmPurchase) {
const confirmed = await this.confirmPurchase(
this.item.currency,
this.item.value * this.selectedAmountToBuy,
);
if (!confirmed) {
return;
}
}
if (this.genericPurchase) {
if (this.item.key === 'rebirth_orb') {
@@ -893,8 +908,8 @@ export default {
purchaseGems () {
this.$root.$emit('bv::show::modal', 'buy-gems');
},
togglePinned () {
this.isPinned = this.$store.dispatch('user:togglePinnedItem', { type: this.item.pinType, path: this.item.path });
async togglePinned () {
this.isPinned = await this.$store.dispatch('user:togglePinnedItem', { type: this.item.pinType, path: this.item.path });
if (!this.isPinned) {
this.text(this.$t('unpinnedItem', { item: this.item.text }));
@@ -76,7 +76,21 @@
:empty-item="false"
:show-popover="Boolean(ctx.item.text)"
@click="selectItem(ctx.item)"
/>
>
<template
slot="itemBadge"
slot-scope="slotProps"
>
<span
class="badge-top"
@click.prevent.stop="togglePinned(slotProps.item)"
>
<pin-badge
:pinned="slotProps.item.pinned"
/>
</span>
</template>
</shop-item>
</template>
</item-rows>
</div>
@@ -108,6 +122,16 @@
}
</style>
<style lang="scss">
.market .badge-pin:not(.pinned) {
display: none;
}
.market .item:hover .badge-pin {
display: block;
}
</style>
<script>
import find from 'lodash/find';
import shops from '@/../../common/script/libs/shops';
@@ -118,7 +142,9 @@ import Checkbox from '@/components/ui/checkbox';
import FilterGroup from '@/components/ui/filterGroup';
import FilterSidebar from '@/components/ui/filterSidebar';
import ItemRows from '@/components/ui/itemRows';
import PinBadge from '@/components/ui/pinBadge';
import ShopItem from '../shopItem';
import pinUtils from '@/mixins/pinUtils';
export default {
components: {
@@ -126,8 +152,10 @@ export default {
FilterGroup,
FilterSidebar,
ItemRows,
PinBadge,
ShopItem,
},
mixins: [pinUtils],
data () {
return {
searchText: null,
@@ -184,8 +212,12 @@ export default {
methods: {
customizationsItems (options = {}) {
const { category, searchBy } = options;
return category.items.filter(item => !searchBy
|| item.text.toLowerCase().includes(searchBy));
return category.items
.filter(item => !searchBy || item.text.toLowerCase().includes(searchBy))
.map(item => ({
...item,
pinned: this.isPinned(item),
}));
},
emptyClick (identifier, event) {
if (event.target.tagName !== 'A') return;
@@ -0,0 +1,207 @@
<template>
<b-modal
id="purchase-confirm-modal"
:hide-footer="true"
:hide-header="true"
modal-class="purchase-confirm-modal"
centered
>
<div class="modal-content-wrapper">
<div class="top-bar"></div>
<div class="modal-body-content">
<div
class="currency-chip"
:class="currency"
>
<span
class="svg-icon icon-24"
v-html="icons[currency]"
></span>
<span class="cost-value">{{ cost }}</span>
</div>
<h2 class="modal-title">
{{ $t('confirmPurchase') }}
</h2>
<p class="modal-subtitle">
{{ confirmationMessage }}
</p>
<div class="button-wrapper">
<button
class="btn btn-primary"
@click="confirm()"
>
{{ $t('confirm') }}
</button>
<button
class="btn-cancel"
@click="cancel()"
>
{{ $t('cancel') }}
</button>
</div>
</div>
</div>
</b-modal>
</template>
<script>
import svgGem from '@/assets/svg/gem.svg?raw';
import svgHourglass from '@/assets/svg/hourglass.svg?raw';
export default {
data () {
return {
confirmationMessage: '',
currency: 'gems',
cost: 0,
resolveCallback: null,
icons: Object.freeze({
gems: svgGem,
hourglasses: svgHourglass,
}),
};
},
mounted () {
this.$root.$on('habitica:purchase-confirm', config => {
this.confirmationMessage = config.message;
this.currency = config.currency || 'gems';
this.cost = config.cost || 0;
this.resolveCallback = config.resolve;
this.$root.$emit('bv::show::modal', 'purchase-confirm-modal');
});
},
beforeDestroy () {
this.$root.$off('habitica:purchase-confirm');
},
methods: {
confirm () {
if (this.resolveCallback) {
this.resolveCallback(true);
}
this.close();
},
cancel () {
if (this.resolveCallback) {
this.resolveCallback(false);
}
this.close();
},
close () {
this.$root.$emit('bv::hide::modal', 'purchase-confirm-modal');
},
},
};
</script>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
::v-deep .purchase-confirm-modal {
.modal-dialog {
max-width: 330px;
margin: auto;
}
.modal-content {
border-radius: 8px;
overflow: hidden;
border: none;
}
.modal-body {
padding: 0;
}
}
.modal-content-wrapper {
display: flex;
flex-direction: column;
}
.top-bar {
height: 8px;
background-color: $purple-300;
}
.modal-body-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24px 24px;
}
.currency-chip {
display: flex;
align-items: center;
gap: 8px;
margin-top: 40px;
padding: 8px 20px;
border-radius: 20px;
font-size: 1.25rem;
font-weight: bold;
line-height: 1.4;
&.gems {
color: $gems-color;
background-color: rgba($green-10, 0.15);
}
&.hourglasses {
color: $hourglass-color;
background-color: rgba($blue-10, 0.15);
}
.icon-24 {
width: 24px;
height: 24px;
}
}
.modal-title {
margin-top: 16px;
margin-bottom: 0;
color: $purple-300;
font-family: 'Roboto Condensed', sans-serif;
font-weight: 700;
font-size: 20px;
text-align: center;
}
.modal-subtitle {
margin-top: 12px;
margin-bottom: 0;
font-family: Roboto, sans-serif;
font-weight: 700;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
color: $gray-50;
}
.button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 16px;
gap: 8px;
}
.btn-cancel {
background: none;
border: none;
color: $purple-300;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
cursor: pointer;
padding: 8px 16px;
&:hover {
text-decoration: underline;
}
}
</style>
@@ -180,6 +180,11 @@
.badge-dialog {
left: -8px;
top: -8px;
.badge-pin {
width: 32px;
height: 32px;
}
}
.modal-content {
@@ -510,8 +515,12 @@ export default {
this.selectedAmountToBuy = 1;
this.$emit('change', $event);
},
buyItem () {
if (!this.confirmPurchase(this.item.currency, this.item.value * this.selectedAmountToBuy)) {
async buyItem () {
const confirmed = await this.confirmPurchase(
this.item.currency,
this.item.value * this.selectedAmountToBuy,
);
if (!confirmed) {
return;
}
this.makeGenericPurchase(this.item, 'buyQuestModal', this.selectedAmountToBuy);
@@ -498,8 +498,13 @@ export default {
await this.triggerGetWorldState();
this.currentEvent = _find(this.currentEventList, event => Boolean(event.season));
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`;
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`;
if (this.currentEvent.season === 'valentines') {
this.imageURLs.background = 'url(/static/npc/spring/seasonal_shop_opened_background.png)';
this.imageURLs.npc = 'url(/static/npc/spring/seasonal_shop_opened_npc.png)';
} else {
this.imageURLs.background = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_background.png)`;
this.imageURLs.npc = `url(/static/npc/${this.currentEvent.season}/seasonal_shop_opened_npc.png)`;
}
},
beforeDestroy () {
this.$root.$off('buyModal::boughtItem');
@@ -1,5 +1,6 @@
<template>
<div>
<group-plan-selection-modal />
<group-plan-creation-modal />
<div class="d-flex justify-content-center">
<div
@@ -315,10 +316,12 @@
import { setup as setupPayments } from '@/libs/payments';
import paymentsMixin from '../../mixins/payments';
import GroupPlanCreationModal from '../group-plans/groupPlanCreationModal.vue';
import GroupPlanSelectionModal from '../group-plans/groupPlanSelectionModal.vue';
export default {
components: {
GroupPlanCreationModal,
GroupPlanSelectionModal,
},
mixins: [paymentsMixin],
data () {
@@ -359,7 +362,7 @@ export default {
if (this.upgradingGroup._id) {
return this.stripeGroup({ group: this.upgradingGroup, upgrade: true });
}
return this.$root.$emit('bv::show::modal', 'create-group');
return this.$root.$emit('bv::show::modal', 'group-plan-selection');
},
},
};
@@ -11,12 +11,12 @@
<privacy-banner
class="privacy-banner"
/>
<div class="bg-purple-300 white">
<div class="bg-purple-300 white pt-5">
<div>
<div
id="intro-signup"
>
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-center pb-5 mb-5">
<div class="w-33 mr-5 mt-5">
<img
src="@/assets/images/home/home-main@3x.png"
@@ -64,9 +64,11 @@
<li>sexual orientation; and</li>
<li>information collected from a known child.</li>
</ul>
<p><strong>
NOTE: Please do not provide us sensitive personal information or sensitive personal data, as those terms are defined under applicable privacy laws, unless we directly request that you do so. If you feel, after careful consideration, that it is necessary to provide us certain sensitive personal information or data, please provide us the minimum amount of such information or data that is necessary.
</strong></p>
<p>
<strong>
NOTE: Please do not provide us sensitive personal information or sensitive personal data, as those terms are defined under applicable privacy laws, unless we directly request that you do so. If you feel, after careful consideration, that it is necessary to provide us certain sensitive personal information or data, please provide us the minimum amount of such information or data that is necessary.
</strong>
</p>
<h3 id="section_1_1">
1.1 Information You Provide Directly
</h3>
@@ -617,7 +619,7 @@
7. General Audience Services
</h2>
<p>
The Service is intended for users 18 years or older; you are not permitted to access or use the Service if you are younger than 18. We do not knowingly collect personal information from children under the age of 18 through the Service. We encourage parents and legal guardians to monitor their childrens Internet usage and to help enforce our Privacy Policy by instructing their children to never provide personal information without their permission. If you have reason to believe that a child under the age of 18 has provided personal information to us, please contact us at <a href='mailto:privacy@habitica.com'>privacy@habitica.com</a>, and we will delete that information from our databases.
The Service is intended for users 18 years or older; you are not permitted to access or use the Service if you are younger than 18. We do not knowingly collect personal information from children under the age of 18 through the Service. We encourage parents and legal guardians to monitor their childrens Internet usage and to help enforce our Privacy Policy by instructing their children to never provide personal information without their permission. If you have reason to believe that a child under the age of 18 has provided personal information to us, please contact us at <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>, and we will delete that information from our databases.
</p>
<h2 id="section_8">
@@ -708,7 +710,7 @@
<p><strong><u>Nevada Residents</u></strong></p>
<p>
Nevada residents may opt out of the sale of certain “covered information” collected by operators of websites or online services. We currently do not sell covered information, as “sale” is defined by such law, and do not have plans to do so. In accordance with Nevada law, you may submit to us a verified request instructing us not to sell your covered information by sending an email to <a href='mailto:privacy@habitica.com'>privacy@habitica.com</a>.
Nevada residents may opt out of the sale of certain “covered information” collected by operators of websites or online services. We currently do not sell covered information, as “sale” is defined by such law, and do not have plans to do so. In accordance with Nevada law, you may submit to us a verified request instructing us not to sell your covered information by sending an email to <a href="mailto:privacy@habitica.com">privacy@habitica.com</a>.
</p>
<p><strong><u>Notice to United Kingdom/European/Switzerland Residents.</u></strong></p>
<p>
@@ -15,8 +15,8 @@
<router-view />
</div>
<div
id="bottom-background"
v-if="loginFlow"
id="bottom-background"
class="bg-purple-300"
>
<div class="seamless_mountains_demo_repeat"></div>
@@ -31,7 +31,10 @@
id="bottom-wrap"
class="purple-4"
>
<div id="bottom-background" v-if="!loginFlow">
<div
v-if="!loginFlow"
id="bottom-background"
>
<div class="seamless_mountains_demo_repeat"></div>
<div class="midground_foreground_extended2"></div>
</div>
@@ -104,9 +107,10 @@
footer, footer a {
background: transparent;
color: $purple-500;
&:hover {
color: $white;
}
}
footer a:hover {
color: $white;
}
h3 {
@@ -117,10 +121,6 @@
border-top-color: $purple-100;
}
.donate-text {
color: $purple-500;
}
.logo {
color: $purple-300;
}
@@ -129,42 +129,27 @@
color: $purple-500;
}
.social .d-flex:hover {
a {
color: $white;
}
svg {
fill: $white;
}
}
.social-circle {
background: $purple-50;
color: $purple-500;
.instagram svg {
svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
.bluesky svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
.facebook svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
}
.tumblr svg {
background-color: $purple-50;
fill: $purple-500;
&:hover {
fill: $white;
}
width: 24px;
height: 24px;
}
}
.btn-contribute {
background: $white;
box-shadow: none;
@@ -274,7 +259,8 @@ export default {
return 'purple-footer';
},
loginFlow () {
return ['login', 'register', 'username'].indexOf(this.$route.name) !== -1;
const loginRoutes = ['forgotPassword', 'login', 'register', 'resetPassword', 'username'];
return loginRoutes.indexOf(this.$route.name) !== -1;
},
showContentWrap () {
return this.$route.name !== 'news';
@@ -158,7 +158,7 @@
BY PURCHASING PREMIUM YOU EXPRESSLY UNDERSTAND AND AGREE TO OUR REFUND POLICY:
</p>
<p>
YOU CAN REQUEST A REFUND OF YOUR MOST RECENT PAYMENT TO US BY CONTACTING US AT <a href='mailto:admin@habitica.com'>ADMIN@HABITICA.COM</a>. THE AMOUNT OF YOUR REFUND, IF ANY, WILL BE BASED ON (1) THE AMOUNT OF YOUR PURCHASED BUT UNUSED SUBSCRIPTION BENEFITS AND (2) THE TERMS IMPOSED ON US BY OUR PAYMENT PROCESSING VENDORS (E.G., WITH RESPECT TO THE DURATION OF THE REFUND PERIOD).
YOU CAN REQUEST A REFUND OF YOUR MOST RECENT PAYMENT TO US BY CONTACTING US AT <a href="mailto:admin@habitica.com">ADMIN@HABITICA.COM</a>. THE AMOUNT OF YOUR REFUND, IF ANY, WILL BE BASED ON (1) THE AMOUNT OF YOUR PURCHASED BUT UNUSED SUBSCRIPTION BENEFITS AND (2) THE TERMS IMPOSED ON US BY OUR PAYMENT PROCESSING VENDORS (E.G., WITH RESPECT TO THE DURATION OF THE REFUND PERIOD).
</p>
<p>
FOR ANY CUSTOMER WHO PURCHASED PREMIUM IN APPLE INC.'s APP STORE ("APP STORE"), PLEASE CONTACT APPLE INC.'s SUPPORT TEAM: <a
@@ -1,65 +1,45 @@
<template>
<b-modal
id="broken-task-modal"
title="Broken Challenge"
size="sm"
:hide-footer="true"
:hide-header="true"
modal-class="broken-task-confirm-modal"
centered
>
<div
v-if="brokenChallengeTask && brokenChallengeTask.challenge"
class="modal-body"
class="modal-content-wrapper"
>
<div
v-if="brokenChallengeTask.challenge.broken === 'TASK_DELETED'
|| brokenChallengeTask.challenge.broken === 'CHALLENGE_TASK_NOT_FOUND'"
>
<h2>{{ $t('brokenTask') }}</h2>
<div>
<div class="top-bar"></div>
<div class="modal-body-content">
<div
class="icon-wrapper"
v-html="icons.alertIcon"
></div>
<h2 class="modal-title">
{{ modalTitle }}
</h2>
<p class="modal-subtitle">
{{ modalSubtitle }}
</p>
<div class="button-wrapper">
<button
class="btn btn-primary"
@click="unlink('keep')"
@click="keepAction()"
>
{{ $t('keepIt') }}
{{ keepButtonText }}
</button>
<button
class="btn btn-danger"
@click="removeTask(obj)"
@click="removeAction()"
>
{{ $t('removeIt') }}
</button>
</div>
</div>
<div v-if="brokenChallengeTask.challenge.broken === 'CHALLENGE_DELETED'">
<h2>{{ $t('brokenChallenge') }}</h2>
<div>
<button
class="btn btn-primary"
@click="unlink('keep-all')"
>
{{ $t('keepTasks') }}
{{ removeButtonText }}
</button>
<button
class="btn btn-danger"
@click="unlink('remove-all')"
class="btn-cancel"
@click="close()"
>
{{ $t('removeTasks') }}
</button>
</div>
</div>
<div v-if="brokenChallengeTask.challenge.broken === 'CHALLENGE_CLOSED'">
<h2 v-html="$t('challengeCompleted', {user: brokenChallengeTask.challenge.winner})"></h2>
<div>
<button
class="btn btn-primary"
@click="unlink('keep-all')"
>
{{ $t('keepTasks') }}
</button>
<button
class="btn btn-danger"
@click="unlink('remove-all')"
>
{{ $t('removeTasks') }}
{{ $t('cancel') }}
</button>
</div>
</div>
@@ -67,23 +47,175 @@
</b-modal>
</template>
<style scoped>
.modal-body {
padding-bottom: 2em;
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
::v-deep .broken-task-confirm-modal {
.modal-dialog {
max-width: 330px;
margin: auto;
}
.modal-content {
border-radius: 8px;
overflow: hidden;
border: none;
}
.modal-body {
padding: 0;
}
}
.modal-content-wrapper {
display: flex;
flex-direction: column;
}
.top-bar {
height: 8px;
background-color: $maroon-100;
}
.modal-body-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24px 24px;
}
.icon-wrapper {
margin-top: 40px;
width: 48px;
height: 48px;
::v-deep svg {
width: 48px;
height: 48px;
path {
fill: #DE3F3F;
}
}
}
.modal-title {
margin-top: 16px;
margin-bottom: 0;
color: $maroon-100;
font-family: 'Roboto Condensed', sans-serif;
font-weight: 700;
font-size: 20px;
text-align: center;
}
.modal-subtitle {
margin-top: 12px;
margin-bottom: 0;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
color: $gray-50;
}
.button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 16px;
gap: 8px;
}
.btn-cancel {
background: none;
border: none;
color: $purple-300;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
cursor: pointer;
padding: 8px 16px;
&:hover {
text-decoration: underline;
}
}
</style>
<script>
import { mapActions } from '@/libs/store';
import notifications from '@/mixins/notifications';
import alertIcon from '@/assets/svg/for-css/alert.svg?raw';
export default {
mixins: [notifications],
data () {
return {
brokenChallengeTask: {},
icons: Object.freeze({
alertIcon,
}),
};
},
computed: {
brokenType () {
return this.brokenChallengeTask.challenge?.broken;
},
isSingleTask () {
return this.brokenType === 'TASK_DELETED'
|| this.brokenType === 'CHALLENGE_TASK_NOT_FOUND';
},
brokenChallengeTaskCount () {
if (!this.brokenChallengeTask.challenge?.id) return 0;
const challengeId = this.brokenChallengeTask.challenge.id;
const tasksData = this.$store.state.tasks.data;
let count = 0;
['habits', 'dailys', 'todos', 'rewards'].forEach(type => {
if (tasksData[type]) {
count += tasksData[type].filter(
t => t.challenge && t.challenge.id === challengeId,
).length;
}
});
return count;
},
modalTitle () {
if (this.isSingleTask) {
return this.$t('brokenTask');
}
if (this.brokenType === 'CHALLENGE_CLOSED') {
return this.$t('challengeCompleted');
}
return this.$t('brokenChallenge');
},
modalSubtitle () {
if (this.isSingleTask) {
return this.$t('brokenTaskDescription');
}
if (this.brokenType === 'CHALLENGE_CLOSED') {
return this.$t('challengeCompletedDescription', { user: this.brokenChallengeTask.challenge?.winner });
}
return this.$t('brokenChallengeDescription');
},
keepButtonText () {
if (this.isSingleTask) {
return this.$t('keepIt');
}
return this.$t('keepTasks');
},
removeButtonText () {
if (this.isSingleTask) {
return this.$t('removeIt');
}
return this.$t('removeTasks');
},
},
mounted () {
this.$root.$on('handle-broken-task', task => {
this.brokenChallengeTask = { ...task };
@@ -99,8 +231,36 @@ export default {
unlinkOneTask: 'tasks:unlinkOneTask',
unlinkAllTasks: 'tasks:unlinkAllTasks',
}),
keepAction () {
if (this.isSingleTask) {
this.unlink('keep');
} else {
this.unlink('keep-all');
}
},
async removeAction () {
if (this.isSingleTask) {
await this.removeTask();
} else {
await this.unlink('remove-all');
}
},
async unlink (keepOption) {
if (keepOption.indexOf('-all') !== -1) {
if (keepOption === 'remove-all') {
const count = this.brokenChallengeTaskCount;
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:delete-task-confirm', {
title: count === 1 ? this.$t('deleteTask') : this.$t('deleteXTasks', { count }),
description: this.$t('brokenChallengeTaskCount', { count }),
message: this.$t('confirmDeleteTasks'),
buttonText: count === 1 ? this.$t('deleteTask') : this.$t('deleteXTasks', { count }),
resolve,
});
});
if (!confirmed) return;
}
await this.unlinkAllTasks({
challengeId: this.brokenChallengeTask.challenge.id,
keep: keepOption,
@@ -122,8 +282,14 @@ export default {
});
this.close();
},
removeTask () {
if (!window.confirm('Are you sure you want to delete this task?')) return; // eslint-disable-line no-alert
async removeTask () {
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:delete-task-confirm', {
message: this.$t('sureDelete'),
resolve,
});
});
if (!confirmed) return;
this.destroyTask(this.brokenChallengeTask);
this.close();
},
+4 -20
View File
@@ -87,7 +87,7 @@
ref="tasksList"
class="sortable-tasks"
:disabled="activeFilter.label === 'scheduled' || !canBeDragged()"
scrollSensitivity="64"
scroll-sensitivity="64"
:delay-on-touch-only="true"
:delay="100"
@update="taskSorted"
@@ -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: {
@@ -0,0 +1,223 @@
<template>
<b-modal
id="delete-task-confirm-modal"
:hide-footer="true"
:hide-header="true"
modal-class="delete-confirm-modal"
centered
>
<div class="modal-content-wrapper">
<div class="top-bar"></div>
<div class="modal-body-content">
<div
class="icon-wrapper"
v-html="icons.alertIcon"
></div>
<h2 class="modal-title">
{{ displayTitle }}
</h2>
<p
v-if="description"
class="modal-description"
>
{{ description }}
</p>
<p class="modal-subtitle">
{{ confirmationMessage }}
</p>
<div class="button-wrapper">
<button
class="btn btn-danger"
@click="confirm()"
>
{{ buttonText }}
</button>
<button
class="btn-cancel"
@click="cancel()"
>
{{ $t('cancel') }}
</button>
</div>
</div>
</div>
</b-modal>
</template>
<script>
import alertIcon from '@/assets/svg/for-css/alert.svg?raw';
export default {
data () {
return {
confirmationMessage: '',
taskType: '',
description: '',
customTitle: '',
customButtonText: '',
resolveCallback: null,
icons: Object.freeze({
alertIcon,
}),
};
},
computed: {
displayTitle () {
if (this.customTitle) return this.customTitle;
return this.$t('deleteType', { type: this.taskType });
},
buttonText () {
if (this.customButtonText) return this.customButtonText;
return this.displayTitle;
},
},
mounted () {
this.$root.$on('habitica:delete-task-confirm', config => {
this.confirmationMessage = config.message;
this.taskType = config.taskType || '';
this.description = config.description || '';
this.customTitle = config.title || '';
this.customButtonText = config.buttonText || '';
this.resolveCallback = config.resolve;
this.$root.$emit('bv::show::modal', 'delete-task-confirm-modal');
});
},
beforeDestroy () {
this.$root.$off('habitica:delete-task-confirm');
},
methods: {
confirm () {
if (this.resolveCallback) {
this.resolveCallback(true);
}
this.close();
},
cancel () {
if (this.resolveCallback) {
this.resolveCallback(false);
}
this.close();
},
close () {
this.$root.$emit('bv::hide::modal', 'delete-task-confirm-modal');
},
},
};
</script>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
::v-deep .delete-confirm-modal {
.modal-dialog {
max-width: 330px;
margin: auto;
}
.modal-content {
border-radius: 8px;
overflow: hidden;
border: none;
}
.modal-body {
padding: 0;
}
}
.modal-content-wrapper {
display: flex;
flex-direction: column;
}
.top-bar {
height: 8px;
background-color: $maroon-100;
}
.modal-body-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 0 24px 24px;
}
.icon-wrapper {
margin-top: 40px;
width: 48px;
height: 48px;
::v-deep svg {
width: 48px;
height: 48px;
path {
fill: #DE3F3F;
}
}
}
.modal-title {
margin-top: 16px;
margin-bottom: 0;
color: $maroon-100;
font-family: 'Roboto Condensed', sans-serif;
font-weight: 700;
font-size: 20px;
text-align: center;
}
.modal-description {
margin-top: 12px;
margin-bottom: 0;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
color: $gray-50;
}
.modal-description + .modal-subtitle {
margin-top: 16px;
}
.modal-subtitle {
margin-top: 12px;
margin-bottom: 0;
font-family: Roboto, sans-serif;
font-weight: 700;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
text-align: center;
color: $gray-50;
}
.button-wrapper {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin-top: 16px;
gap: 8px;
}
.btn-cancel {
background: none;
border: none;
color: $purple-300;
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 14px;
line-height: 24px;
letter-spacing: 0;
cursor: pointer;
padding: 8px 16px;
&:hover {
text-decoration: underline;
}
}
</style>
@@ -48,11 +48,19 @@
/>
<input
:ref="'checklistItem-' + $index"
v-model="item.text"
class="inline-edit-input checklist-item form-control"
type="text"
:disabled="disabled || disableEdit"
:class="summaryClass(item)"
@focus="setActiveItem($index)"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
<span
v-if="!disabled && !disableEdit"
@@ -81,15 +89,30 @@
</span>
<input
ref="newChecklistInput"
v-model="newChecklistItem"
class="inline-edit-input checklist-item form-control"
type="text"
:placeholder="$t('newChecklistItem')"
@keypress.enter="setHasPossibilityOfIMEConversion(false)"
@focus="setActiveItem(-1)"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="newChecklistEnterHandler($event)"
@keyup.enter="addChecklistItem($event, true)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
@blur="addChecklistItem($event, false)"
>
</div>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="activeFieldText"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
</b-collapse>
</div>
</template>
@@ -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();
}
});
},
},
};
</script>
@@ -187,6 +259,7 @@ export default {
@import '@/assets/scss/colors.scss';
.checklist-component {
position: relative;
.chevron-flip {
transform: translateY(-5px) rotate(180deg);
@@ -9,12 +9,27 @@
@toggle="openOrClose($event)"
>
<b-dropdown-header>
<div class="mb-2">
<div class="mb-2 search-input-wrapper">
<b-form-input
ref="searchInput"
v-model="search"
type="text"
:placeholder="searchPlaceholder"
@keyup.enter="handleSubmit"
@focus="setTextbox"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keydown.enter="searchEnterHandler($event)"
@keydown.esc="searchEscHandler($event)"
/>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="search"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
</div>
@@ -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;
+9 -2
View File
@@ -1177,9 +1177,16 @@ export default {
moveToBottom () {
this.$emit('moveTo', this.task, 'bottom');
},
destroy () {
async destroy () {
const type = this.$t(this.task.type);
if (!window.confirm(this.$t('sureDeleteType', { type }))) return; // eslint-disable-line no-alert
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:delete-task-confirm', {
message: this.$t('sureDeleteType', { type }),
taskType: type,
resolve,
});
});
if (!confirmed) return;
this.destroyTask(this.task);
this.$emit('taskDestroyed', this.task);
},
@@ -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)"
>
</div>
<div
@@ -92,11 +99,27 @@
</small>
</div>
<textarea
ref="notesTextarea"
v-model="task.notes"
class="form-control input-notes"
:class="cssClass('input')"
:placeholder="$t('addNotes')"
@focus="setActiveField('notes')"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
></textarea>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="activeFieldText"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
</div>
</div>
<div
@@ -150,14 +173,14 @@
<button
type="button"
class="habit-option-container no-transition
d-flex flex-column justify-content-center align-items-center"
d-flex flex-column justify-content-center align-items-center"
:class="!task.up ? cssClass('habit-control-disabled') : ''"
:disabled="challengeAccessRequired"
@click="toggleUpDirection()"
>
<div
class="habit-option-button no-transition
d-flex justify-content-center align-items-center mb-2"
d-flex justify-content-center align-items-center mb-2"
:class="task.up ? cssClass('bg') : ''"
>
<div
@@ -176,14 +199,14 @@
<button
type="button"
class="habit-option-container no-transition
d-flex flex-column justify-content-center align-items-center"
d-flex flex-column justify-content-center align-items-center"
:class="!task.down ? cssClass('habit-control-disabled') : ''"
:disabled="challengeAccessRequired"
@click="toggleDownDirection()"
>
<div
class="habit-option-button no-transition
d-flex justify-content-center align-items-center mb-2"
d-flex justify-content-center align-items-center mb-2"
:class="task.down ? cssClass('bg') : ''"
>
<div
@@ -382,6 +405,45 @@
</div>
</div>
</div>
<div
v-if="showStatAssignment"
class="stat-assignment option mt-3"
>
<div class="form-group row">
<label
v-once
class="col-12 mb-1"
>{{ $t('assignedStat') }}</label>
<div class="col-12">
<div class="stat-dropdown-container">
<select-list
:items="statOptions"
:value="task.attribute"
key-prop="key"
active-key-prop="key"
@select="task.attribute = $event.key"
>
<template #item="{ item, button }">
<div class="stat-option-content">
<span
class="stat-option-title"
:class="item.key"
>
{{ $t(item.label) }}
</span>
<span
v-if="!button"
class="stat-option-description"
>
{{ $t(item.description) }}
</span>
</div>
</template>
</select-list>
</div>
</div>
</div>
</div>
<div
v-if="task.type === 'habit' && !groupId"
class="option mt-3"
@@ -591,7 +653,7 @@
>
<button
class="btn btn-primary btn-footer
d-flex align-items-center justify-content-center"
d-flex align-items-center justify-content-center"
:class="{'btn-disabled': !canSave}"
type="button"
@click="submit()"
@@ -673,6 +735,7 @@
}
.task-modal-header {
position: relative;
color: $white;
width: 100%;
border-top-left-radius: 8px;
@@ -911,6 +974,87 @@
.streak-addon path {
fill: $gray-200;
}
.stat-dropdown-container {
.select-list {
.selectListItem {
margin-bottom: 0;
&:last-child {
margin-bottom: 0;
}
}
.selectListItem .dropdown-item {
padding: 8px 16px !important;
height: auto !important;
white-space: normal;
word-wrap: break-word;
&:hover,
&:focus {
background-color: rgba($purple-600, 0.25) !important;
}
}
.dropdown-toggle {
display: flex;
align-items: center;
.stat-option-content {
display: flex;
align-items: center;
.stat-option-title {
font-weight: normal;
color: $gray-50;
margin-bottom: 0;
}
}
}
}
.stat-option-content {
display: block;
width: 100%;
.stat-option-title {
display: block;
font-family: Roboto;
font-weight: 700;
font-size: 14px;
line-height: 1.71;
text-transform: capitalize;
margin-bottom: 4px;
&.str {
color: $maroon-100;
}
&.int {
color: $blue-50;
}
&.con {
color: $yellow-5;
}
&.per {
color: $purple-300;
}
}
.stat-option-description {
display: block;
font-family: Roboto;
font-weight: 400;
font-size: 12px;
line-height: 16px;
color: $gray-100;
margin-bottom: 0;
}
}
}
</style>
<style lang="scss" scoped>
@@ -1023,7 +1167,6 @@
.input-group-outer.disabled .input-group-text {
color: $gray-200;
}
</style>
<script>
@@ -1038,8 +1181,11 @@ import SelectMulti from './modal-controls/selectMulti';
import selectDifficulty from '@/components/tasks/modal-controls/selectDifficulty';
import selectTranslatedArray from '@/components/tasks/modal-controls/selectTranslatedArray';
import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import selectList from '@/components/ui/selectList';
import syncTask from '../../mixins/syncTask';
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
import positiveIcon from '@/assets/svg/positive.svg?raw';
import negativeIcon from '@/assets/svg/negative.svg?raw';
@@ -1061,15 +1207,19 @@ export default {
selectTranslatedArray,
toggleCheckbox,
lockableLabel,
selectList,
emojiAutoComplete,
},
directives: {
markdown: markdownDirective,
},
mixins: [syncTask],
mixins: [syncTask, autoCompleteHelperMixin],
// purpose is either create or edit, task is the task created or edited
props: ['task', 'purpose', 'challengeId', 'groupId'],
data () {
return {
textbox: null,
activeField: 'title',
showAssignedSelect: false,
newChecklistItem: null,
icons: Object.freeze({
@@ -1094,6 +1244,12 @@ export default {
con: 'constitution',
per: 'perception',
},
statOptions: [
{ key: 'str', label: 'strength', description: 'strTaskText' },
{ key: 'int', label: 'intelligence', description: 'intTaskText' },
{ key: 'con', label: 'constitution', description: 'conTaskText' },
{ key: 'per', label: 'perception', description: 'perTaskText' },
],
calendarHighlights: { dates: [new Date()] },
};
},
@@ -1187,6 +1343,16 @@ export default {
selectedTags () {
return this.getTagsFor(this.task);
},
activeFieldText () {
if (!this.task) return '';
return this.activeField === 'title' ? (this.task.text || '') : (this.task.notes || '');
},
showStatAssignment () {
return this.task.type !== 'reward'
&& !this.groupId
&& this.user.preferences.automaticAllocation === true
&& this.user.preferences.allocationMode === 'taskbased';
},
},
watch: {
task () {
@@ -1305,9 +1471,16 @@ export default {
}
this.$root.$emit('bv::hide::modal', 'task-modal');
},
destroy () {
async destroy () {
const type = this.$t(this.task.type);
if (!window.confirm(this.$t('sureDeleteType', { type }))) return; // eslint-disable-line no-alert
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:delete-task-confirm', {
message: this.$t('sureDeleteType', { type }),
taskType: type,
resolve,
});
});
if (!confirmed) return;
this.destroyTask(this.task);
this.$emit('taskDestroyed', this.task);
this.$root.$emit('bv::hide::modal', 'task-modal');
@@ -1349,6 +1522,35 @@ export default {
},
focusInput () {
this.$refs.inputToFocus.focus();
this.setActiveField('title');
},
setActiveField (field) {
this.activeField = field;
if (field === 'title') {
this.textbox = this.$refs.inputToFocus;
} else {
this.textbox = this.$refs.notesTextarea;
}
},
titleEnterHandler (e) {
const ac = this._getActiveAutocomplete();
if (ac && ac.selected !== null) {
e.preventDefault();
ac.makeSelection();
} else if (ac) {
ac.cancel();
}
},
selectedAutocomplete (newText, newCaret) {
if (this.activeField === 'title') {
this.task.text = newText;
} else {
this.task.notes = newText;
}
this.$nextTick(() => {
this.textbox.setSelectionRange(newCaret, newCaret);
this.textbox.focus();
});
},
async addTag (name) {
const tagResult = await this.createTag({ name });
+75 -1
View File
@@ -80,9 +80,17 @@
v-html="icons.drag"
></div>
<input
:ref="'tagInput-' + tagIndex"
v-model="tag.name"
class="tag-edit-input inline-edit-input form-control"
type="text"
@focus="setActiveTag(tagIndex)"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
<div
class="input-group-append"
@@ -100,11 +108,18 @@
class="col-6 dragSpace"
>
<input
ref="newTagInput"
v-model="newTag"
class="new-tag-item edit-tag-item inline-edit-input form-control"
type="text"
:placeholder="$t('newTag')"
@keydown.enter="addTag($event, tagsType.key)"
@focus="setActiveTag(-1)"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="newTagEnterHandler($event, tagsType.key)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
</div>
</draggable>
@@ -134,6 +149,15 @@
</div>
</div>
</div>
<emoji-auto-complete
v-if="editingTags"
ref="emojiAutocomplete"
:text="activeTagText"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedTagAutocomplete"
/>
<div class="filter-panel-footer clearfix">
<template v-if="editingTags === true">
<div class="text-center">
@@ -405,6 +429,8 @@ import dragIcon from '@/assets/svg/drag_indicator.svg?raw';
import { mapState, mapActions } from '@/libs/store';
import brokenTaskModal from './brokenTaskModal';
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
export default {
components: {
@@ -414,10 +440,12 @@ export default {
spells,
brokenTaskModal,
draggable,
emojiAutoComplete,
},
directives: {
markdown,
},
mixins: [autoCompleteHelperMixin],
data () {
return {
columns: ['habit', 'daily', 'todo', 'reward'],
@@ -445,10 +473,19 @@ export default {
newTag: null,
editingTask: null,
creatingTask: null,
textbox: null,
activeTagIndex: -1,
};
},
computed: {
...mapState({ user: 'user.data' }),
activeTagText () {
if (this.activeTagIndex === -1) {
return this.newTag || '';
}
const tag = this.tagsSnap.tags[this.activeTagIndex];
return tag ? tag.name || '' : '';
},
tagsByType () {
const userTags = this.user.tags;
const tagsByType = {
@@ -514,6 +551,43 @@ export default {
this.tagsSnap[key].push({ id: uuid(), name: this.newTag });
this.newTag = null;
},
setActiveTag (index) {
this.activeTagIndex = index;
if (index === -1) {
const refArr = this.$refs.newTagInput;
this.textbox = Array.isArray(refArr) ? refArr[0] : refArr;
} else {
const refArr = this.$refs[`tagInput-${index}`];
if (!refArr) {
this.textbox = null;
} else {
this.textbox = Array.isArray(refArr) ? refArr[0] : refArr;
}
}
},
newTagEnterHandler (e, key) {
const ac = this._getActiveAutocomplete();
if (ac && ac.selected !== null) {
e.preventDefault();
ac.makeSelection();
} else {
if (ac) ac.cancel();
this.addTag(e, key);
}
},
selectedTagAutocomplete (newText, newCaret) {
if (this.activeTagIndex === -1) {
this.newTag = newText;
} else {
this.tagsSnap.tags[this.activeTagIndex].name = newText;
}
this.$nextTick(() => {
if (this.textbox) {
this.textbox.setSelectionRange(newCaret, newCaret);
this.textbox.focus();
}
});
},
removeTag (index, key) {
const tagId = this.tagsSnap[key][index].id;
const indexInSelected = this.selectedTags.indexOf(tagId);
@@ -16,7 +16,7 @@
.badge-pin {
background-color: $white;
color: $gray-200;
color: $gray-100;
transition: none;
display: flex;
cursor: pointer;
@@ -32,8 +32,8 @@
}
.svg-icon {
width: 100%;
height: 100%;
width: 16px;
height: 16px;
}
}
@@ -12,7 +12,7 @@
<template #button-content>
<slot
name="item"
:item="selected || placeholder"
:item="selectedItem || placeholder"
:button="true"
>
<!-- Fallback content -->
@@ -134,6 +134,14 @@ export default {
}),
};
},
computed: {
selectedItem () {
if (this.activeKeyProp) {
return this.items.find(item => item[this.activeKeyProp] === this.selected);
}
return this.items.find(item => item === this.selected);
},
},
methods: {
getKeyProp (item) {
return this.keyProp ? item[this.keyProp] : item.key || item.identifier;
@@ -0,0 +1,92 @@
<template>
<div
class="selectable-card"
:class="{ selected }"
@click="$emit('click')"
>
<div
v-if="selected"
class="checkmark-corner"
>
<div
class="svg-icon check-icon"
v-html="icons.check"
></div>
</div>
<slot></slot>
</div>
</template>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.selectable-card {
position: relative;
background: $white;
border: 1px solid $gray-400;
border-radius: 8px;
padding: 16px;
cursor: pointer;
box-shadow: 0px 1px 2px 0px rgba(26, 24, 29, 0.08);
&:hover {
box-shadow: 0px 3px 6px 0px rgba(26, 24, 29, 0.16), 0px 3px 6px 0px rgba(26, 24, 29, 0.24);
}
&.selected {
border: 2px solid $purple-300;
padding: 15px;
box-shadow: 0px 3px 6px 0px rgba(26, 24, 29, 0.16), 0px 3px 6px 0px rgba(26, 24, 29, 0.24);
}
}
.checkmark-corner {
position: absolute;
top: 0;
left: 0;
width: 48px;
height: 48px;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
border-style: solid;
border-width: 48px 48px 0 0;
border-color: $purple-300 transparent transparent transparent;
border-radius: 6px 0 0 0;
}
.check-icon {
position: absolute;
top: 8px;
left: 8px;
width: 16px;
height: 16px;
color: $white;
}
}
</style>
<script>
import svgCheck from '@/assets/svg/check.svg?raw';
export default {
props: {
selected: {
type: Boolean,
default: false,
},
},
emits: ['click'],
data () {
return {
icons: Object.freeze({
check: svgCheck,
}),
};
},
};
</script>
@@ -25,8 +25,8 @@
type="checkbox"
:checked="isChecked"
:value="value"
@change="handleChange"
:disabled="disabled"
@change="handleChange"
>
<label
class="toggle-switch-label"
@@ -398,14 +398,29 @@
:placeholder="$t('imageUrl')"
>
</div>
<div class="form-group">
<div class="form-group" style="position: relative;">
<label>{{ $t('about') }}</label>
<textarea
ref="blurbTextarea"
v-model="editingProfile.blurb"
class="form-control"
rows="5"
:placeholder="$t('displayBlurbPlaceholder')"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
></textarea>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="editingProfile.blurb"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
<!-- include ../../shared/formatting-help-->
</div>
</div>
@@ -1001,6 +1016,8 @@ import mute from '@/assets/svg/mute.svg?raw';
import shadowMute from '@/assets/svg/shadow-mute.svg?raw';
import externalLinks from '../../mixins/externalLinks';
import { userCustomStateMixin } from '../../mixins/userState';
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
// @TODO: EMAILS.COMMUNITY_MANAGER_EMAIL
const COMMUNITY_MANAGER_EMAIL = 'admin@habitica.com';
@@ -1012,8 +1029,9 @@ export default {
MemberDetails,
profileStats,
toggleSwitch,
emojiAutoComplete,
},
mixins: [externalLinks, userCustomStateMixin('userLoggedIn')],
mixins: [externalLinks, userCustomStateMixin('userLoggedIn'), autoCompleteHelperMixin],
props: ['userId', 'startingPage'],
data () {
return {
@@ -1033,6 +1051,7 @@ export default {
mute,
shadowMute,
}),
textbox: null,
userIdToMessage: '',
editing: false,
editingProfile: {
@@ -1121,6 +1140,13 @@ export default {
userLoggedIn () {
this.loadUser();
},
editing (val) {
if (val) {
this.$nextTick(() => {
this.textbox = this.$refs.blurbTextarea;
});
}
},
},
mounted () {
this.loadUser();
@@ -1331,6 +1357,13 @@ export default {
this.$emit('toggled', this.isOpened);
},
selectedAutocomplete (newText, newCaret) {
this.editingProfile.blurb = newText;
this.$nextTick(() => {
this.textbox.setSelectionRange(newCaret, newCaret);
this.textbox.focus();
});
},
reportPlayer () {
this.$root.$emit('habitica::report-profile', {
memberId: this.user._id,
@@ -1340,7 +1373,7 @@ export default {
},
openAdminPanel () {
this.$router.push(`/admin-panel/${this.hero._id}`);
this.$router.push(`/admin/panel/${this.hero._id}`);
},
},
};
File diff suppressed because it is too large Load Diff
+28 -14
View File
@@ -15,46 +15,60 @@ export const autoCompleteHelperMixin = {
};
},
methods: {
_getActiveAutocomplete () {
if (this.$refs.autocomplete && this.$refs.autocomplete.searchActive) {
return this.$refs.autocomplete;
}
if (this.$refs.emojiAutocomplete && this.$refs.emojiAutocomplete.searchActive) {
return this.$refs.emojiAutocomplete;
}
return null;
},
autoCompleteMixinHandleTab (e) {
if (this.$refs.autocomplete.searchActive) {
const ac = this._getActiveAutocomplete();
if (ac) {
e.preventDefault();
if (e.shiftKey) {
this.$refs.autocomplete.selectPrevious();
ac.selectPrevious();
} else {
this.$refs.autocomplete.selectNext();
ac.selectNext();
}
}
},
autoCompleteMixinHandleEscape (e) {
if (this.$refs.autocomplete.searchActive) {
const ac = this._getActiveAutocomplete();
if (ac) {
e.preventDefault();
this.$refs.autocomplete.cancel();
ac.cancel();
}
},
autoCompleteMixinSelectNextAutocomplete (e) {
if (this.$refs.autocomplete.searchActive) {
const ac = this._getActiveAutocomplete();
if (ac) {
e.preventDefault();
this.$refs.autocomplete.selectNext();
ac.selectNext();
}
},
autoCompleteMixinSelectPreviousAutocomplete (e) {
if (this.$refs.autocomplete.searchActive) {
const ac = this._getActiveAutocomplete();
if (ac) {
e.preventDefault();
this.$refs.autocomplete.selectPrevious();
ac.selectPrevious();
}
},
autoCompleteMixinSelectAutocomplete (e) {
if (this.$refs.autocomplete.searchActive) {
if (this.$refs.autocomplete.selected !== null) {
const ac = this._getActiveAutocomplete();
if (ac) {
if (ac.selected !== null) {
e.preventDefault();
this.$refs.autocomplete.makeSelection();
ac.makeSelection();
} else {
// no autocomplete selected, newline instead
this.$refs.autocomplete.cancel();
ac.cancel();
}
}
},
+9 -1
View File
@@ -39,7 +39,15 @@ export default {
};
const purchaseForKey = currencyToPurchaseForKey[currency];
return window.confirm(this.$t(purchaseForKey, { cost })); // eslint-disable-line no-alert
return new Promise(resolve => {
this.$root.$emit('habitica:purchase-confirm', {
message: this.$t(purchaseForKey, { cost }),
currency,
cost,
resolve,
});
});
},
},
};
@@ -153,9 +153,23 @@
:placeholder="$t('needsTextPlaceholder')"
:maxlength="MAX_MESSAGE_LENGTH"
:class="{'has-content': newMessage.trim() !== '', 'disabled': newMessageDisabled}"
@keydown="autoCompleteMixinUpdateCarretPosition"
@keyup.ctrl.enter="sendPrivateMessage()"
@keydown.tab="autoCompleteMixinHandleTab($event)"
@keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)"
@keydown.down="autoCompleteMixinSelectNextAutocomplete($event)"
@keypress.enter="autoCompleteMixinSelectAutocomplete($event)"
@keydown.esc="autoCompleteMixinHandleEscape($event)"
>
</textarea>
<emoji-auto-complete
ref="emojiAutocomplete"
:text="newMessage"
:textbox="textbox"
:coords="mixinData.autoComplete.coords"
:caret-position="mixinData.autoComplete.caretPosition"
@select="selectedAutocomplete"
/>
</div>
<div
class="sub-new-message-row d-flex"
@@ -540,6 +554,7 @@ h3 {
}
.new-message-row {
position: relative;
width: 100%;
padding-left: 1.5rem;
padding-top: 1.5rem;
@@ -676,10 +691,12 @@ import PmNewMessageStarted from './pm-new-message-started.vue';
import StartNewConversationInputHeader from './start-new-conversation-input-header.vue';
import positiveIcon from '@/assets/svg/positive.svg?raw';
import NotificationMixins from '@/mixins/notifications';
import emojiAutoComplete from '@/components/chat/emojiAutoComplete';
import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper';
// extract to a shared path
const CONVERSATIONS_PER_PAGE = 10;
const PM_PER_PAGE = 10;
const PM_PER_PAGE = 50;
const UI_STATES = Object.freeze({
LOADING: 'LOADING',
@@ -700,13 +717,14 @@ export default defineComponent({
toggleSwitch,
userLink,
faceAvatar,
emojiAutoComplete,
},
filters: {
timeAgo (value) {
return moment(new Date(value)).fromNow();
},
},
mixins: [styleHelper, NotificationMixins],
mixins: [styleHelper, NotificationMixins, autoCompleteHelperMixin],
beforeRouteEnter (to, from, next) {
next(vm => {
const data = vm.$store.state.privateMessageOptions;
@@ -751,6 +769,7 @@ export default defineComponent({
/** @type {Record<string, PrivateMessages.PrivateMessageEntry[]>} */
messagesByConversation: {}, // cache {uuid: []}
textbox: null,
newMessage: '',
messages: [],
messagesLoading: false,
@@ -963,6 +982,15 @@ export default defineComponent({
}
},
},
watch: {
shouldShowInputPanel (val) {
if (val) {
this.$nextTick(() => {
this.textbox = this.$refs.textarea;
});
}
},
},
async mounted () {
this.$store.dispatch('common:setTitle', {
section: this.$t('messages'),
@@ -1224,6 +1252,13 @@ export default defineComponent({
triggerStartNewConversationState () {
this.showStartNewConversationInput = true;
},
selectedAutocomplete (newText, newCaret) {
this.newMessage = newText;
this.$nextTick(() => {
this.textbox.setSelectionRange(newCaret, newCaret);
this.textbox.focus();
});
},
async startConversationByUsername (targetUserName) {
// check if the target user exists in current conversations, select that conversation
/** @type {PrivateMessages.ConversationSummaryMessageEntry} */
@@ -217,8 +217,18 @@ export default {
}
},
async changeClassAndClose () {
if (!this.classDisabled && !window.confirm(this.$t('changeClassConfirmCost'))) {
return;
if (!this.classDisabled) {
const confirmed = await new Promise(resolve => {
this.$root.$emit('habitica:purchase-confirm', {
message: this.$t('changeClassConfirmCost'),
currency: 'gems',
cost: 3,
resolve,
});
});
if (!confirmed) {
return;
}
}
this.$root.$once('bv::hide::modal', () => {
@@ -1,7 +1,8 @@
<template>
<tr>
<td colspan="3"
<td
v-if="!mixinData.inlineSettingMixin.modalVisible"
colspan="3"
>
<div class="d-flex justify-content-between align-items-center">
<h3
@@ -18,8 +19,9 @@
</a>
</div>
</td>
<td colspan="3"
<td
v-if="mixinData.inlineSettingMixin.modalVisible"
colspan="3"
>
<h3
v-once
@@ -59,8 +61,8 @@
{{ $t('performanceAnalytics') }}
</label>
<toggle-switch
class="mb-auto"
v-model="user.preferences.analyticsConsent"
class="mb-auto"
@change="prefToggled()"
/>
</div>
@@ -151,14 +153,14 @@ import { mapState } from '@/libs/store';
import alert from '@/assets/svg/for-css/alert.svg?raw';
export default {
mixins: [
GenericUserPreferencesMixin,
InlineSettingMixin,
],
components: {
SaveCancelButtons,
ToggleSwitch,
},
mixins: [
GenericUserPreferencesMixin,
InlineSettingMixin,
],
data () {
return {
icons: Object.freeze({
+43 -24
View File
@@ -14,6 +14,8 @@
<bug-report-success-modal v-if="isUserLoaded" />
<external-link-modal />
<birthday-modal />
<purchase-confirm-modal v-if="isUserLoaded" />
<delete-task-confirm-modal v-if="isUserLoaded" />
<template v-if="isUserLoaded">
<privacy-banner />
<chat-banner />
@@ -138,6 +140,8 @@ import paymentsSuccessModal from '@/components/payments/successModal';
import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
import subCanceledModal from '@/components/payments/canceledModal';
import externalLinkModal from '@/components/externalLinkModal.vue';
import purchaseConfirmModal from '@/components/shops/purchaseConfirmModal.vue';
import deleteTaskConfirmModal from '@/components/tasks/deleteTaskConfirmModal.vue';
import spellsMixin from '@/mixins/spells';
import {
@@ -172,6 +176,8 @@ export default {
bugReportModal,
bugReportSuccessModal,
externalLinkModal,
purchaseConfirmModal,
deleteTaskConfirmModal,
},
mixins: [notifications, spellsMixin],
data () {
@@ -262,7 +268,6 @@ export default {
this.$store.dispatch('user:fetch'),
this.$store.dispatch('tasks:fetchUserTasks'),
]).then(() => {
this.$store.state.isUserLoaded = true;
let analyticsConsent = localStorage.getItem('analyticsConsent');
if (analyticsConsent !== null) {
analyticsConsent = analyticsConsent === 'true';
@@ -270,31 +275,11 @@ export default {
this.$store.dispatch('user:set', { 'preferences.analyticsConsent': analyticsConsent });
}
}
if (window && window['habitica-i18n']) {
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
return null;
}
}
if (window && window['habitica-i18n']) {
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
return null;
}
}
Analytics.updateUser();
return axios.get(
'/api/v4/i18n/browser-script',
{
language: this.user.preferences.language,
headers: {
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Expires: '0',
},
},
);
return this.loadAllTranslations();
}).then(() => {
const i18nData = window && window['habitica-i18n'];
this.$loadLocale(i18nData);
this.$store.state.isUserLoaded = true;
this.hideLoadingScreen();
// Adjust the timezone offset
@@ -310,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);
}
}
@@ -374,6 +363,36 @@ export default {
hideLoadingScreen () {
this.loading = false;
},
async loadContentTranslations () {
const contentTranslations = await axios.get(
'/api/v4/i18n/content',
{
language: this.user.preferences.language,
},
);
const i18nData = window && window['habitica-i18n'];
i18nData.strings = { ...i18nData.strings, ...contentTranslations.data };
this.$loadLocale(i18nData);
},
async loadAllTranslations () {
if (window && window['habitica-i18n']) {
if (this.user.preferences.language === window['habitica-i18n'].language.code) {
return this.loadContentTranslations();
}
}
await axios.get(
'/api/v4/i18n/core',
{
language: this.user.preferences.language,
headers: {
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Expires: '0',
},
},
);
return this.loadContentTranslations();
},
},
};
</script>
-6
View File
@@ -11,7 +11,6 @@ import { DEPRECATED_ROUTES } from '@/router/deprecated-routes';
// NOTE: when adding a page make sure to implement the `common:setTitle` action
const RegisterLoginReset = () => import(/* webpackChunkName: "auth" */'@/components/auth/registerLoginReset');
const Logout = () => import(/* webpackChunkName: "auth" */'@/components/auth/logout');
// Hall
@@ -79,11 +78,6 @@ const router = new VueRouter({
// in the route component to set a specific subtitle for the page.
routes: [
{ name: 'logout', path: '/logout', component: Logout },
{
name: 'resetPassword', path: '/reset-password', component: RegisterLoginReset, meta: { requiresLogin: false },
}, {
name: 'forgotPassword', path: '/forgot-password', component: RegisterLoginReset, meta: { requiresLogin: false },
},
{ name: 'tasks', path: '/', component: UserTasks },
{
name: 'userProfile',
@@ -57,6 +57,9 @@ export const STATIC_ROUTES = {
{
name: 'features', path: 'features', component: FeaturesPage, meta: { requiresLogin: false },
},
{
name: 'forgotPassword', path: '/forgot-password', component: RegisterLoginReset, meta: { requiresLogin: false },
},
{
name: 'front', path: 'front', component: HomePage, meta: { requiresLogin: false },
},
@@ -90,6 +93,9 @@ export const STATIC_ROUTES = {
{
name: 'register', path: '/register', component: RegisterLoginReset, meta: { requiresLogin: false },
},
{
name: 'resetPassword', path: '/reset-password', component: RegisterLoginReset, meta: { requiresLogin: false },
},
{
name: 'terms', path: 'terms', component: TermsPage, meta: { requiresLogin: false },
},
+1 -1
View File
@@ -3,7 +3,7 @@ import Vue from 'vue';
import * as Analytics from '@/libs/analytics';
export async function getChat (store, payload) {
const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat`);
const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat?limit=400`);
return response.data.data;
}
+1 -1
View File
@@ -122,7 +122,7 @@ export default defineConfig({
},
rollupOptions: {
output: {
experimentalMinChunkSize: 1000
experimentalMinChunkSize: 20000
}
}
},
+1 -2
View File
@@ -2,11 +2,10 @@
"challenge": "Предизвикателство",
"challengeDetails": "Предизвикателствата са обществени събития, в които играчите се състезават и печелят награди като изпълняват няколко свързани по някакъв начин задачи.",
"brokenChaLink": "Повредена връзка на предизвикателство",
"brokenTask": "Повредена връзка на предизвикателство: тази задача е била част от предизвикателство, но е била премахната от него. Какво бихте искали да направите?",
"keepIt": "Запазване",
"removeIt": "Премахване",
"brokenChallenge": "Повредена връзка на предизвикателство: тази задача е била част от предизвикателство, но то (или групата) е било изтрито. Какво бихте искали да направите с останалите задачи?",
"challengeCompleted": "Това предизвикателство е приключило и победителят е <span class=\"badge\"><%- user %></span>! Какво искате да направите с останалите задачи?",
"challengeCompleted": "Това предизвикателство е приключило и победителят е <span class=\"badge\"><%= user %></span>! Какво искате да направите с останалите задачи?",
"unsubChallenge": "Повредена връзка на предизвикателство: тази задача е била част от предизвикателство, но Вие сте се отписали от него. Какво искате да направите с останалите задачи?",
"challenges": "Предизвикателства",
"endDate": "Крайна дата",
+2 -2
View File
@@ -182,7 +182,7 @@
"questEggVelociraptorText": "Велоцираптор",
"questEggVelociraptorMountText": "Велоцираптор",
"questEggVelociraptorAdjective": "умен",
"eggNotes": "Намерете излюпваща отвара, която да излеете върху това яйце и от него ще се излюпи <%= eggAdjective(locale) %> <%= eggText(locale) %>.",
"eggNotes": "Намерете излюпваща отвара, която да излеете върху това яйце и от него ще се излюпи <%= eggAdjective %> <%= eggText %>.",
"hatchingPotionBase": "Нормален цвят",
"hatchingPotionWhite": "Бял цвят",
"hatchingPotionDesert": "Пустинен цвят",
@@ -211,7 +211,7 @@
"hatchingPotionGlow": "Светещо в тъмното",
"hatchingPotionFrost": "Скреж",
"hatchingPotionIcySnow": "Леден сняг",
"hatchingPotionNotes": "Излейте това върху яйце и от него ще се излюпи любимец с(ъс) <%= potText(locale) %>.",
"hatchingPotionNotes": "Излейте това върху яйце и от него ще се излюпи любимец с(ъс) <%= potText %>.",
"foodMeat": "Месо",
"foodMeatThe": "Месото",
"foodMeatA": "Месо",
+1 -1
View File
@@ -190,7 +190,7 @@
"messages": "Съобщения",
"emptyMessagesLine1": "Нямате съобщения",
"emptyMessagesLine2": "Можете да изпратите ново съобщение на потребител, като посетите профила им и докоснете бутона \"Съобщение\".",
"userSentMessage": "<span class=\"notification-bold\"><%- user %></span> Ви изпрати съобщение",
"userSentMessage": "<span class=\"notification-bold\"><%= user %></span> Ви изпрати съобщение",
"letsgo": "Хойде!",
"selected": "Избрано",
"howManyToBuy": "Колко искате да купите?",
+14 -14
View File
@@ -24,14 +24,14 @@
"userId": "Потребителски идентификатор",
"invite": "Покана",
"leave": "Напускане",
"invitedToParty": "Получихте покана за присъединяване към групата <span class=\"notification-bold\"><%- party %></span>",
"invitedToPrivateGuild": "Получихте покана за присъединяване към частната гилдия <span class=\"notification-bold\"><%- guild %></span>",
"invitedToPublicGuild": "Получихте покана за присъединяване към гилдията <span class=\"notification-bold-blue\"><%- guild %></span>",
"invitedToParty": "Получихте покана за присъединяване към групата <span class=\"notification-bold\"><%= party %></span>",
"invitedToPrivateGuild": "Получихте покана за присъединяване към частната гилдия <span class=\"notification-bold\"><%= guild %></span>",
"invitedToPublicGuild": "Получихте покана за присъединяване към гилдията <span class=\"notification-bold-blue\"><%= guild %></span>",
"invitationAcceptedHeader": "Поканата Ви беше приета",
"invitationAcceptedBody": "<%= username %> прие поканата Ви да се присъедини към <%= groupName %>!",
"systemMessage": "Системно съобщение",
"newMsgGuild": "Има нови публикации в <span class=\"notification-bold-blue\"><%- name %></span>",
"newMsgParty": "Има нови публикации в групата Ви — <span class=\"notification-bold-blue\"><%- name %></span>",
"newMsgGuild": "Има нови публикации в <span class=\"notification-bold-blue\"><%= name %></span>",
"newMsgParty": "Има нови публикации в групата Ви — <span class=\"notification-bold-blue\"><%= name %></span>",
"chat": "Съобщения",
"sendChat": "Изпращане на съобщението",
"group": "Група",
@@ -151,14 +151,14 @@
"onlyGroupLeaderCanEditTasks": "Нямате право да управлявате задачите!",
"onlyGroupTasksCanBeAssigned": "Само групови задачи могат да бъдат зададени",
"assignedTo": "Назначена на",
"assignedToUser": "Назначена на <strong><%- userName %></strong>",
"assignedToUser": "Назначена на <strong><%= userName %></strong>",
"assignedToMembers": "Назначена на <strong><%= userCount %> членове </strong>",
"assignedToYouAndMembers": "Назначена на Вас и още <strong><%= userCount %> членове</strong>",
"youAreAssigned": "Тази задача е назначена на Вас",
"taskIsUnassigned": "Тази задача не е зададена на никого",
"confirmUnClaim": "Наистина ли искате да оставите тази задача?",
"confirmNeedsWork": "Наистина ли искате да отбележите, че тази задача се нуждае от още работа?",
"userRequestsApproval": "<strong><%- userName %></strong> иска одобрение",
"userRequestsApproval": "<strong><%= userName %></strong> иска одобрение",
"userCountRequestsApproval": "<strong><%= userCount %> членове</strong> искат одобрение",
"youAreRequestingApproval": "Вие искате одобрение",
"chatPrivilegesRevoked": "Не можете да направите това, защото привилегиите Ви в чата са Ви били отнети. За детайли или запитване за връшане на привилегии, моля пратете email на нашия Обществен Оправител на admin@habitica.com или попитайте вашия родител или настойник да им прати email. Моля, напишете и потребителското си име в писмото. Ако модератор вече ви е казал че блокирането ви към чата е временно, няма нужда да пращате email.",
@@ -168,9 +168,9 @@
"claim": "Вземане на Задача",
"removeClaim": "Отказване от задачата",
"onlyGroupLeaderCanManageSubscription": "Само водачът на групата може да управлява абонамента ѝ",
"yourTaskHasBeenApproved": "Задачата Ви <span class=\"notification-green notification-bold\"><%- taskText %></span>, беше одобрена.",
"taskNeedsWork": "<span class=\"notification-bold\"><%- managerName %></span> отбеляза, че задачата <span class=\"notification-bold\"><%- taskText %></span> се нуждае от още работа.",
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%- user %></span> помоли следната задача да бъде одобрена: <span class=\"notification-bold\"><%- taskName %></span>",
"yourTaskHasBeenApproved": "Задачата Ви <span class=\"notification-green notification-bold\"><%= taskText %></span>, беше одобрена.",
"taskNeedsWork": "<span class=\"notification-bold\"><%= managerName %></span> отбеляза, че задачата <span class=\"notification-bold\"><%= taskText %></span> се нуждае от още работа.",
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%= user %></span> помоли следната задача да бъде одобрена: <span class=\"notification-bold\"><%= taskName %></span>",
"approve": "Одобряване",
"approveTask": "Одобряване на задачата",
"needsWork": "Нуждае се от още работа",
@@ -183,8 +183,8 @@
"userIsClamingTask": "`<%= username %> пое:` <%= task %>",
"approvalRequested": "Заявено е одобрение",
"cantDeleteAssignedGroupTasks": "Не можете да изтриете груповите задачи, които са Ви разпределени.",
"groupPlanUpgraded": "<strong><%- groupName %></strong> премина към групов план!",
"groupPlanCreated": "Групата <strong><%- groupName %></strong> беше създадена!",
"groupPlanUpgraded": "<strong><%= groupName %></strong> премина към групов план!",
"groupPlanCreated": "Групата <strong><%= groupName %></strong> беше създадена!",
"onlyGroupLeaderCanInviteToGroupPlan": "Само водачът на групата може да кани хора в група с абонамент.",
"paymentDetails": "Подробности за разплащането",
"aboutToJoinCancelledGroupPlan": "На път сте да се присъедините към група, чийто план е прекратен. НЯМА да получите безплатен абонамент.",
@@ -325,8 +325,8 @@
"PMDisabled": "Деактивиране на лични съобщения",
"groupActivityNotificationTitle": "<%= user %> публикува в <%= group %>",
"suggestedGroup": "Предложено, защото сте нови в Habitica.",
"taskClaimed": "<%- userName %> взеха задачата <span class=\"notification-bold\"><%- taskText %></span>.",
"youHaveBeenAssignedTask": "<%- managerName %> ви присвои задачата <span class=\"notification-bold\"><%- taskText %></span>.",
"taskClaimed": "<%= userName %> взеха задачата <span class=\"notification-bold\"><%= taskText %></span>.",
"youHaveBeenAssignedTask": "<%= managerName %> ви присвои задачата <span class=\"notification-bold\"><%= taskText %></span>.",
"userWithUsernameOrUserIdNotFound": "Потребителското име или Потребителският Идентификатор не бяха намерени.",
"usernameOrUserId": "Потребителско име или Потребителски Идентификатор",
"sendGiftToWhom": "На кой бихте искали да пратите подарък?",
+2 -3
View File
@@ -82,13 +82,12 @@
"paymentMethods": "Купуване чрез",
"paymentSuccessful": "Плащането Ви беше успешно!",
"paymentYouReceived": "Получихте:",
"paymentYouSentGems": "Изпратихте на <strong><%- name %></strong>:",
"paymentYouSentSubscription": "Изпратихте на <strong><%- name %></strong> <%= months %>-месечен абонамент за Хабитика.",
"paymentYouSentGems": "Изпратихте на <strong><%= name %></strong>:",
"paymentYouSentSubscription": "Изпратихте на <strong><%= name %></strong> <%= months %>-месечен абонамент за Хабитика.",
"paymentSubBilling": "Абонаментът Ви ще бъде таксуван с <strong>$<%= amount %></strong> всеки <strong><%= months %> месеца</strong>.",
"success": "Готово!",
"classGear": "Снаряжение за класа",
"classGearText": "Поздравления за избора на клас! Добавих новата Ви основна екипировка в инвентара Ви. Погледнете по-долу, за да я екипирате!",
"autoAllocate": "Автоматично разпределяне",
"spells": "Умения",
"skillsTitle": "Умения",
"toDo": "Задача",
+2 -2
View File
@@ -66,8 +66,8 @@
"mountNotOwned": "Не притежавате този прево.",
"feedPet": "Искате ли да дадете <%= text %> на <%= name %>?",
"raisedPet": "Вие отгледахте <%= pet %>!",
"petName": "<%= egg(locale) %> с(ъс) <%= potion(locale) %>",
"mountName": "<%= mount(locale) %> с(ъс) <%= potion(locale) %>",
"petName": "<%= egg %> с(ъс) <%= potion %>",
"mountName": "<%= mount %> с(ъс) <%= potion %>",
"keyToPets": "Ключ от зверилника за любимци",
"keyToPetsDesc": "Освобождаване на всички стандартни любимци, за да можете да ги съберете отново. (Това не засяга любимците от мисии и редките любимци.)",
"keyToMounts": "Ключ от зверилника за превози",
+9 -4
View File
@@ -104,15 +104,15 @@
"achievementSkeletonCrewModalText": "Posbíral/a jsi všechna kostnatá zvířata!",
"achievementSkeletonCrewText": "Posbíral/a všechna kostnatá zvířata.",
"achievementLegendaryBestiaryModalText": "Posbíral/a jsi všechny mytické mazlíčky!",
"achievementLegendaryBestiaryText": "Posbíral/a jsi všechny základní barvy mytických mazlíčků: draka, létajícího prasete, gryfona, mořského hada a jednorožce!",
"achievementLegendaryBestiaryText": "Posbíral/a všechny barvy mytických mazlíčků: drak, létající prase, gryfon, mořský hady a jednorožec!",
"achievementLegendaryBestiary": "Legendární bestiář",
"achievementSeasonalSpecialist": "Sezónní specialista",
"achievementVioletsAreBlueText": "Získal/a všechny cukrově modré mazlíčky.",
"achievementVioletsAreBlue": "Fialky jsou Modré",
"achievementVioletsAreBlue": "Fialky jsou modré",
"achievementVioletsAreBlueModalText": "Posbíral/a jsi všechny mazlíčky z Modré Cukrové Vaty!",
"achievementSeasonalSpecialistModalText": "Dokončl/a jsi všechny sezónní úkoly!",
"achievementDomesticatedModalText": "Sesbíral/a jsi všechna domácí zvířata!",
"achievementSeasonalSpecialistText": "Dokončil/a jsi všechny Jarní a Zimní sezónní úkoly: Honba za vajíčky, Pastičkář Santa, a najdi Cuba!",
"achievementSeasonalSpecialistText": "Splnil/a všechny jarní a zimní sezonní úkoly: Lov Vajec, Uvězněný Santa, a Najdi Mládě!",
"achievementWildBlueYonderText": "Ochočil/a všechny zvířata z Modré Cukrové Vaty.",
"achievementWildBlueYonderModalText": "Ochočil/a jsi všechny mazlíčky z Modré Cukrové Vaty!",
"achievementDomesticatedText": "Vylíhl/a všechna standardní zbarvení domácích zvířat: Fretka, morče, kohout, létající prasátko, krysa, králík, kůň a kráva!",
@@ -157,5 +157,10 @@
"achievementBonelessBossModalText": "Získal/a jsi všechny bezobratlé mazlíčky!",
"achievementDuneBuddy": "Kámoš z dun",
"achievementDuneBuddyText": "Vylíhl/a jsi všechny, v poušti se vyskytující, mazlíčky: pásovce, kaktus, lišku, žábu, hada a pavouka!",
"achievementDuneBuddyModalText": "Sesbíral jsi všechna zvířata žijící v poušti!"
"achievementDuneBuddyModalText": "Sesbíral jsi všechna zvířata žijící v poušti!",
"achievementRodentRulerText": "Vylíhly se všechny standardní barvy hlodavců: morčata, krysy a veverky!",
"achievementCatsModalText": "Nasbíral jsi všechny kočičí mazlíčky!",
"achievementRoughRiderModalText": "Nasbíral jsi všechny základní barvy nepohodlných mazlíčků a mountů!",
"achievementRodentRulerModalText": "Nasbíral jsi všechny hlodavce!",
"achievementCatsText": "Vylíhly se všechny standardní barvy kočičích mazlíčků: gepard, lev, šavlozubý tygr a tygr!"
}
+4 -1
View File
@@ -735,5 +735,8 @@
"backgroundMaskMakersWorkshopText": "Maskářova dílna",
"backgroundMaskMakersWorkshopNotes": "Vyzkoušej novou tvář v maskářově dílně.",
"backgroundCemeteryGateText": "Hřbitovní brána",
"backgroundCemeteryGateNotes": "Straš u hřbitovní brány."
"backgroundCemeteryGateNotes": "Straš u hřbitovní brány.",
"backgroundAutumnBridgeText": "Podzimní most",
"backgroundAutumnBridgeNotes": "Obdivuj krásu podzimního mostu.",
"backgroundInsideACrystalText": "Uvnitř krystalu."
}
+1 -2
View File
@@ -2,11 +2,10 @@
"challenge": "Výzva",
"challengeDetails": "Výzvy jsou komunitní události, ve kterých hráči soutěží a získávají odměny za plnění úkolů.",
"brokenChaLink": "Nefunkční odkaz na výzvu",
"brokenTask": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ale byl z ní odstraněn. Co chceš dělat?",
"keepIt": "Ponechat",
"removeIt": "Odstranit",
"brokenChallenge": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ale ta (nebo skupina, která ji vytvořila) byla odstraněna. Co chceš dělat s osiřelými úkoly?",
"challengeCompleted": "Výzva byla ukončena a vítězem se stal <span class=\"badge\"><%- user %></span>! Co chceš dělat s osiřelými úkoly?",
"challengeCompleted": "Výzva byla ukončena a vítězem se stal <span class=\"badge\"><%= user %></span>! Co chceš dělat s osiřelými úkoly?",
"unsubChallenge": "Nefunkční odkaz na výzvu: tento úkol byl součástí výzvy, ze které jsi se odhlásil/a. Co chceš dělat s osiřelými úkoly?",
"challenges": "Výzvy",
"endDate": "Končí",
+2 -2
View File
@@ -182,7 +182,7 @@
"questEggVelociraptorText": "Velociraptor",
"questEggVelociraptorMountText": "Velociraptor",
"questEggVelociraptorAdjective": "chytrý",
"eggNotes": "Najdi líhnoucí lektvar, nalij ho na vejce a to se vylíhne v <%= eggAdjective(locale) %> <%= eggText(locale) %>.",
"eggNotes": "Najdi líhnoucí lektvar, nalij ho na vejce a to se vylíhne v <%= eggAdjective %> <%= eggText %>.",
"hatchingPotionBase": "Základní",
"hatchingPotionWhite": "Bílý",
"hatchingPotionDesert": "Pouštní",
@@ -211,7 +211,7 @@
"hatchingPotionGlow": "Ve tmě svítící",
"hatchingPotionFrost": "Zmrzlý",
"hatchingPotionIcySnow": "Ledově Sněhový",
"hatchingPotionNotes": "Nalij ho na vejce a vylíhne se ti <%= potText(locale) %> mazlíček.",
"hatchingPotionNotes": "Nalij ho na vejce a vylíhne se ti <%= potText %> mazlíček.",
"foodMeat": "Maso",
"foodMeatThe": "Maso",
"foodMeatA": "Maso",
+13 -7
View File
@@ -3,9 +3,9 @@
"iosFaqStillNeedHelp": "Jestli máš otázku, která není na tomto seznamu nebo na [Wiki FAQ](http://habitica.fandom.com/wiki/FAQ), použij formulář Ask a Question v sekci Nápověda na horní liště rozhraní. Jsme rádi když můžeme pomoct.",
"androidFaqStillNeedHelp": "If you have a question that isn't on this list or on the [Wiki FAQ](http://habitica.fandom.com/wiki/FAQ), come ask in the Tavern chat under Menu > Tavern! We're happy to help.",
"webFaqStillNeedHelp": "Pokud máš otázku, která není na tomto seznamu nebo na [Wiki FAQ](https://habitica.fandom.com/wiki/FAQ), přijď se zeptat do [Cechu „Habitica Help‟](https://habitica.com/groups/guild/5481ccf3-5d2d-48a9-a871-70a7380cee5a)! Rádi ti pomůžeme.",
"webFaqAnswer25": "Habitica používá tři různé typy úkolů, které se přizpůsobují tvým potřebám: Návyky, Denní úkoly a Úkolníček.\n\nNávyky mohou být pozitivní či negativní a vyjadřují něco, co můžeš chtít zaznamenat několikrát denně, nebo dle nestálého rozvrhu. Pozitivní návyky ti získají odměny, jako zlaťáky a zkušenosti , zatímco negativní návyky způsobí, že ztratíš body zdraví. \n\nDenní úkoly jsou úkoly, které chceš splnit pravidelněji, například jednou denně, třikrát týdně, nebo čtyřikrát za měsíc. Nesplněné denní úkoly tě stojí body zdraví, ale zároveň čím jsou náročnější, tím lepší odměnu nabízejí.\n\nÚkolníček zahrnuje jednorázové úkoly, ze kterých po jejich splnění získáš odměny. Úkoly v úkolníčku mohou mít zadané datum dokončení, ale pokud ho nestihneš, neztratíš žádné zkušenostní body.\n\nVyber si takový typ úkolu, který ti nejlépe pomůže dosáhnout tvých cílů!",
"webFaqAnswer25": "Habitica používá tři různé typy úkolů, které se přizpůsobují tvým potřebám: Návyky, Denní úkoly a Úkolníček.\n\nNávyky mohou být pozitivní či negativní a vyjadřují něco, co můžeš chtít zaznamenat několikrát denně, nebo dle nestálého rozvrhu. Pozitivní návyky ti získají odměny, jako zlaťáky a zkušenosti , zatímco negativní návyky způsobí, že ztratíš body zdraví.\n\nDenní úkoly jsou úkoly, které chceš splnit pravidelněji, například jednou denně, třikrát týdně, nebo čtyřikrát za měsíc. Nesplněné denní úkoly tě stojí body zdraví, ale zároveň čím jsou náročnější, tím lepší odměnu nabízejí.\n\nÚkolníček zahrnuje jednorázové úkoly, ze kterých po jejich splnění získáš odměny. Úkoly v úkolníčku mohou mít zadané datum dokončení, ale pokud ho nestihneš, neztratíš žádné zkušenostní body.\n\nVyber si takový typ úkolu, který ti nejlépe pomůže dosáhnout tvých cílů!",
"webFaqAnswer26": "Pozitivní návyky (návyky, které chceš udržovat; měly by mít tlačítko plus)\n\n * Sněz vitamíny\n * Vyčisti si zuby\n * Hodina učení se\n\nNegativní návyky (návyky které chceš omezit nebo se jim zcela vyhnout; měly by mít tlačítko mínus)\n\n * Kouření\n * Bezmyšlenkovité scrollování\n * Kousání si nehtů\n\nOboustranné návyky (Návyky které mají jak pozitivní, tak negativní možnost; měly by mít tlačítko plus i mínus)\n\n * Pít vodu vs. Pít limonádu\n * Učit se vs. prokrastinovat\n\nNávrhy denních úkolů (úkoly, které chceš plnit pravidelně)\n * Umýt nádobí\n * Zalít kytky\n * 30 minut nějaké fyzické aktivity\n\nNávrhy úkolů do Úkolníčku (úkoly co chceš splnit jen jednou)\n\n * Objednat se k doktorovi\n * Zorganizovat obsah skříně\n * Dopsat esej",
"webFaqAnswer35": "Jakmile jsi nakrmil svého mazlíčka natolik, že vyrostl v dospělé zvíře, budeš ten typ mazlíčka muset nechat vylíhnout znovu, pokud ho chceš mít nadále ve stáji.\n\nPokud chceš vidět zvířata na mobilních aplikacích:\n\n * Na menu vyber “Mazlíčci & zvířata” (Pets & Mounts) a klikni na popisek Zvířata (Mounts)\n\nPokud chceš vidět zvířata na webových stránkách:\n\n * Z inventáře na menu vyber “Stáj” and sjeď dolů, k sekci Stáj",
"webFaqAnswer35": "Jakmile jsi nakrmil svého mazlíčka natolik, že vyrostl v dospělé zvíře, budeš ten typ mazlíčka muset nechat vylíhnout znovu, pokud ho chceš mít nadále ve stáji.\n\nPokud chceš vidět zvířata na mobilních aplikacích:\n\n * Na menu vyber “Mazlíčci & zvířata” (Pets & Mounts) a klikni na popisek Zvířata (Mounts)\n\nPokud chceš vidět zvířata na webových stránkách:\n\n * Z inventáře na menu vyber “Domácí zvířata a mounti” and sjeď dolů, k sekci Stáj",
"commonQuestions": "Časté otázky",
"faqQuestion25": "Jaké různé úkoly existují?",
"faqQuestion26": "Jaké úkoly mohu například vytvořit?",
@@ -13,19 +13,25 @@
"faqQuestion29": "Jak získám zpět Zdraví?",
"webFaqAnswer29": "Můžeš získat 15 bodů zdraví zakoupením Lektvaru zdraví ze sloupce Odměny za 25 zlaťáků. Navíc, pokud postoupíš do další úrovně, tak se ti všechno zdraví automaticky obnoví!",
"faqQuestion30": "Co se stane, když mi dojde zdraví?",
"webFaqAnswer30": "Pokud tvé zdraví dosáhne hodnoty nula, přijdeš o jednu úroveň, všechny zlaťáky a jeden kousek vybavení, který se dá znovu zakoupit.",
"webFaqAnswer30": "Pokud tvé zdraví dosáhne hodnoty nula, přijdeš o jednu úroveň, všechny zlaťáky a jeden kousek vybavení, který se dá znovu zakoupit. Můžete se znovu postavit plněním úkolů a opětovným zvyšováním úrovně.",
"faqQuestion31": "Proč jsem ztratil body, když jsem řešil úkol, který nebyl negativní?",
"webFaqAnswer31": "Když doděláš úkol a ztratíš zdraví i když bys správně neměl, narazil jsi na zpoždění, během kterého server synchronizoval změny na jiných platformách. Například, pokud použiješ zlaťáky, manu nebo ztratíš zkušenosti na aplikaci na mobilu a pak dokončíš akci na webově stránce, server jednoduše potvrzuje, že se všechno synchronizovalo.",
"faqQuestion32": "Kdy si mohu vybrat třídu?",
"webFaqAnswer32": "V Habitice existují čtyři třídy: Válečník, Mág, Zloděj a Léčitel. Všichni hráči začínají jako válečníci, dokud nedosáhnou 10. úrovně. Jakmile dosáhneš 10. úrovně, dostaneš na výběr, jestli chceš zůstat válečníkem, nebo si vybrat jinou třídu. \n\nKaždá třída využívá rozdílné vybavení a schopnosti. Pokud si nechceš vybírat třídu, můžeš vybrat „Zatím nic.“ Pokud sis zatím nevybral, můžeš později třídní systém vždycky znovu aktivovat v nastavení.",
"faqQuestion32": "Jak si mohu vybrat kurz?",
"webFaqAnswer32": "Všichni hráči začínají jako válečníci, dokud nedosáhnou 10. úrovně. Jakmile dosáhneš 10. úrovně, dostaneš na výběr, jestli chceš zůstat válečníkem, nebo si vybrat jinou třídu. \n\nKaždá třída využívá rozdílné vybavení a schopnosti. Pokud si nechceš vybírat třídu, můžeš vybrat „Zatím nic.“ Pokud sis zatím nevybral, můžeš později třídní systém vždycky znovu aktivovat v nastavení.\n\nPokud chcete změnit svou třídu po dosažení úrovně 10, můžete tak učinit pomocí Koule znovuzrození. Koule znovuzrození je k dispozici na trhu za 6 drahokamů na úrovni 50 nebo zdarma na úrovni 100.\n\nAlternativně můžete změnit třídu kdykoli v nastavení za 3 drahokamy. Tím se vaše úroveň nevynuluje jako v případě Koule znovuzrození, ale budete moci přerozdělit body dovedností, které jste nashromáždili při postupu na vyšší úroveň, tak aby odpovídaly vaší nové třídě.",
"faqQuestion33": "Co je to za modrou čáru s popisem Mana, která se objeví po dosažení 10. úrovně?",
"webFaqAnswer33": "Poté, co odemkneš třídní systém, tak odemkneš i schopnosti, jež ke svému použití vyžadují manu. Mana je učena tvou INT (inteligencí) a dá se měnit pomocí schopností a vybavení.",
"faqQuestion34": "Jaký typ jídla má rád můj mazlíček?",
"webFaqAnswer34": "Mazlíčci mají rádi jídla, která jim jdou barevně k srsti. Základní mazlíčci jsou výjimka, ale všichni základní mazlíčci mají rádi stejný předmět. Dole vidíš jídla, která mají specifičtí mazlíčci rádi:\n\n * Základní mazlíčci mají rádi maso\n * Bílí mazlíčci mají rádi mléko\n * Pouštní mazlíčci mají rádi brambory\n * Červení mazlíčci mají rádi jahody\n * Stínoví mazlíčci mají rádi čokoládu\n * Kostnatí mazlíčci mají rádi ryby\n * Zombie mazlíčci mají rádi hnijící maso\n * Cukrově růžoví mazlíčci mají rádi růžovou cukrovou vatu\n * Cukrově modří mazlíčci mají rádi modrou cukrovou vatu\n * Zlatí mazlíčci mají rádi med",
"faqQuestion35": "Nakrmil jsem svého mazlíčka a on zmizel! Co se stalo?",
"faqQuestion36": "Jak mohu změnit vzhled své postavy?",
"webFaqAnswer36": "Existuje nespočet způsobů jak změnit vzhled své postavy na Habitice! Můžeš změnit jeho tělesnou stavbu, barvu a styl vlasů, barvu kůže nebo třeba přidat brýle a pohybové pomůcky tím, že na menu vybereš Upravit postavu.\n\nAbys upravil postavu na mobilní aplikaci:\n * v menu vyber “Customize Avatar”\n\nAbys upravil postavu na webových stránkách:\n * Z uživatelského menu v navigaci, v pravém rohu, vyber \"Upravit postavu\"",
"webFaqAnswer36": "Existuje nespočet způsobů jak změnit vzhled své postavy na Habitice! Můžeš změnit jeho tělesnou stavbu, barvu a styl vlasů, barvu kůže nebo třeba přidat brýle a pohybové pomůcky tím, že na menu vybereš Přizpůsobit postavu.\n\nAbys upravil postavu na mobilní aplikaci:\n * v menu vyber “Customize Avatar”\n\nAbys upravil postavu na webových stránkách:\n * Z uživatelského menu v navigaci, v pravém rohu, vyber \"Přizpůsobit postavu\"",
"faqQuestion27": "Proč úkoly mění barvy?",
"webFaqAnswer27": "Barva úkolu je vizuální ukázkou hodnoty úkolu. Všechny úkoly začínají neutrálně žlutě, modrá je lepší a červená horší. Zde uvidíš jak typ úkolu určuje hodnotu úkolu:\n\nNávyky zmodrají nebo zčervenají podle toho, jestli klikneš na tlačítko plus nebo mínus. Pokud je nebudeš plnit, tak pozitivní a negativní úkoly oslabíš až na žlutou. Dvojité návyky mění barvy pouze na základě tvých zadání.\n\nDenní úkoly mění barvu podle toho, jak často jsou plněny a když se plní, stávají se modřejšími, nebo pokud jsou zanedbány, zčervenají.\n\nČím déle jsou úkoly v úkolníčku nesplněné, tím červenějšími se stávají.\n\nČím červenější úkol, tím víc zlaťáků a zkušeností získáš za jeho splnění, takže se vrhni i na ty nejdrsnější úkoly!",
"faqQuestion28": "Pokud potřebuji pauzu, mohu si pozastavit denní úkoly?"
"faqQuestion28": "Pokud potřebuji pauzu, mohu si pozastavit denní úkoly?",
"faqQuestion37": "Proč se mé vybavení neukazuje na mé postavě?",
"webFaqAnswer37": "Zkontrolujte, zda je zapnutá možnost Kostým. Pokud má váš avatar na sobě kostým, zobrazí se místo vaší bojové výstroje tato sada vybavení.\n\nZapnutí kostýmu v mobilních aplikacích:\n * V nabídce vyberte „Vybavení“ a najděte přepínač Kostým.\n\nZapnutí kostýmu na webových stránkách:\n * V inventáři vyberte „Vybavení“ a najděte přepínač Kostým v záložce Kostým v zásuvce Vybavení",
"faqQuestion38": "Proč nemohu zakoupit určité položky?",
"webFaqAnswer38": "Noví hráči Habitica mohou zakoupit pouze základní vybavení třídy válečník. Hráči musí nakupovat vybavení v pořadí, aby odemkli další kus.\n\nMnoho kusů vybavení je specifických pro danou třídu, což znamená, že hráč může zakoupit pouze vybavení patřící k jeho aktuální třídě.",
"faqQuestion39": "Kde mohu získat další vybavení?",
"faqQuestion40": "Co jsou gemy a jak je dostanu?"
}
+1 -1
View File
@@ -190,7 +190,7 @@
"messages": "Zprávy",
"emptyMessagesLine1": "Nemáš žádné zprávy",
"emptyMessagesLine2": "Novou zprávu uživateli/Česku můžeš poslat tak, že navštívíš jeho/její profil a klikneš na tlačítko “Zprávy”.",
"userSentMessage": "<span class=\"notification-bold\"><%- user %></span> ti poslal/a zprávu",
"userSentMessage": "<span class=\"notification-bold\"><%= user %></span> ti poslal/a zprávu",
"letsgo": "Pojďmě!",
"selected": "Vybrané",
"howManyToBuy": "Kolik by jsi chtěl koupit?",
+15 -15
View File
@@ -24,14 +24,14 @@
"userId": "Uživatelské ID",
"invite": "Pozvat",
"leave": "Odejít",
"invitedToParty": "Byl jsi pozván do družiny <span class=\"notification-bold\"><%- party %></span>",
"invitedToPrivateGuild": "Byl jsi pozván do soukromého cechu <span class=\"notification-bold\"><%- guild %></span>",
"invitedToPublicGuild": "Byl jsi pozván do cechu <span class=\"notification-bold-blue\"><%- guild %></span>",
"invitedToParty": "Byl jsi pozván do družiny <span class=\"notification-bold\"><%= party %></span>",
"invitedToPrivateGuild": "Byl jsi pozván do soukromého cechu <span class=\"notification-bold\"><%= guild %></span>",
"invitedToPublicGuild": "Byl jsi pozván do cechu <span class=\"notification-bold-blue\"><%= guild %></span>",
"invitationAcceptedHeader": "Tvá pozvánka byla přijata",
"invitationAcceptedBody": "<%= username %> přijal tvoji pozvánku do <%= groupName %>!",
"systemMessage": "Systémová zpráva",
"newMsgGuild": "<span class=\"notification-bold-blue\"><%- name %></span> má nový příspěvek",
"newMsgParty": "Tvá družina, <span class=\"notification-bold-blue\"><%- name %></span>, má nový příspěvek",
"newMsgGuild": "<span class=\"notification-bold-blue\"><%= name %></span> má nový příspěvek",
"newMsgParty": "Tvá družina, <span class=\"notification-bold-blue\"><%= name %></span>, má nový příspěvek",
"chat": "Chat",
"sendChat": "Poslat zprávu",
"group": "Skupina",
@@ -151,14 +151,14 @@
"onlyGroupLeaderCanEditTasks": "Not authorized to manage tasks!",
"onlyGroupTasksCanBeAssigned": "Only group tasks can be assigned",
"assignedTo": "Přiřadit k",
"assignedToUser": "Přiřazeno <strong><%- userName %></strong>",
"assignedToUser": "Přiřazeno <strong><%= userName %></strong>",
"assignedToMembers": "Přiřazeno <strong><%= userCount %> members</strong>",
"assignedToYouAndMembers": "Přiřazeno vám a <strong><%= userCount %> members</strong>",
"youAreAssigned": "Jsi přiřazen/a k tomuto úkolu",
"taskIsUnassigned": "This task is unassigned",
"confirmUnClaim": "Are you sure you want to unclaim this task?",
"confirmNeedsWork": "Are you sure you want to mark this task as needing work?",
"userRequestsApproval": "<strong><%- userName %></strong> požaduje schválení",
"userRequestsApproval": "<strong><%= userName %></strong> požaduje schválení",
"userCountRequestsApproval": "<strong><%= userCount %> members</strong> požadují schválení",
"youAreRequestingApproval": "You are requesting approval",
"chatPrivilegesRevoked": "Toto nelze provést, protože vaše oprávnění k chatu byla odstraněna. Chcete-li získat další informace nebo se zeptat, zda lze vaše oprávnění vrátit, pošlete e-mail našemu komunitnímu manažerovi na adrese admin@habitica.com nebo požádejte svého rodiče nebo zákonného zástupce o zaslání e-mailu. Do e-mailu uveďte prosím své @uživatelskéjméno. Pokud vám moderátor již řekl, že váš zákaz chatu je dočasný, nemusíte posílat e-maily.",
@@ -168,9 +168,9 @@
"claim": "Nárokovat úkol",
"removeClaim": "Remove Claim",
"onlyGroupLeaderCanManageSubscription": "Only the group leader can manage the group's subscription",
"yourTaskHasBeenApproved": "Váš úkol <span class=\"notification-green notification-bold\"><%- taskText %></span> byl schválený.",
"taskNeedsWork": "<span class=\"notification-bold\"><%- managerName %></span> marked <span class=\"notification-bold\"><%- taskText %></span> as needing additional work.",
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%- user %></span> requests approval for <span class=\"notification-bold\"><%- taskName %></span>",
"yourTaskHasBeenApproved": "Váš úkol <span class=\"notification-green notification-bold\"><%= taskText %></span> byl schválený.",
"taskNeedsWork": "<span class=\"notification-bold\"><%= managerName %></span> marked <span class=\"notification-bold\"><%= taskText %></span> as needing additional work.",
"userHasRequestedTaskApproval": "<span class=\"notification-bold\"><%= user %></span> requests approval for <span class=\"notification-bold\"><%= taskName %></span>",
"approve": "Approve",
"approveTask": "Approve Task",
"needsWork": "Needs Work",
@@ -183,8 +183,8 @@
"userIsClamingTask": "`<%= username %> has claimed:` <%= task %>",
"approvalRequested": "Approval Requested",
"cantDeleteAssignedGroupTasks": "Can't delete group tasks that are assigned to you.",
"groupPlanUpgraded": "<strong><%- groupName %></strong> was upgraded to a Group Plan!",
"groupPlanCreated": "<strong><%- groupName %></strong> was created!",
"groupPlanUpgraded": "<strong><%= groupName %></strong> was upgraded to a Group Plan!",
"groupPlanCreated": "<strong><%= groupName %></strong> was created!",
"onlyGroupLeaderCanInviteToGroupPlan": "Only the group leader can invite users to a group with a subscription.",
"paymentDetails": "Payment Details",
"aboutToJoinCancelledGroupPlan": "You are about to join a group with a canceled plan. You will NOT receive a free subscription.",
@@ -321,8 +321,8 @@
"allAssignedCompletion": "All - Completes when all assigned users finish",
"groupActivityNotificationTitle": "<%= user %> publikoval v <%= group %>",
"suggestedGroup": "Navrženo, protože jste v Habitica nový/á.",
"taskClaimed": "<%- userName %> nárokoval úkol <span class=\"notification-bold\"><%- taskText %></span>.",
"youHaveBeenAssignedTask": "<%- managerName %> vám přidělil úkol <span class=\"notification-bold\"><%- taskText %></span>.",
"taskClaimed": "<%= userName %> nárokoval úkol <span class=\"notification-bold\"><%= taskText %></span>.",
"youHaveBeenAssignedTask": "<%= managerName %> vám přidělil úkol <span class=\"notification-bold\"><%= taskText %></span>.",
"pmReported": "Děkujeme za nahlášení této zprávy.",
"newPartyPlaceholder": "Zadej jméno tvé družiny.",
"userWithUsernameOrUserIdNotFound": "Uživatelské jméno nebo uživatelské ID nebylo nalezeno.",
@@ -336,7 +336,7 @@
"PMDisabled": "Zakaž soukromé zprávy",
"unassigned": "Nepřiřazeno",
"claimRewards": "Vyzvedni si odměnu",
"assignedDateAndUser": "Přiřazeno uživatelem/kou <strong>@<%- username %></strong> dne <strong><%= date %></strong>",
"assignedDateAndUser": "Přiřazeno uživatelem/kou <strong>@<%= username %></strong> dne <strong><%= date %></strong>",
"assignedDateOnly": "Přiřazeno k <strong><%= date %></strong>",
"managerNotes": "Poznámky manažera",
"thisTaskApproved": "Tento úkol byl schválen",
+2 -3
View File
@@ -82,13 +82,12 @@
"paymentMethods": "Platební metody",
"paymentSuccessful": "Tvá platba proběhla úspěšně!",
"paymentYouReceived": "Obdržel jsi:",
"paymentYouSentGems": "Poslal/a jsi <strong><%- name %></strong>:",
"paymentYouSentSubscription": "Poslal/a jsi <strong><%- name %></strong> předplatné na <%= months %>-měsíce/ů v Habitica.",
"paymentYouSentGems": "Poslal/a jsi <strong><%= name %></strong>:",
"paymentYouSentSubscription": "Poslal/a jsi <strong><%= name %></strong> předplatné na <%= months %>-měsíce/ů v Habitica.",
"paymentSubBilling": "Tvoje předplatné ve výši <strong>$<%= amount %></strong> bude účtovano každé/ých <strong><%= months %> měsíce/ů </strong>.",
"success": "Úspěch!",
"classGear": "Vybavení pro tvé povolání",
"classGearText": "Gratuluji k vybrání povolání! Přidal jsem ti základní zbraň do tvého inventáře. Podívej se dolů a vybav se!",
"autoAllocate": "Připisovat automaticky",
"spells": "Dovednosti",
"skillsTitle": "<%= classStr %> Dovednosti",
"toDo": "úkol",

Some files were not shown because too many files have changed in this diff Show More