Compare commits

..

261 Commits

Author SHA1 Message Date
SabreCat
987a27ffa1 5.15.0 2023-12-29 16:07:57 -06:00
SabreCat
7fb1c9db8c Squashed commit of the following:
commit 5b2f90356b
Author: SabreCat <sabrecat@gmail.com>
Date:   Fri Dec 15 16:19:02 2023 -0600

    fix(migration): full URL

commit a2962c0d10
Author: SabreCat <sabrecat@gmail.com>
Date:   Fri Dec 15 15:57:21 2023 -0600

    feat(migration): add birthday notif

commit 9da8e39dd0
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Dec 15 12:32:25 2023 -0500

    feat(script): awards 2024 Habitica birthday robes

commit 7de6b80ee6
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Dec 14 13:11:07 2023 -0500

    feat(content): add Habitica birthday item

commit 1bb95f5867
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Dec 14 11:43:12 2023 -0500

    fix(dates): fix availability dates to canonical

commit 8e7f49f253
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Dec 14 11:37:44 2023 -0500

    feat(content): make 2024 default backgroumd, fix errors

commit acc30f044e
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Dec 13 16:53:33 2023 -0500

    feat(content): the rest of the January content

commit 6a5e45c6d0
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Dec 13 16:49:33 2023 -0500

    feat(content): January content
2023-12-29 16:07:43 -06:00
SabreCat
f66b05f707 fix(potions): use normal release date notes 2023-12-29 15:57:46 -06:00
SabreCat
f8ba191eea fix(gala): correct seasonal shop quests/spell 2023-12-19 10:07:32 -06:00
SabreCat
a03d265cd3 5.14.2 2023-12-18 15:51:02 -06:00
SabreCat
0dbd597d0b Squashed commit of the following:
commit d03bdee783deff98ee24cc211f1498525bf36c70
Merge: b31ba93233 70f5aa1f55
Author: SabreCat <sabe@habitica.com>
Date:   Mon Dec 18 15:47:29 2023 -0600

    Merge branch 'release' into sabrecat/group-task-fixes

commit b31ba93233
Author: SabreCat <sabe@habitica.com>
Date:   Tue Oct 10 12:02:41 2023 -0500

    fix(groups): missing dot path step
    Also sync assignments on unassign as well as assign

commit 8012f0294d
Author: SabreCat <sabe@habitica.com>
Date:   Tue Oct 10 11:47:59 2023 -0500

    fix(groups): use assigned keys as source of truth

commit 616b1e1486
Author: SabreCat <sabe@habitica.com>
Date:   Tue Oct 10 11:33:28 2023 -0500

    fix(groups): clean up groups to copy tasks
    Fixes #14916
2023-12-18 15:50:58 -06:00
SabreCat
b97f85fa60 5.14.1 2023-12-18 07:25:01 -06:00
SabreCat
0a06fe6d2e fix(css): white g1g1 text in buy-gems 2023-12-18 07:24:42 -06:00
SabreCat
70f5aa1f55 5.14.0 2023-12-17 19:44:18 -06:00
SabreCat
5a384d8d16 chore(subproj): update habitica-images 2023-12-17 19:37:32 -06:00
SabreCat
4fea87af8a Squashed commit of the following:
commit 0e20f421645beaf231dd607b1c1d8db62e5804e4
Author: SabreCat <sabrecat@gmail.com>
Date:   Thu Dec 14 15:40:22 2023 -0600

    fix(g1g1): don't break modal off promo season

commit ca5436d05044d256303e9c5dcdf358b64467ae66
Author: SabreCat <sabe@habitica.com>
Date:   Tue Nov 28 20:42:55 2023 -0600

    fix(lint): move import

commit 08f58318da1a06c2d94227afd3224a5571990bac
Author: SabreCat <sabe@habitica.com>
Date:   Tue Nov 28 20:26:14 2023 -0600

    fix(vue): parenthesis

commit 8701ee330365ad0615b2179b801f8e15c3109bdd
Author: SabreCat <sabe@habitica.com>
Date:   Tue Nov 28 20:09:30 2023 -0600

    fix(g1g1): dynamically display end dates
2023-12-17 19:36:45 -06:00
SabreCat
8a45f753ca Merge branch '2023-winner-funderlamb' into release
by @CuriousMagpie
2023-12-17 19:36:15 -06:00
negue
19253cd9b5 Remove Storybook files and packages (#15040) 2023-12-13 20:18:13 +01:00
Natalie
eac5e58ac7 (fix): add quest modal text horizontal padding (#15053) 2023-12-11 16:55:24 -06:00
SabreCat
3eee1a572e Merge branch 'release' into develop 2023-12-07 16:16:25 -06:00
SabreCat
f572aa442e fix(lint): merge #14942 2023-12-07 16:15:53 -06:00
SabreCat
101be5d6a6 5.13.2 2023-12-06 13:36:08 -06:00
SabreCat
1c9c9908c5 fix(dev): use working version of MongoDB on run-rs
https://github.com/vkarpov15/run-rs/issues/47
2023-12-06 13:22:11 -06:00
Phillip Thelen
916cb03a3a automatic lint fixes (#14942)
* automatic lint fixes

* rerun lint

---------

Co-authored-by: negue <eugen.bolz@gmail.com>
2023-12-04 20:37:05 +01:00
SabreCat
bb095ae296 5.13.1 2023-12-01 16:44:47 -06:00
SabreCat
97c63e2be7 fix(faq): correct duped index 2023-12-01 16:44:40 -06:00
SabreCat
b7c6ded375 5.13.0 2023-11-28 15:08:17 -06:00
Natalie
8db8a8267b feat(content): December 2023 Content (#15023)
* feat(content): add December subscriber items, backgrounds, and enchanted armoire

* feat(content): correct item slot for armoire, change release dates for testing

* fix(backgrounds): correct start date

* fix(armoire): Add canonical start date

* fix(test): only count released items

---------

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
Co-authored-by: SabreCat <sabe@habitica.com>
2023-11-28 15:01:27 -06:00
SabreCat
4b9a5472bd 5.12.2 2023-11-22 15:35:41 -06:00
SabreCat
a374add2eb Squashed commit of the following:
commit f1cf5ea5831c24059f3a18f7d0888d30eb0efbc1
Author: SabreCat <sabe@habitica.com>
Date:   Fri Nov 3 16:53:13 2023 -0500

    fix(market): show selected class correctly
2023-11-22 15:35:12 -06:00
SabreCat
592c178544 Squashed commit of the following:
commit 0928372c649ebc8d298b72692e0accca4e912637
Author: SabreCat <sabe@habitica.com>
Date:   Tue Nov 21 15:02:53 2023 -0600

    fix(news): center images
2023-11-22 15:34:46 -06:00
Natalie
ed907c1ae5 fix(dates): upgrade dynamic end dating for quest bundle endings (#15015)
* fix(quest text): Updated end date for Oddballs to November 30.

* fix(dates): remove hard-coded bundle end-dates

* fix(dates): add dynamic dating to end of quest bundles

* fix(dates): commas are important

* fix(dates): November only has 30 days

* fix(dates): upgrade dynamic end dating for quest bundle endings
2023-11-22 15:28:23 -06:00
Natalie
021825ebb9 fix(shop): add November magic hatching potions to featured items in shop (#15014) 2023-11-22 15:14:42 -06:00
SabreCat
45aae7ff62 fix(test): challenge unlink issues 2023-11-16 13:45:18 -06:00
SabreCat
47bd890dd7 5.12.1 2023-11-14 15:49:32 -06:00
Natalie
add435d4f7 fix(dates) remove hard-coded end dates on bundles (#15011)
* fix(quest text): Updated end date for Oddballs to November 30.

* fix(dates): remove hard-coded bundle end-dates
2023-11-14 15:48:59 -06:00
SabreCat
d262a16269 Merge branch 'release' into develop 2023-11-13 14:37:49 -06:00
Weblate
aeef31d0d4 Merge branch 'develop' of github.com:HabitRPG/habitica into develop 2023-11-10 15:33:28 +01:00
SabreCat
5717e58613 Merge branch 'develop' into release 2023-11-07 08:59:53 -06:00
SabreCat
535477b804 5.12.0 2023-11-07 08:57:35 -06:00
Natalie
a80ef78be0 2023 November Content Prebuild (#14954)
* feat(content): October sub items

* feat(content): October bgs and armoire

* feat(content): add Dune Buddy achievement

* feat(releases): timed Backgrounds and Armoire

* feat(content): timed release achievement

* feat(content): November subscriber items

* feat(content): November pet quest bundle

* feat(content): November magic hatching potions

* feat(content): November backgrounds and armoire

* feat(content): new achievement code -- needs work

* update package.lock on local/origin repos

* fix(content): added October headgear info and November set name

* fix(typo): spelling is important

* feat(content): added logic to allow for pets & mounts to be in one achievement and fixed issues with image

* fix(armoire): correct month

* fix(armoire): deprecate armoireEmpty flag

---------

Co-authored-by: SabreCat <sabe@habitica.com>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2023-11-07 08:55:50 -06:00
Nazar Paruna
f119304d9e Translated using Weblate (Ukrainian)
Currently translated at 82.4% (113 of 137 strings)

Translation: Habitica/Faq
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
2023-11-04 18:15:42 +01:00
Zachary Flower
cb168338ee Calculate background visibility, even if the task list is empty (#14870) 2023-11-03 15:58:58 -05:00
Phillip Thelen
1892d6288a Clean up translations (#14985) 2023-11-03 15:54:48 -05:00
Weblate
1aa81d1440 Added translation using Weblate (Chinese (Simplified) (zh_HK))
Added translation using Weblate (tl_PH (generated) (tl_PH))

Added translation using Weblate (no (generated) (no))

Added translation using Weblate (hi_IN (generated) (hi_IN))

Added translation using Weblate (fa_IR (generated) (fa_IR))

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (226 of 226 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (424 of 424 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.9% (1598 of 2961 strings)

Translated using Weblate (Ukrainian)

Currently translated at 79.5% (109 of 137 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 65.6% (90 of 137 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.3% (1579 of 2961 strings)

Translated using Weblate (Russian)

Currently translated at 98.3% (2911 of 2961 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Ukrainian)

Currently translated at 60.5% (83 of 137 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (158 of 158 strings)

Translated using Weblate (Ukrainian)

Currently translated at 57.6% (79 of 137 strings)

Added translation using Weblate (Chinese (Simplified) (zh_HK))

Added translation using Weblate (tl_PH (generated) (tl_PH))

Added translation using Weblate (no (generated) (no))

Added translation using Weblate (hi_IN (generated) (hi_IN))

Added translation using Weblate (fa_IR (generated) (fa_IR))

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (226 of 226 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (424 of 424 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.3% (1579 of 2961 strings)

Translated using Weblate (Ukrainian)

Currently translated at 56.9% (78 of 137 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (256 of 256 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (226 of 226 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (424 of 424 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.0% (1570 of 2961 strings)

Translated using Weblate (Dutch)

Currently translated at 84.1% (2492 of 2961 strings)

Translated using Weblate (Ukrainian)

Currently translated at 39.4% (54 of 137 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (158 of 158 strings)

Translated using Weblate (Ukrainian)

Currently translated at 89.0% (228 of 256 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (108 of 108 strings)

Translated using Weblate (Ukrainian)

Currently translated at 89.0% (228 of 256 strings)

Translated using Weblate (Dutch)

Currently translated at 96.2% (152 of 158 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (424 of 424 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (424 of 424 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (151 of 151 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 96.2% (152 of 158 strings)

Translated using Weblate (French)

Currently translated at 100.0% (256 of 256 strings)

Translated using Weblate (French)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (French)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (French)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (French)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (French)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 56.0% (51 of 91 strings)

Translated using Weblate (French)

Currently translated at 78.5% (201 of 256 strings)

Translated using Weblate (French)

Currently translated at 99.2% (139 of 140 strings)

Translated using Weblate (French)

Currently translated at 100.0% (151 of 151 strings)

Translated using Weblate (French)

Currently translated at 100.0% (151 of 151 strings)

Translated using Weblate (French)

Currently translated at 100.0% (151 of 151 strings)

Translated using Weblate (Korean)

Currently translated at 72.1% (114 of 158 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 42.8% (39 of 91 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 98.4% (186 of 189 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 91.7% (145 of 158 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 64.8% (166 of 256 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (2878 of 2953 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (225 of 225 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.8% (419 of 424 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.3% (150 of 151 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.4% (760 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 86.7% (222 of 256 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (225 of 225 strings)

Translated using Weblate (Ukrainian)

Currently translated at 95.5% (108 of 113 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.8% (419 of 424 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (151 of 151 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (819 of 819 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (158 of 158 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (158 of 158 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (140 of 140 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (279 of 279 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (French)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (French)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (French)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (French)

Currently translated at 100.0% (158 of 158 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Russian)

Currently translated at 98.8% (419 of 424 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (2878 of 2953 strings)

Translated using Weblate (Portuguese)

Currently translated at 60.1% (1775 of 2953 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.7% (224 of 239 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.4% (186 of 189 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Russian)

Currently translated at 99.3% (814 of 819 strings)

Translated using Weblate (French)

Currently translated at 100.0% (819 of 819 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (213 of 213 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (189 of 189 strings)

Translated using Weblate (Portuguese)

Currently translated at 72.6% (308 of 424 strings)

Translated using Weblate (Portuguese)

Currently translated at 72.6% (308 of 424 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Portuguese)

Currently translated at 95.6% (777 of 812 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.3% (154 of 155 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (239 of 239 strings)

Translated using Weblate (Spanish)

Currently translated at 96.8% (217 of 224 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (225 of 225 strings)

Translated using Weblate (French)

Currently translated at 100.0% (151 of 151 strings)

Translated using Weblate (French)

Currently translated at 88.0% (133 of 151 strings)

Translated using Weblate (French)

Currently translated at 85.4% (129 of 151 strings)

Translated using Weblate (French)

Currently translated at 100.0% (224 of 224 strings)

Translated using Weblate (French)

Currently translated at 100.0% (424 of 424 strings)

Translated using Weblate (French)

Currently translated at 100.0% (424 of 424 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2911 of 2911 strings)

Translated using Weblate (French)

Currently translated at 99.6% (2901 of 2911 strings)

Translated using Weblate (French)

Currently translated at 99.6% (2900 of 2911 strings)

Translated using Weblate (French)

Currently translated at 83.4% (126 of 151 strings)

Translated using Weblate (French)

Currently translated at 72.1% (109 of 151 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (424 of 424 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (424 of 424 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 73.6% (67 of 91 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Sinhala)

Currently translated at 24.5% (38 of 155 strings)

Translated using Weblate (French)

Currently translated at 57.2% (87 of 152 strings)

Translated using Weblate (French)

Currently translated at 100.0% (91 of 91 strings)

Co-authored-by: AGM <yoartgm@gmail.com>
Co-authored-by: Aasim Kais <aasimabdullahmohamedkais@gmail.com>
Co-authored-by: Adrián Chaves Fernández <adrian@chaves.io>
Co-authored-by: Ana Paula Fagundes Gomes <paulinha0939@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Icaro <icaro.mascarenhas@outlook.com>
Co-authored-by: Inferno <mishaad051@gmail.com>
Co-authored-by: Jan met de Pet <stijn.koppers@gmail.com>
Co-authored-by: Lapin <sirocuro01@gmail.com>
Co-authored-by: Lucas Rafaldini <lucas.rafaldini@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Oliver Perkins <ollyboros@users.noreply.translate.habitica.com>
Co-authored-by: Raithe <RaitheOfDureya@gmail.com>
Co-authored-by: Sam Abian <samthony35@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Steffen Dobbelaar <steffendobbelaar@gmail.com>
Co-authored-by: Val <3qes0hnzh@mozmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 谢书凝 <xieshun5450@outlook.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/si/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
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/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/character/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/character/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/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/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/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/es/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/gl/
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/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/gl/
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/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2023-11-03 18:22:53 +01:00
SabreCat
efe4483a04 Merge branch 'release' into develop 2023-10-31 15:18:01 -05:00
SabreCat
9cee9ebfe3 5.11.1 2023-10-31 11:42:19 -05:00
SabreCat
33b6de85cc feat(faq): unify platform FAQs
by @CuriousMagpie with edits by @SabreCat
2023-10-31 11:41:24 -05:00
SabreCat
aac3969e6a 5.11.0 2023-10-31 11:28:05 -05:00
SabreCat
61beb770f1 feat(content): subscriber items Nov 2023
by @CuriousMagpie
2023-10-31 11:18:23 -05:00
SabreCat
78ba41c3f8 chore(packages): update lock 2023-10-31 11:10:56 -05:00
SabreCat
1026a721ac Merge branch 'release' into develop 2023-10-25 16:06:52 -05:00
Natalie
affeca6045 fix(strings): remove/change "guild" to "group" (#14872)
* fix(string): questVice1Notes html changed to a mobile-device friendly format

* fix(strings): updated limited.json with "dateEnd" & "monthYYYY" months & put in chronological order

* fix(string): remove extra word from headSpecialSummer2022WarriorNotes

* fix(string): corrected armorSpecialSummer2022MageNotes

* fix: remove duplicated string and adjust upgrade button style

* fix(style): set border radii to 8px on upgrading-group id

* fix(payments): remove duplicate entry from another modal

* chore(fix): restore string inadvertently removed during a refactor

* chore(fix): comma dangle

* chore(typo): who knew, that Y was actually important...

* chore(typo): fix text in questBewilderNotes

* chore(string): clarify polar pets requirements

* couple small changes to the footer as pointed out by users

* chore(fix): correct name of Fabulous Party Hat

* fix(typo): correct February backgrounds release date to 2023, not 2022

* fix(trusted_domains): removed https:// from the beginning of localhost

* fix(config): correct habitica url format too

* fix(strings): remove/change "guild" to "group"

* fix(string): just Bank

---------

Co-authored-by: SabreCat <sabe@habitica.com>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2023-10-25 16:05:31 -05:00
dependabot[bot]
05ac144e4f build(deps): bump jsonwebtoken from 9.0.1 to 9.0.2 (#14855)
Bumps [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) from 9.0.1 to 9.0.2.
- [Changelog](https://github.com/auth0/node-jsonwebtoken/blob/master/CHANGELOG.md)
- [Commits](https://github.com/auth0/node-jsonwebtoken/compare/v9.0.1...v9.0.2)

---
updated-dependencies:
- dependency-name: jsonwebtoken
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 14:26:44 -04:00
dependabot[bot]
fdb5d75372 build(deps): bump jquery from 3.7.0 to 3.7.1 in /website/client (#14859)
Bumps [jquery](https://github.com/jquery/jquery) from 3.7.0 to 3.7.1.
- [Release notes](https://github.com/jquery/jquery/releases)
- [Commits](https://github.com/jquery/jquery/compare/3.7.0...3.7.1)

---
updated-dependencies:
- dependency-name: jquery
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 14:26:24 -04:00
dependabot[bot]
02770dd1a9 build(deps): bump @babel/register from 7.22.5 to 7.22.15 (#14865)
Bumps [@babel/register](https://github.com/babel/babel/tree/HEAD/packages/babel-register) from 7.22.5 to 7.22.15.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.22.15/packages/babel-register)

---
updated-dependencies:
- dependency-name: "@babel/register"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 14:26:06 -04:00
dependabot[bot]
6aed03380a build(deps): bump uuid from 9.0.0 to 9.0.1 in /website/client (#14888)
Bumps [uuid](https://github.com/uuidjs/uuid) from 9.0.0 to 9.0.1.
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v9.0.0...v9.0.1)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 14:24:14 -04:00
dependabot[bot]
be822b6bbe build(deps): bump get-func-name from 2.0.0 to 2.0.2 in /website/client (#14902)
Bumps [get-func-name](https://github.com/chaijs/get-func-name) from 2.0.0 to 2.0.2.
- [Release notes](https://github.com/chaijs/get-func-name/releases)
- [Commits](https://github.com/chaijs/get-func-name/commits/v2.0.2)

---
updated-dependencies:
- dependency-name: get-func-name
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 14:23:35 -04:00
dependabot[bot]
9aa15d9e64 build(deps): bump winston-loggly-bulk from 3.2.1 to 3.3.0 (#14926)
Bumps [winston-loggly-bulk](https://github.com/loggly/winston-loggly-bulk) from 3.2.1 to 3.3.0.
- [Release notes](https://github.com/loggly/winston-loggly-bulk/releases)
- [Commits](https://github.com/loggly/winston-loggly-bulk/compare/v3.2.1...3.3.0)

---
updated-dependencies:
- dependency-name: winston-loggly-bulk
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 14:21:02 -04:00
dependabot[bot]
4e33a0f7f8 build(deps): bump fsevents from 1.2.9 to 1.2.13 in /website/client (#14937)
Bumps [fsevents](https://github.com/fsevents/fsevents) from 1.2.9 to 1.2.13.
- [Release notes](https://github.com/fsevents/fsevents/releases)
- [Commits](https://github.com/fsevents/fsevents/compare/v1.2.9...v1.2.13)

---
updated-dependencies:
- dependency-name: fsevents
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 14:20:34 -04:00
dependabot[bot]
7ff9f67aab build(deps): bump @babel/traverse from 7.22.5 to 7.23.2 (#14953)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.5 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 14:17:03 -04:00
dependabot[bot]
43fcce7242 build(deps): bump core-js from 3.32.2 to 3.33.1 in /website/client (#14955)
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.32.2 to 3.33.1.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.33.1/packages/core-js)

---
updated-dependencies:
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 14:16:37 -04:00
dependabot[bot]
099274969e build(deps): bump nconf from 0.12.0 to 0.12.1 in /website/client (#14963)
Bumps [nconf](https://github.com/flatiron/nconf) from 0.12.0 to 0.12.1.
- [Release notes](https://github.com/flatiron/nconf/releases)
- [Changelog](https://github.com/indexzero/nconf/blob/v0.12.1/CHANGELOG.md)
- [Commits](https://github.com/flatiron/nconf/compare/v0.12.0...v0.12.1)

---
updated-dependencies:
- dependency-name: nconf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 14:13:32 -04:00
dependabot[bot]
5bb97218bd build(deps): bump nconf from 0.12.0 to 0.12.1 (#14966)
Bumps [nconf](https://github.com/flatiron/nconf) from 0.12.0 to 0.12.1.
- [Release notes](https://github.com/flatiron/nconf/releases)
- [Changelog](https://github.com/indexzero/nconf/blob/v0.12.1/CHANGELOG.md)
- [Commits](https://github.com/flatiron/nconf/compare/v0.12.0...v0.12.1)

---
updated-dependencies:
- dependency-name: nconf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-25 14:12:48 -04:00
SabreCat
bc358c3aab 5.10.0 2023-10-24 09:34:31 -05:00
Natalie
581271e930 Reporting challenges (#14756)
* initial commit

* update logic to display flagged challenges properly to users and admins

* add report button to pages 'My Challenges' and 'Discover Challenges'

* allow mods to view flagged messages on challengeDetail view

* update showing flagged challenges for group challenges

* update showing flagged challenges for a specific challenge

* disallow closing a flagged challenge

* update notes to reflect apiParams properly

* fix css spacing

* update challenge en locales

* fix spacing

* update title of closeChallengeModal

* let user know flagged challenges cannot be cloned

* fix linting errors

* ensure flagged challenges cannot be declared with a winner and cloned via API

* define a non user challenge properly

* fix logic to check for a nonParticipant and nonLeader user when grabbing flagged challenges

* fix linting of max character of 100 / line

* remove reporting on 'my challenges' and 'discover challenges'

* WIP(challenges): disable clone button and add notes to new functions

* WIP(challenges): smol changes

* WIP(challenges): clone button only disabled for admin and flagged user; other users can still clone but the flag goes along with the clone

* WIP(challenges): stop flags carrying over on cloned challenges

* WIP(challenges): typo fixing, undoing a smol change

* fix(challenges): improved query logic for flags

* WIP(challenges): more smol changes

* fix(challenges): refactor queries

* fix(challenges): correct My Challenges tab logic

* WIP(challenges): fix clone button state

* WIP(challenges): really fixed clone button & clear flags from clones

* WIP(challenge): implement new design for reporting modal

* WIP(challenge): making things pretty

* WIP(challenge): conquering the close button

* WIP(challenge): fixin some spacing

* WIP(challenge): smol fix

* WIP(challenge): making sure the button is actually disabled

* WIP(challenge): fix blockquote css

* fix(tests): no private guilds

* fix(lint): curlies etc

* fix(test): moderator permission

* fix(lint): sure man whatever

* fix(lint): bad vim no tabby

* fix(test): permissions not contrib lol

* fix(challenges): add icon and fix leaky CSS

* fix(challenge): correct clone button behavior

---------

Co-authored-by: Julius Jung <me@matchajune.io>
Co-authored-by: SabreCat <sabe@habitica.com>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2023-10-24 09:24:56 -05:00
SabreCat
57fe1d6b22 Merge branch 'release' into develop 2023-10-17 17:53:22 -05:00
SabreCat
8537793308 5.9.1 2023-10-17 17:35:05 -05:00
Sabe Jones
38974617b5 fix(event): sale in 2023 not just 2022 2023-10-17 22:34:05 +00:00
Sabe Jones
4974712d6c fix(stats): enforce sensible maxima in db 2023-10-17 22:31:43 +00:00
Sabe Jones
832acb1617 chore(strings): make Stable wording consistent with mobile by @CuriousMagpie 2023-10-17 22:30:25 +00:00
Sabe Jones
ce1d1a763b fix(quests): level up if appropriate after completion 2023-10-17 22:29:02 +00:00
Natalie
7aecadfe68 fix(content): change test strings to Pets and Mounts from Stable(s) (#14912) 2023-10-17 17:27:43 -05:00
SabreCat
06f522e2c9 5.9.0 2023-10-17 09:09:16 -05:00
SabreCat
f7ac6e8eb1 fix(css): achievement layout 2023-10-17 09:09:00 -05:00
SabreCat
8ea1ace239 feat(achievement): add db migration 2023-10-16 20:23:03 -05:00
SabreCat
1da6eeb169 feat(content): Dune Buddy Achievement 2023-10-16 20:15:08 -05:00
SabreCat
b539dd2b2e 5.8.0 2023-10-09 13:32:14 -05:00
SabreCat
71e565ea9a Merge branch 'release' into develop 2023-10-09 13:31:50 -05:00
SabreCat
565f38f7ed fix(gear): capitalization/punctuation 2023-10-09 13:03:45 -05:00
CuriousMagpie
56ccf19c26 feat(content): October bgs and armoire 2023-10-09 12:55:16 -05:00
Weblate
6a5097c16e Translated using Weblate (French)
Currently translated at 91.2% (83 of 91 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (224 of 224 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 81.9% (77 of 94 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 96.4% (109 of 113 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 91.6% (120 of 131 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 26.3% (40 of 152 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 63.7% (58 of 91 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 93.6% (44 of 47 strings)

Translated using Weblate (Estonian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 81.6% (183 of 224 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 90.8% (119 of 131 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 78.4% (331 of 422 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 97.2% (2831 of 2911 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 91.7% (167 of 182 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 23.6% (36 of 152 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 93.0% (711 of 764 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 58.2% (53 of 91 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 91.4% (43 of 47 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 73.5% (597 of 812 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 78.4% (167 of 213 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 97.0% (2825 of 2911 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 94.7% (400 of 422 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 96.8% (2820 of 2911 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 35.5% (54 of 152 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 52.7% (48 of 91 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (213 of 213 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.8% (1568 of 2911 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Arabic)

Currently translated at 98.0% (152 of 155 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Galician)

Currently translated at 98.9% (180 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Galician)

Currently translated at 96.9% (409 of 422 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Turkish)

Currently translated at 97.8% (92 of 94 strings)

Translated using Weblate (French)

Currently translated at 100.0% (812 of 812 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (2911 of 2911 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Polish)

Currently translated at 99.5% (212 of 213 strings)

Translated using Weblate (Polish)

Currently translated at 98.4% (129 of 131 strings)

Translated using Weblate (Polish)

Currently translated at 99.5% (223 of 224 strings)

Translated using Weblate (Polish)

Currently translated at 95.1% (773 of 812 strings)

Translated using Weblate (French)

Currently translated at 99.4% (2896 of 2911 strings)

Translated using Weblate (French)

Currently translated at 51.9% (79 of 152 strings)

Translated using Weblate (French)

Currently translated at 98.3% (799 of 812 strings)

Translated using Weblate (Indonesian)

Currently translated at 78.0% (71 of 91 strings)

Translated using Weblate (Hindi)

Currently translated at 97.8% (184 of 188 strings)

Translated using Weblate (Hindi)

Currently translated at 70.9% (110 of 155 strings)

Translated using Weblate (Hindi)

Currently translated at 29.6% (16 of 54 strings)

Translated using Weblate (Hindi)

Currently translated at 86.6% (13 of 15 strings)

Translated using Weblate (Galician)

Currently translated at 58.6% (1706 of 2911 strings)

Translated using Weblate (Galician)

Currently translated at 65.4% (500 of 764 strings)

Translated using Weblate (French)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (French)

Currently translated at 99.1% (2887 of 2911 strings)

Translated using Weblate (French)

Currently translated at 51.3% (78 of 152 strings)

Translated using Weblate (French)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 68.1% (62 of 91 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.3% (154 of 155 strings)

Translated using Weblate (French)

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (English (en@lolcat))

Currently translated at 5.4% (44 of 812 strings)

Translated using Weblate (English (en@lolcat))

Currently translated at 6.5% (53 of 812 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (812 of 812 strings)

Translated using Weblate (Serbian)

Currently translated at 27.0% (42 of 155 strings)

Translated using Weblate (Belarusian)

Currently translated at 58.1% (1694 of 2911 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.8% (1567 of 2911 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.6% (1562 of 2911 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (224 of 224 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.5% (1558 of 2911 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (812 of 812 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (2911 of 2911 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 65.7% (100 of 152 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.8% (2878 of 2911 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (224 of 224 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 69.2% (63 of 91 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (224 of 224 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (113 of 113 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% (61 of 61 strings)

Translated using Weblate (Russian)

Currently translated at 97.9% (2851 of 2911 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.6% (2873 of 2911 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (812 of 812 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.3% (218 of 224 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (47 of 47 strings)

Co-authored-by: Adrián Chaves Fernández <adrian@chaves.io>
Co-authored-by: Andrii <andrii.koket@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Deleted User <maximrudenkodeal@gmail.com>
Co-authored-by: Florence <paratflo@hotmail.com>
Co-authored-by: Inky Clouds <johnwatsonwrites@gmail.com>
Co-authored-by: Jan <darkbutterfly7530@gmail.com>
Co-authored-by: Justcallme rye <Blizzardscf32@gmail.com>
Co-authored-by: Luísa Bettin <puffycolors@gmail.com>
Co-authored-by: M <maperray@gmail.com>
Co-authored-by: Mateus Felipe Ribeiro Ambrósio <mateus.mfr10@gmail.com>
Co-authored-by: Maxim Rudenko <maximrudenkodeal@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Ognjen <ognjenzxz@gmail.com>
Co-authored-by: Raithe <RaitheOfDureya@gmail.com>
Co-authored-by: Remo Auväärt <remo.auvaart@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Svetlana <shkulepo@rambler.ru>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Victoria Koo <antongvictoria@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yumna Hassan <yumnasu24@gmail.com>
Co-authored-by: Zuz Q <zuzannakunik@gmail.com>
Co-authored-by: billypat <kreideraine@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/hi/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/id/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/en@lolcat/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
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/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/character/hi/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/fr/
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/communityguidelines/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/content/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/death/et/
Translate-URL: https://translate.habitica.com/projects/habitica/death/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/death/hi/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/hi/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
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/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/front/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/be/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
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/generic/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pl/
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/Defaulttasks
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
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
2023-10-09 18:25:48 +02:00
SabreCat
4ff1368db7 5.7.0 2023-10-03 16:36:18 -05:00
SabreCat
7025f92c0e fix(lint): unused var 2023-10-03 16:25:07 -05:00
SabreCat
3c73322980 fix(tests): one more 2023-10-03 16:19:50 -05:00
SabreCat
fbb9b2c65a fix(tests): post merge cleanup 2023-10-03 15:58:31 -05:00
SabreCat
a9f3360890 Squashed commit of the following:
commit 9d99b6b3d7
Author: SabreCat <sabe@habitica.com>
Date:   Thu Sep 14 09:57:42 2023 -0500

    fix(string): just Bank

commit a4d34d487a
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Sep 13 14:50:14 2023 -0400

    fix(strings): remove/change "guild" to "group"

commit dee572cd63
Merge: aefd8ec6c4 e332876d30
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Sep 13 13:54:50 2023 -0400

    Merge branch 'develop' into misc-string-fixes

commit aefd8ec6c4
Author: Sabe Jones <sabrecat@gmail.com>
Date:   Tue Jul 25 14:10:25 2023 -0500

    fix(config): correct habitica url format too

commit 990da1c8f2
Merge: c1d6e0b580 8558dcc3a8
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jul 24 15:54:49 2023 -0400

    Merge branch 'develop' into misc-string-fixes

commit c1d6e0b580
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jul 24 15:53:28 2023 -0400

    fix(trusted_domains): removed https:// from the beginning of localhost

commit b77cb874c9
Merge: 7285aa36bd 71e165433a
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu May 11 16:26:17 2023 -0400

    Merge branch 'develop' into misc-string-fixes

commit 7285aa36bd
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Feb 9 15:31:41 2023 -0500

    fix(typo): correct February backgrounds release date to 2023, not 2022

commit 7aca2ee908
Merge: 30edc124ac 0b8f2bc58e
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Feb 9 15:30:34 2023 -0500

    Merge branch 'develop' into misc-string-fixes

commit 30edc124ac
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jan 30 13:27:10 2023 -0500

    chore(fix): correct name of Fabulous Party Hat

commit 20e336b749
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Jan 20 14:46:27 2023 -0500

    couple small changes to the footer as pointed out by users

commit 16a0255373
Merge: e9a1450b57 d85436afbf
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Jan 20 14:43:16 2023 -0500

    Merge branch 'develop' into misc-string-fixes

commit e9a1450b57
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Jan 4 11:02:56 2023 -0500

    chore(string): clarify polar pets requirements

commit 9d25e07a4b
Merge: 906b7fcd43 c7aadede4d
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Jan 4 11:01:16 2023 -0500

    Merge branch 'develop' into misc-string-fixes

commit 906b7fcd43
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Dec 16 13:00:59 2022 -0500

    chore(typo): fix text in questBewilderNotes

commit d0de66f378
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Nov 8 16:32:26 2022 -0500

    chore(typo): who knew, that Y was actually important...

commit c0a93ae202
Merge: dbae5fb36b 978e8c4320
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Nov 8 16:30:52 2022 -0500

    Merge branch 'develop' into misc-string-fixes

commit dbae5fb36b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Nov 7 13:06:54 2022 -0500

    chore(fix): comma dangle

commit 9365be609f
Merge: 8f2af4a7ea f6e5360bdd
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Nov 7 12:35:56 2022 -0500

    Merge branch 'develop' into misc-string-fixes

commit 8f2af4a7ea
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Nov 7 12:35:04 2022 -0500

    chore(fix): restore string inadvertently removed during a refactor

commit 77fe9450cb
Merge: 5780b71ef8 13c0d12045
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Sep 19 15:07:41 2022 -0400

    Merge branch 'develop' into misc-string-fixes

commit 5780b71ef8
Author: SabreCat <sabe@habitica.com>
Date:   Wed Sep 14 16:27:21 2022 -0500

    fix(payments): remove duplicate entry from another modal

commit f36d0ca0e2
Merge: 92b26fdecd 5f440f1bfa
Author: SabreCat <sabe@habitica.com>
Date:   Wed Sep 14 16:15:01 2022 -0500

    Merge branch 'develop' into misc-string-fixes

commit 92b26fdecd
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Jul 27 18:05:53 2022 -0400

    fix(style): set border radii to 8px on upgrading-group id

commit f359142aa5
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Jul 27 17:47:19 2022 -0400

    fix: remove duplicated string and adjust upgrade button style

commit b67ef33b63
Merge: bd736235cc c09f078409
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Jul 27 17:09:23 2022 -0400

    Merge branch 'misc-string-fixes' of https://github.com/CuriousMagpie/habitica into misc-string-fixes

commit c09f078409
Merge: 05811e3c70 ef3767f80b
Author: Natalie L <78037386+CuriousMagpie@users.noreply.github.com>
Date:   Wed Jul 27 17:05:52 2022 -0400

    Merge branch 'HabitRPG:develop' into misc-string-fixes

commit bd736235cc
Merge: 88368a8c21 ef3767f80b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Jul 27 17:03:30 2022 -0400

    Merge remote-tracking branch 'upstream/develop' into misc-string-fixes

commit 05811e3c70
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Jun 24 15:01:23 2022 -0400

    fix(string): corrected armorSpecialSummer2022MageNotes

commit 4031c97253
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 23 12:26:51 2022 -0400

    fix(string): remove extra word from headSpecialSummer2022WarriorNotes

commit e4af5c250f
Merge: 88368a8c21 76de241675
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jun 23 11:27:04 2022 -0400

    Merge branch 'develop' into misc-string-fixes

commit 88368a8c21
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Jun 21 13:49:46 2022 -0400

    fix(strings): updated limited.json with "dateEnd" & "monthYYYY" months & put in chronological order

commit 6ee08377e9
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Jun 20 15:36:58 2022 -0400

    fix(string): questVice1Notes html changed to a mobile-device friendly format
2023-10-03 13:32:11 -05:00
SabreCat
a0941ffa84 Squashed commit of the following:
commit 16d8b87e90
Merge: 07387faf48 6bea232d47
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Sep 14 22:30:00 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 07387faf48
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Sep 13 23:38:37 2023 +0200

    remove generate promoCode from ui

commit 6bea232d47
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Sep 11 12:55:31 2023 -0400

    build(deps): bump core-js from 3.32.1 to 3.32.2 in /website/client (#14867)

    Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.32.1 to 3.32.2.
    - [Release notes](https://github.com/zloirock/core-js/releases)
    - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/zloirock/core-js/commits/v3.32.2/packages/core-js)

    ---
    updated-dependencies:
    - dependency-name: core-js
      dependency-type: direct:production
      update-type: version-update:semver-patch
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit cebb3f0f25
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Sep 11 12:43:49 2023 -0400

    build(deps): bump webpack from 4.46.0 to 4.47.0 in /website/client (#14868)

    Bumps [webpack](https://github.com/webpack/webpack) from 4.46.0 to 4.47.0.
    - [Release notes](https://github.com/webpack/webpack/releases)
    - [Commits](https://github.com/webpack/webpack/compare/v4.46.0...v4.47.0)

    ---
    updated-dependencies:
    - dependency-name: webpack
      dependency-type: direct:production
      update-type: version-update:semver-minor
    ...

    Signed-off-by: dependabot[bot] <support@github.com>
    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit ea8563cd17
Merge: 3e16584dcf 6259955891
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Aug 29 21:23:02 2023 +0200

    Merge remote-tracking branch 'origin/negue/ui/setting' into negue/ui/setting

commit 3e16584dcf
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Aug 29 21:22:06 2023 +0200

    fix PR comments

commit 84ba44fb19
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Aug 29 20:38:54 2023 +0200

    fix PR comments

commit 6259955891
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Aug 25 11:20:26 2023 -0400

    update form.scss

commit da82bd8e68
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Aug 24 21:40:02 2023 +0200

    remove ending

commit 82e5fd2a83
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Aug 21 22:25:41 2023 +0200

    fix spacing

commit 9ad06ea88b
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Aug 21 22:09:22 2023 +0200

    clean up debug row for login methods

commit 41cde37675
Merge: 8c568060f9 82ebe71eb4
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Aug 21 21:51:22 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 8c568060f9
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Aug 21 21:49:31 2023 +0200

    fix PR comments

commit 36f7a4711d
Merge: d279af7897 647b27c55f
Author: negue <eugen.bolz@gmail.com>
Date:   Fri Aug 11 20:04:15 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit d279af7897
Merge: ffbed3e044 b20ea44d49
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Aug 9 21:13:37 2023 +0200

    Merge branch 'negue/refactor/routes' into negue/ui/setting

commit b20ea44d49
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Aug 9 21:04:12 2023 +0200

    Split Vue.Router routes

commit ffbed3e044
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jul 23 00:00:24 2023 +0200

    remove console

commit 4c350b0180
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jul 22 23:34:20 2023 +0200

    update Bailey Notification Text + fix popover

commit c105b9ecf9
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jul 22 23:21:53 2023 +0200

    fix change password setting

commit 06410b4807
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jul 22 22:50:00 2023 +0200

    fix reset account texts

commit ccfdd9bb9c
Merge: 35c75304f1 8558dcc3a8
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jul 22 22:48:13 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 35c75304f1
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jul 2 20:16:06 2023 +0200

    more fixes

commit 203e961464
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jul 2 19:45:17 2023 +0200

    fix notification settings

commit ec94604791
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 22:00:45 2023 +0200

    applied same styling to promoCode.vue

commit 0177b3a76b
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 21:41:05 2023 +0200

    move promoCode.vue to pages/settings

commit 8fbb600273
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 21:40:35 2023 +0200

    saveCancelButtons.vue allow to hide the cancel part

commit 4915f2a3fb
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 21:09:07 2023 +0200

    Hide Transactions Page again

commit 8b5ae17f02
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 20:52:03 2023 +0200

    also check for invalid arguments in the password settings

commit aa97ed5299
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 20:25:53 2023 +0200

    fix localhost externalLinks check

commit 87a4e4931b
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Jun 25 20:01:31 2023 +0200

    show notification on username change + fix userEmail checks

commit 6a6f55f6fc
Merge: f9ff5e5c55 e49d26eacd
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jun 24 22:54:00 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit f9ff5e5c55
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 22:41:42 2023 +0200

    check password inputs and mark invalid for "password change" setting

commit 4497514eeb
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 21:59:21 2023 +0200

    show notification when chaning display name

commit 3232f12f0d
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 21:55:25 2023 +0200

    check current password valid style in "delete account" and "reset account"

commit 582a2f1304
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 21:27:20 2023 +0200

    mark password field of email setting as invalid on wrong password

commit 8e3b8a962a
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 21:24:46 2023 +0200

    refactor currentPasswordInput.vue to use validatedTextInput.vue

commit 61521507a4
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 20:20:56 2023 +0200

    fix username setting:
    - unsaved values check
    - @ char must be first in input, otherwise not remove it for checks

commit f74c29a065
Merge: c4b6f0c39c d4a5823916
Author: negue <eugen.bolz@gmail.com>
Date:   Tue May 30 19:54:06 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit c4b6f0c39c
Merge: 37eee140ad 6e3a367832
Author: negue <eugen.bolz@gmail.com>
Date:   Fri May 12 22:08:08 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 37eee140ad
Author: negue <eugen.bolz@gmail.com>
Date:   Fri May 12 21:57:27 2023 +0200

    delete account without password

commit 48a6801f4e
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 22:06:29 2023 +0200

    fix duplicate json entry

commit 47a2189f49
Merge: a56b4a4457 49f45d27e3
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 21:48:21 2023 +0200

    Merge remote-tracking branch 'origin/release' into negue/ui/setting

commit a56b4a4457
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 21:37:31 2023 +0200

    show current class on setting panel

commit 9c973cca2a
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 21:15:46 2023 +0200

    fix selectDifficulty.vue - refactor selectList.vue

commit 95b37b3ba3
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 20:45:09 2023 +0200

    migrate restoreValues fix to new setting component

commit 7947b1c67d
Merge: ad3e4d604a 71e165433a
Author: negue <eugen.bolz@gmail.com>
Date:   Mon May 8 20:41:31 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit ad3e4d604a
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Apr 29 01:18:25 2023 +0200

    style fixes

commit cea13d5bc3
Merge: 73a5e5fcab b159182188
Author: negue <eugen.bolz@gmail.com>
Date:   Fri Apr 28 23:58:09 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 73a5e5fcab
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Apr 25 20:51:14 2023 +0200

    style / padding issues

commit 0a10eb32cc
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Apr 15 20:54:08 2023 +0200

    fix "setting new password" invalid check

commit a79bec3fa5
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Apr 11 23:15:15 2023 +0200

    add password for other logins

commit 9ff17fd6dd
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Apr 11 23:05:19 2023 +0200

    "fix values" use keydown event to mark as change

commit 1f470942a9
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Apr 6 00:19:18 2023 +0200

    delete old api.vue

commit b4904a8b84
Merge: b5da7ccc70 c8b98678d0
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Apr 6 00:18:07 2023 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit b5da7ccc70
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Apr 6 00:11:36 2023 +0200

    refactor webhook ui to use save/cancel buttons

commit f49f67ff5c
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Apr 5 22:56:37 2023 +0200

    remove unused settings

commit cc73b44b25
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Mar 29 23:40:30 2023 +0200

    remove advancedCollapsed settings to start it opened

commit e0300e8710
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Mar 29 22:58:09 2023 +0200

    remove displayInviteToPartyWhenPartyIs1 setting

commit 1741ddfc64
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Mar 20 23:00:17 2023 +0100

    webhook margins

commit 24a43d027c
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Mar 20 22:40:19 2023 +0100

    userid tooltip

commit 42fcb20bc4
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Mar 16 00:51:10 2023 +0100

    remove balance for choosing class

commit 160848473d
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Mar 16 00:20:56 2023 +0100

    show real class setting modal if enough gems available

commit f74ba9738d
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Mar 16 00:10:53 2023 +0100

    update apple icon and size

commit bf961bc728
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Mar 15 23:59:42 2023 +0100

    Copied API Token Notification

commit 28f0220b4e
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Mar 15 23:53:33 2023 +0100

    remove blue color of setting links

commit b53ccace95
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Mar 15 23:43:06 2023 +0100

    fix username/email setting input width

commit 1dfa5b275d
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Mar 15 23:11:32 2023 +0100

    developer mode

commit 776618d2db
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Mar 14 21:11:52 2023 +0100

    Add new Pause Dailies Setting

commit 576c80af7e
Merge: dec1a1159d 377b152ffd
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Mar 14 21:04:05 2023 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit dec1a1159d
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Mar 14 21:00:52 2023 +0100

    developer mode dummy row

commit 1e80a7d145
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Mar 11 00:03:33 2023 +0100

    WIP webhook row

commit cc4bedbe2d
Author: negue <eugen.bolz@gmail.com>
Date:   Fri Mar 10 20:28:57 2023 +0100

    add spritely login creds message to the new api-row / redirect old url to the new one

commit f9833aa78a
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Mar 9 02:23:39 2023 +0100

    API Token Row

commit 123c9b9bb1
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Mar 6 22:46:50 2023 +0100

    "Your User Data" Row instead of Page

commit 0ade5663ae
Author: negue <eugen.bolz@gmail.com>
Date:   Fri Mar 3 22:43:03 2023 +0100

    userid row

commit b4f2236ab8
Author: negue <eugen.bolz@gmail.com>
Date:   Fri Mar 3 22:22:32 2023 +0100

    rename folder of setting rows

commit 3b050861c4
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Feb 21 21:11:48 2023 +0100

    move remaining setting to generalSettings.vue - delete site.vue - start with siteData.vue

commit b09298fb01
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Feb 21 20:56:03 2023 +0100

    move taskSettings.vue and add it to the settings list

commit 5ed25066ec
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Feb 21 20:06:13 2023 +0100

    size/margin for transactions

commit 25e77cbd95
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Feb 21 19:52:12 2023 +0100

    move purchaseHistory.vue

commit 8e4e1bcb0f
Merge: bb14d09aa4 85c50d50e9
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Feb 21 19:04:31 2023 +0100

    Merge remote-tracking branch 'origin/negue/ui/setting' into negue/ui/setting

commit 85c50d50e9
Author: SabreCat <sabe@habitica.com>
Date:   Thu Feb 16 14:23:27 2023 -0600

    fix(css): remove redundant formatting for a elements

commit bb14d09aa4
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Feb 16 01:34:09 2023 +0100

    remove console

commit 8c5e722c72
Author: negue <eugen.bolz@gmail.com>
Date:   Thu Feb 16 01:26:43 2023 +0100

    first try with the refactored UI of Login Methods

commit 9c8770051d
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 19:13:16 2023 +0100

    fix dayStartAdjustmentSetting.vue for 0 value

commit ee2ff3881b
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 18:37:46 2023 +0100

    fix color after refactor

commit 121e7485ca
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 18:29:00 2023 +0100

    mark audioThemeSetting as changed

commit 98c6570003
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 18:05:55 2023 +0100

    fix ul/li style in resetAccount.vue

commit fed824f705
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 17:49:36 2023 +0100

    fix color of gem price

commit 80365e537d
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Feb 11 17:44:55 2023 +0100

    fix "fixValuesSetting.vue"

commit d3e15c5413
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Feb 8 01:06:27 2023 +0100

    open forgot password in new tab

commit 31edec9ec5
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Feb 8 01:03:19 2023 +0100

    move validatedTextInput.vue to shared components + fix check pos/size + input-error cleanup

commit 2adfd8c259
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Feb 5 20:19:30 2023 +0100

    hide class setting until level 10

commit 64fb4c0cf9
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Feb 5 19:32:40 2023 +0100

    delete old modals (refactored into new settings ui)

commit b5be137a8d
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Feb 5 19:27:26 2023 +0100

    enable forgot password link in settings

commit bec75c6e12
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Feb 5 18:52:54 2023 +0100

    reset account + password required in api

commit 64f7e7a1d9
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Jan 30 23:22:55 2023 +0100

    fix compile

commit 7ffb5101be
Merge: 2bfb130b92 9f64633a57
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Jan 30 22:47:05 2023 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 2bfb130b92
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Jan 30 22:44:23 2023 +0100

    remove restore-modal and replace it with the finished fix values setting

commit 89530a133c
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Jan 18 19:22:36 2023 +0100

    wip fix values

commit 428647fc71
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jan 14 21:50:22 2023 +0100

    refactor change class to design update + clean up old site.vue settings

commit 1f16819bc1
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Jan 11 22:41:05 2023 +0100

    WIP fix values

commit 6fef3d0579
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jan 7 22:51:30 2023 +0100

    check for unsaved changes when pressing cancel

commit bef8a4cdfc
Merge: 494f32c3e3 c7aadede4d
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Jan 7 22:10:53 2023 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 494f32c3e3
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Dec 21 00:55:31 2022 +0100

    Class Setting

commit bda210cfbb
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Dec 20 23:01:41 2022 +0100

    removes username, email and display name from site.vue

commit 38198d7df6
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Dec 20 22:36:27 2022 +0100

    WIP class setting

commit dddcfa637f
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Dec 20 22:31:36 2022 +0100

    fix styles

commit ce0a5cf974
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Dec 11 23:57:07 2022 +0100

    Scroll into opened Setting

commit 7e0a95ddff
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Dec 11 23:43:44 2022 +0100

    Audio Theme Setting

commit 9c556662fe
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Dec 11 00:25:30 2022 +0100

    prepare header settings but still hidden

commit 30d8b27534
Merge: a1d1a788b2 580139ff69
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Dec 10 23:36:36 2022 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit a1d1a788b2
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Dec 10 23:34:33 2022 +0100

    DayStartAdjustmentSetting

commit ddee94a393
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Dec 10 20:00:12 2022 +0100

    disable reset account button when password empty

commit 30a6db4c2d
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Dec 10 19:54:21 2022 +0100

    hide & reset previous setting when switching to a different one

commit 78093848d7
Author: negue <eugen.bolz@gmail.com>
Date:   Wed Dec 7 22:19:15 2022 +0100

    validated text input (in/valid border color + icon)

commit e1b444ea63
Author: negue <eugen.bolz@gmail.com>
Date:   Tue Dec 6 22:09:54 2022 +0100

    re-enable box-shadow on hover

commit 96dc4e47ae
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 28 01:13:47 2022 +0100

    remove console log

commit 69ad07daad
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 28 01:01:17 2022 +0100

    dateFormatSetting

commit bc11c0cf75
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 28 00:49:24 2022 +0100

    move shared components / mixins

commit 0d1a189c64
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 28 00:44:21 2022 +0100

    language Setting + imports cleanup

commit 29ebd89030
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 27 23:23:02 2022 +0100

    fix icon size + fix display name valid checks

commit 5c7747517b
Merge: fd5cbc3026 90b34c4dac
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 27 23:08:35 2022 +0100

    Merge remote-tracking branch 'origin/release' into negue/ui/setting

commit fd5cbc3026
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 23 00:14:21 2022 +0100

    fix conflicts

commit 49361217b0
Merge: edb427158f 04e2a39a9f
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 23 00:12:38 2022 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit edb427158f
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 23 00:03:19 2022 +0100

    disable save button if nothing was changed

commit c7e40e9446
Author: negue <negue@users.noreply.github.com>
Date:   Tue Nov 22 23:36:37 2022 +0100

    delete account row

commit 4bf740c531
Author: negue <negue@users.noreply.github.com>
Date:   Tue Nov 22 23:14:24 2022 +0100

    Shared Modal Visible State

commit d718153717
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 20 18:06:20 2022 +0100

    resetAccount

commit e25922f8b3
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 16 23:39:26 2022 +0100

    rename functional components for compiler

commit fdbc2c0eee
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 16 01:44:50 2022 +0100

    password setting row

commit 5fd5e6275a
Author: negue <negue@users.noreply.github.com>
Date:   Tue Nov 15 17:35:44 2022 +0100

    update package-lock.json again

commit 9d742fd9a1
Author: negue <negue@users.noreply.github.com>
Date:   Tue Nov 15 17:24:15 2022 +0100

    update package-lock.json

commit cd588e74d5
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 14 02:12:39 2022 +0100

    displayNameSetting.vue

commit 265970c5ef
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 14 02:09:47 2022 +0100

    fix lint

commit a2b510caca
Merge: 0bae5fbe02 4dca69f14b
Author: negue <negue@users.noreply.github.com>
Date:   Mon Nov 14 01:15:02 2022 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 0bae5fbe02
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 13 22:00:34 2022 +0100

    userEmailSetting

commit 23da70fa2e
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 13 20:38:14 2022 +0100

    extract save / cancel buttons and the shared inlineSetting "logic"

commit 82047380f3
Author: negue <negue@users.noreply.github.com>
Date:   Sun Nov 13 20:18:21 2022 +0100

    first setting (username) in the new layout

commit 39150349c7
Author: negue <negue@users.noreply.github.com>
Date:   Wed Nov 2 21:42:12 2022 +0100

    Working on M1 - will be reverted on full merge

commit f7787b318c
Merge: 4c0ecc9938 53fb28cc48
Author: negue <negue@users.noreply.github.com>
Date:   Tue Nov 1 14:20:24 2022 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 4c0ecc9938
Merge: 2f53613a45 62b4315b3d
Author: negue <negue@users.noreply.github.com>
Date:   Sun Oct 30 12:49:34 2022 +0100

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit 2f53613a45
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Oct 10 22:54:41 2022 +0200

    split routes for ease of dev

commit 390f0fc69d
Merge: cf222ee63a 137f7d53dc
Author: negue <eugen.bolz@gmail.com>
Date:   Mon Oct 10 22:50:43 2022 +0200

    Merge remote-tracking branch 'origin/develop' into negue/ui/setting

commit cf222ee63a
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Oct 2 23:15:35 2022 +0200

    Update remaining Notification labels

commit f837cce125
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Oct 2 22:45:12 2022 +0200

    move site popup settings to notifications

commit fc5181c3a7
Author: negue <eugen.bolz@gmail.com>
Date:   Sun Oct 2 21:12:24 2022 +0200

    fix styling in notification settings

commit 7b5568ed23
Author: negue <eugen.bolz@gmail.com>
Date:   Sat Sep 10 16:00:56 2022 +0200

    wip notification settings
2023-10-03 13:30:44 -05:00
SabreCat
a9757b2d74 Squashed commit of the following:
commit 3aba0abedd
Author: SabreCat <sabe@habitica.com>
Date:   Mon Oct 2 20:51:20 2023 -0500

    fix(router): use state to pass modal launch info

commit 541eadd319
Merge: c0bb56c8c2 89fff49d02
Author: SabreCat <sabe@habitica.com>
Date:   Mon Oct 2 20:12:40 2023 -0500

    Merge branch 'release' into report-profile-modal

commit c0bb56c8c2
Author: SabreCat <sabe@habitica.com>
Date:   Wed Sep 27 16:15:28 2023 -0500

    test(profiles): add integrations

commit 9b644e9ad8
Author: SabreCat <sabe@habitica.com>
Date:   Tue Sep 26 17:17:22 2023 -0500

    fix(profile): adjust margin

commit bfefe5dfa9
Author: SabreCat <sabe@habitica.com>
Date:   Tue Sep 26 17:12:24 2023 -0500

    fix(profiles): moar layout fixes

commit 8f211ee3e2
Author: SabreCat <sabe@habitica.com>
Date:   Mon Sep 25 17:32:04 2023 -0500

    fix(profile): fix admin actions
    Correct "user is banned" banner
    Fix bouncing modal
    Add "Days" smart plural
    Fix leaky CSS on Market page
    Refactor some redundant functions

commit b1d23ec88b
Merge: ee9709a9e1 a63cc84779
Author: SabreCat <sabe@habitica.com>
Date:   Mon Sep 25 15:37:54 2023 -0500

    Merge branch 'release' into report-profile-modal

commit ee9709a9e1
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Sep 18 16:30:30 2023 -0400

    WIP(profile): add banned banner, toggle switches now toggle, add "days" to Next Login Reward

commit f80928a895
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Sep 18 13:43:34 2023 -0400

    update(node): update node modules

commit 1d552f7e80
Author: SabreCat <sabe@habitica.com>
Date:   Fri Sep 15 16:52:22 2023 -0500

    fix(import): remove empty import

commit f55d74a95d
Author: SabreCat <sabe@habitica.com>
Date:   Fri Sep 15 16:39:50 2023 -0500

    refactor(profiles): remove email feature
    also still more visual cleanup of profile modal

commit 311c743284
Author: SabreCat <sabe@habitica.com>
Date:   Fri Sep 15 15:44:56 2023 -0500

    refactor(profile): remove page view

commit f8632bf50d
Merge: ec85159c65 9e25360102
Author: SabreCat <sabe@habitica.com>
Date:   Fri Sep 15 15:23:21 2023 -0500

    Merge branch 'release' into report-profile-modal

commit ec85159c65
Author: SabreCat <sabe@habitica.com>
Date:   Mon Sep 11 22:53:14 2023 -0500

    feat(profiles): load modal instead of page?

commit 9986082914
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Sep 8 14:49:57 2023 -0400

    WIP(profile): fixed a comment, woohoo

commit 6262a9ba0c
Merge: ae2b614df2 ea2b007b1a
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Sep 8 13:40:23 2023 -0400

    Merge remote-tracking branch 'origin/report-profile-modal' into report-profile-modal

commit ea2b007b1a
Author: SabreCat <sabe@habitica.com>
Date:   Thu Sep 7 16:54:19 2023 -0500

    fix(profile): focus behavior

commit ae2b614df2
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Sep 7 17:47:08 2023 -0400

    WIP(profile): styling updates

commit 2e0723f1b9
Author: SabreCat <sabe@habitica.com>
Date:   Thu Sep 7 15:37:59 2023 -0500

    feat(moderation): unflag profile
    Also a few stylistic tweaks

commit edcf8113de
Author: SabreCat <sabe@habitica.com>
Date:   Wed Sep 6 16:39:02 2023 -0500

    WIP(profile): dropdown draft

commit 0691483d63
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Sep 6 16:33:30 2023 -0400

    WIP(profile): Styling and string updates

commit 7e9d57d10a
Author: SabreCat <sabe@habitica.com>
Date:   Wed Sep 6 11:40:31 2023 -0500

    feat(profile): functional dropdown buttons

commit a2989b2833
Merge: af6575e40c e072d7c09c
Author: SabreCat <sabe@habitica.com>
Date:   Wed Sep 6 10:04:57 2023 -0500

    Merge branch 'release' into report-profile-modal

commit af6575e40c
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Sep 6 11:01:05 2023 -0400

    WIP(profile): comment cleanup

commit 7b1de37202
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Sep 5 17:22:14 2023 -0400

    WIP(profile): remove shadowban tooltip

commit d1177c32b9
Merge: 321a01b081 31f821021b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Sep 5 17:02:40 2023 -0400

    Merge branch 'sabrecat/report-profile' into report-profile-modal

commit 321a01b081
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Sep 1 16:14:36 2023 -0400

    WIP(profile): close button finally workinating

commit e143d36d28
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Sep 1 15:52:38 2023 -0400

    WIP(profile): close icon moved to profile.vue

commit 31f821021b
Merge: a8f5e25d38 8957c5c009
Author: SabreCat <sabe@habitica.com>
Date:   Fri Sep 1 14:52:31 2023 -0500

    Merge branch 'report-profile-modal' into sabrecat/report-profile

commit 8957c5c009
Merge: d340f06a22 0aec3866a4
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Sep 1 15:38:12 2023 -0400

    Merge remote-tracking branch 'origin/report-profile-modal' into report-profile-modal

commit d340f06a22
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Sep 1 15:37:57 2023 -0400

    WIP(profile): fixed user not found error

commit 0aec3866a4
Merge: b01f323b14 ac7c8e0eb6
Author: Natalie <78037386+CuriousMagpie@users.noreply.github.com>
Date:   Fri Sep 1 15:28:58 2023 -0400

    Merge branch 'HabitRPG:develop' into report-profile-modal

commit a8f5e25d38
Author: SabreCat <sabe@habitica.com>
Date:   Thu Aug 31 17:02:07 2023 -0500

    feat(community): basic "report profile"

commit b01f323b14
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Aug 31 17:42:12 2023 -0400

    WIP(profile): removed refactoring crud, located where close icon should be (profileModal.vue)

commit ce7d51a20c
Merge: 010f2299f0 ac7c8e0eb6
Author: SabreCat <sabe@habitica.com>
Date:   Thu Aug 31 14:20:37 2023 -0500

    Merge branch 'release' into sabrecat/report-profile

commit 18b41acd94
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Aug 31 12:23:41 2023 -0400

    WIP(profile): moar buttonz

commit 9387b3a6bc
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 30 17:21:36 2023 -0400

    WIP(profile): buttons

commit b3ea48c4f5
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Aug 25 15:52:41 2023 -0400

    WIP(profile): work on achievement component

commit a1ceb2ea75
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Aug 25 14:39:12 2023 -0400

    WIP(profile): create achievements component

commit 4a24d9b80b
Merge: 8fe263a377 1e05297e96
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 23 13:14:39 2023 -0400

    Merge branch 'develop' into report-profile-modal

commit 1e05297e96
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 23 13:12:52 2023 -0400

    package updates

commit 8fe263a377
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 23 12:12:36 2023 -0400

    update(dependencies): ran npm install to update dependencies

commit 190fe048a1
Merge: 3ea48ab5cb fa83d1a9cf
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 23 11:52:08 2023 -0400

    Merge branch 'develop' into report-profile-modal

commit 3ea48ab5cb
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Aug 11 17:12:31 2023 -0400

    WIP(user profile): dropdown menu and toggles and colors oh my

commit c301a2b460
Merge: 1da6af11b5 647b27c55f
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Aug 11 12:40:07 2023 -0400

    Merge branch 'develop' into report-profile-modal

commit 1da6af11b5
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Aug 10 16:50:07 2023 -0400

    WIP(user profile): moved some CSS classes out of unscoped and into the scoped section, started on toggle buttons

commit dd55cbc928
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 9 15:38:46 2023 -0400

    WIP(user profile): workin on the hamburger (kebab?) menu

commit 3834093207
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Tue Aug 8 14:14:40 2023 -0400

    WIP(user profiles): working on the drop down menu

commit f2be588195
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon Aug 7 16:10:30 2023 -0400

    WIP(user profile): options menu

commit 010f2299f0
Author: SabreCat <sabe@habitica.com>
Date:   Mon Aug 7 11:49:04 2023 -0500

    fix(lint): eof and const

commit 4551dbf4b3
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Aug 4 15:34:05 2023 -0400

    WIP(user profile): styling the top portion of the modal

commit 19a9fe3644
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Aug 3 15:06:51 2023 -0400

    WIP(user profile): adding buttons

commit dfdb305b1c
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 2 14:41:20 2023 -0400

    WIP(user profile): layout

commit ded4eee693
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Aug 2 12:04:02 2023 -0400

    WIP(user profile): start flex grid & tidy up CSS

commit aaca48be32
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Fri Jul 28 16:44:06 2023 -0400

    WIP(user profile): mostly css updates

commit e531985b87
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Thu Jul 27 16:49:44 2023 -0400

    WIP(user profile): one infinitesimal change that's hardly worth the electricity it's made from

commit eb4021fcc7
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Jul 26 16:33:05 2023 -0400

    feat(content): upgrade profile page

commit 1b25394f3e
Merge: c50cee0d88 8558dcc3a8
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed Jul 26 11:50:12 2023 -0400

    Merge branch 'develop' into report-profile-modal

commit c50cee0d88
Author: SabreCat <sabe@habitica.com>
Date:   Wed Jul 12 16:32:25 2023 -0500

    fix(flagging): debug params issue
    Also add and document the "source" body param

commit 55848c58be
Author: SabreCat <sabe@habitica.com>
Date:   Mon Jul 10 16:24:20 2023 -0500

    WIP(members): basic report a user API

commit dda6180792
Author: SabreCat <sabe@habitica.com>
Date:   Thu Jul 6 10:05:07 2023 -0500

    fix(lint): remove console.info
2023-10-03 13:29:26 -05:00
SabreCat
89fff49d02 5.6.0 2023-09-29 15:13:32 -05:00
CuriousMagpie
b8b0d668c9 feat(content): October sub items 2023-09-29 14:51:45 -05:00
SabreCat
1d3006ae29 chore(event): schedule spooky gems 2023-09-27 14:09:50 -05:00
SabreCat
a63cc84779 5.5.0 2023-09-20 19:47:44 -05:00
SabreCat
a06974d354 chore(subproj): update images 2023-09-20 19:47:37 -05:00
Natalie
f72eef6bff feat(content): prebuild Fall Festival (#14869)
* feat(content): prebuild Fall Festival

* fix(typos): because 2023 is not the same as 2024

* feat(css): having stylesheets is important

* feat(content): ready for review & testing

* fix(tests): account for Sept 09 bundle

* fix(gala): use multi event list more
fix a couple of strings too

* feat(content): Warrior and Rogue text
also fix timing of quest bundle feature

* fix(strings): correct stat boosts

* fix(content): missing mage
also adds missing margin to purchase gems button in buy modal

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-09-20 19:46:34 -05:00
SabreCat
9e25360102 5.4.1 2023-09-12 09:46:28 -05:00
SabreCat
ce70c73d49 fix(test): temporarily use real timer 2023-09-12 09:45:40 -05:00
SabreCat
cdd87abcf9 Merge branch 'release' into 2023-09-pet-quest-bundle 2023-09-12 09:42:48 -05:00
dependabot[bot]
6bea232d47 build(deps): bump core-js from 3.32.1 to 3.32.2 in /website/client (#14867)
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.32.1 to 3.32.2.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.32.2/packages/core-js)

---
updated-dependencies:
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-11 12:55:31 -04:00
dependabot[bot]
cebb3f0f25 build(deps): bump webpack from 4.46.0 to 4.47.0 in /website/client (#14868)
Bumps [webpack](https://github.com/webpack/webpack) from 4.46.0 to 4.47.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v4.46.0...v4.47.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-11 12:43:49 -04:00
SabreCat
e072d7c09c Merge branch 'develop' into release 2023-09-05 13:39:59 -05:00
Weblate
e332876d30 Merge branch 'origin/develop' into Weblate. 2023-09-05 20:32:36 +02:00
SabreCat
08d71cc0bb 5.4.0 2023-09-05 13:30:26 -05:00
Natalie
c4f870c421 feat(content): add September 2023 Backgrounds and Enchanted Armoire items (#14845)
* feat(content): add Sept 2023 backgrounds and enchanted armoire items

* fix(content): fix broken strings in September items

* chore(sprites): update spritesheet

* fix(strings): w.

* fix(strings): capitalization, missing words

---------

Co-authored-by: SabreCat <sabe@habitica.com>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2023-09-05 13:23:20 -05:00
Weblate
6421f6bbe0 Translated using Weblate (Portuguese)
Currently translated at 67.8% (152 of 224 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.4% (181 of 182 strings)

Translated using Weblate (Portuguese)

Currently translated at 81.6% (174 of 213 strings)

Translated using Weblate (Portuguese)

Currently translated at 60.2% (135 of 224 strings)

Translated using Weblate (Korean)

Currently translated at 25.0% (38 of 152 strings)

Translated using Weblate (Galician)

Currently translated at 58.7% (1706 of 2905 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (224 of 224 strings)

Translated using Weblate (Serbian)

Currently translated at 50.0% (4 of 8 strings)

Translated using Weblate (Serbian)

Currently translated at 71.5% (302 of 422 strings)

Translated using Weblate (Serbian)

Currently translated at 59.0% (1714 of 2905 strings)

Translated using Weblate (Serbian)

Currently translated at 50.0% (27 of 54 strings)

Translated using Weblate (Serbian)

Currently translated at 55.7% (449 of 805 strings)

Translated using Weblate (Serbian)

Currently translated at 55.2% (445 of 805 strings)

Translated using Weblate (Serbian)

Currently translated at 53.4% (430 of 805 strings)

Translated using Weblate (Serbian)

Currently translated at 53.2% (429 of 805 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Polish)

Currently translated at 97.3% (148 of 152 strings)

Translated using Weblate (Polish)

Currently translated at 90.1% (137 of 152 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.5% (793 of 805 strings)

Translated using Weblate (Polish)

Currently translated at 87.5% (133 of 152 strings)

Co-authored-by: Adrián Chaves Fernández <adrian@chaves.io>
Co-authored-by: Bartosz Babik <kotka-wali0h@icloud.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Kimminjae <aezir07@gmail.com>
Co-authored-by: LiziKnight <liziknight0316@outlook.com>
Co-authored-by: Mateus Felipe Ribeiro Ambrósio <mateus.mfr10@gmail.com>
Co-authored-by: Ognjen <ognjenzxz@gmail.com>
Co-authored-by: Raithe <RaitheOfDureya@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt/
Translation: Habitica/Backgrounds
Translation: Habitica/Contrib
Translation: Habitica/Defaulttasks
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Groups
Translation: Habitica/Overview
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2023-09-05 17:39:52 +02:00
Weblate
ac7c8e0eb6 Merge branch 'origin/develop' into Weblate. 2023-08-30 21:53:35 +02:00
Weblate
7c9df3b32f Translated using Weblate (Ukrainian)
Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (Polish)

Currently translated at 89.2% (199 of 223 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Polish)

Currently translated at 86.9% (194 of 223 strings)

Translated using Weblate (Polish)

Currently translated at 60.7% (1763 of 2901 strings)

Translated using Weblate (Polish)

Currently translated at 60.7% (1763 of 2901 strings)

Translated using Weblate (Polish)

Currently translated at 78.2% (119 of 152 strings)

Co-authored-by: Bartosz Babik <kotka-wali0h@icloud.com>
Co-authored-by: Bogdan Derdziak <bagtirr@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: konhi <hello.konhi@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pl/
Translation: Habitica/Achievements
Translation: Habitica/Contrib
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Messages
Translation: Habitica/Pets
Translation: Habitica/Rebirth
Translation: Habitica/Subscriber
2023-08-30 21:53:26 +02:00
SabreCat
edefae72bd 5.3.0 2023-08-30 14:52:04 -05:00
SabreCat
132ecc8bb1 fix(images): correct Tiptop Teapot set 2023-08-30 14:51:23 -05:00
Natalie
ca5b02d0ea feat(content): September 2023 subscriber items (#14844)
* feat(content): add June subscriber items

* feat(content): September subscriber items

* fix(image)

fixed filename for April sub item shop set

* chore(sprites): update habitica-images

* fix(images): add missing subscriber items

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-08-30 14:33:44 -05:00
CuriousMagpie
941194b7c7 feat(content): add September pet quest bundle 2023-08-29 16:59:05 -04:00
Weblate
d8a7cad1a1 Translated using Weblate (Russian)
Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Polish)

Currently translated at 73.0% (111 of 152 strings)

Translated using Weblate (Polish)

Currently translated at 71.7% (109 of 152 strings)

Translated using Weblate (Polish)

Currently translated at 65.7% (100 of 152 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (Korean)

Currently translated at 24.3% (37 of 152 strings)

Translated using Weblate (Korean)

Currently translated at 60.1% (1746 of 2901 strings)

Translated using Weblate (Polish)

Currently translated at 55.2% (84 of 152 strings)

Translated using Weblate (Polish)

Currently translated at 32.8% (50 of 152 strings)

Translated using Weblate (Polish)

Currently translated at 60.7% (1762 of 2901 strings)

Translated using Weblate (Polish)

Currently translated at 31.5% (48 of 152 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (155 of 155 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (213 of 213 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (223 of 223 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Russian)

Currently translated at 96.6% (408 of 422 strings)

Translated using Weblate (Ukrainian)

Currently translated at 53.4% (1550 of 2901 strings)

Translated using Weblate (Russian)

Currently translated at 96.8% (217 of 224 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Russian)

Currently translated at 75.0% (114 of 152 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (213 of 213 strings)

Translated using Weblate (Ukrainian)

Currently translated at 52.4% (1522 of 2901 strings)

Translated using Weblate (Russian)

Currently translated at 78.0% (71 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 52.4% (1522 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 52.4% (1522 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 65.9% (60 of 91 strings)

Translated using Weblate (Korean)

Currently translated at 20.3% (31 of 152 strings)

Translated using Weblate (Korean)

Currently translated at 98.4% (185 of 188 strings)

Translated using Weblate (Korean)

Currently translated at 73.5% (592 of 805 strings)

Translated using Weblate (Korean)

Currently translated at 59.1% (132 of 223 strings)

Translated using Weblate (Korean)

Currently translated at 81.8% (625 of 764 strings)

Translated using Weblate (Korean)

Currently translated at 59.4% (1726 of 2901 strings)

Translated using Weblate (Korean)

Currently translated at 50.5% (46 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (223 of 223 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.1% (410 of 422 strings)

Translated using Weblate (Ukrainian)

Currently translated at 51.8% (1504 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 51.8% (1504 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 51.8% (1504 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.5% (793 of 805 strings)

Co-authored-by: Adrián Chaves Fernández <adrian@chaves.io>
Co-authored-by: Bartosz Babik <kotka-wali0h@icloud.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Kimminjae <aezir07@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Sabe Jones <sabe@habitica.com>
Co-authored-by: Svetlana <shkulepo@rambler.ru>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Vladyslav <vladignatiuk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 김경은 <kekim.lang@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pl/
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/character/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Contrib
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/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2023-08-28 22:44:39 +02:00
dependabot[bot]
fa83d1a9cf build(deps-dev): bump sinon from 15.1.2 to 15.2.0 (#14723)
Bumps [sinon](https://github.com/sinonjs/sinon) from 15.1.2 to 15.2.0.
- [Release notes](https://github.com/sinonjs/sinon/releases)
- [Changelog](https://github.com/sinonjs/sinon/blob/main/docs/changelog.md)
- [Commits](https://github.com/sinonjs/sinon/compare/v15.1.2...v15.2.0)

---
updated-dependencies:
- dependency-name: sinon
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:35:52 -04:00
dependabot[bot]
d94851f759 build(deps-dev): bump chalk from 5.2.0 to 5.3.0 (#14736)
Bumps [chalk](https://github.com/chalk/chalk) from 5.2.0 to 5.3.0.
- [Release notes](https://github.com/chalk/chalk/releases)
- [Commits](https://github.com/chalk/chalk/compare/v5.2.0...v5.3.0)

---
updated-dependencies:
- dependency-name: chalk
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:33:18 -04:00
dependabot[bot]
2137c190b3 build(deps): bump smartbanner.js in /website/client (#14745)
Bumps [smartbanner.js](https://github.com/ain/smartbanner.js) from 1.19.2 to 1.19.3.
- [Commits](https://github.com/ain/smartbanner.js/compare/v1.19.2...v1.19.3)

---
updated-dependencies:
- dependency-name: smartbanner.js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:32:42 -04:00
dependabot[bot]
428e693711 build(deps): bump jsonwebtoken from 9.0.0 to 9.0.1 (#14747)
Bumps [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) from 9.0.0 to 9.0.1.
- [Changelog](https://github.com/auth0/node-jsonwebtoken/blob/master/CHANGELOG.md)
- [Commits](https://github.com/auth0/node-jsonwebtoken/compare/v9.0.0...v9.0.1)

---
updated-dependencies:
- dependency-name: jsonwebtoken
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:32:14 -04:00
dependabot[bot]
a7aa489960 build(deps): bump winston from 3.9.0 to 3.10.0 (#14759)
Bumps [winston](https://github.com/winstonjs/winston) from 3.9.0 to 3.10.0.
- [Release notes](https://github.com/winstonjs/winston/releases)
- [Changelog](https://github.com/winstonjs/winston/blob/master/CHANGELOG.md)
- [Commits](https://github.com/winstonjs/winston/compare/v3.9.0...v3.10.0)

---
updated-dependencies:
- dependency-name: winston
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:29:45 -04:00
dependabot[bot]
c917c7c4a9 build(deps): bump fast-xml-parser from 4.2.4 to 4.2.6 (#14768)
Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) from 4.2.4 to 4.2.6.
- [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases)
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v4.2.4...v4.2.6)

---
updated-dependencies:
- dependency-name: fast-xml-parser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:27:43 -04:00
dependabot[bot]
e661838ed7 build(deps): bump word-wrap from 1.2.3 to 1.2.4 in /website/client (#14769)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:27:07 -04:00
dependabot[bot]
af09b6b454 build(deps): bump word-wrap from 1.2.3 to 1.2.4 (#14771)
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:26:29 -04:00
dependabot[bot]
f8b3891a0c build(deps): bump xml2js from 0.6.0 to 0.6.2 (#14789)
Bumps [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js) from 0.6.0 to 0.6.2.
- [Commits](https://github.com/Leonidas-from-XIV/node-xml2js/compare/0.6.0...0.6.2)

---
updated-dependencies:
- dependency-name: xml2js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:25:12 -04:00
dependabot[bot]
631d18244b build(deps): bump rate-limiter-flexible from 2.4.1 to 2.4.2 (#14792)
Bumps [rate-limiter-flexible](https://github.com/animir/node-rate-limiter-flexible) from 2.4.1 to 2.4.2.
- [Release notes](https://github.com/animir/node-rate-limiter-flexible/releases)
- [Commits](https://github.com/animir/node-rate-limiter-flexible/commits)

---
updated-dependencies:
- dependency-name: rate-limiter-flexible
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:23:47 -04:00
dependabot[bot]
7883ba8228 chore(deps): bump @babel/preset-env from 7.22.5 to 7.22.10 (#14814)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.22.5 to 7.22.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.22.10/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:17:58 -04:00
dependabot[bot]
605c2265c5 chore(deps): bump superagent from 8.0.9 to 8.1.2 (#14823)
Bumps [superagent](https://github.com/ladjs/superagent) from 8.0.9 to 8.1.2.
- [Release notes](https://github.com/ladjs/superagent/releases)
- [Changelog](https://github.com/ladjs/superagent/blob/master/HISTORY.md)
- [Commits](https://github.com/ladjs/superagent/compare/v8.0.9...v8.1.2)

---
updated-dependencies:
- dependency-name: superagent
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:17:30 -04:00
dependabot[bot]
1bb0319012 chore(deps): bump bcrypt from 5.1.0 to 5.1.1 (#14824)
Bumps [bcrypt](https://github.com/kelektiv/node.bcrypt.js) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/kelektiv/node.bcrypt.js/releases)
- [Changelog](https://github.com/kelektiv/node.bcrypt.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kelektiv/node.bcrypt.js/compare/v5.1.0...v5.1.1)

---
updated-dependencies:
- dependency-name: bcrypt
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:17:01 -04:00
dependabot[bot]
1f0fd7d8b4 chore(deps): bump core-js from 3.31.0 to 3.32.1 in /website/client (#14827)
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.31.0 to 3.32.1.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.32.1/packages/core-js)

---
updated-dependencies:
- dependency-name: core-js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:13:07 -04:00
dependabot[bot]
6b4b53a430 chore(deps): bump intro.js from 7.0.1 to 7.2.0 in /website/client (#14828)
Bumps [intro.js](https://github.com/usablica/intro.js) from 7.0.1 to 7.2.0.
- [Release notes](https://github.com/usablica/intro.js/releases)
- [Changelog](https://github.com/usablica/intro.js/blob/master/tsconfig.release.json)
- [Commits](https://github.com/usablica/intro.js/compare/v7.0.1...v7.2.0)

---
updated-dependencies:
- dependency-name: intro.js
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-22 14:12:31 -04:00
SabreCat
331ea18c42 Merge branch 'develop' into release 2023-08-22 12:33:29 -05:00
Weblate
db53d87cba Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Ukrainian)

Currently translated at 50.1% (1455 of 2901 strings)

Translated using Weblate (Croatian)

Currently translated at 87.2% (328 of 376 strings)

Translated using Weblate (Ukrainian)

Currently translated at 49.3% (1431 of 2901 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (799 of 805 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.8% (2867 of 2901 strings)

Translated using Weblate (Portuguese)

Currently translated at 61.1% (1775 of 2901 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 64.4% (98 of 152 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Japanese)

Currently translated at 98.4% (129 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 49.0% (1422 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 48.8% (1418 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 48.8% (1417 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Ukrainian)

Currently translated at 48.6% (1410 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 48.6% (1410 of 2901 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.8% (2867 of 2901 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.7% (375 of 376 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.8% (2867 of 2901 strings)

Translated using Weblate (Portuguese)

Currently translated at 61.1% (1775 of 2901 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.5% (222 of 223 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Ukrainian)

Currently translated at 48.5% (1407 of 2901 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.8% (2867 of 2901 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 64.4% (98 of 152 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.9% (186 of 188 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 98.5% (793 of 805 strings)

Translated using Weblate (Ukrainian)

Currently translated at 48.0% (1393 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 48.0% (1393 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.9% (1392 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.9% (1392 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.9% (1391 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.9% (1391 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.9% (1390 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.9% (1390 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.8% (1389 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.8% (1389 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.8% (1388 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.8% (1388 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.8% (1387 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.8% (1387 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.7% (1386 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.7% (1386 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.7% (1385 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.7% (1385 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.6% (1383 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.6% (1383 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.6% (1382 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.6% (1382 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.6% (1381 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.6% (1381 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.5% (1380 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.5% (1380 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.5% (1379 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.5% (1379 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.5% (1378 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.5% (1378 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.3% (1373 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.2% (1371 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.2% (1370 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.1% (1369 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.1% (1369 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.1% (1369 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.1% (1367 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.1% (1367 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.0% (1366 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.0% (1366 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 47.0% (1364 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 46.8% (1358 of 2901 strings)

Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Deni Zubin <deni.zubin@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Vladyslav <vladignatiuk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 照水 <d332zms@hotmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
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/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translation: Habitica/Backgrounds
Translation: Habitica/Character
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Limited
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Questscontent
Translation: Habitica/Subscriber
2023-08-22 19:34:17 +02:00
SabreCat
ebebbbaac5 5.2.0 2023-08-22 12:25:30 -05:00
SabreCat
63376b918e Squashed commit of the following:
commit b7fb903dcab2dbdc55ddd27e9cbd8054f0d5e2a8
Author: SabreCat <sabe@habitica.com>
Date:   Sat Aug 19 19:44:42 2023 -0500

    fix(invites): add missing param

commit 30053cc8b86fc1992d872a068e60f3dd5a456a07
Author: SabreCat <sabe@habitica.com>
Date:   Sat Aug 19 19:06:51 2023 -0500

    fix(party): enforce size limit when using @-names

commit 62dd314cda4165bedbc6b490a8e2f21de87deaf4
Author: SabreCat <sabe@habitica.com>
Date:   Sat Aug 19 19:01:15 2023 -0500

    Revert "Revert "fix(parties): actual 30 not 29""

    This reverts commit 63414a80fe.
2023-08-22 12:25:17 -05:00
SabreCat
ba96cd6e24 Squashed commit of the following:
commit 22971a0c0bd1c25e147fdc9d662fd55ca522193b
Author: SabreCat <sabe@habitica.com>
Date:   Fri Aug 18 21:13:49 2023 -0500

    fix(auth): don't mix include/exclude

commit efbb8fa136587a4c781660246cb426968cfe108a
Author: SabreCat <sabe@habitica.com>
Date:   Fri Aug 18 20:51:30 2023 -0500

    refactor(auth): remove unneeded query field
2023-08-22 12:24:16 -05:00
SabreCat
9e0e2a83be Squashed commit of the following:
commit 474fa530a0392ff6e7461eaff64c81a67b5e4eff
Author: SabreCat <sabe@habitica.com>
Date:   Fri Aug 18 21:44:09 2023 -0500

    fix(challenges): filter out groups without perms
2023-08-22 12:23:39 -05:00
SabreCat
1fa926ac04 Squashed commit of the following:
commit 91d5efa683bb2f1c71b291fab4ff5924bddae1ce
Author: SabreCat <sabe@habitica.com>
Date:   Fri Aug 18 22:18:57 2023 -0500

    refactor(static): remove broken, unused apps page
2023-08-22 12:23:20 -05:00
SabreCat
c71f0b3fda Squashed commit of the following:
commit 6d74f87db332fd28c7522cabc0f96a390d36e64f
Author: SabreCat <sabe@habitica.com>
Date:   Fri Aug 18 20:04:52 2023 -0500

    fix(i18n): default to EN for empties
2023-08-22 12:23:02 -05:00
Natalie L
e8f5958f77 feat(content): add boneless boss achievement (#14788)
* feat(content): add June subscriber items

* feat(content): add boneless boss achievement

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-08-22 12:23:43 -05:00
SabreCat
2803db73e5 5.1.4 2023-08-17 15:39:35 -05:00
SabreCat
0f9adf6675 Merge branch 'sabrecat/more-group-fixes' into release 2023-08-17 15:29:36 -05:00
SabreCat
63414a80fe Revert "fix(parties): actual 30 not 29"
This reverts commit bf0e640fa6.
2023-08-17 14:22:33 -05:00
SabreCat
e73e8bfb9e Revert "fix(parties): actual 30 not 29"
This reverts commit bf0e640fa6.
2023-08-16 16:56:01 -05:00
SabreCat
694fe5a273 Merge branch 'develop' into release 2023-08-16 16:40:49 -05:00
Weblate
82ebe71eb4 Translated using Weblate (Ukrainian)
Currently translated at 46.8% (1358 of 2901 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (419 of 422 strings)

Translated using Weblate (Ukrainian)

Currently translated at 46.8% (1358 of 2901 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (English (United Kingdom))

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Ukrainian)

Currently translated at 46.7% (1356 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.3% (800 of 805 strings)

Translated using Weblate (Ukrainian)

Currently translated at 46.7% (1356 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 46.7% (1356 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 46.4% (1348 of 2901 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.1% (112 of 113 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (419 of 422 strings)

Translated using Weblate (Ukrainian)

Currently translated at 45.5% (1321 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 45.5% (1320 of 2901 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 45.3% (1316 of 2901 strings)

Translated using Weblate (Ukrainian)

Currently translated at 44.5% (1293 of 2901 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (223 of 223 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (805 of 805 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.1% (410 of 422 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (419 of 422 strings)

Translated using Weblate (French)

Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Ukrainian)

Currently translated at 44.8% (1292 of 2881 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (223 of 224 strings)

Translated using Weblate (French)

Currently translated at 46.7% (71 of 152 strings)

Translated using Weblate (Ukrainian)

Currently translated at 72.5% (66 of 91 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 62.6% (57 of 91 strings)

Translated using Weblate (Japanese)

Currently translated at 64.8% (59 of 91 strings)

Translated using Weblate (French)

Currently translated at 80.2% (73 of 91 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (213 of 213 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (213 of 213 strings)

Translated using Weblate (French)

Currently translated at 100.0% (223 of 223 strings)

Translated using Weblate (French)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (French)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (French)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (French)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (French)

Currently translated at 99.5% (420 of 422 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2881 of 2881 strings)

Translated using Weblate (French)

Currently translated at 100.0% (224 of 224 strings)

Translated using Weblate (French)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (French)

Currently translated at 44.7% (68 of 152 strings)

Translated using Weblate (French)

Currently translated at 65.9% (60 of 91 strings)

Translated using Weblate (French)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (French)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (French)

Currently translated at 100.0% (213 of 213 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 44.3% (1279 of 2881 strings)

Translated using Weblate (French)

Currently translated at 99.2% (2860 of 2881 strings)

Translated using Weblate (French)

Currently translated at 37.5% (57 of 152 strings)

Translated using Weblate (Ukrainian)

Currently translated at 68.1% (62 of 91 strings)

Translated using Weblate (Arabic)

Currently translated at 90.4% (340 of 376 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (113 of 113 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Japanese)

Currently translated at 94.7% (400 of 422 strings)

Translated using Weblate (Ukrainian)

Currently translated at 43.3% (1250 of 2881 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 62.6% (57 of 91 strings)

Translated using Weblate (Russian)

Currently translated at 97.8% (46 of 47 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 94.3% (398 of 422 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (422 of 422 strings)

Translated using Weblate (Ukrainian)

Currently translated at 42.7% (1231 of 2881 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (224 of 224 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Ukrainian)

Currently translated at 61.5% (56 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (213 of 213 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 93.6% (395 of 422 strings)

Co-authored-by: Ali Adnan <ali0088552211@gmail.com>
Co-authored-by: Carlos Henrique Silva <marcoantoniobad@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: Jerry Chen <minecjraft@qq.com>
Co-authored-by: Lucas Fieri <lucasfieri@gmail.com>
Co-authored-by: Sha Kong-Brooks <sha.kongbrooks@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Vladyslav <vladignatiuk@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Естай <akseleu@yahoo.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/death/en_GB/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
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/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
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/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
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/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
2023-08-16 23:02:00 +02:00
SabreCat
58d58ff962 fix(guilds): correct various errors 2023-08-16 15:56:10 -05:00
SabreCat
04dcb27501 5.1.3 2023-08-16 15:01:54 -05:00
SabreCat
c9395ba1ca fix(strings): patch up Orb, quests, potions 2023-08-16 15:01:41 -05:00
SabreCat
e9845e7b01 Merge branch 'release' into develop 2023-08-15 15:51:20 -05:00
SabreCat
faa7ff6328 5.1.2 2023-08-15 15:51:10 -05:00
SabreCat
50cc7ee09a fix(profile): skip redundant navigation 2023-08-15 15:48:30 -05:00
SabreCat
db56134832 Merge branch 'sabrecat/leave-challenge' into release 2023-08-15 15:32:23 -05:00
SabreCat
1ea954ab10 fix(challenge): don't pierce privacy on GET/:id 2023-08-15 15:21:28 -05:00
SabreCat
e405372319 Merge branch 'release' into develop 2023-08-15 14:55:06 -05:00
SabreCat
8f64afe9df fix(challenges): leave chal from invalid group 2023-08-15 14:54:22 -05:00
SabreCat
bf0e640fa6 fix(parties): actual 30 not 29 2023-08-15 14:33:32 -05:00
SabreCat
a6792a4f08 5.1.1 2023-08-14 14:36:49 -05:00
SabreCat
0007736f5c Merge branch 'sabrecat/distant-cliff' into release 2023-08-14 14:36:41 -05:00
Natalie L
d564944507 feat(content): add August Pet Quest Bundles and Magic Hatching Potions (#14786)
* feat(content): add June subscriber items

* feat(content): add August pet quest bundle and magic hatching potions

* fix(time): updated start time to 0800EDT

* fix(content): correct start date for potions

---------

Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2023-08-14 14:37:42 -05:00
negue
b679cfb935 Split Vue.Router routes (#14812) 2023-08-11 15:34:59 -05:00
SabreCat
464e4f10b2 feat(chats): increase chat entries to 400 2023-08-11 14:34:27 -05:00
SabreCat
647b27c55f Merge branch 'release' into develop 2023-08-10 16:32:27 -05:00
SabreCat
ac4e6490d9 5.1.0 2023-08-10 12:52:42 -05:00
SabreCat
a3784e98a3 fix(gear): correct stat assignment 2023-08-10 12:52:02 -05:00
SabreCat
5931f02692 feat(content): 2023 August Armoire and Backgrounds by @CuriousMagpie 2023-08-10 12:36:48 -05:00
SabreCat
e21aa074e4 5.0.1 2023-08-08 14:47:44 -05:00
SabreCat
fd038bd150 fix(groups): redirect guild url to group plan 2023-08-08 13:44:21 -05:00
Weblate
699be3a3cc Merge branch 'origin/develop' into Weblate. 2023-08-08 16:28:31 +02:00
SabreCat
bb7e0e22ec 5.0.0 2023-08-08 09:24:27 -05:00
SabreCat
a79a088a8f Merge branch 'sabrecat/unsociable' into release 2023-08-08 09:24:06 -05:00
SabreCat
60c0e6b3df fix(lint): whitespace 2023-08-08 01:26:56 -05:00
SabreCat
43c10f75c3 fix(lint): arrow fn 2023-08-08 01:08:11 -05:00
SabreCat
d4e3e83d46 fix(challenges): map IDs 2023-08-07 23:53:55 -05:00
SabreCat
013f8bcca7 fix(challenges): fetch purchased 2023-08-07 22:56:58 -05:00
SabreCat
ebd0cb72de fix(challenges): better screening 2023-08-07 22:26:56 -05:00
SabreCat
c44b1670cf fix(challenges): revert to working 2023-08-07 22:00:47 -05:00
SabreCat
39477c6f11 fix(lint): or/and, import 2023-08-07 21:33:31 -05:00
SabreCat
1371b80635 fix(groups): add back string
also fix party seeking analytics and a spurious text decoration
2023-08-07 15:55:29 -05:00
SabreCat
9fa355fbcc fix(sunset): release candidate 2023-08-07 15:04:44 -05:00
Weblate
b594d2bb29 Translated using Weblate (French)
Currently translated at 97.7% (218 of 223 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (420 of 420 strings)

Translated using Weblate (Ukrainian)

Currently translated at 42.7% (1230 of 2875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (150 of 150 strings)

Translated using Weblate (Persian)

Currently translated at 10.5% (16 of 152 strings)

Translated using Weblate (French)

Currently translated at 97.3% (217 of 223 strings)

Translated using Weblate (Turkish)

Currently translated at 81.8% (108 of 132 strings)

Translated using Weblate (Turkish)

Currently translated at 75.0% (6 of 8 strings)

Translated using Weblate (Turkish)

Currently translated at 52.3% (144 of 275 strings)

Translated using Weblate (Ukrainian)

Currently translated at 41.8% (1204 of 2875 strings)

Translated using Weblate (Turkish)

Currently translated at 59.7% (1718 of 2875 strings)

Translated using Weblate (French)

Currently translated at 98.5% (2833 of 2875 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 93.3% (140 of 150 strings)

Translated using Weblate (French)

Currently translated at 34.6% (52 of 150 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Turkish)

Currently translated at 62.7% (501 of 798 strings)

Translated using Weblate (Italian)

Currently translated at 98.8% (789 of 798 strings)

Translated using Weblate (French)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 76.2% (93 of 122 strings)

Translated using Weblate (Filipino)

Currently translated at 80.9% (646 of 798 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 40.9% (1176 of 2875 strings)

Translated using Weblate (Russian)

Currently translated at 98.7% (2838 of 2875 strings)

Translated using Weblate (Indonesian)

Currently translated at 84.3% (2425 of 2875 strings)

Translated using Weblate (Russian)

Currently translated at 65.3% (98 of 150 strings)

Translated using Weblate (Dutch)

Currently translated at 97.3% (146 of 150 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (150 of 150 strings)

Translated using Weblate (Polish)

Currently translated at 95.9% (766 of 798 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (420 of 420 strings)

Translated using Weblate (Indonesian)

Currently translated at 72.6% (109 of 150 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Ukrainian)

Currently translated at 39.4% (1134 of 2875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 71.3% (107 of 150 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Polish)

Currently translated at 98.5% (135 of 137 strings)

Translated using Weblate (Polish)

Currently translated at 86.0% (192 of 223 strings)

Translated using Weblate (Polish)

Currently translated at 99.2% (131 of 132 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Polish)

Currently translated at 99.5% (418 of 420 strings)

Translated using Weblate (Ukrainian)

Currently translated at 38.1% (1097 of 2875 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Polish)

Currently translated at 98.1% (216 of 220 strings)

Translated using Weblate (Polish)

Currently translated at 96.4% (405 of 420 strings)

Translated using Weblate (Ukrainian)

Currently translated at 38.0% (1093 of 2875 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 59.3% (1706 of 2875 strings)

Translated using Weblate (Galician)

Currently translated at 95.7% (45 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (420 of 420 strings)

Translated using Weblate (Korean)

Currently translated at 73.5% (587 of 798 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 37.9% (1090 of 2875 strings)

Translated using Weblate (Korean)

Currently translated at 29.2% (31 of 106 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 57.5% (61 of 106 strings)

Translated using Weblate (German)

Currently translated at 71.6% (76 of 106 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Filipino)

Currently translated at 78.3% (625 of 798 strings)

Translated using Weblate (Filipino)

Currently translated at 85.4% (358 of 419 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Ukrainian)

Currently translated at 37.6% (1081 of 2875 strings)

Translated using Weblate (Indonesian)

Currently translated at 79.4% (2285 of 2875 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (German)

Currently translated at 99.2% (131 of 132 strings)

Translated using Weblate (German)

Currently translated at 83.6% (102 of 122 strings)

Translated using Weblate (German)

Currently translated at 99.4% (187 of 188 strings)

Translated using Weblate (Russian)

Currently translated at 99.2% (792 of 798 strings)

Translated using Weblate (German)

Currently translated at 100.0% (798 of 798 strings)

Translated using Weblate (Ukrainian)

Currently translated at 36.6% (1054 of 2875 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (French)

Currently translated at 98.5% (2832 of 2875 strings)

Translated using Weblate (French)

Currently translated at 98.2% (2825 of 2875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 35.1% (1011 of 2875 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 34.0% (980 of 2875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 32.8% (945 of 2875 strings)

Translated using Weblate (Indonesian)

Currently translated at 78.8% (2266 of 2875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 32.4% (933 of 2875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (106 of 106 strings)

Translated using Weblate (Indonesian)

Currently translated at 78.4% (2256 of 2875 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (131 of 132 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 95.9% (402 of 419 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 95.9% (402 of 419 strings)

Translated using Weblate (Ukrainian)

Currently translated at 32.1% (924 of 2875 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (798 of 798 strings)

Translated using Weblate (Dutch)

Currently translated at 92.2% (736 of 798 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.4% (786 of 798 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Indonesian)

Currently translated at 78.1% (2246 of 2875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 32.1% (924 of 2875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 31.4% (903 of 2875 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (223 of 223 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Indonesian)

Currently translated at 77.7% (2236 of 2875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 30.8% (887 of 2875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (223 of 223 strings)

Translated using Weblate (Ukrainian)

Currently translated at 30.7% (884 of 2875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 97.3% (744 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.0% (734 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 96.0% (734 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (223 of 223 strings)

Translated using Weblate (Ukrainian)

Currently translated at 30.7% (884 of 2875 strings)

Translated using Weblate (Indonesian)

Currently translated at 77.4% (2226 of 2875 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.0% (105 of 106 strings)

Translated using Weblate (Ukrainian)

Currently translated at 95.5% (730 of 764 strings)

Translated using Weblate (Czech)

Currently translated at 85.5% (130 of 152 strings)

Translated using Weblate (Russian)

Currently translated at 98.6% (2837 of 2875 strings)

Translated using Weblate (Russian)

Currently translated at 99.4% (187 of 188 strings)

Translated using Weblate (Russian)

Currently translated at 99.1% (791 of 798 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (218 of 222 strings)

Translated using Weblate (Indonesian)

Currently translated at 77.0% (2216 of 2875 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (798 of 798 strings)

Translated using Weblate (English (en@lolcat))

Currently translated at 28.9% (44 of 152 strings)

Translated using Weblate (Ukrainian)

Currently translated at 29.0% (833 of 2871 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.9% (2207 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.1% (104 of 106 strings)

Translated using Weblate (Ukrainian)

Currently translated at 94.6% (723 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (798 of 798 strings)

Translated using Weblate (Dutch)

Currently translated at 91.6% (731 of 798 strings)

Translated using Weblate (Polish)

Currently translated at 61.1% (1755 of 2871 strings)

Co-authored-by: @SpiffZyx <spiffzyx@icloud.com>
Co-authored-by: Adrián Chaves Fernández <adrian@chaves.io>
Co-authored-by: Alejo González <alejoplus.max@gmail.com>
Co-authored-by: Ali Ghaffaari <ali.ghaffaari@gmail.com>
Co-authored-by: Antonio <nikola.orwell@gmail.com>
Co-authored-by: Bogdan Derdziak <bagtirr@gmail.com>
Co-authored-by: Brooke Abbey <brookeoclock@gmail.com>
Co-authored-by: BryanLim <youmakemysonlooklikeelonmusk@gmail.com>
Co-authored-by: Dahae Kim <SuperCoonMochi@gmail.com>
Co-authored-by: Deni Zubin <deni.zubin@gmail.com>
Co-authored-by: Ellen A M <ellen_a_m@hotmail.com>
Co-authored-by: Emmanuel Kan <berinojke@gmail.com>
Co-authored-by: Falzart <muh_fauzi_ramadhan@yahoo.co.id>
Co-authored-by: HenryFord <mihka2018geimer@gmail.com>
Co-authored-by: Jerry Chen <minecjraft@qq.com>
Co-authored-by: Julian H <julian.henin29@gmail.com>
Co-authored-by: LiziKnight <liziknight0316@outlook.com>
Co-authored-by: Lolzia <zuzaksup@gmail.com>
Co-authored-by: Mandy Mielke <mielkemandy@outlook.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Nodaysoff <convoron@yandex.ru>
Co-authored-by: Raithe <RaitheOfDureya@gmail.com>
Co-authored-by: Rémi <rem130499@gmail.com>
Co-authored-by: Sergey Shevelev <vlkgamer45@gmail.com>
Co-authored-by: Sofia <cornaasasa@gmail.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Val <3qes0hnzh@mozmail.com>
Co-authored-by: Vanadium <v1512137980@gmail.com>
Co-authored-by: Vlada <vladaplisak@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: billypat <kreideraine@gmail.com>
Co-authored-by: sam de wit <samedewit@gmail.com>
Co-authored-by: Естай <akseleu@yahoo.com>
Co-authored-by: 照水 <d332zms@hotmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/en@lolcat/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fa/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hr/
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/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pl/
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/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/character/de/
Translate-URL: https://translate.habitica.com/projects/habitica/character/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ru/
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_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/de/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/id/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/
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/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/front/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/front/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/id/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ru/
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/generic/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/id/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
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/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/de/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/id/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pl/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Contrib
Translation: Habitica/Defaulttasks
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/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2023-08-07 19:59:53 +02:00
SabreCat
a8b52ab656 fix(chat): correctly map CG bullet points 2023-08-07 12:51:19 -05:00
SabreCat
cce6d91611 fix(groups): return Plans on GET guilds 2023-08-07 12:24:43 -05:00
SabreCat
3109a03055 fix(sunset): Challenge filtering, transactions 2023-08-03 16:30:00 -05:00
SabreCat
14518b8213 fix(tests): avoid mystery pollution in challenges 2023-08-02 20:35:52 -05:00
SabreCat
3ad31c7cd0 fix(tests): release candidate 2023-08-02 20:04:09 -05:00
SabreCat
bfa6d24e47 fix(lint): whoops only 2023-08-02 18:33:40 -05:00
SabreCat
8c88f56d08 fix(tests): chat related 2023-08-02 18:07:19 -05:00
SabreCat
1df3f9d9f3 fix(tests): lint, GET group-plans 2023-08-02 17:00:41 -05:00
SabreCat
b5a0dad7f7 fix(tests): GET groups 2023-08-02 16:48:41 -05:00
SabreCat
9b34c3e11a fix(tests): GET invites 2023-08-02 16:17:16 -05:00
SabreCat
9d61bd724a fix(tests): GET members 2023-08-02 16:02:53 -05:00
SabreCat
13c21139dd fix(tests): GET groups 2023-08-02 15:14:02 -05:00
SabreCat
8e85de53cb fix(tests): POST groups 2023-08-01 20:35:00 -05:00
SabreCat
bf222351e5 4.277.4 2023-08-01 18:38:31 -05:00
SabreCat
6867aab74b fix(faq): better mobile routing, new q 2023-08-01 18:38:21 -05:00
SabreCat
0cae808b7e fix(lint): more destructuring fanciness 2023-08-01 17:45:09 -05:00
SabreCat
81be8316a0 fix(lint): why weren't we destructuring already 2023-08-01 17:20:00 -05:00
SabreCat
d7071d6b4d fix(tests): leave/reject/quests 2023-08-01 16:53:48 -05:00
SabreCat
150cd16b1c Merge branch 'release' into sabrecat/unsociable 2023-08-01 15:27:16 -05:00
SabreCat
38ac4c53d1 4.277.3 2023-08-01 13:44:54 -05:00
SabreCat
9b8bb99039 fix(profile): revert accidental changes 2023-08-01 13:44:49 -05:00
SabreCat
1532f8f774 Merge branch 'sabrecat/chat-warning' into sabrecat/unsociable 2023-07-31 18:44:43 -05:00
SabreCat
aa4426c800 fix(event): corrections to contributor goodies 2023-07-31 18:44:08 -05:00
SabreCat
0140b9beb7 feat(chat): veteran awards 2023-07-31 16:38:30 -05:00
SabreCat
7697d87358 Merge branch 'develop' into sabrecat/unsociable 2023-07-31 12:08:58 -05:00
SabreCat
c2ced5c925 fix(tests): manyfix 2023-07-28 16:29:51 -05:00
SabreCat
b09ae3f053 fix(lint): commas etc 2023-07-28 16:15:35 -05:00
SabreCat
4c60371ebd fix(tests): fix fix fix 2023-07-28 16:08:14 -05:00
SabreCat
16be591ed8 fix(tests): subscriptions, group updates 2023-07-28 15:03:56 -05:00
SabreCat
f75a4f6982 fix(tests): quest block 2023-07-28 14:27:36 -05:00
SabreCat
330c3e1bf6 fix(lint): remove unused fn 2023-07-27 16:34:29 -05:00
SabreCat
0ba3cd3bdf fix(tests): cleanup continues 2023-07-27 16:18:25 -05:00
SabreCat
2cfe11619a fix(lint): quotes, destructuring, space 2023-07-26 17:09:29 -05:00
SabreCat
7607c67070 fix(tests): update challenges 2023-07-26 16:59:57 -05:00
SabreCat
d394858022 fix(tests): new approach attempt 2023-07-25 18:00:31 -05:00
SabreCat
26f5ef093f fix(tests): update Challenges block for sunset 2023-07-25 15:39:17 -05:00
SabreCat
714319d67b Merge branch 'sabrecat/item-notification-url' of https://github.com/HabitRPG/habitica into sabrecat/unsociable 2023-07-25 15:37:28 -05:00
SabreCat
cbaa3180cc Merge branch 'develop' into sabrecat/unsociable 2023-07-25 15:17:13 -05:00
SabreCat
b4866fd3b1 chore(content): update migration for new scheme 2023-07-25 15:09:50 -05:00
SabreCat
86646bbbdb Merge branch 'develop' into sabrecat/item-notification-url 2023-07-25 14:42:21 -05:00
SabreCat
282abecd21 fix(lint): remove whitespace 2023-07-20 16:07:16 -05:00
SabreCat
ba61c91296 refactor(notifs): universal routing for migrations 2023-07-19 16:42:45 -05:00
SabreCat
d49736dd69 fix(migration): include purchased fields 2023-07-19 15:21:00 -05:00
SabreCat
16b766beef fix(migration): actually terminate 2023-07-19 14:46:04 -05:00
SabreCat
a26c5906d6 chore(guilds): migration to return banked Gems 2023-07-17 16:55:21 -05:00
SabreCat
08a5bff815 Merge branch 'release' into sabrecat/unsociable 2023-07-17 16:11:14 -05:00
SabreCat
9706d7ac64 Merge branch 'natalie/antisocial' into sabrecat/unsociable 2023-07-11 12:04:50 -05:00
CuriousMagpie
93564c5d52 WIP(faq): contributor gear date correction 2023-07-11 11:33:41 -04:00
SabreCat
2e94bfc489 fix(faq): updated Linguist vebiage
by @CuriousMagpie
2023-07-06 15:28:08 -05:00
CuriousMagpie
5c8a3f7771 WIP(faq): correct Linguist guidelines link 2023-07-06 13:20:10 -04:00
CuriousMagpie
8ac03e311b WIP(faq): update Linguists section 2023-06-30 12:56:39 -04:00
SabreCat
f84b5f163c Merge branch 'develop' into sabrecat/unsociable 2023-06-29 14:53:00 -05:00
CuriousMagpie
ad118095ef WIP(faq): add link to translate.habitica.com to sunsetFaqPara14 2023-06-21 11:45:23 -04:00
SabreCat
7ecad94a51 fix(faq): remove timezone 2023-06-20 17:28:54 -05:00
SabreCat
328b37322e wip(faq): layout rewrite 2023-06-20 17:28:04 -05:00
CuriousMagpie
81f7fbc2d5 WIP(faq): rawr why css no work? 2023-06-20 18:07:21 -04:00
SabreCat
9590ce939a Merge remote-tracking branch 'private/natalie/antisocial' into sabrecat/unsociable 2023-06-20 15:16:29 -05:00
CuriousMagpie
fff6fbfbd6 WIP(faq): line 155 2023-06-20 16:16:04 -04:00
SabreCat
52abf8acf3 Merge remote-tracking branch 'private/natalie/antisocial' into sabrecat/unsociable 2023-06-20 15:14:43 -05:00
SabreCat
bc61443246 Merge remote-tracking branch 'private/natalie/string-sweep' into sabrecat/unsociable 2023-06-20 15:14:06 -05:00
CuriousMagpie
cd8594a8b9 Merge branch 'sabrecat/unsociable' into natalie/antisocial 2023-06-20 16:13:10 -04:00
CuriousMagpie
f3600f64e8 WIP(faq): add date 2023-06-20 16:10:56 -04:00
SabreCat
752cd57bb1 Merge branch 'natalie/antisocial' into sabrecat/unsociable 2023-06-19 19:06:05 -05:00
SabreCat
5d6bf131f4 fix(sunset): update layouts and links 2023-06-19 19:04:07 -05:00
SabreCat
8445f45b31 Merge branch 'release' into sabrecat/unsociable 2023-06-19 18:14:22 -05:00
CuriousMagpie
7dab47db16 WIP(string-sweep): inn complete 2023-06-15 14:05:00 -04:00
CuriousMagpie
a88ca5a1a8 WIP(string-sweep): tavern complete 2023-06-15 13:59:36 -04:00
CuriousMagpie
c91d115793 WIP(string-sweep): guild/s complete 2023-06-15 13:47:14 -04:00
CuriousMagpie
e3502bd280 Merge remote-tracking branch 'private/sabrecat/unsociable' into natalie/string-sweep 2023-06-15 12:42:13 -04:00
CuriousMagpie
8b5ff7c2f9 WIP(faq): working on Dan'l 2023-06-15 11:40:12 -04:00
SabreCat
3ac260026b WIP(sunset): fixes 2023-06-14 17:11:52 -05:00
CuriousMagpie
80e7fda8ef WIP(string-sweep): de-guildify more strings 2023-06-14 18:05:29 -04:00
SabreCat
1e7ea399b1 fix(sunset): don't show banner after rollout 2023-06-14 16:45:21 -05:00
CuriousMagpie
9c889a42aa Merge remote-tracking branch 'private/sabrecat/unsociable' into natalie/string-sweep 2023-06-14 17:45:03 -04:00
SabreCat
952b99599b Merge branch 'sabrecat/chat-warning' into sabrecat/unsociable 2023-06-14 16:41:37 -05:00
CuriousMagpie
973fa2edc2 WIP(faq): update spacing and font size 2023-06-14 16:43:18 -04:00
CuriousMagpie
5e04040f5f WIP(faq): initial round of edits 2023-06-14 12:09:49 -04:00
CuriousMagpie
2fc9480ae9 WIP(string-sweep): first smol batch 2023-06-12 14:56:07 -04:00
SabreCat
429afc1e71 feat(404): route retired links 2023-06-09 16:33:27 -05:00
SabreCat
80da313844 chore(cg): update Guidelines 2023-06-08 17:20:56 -05:00
SabreCat
de057dc1b2 WIP(sunset): close X, CG revisions 2023-06-07 17:03:07 -05:00
SabreCat
e1398e8d7c Merge branch 'develop' into sabrecat/unsociable 2023-06-06 09:22:37 -05:00
SabreCat
7e2a35d7a9 WIP(sunset): fix private Guild and report form 2023-06-05 16:36:00 -05:00
SabreCat
df5d1e95d1 chore(sunset): end standard guild creation API 2023-06-02 16:09:11 -05:00
SabreCat
f4e8bf9c2e feat(form): Ask a Question mode on bug report 2023-06-02 15:58:24 -05:00
SabreCat
9c7f1ae630 Merge branch 'release' into sabrecat/unsociable 2023-06-02 15:01:57 -05:00
SabreCat
8d73e2949a Merge branch 'release' into sabrecat/unsociable 2023-06-01 15:42:47 -05:00
SabreCat
9228b070fa Merge branch 'release' into sabrecat/unsociable 2023-05-24 14:17:49 -05:00
SabreCat
8d9602fb16 WIP(chat): first pass deprecation 2023-05-17 16:33:44 -05:00
2099 changed files with 19152 additions and 287262 deletions

View File

@@ -104,42 +104,6 @@ updates:
- 7.7.0
- 7.8.0
- 7.9.0
- dependency-name: "@storybook/addon-knobs"
versions:
- 6.1.17
- 6.1.18
- 6.1.20
- 6.1.21
- 6.2.2
- 6.2.3
- 6.2.7
- dependency-name: "@storybook/addon-links"
versions:
- 6.1.17
- 6.1.18
- 6.1.20
- 6.1.21
- 6.2.2
- 6.2.3
- 6.2.7
- dependency-name: "@storybook/vue"
versions:
- 6.1.17
- 6.1.18
- 6.1.20
- 6.1.21
- 6.2.2
- 6.2.3
- 6.2.7
- dependency-name: "@storybook/addon-actions"
versions:
- 6.1.17
- 6.1.18
- 6.1.20
- 6.1.21
- 6.2.2
- 6.2.3
- 6.2.7
- dependency-name: core-js
versions:
- 3.10.0

View File

@@ -51,7 +51,7 @@ 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.2.8', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']);
const runRsProcess = spawn('run-rs', ['-v', '4.1.1', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']);
for await (const chunk of runRsProcess.stdout) {
const stringChunk = chunk.toString();

View File

@@ -0,0 +1,72 @@
/* eslint-disable no-console */
import { model as User } from '../../../website/server/models/user';
import { model as Group } from '../../../website/server/models/group';
const guildsPerRun = 500;
const progressCount = 1000;
const guildsQuery = {
type: 'guild',
};
let count = 0;
async function updateGroup (guild) {
count++;
if (count % progressCount === 0) {
console.warn(`${count} ${guild._id}`);
}
if (guild.hasActiveGroupPlan()) {
return console.warn(`Guild ${guild._id} is active Group Plan`);
}
const leader = await User
.findOne({ _id: guild.leader })
.select({ _id: true })
.exec();
if (!leader) {
return console.warn(`Leader not found for Guild ${guild._id}`);
}
if (guild.balance > 0) {
await leader.updateBalance(
guild.balance,
'create_guild',
'',
`Guild Bank refund for ${guild.name} (${guild._id})`,
);
}
return guild.updateOne({ $set: { balance: 0 } }).exec();
}
export default async function processGroups () {
const guildFields = {
_id: 1,
balance: 1,
leader: 1,
name: 1,
purchased: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const foundGroups = await Group // eslint-disable-line no-await-in-loop
.find(guildsQuery)
.limit(guildsPerRun)
.sort({ _id: 1 })
.select(guildFields)
.exec();
if (foundGroups.length === 0) {
console.warn('All appropriate Guilds found and modified.');
console.warn(`\n${count} Guilds processed\n`);
break;
} else {
guildsQuery._id = {
$gt: foundGroups[foundGroups.length - 1],
};
}
await Promise.all(foundGroups.map(guild => updateGroup(guild))); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,62 @@
/* eslint-disable no-console */
import { model as User } from '../../../website/server/models/user';
import { TransactionModel as Transaction } from '../../../website/server/models/transaction';
const transactionsPerRun = 500;
const progressCount = 1000;
const transactionsQuery = {
transactionType: 'create_guild',
amount: { $gt: 0 },
};
let count = 0;
async function updateTransaction (transaction) {
count++;
if (count % progressCount === 0) {
console.warn(`${count} ${transaction._id}`);
}
const leader = await User
.findOne({ _id: transaction.userId })
.select({ _id: true })
.exec();
if (!leader) {
return console.warn(`User not found for transaction ${transaction._id}`);
}
return leader.updateOne(
{ $inc: { balance: transaction.amount }},
).exec();
}
export default async function processTransactions () {
const transactionFields = {
_id: 1,
userId: 1,
currency: 1,
amount: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const foundTransactions = await Transaction // eslint-disable-line no-await-in-loop
.find(transactionsQuery)
.limit(transactionsPerRun)
.sort({ _id: 1 })
.select(transactionFields)
.lean()
.exec();
if (foundTransactions.length === 0) {
console.warn('All appropriate transactions found and modified.');
console.warn(`\n${count} transactions processed\n`);
break;
} else {
transactionsQuery._id = {
$gt: foundTransactions[foundTransactions.length - 1],
};
}
await Promise.all(foundTransactions.map(txn => updateTransaction(txn))); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,144 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230808_veteran_pet_ladder';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {};
let push = { notifications: { $each: [] }};
set.migration = MIGRATION_NAME;
if (user.items.pets['Fox-Veteran']) {
set['items.pets.Dragon-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_dragon',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Dragon.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Bear-Veteran']) {
set['items.pets.Fox-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_fox',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Fox.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Lion-Veteran']) {
set['items.pets.Bear-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_bear',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Bear.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Tiger-Veteran']) {
set['items.pets.Lion-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_lion',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Lion.',
destination: '/inventory/stable',
},
seen: false,
});
} else if (user.items.pets['Wolf-Veteran']) {
set['items.pets.Tiger-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_tiger',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Tiger.',
destination: '/inventory/stable',
},
seen: false,
});
} else {
set['items.pets.Wolf-Veteran'] = 5;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'icon_pet_veteran_wolf',
title: 'Youve received a Veteran Pet!',
text: 'To commemorate being here for a new era of Habitica, weve awarded you a Veteran Wolf.',
destination: '/inventory/stable',
},
seen: false,
});
}
if (user.contributor.level > 0) {
set['items.gear.owned.armor_special_heroicTunic'] = true;
set['items.gear.owned.back_special_heroicAureole'] = true;
set['items.gear.owned.headAccessory_special_heroicCirclet'] = true;
push.notifications.$each.push({
type: 'ITEM_RECEIVED',
data: {
icon: 'heroic_set_icon',
title: 'Youve received the Heroic Set!',
text: 'To commemorate your hard work as a contributor, weve awarded you the Heroic Circlet, Heroic Aureole, and Heroic Tunic.',
destination: '/inventory/equipment',
},
seen: false,
});
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({_id: user._id}, {$set: set, $push: push}).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
// 'auth.timestamps.loggedin': { $gt: new Date('2023-07-08') },
};
const fields = {
_id: 1,
items: 1,
migration: 1,
contributor: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,118 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20231017_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['Armadillo-Base']
&& pets['Armadillo-CottonCandyBlue']
&& pets['Armadillo-CottonCandyPink']
&& pets['Armadillo-Desert']
&& pets['Armadillo-Golden']
&& pets['Armadillo-Red']
&& pets['Armadillo-Shade']
&& pets['Armadillo-Skeleton']
&& pets['Armadillo-White']
&& pets['Armadillo-Zombie']
&& pets['Cactus-Base']
&& pets['Cactus-CottonCandyBlue']
&& pets['Cactus-CottonCandyPink']
&& pets['Cactus-Desert']
&& pets['Cactus-Golden']
&& pets['Cactus-Red']
&& pets['Cactus-Shade']
&& pets['Cactus-Skeleton']
&& pets['Cactus-White']
&& pets['Cactus-Zombie']
&& pets['Fox-Base']
&& pets['Fox-CottonCandyBlue']
&& pets['Fox-CottonCandyPink']
&& pets['Fox-Desert']
&& pets['Fox-Golden']
&& pets['Fox-Red']
&& pets['Fox-Shade']
&& pets['Fox-Skeleton']
&& pets['Fox-White']
&& pets['Fox-Zombie']
&& pets['Frog-Base']
&& pets['Frog-CottonCandyBlue']
&& pets['Frog-CottonCandyPink']
&& pets['Frog-Desert']
&& pets['Frog-Golden']
&& pets['Frog-Red']
&& pets['Frog-Shade']
&& pets['Frog-Skeleton']
&& pets['Frog-White']
&& pets['Frog-Zombie']
&& pets['Snake-Base']
&& pets['Snake-CottonCandyBlue']
&& pets['Snake-CottonCandyPink']
&& pets['Snake-Desert']
&& pets['Snake-Golden']
&& pets['Snake-Red']
&& pets['Snake-Shade']
&& pets['Snake-Skeleton']
&& pets['Snake-White']
&& pets['Snake-Zombie']
&& pets['Spider-Base']
&& pets['Spider-CottonCandyBlue']
&& pets['Spider-CottonCandyPink']
&& pets['Spider-Desert']
&& pets['Spider-Golden']
&& pets['Spider-Red']
&& pets['Spider-Shade']
&& pets['Spider-Skeleton']
&& pets['Spider-White']
&& pets['Spider-Zombie']) {
set['achievements.duneBuddy'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.updateOne({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-09-16') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,124 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20231114_pet_group_achievements';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
let set = {
migration: MIGRATION_NAME,
};
if (user && user.items && user.items.pets) {
const pets = user.items.pets;
if (pets['Cactus-Zombie'] > 0
&& pets['Cactus-Skeleton'] > 0
&& pets['Cactus-Base'] > 0
&& pets['Cactus-Desert'] > 0
&& pets['Cactus-Red'] > 0
&& pets['Cactus-Shade'] > 0
&& pets['Cactus-White']> 0
&& pets['Cactus-Golden'] > 0
&& pets['Cactus-CottonCandyBlue'] > 0
&& pets['Cactus-CottonCandyPink'] > 0
&& pets['Hedgehog-Zombie'] > 0
&& pets['Hedgehog-Skeleton'] > 0
&& pets['Hedgehog-Base'] > 0
&& pets['Hedgehog-Desert'] > 0
&& pets['Hedgehog-Red'] > 0
&& pets['Hedgehog-Shade'] > 0
&& pets['Hedgehog-White'] > 0
&& pets['Hedgehog-Golder'] > 0
&& pets['Hedgehog-CottonCandyBlue'] > 0
&& pets['Hedgehog-CottonCandyPink'] > 0
&& pets['Rock-Zombie'] > 0
&& pets['Rock-Skeleton'] > 0
&& pets['Rock-Base'] > 0
&& pets['Rock-Desert'] > 0
&& pets['Rock-Red'] > 0
&& pets['Rock-Shade'] > 0
&& pets['Rock-White'] > 0
&& pets['Rock-Golden'] > 0
&& pets['Rock-CottonCandyBlue'] > 0
&& pets['Rock-CottonCandyPink'] > 0 ) {
set['achievements.roughRider'] = true;
}
}
if (user && user.items && user.items.mounts) {
const mounts = user.items.mounts;
if (mounts['Cactus-Zombie']
&& mounts['Cactus-Skeleton']
&& mounts['Cactus-Base']
&& mounts['Cactus-Desert']
&& mounts['Cactus-Red']
&& mounts['Cactus-Shade']
&& mounts['Cactus-White']
&& mounts['Cactus-Golden']
&& mounts['Cactus-CottonCandyPink']
&& mounts['Cactus-CottonCandyBlue']
&& mounts['Hedgehog-Zombie']
&& mounts['Hedgehog-Skeleton']
&& mounts['Hedgehog-Base']
&& mounts['Hedgehog-Desert']
&& mounts['Hedgehog-Red']
&& mounts['Hedgehog-Shade']
&& mounts['Hedgehog-White']
&& mounts['Hedgehog-Golden']
&& mounts['Hedgehog-CottonCandyPink']
&& mounts['Hedgehog-CottonCandyBlue']
&& mounts['Rock-Zombie']
&& mounts['Rock-Skeleton']
&& mounts['Rock-Base']
&& mounts['Rock-Desert']
&& mounts['Rock-Red']
&& mounts['Rock-Shade']
&& mounts['Rock-White']
&& mounts['Rock-Golden']
&& mounts['Rock-CottonCandyPink']
&& mounts['Rock-CottonCandyBlue'] ) {
set['achievements.roughRider'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({ _id: user._id }, { $set: set }).exec();
}
module.exports = async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-02-01') },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1]._id,
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,87 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20231228_nye';
import { model as User } from '../../../website/server/models/user';
import { v4 as uuid } from 'uuid';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
const set = { migration: MIGRATION_NAME };
let push = {};
if (typeof user.items.gear.owned.head_special_nye2022 !== 'undefined') {
set['items.gear.owned.head_special_nye2023'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2021 !== 'undefined') {
set['items.gear.owned.head_special_nye2022'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2020 !== 'undefined') {
set['items.gear.owned.head_special_nye2021'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2019 !== 'undefined') {
set['items.gear.owned.head_special_nye2020'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2018 !== 'undefined') {
set['items.gear.owned.head_special_nye2019'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2017 !== 'undefined') {
set['items.gear.owned.head_special_nye2018'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2016 !== 'undefined') {
set['items.gear.owned.head_special_nye2017'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2015 !== 'undefined') {
set['items.gear.owned.head_special_nye2016'] = true;
} else if (typeof user.items.gear.owned.head_special_nye2014 !== 'undefined') {
set['items.gear.owned.head_special_nye2015'] = true;
} else if (typeof user.items.gear.owned.head_special_nye !== 'undefined') {
set['items.gear.owned.head_special_nye2014'] = true;
} else {
set['items.gear.owned.head_special_nye'] = true;
}
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_head_special_nye',
title: 'Happy New Year!',
text: 'Check your Equipment for this year\'s party hat!',
destination: 'inventory/equipment',
},
seen: false,
};
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.updateOne({_id: user._id}, {$set: set, $push: push}).exec();
}
export default async function processUsers () {
let query = {
'auth.timestamps.loggedin': { $gt: new Date('2023-12-01') },
migration: { $ne: MIGRATION_NAME },
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

View File

@@ -0,0 +1,102 @@
/* eslint-disable no-console */
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const MIGRATION_NAME = '20240131_habit_birthday';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count += 1;
const inc = {
'items.food.Cake_Skeleton': 1,
'items.food.Cake_Base': 1,
'items.food.Cake_CottonCandyBlue': 1,
'items.food.Cake_CottonCandyPink': 1,
'items.food.Cake_Shade': 1,
'items.food.Cake_White': 1,
'items.food.Cake_Golden': 1,
'items.food.Cake_Zombie': 1,
'items.food.Cake_Desert': 1,
'items.food.Cake_Red': 1,
'achievements.habitBirthdays': 1,
};
const set = {};
const push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_cake',
title: 'Happy Habit Birthday!',
text: 'Habitica turns 11 today! Enjoy free party robes and cake!',
destination: 'inventory/equipment',
},
seen: false,
},
};
set.migration = MIGRATION_NAME;
if (typeof user.items.gear.owned.armor_special_birthday2023 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2024'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2022 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2023'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2021 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2022'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2020 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2021'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2019 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2020'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2018 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2019'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2017 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2018'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2016 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2017'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday2015 !== 'undefined') {
set['items.gear.owned.armor_special_birthday2016'] = true;
} else if (typeof user.items.gear.owned.armor_special_birthday !== 'undefined') {
set['items.gear.owned.armor_special_birthday2015'] = true;
} else {
set['items.gear.owned.armor_special_birthday'] = true;
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.updateOne({_id: user._id}, {$inc: inc, $set: set, $push: push}).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2023-12-23')},
};
const fields = {
_id: 1,
items: 1,
};
while (true) { // eslint-disable-line no-constant-condition
const users = await User // eslint-disable-line no-await-in-loop
.find(query)
.limit(250)
.sort({_id: 1})
.select(fields)
.lean()
.exec();
if (users.length === 0) {
console.warn('All appropriate users found and modified.');
console.warn(`\n${count} users processed\n`);
break;
} else {
query._id = {
$gt: users[users.length - 1],
};
}
await Promise.all(users.map(updateUser)); // eslint-disable-line no-await-in-loop
}
};

2421
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +1,23 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.277.2",
"version": "5.15.0",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"@babel/register": "^7.22.5",
"@babel/core": "^7.22.10",
"@babel/preset-env": "^7.22.10",
"@babel/register": "^7.22.15",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.1.3",
"@parse/node-apn": "^5.2.3",
"@slack/webhook": "^6.1.0",
"accepts": "^1.3.8",
"amazon-payments": "^0.2.9",
"amplitude": "^6.0.0",
"apidoc": "^0.54.0",
"apple-auth": "^1.0.9",
"bcrypt": "^5.1.0",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"bootstrap": "^4.6.0",
"bootstrap": "^4.6.2",
"compression": "^1.7.4",
"cookie-session": "^2.0.0",
"coupon-code": "^0.4.5",
@@ -31,52 +31,53 @@
"express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0",
"glob": "^8.1.0",
"got": "^11.8.3",
"got": "^11.8.6",
"gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
"gulp-imagemin": "^7.1.0",
"gulp-nodemon": "^2.5.0",
"nodemon": "^2.0.20",
"gulp.spritesmith": "^6.13.0",
"habitica-markdown": "^3.0.0",
"helmet": "^4.6.0",
"image-size": "^1.0.2",
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0",
"jsonwebtoken": "^9.0.0",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^2.1.5",
"lodash": "^4.17.21",
"merge-stream": "^2.0.0",
"method-override": "^3.0.0",
"moment": "^2.29.4",
"moment-recur": "^1.0.7",
"mongoose": "^5.13.7",
"mongoose": "^5.13.20",
"morgan": "^1.10.0",
"nconf": "^0.12.0",
"nconf": "^0.12.1",
"node-gcm": "^1.0.5",
"on-headers": "^1.0.2",
"passport": "^0.5.0",
"passport": "^0.5.3",
"passport-facebook": "^3.0.0",
"passport-google-oauth2": "^0.2.0",
"passport-google-oauth20": "2.0.0",
"paypal-rest-sdk": "^1.8.1",
"pp-ipn": "^1.1.0",
"ps-tree": "^1.0.0",
"rate-limiter-flexible": "^2.4.0",
"rate-limiter-flexible": "^2.4.2",
"redis": "^3.1.2",
"regenerator-runtime": "^0.13.11",
"remove-markdown": "^0.5.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.2",
"stripe": "^12.9.0",
"superagent": "^8.0.9",
"stripe": "^12.18.0",
"superagent": "^8.1.2",
"universal-analytics": "^0.5.3",
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.9.0",
"validator": "^13.11.0",
"vinyl-buffer": "^1.0.1",
"winston": "^3.9.0",
"winston-loggly-bulk": "^3.2.1",
"xml2js": "^0.6.0"
"winston": "^3.10.0",
"winston-loggly-bulk": "^3.3.0",
"xml2js": "^0.6.2"
},
"private": true,
"engines": {
@@ -105,16 +106,16 @@
"client:unit": "cd website/client && npm run test:unit",
"start": "gulp nodemon",
"debug": "gulp nodemon --inspect",
"mongo:dev": "run-rs -v 4.2.8 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
"mongo:dev": "run-rs -v 4.1.1 -l ubuntu1804 --keep --dbpath mongodb-data --number 1 --quiet",
"postinstall": "git config --global url.\"https://\".insteadOf git:// && gulp build && cd website/client && npm install",
"apidoc": "gulp apidoc"
},
"devDependencies": {
"axios": "^1.3.6",
"axios": "^1.4.0",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0",
"chalk": "^5.2.0",
"chalk": "^5.3.0",
"cross-spawn": "^7.0.3",
"expect.js": "^0.3.1",
"istanbul": "^1.1.0-alpha.1",
@@ -122,7 +123,7 @@
"monk": "^7.3.4",
"require-again": "^2.0.0",
"run-rs": "^0.7.7",
"sinon": "^15.1.2",
"sinon": "^15.2.0",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
},

View File

@@ -16,60 +16,7 @@ describe('GET /challenges/:challengeId', () => {
});
});
context('public guild', () => {
let groupLeader;
let group;
let challenge;
let user;
beforeEach(async () => {
user = await generateUser();
const populatedGroup = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'public' },
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
});
it('should return challenge data', async () => {
await challenge.sync();
const chal = await user.get(`/challenges/${challenge._id}`);
expect(chal.memberCount).to.equal(challenge.memberCount);
expect(chal.name).to.equal(challenge.name);
expect(chal._id).to.equal(challenge._id);
expect(chal.leader).to.eql({
_id: groupLeader._id,
id: groupLeader._id,
profile: { name: groupLeader.profile.name },
auth: {
local: {
username: groupLeader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(chal.group).to.eql({
_id: group._id,
categories: [],
id: group.id,
name: group.name,
summary: group.name,
type: group.type,
privacy: group.privacy,
leader: groupLeader.id,
});
});
});
context('private guild', () => {
context('Group Plan', () => {
let groupLeader;
let challengeLeader;
let group;
@@ -84,14 +31,14 @@ describe('GET /challenges/:challengeId', () => {
const populatedGroup = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
members: 2,
upgradeToGroupPlan: true,
});
groupLeader = populatedGroup.groupLeader;
group = populatedGroup.group;
members = populatedGroup.members;
challengeLeader = members[0]; // eslint-disable-line prefer-destructuring
otherMember = members[1]; // eslint-disable-line prefer-destructuring
[challengeLeader, otherMember] = members;
challenge = await generateChallenge(challengeLeader, group);
});

View File

@@ -71,42 +71,18 @@ describe('GET /challenges/:challengeId/members', () => {
});
});
it('works with challenges belonging to public guild', async () => {
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const challenge = await generateChallenge(leader, group);
await leader.post(`/challenges/${challenge._id}/join`);
const res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: leader._id,
id: leader._id,
profile: { name: leader.profile.name },
auth: {
local: {
username: leader.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
expect(res[0]).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
expect(res[0].profile).to.have.all.keys(['name']);
});
it('populates only some fields', async () => {
const anotherUser = await generateUser({ balance: 3 });
const group = await generateGroup(anotherUser, { type: 'guild', privacy: 'public', name: generateUUID() });
const challenge = await generateChallenge(anotherUser, group);
await anotherUser.post(`/challenges/${challenge._id}/join`);
const group = await generateGroup(user, { type: 'party', privacy: 'private', name: generateUUID() });
const challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
const res = await user.get(`/challenges/${challenge._id}/members`);
expect(res[0]).to.eql({
_id: anotherUser._id,
id: anotherUser._id,
profile: { name: anotherUser.profile.name },
_id: user._id,
id: user._id,
profile: { name: user.profile.name },
auth: {
local: {
username: anotherUser.auth.local.username,
username: user.auth.local.username,
},
},
flags: {

View File

@@ -72,20 +72,6 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
});
});
it('works with challenges belonging to a public guild', async () => {
const groupLeader = await generateUser({ balance: 4 });
const group = await generateGroup(groupLeader, { type: 'guild', privacy: 'public', name: generateUUID() });
const challenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${challenge._id}/join`);
const taskText = 'Test Text';
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{ type: 'habit', text: taskText }]);
const memberProgress = await user.get(`/challenges/${challenge._id}/members/${groupLeader._id}`);
expect(memberProgress).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile', 'tasks']);
expect(memberProgress.profile).to.have.all.keys(['name']);
expect(memberProgress.tasks.length).to.equal(1);
});
it('returns the member tasks for the challenges', async () => {
const group = await generateGroup(user, { type: 'party', name: generateUUID() });
const challenge = await generateChallenge(user, group);

View File

@@ -7,117 +7,7 @@ import {
import { TAVERN_ID } from '../../../../../website/common/script/constants';
describe('GET challenges/groups/:groupId', () => {
context('Public Guild', () => {
let publicGuild; let user; let nonMember; let challenge; let
challenge2;
before(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
},
});
publicGuild = group;
user = groupLeader;
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
await user.post(`/challenges/${challenge._id}/join`);
challenge2 = await generateChallenge(user, group);
await user.post(`/challenges/${challenge2._id}/join`);
});
it('should return group challenges for non member with populated leader', async () => {
const challenges = await nonMember.get(`/challenges/groups/${publicGuild._id}`);
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
it('should return group challenges for member with populated leader', async () => {
const challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.exist;
expect(foundChallenge1.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql({
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
username: user.auth.local.username,
},
},
flags: {
verifiedUsername: true,
},
});
});
it('should return newest challenges first', async () => {
let challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
let foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id });
expect(foundChallengeIndex).to.eql(0);
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
expect(foundChallengeIndex).to.eql(0);
});
});
context('Private Guild', () => {
context('Group Plan', () => {
let privateGuild; let user; let nonMember; let challenge; let
challenge2;
@@ -128,6 +18,7 @@ describe('GET challenges/groups/:groupId', () => {
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
privateGuild = group;
@@ -186,68 +77,6 @@ describe('GET challenges/groups/:groupId', () => {
});
});
context('official challenge is present', () => {
let publicGuild; let user; let officialChallenge; let unofficialChallenges;
before(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
},
});
user = groupLeader;
publicGuild = group;
await user.update({
'permissions.challengeAdmin': true,
});
officialChallenge = await generateChallenge(user, group, {
categories: [{
name: 'habitica_official',
slug: 'habitica_official',
}],
});
await user.post(`/challenges/${officialChallenge._id}/join`);
// We add 10 extra challenges to test whether the official challenge
// (the oldest) makes it to the front page.
unofficialChallenges = [];
for (let i = 0; i < 10; i += 1) {
const challenge = await generateChallenge(user, group); // eslint-disable-line
await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line
unofficialChallenges.push(challenge);
}
});
it('should return official challenges first', async () => {
const challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
const foundChallengeIndex = _.findIndex(challenges, { _id: officialChallenge._id });
expect(foundChallengeIndex).to.eql(0);
});
it('should return newest challenges first, after official ones', async () => {
let challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
unofficialChallenges.forEach((chal, index) => {
const foundChallengeIndex = _.findIndex(challenges, { _id: chal._id });
expect(foundChallengeIndex).to.eql(10 - index);
});
const newChallenge = await generateChallenge(user, publicGuild);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get(`/challenges/groups/${publicGuild._id}`);
const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
expect(foundChallengeIndex).to.eql(1);
});
});
context('Party', () => {
let party; let user; let nonMember; let challenge; let
challenge2;
@@ -401,7 +230,7 @@ describe('GET challenges/groups/:groupId', () => {
});
});
it('should return tavern challenges using ID "habitrpg', async () => {
it('should return tavern challenges using ID "habitrpg"', async () => {
const challenges = await user.get('/challenges/groups/habitrpg');
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
@@ -435,5 +264,58 @@ describe('GET challenges/groups/:groupId', () => {
},
});
});
context('official challenge is present', () => {
let officialChallenge; let unofficialChallenges;
before(async () => {
await user.update({
'permissions.challengeAdmin': true,
balance: 3,
});
officialChallenge = await generateChallenge(user, tavern, {
categories: [{
name: 'habitica_official',
slug: 'habitica_official',
}],
prize: 1,
});
await user.post(`/challenges/${officialChallenge._id}/join`);
// We add 10 extra challenges to test whether the official challenge
// (the oldest) makes it to the front page.
unofficialChallenges = [];
for (let i = 0; i < 10; i += 1) {
const challenge = await generateChallenge(user, tavern, { prize: 1 }); // eslint-disable-line
await user.post(`/challenges/${challenge._id}/join`); // eslint-disable-line
unofficialChallenges.push(challenge);
}
});
it('should return official challenges first', async () => {
const challenges = await user.get('/challenges/groups/habitrpg');
const foundChallengeIndex = _.findIndex(challenges, { _id: officialChallenge._id });
expect(foundChallengeIndex).to.eql(0);
});
it('should return newest challenges first, after official ones', async () => {
let challenges = await user.get('/challenges/groups/habitrpg');
unofficialChallenges.forEach((chal, index) => {
const foundChallengeIndex = _.findIndex(challenges, { _id: chal._id });
expect(foundChallengeIndex).to.eql(10 - index);
});
const newChallenge = await generateChallenge(user, tavern, { prize: 1 });
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/groups/habitrpg');
const foundChallengeIndex = _.findIndex(challenges, { _id: newChallenge._id });
expect(foundChallengeIndex).to.eql(1);
});
});
});
});

View File

@@ -2,39 +2,44 @@ import {
generateUser,
generateChallenge,
createAndPopulateGroup,
resetHabiticaDB,
} from '../../../../helpers/api-integration/v3';
import { TAVERN_ID } from '../../../../../website/common/script/constants';
describe('GET challenges/user', () => {
context('no official challenges', () => {
let user; let member; let nonMember; let challenge; let challenge2;
let publicGuild; let userData; let groupData;
let user; let member; let nonMember; let challenge; let challenge2; let publicChallenge;
let groupPlan; let userData; let groupData; let tavern; let tavernData;
before(async () => {
await resetHabiticaDB();
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
});
publicGuild = group;
groupPlan = group;
groupData = {
_id: publicGuild._id,
_id: groupPlan._id,
categories: [],
id: publicGuild._id,
type: publicGuild.type,
privacy: publicGuild.privacy,
name: publicGuild.name,
summary: publicGuild.name,
leader: publicGuild.leader._id,
id: groupPlan._id,
type: groupPlan.type,
privacy: groupPlan.privacy,
name: groupPlan.name,
summary: groupPlan.name,
leader: groupPlan.leader._id,
};
user = groupLeader;
userData = {
_id: publicGuild.leader._id,
id: publicGuild.leader._id,
_id: groupPlan.leader._id,
id: groupPlan.leader._id,
profile: { name: user.profile.name },
auth: {
local: {
@@ -46,17 +51,31 @@ describe('GET challenges/user', () => {
},
};
tavern = await user.get(`/groups/${TAVERN_ID}`);
tavernData = {
_id: TAVERN_ID,
categories: [],
id: TAVERN_ID,
type: tavern.type,
privacy: tavern.privacy,
name: tavern.name,
summary: tavern.name,
leader: tavern.leader._id,
};
member = members[0]; // eslint-disable-line prefer-destructuring
nonMember = await generateUser();
challenge = await generateChallenge(user, group);
challenge2 = await generateChallenge(user, group);
await user.update({ balance: 0.25 });
publicChallenge = await generateChallenge(user, tavern, { prize: 1 });
await nonMember.post(`/challenges/${challenge._id}/join`);
await member.post(`/challenges/${challenge._id}/join`);
});
context('all challenges', () => {
it('should return challenges user has joined', async () => {
const challenges = await nonMember.get('/challenges/user?page=0');
const challenges = await member.get('/challenges/user?page=0');
const foundChallenge = _.find(challenges, { _id: challenge._id });
expect(foundChallenge).to.exist;
@@ -64,11 +83,13 @@ describe('GET challenges/user', () => {
expect(foundChallenge.group).to.eql(groupData);
});
it('should not return challenges a non-member has not joined', async () => {
it('should return public challenges', async () => {
const challenges = await nonMember.get('/challenges/user?page=0');
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist;
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
});
it('should return challenges user has created', async () => {
@@ -100,10 +121,10 @@ describe('GET challenges/user', () => {
it('should return newest challenges first', async () => {
let challenges = await user.get('/challenges/user?page=0');
let foundChallengeIndex = _.findIndex(challenges, { _id: challenge2._id });
let foundChallengeIndex = _.findIndex(challenges, { _id: publicChallenge._id });
expect(foundChallengeIndex).to.eql(0);
const newChallenge = await generateChallenge(user, publicGuild);
const newChallenge = await generateChallenge(user, groupPlan);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/user?page=0');
@@ -113,52 +134,23 @@ describe('GET challenges/user', () => {
});
it('should not return challenges user doesn\'t have access to', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
summary: 'summary for TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
});
const privateChallenge = await generateChallenge(groupLeader, group);
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
const challenges = await nonMember.get('/challenges/user?page=0');
const foundChallenge = _.find(challenges, { _id: privateChallenge._id });
const foundChallenge = _.find(challenges, { _id: challenge._id });
expect(foundChallenge).to.not.exist;
});
it('should not return challenges user doesn\'t have access to, even with query parameters', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestPrivateGuild',
summary: 'summary for TestPrivateGuild',
type: 'guild',
privacy: 'private',
},
});
const privateChallenge = await generateChallenge(groupLeader, group, {
categories: [{
name: 'academics',
slug: 'academics',
}],
});
await groupLeader.post(`/challenges/${privateChallenge._id}/join`);
const challenges = await nonMember.get('/challenges/user?page=0&categories=academics&owned=not_owned');
const foundChallenge = _.find(challenges, { _id: privateChallenge._id });
const foundChallenge = _.find(challenges, { _id: challenge._id });
expect(foundChallenge).to.not.exist;
});
});
context('my challenges', () => {
it('should return challenges user has joined', async () => {
const challenges = await nonMember.get(`/challenges/user?page=0&member=${true}`);
const challenges = await member.get(`/challenges/user?page=0&member=${true}`);
const foundChallenge = _.find(challenges, { _id: challenge._id });
expect(foundChallenge).to.exist;
@@ -177,6 +169,10 @@ describe('GET challenges/user', () => {
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql(userData);
expect(foundChallenge2.group).to.eql(groupData);
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
});
it('should return challenges user has created if filter by owned', async () => {
@@ -190,6 +186,10 @@ describe('GET challenges/user', () => {
expect(foundChallenge2).to.exist;
expect(foundChallenge2.leader).to.eql(userData);
expect(foundChallenge2.group).to.eql(groupData);
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.exist;
expect(foundPublicChallenge.leader).to.eql(userData);
expect(foundPublicChallenge.group).to.eql(tavernData);
});
it('should not return challenges user has created if filter by not owned', async () => {
@@ -199,36 +199,40 @@ describe('GET challenges/user', () => {
expect(foundChallenge1).to.not.exist;
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist;
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.not.exist;
});
it('should not return challenges in user groups', async () => {
const challenges = await member.get(`/challenges/user?page=0&member=${true}`);
const foundChallenge1 = _.find(challenges, { _id: challenge._id });
expect(foundChallenge1).to.not.exist;
const foundChallenge2 = _.find(challenges, { _id: challenge2._id });
expect(foundChallenge2).to.not.exist;
});
it('should not return public challenges', async () => {
const challenges = await member.get(`/challenges/user?page=0&member=${true}`);
const foundPublicChallenge = _.find(challenges, { _id: publicChallenge._id });
expect(foundPublicChallenge).to.not.exist;
});
});
});
context('official challenge is present', () => {
let user; let officialChallenge; let unofficialChallenges; let
publicGuild;
group;
before(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
({ group, groupLeader: user } = await createAndPopulateGroup({
groupDetails: {
name: 'TestGuild',
summary: 'summary for TestGuild',
type: 'guild',
privacy: 'public',
privacy: 'private',
},
});
user = groupLeader;
publicGuild = group;
upgradeToGroupPlan: true,
}));
await user.update({
'permissions.challengeAdmin': true,
@@ -271,7 +275,7 @@ describe('GET challenges/user', () => {
}
});
const newChallenge = await generateChallenge(user, publicGuild);
const newChallenge = await generateChallenge(user, group);
await user.post(`/challenges/${newChallenge._id}/join`);
challenges = await user.get('/challenges/user?page=0');
@@ -294,9 +298,10 @@ describe('GET challenges/user', () => {
groupDetails: {
name: 'TestGuild',
type: 'guild',
privacy: 'public',
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
});
user = groupLeader;

View File

@@ -0,0 +1,62 @@
import { v4 as generateUUID } from 'uuid';
import {
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /challenges/:challengeId/flag', () => {
let user;
let challenge;
beforeEach(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'TestParty',
type: 'party',
privacy: 'private',
},
members: 1,
});
user = groupLeader;
challenge = await generateChallenge(user, group);
});
it('returns an error when challenge is not found', async () => {
await expect(user.post(`/challenges/${generateUUID()}/flag`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('flags a challenge', async () => {
const flagResult = await user.post(`/challenges/${challenge._id}/flag`);
expect(flagResult.challenge.flags[user._id]).to.equal(true);
expect(flagResult.challenge.flagCount).to.equal(1);
});
it('flags a challenge with a higher count when from an admin', async () => {
await user.update({ 'contributor.admin': true });
const flagResult = await user.post(`/challenges/${challenge._id}/flag`);
expect(flagResult.challenge.flags[user._id]).to.equal(true);
expect(flagResult.challenge.flagCount).to.equal(5);
});
it('returns an error when user tries to flag a challenge that is already flagged', async () => {
await user.post(`/challenges/${challenge._id}/flag`);
await expect(user.post(`/challenges/${challenge._id}/flag`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('messageChallengeFlagAlreadyReported'),
});
});
});

View File

@@ -0,0 +1,58 @@
import { v4 as generateUUID } from 'uuid';
import {
generateChallenge,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /challenges/:challengeId/clearflags', () => {
let admin;
let nonAdmin;
let challenge;
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'TestParty',
type: 'party',
privacy: 'private',
},
members: 1,
});
admin = groupLeader;
[nonAdmin] = members;
await admin.update({ 'permissions.moderator': true });
challenge = await generateChallenge(admin, group);
await admin.post(`/challenges/${challenge._id}/flag`);
});
it('returns error when non-admin attempts to clear flags', async () => {
await expect(nonAdmin.post(`/challenges/${generateUUID()}/clearflags`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupChatAdminClearFlagCount'),
});
});
it('returns an error when challenge is not found', async () => {
await expect(admin.post(`/challenges/${generateUUID()}/clearflags`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('challengeNotFound'),
});
});
it('clears flags and sets mod flag to false', async () => {
await nonAdmin.post(`/challenges/${challenge._id}/flag`);
const flagResult = await admin.post(`/challenges/${challenge._id}/clearflags`);
expect(flagResult.challenge.flagCount).to.eql(0);
expect(flagResult.challenge.flags).to.have.property(admin._id, false);
expect(flagResult.challenge.flags).to.have.property(nonAdmin._id, true);
});
});

View File

@@ -42,26 +42,7 @@ describe('POST /challenges', () => {
});
});
it('returns error when creating a challenge in a public guild and you are not a member of it', async () => {
const user = await generateUser();
const { group } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
});
await expect(user.post('/challenges', {
group: group._id,
prize: 4,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('mustBeGroupMember'),
});
});
it('return error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
it('returns error when creating a challenge with summary with greater than MAX_SUMMARY_SIZE_FOR_CHALLENGES characters', async () => {
const user = await generateUser();
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_CHALLENGES + 1);
const group = createAndPopulateGroup({
@@ -77,7 +58,7 @@ describe('POST /challenges', () => {
});
});
context('Creating a challenge for a valid group', () => {
context('creating a Challenge for a Group Plan', () => {
let groupLeader;
let group;
let groupMember;
@@ -94,9 +75,11 @@ describe('POST /challenges', () => {
challenges: true,
},
},
upgradeToGroupPlan: true,
});
groupLeader = await populatedGroup.groupLeader.sync();
await groupLeader.update({ permissions: {} });
group = populatedGroup.group;
groupMember = populatedGroup.members[0]; // eslint-disable-line prefer-destructuring
});

View File

@@ -18,6 +18,7 @@ describe('PUT /challenges/:challengeId', () => {
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
});
privateGuild = group;

View File

@@ -1,7 +1,6 @@
import { v4 as generateUUID } from 'uuid';
import {
createAndPopulateGroup,
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -10,27 +9,30 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
admin;
before(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
privacy: 'private',
},
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
members: 2,
upgradeToGroupPlan: true,
});
groupWithChat = group;
user = groupLeader;
message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
message = message.message;
userThatDidNotCreateChat = await generateUser();
admin = await generateUser({ 'permissions.moderator': true });
userThatDidNotCreateChat = members[0]; // eslint-disable-line prefer-destructuring
admin = members[1]; // eslint-disable-line prefer-destructuring
await admin.update({ permissions: { moderator: true } });
});
context('Chat errors', () => {
it('returns an error is message does not exist', async () => {
it('returns an error if message does not exist', async () => {
const fakeChatId = generateUUID();
await expect(user.del(`/groups/${groupWithChat._id}/chat/${fakeChatId}`)).to.eventually.be.rejected.and.eql({
code: 404,
@@ -56,7 +58,7 @@ describe('DELETE /groups/:groupId/chat/:chatId', () => {
nextMessage = nextMessage.message;
});
it('allows creator to delete a their message', async () => {
it('allows creator to delete their message', async () => {
await user.del(`/groups/${groupWithChat._id}/chat/${nextMessage.id}`);
const returnedMessages = await user.get(`/groups/${groupWithChat._id}/chat/`);

View File

@@ -1,6 +1,6 @@
import {
generateUser,
generateGroup,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -11,48 +11,22 @@ describe('GET /groups/:groupId/chat', () => {
user = await generateUser();
});
context('public Guild', () => {
let group;
before(async () => {
const leader = await generateUser({ balance: 2 });
group = await generateGroup(leader, {
name: 'test group',
type: 'guild',
privacy: 'public',
}, {
chat: [
{ text: 'Hello', flags: {}, id: 1 },
{ text: 'Welcome to the Guild', flags: {}, id: 2 },
],
});
});
it('returns Guild chat', async () => {
const chat = await user.get(`/groups/${group._id}/chat`);
expect(chat[0].id).to.eql(group.chat[0].id);
expect(chat[1].id).to.eql(group.chat[1].id);
});
});
context('private Guild', () => {
let group;
before(async () => {
const leader = await generateUser({ balance: 2 });
group = await generateGroup(leader, {
name: 'test group',
type: 'guild',
privacy: 'private',
}, {
({ group } = await createAndPopulateGroup({
groupDetails: {
name: 'test group',
type: 'guild',
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
chat: [
'Hello',
'Welcome to the Guild',
],
});
}));
});
it('returns error if user is not member of requested private group', async () => {

View File

@@ -1,32 +1,42 @@
import { find } from 'lodash';
import find from 'lodash/find';
import moment from 'moment';
import nconf from 'nconf';
import { IncomingWebhook } from '@slack/webhook';
import {
generateUser,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat/:chatId/flag', () => {
let user; let admin; let anotherUser; let newUser; let
group;
group; let members; let userToDelete;
const TEST_MESSAGE = 'Test Message';
const USER_AGE_FOR_FLAGGING = 3;
beforeEach(async () => {
user = await generateUser({ balance: 1, 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
admin = await generateUser({ balance: 1, 'permissions.moderator': true });
anotherUser = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
newUser = await generateUser({ 'auth.timestamps.created': moment().subtract(1, 'days').toDate() });
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
({ group, groupLeader: user, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'private',
},
leaderDetails: {
'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate(),
},
members: 4,
upgradeToGroupPlan: true,
}));
group = await user.post('/groups', {
name: 'Test Guild',
type: 'guild',
privacy: 'public',
[admin, anotherUser, newUser, userToDelete] = members;
await user.update({ permissions: {} });
await admin.update({ permissions: { moderator: true } });
await anotherUser.update({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
await newUser.update({ 'auth.timestamps.created': moment().subtract(1, 'days').toDate() });
await userToDelete.update({
'auth.timestamps.created': moment().subtract(1, 'days').toDate(),
'purchased.plan.dateTerminated': moment().subtract(1, 'minutes').toDate(),
});
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
});
afterEach(() => {
@@ -69,8 +79,8 @@ describe('POST /chat/:chatId/flag', () => {
fallback: 'Flag Message',
color: 'danger',
author_name: `@${anotherUser.auth.local.username} ${anotherUser.profile.name} (${anotherUser.auth.local.email}; ${anotherUser._id})\n${timestamp}`,
title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`,
title: 'Flag in Test Guild - (private guild)',
title_link: undefined,
text: TEST_MESSAGE,
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.>`,
mrkdwn_in: [
@@ -78,7 +88,7 @@ describe('POST /chat/:chatId/flag', () => {
],
}],
});
/* eslint-ensable camelcase */
/* eslint-enable camelcase */
});
it('Does not increment message flag count and sends different message to moderator Slack when user is new', async () => {
@@ -104,8 +114,8 @@ describe('POST /chat/:chatId/flag', () => {
fallback: 'Flag Message',
color: 'danger',
author_name: `@${newUser.auth.local.username} ${newUser.profile.name} (${newUser.auth.local.email}; ${newUser._id})\n${timestamp}`,
title: 'Flag in Test Guild',
title_link: `${BASE_URL}/groups/guild/${group._id}`,
title: 'Flag in Test Guild - (private guild)',
title_link: undefined,
text: TEST_MESSAGE,
footer: `<https://habitrpg.github.io/flag-o-rama/?groupId=${group._id}&chatId=${message.id}|Flag this message.> ${automatedComment}`,
mrkdwn_in: [
@@ -113,15 +123,12 @@ describe('POST /chat/:chatId/flag', () => {
],
}],
});
/* eslint-ensable camelcase */
/* eslint-enable camelcase */
});
it('Flags a chat when the author\'s account was deleted', async () => {
const deletedUser = await generateUser({
'auth.timestamps.created': new Date('2022-01-01'),
});
const { message } = await deletedUser.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
await deletedUser.del('/user', {
const { message } = await userToDelete.post(`/groups/${group._id}/chat`, { message: TEST_MESSAGE });
await userToDelete.del('/user', {
password: 'password',
});

View File

@@ -6,27 +6,27 @@ import {
describe('POST /chat/:chatId/like', () => {
let user;
let groupWithChat;
const testMessage = 'Test Message';
let anotherUser;
let groupWithChat;
let members;
const testMessage = 'Test Message';
before(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
({ group: groupWithChat, groupLeader: user, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'public',
privacy: 'private',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
upgradeToGroupPlan: true,
}));
user = groupLeader;
groupWithChat = group;
anotherUser = members[0]; // eslint-disable-line prefer-destructuring
[anotherUser] = members;
await anotherUser.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});

View File

@@ -1,41 +1,33 @@
import { IncomingWebhook } from '@slack/webhook';
import nconf from 'nconf';
import { v4 as generateUUID } from 'uuid';
import {
createAndPopulateGroup,
generateUser,
translate as t,
sleep,
server,
} from '../../../../helpers/api-integration/v3';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
TAVERN_ID,
} from '../../../../../website/server/models/group';
import { CHAT_FLAG_FROM_SHADOW_MUTE, MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants';
import { MAX_MESSAGE_LENGTH } from '../../../../../website/common/script/constants';
import * as email from '../../../../../website/server/libs/email';
const BASE_URL = nconf.get('BASE_URL');
describe('POST /chat', () => {
let user; let groupWithChat; let member; let
additionalMember;
const testMessage = 'Test Message';
const testBannedWordMessage = 'TESTPLACEHOLDERSWEARWORDHERE';
const testBannedWordMessage1 = 'TESTPLACEHOLDERSWEARWORDHERE1';
const testSlurMessage = 'message with TESTPLACEHOLDERSLURWORDHERE';
const testSlurMessage1 = 'TESTPLACEHOLDERSLURWORDHERE1';
const bannedWordErrorMessage = t('bannedWordUsed', { swearWordsUsed: testBannedWordMessage });
before(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'public',
privacy: 'private',
},
members: 2,
upgradeToGroupPlan: true,
});
user = groupLeader;
await user.update({
@@ -43,8 +35,7 @@ describe('POST /chat', () => {
'auth.timestamps.created': new Date('2022-01-01'),
}); // prevent tests accidentally throwing messageGroupChatSpam
groupWithChat = group;
member = members[0]; // eslint-disable-line prefer-destructuring
additionalMember = members[1]; // eslint-disable-line prefer-destructuring
[member, additionalMember] = members;
await member.update({ 'auth.timestamps.created': new Date('2022-01-01') });
await additionalMember.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
@@ -89,32 +80,12 @@ describe('POST /chat', () => {
member.update({ 'flags.chatRevoked': false });
});
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
const userWithChatRevoked = await member.update({ 'flags.chatRevoked': true });
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('does not error when chat privileges are revoked when sending a message to a private guild', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Private Guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
const privateGuildMemberWithChatsRevoked = members[0];
await privateGuildMemberWithChatsRevoked.update({
await member.update({
'flags.chatRevoked': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await privateGuildMemberWithChatsRevoked.post(`/groups/${group._id}/chat`, { message: testMessage });
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
});
@@ -152,54 +123,12 @@ describe('POST /chat', () => {
member.update({ 'flags.chatShadowMuted': false });
});
it('creates a chat with flagCount already set and notifies mods when sending a message to a public guild', async () => {
const userWithChatShadowMuted = await member.update({ 'flags.chatShadowMuted': true });
const message = await userWithChatShadowMuted.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(CHAT_FLAG_FROM_SHADOW_MUTE);
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][1]).to.eql('shadow-muted-post-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `@${member.auth.local.username} / ${member.profile.name} posted while shadow-muted`,
attachments: [{
fallback: 'Shadow-Muted Message',
color: 'danger',
author_name: `@${member.auth.local.username} ${member.profile.name} (${member.auth.local.email}; ${member._id})`,
title: 'Shadow-Muted Post in Test Guild',
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
text: testMessage,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
});
it('creates a chat with zero flagCount when sending a message to a private guild', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Private Guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
const userWithChatShadowMuted = members[0];
await userWithChatShadowMuted.update({
await member.update({
'flags.chatShadowMuted': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await userWithChatShadowMuted.post(`/groups/${group._id}/chat`, { message: testMessage });
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
@@ -226,100 +155,9 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
});
it('creates a chat with zero flagCount when non-shadow-muted user sends a message to a public guild', async () => {
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.flagCount).to.eql(0);
});
});
context('banned word', () => {
it('returns an error when chat message contains a banned word in tavern', async () => {
await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('returns an error when chat message contains a banned word in a public guild', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
await expect(members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is part of a phrase', async () => {
const wordInPhrase = `phrase ${testBannedWordMessage} end`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is surrounded by non alphabet characters', async () => {
const wordInPhrase = `_!${testBannedWordMessage}@_`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: bannedWordErrorMessage,
});
});
it('errors when word is typed in mixed case', async () => {
const substrLength = Math.floor(testBannedWordMessage.length / 2);
const chatMessage = testBannedWordMessage.substring(0, substrLength).toLowerCase()
+ testBannedWordMessage.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed', { swearWordsUsed: chatMessage }),
});
});
it('checks error message has all the banned words used, regardless of case', async () => {
const testBannedWords = [
testBannedWordMessage.toUpperCase(),
testBannedWordMessage1.toLowerCase(),
];
const chatMessage = `Mixing ${testBannedWords[0]} and ${testBannedWords[1]} is bad for you.`;
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected
.and.have.property('message')
.that.includes(testBannedWords.join(', '));
});
it('does not error when bad word is suffix of a word', async () => {
const wordAsSuffix = `prefix${testBannedWordMessage}`;
const message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix });
expect(message.message.id).to.exist;
});
it('does not error when bad word is prefix of a word', async () => {
const wordAsPrefix = `${testBannedWordMessage}suffix`;
const message = await user.post('/groups/habitrpg/chat', { message: wordAsPrefix });
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a party', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
@@ -336,37 +174,8 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a public guild in which banned words are allowed', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
// Update the bannedWordsAllowed property for the group
group.update({ bannedWordsAllowed: true });
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a private guild', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'private guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
const message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage });
const message = await member.post(`/groups/${groupWithChat._id}/chat`, { message: testBannedWordMessage });
expect(message.message.id).to.exist;
});
@@ -383,45 +192,6 @@ describe('POST /chat', () => {
user.update({ 'flags.chatRevoked': false });
});
it('errors and revokes privileges when chat message contains a banned slur', async () => {
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: testSlurMessage })).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
// Email sent to mods
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn.args[0][1]).to.eql('slur-report-to-mods');
// Slack message to mods
expect(IncomingWebhook.prototype.send).to.be.calledOnce;
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `${user.profile.name} (${user.id}) tried to post a slur`,
attachments: [{
fallback: 'Slur Message',
color: 'danger',
author_name: `@${user.auth.local.username} ${user.profile.name} (${user.auth.local.email}; ${user._id})`,
title: 'Slur in Test Guild',
title_link: `${BASE_URL}/groups/guild/${groupWithChat.id}`,
text: testSlurMessage,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
// Chat privileges are revoked
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('allows slurs in private groups', async () => {
const { group, members } = await createAndPopulateGroup({
groupDetails: {
@@ -437,28 +207,17 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
});
it('errors when slur is typed in mixed case', async () => {
const substrLength = Math.floor(testSlurMessage1.length / 2);
const chatMessage = testSlurMessage1.substring(0, substrLength).toLowerCase()
+ testSlurMessage1.substring(substrLength).toUpperCase();
await expect(user.post('/groups/habitrpg/chat', { message: chatMessage }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedSlurUsed'),
});
});
});
it('errors when user account is too young', async () => {
const brandNewUser = await generateUser();
await expect(brandNewUser.post('/groups/habitrpg/chat', { message: 'hi im new' }))
await user.update({ 'auth.timestamps.created': new Date() });
await expect(user.post(`/groups/${groupWithChat._id}/chat`, { message: 'hi im new' }))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('chatTemporarilyUnavailable'),
});
await user.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('creates a chat', async () => {
@@ -519,54 +278,42 @@ describe('POST /chat', () => {
const mount = 'test-mount';
const pet = 'test-pet';
const style = 'test-style';
const userWithStyle = await generateUser({
await user.update({
'items.currentMount': mount,
'items.currentPet': pet,
'preferences.style': style,
'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithStyle.sync();
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.userStyles.items.currentMount).to.eql(userWithStyle.items.currentMount);
expect(message.message.userStyles.items.currentPet).to.eql(userWithStyle.items.currentPet);
expect(message.message.userStyles.preferences.style).to.eql(userWithStyle.preferences.style);
expect(message.message.userStyles.preferences.hair).to.eql(userWithStyle.preferences.hair);
expect(message.message.userStyles.preferences.skin).to.eql(userWithStyle.preferences.skin);
expect(message.message.userStyles.preferences.shirt).to.eql(userWithStyle.preferences.shirt);
expect(message.message.userStyles.preferences.chair).to.eql(userWithStyle.preferences.chair);
expect(message.message.userStyles.items.currentMount).to.eql(user.items.currentMount);
expect(message.message.userStyles.items.currentPet).to.eql(user.items.currentPet);
expect(message.message.userStyles.preferences.style).to.eql(user.preferences.style);
expect(message.message.userStyles.preferences.hair).to.eql(user.preferences.hair);
expect(message.message.userStyles.preferences.skin).to.eql(user.preferences.skin);
expect(message.message.userStyles.preferences.shirt).to.eql(user.preferences.shirt);
expect(message.message.userStyles.preferences.chair).to.eql(user.preferences.chair);
expect(message.message.userStyles.preferences.background)
.to.eql(userWithStyle.preferences.background);
.to.eql(user.preferences.background);
});
it('creates equipped to user styles', async () => {
const userWithStyle = await generateUser({
'preferences.costume': false,
'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithStyle.sync();
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.userStyles.items.gear.equipped)
.to.eql(userWithStyle.items.gear.equipped);
.to.eql(user.items.gear.equipped);
expect(message.message.userStyles.items.gear.costume).to.not.exist;
});
it('creates costume to user styles', async () => {
const userWithStyle = await generateUser({
'preferences.costume': true,
'auth.timestamps.created': new Date('2022-01-01'),
});
await userWithStyle.sync();
await user.update({ 'preferences.costume': true });
const message = await userWithStyle.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
expect(message.message.id).to.exist;
expect(message.message.userStyles.items.gear.costume).to.eql(userWithStyle.items.gear.costume);
expect(message.message.userStyles.items.gear.costume).to.eql(user.items.gear.costume);
expect(message.message.userStyles.items.gear.equipped).to.not.exist;
});
@@ -576,12 +323,11 @@ describe('POST /chat', () => {
tier: 800,
tokensApplied: true,
};
const backer = await generateUser({
await user.update({
backer: backerInfo,
'auth.timestamps.created': new Date('2022-01-01'),
});
const message = await backer.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const messageBackerInfo = message.message.backer;
expect(messageBackerInfo.npc).to.equal(backerInfo.npc);
@@ -661,43 +407,5 @@ describe('POST /chat', () => {
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
expect(memberWithNotification.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === group._id)).to.exist;
});
it('does not notify other users of a new message that is already hidden from shadow-muting', async () => {
await user.update({ 'flags.chatShadowMuted': true });
const message = await user.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage });
const memberWithNotification = await member.get('/user');
await user.update({ 'flags.chatShadowMuted': false });
expect(message.message.id).to.exist;
expect(memberWithNotification.newMessages[`${groupWithChat._id}`]).to.not.exist;
expect(memberWithNotification.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupWithChat._id)).to.not.exist;
});
});
context('Spam prevention', () => {
it('Returns an error when the user has been posting too many messages', async () => {
// Post as many messages are needed to reach the spam limit
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i += 1) {
const result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
await expect(additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupChatSpam'),
});
});
it('contributor should not receive spam alert', async () => {
const userSocialite = await member.update({ 'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL });
// Post 1 more message than the spam limit to ensure they do not reach the limit
for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i += 1) {
const result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
});
});
});

View File

@@ -12,18 +12,19 @@ describe('POST /groups/:id/chat/seen', () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
privacy: 'private',
},
members: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
upgradeToGroupPlan: true,
});
guild = group;
guildLeader = groupLeader;
guildMember = members[0]; // eslint-disable-line prefer-destructuring
[guildMember] = members;
guildMessage = await guildLeader.post(`/groups/${guild._id}/chat`, { message: 'Some guild message' });
guildMessage = guildMessage.message;

View File

@@ -2,7 +2,6 @@ import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
import {
createAndPopulateGroup,
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import config from '../../../../../config.json';
@@ -13,21 +12,24 @@ describe('POST /groups/:id/chat/:id/clearflags', () => {
admin;
before(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
privacy: 'private',
},
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
upgradeToGroupPlan: true,
members: 2,
});
groupWithChat = group;
author = groupLeader;
nonAdmin = await generateUser({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
admin = await generateUser({ 'permissions.moderator': true });
[nonAdmin, admin] = members;
await nonAdmin.update({ 'auth.timestamps.created': moment().subtract(USER_AGE_FOR_FLAGGING + 1, 'days').toDate() });
await admin.update({ 'permissions.moderator': true });
message = await author.post(`/groups/${groupWithChat._id}/chat`, { message: 'Some message' });
message = message.message;

View File

@@ -1,6 +1,5 @@
import {
generateUser,
generateGroup,
createAndPopulateGroup,
} from '../../../../helpers/api-integration/v3';
describe('GET /group-plans', () => {
@@ -8,20 +7,15 @@ describe('GET /group-plans', () => {
let groupPlan;
before(async () => {
user = await generateUser({ balance: 4 });
groupPlan = await generateGroup(user,
{
name: 'public guild - is member',
({ group: groupPlan, groupLeader: user } = await createAndPopulateGroup({
groupDetails: {
name: 'group plan - is member',
type: 'guild',
privacy: 'public',
privacy: 'private',
},
{
purchased: {
plan: {
customerId: 'existings',
},
},
});
upgradeToGroupPlan: true,
leaderDetails: { balance: 4 },
}));
});
it('returns group plans for the user', async () => {

View File

@@ -1,70 +1,63 @@
import {
generateUser,
createAndPopulateGroup,
resetHabiticaDB,
generateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
import {
TAVERN_ID,
} from '../../../../../website/server/models/group';
import apiError from '../../../../../website/server/libs/apiError';
describe('GET /groups', () => {
let user;
let userInGuild;
const NUMBER_OF_PUBLIC_GUILDS = 2;
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER = 2;
const NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER = 1;
const NUMBER_OF_USERS_PRIVATE_GUILDS = 1;
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 5;
const GUILD_PER_PAGE = 30;
let user; let leader; let members;
let secondGroup; let secondLeader;
const NUMBER_OF_USERS_PRIVATE_GUILDS = 2;
const NUMBER_OF_GROUPS_USER_CAN_VIEW = 3;
const categories = [{
slug: 'newCat',
name: 'New Category',
}];
let publicGuildNotMember;
let privateGuildUserIsMemberOf;
before(async () => {
await resetHabiticaDB();
const leader = await generateUser({ balance: 10 });
user = await generateUser({ balance: 4 });
({
group: privateGuildUserIsMemberOf,
groupLeader: leader,
members,
} = await createAndPopulateGroup({
groupDetails: {
name: 'private guild - is member',
type: 'guild',
privacy: 'private',
categories,
},
leaderDetails: {
balance: 10,
},
members: 1,
upgradeToGroupPlan: true,
}));
[user] = members;
await user.update({ balance: 4 });
const publicGuildUserIsMemberOf = await generateGroup(leader, {
name: 'public guild - is member',
type: 'guild',
privacy: 'public',
summary: 'ohayou kombonwa',
description: 'oyasumi',
});
await leader.post(`/groups/${publicGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] });
await user.post(`/groups/${publicGuildUserIsMemberOf._id}/join`);
({ group: secondGroup, groupLeader: secondLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'c++ coders',
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
}));
userInGuild = await generateUser({ guilds: [publicGuildUserIsMemberOf._id] });
await secondLeader.post(`/groups/${secondGroup._id}/invite`, { uuids: [user._id] });
await user.post(`/groups/${secondGroup._id}/join`);
publicGuildNotMember = await generateGroup(leader, {
name: 'public guild - is not member',
type: 'guild',
privacy: 'public',
summary: 'Natsume Soseki',
description: 'Kinnosuke no Hondana',
categories,
});
privateGuildUserIsMemberOf = await generateGroup(leader, {
name: 'private guild - is member',
type: 'guild',
privacy: 'private',
categories,
});
await leader.post(`/groups/${privateGuildUserIsMemberOf._id}/invite`, { uuids: [user._id] });
await user.post(`/groups/${privateGuildUserIsMemberOf._id}/join`);
await generateGroup(leader, {
name: 'private guild - is not member',
type: 'guild',
privacy: 'private',
await createAndPopulateGroup({
groupDetails: {
name: 'private guild - is not member',
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
await generateGroup(leader, {
@@ -98,172 +91,16 @@ describe('GET /groups', () => {
});
});
it('returns only the tavern when tavern passed in as query', async () => {
await expect(user.get('/groups?type=tavern'))
.to.eventually.have.a.lengthOf(1)
.and.to.have.nested.property('[0]')
.and.to.have.property('_id', TAVERN_ID);
});
it('returns only the user\'s party when party passed in as query', async () => {
await expect(user.get('/groups?type=party'))
.to.eventually.have.a.lengthOf(1)
.and.to.have.nested.property('[0]');
});
it('returns all public guilds when publicGuilds passed in as query', async () => {
await expect(user.get('/groups?type=publicGuilds'))
.to.eventually.have.a.lengthOf(NUMBER_OF_PUBLIC_GUILDS);
});
describe('filters', () => {
it('returns public guilds filtered by category', async () => {
const guilds = await user.get(`/groups?type=publicGuilds&categories=${categories[0].slug}`);
expect(guilds[0]._id).to.equal(publicGuildNotMember._id);
});
it('returns private guilds filtered by category', async () => {
const guilds = await user.get(`/groups?type=privateGuilds&categories=${categories[0].slug}`);
expect(guilds[0]._id).to.equal(privateGuildUserIsMemberOf._id);
});
it('filters public guilds by size', async () => {
await generateGroup(user, {
name: 'guild1',
type: 'guild',
privacy: 'public',
memberCount: 1,
});
// @TODO: anyway to set higher memberCount in tests right now?
const guilds = await user.get('/groups?type=publicGuilds&minMemberCount=3');
expect(guilds.length).to.equal(0);
});
it('filters private guilds by size', async () => {
await generateGroup(user, {
name: 'guild1',
type: 'guild',
privacy: 'private',
memberCount: 1,
});
// @TODO: anyway to set higher memberCount in tests right now?
const guilds = await user.get('/groups?type=privateGuilds&minMemberCount=3');
expect(guilds.length).to.equal(0);
});
it('filters public guilds by leader role', async () => {
const guilds = await user.get('/groups?type=publicGuilds&leader=true');
expect(guilds.length).to.equal(NUMBER_OF_PUBLIC_GUILDS_USER_IS_LEADER);
});
it('filters public guilds by member role', async () => {
const guilds = await userInGuild.get('/groups?type=publicGuilds&member=true');
expect(guilds.length).to.equal(1);
expect(guilds[0].name).to.have.string('is member');
});
it('filters public guilds by single-word search term', async () => {
const guilds = await user.get('/groups?type=publicGuilds&search=kom');
expect(guilds.length).to.equal(1);
expect(guilds[0].summary).to.have.string('ohayou kombonwa');
});
it('filters public guilds by single-word search term left and right-padded by spaces', async () => {
const guilds = await user.get('/groups?type=publicGuilds&search=++++ohayou+kombonwa+++++');
expect(guilds.length).to.equal(1);
expect(guilds[0].summary).to.have.string('ohayou kombonwa');
});
it('filters public guilds by two-words search term separated by multiple spaces', async () => {
const guilds = await user.get('/groups?type=publicGuilds&search=kinnosuke+++++hon');
expect(guilds.length).to.equal(1);
expect(guilds[0].description).to.have.string('Kinnosuke');
});
});
describe('public guilds pagination', () => {
it('req.query.paginate must be a boolean string', async () => {
await expect(user.get('/groups?paginate=aString&type=publicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('req.query.paginate can only be true when req.query.type includes publicGuilds', async () => {
await expect(user.get('/groups?paginate=true&type=notPublicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: apiError('guildsOnlyPaginate'),
});
});
it('req.query.page can\'t be negative', async () => {
await expect(user.get('/groups?paginate=true&page=-1&type=publicGuilds'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Invalid request parameters.',
});
});
it('returns 30 guilds per page ordered by number of members', async () => {
await user.update({ balance: 9000 });
const delay = () => new Promise(resolve => setTimeout(resolve, 40));
const promises = [];
for (let i = 0; i < 60; i += 1) {
promises.push(generateGroup(user, {
name: `public guild ${i} - is member`,
type: 'guild',
privacy: 'public',
}));
await delay(); // eslint-disable-line no-await-in-loop
}
const groups = await Promise.all(promises);
// update group number 32 and not the first to make sure sorting works
await groups[32].update({ name: 'guild with most members', memberCount: 199 });
await groups[33].update({ name: 'guild with less members', memberCount: -100 });
const page0 = await expect(user.get('/groups?type=publicGuilds&paginate=true'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
expect(page0[0].name).to.equal('guild with most members');
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=1'))
.to.eventually.have.a.lengthOf(GUILD_PER_PAGE);
const page2 = await expect(user.get('/groups?type=publicGuilds&paginate=true&page=2'))
// 1 created now, 4 by other tests, -1 for no more tavern.
.to.eventually.have.a.lengthOf(1 + 4 - 1);
expect(page2[3].name).to.equal('guild with less members');
}).timeout(10000);
});
it('makes sure that the tavern doesn\'t show up when guilds is passed as a query', async () => {
const guilds = await user.get('/groups?type=guilds');
expect(guilds.find(g => g.id === TAVERN_ID)).to.be.undefined;
});
it('makes sure that the tavern doesn\'t show up when publicGuilds is passed as a query', async () => {
const guilds = await user.get('/groups?type=publicGuilds');
expect(guilds.find(g => g.id === TAVERN_ID)).to.be.undefined;
});
it('returns all the user\'s guilds when guilds passed in as query', async () => {
await expect(user.get('/groups?type=guilds'))
.to.eventually.have.a
.lengthOf(NUMBER_OF_PUBLIC_GUILDS_USER_IS_MEMBER + NUMBER_OF_USERS_PRIVATE_GUILDS);
.lengthOf(NUMBER_OF_USERS_PRIVATE_GUILDS);
});
it('returns all private guilds user is a part of when privateGuilds passed in as query', async () => {
@@ -272,21 +109,21 @@ describe('GET /groups', () => {
});
it('returns a list of groups user has access to', async () => {
await expect(user.get('/groups?type=privateGuilds,publicGuilds,party,tavern'))
.to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW - 1); // -1 for no Tavern.
await expect(user.get('/groups?type=privateGuilds,party'))
.to.eventually.have.lengthOf(NUMBER_OF_GROUPS_USER_CAN_VIEW);
});
it('returns a list of groups user has access to', async () => {
const group = await generateGroup(user, {
name: 'c++ coders',
type: 'guild',
privacy: 'public',
describe('filters', () => {
it('returns private guilds filtered by category', async () => {
const guilds = await user.get(`/groups?type=privateGuilds&categories=${categories[0].slug}`);
expect(guilds[0]._id).to.equal(privateGuildUserIsMemberOf._id);
});
// search for 'c++ coders'
await expect(user.get('/groups?type=publicGuilds&paginate=true&page=0&search=c%2B%2B+coders'))
.to.eventually.have.lengthOf(1)
.and.to.have.nested.property('[0]')
.and.to.have.property('_id', group._id);
it('filters private guilds by size', async () => {
const guilds = await user.get('/groups?type=privateGuilds&minMemberCount=3');
expect(guilds.length).to.equal(0);
});
});
});

View File

@@ -3,6 +3,7 @@ import {
generateUser,
generateGroup,
translate as t,
createAndPopulateGroup,
} from '../../../../helpers/api-integration/v3';
describe('GET /groups/:groupId/invites', () => {
@@ -71,15 +72,16 @@ describe('GET /groups/:groupId/invites', () => {
});
it('returns only first 30 invites by default (req.query.limit not specified)', async () => {
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const invitesToGenerate = [];
for (let i = 0; i < 31; i += 1) {
invitesToGenerate.push(generateUser());
}
const generatedInvites = await Promise.all(invitesToGenerate);
await leader.post(`/groups/${group._id}/invite`, { uuids: generatedInvites.map(invite => invite._id) });
const { group, groupLeader: leader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 31,
upgradeToGroupPlan: true,
});
const res = await leader.get(`/groups/${group._id}/invites`);
expect(res.length).to.equal(30);
@@ -90,8 +92,16 @@ describe('GET /groups/:groupId/invites', () => {
}).timeout(10000);
it('returns an error if req.query.limit is over 60', async () => {
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const { group, groupLeader: leader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 1,
upgradeToGroupPlan: true,
});
await expect(leader.get(`/groups/${group._id}/invites?limit=61`)).to.eventually.be.rejected.and.eql({
code: 400,
@@ -101,8 +111,16 @@ describe('GET /groups/:groupId/invites', () => {
});
it('returns an error if req.query.limit is under 1', async () => {
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const { group, groupLeader: leader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 1,
upgradeToGroupPlan: true,
});
await expect(leader.get(`/groups/${group._id}/invites?limit=-1`)).to.eventually.be.rejected.and.eql({
code: 400,
@@ -112,8 +130,16 @@ describe('GET /groups/:groupId/invites', () => {
});
it('returns an error if req.query.limit is not an integer', async () => {
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const { group, groupLeader: leader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 1,
upgradeToGroupPlan: true,
});
await expect(leader.get(`/groups/${group._id}/invites?limit=1.3`)).to.eventually.be.rejected.and.eql({
code: 400,
@@ -123,15 +149,16 @@ describe('GET /groups/:groupId/invites', () => {
});
it('returns up to 60 invites when req.query.limit is specified', async () => {
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const invitesToGenerate = [];
for (let i = 0; i < 31; i += 1) {
invitesToGenerate.push(generateUser());
}
const generatedInvites = await Promise.all(invitesToGenerate);
await leader.post(`/groups/${group._id}/invite`, { uuids: generatedInvites.map(invite => invite._id) });
const { group, groupLeader: leader } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 31,
upgradeToGroupPlan: true,
});
let res = await leader.get(`/groups/${group._id}/invites?limit=14`);
expect(res.length).to.equal(14);
@@ -149,17 +176,20 @@ describe('GET /groups/:groupId/invites', () => {
}).timeout(30000);
it('supports using req.query.lastId to get more invites', async function test () {
let group; let invitees;
this.timeout(30000); // @TODO: times out after 8 seconds
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
({ group, groupLeader: user, invitees } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'private',
name: generateUUID(),
},
leaderDetails: { balance: 4 },
invites: 32,
upgradeToGroupPlan: true,
}));
const invitesToGenerate = [];
for (let i = 0; i < 32; i += 1) {
invitesToGenerate.push(generateUser());
}
const generatedInvites = await Promise.all(invitesToGenerate); // Group has 32 invites
const expectedIds = generatedInvites.map(generatedInvite => generatedInvite._id);
await user.post(`/groups/${group._id}/invite`, { uuids: expectedIds });
const expectedIds = invitees.map(generatedInvite => generatedInvite._id);
const res = await user.get(`/groups/${group._id}/invites`);
expect(res.length).to.equal(30);

View File

@@ -1,5 +1,6 @@
import { v4 as generateUUID } from 'uuid';
import {
createAndPopulateGroup,
generateUser,
generateGroup,
translate as t,
@@ -75,7 +76,15 @@ describe('GET /groups/:groupId/members', () => {
});
it('req.query.includeAllPublicFields === true works with guilds', async () => {
const group = await generateGroup(user, { type: 'guild', name: generateUUID() });
let group;
({ group, groupLeader: user } = await createAndPopulateGroup({
type: 'guild',
privacy: 'private',
name: generateUUID(),
upgradeToGroupPlan: true,
members: 1,
}));
const [memberRes] = await user.get(`/groups/${group._id}/members?includeAllPublicFields=true`);
expect(memberRes).to.have.all.keys([ // works as: object has all and only these keys
@@ -206,20 +215,20 @@ describe('GET /groups/:groupId/members', () => {
it('supports using req.query.lastId to get more members', async function test () {
this.timeout(30000); // @TODO: times out after 8 seconds
const leader = await generateUser({ balance: 4 });
const group = await generateGroup(leader, { type: 'guild', privacy: 'public', name: generateUUID() });
const { group, groupLeader: leader, members: generatedUsers } = await createAndPopulateGroup({
type: 'guild',
privacy: 'private',
name: generateUUID(),
upgradeToGroupPlan: true,
leaderDetails: { balance: 4 },
members: 57,
});
const usersToGenerate = [];
for (let i = 0; i < 57; i += 1) {
usersToGenerate.push(generateUser({ guilds: [group._id] }));
}
// Group has 59 members (1 is the leader)
const generatedUsers = await Promise.all(usersToGenerate);
const expectedIds = [leader._id].concat(generatedUsers.map(generatedUser => generatedUser._id));
const res = await user.get(`/groups/${group._id}/members`);
const res = await leader.get(`/groups/${group._id}/members`);
expect(res.length).to.equal(30);
const res2 = await user.get(`/groups/${group._id}/members?lastId=${res[res.length - 1]._id}`);
const res2 = await leader.get(`/groups/${group._id}/members?lastId=${res[res.length - 1]._id}`);
expect(res2.length).to.equal(28);
const resIds = res.concat(res2).map(member => member._id);

View File

@@ -11,7 +11,6 @@ import {
describe('GET /groups/:id', () => {
const typesOfGroups = {};
typesOfGroups['public guild'] = { type: 'guild', privacy: 'public' };
typesOfGroups['private guild'] = { type: 'guild', privacy: 'private' };
typesOfGroups.party = { type: 'party', privacy: 'private' };
@@ -24,10 +23,11 @@ describe('GET /groups/:id', () => {
const groupData = await createAndPopulateGroup({
members: 30,
groupDetails,
upgradeToGroupPlan: groupDetails.type === 'guild',
});
leader = groupData.groupLeader;
member = groupData.members[0]; // eslint-disable-line prefer-destructuring
[member] = groupData.members;
createdGroup = groupData.group;
});
@@ -49,34 +49,6 @@ describe('GET /groups/:id', () => {
});
});
context('Non-member of a public guild', () => {
let nonMember; let
createdGroup;
before(async () => {
const groupData = await createAndPopulateGroup({
members: 1,
groupDetails: {
name: 'test guild',
type: 'guild',
privacy: 'public',
},
});
createdGroup = groupData.group;
nonMember = await generateUser();
});
it('returns the group object for a non-member', async () => {
const group = await nonMember.get(`/groups/${createdGroup._id}`);
expect(group._id).to.eql(createdGroup._id);
expect(group.name).to.eql(createdGroup.name);
expect(group.type).to.eql(createdGroup.type);
expect(group.privacy).to.eql(createdGroup.privacy);
});
});
context('Non-member of a private guild', () => {
let nonMember; let
createdGroup;
@@ -89,6 +61,7 @@ describe('GET /groups/:id', () => {
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
createdGroup = groupData.group;
@@ -218,7 +191,7 @@ describe('GET /groups/:id', () => {
});
context('Flagged messages', () => {
let group;
let group; let members;
const chat1 = {
id: 'chat1',
@@ -268,7 +241,7 @@ describe('GET /groups/:id', () => {
groupDetails: {
name: 'test guild',
type: 'guild',
privacy: 'public',
privacy: 'private',
chat: [
chat1,
chat2,
@@ -277,9 +250,11 @@ describe('GET /groups/:id', () => {
chat5,
],
},
members: 1,
upgradeToGroupPlan: true,
});
group = groupData.group;
({ group, members } = groupData);
await group.addChat([chat1, chat2, chat3, chat4, chat5]);
});
@@ -287,8 +262,8 @@ describe('GET /groups/:id', () => {
context('non-admin', () => {
let nonAdmin;
beforeEach(async () => {
nonAdmin = await generateUser();
beforeEach(() => {
[nonAdmin] = members;
});
it('does not include messages with a flag count of 2 or greater', async () => {
@@ -314,9 +289,8 @@ describe('GET /groups/:id', () => {
let admin;
beforeEach(async () => {
admin = await generateUser({
'permissions.moderator': true,
});
[admin] = members;
await admin.update({ permissions: { moderator: true } });
});
it('includes all messages', async () => {

View File

@@ -2,7 +2,6 @@ import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
import { model as Group } from '../../../../../website/server/models/group';
import { MAX_SUMMARY_SIZE_FOR_GUILDS } from '../../../../../website/common/script/constants';
describe('POST /group', () => {
@@ -35,8 +34,8 @@ describe('POST /group', () => {
it('sets the group leader to the user who created the group', async () => {
const group = await user.post('/groups', {
name: 'Test Public Guild',
type: 'guild',
name: 'Test Party',
type: 'party',
});
expect(group.leader).to.eql({
@@ -51,7 +50,7 @@ describe('POST /group', () => {
const name = 'Test Group';
const group = await user.post('/groups', {
name,
type: 'guild',
type: 'party',
});
const updatedGroup = await user.get(`/groups/${group._id}`);
@@ -64,7 +63,7 @@ describe('POST /group', () => {
const summary = 'Test Summary';
const group = await user.post('/groups', {
name,
type: 'guild',
type: 'party',
summary,
});
@@ -78,7 +77,7 @@ describe('POST /group', () => {
const summary = 'A'.repeat(MAX_SUMMARY_SIZE_FOR_GUILDS + 1);
await expect(user.post('/groups', {
name,
type: 'guild',
type: 'party',
summary,
})).to.eventually.be.rejected.and.eql({
code: 400,
@@ -88,157 +87,6 @@ describe('POST /group', () => {
});
});
context('Guilds', () => {
it('returns an error when a user with insufficient funds attempts to create a guild', async () => {
await user.update({ balance: 0 });
await expect(
user.post('/groups', {
name: 'Test Public Guild',
type: 'guild',
}),
).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageInsufficientGems'),
});
});
it('adds guild to user\'s list of guilds', async () => {
const guild = await user.post('/groups', {
name: 'some guild',
type: 'guild',
privacy: 'public',
});
const updatedUser = await user.get('/user');
expect(updatedUser.guilds).to.include(guild._id);
});
it('awards the Joined Guild achievement', async () => {
await user.post('/groups', {
name: 'some guild',
type: 'guild',
privacy: 'public',
});
const updatedUser = await user.get('/user');
expect(updatedUser.achievements.joinedGuild).to.eql(true);
});
context('public guild', () => {
it('creates a group', async () => {
const groupName = 'Test Public Guild';
const groupType = 'guild';
const groupPrivacy = 'public';
const publicGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(publicGuild._id).to.exist;
expect(publicGuild.name).to.equal(groupName);
expect(publicGuild.type).to.equal(groupType);
expect(publicGuild.memberCount).to.equal(1);
expect(publicGuild.privacy).to.equal(groupPrivacy);
expect(publicGuild.leader).to.eql({
_id: user._id,
profile: {
name: user.profile.name,
},
});
});
it('returns an error when a user with no chat privileges attempts to create a public guild', async () => {
await user.update({ 'flags.chatRevoked': true });
await expect(
user.post('/groups', {
name: 'Test Public Guild',
type: 'guild',
privacy: 'public',
}),
).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
});
context('private guild', () => {
const groupName = 'Test Private Guild';
const groupType = 'guild';
const groupPrivacy = 'private';
it('creates a group', async () => {
const privateGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(privateGuild._id).to.exist;
expect(privateGuild.name).to.equal(groupName);
expect(privateGuild.type).to.equal(groupType);
expect(privateGuild.memberCount).to.equal(1);
expect(privateGuild.privacy).to.equal(groupPrivacy);
expect(privateGuild.leader).to.eql({
_id: user._id,
profile: {
name: user.profile.name,
},
});
});
it('creates a private guild when the user has no chat privileges', async () => {
await user.update({ 'flags.chatRevoked': true });
const privateGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(privateGuild._id).to.exist;
});
it('deducts gems from user and adds them to guild bank', async () => {
const privateGuild = await user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
expect(privateGuild.balance).to.eql(1);
const updatedUser = await user.get('/user');
expect(updatedUser.balance).to.eql(user.balance - 1);
});
it('does not deduct the gems from user when guild creation fails', async () => {
const stub = sinon.stub(Group.prototype, 'save').rejects();
const promise = user.post('/groups', {
name: groupName,
type: groupType,
privacy: groupPrivacy,
});
await expect(promise).to.eventually.be.rejected;
const updatedUser = await user.get('/user');
expect(updatedUser.balance).to.eql(user.balance);
stub.restore();
});
});
});
context('Parties', () => {
const partyName = 'Test Party';
const partyType = 'party';

View File

@@ -18,81 +18,24 @@ describe('POST /group/:groupId/join', () => {
});
});
context('Joining a public guild', () => {
let user; let joiningUser; let
publicGuild;
beforeEach(async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'public',
},
});
publicGuild = group;
user = groupLeader;
joiningUser = await generateUser();
});
it('allows non-invited users to join public guilds', async () => {
const res = await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get('/user')).to.eventually.have.property('guilds').to.include(publicGuild._id);
expect(res.leader._id).to.eql(user._id);
expect(res.leader.profile.name).to.eql(user.profile.name);
});
it('returns an error if user was already a member', async () => {
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.post(`/groups/${publicGuild._id}/join`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('youAreAlreadyInGroup'),
});
});
it('promotes joining member in a public empty guild to leader', async () => {
await user.post(`/groups/${publicGuild._id}/leave`);
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.nested.property('leader._id', joiningUser._id);
});
it('increments memberCount when joining guilds', async () => {
const oldMemberCount = publicGuild.memberCount;
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get(`/groups/${publicGuild._id}`)).to.eventually.have.property('memberCount', oldMemberCount + 1);
});
it('awards Joined Guild achievement', async () => {
await joiningUser.post(`/groups/${publicGuild._id}/join`);
await expect(joiningUser.get('/user')).to.eventually.have.nested.property('achievements.joinedGuild', true);
});
});
context('Joining a private guild', () => {
let user; let invitedUser; let
guild;
let user;
let invitedUser;
let guild;
let invitees;
beforeEach(async () => {
const { group, groupLeader, invitees } = await createAndPopulateGroup({
({ group: guild, groupLeader: user, invitees } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'private',
},
invites: 1,
});
upgradeToGroupPlan: true,
}));
guild = group;
user = groupLeader;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
[invitedUser] = invitees;
});
it('returns error when user is not invited to private guild', async () => {
@@ -182,7 +125,7 @@ describe('POST /group/:groupId/join', () => {
party = group;
user = groupLeader;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
[invitedUser] = invitees;
});
it('returns error when user is not invited to party', async () => {

View File

@@ -5,7 +5,6 @@ import {
generateChallenge,
checkExistence,
createAndPopulateGroup,
sleep,
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -14,253 +13,187 @@ import payments from '../../../../../website/server/libs/payments/payments';
import calculateSubscriptionTerminationDate from '../../../../../website/server/libs/payments/calculateSubscriptionTerminationDate';
describe('POST /groups/:groupId/leave', () => {
const typesOfGroups = {
'public guild': { type: 'guild', privacy: 'public' },
'private guild': { type: 'guild', privacy: 'private' },
party: { type: 'party', privacy: 'private' },
};
let groupToLeave;
let leader;
let member;
let members;
let memberCount;
each(typesOfGroups, (groupDetails, groupType) => {
context(`Leaving a ${groupType}`, () => {
let groupToLeave;
let leader;
let member;
let memberCount;
context('Leaving a Group Plan', () => {
beforeEach(async () => {
({ group: groupToLeave, groupLeader: leader, members } = await createAndPopulateGroup({
type: 'guild',
privacy: 'private',
members: 1,
upgradeToGroupPlan: true,
}));
[member] = members;
memberCount = groupToLeave.memberCount;
await leader.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('prevents non members from leaving', async () => {
const user = await generateUser();
await expect(user.post(`/groups/${groupToLeave._id}/leave`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('lets user leave', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
const userThatLeftGroup = await member.get('/user');
expect(userThatLeftGroup.guilds).to.be.empty;
expect(userThatLeftGroup.party._id).to.not.exist;
await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
});
it('removes new messages for that group from user', async () => {
await leader.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
await member.sync();
expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.exist;
expect(member.newMessages[groupToLeave._id]).to.not.be.empty;
await member.post(`/groups/${groupToLeave._id}/leave`);
await member.sync();
expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.not.exist;
expect(member.newMessages[groupToLeave._id]).to.be.undefined;
});
context('with challenges', () => {
let challenge;
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails,
members: 1,
});
challenge = await generateChallenge(leader, groupToLeave);
await member.post(`/challenges/${challenge._id}/join`);
groupToLeave = group;
leader = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
memberCount = group.memberCount;
await members[0].update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
it('prevents non members from leaving', async () => {
const user = await generateUser();
await expect(user.post(`/groups/${groupToLeave._id}/leave`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
await leader.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
type: 'habit',
});
});
it(`lets user leave a ${groupType}`, async () => {
it('removes all challenge tasks when keep parameter is set to remove', async () => {
await member.post(`/groups/${groupToLeave._id}/leave?keep=remove-all`);
const userWithoutChallengeTasks = await member.get('/user');
expect(userWithoutChallengeTasks.challenges).to.not.include(challenge._id);
expect(userWithoutChallengeTasks.tasksOrder.habits).to.be.empty;
});
it('keeps all challenge tasks when keep parameter is not set', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
const userThatLeftGroup = await member.get('/user');
const userWithChallengeTasks = await member.get('/user');
expect(userThatLeftGroup.guilds).to.be.empty;
expect(userThatLeftGroup.party._id).to.not.exist;
await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty;
});
it(`sets a new group leader when leader leaves a ${groupType}`, async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`);
it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`, { keepChallenges: 'remain-in-challenges' });
await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
expect(groupToLeave.leader).to.equal(member._id);
const userWithChallengeTasks = await member.get('/user');
expect(userWithChallengeTasks.challenges).to.include(challenge._id);
});
it('removes new messages for that group from user', async () => {
await member.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
await sleep(0.5);
const userWithChallengeTasks = await member.get('/user');
await leader.sync();
expect(leader.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.exist;
expect(leader.newMessages[groupToLeave._id]).to.not.be.empty;
await leader.post(`/groups/${groupToLeave._id}/leave`);
await leader.sync();
expect(leader.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.not.exist;
expect(leader.newMessages[groupToLeave._id]).to.be.undefined;
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id);
});
context('with challenges', () => {
let challenge;
beforeEach(async () => {
challenge = await generateChallenge(leader, groupToLeave);
await leader.post(`/challenges/${challenge._id}/join`);
await leader.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
type: 'habit',
});
await sleep(0.5);
});
it('removes all challenge tasks when keep parameter is set to remove', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave?keep=remove-all`);
const userWithoutChallengeTasks = await leader.get('/user');
expect(userWithoutChallengeTasks.challenges).to.not.include(challenge._id);
expect(userWithoutChallengeTasks.tasksOrder.habits).to.be.empty;
});
it('keeps all challenge tasks when keep parameter is not set', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await leader.get('/user');
// @TODO find elegant way to assert against the task existing
expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty;
});
it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`, { keepChallenges: 'remain-in-challenges' });
const userWithChallengeTasks = await leader.get('/user');
expect(userWithChallengeTasks.challenges).to.include(challenge._id);
});
it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await leader.get('/user');
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id);
});
});
it('prevents quest leader from leaving a groupToLeave');
it('prevents a user from leaving during an active quest');
});
});
context('Leaving a group as the last member', () => {
context('private guild', () => {
let privateGuild;
let leader;
let invitedUser;
context('Leaving a Party', () => {
let invitees;
let invitedUser;
beforeEach(async () => {
const { group, groupLeader, invitees } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Private Guild',
type: 'guild',
},
invites: 1,
leaderDetails: {
'auth.timestamps.created': new Date('2022-01-01'),
balance: 10,
},
});
beforeEach(async () => {
({
group: groupToLeave,
groupLeader: leader,
members,
invitees,
} = await createAndPopulateGroup({
type: 'party',
privacy: 'private',
members: 1,
invites: 1,
}));
privateGuild = group;
leader = groupLeader;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
[member] = members;
[invitedUser] = invitees;
memberCount = groupToLeave.memberCount;
await leader.update({ 'auth.timestamps.created': new Date('2022-01-01') });
});
await leader.post(`/groups/${group._id}/chat`, { message: 'Some message' });
});
it('removes a group when the last member leaves', async () => {
await leader.post(`/groups/${privateGuild._id}/leave`);
await expect(checkExistence('groups', privateGuild._id)).to.eventually.equal(false);
});
it('removes invitations when the last member leaves', async () => {
await leader.post(`/groups/${privateGuild._id}/leave`);
const userWithoutInvitation = await invitedUser.get('/user');
expect(userWithoutInvitation.invitations.guilds).to.be.empty;
it('prevents non members from leaving', async () => {
const user = await generateUser();
await expect(user.post(`/groups/${groupToLeave._id}/leave`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
context('public guild', () => {
let publicGuild;
let leader;
let invitedUser;
it('lets user leave', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
beforeEach(async () => {
const { group, groupLeader, invitees } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Public Guild',
type: 'guild',
privacy: 'public',
},
invites: 1,
});
const userThatLeftGroup = await member.get('/user');
publicGuild = group;
leader = groupLeader;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
});
it('keeps the group when the last member leaves', async () => {
await leader.post(`/groups/${publicGuild._id}/leave`);
await expect(checkExistence('groups', publicGuild._id)).to.eventually.equal(true);
});
it('keeps the invitations when the last member leaves a public guild', async () => {
await leader.post(`/groups/${publicGuild._id}/leave`);
const userWithoutInvitation = await invitedUser.get('/user');
expect(userWithoutInvitation.invitations.guilds).to.not.be.empty;
});
it('deletes non existent guild from user when user tries to leave', async () => {
const nonExistentGuildId = generateUUID();
const userWithNonExistentGuild = await generateUser({ guilds: [nonExistentGuildId] });
expect(userWithNonExistentGuild.guilds).to.contain(nonExistentGuildId);
await expect(userWithNonExistentGuild.post(`/groups/${nonExistentGuildId}/leave`))
.to.eventually.be.rejected;
await userWithNonExistentGuild.sync();
expect(userWithNonExistentGuild.guilds).to.not.contain(nonExistentGuildId);
});
expect(userThatLeftGroup.guilds).to.be.empty;
expect(userThatLeftGroup.party._id).to.not.exist;
await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
});
context('party', () => {
let party;
let leader;
let invitedUser;
it('sets a new group leader when leader leaves', async () => {
await leader.post(`/groups/${groupToLeave._id}/leave`);
beforeEach(async () => {
const { group, groupLeader, invitees } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Party',
type: 'party',
},
invites: 1,
});
await groupToLeave.sync();
expect(groupToLeave.memberCount).to.equal(memberCount - 1);
expect(groupToLeave.leader).to.equal(member._id);
});
party = group;
leader = groupLeader;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
});
it('removes new messages for that group from user', async () => {
await leader.post(`/groups/${groupToLeave._id}/chat`, { message: 'Some message' });
await member.sync();
it('removes a group when the last member leaves a party', async () => {
await leader.post(`/groups/${party._id}/leave`);
expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.exist;
expect(member.newMessages[groupToLeave._id]).to.not.be.empty;
await expect(checkExistence('party', party._id)).to.eventually.equal(false);
});
await member.post(`/groups/${groupToLeave._id}/leave`);
await member.sync();
it('removes invitations when the last member leaves a party', async () => {
await leader.post(`/groups/${party._id}/leave`);
expect(member.notifications.find(n => n.type === 'NEW_CHAT_MESSAGE' && n.data.group.id === groupToLeave._id)).to.not.exist;
expect(member.newMessages[groupToLeave._id]).to.be.undefined;
});
const userWithoutInvitation = await invitedUser.get('/user');
it('removes a party when the last member leaves', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
await leader.post(`/groups/${groupToLeave._id}/leave`);
expect(userWithoutInvitation.invitations.parties[0]).to.be.undefined;
});
await expect(checkExistence('party', groupToLeave._id)).to.eventually.equal(false);
});
it('removes invitations when the last member leaves a party', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
await leader.post(`/groups/${groupToLeave._id}/leave`);
const userWithoutInvitation = await invitedUser.get('/user');
expect(userWithoutInvitation.invitations.parties[0]).to.be.undefined;
});
it('deletes non existent party from user when user tries to leave', async () => {
@@ -275,23 +208,71 @@ describe('POST /groups/:groupId/leave', () => {
expect(userWithNonExistentParty.party).to.eql({});
});
context('with challenges', () => {
let challenge;
beforeEach(async () => {
challenge = await generateChallenge(leader, groupToLeave);
await member.post(`/challenges/${challenge._id}/join`);
await leader.post(`/tasks/challenge/${challenge._id}`, {
text: 'test habit',
type: 'habit',
});
});
it('removes all challenge tasks when keep parameter is set to remove', async () => {
await member.post(`/groups/${groupToLeave._id}/leave?keep=remove-all`);
const userWithoutChallengeTasks = await member.get('/user');
expect(userWithoutChallengeTasks.challenges).to.not.include(challenge._id);
expect(userWithoutChallengeTasks.tasksOrder.habits).to.be.empty;
});
it('keeps all challenge tasks when keep parameter is not set', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await member.get('/user');
expect(userWithChallengeTasks.tasksOrder.habits).to.not.be.empty;
});
it('keeps the user in the challenge when the keepChallenges parameter is set to remain-in-challenges', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`, { keepChallenges: 'remain-in-challenges' });
const userWithChallengeTasks = await member.get('/user');
expect(userWithChallengeTasks.challenges).to.include(challenge._id);
});
it('drops the user in the challenge when the keepChallenges parameter isn\'t set', async () => {
await member.post(`/groups/${groupToLeave._id}/leave`);
const userWithChallengeTasks = await member.get('/user');
expect(userWithChallengeTasks.challenges).to.not.include(challenge._id);
});
});
});
const typesOfGroups = {
'private guild': { type: 'guild', privacy: 'private' },
party: { type: 'party', privacy: 'private' },
};
each(typesOfGroups, (groupDetails, groupType) => {
context(`Leaving a group plan when the group is a ${groupType}`, () => {
if (groupDetails.privacy === 'public') return; // public guilds cannot be group plans
let groupWithPlan;
let leader;
let member;
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
({ group: groupWithPlan, groupLeader: leader, members } = await createAndPopulateGroup({
groupDetails,
members: 1,
});
leader = groupLeader;
member = members[0]; // eslint-disable-line prefer-destructuring
groupWithPlan = group;
upgradeToGroupPlan: true,
}));
[member] = members;
const userWithFreePlan = await User.findById(leader._id).exec();
// Create subscription
@@ -321,45 +302,21 @@ describe('POST /groups/:groupId/leave', () => {
await member.sync();
expect(member.purchased.plan.dateTerminated).to.exist;
});
it('preserves the free subscription when leaving a any other group without a plan', async () => {
// Joining a guild without a group plan
const { group: groupWithNoPlan } = await createAndPopulateGroup({
groupDetails: {
name: 'Group Without Plan',
type: 'guild',
privacy: 'public',
},
});
await member.post(`/groups/${groupWithNoPlan._id}/join`);
await member.sync();
expect(member.purchased.plan.planId).to.equal('group_plan_auto');
expect(member.purchased.plan.dateTerminated).to.not.exist;
// Leaving the guild without a group plan
await member.post(`/groups/${groupWithNoPlan._id}/leave`);
await member.sync();
expect(member.purchased.plan.dateTerminated).to.not.exist;
});
});
});
each(typesOfGroups, (groupDetails, groupType) => {
context(`Leaving a group with extraMonths left plan when the group is a ${groupType}`, () => {
if (groupDetails.privacy === 'public') return; // public guilds cannot be group plans
const extraMonths = 12;
let groupWithPlan;
let member;
beforeEach(async () => {
const { group, members } = await createAndPopulateGroup({
({ group: groupWithPlan, members } = await createAndPopulateGroup({
groupDetails,
members: 1,
upgradeToGroupPlan: true,
});
}));
[member] = members;
groupWithPlan = group;
await member.update({
'purchased.plan.extraMonths': extraMonths,
});

View File

@@ -5,43 +5,6 @@ import {
} from '../../../../helpers/api-integration/v3';
describe('POST /group/:groupId/reject-invite', () => {
context('Rejecting a public guild invite', () => {
let publicGuild; let
invitedUser;
beforeEach(async () => {
const { group, invitees } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Guild',
type: 'guild',
privacy: 'public',
},
invites: 1,
});
publicGuild = group;
invitedUser = invitees[0]; // eslint-disable-line prefer-destructuring
});
it('returns error when user is not invited', async () => {
const userWithoutInvite = await generateUser();
await expect(userWithoutInvite.post(`/groups/${publicGuild._id}/reject-invite`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupRequiresInvite'),
});
});
it('clears invitation from user', async () => {
await invitedUser.post(`/groups/${publicGuild._id}/reject-invite`);
await expect(invitedUser.get('/user'))
.to.eventually.have.nested.property('invitations.guilds')
.to.not.include({ id: publicGuild._id });
});
});
context('Rejecting a private guild invite', () => {
let invitedUser; let
guild;
@@ -54,6 +17,7 @@ describe('POST /group/:groupId/reject-invite', () => {
privacy: 'private',
},
invites: 1,
upgradeToGroupPlan: true,
});
guild = group;

View File

@@ -25,6 +25,7 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
},
invites: 1,
members: 2,
upgradeToGroupPlan: true,
});
guild = group;
@@ -129,9 +130,11 @@ describe('POST /groups/:groupId/removeMember/:memberId', () => {
it('sends email to removed user', async () => {
await leader.post(`/groups/${guild._id}/removeMember/${member._id}`);
expect(email.sendTxn).to.be.calledOnce;
expect(email.sendTxn).to.be.calledTwice;
expect(email.sendTxn.args[0][0]._id).to.eql(member._id);
expect(email.sendTxn.args[0][1]).to.eql('kicked-from-guild');
expect(email.sendTxn.args[1][0]._id).to.eql(member._id);
expect(email.sendTxn.args[1][1]).to.eql('group-member-removed');
});
});

View File

@@ -3,24 +3,23 @@ import nconf from 'nconf';
import {
createAndPopulateGroup,
generateUser,
generateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
const INVITES_LIMIT = 100;
const PARTY_LIMIT_MEMBERS = 29;
const PARTY_LIMIT_MEMBERS = 30;
const MAX_EMAIL_INVITES_BY_USER = 200;
describe('Post /groups/:groupId/invite', () => {
let inviter;
let group;
const groupName = 'Test Public Guild';
const groupName = 'Test Party';
beforeEach(async () => {
inviter = await generateUser({ balance: 4 });
group = await inviter.post('/groups', {
name: groupName,
type: 'guild',
type: 'party',
});
});
@@ -65,45 +64,44 @@ describe('Post /groups/:groupId/invite', () => {
it('invites a user to a group by username', async () => {
const userToInvite = await generateUser();
await expect(inviter.post(`/groups/${group._id}/invite`, {
const response = await inviter.post(`/groups/${group._id}/invite`, {
usernames: [userToInvite.auth.local.lowerCaseUsername],
})).to.eventually.deep.equal([{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
}]);
});
expect(response).to.be.an('Array');
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[0]._id).to.be.a('String');
expect(response[0].id).to.eql(group._id);
expect(response[0].name).to.eql(groupName);
expect(response[0].inviter).to.eql(inviter._id);
await expect(userToInvite.get('/user'))
.to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
.to.eventually.have.nested.property('invitations.parties[0].id', group._id);
});
it('invites multiple users to a group by uuid', async () => {
const userToInvite = await generateUser();
const userToInvite2 = await generateUser();
await expect(inviter.post(`/groups/${group._id}/invite`, {
const response = await (inviter.post(`/groups/${group._id}/invite`, {
usernames: [
userToInvite.auth.local.lowerCaseUsername,
userToInvite2.auth.local.lowerCaseUsername,
],
})).to.eventually.deep.equal([
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
]);
}));
expect(response).to.be.an('Array');
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[0]._id).to.be.a('String');
expect(response[0].id).to.eql(group._id);
expect(response[0].name).to.eql(groupName);
expect(response[0].inviter).to.eql(inviter._id);
expect(response[1]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[1]._id).to.be.a('String');
expect(response[1].id).to.eql(group._id);
expect(response[1].name).to.eql(groupName);
expect(response[1].inviter).to.eql(inviter._id);
await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
});
});
@@ -214,42 +212,42 @@ describe('Post /groups/:groupId/invite', () => {
it('invites a user to a group by uuid', async () => {
const userToInvite = await generateUser();
await expect(inviter.post(`/groups/${group._id}/invite`, {
const response = await inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
})).to.eventually.deep.equal([{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
}]);
});
expect(response).to.be.an('Array');
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[0]._id).to.be.a('String');
expect(response[0].id).to.eql(group._id);
expect(response[0].name).to.eql(groupName);
expect(response[0].inviter).to.eql(inviter._id);
await expect(userToInvite.get('/user'))
.to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
.to.eventually.have.nested.property('invitations.parties[0].id', group._id);
});
it('invites multiple users to a group by uuid', async () => {
const userToInvite = await generateUser();
const userToInvite2 = await generateUser();
await expect(inviter.post(`/groups/${group._id}/invite`, {
const response = await inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id, userToInvite2._id],
})).to.eventually.deep.equal([
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
{
id: group._id,
name: groupName,
inviter: inviter._id,
publicGuild: false,
},
]);
});
await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
expect(response).to.be.an('Array');
expect(response[0]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[0]._id).to.be.a('String');
expect(response[0].id).to.eql(group._id);
expect(response[0].name).to.eql(groupName);
expect(response[0].inviter).to.eql(inviter._id);
expect(response[1]).to.have.all.keys(['_id', 'id', 'name', 'inviter']);
expect(response[1]._id).to.be.a('String');
expect(response[1].id).to.eql(group._id);
expect(response[1].name).to.eql(groupName);
expect(response[1].inviter).to.eql(inviter._id);
await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.parties[0].id', group._id);
});
it('returns an error when inviting multiple users and a user is not found', async () => {
@@ -338,12 +336,8 @@ describe('Post /groups/:groupId/invite', () => {
invitesSent: MAX_EMAIL_INVITES_BY_USER,
balance: 4,
});
const tmpGroup = await inviterWithMax.post('/groups', {
name: groupName,
type: 'guild',
});
await expect(inviterWithMax.post(`/groups/${tmpGroup._id}/invite`, {
await expect(inviterWithMax.post(`/groups/${group._id}/invite`, {
emails: [testInvite],
inviter: 'inviter name',
}))
@@ -419,15 +413,15 @@ describe('Post /groups/:groupId/invite', () => {
});
const invitedUser = await newUser.get('/user');
expect(invitedUser.invitations.guilds[0].id).to.equal(group._id);
expect(invitedUser.invitations.parties[0].id).to.equal(group._id);
expect(invite).to.exist;
});
it('invites marks invite with cancelled plan', async () => {
const cancelledPlanGroup = await generateGroup(inviter, {
type: 'guild',
name: generateUUID(),
});
it('invites user to group with cancelled plan', async () => {
let cancelledPlanGroup;
({ group: cancelledPlanGroup, groupLeader: inviter } = await createAndPopulateGroup({
upgradeToGroupPlan: true,
}));
await cancelledPlanGroup.createCancelledSubscription();
const newUser = await generateUser();
@@ -437,13 +431,13 @@ describe('Post /groups/:groupId/invite', () => {
});
const invitedUser = await newUser.get('/user');
expect(invitedUser.invitations.guilds[0].id).to.equal(cancelledPlanGroup._id);
expect(invitedUser.invitations.guilds[0].cancelledPlan).to.be.true;
expect(invitedUser.invitations.parties[0].id).to.equal(cancelledPlanGroup._id);
expect(invitedUser.invitations.parties[0].cancelledPlan).to.be.true;
expect(invite).to.exist;
});
});
describe('guild invites', () => {
describe('party invites', () => {
it('returns an error when inviter has no chat privileges', async () => {
const inviterMuted = await inviter.update({ 'flags.chatRevoked': true });
const userToInvite = await generateUser();
@@ -457,103 +451,13 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('returns an error when invited user is already invited to the group', async () => {
const userToInvite = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
});
await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userAlreadyInvitedToGroup', { userId: userToInvite._id, username: userToInvite.profile.name }),
});
});
it('returns an error when invited user is already in the group', async () => {
const userToInvite = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
});
await userToInvite.post(`/groups/${group._id}/join`);
await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userAlreadyInGroup', { userId: userToInvite._id, username: userToInvite.profile.name }),
});
});
it('allows 30+ members in a guild', async () => {
const invitesToGenerate = [];
// Generate 30 users to invite (30 + leader = 31 members)
for (let i = 0; i < PARTY_LIMIT_MEMBERS; i += 1) {
invitesToGenerate.push(generateUser());
}
const generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
expect(await inviter.post(`/groups/${group._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
})).to.be.an('array');
}).timeout(10000);
// @TODO: Add this after we are able to mock the group plan route
xit('returns an error when a non-leader invites to a group plan', async () => {
const userToInvite = await generateUser();
const nonGroupLeader = await generateUser();
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [nonGroupLeader._id],
});
await nonGroupLeader.post(`/groups/${group._id}/join`);
await expect(nonGroupLeader.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyGroupLeaderCanInviteToGroupPlan'),
});
});
});
describe('party invites', () => {
let party;
beforeEach(async () => {
party = await inviter.post('/groups', {
name: 'Test Party',
type: 'party',
});
});
it('returns an error when inviter has no chat privileges', async () => {
const inviterMuted = await inviter.update({ 'flags.chatRevoked': true });
const userToInvite = await generateUser();
await expect(inviterMuted.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('chatPrivilegesRevoked'),
});
});
it('returns an error when invited user has a pending invitation to the party', async () => {
const userToInvite = await generateUser();
await inviter.post(`/groups/${party._id}/invite`, {
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
});
await expect(inviter.post(`/groups/${party._id}/invite`, {
await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
@@ -566,13 +470,13 @@ describe('Post /groups/:groupId/invite', () => {
it('returns an error when invited user is already in a party of more than 1 member', async () => {
const userToInvite = await generateUser();
const userToInvite2 = await generateUser();
await inviter.post(`/groups/${party._id}/invite`, {
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id, userToInvite2._id],
});
await userToInvite.post(`/groups/${party._id}/join`);
await userToInvite2.post(`/groups/${party._id}/join`);
await userToInvite.post(`/groups/${group._id}/join`);
await userToInvite2.post(`/groups/${group._id}/join`);
await expect(inviter.post(`/groups/${party._id}/invite`, {
await expect(inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
}))
.to.eventually.be.rejected.and.eql({
@@ -596,7 +500,7 @@ describe('Post /groups/:groupId/invite', () => {
});
// Invite to first party
await inviter.post(`/groups/${party._id}/invite`, {
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
});
@@ -609,28 +513,27 @@ describe('Post /groups/:groupId/invite', () => {
const invitedUser = await userToInvite.get('/user');
expect(invitedUser.invitations.parties.length).to.equal(2);
expect(invitedUser.invitations.parties[0].id).to.equal(party._id);
expect(invitedUser.invitations.parties[0].id).to.equal(group._id);
expect(invitedUser.invitations.parties[1].id).to.equal(party2._id);
});
it('allow inviting a user if party id is not associated with a real party', async () => {
it('allows inviting a user if party id is not associated with a real party', async () => {
const userToInvite = await generateUser({
party: { _id: generateUUID() },
});
await inviter.post(`/groups/${party._id}/invite`, {
await inviter.post(`/groups/${group._id}/invite`, {
uuids: [userToInvite._id],
});
expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(party._id);
expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(group._id);
});
});
describe('party size limits', () => {
let party;
let partyLeader;
beforeEach(async () => {
group = await createAndPopulateGroup({
({ group, groupLeader: partyLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Party',
type: 'party',
@@ -638,9 +541,7 @@ describe('Post /groups/:groupId/invite', () => {
},
// Generate party with 20 members
members: PARTY_LIMIT_MEMBERS - 10,
});
party = group.group;
partyLeader = group.groupLeader;
}));
});
it('allows 30 members in a party', async () => {
@@ -651,7 +552,7 @@ describe('Post /groups/:groupId/invite', () => {
}
const generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
expect(await partyLeader.post(`/groups/${party._id}/invite`, {
expect(await partyLeader.post(`/groups/${group._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
})).to.be.an('array');
}).timeout(10000);
@@ -664,13 +565,13 @@ describe('Post /groups/:groupId/invite', () => {
}
const generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
await expect(partyLeader.post(`/groups/${party._id}/invite`, {
await expect(partyLeader.post(`/groups/${group._id}/invite`, {
uuids: generatedInvites.map(invite => invite._id),
}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('partyExceedsMembersLimit', { maxMembersParty: PARTY_LIMIT_MEMBERS + 1 }),
message: t('partyExceedsMembersLimit', { maxMembersParty: PARTY_LIMIT_MEMBERS }),
});
}).timeout(10000);
});

View File

@@ -17,9 +17,10 @@ describe('POST /group/:groupId/add-manager', () => {
groupDetails: {
name: groupName,
type: groupType,
privacy: 'public',
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
});
groupToUpdate = group;

View File

@@ -23,10 +23,11 @@ describe('PUT /group', () => {
groupDetails: {
name: groupName,
type: groupType,
privacy: 'public',
privacy: 'private',
categories: groupCategories,
},
members: 1,
upgradeToGroupPlan: true,
});
adminUser = await generateUser({ 'permissions.moderator': true });
groupToUpdate = group;
@@ -106,14 +107,28 @@ describe('PUT /group', () => {
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
it('allows a leader to change leaders', async () => {
const updatedGroup = await leader.put(`/groups/${groupToUpdate._id}`, {
it('does not allow a leader to change leader of active group plan', async () => {
await expect(leader.put(`/groups/${groupToUpdate._id}`, {
name: groupUpdatedName,
leader: nonLeader._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cannotChangeLeaderWithActiveGroupPlan'),
});
});
it('allows a leader of a party to change leaders', async () => {
const { group: party, groupLeader: partyLeader, members } = await createAndPopulateGroup({
members: 1,
});
const updatedGroup = await partyLeader.put(`/groups/${party._id}`, {
name: groupUpdatedName,
leader: members[0]._id,
});
expect(updatedGroup.leader._id).to.eql(nonLeader._id);
expect(updatedGroup.leader.profile.name).to.eql(nonLeader.profile.name);
expect(updatedGroup.leader._id).to.eql(members[0]._id);
expect(updatedGroup.leader.profile.name).to.eql(members[0].profile.name);
expect(updatedGroup.name).to.equal(groupUpdatedName);
});
@@ -122,15 +137,16 @@ describe('PUT /group', () => {
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
const updateGroupDetails = {
id: group._id,
name: 'public guild',
type: 'guild',
privacy: 'public',
privacy: 'private',
bannedWordsAllowed: true,
};
@@ -150,9 +166,11 @@ describe('PUT /group', () => {
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
await groupLeader.update({ permissions: {} });
const updateGroupDetails = {
id: group._id,

View File

@@ -0,0 +1,64 @@
import { v4 as generateUUID } from 'uuid';
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /members/:memberId/clear-flags', () => {
let reporter;
let admin;
let moderator;
beforeEach(async () => {
reporter = await generateUser();
admin = await generateUser({ permissions: { userSupport: true } });
moderator = await generateUser({ permissions: { moderator: true } });
await reporter.post(`/members/${admin._id}/flag`);
});
context('error cases', () => {
it('returns error when memberId is not a UUID', async () => {
await expect(moderator.post('/members/gribbly/clear-flags'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns error when member with UUID is not found', async () => {
const randomId = generateUUID();
await expect(moderator.post(`/members/${randomId}/clear-flags`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', { userId: randomId }),
});
});
it('returns error when requesting user is not a moderator', async () => {
await expect(reporter.post(`/members/${admin._id}/clear-flags`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'Only a moderator may clear reports from a profile.',
});
});
});
context('valid request', () => {
it('removes a single flag from user', async () => {
await expect(moderator.post(`/members/${admin._id}/clear-flags`)).to.eventually.be.ok;
const updatedTarget = await admin.get(`/hall/heroes/${admin._id}`);
expect(updatedTarget.profile.flags).to.eql({});
});
it('removes multiple flags from user', async () => {
await moderator.post(`/members/${admin._id}/flag`);
await expect(moderator.post(`/members/${admin._id}/clear-flags`)).to.eventually.be.ok;
const updatedTarget = await admin.get(`/hall/heroes/${admin._id}`);
expect(updatedTarget.profile.flags).to.eql({});
});
});
});

View File

@@ -0,0 +1,151 @@
import { v4 as generateUUID } from 'uuid';
import moment from 'moment';
import nconf from 'nconf';
import { IncomingWebhook } from '@slack/webhook';
import {
generateUser,
translate as t,
} from '../../../../helpers/api-integration/v3';
describe('POST /members/:memberId/flag', () => {
let reporter;
let target;
beforeEach(async () => {
reporter = await generateUser();
target = await generateUser({
'profile.blurb': 'Naughty Text',
'profile.imageUrl': 'https://evil.com/',
});
});
context('error cases', () => {
it('returns error when memberId is not a UUID', async () => {
await expect(reporter.post('/members/gribbly/flag'))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('invalidReqParams'),
});
});
it('returns error when member with UUID is not found', async () => {
const randomId = generateUUID();
await expect(reporter.post(`/members/${randomId}/flag`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('userWithIDNotFound', { userId: randomId }),
});
});
it('returns error when non-admin flags same profile twice', async () => {
await reporter.post(`/members/${target._id}/flag`);
await expect(reporter.post(`/members/${target._id}/flag`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: 'A profile can not be flagged more than once by the same user.',
});
});
});
context('valid request', () => {
let admin;
const comment = 'this profile is bad';
const source = 'Third Party Script';
beforeEach(async () => {
admin = await generateUser({ 'permissions.userSupport': true });
sandbox.stub(IncomingWebhook.prototype, 'send').returns(Promise.resolve());
});
afterEach(() => {
sandbox.restore();
});
it('adds flags object to target user', async () => {
await reporter.post(`/members/${target._id}/flag`);
const updatedTarget = await admin.get(`/hall/heroes/${target._id}`);
expect(updatedTarget.profile.flags[reporter._id]).to.have.all.keys([
'comment',
'source',
'timestamp',
]);
expect(moment(updatedTarget.profile.flags[reporter._id].timestamp).toDate()).to.be.a('date');
});
it('allows addition of a comment and source', async () => {
await reporter.post(`/members/${target._id}/flag`, {
comment,
source,
});
const updatedTarget = await admin.get(`/hall/heroes/${target._id}`);
expect(updatedTarget.profile.flags[reporter._id].comment).to.eql(comment);
expect(updatedTarget.profile.flags[reporter._id].source).to.eql(source);
});
it('allows moderator to flag twice', async () => {
const moderator = await generateUser({ 'permissions.moderator': true });
await moderator.post(`/members/${target._id}/flag`);
await expect(moderator.post(`/members/${target._id}/flag`)).to.eventually.be.ok;
});
it('allows multiple non-moderators to flag individually', async () => {
await admin.post(`/members/${target._id}/flag`);
await reporter.post(`/members/${target._id}/flag`);
const updatedTarget = await admin.get(`/hall/heroes/${target._id}`);
expect(updatedTarget.profile.flags[admin._id]).to.exist;
expect(updatedTarget.profile.flags[reporter._id]).to.exist;
});
it('sends a flag report to moderation Slack', async () => {
const BASE_URL = nconf.get('BASE_URL');
await reporter.post(`/members/${target._id}/flag`, {
comment,
source,
});
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `@${reporter.auth.local.username} (${reporter._id}; language: ${reporter.preferences.language}) flagged @${target.auth.local.username}'s profile from ${source} and commented: ${comment}`,
attachments: [{
fallback: 'Flag Profile',
color: 'danger',
title: 'User Profile Report',
title_link: `${BASE_URL}/profile/${target._id}`,
text: `Display Name: ${target.profile.name}\n\nImage URL: ${target.profile.imageUrl}\n\nAbout: ${target.profile.blurb}`,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
});
it('excludes empty fields when sending Slack message', async () => {
const BASE_URL = nconf.get('BASE_URL');
await reporter.post(`/members/${admin._id}/flag`, {
comment,
source,
});
/* eslint-disable camelcase */
expect(IncomingWebhook.prototype.send).to.be.calledWith({
text: `@${reporter.auth.local.username} (${reporter._id}; language: ${reporter.preferences.language}) flagged @${admin.auth.local.username}'s profile from ${source} and commented: ${comment}`,
attachments: [{
fallback: 'Flag Profile',
color: 'danger',
title: 'User Profile Report',
title_link: `${BASE_URL}/profile/${admin._id}`,
text: `Display Name: ${admin.profile.name}`,
mrkdwn_in: [
'text',
],
}],
});
/* eslint-enable camelcase */
});
});
});

View File

@@ -1,6 +1,6 @@
import {
createAndPopulateGroup,
generateUser,
generateGroup,
translate as t,
} from '../../../../../helpers/api-integration/v3';
import amzLib from '../../../../../../website/server/libs/payments/amazon';
@@ -50,22 +50,21 @@ describe('payments : amazon #subscribeCancel', () => {
});
it('cancels a group subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
({ group, groupLeader: user } = await createAndPopulateGroup({
groupDetails: {
name: 'test group',
type: 'guild',
privacy: 'private',
},
leaderDetails: {
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
},
upgradeToGroupPlan: true,
}));
await user.get(`${endpoint}&groupId=${group._id}`);

View File

@@ -70,8 +70,8 @@ describe('payments - amazon - #subscribe', () => {
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
type: 'party',
privacy: 'private',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),

View File

@@ -1,7 +1,7 @@
import {
generateUser,
generateGroup,
translate as t,
createAndPopulateGroup,
} from '../../../../../helpers/api-integration/v3';
import stripePayments from '../../../../../../website/server/libs/payments/stripe';
@@ -48,22 +48,21 @@ describe('payments - stripe - #subscribeCancel', () => {
});
it('cancels a group subscription', async () => {
user = await generateUser({
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
});
group = await generateGroup(user, {
name: 'test group',
type: 'guild',
privacy: 'public',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
});
({ group, groupLeader: user } = await createAndPopulateGroup({
groupDetails: {
name: 'test group',
type: 'guild',
privacy: 'private',
},
leaderDetails: {
'profile.name': 'sender',
'purchased.plan.customerId': 'customer-id',
'purchased.plan.planId': 'basic_3mo',
'purchased.plan.lastBillingDate': new Date(),
balance: 2,
},
upgradeToGroupPlan: true,
}));
await user.get(`${endpoint}&groupId=${group._id}`);

View File

@@ -53,6 +53,7 @@ describe('POST /groups/:groupId/quests/accept', () => {
it('does not accept quest for a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/accept`))

View File

@@ -43,6 +43,7 @@ describe('POST /groups/:groupId/quests/force-start', () => {
it('does not force start quest for a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/force-start`))

View File

@@ -51,14 +51,13 @@ describe('POST /groups/:groupId/quests/invite/:questKey', () => {
});
it('does not issue invites for Guilds', async () => {
const { group } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'public' },
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
members: 1,
upgradeToGroupPlan: true,
});
const alternateGroup = group;
await expect(leader.post(`/groups/${alternateGroup._id}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({
await expect(groupLeader.post(`/groups/${group._id}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('guildQuestsNotSupported'),

View File

@@ -52,6 +52,7 @@ describe('POST /groups/:groupId/quests/abort', () => {
it('returns an error when group is a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/abort`))

View File

@@ -52,6 +52,7 @@ describe('POST /groups/:groupId/quests/cancel', () => {
it('returns an error when group is a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/cancel`))

View File

@@ -51,6 +51,7 @@ describe('POST /groups/:groupId/quests/leave', () => {
it('returns an error when group is a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/leave`))

View File

@@ -53,6 +53,7 @@ describe('POST /groups/:groupId/quests/reject', () => {
it('returns an error when group is a guild', async () => {
const { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/reject`))

View File

@@ -90,7 +90,9 @@ describe('POST /tasks/unlink-one/:taskId', () => {
});
});
it('unlinks a task from a challenge and saves it on keep=keep', async () => {
// These tests are causing trouble. Race condition?
xit('unlinks a task from a challenge and saves it on keep=keep', async () => {
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
let [daily] = await user.get('/tasks/user');
await user.del(`/challenges/${challenge._id}`);
@@ -99,7 +101,7 @@ describe('POST /tasks/unlink-one/:taskId', () => {
expect(daily.challenge).to.eql({});
});
it('unlinks a task from a challenge and deletes it on keep=remove', async () => {
xit('unlinks a task from a challenge and deletes it on keep=remove', async () => {
await user.post(`/tasks/challenge/${challenge._id}`, tasksToTest.daily);
const [daily] = await user.get('/tasks/user');
await user.del(`/challenges/${challenge._id}`);

View File

@@ -1,6 +1,5 @@
import {
generateUser,
generateGroup,
createAndPopulateGroup,
} from '../../../../../helpers/api-integration/v3';
describe('POST group-tasks/:taskId/move/to/:position', () => {
@@ -8,8 +7,12 @@ describe('POST group-tasks/:taskId/move/to/:position', () => {
guild;
beforeEach(async () => {
user = await generateUser({ balance: 1 });
guild = await generateGroup(user, { type: 'guild' }, { 'purchased.plan.customerId': 'group-unlimited' });
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
guild = group;
user = groupLeader;
});
it('can move task to new position', async () => {

View File

@@ -1,5 +1,4 @@
import {
find,
each,
map,
} from 'lodash';
@@ -198,95 +197,6 @@ describe('DELETE /user', () => {
await expect(checkExistence('party', party._id)).to.eventually.eql(false);
});
});
context('last member of a private guild', () => {
let privateGuild;
beforeEach(async () => {
privateGuild = await generateGroup(user, {
type: 'guild',
privacy: 'private',
});
});
it('deletes guild when user is the only member', async () => {
await user.del('/user', {
password,
});
await expect(checkExistence('groups', privateGuild._id)).to.eventually.eql(false);
});
});
context('groups user is leader of', () => {
let guild; let oldLeader; let
newLeader;
beforeEach(async () => {
const { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 1,
});
guild = group;
newLeader = members[0]; // eslint-disable-line prefer-destructuring
oldLeader = groupLeader;
});
it('chooses new group leader for any group user was the leader of', async () => {
await oldLeader.del('/user', {
password,
});
const updatedGuild = await newLeader.get(`/groups/${guild._id}`);
expect(updatedGuild.leader).to.exist;
expect(updatedGuild.leader._id).to.not.eql(oldLeader._id);
});
});
context('groups user is a part of', () => {
let group1; let group2; let userToDelete; let
otherUser;
beforeEach(async () => {
userToDelete = await generateUser({ balance: 10 });
group1 = await generateGroup(userToDelete, {
type: 'guild',
privacy: 'public',
});
const { group, members } = await createAndPopulateGroup({
groupDetails: {
type: 'guild',
privacy: 'public',
},
members: 3,
});
group2 = group;
otherUser = members[0]; // eslint-disable-line prefer-destructuring
await userToDelete.post(`/groups/${group2._id}/join`);
});
it('removes user from all groups user was a part of', async () => {
await userToDelete.del('/user', {
password,
});
const updatedGroup1Members = await otherUser.get(`/groups/${group1._id}/members`);
const updatedGroup2Members = await otherUser.get(`/groups/${group2._id}/members`);
const userInGroup = find(updatedGroup2Members, member => member._id === userToDelete._id);
expect(updatedGroup1Members).to.be.empty;
expect(updatedGroup2Members).to.not.be.empty;
expect(userInGroup).to.not.exist;
});
});
});
context('user with Google auth', async () => {

View File

@@ -51,6 +51,7 @@ describe('POST /user/purchase/:type/:key', () => {
type: 'guild',
privacy: 'private',
},
upgradeToGroupPlan: true,
});
await group.update({
'leaderOnly.getGems': true,
@@ -77,6 +78,7 @@ describe('POST /user/purchase/:type/:key', () => {
privacy: 'private',
},
members: 1,
upgradeToGroupPlan: true,
});
await group.update({
'leaderOnly.getGems': true,

View File

@@ -714,31 +714,6 @@ describe('POST /user/auth/local/register', () => {
expect(user.invitations.party).to.eql({});
});
it('adds a user to a guild on an invite of type other than party', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
const invite = encrypt(JSON.stringify({
id: group._id,
inviter: groupLeader._id,
sentAt: Date.now(),
}));
const user = await api.post(`/user/auth/local/register?groupInvite=${invite}`, {
username,
email,
password,
confirmPassword: password,
});
expect(user.invitations.guilds[0]).to.eql({
id: group._id,
name: group.name,
inviter: groupLeader._id,
});
});
});
context('successful login via api', () => {

View File

@@ -12,7 +12,7 @@ describe('GET /faq', () => {
expect(res).to.have.property('stillNeedHelp');
expect(res.stillNeedHelp.ios).to.equal(translate('iosFaqStillNeedHelp'));
expect(res).to.have.property('questions');
expect(res.questions[0].question).to.equal(translate('faqQuestion0'));
expect(res.questions[0].question).to.equal(translate('faqQuestion25'));
});
it('returns faq not in English', async () => {
@@ -29,20 +29,6 @@ describe('GET /faq', () => {
});
describe('platform parameter', () => {
it('returns faq with answers for ios platform only', async () => {
const res = await requester().get('/faq?platform=ios');
expect(res).to.have.property('stillNeedHelp');
expect(res.stillNeedHelp).to.eql({ ios: translate('iosFaqStillNeedHelp') });
expect(res).to.have.property('questions');
expect(res.questions[0]).to.eql({
exclusions: [],
heading: 'overview',
question: translate('faqQuestion0'),
ios: translate('iosFaqAnswer0'),
});
});
it('returns an error when invalid platform parameter is specified', async () => {
const request = requester().get('/faq?platform=wrong');
await expect(request)
@@ -60,9 +46,9 @@ describe('GET /faq', () => {
expect(res).to.have.property('questions');
expect(res.questions[0]).to.eql({
exclusions: [],
heading: 'overview',
question: translate('faqQuestion0'),
android: translate('androidFaqAnswer0'),
heading: 'task-types',
question: translate('faqQuestion25'),
web: translate('webFaqAnswer25'),
});
});
});

View File

@@ -21,7 +21,9 @@ describe('POST /user/reset', () => {
type: 'habit',
});
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@@ -39,7 +41,9 @@ describe('POST /user/reset', () => {
type: 'daily',
});
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@@ -57,7 +61,9 @@ describe('POST /user/reset', () => {
type: 'todo',
});
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@@ -75,7 +81,9 @@ describe('POST /user/reset', () => {
type: 'reward',
});
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({
@@ -87,6 +95,26 @@ describe('POST /user/reset', () => {
expect(user.tasksOrder.rewards).to.be.empty;
});
it('does not allow to reset if the password is missing', async () => {
await expect(user.post('/user/reset', {
password: '',
})).to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('missingPassword'),
});
});
it('does not allow to reset if the password is wrong', async () => {
await expect(user.post('/user/reset', {
password: 'passdw',
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('wrongPassword'),
});
});
it('does not delete challenge or group tasks', async () => {
const guild = await generateGroup(user, {}, { 'purchased.plan.customerId': 'group-unlimited' });
const challenge = await generateChallenge(user, guild);
@@ -102,7 +130,9 @@ describe('POST /user/reset', () => {
});
await user.post(`/tasks/${groupTask._id}/assign`, [user._id]);
await user.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
await user.sync();
await user.put('/user', {
@@ -133,7 +163,9 @@ describe('POST /user/reset', () => {
},
});
await hero.post('/user/reset');
await user.post('/user/reset', {
password: 'password',
});
const heroRes = await admin.get(`/hall/heroes/${hero.auth.local.username}`);

View File

@@ -665,6 +665,7 @@ describe('POST /user/auth/local/register', () => {
it('adds a user to a guild on an invite of type other than party', async () => {
const { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
upgradeToGroupPlan: true,
});
const invite = encrypt(JSON.stringify({

View File

@@ -126,13 +126,5 @@ describe('shared.ops.addTask', () => {
expect(addTask(user)._editing).not.be.ok;
expect(addTask(user)._edit).to.not.be.ok;
});
it('respects advancedCollapsed preference', () => {
user.preferences.advancedCollapsed = true;
expect(addTask(user)._advanced).not.be.ok;
user.preferences.advancedCollapsed = false;
expect(addTask(user)._advanced).to.be.ok;
});
});
});

View File

@@ -17,9 +17,9 @@ function getFullArmoire () {
_.each(content.gearTypes, type => {
_.each(content.gear.tree[type].armoire, gearObject => {
const armoireKey = gearObject.key;
fullArmoire[armoireKey] = true;
if (gearObject.released) {
fullArmoire[gearObject.key] = true;
}
});
});

View File

@@ -197,14 +197,14 @@ describe('shared.ops.purchase', () => {
it('purchases quest bundles', async () => {
const startingBalance = user.balance;
const clock = sandbox.useFakeTimers(moment('2019-05-20').valueOf());
const clock = sandbox.useFakeTimers(moment('2022-03-16').valueOf());
const type = 'bundles';
const key = 'featheredFriends';
const key = 'cuddleBuddies';
const price = 1.75;
const questList = [
'falcon',
'harpy',
'owl',
'bunny',
'ferret',
'guineapig',
];
await purchase(user, { params: { type, key } });

View File

@@ -15,12 +15,6 @@ describe('FAQ Locales', () => {
});
});
it('has a valid ios answers', () => {
each(questions, question => {
expectValidTranslationString(question.ios);
});
});
it('has a valid web answers', () => {
each(questions, question => {
expectValidTranslationString(question.web);
@@ -29,10 +23,6 @@ describe('FAQ Locales', () => {
});
describe('Still Need Help Message', () => {
it('has a valid ios message', () => {
expectValidTranslationString(stillNeedHelp.ios);
});
it('has a valid web message', () => {
expectValidTranslationString(stillNeedHelp.web);
});

View File

@@ -127,6 +127,9 @@ export async function createAndPopulateGroup (settings = {}) {
const upgradeToGroupPlan = settings.upgradeToGroupPlan || false;
const { groupDetails } = settings;
const leaderDetails = settings.leaderDetails || { balance: 10 };
if (upgradeToGroupPlan) {
leaderDetails.permissions = { fullAccess: true };
}
const groupLeader = await generateUser(leaderDetails);
const group = await generateGroup(groupLeader, groupDetails);

View File

@@ -120,6 +120,9 @@ export async function createAndPopulateGroup (settings = {}) {
const upgradeToGroupPlan = settings.upgradeToGroupPlan || false;
const { groupDetails } = settings;
const leaderDetails = settings.leaderDetails || { balance: 10 };
if (upgradeToGroupPlan) {
leaderDetails.permissions = { fullAccess: true };
}
const groupLeader = await generateUser(leaderDetails);
const group = await generateGroup(groupLeader, groupDetails);

View File

@@ -14,9 +14,8 @@ module.exports = {
// TODO find a way to let eslint understand webpack aliases
'import/no-unresolved': 'off',
'import/extensions': 'off',
'vue/component-tags-order': 'off',
'vue/no-mutating-props': 'off',
'vue/no-v-html': 'off',
'vue/no-mutating-props': 'warn',
'vue/html-self-closing': ['error', {
html: {
void: 'never',

View File

@@ -28,30 +28,12 @@ npm run lint
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).
## Storybook
### Vue Structure
Storybook is mainly used while working on UI-Components to see changes faster instead of using the website.
Currently pages and components are mixed in `/src/components` this is not a good way to find the files easy.
### Start Storybook
Thats why each changed / upcoming page / component should be put in either `/src/components` or in the `/src/pages` directory.
```
npm run storybook:serve
```
Inside Pages, each page can have a subfolder which contains sub-components only needed for that page - otherwise it has to be added to the normal components folder.
This will start the storybook process, every `*.stories.js`-File is searched and added to the storybook overview.
### Storybook Worklow
Usually when you working on `component-name.vue` you also create a `component-name.stories.js` file.
Example of the stories structure - [Storybook Docs][StorybookDocsExample] - [CountBadge][CountBadgeExample]
[StorybookDocsExample]: https://storybook.js.org/docs/guides/guide-vue/#step-4-write-your-stories
[CountBadgeExample]: src/components/ui/countBadge.stories.js
Each function or example of this component will be put after `storiesOf('Your Component', module)`,
in a separate `.add('function of component', ...`
### Storybook Build
After each client build, storybook build is also triggered and will be available in `dist/storybook`
At the end of all the changes - the components should only contain components needed between all pages

View File

@@ -1,8 +0,0 @@
# Storybook Updates
Addon-Knobs has been deprecated inorder to update to the new `addons-controls` we need to:
1. Change to the new export syntax for each story `export const StoryTemplate`
2. And use `StoryTemplate.args` and `StoryTemplate.argTypes` to have the same `knobs` as before
Maybe this can be made story by story and not all at once

View File

@@ -1,5 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
import '@storybook/addon-actions/register';
import '@storybook/addon-knobs/register';
import '@storybook/addon-links/register';
import '@storybook/addon-notes/register';

View File

@@ -1,65 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
import { configure } from '@storybook/vue';
import './margin.css';
import '../../src/assets/scss/index.scss';
import '../../src/assets/scss/sprites.scss';
import Vue from 'vue';
import BootstrapVue from 'bootstrap-vue';
import StoreModule from '@/libs/store';
import getStore from '@/store';
import '../../src/filters/registerGlobals';
import i18n from '../../../common/script/i18n';
// couldn't inject the languages easily,
// so just a "$t()" string to show that this will be translated
i18n.t = function translateString (...args) {
return `$t(${JSON.stringify(args)})`;
};
Vue.prototype.$t = i18n.t;
Vue.use(BootstrapVue);
Vue.use(StoreModule);
const store = getStore();
store.state.user.data = {
stats: {},
tags: [],
items: {
quests: {
moon1: 3,
},
},
party: {
quest: {
},
},
preferences: {
},
auth: {
local: {
// email: 'example@example.com',
},
facebook: {
emails: [
{
value: 'test@test.de',
},
],
},
},
};
Vue.prototype.$store = store;
const req = require.context('../../src', true, /.stories.js$/);
function loadStories () {
req.keys().forEach(filename => req(filename));
}
configure(loadStories, module);

View File

@@ -1,14 +0,0 @@
.background {
background: teal;
display: inline-block;
}
.inline-block {
display: inline-block;
}
.component-showcase {
position: absolute;
margin: 20px;
width: calc(100% - 40px);
}

View File

@@ -1,107 +0,0 @@
import { v4 as generateUUID } from 'uuid';
import getters from '@/store/getters';
export const userStyles = {
contributor: {
admin: true,
level: 9,
text: '',
},
items: {
gear: {
equipped: {
armor: 'armor_special_2',
head: 'head_special_2',
shield: 'shield_special_goldenknight',
headAccessory: 'headAccessory_base_0',
eyewear: 'eyewear_base_0',
weapon: 'weapon_special_1',
back: 'back_base_0',
},
costume: {
armor: 'armor_special_fallRogue',
head: 'head_special_fallRogue',
shield: 'shield_armoire_shieldOfDiamonds',
body: 'body_mystery_201706',
eyewear: 'eyewear_special_blackHalfMoon',
back: 'back_base_0',
headAccessory: 'headAccessory_special_wolfEars',
weapon: 'weapon_armoire_lamplighter',
},
},
},
preferences: {
hair: {
color: 'black', base: 0, bangs: 3, beard: 0, mustache: 0, flower: 0,
},
tasks: { groupByChallenge: false, confirmScoreNotes: false },
size: 'broad',
skin: 'wolf',
shirt: 'zombie',
chair: 'none',
sleep: true,
disableClasses: false,
background: 'midnight_castle',
costume: true,
},
stats: {
buffs: {
str: 0,
int: 0,
per: 0,
con: 0,
stealth: 0,
streaks: false,
snowball: false,
spookySparkles: false,
shinySeed: false,
seafoam: false,
},
training: {
int: 0, per: 0, str: 0, con: 0,
},
hp: 50,
mp: 158,
exp: 227,
gp: 464.31937261345155,
lvl: 17,
class: 'rogue',
points: 17,
str: 0,
con: 0,
int: 0,
per: 0,
toNextLevel: 380,
maxHealth: 50,
maxMP: 158,
},
profile: {
name: 'user',
},
_id: generateUUID(),
flags: {
classSelected: true,
},
};
export function mockStore ({
userData,
...state
}) {
return {
getters,
dispatch: () => {
},
watch: () => {
},
state: {
user: {
data: {
...userData,
},
},
...state,
},
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -8,17 +8,9 @@
"test:unit": "vue-cli-service test:unit --require ./tests/unit/helpers.js",
"lint": "vue-cli-service lint .",
"lint-no-fix": "vue-cli-service lint --no-fix .",
"postinstall": "node ./scripts/npm-postinstall.js",
"storybook:build": "vue-cli-service storybook:build -c config/storybook -o dist/storybook",
"storybook:serve": "vue-cli-service storybook:serve -p 6006 -c config/storybook"
"postinstall": "node ./scripts/npm-postinstall.js"
},
"dependencies": {
"@storybook/addon-actions": "6.5.8",
"@storybook/addon-knobs": "6.2.9",
"@storybook/addon-links": "6.5.8",
"@storybook/addon-notes": "5.3.21",
"@storybook/addons": "6.5.9",
"@storybook/vue": "6.5.14",
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^4.5.19",
"@vue/cli-plugin-router": "^5.0.8",
@@ -32,38 +24,39 @@
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1",
"chai": "^4.3.7",
"core-js": "^3.31.0",
"core-js": "^3.33.1",
"dompurify": "^3.0.3",
"eslint": "^6.8.0",
"eslint-config-habitrpg": "^6.2.0",
"eslint-plugin-mocha": "^5.3.0",
"eslint-plugin-vue": "^6.2.2",
"eslint-plugin-vue": "^7.20.0",
"habitica-markdown": "^3.0.0",
"hellojs": "^1.20.0",
"inspectpack": "^4.7.1",
"intro.js": "^7.0.1",
"jquery": "^3.7.0",
"intro.js": "^7.2.0",
"jquery": "^3.7.1",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nconf": "^0.12.0",
"nconf": "^0.12.1",
"sass": "^1.63.4",
"sass-loader": "^8.0.2",
"smartbanner.js": "^1.19.2",
"smartbanner.js": "^1.19.3",
"stopword": "^2.0.8",
"svg-inline-loader": "^0.8.2",
"svg-url-loader": "^7.1.1",
"svgo": "^1.3.2",
"svgo-loader": "^2.2.1",
"uuid": "^9.0.0",
"uuid": "^9.0.1",
"validator": "^13.9.0",
"vue": "^2.7.10",
"vue-cli-plugin-storybook": "2.1.0",
"vue-fragment": "^1.6.0",
"vue-mugen-scroll": "^0.2.6",
"vue-router": "^3.6.5",
"vue-template-compiler": "^2.7.10",
"vue-template-babel-compiler": "^2.0.0",
"vuedraggable": "^2.24.3",
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#153d339e4dbebb73733658aeda1d5b7fcc55b0a0",
"webpack": "^4.46.0"
"webpack": "^4.47.0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.21.0"

View File

@@ -6,8 +6,4 @@ if (process.env.NODE_ENV === 'production') {
execSync('npm run build', {
stdio: 'inherit',
});
/* execSync('npm run storybook:build', {
stdio: 'inherit',
}); */
}

View File

@@ -94,6 +94,12 @@
height: 90px;
}
.back_special_heroicAureole {
width: 114px;
height: 90px;
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_special_heroicAureole.gif") no-repeat;
}
.head_special_0 {
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
}
@@ -192,14 +198,6 @@
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat;
}
/* FIXME figure out how to handle customize menu!!
.customize-menu .f_head_0 {
width: 60px;
height: 60px;
background-position: -1917px -9px;
}
*/
[class*="Mount_Head_"],
[class*="Mount_Body_"] {
margin-top:18px; /* Sprite accommodates 105x123 box */

File diff suppressed because it is too large Load Diff

View File

@@ -5,15 +5,15 @@
font-weight: bold;
line-height: 1.71;
border: 1px solid transparent;
padding: 0.219rem 0.75rem;
border-radius: 2px;
padding: 4px 12px;
border-radius: 4px;
box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24);
color: $white;
&:hover, &:focus {
box-shadow: 0 3px 6px 0 rgba($black, 0.16), 0 3px 6px 0 rgba($black, 0.24);
&:disabled, &.disabled, &.btn-flat {
&.btn-flat {
box-shadow: none;
}
}
@@ -264,6 +264,10 @@
box-shadow: none;
}
.btn-cancel {
color: $blue-10;
}
.btn-small {
font-size: 12px;
line-height: 1.33;

View File

@@ -7,6 +7,10 @@
.dropdown-toggle:hover {
--caret-color: #{$purple-300};
&.disabled {
pointer-events: none;
}
}
.dropdown.show > .dropdown-toggle:not(.btn-success) {
@@ -136,6 +140,8 @@
.dropdown-menu.show {
min-width: 100% !important;
overflow: scroll;
max-height: 400px;
}
}

View File

@@ -26,11 +26,11 @@ input, textarea, input.form-control, textarea.form-control {
color: $gray-50;
border: 1px solid $gray-400;
&:hover:not(:disabled) {
&:hover:not(:disabled):not(:read-only) {
border-color: $gray-300;
}
&:active:not(:disabled), &:focus:not(:disabled) {
&:active:not(:disabled):not(:read-only), &:focus:not(:disabled):not(:read-only) {
border-color: $purple-400;
outline: 0;
box-shadow: none;
@@ -56,13 +56,13 @@ input, textarea, input.form-control, textarea.form-control {
&.input-valid, &.input-invalid {
background-repeat: no-repeat;
background-position: center right 16px;
background-position: center right 0.5rem;
}
&.input-valid {
padding-right: 37px;
padding-right: 27px;
background-image: url(~@/assets/svg/for-css/check.svg);
background-size: 13px 10px;
background-size: 1rem;
}
&.input-invalid {
@@ -91,8 +91,10 @@ input, textarea, input.form-control, textarea.form-control {
border-color: $gray-300;
}
&:focus, &:active, &:focus-within {
border: solid 1px $purple-400;
&:not(:read-only) {
&:focus, &:active, &:focus-within {
border: solid 1px $purple-400;
}
}
.input-group-prepend , .input-group-append {
@@ -163,8 +165,22 @@ input, textarea, input.form-control, textarea.form-control {
input {
height: 30px;
border: 0;
background: $white !important;
}
&.is-valid {
border-color: $green-10 !important;
}
&.is-invalid {
border-color: $red-100 !important;
}
}
.input-error {
font-size: 12px;
line-height: 1.33;
color: $maroon-10;
}
.input-group-spaced {
@@ -231,20 +247,20 @@ $bg-disabled-control: $gray-10;
background-color: inherit;
}
&:focus:not(:checked):not(:disabled)~.custom-control-label::before,
&:focus:not(:checked):not(:disabled)~.custom-control-label::before,
&:active:not(:checked):not(:disabled)~.custom-control-label::before {
border: 2px solid $gray-300;
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
}
&:focus:checked:not(:disabled)~.custom-control-label::before,
&:focus:checked:not(:disabled)~.custom-control-label::before,
&:active:checked:not(:disabled)~.custom-control-label::before {
box-shadow: 0 0 0 2px rgba(146, 92, 243, 0.5);
border-color: 2 px solid $purple-400;
background-color: $purple-400;
}
&:focus:disabled~.custom-control-label::before,
&:focus:disabled~.custom-control-label::before,
&:active:disabled~.custom-control-label::before {
box-shadow: 0 0 0 6px rgba($bg-disabled-control, 0.1);
}
@@ -398,8 +414,6 @@ $bg-color: $purple-400;
margin-top: 0 !important;
}
// Disable default style Firefox for invalid elements.
// Selectors taken from view-source:resource://gre-resources/forms.css on Firefox
:not(output):-moz-ui-invalid {

View File

@@ -1,55 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1000"
viewBox="0 0 1000 1187.198"
version="1.1"
height="1187.198"
id="svg2"
inkscape:version="0.91 r13725"
sodipodi:docname="Apple_1998.svg">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="705"
id="namedview6"
showgrid="false"
inkscape:zoom="0.1767767"
inkscape:cx="-1066.5045"
inkscape:cy="964.94669"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="m 979.04184,925.18785 c -17.95397,41.47737 -39.20563,79.65705 -63.82824,114.75895 -33.56298,47.8528 -61.04356,80.9761 -82.22194,99.3698 -32.83013,30.192 -68.00529,45.6544 -105.67203,46.5338 -27.04089,0 -59.6512,-7.6946 -97.61105,-23.3035 -38.08442,-15.5358 -73.08371,-23.2303 -105.08578,-23.2303 -33.56296,0 -69.55888,7.6945 -108.06101,23.2303 -38.5608,15.6089 -69.62484,23.7432 -93.37541,24.5493 -36.12049,1.5389 -72.1237,-14.3632 -108.06101,-47.7796 -22.93711,-20.0059 -51.62684,-54.3017 -85.99592,-102.8874 C 92.254176,984.54592 61.937588,924.38175 38.187028,855.7902 12.750995,781.70252 0,709.95986 0,640.50361 0,560.94181 17.191859,492.32094 51.626869,434.81688 78.689754,388.62753 114.69299,352.19192 159.75381,325.44413 c 45.06086,-26.74775 93.74914,-40.37812 146.18212,-41.25019 28.68971,0 66.3125,8.8744 113.06613,26.31542 46.62174,17.49964 76.55727,26.37404 89.68198,26.37404 9.8124,0 43.06758,-10.37669 99.4431,-31.06405 53.31237,-19.18512 98.30724,-27.12887 135.16787,-23.99975 99.8828,8.06098 174.92313,47.43518 224.82789,118.37174 -89.33023,54.12578 -133.51903,129.93556 -132.63966,227.18753 0.8061,75.75115 28.28668,138.78795 82.2952,188.8393 24.47603,23.23022 51.81008,41.18421 82.22186,53.93522 -6.59525,19.12648 -13.557,37.44688 -20.95846,55.03446 z M 749.96366,23.751237 c 0,59.37343 -21.69138,114.810233 -64.92748,166.121963 -52.17652,60.99961 -115.28658,96.24803 -183.72426,90.68597 -0.87204,-7.12298 -1.37769,-14.61967 -1.37769,-22.49743 0,-56.99843 24.81315,-117.99801 68.87738,-167.873453 21.99909,-25.25281 49.978,-46.25018 83.90738,-63.00018 C 686.57507,10.688027 718.59913,1.5631274 748.71783,5.2734376e-4 749.59727,7.9378274 749.96366,15.875627 749.96366,23.750467 Z"
id="path4"
inkscape:connector-curvature="0" />
<svg width="13" height="16" viewBox="0 0 13 16" xmlns="http://www.w3.org/2000/svg">
<path d="M8.841 2.564c-.567.672-1.474 1.202-2.382 1.126-.113-.908.331-1.873.851-2.47C7.877.53 8.87.039 9.673 0c.095.946-.274 1.873-.832 2.564zm.823 1.306c-1.314-.076-2.439.747-3.063.747-.633 0-1.588-.71-2.627-.69-1.352.018-2.609.785-3.299 2.005-1.418 2.441-.369 6.055 1.002 8.042.67.984 1.474 2.063 2.533 2.025 1.002-.038 1.399-.653 2.609-.653 1.219 0 1.569.653 2.627.634 1.097-.019 1.787-.984 2.458-1.968.765-1.116 1.077-2.204 1.096-2.261-.019-.019-2.117-.823-2.136-3.245-.019-2.025 1.654-2.99 1.73-3.047-.946-1.4-2.42-1.551-2.93-1.59z" fill="#1A181D" fill-rule="nonzero"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 670 B

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#nw2v1izcda)">
<path d="m8 4.01 2.2 5.4 3.8-3.4-2 6H4l-2-6 3.8 3.4L8 4.01zm0-2c-.81 0-1.55.49-1.85 1.25L5.02 6.03 3.33 4.52A1.98 1.98 0 0 0 2 4.01 2.002 2.002 0 0 0 .1 6.64l2 6A2 2 0 0 0 4 14.01h8a2 2 0 0 0 1.9-1.37l1.97-5.9a1.997 1.997 0 0 0-1.85-2.73h-.04c-.5.01-.95.2-1.3.51L11 6.02 9.87 3.25A2.012 2.012 0 0 0 8.02 2L8 2.01z" fill="#4E4A57"/>
</g>
<defs>
<clipPath id="nw2v1izcda">
<path fill="#fff" d="M0 0h16v16H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 617 B

View File

@@ -1,3 +1,18 @@
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="10" viewBox="0 0 13 10">
<path fill="#24CC8F" fill-rule="evenodd" d="M4.662 9.832c-.312 0-.61-.123-.831-.344L0 5.657l1.662-1.662 2.934 2.934L10.534 0l1.785 1.529-6.764 7.893a1.182 1.182 0 0 1-.848.409l-.045.001"/>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<defs>
<path id="vm46q29nca" d="M6.662 12.832c-.312 0-.61-.123-.831-.344L2 8.657l1.662-1.662 2.934 2.934L12.534 3l1.785 1.529-6.764 7.893c-.214.248-.521.396-.848.409l-.045.001"/>
</defs>
<g fill="none" fill-rule="evenodd">
<g>
<g transform="translate(-306 -8) translate(306 8)">
<mask id="c8uzbxs4ob" fill="#fff">
<use xlink:href="#vm46q29nca"/>
</mask>
<use fill="#878190" xlink:href="#vm46q29nca"/>
<g fill="#20B780" mask="url(#c8uzbxs4ob)">
<path d="M0 0H16V16H0z"/>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 284 B

After

Width:  |  Height:  |  Size: 808 B

View File

@@ -1 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="17"><defs><path id="a" d="M10 13v1H6v-1h4zm0-2v1H6v-1h4zM8 2l5 6h-3v2H6V8H3l5-6z"/></defs><g transform="rotate(-90 8 8)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><use fill="#BDA8FF" xlink:href="#a"/><g fill="#878190" mask="url(#b)"><path d="M0 0h16v16H0z"/></g></g></svg>
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" >
<path d="M10 13v1H6v-1h4zm0-2v1H6v-1h4zM8 2l5 6h-3v2H6V8H3l5-6z" id="myc95n2o6a"/>
</svg>

Before

Width:  |  Height:  |  Size: 419 B

After

Width:  |  Height:  |  Size: 183 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
<g fill="none" fill-rule="evenodd" opacity=".75" transform="translate(3 2)">
<path fill="#878190" d="M4 9h2V7H4v2zm4 1H2V6h6v4zM5 2c1.103 0 2 .897 2 2H3c0-1.103.897-2 2-2zm4 2.277V4c0-2.209-1.791-4-4-4S1 1.791 1 4v.277C.405 4.624 0 5.262 0 6v4c0 1.105.895 2 2 2h6c1.105 0 2-.895 2-2V6c0-.738-.405-1.376-1-1.723z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 470 B

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#464rik5fna)">
<path d="M12 2c1.1 0 2 .9 2 2v6c0 1.1-.9 2-2 2l-4 2v-2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8zm0-2H4C1.79 0 0 1.79 0 4v6c0 2.21 1.79 4 4 4h2c0 .69.36 1.34.95 1.7a1.993 1.993 0 0 0 1.94.09l3.65-1.83A4 4 0 0 0 15.99 10V4c0-2.21-1.79-4-4-4H12zM9.41 7l1.29-1.29A.996.996 0 1 0 9.29 4.3L8 5.59 6.71 4.3A.996.996 0 1 0 5.3 5.71L6.59 7 5.3 8.29a.996.996 0 0 0 .71 1.7c.26 0 .51-.1.71-.29l1.29-1.29L9.3 9.7c.2.2.45.29.71.29.26 0 .51-.1.71-.29a.996.996 0 0 0 0-1.41L9.43 7h-.02z" fill="#4E4A57"/>
</g>
<defs>
<clipPath id="464rik5fna">
<path fill="#fff" d="M0 0h16v16H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 770 B

View File

@@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#mzh0y8jf2a)">
<path d="M12 2c1.1 0 2 .9 2 2v6c0 1.1-.9 2-2 2l-4 2v-2H4c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h8zm0-2H4C1.79 0 0 1.79 0 4v6c0 2.21 1.79 4 4 4h2c0 .69.36 1.34.95 1.7a1.993 1.993 0 0 0 1.94.09l3.65-1.83A4 4 0 0 0 15.99 10V4c0-2.21-1.79-4-4-4H12zm-.17 7c0-.55-.45-1-1-1H5.17c-.55 0-1 .45-1 1s.45 1 1 1h5.66c.55 0 1-.45 1-1z" fill="#4E4A57"/>
</g>
<defs>
<clipPath id="mzh0y8jf2a">
<path fill="#fff" d="M0 0h16v16H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 618 B

View File

@@ -1,26 +1,35 @@
<template>
<div class="row">
<div class="col-12 text-center">
<div class="col-6 text-center mx-auto mb-5">
<!-- @TODO i18n. How to setup the strings with the router-link inside?-->
<img
class="not-found-img"
:class="retiredChatPage ? 'mt-5' : 'image-404'"
src="~@/assets/images/404.png"
>
<h1 class="not-found">
Sometimes even the bravest adventurer gets lost.
</h1>
<h2 class="not-found">
Looks like this link is broken or the page may have moved, sorry!
</h2>
<h2 class="not-found">
Head back to the
<router-link to="/">
Homepage
</router-link>or
<router-link :to="contactUsLink">
Contact Us
</router-link>about the issue.
</h2>
<div v-if="retiredChatPage">
<h1>
{{ $t('tavernDiscontinued') }}
</h1>
<p>{{ $t('tavernDiscontinuedDetail') }}</p>
<p v-html="$t('tavernDiscontinuedLinks')"></p>
</div>
<div v-else>
<h1>
Sometimes even the bravest adventurer gets lost.
</h1>
<p class="mb-0">
Looks like this link is broken or the page may have moved, sorry!
</p>
<p>
Head back to the
<router-link to="/">
Homepage
</router-link>or
<router-link :to="contactUsLink">
Contact Us
</router-link>about the issue.
</p>
</div>
</div>
</div>
</template>
@@ -37,6 +46,9 @@ export default {
}
return { name: 'contact' };
},
retiredChatPage () {
return this.$route.fullPath.indexOf('/groups') !== -1;
},
},
};
</script>
@@ -44,28 +56,20 @@ export default {
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
.col-12 {
margin-bottom: 120px;
}
.not-found-img {
margin-top: 152px;
margin-bottom: 42px;
}
h1.not-found {
line-height: 1.33;
h1, .static-wrapper h1 {
color: $purple-200;
margin-bottom: 8px;
font-weight: normal;
margin-top: 0px;
line-height: 1.33;
margin-top: 3rem;
margin-bottom: 1rem;
}
h2.not-found {
line-height: 1.4;
font-weight: normal;
color: $gray-200;
margin-bottom: 0px;
margin-top: 0px;
p {
font-size: 16px;
line-height: 1.75;
}
.image-404 {
margin-top: 104px;
}
</style>

View File

@@ -91,6 +91,7 @@ export default {
this.close();
},
hide () {
this.$store.dispatch('user:statSync');
this.$store.dispatch('user:set', { 'party.quest.completed': '' });
},
},

View File

@@ -1,60 +0,0 @@
<template>
<b-modal
id="testing"
:title="$t('guildReminderTitle')"
size="lg"
:hide-footer="true"
>
<div class="modal-body text-center">
<br>
<div class="scene_guilds"></div>
<br>
<h4>{{ $t('guildReminderText1') }}</h4>
</div>
<div class="modal-footer">
<div class="container-fluid">
<div class="row">
<div class="col-6 text-center">
<button
class="btn btn-secondary"
@click="close()"
>
{{ $t('guildReminderDismiss') }}
</button>
</div>
<div
class="col-6 text-center"
@click="close()"
>
<div
class="btn btn-primary"
@click="takeMethere()"
>
{{ $t('guildReminderCTA') }}
</div>
</div>
</div>
</div>
</div>
</b-modal>
</template>
<style scoped>
.scene_guilds {
margin: 0 auto;
}
</style>
<script>
export default {
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'testing');
},
takeMethere () {
this.$router.push('/groups/discovery');
this.close();
},
},
};
</script>

View File

@@ -1,58 +0,0 @@
<template>
<b-modal
id="testingletiant"
:title="$t('guildReminderTitle')"
size="lg"
:hide-footer="true"
>
<div class="modal-content"></div>
<div class="modal-body text-center">
<br>
<div class="scene_guilds"></div>
<br>
<h4>{{ $t('guildReminderText2') }}</h4>
</div>
<div class="modal-footer">
<div class="container-fluid">
<div class="row">
<div class="col-6 text-center">
<button
class="btn btn-secondary"
@click="close()"
>
{{ $t('guildReminderDismiss') }}
</button>
</div>
<div class="col-6 text-center">
<div
class="btn btn-primary"
@click="takeMethere()"
>
{{ $t('guildReminderCTA') }}
</div>
</div>
</div>
</div>
</div>
</b-modal>
</template>
<style scoped>
.scene_guilds {
margin: 0 auto;
}
</style>
<script>
export default {
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'testingletiant');
},
takeMethere () {
this.$router.push('/groups/discovery');
this.close();
},
},
};
</script>

View File

@@ -119,6 +119,10 @@ export default {
SubscriptionAndPerks,
},
mixins: [userStateMixin],
beforeRouteUpdate (to, from, next) {
this.userIdentifier = to.params.userIdentifier;
next();
},
data () {
return {
userIdentifier: '',
@@ -184,9 +188,5 @@ export default {
this.resetCounter += 1; // tell child components to reinstantiate from scratch
},
},
beforeRouteUpdate (to, from, next) {
this.userIdentifier = to.params.userIdentifier;
next();
},
};
</script>

View File

@@ -57,13 +57,13 @@
<div class="form-inline">
Perk month count:
<input
v-model="hero.purchased.plan.perkMonthCount"
class="form-control"
type="number"
min="0"
max="2"
step="1"
>
v-model="hero.purchased.plan.perkMonthCount"
class="form-control"
type="number"
min="0"
max="2"
step="1"
>
</div>
<div>
Next Mystic Hourglass:

View File

@@ -731,6 +731,8 @@ export default {
},
},
mounted () {
this.forgotPassword = this.$route.path.startsWith('/forgot-password');
hello.init({
google: process.env.GOOGLE_CLIENT_ID, // eslint-disable-line
});

View File

@@ -1,5 +1,6 @@
<template>
<div
v-if="member.preferences"
class="avatar"
:style="{width, height, paddingTop}"
:class="backgroundClass"
@@ -135,10 +136,10 @@ import foolPet from '../mixins/foolPet';
import ClassBadge from '@/components/members/classBadge';
export default {
mixins: [foolPet],
components: {
ClassBadge,
},
mixins: [foolPet],
props: {
member: {
type: Object,
@@ -184,9 +185,11 @@ export default {
currentEventList: 'worldState.data.currentEventList',
}),
hasClass () {
if (!this.member) return false;
return this.$store.getters['members:hasClass'](this.member);
},
isBuffed () {
if (!this.member) return false;
return this.$store.getters['members:isBuffed'](this.member);
},
paddingTop () {
@@ -197,28 +200,30 @@ export default {
let val = '24px';
if (!this.avatarOnly) {
if (this.member.items.currentPet) val = '24px';
if (this.member.items.currentMount) val = '0px';
if (this.member?.items.currentPet) val = '24px';
if (this.member?.items.currentMount) val = '0px';
}
return val;
},
backgroundClass () {
const { background } = this.member.preferences;
if (this.member) {
const { background } = this.member.preferences;
const allowToShowBackground = !this.avatarOnly || this.withBackground;
const allowToShowBackground = !this.avatarOnly || this.withBackground;
if (this.overrideAvatarGear && this.overrideAvatarGear.background) {
return `background_${this.overrideAvatarGear.background}`;
if (this.overrideAvatarGear && this.overrideAvatarGear.background) {
return `background_${this.overrideAvatarGear.background}`;
}
if (background && allowToShowBackground) {
return `background_${this.member.preferences.background}`;
}
}
if (background && allowToShowBackground) {
return `background_${this.member.preferences.background}`;
}
return '';
},
visualBuffs () {
if (!this.member) return {};
return {
snowball: `avatar_snowball_${this.member.stats.class}`,
spookySparkles: 'ghost',
@@ -227,15 +232,16 @@ export default {
};
},
skinClass () {
if (!this.member) return '';
const baseClass = `skin_${this.member.preferences.skin}`;
return `${baseClass}${this.member.preferences.sleep ? '_sleep' : ''}`;
},
costumeClass () {
return this.member.preferences.costume ? 'costume' : 'equipped';
return this.member?.preferences.costume ? 'costume' : 'equipped';
},
specialMountClass () {
if (!this.avatarOnly && this.member.items.currentMount && this.member.items.currentMount.includes('Kangaroo')) {
if (!this.avatarOnly && this.member?.items.currentMount && this.member?.items.currentMount.includes('Kangaroo')) {
return 'offset-kangaroo';
}
@@ -248,12 +254,13 @@ export default {
)) {
return this.foolPet(this.member.items.currentPet);
}
if (this.member.items.currentPet) return `Pet-${this.member.items.currentPet}`;
if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`;
return '';
},
},
methods: {
getGearClass (gearType) {
if (!this.member) return '';
let result = this.member.items.gear[this.costumeClass][gearType];
if (this.overrideAvatarGear && this.overrideAvatarGear[gearType]) {
@@ -263,6 +270,7 @@ export default {
return result;
},
hideGear (gearType) {
if (!this.member) return true;
if (gearType === 'weapon') {
const equippedWeapon = this.member.items.gear[this.costumeClass][gearType];
@@ -288,6 +296,7 @@ export default {
this.$root.$emit('castEnd', this.member, 'user', e);
},
showAvatar () {
if (!this.member) return false;
if (!this.showVisualBuffs) return true;
const { buffs } = this.member.stats;

View File

@@ -1,43 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
import { storiesOf } from '@storybook/vue';
import { withKnobs } from '@storybook/addon-knobs';
import bugReportModal from '@/components/bugReportModal';
import bugReportSuccessModal from '@/components/bugReportSuccessModal';
const stories = storiesOf('Bug Report Modal', module);
stories.addDecorator(withKnobs);
stories
.add('bugReportModal', () => ({
components: { bugReportModal },
data () {
return {
};
},
template: `
<div>
<bug-report-modal></bug-report-modal>
</div>
`,
mounted () {
this.$root.$emit('bv::show::modal', 'bug-report-modal');
},
}))
.add('bugReportSuccessModal', () => ({
components: { bugReportSuccessModal },
data () {
return {
};
},
template: `
<div>
<bug-report-success-modal></bug-report-success-modal>
</div>
`,
mounted () {
this.$root.$emit('bv::show::modal', 'bug-report-success-modal');
},
}));

View File

@@ -8,23 +8,15 @@
slot="modal-header"
class="bug-report-modal-header"
>
<h2 v-once>
{{ $t('reportBug') }}
<h2>
{{ question ? $t('askQuestion') : $t('reportBug') }}
</h2>
<div
v-once
class="report-bug-header-describe"
>
{{ $t('reportBugHeaderDescribe') }}
</div>
<div class="dialog-close">
<close-icon
:purple="true"
@click="close()"
/>
<div class="report-bug-header-describe">
{{ question ? $t('askQuestionHeaderDescribe') : $t('reportBugHeaderDescribe') }}
</div>
<close-x
@close="close()"
/>
</div>
<div>
<form
@@ -40,11 +32,8 @@
>
{{ $t('email') }}
</label>
<div
v-once
class="mb-2 description-label"
>
{{ $t('reportEmailText') }}
<div class="mb-2 description-label">
{{ question ? $t('questionEmailText') : $t('reportEmailText') }}
</div>
<input
id="emailInput"
@@ -64,22 +53,19 @@
</div>
</div>
<label v-once>
{{ $t('reportDescription') }}
<label>
{{ question ? $t('question') : $t('reportDescription') }}
</label>
<div
v-once
class="mb-2 description-label"
>
{{ $t('reportDescriptionText') }}
<div class="mb-2 description-label">
{{ question ? $t('questionDescriptionText') : $t('reportDescriptionText') }}
</div>
<textarea
v-model="message"
class="form-control"
rows="5"
:required="true"
:placeholder="$t('reportDescriptionPlaceholder')"
:class="{'input-invalid': messageInvalid && this.message.length === 0}"
:placeholder="question ? $t('questionPlaceholder') : $t('reportDescriptionPlaceholder')"
:class="{'input-invalid': messageInvalid && message.length === 0}"
>
</textarea>
@@ -89,7 +75,7 @@
type="submit"
:disabled="!message || !emailValid"
>
{{ $t('submitBugReport') }}
{{ question ? $t('submitQuestion') : $t('submitBugReport') }}
</button>
</form>
</div>
@@ -141,7 +127,7 @@ h2 {
.bug-report-modal-header {
color: $white;
width: 100%;
padding: 2rem 3rem 1.5rem 1.5rem;
padding: 1.5rem 3rem 1.5rem 1.5rem;
background-image: linear-gradient(288deg, #{$purple-200}, #{$purple-300});
}
@@ -182,13 +168,13 @@ label {
<script>
import axios from 'axios';
import isEmail from 'validator/lib/isEmail';
import closeIcon from '@/components/shared/closeIcon';
import closeX from '@/components/ui/closeX';
import { mapState } from '@/libs/store';
import { MODALS } from '@/libs/consts';
export default {
components: {
closeIcon,
closeX,
},
data () {
return {
@@ -211,6 +197,7 @@ export default {
await axios.post('/api/v4/bug-report', {
message: this.message,
email: this.email,
question: this.question,
});
this.message = '';
@@ -233,6 +220,9 @@ export default {
if (this.email.length <= 3) return false;
return !this.emailValid;
},
question () {
return this.$store.state.bugReportOptions.question;
},
},
mounted () {
const { user } = this;

View File

@@ -1,5 +1,6 @@
<template>
<div class="row">
<report-challenge-modal />
<challenge-modal @updatedChallenge="updatedChallenge" />
<leave-challenge-modal
:challenge-id="challenge._id"
@@ -9,11 +10,27 @@
:members="members"
:challenge-id="challenge._id"
:prize="challenge.prize"
:flag-count="challenge.flagCount"
/>
<challenge-member-progress-modal :challenge-id="challenge._id" />
<div class="col-12 col-md-8 standard-page">
<div class="row">
<div class="col-12 col-md-6">
<div
v-if="canViewFlags"
class="flagged"
>
<div
v-if="flaggedNotHidden"
>
{{ $t("flaggedNotHidden") }}
</div>
<div
v-else-if="flaggedAndHidden"
>
{{ $t("flaggedAndHidden") }}
</div>
</div>
<h1 v-markdown="challenge.name"></h1>
<div>
<span class="mr-1 ml-0 d-block">
@@ -41,7 +58,7 @@
createdBy string (helps with RTL languages)-->
<!-- @TODO: Implement in V2 strong.margin-left
(v-once).svg-icon.calendar-icon(v-html="icons.calendarIcon")
| {{$t('endDate')}}
{{$t('endDate')}}
// "endDate": "End Date: <% endDate %>",-->
<!-- span {{challenge.endDate}}-->
</div>
@@ -169,13 +186,16 @@
v-if="isLeader || isAdmin"
class="button-container"
>
<button
v-once
class="btn btn-primary"
@click="cloneChallenge()"
>
{{ $t('clone') }}
</button>
<div>
<button
class="btn"
:disabled="flaggedAndHidden"
:class="flaggedAndHidden ? 'disabled btn-disabled' : 'btn-primary'"
@click="cloneChallenge()"
>
{{ $t('clone') }}
</button>
</div>
</div>
<div
v-if="isLeader || isAdmin"
@@ -201,6 +221,17 @@
{{ $t('endChallenge') }}
</button>
</div>
<div
class="button-container"
>
<button
v-once
class="btn btn-danger"
@click="reportChallenge()"
>
{{ $t('report') }}
</button>
</div>
<div>
<sidebar-section :title="$t('challengeSummary')">
<p v-markdown="challenge.summary"></p>
@@ -249,6 +280,17 @@
}
}
.btn-disabled {
background-color: $gray-700;
color: $gray-50;
box-shadow: none;
cursor: arrow;
&:hover {
box-shadow: none;
}
}
.calendar-icon {
width: 12px;
display: inline-block;
@@ -312,6 +354,15 @@
margin-right: .5em;
}
}
.flagged {
margin-left: 0em;
color: $red-10;
span {
margin-left: 0em;
}
}
</style>
<script>
@@ -332,6 +383,7 @@ import challengeModal from './challengeModal';
import challengeMemberProgressModal from './challengeMemberProgressModal';
import challengeMemberSearchMixin from '@/mixins/challengeMemberSearch';
import leaveChallengeModal from './leaveChallengeModal';
import reportChallengeModal from './reportChallengeModal';
import sidebarSection from '../sidebarSection';
import userLink from '../userLink';
import groupLink from '../groupLink';
@@ -350,6 +402,7 @@ export default {
components: {
closeChallengeModal,
leaveChallengeModal,
reportChallengeModal,
challengeModal,
challengeMemberProgressModal,
memberSearchDropdown,
@@ -360,6 +413,11 @@ export default {
groupLink,
},
mixins: [challengeMemberSearchMixin, externalLinks, userStateMixin],
async beforeRouteUpdate (to, from, next) {
this.searchId = to.params.challengeId;
await this.loadChallenge();
next();
},
props: ['challengeId'],
data () {
return {
@@ -401,6 +459,20 @@ export default {
canJoin () {
return !this.isMember;
},
// canViewFlags should allow only moderators/admins to see flags
canViewFlags () {
const isModerator = this.hasPermission(this.user, 'moderator');
if (isModerator && this.challenge.flagCount > 0) return true;
return false;
},
// flaggedNotHidden should allow mods/admins & challenge owner to see flags
flaggedNotHidden () {
return this.challenge.flagCount === 1;
},
// flaggedAndHidden should only allow admin to see challenge & flags
flaggedAndHidden () {
return this.challenge.flagCount > 1;
},
},
watch: {
'challenge.name': {
@@ -420,11 +492,6 @@ export default {
updated () {
this.handleExternalLinks();
},
async beforeRouteUpdate (to, from, next) {
this.searchId = to.params.challengeId;
await this.loadChallenge();
next();
},
methods: {
cleanUpTask (task) {
const cleansedTask = omit(task, TASK_KEYS_TO_REMOVE);
@@ -589,6 +656,14 @@ export default {
challenge: this.challenge,
});
},
reportChallenge () {
this.$root.$emit('habitica::report-challenge', {
challenge: this.challenge,
});
},
async showCannotCloneModal () {
this.$root.$emit('bv::show::modal', 'cannot-clone-modal');
},
},
};
</script>

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