diff --git a/install/package.json b/install/package.json index c51992816c..e8fa9735fc 100644 --- a/install/package.json +++ b/install/package.json @@ -59,7 +59,7 @@ "morgan": "^1.9.0", "mousetrap": "^1.6.1", "nconf": "^0.9.1", - "nodebb-plugin-composer-default": "6.0.7", + "nodebb-plugin-composer-default": "6.0.8", "nodebb-plugin-dbsearch": "2.0.9", "nodebb-plugin-emoji": "2.0.9", "nodebb-plugin-emoji-android": "2.0.0", @@ -69,7 +69,7 @@ "nodebb-plugin-spam-be-gone": "0.5.1", "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "5.0.0", - "nodebb-theme-persona": "7.2.8", + "nodebb-theme-persona": "7.2.9", "nodebb-theme-slick": "1.1.2", "nodebb-theme-vanilla": "8.1.4", "nodebb-widget-essentials": "4.0.1", diff --git a/public/language/cs/admin/appearance/customise.json b/public/language/cs/admin/appearance/customise.json index c22a869f80..0cd4e0f8da 100644 --- a/public/language/cs/admin/appearance/customise.json +++ b/public/language/cs/admin/appearance/customise.json @@ -3,12 +3,12 @@ "custom-css.description": "Zadejte vlastní deklarace CSS, které budou použity na všechny ostatních styly.", "custom-css.enable": "Povolit uživatelské CSS", - "custom-js": "Custom Javascript", - "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", - "custom-js.enable": "Enable Custom Javascript", + "custom-js": "Uživatelský Javascript", + "custom-js.description": "Zadejte zde váš javascriptový kód. Bude spuštěn, jakmile se stránka plně načte.", + "custom-js.enable": "Povolit uživatelský Javascript", "custom-header": "Uživatelská hlavička", - "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", + "custom-header.description": "Zde zadejte uživatelské HTML (mimo Meta Tags, atd.), které bude připojeno k části značek <head> vašeho fóra.. Značky pro „script” jsou povoleny, ale nedoporučujeme je, neboť je dostupný Uživatelský Javascript .", "custom-header.enable": "Povolit uživatelskou hlavičku", "custom-css.livereload": "Povolit aktuální znovu načtení", diff --git a/public/language/cs/admin/manage/post-queue.json b/public/language/cs/admin/manage/post-queue.json index 57fe1f8ab7..e46490b1b7 100644 --- a/public/language/cs/admin/manage/post-queue.json +++ b/public/language/cs/admin/manage/post-queue.json @@ -7,5 +7,5 @@ "content": "Obsah", "posted": "Přidáno", "reply-to": "Odpovědět na \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Kvůli úpravám a před odesláním příspěvku můžete klikat na obsah." } \ No newline at end of file diff --git a/public/language/cs/admin/menu.json b/public/language/cs/admin/menu.json index 8b9812e514..7de94b7a5c 100644 --- a/public/language/cs/admin/menu.json +++ b/public/language/cs/admin/menu.json @@ -39,7 +39,7 @@ "section-appearance": "Vzhled", "appearance/themes": "Motivy", "appearance/skins": "Vzhledy", - "appearance/customise": "Custom Content (HTML/JS/CSS)", + "appearance/customise": "Uživatelský obsah (HTML/JS/CSS)", "section-extend": "Rozšířit", "extend/plugins": "Rozšíření", @@ -65,7 +65,7 @@ "logout": "Odhlásit", "view-forum": "Zobrazit fórum", - "search.placeholder": "Search for settings", + "search.placeholder": "Hledat nastavení", "search.no-results": "Žádné výsledky…", "search.search-forum": "Prohledat fórum pro ", "search.keep-typing": "Pište dále pro zobrazení výsledků…", diff --git a/public/language/cs/admin/settings/notifications.json b/public/language/cs/admin/settings/notifications.json index fd95917606..39bc83bdcb 100644 --- a/public/language/cs/admin/settings/notifications.json +++ b/public/language/cs/admin/settings/notifications.json @@ -2,5 +2,5 @@ "notifications": "Oznámení", "welcome-notification": "Uvítání", "welcome-notification-link": "Odkaz na uvítání", - "welcome-notification-uid": "Welcome Notification User (UID)" + "welcome-notification-uid": "Uvítání uživatele (UID)" } \ No newline at end of file diff --git a/public/language/cs/admin/settings/pagination.json b/public/language/cs/admin/settings/pagination.json index dc1faf68da..34052b2a9c 100644 --- a/public/language/cs/admin/settings/pagination.json +++ b/public/language/cs/admin/settings/pagination.json @@ -3,9 +3,9 @@ "enable": "Stránkovat témata a příspěvky namísto nekonečného posouvání", "topics": "Stránkování témat", "posts-per-page": "Příspěvků na stránku", - "max-posts-per-page": "Maximum posts per page", + "max-posts-per-page": "Maximální množství příspěvků na stránku", "categories": "Stránkování kategorii", "topics-per-page": "Témat na stránku", - "max-topics-per-page": "Maximum topics per page", + "max-topics-per-page": "Maximální množství témat na stránku", "initial-num-load": "Počáteční počet témat pro načtení u nepřečtených, posledních a polulárních" } \ No newline at end of file diff --git a/public/language/cs/admin/settings/post.json b/public/language/cs/admin/settings/post.json index 67d297b33b..de8832b501 100644 --- a/public/language/cs/admin/settings/post.json +++ b/public/language/cs/admin/settings/post.json @@ -3,8 +3,8 @@ "sorting.post-default": "Výchozí třídění příspěvků", "sorting.oldest-to-newest": "Od nejstarších po nejnovější", "sorting.newest-to-oldest": "Od nejnovějších po nejstarší", - "sorting.most-votes": "Dle hlasování", - "sorting.most-posts": "Most Posts", + "sorting.most-votes": "Dle počtu hlasů", + "sorting.most-posts": "Dle počtu příspěvků", "sorting.topic-default": "Výchozí třídění tématu", "restrictions": "Omezení příspěvků", "restrictions.post-queue": "Povolit frontu pro příspěvky", diff --git a/public/language/cs/admin/settings/user.json b/public/language/cs/admin/settings/user.json index 2ad41f23cf..e683492860 100644 --- a/public/language/cs/admin/settings/user.json +++ b/public/language/cs/admin/settings/user.json @@ -19,8 +19,8 @@ "themes": "Motivy", "disable-user-skins": "Zabránit uživateli ve výběru vlastního vzhledu", "account-protection": "Ochrana účtu", - "admin-relogin-duration": "Admin relogin duration (minutes)", - "admin-relogin-duration-help": "After a set amount of time accessing the admin section will require re-login, set to 0 to disable", + "admin-relogin-duration": "Doba pro opětovné přihlášení správce (minuty)", + "admin-relogin-duration-help": "Po nastavení počtu přístupu do správcovské části, bude vyžadováno opětovné přihlášení. Pro zakázání, nastavte na 0.", "login-attempts": "Počet pokusů o přihlášení za hodinu", "login-attempts-help": "Překročí-li pokusy o přihlášení uživatele/ů tuto hranici, účet bude uzamknut na určený čas", "lockout-duration": "Délka blokování účtu (v minutách)", diff --git a/public/language/cs/email.json b/public/language/cs/email.json index 1d1efa10e9..b330291160 100644 --- a/public/language/cs/email.json +++ b/public/language/cs/email.json @@ -30,7 +30,7 @@ "notif.chat.unsub.info": "Toto upozornění na chat vám bylo odesláno na základě vašeho nastavení odběru.", "notif.post.cta": "Klikněte zde pro přečtené celého tématu", "notif.post.unsub.info": "Toto upozornění na příspěvek vám bylo odesláno na základě vašeho nastavení odběru.", - "notif.cta": "Click here to go to forum", + "notif.cta": "Pro přejití na fórum, klikněte zde", "test.text1": "Tento testovací e-mail slouží k ověření, že je e-mailer správně nastaven pro práci s NodeBB.", "unsub.cta": "Chcete-li změnit tyto nastavení, klikněte zde.", "banned.subject": "Byl jste zablokován od %1", diff --git a/public/language/cs/error.json b/public/language/cs/error.json index 43ea641999..18576ee9bc 100644 --- a/public/language/cs/error.json +++ b/public/language/cs/error.json @@ -11,7 +11,7 @@ "invalid-uid": "Neplatné ID uživatele", "invalid-username": "Neplatné uživatelské jméno", "invalid-email": "Neplatný e-mail", - "invalid-title": "Invalid title", + "invalid-title": "Neplatný název", "invalid-user-data": "Neplatná uživatelská data", "invalid-password": "Neplatné heslo", "invalid-login-credentials": "Neplatné přihlašovací údaje", @@ -81,7 +81,7 @@ "cant-ban-other-admins": "Nemůžete zablokovat jiné správce.", "cant-remove-last-admin": "Jste jediným správcem. Před vlastním odebráním oprávnění správce nejdříve přidejte jiného uživatele jako správce", "cant-delete-admin": "Před odstraněním účtu mu nejprve odeberte oprávnění správce.", - "invalid-image": "Invalid image", + "invalid-image": "Neplatný obrázek", "invalid-image-type": "Neplatný typ obrázku. Povolené typy jsou: %1", "invalid-image-extension": "Neplatná přípona obrázku", "invalid-file-type": "Neplatný typ souboru. Povolené typy jsou: %1", @@ -119,13 +119,13 @@ "not-enough-reputation-to-downvote": "Nemáte dostatečnou reputaci pro vyjádření nesouhlasu u tohoto příspěvku", "not-enough-reputation-to-flag": "Pro označení tohoto příspěvku nemáte dostatečnou reputaci", "already-flagged": "Tento příspěvek jste již označil", - "self-vote": "You cannot vote on your own post", + "self-vote": "U svého vlastního příspěvku nemůžete hlasovat", "reload-failed": "Vyskytla se chyba v NodeBB při znovu načtení: \"%1\". NodeBB bude pokračovat v běhu na straně klienta, nicméně byste měl/a přenastavit zpět to, co jste udělal/a před opětovným načtením.", "registration-error": "Chyba při registraci", "parse-error": "Při analýze odpovědi serveru nastala chyba", "wrong-login-type-email": "Pro přihlášení použijte vaši e-mailovou adresu", "wrong-login-type-username": "Pro přihlášení použijte vaše uživatelské jméno", - "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-registration-disabled": "Registrace byla zakázána pro účty - %1. Nejprve si zaregistrujte e-mailovou adresu", "invite-maximum-met": "Již jste pozval/a maximálně možný počet lidí (%1 z %2).", "no-session-found": "Nebyla nalezena relace s přihlášením.", "not-in-room": "Uživatel není přítomen v místnosti", @@ -135,5 +135,5 @@ "invalid-home-page-route": "Neplatná cesta k domovské stránkce", "invalid-session": "Nesoulad v relacích", "invalid-session-text": "Zdá se, že vše relace s přihlášením již není aktivní nebo již neodpovídá s relací na serveru. Obnovte prosím tuto stránku.", - "no-topics-selected": "No topics selected!" + "no-topics-selected": "Žádná vybraná témata." } \ No newline at end of file diff --git a/public/language/cs/flags.json b/public/language/cs/flags.json index e221f6965b..eb37572f8a 100644 --- a/public/language/cs/flags.json +++ b/public/language/cs/flags.json @@ -54,11 +54,11 @@ "modal-body": "Zadejte váš důvod k označení %1 %2 pro kontrolu. Nebo použijte tlačítko je-li dostupné.", "modal-reason-spam": "Spam", "modal-reason-offensive": "Urážlivé", - "modal-reason-other": "Other (specify below)", + "modal-reason-other": "Jiné (popište níže)", "modal-reason-custom": "Důvod ohlášení tohoto obsahu…", "modal-submit": "Předat hlášení", "modal-submit-success": "Obsah byl označen pro moderaci.", - "modal-submit-confirm": "Confirm Submission", - "modal-submit-confirm-text": "You have a custom reason specified already. Are you sure you wish to submit via quick-report?", - "modal-submit-confirm-text-help": "Submitting a quick report will overwrite any custom reasons defined." + "modal-submit-confirm": "Potvrdit hlášení", + "modal-submit-confirm-text": "Již jste zadal/a nějaký důvod. Jste si jist/a, že chcete nahlásit pomocí rychlé zprávy?", + "modal-submit-confirm-text-help": "Zaslání rychlé zprávy přepíše jiné zadané důvody." } \ No newline at end of file diff --git a/public/language/cs/notifications.json b/public/language/cs/notifications.json index 8182ae3f47..284b394c6f 100644 --- a/public/language/cs/notifications.json +++ b/public/language/cs/notifications.json @@ -8,8 +8,8 @@ "outgoing_link_message": "Opouštíte %1", "continue_to": "Pokračovat na %1", "return_to": "Vrátit se na %1", - "new_notification": "Nové upozornění", - "new_notification_from": "You have a new Notification from %1", + "new_notification": "Nové oznámení", + "new_notification_from": "Máte nové upozornění od %1", "you_have_unread_notifications": "Máte nepřečtená upozornění.", "all": "Vše", "topics": "Témata", @@ -47,18 +47,18 @@ "email-confirmed-message": "Děkujeme za ověření vaší e-mailové adresy. Váš účet je nyní aktivní.", "email-confirm-error-message": "Nastal problém s ověřením vaší e-mailové adresy. Kód je pravděpodobně neplatný nebo jeho platnost vypršela.", "email-confirm-sent": "Ověřovací e-mail odeslán.", - "none": "None", - "notification_only": "Notification Only", - "email_only": "Email Only", - "notification_and_email": "Notification & Email", - "notificationType_upvote": "When someone upvotes your post", - "notificationType_new-topic": "When someone you follow posts a topic", - "notificationType_new-reply": "When a new reply is posted in a topic you are watching", - "notificationType_follow": "When someone starts following you", - "notificationType_new-chat": "When you receive a chat message", - "notificationType_group-invite": "When you receive a group invite", - "notificationType_new-register": "When someone gets added to registration queue", - "notificationType_post-queue": "When a new post is queued", - "notificationType_new-post-flag": "When a post is flagged", - "notificationType_new-user-flag": "When a user is flagged" + "none": "Nic", + "notification_only": "Jen oznámení", + "email_only": "Jen e-mail", + "notification_and_email": "Oznámení a e-mail", + "notificationType_upvote": "Vyjádří-li někdo souhlas s vaším příspěvkem", + "notificationType_new-topic": "Začne-li někdo sledovat příspěvky a téma", + "notificationType_new-reply": "Bude-li přidán nový příspěvek v tématu, které sledujete", + "notificationType_follow": "Začne-li vás někdo sledovat", + "notificationType_new-chat": "Obdržíte-li novou konverzační zprávu", + "notificationType_group-invite": "Obdržíte-li pozvání ke skupině", + "notificationType_new-register": "Bude-li někdo přidán do registrační fronty", + "notificationType_post-queue": "Bude-li přidán nový příspěvek do fronty", + "notificationType_new-post-flag": "Bude-li příspěvek označen", + "notificationType_new-user-flag": "Bude-li uživatel označen" } \ No newline at end of file diff --git a/public/language/cs/pages.json b/public/language/cs/pages.json index 8a0270a9db..befc7ceba3 100644 --- a/public/language/cs/pages.json +++ b/public/language/cs/pages.json @@ -44,7 +44,7 @@ "account/bookmarks": "%1's zazáložkované příspěvky", "account/settings": "Uživatelské nastavení", "account/watched": "Témata sledovaná uživatelem %1", - "account/ignored": "Topics ignored by %1", + "account/ignored": "Témata ignorovaná uživatelem %1", "account/upvoted": "Souhlasí s příspěvkem %1", "account/downvoted": "Nesouhlasí s příspěvkem %1", "account/best": "Nejlepší příspěvky od %1", diff --git a/public/language/cs/topic.json b/public/language/cs/topic.json index 7cedca5e24..80e7561e26 100644 --- a/public/language/cs/topic.json +++ b/public/language/cs/topic.json @@ -68,8 +68,8 @@ "thread_tools.restore_confirm": "Jste si jist/a, že chcete toto téma obnovit?", "thread_tools.purge": "Vyčistit téma", "thread_tools.purge_confirm": "Jste si jist/a, že chcete vyčistit toto téma?", - "thread_tools.merge_topics": "Merge Topics", - "thread_tools.merge": "Merge", + "thread_tools.merge_topics": "Sloučit témata", + "thread_tools.merge": "Sloučit", "topic_move_success": "Toto téma bylo úspěšně přesunuto do %1", "post_delete_confirm": "Jste si jist/a, že chcete odstranit tento příspěvek?", "post_restore_confirm": "Jste si jist/a, že chcete obnovit tento příspěvek?", @@ -91,7 +91,7 @@ "fork_pid_count": "Vybráno %1 příspěvek/ů", "fork_success": "Téma úspěšně rozděleno. Pro přejití na rozdělené téma, zde klikněte.", "delete_posts_instruction": "Klikněte na příspěvek, který chcete odstranit/vyčistit", - "merge_topics_instruction": "Click the topics you want to merge", + "merge_topics_instruction": "Pro sloučení témat, klikněte na ně", "composer.title_placeholder": "Zadejte název tématu…", "composer.handle_placeholder": "Jméno", "composer.discard": "Zrušit", diff --git a/public/language/cs/unread.json b/public/language/cs/unread.json index 60381d5efd..35035c5cb0 100644 --- a/public/language/cs/unread.json +++ b/public/language/cs/unread.json @@ -10,6 +10,6 @@ "all-topics": "Všechna témata", "new-topics": "Nová témata", "watched-topics": "Sledovaná témata", - "unreplied-topics": "Unreplied Topics", - "multiple-categories-selected": "Multiple Selected" + "unreplied-topics": "Neodpovězené témata", + "multiple-categories-selected": "Vícenásobný výběr" } \ No newline at end of file diff --git a/public/language/cs/user.json b/public/language/cs/user.json index 0042eb8ec6..8fce80c61c 100644 --- a/public/language/cs/user.json +++ b/public/language/cs/user.json @@ -25,7 +25,7 @@ "reputation": "Reputace", "bookmarks": "Záložky", "watched": "Sledován", - "ignored": "Ignored", + "ignored": "Ignorován", "followers": "Sledují ho", "following": "Sleduje", "aboutme": "O mně", @@ -85,7 +85,7 @@ "has_no_posts": "Tento uživatel ještě nic nenapsal.", "has_no_topics": "Tento uživatel ještě nezaložil žádné téma.", "has_no_watched_topics": "Tento uživatel zatím nesleduje žádná témata.", - "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_ignored_topics": "Tento uživatel ještě neignoruje žádné témata.", "has_no_upvoted_posts": "Tento uživatel zatím nevyjádřil souhlas u žádného příspěvku.", "has_no_downvoted_posts": "Tento uživatel zatím nevyjádřil nesouhlas u žádného příspěvku.", "has_no_voted_posts": "Tento uživatel nemá žádné hlasovací příspěvky", @@ -101,11 +101,11 @@ "outgoing-message-sound": "Zvuk odchozí zprávy", "notification-sound": "Zvuk oznámení", "no-sound": "Bez zvuku", - "upvote-notif-freq": "Upvote Notification Frequency", - "upvote-notif-freq.all": "All Upvotes", - "upvote-notif-freq.everyTen": "Every Ten Upvotes", - "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", - "upvote-notif-freq.disabled": "Disabled", + "upvote-notif-freq": "Frekvence upozornění na souhlasy", + "upvote-notif-freq.all": "Všechny souhlasy", + "upvote-notif-freq.everyTen": "Každý desátý souhlas", + "upvote-notif-freq.logarithmic": "Dle 10, 100, 1000...", + "upvote-notif-freq.disabled": "Zakázáno", "browsing": "Nastavení prohlížení", "open_links_in_new_tab": "Otevřít odchozí odkaz v nové záložce", "enable_topic_searching": "Povolit vyhledávání v tématu", @@ -126,9 +126,9 @@ "sso.title": "Služby jednotného přihlášení", "sso.associated": "Přiřazeno k", "sso.not-associated": "Zde klikněte pro přiřazení k", - "sso.dissociate": "Dissociate", - "sso.dissociate-confirm-title": "Confirm Dissociation", - "sso.dissociate-confirm": "Are you sure you wish to dissociate your account from %1?", + "sso.dissociate": "Odloučit", + "sso.dissociate-confirm-title": "Potvrdit odloučení", + "sso.dissociate-confirm": "Jste si jist/a, že chcete odloučit váš účet z %1?", "info.latest-flags": "Poslední označené", "info.no-flags": "Nebyly nalezeny žádné označené příspěvky", "info.ban-history": "Poslední historie blokovaných", diff --git a/public/language/en-GB/admin/manage/admins-mods.json b/public/language/en-GB/admin/manage/admins-mods.json new file mode 100644 index 0000000000..e0f39ed5d4 --- /dev/null +++ b/public/language/en-GB/admin/manage/admins-mods.json @@ -0,0 +1,10 @@ +{ + "administrators": "Administrators", + "global-moderators": "Global Moderators", + "no-global-moderators": "No Global Moderators", + "moderators-of-category": "%1 Moderators", + "no-moderators": "No Moderators", + "add-administrator": "Add Administrator", + "add-global-moderator": "Add Global Moderator", + "add-moderator": "Add Moderator" +} \ No newline at end of file diff --git a/public/language/en-GB/admin/manage/privileges.json b/public/language/en-GB/admin/manage/privileges.json new file mode 100644 index 0000000000..b5b4f35885 --- /dev/null +++ b/public/language/en-GB/admin/manage/privileges.json @@ -0,0 +1,4 @@ +{ + "global": "Global", + "global.no-users": "No user-specific global privileges." +} \ No newline at end of file diff --git a/public/language/en-GB/admin/manage/users.json b/public/language/en-GB/admin/manage/users.json index 5b68fcdc91..9dcc1a0f32 100644 --- a/public/language/en-GB/admin/manage/users.json +++ b/public/language/en-GB/admin/manage/users.json @@ -71,9 +71,15 @@ "alerts.lockout-reset-success": "Lockout(s) reset!", "alerts.flag-reset-success": "Flags(s) reset!", "alerts.no-remove-yourself-admin": "You can't remove yourself as Administrator!", - "alerts.make-admin-success": "User(s) are now administrators.", - "alerts.confirm-remove-admin": "Do you really want to remove admins?", - "alerts.remove-admin-success": "User(s) are no longer administrators.", + "alerts.make-admin-success": "User is now administrator.", + "alerts.confirm-remove-admin": "Do you really want to remove this administrator?", + "alerts.remove-admin-success": "User is no longer administrator.", + "alerts.make-global-mod-success": "User is now global moderator.", + "alerts.confirm-remove-global-mod": "Do you really want to remove this global moderator?", + "alerts.remove-global-mod-success": "User is no longer global noderator", + "alerts.make-moderator-success": "User is now moderator.", + "alerts.confirm-remove-moderator": "Do you really want to remove this moderator?", + "alerts.remove-moderator-success": "User is no longer moderator.", "alerts.confirm-validate-email": "Do you want to validate email(s) of these user(s)?", "alerts.validate-email-success": "Emails validated", "alerts.password-reset-confirm": "Do you want to send password reset email(s) to these user(s)?", diff --git a/public/language/en-GB/admin/menu.json b/public/language/en-GB/admin/menu.json index 2b836ed0f7..8f44bcd304 100644 --- a/public/language/en-GB/admin/menu.json +++ b/public/language/en-GB/admin/menu.json @@ -9,8 +9,10 @@ "section-manage": "Manage", "manage/categories": "Categories", + "manage/privileges": "Privileges", "manage/tags": "Tags", "manage/users": "Users", + "manage/admins-mods": "Admins & Mods", "manage/registration": "Registration Queue", "manage/post-queue": "Post Queue", "manage/groups": "Groups", diff --git a/public/language/fr/admin/appearance/customise.json b/public/language/fr/admin/appearance/customise.json index ada04f6546..187f799efb 100644 --- a/public/language/fr/admin/appearance/customise.json +++ b/public/language/fr/admin/appearance/customise.json @@ -3,9 +3,9 @@ "custom-css.description": "Entrez vos propres déclarations de CSS ici, elles seront appliquées après tous les autres styles.", "custom-css.enable": "Activer les CSS personnalisés", - "custom-js": "Custom Javascript", - "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", - "custom-js.enable": "Enable Custom Javascript", + "custom-js": "Javascript personnalisé", + "custom-js.description": "Entrez votre Javascript ici. Celui-ci sera exécute après le chargement complet de la page.", + "custom-js.enable": "Activer le Javascript personnalisé", "custom-header": "En-tête personnalisé", "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", diff --git a/public/language/it/email.json b/public/language/it/email.json index bcada51df4..f6fdaef532 100644 --- a/public/language/it/email.json +++ b/public/language/it/email.json @@ -13,7 +13,7 @@ "reset.text1": "Abbiamo ricevuto una richiesta di reset della tua password, probabilmente perché l'hai dimenticata. Se non è così si prega di ignorare questa email.", "reset.text2": "Per confermare il reset della password per favore clicca il seguente link:", "reset.cta": "Clicca qui per resettare la tua password", - "reset.notify.subject": "Possword modificata con successo.", + "reset.notify.subject": "Password modificata con successo.", "reset.notify.text1": "Ti informiamo che in data %1, la password è stata cambiata con successo.", "reset.notify.text2": "Se non hai autorizzato questo, per favore notifica immediatamente un amministratore.", "digest.notifications": "Hai una notifica non letta da %1:", @@ -33,9 +33,9 @@ "notif.cta": "Vai alla discussione", "test.text1": "Questa è una email di test per verificare che il servizio di invio email è configurato correttamente sul tuo NodeBB.", "unsub.cta": "Clicca qui per modificare queste impostazioni", - "banned.subject": "You have been banned from %1", - "banned.text1": "The user %1 has been banned from %2.", - "banned.text2": "This ban will last until %1.", - "banned.text3": "This is the reason why you have been banned:", + "banned.subject": "Sei stato bannato da %1", + "banned.text1": "%1 è stato bannato da %2", + "banned.text2": "Questo ban durerà fino a %1.", + "banned.text3": "Il motivo del ban è:", "closing": "Grazie!" } \ No newline at end of file diff --git a/public/language/it/error.json b/public/language/it/error.json index 9fa55ff42e..0ad1f963ca 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -11,10 +11,10 @@ "invalid-uid": "ID Utente non valido", "invalid-username": "Nome utente non valido", "invalid-email": "Email non valida", - "invalid-title": "Invalid title", + "invalid-title": "Titolo non Valido", "invalid-user-data": "Dati Utente non validi", "invalid-password": "Password non valida", - "invalid-login-credentials": "Invalid login credentials", + "invalid-login-credentials": "Credenziali non Valide", "invalid-username-or-password": "Si prega di specificare sia un nome utente che una password", "invalid-search-term": "Termine di ricerca non valido", "csrf-invalid": "Non siamo riusciti a farti connettere, probabilmente perché la sessione è scaduta. Per favore riprova.", @@ -66,7 +66,7 @@ "content-too-long": "Inserisci un post più breve. I post non possono essere più lunghi di %1 caratteri.", "title-too-short": "Inserisci un titolo più lungo. I titoli devono contenere almeno %1 caratteri.", "title-too-long": "Inserisci un titolo più corto. I titoli non possono essere più lunghi di %1 caratteri.", - "category-not-selected": "Category not selected.", + "category-not-selected": "Categoria non selezionata.", "too-many-posts": "È possibile inserire un Post ogni %1 secondi - si prega di attendere prima di postare di nuovo", "too-many-posts-newbie": "Come nuovo utente puoi postare solamente una volta ogni %1 secondi finché non hai raggiunto un livello di reputazione %2 - per favore attendi prima di scrivere ancora", "tag-too-short": "Inserisci un tag più lungo. I tag devono contenere almeno %1 caratteri.", @@ -76,12 +76,12 @@ "still-uploading": "Per favore attendere il completamento degli uploads.", "file-too-big": "La dimensione massima consentita è di %1 kB - si prega di caricare un file più piccolo", "guest-upload-disabled": "Il caricamento da ospite è stato disattivato", - "already-bookmarked": "You have already bookmarked this post", - "already-unbookmarked": "You have already unbookmarked this post", + "already-bookmarked": "Hai già aggiunto questa discussione ai preferiti.", + "already-unbookmarked": "Hai già rimosso questa discussione dai preferiti", "cant-ban-other-admins": "Non puoi bannare altri amministratori!", "cant-remove-last-admin": "Sei l'unico Amministratore. Aggiungi un altro amministratore prima di rimuovere il tuo ruolo", "cant-delete-admin": "Togli i privilegi amministrativi da questo account prima di provare ad eliminarlo.", - "invalid-image": "Invalid image", + "invalid-image": "Immagine non Valida", "invalid-image-type": "Tipo dell'immagine non valido. I tipi permessi sono: %1", "invalid-image-extension": "Estensione immagine non valida", "invalid-file-type": "Tipo di file non valido. I formati consentiti sono: %1", @@ -109,7 +109,7 @@ "chat-disabled": "Il sistema di chat è stato disabilitato", "too-many-messages": "Hai inviato troppi messaggi, aspetta un attimo.", "invalid-chat-message": "Messaggio chat non valido", - "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "chat-message-too-long": "I messaggi in chat non possono superare i %1 caratteri.", "cant-edit-chat-message": "Non ti è permesso di modificare questo messaggio", "cant-remove-last-user": "Non puoi rimuovere l'ultimo utente", "cant-delete-chat-message": "Non ti è permesso di eliminare questo messaggio", @@ -119,7 +119,7 @@ "not-enough-reputation-to-downvote": "Non hai i privilegi per votare negativamente questo post", "not-enough-reputation-to-flag": "Tu non hai abbastanza reputazione per segnalare questo Post", "already-flagged": "Hai già messo marcato questo post", - "self-vote": "You cannot vote on your own post", + "self-vote": "Non puoi votare la tua stessa discussione", "reload-failed": "NodeBB ha incontrato un problema durante il ricaricamento: \"%1\". NodeBB continuerà a servire gli assets esistenti lato client, così puoi annullare quello che hai fatto prima di ricaricare.", "registration-error": "Errore nella registrazione", "parse-error": "Qualcosa è andato storto durante l'analisi della risposta proveniente dal server", @@ -135,5 +135,5 @@ "invalid-home-page-route": "Percorso della pagina iniziale non valido", "invalid-session": "Discrepanza della sessione", "invalid-session-text": "Sembra che la tua sessione non sia più attiva, oppure non corrisponde con il server. Per favore ricarica la pagina.", - "no-topics-selected": "No topics selected!" + "no-topics-selected": "Nessuna discussione selezionata!" } \ No newline at end of file diff --git a/public/language/it/flags.json b/public/language/it/flags.json index d05a5b25a8..14aaf7af02 100644 --- a/public/language/it/flags.json +++ b/public/language/it/flags.json @@ -27,12 +27,12 @@ "quick-links": "Quick Links", "flagged-user": "Flagged User", - "view-profile": "View Profile", + "view-profile": "Vedi Profilo", "start-new-chat": "Start New Chat", "go-to-target": "View Flag Target", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "Vedi Profilo", + "user-edit": "Modifica Profilo", "notes": "Flag Notes", "add-note": "Add Note", @@ -44,21 +44,21 @@ "state-all": "All states", "state-open": "New/Open", - "state-wip": "Work in Progress", - "state-resolved": "Resolved", + "state-wip": "Lavori in Corso", + "state-resolved": "Risolto", "state-rejected": "Rejected", - "no-assignee": "Not Assigned", + "no-assignee": "Non Assegnato", "note-added": "Note Added", - "modal-title": "Report Inappropriate Content", + "modal-title": "Segnala Contenuto Inappropriato", "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", "modal-reason-spam": "Spam", - "modal-reason-offensive": "Offensive", - "modal-reason-other": "Other (specify below)", - "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", - "modal-submit-success": "Content has been flagged for moderation.", - "modal-submit-confirm": "Confirm Submission", + "modal-reason-offensive": "Offensivo", + "modal-reason-other": "Altro (Specificare di seguito)", + "modal-reason-custom": "Motivazione della segnalazione...", + "modal-submit": "Invia la Segnalazione", + "modal-submit-success": "Il contenuto è stato segnalato.", + "modal-submit-confirm": "Conferma la Segnalazione", "modal-submit-confirm-text": "You have a custom reason specified already. Are you sure you wish to submit via quick-report?", "modal-submit-confirm-text-help": "Submitting a quick report will overwrite any custom reasons defined." } \ No newline at end of file diff --git a/public/language/it/global.json b/public/language/it/global.json index 7325219082..4279338d8d 100644 --- a/public/language/it/global.json +++ b/public/language/it/global.json @@ -104,6 +104,6 @@ "cookies.accept": "Ho capito!", "cookies.learn_more": "Scopri di più", "edited": "Modificato", - "disabled": "Disabled", - "select": "Select" + "disabled": "Disabilitato", + "select": "Seleziona" } \ No newline at end of file diff --git a/public/language/it/groups.json b/public/language/it/groups.json index 835ada672f..53f27c12c4 100644 --- a/public/language/it/groups.json +++ b/public/language/it/groups.json @@ -27,7 +27,7 @@ "details.disableJoinRequests": "Disabilita le richieste d'adesione", "details.grant": "Concedi / Rimuovi la Proprietà", "details.kick": "Espelli", - "details.kick_confirm": "Are you sure you want to remove this member from the group?", + "details.kick_confirm": "Sei sicuro di voler rimuovere questo membro dal gruppo?", "details.owner_options": "Amministratore del Grupo", "details.group_name": "Nome Gruppo", "details.member_count": "Totale Membri", @@ -53,6 +53,6 @@ "new-group.group_name": "Nome Gruppo:", "upload-group-cover": "Carica copertina del gruppo", "bulk-invite-instructions": "Inserisci una lista di nomi utente da invitare in questo gruppo separati da virgole", - "bulk-invite": "Bulk Invite", + "bulk-invite": "Invita in Massa", "remove_group_cover_confirm": "Sei sicuro di voler rimuovere l'immagine copertina?" } \ No newline at end of file diff --git a/public/language/it/modules.json b/public/language/it/modules.json index 5fcef5d582..d379688576 100644 --- a/public/language/it/modules.json +++ b/public/language/it/modules.json @@ -20,7 +20,7 @@ "chat.three_months": "3 Mesi", "chat.delete_message_confirm": "Sei sicuro di voler eliminare questo messaggio?", "chat.add-users-to-room": "Aggiungi utenti alla stanza", - "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.confirm-chat-with-dnd-user": "Questo utente ha impostato il suo stato su Non Disturbare. Sei sicuro di voler iniziare una conversazione?", "composer.compose": "Componi", "composer.show_preview": "Visualizza Anteprima", "composer.hide_preview": "Nascondi Anteprima", diff --git a/public/language/it/notifications.json b/public/language/it/notifications.json index 5aa5225d1b..7a1b0c4b36 100644 --- a/public/language/it/notifications.json +++ b/public/language/it/notifications.json @@ -29,9 +29,9 @@ "user_flagged_post_in": "%1 ha segnalato un post in %2", "user_flagged_post_in_dual": "%1 e %2 hanno segnalato un post in %3", "user_flagged_post_in_multiple": "%1 ed altri %2 hanno segnalato un post in %3", - "user_flagged_user": "%1 flagged a user profile (%2)", - "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)", - "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)", + "user_flagged_user": "%1 ha segnalato un utente (%2)", + "user_flagged_user_dual": "%1 e %2 hanno segnalato un utente (%3)", + "user_flagged_user_multiple": "%1 e altri %2 hanno segnalato un utente (%3)", "user_posted_to": "%1 ha postato una risposta a: %2", "user_posted_to_dual": "%1 e %2 hanno postato una risposta su: %3", "user_posted_to_multiple": "%1 ed altri %2 hanno postato una risposta su: %3", @@ -42,23 +42,23 @@ "new_register": "%1 ha inviato una richiesta di registrazione.", "new_register_multiple": "Ci sono %1 richieste di registrazione che attendono di essere esaminate.", "flag_assigned_to_you": "Flag %1 has been assigned to you", - "post_awaiting_review": "Post awaiting review", + "post_awaiting_review": "Post in attesa di revisione", "email-confirmed": "Email Confermata", "email-confirmed-message": "Grazie per aver validato la tua email. Il tuo account è ora completamente attivato.", "email-confirm-error-message": "C'è stato un problema nella validazione del tuo indirizzo email. Potrebbe essere il codice non valido o scaduto.", "email-confirm-sent": "Email di conferma inviata.", - "none": "None", - "notification_only": "Notification Only", - "email_only": "Email Only", - "notification_and_email": "Notification & Email", - "notificationType_upvote": "When someone upvotes your post", + "none": "Nessuna Notifica", + "notification_only": "Solo Notifiche", + "email_only": "Solo Email", + "notification_and_email": "Email e Notifica", + "notificationType_upvote": "Quando il tuo post riceve un Mi Piace", "notificationType_new-topic": "When someone you follow posts a topic", "notificationType_new-reply": "When a new reply is posted in a topic you are watching", "notificationType_follow": "When someone starts following you", "notificationType_new-chat": "When you receive a chat message", "notificationType_group-invite": "When you receive a group invite", "notificationType_new-register": "When someone gets added to registration queue", - "notificationType_post-queue": "When a new post is queued", - "notificationType_new-post-flag": "When a post is flagged", - "notificationType_new-user-flag": "When a user is flagged" + "notificationType_post-queue": "Quando un nuovo post è in attesa di revisione", + "notificationType_new-post-flag": "Quando un post viene segnalato", + "notificationType_new-user-flag": "Quando un utente viene segnalato" } \ No newline at end of file diff --git a/public/language/it/pages.json b/public/language/it/pages.json index 0b1be5dc1e..0be5a17a98 100644 --- a/public/language/it/pages.json +++ b/public/language/it/pages.json @@ -7,7 +7,7 @@ "popular-alltime": "Discussioni più popolari di sempre", "recent": "Discussioni Recenti", "moderator-tools": "Moderator Tools", - "flagged-content": "Flagged Content", + "flagged-content": "Contenuti Segnalati", "ip-blacklist": "Lista nera degli IP", "post-queue": "Post Queue", "users/online": "Utenti Online", @@ -44,7 +44,7 @@ "account/bookmarks": "%1 Post tra i favoriti", "account/settings": "Impostazioni Utente", "account/watched": "Discussioni osservate da %1", - "account/ignored": "Topics ignored by %1", + "account/ignored": "Discussioni ignorate da %1", "account/upvoted": "Post apprezzati da %1", "account/downvoted": "Post votati negativamente da %1", "account/best": "I migliori post di %1", diff --git a/public/language/it/register.json b/public/language/it/register.json index e3afd5aed2..49de769d43 100644 --- a/public/language/it/register.json +++ b/public/language/it/register.json @@ -1,5 +1,5 @@ { - "register": "Registrazione", + "register": "Registrati", "cancel_registration": "Cancella Registrazione", "help.email": "Come opzione predefinita, il tuo indirizzo email non verrà reso pubblico.", "help.username_restrictions": "Un nome utente unico, di almeno %1 caratteri e al massimo di %2. Gli altri utenti ti possono menzionare usando @username.", diff --git a/public/language/it/search.json b/public/language/it/search.json index 980e2c28ba..dc33a58737 100644 --- a/public/language/it/search.json +++ b/public/language/it/search.json @@ -12,7 +12,7 @@ "reply-count": "Numero Risposte", "at-least": "Almeno", "at-most": "Al massimo", - "relevance": "Relevance", + "relevance": "Rilevanza", "post-time": "Ora invio", "newer-than": "Più nuovi di", "older-than": "Più vecchi di", diff --git a/public/language/it/success.json b/public/language/it/success.json index cf8dd4cad5..35f6d5b2a9 100644 --- a/public/language/it/success.json +++ b/public/language/it/success.json @@ -1,7 +1,7 @@ { "success": "Riuscito", "topic-post": "Hai postato correttamente.", - "post-queued": "Your post is queued for approval.", + "post-queued": "La tua discussione è in attesa di approvazione.", "authentication-successful": "Autenticazione Riuscita", "settings-saved": "Impostazioni salvate!" } \ No newline at end of file diff --git a/public/language/it/topic.json b/public/language/it/topic.json index 747ba4876b..5b15970770 100644 --- a/public/language/it/topic.json +++ b/public/language/it/topic.json @@ -13,9 +13,9 @@ "notify_me": "Ricevi notifiche di nuove risposte in questa discussione", "quote": "Cita", "reply": "Rispondi", - "replies_to_this_post": "%1 Replies", - "one_reply_to_this_post": "1 Reply", - "last_reply_time": "Last reply", + "replies_to_this_post": "%1 Risposte", + "one_reply_to_this_post": "1 Risposta", + "last_reply_time": "Ultima Risposta", "reply-as-topic": "Topic risposta", "guest-login-reply": "Effettua il Log in per rispondere", "edit": "Modifica", @@ -59,7 +59,7 @@ "thread_tools.unlock": "Sblocca Discussione", "thread_tools.move": "Sposta Discussione", "thread_tools.move_all": "Sposta Tutto", - "thread_tools.select_category": "Select Category", + "thread_tools.select_category": "Seleziona Categoria", "thread_tools.fork": "Dividi Discussione", "thread_tools.delete": "Elimina Discussione", "thread_tools.delete-posts": "Cancella post", @@ -68,8 +68,8 @@ "thread_tools.restore_confirm": "Sei sicuro di voler ripristinare questa discussione?", "thread_tools.purge": "Svuota Discussione", "thread_tools.purge_confirm": "Sei sicuro di voler svuotare questa discussione?", - "thread_tools.merge_topics": "Merge Topics", - "thread_tools.merge": "Merge", + "thread_tools.merge_topics": "Unisci le Discussioni", + "thread_tools.merge": "Unisci", "topic_move_success": "Questa discussione è stata correttamente spostata in %1", "post_delete_confirm": "Sei sicuro di voler cancellare questo post?", "post_restore_confirm": "Sei sicuro di voler ripristinare questo post?", @@ -91,7 +91,7 @@ "fork_pid_count": "%1 post selezionati", "fork_success": "Topic Diviso con successo ! Clicca qui per andare al Topic Diviso.", "delete_posts_instruction": "Clicca sui post che vuoi cancellare/eliminare", - "merge_topics_instruction": "Click the topics you want to merge", + "merge_topics_instruction": "Clicca sulle discussioni che vuoi unire", "composer.title_placeholder": "Inserisci qui il titolo della discussione...", "composer.handle_placeholder": "Nome", "composer.discard": "Annulla", diff --git a/public/language/it/unread.json b/public/language/it/unread.json index c1244d0fe3..2f2afebd23 100644 --- a/public/language/it/unread.json +++ b/public/language/it/unread.json @@ -10,6 +10,6 @@ "all-topics": "Tutte le Discussioni", "new-topics": "Nuova Discussione", "watched-topics": "Discussioni seguite", - "unreplied-topics": "Nessuna Risposta", - "multiple-categories-selected": "Multiple Selected" + "unreplied-topics": "Discussioni Senza Risposta", + "multiple-categories-selected": "Più Categorie" } \ No newline at end of file diff --git a/public/language/it/user.json b/public/language/it/user.json index a24f325d24..d3b66bfa4a 100644 --- a/public/language/it/user.json +++ b/public/language/it/user.json @@ -19,13 +19,13 @@ "location": "Località", "age": "Età", "joined": "Iscrizione", - "lastonline": "Ultima volta in linea", + "lastonline": "Ultimo Accesso", "profile": "Profilo", "profile_views": "Visite al profilo", "reputation": "Reputazione", - "bookmarks": "Favoriti", + "bookmarks": "Preferiti", "watched": "Osservati", - "ignored": "Ignored", + "ignored": "Ignorati", "followers": "Da chi è seguito", "following": "Chi segue", "aboutme": "Su di me", @@ -61,7 +61,7 @@ "username_taken_workaround": "Il nome utente che hai richiesto era già stato utilizzato, quindi lo abbiamo modificato leggermente. Ora il tuo è %1", "password_same_as_username": "La tua password è uguale al tuo username, per piacere scegli un'altra password", "password_same_as_email": "La tua password sembra coincidere con la tua email, per favore fornisci un'altra password.", - "weak_password": "Weak password.", + "weak_password": "Password debole.", "upload_picture": "Carica foto", "upload_a_picture": "Carica una foto", "remove_uploaded_picture": "Elimina foto caricata", @@ -85,7 +85,7 @@ "has_no_posts": "Questo utente non ha ancora scritto niente.", "has_no_topics": "Questo utente non ha ancora avviato discussioni.", "has_no_watched_topics": "Questo utente non sta osservando discussioni.", - "has_no_ignored_topics": "This user hasn't ignored any topics yet.", + "has_no_ignored_topics": "Questo utente non sta ignorando discussioni.", "has_no_upvoted_posts": "Questo utente non ha ancora apprezzato nessun post.", "has_no_downvoted_posts": "Questo utente non ha ancora votato negativamente alcun post", "has_no_voted_posts": "Questo utente non ha post votati", @@ -94,18 +94,18 @@ "paginate_description": "Non utilizzare lo scroll infinito per discussioni e messaggi", "topics_per_page": "Discussioni per Pagina", "posts_per_page": "Post per Pagina", - "max_items_per_page": "Maximum %1", + "max_items_per_page": "Massimo %1", "notification_sounds": "Riproduci un suono quando si riceve una notifica", "notifications_and_sounds": "Notifiche e Suoni", "incoming-message-sound": "Suono messaggio in entrata", "outgoing-message-sound": "Suono messaggio in uscita", "notification-sound": "Suono di notifica", "no-sound": "Nessun suono", - "upvote-notif-freq": "Upvote Notification Frequency", - "upvote-notif-freq.all": "All Upvotes", - "upvote-notif-freq.everyTen": "Every Ten Upvotes", - "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", - "upvote-notif-freq.disabled": "Disabled", + "upvote-notif-freq": "Frequenza Notifiche dei Mi Piace ", + "upvote-notif-freq.all": "Tutti i Mi Piace", + "upvote-notif-freq.everyTen": "Ogni Dieci Mi Piace", + "upvote-notif-freq.logarithmic": "Ogni 10, 100, 1000...", + "upvote-notif-freq.disabled": "Disabilitate", "browsing": "Impostazioni di Navigazione", "open_links_in_new_tab": "Apri i link web in una nuova pagina", "enable_topic_searching": "Abilita la ricerca negli argomenti", diff --git a/public/language/nl/email.json b/public/language/nl/email.json index 48f35a2c6f..bd474f945e 100644 --- a/public/language/nl/email.json +++ b/public/language/nl/email.json @@ -18,7 +18,7 @@ "reset.notify.text2": "Neem onmiddellijk contact met een beheerder op wanneer je hiervoor geen toestemming hebt gegeven.", "digest.notifications": "Er zijn ongelezen notificaties van %1:", "digest.latest_topics": "De meest recente onderwerpen van %1", - "digest.cta": "Klik hier om deze website te bezoeken %1 ", + "digest.cta": "Klik hier om %1 te bezoeken ", "digest.unsub.info": "Deze samenvatting hebben we naar je verzonden omdat je dat hebt ingesteld.", "digest.no_topics": "In de afgelopen %1 zijn er geen actieve onderwerpen geweest.", "digest.day": "dag", @@ -30,7 +30,7 @@ "notif.chat.unsub.info": "Deze notificatie is verzonden vanwege de gebruikersinstellingen voor abonnementen.", "notif.post.cta": "Klik hier om het volledige bericht te lezen", "notif.post.unsub.info": "Deze notificatie is door ons verzonden vanwege gebruikersinstellingen voor abonnementen en berichten.", - "notif.cta": "Click here to go to forum", + "notif.cta": "Klik hier om naar het forum te gaan", "test.text1": "Dit is een testbericht om te verifiëren dat NodeBB de e-mailberichtservice correct heeft opgezet.", "unsub.cta": "Klik hier om deze instellingen te wijzigen", "banned.subject": "U bent verbannen van %1", diff --git a/public/language/nl/error.json b/public/language/nl/error.json index 460254c097..48931aa461 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -119,13 +119,13 @@ "not-enough-reputation-to-downvote": "Je hebt onvoldoende reputatie om een negatieve stem uit te mogen brengen", "not-enough-reputation-to-flag": "Je hebt onvoldoende reputatie om dit bericht aan de beheerders te mogen melden", "already-flagged": "Je hebt deze post al gerapporteerd", - "self-vote": "You cannot vote on your own post", + "self-vote": "Het is niet mogelijk om op je eigen bericht te stemmen", "reload-failed": "Tijdens het herladen van \"%1\" is NodeBB een fout of probleem tegengekomen. NodeBB blijft operationeel. Echter het is verstandig om de oorzaak te onderzoeken en wellicht de vorige actie, voor het herladen, ongedaan te maken.", "registration-error": "Fout tijdens registratie", "parse-error": "Tijdens het verwerken van het antwoord van de server is er iets misgegaan.", "wrong-login-type-email": "Gebruik je e-mailadres om in te loggen", "wrong-login-type-username": "Gebruik je gebruikersnaam om in te loggen", - "sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first", + "sso-registration-disabled": "Registratie is uitgeschakeld voor %1 accounts, registreer eerst met een e-mailadres", "invite-maximum-met": "Je heb het maximum aantal mensen uitgenodigd (%1 van de %2).", "no-session-found": "Geen login sessie gevonden!", "not-in-room": "Gebruiker niet in de chat", @@ -135,5 +135,5 @@ "invalid-home-page-route": "Onbekende homepage route", "invalid-session": "Verkeerde sessie combinatie", "invalid-session-text": "Het lijkt erop dat je login sessie niet meer actief is of niet langer synchroon is met de server. Ververs de pagina.", - "no-topics-selected": "No topics selected!" + "no-topics-selected": "Geen onderwerpen geselecteerd!" } \ No newline at end of file diff --git a/public/language/nl/notifications.json b/public/language/nl/notifications.json index c19dbaef83..78ec3289b5 100644 --- a/public/language/nl/notifications.json +++ b/public/language/nl/notifications.json @@ -9,7 +9,7 @@ "continue_to": "Door naar %1", "return_to": "Terug naar %1", "new_notification": "Nieuwe notificatie", - "new_notification_from": "You have a new Notification from %1", + "new_notification_from": "Je hebt een nieuwe notificatie van %1", "you_have_unread_notifications": "Je hebt nieuwe notificaties.", "all": "Alles", "topics": "Onderwerpen", @@ -47,18 +47,18 @@ "email-confirmed-message": "Bedankt voor het bevestigen van je e-mailadres. Je account is nu volledig geactiveerd.", "email-confirm-error-message": "Er was een probleem met het bevestigen van dit e-mailadres. Misschien is de code niet goed ingevoerd of was de beschikbare tijd inmiddels verstreken.", "email-confirm-sent": "Bevestigingsmail verstuurd.", - "none": "None", - "notification_only": "Notification Only", - "email_only": "Email Only", - "notification_and_email": "Notification & Email", - "notificationType_upvote": "When someone upvotes your post", - "notificationType_new-topic": "When someone you follow posts a topic", - "notificationType_new-reply": "When a new reply is posted in a topic you are watching", - "notificationType_follow": "When someone starts following you", - "notificationType_new-chat": "When you receive a chat message", - "notificationType_group-invite": "When you receive a group invite", - "notificationType_new-register": "When someone gets added to registration queue", - "notificationType_post-queue": "When a new post is queued", - "notificationType_new-post-flag": "When a post is flagged", - "notificationType_new-user-flag": "When a user is flagged" + "none": "Geen", + "notification_only": "Alleen notificatie", + "email_only": "Alleen e-mail", + "notification_and_email": "Notificatie & e-mail", + "notificationType_upvote": "Als iemand positief stemt voor je bericht", + "notificationType_new-topic": "Wanneer iemand die jij volgt een onderwerp post", + "notificationType_new-reply": "Als een nieuwe reactie komt op een onderwerp dat je volgt", + "notificationType_follow": "Als iemand begint met jou te volgen", + "notificationType_new-chat": "Als je een chat-bericht ontvangt", + "notificationType_group-invite": "Als je een uitnodiging voor een groep ontvangt", + "notificationType_new-register": "Als iemand wordt toegevoegd aan een registratiewachtrij", + "notificationType_post-queue": "Als een bericht aan de wachtrij wordt toegevoegd", + "notificationType_new-post-flag": "Als een bericht wordt gevlagd", + "notificationType_new-user-flag": "Als een gebruiker wordt gevlagd" } \ No newline at end of file diff --git a/public/language/nl/topic.json b/public/language/nl/topic.json index b1d33fd9da..29b4eb9e75 100644 --- a/public/language/nl/topic.json +++ b/public/language/nl/topic.json @@ -68,8 +68,8 @@ "thread_tools.restore_confirm": "Zeker weten dit onderwerp te herstellen?", "thread_tools.purge": "Wis onderwerp ", "thread_tools.purge_confirm": "Weet je zeker dat je dit onderwerp wil verwijderen?", - "thread_tools.merge_topics": "Merge Topics", - "thread_tools.merge": "Merge", + "thread_tools.merge_topics": "Onderwerpen samenvoegen", + "thread_tools.merge": "Samenvoegen", "topic_move_success": "Verplaatsen van onderwerp naar %1 succesvol", "post_delete_confirm": "Is het absoluut de bedoeling dit bericht te verwijderen?", "post_restore_confirm": "Is het de bedoeling dit bericht te herstellen?", @@ -91,7 +91,7 @@ "fork_pid_count": "%1 bericht(en) geselecteerd", "fork_success": "Onderwerp is succesvol afgesplitst. Klik hier om het nieuwe onderwerp te zien.", "delete_posts_instruction": "Klik op de berichten die verwijderd moeten worden", - "merge_topics_instruction": "Click the topics you want to merge", + "merge_topics_instruction": "Klik op de onderwerpen die samengevoegd moeten worden", "composer.title_placeholder": "Voer hier de titel van het onderwerp in...", "composer.handle_placeholder": "Naam", "composer.discard": "Annuleren", diff --git a/public/language/nl/user.json b/public/language/nl/user.json index 6351ae8827..9c2af5d6f5 100644 --- a/public/language/nl/user.json +++ b/public/language/nl/user.json @@ -101,11 +101,11 @@ "outgoing-message-sound": "Uitgaand bericht geluid", "notification-sound": "Notificatie geluid", "no-sound": "Geen geluid", - "upvote-notif-freq": "Upvote Notification Frequency", - "upvote-notif-freq.all": "All Upvotes", - "upvote-notif-freq.everyTen": "Every Ten Upvotes", - "upvote-notif-freq.logarithmic": "On 10, 100, 1000...", - "upvote-notif-freq.disabled": "Disabled", + "upvote-notif-freq": "Notificatie frequentie voor Upvotes", + "upvote-notif-freq.all": "Alle Upvotes", + "upvote-notif-freq.everyTen": "Elke tien Upvotes", + "upvote-notif-freq.logarithmic": "Bij 10, 100, 1000...", + "upvote-notif-freq.disabled": "Uitgeschakeld", "browsing": "Instellingen voor bladeren", "open_links_in_new_tab": "Open uitgaande links naar een externe site in een nieuw tabblad", "enable_topic_searching": "Inschakelen mogelijkheid op onderwerp te kunnen zoeken", diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less index 124be23b5c..d7cd7e6ae4 100644 --- a/public/less/admin/admin.less +++ b/public/less/admin/admin.less @@ -14,6 +14,7 @@ @import "./manage/groups"; @import "./manage/registration"; @import "./manage/users"; +@import "./manage/admins-mods"; @import "./appearance/customise"; @import "./appearance/themes"; @import "./extend/plugins"; diff --git a/public/less/admin/manage/admins-mods.less b/public/less/admin/manage/admins-mods.less new file mode 100644 index 0000000000..0bf7ac46cd --- /dev/null +++ b/public/less/admin/manage/admins-mods.less @@ -0,0 +1,27 @@ +.admins-mods { + .user-card { + margin-right: 10px; + padding: 2px; + } + + .remove-user-icon { + margin-right: 5px; + margin-left: 5px; + } + + .category-depth-1 { + margin-left: 30px; + } + .category-depth-2 { + margin-left: 60px; + } + .category-depth-3 { + margin-left: 90px; + } + .category-depth-4 { + margin-left: 120px; + } + .category-depth-5 { + margin-left: 150px; + } +} \ No newline at end of file diff --git a/public/src/admin/manage/admins-mods.js b/public/src/admin/manage/admins-mods.js new file mode 100644 index 0000000000..9f642f1456 --- /dev/null +++ b/public/src/admin/manage/admins-mods.js @@ -0,0 +1,142 @@ +'use strict'; + +define('admin/manage/admins-mods', ['translator', 'benchpress', 'autocomplete'], function (translator, Benchpress, autocomplete) { + var AdminsMods = {}; + + AdminsMods.init = function () { + autocomplete.user($('#admin-search'), function (ev, ui) { + socket.emit('admin.user.makeAdmins', [ui.item.user.uid], function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[admin/manage/users:alerts.make-admin-success]]'); + $('#admin-search').val(''); + + if ($('.administrator-area [data-uid="' + ui.item.user.uid + '"]').length) { + return; + } + + app.parseAndTranslate('admin/manage/admins-mods', 'admins.members', { admins: { members: [ui.item.user] } }, function (html) { + $('.administrator-area').prepend(html); + }); + }); + }); + + $('.administrator-area').on('click', '.remove-user-icon', function () { + var userCard = $(this).parents('[data-uid]'); + var uid = userCard.attr('data-uid'); + if (parseInt(uid, 10) === parseInt(app.user.uid, 10)) { + return app.alertError('[[admin/manage/users:alerts.no-remove-yourself-admin]]'); + } + bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-admin]]', function (confirm) { + if (confirm) { + socket.emit('admin.user.removeAdmins', [uid], function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[admin/manage/users:alerts.remove-admin-success]]'); + userCard.remove(); + }); + } + }); + }); + + autocomplete.user($('#global-mod-search'), function (ev, ui) { + socket.emit('admin.groups.join', { + groupName: 'Global Moderators', + uid: ui.item.user.uid, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[admin/manage/users:alerts.make-global-mod-success]]'); + $('#global-mod-search').val(''); + + if ($('.global-moderator-area [data-uid="' + ui.item.user.uid + '"]').length) { + return; + } + + app.parseAndTranslate('admin/manage/admins-mods', 'globalMods.members', { globalMods: { members: [ui.item.user] } }, function (html) { + $('.global-moderator-area').prepend(html); + $('#no-global-mods-warning').addClass('hidden'); + }); + }); + }); + + $('.global-moderator-area').on('click', '.remove-user-icon', function () { + var userCard = $(this).parents('[data-uid]'); + var uid = userCard.attr('data-uid'); + + bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-global-mod]]', function (confirm) { + if (confirm) { + socket.emit('admin.groups.leave', { uid: uid, groupName: 'Global Moderators' }, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[admin/manage/users:alerts.remove-global-mod-success]]'); + userCard.remove(); + if (!$('.global-moderator-area').children().length) { + $('#no-global-mods-warning').removeClass('hidden'); + } + }); + } + }); + }); + + + autocomplete.user($('.moderator-search'), function (ev, ui) { + var input = $(ev.target); + var cid = $(ev.target).attr('data-cid'); + socket.emit('admin.categories.setPrivilege', { + cid: cid, + privilege: ['moderate'], + set: true, + member: ui.item.user.uid, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[admin/manage/users:alerts.make-moderator-success]]'); + input.val(''); + + if ($('.moderator-area[data-cid="' + cid + '"] [data-uid="' + ui.item.user.uid + '"]').length) { + return; + } + + app.parseAndTranslate('admin/manage/admins-mods', 'globalMods', { globalMods: [ui.item.user] }, function (html) { + $('.moderator-area[data-cid="' + cid + '"]').prepend(html); + $('.no-moderator-warning[data-cid="' + cid + '"]').addClass('hidden'); + }); + }); + }); + + $('.moderator-area').on('click', '.remove-user-icon', function () { + var moderatorArea = $(this).parents('[data-cid]'); + var cid = moderatorArea.attr('data-cid'); + var userCard = $(this).parents('[data-uid]'); + var uid = userCard.attr('data-uid'); + + bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-moderator]]', function (confirm) { + if (confirm) { + socket.emit('admin.categories.setPrivilege', { + cid: cid, + privilege: ['moderate'], + set: false, + member: uid, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('[[admin/manage/users:alerts.remove-moderator-success]]'); + userCard.remove(); + if (!moderatorArea.children().length) { + $('.no-moderator-warning[data-cid="' + cid + '"]').removeClass('hidden'); + } + }); + } + }); + }); + }; + + return AdminsMods; +}); diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js index 8ac0ecdf15..fb4ae062b8 100644 --- a/public/src/admin/manage/category.js +++ b/public/src/admin/manage/category.js @@ -1,6 +1,5 @@ 'use strict'; - define('admin/manage/category', [ 'uploader', 'iconSelect', @@ -8,8 +7,7 @@ define('admin/manage/category', [ 'autocomplete', 'translator', 'categorySelector', - 'benchpress', -], function (uploader, iconSelect, colorpicker, autocomplete, translator, categorySelector, Benchpress) { +], function (uploader, iconSelect, colorpicker, autocomplete, translator, categorySelector) { var Category = {}; var modified_categories = {}; @@ -100,7 +98,7 @@ define('admin/manage/category', [ }); $('.copy-settings').on('click', function () { - selectCategoryModal(function (cid) { + categorySelector.modal(function (cid) { socket.emit('admin.categories.copySettingsFrom', { fromCid: cid, toCid: ajaxify.data.category.cid }, function (err) { if (err) { return app.alertError(err.message); @@ -169,8 +167,6 @@ define('admin/manage/category', [ $('button[data-action="setParent"]').removeClass('hide'); }); }); - - Category.setupPrivilegeTable(); }; function modified(el) { @@ -208,102 +204,12 @@ define('admin/manage/category', [ }); } - Category.setupPrivilegeTable = function () { - $('.privilege-table-container').on('change', 'input[type="checkbox"]', function () { - var checkboxEl = $(this); - var privilege = checkboxEl.parent().attr('data-privilege'); - var state = checkboxEl.prop('checked'); - var rowEl = checkboxEl.parents('tr'); - var member = rowEl.attr('data-group-name') || rowEl.attr('data-uid'); - var isPrivate = parseInt(rowEl.attr('data-private') || 0, 10); - var isGroup = rowEl.attr('data-group-name') !== undefined; - - if (member) { - if (isGroup && privilege === 'groups:moderate' && !isPrivate && state) { - bootbox.confirm('[[admin/manage/categories:alert.confirm-moderate]]', function (confirm) { - if (confirm) { - Category.setPrivilege(member, privilege, state, checkboxEl); - } else { - checkboxEl.prop('checked', !checkboxEl.prop('checked')); - } - }); - } else { - Category.setPrivilege(member, privilege, state, checkboxEl); - } - } else { - app.alertError('[[error:invalid-data]]'); - } - }); - - $('.privilege-table-container').on('click', '[data-action="search.user"]', Category.addUserToPrivilegeTable); - $('.privilege-table-container').on('click', '[data-action="search.group"]', Category.addGroupToPrivilegeTable); - $('.privilege-table-container').on('click', '[data-action="copyToChildren"]', Category.copyPrivilegesToChildren); - $('.privilege-table-container').on('click', '[data-action="copyPrivilegesFrom"]', Category.copyPrivilegesFromCategory); - - Category.exposeAssumedPrivileges(); - }; - - Category.refreshPrivilegeTable = function () { - socket.emit('admin.categories.getPrivilegeSettings', ajaxify.data.category.cid, function (err, privileges) { - if (err) { - return app.alertError(err.message); - } - - Benchpress.parse('admin/partials/categories/privileges', { - privileges: privileges, - }, function (html) { - translator.translate(html, function (html) { - $('.privilege-table-container').html(html); - Category.exposeAssumedPrivileges(); - }); - }); - }); - }; - - Category.exposeAssumedPrivileges = function () { - /* - If registered-users has a privilege enabled, then all users and groups of that privilege - should be assumed to have that privilege as well, even if not set in the db, so reflect - this arrangement in the table - */ - var privs = []; - $('.privilege-table tr[data-group-name="registered-users"] td input[type="checkbox"]').parent().each(function (idx, el) { - if ($(el).find('input').prop('checked')) { - privs.push(el.getAttribute('data-privilege')); - } - }); - for (var x = 0, numPrivs = privs.length; x < numPrivs; x += 1) { - var inputs = $('.privilege-table tr[data-group-name]:not([data-group-name="registered-users"],[data-group-name="guests"]) td[data-privilege="' + privs[x] + '"] input'); - inputs.each(function (idx, el) { - if (!el.checked) { - el.indeterminate = true; - } - }); - } - }; - - Category.setPrivilege = function (member, privilege, state, checkboxEl) { - socket.emit('admin.categories.setPrivilege', { - cid: ajaxify.data.category.cid, - privilege: privilege, - set: state, - member: member, - }, function (err) { - if (err) { - return app.alertError(err.message); - } - - checkboxEl.replaceWith(''); - Category.refreshPrivilegeTable(); - }); - }; - Category.launchParentSelector = function () { var categories = ajaxify.data.allCategories.filter(function (category) { return category && !category.disabled && parseInt(category.cid, 10) !== parseInt(ajaxify.data.category.cid, 10); }); - selectCategoryModal(categories, function (parentCid) { + categorySelector.modal(categories, function (parentCid) { var payload = {}; payload[ajaxify.data.category.cid] = { @@ -327,117 +233,5 @@ define('admin/manage/category', [ }); }; - Category.addUserToPrivilegeTable = function () { - var modal = bootbox.dialog({ - title: '[[admin/manage/categories:alert.find-user]]', - message: '', - show: true, - }); - - modal.on('shown.bs.modal', function () { - var inputEl = modal.find('input'); - - autocomplete.user(inputEl, function (ev, ui) { - socket.emit('admin.categories.setPrivilege', { - cid: ajaxify.data.category.cid, - privilege: ['find', 'read', 'topics:read'], - set: true, - member: ui.item.user.uid, - }, function (err) { - if (err) { - return app.alertError(err.message); - } - - Category.refreshPrivilegeTable(); - modal.modal('hide'); - }); - }); - }); - }; - - Category.addGroupToPrivilegeTable = function () { - var modal = bootbox.dialog({ - title: '[[admin/manage/categories:alert.find-group]]', - message: '', - show: true, - }); - - modal.on('shown.bs.modal', function () { - var inputEl = modal.find('input'); - - autocomplete.group(inputEl, function (ev, ui) { - socket.emit('admin.categories.setPrivilege', { - cid: ajaxify.data.category.cid, - privilege: ['groups:find', 'groups:read', 'groups:topics:read'], - set: true, - member: ui.item.group.name, - }, function (err) { - if (err) { - return app.alertError(err.message); - } - - Category.refreshPrivilegeTable(); - modal.modal('hide'); - }); - }); - }); - }; - - Category.copyPrivilegesToChildren = function () { - socket.emit('admin.categories.copyPrivilegesToChildren', ajaxify.data.category.cid, function (err) { - if (err) { - return app.alertError(err.message); - } - app.alertSuccess('Privileges copied!'); - }); - }; - - Category.copyPrivilegesFromCategory = function () { - selectCategoryModal(function (cid) { - socket.emit('admin.categories.copyPrivilegesFrom', { toCid: ajaxify.data.category.cid, fromCid: cid }, function (err) { - if (err) { - return app.alertError(err.message); - } - ajaxify.refresh(); - }); - }); - }; - - function selectCategoryModal(categories, callback) { - if (typeof categories === 'function') { - callback = categories; - categories = ajaxify.data.allCategories; - } - Benchpress.parse('admin/partials/categories/select-category', { - categories: categories, - }, function (html) { - translator.translate(html, function (html) { - var modal = bootbox.dialog({ - title: '[[modules:composer.select_category]]', - message: html, - buttons: { - save: { - label: '[[global:select]]', - className: 'btn-primary', - callback: submit, - }, - }, - }); - categorySelector.init(modal.find('[component="category-selector"]')); - function submit(ev) { - ev.preventDefault(); - var selectedCategory = categorySelector.getSelectedCategory(); - if (selectedCategory) { - callback(selectedCategory.cid); - modal.modal('hide'); - } - return false; - } - - modal.find('form').on('submit', submit); - }); - }); - } - return Category; }); diff --git a/public/src/admin/manage/privileges.js b/public/src/admin/manage/privileges.js new file mode 100644 index 0000000000..74aea3a195 --- /dev/null +++ b/public/src/admin/manage/privileges.js @@ -0,0 +1,194 @@ +'use strict'; + +define('admin/manage/privileges', [ + 'autocomplete', + 'translator', + 'benchpress', + 'categorySelector', +], function (autocomplete, translator, Benchpress, categorySelector) { + var Privileges = {}; + + var cid; + + Privileges.init = function () { + cid = ajaxify.data.cid || 0; + + $('#category-selector').on('change', function () { + var val = $(this).val(); + ajaxify.go('admin/manage/privileges/' + (val === 'global' ? '' : $(this).val())); + }); + + + Privileges.setupPrivilegeTable(); + }; + + Privileges.setupPrivilegeTable = function () { + $('.privilege-table-container').on('change', 'input[type="checkbox"]', function () { + var checkboxEl = $(this); + var privilege = checkboxEl.parent().attr('data-privilege'); + var state = checkboxEl.prop('checked'); + var rowEl = checkboxEl.parents('tr'); + var member = rowEl.attr('data-group-name') || rowEl.attr('data-uid'); + var isPrivate = parseInt(rowEl.attr('data-private') || 0, 10); + var isGroup = rowEl.attr('data-group-name') !== undefined; + + if (member) { + if (isGroup && privilege === 'groups:moderate' && !isPrivate && state) { + bootbox.confirm('[[admin/manage/categories:alert.confirm-moderate]]', function (confirm) { + if (confirm) { + Privileges.setPrivilege(member, privilege, state, checkboxEl); + } else { + checkboxEl.prop('checked', !checkboxEl.prop('checked')); + } + }); + } else { + Privileges.setPrivilege(member, privilege, state, checkboxEl); + } + } else { + app.alertError('[[error:invalid-data]]'); + } + }); + + $('.privilege-table-container').on('click', '[data-action="search.user"]', Privileges.addUserToPrivilegeTable); + $('.privilege-table-container').on('click', '[data-action="search.group"]', Privileges.addGroupToPrivilegeTable); + $('.privilege-table-container').on('click', '[data-action="copyToChildren"]', Privileges.copyPrivilegesToChildren); + $('.privilege-table-container').on('click', '[data-action="copyPrivilegesFrom"]', Privileges.copyPrivilegesFromCategory); + + Privileges.exposeAssumedPrivileges(); + }; + + Privileges.refreshPrivilegeTable = function () { + socket.emit('admin.categories.getPrivilegeSettings', cid, function (err, privileges) { + if (err) { + return app.alertError(err.message); + } + var tpl = cid ? 'admin/partials/categories/privileges' : 'admin/partials/global/privileges'; + Benchpress.parse(tpl, { + privileges: privileges, + }, function (html) { + translator.translate(html, function (html) { + $('.privilege-table-container').html(html); + Privileges.exposeAssumedPrivileges(); + }); + }); + }); + }; + + Privileges.exposeAssumedPrivileges = function () { + /* + If registered-users has a privilege enabled, then all users and groups of that privilege + should be assumed to have that privilege as well, even if not set in the db, so reflect + this arrangement in the table + */ + var privs = []; + $('.privilege-table tr[data-group-name="registered-users"] td input[type="checkbox"]').parent().each(function (idx, el) { + if ($(el).find('input').prop('checked')) { + privs.push(el.getAttribute('data-privilege')); + } + }); + for (var x = 0, numPrivs = privs.length; x < numPrivs; x += 1) { + var inputs = $('.privilege-table tr[data-group-name]:not([data-group-name="registered-users"],[data-group-name="guests"]) td[data-privilege="' + privs[x] + '"] input'); + inputs.each(function (idx, el) { + if (!el.checked) { + el.indeterminate = true; + } + }); + } + }; + + Privileges.setPrivilege = function (member, privilege, state, checkboxEl) { + socket.emit('admin.categories.setPrivilege', { + cid: cid, + privilege: privilege, + set: state, + member: member, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + + checkboxEl.replaceWith(''); + Privileges.refreshPrivilegeTable(); + }); + }; + + Privileges.addUserToPrivilegeTable = function () { + var modal = bootbox.dialog({ + title: '[[admin/manage/categories:alert.find-user]]', + message: '', + show: true, + }); + + modal.on('shown.bs.modal', function () { + var inputEl = modal.find('input'); + + autocomplete.user(inputEl, function (ev, ui) { + var defaultPrivileges = cid ? ['find', 'read', 'topics:read'] : ['chat']; + socket.emit('admin.categories.setPrivilege', { + cid: cid, + privilege: defaultPrivileges, + set: true, + member: ui.item.user.uid, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + + Privileges.refreshPrivilegeTable(); + modal.modal('hide'); + }); + }); + }); + }; + + Privileges.addGroupToPrivilegeTable = function () { + var modal = bootbox.dialog({ + title: '[[admin/manage/categories:alert.find-group]]', + message: '', + show: true, + }); + + modal.on('shown.bs.modal', function () { + var inputEl = modal.find('input'); + + autocomplete.group(inputEl, function (ev, ui) { + var defaultPrivileges = cid ? ['groups:find', 'groups:read', 'groups:topics:read'] : ['groups:chat']; + socket.emit('admin.categories.setPrivilege', { + cid: cid, + privilege: defaultPrivileges, + set: true, + member: ui.item.group.name, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + + Privileges.refreshPrivilegeTable(); + modal.modal('hide'); + }); + }); + }); + }; + + Privileges.copyPrivilegesToChildren = function () { + socket.emit('admin.categories.copyPrivilegesToChildren', cid, function (err) { + if (err) { + return app.alertError(err.message); + } + app.alertSuccess('Privileges copied!'); + }); + }; + + Privileges.copyPrivilegesFromCategory = function () { + categorySelector.modal(function (fromCid) { + socket.emit('admin.categories.copyPrivilegesFrom', { toCid: cid, fromCid: fromCid }, function (err) { + if (err) { + return app.alertError(err.message); + } + ajaxify.refresh(); + }); + }); + }; + + return Privileges; +}); diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index b6c7b7aa03..96da2ef057 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -127,36 +127,6 @@ define('admin/manage/users', ['translator', 'benchpress'], function (translator, socket.emit('admin.user.resetLockouts', uids, done('[[admin/manage/users:alerts.lockout-reset-success]]')); }); - $('.admin-user').on('click', function () { - var uids = getSelectedUids(); - if (!uids.length) { - return; - } - - if (uids.indexOf(app.user.uid.toString()) !== -1) { - app.alertError('[[admin/manage/users:alerts.no-remove-yourself-admin]]'); - } else { - socket.emit('admin.user.makeAdmins', uids, done('[[admin/manage/users:alerts.make-admin-success]]', '.administrator', true)); - } - }); - - $('.remove-admin-user').on('click', function () { - var uids = getSelectedUids(); - if (!uids.length) { - return; - } - - if (uids.indexOf(app.user.uid.toString()) !== -1) { - app.alertError('[[admin/manage/users:alerts.no-remove-yourself-admin]]'); - } else { - bootbox.confirm('[[admin/manage/users:alerts.confirm-remove-admin]]', function (confirm) { - if (confirm) { - socket.emit('admin.user.removeAdmins', uids, done('[[admin/manage/users:alerts.remove-admin-success]]', '.administrator', false)); - } - }); - } - }); - $('.validate-email').on('click', function () { var uids = getSelectedUids(); if (!uids.length) { diff --git a/public/src/client/account/edit/password.js b/public/src/client/account/edit/password.js index 44723014ce..1585a85577 100644 --- a/public/src/client/account/edit/password.js +++ b/public/src/client/account/edit/password.js @@ -63,7 +63,7 @@ define('forum/account/edit/password', ['forum/account/header', 'translator', 'zx onPasswordConfirmChanged(); var btn = $(this); - if ((passwordvalid && passwordsmatch) || app.user.isAdmin) { + if (passwordvalid && passwordsmatch) { btn.addClass('disabled').find('i').removeClass('hide'); socket.emit('user.changePassword', { currentPassword: currentPassword.val(), diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 59e923fb8a..b0ce8883ce 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -335,7 +335,7 @@ define('forum/chats', [ ajaxify.data = payload; Chats.setActive(); Chats.addEventListeners(); - + messages.scrollToBottom($('.expanded-chat ul')); if (history.pushState) { history.pushState({ url: 'user/' + payload.userslug + '/chats/' + payload.roomId, diff --git a/public/src/modules/autocomplete.js b/public/src/modules/autocomplete.js index 68cabdb45b..6c32cb36f2 100644 --- a/public/src/modules/autocomplete.js +++ b/public/src/modules/autocomplete.js @@ -29,6 +29,11 @@ define('autocomplete', function () { uid: user.uid, name: user.username, slug: user.userslug, + username: user.username, + userslug: user.userslug, + picture: user.picture, + 'icon:text': user['icon:text'], + 'icon:bgColor': user['icon:bgColor'], }, }; }); diff --git a/public/src/modules/categorySelector.js b/public/src/modules/categorySelector.js index 882206a42d..b9b8c9e3a4 100644 --- a/public/src/modules/categorySelector.js +++ b/public/src/modules/categorySelector.js @@ -1,7 +1,6 @@ 'use strict'; - -define('categorySelector', function () { +define('categorySelector', ['benchpress', 'translator'], function (Benchpress, translator) { var categorySelector = {}; var selectedCategory; var el; @@ -29,6 +28,42 @@ define('categorySelector', function () { el.find('[component="category-selector-selected"]').html(categoryEl.find('[component="category-markup"]').html()); }; + categorySelector.modal = function (categories, callback) { + if (typeof categories === 'function') { + callback = categories; + categories = ajaxify.data.allCategories; + } + Benchpress.parse('admin/partials/categories/select-category', { + categories: categories, + }, function (html) { + translator.translate(html, function (html) { + var modal = bootbox.dialog({ + title: '[[modules:composer.select_category]]', + message: html, + buttons: { + save: { + label: '[[global:select]]', + className: 'btn-primary', + callback: submit, + }, + }, + }); + categorySelector.init(modal.find('[component="category-selector"]')); + function submit(ev) { + ev.preventDefault(); + var selectedCategory = categorySelector.getSelectedCategory(); + if (selectedCategory) { + callback(selectedCategory.cid); + modal.modal('hide'); + } + return false; + } + + modal.find('form').on('submit', submit); + }); + }); + }; + return categorySelector; }); diff --git a/src/categories.js b/src/categories.js index 6013091050..452762ae38 100644 --- a/src/categories.js +++ b/src/categories.js @@ -338,7 +338,7 @@ Categories.buildForSelect = function (uid, privilege, callback) { }; Categories.buildForSelectCategories = function (categories, callback) { - function recursive(category, categoriesData, level) { + function recursive(category, categoriesData, level, depth) { if (category.link) { return; } @@ -347,10 +347,11 @@ Categories.buildForSelectCategories = function (categories, callback) { category.value = category.cid; category.level = level; category.text = level + bullet + category.name; + category.depth = depth; categoriesData.push(category); category.children.forEach(function (child) { - recursive(child, categoriesData, '    ' + level); + recursive(child, categoriesData, '    ' + level, depth + 1); }); } @@ -361,7 +362,7 @@ Categories.buildForSelectCategories = function (categories, callback) { }); categories.forEach(function (category) { - recursive(category, categoriesData, ''); + recursive(category, categoriesData, '', 0); }); callback(null, categoriesData); }; diff --git a/src/categories/create.js b/src/categories/create.js index d4a74084d9..9cd698b6f3 100644 --- a/src/categories/create.js +++ b/src/categories/create.js @@ -62,7 +62,6 @@ module.exports = function (Categories) { 'posts:edit', 'posts:delete', 'topics:delete', - 'upload:post:image', ]; async.series([ diff --git a/src/categories/data.js b/src/categories/data.js index 73c9300902..a1a9d5c587 100644 --- a/src/categories/data.js +++ b/src/categories/data.js @@ -31,7 +31,11 @@ module.exports = function (Categories) { category.name = validator.escape(String(category.name || '')); category.disabled = category.hasOwnProperty('disabled') ? parseInt(category.disabled, 10) === 1 : undefined; category.isSection = category.hasOwnProperty('isSection') ? parseInt(category.isSection, 10) === 1 : undefined; - category.icon = category.icon || 'hidden'; + + if (category.hasOwnProperty('icon')) { + category.icon = category.icon || 'hidden'; + } + if (category.hasOwnProperty('post_count')) { category.post_count = category.post_count || 0; category.totalPostCount = category.post_count; diff --git a/src/cli/package-install.js b/src/cli/package-install.js index 5f6f9917a5..e923094b05 100644 --- a/src/cli/package-install.js +++ b/src/cli/package-install.js @@ -30,7 +30,7 @@ function updatePackageFile() { exports.updatePackageFile = updatePackageFile; function installAll() { - process.stdout.write('\n'); + process.stdout.write(' started\n'.green); var prod = global.env !== 'development'; var command = 'npm install'; diff --git a/src/cli/upgrade-plugins.js b/src/cli/upgrade-plugins.js index 3be00cb5d1..a61a711bf7 100644 --- a/src/cli/upgrade-plugins.js +++ b/src/cli/upgrade-plugins.js @@ -212,7 +212,7 @@ function upgradePlugins(callback) { }); } else { console.log('Package upgrades skipped'.yellow + '. Check for upgrades at any time by running "'.reset + './nodebb upgrade -p'.green + '".'.reset); - callback(null, true); + callback(); } }); }); diff --git a/src/cli/upgrade.js b/src/cli/upgrade.js index e5ab2b6c0c..2462f1f168 100644 --- a/src/cli/upgrade.js +++ b/src/cli/upgrade.js @@ -16,6 +16,7 @@ var steps = { handler: function (next) { packageInstall.updatePackageFile(); packageInstall.preserveExtraneousPlugins(); + process.stdout.write(' OK\n'.green); next(); }, }, @@ -54,11 +55,8 @@ function runSteps(tasks) { tasks = tasks.map(function (key, i) { return function (next) { process.stdout.write('\n' + ((i + 1) + '. ').bold + steps[key].message.yellow); - return steps[key].handler(function (err, inhibitOk) { + return steps[key].handler(function (err) { if (err) { return next(err); } - if (!inhibitOk) { - process.stdout.write(' OK'.green + '\n'.reset); - } next(); }); }; diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js index c3b9990c26..ff5a07d157 100644 --- a/src/controllers/accounts/chats.js +++ b/src/controllers/accounts/chats.js @@ -5,6 +5,7 @@ var async = require('async'); var messaging = require('../../messaging'); var meta = require('../../meta'); var user = require('../../user'); +var privileges = require('../../privileges'); var helpers = require('../helpers'); var chatsController = module.exports; @@ -26,6 +27,13 @@ chatsController.get = function (req, res, callback) { if (!uid) { return callback(); } + + privileges.global.can('chat', req.uid, next); + }, + function (canChat, next) { + if (!canChat) { + return next(new Error('[[error:no-privileges]]')); + } messaging.getRecentChats(req.uid, uid, 0, 19, next); }, function (_recentChats, next) { diff --git a/src/controllers/admin.js b/src/controllers/admin.js index 6ef000fa14..dc3b6862ae 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -3,6 +3,8 @@ var adminController = { dashboard: require('./admin/dashboard'), categories: require('./admin/categories'), + privileges: require('./admin/privileges'), + adminsMods: require('./admin/admins-mods'), tags: require('./admin/tags'), postQueue: require('./admin/postqueue'), blacklist: require('./admin/blacklist'), diff --git a/src/controllers/admin/admins-mods.js b/src/controllers/admin/admins-mods.js new file mode 100644 index 0000000000..97d5828c16 --- /dev/null +++ b/src/controllers/admin/admins-mods.js @@ -0,0 +1,50 @@ +'use strict'; + +var async = require('async'); + +var groups = require('../../groups'); +var categories = require('../../categories'); + +var AdminsMods = module.exports; + +AdminsMods.get = function (req, res, next) { + async.waterfall([ + function (next) { + async.parallel({ + admins: function (next) { + groups.get('administrators', { uid: req.uid }, next); + }, + globalMods: function (next) { + groups.get('Global Moderators', { uid: req.uid }, next); + }, + categories: function (next) { + getModeratorsOfCategories(req.uid, next); + }, + }, next); + }, + function (results) { + res.render('admin/manage/admins-mods', results); + }, + ], next); +}; + +function getModeratorsOfCategories(uid, callback) { + async.waterfall([ + function (next) { + categories.buildForSelect(uid, 'find', next); + }, + function (categoryData, next) { + async.map(categoryData, function (category, next) { + async.waterfall([ + function (next) { + categories.getModerators(category.cid, next); + }, + function (moderators, next) { + category.moderators = moderators; + next(null, category); + }, + ], next); + }, next); + }, + ], callback); +} diff --git a/src/controllers/admin/categories.js b/src/controllers/admin/categories.js index 0b78912644..e03b51745c 100644 --- a/src/controllers/admin/categories.js +++ b/src/controllers/admin/categories.js @@ -3,7 +3,6 @@ var async = require('async'); var categories = require('../../categories'); -var privileges = require('../../privileges'); var analytics = require('../../analytics'); var plugins = require('../../plugins'); var translator = require('../../translator'); @@ -15,7 +14,6 @@ categoriesController.get = function (req, res, callback) { function (next) { async.parallel({ category: async.apply(categories.getCategories, [req.params.category_id], req.user.uid), - privileges: async.apply(privileges.categories.list, req.params.category_id), allCategories: async.apply(categories.buildForSelect, req.uid, 'read'), }, next); }, @@ -36,7 +34,6 @@ categoriesController.get = function (req, res, callback) { req: req, res: res, category: category, - privileges: data.privileges, allCategories: data.allCategories, }, next); }, @@ -44,7 +41,6 @@ categoriesController.get = function (req, res, callback) { data.category.name = translator.escape(String(data.category.name)); res.render('admin/manage/category', { category: data.category, - privileges: data.privileges, allCategories: data.allCategories, }); }, diff --git a/src/controllers/admin/privileges.js b/src/controllers/admin/privileges.js new file mode 100644 index 0000000000..92dbe27ef9 --- /dev/null +++ b/src/controllers/admin/privileges.js @@ -0,0 +1,39 @@ +'use strict'; + +var async = require('async'); + +var categories = require('../../categories'); +var privileges = require('../../privileges'); + +var privilegesController = module.exports; + +privilegesController.get = function (req, res, callback) { + var cid = req.params.cid ? req.params.cid : 0; + async.waterfall([ + function (next) { + async.parallel({ + privileges: function (next) { + if (!cid) { + privileges.global.list(next); + } else { + privileges.categories.list(cid, next); + } + }, + allCategories: async.apply(categories.buildForSelect, req.uid, 'read'), + }, next); + }, + function (data) { + data.allCategories.forEach(function (category) { + if (category) { + category.selected = parseInt(category.cid, 10) === parseInt(cid, 10); + } + }); + + res.render('admin/manage/privileges', { + privileges: data.privileges, + allCategories: data.allCategories, + cid: cid, + }); + }, + ], callback); +}; diff --git a/src/controllers/home.js b/src/controllers/home.js index 6c67e7aaa2..35a6cfe6a0 100644 --- a/src/controllers/home.js +++ b/src/controllers/home.js @@ -1,61 +1,56 @@ 'use strict'; +var async = require('async'); var plugins = require('../plugins'); var meta = require('../meta'); var user = require('../user'); -var pubsub = require('../pubsub'); -var adminHomePageRoute; -var getRoute; - -function configUpdated() { - adminHomePageRoute = (meta.config.homePageRoute || meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories'; - getRoute = parseInt(meta.config.allowUserHomePage, 10) ? getRouteAllowUserHomePage : getRouteDisableUserHomePage; -} - -function getRouteDisableUserHomePage(uid, next) { - next(null, adminHomePageRoute); +function adminHomePageRoute() { + return (meta.config.homePageRoute || meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories'; } -function getRouteAllowUserHomePage(uid, next) { - user.getSettings(uid, function (err, settings) { - if (err) { - return next(err); - } - - var route = adminHomePageRoute; - - if (settings.homePageRoute !== 'undefined' && settings.homePageRoute !== 'none') { - route = settings.homePageRoute || route; - } - - next(null, route); - }); +function getUserHomeRoute(uid, callback) { + async.waterfall([ + function (next) { + user.getSettings(uid, next); + }, + function (settings, next) { + var route = adminHomePageRoute(); + + if (settings.homePageRoute !== 'undefined' && settings.homePageRoute !== 'none') { + route = settings.homePageRoute || route; + } + + next(null, route); + }, + ], callback); } -pubsub.on('config:update', configUpdated); -configUpdated(); - function rewrite(req, res, next) { if (req.path !== '/' && req.path !== '/api/' && req.path !== '/api') { return next(); } - getRoute(req.uid, function (err, route) { - if (err) { - return next(err); - } - - var hook = 'action:homepage.get:' + route; - - if (!plugins.hasListeners(hook)) { - req.url = req.path + (!req.path.endsWith('/') ? '/' : '') + route; - } else { - res.locals.homePageRoute = route; - } - - next(); - }); + async.waterfall([ + function (next) { + if (parseInt(meta.config.allowUserHomePage, 10)) { + getUserHomeRoute(req.uid, next); + } else { + next(null, adminHomePageRoute()); + } + }, + function (route, next) { + var hook = 'action:homepage.get:' + route; + + if (!plugins.hasListeners(hook)) { + req.url = req.path + (!req.path.endsWith('/') ? '/' : '') + route; + } else { + res.locals.homePageRoute = route; + } + + next(); + }, + ], next); } exports.rewrite = rewrite; diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index 0a91cd5dcc..e7e77c4a4f 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -37,9 +37,6 @@ uploadsController.upload = function (req, res, filesIterator) { uploadsController.uploadPost = function (req, res, next) { uploadsController.upload(req, res, function (uploadedFile, next) { - if (!parseInt(req.body.cid, 10)) { - return next(new Error('[[error:category-not-selected]]')); - } var isImage = uploadedFile.type.match(/image./); if (isImage) { uploadAsImage(req, uploadedFile, next); @@ -52,7 +49,7 @@ uploadsController.uploadPost = function (req, res, next) { function uploadAsImage(req, uploadedFile, callback) { async.waterfall([ function (next) { - privileges.categories.can('upload:post:image', req.body.cid, req.uid, next); + privileges.global.can('upload:post:image', req.uid, next); }, function (canUpload, next) { if (!canUpload) { @@ -82,7 +79,7 @@ function uploadAsImage(req, uploadedFile, callback) { function uploadAsFile(req, uploadedFile, callback) { async.waterfall([ function (next) { - privileges.categories.can('upload:post:file', req.body.cid, req.uid, next); + privileges.global.can('upload:post:file', req.uid, next); }, function (canUpload, next) { if (!canUpload) { diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js index 278ae6c413..b8ceaa4f6f 100644 --- a/src/database/mongo/main.js +++ b/src/database/mongo/main.js @@ -83,8 +83,8 @@ module.exports = function (db, module) { if (!key) { return callback(); } - db.collection('objects').findAndModify({ _key: key }, {}, { $inc: { value: 1 } }, { new: true, upsert: true }, function (err, result) { - callback(err, result && result.value ? result.value.value : null); + db.collection('objects').findAndModify({ _key: key }, {}, { $inc: { data: 1 } }, { new: true, upsert: true }, function (err, result) { + callback(err, result && result.value ? result.value.data : null); }); }; @@ -108,6 +108,7 @@ module.exports = function (db, module) { if (!data) { return callback(null, null); } + delete data.expireAt; var keys = Object.keys(data); if (keys.length === 4 && data.hasOwnProperty('_key') && data.hasOwnProperty('score') && data.hasOwnProperty('value')) { return callback(null, 'zset'); diff --git a/src/install.js b/src/install.js index b55b1ed08e..2906adc9e8 100644 --- a/src/install.js +++ b/src/install.js @@ -353,6 +353,11 @@ function createGlobalModeratorsGroup(next) { ], next); } +function giveGlobalPrivileges(next) { + var privileges = require('./privileges'); + privileges.global.give(['chat', 'upload:post:image'], 'registered-users', next); +} + function createCategories(next) { var Categories = require('./categories'); @@ -498,6 +503,7 @@ install.setup = function (callback) { createCategories, createAdministrator, createGlobalModeratorsGroup, + giveGlobalPrivileges, createMenuItems, createWelcomePost, enableDefaultPlugins, diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index 3116c31a2b..f0220f2929 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -76,6 +76,7 @@ module.exports = function (Messaging) { notifications.create({ type: 'new-chat', + subject: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', bodyLong: messageObj.content, nid: 'chat_' + fromuid + '_' + roomId, diff --git a/src/meta/build.js b/src/meta/build.js index 2beb5f8af9..552c9aa55c 100644 --- a/src/meta/build.js +++ b/src/meta/build.js @@ -212,7 +212,7 @@ function build(targets, callback) { } winston.info('[build] Asset compilation successful. Completed in ' + totalTime + 'sec.'); - callback(null, true); + callback(); }); } diff --git a/src/middleware/header.js b/src/middleware/header.js index 5a896fcdd7..a0cf65d396 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -12,6 +12,7 @@ var meta = require('../meta'); var plugins = require('../plugins'); var navigation = require('../navigation'); var translator = require('../translator'); +var privileges = require('../privileges'); var utils = require('../utils'); var controllers = { @@ -77,6 +78,9 @@ module.exports = function (middleware) { isModerator: function (next) { user.isModeratorOfAnyCategory(req.uid, next); }, + privileges: function (next) { + privileges.global.get(req.uid, next); + }, user: function (next) { var userData = { uid: 0, @@ -132,6 +136,8 @@ module.exports = function (middleware) { results.user.isAdmin = results.isAdmin; results.user.isGlobalMod = results.isGlobalMod; results.user.isMod = !!results.isModerator; + results.user.privileges = results.privileges; + results.user.uid = parseInt(results.user.uid, 10); results.user.email = String(results.user.email); results.user['email:confirmed'] = parseInt(results.user['email:confirmed'], 10) === 1; @@ -183,6 +189,7 @@ module.exports = function (middleware) { templateValues.isAdmin = results.user.isAdmin; templateValues.isGlobalMod = results.user.isGlobalMod; templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod; + templateValues.canChat = results.canChat && parseInt(meta.config.disableChat, 10) !== 1; templateValues.user = results.user; templateValues.userJSON = jsesc(JSON.stringify(results.user), { isScriptContext: true }); templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS; diff --git a/src/notifications.js b/src/notifications.js index 6a7940d50d..4bf9782e61 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -220,7 +220,7 @@ function pushToUids(uids, notification, callback) { async.eachLimit(uids, 3, function (uid, next) { emailer.send('notification', uid, { path: notification.path, - subject: '[[notifications:new_notification_from, ' + meta.config.title + ']]', + subject: notification.subject || '[[notifications:new_notification_from, ' + meta.config.title + ']]', intro: utils.stripHTMLTags(notification.bodyShort), body: utils.stripHTMLTags(notification.bodyLong || ''), showUnsubscribe: true, diff --git a/src/plugins.js b/src/plugins.js index 653edee5fe..f11ed63494 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -213,8 +213,8 @@ Plugins.list = function (matching, callback) { require('request')(url, { json: true, }, function (err, res, body) { - if (err) { - winston.error('Error parsing plugins', err); + if (err || (res && res.statusCode !== 200)) { + winston.error('Error loading ' + url, err || body); return Plugins.normalise([], callback); } @@ -225,7 +225,7 @@ Plugins.list = function (matching, callback) { Plugins.normalise = function (apiReturn, callback) { var pluginMap = {}; var dependencies = require(path.join(nconf.get('base_dir'), 'package.json')).dependencies; - apiReturn = apiReturn || []; + apiReturn = Array.isArray(apiReturn) ? apiReturn : []; for (var i = 0; i < apiReturn.length; i += 1) { apiReturn[i].id = apiReturn[i].name; apiReturn[i].installed = false; diff --git a/src/privileges.js b/src/privileges.js index c1ac018ec7..64d16d3e5c 100644 --- a/src/privileges.js +++ b/src/privileges.js @@ -12,8 +12,6 @@ privileges.privilegeLabels = [ { name: 'Edit Posts' }, { name: 'Delete Posts' }, { name: 'Delete Topics' }, - { name: 'Upload Images' }, - { name: 'Upload Files' }, { name: 'Purge' }, { name: 'Moderate' }, ]; @@ -28,8 +26,6 @@ privileges.userPrivilegeList = [ 'posts:edit', 'posts:delete', 'topics:delete', - 'upload:post:image', - 'upload:post:file', 'purge', 'moderate', ]; @@ -40,6 +36,7 @@ privileges.groupPrivilegeList = privileges.userPrivilegeList.map(function (privi privileges.privilegeList = privileges.userPrivilegeList.concat(privileges.groupPrivilegeList); +require('./privileges/global')(privileges); require('./privileges/categories')(privileges); require('./privileges/topics')(privileges); require('./privileges/posts')(privileges); diff --git a/src/privileges/categories.js b/src/privileges/categories.js index 69aec85135..60822c4e46 100644 --- a/src/privileges/categories.js +++ b/src/privileges/categories.js @@ -15,121 +15,20 @@ module.exports = function (privileges) { privileges.categories.list = function (cid, callback) { // Method used in admin/category controller to show all users/groups with privs in that given cid - var privilegeLabels = privileges.privilegeLabels.slice(); - var userPrivilegeList = privileges.userPrivilegeList.slice(); - var groupPrivilegeList = privileges.groupPrivilegeList.slice(); async.waterfall([ function (next) { async.parallel({ labels: function (next) { async.parallel({ - users: async.apply(plugins.fireHook, 'filter:privileges.list_human', privilegeLabels), - groups: async.apply(plugins.fireHook, 'filter:privileges.groups.list_human', privilegeLabels), + users: async.apply(plugins.fireHook, 'filter:privileges.list_human', privileges.privilegeLabels.slice()), + groups: async.apply(plugins.fireHook, 'filter:privileges.groups.list_human', privileges.privilegeLabels.slice()), }, next); }, users: function (next) { - var userPrivileges; - var memberSets; - async.waterfall([ - async.apply(plugins.fireHook, 'filter:privileges.list', userPrivilegeList), - function (_privs, next) { - userPrivileges = _privs; - groups.getMembersOfGroups(userPrivileges.map(function (privilege) { - return 'cid:' + cid + ':privileges:' + privilege; - }), next); - }, - function (_memberSets, next) { - memberSets = _memberSets.map(function (set) { - return set.map(function (uid) { - return parseInt(uid, 10); - }); - }); - - var members = _.uniq(_.flatten(memberSets)); - - user.getUsersFields(members, ['picture', 'username'], next); - }, - function (memberData, next) { - memberData.forEach(function (member) { - member.privileges = {}; - for (var x = 0, numPrivs = userPrivileges.length; x < numPrivs; x += 1) { - member.privileges[userPrivileges[x]] = memberSets[x].indexOf(parseInt(member.uid, 10)) !== -1; - } - }); - - next(null, memberData); - }, - ], next); + helpers.getUserPrivileges(cid, 'filter:privileges.list', privileges.userPrivilegeList, next); }, groups: function (next) { - var groupPrivileges; - async.waterfall([ - async.apply(plugins.fireHook, 'filter:privileges.groups.list', groupPrivilegeList), - function (_privs, next) { - groupPrivileges = _privs; - async.parallel({ - memberSets: function (next) { - groups.getMembersOfGroups(groupPrivileges.map(function (privilege) { - return 'cid:' + cid + ':privileges:' + privilege; - }), next); - }, - groupNames: function (next) { - groups.getGroups('groups:createtime', 0, -1, next); - }, - }, next); - }, - function (results, next) { - var memberSets = results.memberSets; - var uniqueGroups = _.uniq(_.flatten(memberSets)); - - var groupNames = results.groupNames.filter(function (groupName) { - return groupName.indexOf(':privileges:') === -1 && uniqueGroups.indexOf(groupName) !== -1; - }); - - groupNames = groups.ephemeralGroups.concat(groupNames); - var registeredUsersIndex = groupNames.indexOf('registered-users'); - if (registeredUsersIndex !== -1) { - groupNames.splice(0, 0, groupNames.splice(registeredUsersIndex, 1)[0]); - } else { - groupNames = ['registered-users'].concat(groupNames); - } - - var adminIndex = groupNames.indexOf('administrators'); - if (adminIndex !== -1) { - groupNames.splice(adminIndex, 1); - } - - var memberPrivs; - - var memberData = groupNames.map(function (member) { - memberPrivs = {}; - - for (var x = 0, numPrivs = groupPrivileges.length; x < numPrivs; x += 1) { - memberPrivs[groupPrivileges[x]] = memberSets[x].indexOf(member) !== -1; - } - return { - name: member, - privileges: memberPrivs, - }; - }); - - next(null, memberData); - }, - function (memberData, next) { - // Grab privacy info for the groups as well - async.map(memberData, function (member, next) { - async.waterfall([ - function (next) { - groups.isPrivate(member.name, next); - }, - function (isPrivate, next) { - member.isPrivate = isPrivate; - next(null, member); - }, - ], next); - }, next); - }, - ], next); + helpers.getGroupPrivileges(cid, 'filter:privileges.groups.list', privileges.groupPrivilegeList, next); }, }, next); }, @@ -299,19 +198,13 @@ module.exports = function (privileges) { }; privileges.categories.give = function (privileges, cid, groupName, callback) { - giveOrRescind(groups.join, privileges, cid, groupName, callback); + helpers.giveOrRescind(groups.join, privileges, cid, groupName, callback); }; privileges.categories.rescind = function (privileges, cid, groupName, callback) { - giveOrRescind(groups.leave, privileges, cid, groupName, callback); + helpers.giveOrRescind(groups.leave, privileges, cid, groupName, callback); }; - function giveOrRescind(method, privileges, cid, groupName, callback) { - async.eachSeries(privileges, function (privilege, next) { - method('cid:' + cid + ':privileges:groups:' + privilege, groupName, next); - }, callback); - } - privileges.categories.canMoveAllTopics = function (currentCid, targetCid, uid, callback) { async.waterfall([ function (next) { diff --git a/src/privileges/global.js b/src/privileges/global.js new file mode 100644 index 0000000000..f1f88c4fff --- /dev/null +++ b/src/privileges/global.js @@ -0,0 +1,128 @@ + +'use strict'; + +var async = require('async'); +var _ = require('lodash'); + +var user = require('../user'); +var groups = require('../groups'); +var helpers = require('./helpers'); +var plugins = require('../plugins'); + +module.exports = function (privileges) { + privileges.global = {}; + + privileges.global.privilegeLabels = [ + { name: 'Chat' }, + { name: 'Upload Images' }, + { name: 'Upload Files' }, + ]; + + privileges.global.userPrivilegeList = [ + 'chat', + 'upload:post:image', + 'upload:post:file', + ]; + + privileges.global.groupPrivilegeList = privileges.global.userPrivilegeList.map(function (privilege) { + return 'groups:' + privilege; + }); + + privileges.global.list = function (callback) { + async.waterfall([ + function (next) { + async.parallel({ + labels: function (next) { + async.parallel({ + users: async.apply(plugins.fireHook, 'filter:privileges.global.list_human', privileges.global.privilegeLabels.slice()), + groups: async.apply(plugins.fireHook, 'filter:privileges.global.groups.list_human', privileges.global.privilegeLabels.slice()), + }, next); + }, + users: function (next) { + helpers.getUserPrivileges(0, 'filter:privileges.global.list', privileges.global.userPrivilegeList, next); + }, + groups: function (next) { + helpers.getGroupPrivileges(0, 'filter:privileges.global.groups.list', privileges.global.groupPrivilegeList, next); + }, + }, next); + }, + function (payload, next) { + // This is a hack because I can't do {labels.users.length} to echo the count in templates.js + payload.columnCount = payload.labels.users.length + 2; + next(null, payload); + }, + ], callback); + }; + + privileges.global.get = function (uid, callback) { + async.waterfall([ + function (next) { + async.parallel({ + privileges: function (next) { + helpers.isUserAllowedTo(privileges.global.userPrivilegeList, uid, 0, next); + }, + isAdministrator: function (next) { + user.isAdministrator(uid, next); + }, + isGlobalModerator: function (next) { + user.isGlobalModerator(uid, next); + }, + }, next); + }, + function (results, next) { + var privData = _.zipObject(privileges.global.userPrivilegeList, results.privileges); + var isAdminOrMod = results.isAdministrator || results.isGlobalModerator; + + plugins.fireHook('filter:privileges.global.get', { + chat: privData.chat || isAdminOrMod, + 'upload:post:image': privData['upload:post:image'] || isAdminOrMod, + 'upload:post:file': privData['upload:post:file'] || isAdminOrMod, + }, next); + }, + ], callback); + }; + + privileges.global.can = function (privilege, uid, callback) { + helpers.some([ + function (next) { + helpers.isUserAllowedTo(privilege, uid, [0], function (err, results) { + next(err, Array.isArray(results) && results.length ? results[0] : false); + }); + }, + function (next) { + user.isGlobalModerator(uid, next); + }, + function (next) { + user.isAdministrator(uid, next); + }, + ], callback); + }; + + privileges.global.give = function (privileges, groupName, callback) { + helpers.giveOrRescind(groups.join, privileges, 0, groupName, callback); + }; + + privileges.global.rescind = function (privileges, groupName, callback) { + helpers.giveOrRescind(groups.leave, privileges, 0, groupName, callback); + }; + + privileges.global.userPrivileges = function (uid, callback) { + var tasks = {}; + + privileges.global.userPrivilegeList.forEach(function (privilege) { + tasks[privilege] = async.apply(groups.isMember, uid, 'cid:0:privileges:' + privilege); + }); + + async.parallel(tasks, callback); + }; + + privileges.global.groupPrivileges = function (groupName, callback) { + var tasks = {}; + + privileges.global.groupPrivilegeList.forEach(function (privilege) { + tasks[privilege] = async.apply(groups.isMember, groupName, 'cid:0:privileges:' + privilege); + }); + + async.parallel(tasks, callback); + }; +}; diff --git a/src/privileges/helpers.js b/src/privileges/helpers.js index 0f56e4f9c8..c3452c495e 100644 --- a/src/privileges/helpers.js +++ b/src/privileges/helpers.js @@ -2,7 +2,11 @@ 'use strict'; var async = require('async'); +var _ = require('lodash'); + var groups = require('../groups'); +var user = require('../user'); +var plugins = require('../plugins'); var helpers = module.exports; @@ -111,3 +115,115 @@ function isGuestAllowedToPrivileges(privileges, cid, callback) { groups.isMemberOfGroups('guests', groupKeys, callback); } + +helpers.getUserPrivileges = function (cid, hookName, userPrivilegeList, callback) { + var userPrivileges; + var memberSets; + async.waterfall([ + async.apply(plugins.fireHook, hookName, userPrivilegeList.slice()), + function (_privs, next) { + userPrivileges = _privs; + groups.getMembersOfGroups(userPrivileges.map(function (privilege) { + return 'cid:' + cid + ':privileges:' + privilege; + }), next); + }, + function (_memberSets, next) { + memberSets = _memberSets.map(function (set) { + return set.map(function (uid) { + return parseInt(uid, 10); + }); + }); + + var members = _.uniq(_.flatten(memberSets)); + + user.getUsersFields(members, ['picture', 'username'], next); + }, + function (memberData, next) { + memberData.forEach(function (member) { + member.privileges = {}; + for (var x = 0, numPrivs = userPrivileges.length; x < numPrivs; x += 1) { + member.privileges[userPrivileges[x]] = memberSets[x].indexOf(parseInt(member.uid, 10)) !== -1; + } + }); + + next(null, memberData); + }, + ], callback); +}; + +helpers.getGroupPrivileges = function (cid, hookName, groupPrivilegeList, callback) { + var groupPrivileges; + async.waterfall([ + async.apply(plugins.fireHook, hookName, groupPrivilegeList.slice()), + function (_privs, next) { + groupPrivileges = _privs; + async.parallel({ + memberSets: function (next) { + groups.getMembersOfGroups(groupPrivileges.map(function (privilege) { + return 'cid:' + cid + ':privileges:' + privilege; + }), next); + }, + groupNames: function (next) { + groups.getGroups('groups:createtime', 0, -1, next); + }, + }, next); + }, + function (results, next) { + var memberSets = results.memberSets; + var uniqueGroups = _.uniq(_.flatten(memberSets)); + + var groupNames = results.groupNames.filter(function (groupName) { + return groupName.indexOf(':privileges:') === -1 && uniqueGroups.indexOf(groupName) !== -1; + }); + + groupNames = groups.ephemeralGroups.concat(groupNames); + var registeredUsersIndex = groupNames.indexOf('registered-users'); + if (registeredUsersIndex !== -1) { + groupNames.splice(0, 0, groupNames.splice(registeredUsersIndex, 1)[0]); + } else { + groupNames = ['registered-users'].concat(groupNames); + } + + var adminIndex = groupNames.indexOf('administrators'); + if (adminIndex !== -1) { + groupNames.splice(adminIndex, 1); + } + + var memberPrivs; + + var memberData = groupNames.map(function (member) { + memberPrivs = {}; + + for (var x = 0, numPrivs = groupPrivileges.length; x < numPrivs; x += 1) { + memberPrivs[groupPrivileges[x]] = memberSets[x].indexOf(member) !== -1; + } + return { + name: member, + privileges: memberPrivs, + }; + }); + + next(null, memberData); + }, + function (memberData, next) { + // Grab privacy info for the groups as well + async.map(memberData, function (member, next) { + async.waterfall([ + function (next) { + groups.isPrivate(member.name, next); + }, + function (isPrivate, next) { + member.isPrivate = isPrivate; + next(null, member); + }, + ], next); + }, next); + }, + ], callback); +}; + +helpers.giveOrRescind = function (method, privileges, cid, groupName, callback) { + async.eachSeries(privileges, function (privilege, next) { + method('cid:' + cid + ':privileges:groups:' + privilege, groupName, next); + }, callback); +}; diff --git a/src/routes/admin.js b/src/routes/admin.js index ba4048516e..db0ce7798c 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -55,6 +55,7 @@ function addRoutes(router, middleware, controllers) { router.get('/manage/categories/:category_id', middlewares, controllers.admin.categories.get); router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics); + router.get('/manage/privileges/:cid?', middlewares, controllers.admin.privileges.get); router.get('/manage/tags', middlewares, controllers.admin.tags.get); router.get('/manage/post-queue', middlewares, controllers.admin.postQueue.get); router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get); @@ -71,6 +72,8 @@ function addRoutes(router, middleware, controllers) { router.get('/manage/users/banned', middlewares, controllers.admin.users.banned); router.get('/manage/registration', middlewares, controllers.admin.users.registrationQueue); + router.get('/manage/admins-mods', middlewares, controllers.admin.adminsMods.get); + router.get('/manage/groups', middlewares, controllers.admin.groups.list); router.get('/manage/groups/:name', middlewares, controllers.admin.groups.get); diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js index 232b2041d1..7bd491c8cd 100644 --- a/src/socket.io/admin/categories.js +++ b/src/socket.io/admin/categories.js @@ -83,7 +83,11 @@ Categories.setPrivilege = function (socket, data, callback) { }; Categories.getPrivilegeSettings = function (socket, cid, callback) { - privileges.categories.list(cid, callback); + if (!parseInt(cid, 10)) { + privileges.global.list(callback); + } else { + privileges.categories.list(cid, callback); + } }; Categories.copyPrivilegesToChildren = function (socket, cid, callback) { diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 7ff5accf40..a0328ab82d 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -11,6 +11,7 @@ var Messaging = require('../messaging'); var utils = require('../utils'); var server = require('./'); var user = require('../user'); +var privileges = require('../privileges'); var SocketModules = module.exports; @@ -73,6 +74,12 @@ SocketModules.chats.newRoom = function (socket, data, callback) { async.waterfall([ function (next) { + privileges.global.can('chat', socket.uid, next); + }, + function (canChat, next) { + if (!canChat) { + return next(new Error('[[error:no-privileges]]')); + } Messaging.canMessageUser(socket.uid, data.touid, next); }, function (next) { @@ -92,6 +99,13 @@ SocketModules.chats.send = function (socket, data, callback) { async.waterfall([ function (next) { + privileges.global.can('chat', socket.uid, next); + }, + function (canChat, next) { + if (!canChat) { + return next(new Error('[[error:no-privileges]]')); + } + plugins.fireHook('filter:messaging.send', { data: data, uid: socket.uid, @@ -133,6 +147,13 @@ SocketModules.chats.loadRoom = function (socket, data, callback) { async.waterfall([ function (next) { + privileges.global.can('chat', socket.uid, next); + }, + function (canChat, next) { + if (!canChat) { + return next(new Error('[[error:no-privileges]]')); + } + Messaging.isUserInRoom(socket.uid, data.roomId, next); }, function (inRoom, next) { @@ -174,6 +195,13 @@ SocketModules.chats.addUserToRoom = function (socket, data, callback) { var uid; async.waterfall([ function (next) { + privileges.global.can('chat', socket.uid, next); + }, + function (canChat, next) { + if (!canChat) { + return next(new Error('[[error:no-privileges]]')); + } + Messaging.getUserCountInRoom(data.roomId, next); }, function (userCount, next) { diff --git a/src/topics/follow.js b/src/topics/follow.js index cf8754bcc5..a590ad2392 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -219,6 +219,7 @@ module.exports = function (Topics) { notifications.create({ type: 'new-reply', + subject: title, bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]', bodyLong: postData.content, pid: postData.pid, diff --git a/src/upgrades/1.8.0/chat_privilege.js b/src/upgrades/1.8.0/chat_privilege.js new file mode 100644 index 0000000000..c4bd2ff8d1 --- /dev/null +++ b/src/upgrades/1.8.0/chat_privilege.js @@ -0,0 +1,12 @@ +'use strict'; + + +var groups = require('../../groups'); + +module.exports = { + name: 'Give chat privilege to registered-users', + timestamp: Date.UTC(2017, 11, 18), + method: function (callback) { + groups.join('cid:0:privileges:groups:chat', 'registered-users', callback); + }, +}; diff --git a/src/upgrades/1.8.0/global_upload_privilege.js b/src/upgrades/1.8.0/global_upload_privilege.js new file mode 100644 index 0000000000..22473a9ee0 --- /dev/null +++ b/src/upgrades/1.8.0/global_upload_privilege.js @@ -0,0 +1,45 @@ +'use strict'; + + +var async = require('async'); +var groups = require('../../groups'); +var privileges = require('../../privileges'); +var db = require('../../database'); + +module.exports = { + name: 'Give upload privilege to registered-users globally if it is given on a category', + timestamp: Date.UTC(2018, 0, 3), + method: function (callback) { + db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) { + if (err) { + return callback(err); + } + async.eachSeries(cids, function (cid, next) { + getGroupPrivileges(cid, function (err, groupPrivileges) { + if (err) { + return next(err); + } + + var privs = []; + if (groupPrivileges['groups:upload:post:image']) { + privs.push('upload:post:image'); + } + if (groupPrivileges['groups:upload:post:file']) { + privs.push('upload:post:file'); + } + privileges.global.give(privs, 'registered-users', next); + }); + }, callback); + }); + }, +}; + +function getGroupPrivileges(cid, callback) { + var tasks = {}; + + ['groups:upload:post:image', 'groups:upload:post:file'].forEach(function (privilege) { + tasks[privilege] = async.apply(groups.isMember, 'registered-users', 'cid:' + cid + ':privileges:' + privilege); + }); + + async.parallel(tasks, callback); +} diff --git a/src/views/admin/manage/admins-mods.tpl b/src/views/admin/manage/admins-mods.tpl new file mode 100644 index 0000000000..76d3982302 --- /dev/null +++ b/src/views/admin/manage/admins-mods.tpl @@ -0,0 +1,64 @@ +
+

[[admin/manage/admins-mods:administrators]]

+
+ +
+ + + +
{admins.members.icon:text}
+ + {admins.members.username} + +
+ +
+ + +
+ +

[[admin/manage/admins-mods:global-moderators]]

+
+ +
+ + + +
{globalMods.members.icon:text}
+ + {globalMods.members.username} + +
+ +
+ +
[[admin/manage/admins-mods:no-global-moderators]]
+ + + +
+ + +
+

[[admin/manage/admins-mods:moderators-of-category, {categories.name}]]

+
+ +
+ + + +
{categories.moderators.icon:text}
+ + {categories.moderators.username} + +
+ +
+ +
[[admin/manage/admins-mods:no-moderators]]
+ + +
+
+ +
diff --git a/src/views/admin/manage/category.tpl b/src/views/admin/manage/category.tpl index 7c39ddee44..cf8ab89312 100644 --- a/src/views/admin/manage/category.tpl +++ b/src/views/admin/manage/category.tpl @@ -2,15 +2,7 @@
- -
+
- +
@@ -174,19 +166,6 @@
- -
-

- [[admin/manage/categories:privileges.description]] -

-

- [[admin/manage/categories:privileges.warning]] -

-
-
- -
-
diff --git a/src/views/admin/manage/privileges.tpl b/src/views/admin/manage/privileges.tpl new file mode 100644 index 0000000000..42d8be735e --- /dev/null +++ b/src/views/admin/manage/privileges.tpl @@ -0,0 +1,31 @@ +
+
+
+
+ +
+
+ +
+ +
+

+ [[admin/manage/categories:privileges.description]] +

+
+
+ + + + + +
+
+
+
diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl index 3eca4f998d..c52cccbe63 100644 --- a/src/views/admin/manage/users.tpl +++ b/src/views/admin/manage/users.tpl @@ -8,9 +8,6 @@