diff --git a/public/language/ar/modules.json b/public/language/ar/modules.json
index 68cedc6c3f..ddaace29f8 100644
--- a/public/language/ar/modules.json
+++ b/public/language/ar/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/bg/modules.json b/public/language/bg/modules.json
index 715ec35e02..1a753d8b19 100644
--- a/public/language/bg/modules.json
+++ b/public/language/bg/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "В тази стая",
"chat.kick": "Изгонване",
"chat.show-ip": "Показване на IP адреса",
+ "chat.copy-link": "Copy link",
"chat.owner": "Собственик на стаята",
"chat.grant-rescind-ownership": "Даване/отнемане на собственост",
"chat.system.user-join": "%1 се присъедини към стаята ",
diff --git a/public/language/bn/modules.json b/public/language/bn/modules.json
index 0acbf5265d..f75974dc76 100644
--- a/public/language/bn/modules.json
+++ b/public/language/bn/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/cs/modules.json b/public/language/cs/modules.json
index 08bcc63390..2956e3de31 100644
--- a/public/language/cs/modules.json
+++ b/public/language/cs/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "V této místnosti",
"chat.kick": "Vykopnout",
"chat.show-ip": "Zobrazit IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Majitel místnosti",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/da/modules.json b/public/language/da/modules.json
index 4639c9f1ab..ccc0e8f08d 100644
--- a/public/language/da/modules.json
+++ b/public/language/da/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/de/modules.json b/public/language/de/modules.json
index 2e88897593..207d450380 100644
--- a/public/language/de/modules.json
+++ b/public/language/de/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In diesem Chat-Room",
"chat.kick": "Rauswerfen",
"chat.show-ip": "IP anzeigen",
+ "chat.copy-link": "Copy link",
"chat.owner": "Raumbesitzer",
"chat.grant-rescind-ownership": "Erteilung/Aufhebung des Eigentums",
"chat.system.user-join": "%1 hat den Raum betreten ",
diff --git a/public/language/el/modules.json b/public/language/el/modules.json
index 98a6041312..0bd634c214 100644
--- a/public/language/el/modules.json
+++ b/public/language/el/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/en-US/modules.json b/public/language/en-US/modules.json
index 98a6041312..0bd634c214 100644
--- a/public/language/en-US/modules.json
+++ b/public/language/en-US/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/en-x-pirate/modules.json b/public/language/en-x-pirate/modules.json
index b2ce6a6339..96bdb6b14e 100644
--- a/public/language/en-x-pirate/modules.json
+++ b/public/language/en-x-pirate/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/es/modules.json b/public/language/es/modules.json
index 22ded29cc9..119aab5b91 100644
--- a/public/language/es/modules.json
+++ b/public/language/es/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "En esta sala",
"chat.kick": "Expulsar",
"chat.show-ip": "Mostrar IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/et/modules.json b/public/language/et/modules.json
index d50f5ea9c2..f8eed1d1d0 100644
--- a/public/language/et/modules.json
+++ b/public/language/et/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/fa-IR/modules.json b/public/language/fa-IR/modules.json
index 32fb993062..ce76938e40 100644
--- a/public/language/fa-IR/modules.json
+++ b/public/language/fa-IR/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "در این چت روم",
"chat.kick": "اخراج",
"chat.show-ip": "نشان دادن IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "مدیر چت روم",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/fi/modules.json b/public/language/fi/modules.json
index 802e6f4036..c6beaaa3cd 100644
--- a/public/language/fi/modules.json
+++ b/public/language/fi/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/fr/modules.json b/public/language/fr/modules.json
index e9356d7081..5e7ae37ef4 100644
--- a/public/language/fr/modules.json
+++ b/public/language/fr/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "Dans cet espace de discussion",
"chat.kick": "Exclure",
"chat.show-ip": "Voir IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Espace Admin",
"chat.grant-rescind-ownership": "Promouvoir/rétrograder comme propriétaire",
"chat.system.user-join": "%1 a rejoint la discussion ",
diff --git a/public/language/gl/modules.json b/public/language/gl/modules.json
index dfbbaf172e..478d2e227d 100644
--- a/public/language/gl/modules.json
+++ b/public/language/gl/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/he/modules.json b/public/language/he/modules.json
index 06c6e81045..88ce48a87d 100644
--- a/public/language/he/modules.json
+++ b/public/language/he/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "בתוך חדר זה",
"chat.kick": "הוצא",
"chat.show-ip": "הצג IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "מנהלי החדר",
"chat.grant-rescind-ownership": "הענק/בטל בעלות",
"chat.system.user-join": "%1 הצטרף לחדר ",
diff --git a/public/language/hr/modules.json b/public/language/hr/modules.json
index 758078f326..535ca6816b 100644
--- a/public/language/hr/modules.json
+++ b/public/language/hr/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/hu/modules.json b/public/language/hu/modules.json
index 03811c99c7..6a7692cbc9 100644
--- a/public/language/hu/modules.json
+++ b/public/language/hu/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "Ebben a szobában",
"chat.kick": "Kirúgás",
"chat.show-ip": "IP cím mutatása",
+ "chat.copy-link": "Copy link",
"chat.owner": "Szoba tulajdonos",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/hy/modules.json b/public/language/hy/modules.json
index c4773cf8e6..f9706f24c5 100644
--- a/public/language/hy/modules.json
+++ b/public/language/hy/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "Այս սենյակում",
"chat.kick": "Kick",
"chat.show-ip": "Ցույց տալ IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Սենյակի սեփականատեր",
"chat.grant-rescind-ownership": "Տրամադրել/վերացնել սեփականության իրավունքը",
"chat.system.user-join": "%1-ը միացել է սենյակին ",
diff --git a/public/language/id/modules.json b/public/language/id/modules.json
index b027ef70c1..97849e4b85 100644
--- a/public/language/id/modules.json
+++ b/public/language/id/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/it/modules.json b/public/language/it/modules.json
index 53f06ad8c0..0ca700cedc 100644
--- a/public/language/it/modules.json
+++ b/public/language/it/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In questa stanza",
"chat.kick": "Butta fuori",
"chat.show-ip": "Mostra indirizzo IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Propietario stanza",
"chat.grant-rescind-ownership": "Concedi/Revoca Proprietà",
"chat.system.user-join": "%1 si è unito alla stanza ",
diff --git a/public/language/ja/modules.json b/public/language/ja/modules.json
index 4ae805be4d..b44d722a68 100644
--- a/public/language/ja/modules.json
+++ b/public/language/ja/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "この部屋内",
"chat.kick": "キック",
"chat.show-ip": "IP表示",
+ "chat.copy-link": "Copy link",
"chat.owner": "部屋の管理者",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/ko/modules.json b/public/language/ko/modules.json
index 44e872ddd5..ae568ff649 100644
--- a/public/language/ko/modules.json
+++ b/public/language/ko/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "채팅 참여자",
"chat.kick": "추방",
"chat.show-ip": "IP 보이기",
+ "chat.copy-link": "Copy link",
"chat.owner": "채팅 관리자",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/lt/modules.json b/public/language/lt/modules.json
index 8d3d6d2b15..11180da6f0 100644
--- a/public/language/lt/modules.json
+++ b/public/language/lt/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/lv/modules.json b/public/language/lv/modules.json
index afdcec9197..66032799e1 100644
--- a/public/language/lv/modules.json
+++ b/public/language/lv/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "Šajā tērzētavā",
"chat.kick": "Izslēgt",
"chat.show-ip": "Rādīt IP adresi",
+ "chat.copy-link": "Copy link",
"chat.owner": "Tērzētavas īpašnieks",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/ms/modules.json b/public/language/ms/modules.json
index 18c4865e54..bc9e268850 100644
--- a/public/language/ms/modules.json
+++ b/public/language/ms/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/nb/modules.json b/public/language/nb/modules.json
index e3a941f366..f565ef1348 100644
--- a/public/language/nb/modules.json
+++ b/public/language/nb/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/nl/modules.json b/public/language/nl/modules.json
index 2cf149930a..4254d47faf 100644
--- a/public/language/nl/modules.json
+++ b/public/language/nl/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In deze chat room",
"chat.kick": "Schop",
"chat.show-ip": "Geef IP weer",
+ "chat.copy-link": "Copy link",
"chat.owner": "Chatroom-eigenaar",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/pl/modules.json b/public/language/pl/modules.json
index 032da38b4e..82429f519b 100644
--- a/public/language/pl/modules.json
+++ b/public/language/pl/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "W tym pokoju",
"chat.kick": "Wyrzuć",
"chat.show-ip": "Pokaż IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Właściciel pokoju",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/pt-BR/modules.json b/public/language/pt-BR/modules.json
index 3ae0e95eae..5a9547f678 100644
--- a/public/language/pt-BR/modules.json
+++ b/public/language/pt-BR/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "Nesta sala",
"chat.kick": "Expulsar",
"chat.show-ip": "Mostrar IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Dono da Sala",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/pt-PT/modules.json b/public/language/pt-PT/modules.json
index b97f848659..fd2ed13e64 100644
--- a/public/language/pt-PT/modules.json
+++ b/public/language/pt-PT/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "Participantes nesta sala",
"chat.kick": "Expulsar",
"chat.show-ip": "Mostrar IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Dono da Sala",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/ro/modules.json b/public/language/ro/modules.json
index ede5dea94f..923a63764c 100644
--- a/public/language/ro/modules.json
+++ b/public/language/ro/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/ru/modules.json b/public/language/ru/modules.json
index 69769d997e..c4c30f03fb 100644
--- a/public/language/ru/modules.json
+++ b/public/language/ru/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "В этой комнате",
"chat.kick": "Исключить",
"chat.show-ip": "Показать IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Владелец комнаты",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/rw/modules.json b/public/language/rw/modules.json
index eb2599e1fd..86a2ee9757 100644
--- a/public/language/rw/modules.json
+++ b/public/language/rw/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/sc/modules.json b/public/language/sc/modules.json
index 1675ac1fb6..622f68e00d 100644
--- a/public/language/sc/modules.json
+++ b/public/language/sc/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/sk/modules.json b/public/language/sk/modules.json
index 91ecfb427f..525dbf2206 100644
--- a/public/language/sk/modules.json
+++ b/public/language/sk/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "V tejto miestnosti",
"chat.kick": "Vykopnúť",
"chat.show-ip": "Zobraziť IP adresu",
+ "chat.copy-link": "Copy link",
"chat.owner": "Majiteľ miestnosti",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/sl/modules.json b/public/language/sl/modules.json
index 07fd6024d2..33cb814c8b 100644
--- a/public/language/sl/modules.json
+++ b/public/language/sl/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "In this room",
"chat.kick": "Kick",
"chat.show-ip": "Pokaži IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/sq-AL/modules.json b/public/language/sq-AL/modules.json
index 390133f050..b8eec66eb8 100644
--- a/public/language/sq-AL/modules.json
+++ b/public/language/sq-AL/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "Në këtë dhomë",
"chat.kick": "Largo",
"chat.show-ip": "Shfaq IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Administratori i hapësirës",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/sr/modules.json b/public/language/sr/modules.json
index 79ddec2c79..e6de178576 100644
--- a/public/language/sr/modules.json
+++ b/public/language/sr/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "У овој соби",
"chat.kick": "Избаци",
"chat.show-ip": "Прикажи IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Власник собе",
"chat.grant-rescind-ownership": "Додели/поништи власништво",
"chat.system.user-join": "%1 се придружио соби ",
diff --git a/public/language/sv/modules.json b/public/language/sv/modules.json
index 0335e67f7b..3d498c4760 100644
--- a/public/language/sv/modules.json
+++ b/public/language/sv/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "I detta rum",
"chat.kick": "Sparka ut",
"chat.show-ip": "Visa IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Rummets ägare",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/th/modules.json b/public/language/th/modules.json
index bc46f7729e..633a0efa03 100644
--- a/public/language/th/modules.json
+++ b/public/language/th/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "ในห้องนี้",
"chat.kick": "Kick",
"chat.show-ip": "Show IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Room Owner",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/tr/modules.json b/public/language/tr/modules.json
index 719464d5f2..7e5a72bf35 100644
--- a/public/language/tr/modules.json
+++ b/public/language/tr/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "Bu odada",
"chat.kick": "Dışarı At",
"chat.show-ip": "IP Göster",
+ "chat.copy-link": "Copy link",
"chat.owner": "Oda Sahibi",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 odaya katıldı ",
diff --git a/public/language/uk/modules.json b/public/language/uk/modules.json
index b7e592222f..87be11241a 100644
--- a/public/language/uk/modules.json
+++ b/public/language/uk/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "У цій кімнаті",
"chat.kick": "Штурхнути",
"chat.show-ip": "Показати IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Власник кімнати",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/language/vi/modules.json b/public/language/vi/modules.json
index 823ac679a7..de9ad3c35f 100644
--- a/public/language/vi/modules.json
+++ b/public/language/vi/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "Trong phòng này",
"chat.kick": "Loại ra",
"chat.show-ip": "Hiện IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "Chủ Phòng",
"chat.grant-rescind-ownership": "Cấp/Hủy bỏ Quyền sở hữu",
"chat.system.user-join": "%1 đã tham gia phòng ",
diff --git a/public/language/zh-CN/modules.json b/public/language/zh-CN/modules.json
index e071c0bb7b..49fbfbae6e 100644
--- a/public/language/zh-CN/modules.json
+++ b/public/language/zh-CN/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "在此房间",
"chat.kick": "踢出",
"chat.show-ip": "显示 IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "房间所有者",
"chat.grant-rescind-ownership": "给予/撤销所有权",
"chat.system.user-join": "%1 加入了房间",
diff --git a/public/language/zh-TW/modules.json b/public/language/zh-TW/modules.json
index e1c5cb7257..486c562316 100644
--- a/public/language/zh-TW/modules.json
+++ b/public/language/zh-TW/modules.json
@@ -68,6 +68,7 @@
"chat.in-room": "在此房間",
"chat.kick": "踢出",
"chat.show-ip": "顯示 IP",
+ "chat.copy-link": "Copy link",
"chat.owner": "房間所有者",
"chat.grant-rescind-ownership": "Grant/Rescind Ownership",
"chat.system.user-join": "%1 has joined the room ",
diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml
index b1dabb778b..1c4dfd44a4 100644
--- a/public/openapi/write.yaml
+++ b/public/openapi/write.yaml
@@ -112,6 +112,16 @@ paths:
$ref: 'write/categories.yaml'
/categories/{cid}:
$ref: 'write/categories/cid.yaml'
+ /categories/{cid}/count:
+ $ref: 'write/categories/cid/count.yaml'
+ /categories/{cid}/posts:
+ $ref: 'write/categories/cid/posts.yaml'
+ /categories/{cid}/children:
+ $ref: 'write/categories/cid/children.yaml'
+ /categories/{cid}/topics:
+ $ref: 'write/categories/cid/topics.yaml'
+ /categories/{cid}/watch:
+ $ref: 'write/categories/cid/watch.yaml'
/categories/{cid}/privileges:
$ref: 'write/categories/cid/privileges.yaml'
/categories/{cid}/privileges/{privilege}:
@@ -198,6 +208,8 @@ paths:
$ref: 'write/flags/flagId/notes.yaml'
/flags/{flagId}/notes/{datetime}:
$ref: 'write/flags/flagId/notes/datetime.yaml'
+ /search/categories:
+ $ref: 'write/search/categories.yaml'
/admin/settings/{setting}:
$ref: 'write/admin/settings/setting.yaml'
/admin/analytics:
diff --git a/public/openapi/write/categories.yaml b/public/openapi/write/categories.yaml
index 5c26b53633..9c08994759 100644
--- a/public/openapi/write/categories.yaml
+++ b/public/openapi/write/categories.yaml
@@ -1,3 +1,25 @@
+get:
+ tags:
+ - categories
+ summary: list categories
+ description: This operation returns a flat list of categories available to the calling user
+ responses:
+ '200':
+ description: categories successfully listed
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ $ref: ../components/schemas/Status.yaml#/Status
+ response:
+ type: object
+ properties:
+ categories:
+ type: array
+ items:
+ $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject
post:
tags:
- categories
diff --git a/public/openapi/write/categories/cid/children.yaml b/public/openapi/write/categories/cid/children.yaml
new file mode 100644
index 0000000000..de65fa1449
--- /dev/null
+++ b/public/openapi/write/categories/cid/children.yaml
@@ -0,0 +1,36 @@
+get:
+ tags:
+ - categories
+ summary: get subcategories
+ description: |
+ This operation returns the requested category's children (aka subcategories).
+
+ It is important to note that the number of subcategories returned is dependent on the configured value for that category.
+ If a lower number is specified than there are children, then the list will be truncated to that number.
+
+ This is defined by the `subCategoriesPerPage` key in the category's hash.
+ parameters:
+ - in: path
+ name: cid
+ schema:
+ type: string
+ required: true
+ description: a valid category id, `0` for global privileges, `admin` for admin privileges
+ example: 1
+ responses:
+ '200':
+ description: categories count successfully retrieved
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ $ref: ../../../components/schemas/Status.yaml#/Status
+ response:
+ type: object
+ properties:
+ categories:
+ type: array
+ items:
+ $ref: ../../../components/schemas/CategoryObject.yaml#/CategoryObject
\ No newline at end of file
diff --git a/public/openapi/write/categories/cid/count.yaml b/public/openapi/write/categories/cid/count.yaml
new file mode 100644
index 0000000000..886152add9
--- /dev/null
+++ b/public/openapi/write/categories/cid/count.yaml
@@ -0,0 +1,28 @@
+get:
+ tags:
+ - categories
+ summary: get topic count
+ description: This operation returns the count of topics in a given category (excluding its subcategories)
+ parameters:
+ - in: path
+ name: cid
+ schema:
+ type: string
+ required: true
+ description: a valid category id, `0` for global privileges, `admin` for admin privileges
+ example: 1
+ responses:
+ '200':
+ description: categories count successfully retrieved
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ $ref: ../../../components/schemas/Status.yaml#/Status
+ response:
+ type: object
+ properties:
+ count:
+ type: number
\ No newline at end of file
diff --git a/public/openapi/write/categories/cid/posts.yaml b/public/openapi/write/categories/cid/posts.yaml
new file mode 100644
index 0000000000..0e46ef67a8
--- /dev/null
+++ b/public/openapi/write/categories/cid/posts.yaml
@@ -0,0 +1,28 @@
+get:
+ tags:
+ - categories
+ summary: get topic posts
+ description: This operation returns a list of posts in the category, across all topics in that category (excluding its subcategories)
+ parameters:
+ - in: path
+ name: cid
+ schema:
+ type: string
+ required: true
+ description: a valid category id, `0` for global privileges, `admin` for admin privileges
+ example: 1
+ responses:
+ '200':
+ description: categories posts successfully retrieved
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ $ref: ../../../components/schemas/Status.yaml#/Status
+ response:
+ type: object
+ properties:
+ posts:
+ $ref: ../../../components/schemas/PostsObject.yaml#/PostsObject
\ No newline at end of file
diff --git a/public/openapi/write/categories/cid/topics.yaml b/public/openapi/write/categories/cid/topics.yaml
new file mode 100644
index 0000000000..a14664b20c
--- /dev/null
+++ b/public/openapi/write/categories/cid/topics.yaml
@@ -0,0 +1,75 @@
+get:
+ tags:
+ - categories
+ summary: get topics
+ description: |
+ This operation returns a set of topics in the requested category.
+
+ The number of topics returned is defined by the "Topics per Page" (`topicsPerPage`) setting under ACP > Settings > Pagination.
+ parameters:
+ - in: path
+ name: cid
+ schema:
+ type: string
+ required: true
+ description: a valid category id, `0` for global privileges, `admin` for admin privileges
+ example: 1
+ - in: query
+ name: 'query'
+ schema:
+ type: string
+ required: false
+ description: Likely unused — a URI-encoded JSON string containing values that are passed to `getCategoryTopics`.
+ example: ''
+ - in: query
+ name: 'after'
+ schema:
+ type: string
+ required: false
+ description: The index to start at when querying for the next set of topics. This parameter would be more aptly named `start`.
+ example: '0'
+ - in: query
+ name: 'sort'
+ schema:
+ type: string
+ required: false
+ description: Likely deprecated — the sorting method of topics (use `categoryTopicSort` instead.)
+ example: ''
+ - in: query
+ name: 'categoryTopicSort'
+ schema:
+ type: string
+ required: false
+ description: The sorting method of topics
+ example: 'newest_to_oldest'
+ - in: query
+ name: 'direction'
+ schema:
+ type: string
+ required: false
+ description: The sorting of returned results (if you scroll up you want the topics reversed). Set to "-1" for reversed results.
+ example: '1'
+ responses:
+ '200':
+ description: categories topics successfully retrieved
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ $ref: ../../../components/schemas/Status.yaml#/Status
+ response:
+ type: object
+ properties:
+ topics:
+ type: array
+ items:
+ $ref: ../../../components/schemas/TopicObject.yaml#/TopicObject
+ nextStart:
+ type: number
+ privileges:
+ type: object
+ additionalProperties:
+ type: boolean
+ description: A set of privileges with either true or false
\ No newline at end of file
diff --git a/public/openapi/write/categories/cid/watch.yaml b/public/openapi/write/categories/cid/watch.yaml
new file mode 100644
index 0000000000..06fc399dbe
--- /dev/null
+++ b/public/openapi/write/categories/cid/watch.yaml
@@ -0,0 +1,102 @@
+put:
+ tags:
+ - categories
+ summary: update watch state
+ description: |
+ This operation changes the watch state for the category.
+
+ Note that a category can be watched, not watched, or ignored:
+
+ * A category that is watched will have topics that show up in both `/unread` and `/recent`
+ * A category that is *not* watched will have topics that show up in `/recent` but not `/unread`
+ * A category that is ignored will not have topics that show up in either route.
+
+ This API call does not pertain to notifications for new topics in categories.
+ That behaviour is handled by a third-party plugin — nodebb-plugin-category-notifications
+
+ N.B. When a category's watch state is updated, all of that category's children also have their watch states updated.
+ parameters:
+ - in: path
+ name: cid
+ schema:
+ type: string
+ required: true
+ description: a valid category id, `0` for global privileges, `admin` for admin privileges
+ example: 1
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ uid:
+ type: number
+ description: This value is optional, it allows privileged uids to use this call to affect other user accounts.
+ example: 1
+ state:
+ type: string
+ enum: ['watching', 'notwatching', 'ignoring']
+ example: 'watching'
+ responses:
+ '200':
+ description: categories watch state successfully updated
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ $ref: ../../../components/schemas/Status.yaml#/Status
+ response:
+ type: object
+ properties:
+ modified:
+ type: array
+ description: A list of cids that have had their watch states modified.
+ items:
+ type: number
+delete:
+ tags:
+ - categories
+ summary: update watch state
+ description: |
+ Like the corresponding `PUT` method, this operation changes the watch state for the category.
+ However, it does not take a `state` parameter. It is assumed to be whatever the system default is (`categoryWatchState`).
+ parameters:
+ - in: path
+ name: cid
+ schema:
+ type: string
+ required: true
+ description: a valid category id, `0` for global privileges, `admin` for admin privileges
+ example: 1
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ uid:
+ type: number
+ description: This value is optional, it allows privileged uids to use this call to affect other user accounts.
+ example: 1
+ responses:
+ '200':
+ description: categories watch state successfully updated
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ $ref: ../../../components/schemas/Status.yaml#/Status
+ response:
+ type: object
+ properties:
+ modified:
+ type: array
+ description: A list of cids that have had their watch states modified.
+ items:
+ type: number
\ No newline at end of file
diff --git a/public/openapi/write/search/categories.yaml b/public/openapi/write/search/categories.yaml
new file mode 100644
index 0000000000..86d9115235
--- /dev/null
+++ b/public/openapi/write/search/categories.yaml
@@ -0,0 +1,96 @@
+get:
+ tags:
+ - search
+ summary: find categories by keyword
+ description: |
+ This operation returns a set of categories matching the keyword search.
+
+ A number of filtering options are available, and can be passed in via query string.
+ parameters:
+ - in: query
+ name: 'search'
+ schema:
+ type: string
+ required: false
+ description: The keyword used in the category search
+ example: 'announcements'
+ - in: query
+ name: 'query'
+ schema:
+ type: string
+ required: false
+ description: Likely unused — a URI-encoded JSON string containing values that are passed to `getRecentTopicReplies`.
+ example: ''
+ - in: query
+ name: 'parentCid'
+ schema:
+ type: array
+ required: false
+ description: A list of category IDs. The values received are simply reflected back in the results. Matching cids will have "selected" set to true.
+ example: '0'
+ - in: query
+ name: 'selectedCids'
+ schema:
+ type: array
+ required: false
+ description: Likely deprecated — the sorting method of topics (use `categoryTopicSort` instead.)
+ example: ''
+ - in: query
+ name: 'categoryTopicSort'
+ schema:
+ type: string
+ required: false
+ description: The sorting method of topics
+ example: 'newest_to_oldest'
+ - in: query
+ name: 'direction'
+ schema:
+ type: string
+ required: false
+ description: The sorting of returned results (if you scroll up you want the topics reversed). Set to "-1" for reversed results.
+ example: '1'
+ responses:
+ '200':
+ description: matching categories successfully retrieved
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ status:
+ $ref: ../../components/schemas/Status.yaml#/Status
+ response:
+ type: object
+ properties:
+ categories:
+ type: array
+ items:
+ type: object
+ properties:
+ cid:
+ type: number
+ description: A category identifier assigned upon category creation (this value cannot be changed)
+ name:
+ type: string
+ description: The category's name/title
+ level:
+ type: number
+ icon:
+ type: string
+ description: A FontAwesome icon string
+ example: fa-comments-o
+ bgColor:
+ type: string
+ description: Theme-related, a six-character hexadecimal string representing the background colour of the category
+ color:
+ type: string
+ description: Theme-related, a six-character hexadecimal string representing the foreground/text colour of the category
+ parentCid:
+ type: number
+ description: The category identifier for the category that is the immediate ancestor of the current category
+ imageClass:
+ type: string
+ enum: [auto, cover, contain]
+ description: The `background-position` of the category background image, if one is set
+ selected:
+ type: boolean
\ No newline at end of file
diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js
index 9bbc9c47df..b6255fa251 100644
--- a/public/src/admin/manage/category.js
+++ b/public/src/admin/manage/category.js
@@ -109,11 +109,14 @@ define('admin/manage/category', [
callback: function () {
modal.find('.modal-footer button').prop('disabled', true);
- const intervalId = setInterval(function () {
- socket.emit('categories.getTopicCount', ajaxify.data.category.cid, function (err, count) {
- if (err) {
- return alerts.error(err);
- }
+ const intervalId = setInterval(async () => {
+ if (!ajaxify.data.category) {
+ // Already navigated away
+ return;
+ }
+
+ try {
+ const { count } = await api.get(`/categories/${ajaxify.data.category.cid}/count`);
let percent = 0;
if (ajaxify.data.category.topic_count > 0) {
@@ -121,16 +124,21 @@ define('admin/manage/category', [
}
modal.find('.progress-bar').css({ width: percent + '%' });
- });
+ } catch (err) {
+ clearInterval(intervalId);
+ alerts.error(err);
+ }
}, 1000);
api.del('/categories/' + ajaxify.data.category.cid).then(() => {
- if (intervalId) {
- clearInterval(intervalId);
- }
- modal.modal('hide');
- alerts.success('[[admin/manage/categories:alert.purge-success]]');
- ajaxify.go('admin/manage/categories');
+ setTimeout(() => {
+ if (intervalId) {
+ clearInterval(intervalId);
+ }
+ modal.modal('hide');
+ alerts.success('[[admin/manage/categories:alert.purge-success]]');
+ ajaxify.go('admin/manage/categories');
+ }, 5000);
}).catch(alerts.error);
return false;
diff --git a/public/src/client/account/categories.js b/public/src/client/account/categories.js
index 8e162db809..bb6849b166 100644
--- a/public/src/client/account/categories.js
+++ b/public/src/client/account/categories.js
@@ -1,7 +1,7 @@
'use strict';
-define('forum/account/categories', ['forum/account/header', 'alerts'], function (header, alerts) {
+define('forum/account/categories', ['forum/account/header', 'alerts', 'api'], function (header, alerts, api) {
const Categories = {};
Categories.init = function () {
@@ -11,36 +11,32 @@ define('forum/account/categories', ['forum/account/header', 'alerts'], function
handleIgnoreWatch(category.cid);
});
- $('[component="category/watch/all"]').find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', function () {
+ $('[component="category/watch/all"]').find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', async (e) => {
const cids = [];
- const state = $(this).attr('data-state');
+ const state = e.currentTarget.getAttribute('data-state');
+ const { uid } = ajaxify.data;
$('[data-parent-cid="0"]').each(function (index, el) {
cids.push($(el).attr('data-cid'));
});
- socket.emit('categories.setWatchState', { cid: cids, state: state, uid: ajaxify.data.uid }, function (err, modified_cids) {
- if (err) {
- return alerts.error(err);
- }
- updateDropdowns(modified_cids, state);
- });
+ let modified_cids = await Promise.all(cids.map(async cid => api.put(`/categories/${cid}/watch`, { state, uid })));
+ modified_cids = modified_cids
+ .reduce((memo, cur) => memo.concat(cur.modified), [])
+ .filter((cid, idx, arr) => arr.indexOf(cid) === idx);
+
+ updateDropdowns(modified_cids, state);
});
};
function handleIgnoreWatch(cid) {
const category = $('[data-cid="' + cid + '"]');
- category.find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', function () {
- const $this = $(this);
- const state = $this.attr('data-state');
+ category.find('[component="category/watching"], [component="category/ignoring"], [component="category/notwatching"]').on('click', async (e) => {
+ const state = e.currentTarget.getAttribute('data-state');
+ const { uid } = ajaxify.data;
- socket.emit('categories.setWatchState', { cid: cid, state: state, uid: ajaxify.data.uid }, function (err, modified_cids) {
- if (err) {
- return alerts.error(err);
- }
- updateDropdowns(modified_cids, state);
-
- alerts.success('[[category:' + state + '.message]]');
- });
+ const { modified } = await api.put(`/categories/${cid}/watch`, { state, uid });
+ updateDropdowns(modified, state);
+ alerts.success('[[category:' + state + '.message]]');
});
}
diff --git a/public/src/client/category.js b/public/src/client/category.js
index a69ef64616..e1ab97431f 100644
--- a/public/src/client/category.js
+++ b/public/src/client/category.js
@@ -9,7 +9,8 @@ define('forum/category', [
'categorySelector',
'hooks',
'alerts',
-], function (infinitescroll, share, navigator, topicList, sort, categorySelector, hooks, alerts) {
+ 'api',
+], function (infinitescroll, share, navigator, topicList, sort, categorySelector, hooks, alerts, api) {
const Category = {};
$(window).on('action:ajaxify.start', function (ev, data) {
@@ -68,7 +69,7 @@ define('forum/category', [
const $this = $(this);
const state = $this.attr('data-state');
- socket.emit('categories.setWatchState', { cid: cid, state: state }, function (err) {
+ api.put(`/categories/${cid}/watch`, { state }, (err) => {
if (err) {
return alerts.error(err);
}
@@ -88,28 +89,22 @@ define('forum/category', [
}
function handleLoadMoreSubcategories() {
- $('[component="category/load-more-subcategories"]').on('click', function () {
+ $('[component="category/load-more-subcategories"]').on('click', async function () {
const btn = $(this);
- socket.emit('categories.loadMoreSubCategories', {
- cid: ajaxify.data.cid,
- start: ajaxify.data.nextSubCategoryStart,
- }, function (err, data) {
- if (err) {
- return alerts.error(err);
- }
- btn.toggleClass('hidden', !data.length || data.length < ajaxify.data.subCategoriesPerPage);
- if (!data.length) {
- return;
- }
- app.parseAndTranslate('category', 'children', { children: data }, function (html) {
- html.find('.timeago').timeago();
- $('[component="category/subcategory/container"]').append(html);
- ajaxify.data.nextSubCategoryStart += ajaxify.data.subCategoriesPerPage;
- ajaxify.data.subCategoriesLeft -= data.length;
- btn.toggleClass('hidden', ajaxify.data.subCategoriesLeft <= 0)
- .translateText('[[category:x-more-categories, ' + ajaxify.data.subCategoriesLeft + ']]');
- });
+ const { categories: data } = await api.get(`/categories/${ajaxify.data.cid}/children?start=${ajaxify.data.nextSubCategoryStart}`);
+ btn.toggleClass('hidden', !data.length || data.length < ajaxify.data.subCategoriesPerPage);
+ if (!data.length) {
+ return;
+ }
+ app.parseAndTranslate('category', 'children', { children: data }, function (html) {
+ html.find('.timeago').timeago();
+ $('[component="category/subcategory/container"]').append(html);
+ ajaxify.data.nextSubCategoryStart += ajaxify.data.subCategoriesPerPage;
+ ajaxify.data.subCategoriesLeft -= data.length;
+ btn.toggleClass('hidden', ajaxify.data.subCategoriesLeft <= 0)
+ .translateText('[[category:x-more-categories, ' + ajaxify.data.subCategoriesLeft + ']]');
});
+
return false;
});
}
@@ -118,14 +113,9 @@ define('forum/category', [
navigator.scrollTop(0);
};
- Category.toBottom = function () {
- socket.emit('categories.getTopicCount', ajaxify.data.cid, function (err, count) {
- if (err) {
- return alerts.error(err);
- }
-
- navigator.scrollBottom(count - 1);
- });
+ Category.toBottom = async () => {
+ const { count } = await api.get(`/categories/${ajaxify.data.category.cid}/count`);
+ navigator.scrollBottom(count - 1);
};
function loadTopicsAfter(after, direction, callback) {
@@ -133,7 +123,7 @@ define('forum/category', [
hooks.fire('action:topics.loading');
const params = utils.params();
- infinitescroll.loadMore('categories.loadMore', {
+ infinitescroll.loadMore(`/categories/${ajaxify.data.cid}/topics`, {
cid: ajaxify.data.cid,
after: after,
direction: direction,
diff --git a/public/src/client/infinitescroll.js b/public/src/client/infinitescroll.js
index bd6f98d178..838f164f32 100644
--- a/public/src/client/infinitescroll.js
+++ b/public/src/client/infinitescroll.js
@@ -1,7 +1,7 @@
'use strict';
-define('forum/infinitescroll', ['hooks', 'alerts'], function (hooks, alerts) {
+define('forum/infinitescroll', ['hooks', 'alerts', 'api'], function (hooks, alerts, api) {
const scroll = {};
let callback;
let previousScrollTop = 0;
@@ -72,7 +72,9 @@ define('forum/infinitescroll', ['hooks', 'alerts'], function (hooks, alerts) {
const hookData = { method: method, data: data };
hooks.fire('action:infinitescroll.loadmore', hookData);
- socket.emit(hookData.method, hookData.data, function (err, data) {
+ const call = hookData.method.startsWith('/') ? api.get : socket.emit;
+
+ call(hookData.method, hookData.data, function (err, data) {
if (err) {
loadingMore = false;
return alerts.error(err);
diff --git a/public/src/sockets.js b/public/src/sockets.js
index 1a87e57646..e4ef8273e1 100644
--- a/public/src/sockets.js
+++ b/public/src/sockets.js
@@ -111,8 +111,8 @@ app = window.app || {};
alerts.alert(params);
});
});
- socket.on('event:deprecated_call', function (data) {
- console.warn('[socket.io] ', data.eventName, 'is now deprecated in favour of', data.replacement);
+ socket.on('event:deprecated_call', (data) => {
+ console.warn('[socket.io]', data.eventName, 'is now deprecated', data.replacement ? `in favour of ${data.replacement}` : 'with no alternative planned.');
});
socket.on('event:livereload', function () {
diff --git a/src/api/categories.js b/src/api/categories.js
index c37e287221..774091fd61 100644
--- a/src/api/categories.js
+++ b/src/api/categories.js
@@ -1,6 +1,8 @@
'use strict';
+const meta = require('../meta');
const categories = require('../categories');
+const topics = require('../topics');
const events = require('../events');
const user = require('../user');
const groups = require('../groups');
@@ -15,6 +17,22 @@ const hasAdminPrivilege = async (uid, privilege = 'categories') => {
}
};
+categoriesAPI.list = async (caller) => {
+ async function getCategories() {
+ const cids = await categories.getCidsByPrivilege('categories:cid', caller.uid, 'find');
+ return await categories.getCategoriesData(cids);
+ }
+
+ const [isAdmin, categoriesData] = await Promise.all([
+ user.isAdministrator(caller.uid),
+ getCategories(),
+ ]);
+
+ return {
+ categories: categoriesData.filter(category => category && (!category.disabled || isAdmin)),
+ };
+};
+
categoriesAPI.get = async function (caller, data) {
const [userPrivileges, category] = await Promise.all([
privileges.categories.get(data.cid, caller.uid),
@@ -61,6 +79,100 @@ categoriesAPI.delete = async function (caller, { cid }) {
});
};
+categoriesAPI.getTopicCount = async (caller, { cid }) => {
+ const count = await categories.getCategoryField(cid, 'topic_count');
+ return { count };
+};
+
+categoriesAPI.getPosts = async (caller, { cid }) => await categories.getRecentReplies(cid, caller.uid, 0, 4);
+
+categoriesAPI.getChildren = async (caller, { cid, start }) => {
+ if (!start || start < 0) {
+ start = 0;
+ }
+ start = parseInt(start, 10);
+
+ const allowed = await privileges.categories.can('read', cid, caller.uid);
+ if (!allowed) {
+ throw new Error('[[error:no-privileges]]');
+ }
+
+ const category = await categories.getCategoryData(cid);
+ await categories.getChildrenTree(category, caller.uid);
+ const allCategories = [];
+ categories.flattenCategories(allCategories, category.children);
+ await categories.getRecentTopicReplies(allCategories, caller.uid);
+
+ const payload = category.children.slice(start, start + category.subCategoriesPerPage);
+ return { categories: payload };
+};
+
+categoriesAPI.getTopics = async (caller, data) => {
+ data.query = data.query || {};
+ const [userPrivileges, settings, targetUid] = await Promise.all([
+ privileges.categories.get(data.cid, caller.uid),
+ user.getSettings(caller.uid),
+ user.getUidByUserslug(data.query.author),
+ ]);
+
+ if (!userPrivileges.read) {
+ throw new Error('[[error:no-privileges]]');
+ }
+
+ const infScrollTopicsPerPage = 20;
+ const sort = data.sort || data.categoryTopicSort || meta.config.categoryTopicSort || 'newest_to_oldest';
+
+ let start = Math.max(0, parseInt(data.after || 0, 10));
+
+ if (data.direction === -1) {
+ start -= infScrollTopicsPerPage;
+ }
+
+ let stop = start + infScrollTopicsPerPage - 1;
+
+ start = Math.max(0, start);
+ stop = Math.max(0, stop);
+ const result = await categories.getCategoryTopics({
+ uid: caller.uid,
+ cid: data.cid,
+ start,
+ stop,
+ sort,
+ settings,
+ query: data.query,
+ tag: data.query.tag,
+ targetUid,
+ });
+ categories.modifyTopicsByPrivilege(result.topics, userPrivileges);
+
+ return { ...result, privileges: userPrivileges };
+};
+
+categoriesAPI.setWatchState = async (caller, { cid, state, uid }) => {
+ let targetUid = caller.uid;
+ const cids = Array.isArray(cid) ? cid.map(cid => parseInt(cid, 10)) : [parseInt(cid, 10)];
+ if (uid) {
+ targetUid = uid;
+ }
+ await user.isAdminOrGlobalModOrSelf(caller.uid, targetUid);
+ const allCids = await categories.getAllCidsFromSet('categories:cid');
+ const categoryData = await categories.getCategoriesFields(allCids, ['cid', 'parentCid']);
+
+ // filter to subcategories of cid
+ let cat;
+ do {
+ cat = categoryData.find(c => !cids.includes(c.cid) && cids.includes(c.parentCid));
+ if (cat) {
+ cids.push(cat.cid);
+ }
+ } while (cat);
+
+ await user.setCategoryWatchState(targetUid, cids, state);
+ await topics.pushUnreadCount(targetUid);
+
+ return { cids };
+};
+
categoriesAPI.getPrivileges = async (caller, { cid }) => {
await hasAdminPrivilege(caller.uid, 'privileges');
diff --git a/src/api/index.js b/src/api/index.js
index 9e5446c325..c454de93a5 100644
--- a/src/api/index.js
+++ b/src/api/index.js
@@ -9,6 +9,7 @@ module.exports = {
posts: require('./posts'),
chats: require('./chats'),
categories: require('./categories'),
+ search: require('./search'),
flags: require('./flags'),
files: require('./files'),
utils: require('./utils'),
diff --git a/src/api/search.js b/src/api/search.js
new file mode 100644
index 0000000000..18bd9fa160
--- /dev/null
+++ b/src/api/search.js
@@ -0,0 +1,105 @@
+'use strict';
+
+const _ = require('lodash');
+
+const categories = require('../categories');
+const privileges = require('../privileges');
+const meta = require('../meta');
+const plugins = require('../plugins');
+
+const controllersHelpers = require('../controllers/helpers');
+
+const searchApi = module.exports;
+
+searchApi.categories = async (caller, data) => {
+ // used by categorySearch module
+
+ let cids = [];
+ let matchedCids = [];
+ const privilege = data.privilege || 'topics:read';
+ data.states = (data.states || ['watching', 'notwatching', 'ignoring']).map(
+ state => categories.watchStates[state]
+ );
+ data.parentCid = parseInt(data.parentCid || 0, 10);
+
+ if (data.search) {
+ ({ cids, matchedCids } = await findMatchedCids(caller.uid, data));
+ } else {
+ cids = await loadCids(caller.uid, data.parentCid);
+ }
+
+ const visibleCategories = await controllersHelpers.getVisibleCategories({
+ cids, uid: caller.uid, states: data.states, privilege, showLinks: data.showLinks, parentCid: data.parentCid,
+ });
+
+ if (Array.isArray(data.selectedCids)) {
+ data.selectedCids = data.selectedCids.map(cid => parseInt(cid, 10));
+ }
+
+ let categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass'], data.parentCid);
+ categoriesData = categoriesData.slice(0, 200);
+
+ categoriesData.forEach((category) => {
+ category.selected = data.selectedCids ? data.selectedCids.includes(category.cid) : false;
+ if (matchedCids.includes(category.cid)) {
+ category.match = true;
+ }
+ });
+ const result = await plugins.hooks.fire('filter:categories.categorySearch', {
+ categories: categoriesData,
+ ...data,
+ uid: caller.uid,
+ });
+
+ return { categories: result.categories };
+};
+
+async function findMatchedCids(uid, data) {
+ const result = await categories.search({
+ uid: uid,
+ query: data.search,
+ qs: data.query,
+ paginate: false,
+ });
+
+ let matchedCids = result.categories.map(c => c.cid);
+ // no need to filter if all 3 states are used
+ const filterByWatchState = !Object.values(categories.watchStates)
+ .every(state => data.states.includes(state));
+
+ if (filterByWatchState) {
+ const states = await categories.getWatchState(matchedCids, uid);
+ matchedCids = matchedCids.filter((cid, index) => data.states.includes(states[index]));
+ }
+
+ const rootCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getParentCids))));
+ const allChildCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getChildrenCids))));
+
+ return {
+ cids: _.uniq(rootCids.concat(allChildCids).concat(matchedCids)),
+ matchedCids: matchedCids,
+ };
+}
+
+async function loadCids(uid, parentCid) {
+ let resultCids = [];
+ async function getCidsRecursive(cids) {
+ const categoryData = await categories.getCategoriesFields(cids, ['subCategoriesPerPage']);
+ const cidToData = _.zipObject(cids, categoryData);
+ await Promise.all(cids.map(async (cid) => {
+ const allChildCids = await categories.getAllCidsFromSet(`cid:${cid}:children`);
+ if (allChildCids.length) {
+ const childCids = await privileges.categories.filterCids('find', allChildCids, uid);
+ resultCids.push(...childCids.slice(0, cidToData[cid].subCategoriesPerPage));
+ await getCidsRecursive(childCids);
+ }
+ }));
+ }
+
+ const allRootCids = await categories.getAllCidsFromSet(`cid:${parentCid}:children`);
+ const rootCids = await privileges.categories.filterCids('find', allRootCids, uid);
+ const pageCids = rootCids.slice(0, meta.config.categoriesPerPage);
+ resultCids = pageCids;
+ await getCidsRecursive(pageCids);
+ return resultCids;
+}
diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js
index d84f2bddfb..80ee961fbf 100644
--- a/src/controllers/write/categories.js
+++ b/src/controllers/write/categories.js
@@ -1,12 +1,17 @@
'use strict';
const categories = require('../../categories');
+const meta = require('../../meta');
const api = require('../../api');
const helpers = require('../helpers');
const Categories = module.exports;
+Categories.list = async (req, res) => {
+ helpers.formatApiResponse(200, res, await api.categories.list(req));
+};
+
Categories.get = async (req, res) => {
helpers.formatApiResponse(200, res, await api.categories.get(req, req.params));
};
@@ -31,6 +36,47 @@ Categories.delete = async (req, res) => {
helpers.formatApiResponse(200, res);
};
+Categories.getTopicCount = async (req, res) => {
+ helpers.formatApiResponse(200, res, await api.categories.getTopicCount(req, { ...req.params }));
+};
+
+Categories.getPosts = async (req, res) => {
+ const posts = await api.categories.getPosts(req, { ...req.params });
+ helpers.formatApiResponse(200, res, { posts });
+};
+
+Categories.getChildren = async (req, res) => {
+ const { cid } = req.params;
+ const { start } = req.query;
+ helpers.formatApiResponse(200, res, await api.categories.getChildren(req, { cid, start }));
+};
+
+Categories.getTopics = async (req, res) => {
+ const { cid } = req.params;
+ const result = await api.categories.getTopics(req, { ...req.query, cid });
+
+ helpers.formatApiResponse(200, res, result);
+};
+
+Categories.setWatchState = async (req, res) => {
+ const { cid } = req.params;
+ let { uid, state } = req.body;
+
+ if (req.method === 'DELETE') {
+ // DELETE is always setting state to system default in acp
+ state = categories.watchStates[meta.config.categoryWatchState];
+ } else if (Object.keys(categories.watchStates).includes(state)) {
+ state = categories.watchStates[state]; // convert to integer for backend processing
+ } else {
+ console.log('throwing', cid, uid, state);
+ throw new Error('[[error:invalid-data]]');
+ }
+
+ const { cids: modified } = await api.categories.setWatchState(req, { cid, state, uid });
+
+ helpers.formatApiResponse(200, res, { modified });
+};
+
Categories.getPrivileges = async (req, res) => {
const privilegeSet = await api.categories.getPrivileges(req, { cid: req.params.cid });
helpers.formatApiResponse(200, res, privilegeSet);
diff --git a/src/controllers/write/index.js b/src/controllers/write/index.js
index 46a8dd8110..26c74128d8 100644
--- a/src/controllers/write/index.js
+++ b/src/controllers/write/index.js
@@ -10,6 +10,7 @@ Write.tags = require('./tags');
Write.posts = require('./posts');
Write.chats = require('./chats');
Write.flags = require('./flags');
+Write.search = require('./search');
Write.admin = require('./admin');
Write.files = require('./files');
Write.utilities = require('./utilities');
diff --git a/src/controllers/write/search.js b/src/controllers/write/search.js
new file mode 100644
index 0000000000..a6acd0a59a
--- /dev/null
+++ b/src/controllers/write/search.js
@@ -0,0 +1,10 @@
+'use strict';
+
+const api = require('../../api');
+const helpers = require('../helpers');
+
+const Search = module.exports;
+
+Search.categories = async (req, res) => {
+ helpers.formatApiResponse(200, res, await api.search.categories(req, req.query));
+};
diff --git a/src/middleware/assert.js b/src/middleware/assert.js
index 553114f870..6c0f5ef72f 100644
--- a/src/middleware/assert.js
+++ b/src/middleware/assert.js
@@ -11,6 +11,7 @@ const nconf = require('nconf');
const file = require('../file');
const user = require('../user');
const groups = require('../groups');
+const categories = require('../categories');
const topics = require('../topics');
const posts = require('../posts');
const messaging = require('../messaging');
@@ -39,6 +40,14 @@ Assert.group = helpers.try(async (req, res, next) => {
next();
});
+Assert.category = helpers.try(async (req, res, next) => {
+ if (!await categories.exists(req.params.cid)) {
+ return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-category]]'));
+ }
+
+ next();
+});
+
Assert.topic = helpers.try(async (req, res, next) => {
if (!await topics.exists(req.params.tid)) {
return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-topic]]'));
diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js
index ed3ffd2dce..ca149a54da 100644
--- a/src/routes/write/categories.js
+++ b/src/routes/write/categories.js
@@ -10,11 +10,20 @@ const { setupApiRoute } = routeHelpers;
module.exports = function () {
const middlewares = [middleware.ensureLoggedIn];
+ setupApiRoute(router, 'get', '/', [...middlewares], controllers.write.categories.list);
setupApiRoute(router, 'post', '/', [...middlewares, middleware.checkRequired.bind(null, ['name'])], controllers.write.categories.create);
setupApiRoute(router, 'get', '/:cid', [], controllers.write.categories.get);
setupApiRoute(router, 'put', '/:cid', [...middlewares], controllers.write.categories.update);
setupApiRoute(router, 'delete', '/:cid', [...middlewares], controllers.write.categories.delete);
+ setupApiRoute(router, 'get', '/:cid/count', [...middlewares, middleware.assert.category], controllers.write.categories.getTopicCount);
+ setupApiRoute(router, 'get', '/:cid/posts', [...middlewares, middleware.assert.category], controllers.write.categories.getPosts);
+ setupApiRoute(router, 'get', '/:cid/children', [...middlewares, middleware.assert.category], controllers.write.categories.getChildren);
+ setupApiRoute(router, 'get', '/:cid/topics', [...middlewares, middleware.assert.category], controllers.write.categories.getTopics);
+
+ setupApiRoute(router, 'put', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState);
+ setupApiRoute(router, 'delete', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState);
+
setupApiRoute(router, 'get', '/:cid/privileges', [...middlewares], controllers.write.categories.getPrivileges);
setupApiRoute(router, 'put', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege);
setupApiRoute(router, 'delete', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege);
diff --git a/src/routes/write/index.js b/src/routes/write/index.js
index 8e29c3ddd1..2ebec74ce1 100644
--- a/src/routes/write/index.js
+++ b/src/routes/write/index.js
@@ -41,6 +41,7 @@ Write.reload = async (params) => {
router.use('/api/v3/posts', require('./posts')());
router.use('/api/v3/chats', require('./chats')());
router.use('/api/v3/flags', require('./flags')());
+ router.use('/api/v3/search', require('./search')());
router.use('/api/v3/admin', require('./admin')());
router.use('/api/v3/files', require('./files')());
router.use('/api/v3/utilities', require('./utilities')());
diff --git a/src/routes/write/search.js b/src/routes/write/search.js
new file mode 100644
index 0000000000..01b98cdeed
--- /dev/null
+++ b/src/routes/write/search.js
@@ -0,0 +1,19 @@
+'use strict';
+
+const router = require('express').Router();
+// const middleware = require('../../middleware');
+const controllers = require('../../controllers');
+const routeHelpers = require('../helpers');
+
+const { setupApiRoute } = routeHelpers;
+
+module.exports = function () {
+ // const middlewares = [];
+
+ // maybe redirect to /search/posts?
+ // setupApiRoute(router, 'post', '/', [...middlewares], controllers.write.search.TBD);
+
+ setupApiRoute(router, 'get', '/categories', [], controllers.write.search.categories);
+
+ return router;
+};
diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js
index 169e207b0d..934defaeac 100644
--- a/src/socket.io/categories.js
+++ b/src/socket.io/categories.js
@@ -1,31 +1,30 @@
'use strict';
const categories = require('../categories');
-const privileges = require('../privileges');
const user = require('../user');
const topics = require('../topics');
+const api = require('../api');
+
+const sockets = require('.');
const SocketCategories = module.exports;
require('./categories/search')(SocketCategories);
SocketCategories.getRecentReplies = async function (socket, cid) {
- return await categories.getRecentReplies(cid, socket.uid, 0, 4);
+ sockets.warnDeprecated(socket, 'GET /api/v3/categories/:cid/posts');
+ return await api.categories.getPosts(socket, { cid });
};
SocketCategories.get = async function (socket) {
- async function getCategories() {
- const cids = await categories.getCidsByPrivilege('categories:cid', socket.uid, 'find');
- return await categories.getCategoriesData(cids);
- }
- const [isAdmin, categoriesData] = await Promise.all([
- user.isAdministrator(socket.uid),
- getCategories(),
- ]);
- return categoriesData.filter(category => category && (!category.disabled || isAdmin));
+ sockets.warnDeprecated(socket, 'GET /api/v3/categories');
+ const { categories } = await api.categories.list(socket);
+ return categories;
};
SocketCategories.getWatchedCategories = async function (socket) {
+ sockets.warnDeprecated(socket);
+
const [categoriesData, ignoredCids] = await Promise.all([
categories.getCategoriesByPrivilege('cid:0:children', socket.uid, 'find'),
user.getIgnoredCategories(socket.uid),
@@ -38,63 +37,40 @@ SocketCategories.loadMore = async function (socket, data) {
throw new Error('[[error:invalid-data]]');
}
data.query = data.query || {};
- const [userPrivileges, settings, targetUid] = await Promise.all([
- privileges.categories.get(data.cid, socket.uid),
- user.getSettings(socket.uid),
- user.getUidByUserslug(data.query.author),
- ]);
- if (!userPrivileges.read) {
- throw new Error('[[error:no-privileges]]');
- }
+ const result = await api.categories.getTopics(socket, data);
- const infScrollTopicsPerPage = 20;
- const sort = data.sort || data.categoryTopicSort;
-
- let start = Math.max(0, parseInt(data.after, 10));
-
- if (data.direction === -1) {
- start -= infScrollTopicsPerPage;
- }
-
- let stop = start + infScrollTopicsPerPage - 1;
-
- start = Math.max(0, start);
- stop = Math.max(0, stop);
- const result = await categories.getCategoryTopics({
- uid: socket.uid,
- cid: data.cid,
- start: start,
- stop: stop,
- sort: sort,
- settings: settings,
- query: data.query,
- tag: data.query.tag,
- targetUid: targetUid,
- });
- categories.modifyTopicsByPrivilege(result.topics, userPrivileges);
-
- result.privileges = userPrivileges;
+ // Backwards compatibility — unsure of current usage.
result.template = {
category: true,
name: 'category',
};
+
return result;
};
SocketCategories.getTopicCount = async function (socket, cid) {
- return await categories.getCategoryField(cid, 'topic_count');
+ sockets.warnDeprecated(socket, 'GET /api/v3/categories/:cid');
+
+ const { count } = await api.categories.getTopicCount(socket, { cid });
+ return count;
};
SocketCategories.getCategoriesByPrivilege = async function (socket, privilege) {
+ sockets.warnDeprecated(socket);
+
return await categories.getCategoriesByPrivilege('categories:cid', socket.uid, privilege);
};
SocketCategories.getMoveCategories = async function (socket, data) {
+ sockets.warnDeprecated(socket);
+
return await SocketCategories.getSelectCategories(socket, data);
};
SocketCategories.getSelectCategories = async function (socket) {
+ sockets.warnDeprecated(socket);
+
const [isAdmin, categoriesData] = await Promise.all([
user.isAdministrator(socket.uid),
categories.buildForSelect(socket.uid, 'find', ['disabled', 'link']),
@@ -103,19 +79,27 @@ SocketCategories.getSelectCategories = async function (socket) {
};
SocketCategories.setWatchState = async function (socket, data) {
+ sockets.warnDeprecated(socket, 'PUT/DELETE /api/v3/categories/:cid/watch');
+
if (!data || !data.cid || !data.state) {
throw new Error('[[error:invalid-data]]');
}
- return await ignoreOrWatch(async (uid, cids) => {
- await user.setCategoryWatchState(uid, cids, categories.watchStates[data.state]);
- }, socket, data);
+
+ data.state = categories.watchStates[data.state];
+
+ await api.categories.setWatchState(socket, data);
+ return data.cid;
};
SocketCategories.watch = async function (socket, data) {
+ sockets.warnDeprecated(socket);
+
return await ignoreOrWatch(user.watchCategory, socket, data);
};
SocketCategories.ignore = async function (socket, data) {
+ sockets.warnDeprecated(socket);
+
return await ignoreOrWatch(user.ignoreCategory, socket, data);
};
@@ -144,24 +128,20 @@ async function ignoreOrWatch(fn, socket, data) {
}
SocketCategories.isModerator = async function (socket, cid) {
+ sockets.warnDeprecated(socket);
+
return await user.isModerator(socket.uid, cid);
};
SocketCategories.loadMoreSubCategories = async function (socket, data) {
+ sockets.warnDeprecated(socket, `GET /api/v3/categories/:cid/children`);
+
if (!data || !data.cid || !(parseInt(data.start, 10) >= 0)) {
throw new Error('[[error:invalid-data]]');
}
- const allowed = await privileges.categories.can('read', data.cid, socket.uid);
- if (!allowed) {
- throw new Error('[[error:no-privileges]]');
- }
- const category = await categories.getCategoryData(data.cid);
- await categories.getChildrenTree(category, socket.uid);
- const allCategories = [];
- categories.flattenCategories(allCategories, category.children);
- await categories.getRecentTopicReplies(allCategories, socket.uid);
- const start = parseInt(data.start, 10);
- return category.children.slice(start, start + category.subCategoriesPerPage);
+
+ const { categories: children } = await api.categories.getChildren(socket, data);
+ return children;
};
require('../promisify')(SocketCategories);
diff --git a/src/socket.io/categories/search.js b/src/socket.io/categories/search.js
index ad04c20edf..dbb355ce89 100644
--- a/src/socket.io/categories/search.js
+++ b/src/socket.io/categories/search.js
@@ -1,101 +1,13 @@
'use strict';
-const _ = require('lodash');
-
-const meta = require('../../meta');
-const categories = require('../../categories');
-const privileges = require('../../privileges');
-const controllersHelpers = require('../../controllers/helpers');
-const plugins = require('../../plugins');
+const sockets = require('..');
+const api = require('../../api');
module.exports = function (SocketCategories) {
- // used by categorySearch module
SocketCategories.categorySearch = async function (socket, data) {
- let cids = [];
- let matchedCids = [];
- const privilege = data.privilege || 'topics:read';
- data.states = (data.states || ['watching', 'notwatching', 'ignoring']).map(
- state => categories.watchStates[state]
- );
+ sockets.warnDeprecated(socket, 'GET /api/v3/search/categories');
- if (data.search) {
- ({ cids, matchedCids } = await findMatchedCids(socket.uid, data));
- } else {
- cids = await loadCids(socket.uid, data.parentCid);
- }
-
- const visibleCategories = await controllersHelpers.getVisibleCategories({
- cids, uid: socket.uid, states: data.states, privilege, showLinks: data.showLinks, parentCid: data.parentCid,
- });
-
- if (Array.isArray(data.selectedCids)) {
- data.selectedCids = data.selectedCids.map(cid => parseInt(cid, 10));
- }
-
- let categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass'], data.parentCid);
- categoriesData = categoriesData.slice(0, 200);
-
- categoriesData.forEach((category) => {
- category.selected = data.selectedCids ? data.selectedCids.includes(category.cid) : false;
- if (matchedCids.includes(category.cid)) {
- category.match = true;
- }
- });
- const result = await plugins.hooks.fire('filter:categories.categorySearch', {
- categories: categoriesData,
- ...data,
- uid: socket.uid,
- });
- return result.categories;
+ const { categories } = await api.search.categories(socket, data);
+ return categories;
};
-
- async function findMatchedCids(uid, data) {
- const result = await categories.search({
- uid: uid,
- query: data.search,
- qs: data.query,
- paginate: false,
- });
-
- let matchedCids = result.categories.map(c => c.cid);
- // no need to filter if all 3 states are used
- const filterByWatchState = !Object.values(categories.watchStates)
- .every(state => data.states.includes(state));
-
- if (filterByWatchState) {
- const states = await categories.getWatchState(matchedCids, uid);
- matchedCids = matchedCids.filter((cid, index) => data.states.includes(states[index]));
- }
-
- const rootCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getParentCids))));
- const allChildCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getChildrenCids))));
-
- return {
- cids: _.uniq(rootCids.concat(allChildCids).concat(matchedCids)),
- matchedCids: matchedCids,
- };
- }
-
- async function loadCids(uid, parentCid) {
- let resultCids = [];
- async function getCidsRecursive(cids) {
- const categoryData = await categories.getCategoriesFields(cids, ['subCategoriesPerPage']);
- const cidToData = _.zipObject(cids, categoryData);
- await Promise.all(cids.map(async (cid) => {
- const allChildCids = await categories.getAllCidsFromSet(`cid:${cid}:children`);
- if (allChildCids.length) {
- const childCids = await privileges.categories.filterCids('find', allChildCids, uid);
- resultCids.push(...childCids.slice(0, cidToData[cid].subCategoriesPerPage));
- await getCidsRecursive(childCids);
- }
- }));
- }
-
- const allRootCids = await categories.getAllCidsFromSet(`cid:${parentCid}:children`);
- const rootCids = await privileges.categories.filterCids('find', allRootCids, uid);
- const pageCids = rootCids.slice(0, meta.config.categoriesPerPage);
- resultCids = pageCids;
- await getCidsRecursive(pageCids);
- return resultCids;
- }
};
diff --git a/src/socket.io/index.js b/src/socket.io/index.js
index 8f03eb2a9d..c10f271585 100644
--- a/src/socket.io/index.js
+++ b/src/socket.io/index.js
@@ -333,5 +333,9 @@ Sockets.warnDeprecated = (socket, replacement) => {
replacement: replacement,
});
}
- winston.warn(`[deprecated]\n ${new Error('-').stack.split('\n').slice(2, 5).join('\n')}\n use ${replacement}`);
+ winston.warn([
+ '[deprecated]',
+ `${new Error('-').stack.split('\n').slice(2, 5).join('\n')}`,
+ ` ${replacement ? `use ${replacement}` : 'there is no replacement for this call.'}`,
+ ].join('\n'));
};