mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-02-26 00:21:16 +01:00
Merge branch 'develop' of https://github.com/NodeBB/NodeBB into develop
This commit is contained in:
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 се присъедини към стаята <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 הצטרף לחדר <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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-ը միացել է սենյակին <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 се придружио соби <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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ı <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>加入了房间",
|
||||
|
||||
@@ -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 <span class=\"timeago\" title=\"%2\"></span>",
|
||||
|
||||
@@ -112,6 +112,16 @@ paths:
|
||||
$ref: 'write/categories.yaml'
|
||||
/categories/{cid}:
|
||||
$ref: 'write/categories/cid.yaml'
|
||||
/categories/{cid}/count:
|
||||
$ref: 'write/categories/cid/count.yaml'
|
||||
/categories/{cid}/posts:
|
||||
$ref: 'write/categories/cid/posts.yaml'
|
||||
/categories/{cid}/children:
|
||||
$ref: 'write/categories/cid/children.yaml'
|
||||
/categories/{cid}/topics:
|
||||
$ref: 'write/categories/cid/topics.yaml'
|
||||
/categories/{cid}/watch:
|
||||
$ref: 'write/categories/cid/watch.yaml'
|
||||
/categories/{cid}/privileges:
|
||||
$ref: 'write/categories/cid/privileges.yaml'
|
||||
/categories/{cid}/privileges/{privilege}:
|
||||
@@ -198,6 +208,8 @@ paths:
|
||||
$ref: 'write/flags/flagId/notes.yaml'
|
||||
/flags/{flagId}/notes/{datetime}:
|
||||
$ref: 'write/flags/flagId/notes/datetime.yaml'
|
||||
/search/categories:
|
||||
$ref: 'write/search/categories.yaml'
|
||||
/admin/settings/{setting}:
|
||||
$ref: 'write/admin/settings/setting.yaml'
|
||||
/admin/analytics:
|
||||
|
||||
@@ -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
|
||||
|
||||
36
public/openapi/write/categories/cid/children.yaml
Normal file
36
public/openapi/write/categories/cid/children.yaml
Normal file
@@ -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
|
||||
28
public/openapi/write/categories/cid/count.yaml
Normal file
28
public/openapi/write/categories/cid/count.yaml
Normal file
@@ -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
|
||||
28
public/openapi/write/categories/cid/posts.yaml
Normal file
28
public/openapi/write/categories/cid/posts.yaml
Normal file
@@ -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
|
||||
75
public/openapi/write/categories/cid/topics.yaml
Normal file
75
public/openapi/write/categories/cid/topics.yaml
Normal file
@@ -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
|
||||
102
public/openapi/write/categories/cid/watch.yaml
Normal file
102
public/openapi/write/categories/cid/watch.yaml
Normal file
@@ -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
|
||||
96
public/openapi/write/search/categories.yaml
Normal file
96
public/openapi/write/search/categories.yaml
Normal file
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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]]');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ define('forum/category', [
|
||||
'categorySelector',
|
||||
'hooks',
|
||||
'alerts',
|
||||
], function (infinitescroll, share, navigator, topicList, sort, categorySelector, hooks, alerts) {
|
||||
'api',
|
||||
], function (infinitescroll, share, navigator, topicList, sort, categorySelector, hooks, alerts, api) {
|
||||
const Category = {};
|
||||
|
||||
$(window).on('action:ajaxify.start', function (ev, data) {
|
||||
@@ -68,7 +69,7 @@ define('forum/category', [
|
||||
const $this = $(this);
|
||||
const state = $this.attr('data-state');
|
||||
|
||||
socket.emit('categories.setWatchState', { cid: cid, state: state }, function (err) {
|
||||
api.put(`/categories/${cid}/watch`, { state }, (err) => {
|
||||
if (err) {
|
||||
return alerts.error(err);
|
||||
}
|
||||
@@ -88,28 +89,22 @@ define('forum/category', [
|
||||
}
|
||||
|
||||
function handleLoadMoreSubcategories() {
|
||||
$('[component="category/load-more-subcategories"]').on('click', function () {
|
||||
$('[component="category/load-more-subcategories"]').on('click', async function () {
|
||||
const btn = $(this);
|
||||
socket.emit('categories.loadMoreSubCategories', {
|
||||
cid: ajaxify.data.cid,
|
||||
start: ajaxify.data.nextSubCategoryStart,
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
return alerts.error(err);
|
||||
}
|
||||
btn.toggleClass('hidden', !data.length || data.length < ajaxify.data.subCategoriesPerPage);
|
||||
if (!data.length) {
|
||||
return;
|
||||
}
|
||||
app.parseAndTranslate('category', 'children', { children: data }, function (html) {
|
||||
html.find('.timeago').timeago();
|
||||
$('[component="category/subcategory/container"]').append(html);
|
||||
ajaxify.data.nextSubCategoryStart += ajaxify.data.subCategoriesPerPage;
|
||||
ajaxify.data.subCategoriesLeft -= data.length;
|
||||
btn.toggleClass('hidden', ajaxify.data.subCategoriesLeft <= 0)
|
||||
.translateText('[[category:x-more-categories, ' + ajaxify.data.subCategoriesLeft + ']]');
|
||||
});
|
||||
const { categories: data } = await api.get(`/categories/${ajaxify.data.cid}/children?start=${ajaxify.data.nextSubCategoryStart}`);
|
||||
btn.toggleClass('hidden', !data.length || data.length < ajaxify.data.subCategoriesPerPage);
|
||||
if (!data.length) {
|
||||
return;
|
||||
}
|
||||
app.parseAndTranslate('category', 'children', { children: data }, function (html) {
|
||||
html.find('.timeago').timeago();
|
||||
$('[component="category/subcategory/container"]').append(html);
|
||||
ajaxify.data.nextSubCategoryStart += ajaxify.data.subCategoriesPerPage;
|
||||
ajaxify.data.subCategoriesLeft -= data.length;
|
||||
btn.toggleClass('hidden', ajaxify.data.subCategoriesLeft <= 0)
|
||||
.translateText('[[category:x-more-categories, ' + ajaxify.data.subCategoriesLeft + ']]');
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
@@ -118,14 +113,9 @@ define('forum/category', [
|
||||
navigator.scrollTop(0);
|
||||
};
|
||||
|
||||
Category.toBottom = function () {
|
||||
socket.emit('categories.getTopicCount', ajaxify.data.cid, function (err, count) {
|
||||
if (err) {
|
||||
return alerts.error(err);
|
||||
}
|
||||
|
||||
navigator.scrollBottom(count - 1);
|
||||
});
|
||||
Category.toBottom = async () => {
|
||||
const { count } = await api.get(`/categories/${ajaxify.data.category.cid}/count`);
|
||||
navigator.scrollBottom(count - 1);
|
||||
};
|
||||
|
||||
function loadTopicsAfter(after, direction, callback) {
|
||||
@@ -133,7 +123,7 @@ define('forum/category', [
|
||||
|
||||
hooks.fire('action:topics.loading');
|
||||
const params = utils.params();
|
||||
infinitescroll.loadMore('categories.loadMore', {
|
||||
infinitescroll.loadMore(`/categories/${ajaxify.data.cid}/topics`, {
|
||||
cid: ajaxify.data.cid,
|
||||
after: after,
|
||||
direction: direction,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const meta = require('../meta');
|
||||
const categories = require('../categories');
|
||||
const topics = require('../topics');
|
||||
const events = require('../events');
|
||||
const user = require('../user');
|
||||
const groups = require('../groups');
|
||||
@@ -15,6 +17,22 @@ const hasAdminPrivilege = async (uid, privilege = 'categories') => {
|
||||
}
|
||||
};
|
||||
|
||||
categoriesAPI.list = async (caller) => {
|
||||
async function getCategories() {
|
||||
const cids = await categories.getCidsByPrivilege('categories:cid', caller.uid, 'find');
|
||||
return await categories.getCategoriesData(cids);
|
||||
}
|
||||
|
||||
const [isAdmin, categoriesData] = await Promise.all([
|
||||
user.isAdministrator(caller.uid),
|
||||
getCategories(),
|
||||
]);
|
||||
|
||||
return {
|
||||
categories: categoriesData.filter(category => category && (!category.disabled || isAdmin)),
|
||||
};
|
||||
};
|
||||
|
||||
categoriesAPI.get = async function (caller, data) {
|
||||
const [userPrivileges, category] = await Promise.all([
|
||||
privileges.categories.get(data.cid, caller.uid),
|
||||
@@ -61,6 +79,100 @@ categoriesAPI.delete = async function (caller, { cid }) {
|
||||
});
|
||||
};
|
||||
|
||||
categoriesAPI.getTopicCount = async (caller, { cid }) => {
|
||||
const count = await categories.getCategoryField(cid, 'topic_count');
|
||||
return { count };
|
||||
};
|
||||
|
||||
categoriesAPI.getPosts = async (caller, { cid }) => await categories.getRecentReplies(cid, caller.uid, 0, 4);
|
||||
|
||||
categoriesAPI.getChildren = async (caller, { cid, start }) => {
|
||||
if (!start || start < 0) {
|
||||
start = 0;
|
||||
}
|
||||
start = parseInt(start, 10);
|
||||
|
||||
const allowed = await privileges.categories.can('read', cid, caller.uid);
|
||||
if (!allowed) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
const category = await categories.getCategoryData(cid);
|
||||
await categories.getChildrenTree(category, caller.uid);
|
||||
const allCategories = [];
|
||||
categories.flattenCategories(allCategories, category.children);
|
||||
await categories.getRecentTopicReplies(allCategories, caller.uid);
|
||||
|
||||
const payload = category.children.slice(start, start + category.subCategoriesPerPage);
|
||||
return { categories: payload };
|
||||
};
|
||||
|
||||
categoriesAPI.getTopics = async (caller, data) => {
|
||||
data.query = data.query || {};
|
||||
const [userPrivileges, settings, targetUid] = await Promise.all([
|
||||
privileges.categories.get(data.cid, caller.uid),
|
||||
user.getSettings(caller.uid),
|
||||
user.getUidByUserslug(data.query.author),
|
||||
]);
|
||||
|
||||
if (!userPrivileges.read) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
|
||||
const infScrollTopicsPerPage = 20;
|
||||
const sort = data.sort || data.categoryTopicSort || meta.config.categoryTopicSort || 'newest_to_oldest';
|
||||
|
||||
let start = Math.max(0, parseInt(data.after || 0, 10));
|
||||
|
||||
if (data.direction === -1) {
|
||||
start -= infScrollTopicsPerPage;
|
||||
}
|
||||
|
||||
let stop = start + infScrollTopicsPerPage - 1;
|
||||
|
||||
start = Math.max(0, start);
|
||||
stop = Math.max(0, stop);
|
||||
const result = await categories.getCategoryTopics({
|
||||
uid: caller.uid,
|
||||
cid: data.cid,
|
||||
start,
|
||||
stop,
|
||||
sort,
|
||||
settings,
|
||||
query: data.query,
|
||||
tag: data.query.tag,
|
||||
targetUid,
|
||||
});
|
||||
categories.modifyTopicsByPrivilege(result.topics, userPrivileges);
|
||||
|
||||
return { ...result, privileges: userPrivileges };
|
||||
};
|
||||
|
||||
categoriesAPI.setWatchState = async (caller, { cid, state, uid }) => {
|
||||
let targetUid = caller.uid;
|
||||
const cids = Array.isArray(cid) ? cid.map(cid => parseInt(cid, 10)) : [parseInt(cid, 10)];
|
||||
if (uid) {
|
||||
targetUid = uid;
|
||||
}
|
||||
await user.isAdminOrGlobalModOrSelf(caller.uid, targetUid);
|
||||
const allCids = await categories.getAllCidsFromSet('categories:cid');
|
||||
const categoryData = await categories.getCategoriesFields(allCids, ['cid', 'parentCid']);
|
||||
|
||||
// filter to subcategories of cid
|
||||
let cat;
|
||||
do {
|
||||
cat = categoryData.find(c => !cids.includes(c.cid) && cids.includes(c.parentCid));
|
||||
if (cat) {
|
||||
cids.push(cat.cid);
|
||||
}
|
||||
} while (cat);
|
||||
|
||||
await user.setCategoryWatchState(targetUid, cids, state);
|
||||
await topics.pushUnreadCount(targetUid);
|
||||
|
||||
return { cids };
|
||||
};
|
||||
|
||||
categoriesAPI.getPrivileges = async (caller, { cid }) => {
|
||||
await hasAdminPrivilege(caller.uid, 'privileges');
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
105
src/api/search.js
Normal file
105
src/api/search.js
Normal file
@@ -0,0 +1,105 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const categories = require('../categories');
|
||||
const privileges = require('../privileges');
|
||||
const meta = require('../meta');
|
||||
const plugins = require('../plugins');
|
||||
|
||||
const controllersHelpers = require('../controllers/helpers');
|
||||
|
||||
const searchApi = module.exports;
|
||||
|
||||
searchApi.categories = async (caller, data) => {
|
||||
// used by categorySearch module
|
||||
|
||||
let cids = [];
|
||||
let matchedCids = [];
|
||||
const privilege = data.privilege || 'topics:read';
|
||||
data.states = (data.states || ['watching', 'notwatching', 'ignoring']).map(
|
||||
state => categories.watchStates[state]
|
||||
);
|
||||
data.parentCid = parseInt(data.parentCid || 0, 10);
|
||||
|
||||
if (data.search) {
|
||||
({ cids, matchedCids } = await findMatchedCids(caller.uid, data));
|
||||
} else {
|
||||
cids = await loadCids(caller.uid, data.parentCid);
|
||||
}
|
||||
|
||||
const visibleCategories = await controllersHelpers.getVisibleCategories({
|
||||
cids, uid: caller.uid, states: data.states, privilege, showLinks: data.showLinks, parentCid: data.parentCid,
|
||||
});
|
||||
|
||||
if (Array.isArray(data.selectedCids)) {
|
||||
data.selectedCids = data.selectedCids.map(cid => parseInt(cid, 10));
|
||||
}
|
||||
|
||||
let categoriesData = categories.buildForSelectCategories(visibleCategories, ['disabledClass'], data.parentCid);
|
||||
categoriesData = categoriesData.slice(0, 200);
|
||||
|
||||
categoriesData.forEach((category) => {
|
||||
category.selected = data.selectedCids ? data.selectedCids.includes(category.cid) : false;
|
||||
if (matchedCids.includes(category.cid)) {
|
||||
category.match = true;
|
||||
}
|
||||
});
|
||||
const result = await plugins.hooks.fire('filter:categories.categorySearch', {
|
||||
categories: categoriesData,
|
||||
...data,
|
||||
uid: caller.uid,
|
||||
});
|
||||
|
||||
return { categories: result.categories };
|
||||
};
|
||||
|
||||
async function findMatchedCids(uid, data) {
|
||||
const result = await categories.search({
|
||||
uid: uid,
|
||||
query: data.search,
|
||||
qs: data.query,
|
||||
paginate: false,
|
||||
});
|
||||
|
||||
let matchedCids = result.categories.map(c => c.cid);
|
||||
// no need to filter if all 3 states are used
|
||||
const filterByWatchState = !Object.values(categories.watchStates)
|
||||
.every(state => data.states.includes(state));
|
||||
|
||||
if (filterByWatchState) {
|
||||
const states = await categories.getWatchState(matchedCids, uid);
|
||||
matchedCids = matchedCids.filter((cid, index) => data.states.includes(states[index]));
|
||||
}
|
||||
|
||||
const rootCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getParentCids))));
|
||||
const allChildCids = _.uniq(_.flatten(await Promise.all(matchedCids.map(categories.getChildrenCids))));
|
||||
|
||||
return {
|
||||
cids: _.uniq(rootCids.concat(allChildCids).concat(matchedCids)),
|
||||
matchedCids: matchedCids,
|
||||
};
|
||||
}
|
||||
|
||||
async function loadCids(uid, parentCid) {
|
||||
let resultCids = [];
|
||||
async function getCidsRecursive(cids) {
|
||||
const categoryData = await categories.getCategoriesFields(cids, ['subCategoriesPerPage']);
|
||||
const cidToData = _.zipObject(cids, categoryData);
|
||||
await Promise.all(cids.map(async (cid) => {
|
||||
const allChildCids = await categories.getAllCidsFromSet(`cid:${cid}:children`);
|
||||
if (allChildCids.length) {
|
||||
const childCids = await privileges.categories.filterCids('find', allChildCids, uid);
|
||||
resultCids.push(...childCids.slice(0, cidToData[cid].subCategoriesPerPage));
|
||||
await getCidsRecursive(childCids);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const allRootCids = await categories.getAllCidsFromSet(`cid:${parentCid}:children`);
|
||||
const rootCids = await privileges.categories.filterCids('find', allRootCids, uid);
|
||||
const pageCids = rootCids.slice(0, meta.config.categoriesPerPage);
|
||||
resultCids = pageCids;
|
||||
await getCidsRecursive(pageCids);
|
||||
return resultCids;
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
'use strict';
|
||||
|
||||
const categories = require('../../categories');
|
||||
const meta = require('../../meta');
|
||||
const api = require('../../api');
|
||||
|
||||
const helpers = require('../helpers');
|
||||
|
||||
const Categories = module.exports;
|
||||
|
||||
Categories.list = async (req, res) => {
|
||||
helpers.formatApiResponse(200, res, await api.categories.list(req));
|
||||
};
|
||||
|
||||
Categories.get = async (req, res) => {
|
||||
helpers.formatApiResponse(200, res, await api.categories.get(req, req.params));
|
||||
};
|
||||
@@ -31,6 +36,47 @@ Categories.delete = async (req, res) => {
|
||||
helpers.formatApiResponse(200, res);
|
||||
};
|
||||
|
||||
Categories.getTopicCount = async (req, res) => {
|
||||
helpers.formatApiResponse(200, res, await api.categories.getTopicCount(req, { ...req.params }));
|
||||
};
|
||||
|
||||
Categories.getPosts = async (req, res) => {
|
||||
const posts = await api.categories.getPosts(req, { ...req.params });
|
||||
helpers.formatApiResponse(200, res, { posts });
|
||||
};
|
||||
|
||||
Categories.getChildren = async (req, res) => {
|
||||
const { cid } = req.params;
|
||||
const { start } = req.query;
|
||||
helpers.formatApiResponse(200, res, await api.categories.getChildren(req, { cid, start }));
|
||||
};
|
||||
|
||||
Categories.getTopics = async (req, res) => {
|
||||
const { cid } = req.params;
|
||||
const result = await api.categories.getTopics(req, { ...req.query, cid });
|
||||
|
||||
helpers.formatApiResponse(200, res, result);
|
||||
};
|
||||
|
||||
Categories.setWatchState = async (req, res) => {
|
||||
const { cid } = req.params;
|
||||
let { uid, state } = req.body;
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
// DELETE is always setting state to system default in acp
|
||||
state = categories.watchStates[meta.config.categoryWatchState];
|
||||
} else if (Object.keys(categories.watchStates).includes(state)) {
|
||||
state = categories.watchStates[state]; // convert to integer for backend processing
|
||||
} else {
|
||||
console.log('throwing', cid, uid, state);
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const { cids: modified } = await api.categories.setWatchState(req, { cid, state, uid });
|
||||
|
||||
helpers.formatApiResponse(200, res, { modified });
|
||||
};
|
||||
|
||||
Categories.getPrivileges = async (req, res) => {
|
||||
const privilegeSet = await api.categories.getPrivileges(req, { cid: req.params.cid });
|
||||
helpers.formatApiResponse(200, res, privilegeSet);
|
||||
|
||||
@@ -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');
|
||||
|
||||
10
src/controllers/write/search.js
Normal file
10
src/controllers/write/search.js
Normal file
@@ -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));
|
||||
};
|
||||
@@ -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]]'));
|
||||
|
||||
@@ -10,11 +10,20 @@ const { setupApiRoute } = routeHelpers;
|
||||
module.exports = function () {
|
||||
const middlewares = [middleware.ensureLoggedIn];
|
||||
|
||||
setupApiRoute(router, 'get', '/', [...middlewares], controllers.write.categories.list);
|
||||
setupApiRoute(router, 'post', '/', [...middlewares, middleware.checkRequired.bind(null, ['name'])], controllers.write.categories.create);
|
||||
setupApiRoute(router, 'get', '/:cid', [], controllers.write.categories.get);
|
||||
setupApiRoute(router, 'put', '/:cid', [...middlewares], controllers.write.categories.update);
|
||||
setupApiRoute(router, 'delete', '/:cid', [...middlewares], controllers.write.categories.delete);
|
||||
|
||||
setupApiRoute(router, 'get', '/:cid/count', [...middlewares, middleware.assert.category], controllers.write.categories.getTopicCount);
|
||||
setupApiRoute(router, 'get', '/:cid/posts', [...middlewares, middleware.assert.category], controllers.write.categories.getPosts);
|
||||
setupApiRoute(router, 'get', '/:cid/children', [...middlewares, middleware.assert.category], controllers.write.categories.getChildren);
|
||||
setupApiRoute(router, 'get', '/:cid/topics', [...middlewares, middleware.assert.category], controllers.write.categories.getTopics);
|
||||
|
||||
setupApiRoute(router, 'put', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState);
|
||||
setupApiRoute(router, 'delete', '/:cid/watch', [...middlewares, middleware.assert.category], controllers.write.categories.setWatchState);
|
||||
|
||||
setupApiRoute(router, 'get', '/:cid/privileges', [...middlewares], controllers.write.categories.getPrivileges);
|
||||
setupApiRoute(router, 'put', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege);
|
||||
setupApiRoute(router, 'delete', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege);
|
||||
|
||||
@@ -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')());
|
||||
|
||||
19
src/routes/write/search.js
Normal file
19
src/routes/write/search.js
Normal file
@@ -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;
|
||||
};
|
||||
@@ -1,31 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const categories = require('../categories');
|
||||
const privileges = require('../privileges');
|
||||
const user = require('../user');
|
||||
const topics = require('../topics');
|
||||
const api = require('../api');
|
||||
|
||||
const sockets = require('.');
|
||||
|
||||
const SocketCategories = module.exports;
|
||||
|
||||
require('./categories/search')(SocketCategories);
|
||||
|
||||
SocketCategories.getRecentReplies = async function (socket, cid) {
|
||||
return await categories.getRecentReplies(cid, socket.uid, 0, 4);
|
||||
sockets.warnDeprecated(socket, 'GET /api/v3/categories/:cid/posts');
|
||||
return await api.categories.getPosts(socket, { cid });
|
||||
};
|
||||
|
||||
SocketCategories.get = async function (socket) {
|
||||
async function getCategories() {
|
||||
const cids = await categories.getCidsByPrivilege('categories:cid', socket.uid, 'find');
|
||||
return await categories.getCategoriesData(cids);
|
||||
}
|
||||
const [isAdmin, categoriesData] = await Promise.all([
|
||||
user.isAdministrator(socket.uid),
|
||||
getCategories(),
|
||||
]);
|
||||
return categoriesData.filter(category => category && (!category.disabled || isAdmin));
|
||||
sockets.warnDeprecated(socket, 'GET /api/v3/categories');
|
||||
const { categories } = await api.categories.list(socket);
|
||||
return categories;
|
||||
};
|
||||
|
||||
SocketCategories.getWatchedCategories = async function (socket) {
|
||||
sockets.warnDeprecated(socket);
|
||||
|
||||
const [categoriesData, ignoredCids] = await Promise.all([
|
||||
categories.getCategoriesByPrivilege('cid:0:children', socket.uid, 'find'),
|
||||
user.getIgnoredCategories(socket.uid),
|
||||
@@ -38,63 +37,40 @@ SocketCategories.loadMore = async function (socket, data) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
data.query = data.query || {};
|
||||
const [userPrivileges, settings, targetUid] = await Promise.all([
|
||||
privileges.categories.get(data.cid, socket.uid),
|
||||
user.getSettings(socket.uid),
|
||||
user.getUidByUserslug(data.query.author),
|
||||
]);
|
||||
|
||||
if (!userPrivileges.read) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
const result = await api.categories.getTopics(socket, data);
|
||||
|
||||
const infScrollTopicsPerPage = 20;
|
||||
const sort = data.sort || data.categoryTopicSort;
|
||||
|
||||
let start = Math.max(0, parseInt(data.after, 10));
|
||||
|
||||
if (data.direction === -1) {
|
||||
start -= infScrollTopicsPerPage;
|
||||
}
|
||||
|
||||
let stop = start + infScrollTopicsPerPage - 1;
|
||||
|
||||
start = Math.max(0, start);
|
||||
stop = Math.max(0, stop);
|
||||
const result = await categories.getCategoryTopics({
|
||||
uid: socket.uid,
|
||||
cid: data.cid,
|
||||
start: start,
|
||||
stop: stop,
|
||||
sort: sort,
|
||||
settings: settings,
|
||||
query: data.query,
|
||||
tag: data.query.tag,
|
||||
targetUid: targetUid,
|
||||
});
|
||||
categories.modifyTopicsByPrivilege(result.topics, userPrivileges);
|
||||
|
||||
result.privileges = userPrivileges;
|
||||
// Backwards compatibility — unsure of current usage.
|
||||
result.template = {
|
||||
category: true,
|
||||
name: 'category',
|
||||
};
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
SocketCategories.getTopicCount = async function (socket, cid) {
|
||||
return await categories.getCategoryField(cid, 'topic_count');
|
||||
sockets.warnDeprecated(socket, 'GET /api/v3/categories/:cid');
|
||||
|
||||
const { count } = await api.categories.getTopicCount(socket, { cid });
|
||||
return count;
|
||||
};
|
||||
|
||||
SocketCategories.getCategoriesByPrivilege = async function (socket, privilege) {
|
||||
sockets.warnDeprecated(socket);
|
||||
|
||||
return await categories.getCategoriesByPrivilege('categories:cid', socket.uid, privilege);
|
||||
};
|
||||
|
||||
SocketCategories.getMoveCategories = async function (socket, data) {
|
||||
sockets.warnDeprecated(socket);
|
||||
|
||||
return await SocketCategories.getSelectCategories(socket, data);
|
||||
};
|
||||
|
||||
SocketCategories.getSelectCategories = async function (socket) {
|
||||
sockets.warnDeprecated(socket);
|
||||
|
||||
const [isAdmin, categoriesData] = await Promise.all([
|
||||
user.isAdministrator(socket.uid),
|
||||
categories.buildForSelect(socket.uid, 'find', ['disabled', 'link']),
|
||||
@@ -103,19 +79,27 @@ SocketCategories.getSelectCategories = async function (socket) {
|
||||
};
|
||||
|
||||
SocketCategories.setWatchState = async function (socket, data) {
|
||||
sockets.warnDeprecated(socket, 'PUT/DELETE /api/v3/categories/:cid/watch');
|
||||
|
||||
if (!data || !data.cid || !data.state) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
return await ignoreOrWatch(async (uid, cids) => {
|
||||
await user.setCategoryWatchState(uid, cids, categories.watchStates[data.state]);
|
||||
}, socket, data);
|
||||
|
||||
data.state = categories.watchStates[data.state];
|
||||
|
||||
await api.categories.setWatchState(socket, data);
|
||||
return data.cid;
|
||||
};
|
||||
|
||||
SocketCategories.watch = async function (socket, data) {
|
||||
sockets.warnDeprecated(socket);
|
||||
|
||||
return await ignoreOrWatch(user.watchCategory, socket, data);
|
||||
};
|
||||
|
||||
SocketCategories.ignore = async function (socket, data) {
|
||||
sockets.warnDeprecated(socket);
|
||||
|
||||
return await ignoreOrWatch(user.ignoreCategory, socket, data);
|
||||
};
|
||||
|
||||
@@ -144,24 +128,20 @@ async function ignoreOrWatch(fn, socket, data) {
|
||||
}
|
||||
|
||||
SocketCategories.isModerator = async function (socket, cid) {
|
||||
sockets.warnDeprecated(socket);
|
||||
|
||||
return await user.isModerator(socket.uid, cid);
|
||||
};
|
||||
|
||||
SocketCategories.loadMoreSubCategories = async function (socket, data) {
|
||||
sockets.warnDeprecated(socket, `GET /api/v3/categories/:cid/children`);
|
||||
|
||||
if (!data || !data.cid || !(parseInt(data.start, 10) >= 0)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
const allowed = await privileges.categories.can('read', data.cid, socket.uid);
|
||||
if (!allowed) {
|
||||
throw new Error('[[error:no-privileges]]');
|
||||
}
|
||||
const category = await categories.getCategoryData(data.cid);
|
||||
await categories.getChildrenTree(category, socket.uid);
|
||||
const allCategories = [];
|
||||
categories.flattenCategories(allCategories, category.children);
|
||||
await categories.getRecentTopicReplies(allCategories, socket.uid);
|
||||
const start = parseInt(data.start, 10);
|
||||
return category.children.slice(start, start + category.subCategoriesPerPage);
|
||||
|
||||
const { categories: children } = await api.categories.getChildren(socket, data);
|
||||
return children;
|
||||
};
|
||||
|
||||
require('../promisify')(SocketCategories);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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'));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user