Compare commits

...

548 Commits

Author SHA1 Message Date
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
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
f8bcc81fe6 4.277.2 2023-08-01 13:02:00 -05:00
SabreCat
cc18acd69a Merge branch 'sabrecat/chat-warning' into release 2023-08-01 13:01:15 -05:00
SabreCat
c5a2e5a2e0 fix(chat): fill in the blank 2023-07-31 19:25:07 -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
8f92993045 4.277.1 2023-07-31 12:10:32 -05:00
SabreCat
7697d87358 Merge branch 'develop' into sabrecat/unsociable 2023-07-31 12:08:58 -05:00
SabreCat
712205d253 Merge branch 'develop' into sabrecat/chat-warning 2023-07-31 12:07:48 -05:00
Sabe Jones
ede036e94b Universal routing for migrations (#14772)
* refactor(notifs): universal routing for migrations

* fix(lint): remove whitespace

* chore(content): update migration for new scheme

* fix(migration): account for cake

* chore(images): update sprite CSS

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-07-31 12:07:46 -05:00
SabreCat
67c16c137b Merge branch 'release' into develop 2023-07-31 12:07:26 -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
CuriousMagpie
652dfa6ecc feat(content): upgrade profile page 2023-07-26 16:33:05 -04: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
5c4aa664b5 4.277.0 2023-07-25 14:17:04 -05:00
SabreCat
184a9df775 Merge branch 'develop' into release 2023-07-25 14:16:54 -05:00
Phillip Thelen
754d46f1f3 Optimise looking for party request (#14773)
* Reset looking for party state if a user joins a party

* filter out users that already received an invite in query

* fix(lfp): add back in-party filter
Temporary until migration to clear seeking flag from users in party

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-07-25 14:16:03 -05:00
Sabe Jones
683649ff1a Don't collapse different spell targets in party chat (#14775)
* fix(spells): don't collapse different targets

* fix(lint): indentation

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-07-25 14:15:12 -05:00
Natalie L
08ac059a7f feat(content): add August 2023 subscriber items (#14784)
* feat(content): add June subscriber items

* feat(content): add August subscriber items
2023-07-25 14:14:28 -05:00
Weblate
7c9b0f207c Merge branch 'origin/develop' into Weblate. 2023-07-25 21:11:35 +02:00
Weblate
f193b8de2c Translated using Weblate (English (en@lolcat))
Currently translated at 5.5% (44 of 798 strings)

Translated using Weblate (English (en@lolcat))

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Russian)

Currently translated at 98.8% (2837 of 2871 strings)

Translated using Weblate (Ukrainian)

Currently translated at 90.5% (692 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 91.3% (729 of 798 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Japanese)

Currently translated at 99.6% (2862 of 2871 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (798 of 798 strings)

Translated using Weblate (English (en@lolcat))

Currently translated at 3.6% (29 of 798 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.6% (2197 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 85.2% (651 of 764 strings)

Translated using Weblate (Japanese)

Currently translated at 99.4% (187 of 188 strings)

Translated using Weblate (Japanese)

Currently translated at 99.2% (792 of 798 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.2% (2187 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 29.0% (833 of 2871 strings)

Translated using Weblate (Russian)

Currently translated at 98.5% (2830 of 2871 strings)

Translated using Weblate (Russian)

Currently translated at 98.5% (2830 of 2871 strings)

Translated using Weblate (Ukrainian)

Currently translated at 84.4% (645 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 84.2% (644 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 75.9% (2177 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (188 of 188 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (798 of 798 strings)

Translated using Weblate (French)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (French)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Ukrainian)

Currently translated at 83.6% (639 of 764 strings)

Translated using Weblate (French)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (French)

Currently translated at 100.0% (791 of 791 strings)

Translated using Weblate (Indonesian)

Currently translated at 75.5% (2167 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 83.6% (639 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 75.2% (2157 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 82.9% (634 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 29.0% (833 of 2871 strings)

Translated using Weblate (Indonesian)

Currently translated at 74.8% (2147 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 81.4% (622 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Ukrainian)

Currently translated at 78.1% (597 of 764 strings)

Translated using Weblate (Croatian)

Currently translated at 77.0% (94 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 74.5% (2137 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 77.7% (594 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 74.1% (2127 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 77.4% (592 of 764 strings)

Translated using Weblate (Vietnamese)

Currently translated at 97.3% (148 of 152 strings)

Translated using Weblate (Dutch)

Currently translated at 86.6% (2484 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 73.8% (2117 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 76.8% (587 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Ukrainian)

Currently translated at 29.0% (833 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 76.7% (586 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Indonesian)

Currently translated at 73.4% (2107 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 76.7% (586 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 72.7% (2087 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 72.2% (2071 of 2867 strings)

Translated using Weblate (Spanish)

Currently translated at 51.8% (55 of 106 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 46.2% (49 of 106 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 74.5% (91 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 71.9% (2064 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 76.7% (586 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 71.6% (2054 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 97.1% (103 of 106 strings)

Translated using Weblate (Ukrainian)

Currently translated at 76.7% (586 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 28.6% (822 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 28.6% (822 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (222 of 222 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Ukrainian)

Currently translated at 28.5% (819 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 28.5% (819 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 28.5% (819 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 28.5% (819 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 71.2% (2043 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 81.1% (86 of 106 strings)

Translated using Weblate (Ukrainian)

Currently translated at 81.1% (86 of 106 strings)

Translated using Weblate (Ukrainian)

Currently translated at 81.1% (86 of 106 strings)

Translated using Weblate (Ukrainian)

Currently translated at 76.7% (586 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 76.7% (586 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 75.0% (573 of 764 strings)

Translated using Weblate (Dutch)

Currently translated at 86.6% (2483 of 2867 strings)

Translated using Weblate (Dutch)

Currently translated at 99.1% (121 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 70.9% (2033 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 70.5% (2024 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 70.1% (2011 of 2867 strings)

Translated using Weblate (Russian)

Currently translated at 99.7% (789 of 791 strings)

Translated using Weblate (Indonesian)

Currently translated at 69.2% (1984 of 2867 strings)

Translated using Weblate (Korean)

Currently translated at 73.8% (584 of 791 strings)

Translated using Weblate (Croatian)

Currently translated at 75.4% (92 of 122 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (222 of 222 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 68.8% (1974 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 68.6% (1968 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 68.6% (1968 of 2867 strings)

Translated using Weblate (Latvian)

Currently translated at 23.5% (25 of 106 strings)

Translated using Weblate (Latvian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Indonesian)

Currently translated at 68.3% (1959 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 68.3% (1959 of 2867 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Indonesian)

Currently translated at 68.3% (1959 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 68.3% (1959 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 68.0% (1950 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 67.1% (1926 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 67.1% (1924 of 2867 strings)

Translated using Weblate (Dutch)

Currently translated at 68.8% (73 of 106 strings)

Translated using Weblate (Latvian)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Japanese)

Currently translated at 97.6% (409 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.9% (1919 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Translated using Weblate (Latvian)

Currently translated at 70.3% (107 of 152 strings)

Translated using Weblate (Japanese)

Currently translated at 99.7% (2860 of 2867 strings)

Translated using Weblate (Japanese)

Currently translated at 99.5% (221 of 222 strings)

Translated using Weblate (Japanese)

Currently translated at 99.7% (2860 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Translated using Weblate (Turkish)

Currently translated at 62.9% (498 of 791 strings)

Translated using Weblate (Belarusian)

Currently translated at 23.0% (35 of 152 strings)

Translated using Weblate (Polish)

Currently translated at 98.6% (150 of 152 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Turkish)

Currently translated at 96.8% (91 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 28.4% (816 of 2867 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Translated using Weblate (Belarusian)

Currently translated at 17.1% (26 of 152 strings)

Translated using Weblate (Belarusian)

Currently translated at 11.1% (17 of 152 strings)

Translated using Weblate (Dutch)

Currently translated at 86.5% (2480 of 2867 strings)

Translated using Weblate (Dutch)

Currently translated at 65.0% (69 of 106 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Korean)

Currently translated at 73.1% (579 of 791 strings)

Translated using Weblate (Croatian)

Currently translated at 91.7% (167 of 182 strings)

Translated using Weblate (Croatian)

Currently translated at 53.6% (119 of 222 strings)

Translated using Weblate (Croatian)

Currently translated at 80.7% (617 of 764 strings)

Translated using Weblate (Croatian)

Currently translated at 67.0% (63 of 94 strings)

Translated using Weblate (Croatian)

Currently translated at 88.9% (194 of 218 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Croatian)

Currently translated at 81.9% (308 of 376 strings)

Translated using Weblate (Croatian)

Currently translated at 63.1% (77 of 122 strings)

Translated using Weblate (Croatian)

Currently translated at 71.8% (158 of 220 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 79.2% (84 of 106 strings)

Translated using Weblate (Ukrainian)

Currently translated at 75.0% (573 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 97.7% (217 of 222 strings)

Translated using Weblate (Dutch)

Currently translated at 95.4% (212 of 222 strings)

Translated using Weblate (Turkish)

Currently translated at 75.8% (318 of 419 strings)

Translated using Weblate (Ukrainian)

Currently translated at 73.5% (78 of 106 strings)

Translated using Weblate (Dutch)

Currently translated at 51.8% (55 of 106 strings)

Translated using Weblate (Dutch)

Currently translated at 91.7% (726 of 791 strings)

Translated using Weblate (Turkish)

Currently translated at 59.2% (469 of 791 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Translated using Weblate (Portuguese)

Currently translated at 61.9% (1775 of 2867 strings)

Translated using Weblate (Polish)

Currently translated at 96.0% (146 of 152 strings)

Translated using Weblate (Polish)

Currently translated at 61.1% (1754 of 2867 strings)

Translated using Weblate (Polish)

Currently translated at 75.9% (580 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 96.4% (763 of 791 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Translated using Weblate (Ukrainian)

Currently translated at 64.1% (68 of 106 strings)

Translated using Weblate (Ukrainian)

Currently translated at 64.1% (68 of 106 strings)

Translated using Weblate (Ukrainian)

Currently translated at 75.0% (573 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.8% (414 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Translated using Weblate (Polish)

Currently translated at 96.3% (762 of 791 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Translated using Weblate (Croatian)

Currently translated at 79.5% (105 of 132 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Croatian)

Currently translated at 72.0% (302 of 419 strings)

Translated using Weblate (Croatian)

Currently translated at 61.4% (75 of 122 strings)

Translated using Weblate (Polish)

Currently translated at 95.5% (756 of 791 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Croatian)

Currently translated at 27.3% (29 of 106 strings)

Translated using Weblate (Croatian)

Currently translated at 78.7% (104 of 132 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Croatian)

Currently translated at 61.4% (75 of 122 strings)

Translated using Weblate (Croatian)

Currently translated at 71.3% (157 of 220 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Translated using Weblate (Japanese)

Currently translated at 98.9% (756 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 72.9% (89 of 122 strings)

Translated using Weblate (Polish)

Currently translated at 95.3% (754 of 791 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2867 of 2867 strings)

Translated using Weblate (Turkish)

Currently translated at 46.2% (49 of 106 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Turkish)

Currently translated at 73.5% (562 of 764 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Polish)

Currently translated at 95.1% (753 of 791 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (222 of 222 strings)

Translated using Weblate (Turkish)

Currently translated at 71.3% (87 of 122 strings)

Translated using Weblate (Arabic)

Currently translated at 23.5% (25 of 106 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (222 of 222 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Turkish)

Currently translated at 48.0% (132 of 275 strings)

Translated using Weblate (Turkish)

Currently translated at 75.6% (317 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Turkish)

Currently translated at 41.5% (44 of 106 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Turkish)

Currently translated at 93.6% (44 of 47 strings)

Translated using Weblate (Turkish)

Currently translated at 93.4% (142 of 152 strings)

Translated using Weblate (Korean)

Currently translated at 72.9% (577 of 791 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Translated using Weblate (Polish)

Currently translated at 94.5% (748 of 791 strings)

Translated using Weblate (Polish)

Currently translated at 94.5% (748 of 791 strings)

Translated using Weblate (Polish)

Currently translated at 94.5% (748 of 791 strings)

Translated using Weblate (Polish)

Currently translated at 94.0% (744 of 791 strings)

Translated using Weblate (Russian)

Currently translated at 97.9% (2808 of 2867 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2867 of 2867 strings)

Translated using Weblate (Russian)

Currently translated at 99.6% (761 of 764 strings)

Translated using Weblate (Polish)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Russian)

Currently translated at 99.6% (788 of 791 strings)

Translated using Weblate (Polish)

Currently translated at 94.0% (744 of 791 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (222 of 222 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (218 of 222 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Russian)

Currently translated at 98.8% (414 of 419 strings)

Translated using Weblate (Russian)

Currently translated at 99.4% (760 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (222 of 222 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.5% (1909 of 2867 strings)

Co-authored-by: Adrián Chaves Fernández <adrian@chaves.io>
Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Anony Moly <supersoda1233@gmail.com>
Co-authored-by: Bengisu Diri <bengisudiri@hotmail.com>
Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Bogdan Derdziak <bagtirr@gmail.com>
Co-authored-by: BryanLim <youmakemysonlooklikeelonmusk@gmail.com>
Co-authored-by: Chucklestone <utkuulassimsek@gmail.com>
Co-authored-by: Deni Zubin <deni.zubin@gmail.com>
Co-authored-by: Elsyana Z <frostaxter@gmail.com>
Co-authored-by: Evan Kletniek <rutakl2010@gmail.com>
Co-authored-by: Falzart <muh_fauzi_ramadhan@yahoo.co.id>
Co-authored-by: Full Name <supersoda1233@gmail.com>
Co-authored-by: Hinano_Miyako <sinemgokcebircan@gmail.com>
Co-authored-by: John Collins <munmedia9865@gmail.com>
Co-authored-by: João Pedro <lolpeople.mega@gmail.com>
Co-authored-by: Julian H <julian.henin29@gmail.com>
Co-authored-by: Justcallme rye <Blizzardscf32@gmail.com>
Co-authored-by: Kiryla <ansgar@tut.by>
Co-authored-by: LiziKnight <liziknight0316@outlook.com>
Co-authored-by: Marek Tomek <markowalzky2@gmail.com>
Co-authored-by: Maria G <magu18ab@hotmail.com>
Co-authored-by: Melina Rake <melinarake@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Nodaysoff <convoron@yandex.ru>
Co-authored-by: Oleksandr Shtonda <oleksandrshtonda@gmail.com>
Co-authored-by: Omar Bertolla <scaram@icloud.com>
Co-authored-by: Rayane Benamre <thegamercore3@gmail.com>
Co-authored-by: Simon Hagman <dragonzimpan@gmail.com>
Co-authored-by: Svetlana <shkulepo@rambler.ru>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yuliia Pastukh <yuliya.bratash666@gmail.com>
Co-authored-by: billypat <kreideraine@gmail.com>
Co-authored-by: inesa <inessami200@gmail.com>
Co-authored-by: minhtan <minhtan11221122@gmail.com>
Co-authored-by: sam de wit <samedewit@gmail.com>
Co-authored-by: Ілля <bo4onok5@gmail.com>
Co-authored-by: Катерина <kate0712ann@gmail.com>
Co-authored-by: 왕효준 <gywns0417@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/be/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/it/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/lv/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/vi/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/en@lolcat/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/id/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
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/challenge/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/id/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/en@lolcat/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/death/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/death/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/lv/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/id/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pl/
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/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/lv/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pl/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/id/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/uk/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Contrib
Translation: Habitica/Death
Translation: Habitica/Defaulttasks
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/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2023-07-25 21:11:22 +02:00
Natalie L
812e2132d9 fix(config.json.example) (#14787)
* 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

---------

Co-authored-by: SabreCat <sabe@habitica.com>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2023-07-25 14:10:47 -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
8558dcc3a8 4.276.2 2023-07-18 10:34:02 -05:00
SabreCat
f8a8b61726 Merge branch 'phillip/chat-skill-merge' into release 2023-07-18 10:33:55 -05:00
SabreCat
067a1de49e fix(lint): newlines, console 2023-07-18 10:20:58 -05:00
SabreCat
65ef3bfeca fix(migration): casing 2023-07-18 10:04:56 -05:00
SabreCat
af04657856 feat(event): Summer Splash Orcas 2023-07-18 09:12:28 -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
6089b02746 4.276.1 2023-07-13 14:45:37 -05:00
SabreCat
f3f69b1871 fix(lfp): white background for seeker card 2023-07-13 14:45:31 -05:00
SabreCat
259f7ef588 fix(chat): add default to switch block 2023-07-11 18:25:59 -05:00
SabreCat
106a0c9ed8 fix(chat): collapse repetitive spells for real now 2023-07-11 17:01:17 -05:00
SabreCat
578083dde6 chore(faq): sync up 2023-07-11 14:15:27 -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
Phillip Thelen
74ba5c0b27 bring merging to MVP 2023-07-11 15:10:50 +02:00
SabreCat
bb54a6532d fix(chat): correct bad length check 2023-07-10 15:37:46 -05:00
SabreCat
3c36c59bb3 Merge branch 'develop' into phillip/chat-skill-merge 2023-07-10 15:12:12 -05:00
SabreCat
2308961de6 fix(lint): address fatal errors 2023-07-10 15:11:22 -05:00
Phillip Thelen
2d71a902f1 Merge skill casting messages together 2023-07-10 16:27:31 +02:00
SabreCat
2e94bfc489 fix(faq): updated Linguist vebiage
by @CuriousMagpie
2023-07-06 15:28:08 -05:00
SabreCat
1deb903186 Merge branch 'release' into sabrecat/chat-warning 2023-07-06 15:23:46 -05:00
SabreCat
8c00b91cc6 feat(faq): update sunset faq 2023-07-06 15:23:42 -05:00
CuriousMagpie
5c8a3f7771 WIP(faq): correct Linguist guidelines link 2023-07-06 13:20:10 -04:00
SabreCat
70d59be39b 4.276.0 2023-07-04 13:23:29 -05:00
Natalie L
c562c93158 feat(content): add July 2023 backgrounds and Enchanted Armoire (#14733)
* feat(content): add June subscriber items

* feat(content): add July backgrounds and Enchanted Armoire Items

* Update backgrounds.json

---------

Co-authored-by: SabreCat <sabe@habitica.com>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2023-07-04 13:23:06 -05:00
CuriousMagpie
8ac03e311b WIP(faq): update Linguists section 2023-06-30 12:56:39 -04:00
SabreCat
7a430889a8 Merge branch 'develop' into sabrecat/chat-warning 2023-06-29 14:53:15 -05:00
SabreCat
f84b5f163c Merge branch 'develop' into sabrecat/unsociable 2023-06-29 14:53:00 -05:00
SabreCat
519da49886 4.275.0 2023-06-29 14:51:37 -05:00
Weblate
79d50cb3e0 Merge branch 'origin/develop' into Weblate. 2023-06-29 21:36:09 +02:00
Weblate
c588c2b2ff Translated using Weblate (Korean)
Currently translated at 97.8% (46 of 47 strings)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Deleted translation using Weblate (Acehnese)

Added translation using Weblate (Acehnese)

Translated using Weblate (Scots)

Currently translated at 90.9% (111 of 122 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% (2863 of 2863 strings)

Translated using Weblate (Portuguese)

Currently translated at 62.0% (1777 of 2863 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (218 of 218 strings)

Added translation using Weblate (Acehnese)

Translated using Weblate (Russian)

Currently translated at 97.4% (2790 of 2863 strings)

Translated using Weblate (Russian)

Currently translated at 96.7% (2769 of 2863 strings)

Translated using Weblate (Croatian)

Currently translated at 26.4% (28 of 106 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (791 of 791 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.0% (1892 of 2863 strings)

Translated using Weblate (Swedish)

Currently translated at 98.6% (150 of 152 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.3% (2817 of 2863 strings)

Translated using Weblate (Swedish)

Currently translated at 98.6% (150 of 152 strings)

Translated using Weblate (Russian)

Currently translated at 96.4% (2761 of 2863 strings)

Translated using Weblate (Swedish)

Currently translated at 76.6% (321 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.0% (1892 of 2863 strings)

Translated using Weblate (Malay)

Currently translated at 48.2% (54 of 112 strings)

Translated using Weblate (Korean)

Currently translated at 72.9% (577 of 791 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.0% (1892 of 2863 strings)

Translated using Weblate (Galician)

Currently translated at 65.5% (501 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Malay)

Currently translated at 54.3% (430 of 791 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.0% (1892 of 2863 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2863 of 2863 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.0% (1892 of 2863 strings)

Translated using Weblate (Russian)

Currently translated at 99.1% (784 of 791 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (791 of 791 strings)

Translated using Weblate (Korean)

Currently translated at 72.3% (572 of 791 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Japanese)

Currently translated at 97.4% (268 of 275 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (137 of 137 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (275 of 275 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.0% (1892 of 2863 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (2859 of 2863 strings)

Translated using Weblate (German)

Currently translated at 63.2% (67 of 106 strings)

Translated using Weblate (German)

Currently translated at 81.1% (99 of 122 strings)

Translated using Weblate (Galician)

Currently translated at 98.5% (135 of 137 strings)

Translated using Weblate (German)

Currently translated at 100.0% (791 of 791 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (106 of 106 strings)

Translated using Weblate (Galician)

Currently translated at 98.5% (271 of 275 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 (German)

Currently translated at 99.4% (787 of 791 strings)

Translated using Weblate (German)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Korean)

Currently translated at 99.7% (375 of 376 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.5% (2851 of 2863 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.0% (1892 of 2863 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (106 of 106 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.0% (1892 of 2863 strings)

Translated using Weblate (Russian)

Currently translated at 95.0% (116 of 122 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (217 of 221 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.3% (2844 of 2863 strings)

Translated using Weblate (Russian)

Currently translated at 90.9% (111 of 122 strings)

Translated using Weblate (Russian)

Currently translated at 98.8% (782 of 791 strings)

Translated using Weblate (Malay)

Currently translated at 53.2% (421 of 791 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.2% (131 of 132 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.5% (413 of 419 strings)

Translated using Weblate (Hebrew)

Currently translated at 63.7% (267 of 419 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.3% (2843 of 2863 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.0% (1891 of 2863 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Ukrainian)

Currently translated at 55.6% (59 of 106 strings)

Translated using Weblate (Dutch)

Currently translated at 49.0% (52 of 106 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (791 of 791 strings)

Translated using Weblate (Dutch)

Currently translated at 89.7% (710 of 791 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Malay)

Currently translated at 52.3% (414 of 791 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.0% (1891 of 2863 strings)

Translated using Weblate (Hebrew)

Currently translated at 56.5% (237 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.7% (1891 of 2831 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Galician)

Currently translated at 65.5% (501 of 764 strings)

Translated using Weblate (Russian)

Currently translated at 98.1% (217 of 221 strings)

Translated using Weblate (Hebrew)

Currently translated at 60.1% (133 of 221 strings)

Translated using Weblate (Hebrew)

Currently translated at 50.3% (211 of 419 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (217 of 218 strings)

Translated using Weblate (Russian)

Currently translated at 98.3% (179 of 182 strings)

Translated using Weblate (Russian)

Currently translated at 98.3% (179 of 182 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (221 of 221 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 65.5% (501 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (106 of 106 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Malay)

Currently translated at 52.0% (412 of 791 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.4% (1881 of 2831 strings)

Translated using Weblate (Hebrew)

Currently translated at 41.0% (325 of 791 strings)

Translated using Weblate (Ukrainian)

Currently translated at 98.6% (150 of 152 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (106 of 106 strings)

Translated using Weblate (Dutch)

Currently translated at 97.3% (148 of 152 strings)

Translated using Weblate (Malay)

Currently translated at 82.0% (87 of 106 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 65.2% (1848 of 2831 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (221 of 221 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (106 of 106 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (132 of 132 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 (Indonesian)

Currently translated at 92.6% (388 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 64.9% (1838 of 2831 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 91.1% (382 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 64.2% (1818 of 2831 strings)

Translated using Weblate (Indonesian)

Currently translated at 98.6% (754 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (221 of 221 strings)

Translated using Weblate (French)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Portuguese)

Currently translated at 38.6% (41 of 106 strings)

Translated using Weblate (Malay)

Currently translated at 74.5% (79 of 106 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.0% (219 of 221 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 91.1% (382 of 419 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2835 of 2835 strings)

Translated using Weblate (Indonesian)

Currently translated at 63.5% (1798 of 2831 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (106 of 106 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 97.3% (744 of 764 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (791 of 791 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Indonesian)

Currently translated at 89.4% (375 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 62.8% (1778 of 2831 strings)

Translated using Weblate (Indonesian)

Currently translated at 93.4% (714 of 764 strings)

Co-authored-by: Adrián Chaves Fernández <adrian@chaves.io>
Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Andreway <watermelontvandreway2@gmail.com>
Co-authored-by: Antje Schubert <antje.schubert96@web.de>
Co-authored-by: Arthur Ouzlaner <panther1984@gmail.com>
Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: BryanLim <youmakemysonlooklikeelonmusk@gmail.com>
Co-authored-by: Deni Zubin <deni.zubin@gmail.com>
Co-authored-by: Falzart <muh_fauzi_ramadhan@yahoo.co.id>
Co-authored-by: Hanafi <naflizo@gmail.com>
Co-authored-by: Jan Willem Middag <jwmiddag@gmail.com>
Co-authored-by: Jinnel <1210678078@qq.com>
Co-authored-by: LiziKnight <liziknight0316@outlook.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Rostislav <rostislav.zem@gmail.com>
Co-authored-by: Sara Olson <sara@habitica.com>
Co-authored-by: Simon Hagman <dragonzimpan@gmail.com>
Co-authored-by: Svetlana <shkulepo@rambler.ru>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Thiago Braga <thibraga06@gmail.com>
Co-authored-by: Timur Niyazov <timmy568@gmail.com>
Co-authored-by: Vladyslav Yavnyk <yavnyjvladislav@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: lilia petervari <lilipetervari@gmail.com>
Co-authored-by: Ілля <bo4onok5@gmail.com>
Co-authored-by: Естай <akseleu@yahoo.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/de/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/he/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/nl/
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/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/he/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/nl/
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/character/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/de/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/sco/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/ko/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/id/
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/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/es/
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/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/he/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/id/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/sv/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/id/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/fr/
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/pets/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/id/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/he/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/id/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt_BR/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
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/Limited
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-06-29 21:35:54 +02:00
Natalie L
77a490283c feat(content): add July 2023 subscriber items (#14732)
* feat(content): add June subscriber items

* feat(content): add July subscriber items
2023-06-29 14:35:05 -05:00
dependabot[bot]
e49d26eacd build(deps): bump stripe from 12.8.0 to 12.9.0 (#14699)
Bumps [stripe](https://github.com/stripe/stripe-node) from 12.8.0 to 12.9.0.
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v12.8.0...v12.9.0)

---
updated-dependencies:
- dependency-name: stripe
  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-06-21 13:50:08 -04:00
dependabot[bot]
7b0fd57eb9 build(deps): bump @babel/core from 7.22.1 to 7.22.5 (#14700)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.22.1 to 7.22.5.
- [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.5/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  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-06-21 13:49:41 -04:00
dependabot[bot]
7171334e31 build(deps): bump @babel/register from 7.21.0 to 7.22.5 (#14702)
Bumps [@babel/register](https://github.com/babel/babel/tree/HEAD/packages/babel-register) from 7.21.0 to 7.22.5.
- [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.5/packages/babel-register)

---
updated-dependencies:
- dependency-name: "@babel/register"
  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-06-21 13:49:16 -04:00
dependabot[bot]
a3235214b2 build(deps): bump core-js from 3.30.2 to 3.31.0 in /website/client (#14704)
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.30.2 to 3.31.0.
- [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.31.0/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-06-21 13:48:45 -04:00
dependabot[bot]
fca234c45a build(deps-dev): bump sinon from 15.1.0 to 15.1.2 (#14713)
Bumps [sinon](https://github.com/sinonjs/sinon) from 15.1.0 to 15.1.2.
- [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.0...v15.1.2)

---
updated-dependencies:
- dependency-name: sinon
  dependency-type: direct:development
  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-06-21 13:47:21 -04:00
dependabot[bot]
7519023f06 build(deps): bump sass from 1.62.1 to 1.63.4 in /website/client (#14719)
Bumps [sass](https://github.com/sass/dart-sass) from 1.62.1 to 1.63.4.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.62.1...1.63.4)

---
updated-dependencies:
- dependency-name: sass
  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-06-21 13:46:01 -04: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
SabreCat
df84d7c7b1 Merge branch 'release' into develop 2023-06-19 16:36:39 -05:00
SabreCat
e837ebec49 4.274.0 2023-06-19 16:36:13 -05:00
Natalie L
c7ed693e18 feat(gala): Add 2023 Summer Splash Gala Items (#14712)
* feat(content): add June subscriber items

* feat(gala): add images

* feat(gala): add code

* feat(gala): text strings

* feat(gala): testing and final updates

* feat(gala): fixed a couple of typos

* fix(event): proofread strings and dates
Also replace empty descriptions for Rogue and Healer

* fix(event): NO EGG

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-06-19 16:36:05 -05:00
Ash
e72a25ad02 Fixes #14438: a11y: add semantic roles to habit and todo controls (#14467)
* a11y: add aria roles to habit control

* a11y: add role to todo checkboxes

* a11y: add aria-labels to score buttons
Helps with screen readers

* add i18n to aria-labels
2023-06-19 17:00:16 -04: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
SabreCat
2c12d5ee29 4.273.3 2023-06-13 14:57:37 -05:00
Weblate
c3f0abadd7 Merge branch 'origin/develop' into Weblate. 2023-06-13 21:54:27 +02:00
Phillip Thelen
adf0a2efca Fix perkMonthCount not being editable/saving (#14711)
Co-authored-by: SabreCat <sabe@habitica.com>
2023-06-13 14:51:58 -05:00
SabreCat
e4523c09dc Merge branch 'release' into develop 2023-06-13 14:40:41 -05:00
SabreCat
91d98b86e1 fix(lint): whitespace 2023-06-13 14:40:26 -05:00
Weblate
779fb8bce5 Translated using Weblate (Indonesian)
Currently translated at 62.0% (1758 of 2831 strings)

Translated using Weblate (Malay)

Currently translated at 70.7% (75 of 106 strings)

Translated using Weblate (Japanese)

Currently translated at 98.9% (756 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 61.9% (1753 of 2831 strings)

Translated using Weblate (Indonesian)

Currently translated at 87.8% (368 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 61.8% (1752 of 2831 strings)

Translated using Weblate (Indonesian)

Currently translated at 92.1% (704 of 764 strings)

Translated using Weblate (Malay)

Currently translated at 61.3% (65 of 106 strings)

Translated using Weblate (Indonesian)

Currently translated at 85.9% (360 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 61.4% (1740 of 2831 strings)

Translated using Weblate (Indonesian)

Currently translated at 89.5% (684 of 764 strings)

Translated using Weblate (Malay)

Currently translated at 59.4% (63 of 106 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 92.6% (202 of 218 strings)

Translated using Weblate (Indonesian)

Currently translated at 84.7% (355 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 60.6% (1718 of 2831 strings)

Translated using Weblate (Indonesian)

Currently translated at 83.9% (641 of 764 strings)

Translated using Weblate (Irish)

Currently translated at 75.0% (114 of 152 strings)

Translated using Weblate (Irish)

Currently translated at 75.0% (114 of 152 strings)

Translated using Weblate (Russian)

Currently translated at 95.4% (211 of 221 strings)

Translated using Weblate (Russian)

Currently translated at 99.4% (760 of 764 strings)

Translated using Weblate (English (Pirate))

Currently translated at 82.2% (125 of 152 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Russian)

Currently translated at 96.3% (213 of 221 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Russian)

Currently translated at 95.5% (259 of 271 strings)

Translated using Weblate (Indonesian)

Currently translated at 60.6% (1718 of 2831 strings)

Translated using Weblate (Russian)

Currently translated at 89.6% (95 of 106 strings)

Translated using Weblate (Indonesian)

Currently translated at 83.3% (637 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Malay)

Currently translated at 52.8% (56 of 106 strings)

Translated using Weblate (Indonesian)

Currently translated at 83.3% (637 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (791 of 791 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Malay)

Currently translated at 98.6% (150 of 152 strings)

Translated using Weblate (Russian)

Currently translated at 98.5% (413 of 419 strings)

Translated using Weblate (Serbian)

Currently translated at 23.5% (25 of 106 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (784 of 784 strings)

Translated using Weblate (Malay)

Currently translated at 52.8% (56 of 106 strings)

Translated using Weblate (Malay)

Currently translated at 85.1% (115 of 135 strings)

Translated using Weblate (Indonesian)

Currently translated at 81.6% (342 of 419 strings)

Co-authored-by: Abiel Meza <mezaabiel@gmail.com>
Co-authored-by: Anastasia Wysocka <legitemail.uwu420@gmail.com>
Co-authored-by: Edward McGibney <edwardmcgibney95@gmail.com>
Co-authored-by: Falzart <muh_fauzi_ramadhan@yahoo.co.id>
Co-authored-by: Hanafi <naflizo@gmail.com>
Co-authored-by: Lauren C <laurenc7834@gmail.com>
Co-authored-by: LiziKnight <liziknight0316@outlook.com>
Co-authored-by: Miroslav <entferner@yandex.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: i3beograd <milica.the@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/en@pirate/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ga/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/id/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/id/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/es/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/nb_NO/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/id/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/id/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ms/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Npc
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2023-06-13 09:29:03 +02:00
SabreCat
f0fc83ed85 Merge branch 'release' into develop 2023-06-12 15:02:18 -05:00
SabreCat
30d2108c78 4.273.2 2023-06-12 15:02:06 -05:00
Natalie L
ab68e8a5fe feat(content): add June pet quest bundle (#14694)
* feat(content): add June subscriber items

* feat(content): add June pet quest bundle

* fix(bundle): correct timing and visual issues

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-06-12 15:01:34 -05: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
dependabot[bot]
31e9100ba2 build(deps): bump @babel/preset-env from 7.21.5 to 7.22.5 (#14695)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.21.5 to 7.22.5.
- [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.5/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  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-06-08 15:45:46 -04:00
dependabot[bot]
0070f366bb build(deps): bump xml2js from 0.5.0 to 0.6.0 (#14673)
Bumps [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js) from 0.5.0 to 0.6.0.
- [Commits](https://github.com/Leonidas-from-XIV/node-xml2js/compare/0.5.0...0.6.0)

---
updated-dependencies:
- dependency-name: xml2js
  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-06-08 15:38:28 -04:00
dependabot[bot]
2be6865a5c build(deps): bump winston from 3.8.2 to 3.9.0 (#14676)
Bumps [winston](https://github.com/winstonjs/winston) from 3.8.2 to 3.9.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.8.2...v3.9.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-06-08 15:37:58 -04:00
dependabot[bot]
db85768e9d build(deps): bump stripe from 12.6.0 to 12.8.0 (#14690)
Bumps [stripe](https://github.com/stripe/stripe-node) from 12.6.0 to 12.8.0.
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v12.6.0...v12.8.0)

---
updated-dependencies:
- dependency-name: stripe
  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-06-08 15:34:03 -04:00
dependabot[bot]
3d40413882 build(deps): bump fast-xml-parser and is-svg (#14693)
Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) and [is-svg](https://github.com/sindresorhus/is-svg). These dependencies needed to be updated together.

Updates `fast-xml-parser` from 3.19.0 to 4.2.4
- [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/commits)

Updates `is-svg` from 4.3.1 to 4.4.0
- [Release notes](https://github.com/sindresorhus/is-svg/releases)
- [Commits](https://github.com/sindresorhus/is-svg/compare/v4.3.1...v4.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-08 15:32:28 -04:00
dependabot[bot]
cc88e75950 build(deps): bump @babel/core from 7.21.8 to 7.22.1 (#14670)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.21.8 to 7.22.1.
- [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.1/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  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-06-08 15:31:54 -04:00
SabreCat
de057dc1b2 WIP(sunset): close X, CG revisions 2023-06-07 17:03:07 -05:00
SabreCat
ff860b04fc Merge remote-tracking branch 'private/natalie/antisocial' into sabrecat/chat-warning 2023-06-07 15:02:11 -05:00
SabreCat
45dedbbdaa fix(banners): correct dismiss/show behavior 2023-06-07 15:00:51 -05:00
CuriousMagpie
b6359ad032 WIP(faq): pixelate Daniel's border 2023-06-07 13:33:58 -04:00
SabreCat
a5ae3e5877 4.273.1 2023-06-06 16:33:33 -05:00
SabreCat
60ed9d2944 Merge branch 'develop' into release 2023-06-06 16:33:27 -05:00
Natalie L
91fc4235aa fix(string): remove "due" string (#14683) 2023-06-06 16:33:01 -05:00
SabreCat
42e8dd1361 fix(vue): correct bad popovers breaking Chrome 2023-06-06 16:30:37 -05:00
CuriousMagpie
658a02bfc3 WIP(faq): add Daniel to sidebar 2023-06-06 15:26:31 -04:00
SabreCat
e1398e8d7c Merge branch 'develop' into sabrecat/unsociable 2023-06-06 09:22:37 -05:00
SabreCat
0185a1fbd6 Merge branch 'develop' into sabrecat/chat-warning 2023-06-06 09:22:23 -05:00
SabreCat
0a4bbbf173 4.273.0 2023-06-06 09:21:35 -05:00
SabreCat
df22f5f7bf Merge branch 'develop' into release 2023-06-06 09:21:25 -05:00
Natalie L
bb28bb5969 feat(content): add June backgrounds and Enchanted Armoire items (#14684)
* feat(content): add June subscriber items

* feat(content): add July backgrounds and Enchanted Armoire items

* feat(fix): correct sizing for aquarium background

* fix(strings): JSON formatting

* fix(sprites): add missing broad variant

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-06-06 09:17:22 -05:00
SabreCat
3b6c39dc9b fix(banner): restore close X on pause 2023-06-06 08:57:10 -05:00
Weblate
e4e8e0ff60 Translated using Weblate (Malay)
Currently translated at 51.8% (55 of 106 strings)

Translated using Weblate (Japanese)

Currently translated at 96.8% (214 of 221 strings)

Translated using Weblate (Malay)

Currently translated at 50.9% (54 of 106 strings)

Translated using Weblate (Indonesian)

Currently translated at 81.1% (340 of 419 strings)

Translated using Weblate (Portuguese)

Currently translated at 60.1% (163 of 271 strings)

Translated using Weblate (Indonesian)

Currently translated at 83.3% (637 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (106 of 106 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (218 of 218 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 (Belarusian)

Currently translated at 71.3% (157 of 220 strings)

Translated using Weblate (Japanese)

Currently translated at 95.9% (212 of 221 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Japanese)

Currently translated at 97.4% (264 of 271 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Japanese)

Currently translated at 96.4% (404 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 80.6% (338 of 419 strings)

Translated using Weblate (Japanese)

Currently translated at 99.8% (2828 of 2831 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (106 of 106 strings)

Translated using Weblate (Indonesian)

Currently translated at 83.3% (637 of 764 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (784 of 784 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Japanese)

Currently translated at 94.1% (208 of 221 strings)

Translated using Weblate (Indonesian)

Currently translated at 91.8% (249 of 271 strings)

Translated using Weblate (Indonesian)

Currently translated at 79.4% (333 of 419 strings)

Translated using Weblate (Indonesian)

Currently translated at 60.6% (1718 of 2831 strings)

Translated using Weblate (Japanese)

Currently translated at 78.3% (83 of 106 strings)

Translated using Weblate (Indonesian)

Currently translated at 83.3% (637 of 764 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (French)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Indonesian)

Currently translated at 83.3% (637 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 60.2% (1706 of 2831 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (221 of 221 strings)

Translated using Weblate (Indonesian)

Currently translated at 78.5% (213 of 271 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 54.7% (58 of 106 strings)

Translated using Weblate (Indonesian)

Currently translated at 83.3% (637 of 764 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (152 of 152 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (419 of 419 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (106 of 106 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (220 of 220 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (221 of 221 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (132 of 132 strings)

Translated using Weblate (Indonesian)

Currently translated at 63.4% (172 of 271 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (106 of 106 strings)

Translated using Weblate (Indonesian)

Currently translated at 83.3% (637 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (784 of 784 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (152 of 152 strings)

Co-authored-by: Adrián Chaves Fernández <adrian@chaves.io>
Co-authored-by: Falzart <muh_fauzi_ramadhan@yahoo.co.id>
Co-authored-by: Hanafi <naflizo@gmail.com>
Co-authored-by: Hanna Aniskevich <northernwind@tut.by>
Co-authored-by: LiziKnight <liziknight0316@outlook.com>
Co-authored-by: Sara Olson <sara@habitica.com>
Co-authored-by: TOMA Mitsuru <toma0001@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yatharth <megacutiemauandtuchchu@gmail.com>
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/id/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/id/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/id/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/id/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/content/id/
Translate-URL: https://translate.habitica.com/projects/habitica/content/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ms/
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/id/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
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/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/id/
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/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/id/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/id/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt/
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/id/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/id/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/id/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/id/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/id/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/be/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/gl/
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/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/gl/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
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/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Settings
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2023-06-06 05:00:15 +02:00
SabreCat
7e2a35d7a9 WIP(sunset): fix private Guild and report form 2023-06-05 16:36:00 -05:00
SabreCat
84e5c00be1 WIP(announcement): correct layout and close X 2023-06-05 16:16:19 -05:00
SabreCat
187029f44f Merge remote-tracking branch 'private/natalie/antisocial' into sabrecat/chat-warning 2023-06-05 15:54:41 -05:00
CuriousMagpie
efbc7d1460 WIP(faq): sidebar formatting 2023-06-05 16:53:48 -04:00
SabreCat
36f84d083e Merge remote-tracking branch 'private/natalie/antisocial' into sabrecat/chat-warning 2023-06-05 15:32:46 -05:00
CuriousMagpie
2154ba5451 WIP(fix): change URL 2023-06-05 12:33:40 -04:00
SabreCat
cf0e45c68c Merge branch 'natalie/antisocial' into sabrecat/chat-warning 2023-06-02 16:14:20 -05:00
SabreCat
df5d1e95d1 chore(sunset): end standard guild creation API 2023-06-02 16:09:11 -05:00
CuriousMagpie
93d9038765 WIP(faq): initial formatting of main text, started on sidebar 2023-06-02 17:00:13 -04: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
302eabb30f WIP(faq): hack background to white 2023-06-02 14:58:16 -05:00
SabreCat
09695f637e Merge branch 'release' into natalie/antisocial 2023-06-02 14:12:52 -05:00
SabreCat
e9a15fcb83 fix(strings): JSON formatting 2023-06-02 13:55:04 -05:00
CuriousMagpie
97c8138340 feat(content): minor updates 2023-06-02 11:31:43 -04:00
SabreCat
a65b0d1f4d fix(banner): make dismissable 2023-06-01 16:33:11 -05:00
SabreCat
8d73e2949a Merge branch 'release' into sabrecat/unsociable 2023-06-01 15:42:47 -05:00
SabreCat
c0cf647873 fix(banner): only show when relevant 2023-05-31 16:55:21 -05:00
CuriousMagpie
d20e976176 feat(style): css work 2023-05-31 14:27:52 -04:00
SabreCat
739016ba01 WIP(chat): add warning banner 2023-05-30 16:25:29 -05:00
SabreCat
a5602eec8d 4.272.0 2023-05-30 15:28:16 -05:00
Natalie L
867eed176e feat(content): add June subscriber items (#14669)
Co-authored-by: SabreCat <sabe@habitica.com>
2023-05-30 15:26:16 -05:00
SabreCat
ba883ae104 chore(subproj): update habitica-images 2023-05-30 14:55:00 -05:00
SabreCat
deba7b6220 feat(faq): update for mobile workflows 2023-05-30 14:50:13 -05:00
CuriousMagpie
55d6ee3f7e feat(content): add staff and tiers 2023-05-30 12:01:21 -04:00
SabreCat
69c538858b 4.271.2 2023-05-25 14:42:53 -05:00
SabreCat
17072dcc45 Merge branch 'due-dates-in-todos' into release 2023-05-25 14:42:46 -05:00
SabreCat
2448f401f2 Merge branch 'increment-component' into release 2023-05-25 14:42:42 -05:00
CuriousMagpie
9ef13dad68 feat(content): starting add styling, string updates 2023-05-25 12:25:08 -04:00
CuriousMagpie
14fa69719b feat(content): static page created and strings in place 2023-05-24 16:18:31 -04:00
SabreCat
9228b070fa Merge branch 'release' into sabrecat/unsociable 2023-05-24 14:17:49 -05:00
SabreCat
5745e3df5f 4.271.1 2023-05-24 13:30:21 -05:00
Phillip Thelen
d4a5823916 Fix one-off issue for monthly subs (#14643)
* Fix initial plan.consecutive.offset for 1 month subs

* fix initial values for group plan subs

* Make perkMonthCount editable in admin panel

* Add aditional info to admin panel

* Implement automatic fix for affected users

* fix(lint): exclusive test, code style

* fixes

* fix issue with initialization

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-05-24 13:29:42 -05:00
CuriousMagpie
929b0196a4 add strings 2023-05-24 13:24:44 -04:00
CuriousMagpie
1b91f620e1 initial commit 2023-05-23 16:01:40 -04:00
CuriousMagpie
86b15cb580 fix(style): css fix, today if-statement added 2023-05-23 15:19:26 -04:00
SabreCat
8e5b66a73e Merge branch 'release' into develop 2023-05-23 09:16:50 -05:00
SabreCat
f755d4c133 4.271.0 2023-05-23 09:07:47 -05:00
SabreCat
102c71c4ca Merge remote-tracking branch 'CuriousMagpie/2023-05-pet-quest-bundle' into release 2023-05-22 15:14:18 -05:00
SabreCat
a7bde80349 Squashed commit of the following:
commit 27287ac3aa
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon May 22 15:59:20 2023 -0400

    fix(typo): typos fixed

commit a4df8097cf
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon May 22 15:57:17 2023 -0400

    feat(content): add migration script

commit 23ff7845c1
Merge: d02644e21b 8ba7117fa5
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Mon May 22 15:42:32 2023 -0400

    Merge branch 'develop' into achievement-dinosaur-dynasty

commit 8ba7117fa5
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon May 22 12:35:07 2023 -0400

    build(deps): bump stripe from 12.5.0 to 12.6.0 (#14662)

    Bumps [stripe](https://github.com/stripe/stripe-node) from 12.5.0 to 12.6.0.
    - [Release notes](https://github.com/stripe/stripe-node/releases)
    - [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/stripe/stripe-node/compare/v12.5.0...v12.6.0)

    ---
    updated-dependencies:
    - dependency-name: stripe
      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 fe5d4a0551
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon May 22 12:34:28 2023 -0400

    build(deps-dev): bump sinon from 15.0.4 to 15.1.0 (#14661)

    Bumps [sinon](https://github.com/sinonjs/sinon) from 15.0.4 to 15.1.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.0.4...v15.1.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>

commit d02644e21b
Author: CuriousMagpie <eilatan@gmail.com>
Date:   Wed May 17 11:36:28 2023 -0400

    feat(content): add dinosaur dynasty achievement
2023-05-22 15:13:49 -05:00
SabreCat
bedce203ee 4.270.3 2023-05-22 13:28:31 -05:00
dependabot[bot]
8ba7117fa5 build(deps): bump stripe from 12.5.0 to 12.6.0 (#14662)
Bumps [stripe](https://github.com/stripe/stripe-node) from 12.5.0 to 12.6.0.
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v12.5.0...v12.6.0)

---
updated-dependencies:
- dependency-name: stripe
  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-05-22 12:35:07 -04:00
dependabot[bot]
fe5d4a0551 build(deps-dev): bump sinon from 15.0.4 to 15.1.0 (#14661)
Bumps [sinon](https://github.com/sinonjs/sinon) from 15.0.4 to 15.1.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.0.4...v15.1.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-05-22 12:34:28 -04:00
SabreCat
deebc09a79 fix(analytics): typo 2023-05-22 09:54:10 -05:00
SabreCat
b63f2fa1fa fix(analytics): add missing headers to 3 events 2023-05-19 14:54:30 -05:00
SabreCat
8d9602fb16 WIP(chat): first pass deprecation 2023-05-17 16:33:44 -05:00
CuriousMagpie
60b180681e Merge remote-tracking branch 'origin/due-dates-in-todos' into due-dates-in-todos 2023-05-17 13:30:45 -04:00
CuriousMagpie
7c1c18a329 fix(styling): update colors to be a11y-friendly and to show items due today in gray 2023-05-17 13:29:26 -04:00
CuriousMagpie
0b0cbb45f4 feat(content): add May pet quest bundle 2023-05-17 10:49:37 -04:00
SabreCat
0e03f079a7 Merge branch 'due-dates-in-todos' of https://github.com/CuriousMagpie/habitica into due-dates-in-todos 2023-05-16 14:27:56 -05:00
SabreCat
a71e44b331 fix(test): remove test for old function 2023-05-16 14:27:15 -05:00
Sabe Jones
48917fd8be Merge branch 'develop' into due-dates-in-todos 2023-05-16 14:18:22 -05:00
SabreCat
2a054a25ee fix(test): include user pref needed for date 2023-05-16 14:15:34 -05:00
SabreCat
d176c31382 4.270.2 2023-05-16 12:22:21 -05:00
Phillip Thelen
8150fef993 Database Access optimisations (#14544)
* Optimize database access during spell casting

* load less data when casting spells

* Begin migrating update calls to updateOne and updateMany

* Only update user objects that don’t have notification yet

* fix test

* fix spy

* Don’t unnecessarily update user when requesting invalid guild

* fix sort order for middlewares to not load user twice every request

* fix tests

* fix integration test

* fix skill usage not always deducting mp

* addtest case for blessing spell

* fix healAll

* fix lint

* Fix error for when some spells are used outside of party

* Add check to not run bulk spells in web client

* fix(tags): change const to let

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-05-16 12:21:45 -05:00
SabreCat
f0637dcf49 4.270.1 2023-05-15 16:15:22 -05:00
SabreCat
0518b90eab Merge branch 'develop' into release 2023-05-15 16:11:30 -05:00
SabreCat
f968bdd3a9 fix(analytics): re-enable group chat tracking 2023-05-15 16:02:24 -05:00
negue
e3a1ea6180 save task column filter (#14587)
* save task column filter

* remove old setting

* fix tests
2023-05-15 16:01:32 -05:00
Natalie L
17f6054ef0 feat(content): add May magic hatching potions (#14641) 2023-05-15 15:59:49 -05:00
dependabot[bot]
9b8f213c63 build(deps): bump jquery from 3.6.4 to 3.7.0 in /website/client (#14653)
Bumps [jquery](https://github.com/jquery/jquery) from 3.6.4 to 3.7.0.
- [Release notes](https://github.com/jquery/jquery/releases)
- [Commits](https://github.com/jquery/jquery/compare/3.6.4...3.7.0)

---
updated-dependencies:
- dependency-name: jquery
  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-05-15 13:46:41 -04:00
CuriousMagpie
daccade2e2 disabled sell button when user tries to sell more items than they own 2023-05-15 12:03:58 -04:00
dependabot[bot]
48bb3e2886 build(deps): bump core-js from 3.30.1 to 3.30.2 in /website/client (#14635)
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.30.1 to 3.30.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.30.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-05-12 16:47:35 -05:00
dependabot[bot]
308d557770 build(deps): bump superagent from 8.0.6 to 8.0.9 (#14473)
Bumps [superagent](https://github.com/ladjs/superagent) from 8.0.6 to 8.0.9.
- [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.6...v8.0.9)

---
updated-dependencies:
- dependency-name: superagent
  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-05-12 16:35:21 -05:00
dependabot[bot]
0f4816c674 build(deps): bump intro.js from 6.0.0 to 7.0.1 in /website/client (#14558)
Bumps [intro.js](https://github.com/usablica/intro.js) from 6.0.0 to 7.0.1.
- [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/v6.0.0...v7.0.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-12 16:35:06 -05:00
dependabot[bot]
f1b98a530d build(deps): bump jsonwebtoken from 8.5.1 to 9.0.0 (#14418)
Bumps [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) from 8.5.1 to 9.0.0.
- [Release notes](https://github.com/auth0/node-jsonwebtoken/releases)
- [Changelog](https://github.com/auth0/node-jsonwebtoken/blob/master/CHANGELOG.md)
- [Commits](https://github.com/auth0/node-jsonwebtoken/compare/v8.5.1...v9.0.0)

---
updated-dependencies:
- dependency-name: jsonwebtoken
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-12 16:29:16 -05:00
dependabot[bot]
1498eba8d4 build(deps): bump @babel/preset-env from 7.20.2 to 7.21.5 (#14616)
Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.20.2 to 7.21.5.
- [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.21.5/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/preset-env"
  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-05-12 16:28:41 -05:00
dependabot[bot]
fc16ffbf2d build(deps): bump dompurify from 2.4.3 to 3.0.3 in /website/client (#14639)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.4.3 to 3.0.3.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.4.3...3.0.3)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-12 16:22:06 -05:00
dependabot[bot]
021180fa59 build(deps): bump uuid from 8.3.2 to 9.0.0 in /website/client (#14227)
Bumps [uuid](https://github.com/uuidjs/uuid) from 8.3.2 to 9.0.0.
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v8.3.2...v9.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-12 16:13:53 -05:00
dependabot[bot]
102e6a64ad chore(deps): bump sass from 1.34.0 to 1.62.1 in /website/client (#14614)
Bumps [sass](https://github.com/sass/dart-sass) from 1.34.0 to 1.62.1.
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.34.0...1.62.1)

---
updated-dependencies:
- dependency-name: sass
  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-05-12 16:07:36 -05:00
SabreCat
79a5c2ec5f Merge branch 'develop' into due-dates-in-todos 2023-05-12 16:06:18 -05:00
dependabot[bot]
8cd6e1654f build(deps): bump @babel/core from 7.21.4 to 7.21.8 (#14630)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.21.4 to 7.21.8.
- [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.21.8/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  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-05-12 16:05:20 -05:00
dependabot[bot]
63ea21c46d build(deps): bump stripe from 11.10.0 to 12.5.0 (#14646)
Bumps [stripe](https://github.com/stripe/stripe-node) from 11.10.0 to 12.5.0.
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v11.10.0...v12.5.0)

---
updated-dependencies:
- dependency-name: stripe
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-12 16:03:29 -05:00
CuriousMagpie
0a23dd5311 attempt to fix sellModal 2023-05-11 16:25:11 -04:00
SabreCat
6e3a367832 Merge branch 'release' into develop 2023-05-11 14:13:58 -05:00
SabreCat
f3348aca4c 4.270.0 2023-05-09 10:29:19 -05:00
SabreCat
90e1bc9d5e Merge branch 'sabrecat/link-tweak' into release 2023-05-09 10:29:14 -05:00
Natalie L
453bf3a961 feat(content): add May Backgrounds and Enchanted Armoire items (#14628) 2023-05-09 10:26:00 -05:00
SabreCat
b026daec90 fix(rya): wait for DOM finished before update 2023-05-05 16:27:19 -05:00
SabreCat
49f45d27e3 4.269.1 2023-05-04 16:08:53 -05:00
CuriousMagpie
479cfb76ef fix(to do dates): locate a string for "Due" 2023-05-04 16:40:11 -04:00
CuriousMagpie
0e0cd99ded fix(to do dates): Add the word "Due" to the HTML 2023-05-04 16:35:27 -04:00
CuriousMagpie
7e210c56b0 fix(to do dates): change formatDueDate () to show exact due dates 2023-05-04 16:29:54 -04:00
SabreCat
d92a03048b fix(analytics): tweak CTA event 2023-05-04 14:20:49 -05:00
SabreCat
8183699cb7 feat(analytics): differentiate party CTA scenarios 2023-05-03 16:04:22 -05:00
SabreCat
9f9e6c4950 feat(links): skip modal for user's own tasks 2023-05-03 16:00:58 -05:00
SabreCat
c77dd5f200 fix(analytics): move lfp events out of mounted 2023-05-02 16:52:50 -05:00
CuriousMagpie
06ac6ae80c fix(html): fix behavior of buyModal when gems are being purchased; typo correction 2023-05-02 13:23:31 -04:00
SabreCat
13e87b1ea0 fix(tests): linting 2023-05-02 10:11:57 -05:00
CuriousMagpie
4a32a29bea Merge branch 'develop' into increment-component 2023-05-02 11:03:46 -04:00
SabreCat
71e165433a Merge branch 'release' into develop 2023-05-02 09:53:39 -05:00
SabreCat
c2515a4042 4.269.0 2023-05-02 09:51:55 -05:00
SabreCat
e31bfdc22b fix(stats): allow negative EXP 2023-05-02 09:51:49 -05:00
SabreCat
e9e4265545 Squashed commit of the following:
commit 00affb306655a543f5d29b3af6361e686b577a97
Author: SabreCat <sabe@habitica.com>
Date:   Tue May 2 09:47:25 2023 -0500

    fix(tests): account for invite limit changes

commit 47661117f9fd661b8bc8f63b7cc7c8d5f8fa0fd7
Author: SabreCat <sabe@habitica.com>
Date:   Mon May 1 17:39:29 2023 -0500

    fix(lfp): final polish

commit 6a1e5af1db0dd90be3ced7e223f53c9183a206f5
Merge: 728ed2ddad 9e0777bb42
Author: SabreCat <sabe@habitica.com>
Date:   Mon May 1 16:54:12 2023 -0500

    Merge branch 'release' into sabrecat/party-seeking

commit 728ed2ddad7f0962d28f1ab0a271e3555b19296c
Author: SabreCat <sabe@habitica.com>
Date:   Thu Apr 27 16:51:06 2023 -0500

    fix(lfp): loading layout and page visit event

commit 8a56ab329bff922e05963e3ef78fbc26ff273924
Author: SabreCat <sabe@habitica.com>
Date:   Wed Apr 26 16:54:46 2023 -0500

    fix(faq): copy and style updates

commit 6fd00d7f30150a1802e5a37edbb914ef120caf9a
Author: SabreCat <sabe@habitica.com>
Date:   Fri Apr 21 17:12:52 2023 -0500

    feat(lfp): fixes, analytics, FAQ

commit 4b5d7304ad7cfc5f72320b23456ed2898e53caac
Author: SabreCat <sabe@habitica.com>
Date:   Mon Apr 17 15:13:03 2023 -0500

    fix(lfp): smol tweaks

commit 9a5476a2558eb17a603f4aae1b5b2d35773be8b4
Author: SabreCat <sabe@habitica.com>
Date:   Thu Apr 13 16:03:33 2023 -0500

    feat(lfp): refresh button

commit aa58f5018469f38a9a9d31c3bffa26bb88a8c672
Merge: bbb03d006e c8adf20804
Author: SabreCat <sabe@habitica.com>
Date:   Tue Apr 11 17:44:56 2023 -0500

    Merge branch 'release' into sabrecat/party-seeking

commit bbb03d006e8b122bb7206bdc778a31de422167bb
Author: SabreCat <sabe@habitica.com>
Date:   Tue Apr 4 18:30:50 2023 -0500

    fix(lint): whitespace and const

commit 23683ad29a4cce0b0da061ad6c030982034c0a9c
Author: SabreCat <sabe@habitica.com>
Date:   Tue Apr 4 17:02:57 2023 -0500

    chore(LFP): add analytics
    also re-fix loading state

commit 4477d84f5266c87f5583368029b72153f00f0568
Author: SabreCat <sabe@habitica.com>
Date:   Mon Apr 3 16:24:26 2023 -0500

    fix(LFP): address issues with loading

commit bdc5154f24bb5e50963376c3c0c9cc73c0b05ccc
Merge: 81923eef6f 229ed46425
Author: SabreCat <sabe@habitica.com>
Date:   Mon Apr 3 15:58:12 2023 -0500

    Merge branch 'release' into sabrecat/party-seeking

commit 81923eef6f0c627d079475a28f9d93d8e4628934
Author: SabreCat <sabe@habitica.com>
Date:   Thu Mar 30 16:44:49 2023 -0500

    feat(LFP): release candidate

commit fe1f8939fc6b09d36cfaf0b6e5838df04e41009d
Author: SabreCat <sabe@habitica.com>
Date:   Wed Mar 29 17:35:54 2023 -0500

    WIP(LFP): fixes

commit afc361f5a9f806cbd814ad910d1274e3a6609efd
Merge: d6b5cbdebc 7ede3acd01
Author: SabreCat <sabe@habitica.com>
Date:   Wed Mar 29 16:24:39 2023 -0500

    Merge branch 'release' into sabrecat/party-seeking

commit d6b5cbdebc2829e9325ea57fb5deeccc128c1635
Author: SabreCat <sabe@habitica.com>
Date:   Tue Mar 28 16:13:18 2023 -0500

    WIP(LFP): change copy, add close X, fix API response

commit 4274a4625862351ef0ecf33c8a3249ca5ebec7cb
Author: SabreCat <sabe@habitica.com>
Date:   Mon Mar 27 17:07:36 2023 -0500

    fix(LFP): layout, unset when stopping

commit 95abfcfa5f13c9cce6385206947a47f85b76d11d
Merge: 4a360eedd8 53c536b525
Author: SabreCat <sabe@habitica.com>
Date:   Mon Mar 27 16:32:46 2023 -0500

    Merge branch 'release' into sabrecat/party-seeking

commit 4a360eedd8b9cf41d3a0fe7a4cfaa72c5bd7bd26
Author: SabreCat <sabe@habitica.com>
Date:   Thu Mar 23 16:54:49 2023 -0500

    feat(LFP): completed style and infinite scroll

commit bbc439d9d03c9631a450236eb33af66f0428fa50
Author: SabreCat <sabe@habitica.com>
Date:   Tue Mar 21 10:40:26 2023 -0500

    WIP(LFP): nicer layout, buffs fix

commit 1658688597456663477ab19da61ae1b9bc85cf2a
Merge: 664507434f 027e61a93e
Author: SabreCat <sabe@habitica.com>
Date:   Tue Mar 21 09:29:02 2023 -0500

    Merge branch 'release' into sabrecat/party-seeking

commit 664507434f2f76e6bf3b61cdc9e3daddb81204af
Author: SabreCat <sabe@habitica.com>
Date:   Fri Mar 17 17:13:08 2023 -0500

    WIP(LFP): API and client adjustments

commit 2f0b6f2517f9e2d634cb23ee303cfb4542e998ce
Merge: 0db1704325 a16098ccda
Author: SabreCat <sabe@habitica.com>
Date:   Fri Mar 17 16:45:13 2023 -0500

    Merge branch 'release' into sabrecat/party-seeking

commit 0db1704325c3555f0b5d9c8d1dfc44177e90c093
Author: SabreCat <sabe@habitica.com>
Date:   Mon Mar 13 14:48:10 2023 -0500

    fix(modal): scrollbar for squashed viewports

commit 733c35192e0a4e31e1bebfdd7488cfc1f7587f36
Author: SabreCat <sabe@habitica.com>
Date:   Thu Mar 9 12:51:02 2023 -0600

    WIP(party): seekers functional rough

commit d4b854410b557db26eec6e6a26b6d174c02cee3a
Merge: 7fe919825a 0b6b967753
Author: SabreCat <sabe@habitica.com>
Date:   Thu Mar 9 10:07:28 2023 -0600

    Merge branch 'release' into sabrecat/party-seeking

commit 7fe919825abfb6d518cb93b91f5997d3831bd0b5
Author: SabreCat <sabe@habitica.com>
Date:   Thu Mar 2 14:40:09 2023 -0600

    feat(party): several adjustments to seeking feature

commit c93900efcf925f7aaa4c4cb56b4451f19adfb1b3
Author: SabreCat <sabe@habitica.com>
Date:   Wed Mar 1 20:37:11 2023 -0600

    feat(party): initial Seeking API

commit 8bb784daeceb14c23992a6f3af1054a900fc26c1
Merge: e19a661a21 f327795761
Author: SabreCat <sabe@habitica.com>
Date:   Wed Mar 1 18:58:20 2023 -0600

    Merge branch 'release' into sabrecat/party-seeking

commit e19a661a2163a50307a286379bffb44201ed392e
Author: SabreCat <sabe@habitica.com>
Date:   Fri Feb 24 15:51:42 2023 -0600

    WIP(parties): add seeking flag and modal toggle
2023-05-02 09:51:33 -05:00
SabreCat
9e0777bb42 fix(group-plans): tokenize and update ad text 2023-05-01 16:30:21 -05:00
SabreCat
c1532996d8 4.268.1 2023-05-01 10:47:53 -05:00
SabreCat
5aa2d9c68d fix(stats): allow negative HP 2023-05-01 10:47:42 -05:00
SabreCat
6ed5a0f44b 4.268.0 2023-04-28 16:19:07 -05:00
Natalie L
76845f5f20 feat(content): add May subscriber items (#14613)
Co-authored-by: SabreCat <sabe@habitica.com>
2023-04-28 16:18:45 -05:00
SabreCat
c931823f62 chore(subproj): update habitica-images 2023-04-28 15:55:06 -05:00
dependabot[bot]
b159182188 build(deps): bump json5 from 1.0.1 to 1.0.2 (#14441)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-28 16:39:05 -04:00
dependabot[bot]
ca1b8370a0 build(deps): bump stopword from 2.0.7 to 2.0.8 in /website/client (#14560)
Bumps [stopword](https://github.com/fergiemcdowall/stopword) from 2.0.7 to 2.0.8.
- [Release notes](https://github.com/fergiemcdowall/stopword/releases)
- [Commits](https://github.com/fergiemcdowall/stopword/compare/v.2.0.7...v2.0.8)

---
updated-dependencies:
- dependency-name: stopword
  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-04-28 16:35:11 -04:00
dependabot[bot]
a397da2b93 build(deps): bump jquery from 3.6.3 to 3.6.4 in /website/client (#14540)
Bumps [jquery](https://github.com/jquery/jquery) from 3.6.3 to 3.6.4.
- [Release notes](https://github.com/jquery/jquery/releases)
- [Commits](https://github.com/jquery/jquery/compare/3.6.3...3.6.4)

---
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-04-28 16:32:06 -04:00
dependabot[bot]
b5acc0e0d6 build(deps-dev): bump @babel/plugin-proposal-optional-chaining (#14522)
Bumps [@babel/plugin-proposal-optional-chaining](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-proposal-optional-chaining) from 7.20.7 to 7.21.0.
- [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.21.0/packages/babel-plugin-proposal-optional-chaining)

---
updated-dependencies:
- dependency-name: "@babel/plugin-proposal-optional-chaining"
  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-04-28 16:30:42 -04:00
dependabot[bot]
2635c5fcee build(deps): bump @babel/register from 7.18.9 to 7.21.0 (#14517)
Bumps [@babel/register](https://github.com/babel/babel/tree/HEAD/packages/babel-register) from 7.18.9 to 7.21.0.
- [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.21.0/packages/babel-register)

---
updated-dependencies:
- dependency-name: "@babel/register"
  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-04-28 16:29:55 -04:00
dependabot[bot]
ee2936834a build(deps): bump body-parser from 1.20.1 to 1.20.2 (#14518)
Bumps [body-parser](https://github.com/expressjs/body-parser) from 1.20.1 to 1.20.2.
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.1...1.20.2)

---
updated-dependencies:
- dependency-name: body-parser
  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-04-28 16:28:07 -04:00
dependabot[bot]
c94a5304c7 build(deps): bump xml2js from 0.4.23 to 0.5.0 (#14574)
Bumps [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js) from 0.4.23 to 0.5.0.
- [Release notes](https://github.com/Leonidas-from-XIV/node-xml2js/releases)
- [Commits](https://github.com/Leonidas-from-XIV/node-xml2js/commits)

---
updated-dependencies:
- dependency-name: xml2js
  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-04-28 16:07:53 -04:00
dependabot[bot]
c6b004a474 build(deps): bump apple-auth from 1.0.7 to 1.0.9 (#14578)
Bumps [apple-auth](https://github.com/ananay/apple-auth) from 1.0.7 to 1.0.9.
- [Release notes](https://github.com/ananay/apple-auth/releases)
- [Commits](https://github.com/ananay/apple-auth/compare/1.0.7...1.0.9)

---
updated-dependencies:
- dependency-name: apple-auth
  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-04-28 16:07:01 -04:00
dependabot[bot]
de918ec43b build(deps): bump core-js from 3.27.2 to 3.30.1 in /website/client (#14601)
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.27.2 to 3.30.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.30.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-04-28 15:41:26 -04:00
dependabot[bot]
069e994b25 build(deps): bump @babel/core from 7.20.12 to 7.21.4 (#14566)
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.20.12 to 7.21.4.
- [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.21.4/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  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-04-28 15:39:50 -04:00
dependabot[bot]
663692f2d5 chore(deps-dev): bump sinon from 15.0.1 to 15.0.4 (#14605)
Bumps [sinon](https://github.com/sinonjs/sinon) from 15.0.1 to 15.0.4.
- [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.0.1...v15.0.4)

---
updated-dependencies:
- dependency-name: sinon
  dependency-type: direct:development
  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-04-28 15:38:03 -04:00
dependabot[bot]
0ba4761083 chore(deps-dev): bump axios from 1.2.2 to 1.3.6 (#14606)
Bumps [axios](https://github.com/axios/axios) from 1.2.2 to 1.3.6.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/1.2.2...v1.3.6)

---
updated-dependencies:
- dependency-name: axios
  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-04-28 15:37:30 -04:00
dependabot[bot]
afad3815a2 chore(deps): bump smartbanner.js in /website/client (#14610)
Bumps [smartbanner.js](https://github.com/ain/smartbanner.js) from 1.19.1 to 1.19.2.
- [Release notes](https://github.com/ain/smartbanner.js/releases)
- [Commits](https://github.com/ain/smartbanner.js/compare/v1.19.1...v1.19.2)

---
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-04-28 15:25:55 -04:00
SabreCat
33f0a11f19 4.267.3 2023-04-27 15:35:55 -05:00
SabreCat
6cb3dcd76a fix(fcv): disallow negative values 2023-04-27 15:21:37 -05:00
SabreCat
61e41b539d feat(admin): show 3p note in panel 2023-04-26 16:13:42 -05:00
SabreCat
d8cb8869e9 Merge branch 'release' into sabrecat/3p-fixes 2023-04-26 15:41:53 -05:00
SabreCat
57027a1a62 4.267.2 2023-04-25 15:50:55 -05:00
SabreCat
92b4a8029d fix(links): handle meta key 2023-04-25 15:14:59 -05:00
SabreCat
7b4dd36827 4.267.1 2023-04-20 15:30:46 -05:00
SabreCat
be65042463 Merge branch 'external-link-modal' into release 2023-04-20 15:30:37 -05:00
SabreCat
cccb6a9c02 Merge branch 'develop' into release 2023-04-20 15:30:29 -05:00
SabreCat
0ac2f53405 fix(3p): update timing 1/day at most 2023-04-20 15:19:05 -05:00
Weblate
916c7c49e7 Translated using Weblate (Danish)
Currently translated at 79.2% (321 of 405 strings)

Translated using Weblate (Slovak)

Currently translated at 54.5% (119 of 218 strings)

Translated using Weblate (Slovak)

Currently translated at 67.0% (63 of 94 strings)

Translated using Weblate (Hebrew)

Currently translated at 70.9% (93 of 131 strings)

Translated using Weblate (Slovak)

Currently translated at 80.2% (325 of 405 strings)

Translated using Weblate (Slovak)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Hebrew)

Currently translated at 45.5% (1281 of 2813 strings)

Translated using Weblate (Slovak)

Currently translated at 87.9% (160 of 182 strings)

Translated using Weblate (Hebrew)

Currently translated at 74.1% (135 of 182 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Slovak)

Currently translated at 53.2% (410 of 770 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Malay)

Currently translated at 47.2% (128 of 271 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2813 of 2813 strings)

Translated using Weblate (Hebrew)

Currently translated at 45.2% (1272 of 2813 strings)

Translated using Weblate (Hebrew)

Currently translated at 76.1% (163 of 214 strings)

Translated using Weblate (Hebrew)

Currently translated at 91.9% (137 of 149 strings)

Translated using Weblate (Hebrew)

Currently translated at 87.9% (131 of 149 strings)

Translated using Weblate (Hebrew)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Italian)

Currently translated at 98.1% (2760 of 2813 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Hebrew)

Currently translated at 61.0% (133 of 218 strings)

Translated using Weblate (Hebrew)

Currently translated at 64.2% (72 of 112 strings)

Translated using Weblate (Hebrew)

Currently translated at 67.9% (89 of 131 strings)

Translated using Weblate (Hebrew)

Currently translated at 26.1% (71 of 271 strings)

Translated using Weblate (Hebrew)

Currently translated at 45.6% (185 of 405 strings)

Translated using Weblate (Hebrew)

Currently translated at 45.1% (1271 of 2813 strings)

Translated using Weblate (Hebrew)

Currently translated at 82.9% (39 of 47 strings)

Translated using Weblate (Hebrew)

Currently translated at 84.4% (157 of 186 strings)

Translated using Weblate (Ukrainian)

Currently translated at 28.4% (799 of 2813 strings)

Translated using Weblate (Indonesian)

Currently translated at 62.7% (170 of 271 strings)

Translated using Weblate (Hebrew)

Currently translated at 75.1% (112 of 149 strings)

Translated using Weblate (Hebrew)

Currently translated at 73.1% (109 of 149 strings)

Translated using Weblate (Hebrew)

Currently translated at 73.1% (109 of 149 strings)

Translated using Weblate (Hebrew)

Currently translated at 73.1% (109 of 149 strings)

Translated using Weblate (Hebrew)

Currently translated at 69.1% (103 of 149 strings)

Translated using Weblate (Hebrew)

Currently translated at 69.1% (103 of 149 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2809 of 2809 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2809 of 2809 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Irish)

Currently translated at 12.5% (14 of 112 strings)

Translated using Weblate (Irish)

Currently translated at 60.4% (1700 of 2813 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.0% (144 of 218 strings)

Translated using Weblate (Irish)

Currently translated at 10.7% (12 of 112 strings)

Translated using Weblate (Irish)

Currently translated at 74.4% (111 of 149 strings)

Translated using Weblate (Irish)

Currently translated at 60.3% (1698 of 2813 strings)

Translated using Weblate (Irish)

Currently translated at 5.3% (6 of 112 strings)

Translated using Weblate (Irish)

Currently translated at 100.0% (1 of 1 strings)

Translated using Weblate (Irish)

Currently translated at 74.4% (111 of 149 strings)

Translated using Weblate (Irish)

Currently translated at 50.0% (4 of 8 strings)

Translated using Weblate (Irish)

Currently translated at 89.2% (191 of 214 strings)

Translated using Weblate (Irish)

Currently translated at 29.6% (16 of 54 strings)

Translated using Weblate (Irish)

Currently translated at 93.3% (14 of 15 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Indonesian)

Currently translated at 95.6% (174 of 182 strings)

Translated using Weblate (Malay)

Currently translated at 48.0% (87 of 181 strings)

Translated using Weblate (Arabic)

Currently translated at 45.0% (27 of 60 strings)

Translated using Weblate (Arabic)

Currently translated at 56.8% (124 of 218 strings)

Translated using Weblate (Arabic)

Currently translated at 81.1% (620 of 764 strings)

Translated using Weblate (Arabic)

Currently translated at 61.4% (75 of 122 strings)

Translated using Weblate (French)

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (French)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (French)

Currently translated at 100.0% (222 of 222 strings)

Translated using Weblate (Malay)

Currently translated at 45.0% (122 of 271 strings)

Translated using Weblate (Turkish)

Currently translated at 95.7% (90 of 94 strings)

Translated using Weblate (Turkish)

Currently translated at 95.5% (107 of 112 strings)

Translated using Weblate (Ukrainian)

Currently translated at 27.6% (777 of 2813 strings)

Translated using Weblate (Indonesian)

Currently translated at 68.5% (37 of 54 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (121 of 121 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Turkish)

Currently translated at 87.9% (131 of 149 strings)

Translated using Weblate (Malay)

Currently translated at 44.6% (121 of 271 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Malay)

Currently translated at 46.6% (28 of 60 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (222 of 222 strings)

Translated using Weblate (Ukrainian)

Currently translated at 74.0% (566 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Indonesian)

Currently translated at 94.6% (356 of 376 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (222 of 222 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (222 of 222 strings)

Translated using Weblate (Indonesian)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (121 of 121 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Spanish)

Currently translated at 91.9% (2587 of 2813 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Galician)

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

Translated using Weblate (Galician)

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (221 of 221 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (376 of 376 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (221 of 221 strings)

Translated using Weblate (Belarusian)

Currently translated at 4.0% (6 of 149 strings)

Translated using Weblate (Hungarian)

Currently translated at 55.0% (120 of 218 strings)

Translated using Weblate (Hungarian)

Currently translated at 78.6% (103 of 131 strings)

Translated using Weblate (Hungarian)

Currently translated at 39.8% (108 of 271 strings)

Translated using Weblate (Hungarian)

Currently translated at 74.3% (301 of 405 strings)

Translated using Weblate (Ukrainian)

Currently translated at 27.4% (772 of 2813 strings)

Translated using Weblate (Indonesian)

Currently translated at 61.0% (1718 of 2813 strings)

Translated using Weblate (Hungarian)

Currently translated at 60.3% (1697 of 2813 strings)

Translated using Weblate (Hungarian)

Currently translated at 88.4% (161 of 182 strings)

Translated using Weblate (Hungarian)

Currently translated at 66.6% (40 of 60 strings)

Translated using Weblate (Hungarian)

Currently translated at 73.8% (564 of 764 strings)

Translated using Weblate (Hungarian)

Currently translated at 62.2% (76 of 122 strings)

Translated using Weblate (Hungarian)

Currently translated at 60.4% (90 of 149 strings)

Translated using Weblate (Hungarian)

Currently translated at 57.0% (85 of 149 strings)

Translated using Weblate (Ukrainian)

Currently translated at 27.4% (771 of 2813 strings)

Translated using Weblate (Russian)

Currently translated at 85.2% (104 of 122 strings)

Translated using Weblate (Malay)

Currently translated at 46.4% (52 of 112 strings)

Translated using Weblate (Malay)

Currently translated at 53.5% (412 of 770 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Arabic)

Currently translated at 86.1% (663 of 770 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Indonesian)

Currently translated at 81.6% (107 of 131 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (121 of 121 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Malay)

Currently translated at 43.5% (118 of 271 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.5% (217 of 218 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (121 of 121 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 63.9% (78 of 122 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 63.9% (78 of 122 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2813 of 2813 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Galician)

Currently translated at 73.0% (296 of 405 strings)

Translated using Weblate (Spanish)

Currently translated at 91.9% (2587 of 2813 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Galician)

Currently translated at 64.6% (262 of 405 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (218 of 218 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Galician)

Currently translated at 42.4% (115 of 271 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Galician)

Currently translated at 40.9% (166 of 405 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Croatian)

Currently translated at 99.2% (269 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2809 of 2809 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (121 of 121 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 71.5% (133 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Galician)

Currently translated at 94.3% (202 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Ukrainian)

Currently translated at 73.8% (564 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Croatian)

Currently translated at 99.2% (764 of 770 strings)

Translated using Weblate (Croatian)

Currently translated at 99.2% (134 of 135 strings)

Translated using Weblate (Croatian)

Currently translated at 92.3% (12 of 13 strings)

Translated using Weblate (Croatian)

Currently translated at 80.7% (617 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 73.6% (563 of 764 strings)

Translated using Weblate (Croatian)

Currently translated at 84.0% (647 of 770 strings)

Translated using Weblate (Croatian)

Currently translated at 83.8% (646 of 770 strings)

Translated using Weblate (Croatian)

Currently translated at 83.7% (645 of 770 strings)

Translated using Weblate (Croatian)

Currently translated at 83.7% (645 of 770 strings)

Translated using Weblate (Croatian)

Currently translated at 83.6% (644 of 770 strings)

Translated using Weblate (Croatian)

Currently translated at 83.5% (643 of 770 strings)

Translated using Weblate (Galician)

Currently translated at 96.7% (59 of 61 strings)

Translated using Weblate (Croatian)

Currently translated at 83.3% (642 of 770 strings)

Translated using Weblate (Croatian)

Currently translated at 83.2% (641 of 770 strings)

Translated using Weblate (Croatian)

Currently translated at 83.1% (640 of 770 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 71.5% (133 of 186 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Galician)

Currently translated at 94.9% (356 of 375 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Galician)

Currently translated at 94.3% (202 of 214 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (2 of 2 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (1 of 1 strings)

Translated using Weblate (French)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Croatian)

Currently translated at 91.7% (167 of 182 strings)

Translated using Weblate (Croatian)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Croatian)

Currently translated at 98.6% (147 of 149 strings)

Translated using Weblate (Croatian)

Currently translated at 96.3% (261 of 271 strings)

Translated using Weblate (Croatian)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Indonesian)

Currently translated at 74.0% (566 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (121 of 121 strings)

Translated using Weblate (Croatian)

Currently translated at 36.2% (66 of 182 strings)

Translated using Weblate (Croatian)

Currently translated at 75.0% (6 of 8 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Ukrainian)

Currently translated at 73.4% (561 of 764 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (121 of 121 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (121 of 121 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (1 of 1 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (1 of 1 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Turkish)

Currently translated at 60.7% (468 of 770 strings)

Translated using Weblate (Malay)

Currently translated at 44.7% (81 of 181 strings)

Translated using Weblate (Indonesian)

Currently translated at 98.3% (119 of 121 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Galician)

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 (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Galician)

Currently translated at 42.3% (326 of 770 strings)

Translated using Weblate (Galician)

Currently translated at 65.7% (502 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 40.9% (166 of 405 strings)

Translated using Weblate (Galician)

Currently translated at 94.9% (356 of 375 strings)

Translated using Weblate (Galician)

Currently translated at 43.6% (336 of 770 strings)

Translated using Weblate (Galician)

Currently translated at 66.6% (509 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 42.4% (172 of 405 strings)

Translated using Weblate (Galician)

Currently translated at 96.8% (363 of 375 strings)

Translated using Weblate (Ukrainian)

Currently translated at 72.9% (557 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 49.4% (381 of 770 strings)

Translated using Weblate (Galician)

Currently translated at 70.6% (540 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 36.1% (98 of 271 strings)

Translated using Weblate (Galician)

Currently translated at 51.3% (208 of 405 strings)

Translated using Weblate (Indonesian)

Currently translated at 64.5% (140 of 217 strings)

Translated using Weblate (Ukrainian)

Currently translated at 26.9% (756 of 2809 strings)

Translated using Weblate (Ukrainian)

Currently translated at 71.5% (547 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 64.2% (36 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 63.3% (83 of 131 strings)

Translated using Weblate (Galician)

Currently translated at 93.3% (56 of 60 strings)

Translated using Weblate (Galician)

Currently translated at 71.5% (133 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 52.7% (406 of 770 strings)

Translated using Weblate (Galician)

Currently translated at 36.8% (80 of 217 strings)

Translated using Weblate (Galician)

Currently translated at 76.9% (10 of 13 strings)

Translated using Weblate (Galician)

Currently translated at 73.2% (560 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 79.7% (75 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 87.5% (7 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 10.0% (15 of 149 strings)

Translated using Weblate (Galician)

Currently translated at 77.0% (47 of 61 strings)

Translated using Weblate (Galician)

Currently translated at 36.5% (99 of 271 strings)

Translated using Weblate (Galician)

Currently translated at 56.2% (228 of 405 strings)

Translated using Weblate (Galician)

Currently translated at 94.3% (202 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 60.7% (1706 of 2809 strings)

Translated using Weblate (Galician)

Currently translated at 98.1% (53 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 93.6% (44 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 43.4% (53 of 122 strings)

Translated using Weblate (Ukrainian)

Currently translated at 26.9% (756 of 2809 strings)

Translated using Weblate (Indonesian)

Currently translated at 94.9% (356 of 375 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Ukrainian)

Currently translated at 71.5% (547 of 764 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Galician)

Currently translated at 48.3% (29 of 60 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (54 of 54 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Galician)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Malay)

Currently translated at 42.4% (115 of 271 strings)

Translated using Weblate (Ukrainian)

Currently translated at 26.4% (742 of 2809 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Ukrainian)

Currently translated at 71.2% (544 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 92.0% (2587 of 2809 strings)

Translated using Weblate (Ukrainian)

Currently translated at 70.9% (542 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 80.1% (105 of 131 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2809 of 2809 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2809 of 2809 strings)

Translated using Weblate (Ukrainian)

Currently translated at 26.0% (732 of 2809 strings)

Translated using Weblate (Dutch)

Currently translated at 98.3% (120 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 95.8% (116 of 121 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (270 of 271 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (2804 of 2809 strings)

Translated using Weblate (Malay)

Currently translated at 42.8% (48 of 112 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.2% (269 of 271 strings)

Translated using Weblate (Indonesian)

Currently translated at 77.5% (314 of 405 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.6% (2799 of 2809 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2809 of 2809 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Polish)

Currently translated at 87.0% (189 of 217 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.5% (267 of 271 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.0% (2782 of 2809 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.0% (2782 of 2809 strings)

Translated using Weblate (Polish)

Currently translated at 96.6% (144 of 149 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2809 of 2809 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2809 of 2809 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Indonesian)

Currently translated at 98.1% (210 of 214 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (186 of 186 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (French)

Currently translated at 99.1% (2786 of 2809 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (French)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (French)

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (French)

Currently translated at 99.0% (2783 of 2809 strings)

Translated using Weblate (French)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.9% (2808 of 2809 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Spanish)

Currently translated at 92.9% (252 of 271 strings)

Translated using Weblate (Ukrainian)

Currently translated at 25.0% (704 of 2809 strings)

Translated using Weblate (Dutch)

Currently translated at 88.0% (2474 of 2809 strings)

Translated using Weblate (Ukrainian)

Currently translated at 70.5% (539 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Spanish)

Currently translated at 99.7% (768 of 770 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Spanish)

Currently translated at 99.5% (220 of 221 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Ukrainian)

Currently translated at 67.6% (517 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.7% (404 of 405 strings)

Translated using Weblate (Spanish)

Currently translated at 92.0% (2587 of 2809 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (271 of 271 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (2805 of 2809 strings)

Translated using Weblate (Dutch)

Currently translated at 87.3% (2453 of 2809 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Indonesian)

Currently translated at 86.8% (53 of 61 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.1% (2785 of 2809 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Serbian)

Currently translated at 28.1% (42 of 149 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (404 of 404 strings)

Translated using Weblate (Indonesian)

Currently translated at 61.7% (1718 of 2781 strings)

Translated using Weblate (Indonesian)

Currently translated at 66.6% (36 of 54 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (221 of 221 strings)

Translated using Weblate (Indonesian)

Currently translated at 62.9% (34 of 54 strings)

Translated using Weblate (Indonesian)

Currently translated at 62.9% (34 of 54 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (404 of 404 strings)

Translated using Weblate (Serbian)

Currently translated at 26.1% (39 of 149 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.3% (148 of 149 strings)

Translated using Weblate (Serbian)

Currently translated at 24.8% (37 of 149 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (404 of 404 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (149 of 149 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (149 of 149 strings)

Co-authored-by: Adrián Chaves Fernández <adrian@chaves.io>
Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Andressa Murari Sudré <andressa@sudre.com.br>
Co-authored-by: Arthur Ouzlaner <panther1984@gmail.com>
Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Delta S <deseji93@gmail.com>
Co-authored-by: Diana <dianazabiyaka@gmail.com>
Co-authored-by: Duygu Erates <duygu.erates@gmail.com>
Co-authored-by: Edward McGibney <edwardmcgibney95@gmail.com>
Co-authored-by: Ellen A M <ellen_a_m@hotmail.com>
Co-authored-by: Ewa Trybuszewski <trybuszewskiewa@gmail.com>
Co-authored-by: Falzart Werefox <muh_fauzi_ramadhan@yahoo.co.id>
Co-authored-by: HURZHII ORYNA <radovaarina@gmail.com>
Co-authored-by: Hanafi <naflizo@gmail.com>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: Jerry Chen <minecjraft@qq.com>
Co-authored-by: Kit Postovnev <sahnonik231@gmail.com>
Co-authored-by: Kovács Máté <kovacsur10@gmail.com>
Co-authored-by: Lauren C <laurenc7834@gmail.com>
Co-authored-by: LiziKnight <liziknight0316@outlook.com>
Co-authored-by: Luna <lunadammandersen@gmail.com>
Co-authored-by: M <maperray@gmail.com>
Co-authored-by: Matúš Goljer <matus.goljer@gmail.com>
Co-authored-by: Michael <mishaopanasuk3@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Ofek yeshurun <ofek.yeshurun@gmail.com>
Co-authored-by: Raithe <RaitheOfDureya@gmail.com>
Co-authored-by: Rara <annisarahmah.xmipa2@gmail.com>
Co-authored-by: Razi H <razi.haj@gmail.com>
Co-authored-by: Sabe Jones <sabe@habitica.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sciuridae <sweetvshoney@163.com>
Co-authored-by: Stefan Trbojević <garkogidre@gufum.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yuliia Pastukh <yuliya.bratash666@gmail.com>
Co-authored-by: baozidai <baozidai@outlook.com>
Co-authored-by: burcu atalay <burcuatalay1996@gmail.com>
Co-authored-by: elsaid ata <elsaidata75@gmail.com>
Co-authored-by: fluffstuff <opositesandreality@gmail.com>
Co-authored-by: helio serra carmo junior <theancientguardian@protonmail.com>
Co-authored-by: Коваленко Олексій Володимирович <alex59.kovalenko@gmail.com>
Co-authored-by: Павло Оборін <paka28065@gmail.com>
Co-authored-by: そら <comi4work@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/be/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ga/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/he/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/id/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/it/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pl/
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/achievements/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/character/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/character/he/
Translate-URL: https://translate.habitica.com/projects/habitica/character/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/character/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/es/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/id/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/content/es/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/content/he/
Translate-URL: https://translate.habitica.com/projects/habitica/content/id/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/he/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/death/ga/
Translate-URL: https://translate.habitica.com/projects/habitica/death/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/death/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/ga/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ms/
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/he/
Translate-URL: https://translate.habitica.com/projects/habitica/front/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/front/id/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/front/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/ga/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/he/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/id/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
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/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/ga/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/he/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/id/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/da/
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/he/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/id/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/sk/
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/inventory/ga/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
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/he/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/id/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/merch/ga/
Translate-URL: https://translate.habitica.com/projects/habitica/merch/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/id/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/noscript/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/he/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/id/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ga/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/he/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/tr/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/id/
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/rebirth/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/es/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/id/
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/settings/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/he/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ar/
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/he/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hu/
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/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/gl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/hr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/id/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/zh_Hans/
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/Inventory
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Merch
Translation: Habitica/Messages
Translation: Habitica/Noscript
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-04-20 18:55:54 +02:00
SabreCat
3cf5b90f04 fix(3p): bad import, change flag format 2023-04-19 09:33:03 -05:00
SabreCat
86efb02358 fix(api): address issues caused by 3p tools
and flag accounts that use them
2023-04-18 15:43:35 -05:00
SabreCat
164121d9e4 fix(links): handling in RYA 2023-04-17 14:48:55 -05:00
SabreCat
a2d209a34b Merge branch 'release' into external-link-modal 2023-04-17 14:41:59 -05:00
SabreCat
c8adf20804 4.267.0 2023-04-10 17:09:37 -05:00
Sabe Jones
de132c59ea fix(typo): . 2023-04-10 16:54:15 -05:00
SabreCat
e5f6c4ba0f fix(links): handle uhuhu 2023-04-10 16:04:46 -05:00
CuriousMagpie
0c85835dc2 update to sellModal, buyModal, and questDialogContent.vue 2023-04-07 10:41:43 -04:00
CuriousMagpie
54df8397a7 fixes to svg and other spacing 2023-04-06 17:00:37 -04:00
CuriousMagpie
360c17c56e feat(content): add April backgrounds and enchanted armoire item 2023-04-06 15:37:32 -04:00
SabreCat
c8b98678d0 Merge branch 'release' into develop 2023-04-05 14:04:45 -05:00
SabreCat
ea7e5d2a8d 4.266.0 2023-04-05 14:04:34 -05:00
SabreCat
afee09e7cb fix(tz): adjust release timing for DST 2023-04-05 14:02:38 -05:00
CuriousMagpie
0644032a4f style: more spacing updates 2023-04-05 13:50:14 -04:00
CuriousMagpie
01fea6b968 feat(fix): add correct string for potion end date 2023-04-05 12:13:40 -04:00
SabreCat
2df6b6461b fix(links): better http/s handling 2023-04-04 18:25:14 -05:00
CuriousMagpie
8c96ac241a updated stylesheet 2023-04-04 17:42:45 -04:00
CuriousMagpie
c0362c614e feat(content): add April fool day potions 2023-04-04 17:14:25 -04:00
CuriousMagpie
44265ac616 style: more spacing updates 2023-04-03 14:56:43 -04:00
SabreCat
229ed46425 fix(tz): dst 2023-04-01 07:08:38 -05:00
CuriousMagpie
ac3b953633 style: vertical spacing tweaks 2023-03-30 09:07:37 -04:00
CuriousMagpie
5de2921d22 Merge branch 'develop' into increment-component 2023-03-30 08:33:24 -04:00
SabreCat
7363f08a86 fix(links): handle ext links in profiles 2023-03-29 16:24:06 -05:00
SabreCat
2322f7e342 Merge branch 'release' into external-link-modal 2023-03-29 16:00:53 -05:00
SabreCat
7ede3acd01 feat(event): add AF2023 event const 2023-03-29 15:35:26 -05:00
SabreCat
57f17a08e8 Merge branch 'release' into develop 2023-03-29 15:08:02 -05:00
SabreCat
63453ce01b 4.265.0 2023-03-29 15:07:47 -05:00
SabreCat
888f6f2486 Merge branch 'sabrecat/fools-2023' into release 2023-03-29 15:07:40 -05:00
Natalie L
e8501f5cf8 feat(content): Add April 2023 subscriber items (#14562)
* feat(content): add April Subscriber items

* feat(content): add slim armor version to April subscriber items
2023-03-29 14:53:51 -05:00
Natalie L
fc49015ff0 chore(content): update press kit faq and third party app list in settings (#14554)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2781 of 2781 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Portuguese)

Currently translated at 81.7% (331 of 405 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Portuguese)

Currently translated at 62.5% (5 of 8 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Portuguese)

Currently translated at 63.8% (1777 of 2781 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Portuguese)

Currently translated at 71.6% (43 of 60 strings)

Translated using Weblate (Portuguese)

Currently translated at 79.7% (609 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (German)

Currently translated at 100.0% (404 of 404 strings)

Translated using Weblate (German)

Currently translated at 79.5% (97 of 122 strings)

Translated using Weblate (German)

Currently translated at 97.7% (753 of 770 strings)

Translated using Weblate (German)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Portuguese)

Currently translated at 77.0% (94 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.7% (310 of 404 strings)

Translated using Weblate (Portuguese)

Currently translated at 76.2% (93 of 122 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 85.6% (2381 of 2781 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (404 of 404 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 95.8% (732 of 764 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Portuguese)

Currently translated at 74.5% (91 of 122 strings)

Translated using Weblate (Portuguese)

Currently translated at 72.1% (88 of 122 strings)

Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Andressa Murari Sudré <andressa@sudre.com.br>
Co-authored-by: Cachinhos <cachnhos@gmail.com>
Co-authored-by: Falzart Werefox <muh_fauzi_ramadhan@yahoo.co.id>
Co-authored-by: Jay <fallacyofwildlifeconservation@gmail.com>
Co-authored-by: Jerry Chen <minecjraft@qq.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yu-Wei Tien <a38498987911@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/de/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/de/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/id/
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/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
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/Limited
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks

* update press kit and third party app list in settings

---------

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Andressa Murari Sudré <andressa@sudre.com.br>
Co-authored-by: Cachinhos <cachnhos@gmail.com>
Co-authored-by: Falzart Werefox <muh_fauzi_ramadhan@yahoo.co.id>
Co-authored-by: Jay <fallacyofwildlifeconservation@gmail.com>
Co-authored-by: Jerry Chen <minecjraft@qq.com>
Co-authored-by: Yu-Wei Tien <a38498987911@gmail.com>
Co-authored-by: SabreCat <sabe@habitica.com>
2023-03-28 16:52:15 -05:00
SabreCat
eb4e930e63 fix(ext-links): warn in Party and PMs, env config 2023-03-28 16:39:48 -05:00
CuriousMagpie
c1a0f8a8d1 style: buyModal, sellModal, buyQuestModal, questRewards 2023-03-28 15:07:49 -04:00
CuriousMagpie
7e9506391f more buyModal styling 2023-03-27 16:13:06 -04:00
CuriousMagpie
3c7ca56089 buyModal styling 2023-03-27 15:58:56 -04:00
CuriousMagpie
0d155535c3 Merge branch 'develop' into increment-component 2023-03-27 14:16:55 -04:00
SabreCat
53c536b525 4.264.4 2023-03-24 19:46:32 -05:00
SabreCat
e2defc675e fix(css): don't make invisible links 2023-03-24 19:46:11 -05:00
CuriousMagpie
09a0d2b3b8 change spacing on buyModal 2023-03-23 16:01:33 -04:00
SabreCat
f0fa2508a9 Merge branch 'release' into develop 2023-03-23 14:35:07 -05:00
SabreCat
232a62ffc7 4.264.3 2023-03-23 14:34:57 -05:00
Phillip Thelen
d2d4af227b Fix an issue with gifting subs (#14550)
* Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2781 of 2781 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Portuguese)

Currently translated at 81.7% (331 of 405 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Portuguese)

Currently translated at 62.5% (5 of 8 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Portuguese)

Currently translated at 63.8% (1777 of 2781 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Portuguese)

Currently translated at 71.6% (43 of 60 strings)

Translated using Weblate (Portuguese)

Currently translated at 79.7% (609 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (German)

Currently translated at 100.0% (404 of 404 strings)

Translated using Weblate (German)

Currently translated at 79.5% (97 of 122 strings)

Translated using Weblate (German)

Currently translated at 97.7% (753 of 770 strings)

Translated using Weblate (German)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Portuguese)

Currently translated at 77.0% (94 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.7% (310 of 404 strings)

Translated using Weblate (Portuguese)

Currently translated at 76.2% (93 of 122 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 85.6% (2381 of 2781 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (404 of 404 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 95.8% (732 of 764 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Portuguese)

Currently translated at 74.5% (91 of 122 strings)

Translated using Weblate (Portuguese)

Currently translated at 72.1% (88 of 122 strings)

Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Andressa Murari Sudré <andressa@sudre.com.br>
Co-authored-by: Cachinhos <cachnhos@gmail.com>
Co-authored-by: Falzart Werefox <muh_fauzi_ramadhan@yahoo.co.id>
Co-authored-by: Jay <fallacyofwildlifeconservation@gmail.com>
Co-authored-by: Jerry Chen <minecjraft@qq.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yu-Wei Tien <a38498987911@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/de/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/de/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/id/
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/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
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/Limited
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks

* gift subscription fix

* remove only

---------

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Andressa Murari Sudré <andressa@sudre.com.br>
Co-authored-by: Cachinhos <cachnhos@gmail.com>
Co-authored-by: Falzart Werefox <muh_fauzi_ramadhan@yahoo.co.id>
Co-authored-by: Jay <fallacyofwildlifeconservation@gmail.com>
Co-authored-by: Jerry Chen <minecjraft@qq.com>
Co-authored-by: Yu-Wei Tien <a38498987911@gmail.com>
Co-authored-by: SabreCat <sabe@habitica.com>
2023-03-23 14:34:40 -05:00
SabreCat
ca1200b689 feat(links): new external link implementation 2023-03-22 18:06:53 -05:00
SabreCat
008579363c Merge remote-tracking branch 'CuriousMagpie/external-link-modal' into external-link-modal 2023-03-22 16:02:34 -05:00
CuriousMagpie
83dcf8d56a tightening up spacing on buyModal, fixing footer 2023-03-22 13:15:11 -04:00
CuriousMagpie
bfc13bc21b Merge branch 'develop' into increment-component 2023-03-22 12:14:04 -04:00
CuriousMagpie
786b1ec670 add underline to cancel link 2023-03-21 17:07:40 -04:00
SabreCat
573de80a91 Merge remote-tracking branch 'CuriousMagpie/external-link-modal' into external-link-modal 2023-03-21 15:17:21 -05:00
CuriousMagpie
5afb46f237 fix close icon on buy & sell and keyboard input into number increment component is now a number, not a string 2023-03-21 16:08:26 -04:00
SabreCat
115340e62d Merge branch 'release' into develop 2023-03-21 13:54:35 -05:00
SabreCat
5de2573521 4.264.2 2023-03-21 13:54:23 -05:00
SabreCat
b472af532c Merge branch 'apple_sub_fix' into release 2023-03-21 13:54:16 -05:00
CuriousMagpie
b264e539f4 add crtl/cmd instructions to skip modal 2023-03-21 14:49:43 -04:00
CuriousMagpie
c726208d6e add cancel option 2023-03-21 13:00:15 -04:00
CuriousMagpie
9cc4fc19d3 more tiny updates 2023-03-21 11:26:31 -04:00
CuriousMagpie
cc81629f09 updates to buyModal styling 2023-03-18 17:18:41 -04:00
SabreCat
b1d2fff13f Merge branch 'release' into develop 2023-03-17 17:25:10 -05:00
SabreCat
027e61a93e 4.264.1 2023-03-17 17:24:56 -05:00
SabreCat
c35afb7cfe fix(string): add missing party invite text 2023-03-17 17:24:49 -05:00
SabreCat
8cd706fd95 feat(links): route to selected link 2023-03-17 16:37:16 -05:00
SabreCat
3a4620976e Merge branch 'release' into external-link-modal 2023-03-17 15:59:28 -05:00
SabreCat
a16098ccda 4.264.0 2023-03-17 15:09:12 -05:00
SabreCat
118c8421fe fix(tz): account for DST 2023-03-17 15:09:02 -05:00
Natalie L
a363e68080 feat(cont): 2023 Spring Fling content (#14525)
* feat(cont): 2023 Spring Fling

* feat(content): 2023 Spring Fling (attempt to add Jungle Buddies Quest Bundle)

* fix(event): various typos and date ranges

* fix(event): correct countdown on Shiny Seed

* fix(quests): correct TypeError in featured list

* feat(content): update Spring gear to canon desc

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-03-17 15:05:04 -05:00
CuriousMagpie
e83db7a28a Merge branch 'develop' into increment-component 2023-03-17 11:47:17 -04:00
SabreCat
1940062200 4.263.1 2023-03-16 16:23:00 -05:00
SabreCat
aea075d0bf chore(content): make Jungle Buddies available 2023-03-16 16:22:47 -05:00
Weblate
7388707a43 Merge branch 'origin/develop' into Weblate. 2023-03-16 18:57:12 +01:00
SabreCat
3e63d74b2c 4.263.0 2023-03-16 12:52:15 -05:00
SabreCat
199ce3e6f7 fix(text): update description of group task approval by @CuriousMagpie 2023-03-16 12:51:51 -05:00
Weblate
597f74c84b Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2781 of 2781 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (56 of 56 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (13 of 13 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (405 of 405 strings)

Translated using Weblate (Portuguese)

Currently translated at 81.7% (331 of 405 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Portuguese)

Currently translated at 62.5% (5 of 8 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Portuguese)

Currently translated at 63.8% (1777 of 2781 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Portuguese)

Currently translated at 71.6% (43 of 60 strings)

Translated using Weblate (Portuguese)

Currently translated at 79.7% (609 of 764 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (47 of 47 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (German)

Currently translated at 100.0% (404 of 404 strings)

Translated using Weblate (German)

Currently translated at 79.5% (97 of 122 strings)

Translated using Weblate (German)

Currently translated at 97.7% (753 of 770 strings)

Translated using Weblate (German)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Portuguese)

Currently translated at 77.0% (94 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 76.7% (310 of 404 strings)

Translated using Weblate (Portuguese)

Currently translated at 76.2% (93 of 122 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 85.6% (2381 of 2781 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (60 of 60 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (404 of 404 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 95.8% (732 of 764 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Portuguese)

Currently translated at 74.5% (91 of 122 strings)

Translated using Weblate (Portuguese)

Currently translated at 72.1% (88 of 122 strings)

Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Andressa Murari Sudré <andressa@sudre.com.br>
Co-authored-by: Cachinhos <cachnhos@gmail.com>
Co-authored-by: Falzart Werefox <muh_fauzi_ramadhan@yahoo.co.id>
Co-authored-by: Jay <fallacyofwildlifeconservation@gmail.com>
Co-authored-by: Jerry Chen <minecjraft@qq.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Yu-Wei Tien <a38498987911@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/de/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/de/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/contrib/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/front/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/de/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/id/
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/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/rebirth/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/spells/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
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/Limited
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Questscontent
Translation: Habitica/Rebirth
Translation: Habitica/Spells
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2023-03-16 17:39:07 +01:00
SabreCat
c83a8c9766 fix(stable): correctly suppress hatching modal 2023-03-15 16:15:55 -05:00
CuriousMagpie
c481354f78 feat(content): add new achievement, Plant Parent 2023-03-15 15:51:04 -05:00
CuriousMagpie
ec9973f9d2 feature(upgrade): added btn-block styling to "Unpause Damage" button 2023-03-15 15:24:02 -05:00
CuriousMagpie
80e193e4ce Merge branch 'develop' into increment-component 2023-03-15 12:38:07 -04:00
CuriousMagpie
971b124b05 apply correct opacity to close icon 2023-03-15 11:21:25 -04:00
SabreCat
de3f1b3f5e chore(sprites): compile 2023-03-14 16:47:14 -05:00
SabreCat
7098d2a72e Merge branch 'release' into sabrecat/fools-2023 2023-03-14 16:46:11 -05:00
SabreCat
bbea789700 fix(test): don't cross two month boundaries when anticipating +1 perk 2023-03-14 16:27:09 -05:00
SabreCat
5359a2bf3d fix(lint): === 2023-03-14 16:14:48 -05:00
Phillip Thelen
93e922e774 fix logic 2023-03-14 15:55:41 +01:00
SabreCat
377b152ffd 4.262.2 2023-03-14 09:25:42 -05:00
SabreCat
8ff8213954 Merge branch 'sabrecat/invite-notif' into release 2023-03-14 09:25:29 -05:00
SabreCat
00bdf81902 4.262.1 2023-03-14 09:15:37 -05:00
SabreCat
4ae50e4d44 Merge branch 'develop' into release 2023-03-14 09:15:18 -05:00
SabreCat
383cd84016 feat(event): Pi Day 2023-03-14 09:12:39 -05:00
Phillip Thelen
f6f1202baf better init for perkMonthCount 2023-03-14 14:37:47 +01:00
Weblate
0c65ff6fec Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (98 of 98 strings)

Translated using Weblate (Cebuano)

Currently translated at 0.0% (0 of 146 strings)

Translated using Weblate (Indonesian)

Currently translated at 61.7% (1718 of 2781 strings)

Translated using Weblate (Portuguese)

Currently translated at 59.9% (130 of 217 strings)

Translated using Weblate (Portuguese)

Currently translated at 81.9% (331 of 404 strings)

Translated using Weblate (Portuguese)

Currently translated at 92.5% (198 of 214 strings)

Translated using Weblate (Portuguese)

Currently translated at 71.3% (87 of 122 strings)

Translated using Weblate (Portuguese)

Currently translated at 96.2% (130 of 135 strings)

Translated using Weblate (Portuguese)

Currently translated at 96.2% (130 of 135 strings)

Translated using Weblate (Portuguese)

Currently translated at 98.5% (133 of 135 strings)

Translated using Weblate (Portuguese)

Currently translated at 63.7% (1774 of 2781 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.3% (145 of 146 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.3% (145 of 146 strings)

Translated using Weblate (Portuguese)

Currently translated at 63.7% (1773 of 2781 strings)

Translated using Weblate (Portuguese)

Currently translated at 63.5% (1766 of 2781 strings)

Translated using Weblate (Italian)

Currently translated at 98.7% (760 of 770 strings)

Translated using Weblate (Estonian)

Currently translated at 13.6% (20 of 146 strings)

Translated using Weblate (Ukrainian)

Currently translated at 67.5% (516 of 764 strings)

Translated using Weblate (Estonian)

Currently translated at 4.1% (6 of 146 strings)

Translated using Weblate (Vietnamese)

Currently translated at 79.6% (613 of 770 strings)

Translated using Weblate (Vietnamese)

Currently translated at 78.9% (608 of 770 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Vietnamese)

Currently translated at 77.2% (595 of 770 strings)

Translated using Weblate (Indonesian)

Currently translated at 98.5% (133 of 135 strings)

Translated using Weblate (Indonesian)

Currently translated at 94.1% (353 of 375 strings)

Translated using Weblate (Malay)

Currently translated at 38.6% (70 of 181 strings)

Translated using Weblate (Vietnamese)

Currently translated at 76.3% (588 of 770 strings)

Translated using Weblate (Vietnamese)

Currently translated at 97.0% (131 of 135 strings)

Translated using Weblate (Vietnamese)

Currently translated at 95.2% (139 of 146 strings)

Translated using Weblate (Vietnamese)

Currently translated at 62.5% (167 of 267 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Indonesian)

Currently translated at 93.4% (200 of 214 strings)

Translated using Weblate (Ukrainian)

Currently translated at 67.4% (515 of 764 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (French)

Currently translated at 100.0% (217 of 217 strings)

Translated using Weblate (Vietnamese)

Currently translated at 76.0% (111 of 146 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2781 of 2781 strings)

Translated using Weblate (Indonesian)

Currently translated at 93.4% (170 of 182 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Spanish)

Currently translated at 93.0% (2587 of 2781 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Serbian)

Currently translated at 94.8% (128 of 135 strings)

Translated using Weblate (Serbian)

Currently translated at 100.0% (1 of 1 strings)

Translated using Weblate (Italian)

Currently translated at 98.7% (760 of 770 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2781 of 2781 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2781 of 2781 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.4% (766 of 770 strings)

Translated using Weblate (Indonesian)

Currently translated at 99.3% (765 of 770 strings)

Translated using Weblate (French)

Currently translated at 100.0% (770 of 770 strings)

Translated using Weblate (Indonesian)

Currently translated at 74.7% (302 of 404 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2777 of 2777 strings)

Translated using Weblate (Indonesian)

Currently translated at 90.6% (194 of 214 strings)

Translated using Weblate (French)

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (French)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 92.8% (169 of 182 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Indonesian)

Currently translated at 61.7% (1714 of 2777 strings)

Translated using Weblate (Indonesian)

Currently translated at 61.7% (1714 of 2777 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (131 of 131 strings)

Translated using Weblate (Indonesian)

Currently translated at 61.6% (1711 of 2777 strings)

Translated using Weblate (Ukrainian)

Currently translated at 66.6% (509 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 61.5% (1709 of 2777 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Malay)

Currently translated at 50.0% (27 of 54 strings)

Translated using Weblate (Indonesian)

Currently translated at 53.3% (32 of 60 strings)

Translated using Weblate (Dutch)

Currently translated at 86.3% (2397 of 2777 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2777 of 2777 strings)

Translated using Weblate (Dutch)

Currently translated at 86.3% (2397 of 2777 strings)

Translated using Weblate (Spanish)

Currently translated at 93.2% (2587 of 2773 strings)

Translated using Weblate (Indonesian)

Currently translated at 93.3% (350 of 375 strings)

Translated using Weblate (Indonesian)

Currently translated at 62.9% (34 of 54 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Czech)

Currently translated at 78.0% (114 of 146 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.9% (756 of 764 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Indonesian)

Currently translated at 88.4% (161 of 182 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Indonesian)

Currently translated at 92.5% (347 of 375 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Spanish)

Currently translated at 93.2% (2587 of 2773 strings)

Translated using Weblate (French)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (French)

Currently translated at 99.0% (757 of 764 strings)

Translated using Weblate (French)

Currently translated at 95.0% (116 of 122 strings)

Translated using Weblate (French)

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (French)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Malay)

Currently translated at 36.6% (41 of 112 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Indonesian)

Currently translated at 86.6% (117 of 135 strings)

Translated using Weblate (Ukrainian)

Currently translated at 24.9% (693 of 2773 strings)

Translated using Weblate (Indonesian)

Currently translated at 94.2% (114 of 121 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Tagalog)

Currently translated at 96.7% (180 of 186 strings)

Translated using Weblate (Tagalog)

Currently translated at 53.2% (115 of 216 strings)

Translated using Weblate (Tagalog)

Currently translated at 42.6% (114 of 267 strings)

Translated using Weblate (Filipino)

Currently translated at 32.8% (910 of 2773 strings)

Translated using Weblate (Ukrainian)

Currently translated at 65.8% (503 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 93.3% (113 of 121 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Filipino)

Currently translated at 32.6% (906 of 2773 strings)

Translated using Weblate (Filipino)

Currently translated at 80.0% (108 of 135 strings)

Translated using Weblate (Filipino)

Currently translated at 32.6% (906 of 2773 strings)

Translated using Weblate (Filipino)

Currently translated at 81.3% (621 of 763 strings)

Translated using Weblate (Filipino)

Currently translated at 71.7% (548 of 764 strings)

Translated using Weblate (Filipino)

Currently translated at 32.5% (903 of 2773 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (764 of 764 strings)

Translated using Weblate (Indonesian)

Currently translated at 84.4% (114 of 135 strings)

Translated using Weblate (Indonesian)

Currently translated at 61.4% (1705 of 2773 strings)

Translated using Weblate (Serbian)

Currently translated at 75.6% (571 of 755 strings)

Translated using Weblate (Serbian)

Currently translated at 96.2% (179 of 186 strings)

Translated using Weblate (Spanish)

Currently translated at 93.2% (2587 of 2773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Italian)

Currently translated at 94.2% (115 of 122 strings)

Translated using Weblate (Italian)

Currently translated at 91.3% (244 of 267 strings)

Translated using Weblate (Portuguese)

Currently translated at 63.6% (1764 of 2773 strings)

Translated using Weblate (Italian)

Currently translated at 88.5% (108 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 92.5% (112 of 121 strings)

Translated using Weblate (Indonesian)

Currently translated at 59.2% (32 of 54 strings)

Translated using Weblate (Spanish)

Currently translated at 93.2% (2587 of 2773 strings)

Translated using Weblate (Arabic)

Currently translated at 85.0% (649 of 763 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 85.8% (2380 of 2773 strings)

Translated using Weblate (Dutch)

Currently translated at 86.0% (2386 of 2773 strings)

Translated using Weblate (Indonesian)

Currently translated at 61.4% (1704 of 2773 strings)

Translated using Weblate (Spanish)

Currently translated at 93.2% (2587 of 2773 strings)

Translated using Weblate (Indonesian)

Currently translated at 92.5% (347 of 375 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Malay)

Currently translated at 33.1% (60 of 181 strings)

Translated using Weblate (Indonesian)

Currently translated at 90.6% (340 of 375 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Ukrainian)

Currently translated at 99.2% (265 of 267 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Indonesian)

Currently translated at 90.1% (338 of 375 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Russian)

Currently translated at 82.7% (101 of 122 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Spanish)

Currently translated at 93.2% (2587 of 2773 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Spanish)

Currently translated at 86.0% (105 of 122 strings)

Translated using Weblate (Spanish)

Currently translated at 97.2% (142 of 146 strings)

Translated using Weblate (Spanish)

Currently translated at 80.3% (98 of 122 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (375 of 375 strings)

Translated using Weblate (Indonesian)

Currently translated at 90.9% (110 of 121 strings)

Translated using Weblate (Indonesian)

Currently translated at 88.5% (332 of 375 strings)

Translated using Weblate (Arabic)

Currently translated at 83.2% (635 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 82.6% (631 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 82.0% (626 of 763 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (374 of 374 strings)

Translated using Weblate (Portuguese)

Currently translated at 63.5% (1761 of 2773 strings)

Translated using Weblate (Arabic)

Currently translated at 81.7% (624 of 763 strings)

Translated using Weblate (Indonesian)

Currently translated at 89.2% (108 of 121 strings)

Translated using Weblate (Arabic)

Currently translated at 81.5% (622 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 80.9% (618 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 78.3% (598 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 77.8% (594 of 763 strings)

Translated using Weblate (Italian)

Currently translated at 82.7% (101 of 122 strings)

Translated using Weblate (Malay)

Currently translated at 20.9% (38 of 181 strings)

Translated using Weblate (Malay)

Currently translated at 19.3% (35 of 181 strings)

Translated using Weblate (Malay)

Currently translated at 18.2% (33 of 181 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2773 of 2773 strings)

Translated using Weblate (Arabic)

Currently translated at 76.8% (586 of 763 strings)

Translated using Weblate (Indonesian)

Currently translated at 87.6% (106 of 121 strings)

Translated using Weblate (Arabic)

Currently translated at 76.2% (582 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 76.1% (581 of 763 strings)

Translated using Weblate (Italian)

Currently translated at 82.7% (101 of 122 strings)

Translated using Weblate (Russian)

Currently translated at 99.8% (762 of 763 strings)

Translated using Weblate (Italian)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2773 of 2773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (182 of 182 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 74.3% (567 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 74.1% (566 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 72.2% (551 of 763 strings)

Translated using Weblate (Portuguese)

Currently translated at 63.3% (1757 of 2773 strings)

Translated using Weblate (Portuguese)

Currently translated at 63.3% (1757 of 2773 strings)

Translated using Weblate (Portuguese)

Currently translated at 85.1% (650 of 763 strings)

Translated using Weblate (Malay)

Currently translated at 17.6% (32 of 181 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Bulgarian)

Currently translated at 73.9% (108 of 146 strings)

Translated using Weblate (Russian)

Currently translated at 80.3% (98 of 122 strings)

Translated using Weblate (Arabic)

Currently translated at 71.8% (548 of 763 strings)

Translated using Weblate (Indonesian)

Currently translated at 97.9% (747 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 71.5% (546 of 763 strings)

Translated using Weblate (Indonesian)

Currently translated at 97.1% (741 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 71.1% (543 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 71.0% (542 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 70.6% (539 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Arabic)

Currently translated at 69.8% (533 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 69.2% (528 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 68.9% (526 of 763 strings)

Translated using Weblate (Italian)

Currently translated at 82.7% (101 of 122 strings)

Translated using Weblate (French)

Currently translated at 95.0% (116 of 122 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (221 of 221 strings)

Translated using Weblate (Malay)

Currently translated at 11.6% (21 of 181 strings)

Translated using Weblate (Indonesian)

Currently translated at 95.9% (732 of 763 strings)

Translated using Weblate (Ukrainian)

Currently translated at 94.7% (253 of 267 strings)

Translated using Weblate (French)

Currently translated at 88.5% (108 of 122 strings)

Translated using Weblate (French)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Italian)

Currently translated at 79.5% (97 of 122 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (181 of 181 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 95.0% (725 of 763 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Filipino)

Currently translated at 83.0% (93 of 112 strings)

Translated using Weblate (Filipino)

Currently translated at 73.0% (195 of 267 strings)

Translated using Weblate (Filipino)

Currently translated at 32.7% (909 of 2773 strings)

Translated using Weblate (Filipino)

Currently translated at 91.9% (343 of 373 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2773 of 2773 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (122 of 122 strings)

Translated using Weblate (Filipino)

Currently translated at 32.8% (910 of 2773 strings)

Translated using Weblate (Filipino)

Currently translated at 32.8% (911 of 2773 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Filipino)

Currently translated at 32.0% (889 of 2773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 95.0% (116 of 122 strings)

Translated using Weblate (Indonesian)

Currently translated at 93.3% (712 of 763 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (2769 of 2773 strings)

Translated using Weblate (Italian)

Currently translated at 99.2% (757 of 763 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Filipino)

Currently translated at 95.6% (178 of 186 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (French)

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (15 of 15 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (French)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 86.8% (106 of 122 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Indonesian)

Currently translated at 84.2% (107 of 127 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 66.9% (511 of 763 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (214 of 214 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (216 of 216 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (755 of 755 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Indonesian)

Currently translated at 92.0% (702 of 763 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Dutch)

Currently translated at 95.3% (205 of 215 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Dutch)

Currently translated at 96.1% (126 of 131 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (8 of 8 strings)

Translated using Weblate (Spanish)

Currently translated at 93.0% (2581 of 2773 strings)

Translated using Weblate (Dutch)

Currently translated at 97.2% (176 of 181 strings)

Translated using Weblate (Dutch)

Currently translated at 95.0% (57 of 60 strings)

Translated using Weblate (Dutch)

Currently translated at 98.6% (144 of 146 strings)

Translated using Weblate (Dutch)

Currently translated at 91.4% (202 of 221 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (404 of 404 strings)

Translated using Weblate (Indonesian)

Currently translated at 91.4% (698 of 763 strings)

Translated using Weblate (German)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (German)

Currently translated at 89.8% (240 of 267 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (2769 of 2773 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2773 of 2773 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (760 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 63.4% (484 of 763 strings)

Translated using Weblate (Russian)

Currently translated at 99.2% (2745 of 2767 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (763 of 763 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.4% (759 of 763 strings)

Translated using Weblate (Indonesian)

Currently translated at 91.0% (695 of 763 strings)

Translated using Weblate (Arabic)

Currently translated at 61.7% (467 of 756 strings)

Translated using Weblate (French)

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (French)

Currently translated at 100.0% (2763 of 2763 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Indonesian)

Currently translated at 90.0% (681 of 756 strings)

Translated using Weblate (Malay)

Currently translated at 10.4% (19 of 181 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (2763 of 2763 strings)

Translated using Weblate (Spanish)

Currently translated at 93.4% (2581 of 2763 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Indonesian)

Currently translated at 89.5% (677 of 756 strings)

Translated using Weblate (Spanish)

Currently translated at 93.4% (2581 of 2763 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Russian)

Currently translated at 95.5% (255 of 267 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Indonesian)

Currently translated at 89.1% (674 of 756 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (2763 of 2763 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.8% (256 of 267 strings)

Translated using Weblate (Malay)

Currently translated at 6.0% (11 of 181 strings)

Translated using Weblate (Filipino)

Currently translated at 98.3% (183 of 186 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Indonesian)

Currently translated at 88.0% (666 of 756 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.9% (2762 of 2763 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Filipino)

Currently translated at 98.3% (183 of 186 strings)

Translated using Weblate (Tagalog)

Currently translated at 96.7% (180 of 186 strings)

Translated using Weblate (Filipino)

Currently translated at 98.9% (184 of 186 strings)

Translated using Weblate (Filipino)

Currently translated at 80.8% (76 of 94 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (61 of 61 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (135 of 135 strings)

Translated using Weblate (Russian)

Currently translated at 99.5% (214 of 215 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Russian)

Currently translated at 94.3% (252 of 267 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 89.5% (239 of 267 strings)

Translated using Weblate (Russian)

Currently translated at 99.3% (2745 of 2763 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Indonesian)

Currently translated at 87.1% (659 of 756 strings)

Translated using Weblate (Portuguese)

Currently translated at 80.9% (179 of 221 strings)

Translated using Weblate (Arabic)

Currently translated at 60.0% (454 of 756 strings)

Translated using Weblate (Arabic)

Currently translated at 67.0% (63 of 94 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (French)

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (French)

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Russian)

Currently translated at 99.1% (2740 of 2763 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (2758 of 2763 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (754 of 756 strings)

Translated using Weblate (French)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (146 of 146 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (215 of 215 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (112 of 112 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (267 of 267 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (2763 of 2763 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (756 of 756 strings)

Translated using Weblate (Indonesian)

Currently translated at 86.7% (656 of 756 strings)

Co-authored-by: An Nguyen <nguyen.thienan.business@gmail.com>
Co-authored-by: Ana Beatriz <anabeatriz.augusto06@yahoo.com>
Co-authored-by: Andressa Murari Sudré <andressa@sudre.com.br>
Co-authored-by: Anfasa Rabbany <lelcraft96@gmail.com>
Co-authored-by: Benoit Hetru <me+hbtc@gahanka.net>
Co-authored-by: Binny <45uipcik@duck.com>
Co-authored-by: Camille <barberacamille47@gmail.com>
Co-authored-by: Chloé Fonson <chl.fsn@gmail.com>
Co-authored-by: Daniel Villarroel Aguilera <y4v7l45j@duck.com>
Co-authored-by: Falzart Werefox <muh_fauzi_ramadhan@yahoo.co.id>
Co-authored-by: Hanafi <naflizo@gmail.com>
Co-authored-by: Harvey James <harveymoldon@gmail.com>
Co-authored-by: Ike Osenberg <ike.osenberg@gmail.com>
Co-authored-by: Jerry Chen <minecjraft@qq.com>
Co-authored-by: Jonathan Garcia <jonathangarcia0@duck.com>
Co-authored-by: Karylle Mae Treuse G. Labitad <treuse2006@gmail.com>
Co-authored-by: Kris <dawnut.x@gmail.com>
Co-authored-by: Kuan Yu Chou <jenny2311@gmail.com>
Co-authored-by: LiziKnight <liziknight0316@outlook.com>
Co-authored-by: M <maperray@gmail.com>
Co-authored-by: Maria Otonuo <mariaotonio@gmail.com>
Co-authored-by: Martim Pinto Paiva <pintopaivam@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Nazar Paruna <nazarparuna@gmail.com>
Co-authored-by: Nero <pablofh004@gmail.com>
Co-authored-by: Nina Glasbergen <ninaglas2002@gmail.com>
Co-authored-by: Phillip Thelen <phillip@habitica.com>
Co-authored-by: Pyotr Stolnikov <pitmysterio@gmail.com>
Co-authored-by: Raithe <RaitheOfDureya@gmail.com>
Co-authored-by: Rara <annisarahmah.xmipa2@gmail.com>
Co-authored-by: Ruben <r.tartwijk@gmail.com>
Co-authored-by: Sandra Marcial <sandramarcial80@gmail.com>
Co-authored-by: Sciuridae <sweetvshoney@163.com>
Co-authored-by: Sergey Shevelev <vlkgamer45@gmail.com>
Co-authored-by: Soul <joninsoul@gmail.com>
Co-authored-by: Tristan Roosipuu <roosipuutristan@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: endriw cisersa batistela correa <endriwbatistela@gmail.com>
Co-authored-by: fluffstuff <opositesandreality@gmail.com>
Co-authored-by: maikodoglas <maickeldouglas.mm@outlook.com>
Co-authored-by: polyglottericus <vincemorel.vilan.889@gmail.com>
Co-authored-by: そら <comi4work@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/bg/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ceb/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/cs/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/et/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/id/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/it/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/ms/
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/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/vi/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/id/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/it/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/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/backgrounds/vi/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/character/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/character/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/character/tl/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/es/
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/it/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/content/es_419/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/content/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/content/id/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/death/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/death/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/id/
Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/id/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/front/es/
Translate-URL: https://translate.habitica.com/projects/habitica/front/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/front/id/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/
Translate-URL: https://translate.habitica.com/projects/habitica/front/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/front/nl/
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/front/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/id/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
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/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hant/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/id/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/id/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/inventory/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/de/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/it/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/tl/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/vi/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/merch/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/messages/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/overview/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/de/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ms/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/ar/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/settings/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/tl/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/fil/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/id/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/sr/
Translate-URL: https://translate.habitica.com/projects/habitica/tasks/vi/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Character
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Death
Translation: Habitica/Defaulttasks
Translation: Habitica/Faq
Translation: Habitica/Front
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Inventory
Translation: Habitica/Limited
Translation: Habitica/Merch
Translation: Habitica/Messages
Translation: Habitica/Npc
Translation: Habitica/Overview
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent
Translation: Habitica/Settings
Translation: Habitica/Subscriber
Translation: Habitica/Tasks
2023-03-14 10:16:35 +01:00
SabreCat
83f5c92ff1 fix(lint): loop no likey ++ 2023-03-13 18:36:14 -05:00
Phillip Thelen
8dcacdc92e Only update user objects that don’t have notification yet (#14514)
* build(deps): bump stopword from 2.0.5 to 2.0.7 in /website/client (#14504)

Bumps [stopword](https://github.com/fergiemcdowall/stopword) from 2.0.5 to 2.0.7.
- [Release notes](https://github.com/fergiemcdowall/stopword/releases)
- [Commits](https://github.com/fergiemcdowall/stopword/commits/v.2.0.7)

---
updated-dependencies:
- dependency-name: stopword
  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>

* build(deps): bump stripe from 11.6.0 to 11.10.0 (#14503)

Bumps [stripe](https://github.com/stripe/stripe-node) from 11.6.0 to 11.10.0.
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v11.6.0...v11.10.0)

---
updated-dependencies:
- dependency-name: stripe
  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>

* build(deps): bump apidoc from 0.53.1 to 0.54.0 (#14501)

Bumps [apidoc](https://github.com/apidoc/apidoc) from 0.53.1 to 0.54.0.
- [Release notes](https://github.com/apidoc/apidoc/releases)
- [Changelog](https://github.com/apidoc/apidoc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/apidoc/apidoc/compare/0.53.1...0.54.0)

---
updated-dependencies:
- dependency-name: apidoc
  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>

* build(deps): bump @sideway/formula in /website/client (#14495)

Bumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sideway/formula/releases)
- [Commits](https://github.com/sideway/formula/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: "@sideway/formula"
  dependency-type: indirect
...

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

* build(deps): bump validator from 13.7.0 to 13.9.0 in /website/client (#14492)

Bumps [validator](https://github.com/validatorjs/validator.js) from 13.7.0 to 13.9.0.
- [Release notes](https://github.com/validatorjs/validator.js/releases)
- [Changelog](https://github.com/validatorjs/validator.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/validatorjs/validator.js/compare/13.7.0...13.9.0)

---
updated-dependencies:
- dependency-name: validator
  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>

* build(deps): bump validator from 13.7.0 to 13.9.0 (#14488)

Bumps [validator](https://github.com/validatorjs/validator.js) from 13.7.0 to 13.9.0.
- [Release notes](https://github.com/validatorjs/validator.js/releases)
- [Changelog](https://github.com/validatorjs/validator.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/validatorjs/validator.js/compare/13.7.0...13.9.0)

---
updated-dependencies:
- dependency-name: validator
  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>

* build(deps): bump hellojs from 1.19.5 to 1.20.0 in /website/client (#14479)

Bumps [hellojs](https://github.com/MrSwitch/hello.js) from 1.19.5 to 1.20.0.
- [Release notes](https://github.com/MrSwitch/hello.js/releases)
- [Changelog](https://github.com/MrSwitch/hello.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MrSwitch/hello.js/compare/v1.19.5...v1.20.0)

---
updated-dependencies:
- dependency-name: hellojs
  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>

* build(deps): bump ua-parser-js from 0.7.31 to 0.7.33 in /website/client (#14463)

Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: indirect
...

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

* build(deps): bump cookiejar from 2.1.3 to 2.1.4 (#14462)

Bumps [cookiejar](https://github.com/bmeck/node-cookiejar) from 2.1.3 to 2.1.4.
- [Release notes](https://github.com/bmeck/node-cookiejar/releases)
- [Commits](https://github.com/bmeck/node-cookiejar/commits)

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

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

* build(deps): bump glob from 8.0.3 to 8.1.0 (#14448)

Bumps [glob](https://github.com/isaacs/node-glob) from 8.0.3 to 8.1.0.
- [Release notes](https://github.com/isaacs/node-glob/releases)
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v8.0.3...v8.1.0)

---
updated-dependencies:
- dependency-name: glob
  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>

* build(deps): bump dompurify from 2.4.1 to 2.4.3 in /website/client (#14443)

Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.4.1 to 2.4.3.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.4.1...2.4.3)

---
updated-dependencies:
- dependency-name: dompurify
  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>

* fixed typo (#14417)

In   "weaponSpecialWinter2023RogueNotes", changed "Incrases" to "Increases"

* Only update user objects that don’t have notification yet

* fix test

* fix spy

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: gwenexner <49045733+gwenexner@users.noreply.github.com>
Co-authored-by: SabreCat <sabe@habitica.com>
2023-03-13 16:24:00 -05:00
SabreCat
7874ae5092 fix(hyperlinks): WHITE inactive drawer tabs 2023-03-13 15:28:26 -05:00
SabreCat
481719e513 Revert "fix(hyperlinks): gray inactive drawer tabs"
This reverts commit 9657112bca.
2023-03-13 15:19:41 -05:00
SabreCat
9657112bca fix(hyperlinks): gray inactive drawer tabs 2023-03-13 14:35:43 -05:00
SabreCat
f7026b2478 Merge branch 'release' into sabrecat/invite-notif 2023-03-13 14:13:06 -05:00
negue
e39b3bdd35 open the modal by click override 2023-03-11 00:40:05 +01:00
SabreCat
a210ab57b0 WIP(external-link): hacky attempt number one 2023-03-10 13:33:51 -06:00
CuriousMagpie
76fa6ec1b8 updates to snumberIncrement, buyModal, and sellModal 2023-03-09 15:54:26 -05:00
CuriousMagpie
3f3e0e2ae8 modal essentially complete but for scripts 2023-03-08 15:17:15 -05:00
CuriousMagpie
65f12ac9ea adding console log messages 2023-03-08 12:55:58 -05:00
CuriousMagpie
d0941810a7 trying to make a modal 2023-03-08 12:52:22 -05:00
CuriousMagpie
b77deb28b4 initial commit 2023-03-08 12:01:50 -05:00
SabreCat
458aee9a3a Merge branch 'release' into develop 2023-03-07 11:48:14 -06:00
SabreCat
0b6b967753 4.262.0 2023-03-07 10:33:10 -06:00
Natalie L
57f86bac70 feat(content): March backgrounds and Enchanted Armoire items (#14524)
* feat(content): March backgrounds and Enchanted Armoire

* fix(backgrounds): trivial syntax consistency

* fix(gear): missing verbiage

* fix(gear): correct usage of <%= attrs %>

* fix(backgrounds): broken string tokens

* fix(backgrounds): more broken tokens

---------

Co-authored-by: SabreCat <sabe@habitica.com>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
2023-03-06 16:57:56 -06:00
Natalie L
f2fe83a469 feature(upgrade): copy Pause Damage button to Settings page (#14532)
* feature(upgrade): copying Pause Damage button to Settings page

* Removing duplicate string 'cause I can't search effectively. 😅
2023-03-06 16:16:23 -06:00
SabreCat
3354ca048c fix(analytics): record missing group ID info
also update subproject while I'm here
2023-03-06 15:36:17 -06:00
SabreCat
99c46602c4 fix(fooling): correct special pet mapping 2023-03-06 09:47:53 -06:00
SabreCat
ee585c0ff3 feat(event): April Fools 2023 2023-03-03 14:32:56 -06:00
CuriousMagpie
1ac4466c24 trying to make more components for the buy/sell modals 2023-03-02 14:33:27 -05:00
Phillip Thelen
0754c0ff05 correctly set limit 2023-03-02 18:04:02 +01:00
CuriousMagpie
03f0061c85 Merge branch 'develop' into increment-component 2023-03-01 15:46:04 -05:00
Phillip Thelen
5d1346e65c fix lint 2023-03-01 15:06:08 +01:00
Phillip Thelen
a2ce0ab099 Set request size limit higher, because of iOS receipt sizes 2023-03-01 14:48:50 +01:00
SabreCat
16ae182f34 Merge branch 'release' into develop 2023-02-28 15:37:35 -06:00
Phillip Thelen
6887fd70c0 try something 2023-02-28 16:17:52 +01:00
SabreCat
f327795761 4.261.1 2023-02-27 16:17:17 -06:00
SabreCat
8cf5a380da fix(strings): more silly 2023-02-27 16:17:09 -06:00
Phillip Thelen
def24142ca remove wrong test 2023-02-27 13:30:44 +01:00
Phillip Thelen
c29049146d prevent sub accidentally also being applied to other account 2023-02-27 12:06:27 +01:00
SabreCat
cc419385f6 4.261.0 2023-02-24 16:32:41 -06:00
SabreCat
45107fe48f Merge branch 'sabrecat/transactions-404' into release 2023-02-24 16:32:35 -06:00
SabreCat
80f517f1ad feat(content): subscriber items March 2023
Also fixes Vue templating in one paragraph of the Community Guidelines.
2023-02-24 16:31:50 -06:00
Phillip Thelen
57fb7ca6f2 fix 2023-02-24 11:25:53 +01:00
Phillip Thelen
62b171ffa5 fix linting 2023-02-24 10:47:50 +01:00
Phillip Thelen
be18476292 correctly set perkMonthCount 2023-02-23 13:20:47 +01:00
SabreCat
b6e9d0c9c0 Merge branch 'sabrecat/transactions-404' into develop 2023-02-22 15:20:16 -06:00
SabreCat
442d9ca9cd fix(permissions): route away from purchase history for non-support 2023-02-22 15:15:03 -06:00
Phillip Thelen
3f56b7fa3f fix offset calculation 2023-02-17 15:42:31 +01:00
SabreCat
14f9debfdb fix(css): remove yet more blue-10 overrides 2023-02-16 14:34:24 -06:00
SabreCat
4a1011f1af fix(hyperlinks): remove some blue overrides 2023-02-16 12:57:35 -06:00
Phillip Thelen
d69de2948b fix import 2023-02-16 09:58:09 +01:00
Phillip Thelen
c5f5da1d32 Merge remote-tracking branch 'origin/develop' into apple_sub_fix 2023-02-16 09:38:14 +01:00
Phillip Thelen
e338fb8ce7 Merge branch 'apple_sub_fix' of https://github.com/HabitRPG/habitica into apple_sub_fix 2023-02-16 09:37:36 +01:00
Phillip Thelen
2d5dcae406 fix tests 2023-02-16 09:37:00 +01:00
SabreCat
9ec1917e6d feat(invites): provide link to inviting user in party notif
Also changes basic links sitewide to purple-300
2023-02-15 16:54:46 -06:00
SabreCat
409ce5dbfb Merge branch 'release' into develop 2023-02-15 15:29:34 -06:00
SabreCat
ab706abed5 fix(test): remove outdated event expectation 2023-02-15 15:29:23 -06:00
CuriousMagpie
c349de6908 finish up sellModal and questModal (also Time Travelers) 2023-02-15 12:54:26 -05:00
Phillip Thelen
3203b09b7a reset perkMonthCount when subscription ends 2023-02-15 10:05:18 +01:00
CuriousMagpie
fd7f3a646e add functionality and styling to sellModal and questModal 2023-02-14 17:22:05 -05:00
CuriousMagpie
7244c1bebc Merge branch 'develop' into increment-component 2023-02-14 15:30:18 -05:00
gwenexner
2ea023299c fixed typo (#14417)
In   "weaponSpecialWinter2023RogueNotes", changed "Incrases" to "Increases"
2023-02-13 16:33:10 -06:00
dependabot[bot]
ce1ce47d18 build(deps): bump dompurify from 2.4.1 to 2.4.3 in /website/client (#14443)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.4.1 to 2.4.3.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/2.4.1...2.4.3)

---
updated-dependencies:
- dependency-name: dompurify
  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-02-13 16:27:33 -06:00
dependabot[bot]
0d1e8ec3f9 build(deps): bump glob from 8.0.3 to 8.1.0 (#14448)
Bumps [glob](https://github.com/isaacs/node-glob) from 8.0.3 to 8.1.0.
- [Release notes](https://github.com/isaacs/node-glob/releases)
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v8.0.3...v8.1.0)

---
updated-dependencies:
- dependency-name: glob
  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-02-13 16:27:03 -06:00
dependabot[bot]
4a849e6d15 build(deps): bump cookiejar from 2.1.3 to 2.1.4 (#14462)
Bumps [cookiejar](https://github.com/bmeck/node-cookiejar) from 2.1.3 to 2.1.4.
- [Release notes](https://github.com/bmeck/node-cookiejar/releases)
- [Commits](https://github.com/bmeck/node-cookiejar/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-13 16:26:33 -06:00
dependabot[bot]
7f65079cfe build(deps): bump ua-parser-js from 0.7.31 to 0.7.33 in /website/client (#14463)
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 0.7.31 to 0.7.33.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/master/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/0.7.31...0.7.33)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-13 16:26:11 -06:00
dependabot[bot]
04f54d5e03 build(deps): bump hellojs from 1.19.5 to 1.20.0 in /website/client (#14479)
Bumps [hellojs](https://github.com/MrSwitch/hello.js) from 1.19.5 to 1.20.0.
- [Release notes](https://github.com/MrSwitch/hello.js/releases)
- [Changelog](https://github.com/MrSwitch/hello.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MrSwitch/hello.js/compare/v1.19.5...v1.20.0)

---
updated-dependencies:
- dependency-name: hellojs
  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-02-13 16:03:23 -06:00
dependabot[bot]
925e2e5ec6 build(deps): bump validator from 13.7.0 to 13.9.0 (#14488)
Bumps [validator](https://github.com/validatorjs/validator.js) from 13.7.0 to 13.9.0.
- [Release notes](https://github.com/validatorjs/validator.js/releases)
- [Changelog](https://github.com/validatorjs/validator.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/validatorjs/validator.js/compare/13.7.0...13.9.0)

---
updated-dependencies:
- dependency-name: validator
  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-02-13 15:41:09 -06:00
dependabot[bot]
a549668522 build(deps): bump validator from 13.7.0 to 13.9.0 in /website/client (#14492)
Bumps [validator](https://github.com/validatorjs/validator.js) from 13.7.0 to 13.9.0.
- [Release notes](https://github.com/validatorjs/validator.js/releases)
- [Changelog](https://github.com/validatorjs/validator.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/validatorjs/validator.js/compare/13.7.0...13.9.0)

---
updated-dependencies:
- dependency-name: validator
  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-02-13 15:40:52 -06:00
dependabot[bot]
02e0e45da6 build(deps): bump @sideway/formula in /website/client (#14495)
Bumps [@sideway/formula](https://github.com/sideway/formula) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/sideway/formula/releases)
- [Commits](https://github.com/sideway/formula/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: "@sideway/formula"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-13 15:40:31 -06:00
dependabot[bot]
1e72dbe155 build(deps): bump apidoc from 0.53.1 to 0.54.0 (#14501)
Bumps [apidoc](https://github.com/apidoc/apidoc) from 0.53.1 to 0.54.0.
- [Release notes](https://github.com/apidoc/apidoc/releases)
- [Changelog](https://github.com/apidoc/apidoc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/apidoc/apidoc/compare/0.53.1...0.54.0)

---
updated-dependencies:
- dependency-name: apidoc
  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-02-13 15:39:05 -06:00
dependabot[bot]
e71f0558fe build(deps): bump stripe from 11.6.0 to 11.10.0 (#14503)
Bumps [stripe](https://github.com/stripe/stripe-node) from 11.6.0 to 11.10.0.
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v11.6.0...v11.10.0)

---
updated-dependencies:
- dependency-name: stripe
  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-02-13 15:38:38 -06:00
dependabot[bot]
b3d83431e6 build(deps): bump stopword from 2.0.5 to 2.0.7 in /website/client (#14504)
Bumps [stopword](https://github.com/fergiemcdowall/stopword) from 2.0.5 to 2.0.7.
- [Release notes](https://github.com/fergiemcdowall/stopword/releases)
- [Commits](https://github.com/fergiemcdowall/stopword/commits/v.2.0.7)

---
updated-dependencies:
- dependency-name: stopword
  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-02-13 15:38:02 -06:00
SabreCat
2ac21104a4 4.260.1 2023-02-13 11:51:45 -06:00
SabreCat
6b95f648c4 Merge remote-tracking branch 'CuriousMagpie/footer-link-updates' into release 2023-02-13 11:28:14 -06:00
SabreCat
f9db4b9b5b fix(event): correct logic for various Valentine's items 2023-02-13 11:25:02 -06:00
Phillip Thelen
6ee2e3a379 Merge remote-tracking branch 'origin/develop' into apple_sub_fix 2023-02-13 17:38:14 +01:00
SabreCat
74da6d8798 4.260.0 2023-02-12 23:17:16 -06:00
Natalie L
a73e4d399e chore(content): add Pink Marble Magic Hatching Potion (#14497)
* chore(content): add quest text

* chore(content): add rest of potion quest content

* fix(image): pad quest scroll image

* feat(quest): add Rage action

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-02-12 23:17:56 -06:00
Natalie L
8f4d668b0f chore(content): add pet quest bundle and magic hatching potions for February (#14499) 2023-02-12 23:17:09 -06:00
CuriousMagpie
945c19cc80 chore(links): add weblate link to footer and update data display tool 2023-02-10 15:11:36 -05:00
gwenexner
7b4cfee290 Fixed release info for Set 105 (#14494)
Removed a stray space at the front of "Set 105", changed year to 2023.
2023-02-10 10:30:27 -06:00
Phillip Thelen
77229f3e5e show year for next hourglass 2023-02-10 17:13:20 +01:00
Phillip Thelen
41cdab1672 adjust tests 2023-02-10 17:13:10 +01:00
Phillip Thelen
58f4dd0c43 fix typo 2023-02-10 17:13:04 +01:00
Phillip Thelen
0ce64a0197 fix next hourglass calculation 2023-02-09 16:20:46 +01:00
Natalie L
0b8f2bc58e update(content): revise community guidelines (#14481)
* update(content); revise community guidelines

* fix(CG): remove outdated spoiler advice, remove unneeded string token

* chore(CG): update date line to intended publication

---------

Co-authored-by: SabreCat <sabe@habitica.com>
2023-02-08 15:04:30 -06:00
SabreCat
015631685b 4.259.1 2023-02-08 15:04:19 -06:00
CuriousMagpie
20df5eeb8f buy modal complete! 2023-02-08 15:16:17 -05:00
CuriousMagpie
23f7dd94b6 more stylin' of the buy modal 2023-02-07 15:24:53 -05:00
CuriousMagpie
7125da4533 Merge branch 'develop' into increment-component 2023-02-07 14:05:48 -05:00
SabreCat
6c536c0b89 Merge branch 'release' into develop 2023-02-07 09:12:03 -06:00
CuriousMagpie
684cb59a7c buyModal styling/functions 2023-02-06 16:23:20 -05:00
CuriousMagpie
9274fe9a10 Merge branch 'develop' into increment-component 2023-02-06 14:30:34 -05:00
Phillip Thelen
ad6555c92b fix import 2023-02-06 17:16:43 +01:00
Phillip Thelen
c04e8ea514 comma 2023-02-06 13:01:06 +01:00
Phillip Thelen
aec2409227 lint fixes 2023-02-06 12:59:54 +01:00
Phillip Thelen
87aebcc19e Merge branch 'apple_sub_fix' of https://github.com/HabitRPG/habitica into apple_sub_fix
# Conflicts:
#	test/api/unit/libs/payments/apple.test.js
#	website/server/libs/payments/subscriptions.js
2023-02-06 11:19:00 +01:00
Phillip Thelen
a3bc20f855 Fix case where a number was sometimes a string 2023-02-06 11:17:35 +01:00
Phillip Thelen
86e33b2364 remove log 2023-02-06 11:17:35 +01:00
Phillip Thelen
12479edb77 fix tests and some cases 2023-02-06 11:17:35 +01:00
Phillip Thelen
c0c6657536 handle upgrades and creations better 2023-02-06 11:17:35 +01:00
SabreCat
e81a052f66 fix(lint): line lengths and so on 2023-02-06 11:17:35 +01:00
Phillip Thelen
82a1d6ff0e Improve handling 2023-02-06 11:17:35 +01:00
Phillip Thelen
0f7001b609 fix lint 2023-02-06 11:17:35 +01:00
Phillip Thelen
87558a325e Handle subscription cancelation better 2023-02-06 11:17:35 +01:00
Phillip Thelen
de48925341 remove only 2023-02-06 11:17:35 +01:00
Phillip Thelen
614850e56c fix tests 2023-02-06 11:17:35 +01:00
Phillip Thelen
64a3515c10 Add logic for different types of sub upgrades 2023-02-06 11:17:35 +01:00
Phillip Thelen
8dfa21a4b8 Add field to track when current subscription type started 2023-02-06 11:17:35 +01:00
Phillip Thelen
f9a9d4919b Improve recheck handling for test subs 2023-02-06 11:17:35 +01:00
Phillip Thelen
ddf1b4060d Better handling for cancellation when user had multiple subs 2023-02-06 11:17:35 +01:00
Phillip Thelen
967717a010 Fix logic for apple subscriptions 2023-02-06 11:17:35 +01:00
SabreCat
9b791b4ba0 fix(test): save user to avoid lock errors 2023-02-06 11:17:35 +01:00
SabreCat
5aca5b4be7 fix(test): linting 2023-02-06 11:17:35 +01:00
Phillip Thelen
0dd25b6431 fix issue where subs would be applied multiple times 2023-02-06 11:17:35 +01:00
Phillip Thelen
cf75d941fa fix test 2023-02-06 11:17:35 +01:00
Phillip Thelen
777f7887b4 fix lint errors 2023-02-06 11:17:35 +01:00
Phillip Thelen
f07d0f6441 Implement correct handling for when subs are up/downgraded 2023-02-06 11:17:24 +01:00
Phillip Thelen
98ec1757f9 fix tests 2023-02-06 11:16:24 +01:00
SabreCat
742da1f2c6 fix(typo): customER 2023-02-06 11:16:24 +01:00
SabreCat
b3d5a8d083 fix(lint): line length 2023-02-06 11:16:24 +01:00
Phillip Thelen
b5f2e66025 fix check 2023-02-06 11:16:24 +01:00
Phillip Thelen
9a40674d8d Allow sub upgrades/downgrades on iOS 2023-02-06 11:16:24 +01:00
SabreCat
0f9b6ab591 fix(lint): revert bad lint rule again 2023-01-31 11:00:08 -06:00
CuriousMagpie
a21f083761 gem modal styling/functions 2023-01-25 16:44:14 -05:00
CuriousMagpie
c7e2834fc6 Merge branch 'develop' into increment-component 2023-01-25 14:35:39 -05:00
CuriousMagpie
a08c26b076 calculations working, style updates to subscriber gem modal 2022-12-22 16:50:55 -05:00
CuriousMagpie
f4aa88e1ff more style updates to buy modal 2022-12-21 16:58:22 -05:00
CuriousMagpie
53eab7aa29 Working on styling 2022-12-20 16:53:51 -05:00
CuriousMagpie
8374d61f52 why does NaN ask the user to buy gems? 2022-12-19 17:26:49 -05:00
CuriousMagpie
4c943b7575 number-increment works on buy modal and there was much rejoicing... 2022-12-16 16:51:19 -05:00
CuriousMagpie
24032b57f6 why clicky click no click? 2022-12-15 16:48:59 -05:00
CuriousMagpie
8628c774e5 more css changes to buy modal 2022-12-13 10:53:38 -05:00
CuriousMagpie
523f044914 css changes to buy modal 2022-12-09 17:04:35 -05:00
CuriousMagpie
892c9ad040 trying to make clicky clicky work 2022-12-09 11:47:57 -05:00
Phillip Thelen
cde5fbef85 Fix case where a number was sometimes a string 2022-12-09 12:16:33 +01:00
CuriousMagpie
570f39c620 Merge branch 'develop' into increment-component 2022-12-08 10:22:08 -05:00
CuriousMagpie
a73316ef9f Merge branch 'develop' into increment-component 2022-12-06 11:47:53 -05:00
Phillip Thelen
8b2af1ef56 remove log 2022-12-05 15:38:30 +01:00
Phillip Thelen
21652c2670 fix tests and some cases 2022-12-02 17:12:17 +01:00
Phillip Thelen
d1ee679810 handle upgrades and creations better 2022-12-02 11:51:33 +01:00
CuriousMagpie
6d6195ae6a props attempt 2022-12-01 14:43:08 -05:00
CuriousMagpie
4ba66c7018 unbreaking what was broken yesterday 2022-11-30 17:16:51 -05:00
SabreCat
67988da33c fix(lint): line lengths and so on 2022-11-30 15:27:53 -06:00
CuriousMagpie
54b9424c6e Merge branch 'develop' into increment-component 2022-11-30 12:04:09 -05:00
Phillip Thelen
fae26a517d Improve handling 2022-11-30 15:06:50 +01:00
CuriousMagpie
af574634b0 tried to make if/else logic simpler, ended up breaking everything. 2022-11-29 17:13:48 -05:00
CuriousMagpie
d1e1c09b4a add conditionals, add component to buy & sell modals 2022-11-22 15:28:48 -05:00
CuriousMagpie
4f5a720c30 how to make component show up? 2022-11-18 15:27:18 -05:00
CuriousMagpie
4ddfdb84ac get most of the right parts in the same place 2022-11-17 14:57:58 -05:00
Phillip Thelen
e3c86349b4 fix lint 2022-11-11 13:58:45 +01:00
Phillip Thelen
6604f38144 Handle subscription cancelation better 2022-11-11 13:54:17 +01:00
Phillip Thelen
037882b50a remove only 2022-11-10 13:55:58 +01:00
Phillip Thelen
15deb778fd fix tests 2022-11-10 13:48:58 +01:00
Phillip Thelen
7d2529f5e1 Add logic for different types of sub upgrades 2022-11-09 19:49:53 +01:00
Phillip Thelen
8d732c59c4 Add field to track when current subscription type started 2022-11-08 12:38:24 +01:00
Phillip Thelen
3a34aa4cc5 Improve recheck handling for test subs 2022-11-08 12:19:17 +01:00
Phillip Thelen
e7fc7feddd Better handling for cancellation when user had multiple subs 2022-11-04 13:30:50 +01:00
Phillip Thelen
7fd899b642 Fix logic for apple subscriptions 2022-11-03 17:48:36 +01:00
SabreCat
36d2ad6b9b fix(test): save user to avoid lock errors 2022-11-02 15:15:28 -05:00
SabreCat
164dbdcf10 Merge branch 'develop' into apple_sub_fix 2022-11-02 15:00:24 -05:00
SabreCat
b65fa941b9 fix(test): linting 2022-11-02 14:44:49 -05:00
Phillip Thelen
ab953440e3 fix issue where subs would be applied multiple times 2022-11-02 16:36:09 +01:00
Phillip Thelen
1143f690d1 fix test 2022-10-28 15:28:46 +02:00
Phillip Thelen
08469c556b fix lint errors 2022-10-28 12:41:43 +02:00
Phillip Thelen
13a25ad89e Implement correct handling for when subs are up/downgraded 2022-10-28 11:23:48 +02:00
Phillip Thelen
8e2e170930 fix tests 2022-10-26 16:30:35 +02:00
SabreCat
e6a7d15644 fix(typo): customER 2022-10-25 16:59:59 -05:00
SabreCat
6a4b08203f fix(lint): line length 2022-10-25 16:52:16 -05:00
SabreCat
c9016c8d42 Merge branch 'develop' into phillip/sub_change 2022-10-25 16:47:51 -05:00
Phillip Thelen
31685c3e94 fix check 2022-10-21 16:59:18 +02:00
Phillip Thelen
c25b09c7ed Allow sub upgrades/downgrades on iOS 2022-10-21 16:57:12 +02:00
703 changed files with 23470 additions and 11357 deletions

View File

@@ -86,5 +86,6 @@
"RATE_LIMITER_ENABLED": "false",
"REDIS_HOST": "aaabbbcccdddeeefff",
"REDIS_PORT": "1234",
"REDIS_PASSWORD": "12345678"
"REDIS_PASSWORD": "12345678",
"TRUSTED_DOMAINS": "localhost,habitica.com"
}

View File

@@ -0,0 +1,158 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230522_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['Parrot-Base']
&& pets['Parrot-CottonCandyBlue']
&& pets['Parrot-CottonCandyPink']
&& pets['Parrot-Desert']
&& pets['Parrot-Golden']
&& pets['Parrot-Red']
&& pets['Parrot-Shade']
&& pets['Parrot-Skeleton']
&& pets['Parrot-White']
&& pets['Parrot-Zombie']
&& pets['Rooster-Base']
&& pets['Rooster-CottonCandyBlue']
&& pets['Rooster-CottonCandyPink']
&& pets['Rooster-Desert']
&& pets['Rooster-Golden']
&& pets['Rooster-Red']
&& pets['Rooster-Shade']
&& pets['Rooster-Skeleton']
&& pets['Rooster-White']
&& pets['Rooster-Zombie']
&& pets['Triceratops-Base']
&& pets['Triceratops-CottonCandyBlue']
&& pets['Triceratops-CottonCandyPink']
&& pets['Triceratops-Desert']
&& pets['Triceratops-Golden']
&& pets['Triceratops-Red']
&& pets['Triceratops-Shade']
&& pets['Triceratops-Skeleton']
&& pets['Triceratops-White']
&& pets['Triceratops-Zombie']
&& pets['TRex-Base']
&& pets['TRex-CottonCandyBlue']
&& pets['TRex-CottonCandyPink']
&& pets['TRex-Desert']
&& pets['TRex-Golden']
&& pets['TRex-Red']
&& pets['TRex-Shade']
&& pets['TRex-Skeleton']
&& pets['TRex-White']
&& pets['TRex-Zombie']
&& pets['Pterodactyl-Base']
&& pets['Pterodactyl-CottonCandyBlue']
&& pets['Pterodactyl-CottonCandyPink']
&& pets['Pterodactyl-Desert']
&& pets['Pterodactyl-Golden']
&& pets['Pterodactyl-Red']
&& pets['Pterodactyl-Shade']
&& pets['Pterodactyl-Skeleton']
&& pets['Pterodactyl-White']
&& pets['Pterodactyl-Zombie']
&& pets['Owl-Base']
&& pets['Owl-CottonCandyBlue']
&& pets['Owl-CottonCandyPink']
&& pets['Owl-Desert']
&& pets['Owl-Golden']
&& pets['Owl-Red']
&& pets['Owl-Shade']
&& pets['Owl-Skeleton']
&& pets['Owl-White']
&& pets['Owl-Zombie']
&& pets['Velociraptor-Base']
&& pets['Velociraptor-CottonCandyBlue']
&& pets['Velociraptor-CottonCandyPink']
&& pets['Velociraptor-Desert']
&& pets['Velociraptor-Golden']
&& pets['Velociraptor-Red']
&& pets['Velociraptor-Shade']
&& pets['Velociraptor-Skeleton']
&& pets['Velociraptor-White']
&& pets['Velociraptor-Zombie']
&& pets['Penguin-Base']
&& pets['Penguin-CottonCandyBlue']
&& pets['Penguin-CottonCandyPink']
&& pets['Penguin-Desert']
&& pets['Penguin-Golden']
&& pets['Penguin-Red']
&& pets['Penguin-Shade']
&& pets['Penguin-Skeleton']
&& pets['Penguin-White']
&& pets['Penguin-Zombie']
&& pets['Falcon-Base']
&& pets['Falcon-CottonCandyBlue']
&& pets['Falcon-CottonCandyPink']
&& pets['Falcon-Desert']
&& pets['Falcon-Golden']
&& pets['Falcon-Red']
&& pets['Falcon-Shade']
&& pets['Falcon-Skeleton']
&& pets['Falcon-White']
&& pets['Falcon-Zombie']
&& pets['Peacock-Base']
&& pets['Peacock-CottonCandyBlue']
&& pets['Peacock-CottonCandyPink']
&& pets['Peacock-Desert']
&& pets['Peacock-Golden']
&& pets['Peacock-Red']
&& pets['Peacock-Shade']
&& pets['Peacock-Skeleton']
&& pets['Peacock-White']
&& pets['Peacock-Zombie']) {
set['achievements.dinosaurDynasty'] = true;
}
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await User.update({ _id: user._id }, { $set: set }).exec();
}
export default async function processUsers () {
let query = {
// migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-04-15') },
};
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,79 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230718_summer_splash_orcas';
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 };
const push = {};
if (user && user.items && user.items.pets && typeof user.items.pets['Orca-Base'] !== 'undefined') {
return;
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Orca-Base'] !== 'undefined') {
set['items.pets.Orca-Base'] = 5;
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_orca_pet',
title: 'Orcas for Summer Splash!',
text: 'To celebrate Summer Splash, we\'ve given you an Orca Pet!',
destination: 'stable',
},
seen: false,
};
} else {
set['items.mounts.Orca-Base'] = true;
push.notifications = {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_orca_mount',
title: 'Orcas for Summer Splash!',
text: 'To celebrate Summer Splash, we\'ve given you an Orca Mount!',
destination: 'stable',
},
seen: false,
};
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
return await user.updateOne({ $set: set, $push: push }).exec();
}
export default async function processUsers () {
let query = {
migration: {$ne: MIGRATION_NAME},
'auth.timestamps.loggedin': {$gt: new Date('2023-06-18')},
};
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)
.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,155 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230731_naming_day';
import { v4 as uuid } from 'uuid';
import { model as User } from '../../../website/server/models/user';
const progressCount = 1000;
let count = 0;
async function updateUser (user) {
count++;
let set;
let push;
const inc = {
'items.food.Cake_Base': 1,
'items.food.Cake_CottonCandyBlue': 1,
'items.food.Cake_CottonCandyPink': 1,
'items.food.Cake_Desert': 1,
'items.food.Cake_Golden': 1,
'items.food.Cake_Red': 1,
'items.food.Cake_Shade': 1,
'items.food.Cake_Skeleton': 1,
'items.food.Cake_White': 1,
'items.food.Cake_Zombie': 1,
'achievements.habiticaDays': 1,
};
if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.back_special_namingDay2020 !== 'undefined') {
set = { migration: MIGRATION_NAME };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_cake',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you some cake!',
destination: '/inventory/items',
},
seen: false,
},
};
} else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.body_special_namingDay2018 !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.back_special_namingDay2020': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_back',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Tail and cake!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user && user.items && user.items.gear && user.items.gear.owned && typeof user.items.gear.owned.head_special_namingDay2017 !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.body_special_namingDay2018': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_body',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Cloak and cake!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user && user.items && user.items.pets && typeof user.items.pets['Gryphon-RoyalPurple'] !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.gear.owned.head_special_namingDay2017': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_head',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Helm and cake!',
destination: '/inventory/equipment',
},
seen: false,
},
};
} else if (user && user.items && user.items.mounts && typeof user.items.mounts['Gryphon-RoyalPurple'] !== 'undefined') {
set = { migration: MIGRATION_NAME, 'items.pets.Gryphon-RoyalPurple': 5 };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_pet',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Pet and cake!',
destination: '/inventory/stable',
},
seen: false,
},
};
} else {
set = { migration: MIGRATION_NAME, 'items.mounts.Gryphon-RoyalPurple': true };
push = {
notifications: {
type: 'ITEM_RECEIVED',
data: {
icon: 'notif_namingDay_mount',
title: 'Happy Naming Day!',
text: 'To celebrate the day we became Habitica, weve awarded you a Royal Purple Gryphon Mount and cake!',
destination: '/inventory/stable',
},
seen: false,
},
};
}
if (count % progressCount === 0) console.warn(`${count} ${user._id}`);
if (push) {
return await user.updateOne({ $set: set, $inc: inc, $push: push }).exec();
} else {
return await user.updateOne({ $set: set, $inc: inc }).exec();
}
}
export default async function processUsers () {
let query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2023-07-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)
.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,144 @@
/* eslint-disable no-console */
const MIGRATION_NAME = '20230801_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-01') },
};
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,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

@@ -3,7 +3,7 @@ import { v4 as uuid } from 'uuid';
import { model as User } from '../../website/server/models/user';
const MIGRATION_NAME = '20220314_pi_day';
const MIGRATION_NAME = '20230314_pi_day';
const progressCount = 1000;
let count = 0;
@@ -54,7 +54,7 @@ async function updateUser (user) {
export default async function processUsers () {
const query = {
migration: { $ne: MIGRATION_NAME },
'auth.timestamps.loggedin': { $gt: new Date('2022-02-15') },
'auth.timestamps.loggedin': { $gt: new Date('2023-02-15') },
};
const fields = {

3512
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,22 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "4.259.0",
"version": "5.0.0",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.20.12",
"@babel/preset-env": "^7.20.2",
"@babel/register": "^7.18.9",
"@babel/core": "^7.22.5",
"@babel/preset-env": "^7.22.5",
"@babel/register": "^7.22.5",
"@google-cloud/trace-agent": "^7.1.2",
"@parse/node-apn": "^5.1.3",
"@slack/webhook": "^6.1.0",
"accepts": "^1.3.8",
"amazon-payments": "^0.2.9",
"amplitude": "^6.0.0",
"apidoc": "^0.53.1",
"apple-auth": "^1.0.7",
"apidoc": "^0.54.0",
"apple-auth": "^1.0.9",
"bcrypt": "^5.1.0",
"body-parser": "^1.20.1",
"body-parser": "^1.20.2",
"bootstrap": "^4.6.0",
"compression": "^1.7.4",
"cookie-session": "^2.0.0",
@@ -30,7 +30,7 @@
"express": "^4.18.2",
"express-basic-auth": "^1.2.1",
"express-validator": "^5.2.0",
"glob": "^8.0.3",
"glob": "^8.1.0",
"got": "^11.8.3",
"gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
@@ -42,7 +42,7 @@
"image-size": "^1.0.2",
"in-app-purchase": "^1.11.3",
"js2xmlparser": "^5.0.0",
"jsonwebtoken": "^8.5.1",
"jsonwebtoken": "^9.0.0",
"jwks-rsa": "^2.1.5",
"lodash": "^4.17.21",
"merge-stream": "^2.0.0",
@@ -67,16 +67,16 @@
"remove-markdown": "^0.5.0",
"rimraf": "^3.0.2",
"short-uuid": "^4.2.2",
"stripe": "^11.6.0",
"superagent": "^8.0.6",
"stripe": "^12.9.0",
"superagent": "^8.0.9",
"universal-analytics": "^0.5.3",
"useragent": "^2.1.9",
"uuid": "^9.0.0",
"validator": "^13.7.0",
"validator": "^13.9.0",
"vinyl-buffer": "^1.0.1",
"winston": "^3.8.2",
"winston": "^3.9.0",
"winston-loggly-bulk": "^3.2.1",
"xml2js": "^0.4.23"
"xml2js": "^0.6.0"
},
"private": true,
"engines": {
@@ -110,7 +110,7 @@
"apidoc": "gulp apidoc"
},
"devDependencies": {
"axios": "^1.2.2",
"axios": "^1.3.6",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-moment": "^0.1.0",
@@ -122,7 +122,7 @@
"monk": "^7.3.4",
"require-again": "^2.0.0",
"run-rs": "^0.7.7",
"sinon": "^15.0.1",
"sinon": "^15.1.2",
"sinon-chai": "^3.7.0",
"sinon-stub-promise": "^4.0.0"
},

View File

@@ -231,13 +231,16 @@ describe('cron', async () => {
},
});
// user1 has a 1-month recurring subscription starting today
user1.purchased.plan.customerId = 'subscribedId';
user1.purchased.plan.dateUpdated = moment().toDate();
user1.purchased.plan.planId = 'basic';
user1.purchased.plan.consecutive.count = 0;
user1.purchased.plan.consecutive.offset = 0;
user1.purchased.plan.consecutive.trinkets = 0;
user1.purchased.plan.consecutive.gemCapExtra = 0;
beforeEach(async () => {
user1.purchased.plan.customerId = 'subscribedId';
user1.purchased.plan.dateUpdated = moment().toDate();
user1.purchased.plan.planId = 'basic';
user1.purchased.plan.consecutive.count = 0;
user1.purchased.plan.perkMonthCount = 0;
user1.purchased.plan.consecutive.offset = 0;
user1.purchased.plan.consecutive.trinkets = 0;
user1.purchased.plan.consecutive.gemCapExtra = 0;
});
it('does not increment consecutive benefits after the first month', async () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
@@ -271,6 +274,24 @@ describe('cron', async () => {
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(0);
});
it('increments consecutive benefits after the second month if they also received a 1 month gift subscription', async () => {
user1.purchased.plan.perkMonthCount = 1;
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
.add(2, 'days')
.toDate());
// Add 1 month to simulate what happens a month after the subscription was created.
// Add 2 days so that we're sure we're not affected by any start-of-month effects
// e.g., from time zone oddness.
await cron({
user: user1, tasksByType, daysMissed, analytics,
});
expect(user1.purchased.plan.perkMonthCount).to.equal(0);
expect(user1.purchased.plan.consecutive.count).to.equal(2);
expect(user1.purchased.plan.consecutive.offset).to.equal(0);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(1);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(5);
});
it('increments consecutive benefits after the third month', async () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(3, 'months')
.add(2, 'days')
@@ -315,6 +336,30 @@ describe('cron', async () => {
expect(user1.purchased.plan.consecutive.trinkets).to.equal(3);
expect(user1.purchased.plan.consecutive.gemCapExtra).to.equal(15);
});
it('initializes plan.perkMonthCount if necessary', async () => {
user.purchased.plan.perkMonthCount = undefined;
clock = sinon.useFakeTimers(moment(user.purchased.plan.dateUpdated)
.utcOffset(0)
.startOf('month')
.add(1, 'months')
.add(2, 'days')
.toDate());
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(user.purchased.plan.perkMonthCount).to.equal(1);
user.purchased.plan.perkMonthCount = undefined;
user.purchased.plan.consecutive.count = 8;
clock.restore();
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(2, 'months')
.add(2, 'days')
.toDate());
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(user.purchased.plan.perkMonthCount).to.equal(2);
});
});
describe('for a 3-month recurring subscription', async () => {
@@ -330,13 +375,16 @@ describe('cron', async () => {
},
});
// user3 has a 3-month recurring subscription starting today
user3.purchased.plan.customerId = 'subscribedId';
user3.purchased.plan.dateUpdated = moment().toDate();
user3.purchased.plan.planId = 'basic_3mo';
user3.purchased.plan.consecutive.count = 0;
user3.purchased.plan.consecutive.offset = 3;
user3.purchased.plan.consecutive.trinkets = 1;
user3.purchased.plan.consecutive.gemCapExtra = 5;
beforeEach(async () => {
user3.purchased.plan.customerId = 'subscribedId';
user3.purchased.plan.dateUpdated = moment().toDate();
user3.purchased.plan.planId = 'basic_3mo';
user3.purchased.plan.perkMonthCount = 0;
user3.purchased.plan.consecutive.count = 0;
user3.purchased.plan.consecutive.offset = 3;
user3.purchased.plan.consecutive.trinkets = 1;
user3.purchased.plan.consecutive.gemCapExtra = 5;
});
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
@@ -390,6 +438,21 @@ describe('cron', async () => {
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('keeps existing plan.perkMonthCount intact when incrementing consecutive benefits', async () => {
user3.purchased.plan.perkMonthCount = 2;
user3.purchased.plan.consecutive.trinkets = 1;
user3.purchased.plan.consecutive.gemCapExtra = 5;
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(4, 'months')
.add(2, 'days')
.toDate());
await cron({
user: user3, tasksByType, daysMissed, analytics,
});
expect(user3.purchased.plan.perkMonthCount).to.equal(2);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
expect(user3.purchased.plan.consecutive.gemCapExtra).to.equal(10);
});
it('does not increment consecutive benefits in the second month of the second period that they already have benefits for', async () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(5, 'months')
.add(2, 'days')
@@ -456,13 +519,16 @@ describe('cron', async () => {
},
});
// user6 has a 6-month recurring subscription starting today
user6.purchased.plan.customerId = 'subscribedId';
user6.purchased.plan.dateUpdated = moment().toDate();
user6.purchased.plan.planId = 'google_6mo';
user6.purchased.plan.consecutive.count = 0;
user6.purchased.plan.consecutive.offset = 6;
user6.purchased.plan.consecutive.trinkets = 2;
user6.purchased.plan.consecutive.gemCapExtra = 10;
beforeEach(async () => {
user6.purchased.plan.customerId = 'subscribedId';
user6.purchased.plan.dateUpdated = moment().toDate();
user6.purchased.plan.planId = 'google_6mo';
user6.purchased.plan.perkMonthCount = 0;
user6.purchased.plan.consecutive.count = 0;
user6.purchased.plan.consecutive.offset = 6;
user6.purchased.plan.consecutive.trinkets = 2;
user6.purchased.plan.consecutive.gemCapExtra = 10;
});
it('does not increment consecutive benefits in the first month of the first paid period that they already have benefits for', async () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(1, 'months')
@@ -503,6 +569,19 @@ describe('cron', async () => {
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20);
});
it('keeps existing plan.perkMonthCount intact when incrementing consecutive benefits', async () => {
user6.purchased.plan.perkMonthCount = 2;
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(7, 'months')
.add(2, 'days')
.toDate());
await cron({
user: user6, tasksByType, daysMissed, analytics,
});
expect(user6.purchased.plan.perkMonthCount).to.equal(2);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(4);
expect(user6.purchased.plan.consecutive.gemCapExtra).to.equal(20);
});
it('increments consecutive benefits the month after the third paid period has started', async () => {
clock = sinon.useFakeTimers(moment().utcOffset(0).startOf('month').add(13, 'months')
.add(2, 'days')

View File

@@ -29,8 +29,9 @@ describe('Apple Payments', () => {
.resolves();
iapValidateStub = sinon.stub(iap, 'validate')
.resolves({});
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
sinon.stub(iap, 'isExpired').returns(false);
sinon.stub(iap, 'isCanceled').returns(false);
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
productId: 'com.habitrpg.ios.Habitica.21gems',
@@ -44,6 +45,8 @@ describe('Apple Payments', () => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore();
payments.buySkuItem.restore();
gems.validateGiftMessage.restore();
@@ -218,6 +221,7 @@ describe('Apple Payments', () => {
headers = {};
receipt = `{"token": "${token}"}`;
nextPaymentProcessing = moment.utc().add({ days: 2 });
user = new User();
iapSetupStub = sinon.stub(iap, 'setup')
.resolves();
@@ -228,14 +232,17 @@ describe('Apple Payments', () => {
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().subtract({ day: 1 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: sku,
transactionId: token,
}, {
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: 'wrongsku',
transactionId: token,
}, {
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: sku,
transactionId: token,
}]);
@@ -250,21 +257,12 @@ describe('Apple Payments', () => {
if (payments.createSubscription.restore) payments.createSubscription.restore();
});
it('should throw an error if sku is empty', async () => {
await expect(applePayments.subscribe('', user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 400,
name: 'BadRequest',
message: i18n.t('missingSubscriptionCode'),
});
});
it('should throw an error if receipt is invalid', async () => {
iap.isValidated.restore();
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(false);
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
@@ -295,13 +293,15 @@ describe('Apple Payments', () => {
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
expirationDate: moment.utc().add({ day: 2 }).toDate(),
purchaseDate: new Date(),
productId: option.sku,
transactionId: token,
originalTransactionId: token,
}]);
sub = common.content.subscriptionBlocks[option.subKey];
await applePayments.subscribe(option.sku, user, receipt, headers, nextPaymentProcessing);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
@@ -321,21 +321,253 @@ describe('Apple Payments', () => {
nextPaymentProcessing,
});
});
if (option !== subOptions[3]) {
const newOption = subOptions[3];
it(`upgrades a subscription from ${option.sku} to ${newOption.sku}`, async () => {
const oldSub = common.content.subscriptionBlocks[option.subKey];
oldSub.logic = 'refundAndRepay';
user.profile.name = 'sender';
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
user.purchased.plan.customerId = token;
user.purchased.plan.planId = option.subKey;
user.purchased.plan.additionalData = receipt;
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 2 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: newOption.sku,
transactionId: `${token}new`,
originalTransactionId: token,
}]);
sub = common.content.subscriptionBlocks[newOption.subKey];
await applePayments.subscribe(user,
receipt,
headers,
nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: token,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
updatedFrom: oldSub,
});
});
}
if (option !== subOptions[0]) {
const newOption = subOptions[0];
it(`downgrades a subscription from ${option.sku} to ${newOption.sku}`, async () => {
const oldSub = common.content.subscriptionBlocks[option.subKey];
user.profile.name = 'sender';
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
user.purchased.plan.customerId = token;
user.purchased.plan.planId = option.subKey;
user.purchased.plan.additionalData = receipt;
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 2 }).toDate(),
purchaseDate: moment.utc().valueOf(),
productId: newOption.sku,
transactionId: `${token}new`,
originalTransactionId: token,
}]);
sub = common.content.subscriptionBlocks[newOption.subKey];
await applePayments.subscribe(user,
receipt,
headers,
nextPaymentProcessing);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: token,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
updatedFrom: oldSub,
});
});
}
});
it('errors when a user is already subscribed', async () => {
payments.createSubscription.restore();
user = new User();
await user.save();
it('uses the most recent subscription data', async () => {
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 4 }).toDate(),
purchaseDate: moment.utc().subtract({ day: 5 }).toDate(),
productId: 'com.habitrpg.ios.habitica.subscription.3month',
transactionId: `${token}oldest`,
originalTransactionId: `${token}evenOlder`,
}, {
expirationDate: moment.utc().add({ day: 2 }).toDate(),
purchaseDate: moment.utc().subtract({ day: 1 }).toDate(),
productId: 'com.habitrpg.ios.habitica.subscription.12month',
transactionId: `${token}newest`,
originalTransactionId: `${token}newest`,
}, {
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().subtract({ day: 2 }).toDate(),
productId: 'com.habitrpg.ios.habitica.subscription.6month',
transactionId: token,
originalTransactionId: token,
}]);
sub = common.content.subscriptionBlocks.basic_12mo;
await applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
await expect(applePayments.subscribe(sku, user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED,
});
expect(paymentsCreateSubscritionStub).to.be.calledOnce;
expect(paymentsCreateSubscritionStub).to.be.calledWith({
user,
customerId: `${token}newest`,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
sub,
headers,
additionalData: receipt,
nextPaymentProcessing,
});
});
describe('does not apply multiple times', async () => {
it('errors when a user is using the same subscription', async () => {
payments.createSubscription.restore();
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: token,
originalTransactionId: token,
}]);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED,
});
});
it('errors when a user is using a rebill of the same subscription', async () => {
user = new User();
await user.save();
payments.createSubscription.restore();
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: `${token}renew`,
originalTransactionId: token,
}]);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
await expect(applePayments.subscribe(user, receipt, headers, nextPaymentProcessing))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED,
});
});
it('errors when a different user is using the subscription', async () => {
user = new User();
await user.save();
payments.createSubscription.restore();
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: token,
originalTransactionId: token,
}]);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
const secondUser = new User();
await secondUser.save();
await expect(applePayments.subscribe(
secondUser, receipt, headers, nextPaymentProcessing,
))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED,
});
});
it('errors when a multiple users exist using the subscription', async () => {
user = new User();
await user.save();
payments.createSubscription.restore();
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: token,
originalTransactionId: token,
}]);
await applePayments.subscribe(user, receipt, headers, nextPaymentProcessing);
const secondUser = new User();
secondUser.purchased.plan = user.purchased.plan;
secondUser.purchased.plan.dateTerminate = new Date();
secondUser.save();
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{
expirationDate: moment.utc().add({ day: 1 }).toDate(),
purchaseDate: moment.utc().toDate(),
productId: sku,
transactionId: `${token}new`,
originalTransactionId: token,
}]);
const thirdUser = new User();
await thirdUser.save();
await expect(applePayments.subscribe(
thirdUser, receipt, headers, nextPaymentProcessing,
))
.to.eventually.be.rejected.and.to.eql({
httpCode: 401,
name: 'NotAuthorized',
message: applePayments.constants.RESPONSE_ALREADY_USED,
});
});
});
});
@@ -360,9 +592,9 @@ describe('Apple Payments', () => {
});
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: expirationDate.toDate() }]);
iapIsValidatedStub = sinon.stub(iap, 'isValidated')
.returns(true);
iapIsValidatedStub = sinon.stub(iap, 'isValidated').returns(true);
sinon.stub(iap, 'isCanceled').returns(false);
sinon.stub(iap, 'isExpired').returns(true);
user = new User();
user.profile.name = 'sender';
user.purchased.plan.paymentMethod = applePayments.constants.PAYMENT_METHOD_APPLE;
@@ -377,6 +609,8 @@ describe('Apple Payments', () => {
iap.setup.restore();
iap.validate.restore();
iap.isValidated.restore();
iap.isExpired.restore();
iap.isCanceled.restore();
iap.getPurchaseData.restore();
payments.cancelSubscription.restore();
});
@@ -396,6 +630,8 @@ describe('Apple Payments', () => {
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: expirationDate.add({ day: 1 }).toDate() }]);
iap.isExpired.restore();
sinon.stub(iap, 'isExpired').returns(false);
await expect(applePayments.cancelSubscribe(user, headers))
.to.eventually.be.rejected.and.to.eql({
@@ -418,7 +654,38 @@ describe('Apple Payments', () => {
});
});
it('should cancel a user subscription', async () => {
it('should cancel a cancelled subscription with termination date in the future', async () => {
const futureDate = expirationDate.add({ day: 1 });
iap.getPurchaseData.restore();
iapGetPurchaseDataStub = sinon.stub(iap, 'getPurchaseData')
.returns([{ expirationDate: futureDate }]);
iap.isExpired.restore();
sinon.stub(iap, 'isExpired').returns(false);
iap.isCanceled.restore();
sinon.stub(iap, 'isCanceled').returns(true);
await applePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledOnce;
expect(iapValidateStub).to.be.calledWith(iap.APPLE, receipt);
expect(iapIsValidatedStub).to.be.calledOnce;
expect(iapIsValidatedStub).to.be.calledWith({
expirationDate: futureDate,
});
expect(iapGetPurchaseDataStub).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledOnce;
expect(paymentCancelSubscriptionSpy).to.be.calledWith({
user,
paymentMethod: applePayments.constants.PAYMENT_METHOD_APPLE,
nextBill: futureDate.toDate(),
headers,
});
});
it('should cancel an expired subscription', async () => {
await applePayments.cancelSubscribe(user, headers);
expect(iapSetupStub).to.be.calledOnce;

View File

@@ -203,6 +203,28 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.dateCreated).to.exist;
});
it('sets plan.dateCurrentTypeCreated if it did not previously exist', async () => {
expect(recipient.purchased.plan.dateCurrentTypeCreated).to.not.exist;
await api.createSubscription(data);
expect(recipient.purchased.plan.dateCurrentTypeCreated).to.exist;
});
it('keeps plan.dateCreated when changing subscription type', async () => {
await api.createSubscription(data);
const initialDate = recipient.purchased.plan.dateCreated;
await api.createSubscription(data);
expect(recipient.purchased.plan.dateCreated).to.eql(initialDate);
});
it('sets plan.dateCurrentTypeCreated when changing subscription type', async () => {
await api.createSubscription(data);
const initialDate = recipient.purchased.plan.dateCurrentTypeCreated;
await api.createSubscription(data);
expect(recipient.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
});
it('does not change plan.customerId if it already exists', async () => {
recipient.purchased.plan = plan;
data.customerId = 'purchaserCustomerId';
@@ -213,6 +235,116 @@ describe('payments/index', () => {
expect(recipient.purchased.plan.customerId).to.eql('customer-id');
});
it('sets plan.perkMonthCount to 1 if user is not subscribed', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 1;
recipient.purchased.plan.customerId = undefined;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
});
it('sets plan.perkMonthCount to 1 if field is not initialized', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = -1;
recipient.purchased.plan.customerId = undefined;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
});
it('sets plan.perkMonthCount to 1 if user had previous count but lapsed subscription', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 2;
recipient.purchased.plan.customerId = undefined;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
});
it('adds to plan.perkMonthCount if user is already subscribed', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 1;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(1);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
});
it('awards perks if plan.perkMonthCount reaches 3 with existing subscription', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 2;
data.sub.key = 'basic_earned';
data.gift.subscription.key = 'basic_earned';
data.gift.subscription.months = 1;
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('awards perks if plan.perkMonthCount reaches 3 without existing subscription', async () => {
recipient.purchased.plan.perkMonthCount = 0;
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('awards perks if plan.perkMonthCount reaches 3 without initialized field', async () => {
expect(recipient.purchased.plan.perkMonthCount).to.eql(-1);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(0);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('awards perks if plan.perkMonthCount goes over 3', async () => {
recipient.purchased.plan = plan;
recipient.purchased.plan.perkMonthCount = 2;
data.sub.key = 'basic_earned';
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(0);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(0);
await api.createSubscription(data);
expect(recipient.purchased.plan.perkMonthCount).to.eql(2);
expect(recipient.purchased.plan.consecutive.trinkets).to.eql(1);
expect(recipient.purchased.plan.consecutive.gemCapExtra).to.eql(5);
});
it('sets plan.customerId to "Gift" if it does not already exist', async () => {
expect(recipient.purchased.plan.customerId).to.not.exist;
@@ -379,6 +511,7 @@ describe('payments/index', () => {
expect(user.purchased.plan.customerId).to.eql('customer-id');
expect(user.purchased.plan.dateUpdated).to.exist;
expect(user.purchased.plan.gemsBought).to.eql(0);
expect(user.purchased.plan.perkMonthCount).to.eql(0);
expect(user.purchased.plan.paymentMethod).to.eql('Payment Method');
expect(user.purchased.plan.extraMonths).to.eql(0);
expect(user.purchased.plan.dateTerminated).to.eql(null);
@@ -386,6 +519,63 @@ describe('payments/index', () => {
expect(user.purchased.plan.dateCreated).to.exist;
});
it('sets plan.dateCreated if it did not previously exist', async () => {
expect(user.purchased.plan.dateCreated).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.dateCreated).to.exist;
});
it('sets plan.dateCurrentTypeCreated if it did not previously exist', async () => {
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.dateCurrentTypeCreated).to.exist;
});
it('keeps plan.dateCreated when changing subscription type', async () => {
await api.createSubscription(data);
const initialDate = user.purchased.plan.dateCreated;
await api.createSubscription(data);
expect(user.purchased.plan.dateCreated).to.eql(initialDate);
});
it('sets plan.dateCurrentTypeCreated when changing subscription type', async () => {
await api.createSubscription(data);
const initialDate = user.purchased.plan.dateCurrentTypeCreated;
await api.createSubscription(data);
expect(user.purchased.plan.dateCurrentTypeCreated).to.not.eql(initialDate);
});
it('keeps plan.perkMonthCount when changing subscription type', async () => {
await api.createSubscription(data);
user.purchased.plan.perkMonthCount = 2;
await api.createSubscription(data);
expect(user.purchased.plan.perkMonthCount).to.eql(2);
});
it('sets plan.perkMonthCount to zero when creating new monthly subscription', async () => {
user.purchased.plan.perkMonthCount = 2;
await api.createSubscription(data);
expect(user.purchased.plan.perkMonthCount).to.eql(0);
});
it('sets plan.perkMonthCount to zero when creating new 3 month subscription', async () => {
user.purchased.plan.perkMonthCount = 2;
await api.createSubscription(data);
expect(user.purchased.plan.perkMonthCount).to.eql(0);
});
it('updates plan.consecutive.offset when changing subscription type', async () => {
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(3);
data.sub.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(6);
});
it('awards the Royal Purple Jackalope pet', async () => {
await api.createSubscription(data);
@@ -465,6 +655,89 @@ describe('payments/index', () => {
},
});
});
context('Upgrades subscription', () => {
it('from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.customerId).to.eql('customer-id');
const created = user.purchased.plan.dateCreated;
const updated = user.purchased.plan.dateUpdated;
data.sub.key = 'basic_6mo';
data.updatedFrom = { key: 'basic_earned' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
});
it('from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.customerId).to.eql('customer-id');
const created = user.purchased.plan.dateCreated;
const updated = user.purchased.plan.dateUpdated;
data.sub.key = 'basic_12mo';
data.updatedFrom = { key: 'basic_3mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
});
});
context('Downgrades subscription', () => {
it('from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.customerId).to.eql('customer-id');
const created = user.purchased.plan.dateCreated;
const updated = user.purchased.plan.dateUpdated;
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
});
it('from basic_12mo to basic_3mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.customerId).to.eql('customer-id');
const created = user.purchased.plan.dateCreated;
const updated = user.purchased.plan.dateUpdated;
data.sub.key = 'basic_3mo';
data.updatedFrom = { key: 'basic_12mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.dateCreated).to.eql(created);
expect(user.purchased.plan.dateUpdated).to.not.eql(updated);
expect(user.purchased.plan.customerId).to.eql('customer-id');
});
});
});
context('Block subscription perks', () => {
@@ -475,9 +748,19 @@ describe('payments/index', () => {
});
it('does not add to plans.consecutive.offset if 1 month subscription', async () => {
data.sub.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.extraMonths).to.eql(0);
expect(user.purchased.plan.consecutive.offset).to.eql(0);
});
it('resets plans.consecutive.offset if 1 month subscription', async () => {
user.purchased.plan.consecutive.offset = 1;
await user.save();
data.sub.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.offset).to.eql(0);
});
it('adds 5 to plan.consecutive.gemCapExtra for 3 month block', async () => {
@@ -488,7 +771,6 @@ describe('payments/index', () => {
it('adds 10 to plan.consecutive.gemCapExtra for 6 month block', async () => {
data.sub.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
@@ -496,7 +778,6 @@ describe('payments/index', () => {
it('adds 20 to plan.consecutive.gemCapExtra for 12 month block', async () => {
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
@@ -532,6 +813,532 @@ describe('payments/index', () => {
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
context('Upgrades subscription', () => {
context('Using payDifference logic', () => {
beforeEach(async () => {
data.updatedFrom = { logic: 'payDifference' };
});
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
});
context('Using payFull logic', () => {
beforeEach(async () => {
data.updatedFrom = { logic: 'payFull' };
});
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
});
});
context('Using refundAndRepay logic', () => {
let clock;
beforeEach(async () => {
clock = sinon.useFakeTimers(new Date('2022-01-01'));
data.updatedFrom = { logic: 'refundAndRepay' };
});
context('Upgrades within first half of subscription', () => {
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-10'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 15 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-02-05'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-08'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-31'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2024-01-08'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 2 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-08-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
it('Adds 3 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-07-31'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
});
context('Upgrades within second half of subscription', () => {
it('Adds 10 to plan.consecutive.gemCapExtra from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-20'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('Adds 20 to plan.consecutive.gemCapExtra when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(5);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-02-24'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(25);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-01-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-05-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-03-03'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
});
it('Adds 2 to plan.consecutive.trinkets from basic_earned to basic_6mo after initial cycle', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
data.sub.key = 'basic_6mo';
data.updatedFrom.key = 'basic_earned';
clock.restore();
clock = sinon.useFakeTimers(new Date('2022-05-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_6mo to basic_12mo after initial cycle', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_6mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2023-05-28'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(6);
});
it('Adds 4 to plan.consecutive.trinkets when upgrading from basic_3mo to basic_12mo after initial cycle', async () => {
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_3mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
data.sub.key = 'basic_12mo';
data.updatedFrom.key = 'basic_3mo';
clock.restore();
clock = sinon.useFakeTimers(new Date('2023-09-03'));
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(5);
});
});
afterEach(async () => {
if (clock !== null) clock.restore();
});
});
});
context('Downgrades subscription', () => {
it('does not remove from plan.consecutive.gemCapExtra from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(10);
});
it('does not remove from plan.consecutive.gemCapExtra from basic_12mo to basic_3mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
data.sub.key = 'basic_3mo';
data.updatedFrom = { key: 'basic_12mo' };
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.gemCapExtra).to.eql(20);
});
it('does not remove from plan.consecutive.trinkets from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_6mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_earned');
expect(user.purchased.plan.consecutive.trinkets).to.eql(2);
});
it('does not remove from plan.consecutive.trinkets from basic_12mo to basic_3mo', async () => {
expect(user.purchased.plan.planId).to.not.exist;
data.sub.key = 'basic_12mo';
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.eql('basic_12mo');
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
data.sub.key = 'basic_3mo';
data.updatedFrom = { key: 'basic_12mo' };
await api.createSubscription(data);
expect(user.purchased.plan.consecutive.trinkets).to.eql(4);
});
});
});
context('Mystery Items', () => {

View File

@@ -242,7 +242,7 @@ describe('cron middleware', () => {
sandbox.spy(cronLib, 'recoverCron');
sandbox.stub(User, 'update')
sandbox.stub(User, 'updateOne')
.withArgs({
_id: user._id,
$or: [

View File

@@ -1359,6 +1359,7 @@ describe('Group Model', () => {
describe('#sendChat', () => {
beforeEach(() => {
sandbox.spy(User, 'update');
sandbox.spy(User, 'updateMany');
});
it('formats message', () => {
@@ -1413,8 +1414,8 @@ describe('Group Model', () => {
it('updates users about new messages in party', () => {
party.sendChat({ message: 'message' });
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({
'party._id': party._id,
_id: { $ne: '' },
});
@@ -1427,8 +1428,8 @@ describe('Group Model', () => {
group.sendChat({ message: 'message' });
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({
guilds: group._id,
_id: { $ne: '' },
});
@@ -1437,8 +1438,8 @@ describe('Group Model', () => {
it('does not send update to user that sent the message', () => {
party.sendChat({ message: 'message', user: { _id: 'user-id', profile: { name: 'user' } } });
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({
'party._id': party._id,
_id: { $ne: 'user-id' },
});
@@ -1731,7 +1732,7 @@ describe('Group Model', () => {
});
it('updates participting members (not including user)', async () => {
sandbox.spy(User, 'update');
sandbox.spy(User, 'updateMany');
await party.startQuest(nonParticipatingMember);
@@ -1739,7 +1740,7 @@ describe('Group Model', () => {
questLeader._id, participatingMember._id, sleepingParticipatingMember._id,
];
expect(User.update).to.be.calledWith(
expect(User.updateMany).to.be.calledWith(
{ _id: { $in: members } },
{
$set: {
@@ -1752,11 +1753,11 @@ describe('Group Model', () => {
});
it('updates non-user quest leader and decrements quest scroll', async () => {
sandbox.spy(User, 'update');
sandbox.spy(User, 'updateOne');
await party.startQuest(participatingMember);
expect(User.update).to.be.calledWith(
expect(User.updateOne).to.be.calledWith(
{ _id: questLeader._id },
{
$inc: {
@@ -1818,29 +1819,29 @@ describe('Group Model', () => {
};
it('doesn\'t retry successful operations', async () => {
sandbox.stub(User, 'update').returns(successfulMock);
sandbox.stub(User, 'updateOne').returns(successfulMock);
await party.finishQuest(quest);
expect(User.update).to.be.calledThrice;
expect(User.updateOne).to.be.calledThrice;
});
it('stops retrying when a successful update has occurred', async () => {
const updateStub = sandbox.stub(User, 'update');
const updateStub = sandbox.stub(User, 'updateOne');
updateStub.onCall(0).returns(failedMock);
updateStub.returns(successfulMock);
await party.finishQuest(quest);
expect(User.update.callCount).to.equal(4);
expect(User.updateOne.callCount).to.equal(4);
});
it('retries failed updates at most five times per user', async () => {
sandbox.stub(User, 'update').returns(failedMock);
sandbox.stub(User, 'updateOne').returns(failedMock);
await expect(party.finishQuest(quest)).to.eventually.be.rejected;
expect(User.update.callCount).to.eql(15); // for 3 users
expect(User.updateOne.callCount).to.eql(15); // for 3 users
});
});
@@ -2087,17 +2088,17 @@ describe('Group Model', () => {
context('Party quests', () => {
it('updates participating members with rewards', async () => {
sandbox.spy(User, 'update');
sandbox.spy(User, 'updateOne');
await party.finishQuest(quest);
expect(User.update).to.be.calledThrice;
expect(User.update).to.be.calledWithMatch({
expect(User.updateOne).to.be.calledThrice;
expect(User.updateOne).to.be.calledWithMatch({
_id: questLeader._id,
});
expect(User.update).to.be.calledWithMatch({
expect(User.updateOne).to.be.calledWithMatch({
_id: participatingMember._id,
});
expect(User.update).to.be.calledWithMatch({
expect(User.updateOne).to.be.calledWithMatch({
_id: sleepingParticipatingMember._id,
});
});
@@ -2172,11 +2173,11 @@ describe('Group Model', () => {
});
it('updates all users with rewards', async () => {
sandbox.spy(User, 'update');
sandbox.spy(User, 'updateMany');
await party.finishQuest(tavernQuest);
expect(User.update).to.be.calledOnce;
expect(User.update).to.be.calledWithMatch({});
expect(User.updateMany).to.be.calledOnce;
expect(User.updateMany).to.be.calledWithMatch({});
});
it('sets quest completed to the world quest key', async () => {

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

@@ -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

@@ -2,7 +2,6 @@ import { v4 as generateUUID } from 'uuid';
import {
generateUser,
createAndPopulateGroup,
checkExistence,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -19,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 () => {
@@ -183,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 () => {
@@ -258,47 +200,6 @@ describe('POST /group/:groupId/join', () => {
await expect(user.get('/user')).to.eventually.have.nested.property('items.quests.basilist', 2);
});
it('deletes previous party where the user was the only member', async () => {
const userToInvite = await generateUser();
const oldParty = await userToInvite.post('/groups', { // add user to a party
name: 'Another Test Party',
type: 'party',
});
await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(true);
await user.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
await userToInvite.post(`/groups/${party._id}/join`);
await expect(user.get('/user')).to.eventually.have.nested.property('party._id', party._id);
await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(false);
});
it('does not allow user to leave a party if a quest was active and they were the only member', async () => {
const userToInvite = await generateUser();
const oldParty = await userToInvite.post('/groups', { // add user to a party
name: 'Another Test Party',
type: 'party',
});
await userToInvite.update({
[`items.quests.${PET_QUEST}`]: 1,
});
await userToInvite.post(`/groups/${oldParty._id}/quests/invite/${PET_QUEST}`);
await expect(checkExistence('groups', oldParty._id)).to.eventually.equal(true);
await user.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
await expect(userToInvite.post(`/groups/${party._id}/join`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageCannotLeaveWhileQuesting'),
});
});
it('invites joining member to active quest', async () => {
await user.update({
[`items.quests.${PET_QUEST}`]: 1,

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

@@ -1,8 +1,8 @@
import { v4 as generateUUID } from 'uuid';
import nconf from 'nconf';
import {
createAndPopulateGroup,
generateUser,
generateGroup,
translate as t,
} from '../../../../helpers/api-integration/v3';
@@ -13,13 +13,13 @@ 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',
});
});
@@ -64,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);
});
});
@@ -213,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 () => {
@@ -337,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',
}))
@@ -418,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();
@@ -436,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();
@@ -456,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({
@@ -565,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({
@@ -581,20 +486,7 @@ describe('Post /groups/:groupId/invite', () => {
});
});
it('allow inviting a user to a party if they are partying solo', async () => {
const userToInvite = await generateUser();
await userToInvite.post('/groups', { // add user to a party
name: 'Another Test Party',
type: 'party',
});
await inviter.post(`/groups/${party._id}/invite`, {
uuids: [userToInvite._id],
});
expect((await userToInvite.get('/user')).invitations.parties[0].id).to.equal(party._id);
});
it('allow inviting a user to 2 different parties', async () => {
it('allows inviting a user to 2 different parties', async () => {
// Create another inviter
const inviter2 = await generateUser();
@@ -608,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],
});
@@ -621,43 +513,59 @@ 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 partyLeader;
beforeEach(async () => {
({ group, groupLeader: partyLeader } = await createAndPopulateGroup({
groupDetails: {
name: 'Test Party',
type: 'party',
privacy: 'private',
},
// Generate party with 20 members
members: PARTY_LIMIT_MEMBERS - 10,
}));
});
it('allows 30 members in a party', async () => {
const invitesToGenerate = [];
// Generate 29 users to invite (29 + leader = 30 members)
for (let i = 0; i < PARTY_LIMIT_MEMBERS - 1; i += 1) {
// Generate 10 new invites
for (let i = 1; i < 10; i += 1) {
invitesToGenerate.push(generateUser());
}
const generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
expect(await inviter.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);
it('does not allow 30+ members in a party', async () => {
it('does not allow >30 members in a party', async () => {
const invitesToGenerate = [];
// Generate 30 users to invite (30 + leader = 31 members)
for (let i = 0; i < PARTY_LIMIT_MEMBERS; i += 1) {
// Generate 11 invites
for (let i = 1; i < 11; i += 1) {
invitesToGenerate.push(generateUser());
}
const generatedInvites = await Promise.all(invitesToGenerate);
// Invite users
await expect(inviter.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({

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

@@ -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

@@ -45,11 +45,10 @@ describe('payments : apple #subscribe', () => {
});
expect(subscribeStub).to.be.calledOnce;
expect(subscribeStub.args[0][0]).to.eql(sku);
expect(subscribeStub.args[0][1]._id).to.eql(user._id);
expect(subscribeStub.args[0][2]).to.eql('receipt');
expect(subscribeStub.args[0][3]['x-api-key']).to.eql(user.apiToken);
expect(subscribeStub.args[0][3]['x-api-user']).to.eql(user._id);
expect(subscribeStub.args[0][0]._id).to.eql(user._id);
expect(subscribeStub.args[0][1]).to.eql('receipt');
expect(subscribeStub.args[0][2]['x-api-key']).to.eql(user.apiToken);
expect(subscribeStub.args[0][2]['x-api-user']).to.eql(user._id);
});
});
});

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

@@ -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

@@ -35,13 +35,6 @@ describe('GET /world-state', () => {
});
});
it('returns a string representing the current season for NPC sprites', async () => {
const res = await requester().get('/world-state');
expect(res).to.have.nested.property('npcImageSuffix');
expect(res.npcImageSuffix).to.be.a('string');
});
context('no current event', () => {
beforeEach(async () => {
sinon.stub(worldState, 'getCurrentEvent').returns(null);

View File

@@ -202,18 +202,86 @@ describe('POST /user/class/cast/:spellId', () => {
await group.groupLeader.post('/user/class/cast/mpheal');
promises = [];
promises.push(group.groupLeader.sync());
promises.push(group.members[0].sync());
promises.push(group.members[1].sync());
promises.push(group.members[2].sync());
promises.push(group.members[3].sync());
await Promise.all(promises);
expect(group.groupLeader.stats.mp).to.be.equal(170); // spell caster
expect(group.members[0].stats.mp).to.be.greaterThan(0); // warrior
expect(group.members[1].stats.mp).to.equal(0); // wizard
expect(group.members[2].stats.mp).to.be.greaterThan(0); // rogue
expect(group.members[3].stats.mp).to.be.greaterThan(0); // healer
});
const spellList = [
{
className: 'warrior',
spells: [['smash', 'task'], ['defensiveStance'], ['valorousPresence'], ['intimidate']],
},
{
className: 'wizard',
spells: [['fireball', 'task'], ['mpheal'], ['earth'], ['frost']],
},
{
className: 'healer',
spells: [['heal'], ['brightness'], ['protectAura'], ['healAll']],
},
{
className: 'rogue',
spells: [['pickPocket', 'task'], ['backStab', 'task'], ['toolsOfTrade'], ['stealth']],
},
];
spellList.forEach(async habitClass => {
describe(`For a ${habitClass.className}`, async () => {
habitClass.spells.forEach(async spell => {
describe(`Using ${spell[0]}`, async () => {
it('Deducts MP from spell caster', async () => {
const { groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 3,
});
await groupLeader.update({
'stats.mp': 200, 'stats.class': habitClass.className, 'stats.lvl': 20, 'stats.hp': 40,
});
// need this for task spells and for stealth
const task = await groupLeader.post('/tasks/user', {
text: 'test habit',
type: 'daily',
});
if (spell.length === 2 && spell[1] === 'task') {
await groupLeader.post(`/user/class/cast/${spell[0]}?targetId=${task._id}`);
} else {
await groupLeader.post(`/user/class/cast/${spell[0]}`);
}
await groupLeader.sync();
expect(groupLeader.stats.mp).to.be.lessThan(200);
});
it('works without a party', async () => {
await user.update({
'stats.mp': 200, 'stats.class': habitClass.className, 'stats.lvl': 20, 'stats.hp': 40,
});
// need this for task spells and for stealth
const task = await user.post('/tasks/user', {
text: 'test habit',
type: 'daily',
});
if (spell.length === 2 && spell[1] === 'task') {
await user.post(`/user/class/cast/${spell[0]}?targetId=${task._id}`);
} else {
await user.post(`/user/class/cast/${spell[0]}`);
}
await user.sync();
expect(user.stats.mp).to.be.lessThan(200);
});
});
});
});
});
it('cast bulk', async () => {
let { group, groupLeader } = await createAndPopulateGroup({ // eslint-disable-line prefer-const
groupDetails: { type: 'party', privacy: 'private' },

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

@@ -215,6 +215,7 @@ describe('cron utility functions', () => {
it('monthly plan, next date in 3 months', () => {
const user = baseUserData(60, 0, 'group_plan_auto');
user.purchased.plan.perkMonthCount = 0;
const planContext = getPlanContext(user, now);
@@ -224,6 +225,7 @@ describe('cron utility functions', () => {
it('monthly plan, next date in 1 month', () => {
const user = baseUserData(62, 0, 'group_plan_auto');
user.purchased.plan.perkMonthCount = 2;
const planContext = getPlanContext(user, now);
@@ -248,5 +250,15 @@ describe('cron utility functions', () => {
expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
});
it('multi-month plan with perk count', () => {
const user = baseUserData(60, 1, 'basic_3mo');
user.purchased.plan.perkMonthCount = 2;
const planContext = getPlanContext(user, now);
expect(planContext.nextHourglassDate)
.to.be.sameMoment('2022-07-10T02:00:00.144Z');
});
});
});

View File

@@ -12,8 +12,9 @@ const webhookData = {};
app.use(bodyParser.urlencoded({
extended: true,
limit: '10mb',
}));
app.use(bodyParser.json());
app.use(bodyParser.json({ limit: '10mb' }));
app.post('/webhooks/:id', (req, res) => {
const { id } = req.params;

View File

@@ -53,7 +53,8 @@ function _requestMaker (user, method, additionalSets = {}) {
if (user && user._id && user.apiToken) {
request
.set('x-api-user', user._id)
.set('x-api-key', user.apiToken);
.set('x-api-key', user.apiToken)
.set('x-client', 'habitica-web');
}
if (!isEmpty(additionalSets)) {

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

@@ -1842,9 +1842,9 @@
}
},
"@babel/plugin-proposal-optional-chaining": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.20.7.tgz",
"integrity": "sha512-T+A7b1kfjtRM51ssoOfS1+wbyCVqorfyZhT99TvxxLMirPShD8CzKMRepMlCBGM5RpHMbn8s+5MMHnPstJH6mQ==",
"version": "7.21.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz",
"integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==",
"requires": {
"@babel/helper-plugin-utils": "^7.20.2",
"@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
@@ -1870,9 +1870,9 @@
"integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w=="
},
"@babel/types": {
"version": "7.20.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz",
"integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==",
"version": "7.21.2",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.2.tgz",
"integrity": "sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==",
"requires": {
"@babel/helper-string-parser": "^7.19.4",
"@babel/helper-validator-identifier": "^7.19.1",
@@ -4979,9 +4979,9 @@
}
},
"@sideway/formula": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg=="
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
},
"@sideway/pinpoint": {
"version": "2.0.0",
@@ -16852,9 +16852,9 @@
}
},
"core-js": {
"version": "3.27.2",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.27.2.tgz",
"integrity": "sha512-9ashVQskuh5AZEZ1JdQWp1GqSoC1e1G87MzRqg2gIfVAQ7Qn9K+uFj8EcniUFA4P2NLZfV+TOlX1SzoKfo+s7w=="
"version": "3.31.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.31.0.tgz",
"integrity": "sha512-NIp2TQSGfR6ba5aalZD+ZQ1fSxGhDo/s1w0nx3RYzf2pnJxt7YynxFlFScP6eV7+GZsKO95NSjGxyJsU3DZgeQ=="
},
"core-js-compat": {
"version": "3.11.0",
@@ -17903,9 +17903,9 @@
}
},
"dompurify": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.1.tgz",
"integrity": "sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA=="
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.3.tgz",
"integrity": "sha512-axQ9zieHLnAnHh0sfAamKYiqXMJAVwu+LM/alQ7WDagoWessyWvMSFyW65CqF3owufNu8HBcE4cM2Vflu7YWcQ=="
},
"domutils": {
"version": "1.7.0",
@@ -20652,9 +20652,9 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"hellojs": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/hellojs/-/hellojs-1.19.5.tgz",
"integrity": "sha512-hFlublej5rHFWjGe6MMMmj78otSIXBbrfvtWPZSSRmXKFoLMFlL41PJ1JPS8xwiSIZca2ve8uHoPZUDwU+/8gg=="
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/hellojs/-/hellojs-1.20.0.tgz",
"integrity": "sha512-zfZGRt0J0OpJHnw2GJz4uh+rzMNAMWpIymiaRbSmCqCvqDMLSx2/qR5JucqnHPsZHUEjyKj5aLJ8K54kyHHjLQ=="
},
"hex-color-regex": {
"version": "1.1.0",
@@ -21022,6 +21022,11 @@
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
"integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
},
"immutable": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz",
"integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg=="
},
"import-cwd": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz",
@@ -21382,9 +21387,9 @@
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA=="
},
"intro.js": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/intro.js/-/intro.js-6.0.0.tgz",
"integrity": "sha512-ZUiR6BoLSvPSlLG0boewnWVgji1fE1gBvP/pyw5pgCKXEDQz1mMeUxarggClPNs71UTq364LwSk9zxz17A9gaQ=="
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/intro.js/-/intro.js-7.0.1.tgz",
"integrity": "sha512-1oqz6aOz9cGQ3CrtVYhCSo6AkjnXUn302kcIWLaZ3TI4kKssRXDwDSz4VRoGcfC1jN+WfaSJXRBrITz+QVEBzg=="
},
"invariant": {
"version": "2.2.4",
@@ -21964,9 +21969,9 @@
}
},
"jquery": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.3.tgz",
"integrity": "sha512-bZ5Sy3YzKo9Fyc8wH2iIQK4JImJ6R0GWI9kL1/k7Z91ZBNgkRXE6U0JfHIizZbort8ZunhSI3jw9I6253ahKfg=="
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.0.tgz",
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ=="
},
"js-message": {
"version": "1.0.5",
@@ -27312,17 +27317,19 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"sass": {
"version": "1.34.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.34.0.tgz",
"integrity": "sha512-rHEN0BscqjUYuomUEaqq3BMgsXqQfkcMVR7UhscsAVub0/spUrZGBMxQXFS2kfiDsPLZw5yuU9iJEFNC2x38Qw==",
"version": "1.63.4",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.63.4.tgz",
"integrity": "sha512-Sx/+weUmK+oiIlI+9sdD0wZHsqpbgQg8wSwSnGBjwb5GwqFhYNwwnI+UWZtLjKvKyFlKkatRK235qQ3mokyPoQ==",
"requires": {
"chokidar": ">=3.0.0 <4.0.0"
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"dependencies": {
"anymatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"requires": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@@ -27342,18 +27349,18 @@
}
},
"chokidar": {
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
"integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
"requires": {
"anymatch": "~3.1.1",
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0",
"fsevents": "~2.3.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.5.0"
"readdirp": "~3.6.0"
}
},
"fill-range": {
@@ -27392,9 +27399,9 @@
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
},
"readdirp": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
"integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"requires": {
"picomatch": "^2.2.1"
}
@@ -27746,9 +27753,9 @@
}
},
"smartbanner.js": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/smartbanner.js/-/smartbanner.js-1.19.1.tgz",
"integrity": "sha512-x3alFTlk6pLuqrm9PrYQv1E+86CrEIgPf/KJ+nP5342BmOWstbdR8OwD3TPmM56zHQm4MEr/eoqbEcfTKdvdKw=="
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/smartbanner.js/-/smartbanner.js-1.19.2.tgz",
"integrity": "sha512-hwcGNp5Hza5PJHTmqP6H8q0XBYhloIQyJgdzv0ldz3HQSeEuKB2riVraQXdKuquE6ZU/0M0yubno53xJ/ZiQQg=="
},
"snapdragon": {
"version": "0.8.2",
@@ -28120,9 +28127,9 @@
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"stopword": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/stopword/-/stopword-2.0.5.tgz",
"integrity": "sha512-MgmxgmVs0Uo9G4mMqRc/QBXdPePZUnVNYqnNiv8QdCXTqHmGRw36mrjToCeNxhu/7Ifa7RxAtd/KR/GLZdnN9g=="
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/stopword/-/stopword-2.0.8.tgz",
"integrity": "sha512-btlEC2vEuhCuvshz99hSGsY8GzaP5qzDPQm56j6rR/R38p8xdsOXgU5a6tIgvU/4hcCta1Vlo/2FVXA9m0f8XA=="
},
"store2": {
"version": "2.10.0",
@@ -29770,9 +29777,9 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"ua-parser-js": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz",
"integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ=="
"version": "0.7.33",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw=="
},
"uc.micro": {
"version": "1.0.6",
@@ -30216,9 +30223,9 @@
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
},
"uuid-browser": {
"version": "3.1.0",
@@ -30240,9 +30247,9 @@
}
},
"validator": {
"version": "13.7.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz",
"integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw=="
"version": "13.9.0",
"resolved": "https://registry.npmjs.org/validator/-/validator-13.9.0.tgz",
"integrity": "sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA=="
},
"vary": {
"version": "1.1.2",
@@ -30578,6 +30585,7 @@
"version": "npm:vue-loader@16.8.3",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.8.3.tgz",
"integrity": "sha512-7vKN45IxsKxe5GcVCbc2qFU5aWzyiLrYJyUuMz4BQLKctCj/fmCa0w6fGiiQ2cLFetNcek1ppGJQDCup0c1hpA==",
"optional": true,
"requires": {
"chalk": "^4.1.0",
"hash-sum": "^2.0.0",
@@ -30588,6 +30596,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"optional": true,
"requires": {
"color-convert": "^2.0.1"
}
@@ -30596,6 +30605,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"optional": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -30605,6 +30615,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"optional": true,
"requires": {
"color-name": "~1.1.4"
}
@@ -30612,22 +30623,26 @@
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"optional": true
},
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"optional": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"optional": true
},
"loader-utils": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"optional": true,
"requires": {
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
@@ -30638,6 +30653,7 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"optional": true,
"requires": {
"has-flag": "^4.0.0"
}

View File

@@ -32,30 +32,30 @@
"bootstrap": "^4.6.0",
"bootstrap-vue": "^2.23.1",
"chai": "^4.3.7",
"core-js": "^3.27.2",
"dompurify": "^2.4.1",
"core-js": "^3.31.0",
"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",
"habitica-markdown": "^3.0.0",
"hellojs": "^1.19.5",
"hellojs": "^1.20.0",
"inspectpack": "^4.7.1",
"intro.js": "^6.0.0",
"jquery": "^3.6.3",
"intro.js": "^7.0.1",
"jquery": "^3.7.0",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"nconf": "^0.12.0",
"sass": "^1.34.0",
"sass": "^1.63.4",
"sass-loader": "^8.0.2",
"smartbanner.js": "^1.19.1",
"stopword": "^2.0.5",
"smartbanner.js": "^1.19.2",
"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": "^8.3.2",
"validator": "^13.7.0",
"uuid": "^9.0.0",
"validator": "^13.9.0",
"vue": "^2.7.10",
"vue-cli-plugin-storybook": "2.1.0",
"vue-mugen-scroll": "^0.2.6",
@@ -66,6 +66,6 @@
"webpack": "^4.46.0"
},
"devDependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.20.7"
"@babel/plugin-proposal-optional-chaining": "^7.21.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

View File

@@ -35,11 +35,13 @@
<sub-canceled-modal v-if="isUserLoaded" />
<bug-report-modal v-if="isUserLoaded" />
<bug-report-success-modal v-if="isUserLoaded" />
<external-link-modal />
<birthday-modal />
<snackbars />
<router-view v-if="!isUserLoggedIn || isStaticPage" />
<template v-else>
<template v-if="isUserLoaded">
<chat-banner />
<damage-paused-banner />
<gems-promo-banner />
<gift-promo-banner />
@@ -158,6 +160,7 @@ import { loadProgressBar } from 'axios-progress-bar';
import birthdayModal from '@/components/news/birthdayModal';
import AppMenu from './components/header/menu';
import AppHeader from './components/header/index';
import ChatBanner from './components/header/banners/chatBanner';
import DamagePausedBanner from './components/header/banners/damagePaused';
import GemsPromoBanner from './components/header/banners/gemsPromo';
import GiftPromoBanner from './components/header/banners/giftPromo';
@@ -175,6 +178,7 @@ import amazonPaymentsModal from '@/components/payments/amazonModal';
import paymentsSuccessModal from '@/components/payments/successModal';
import subCancelModalConfirm from '@/components/payments/cancelModalConfirm';
import subCanceledModal from '@/components/payments/canceledModal';
import externalLinkModal from '@/components/externalLinkModal.vue';
import spellsMixin from '@/mixins/spells';
import {
@@ -196,6 +200,7 @@ export default {
AppHeader,
AppFooter,
birthdayModal,
ChatBanner,
DamagePausedBanner,
GemsPromoBanner,
GiftPromoBanner,
@@ -210,6 +215,7 @@ export default {
subCanceledModal,
bugReportModal,
bugReportSuccessModal,
externalLinkModal,
},
mixins: [notifications, spellsMixin],
data () {

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -19,8 +19,12 @@
top: -16px !important;
}
.Pet.Pet-FlyingPig-Veggie, .Pet.Pet-FlyingPig-Dessert, .Pet.Pet-FlyingPig-VirtualPet {
top: -28px !important;
$foolPets: Veggie, Dessert, VirtualPet, TeaShop;
@each $foolPet in $foolPets {
.Pet.Pet-FlyingPig-#{$foolPet} {
top: -28px !important;
}
}
.Pet[class*="Virtual"] {

View File

@@ -24,9 +24,9 @@
}
}
.icon-16 {
width: 16px;
height: 16px;
.icon-10 {
width: 10px;
height: 10px;
}
.icon-12 {
@@ -34,21 +34,26 @@
height: 12px;
}
.icon-16 {
width: 16px;
height: 16px;
}
.icon-24 {
width: 24px;
height: 24px;
}
.icon-32 {
width: 32px;
height: 32px;
}
.icon-48 {
width: 48px;
height: 48px;
}
.icon-10 {
width: 10px;
height: 10px;
}
.inline {
display: inline-block;
}

View File

@@ -50,10 +50,7 @@ h3.markdown {
}
a {
color: $blue-10;
&:hover, &:active, &:focus {
color: $blue-10;
text-decoration: underline;
}
}

View File

@@ -26,19 +26,17 @@ a:not([href]), a:not([href]):hover {
a, a:not([href]):not([tabindex]) {
cursor: pointer;
color: $purple-300;
&.standard-link {
color: $blue-10;
&:hover, &:active, &:focus {
text-decoration: underline;
color: $purple-300;
}
&:hover, &:active, &:focus {
text-decoration: underline;
}
&[disabled="disabled"] {
color: $gray-300;
text-decoration: none;
cursor: default;
}
&[disabled="disabled"] {
color: $gray-300;
text-decoration: none;
cursor: default;
}
&.small-link {

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path id="b" d="m10,6h6V0h-2v2.72C12.49.99,10.3,0,8,0,3.59,0,0,3.59,0,8s3.59,8,8,8c2.69,0,5.2-1.35,6.68-3.6l-1.67-1.1c-1.11,1.69-2.99,2.71-5.01,2.7-3.31,0-6-2.69-6-6s2.69-6,6-6c1.72,0,3.33.74,4.46,2h-2.46v2Z" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 341 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

@@ -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

@@ -22,6 +22,10 @@
Account created:
<strong>{{ hero.auth.timestamps.created | formatDate }}</strong>
</div>
<div v-if="hero.flags.thirdPartyTools">
User has employed <strong>third party tools</strong>. Last known usage:
<strong>{{ hero.flags.thirdPartyTools | formatDate }}</strong>
</div>
<div v-if="cronError">
"lastCron" value:
<strong>{{ hero.lastCron | formatDate }}</strong>

View File

@@ -17,10 +17,18 @@
Payment schedule ("basic-earned" is monthly):
<strong>{{ hero.purchased.plan.planId }}</strong>
</div>
<div v-if="hero.purchased.plan.planId == 'group_plan_auto'">
Group plan ID:
<strong>{{ hero.purchased.plan.owner }}</strong>
</div>
<div v-if="hero.purchased.plan.dateCreated">
Creation date:
<strong>{{ dateFormat(hero.purchased.plan.dateCreated) }}</strong>
</div>
<div v-if="hero.purchased.plan.dateCurrentTypeCreated">
Start date for current subscription type:
<strong>{{ dateFormat(hero.purchased.plan.dateCurrentTypeCreated) }}</strong>
</div>
<div>
Termination date:
<strong
@@ -46,6 +54,17 @@
Perk offset months:
<strong>{{ hero.purchased.plan.consecutive.offset }}</strong>
</div>
<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"
>
</div>
<div>
Next Mystic Hourglass:
<strong>{{ nextHourglassDate }}</strong>
@@ -149,7 +168,7 @@ export default {
nextHourglassDate () {
const currentPlanContext = getPlanContext(this.hero, new Date());
return currentPlanContext.nextHourglassDate.format('MMMM');
return currentPlanContext.nextHourglassDate.format('MMMM YYYY');
},
},
methods: {

View File

@@ -86,6 +86,13 @@
>{{ $t('companyContribute') }}
</a>
</li>
<li>
<a
href="https://translate.habitica.com/"
target="_blank"
>{{ $t('translateHabitica') }}
</a>
</li>
</ul>
</div>
<!-- Support -->
@@ -101,6 +108,7 @@
v-if="user"
>
<a
href=""
target="_blank"
@click.prevent="openBugReportModal()"
>
@@ -205,7 +213,7 @@
</a>
<a
class="social-circle"
href="https://twitter.com/habitica"
href="https://twitter.com/habitica/"
target="_blank"
>
<div
@@ -215,7 +223,7 @@
</a>
<a
class="social-circle"
href="https://www.facebook.com/Habitica"
href="https://www.facebook.com/Habitica/"
target="_blank"
>
<div
@@ -472,10 +480,6 @@ footer {
color: $purple-300;
text-decoration: underline;
}
a:not([href]):not([class]):hover { // needed to make "report a bug"'s hover state correct
color: $purple-300;
text-decoration: underline;
}
column-gap: 1.5rem;
display: grid;
@@ -675,11 +679,6 @@ h3 {
footer {
padding: 24px 16px;
a:not([href]):not([class]):hover { // needed to make "report a bug"'s hover state correct
color: $purple-300;
text-decoration: underline;
}
column-gap: 1.5rem;
display: grid;
grid-template-areas:
@@ -719,10 +718,6 @@ h3 {
@media (max-width: 1024px) and (min-width: 768px) {
footer {
padding: 24px 24px;
a:not([href]):not([class]):hover { // needed to make "report a bug"'s hover state correct
color: $purple-300;
text-decoration: underline;
}
}
.desktop {
@@ -815,7 +810,7 @@ export default {
...mapState({ user: 'user.data' }),
...mapState(['isUserLoaded']),
getDataDisplayToolUrl () {
const base = 'https://oldgods.net/habitrpg/habitrpg_user_data_display.html';
const base = 'https://tools.habitica.com/';
if (!this.user) return null;
return `${base}?uuid=${this.user._id}`;
},

View File

@@ -244,7 +244,7 @@ export default {
petClass () {
if (some(
this.currentEventList,
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'virtual',
event => moment().isBetween(event.start, event.end) && event.aprilFools && event.aprilFools === 'teaShop',
)) {
return this.foolPet(this.member.items.currentPet);
}

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,21 +53,18 @@
</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')"
:placeholder="question ? $t('questionPlaceholder') : $t('reportDescriptionPlaceholder')"
:class="{'input-invalid': messageInvalid && this.message.length === 0}"
>
@@ -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});
}
@@ -159,7 +145,6 @@ label {
}
.cancel-link {
color: $blue-10;
line-height: 1.71;
}
@@ -183,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 {
@@ -212,6 +197,7 @@ export default {
await axios.post('/api/v4/bug-report', {
message: this.message,
email: this.email,
question: this.question,
});
this.message = '';
@@ -234,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

@@ -107,7 +107,6 @@ label {
}
.cancel-link {
color: $blue-10;
line-height: 1.71;
}

View File

@@ -322,6 +322,7 @@ import omit from 'lodash/omit';
import { v4 as uuid } from 'uuid';
import { userStateMixin } from '../../mixins/userState';
import externalLinks from '../../mixins/externalLinks';
import memberSearchDropdown from '@/components/members/memberSearchDropdown';
import closeChallengeModal from './closeChallengeModal';
import Column from '../tasks/column';
@@ -358,7 +359,7 @@ export default {
userLink,
groupLink,
},
mixins: [challengeMemberSearchMixin, userStateMixin],
mixins: [challengeMemberSearchMixin, externalLinks, userStateMixin],
props: ['challengeId'],
data () {
return {
@@ -414,6 +415,10 @@ export default {
mounted () {
if (!this.searchId) this.searchId = this.challengeId;
if (!this.challenge._id) this.loadChallenge();
this.handleExternalLinks();
},
updated () {
this.handleExternalLinks();
},
async beforeRouteUpdate (to, from, next) {
this.searchId = to.params.challengeId;

View File

@@ -120,6 +120,7 @@ import { mapState } from '@/libs/store';
import Sidebar from './sidebar';
import ChallengeItem from './challengeItem';
import challengeModal from './challengeModal';
import externalLinks from '@/mixins/externalLinks';
import challengeUtilities from '@/mixins/challengeUtilities';
import positiveIcon from '@/assets/svg/positive.svg';
@@ -131,7 +132,7 @@ export default {
challengeModal,
MugenScroll,
},
mixins: [challengeUtilities],
mixins: [challengeUtilities, externalLinks],
data () {
return {
loading: true,
@@ -177,6 +178,10 @@ export default {
section: this.$t('challenges'),
});
this.loadChallenges();
this.handleExternalLinks();
},
updated () {
this.handleExternalLinks();
},
methods: {
updateSearch (eventData) {

View File

@@ -81,6 +81,8 @@ import challengeModal from './challengeModal';
import { mapState } from '@/libs/store';
import markdownDirective from '@/directives/markdown';
import externalLinks from '../../mixins/externalLinks';
import challengeItem from './challengeItem';
import challengeIcon from '@/assets/svg/challenge.svg';
@@ -92,6 +94,7 @@ export default {
directives: {
markdown: markdownDirective,
},
mixins: [externalLinks],
props: ['group'],
data () {
return {
@@ -118,6 +121,10 @@ export default {
},
mounted () {
this.loadChallenges();
this.handleExternalLinks();
},
updated () {
this.handleExternalLinks();
},
methods: {
async loadChallenges () {

View File

@@ -145,6 +145,7 @@ import Sidebar from './sidebar';
import ChallengeItem from './challengeItem';
import challengeModal from './challengeModal';
import challengeUtilities from '@/mixins/challengeUtilities';
import externalLinks from '@/mixins/externalLinks';
import challengeIcon from '@/assets/svg/challenge.svg';
import positiveIcon from '@/assets/svg/positive.svg';
@@ -156,7 +157,7 @@ export default {
challengeModal,
MugenScroll,
},
mixins: [challengeUtilities],
mixins: [challengeUtilities, externalLinks],
data () {
return {
icons: Object.freeze({
@@ -203,6 +204,10 @@ export default {
section: this.$t('challenges'),
});
this.loadChallenges();
this.handleExternalLinks();
},
updated () {
this.handleExternalLinks();
},
methods: {
updateSearch (eventData) {

View File

@@ -50,7 +50,6 @@ import notificationsMixin from '@/mixins/notifications';
import Task from '@/components/tasks/task';
import taskDefaults from '@/../../common/script/libs/taskDefaults';
import { TAVERN_ID } from '@/../../common/script/constants';
const baseUrl = 'https://habitica.com';
@@ -89,9 +88,7 @@ export default {
createTask: 'tasks:create',
}),
groupPath () {
if (this.groupId === TAVERN_ID) {
return `${baseUrl}/groups/tavern`;
} if (this.groupType === 'party') {
if (this.groupType === 'party') {
return `${baseUrl}/party`;
}
return `${baseUrl}/groups/guild/${this.groupId}`;

View File

@@ -77,7 +77,6 @@
}
a.cancel-link {
color: $blue-10;
margin-right: .5em;
}

View File

@@ -183,10 +183,8 @@
<div
v-for="bg in backgroundShopSets[0].items"
:key="bg.key"
:id="bg.key"
class="col-2"
:popover-title="bg.text"
:popover="bg.notes"
popover-trigger="mouseenter"
@click="unlock('background.' + bg.key)"
>
<div
@@ -195,6 +193,13 @@
>
<div class="small-rectangle"></div>
</div>
<b-popover
:target="bg.key"
triggers="hover focus"
placement="bottom"
:prevent-overflow="false"
:content="bg.notes"
/>
</div>
</div>
<div
@@ -211,16 +216,21 @@
<div
v-for="bg in backgroundShopSets[2].items"
:key="bg.key"
:id="bg.key"
class="col-4 text-center customize-option background-button"
:popover-title="bg.text"
:popover="bg.notes"
popover-trigger="mouseenter"
@click="unlock('background.' + bg.key)"
>
<div
class="background"
:class="`background_${bg.key}`"
></div>
<b-popover
:target="bg.key"
triggers="hover focus"
placement="bottom"
:prevent-overflow="false"
:content="bg.notes"
/>
</div>
</div>
</div>
@@ -236,10 +246,8 @@
<div
v-for="bg in backgroundShopSets[1].items"
:key="bg.key"
:id="bg.key"
class="col-4 text-center customize-option background-button"
:popover-title="bg.text"
:popover="bg.notes"
popover-trigger="mouseenter"
@click="!user.purchased.background[bg.key]
? backgroundSelected(bg) : unlock('background.' + bg.key)"
>
@@ -270,6 +278,13 @@
:pinned="isBackgroundPinned(bg)"
/>
</span>
<b-popover
:target="bg.key"
triggers="hover focus"
placement="bottom"
:prevent-overflow="false"
:content="bg.notes"
/>
</div>
</div>
</div>
@@ -302,10 +317,8 @@
<div
v-for="bg in set.items"
:key="bg.key"
:id="bg.key"
class="col-4 text-center customize-option background-button"
:popover-title="bg.text"
:popover="bg.notes"
popover-trigger="mouseenter"
@click="!user.purchased.background[bg.key]
? backgroundSelected(bg) : unlock('background.' + bg.key)"
>
@@ -336,6 +349,13 @@
:pinned="isBackgroundPinned(bg)"
/>
</span>
<b-popover
:target="bg.key"
triggers="hover focus"
placement="bottom"
:prevent-overflow="false"
:content="bg.notes"
/>
</div>
<div
v-if="!ownsSet('background', set.items) && set.identifier !== 'incentiveBackgrounds'"
@@ -358,16 +378,21 @@
<div
v-for="(bg) in ownedBackgrounds"
:key="bg.key"
:id="bg.key"
class="col-4 text-center customize-option background-button"
:popover-title="bg.text"
:popover="bg.notes"
popover-trigger="mouseenter"
@click="unlock('background.' + bg.key)"
>
<div
class="background"
:class="[`background_${bg.key}`, backgroundLockedStatus(bg.key)]"
></div>
<b-popover
:target="bg.key"
triggers="hover focus"
placement="bottom"
:prevent-overflow="false"
:content="bg.notes"
/>
</div>
</div>
</div>

View File

@@ -0,0 +1,209 @@
<template>
<b-modal
id="external-link-modal"
size="md"
>
<!-- HEADER -->
<div slot="modal-header">
<div
class="modal-close"
@click="close()"
>
<div
class="icon-close"
v-html="icons.close"
>
</div>
</div>
<div class="exclamation-container d-flex align-items-center justify-content-center">
<div
v-once
class="svg-icon svg-exclamation"
v-html="icons.exclamation"
></div>
</div>
<h2>
{{ $t('leaveHabitica') }}
</h2>
</div>
<!-- BODY -->
<div
class="row leave-warning-text"
v-html="$t('leaveHabiticaText')"
>
</div>
<div
class="skip-modal"
>
{{ $t('skipExternalLinkModal') }}
</div>
<!-- FOOTER -->
<div slot="modal-footer">
<button
v-once
class="btn btn-primary"
@click="proceed()"
>
{{ $t('continue') }}
</button>
<div
v-once
class="close-link justify-content-center"
@click="close()"
>
{{ $t('cancel') }}
</div>
</div>
</b-modal>
</template>
<style lang="scss">
@import '~@/assets/scss/colors.scss';
#external-link-modal {
&.modal {
display: flex !important;
}
.modal-md {
max-width: 448px;
min-width: 330px;
margin: auto;
.modal-close {
position: absolute;
right: 12px;
top: 12px;
cursor: pointer;
.icon-close {
width: 16px;
height: 16px;
vertical-align: middle;
& svg {
fill: $yellow-1;
opacity: 0.75;
}
& :hover {
fill: $yellow-1;
opacity: 1;
}
}
}
.modal-content {
background: transparent;
}
.modal-header {
justify-content: center;
padding-top: 32px;
padding-bottom: 0px;
background: $yellow-100;
border-top-right-radius: 8px;
border-top-left-radius: 8px;
border-bottom: none;
.exclamation-container {
width: 64px;
height: 64px;
border-radius: 50%;
background: $yellow-1;
margin: 0 auto;
margin-bottom: 16px;
}
.svg-exclamation {
width: 8px;
color: $white;
}
h2 {
color: $yellow-1;
margin-bottom: 16px;
}
}
.modal-body {
padding: 16px 44px 20px 44px;
background: $white;
.leave-warning-text {
font-size: 0.875rem;
line-height: 1.71;
text-align: center;
margin-top:24px;
}
.skip-modal {
color: $gray-100;
font-size: 0.75rem;
text-align: center;
line-height: 1.33;
margin-top: 16px;
// padding-bottom: 24px;
}
}
.modal-footer {
background: $white;
border-bottom-right-radius: 8px;
border-bottom-left-radius: 8px;
justify-content: center;
border-top: none;
padding-top: 0;
}
.close-link {
color: $purple-300;
line-height: 1.71;
font-size: 0.875rem;
cursor: pointer;
margin-top:16px;
margin-bottom: 8px;
text-align: center;
&:hover {
text-decoration: underline;
}
}
}
}
</style>
<script>
import exclamationIcon from '@/assets/svg/exclamation.svg';
import closeIcon from '@/assets/svg/new-close.svg';
export default {
data () {
return {
icons: Object.freeze({
close: closeIcon,
exclamation: exclamationIcon,
}),
url: '',
};
},
mounted () {
this.$root.$on('habitica:external-link', url => {
this.url = url;
this.$root.$emit('bv::show::modal', 'external-link-modal');
});
},
beforeDestroy () {
this.$root.$off('habitica:external-link');
},
methods: {
close () {
this.$root.$emit('bv::hide::modal', 'external-link-modal');
},
proceed () {
window.open(this.url, '_blank').focus();
this.close();
},
},
};
</script>

View File

@@ -87,6 +87,8 @@
<script>
import debounce from 'lodash/debounce';
import externalLinks from '../../mixins/externalLinks';
import autocomplete from '../chat/autoComplete';
import communityGuidelines from './communityGuidelines';
import chatMessage from '../chat/chatMessages';
@@ -103,6 +105,7 @@ export default {
communityGuidelines,
chatMessage,
},
mixins: [externalLinks],
props: ['label', 'group', 'placeholder'],
data () {
return {
@@ -132,6 +135,10 @@ export default {
},
mounted () {
this.textbox = this.$refs['user-entry'];
this.handleExternalLinks();
},
updated () {
this.handleExternalLinks();
},
methods: {
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a

View File

@@ -11,9 +11,12 @@
<div class="quest_screen"></div>
<div class="row heading">
<div class="col-12 text-center pr-5 pl-5">
<h2 v-once>
<h1
v-once
class="mb-2"
>
{{ $t('playInPartyTitle') }}
</h2>
</h1>
<p
v-once
class="mb-4"
@@ -22,67 +25,91 @@
</p>
<button
v-once
class="btn btn-primary"
class="btn btn-primary px-4 mb-2"
@click="createParty()"
>
{{ $t('createParty') }}
</button>
</div>
</div>
<close-x
@close="close()"
/>
</div>
<div class="row grey-row">
<div class="col-12 text-center">
<div class="col-12 text-center px-0">
<div class="join-party"></div>
<h2 v-once>
{{ $t('wantToJoinPartyTitle') }}
</h2>
<p v-html="$t('wantToJoinPartyDescription')"></p>
<div
class="form-group"
@click="copyUsername"
<h1
v-once
class="mb-2"
>
<div class="d-flex align-items-center">
<label
v-once
class="mr-3"
>{{ $t('username') }}</label>
<div class="flex-grow-1">
<div class="input-group-prepend input-group-text">
@
<div class="text">
{{ user.auth.local.username }}
</div>
<div
class="svg-icon copy-icon"
v-html="icons.copy"
></div>
<div
v-once
class="small"
>
{{ $t('copy') }}
</div>
</div>
{{ $t('wantToJoinPartyTitle') }}
</h1>
<p
v-once
class="mb-4"
v-html="$t('partyFinderDescription')"
>
</p>
<div
v-if="seeking"
>
<div
class="green-bar mb-3"
>
{{ $t('currentlyLookingForParty') }}
</div>
<div class="d-flex justify-content-center">
<div
class="red-link"
@click="seekParty()"
>
{{ $t('leave') }}
</div>
</div>
</div>
<button
v-else
class="btn btn-primary px-4 mt-2 mb-1"
@click="seekParty()"
>
{{ $t('lookForParty') }}
</button>
</div>
</div>
</b-modal>
</template>
<style>
#create-party-modal .modal-body {
padding: 0rem 0.75rem;
}
<style lang="scss">
#create-party-modal {
display: flex !important;
overflow-y: hidden;
#create-party-modal .modal-dialog {
width: 35.75rem;
}
@media (max-height: 770px) {
overflow-y: auto;
}
#create-party-modal .modal-header {
padding: 0;
border-bottom: 0px;
.modal-body {
padding: 0rem 0.75rem;
}
.modal-content {
border-radius: 8px;
}
.modal-dialog {
width: 566px;
margin: auto;
@media (max-height: 826px) {
margin-top: 56px;
}
}
.modal-header {
padding: 0;
border-bottom: 0px;
}
}
</style>
@@ -107,15 +134,27 @@
cursor: pointer;
}
.green-bar {
height: 32px;
font-size: 14px;
font-weight: bold;
line-height: 1.71;
text-align: center;
color: $green-1;
background-color: $green-100;
border-radius: 2px;
padding: 4px 0px 4px 0px;
}
.grey-row {
background-color: $gray-700;
color: #4e4a57;
padding: 2em;
border-radius: 0px 0px 2px 2px;
border-radius: 0px 0px 8px 8px;
}
h2 {
color: $gray-100;
h1 {
color: $purple-300;
}
.header-wrap {
@@ -132,10 +171,6 @@
border-radius: 2px 2px 0 0;
image-rendering: optimizequality;
}
h2 {
color: $purple-200;
}
}
.heading {
@@ -182,6 +217,21 @@
margin: 0.75rem auto 0.75rem 0.25rem;
}
p {
line-height: 1.71;
}
.red-link {
cursor: pointer;
font-size: 14px;
line-height: 1.71;
text-align: center;
color: $maroon-50;
&:hover {
text-decoration: underline;
}
}
.small {
color: $gray-200;
margin: auto 0.5rem auto 0.25rem;
@@ -192,21 +242,29 @@
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import notifications from '@/mixins/notifications';
import closeX from '../ui/closeX';
import copyIcon from '@/assets/svg/copy.svg';
export default {
components: {
closeX,
},
mixins: [notifications],
data () {
return {
icons: Object.freeze({
copy: copyIcon,
}),
seeking: false,
};
},
computed: {
...mapState({ user: 'user.data' }),
},
mounted () {
this.seeking = Boolean(this.user.party.seeking);
},
methods: {
async createParty () {
const group = {
@@ -223,7 +281,10 @@ export default {
});
this.$root.$emit('bv::hide::modal', 'create-party-modal');
this.$router.push('/party');
await this.$router.push('/party');
},
close () {
this.$root.$emit('bv::hide::modal', 'create-party-modal');
},
copyUsername () {
if (navigator.clipboard) {
@@ -238,6 +299,12 @@ export default {
}
this.text(this.$t('usernameCopied'));
},
seekParty () {
this.$store.dispatch('user:set', {
'party.seeking': !this.user.party.seeking ? new Date() : null,
});
this.seeking = !this.seeking;
},
},
};
</script>

View File

@@ -542,7 +542,8 @@ export default {
await this.$store.dispatch('guilds:leave', data);
if (this.isParty) {
this.$router.push({ name: 'tasks' });
await this.$router.push({ name: 'tasks' });
window.location.reload(true);
}
},
upgradeGroup () {

View File

@@ -4,12 +4,12 @@
<group-plan-creation-modal />
<div>
<div class="header">
<h1 class="text-center">
Need more for your Group?
<h1 v-once class="text-center">
{{ $t('groupPlanTitle') }}
</h1>
<div class="row">
<div class="col-8 offset-2 text-center">
<h2 class="sub-text">
<h2 v-once class="sub-text">
{{ $t('groupBenefitsDescription') }}
</h2>
</div>
@@ -24,8 +24,8 @@
src="~@/assets/images/group-plans/group-14@3x.png"
>
<hr>
<h2>{{ $t('teamBasedTasks') }}</h2>
<p>Set up an easily-viewed shared task list for the group. Assign tasks to your fellow group members, or let them claim their own tasks to make it clear what everyone is working on!</p><!-- eslint-disable-line max-len -->
<h2 v-once> {{ $t('teamBasedTasks') }} </h2>
<p v-once> {{ $t('teamBasedTasksListDesc') }} </p>
</div>
</div>
<div class="col-4">
@@ -35,8 +35,8 @@
src="~@/assets/images/group-plans/group-12@3x.png"
>
<hr>
<h2>Group Management Controls</h2>
<p>Use task approvals to verify that a task that was really completed, add Group Managers to share responsibilities, and enjoy a private group chat for all team members.</p><!-- eslint-disable-line max-len -->
<h2 v-once> {{ $t('groupManagementControls') }} </h2>
<p v-once> {{ $t('groupManagementControlsDesc') }} </p>
</div>
</div>
<div class="col-4">
@@ -46,8 +46,8 @@
src="~@/assets/images/group-plans/group-13@3x.png"
>
<hr>
<h2>In-Game Benefits</h2>
<p>Group members get an exclusive Jackalope Mount, as well as full subscription benefits, including special monthly equipment sets and the ability to buy gems with gold.</p><!-- eslint-disable-line max-len -->
<h2 v-once> {{ $t('inGameBenefits') }} </h2>
<p v-once> {{ $t('inGameBenefitsDesc') }} </p>
</div>
</div>
</div>

View File

@@ -78,7 +78,6 @@
@import '~@/assets/scss/colors.scss';
a:not([href]) {
color: $blue-10;
font-size: 16px;
}

View File

@@ -0,0 +1,344 @@
<template>
<div>
<div class="d-flex justify-content-center">
<div
v-if="seekers.length > 0"
class="fit-content mx-auto mt-4"
>
<div class="d-flex align-items-center">
<h1 v-once class="my-auto mr-auto"> {{ $t('findPartyMembers') }}</h1>
<div
class="btn btn-secondary btn-sync ml-auto my-auto pl-2 pr-3 d-flex"
@click="refreshList()"
>
<div class="svg-icon icon-16 color my-auto mr-2" v-html="icons.sync"></div>
<div class="ml-auto"> {{ $t('refreshList') }} </div>
</div>
</div>
<div class="d-flex flex-wrap seeker-list">
<div
v-for="(seeker, index) in seekers"
:key="seeker._id"
class="seeker"
>
<div class="d-flex">
<avatar
:member="seeker"
:hideClassBadge="true"
@click.native="showMemberModal(seeker._id)"
class="mr-3 mb-2"
/>
<div class="card-data">
<user-link
:user-id="seeker._id"
:name="seeker.profile.name"
:backer="seeker.backer"
:contributor="seeker.contributor"
/>
<div class="small-with-border pb-2 mb-2">
@{{ seeker.auth.local.username }} {{ $t('level') }} {{ seeker.stats.lvl }}
</div>
<div
class="d-flex"
>
<strong v-once> {{ $t('classLabel') }} </strong>
<span
class="svg-icon d-inline-block icon-16 my-auto mx-2"
v-html="icons[seeker.stats.class]"
>
</span>
<strong
:class="`${seeker.stats.class}-color`"
>
{{ $t(seeker.stats.class) }}
</strong>
</div>
<div>
<strong v-once class="mr-2"> {{ $t('checkinsLabel') }} </strong>
{{ seeker.loginIncentives }}
</div>
<div>
<strong v-once class="mr-2"> {{ $t('languageLabel') }} </strong>
{{ displayLanguage(seeker.preferences.language) }}
</div>
</div>
</div>
<strong
v-if="!seeker.invited"
@click="inviteUser(seeker._id, index)"
class="btn btn-primary w-100"
>
{{ $t('inviteToParty') }}
</strong>
<div
v-else
@click="rescindInvite(seeker._id, index)"
class="btn btn-success w-100"
v-html="$t('invitedToYourParty')"
>
</div>
</div>
<mugen-scroll
v-show="loading"
:handler="infiniteScrollTrigger"
:should-handle="!loading && canLoadMore"
:threshold="1"
/>
</div>
</div>
<div
v-if="seekers.length === 0 && !loading"
class="d-flex flex-column empty-state text-center my-5"
>
<div class="gray-circle mb-3 mx-auto d-flex">
<div
class="svg-icon icon-32 color m-auto"
v-html="icons.users"
>
</div>
</div>
<strong class="mb-1"> {{ $t('findMorePartyMembers') }} </strong>
<div v-html="$t('noOneLooking')"></div>
</div>
</div>
<h2
v-show="loading"
class="loading"
:class="seekers.length === 0 ? 'mt-3' : 'mt-0'"
>
{{ $t('loading') }}
</h2>
</div>
</template>
<style lang="scss" scoped>
@import '~@/assets/scss/colors.scss';
h1 {
color: $purple-300;
}
strong {
line-height: 1.71;
}
.avatar {
background-color: $gray-600;
}
.btn-success {
box-shadow: none;
color: $green-1;
font-weight: normal;
&:not(:disabled):not(.disabled):active {
color: $green-1;
}
}
.btn-sync {
min-width: 128px;
max-height: 32px;
.svg-icon {
color: $gray-200;
}
}
.card-data {
width: 267px;
}
.empty-state {
color: $gray-100;
line-height: 1.71;
}
.fit-content {
width: fit-content;
}
.gray-circle {
width: 64px;
height: 64px;
color: $gray-600;
background-color: $gray-200;
border-radius: 100px;
.icon-32 {
height: auto;
}
}
.loading {
text-align: center;
color: $purple-300;
}
.seeker-list {
max-width: 920px;
@media (max-width: 962px) {
max-width: 464px;
};
.seeker {
width: 448px;
margin-bottom: 24px;
padding: 8px;
border-radius: 4px;
box-shadow: 0 1px 3px 0 rgba(26, 24, 29, 0.12), 0 1px 2px 0 rgba(26, 24, 29, 0.24);
background-color: $white;
&:first-of-type {
margin-top: 24px;
}
@media (min-width: 963px) {
&:nth-child(2) {
margin-top: 24px;
}
&:nth-child(even) {
margin-left: 24px;
}
}
}
}
.small-with-border {
border-bottom: 1px solid $gray-500;
color: $gray-100;
font-size: 12px;
font-weight: normal;
line-height: 1.33;
}
.healer-color {
color: $yellow-10;
}
.rogue-color {
color: $purple-200;
}
.warrior-color {
color: $red-50;
}
.wizard-color {
color: $blue-10;
}
</style>
<script>
import debounce from 'lodash/debounce';
import MugenScroll from 'vue-mugen-scroll';
import Avatar from '../avatar';
import userLink from '../userLink';
import { mapState } from '@/libs/store';
import syncIcon from '@/assets/svg/sync-2.svg';
import usersIcon from '@/assets/svg/users.svg';
import warriorIcon from '@/assets/svg/warrior.svg';
import rogueIcon from '@/assets/svg/rogue.svg';
import healerIcon from '@/assets/svg/healer.svg';
import wizardIcon from '@/assets/svg/wizard.svg';
export default {
components: {
Avatar,
MugenScroll,
userLink,
},
data () {
return {
canLoadMore: true,
loading: true,
page: 0,
party: {},
seekers: [],
icons: Object.freeze({
warrior: warriorIcon,
rogue: rogueIcon,
healer: healerIcon,
sync: syncIcon,
users: usersIcon,
wizard: wizardIcon,
}),
};
},
computed: {
...mapState({
availableLanguages: 'i18n.availableLanguages',
user: 'user.data',
}),
},
async mounted () {
try {
this.party = await this.$store.dispatch('guilds:getGroup', { groupId: this.user.party._id });
} catch {
this.$router.push('/');
}
if (!this.party._id || this.party.leader._id !== this.user._id) {
this.$router.push('/');
} else {
this.$store.dispatch('common:setTitle', {
section: this.$t('lookingForPartyTitle'),
});
this.seekers = await this.$store.dispatch('party:lookingForParty');
this.canLoadMore = this.seekers.length === 30;
this.loading = false;
}
},
methods: {
displayLanguage (languageCode) {
const language = this.availableLanguages.find(lang => lang.code === languageCode);
if (language) {
return language.name;
}
return languageCode;
},
infiniteScrollTrigger () {
if (this.canLoadMore) {
this.loading = true;
}
this.loadMore();
},
async inviteUser (userId, index) {
await this.$store.dispatch('guilds:invite', {
invitationDetails: {
inviter: this.user.profile.name,
uuids: [userId],
},
groupId: this.party._id,
});
this.seekers[index].invited = true;
},
loadMore: debounce(async function loadMoreDebounce () {
this.page += 1;
const addlSeekers = await this.$store.dispatch('party:lookingForParty', { page: this.page });
this.seekers = this.seekers.concat(addlSeekers);
this.canLoadMore = this.seekers.length % 30 === 0;
this.loading = false;
}, 1000),
async refreshList () {
this.loading = true;
this.page = 0;
this.seekers = await this.$store.dispatch('party:lookingForParty');
this.canLoadMore = this.seekers.length === 30;
this.loading = false;
},
async rescindInvite (userId, index) {
await this.$store.dispatch('members:removeMember', {
memberId: userId,
groupId: this.party._id,
});
this.seekers[index].invited = false;
},
showMemberModal (userId) {
this.$router.push({ name: 'userProfile', params: { userId } });
},
},
};
</script>

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