From 8393113851ca8640f67a501aa26ffceaa0f19175 Mon Sep 17 00:00:00 2001 From: Misty Release Bot Date: Tue, 31 Oct 2023 14:15:30 +0000 Subject: [PATCH 01/14] chore(i18n): fallback strings for new resources: nodebb.modules --- public/language/ar/modules.json | 1 + public/language/bg/modules.json | 1 + public/language/bn/modules.json | 1 + public/language/cs/modules.json | 1 + public/language/da/modules.json | 1 + public/language/de/modules.json | 1 + public/language/el/modules.json | 1 + public/language/en-US/modules.json | 1 + public/language/en-x-pirate/modules.json | 1 + public/language/es/modules.json | 1 + public/language/et/modules.json | 1 + public/language/fa-IR/modules.json | 1 + public/language/fi/modules.json | 1 + public/language/fr/modules.json | 1 + public/language/gl/modules.json | 1 + public/language/he/modules.json | 1 + public/language/hr/modules.json | 1 + public/language/hu/modules.json | 1 + public/language/hy/modules.json | 1 + public/language/id/modules.json | 1 + public/language/it/modules.json | 1 + public/language/ja/modules.json | 1 + public/language/ko/modules.json | 1 + public/language/lt/modules.json | 1 + public/language/lv/modules.json | 1 + public/language/ms/modules.json | 1 + public/language/nb/modules.json | 1 + public/language/nl/modules.json | 1 + public/language/pl/modules.json | 1 + public/language/pt-BR/modules.json | 1 + public/language/pt-PT/modules.json | 1 + public/language/ro/modules.json | 1 + public/language/ru/modules.json | 1 + public/language/rw/modules.json | 1 + public/language/sc/modules.json | 1 + public/language/sk/modules.json | 1 + public/language/sl/modules.json | 1 + public/language/sq-AL/modules.json | 1 + public/language/sr/modules.json | 1 + public/language/sv/modules.json | 1 + public/language/th/modules.json | 1 + public/language/tr/modules.json | 1 + public/language/uk/modules.json | 1 + public/language/vi/modules.json | 1 + public/language/zh-CN/modules.json | 1 + public/language/zh-TW/modules.json | 1 + 46 files changed, 46 insertions(+) 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 ", From 52b78e83a89dbaa7e5e84b4a4a3c3143fa5e725e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 18 Oct 2023 15:01:01 -0400 Subject: [PATCH 02/14] refactor(socket.io): deprecate categories.getRecentReplies in favour of api.categories.getPosts --- src/api/categories.js | 2 ++ src/controllers/write/categories.js | 5 +++++ src/routes/write/categories.js | 2 ++ src/socket.io/categories.js | 6 +++++- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/api/categories.js b/src/api/categories.js index c37e287221..394db8f95d 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -61,6 +61,8 @@ categoriesAPI.delete = async function (caller, { cid }) { }); }; +categoriesAPI.getPosts = async (caller, { cid }) => await categories.getRecentReplies(cid, caller.uid, 0, 4); + categoriesAPI.getPrivileges = async (caller, { cid }) => { await hasAdminPrivilege(caller.uid, 'privileges'); diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index d84f2bddfb..714816ef9f 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -31,6 +31,11 @@ Categories.delete = async (req, res) => { helpers.formatApiResponse(200, res); }; +Categories.getPosts = async (req, res) => { + const posts = await api.categories.getPosts(req, { ...req.params }); + helpers.formatApiResponse(200, res, posts); +}; + 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/routes/write/categories.js b/src/routes/write/categories.js index ed3ffd2dce..3b96556d20 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -15,6 +15,8 @@ module.exports = function () { setupApiRoute(router, 'put', '/:cid', [...middlewares], controllers.write.categories.update); setupApiRoute(router, 'delete', '/:cid', [...middlewares], controllers.write.categories.delete); + setupApiRoute(router, 'get', '/:cid/posts', [...middlewares], controllers.write.categories.getPosts); + 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/socket.io/categories.js b/src/socket.io/categories.js index 169e207b0d..1db5358354 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -4,13 +4,17 @@ 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) { From 96046373da823ce9a57e8bdd4cb41fa69d9c5080 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 19 Oct 2023 10:21:51 -0400 Subject: [PATCH 03/14] refactor(socket.io): deprecate categories.get in favour of api.categories.list --- src/api/categories.js | 16 ++++++++++++++++ src/controllers/write/categories.js | 4 ++++ src/routes/write/categories.js | 1 + src/socket.io/categories.js | 12 +++--------- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/api/categories.js b/src/api/categories.js index 394db8f95d..47cb0ca8f6 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -15,6 +15,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), diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 714816ef9f..52408a5400 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -7,6 +7,10 @@ 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)); }; diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index 3b96556d20..35957b440b 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -10,6 +10,7 @@ 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); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 1db5358354..8168176656 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -18,15 +18,9 @@ SocketCategories.getRecentReplies = async function (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) { From c442b6e662a875ca26f88e8c252ee38c5b1f700d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 19 Oct 2023 11:26:00 -0400 Subject: [PATCH 04/14] refactor(socket.io): deprecate categories.getTopicCount in favour of api.categories.getTopicCount --- public/src/admin/manage/category.js | 32 ++++++++++++++++++----------- public/src/client/category.js | 14 +++++-------- public/src/sockets.js | 4 ++-- src/api/categories.js | 5 +++++ src/controllers/write/categories.js | 4 ++++ src/routes/write/categories.js | 1 + src/socket.io/categories.js | 6 +++++- src/socket.io/index.js | 6 +++++- 8 files changed, 47 insertions(+), 25 deletions(-) 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/category.js b/public/src/client/category.js index a69ef64616..a89620cb34 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) { @@ -118,14 +119,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) { 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 47cb0ca8f6..28b2539a3a 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -77,6 +77,11 @@ 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.getPrivileges = async (caller, { cid }) => { diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 52408a5400..8d4c0aebcf 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -35,6 +35,10 @@ 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); diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index 35957b440b..44a3020863 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -16,6 +16,7 @@ module.exports = function () { setupApiRoute(router, 'put', '/:cid', [...middlewares], controllers.write.categories.update); setupApiRoute(router, 'delete', '/:cid', [...middlewares], controllers.write.categories.delete); + setupApiRoute(router, 'get', '/:cid/count', [...middlewares], controllers.write.categories.getTopicCount); setupApiRoute(router, 'get', '/:cid/posts', [...middlewares], controllers.write.categories.getPosts); setupApiRoute(router, 'get', '/:cid/privileges', [...middlewares], controllers.write.categories.getPrivileges); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 8168176656..126bf5e7d6 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -24,6 +24,8 @@ SocketCategories.get = async function (socket) { }; 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), @@ -81,7 +83,9 @@ SocketCategories.loadMore = async function (socket, data) { }; 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) { 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')); }; From f1dbfaa2834f31218c9f95a01669baddf7179f3b Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 19 Oct 2023 11:47:10 -0400 Subject: [PATCH 05/14] chore(socket.io): deprecate categories.(isModerator|ignore|watch|getSelectCategories|getMoveCategories|getCategoriesByPrivilege) --- src/socket.io/categories.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 126bf5e7d6..aea08cdfa2 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -84,19 +84,26 @@ SocketCategories.loadMore = async function (socket, data) { SocketCategories.getTopicCount = async function (socket, cid) { 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']), @@ -114,10 +121,14 @@ SocketCategories.setWatchState = async function (socket, data) { }; 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); }; @@ -146,6 +157,8 @@ async function ignoreOrWatch(fn, socket, data) { } SocketCategories.isModerator = async function (socket, cid) { + sockets.warnDeprecated(socket); + return await user.isModerator(socket.uid, cid); }; From d7c6b3d60e684b6e06801e5b248d74cd7fab5e4e Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 23 Oct 2023 12:11:34 -0400 Subject: [PATCH 06/14] refactor(socket.io): deprecate categories.setWatchState in favour of api.categories.setWatchState --- public/src/client/account/categories.js | 36 +++++++++++-------------- public/src/client/category.js | 2 +- src/api/categories.js | 26 ++++++++++++++++++ src/controllers/write/categories.js | 19 +++++++++++++ src/middleware/assert.js | 9 +++++++ src/routes/write/categories.js | 3 +++ src/socket.io/categories.js | 10 ++++--- 7 files changed, 81 insertions(+), 24 deletions(-) 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 a89620cb34..3aff45420e 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -69,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); } diff --git a/src/api/categories.js b/src/api/categories.js index 28b2539a3a..442131b4fd 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -1,6 +1,7 @@ 'use strict'; const categories = require('../categories'); +const topics = require('../topics'); const events = require('../events'); const user = require('../user'); const groups = require('../groups'); @@ -84,6 +85,31 @@ categoriesAPI.getTopicCount = async (caller, { cid }) => { categoriesAPI.getPosts = async (caller, { cid }) => await categories.getRecentReplies(cid, caller.uid, 0, 4); +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/controllers/write/categories.js b/src/controllers/write/categories.js index 8d4c0aebcf..e987b433ce 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -1,6 +1,7 @@ 'use strict'; const categories = require('../../categories'); +const meta = require('../../meta'); const api = require('../../api'); const helpers = require('../helpers'); @@ -44,6 +45,24 @@ Categories.getPosts = async (req, res) => { helpers.formatApiResponse(200, res, posts); }; +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 { + 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/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 44a3020863..f1e2f39504 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -19,6 +19,9 @@ module.exports = function () { setupApiRoute(router, 'get', '/:cid/count', [...middlewares], controllers.write.categories.getTopicCount); setupApiRoute(router, 'get', '/:cid/posts', [...middlewares], controllers.write.categories.getPosts); + 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/socket.io/categories.js b/src/socket.io/categories.js index aea08cdfa2..e2b647f71a 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -112,12 +112,16 @@ 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) { From 010727f5cb8f9bc170bacd221d01111e19ec2a85 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 23 Oct 2023 15:08:52 -0400 Subject: [PATCH 07/14] refactor(socket.io): deprecate categories.loadMoreSubCategories in favour of api.categories.getChildren --- public/src/client/category.js | 34 ++++++++++++----------------- src/api/categories.js | 21 ++++++++++++++++++ src/controllers/write/categories.js | 6 +++++ src/routes/write/categories.js | 5 +++-- src/socket.io/categories.js | 16 +++++--------- 5 files changed, 49 insertions(+), 33 deletions(-) diff --git a/public/src/client/category.js b/public/src/client/category.js index 3aff45420e..b14a80d0c9 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -89,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; }); } diff --git a/src/api/categories.js b/src/api/categories.js index 442131b4fd..7cb7a17f69 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -85,6 +85,27 @@ categoriesAPI.getTopicCount = async (caller, { cid }) => { 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.setWatchState = async (caller, { cid, state, uid }) => { let targetUid = caller.uid; const cids = Array.isArray(cid) ? cid.map(cid => parseInt(cid, 10)) : [parseInt(cid, 10)]; diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index e987b433ce..e068f06305 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -45,6 +45,12 @@ Categories.getPosts = async (req, res) => { 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.setWatchState = async (req, res) => { const { cid } = req.params; let { uid, state } = req.body; diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index f1e2f39504..9121ee9a7f 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -16,8 +16,9 @@ module.exports = function () { setupApiRoute(router, 'put', '/:cid', [...middlewares], controllers.write.categories.update); setupApiRoute(router, 'delete', '/:cid', [...middlewares], controllers.write.categories.delete); - setupApiRoute(router, 'get', '/:cid/count', [...middlewares], controllers.write.categories.getTopicCount); - setupApiRoute(router, 'get', '/:cid/posts', [...middlewares], controllers.write.categories.getPosts); + 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, 'put', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState); setupApiRoute(router, 'delete', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index e2b647f71a..a62d55edfe 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -167,20 +167,14 @@ SocketCategories.isModerator = async function (socket, 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); From f279bca0382d1ee2c1189492b029fba4e4fba932 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 24 Oct 2023 13:54:19 -0400 Subject: [PATCH 08/14] docs(socket.io): added schema for new routes --- public/openapi/write.yaml | 4 +++ public/openapi/write/categories.yaml | 22 +++++++++++++++ .../openapi/write/categories/cid/count.yaml | 28 +++++++++++++++++++ .../openapi/write/categories/cid/posts.yaml | 28 +++++++++++++++++++ 4 files changed, 82 insertions(+) create mode 100644 public/openapi/write/categories/cid/count.yaml create mode 100644 public/openapi/write/categories/cid/posts.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index b1dabb778b..e4723397b4 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -112,6 +112,10 @@ 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}/privileges: $ref: 'write/categories/cid/privileges.yaml' /categories/{cid}/privileges/{privilege}: 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/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 From 54000aabf5d397cba9f5500e4869d637ae8e03a6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 24 Oct 2023 13:55:00 -0400 Subject: [PATCH 09/14] fix(socket.io): update getPosts controller to return object containing posts instead of straight array --- src/controllers/write/categories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index e068f06305..00b90d47dd 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -42,7 +42,7 @@ Categories.getTopicCount = async (req, res) => { Categories.getPosts = async (req, res) => { const posts = await api.categories.getPosts(req, { ...req.params }); - helpers.formatApiResponse(200, res, posts); + helpers.formatApiResponse(200, res, { posts }); }; Categories.getChildren = async (req, res) => { From 5399e86af125cd0d255c3eed39b3c1776428e142 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 24 Oct 2023 14:19:55 -0400 Subject: [PATCH 10/14] docs(socket.io): openapi schema for remaining added routes --- public/openapi/write.yaml | 4 + .../write/categories/cid/children.yaml | 36 +++++++ .../openapi/write/categories/cid/watch.yaml | 102 ++++++++++++++++++ src/controllers/write/categories.js | 1 + 4 files changed, 143 insertions(+) create mode 100644 public/openapi/write/categories/cid/children.yaml create mode 100644 public/openapi/write/categories/cid/watch.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index e4723397b4..f62d1faca0 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -116,6 +116,10 @@ paths: $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}/watch: + $ref: 'write/categories/cid/watch.yaml' /categories/{cid}/privileges: $ref: 'write/categories/cid/privileges.yaml' /categories/{cid}/privileges/{privilege}: 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/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/src/controllers/write/categories.js b/src/controllers/write/categories.js index 00b90d47dd..133ec2c055 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -61,6 +61,7 @@ Categories.setWatchState = async (req, res) => { } 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]]'); } From 1ce4ca54da1cc44885f5777fee26307ef1d4a592 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 26 Oct 2023 16:51:46 -0400 Subject: [PATCH 11/14] refactor(socket.io): deprecate categories.loadMore in favour of api.categories.getTopics --- public/openapi/write.yaml | 2 + .../openapi/write/categories/cid/topics.yaml | 75 +++++++++++++++++++ public/src/client/category.js | 2 +- public/src/client/infinitescroll.js | 6 +- src/api/categories.js | 42 +++++++++++ src/controllers/write/categories.js | 7 ++ src/routes/write/categories.js | 1 + src/socket.io/categories.js | 39 +--------- 8 files changed, 135 insertions(+), 39 deletions(-) create mode 100644 public/openapi/write/categories/cid/topics.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index f62d1faca0..63c52430b8 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -118,6 +118,8 @@ paths: $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: 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/src/client/category.js b/public/src/client/category.js index b14a80d0c9..e1ab97431f 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -123,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/src/api/categories.js b/src/api/categories.js index 7cb7a17f69..774091fd61 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -1,5 +1,6 @@ 'use strict'; +const meta = require('../meta'); const categories = require('../categories'); const topics = require('../topics'); const events = require('../events'); @@ -106,6 +107,47 @@ categoriesAPI.getChildren = async (caller, { cid, start }) => { 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)]; diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 133ec2c055..80ee961fbf 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -51,6 +51,13 @@ Categories.getChildren = async (req, res) => { 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; diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index 9121ee9a7f..ca149a54da 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -19,6 +19,7 @@ module.exports = function () { 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); diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index a62d55edfe..934defaeac 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -1,7 +1,6 @@ 'use strict'; const categories = require('../categories'); -const privileges = require('../privileges'); const user = require('../user'); const topics = require('../topics'); const api = require('../api'); @@ -38,47 +37,15 @@ 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; }; From 00de9d5b077812810563dd71ea93d1a871dca1f9 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 27 Oct 2023 15:19:49 -0400 Subject: [PATCH 12/14] refactor(socket.io): deprecate categories.categorySearch in favour of api.search.categories THIS IS WIP --- src/api/index.js | 1 + src/api/search.js | 104 +++++++++++++++++++++++++++++ src/controllers/write/index.js | 1 + src/controllers/write/search.js | 10 +++ src/routes/write/index.js | 1 + src/routes/write/search.js | 19 ++++++ src/socket.io/categories/search.js | 98 ++------------------------- 7 files changed, 141 insertions(+), 93 deletions(-) create mode 100644 src/api/search.js create mode 100644 src/controllers/write/search.js create mode 100644 src/routes/write/search.js 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..8dad8eb44e --- /dev/null +++ b/src/api/search.js @@ -0,0 +1,104 @@ +'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] + ); + + 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/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/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/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; - } }; From 581516c88ef30784a14849163ec000e768d6bcb2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 31 Oct 2023 11:08:59 -0400 Subject: [PATCH 13/14] fix: made parentCid optional in api.search.categories --- src/api/search.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/search.js b/src/api/search.js index 8dad8eb44e..18bd9fa160 100644 --- a/src/api/search.js +++ b/src/api/search.js @@ -20,6 +20,7 @@ searchApi.categories = async (caller, data) => { 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)); From 4ffe041732582d5124e8a6062b1505a080731253 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 31 Oct 2023 11:09:12 -0400 Subject: [PATCH 14/14] docs: openapi schema for api.search.categories --- public/openapi/write.yaml | 2 + public/openapi/write/search/categories.yaml | 96 +++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 public/openapi/write/search/categories.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 63c52430b8..1c4dfd44a4 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -208,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/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