Merge commit 'f9202aeb05d4f12ec92881737ae31cb702a523df' into v1.7.x

This commit is contained in:
Misty (Bot)
2017-12-27 21:48:01 +00:00
116 changed files with 1330 additions and 668 deletions

View File

@@ -17,89 +17,89 @@
"coveralls": "nyc report --reporter=text-lcov | coveralls && rm -r coverage"
},
"dependencies": {
"ace-builds": "^1.2.9",
"async": "2.6.0",
"autoprefixer": "7.1.6",
"bcryptjs": "2.4.3",
"benchpressjs": "^1.2.0",
"body-parser": "^1.18.2",
"bootstrap": "^3.3.7",
"chart.js": "^2.7.0",
"colors": "^1.1.2",
"compression": "^1.7.1",
"commander": "^2.11.0",
"connect-ensure-login": "^0.1.1",
"connect-flash": "^0.1.1",
"connect-mongo": "2.0.0",
"connect-multiparty": "^2.1.0",
"connect-redis": "3.3.2",
"cookie-parser": "^1.4.3",
"cron": "^1.3.0",
"cropperjs": "^1.1.3",
"csurf": "^1.9.0",
"daemon": "^1.1.0",
"express": "^4.16.2",
"express-session": "^1.15.6",
"express-useragent": "1.0.8",
"graceful-fs": "^4.1.11",
"html-to-text": "3.3.0",
"ipaddr.js": "^1.5.4",
"jimp": "0.2.28",
"jquery": "^3.2.1",
"jsesc": "2.5.1",
"json-2-csv": "^2.1.2",
"less": "^2.7.2",
"lodash": "^4.17.4",
"logrotate-stream": "^0.2.5",
"lru-cache": "4.1.1",
"material-design-lite": "^1.3.0",
"mime": "^2.0.3",
"mkdirp": "^0.5.1",
"mongodb": "2.2.33",
"morgan": "^1.9.0",
"mousetrap": "^1.6.1",
"nconf": "^0.9.1",
"nodebb-plugin-composer-default": "6.0.7",
"nodebb-plugin-dbsearch": "2.0.9",
"nodebb-plugin-emoji": "2.0.7",
"nodebb-plugin-emoji-android": "2.0.0",
"nodebb-plugin-markdown": "8.2.0",
"nodebb-plugin-mentions": "2.2.2",
"nodebb-plugin-soundpack-default": "1.0.0",
"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.4",
"nodebb-theme-slick": "1.1.2",
"nodebb-theme-vanilla": "8.1.2",
"nodebb-widget-essentials": "4.0.1",
"nodemailer": "4.4.0",
"passport": "^0.4.0",
"passport-local": "1.0.0",
"postcss": "6.0.14",
"postcss-clean": "1.1.0",
"promise-polyfill": "^6.0.2",
"prompt": "^1.0.0",
"redis": "2.8.0",
"request": "2.83.0",
"rimraf": "2.6.2",
"rss": "^1.2.2",
"sanitize-html": "^1.14.1",
"semver": "^5.4.1",
"serve-favicon": "^2.4.5",
"sitemap": "^1.13.0",
"socket.io": "2.0.4",
"socket.io-client": "2.0.4",
"socket.io-redis": "5.2.0",
"socketio-wildcard": "2.0.0",
"spdx-license-list": "^3.0.1",
"toobusy-js": "^0.5.1",
"uglify-js": "^3.1.5",
"validator": "9.1.2",
"winston": "^2.4.0",
"xml": "^1.0.1",
"xregexp": "3.2.0",
"zxcvbn": "^4.4.2"
"ace-builds": "^1.2.9",
"async": "2.6.0",
"autoprefixer": "7.1.6",
"bcryptjs": "2.4.3",
"benchpressjs": "^1.2.0",
"body-parser": "^1.18.2",
"bootstrap": "^3.3.7",
"chart.js": "^2.7.0",
"colors": "^1.1.2",
"compression": "^1.7.1",
"commander": "^2.11.0",
"connect-ensure-login": "^0.1.1",
"connect-flash": "^0.1.1",
"connect-mongo": "2.0.0",
"connect-multiparty": "^2.1.0",
"connect-redis": "3.3.2",
"cookie-parser": "^1.4.3",
"cron": "^1.3.0",
"cropperjs": "^1.1.3",
"csurf": "^1.9.0",
"daemon": "^1.1.0",
"express": "^4.16.2",
"express-session": "^1.15.6",
"express-useragent": "1.0.8",
"graceful-fs": "^4.1.11",
"html-to-text": "3.3.0",
"ipaddr.js": "^1.5.4",
"jimp": "0.2.28",
"jquery": "^3.2.1",
"jsesc": "2.5.1",
"json-2-csv": "^2.1.2",
"less": "^2.7.2",
"lodash": "^4.17.4",
"logrotate-stream": "^0.2.5",
"lru-cache": "4.1.1",
"material-design-lite": "^1.3.0",
"mime": "^2.0.3",
"mkdirp": "^0.5.1",
"mongodb": "2.2.33",
"morgan": "^1.9.0",
"mousetrap": "^1.6.1",
"nconf": "^0.9.1",
"nodebb-plugin-composer-default": "6.0.7",
"nodebb-plugin-dbsearch": "2.0.9",
"nodebb-plugin-emoji": "2.0.9",
"nodebb-plugin-emoji-android": "2.0.0",
"nodebb-plugin-markdown": "8.2.2",
"nodebb-plugin-mentions": "2.2.2",
"nodebb-plugin-soundpack-default": "1.0.0",
"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-slick": "1.1.2",
"nodebb-theme-vanilla": "8.1.4",
"nodebb-widget-essentials": "4.0.1",
"nodemailer": "4.4.0",
"passport": "^0.4.0",
"passport-local": "1.0.0",
"postcss": "6.0.14",
"postcss-clean": "1.1.0",
"promise-polyfill": "^6.0.2",
"prompt": "^1.0.0",
"redis": "2.8.0",
"request": "2.83.0",
"rimraf": "2.6.2",
"rss": "^1.2.2",
"sanitize-html": "^1.14.1",
"semver": "^5.4.1",
"serve-favicon": "^2.4.5",
"sitemap": "^1.13.0",
"socket.io": "2.0.4",
"socket.io-client": "2.0.4",
"socket.io-redis": "5.2.0",
"socketio-wildcard": "2.0.0",
"spdx-license-list": "^3.0.1",
"toobusy-js": "^0.5.1",
"uglify-js": "^3.1.5",
"validator": "9.1.2",
"winston": "^2.4.0",
"xml": "^1.0.1",
"xregexp": "3.2.0",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"coveralls": "^3.0.0",

View File

@@ -1,34 +1,34 @@
{
"section-general": "General",
"general/dashboard": "Dashboard",
"general/homepage": "Home Page",
"general/navigation": "Navigation",
"general/languages": "Languages",
"general/sounds": "Sounds",
"general/social": "Social",
"section-general": "عام",
"general/dashboard": "اللوحة الرئيسية",
"general/homepage": "الصفحة الرئيسية",
"general/navigation": "التصفح",
"general/languages": "اللغات",
"general/sounds": "الأصوات",
"general/social": "شبكات التواصل",
"section-manage": "Manage",
"manage/categories": "Categories",
"manage/tags": "Tags",
"manage/users": "Users",
"manage/registration": "Registration Queue",
"manage/post-queue": "Post Queue",
"manage/groups": "Groups",
"manage/ip-blacklist": "IP Blacklist",
"section-manage": "إدارة",
"manage/categories": "الأقسام",
"manage/tags": "الكلمات المفتاحية",
"manage/users": "الأعضاء",
"manage/registration": "قائمة انتظار التسجيل",
"manage/post-queue": "قائمة انتظار المشاركة",
"manage/groups": "المجموعات",
"manage/ip-blacklist": "قائمة حظر عناوين IP",
"section-settings": "Settings",
"settings/general": "General",
"settings/reputation": "Reputation",
"settings/email": "Email",
"settings/user": "User",
"settings/group": "Group",
"settings/guest": "Guests",
"settings/uploads": "Uploads",
"settings/post": "Post",
"settings/chat": "Chat",
"settings/pagination": "Pagination",
"settings/tags": "Tags",
"settings/notifications": "Notifications",
"section-settings": "إعدادات",
"settings/general": "عامة",
"settings/reputation": "السمعة",
"settings/email": "البريد الإلكتروني",
"settings/user": "الأعضاء",
"settings/group": "المجموعات",
"settings/guest": "الزوار",
"settings/uploads": "الرفع",
"settings/post": "المشاركة",
"settings/chat": "الدردشة",
"settings/pagination": "ترقيم الصفحات",
"settings/tags": "الكلمات المفتاحية",
"settings/notifications": "التنبيهات",
"settings/cookies": "Cookies",
"settings/web-crawler": "Web Crawler",
"settings/sockets": "Sockets",

View File

@@ -68,8 +68,8 @@
"thread_tools.restore_confirm": "Наистина ли искате да възстановите тази тема?",
"thread_tools.purge": "Изчистване на темата",
"thread_tools.purge_confirm": "Наистина ли искате да изчистите тази тема?",
"thread_tools.merge_topics": "Merge Topics",
"thread_tools.merge": "Merge",
"thread_tools.merge_topics": "Сливане на темите",
"thread_tools.merge": "Сливане",
"topic_move_success": "Темата беше преместена успешно в %1",
"post_delete_confirm": "Наистина ли искате да изтриете тази публикация?",
"post_restore_confirm": "Наистина ли искате да възстановите тази публикация?",
@@ -91,7 +91,7 @@
"fork_pid_count": "Избрани публикации: %1",
"fork_success": "Темата е разделена успешно! Натиснете тук, за да преминете към отделената тема.",
"delete_posts_instruction": "Натиснете публикациите, които искате да изтриете/изчистите",
"merge_topics_instruction": "Click the topics you want to merge",
"merge_topics_instruction": "Натиснете темите, които искате да слеете",
"composer.title_placeholder": "Въведете заглавието на темата си тук...",
"composer.handle_placeholder": "Име",
"composer.discard": "Отхвърляне",

View File

@@ -3,12 +3,12 @@
"custom-css.description": "Füge hier deine eigenen CSS-Eigenschaften ein, sie werden als letztes angewendet.",
"custom-css.enable": "Benutzerdefiniertes CSS aktivieren",
"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": "Benutzerdefiniertes Javascript",
"custom-js.description": "Füge dein eigenes Javascipt hier ein.\nEs wird ausgeführt nachdem die Seite komplett geladen wurde.",
"custom-js.enable": "Benutzerdefiniertes Javascript aktivieren",
"custom-header": "Benutzerdefinierter Header",
"custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <code>&lt;head&gt;</code> section of your forum's markup. Script tags are allowed, but are discouraged, as the <a href=\"#custom-header\" data-toggle=\"tab\">Custom Javascript</a> tab is available.",
"custom-header.description": "Füge dein benutzerdefiniertes HTML hier ein (wie z.B. meta Tags etc.), welche in die <code>&lt;head&gt;</code> Sektion im Markup des Forums eingefügt werden. script Tags sind erlaubt, es wird aber davon abgeraten, da es den <a href=\"#custom-header\" data-toggle=\"tab\">Benutzerdefiniertes Javascript</a> Tab gibt.",
"custom-header.enable": "Benutzerdefinierten Header aktivieren",
"custom-css.livereload": "Live-Aktualisierung aktivieren",

View File

@@ -39,7 +39,7 @@
"section-appearance": "Aussehen",
"appearance/themes": "Themes",
"appearance/skins": "Skins",
"appearance/customise": "Custom Content (HTML/JS/CSS)",
"appearance/customise": "Benutzerdefinierter Inhalt (HTML/JS/CSS)",
"section-extend": "Erweitert",
"extend/plugins": "Plugins",

View File

@@ -2,5 +2,5 @@
"notifications": "Benachrichtigungen",
"welcome-notification": "Wilkommensnachricht",
"welcome-notification-link": "Wilkommensnachricht-Link",
"welcome-notification-uid": "Welcome Notification User (UID)"
"welcome-notification-uid": "Wilkommensbenachrichtigung Benutzer (UID)"
}

View File

@@ -4,7 +4,7 @@
"sorting.oldest-to-newest": "Von Alt bis Neu",
"sorting.newest-to-oldest": "Von Neu zu Alt",
"sorting.most-votes": "Meiste Bewertungen",
"sorting.most-posts": "Most Posts",
"sorting.most-posts": "Meiste Beiträge",
"sorting.topic-default": "Standardmäßige Themensortierung",
"restrictions": "Posting beschränkungen",
"restrictions.post-queue": "Beitragswarteschlange verwenden",

View File

@@ -19,8 +19,8 @@
"themes": "Themes",
"disable-user-skins": "Verhindere das Benutzer eigene Skins verwenden",
"account-protection": "Kontosicherheit",
"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": "Dauer bis zum erneuten Login (in Minuten)",
"admin-relogin-duration-help": "Nach einer gesetzten Zeit erfordert der Zugriff auf die Admin Sektion einen erneuten Login, 0 deaktiviert dies",
"login-attempts": "Login-Versuche pro Stunde",
"login-attempts-help": "Wenn die loginversuche zu einem Account diese Schwelle überschreiten, wird dieser Account für eine festgelegte Zeit gesperrt",
"lockout-duration": "Account Aussperrzeitraum (Minuten)",

View File

@@ -30,7 +30,7 @@
"notif.chat.unsub.info": "Diese Chat-Benachrichtigung wurde dir aufgrund deiner Abonnement-Einstellungen gesendet.",
"notif.post.cta": "Hier klicken, um das gesamte Thema zu lesen",
"notif.post.unsub.info": "Diese Mitteilung wurde dir aufgrund deiner Abonnement-Einstellungen gesendet.",
"notif.cta": "Click here to go to forum",
"notif.cta": "Klicke hier um das Forum zu besuchen",
"test.text1": "Dies ist eine Test-E-Mail, um zu überprüfen, ob der E-Mailer deines NodeBB korrekt eingestellt wurde.",
"unsub.cta": "Klicke hier, um diese Einstellungen zu ändern",
"banned.subject": "Du wurdest von %1 gebannt.",

View File

@@ -125,7 +125,7 @@
"parse-error": "Beim auswerten der Serverantwort ist etwas schiefgegangen",
"wrong-login-type-email": "Bitte nutze deine E-Mail-Adresse zum einloggen",
"wrong-login-type-username": "Bitte nutze deinen Benutzernamen zum einloggen",
"sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first",
"sso-registration-disabled": "Das Registrieren mit %1-Accounts wurde deaktiviert, bitte registriere dich zuerst mit einer Email-Adresse",
"invite-maximum-met": "Du hast bereits die maximale Anzahl an Personen eingeladen (%1 von %2).",
"no-session-found": "Keine Login-Sitzung gefunden!",
"not-in-room": "Benutzer nicht im Raum",
@@ -135,5 +135,5 @@
"invalid-home-page-route": "Ungültiger Startseitenpfad",
"invalid-session": "Sitzungsdiskrepanz",
"invalid-session-text": "Es scheint als wäre deine Login-Sitzung nicht mehr aktiv oder sie passt nicht mehr mit der des Servers. Bitte aktualisiere diese Seite.",
"no-topics-selected": "No topics selected!"
"no-topics-selected": "Keine Beiträge ausgewählt!"
}

View File

@@ -9,7 +9,7 @@
"continue_to": "Fortfahren zu %1",
"return_to": "Kehre zurück zu %1",
"new_notification": "Neue Benachrichtigung",
"new_notification_from": "You have a new Notification from %1",
"new_notification_from": "Du hast eine neue Nachricht von %1",
"you_have_unread_notifications": "Du hast ungelesene Benachrichtigungen.",
"all": "Alle",
"topics": "Themen",
@@ -47,18 +47,18 @@
"email-confirmed-message": "Vielen Dank für Ihre E-Mail-Validierung. Ihr Konto ist nun vollständig aktiviert.",
"email-confirm-error-message": "Es gab ein Problem bei der Validierung Ihrer E-Mail-Adresse. Möglicherweise ist der Code ungültig oder abgelaufen.",
"email-confirm-sent": "Bestätigungs-E-Mail gesendet.",
"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": "Keine",
"notification_only": "Nur Benachrichtigungen",
"email_only": "Nur Emails",
"notification_and_email": "Benachrichtigungen & Emails",
"notificationType_upvote": "Wenn jemand deinen beitrag positiv bewertet",
"notificationType_new-topic": "Wenn jemand dem du folgst einen Beitrag erstellt",
"notificationType_new-reply": "Wenn es eine neue Antwort auf ein Thema das du beobachtest gibt",
"notificationType_follow": "Wenn dir jemand neues folgt",
"notificationType_new-chat": "Wenn du eine Chat Nachricht erhältst",
"notificationType_group-invite": "Wenn du eine Gruppeneinladung erhältst",
"notificationType_new-register": "Wenn jemand der Registrierungswarteschlange hinzugefügt wird",
"notificationType_post-queue": "Wenn ein neuer Beitrag eingereiht wird",
"notificationType_new-post-flag": "Wenn ein Beitrag gemeldet wird",
"notificationType_new-user-flag": "Wenn ein Benutzer gemeldet wird"
}

View File

@@ -68,8 +68,8 @@
"thread_tools.restore_confirm": "Bist du sicher, dass du dieses Thema wiederherstellen möchtest?",
"thread_tools.purge": "Thema endgültig löschen",
"thread_tools.purge_confirm": "Bist du sicher, dass du dieses Thema endgültig löschen möchtest?",
"thread_tools.merge_topics": "Merge Topics",
"thread_tools.merge": "Merge",
"thread_tools.merge_topics": "Themen vereinen",
"thread_tools.merge": "Vereinen",
"topic_move_success": "Thema wurde erfolgreich nach %1 verschoben.",
"post_delete_confirm": "Sind Sie sicher, dass Sie diesen Beitrag löschen möchten?",
"post_restore_confirm": "Sind Sie sicher, dass Sie diesen Beitrag wiederherstellen möchten?",
@@ -91,7 +91,7 @@
"fork_pid_count": "%1 Beiträge ausgewählt",
"fork_success": "Thema erfolgreich aufgespalten! Klicke hier, um zum abgespaltenen Thema zu gelangen.",
"delete_posts_instruction": "Wähle die zu löschenden Beiträge aus",
"merge_topics_instruction": "Click the topics you want to merge",
"merge_topics_instruction": "Wähle die Themen aus, die du vereinen möchtest",
"composer.title_placeholder": "Hier den Titel des Themas eingeben...",
"composer.handle_placeholder": "Name",
"composer.discard": "Verwerfen",

View File

@@ -27,6 +27,8 @@
"pills.banned": "Banned",
"pills.search": "User Search",
"search.uid": "By User ID",
"search.uid-placeholder": "Enter a user ID to search",
"search.username": "By User Name",
"search.username-placeholder": "Enter a username to search",
"search.email": "By Email",

View File

@@ -5,5 +5,7 @@
"disable-editing-help": "Administrators and global moderators are exempt from this restriction",
"max-length": "Maximum length of chat messages",
"max-room-size": "Maximum number of users in chat rooms",
"delay": "Time between chat messages in milliseconds"
"delay": "Time between chat messages in milliseconds",
"restrictions.seconds-edit-after": "Number of seconds before users are allowed to edit chat messages after posting. (0 disabled)",
"restrictions.seconds-delete-after": "Number of seconds before users are allowed to delete chat messages after posting. (0 disabled)"
}

View File

@@ -20,6 +20,7 @@
"invalid-login-credentials": "Invalid login credentials",
"invalid-username-or-password": "Please specify both a username and password",
"invalid-search-term": "Invalid search term",
"invalid-url": "Invalid URL",
"csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again",
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
@@ -137,6 +138,8 @@
"cant-edit-chat-message": "You are not allowed to edit this message",
"cant-remove-last-user": "You can't remove the last user",
"cant-delete-chat-message": "You are not allowed to delete this message",
"chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting",
"chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting",
"already-voting-for-this-post": "You have already voted for this post.",
"reputation-system-disabled": "Reputation system is disabled.",

View File

@@ -66,6 +66,7 @@
"topics": "Topics",
"posts": "Posts",
"best": "Best",
"votes": "Votes",
"upvoters": "Upvoters",
"upvoted": "Upvoted",
"downvoters": "Downvoters",

View File

@@ -6,6 +6,7 @@
"popular-month": "Popular topics this month",
"popular-alltime": "All time popular topics",
"recent": "Recent Topics",
"top": "Top Voted Topics",
"moderator-tools": "Moderator Tools",
"flagged-content": "Flagged Content",
"ip-blacklist": "IP Blacklist",

View File

@@ -0,0 +1,4 @@
{
"title": "Top",
"no_top_topics": "No top topics"
}

View File

@@ -135,5 +135,5 @@
"invalid-home-page-route": "مسیر صفحه اصلی نامعتبر است",
"invalid-session": "عدم تطابق جلسه",
"invalid-session-text": "به نظر می‌رسد این جلسه برای ورود دیگر فعال نیست و یا با سرور هماهنگ نیست. لطفا این صفحه را رفرش کنید.",
"no-topics-selected": "No topics selected!"
"no-topics-selected": "هیچ موضوعی انتخاب نشده است !"
}

View File

@@ -68,8 +68,8 @@
"thread_tools.restore_confirm": "آیا مطمئنید که می خواهید این موضوع را بازگردانی کنید؟",
"thread_tools.purge": "پاک کردن موضوع",
"thread_tools.purge_confirm": "آیا مطمئنید که میمید این موضوع را پاکسازی کنید؟",
"thread_tools.merge_topics": "Merge Topics",
"thread_tools.merge": "Merge",
"thread_tools.merge_topics": "ادغام موضوع ها",
"thread_tools.merge": "ادغام",
"topic_move_success": "جابه‌جایی این موضوع به %1 باموفقیت انجام شد.",
"post_delete_confirm": "آیا از پاک کردن این پست اطمینان دارید؟",
"post_restore_confirm": "آیا از بازگردانی این پست اطمینان دارید؟",
@@ -91,7 +91,7 @@
"fork_pid_count": "%1 پست (ها) انتخاب شده اند",
"fork_success": "موضوع با موفقیت منشعب شد! برای رفتن به موضوع انشعابی اینجا را کلیک کنید.",
"delete_posts_instruction": "با کلیک بر روی پست شما می خواهید به حذف/پاکسازی",
"merge_topics_instruction": "Click the topics you want to merge",
"merge_topics_instruction": "بر روی عنوان موضوعاتی که می خواهید ادغام کنید کلیک کنید",
"composer.title_placeholder": "عنوان موضوعتان را اینجا بنویسید...",
"composer.handle_placeholder": "نام",
"composer.discard": "دور بیانداز",

View File

@@ -101,10 +101,10 @@
"outgoing-message-sound": "صدای پیام ارسال شده",
"notification-sound": "آگاه‌سازی‌ از طریق صدا",
"no-sound": "بدون صدا",
"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": "تنظیمات اعلان امتیاز مثبت",
"upvote-notif-freq.all": "همه امتیاز های مثبت",
"upvote-notif-freq.everyTen": "هر ده امتیاز مثبت",
"upvote-notif-freq.logarithmic": "هر 10، 10، 1000 ...",
"upvote-notif-freq.disabled": "Disabled",
"browsing": "تنظیمات مرور",
"open_links_in_new_tab": "پیوندهای به بیرون را در برگ جدید باز کن",

View File

@@ -3,9 +3,9 @@
"enable": "Paginuj tematy oraz posty zamiast używać nieskończonego przewijania",
"topics": "Paginacja tematów",
"posts-per-page": "Postów na stronie",
"max-posts-per-page": "Maximum posts per page",
"max-posts-per-page": "Maksymalna liczba postów na stronę",
"categories": "Paginacja kategorii",
"topics-per-page": "Tematów na stronę",
"max-topics-per-page": "Maximum topics per page",
"max-topics-per-page": "Maksymalna liczba tematów na stronę",
"initial-num-load": "Początkowa liczba pozycji do załadowania w Nieprzeczytanych, Ostatnich oraz Popularnych tematów"
}

View File

@@ -4,7 +4,7 @@
"sorting.oldest-to-newest": "Najstarsze do najnowszych",
"sorting.newest-to-oldest": "Najnowsze do najstarszych",
"sorting.most-votes": "Najwięcej głosów",
"sorting.most-posts": "Most Posts",
"sorting.most-posts": "Najwięcej postów",
"sorting.topic-default": "Domyślne sortowanie tematów",
"restrictions": "Ograniczenia pisania",
"restrictions.post-queue": "Włącz kolejkę postów",

View File

@@ -10,6 +10,6 @@
"all-topics": "Wszystkie tematy",
"new-topics": "Nowe tematy",
"watched-topics": "Obserwowane tematy",
"unreplied-topics": "Unreplied Topics",
"multiple-categories-selected": "Multiple Selected"
"unreplied-topics": "Tematy bez odpowiedzi",
"multiple-categories-selected": "Kilka zaznaczonych"
}

View File

@@ -125,7 +125,7 @@
"parse-error": "Нешто је кренуло погрешно приликом анализе одговора сервера",
"wrong-login-type-email": "Користите вашу е-пошту за пријављивање",
"wrong-login-type-username": "Користите ваше корисничко име за пријављивање",
"sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first",
"sso-registration-disabled": "Регистрација је онемогућена за %1 налога, региструјте се са адресом е-поште прво",
"invite-maximum-met": "Позвали сте максимални број особа (%1 од %2).",
"no-session-found": "Није пронађена сесија пријављивања!",
"not-in-room": "Корисник није у соби",
@@ -135,5 +135,5 @@
"invalid-home-page-route": "Неважећа путања почетне странице",
"invalid-session": "Неподударање сесија",
"invalid-session-text": "Изгледа да ваша сесија пријављивања није више активна или се више не подудара са сервером. Поново учитајте ову страницу.",
"no-topics-selected": "No topics selected!"
"no-topics-selected": "Нема одабраних тема!"
}

View File

@@ -68,8 +68,8 @@
"thread_tools.restore_confirm": "Да ли сте сигурни да желите да обновите ову тему?",
"thread_tools.purge": "Очисти тему",
"thread_tools.purge_confirm": "Да ли сте сигурни да желите да очистите ову тему?",
"thread_tools.merge_topics": "Merge Topics",
"thread_tools.merge": "Merge",
"thread_tools.merge_topics": "Споји теме",
"thread_tools.merge": "Споји",
"topic_move_success": "Ова тема је успешно премештена у %1",
"post_delete_confirm": "Да ли сте сигурни да желите да избришете ову поруку?",
"post_restore_confirm": "Да ли сте сигурни да желите да обновите ову поруку?",
@@ -91,7 +91,7 @@
"fork_pid_count": "Одабрано порука: %1",
"fork_success": "Тема је успешно рачвана! Кликните овде за одлазак на рачвану тему.",
"delete_posts_instruction": "Кликните на поруке које желите да избришете/очистите",
"merge_topics_instruction": "Click the topics you want to merge",
"merge_topics_instruction": "Кликните на теме које желите да спојите",
"composer.title_placeholder": "Овде унесите назив теме...",
"composer.handle_placeholder": "Име",
"composer.discard": "Одбаци",

View File

@@ -1,5 +1,5 @@
{
"alert.confirm-reload": "Bạn có thật sự muốn tải lại NodeBB",
"alert.confirm-reload": "Bạn có thật sự muốn xác lập lại NodeBB",
"alert.confirm-restart": "Bạn có thật sự muốn khởi động lại NodeBB",
"acp-title": "%1 | Bảng điểu khiển",

View File

@@ -1,5 +1,5 @@
{
"post-cache": "Cache bài viết",
"post-cache": "Bộ nhớ đệm bài viết",
"posts-in-cache": "Cache cho bài viết",
"average-post-size": "Kích thước bài viết",
"length-to-max": "Độ dài / Tối Đa",

View File

@@ -1,5 +1,5 @@
{
"email-settings": "Email Settings",
"email-settings": "Thiết lập Email",
"address": "Email Address",
"address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.",
"from": "From Name",

View File

@@ -8,13 +8,13 @@
"no_replies": "Chưa có bình luận nào",
"no_new_posts": "Không có bài mới.",
"share_this_category": "Chia sẻ chuyên mục này",
"watch": "Theo dõi",
"watch": "Quan tâm",
"ignore": "Bỏ qua",
"watching": "Đang theo dõi",
"watching": "Đang quan tâm",
"ignoring": "Bỏ qua",
"watching.description": "Hiện các chủ đề chưa đọc",
"ignoring.description": "Không hiện những chủ đề chưa đọc",
"watch.message": "Bạn đang theo dõi các cập nhật ở chuyên mục này và các chuyên mục con",
"ignore.message": "Bạn đang bỏ qua các cập nhật ở chuyên mục này và các chuyên mục con",
"watched-categories": "Các chuyên mục đã xem"
"watched-categories": "Các chuyên mục đã quan tâm"
}

View File

@@ -30,12 +30,12 @@
"notif.chat.unsub.info": "Thông báo tin nhắn này được gửi tới dựa theo cài đặt theo dõi của bạn.",
"notif.post.cta": "Nhấn vào đây để đọc toàn bộ chủ đề",
"notif.post.unsub.info": "Thông báo bài viết này được gửi cho bạn dựa tên thiết lập nhận thông báo của bạn",
"notif.cta": "Click here to go to forum",
"notif.cta": "Click vào đây để đi đến diễn đàn",
"test.text1": "Đây là email kiểm tra xem chức năng gửi mail trên hệ thống NodeBB của bạn có hoạt động tốt hay không.",
"unsub.cta": "Nhấn vào đây để thay đổi cài đặt.",
"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": "Bạn đã bị cấm khỏi %1",
"banned.text1": "Người dùng %1 đã bị cấm khỏi %2",
"banned.text2": "Lệnh cấm sẽ kéo dài đến %1.",
"banned.text3": "Đây là lý do tại sao bạn bị cấm:",
"closing": "Xin cảm ơn!"
}

View File

@@ -1,20 +1,20 @@
{
"invalid-data": "Dữ liệu không hợp lệ",
"invalid-json": "Invalid JSON",
"invalid-json": "JSON không hợp lệ",
"not-logged-in": "Có vẻ bạn chưa đăng nhập.",
"account-locked": "Tài khoản của bạn đang tạm thời bị khóa",
"search-requires-login": "Bạn cần phải có tài khoản để tìm kiếm - vui lòng đăng nhập hoặc đăng ký.",
"goback": "Press back to return to the previous page",
"goback": "Nhấn back để quay về trang trước",
"invalid-cid": "ID chuyên mục không hợp lệ",
"invalid-tid": "ID chủ đề không hợp lệ",
"invalid-pid": "ID bài viết không hợp lệ",
"invalid-uid": "ID tài khoản không hợp lệ",
"invalid-username": "Tên đăng nhập không hợp lệ",
"invalid-email": "Email không hợp lệ",
"invalid-title": "Invalid title",
"invalid-title": "Tiêu đề không hợp lệ",
"invalid-user-data": "Dữ liệu tài khoản không hợp lệ",
"invalid-password": "Mật khẩu không hợp lệ",
"invalid-login-credentials": "Invalid login credentials",
"invalid-login-credentials": "Thông tin đăng nhập không hợp lệ",
"invalid-username-or-password": "Xin hãy nhập cả tên đăng nhập và mật khẩu",
"invalid-search-term": "Từ khóa không hợp lệ",
"csrf-invalid": "Hệ thống không cho phép bạn đăng nhập, có vẻ như phiên đăng nhập cũ đã hết hạn. Hãy thử đăng nhập lại",
@@ -33,7 +33,7 @@
"password-too-long": "Mật khẩu quá dài",
"user-banned": "Tài khoản bị ban",
"user-banned-reason": "Xin lỗi, tài khoản này đã bị khóa (Lí do: %1)",
"user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)",
"user-banned-reason-until": "Rất tiếc, tài khoản này đã bị cấm cho đến %1 (Lý do: %2)",
"user-too-new": "Rất tiếc, bạn phải chờ %1 giây để đăng bài viết đầu tiên.",
"blacklisted-ip": "Rất tiếc, địa chỉ IP của bạn đã bị cấm khỏi cộng đồng. Nếu bạn cảm thấy có gì không đúng, hãy liên lạc với người quản trị.",
"ban-expiry-missing": "Vui lòng cung cấp ngày hết hạn của lệnh cấm",
@@ -81,7 +81,7 @@
"cant-ban-other-admins": "Bạn không thể cấm được các quản trị viên khác",
"cant-remove-last-admin": "Bạn là quản trị viên duy nhất. Hãy cho thành viên khác làm quản trị viên trước khi huỷ bỏ quyền quản trị của bạn.",
"cant-delete-admin": "Hủy quyền quản trị của tài khoản này trước khi xóa",
"invalid-image": "Invalid image",
"invalid-image": "Hình ảnh không hợp lệ",
"invalid-image-type": "Định dạng ảnh không hợp lệ. Những định dạng được cho phép là: %1",
"invalid-image-extension": "Định dạng ảnh không hợp lệ",
"invalid-file-type": "Định dạng file không hợp lệ. Những định dạng được cho phép là: %1",
@@ -109,7 +109,7 @@
"chat-disabled": "Hệ thống chat đã bị vô hiệu hoá",
"too-many-messages": "Bạn đã gửi quá nhiều tin nhắn, vui lòng đợi trong giây lát.",
"invalid-chat-message": "Tin nhắn không hợp lệ",
"chat-message-too-long": "Chat messages can not be longer than %1 characters.",
"chat-message-too-long": "Thông điệp không thể dài hơn %1 chữ.",
"cant-edit-chat-message": "Bạn không được phép chỉnh sửa tin nhắn này",
"cant-remove-last-user": "Bạn không thể xoá thành viên cuối cùng",
"cant-delete-chat-message": "Bạn không được phép xoá tin nhắn này",
@@ -119,13 +119,13 @@
"not-enough-reputation-to-downvote": "Bạn không có đủ phiếu tín nhiệm để downvote bài này",
"not-enough-reputation-to-flag": "Bạn không đủ tín nhiệm để đánh dấu bài viết này",
"already-flagged": "Bạn đã gắn cờ cho bài viết này",
"self-vote": "You cannot vote on your own post",
"self-vote": "Bạn không thể tự bầu cho bài đăng của mình",
"reload-failed": "NodeBB gặp lỗi trong khi tải lại: \"%1\". NodeBB sẽ tiếp tục hoạt động với dữ liệu trước đó, tuy nhiên bạn nên tháo gỡ những gì bạn vừa thực hiện trước khi tải lại.",
"registration-error": "Lỗi đăng kí",
"parse-error": "Có gì không ổn khi nhận kết quả từ máy chủ",
"wrong-login-type-email": "Xin vui lòng sửa dụng email của bạn để đăng nhập",
"wrong-login-type-username": "Vui lòng sử dụng tên đăng nhập của bạn để đăng nhập",
"sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first",
"sso-registration-disabled": "Không thể đăng ký với tài khoản %1, vui lòng đăng ký với địa chỉ email của bạn",
"invite-maximum-met": "Bạn đã sử dụng hết số lượng lời mời bạn có thể gửi (%1 đã gửi trên tổng số %2 được cho phép)",
"no-session-found": "Không tìm thấy phiên đăng nhập!",
"not-in-room": "Thành viên không có trong phòng",
@@ -135,5 +135,5 @@
"invalid-home-page-route": "Đường dẫn trang chủ không hợp lệ",
"invalid-session": "Không đúng session",
"invalid-session-text": "Có vẻ như phiên đăng nhập của bạn đã không còn hoạt động nữa, hoặc không còn đúng với thông tin trên máy chủ. Vui lòng tải lại trang này",
"no-topics-selected": "No topics selected!"
"no-topics-selected": "Không có chủ đề nào đang được chọn!"
}

View File

@@ -104,6 +104,6 @@
"cookies.accept": "Đã rõ!",
"cookies.learn_more": "Xem thêm",
"edited": "Đã cập nhật",
"disabled": "Disabled",
"select": "Select"
"disabled": "Bị khóa",
"select": "Chọn"
}

View File

@@ -1,5 +1,5 @@
{
"name": "Tiếng Việt",
"name": "Tiếng Anh (Anh Quốc/Ca-na-da)",
"code": "vi",
"dir": "ltr"
"dir": "Trái qua phải"
}

View File

@@ -9,17 +9,17 @@
"continue_to": "Tiếp tục tới %1",
"return_to": "Quay lại %1",
"new_notification": "Thông báo mới",
"new_notification_from": "You have a new Notification from %1",
"new_notification_from": "Bạn nhận được 1 thông báo từ %1",
"you_have_unread_notifications": "Bạn có thông báo chưa đọc",
"all": "All",
"topics": "Topics",
"replies": "Replies",
"chat": "Chats",
"follows": "Follows",
"upvote": "Upvotes",
"new-flags": "New Flags",
"my-flags": "Flags assigned to me",
"bans": "Bans",
"all": "Toàn bộ",
"topics": "Chủ đề",
"replies": "Phản hồi",
"chat": "Thông điệp",
"follows": "Lượt theo dõi",
"upvote": "Lượt thích",
"new-flags": "Cảnh báo mới",
"my-flags": "Cảnh báo dành cho tôi",
"bans": "Cấm",
"new_message_from": "Tin nhắn mới từ <strong>%1</strong>",
"upvoted_your_post_in": "<strong>%1</strong> đã bình chọn bài của bạn trong <strong>%2</strong>.",
"upvoted_your_post_in_dual": "<strong>%1</strong> và <strong>%2</strong> đã tán thành với bài viết của bạn trong <strong>%3</strong>.",
@@ -29,9 +29,9 @@
"user_flagged_post_in": "<strong>%1</strong> gắn cờ 1 bài trong <strong>%2</strong>",
"user_flagged_post_in_dual": "<strong>%1</strong> và <strong>%2</strong> đã gắn cờ một bài viết trong <strong>%3</strong>",
"user_flagged_post_in_multiple": "<strong>%1</strong> và %2 người khác đã gắn cờ bài viết của bạn trong <strong>%3</strong>",
"user_flagged_user": "<strong>%1</strong> flagged a user profile (%2)",
"user_flagged_user_dual": "<strong>%1</strong> and <strong>%2</strong> flagged a user profile (%3)",
"user_flagged_user_multiple": "<strong>%1</strong> and %2 others flagged a user profile (%3)",
"user_flagged_user": "<strong>%1</strong> đã cảnh báo một người dùng (%2)",
"user_flagged_user_dual": "<strong>%1</strong> <strong>%2</strong> đã cảnh báo một người dùng (%3)",
"user_flagged_user_multiple": "<strong>%1</strong> %2 người khác đã cảnh báo người dùng (%3)",
"user_posted_to": "<strong>%1</strong> đã trả lời <strong>%2</strong>",
"user_posted_to_dual": "<strong>%1</strong> và <strong>%2</strong> đã trả lời: <strong>%3</strong>",
"user_posted_to_multiple": "<strong>%1</strong> và %2 người khác đã trả lời: <strong>%3</strong>",
@@ -41,24 +41,24 @@
"user_started_following_you_multiple": "<strong>%1</strong> và %2 người khác đã bắt đầu theo dõi bạn.",
"new_register": "<strong>%1</strong> đã gửi một yêu cầu tham gia.",
"new_register_multiple": "Có <strong>%1</strong> đơn đăng ký đang chờ xem xét.",
"flag_assigned_to_you": "<strong>Flag %1</strong> has been assigned to you",
"post_awaiting_review": "Post awaiting review",
"flag_assigned_to_you": "<strong>Cảnh báo %1</strong> đã được ghi nhận đối với bạn",
"post_awaiting_review": "Bài đăng đang chờ xét duyệt",
"email-confirmed": "Đã xác nhận email",
"email-confirmed-message": "Cảm ơn bạn đã xác nhận địa chỉ email của bạn. Tài khoản của bạn đã được kích hoạt đầy đủ.",
"email-confirm-error-message": "Đã có lỗi khi xác nhận địa chỉ email. Có thể đoạn mã không đúng hoặc đã hết hạn.",
"email-confirm-sent": "Email xác nhận đã gửi.",
"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": "Hoàn toàn không",
"notification_only": "Chỉ thông báo",
"email_only": "Chỉ email",
"notification_and_email": "Cả thông báo & email",
"notificationType_upvote": "Khi ai đó thích bài đăng của bạn",
"notificationType_new-topic": "Khi người bạn theo dõi đăng một chủ đề",
"notificationType_new-reply": "Khi phản hồi được đăng trong chủ đề bạn đang quan tâm",
"notificationType_follow": "Khi ai đó theo dõi bạn",
"notificationType_new-chat": "Khi bạn nhận được thông điệp chat",
"notificationType_group-invite": "Khi bạn nhận được lời mời gia nhập nhóm",
"notificationType_new-register": "Khi ai đó được thêm vào lượt chờ đăng ký",
"notificationType_post-queue": "Khi bài đăng được thêm vào lượt chờ",
"notificationType_new-post-flag": "Khi bài đăng được cảnh báo",
"notificationType_new-user-flag": "Khi người dùng bị cảnh báo"
}

View File

@@ -43,8 +43,8 @@
"account/groups": "Nhóm của %1",
"account/bookmarks": "Đã bookmark %1's chủ đề",
"account/settings": "Thiết lập",
"account/watched": "Chủ đề %1 đang theo dõi",
"account/ignored": "Topics ignored by %1",
"account/watched": "Chủ đề được quan tâm bởi %1",
"account/ignored": "Các chủ đề đã bị phớt lờ bởi %1",
"account/upvoted": "Bài viết %1 tán thành",
"account/downvoted": "Bài viết %1 phản đối",
"account/best": "Bài viết hay nhất của %1",

View File

@@ -8,11 +8,11 @@
"posted-by": "Đăng bởi",
"in-categories": "Nằm trong chuyên mục",
"search-child-categories": "Tìm kiếm chuyên mục con",
"has-tags": "Has tags",
"has-tags": "Có thẻ bên trong",
"reply-count": "Số lượt trả lời",
"at-least": "Tối thiểu",
"at-most": "Tối đa",
"relevance": "Relevance",
"relevance": "Mức độ liên quan",
"post-time": "Thời điểm đăng bài",
"newer-than": "Mới hơn",
"older-than": "Cũ hơn",

View File

@@ -1,7 +1,7 @@
{
"success": "Thành công",
"topic-post": "Bạn đã gửi bài thành công",
"post-queued": "Your post is queued for approval.",
"post-queued": "Bài đăng của bạn đang được chờ xét duyệt.",
"authentication-successful": "Xác thực thành công",
"settings-saved": "Đã lưu thiết lập"
}

View File

@@ -14,7 +14,7 @@
"quote": "Trích dẫn",
"reply": "Trả lời",
"replies_to_this_post": "%1 trả lời",
"one_reply_to_this_post": "1 Reply",
"one_reply_to_this_post": "1 Phản hồi",
"last_reply_time": "Trả lời cuối cùng",
"reply-as-topic": "Trả lời dưới dạng chủ đề",
"guest-login-reply": "Hãy đăng nhập để trả lời",
@@ -40,13 +40,13 @@
"markAsUnreadForAll.success": "Chủ đề đã được đánh dấu là chưa đọc toàn bộ",
"mark_unread": "Đánh dấu chưa đọc",
"mark_unread.success": "Chủ đề đã được đánh dấu chưa đọc.",
"watch": "Theo dõi",
"unwatch": "Ngừng theo dõi",
"watch": "Quan tâm",
"unwatch": "Ngừng quan tâm",
"watch.title": "Được thông báo khi có trả lời mới trong chủ đề này",
"unwatch.title": "Ngừng theo dõi chủ đề này",
"unwatch.title": "Ngừng quan tâm chủ đề này",
"share_this_post": "Chia sẻ bài viết này",
"watching": "Đang xem",
"not-watching": "Không xem",
"watching": "Đang quan tâm",
"not-watching": "Không để ý",
"ignoring": "Bỏ qua",
"watching.description": "Thông báo cho tôi các trả lời mới. <br/>Hiển thị các mục chưa đọc",
"not-watching.description": "Không thông báo tôi các trả lời mới. <br/>Hiển thị mục chưa đọc nếu danh mục bị bỏ qua.",
@@ -59,7 +59,7 @@
"thread_tools.unlock": "Mở khóa chủ đề",
"thread_tools.move": "Chuyển chủ đề",
"thread_tools.move_all": "Chuyển tất cả",
"thread_tools.select_category": "Select Category",
"thread_tools.select_category": "Chọn chuyện mục",
"thread_tools.fork": "Tạo bản sao chủ đề",
"thread_tools.delete": "Xóa chủ đề",
"thread_tools.delete-posts": "Xoá bài viết",
@@ -68,8 +68,8 @@
"thread_tools.restore_confirm": "Bạn có muốn phục hồi chủ đề này?",
"thread_tools.purge": "Xóa hẳn chủ đề",
"thread_tools.purge_confirm": "Bạn có muốn xóa hẳn chủ đề này?",
"thread_tools.merge_topics": "Merge Topics",
"thread_tools.merge": "Merge",
"thread_tools.merge_topics": "Xác nhập chủ đề",
"thread_tools.merge": "Xác nhập",
"topic_move_success": "Đã chuyển thành công chủ đề này sang %1",
"post_delete_confirm": "Bạn có chắc là muốn xóa bài gửi này không?",
"post_restore_confirm": "Bạn có chắc là muốn phục hồi bài gửi này không?",
@@ -91,7 +91,7 @@
"fork_pid_count": "%1 bài viết(s) đã được gửi",
"fork_success": "Tạo bản sao thành công! Nhấn vào đây để chuyển tới chủ đề vừa tạo.",
"delete_posts_instruction": "Chọn những bài viết bạn muốn xoá",
"merge_topics_instruction": "Click the topics you want to merge",
"merge_topics_instruction": "Click vào các chủ đề bạn muốn xác nhập",
"composer.title_placeholder": "Nhập tiêu đề cho chủ đề của bạn tại đây...",
"composer.handle_placeholder": "Tên",
"composer.discard": "Huỷ bỏ",

View File

@@ -9,7 +9,7 @@
"topics_marked_as_read.success": "Chủ đề được đánh dấu đã đọc",
"all-topics": "Toàn bộ chủ đề",
"new-topics": "Các chủ đề mới",
"watched-topics": "Các chủ đề đã xem",
"unreplied-topics": "Unreplied Topics",
"multiple-categories-selected": "Multiple Selected"
"watched-topics": "Các chủ đề đuợc quan tâm",
"unreplied-topics": "Chủ đề chưa có phản hồi nào",
"multiple-categories-selected": "Chọn nhiều cùng lúc"
}

View File

@@ -24,17 +24,17 @@
"profile_views": "Số lượt người ghé thăm",
"reputation": "Mức uy tín",
"bookmarks": "Bookmarks",
"watched": "Đã theo dõi",
"ignored": "Ignored",
"watched": "Đã quan tâm",
"ignored": "Phớt lờ",
"followers": "Số người theo dõi",
"following": "Đang theo dõi",
"aboutme": "Giới thiệu bản thân",
"signature": "Chữ ký",
"birthday": "Ngày sinh ",
"chat": "Chat",
"chat_with": "Continue chat with %1",
"new_chat_with": "Start new chat with %1",
"flag-profile": "Flag Profile",
"chat_with": "Tiếp tục chat với %1",
"new_chat_with": "Bắt đầu chat với %1",
"flag-profile": "Cảnh báo người dùng",
"follow": "Theo dõi",
"unfollow": "Hủy theo dõi",
"more": "Xem thêm",
@@ -61,14 +61,14 @@
"username_taken_workaround": "Tên truy cập này đã tồn tại, vì vậy chúng tôi đã sửa đổi nó một chút. Tên truy cập của bạn giờ là <strong>%1</strong>",
"password_same_as_username": "Mật khẩu của bạn trùng với tên đăng nhập, vui lòng chọn một mật khẩu khác.",
"password_same_as_email": "Mật khẩu của bạn trùng với email của bạn, hãy chọn mật khẩu khác.",
"weak_password": "Weak password.",
"weak_password": "Mật khẩu yếu",
"upload_picture": "Tải lên hình ảnh",
"upload_a_picture": "Tải lên một hình ảnh",
"remove_uploaded_picture": "Xoá ảnh đã tải lên",
"upload_cover_picture": "Tải ảnh bìa lên",
"remove_cover_picture_confirm": "Are you sure you want to remove the cover picture?",
"crop_picture": "Crop picture",
"upload_cropped_picture": "Crop and upload",
"remove_cover_picture_confirm": "Bạn có thật sự muốn xóa hình ảnh này?",
"crop_picture": "Cắt nhỏ hình ảnh",
"upload_cropped_picture": "Cắt nhỏ và đăng tải",
"settings": "Thiết lập",
"show_email": "Hiện Email của tôi",
"show_fullname": "Hiện tên đầy đủ",
@@ -84,8 +84,8 @@
"follows_no_one": "Người dùng này hiện chưa theo dõi ai :(",
"has_no_posts": "Thành viên này chưa đăng bài viết nào cả.",
"has_no_topics": "Thành viên này chưa đăng chủ đề nào cả.",
"has_no_watched_topics": "Thành viên này chưa theo dõi chủ đề nào cả.",
"has_no_ignored_topics": "This user hasn't ignored any topics yet.",
"has_no_watched_topics": "Thành viên này chưa quan tâm chủ đề nào cả.",
"has_no_ignored_topics": "Người dùng này chưa bỏ qua bất cứ chủ đề nào.",
"has_no_upvoted_posts": "Thành viên này chưa tán thành bài viết nào cả.",
"has_no_downvoted_posts": "Thành viên này chưa phản đối bài viết nào cả.",
"has_no_voted_posts": "Thành viên này không có bài viết nào được tán thành.",
@@ -94,18 +94,18 @@
"paginate_description": "Phân trang chủ đề và bài viết thay vì sử dụng cuộn vô hạn",
"topics_per_page": "Số chủ đề trong một trang",
"posts_per_page": "Số bài viết trong một trang",
"max_items_per_page": "Maximum %1",
"max_items_per_page": "Tối đa %1",
"notification_sounds": "Phát âm thanh khi bạn nhận được thông báo mới",
"notifications_and_sounds": "Thông báo & Âm thanh",
"incoming-message-sound": "Âm báo tin nhắn tới",
"outgoing-message-sound": "Âm báo tin nhắn đi",
"notification-sound": "Âm thanh thông báo",
"no-sound": "Không có âm thanh",
"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": "Tần suất thông báo lượt thích",
"upvote-notif-freq.all": "Toàn bộ lượt thích",
"upvote-notif-freq.everyTen": "Mỗi 10 lượt thích",
"upvote-notif-freq.logarithmic": "Cứ mỗi 10, 100, 1000...",
"upvote-notif-freq.disabled": "Bị khóa",
"browsing": "Đang xem cài đặt",
"open_links_in_new_tab": "Mở link trong tab mới.",
"enable_topic_searching": "Bật In-topic Searching",
@@ -113,8 +113,8 @@
"delay_image_loading": "Việc tải ảnh đang bị chậm",
"image_load_delay_help": "Nếu được bật, toàn bộ ảnh trong chủ đề sẽ chỉ được tải khi người dùng kéo chuột tới",
"scroll_to_my_post": "Sau khi đăng một trả lời thì hiển thị bài viết mới",
"follow_topics_you_reply_to": "Theo dõi những chủ đề bạn đã bình luận",
"follow_topics_you_create": "Theo dõi những chủ đề do bạn t",
"follow_topics_you_reply_to": "Những chủ đề bạn quan tâm và từng bình luận",
"follow_topics_you_create": "Theo dõi chủ đề bạn tạo",
"grouptitle": "Tên nhóm",
"no-group-title": "Không có tên nhóm",
"select-skin": "Chọn một giao diện",
@@ -126,9 +126,9 @@
"sso.title": "Đăng nhập một lần",
"sso.associated": "Đã liên kết với",
"sso.not-associated": "Nhấn vào đây để liên kết với",
"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": "Tách khỏi",
"sso.dissociate-confirm-title": "Xác nhận việc tách khỏi",
"sso.dissociate-confirm": "Bạn có chắc chắn muốn tách tài khoản của mình khỏi %1?",
"info.latest-flags": "Cờ mới nhất",
"info.no-flags": "Không có bài viết nào bị gắn c",
"info.ban-history": "Lịch sử khóa tài khoản gần đây",
@@ -141,5 +141,5 @@
"info.email-history": "Lịch sử email",
"info.moderation-note": "Ghi chú quản lí",
"info.moderation-note.success": "Đã lưu ghi chú quản l",
"info.moderation-note.add": "Add note"
"info.moderation-note.add": "Thêm ghi chú"
}

View File

@@ -125,7 +125,7 @@
"parse-error": "服务器响应解析出错",
"wrong-login-type-email": "请输入您的电子邮箱地址登录",
"wrong-login-type-username": "请输入您的用户名登录",
"sso-registration-disabled": "Registration has been disabled for %1 accounts, please register with an email address first",
"sso-registration-disabled": "已经禁止注册注册 %1 账户, 请使用邮箱地址注册",
"invite-maximum-met": "您的邀请人数超出了上限 (%1 超过了 %2)。",
"no-session-found": "未登录!",
"not-in-room": "用户已不在聊天室中",
@@ -135,5 +135,5 @@
"invalid-home-page-route": "无效的首页路径",
"invalid-session": "Session 无法匹配",
"invalid-session-text": "您的登入状态已经失效,或者是与服务器信息不匹配。请刷新此页面。",
"no-topics-selected": "No topics selected!"
"no-topics-selected": "没有主题被选中!"
}

View File

@@ -68,8 +68,8 @@
"thread_tools.restore_confirm": "确定要恢复此主题吗?",
"thread_tools.purge": "清除主题",
"thread_tools.purge_confirm": "确认清除此主题吗?",
"thread_tools.merge_topics": "Merge Topics",
"thread_tools.merge": "Merge",
"thread_tools.merge_topics": "合并主题",
"thread_tools.merge": "合并",
"topic_move_success": "此主题已成功移到 %1",
"post_delete_confirm": "确定删除此帖吗?",
"post_restore_confirm": "确定恢复此帖吗?",
@@ -91,7 +91,7 @@
"fork_pid_count": "选择了 %1 个帖子",
"fork_success": "成功分割主题! 点这里跳转到分割后的主题。",
"delete_posts_instruction": "点击想要删除/永久删除的帖子",
"merge_topics_instruction": "Click the topics you want to merge",
"merge_topics_instruction": "点击你想合并的主题",
"composer.title_placeholder": "在此输入您主题的标题...",
"composer.handle_placeholder": "姓名",
"composer.discard": "撤销",

View File

@@ -306,7 +306,7 @@ define('admin/manage/users', ['translator', 'benchpress'], function (translator,
var timeoutId = 0;
$('#search-user-name, #search-user-email, #search-user-ip').on('keyup', function () {
$('#search-user-uid, #search-user-name, #search-user-email, #search-user-ip').on('keyup', function () {
if (timeoutId !== 0) {
clearTimeout(timeoutId);
timeoutId = 0;

View File

@@ -109,7 +109,7 @@ $(document).ready(function () {
url = ajaxify.removeRelativePath(url.replace(/^\/|\/$/g, '')).toLowerCase();
var isClientToAdmin = url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') !== 0;
var isAdminToClient = !url.startsWith('admin') && window.location.pathname.indexOf(RELATIVE_PATH + '/admin') === 0;
var uploadsOrApi = url.startsWith('assets/uploads') || url.startsWith('uploads') || url.startsWith('api');
var uploadsOrApi = url.startsWith('assets/') || url.startsWith('uploads') || url.startsWith('api');
if (isClientToAdmin || isAdminToClient || uploadsOrApi) {
window.open(RELATIVE_PATH + '/' + url, '_top');

View File

@@ -239,25 +239,18 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components'
if (!url) {
return false;
}
socket.emit('user.uploadProfileImageFromUrl', {
uid: ajaxify.data.uid,
uploadModal.modal('hide');
pictureCropper.handleImageCrop({
url: url,
}, function (err, url) {
if (err) {
return app.alertError(err);
}
socketMethod: 'user.uploadCroppedPicture',
aspectRatio: '1 / 1',
allowSkippingCrop: false,
paramName: 'uid',
paramValue: ajaxify.data.theirid,
}, onUploadComplete);
uploadModal.modal('hide');
pictureCropper.handleImageCrop({
url: url,
socketMethod: 'user.uploadCroppedPicture',
aspectRatio: '1 / 1',
allowSkippingCrop: false,
paramName: 'uid',
paramValue: ajaxify.data.theirid,
}, onUploadComplete);
});
return false;
});
});

View File

@@ -90,7 +90,7 @@ define('forum/chats', [
return;
}
loading = true;
var start = parseInt($('.chat-content').children('[data-index]').first().attr('data-index'), 10) + 1;
var start = parseInt(el.children('[data-mid]').length, 10);
socket.emit('modules.chats.getMessages', {
roomId: roomId,
uid: uid,

View File

@@ -75,6 +75,7 @@ define('forum/footer', ['notifications', 'chat', 'components', 'translator'], fu
socket.on('event:new_post', onNewPost);
}
// DEPRECATED: remove in 1.8.0
if (app.user.uid) {
socket.emit('user.getUnreadCounts', function (err, data) {
if (err) {

52
public/src/client/top.js Normal file
View File

@@ -0,0 +1,52 @@
'use strict';
define('forum/top', ['forum/recent', 'forum/infinitescroll'], function (recent, infinitescroll) {
var Top = {};
$(window).on('action:ajaxify.start', function (ev, data) {
if (ajaxify.currentPage !== data.url) {
recent.removeListeners();
}
});
Top.init = function () {
app.enterRoom('top_topics');
recent.watchForNewPosts();
recent.handleCategorySelection();
$('#new-topics-alert').on('click', function () {
$(this).addClass('hide');
});
if (!config.usePagination) {
infinitescroll.init(loadMoreTopics);
}
$(window).trigger('action:topics.loaded', { topics: ajaxify.data.topics });
};
function loadMoreTopics(direction) {
if (direction < 0 || !$('[component="category"]').length) {
return;
}
infinitescroll.loadMore('topics.loadMoreTopTopics', {
after: $('[component="category"]').attr('data-nextstart'),
count: config.topicsPerPage,
cid: utils.params().cid,
filter: ajaxify.data.selectedFilter.filter,
}, function (data, done) {
if (data.topics && data.topics.length) {
recent.onTopicsLoaded('top', data.topics, true, done);
$('[component="category"]').attr('data-nextstart', data.nextStart);
} else {
done();
$('#load-more-btn').hide();
}
});
}
return Top;
});

View File

@@ -157,7 +157,7 @@ define('forum/topic', [
components.get('topic').on('click', '[component="post/parent"]', function (e) {
var toPid = $(this).attr('data-topid');
var toPost = $('[component="post"][data-pid="' + toPid + '"]');
var toPost = $('[component="topic"]>[component="post"][data-pid="' + toPid + '"]');
if (toPost.length) {
e.preventDefault();
navigator.scrollToIndex(toPost.attr('data-index'), true);

View File

@@ -117,7 +117,6 @@ define('alerts', ['translator', 'components', 'benchpress'], function (translato
alert
.on('mouseenter', function () {
$(this).css('transition-duration', 0);
console.log(this);
});
}

View File

@@ -137,14 +137,14 @@ define('notifications', ['sounds', 'translator', 'components', 'navigator', 'ben
return parseInt(a.datetime, 10) > parseInt(b.datetime, 10) ? -1 : 1;
});
translator.toggleTimeagoShorthand();
for (var i = 0; i < notifs.length; i += 1) {
notifs[i].timeago = $.timeago(new Date(parseInt(notifs[i].datetime, 10)));
}
translator.toggleTimeagoShorthand();
Benchpress.parse('partials/notifications_list', { notifications: notifs }, function (html) {
notifList.translateHtml(html);
translator.toggleTimeagoShorthand(function () {
for (var i = 0; i < notifs.length; i += 1) {
notifs[i].timeago = $.timeago(new Date(parseInt(notifs[i].datetime, 10)));
}
translator.toggleTimeagoShorthand();
Benchpress.parse('partials/notifications_list', { notifications: notifs }, function (html) {
notifList.translateHtml(html);
});
});
});
};

View File

@@ -576,23 +576,29 @@
adaptor.getTranslations(language, namespace, callback);
},
toggleTimeagoShorthand: function toggleTimeagoShorthand() {
var tmp = assign({}, jQuery.timeago.settings.strings);
jQuery.timeago.settings.strings = assign({}, adaptor.timeagoShort);
adaptor.timeagoShort = assign({}, tmp);
toggleTimeagoShorthand: function toggleTimeagoShorthand(callback) {
function toggle() {
var tmp = assign({}, jQuery.timeago.settings.strings);
jQuery.timeago.settings.strings = assign({}, adaptor.timeagoShort);
adaptor.timeagoShort = assign({}, tmp);
if (typeof callback === 'function') {
callback();
}
}
if (!adaptor.timeagoShort) {
var languageCode = utils.userLangToTimeagoCode(config.userLang);
var originalSettings = assign({}, jQuery.timeago.settings.strings);
jQuery.getScript(config.relative_path + '/assets/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function () {
adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings);
jQuery.timeago.settings.strings = assign({}, originalSettings);
toggle();
});
} else {
toggle();
}
},
prepareDOM: function prepareDOM() {
// Load the appropriate timeago locale file,
// and correct NodeBB language codes to timeago codes, if necessary
var languageCode = utils.userLangToTimeagoCode(config.userLang);
adaptor.timeagoShort = assign({}, jQuery.timeago.settings.strings);
jQuery.getScript(config.relative_path + '/assets/vendor/jquery/timeago/locales/jquery.timeago.' + languageCode + '-short.js').done(function () {
// Switch back to long-form
adaptor.toggleTimeagoShorthand();
});
// Add directional code if necessary
adaptor.translate('[[language:dir]]', function (value) {
if (value && !$('html').attr('data-dir')) {

View File

@@ -138,6 +138,8 @@ module.exports = function (Categories) {
if (sort === 'most_posts') {
set = 'cid:' + cid + ':tids:posts';
} else if (sort === 'most_votes') {
set = 'cid:' + cid + ':tids:votes';
}
if (data.targetUid) {
@@ -163,7 +165,7 @@ module.exports = function (Categories) {
Categories.getSortedSetRangeDirection = function (sort, callback) {
sort = sort || 'newest_to_oldest';
var direction = sort === 'newest_to_oldest' || sort === 'most_posts' ? 'highest-to-lowest' : 'lowest-to-highest';
var direction = sort === 'newest_to_oldest' || sort === 'most_posts' || sort === 'most_votes' ? 'highest-to-lowest' : 'lowest-to-highest';
plugins.fireHook('filter:categories.getSortedSetRangeDirection', {
sort: sort,
direction: direction,

View File

@@ -3,7 +3,7 @@
var fs = require('fs');
var path = require('path');
var packageInstall = require('../meta/package-install');
var packageInstall = require('./package-install');
var dirname = require('./paths').baseDir;
// check to make sure dependencies are installed
@@ -12,12 +12,17 @@ try {
} catch (e) {
if (e.code === 'ENOENT') {
console.warn('package.json not found.');
console.log('Populating package.json...\n');
console.log('Populating package.json...');
packageInstall.updatePackageFile();
packageInstall.preserveExtraneousPlugins();
console.log('OK'.green + '\n'.reset);
try {
require('colors');
console.log('OK'.green);
} catch (e) {
console.log('OK');
}
} else {
throw e;
}
@@ -33,7 +38,7 @@ try {
console.warn('Dependencies not yet installed.');
console.log('Installing them now...\n');
packageInstall.npmInstallProduction();
packageInstall.installAll();
require('colors');
console.log('OK'.green + '\n'.reset);

View File

@@ -29,14 +29,27 @@ function updatePackageFile() {
exports.updatePackageFile = updatePackageFile;
function npmInstallProduction() {
cproc.execSync('npm i --production', {
function installAll() {
process.stdout.write('\n');
var prod = global.env !== 'development';
var command = 'npm install';
try {
var packageManager = require('nconf').get('package_manager');
if (packageManager === 'yarn') {
command = 'yarn';
}
} catch (e) {
// ignore
}
cproc.execSync(command + (prod ? ' --production' : ''), {
cwd: path.join(__dirname, '../../'),
stdio: [0, 1, 2],
});
}
exports.npmInstallProduction = npmInstallProduction;
exports.installAll = installAll;
function preserveExtraneousPlugins() {
// Skip if `node_modules/` is not found or inaccessible

View File

@@ -7,9 +7,18 @@ var cproc = require('child_process');
var semver = require('semver');
var fs = require('fs');
var path = require('path');
var nconf = require('nconf');
var paths = require('./paths');
var packageManager = nconf.get('package_manager');
var packageManagerExecutable = packageManager === 'yarn' ? 'yarn' : 'npm';
var packageManagerInstallArgs = packageManager === 'yarn' ? ['add'] : ['install', '--save'];
if (process.platform === 'win32') {
packageManagerExecutable += '.cmd';
}
var dirname = paths.baseDir;
function getModuleVersions(modules, callback) {
@@ -38,58 +47,54 @@ function getInstalledPlugins(callback) {
async.parallel({
files: async.apply(fs.readdir, path.join(dirname, 'node_modules')),
deps: async.apply(fs.readFile, path.join(dirname, 'package.json'), { encoding: 'utf-8' }),
bundled: async.apply(fs.readFile, path.join(dirname, 'install/package.json'), { encoding: 'utf-8' }),
}, function (err, payload) {
if (err) {
return callback(err);
}
var isNbbModule = /^nodebb-(?:plugin|theme|widget|rewards)-[\w-]+$/;
var moduleName;
var isGitRepo;
var checklist;
payload.files = payload.files.filter(function (file) {
return isNbbModule.test(file);
});
try {
payload.deps = JSON.parse(payload.deps).dependencies;
payload.bundled = [];
payload.installed = [];
payload.deps = Object.keys(JSON.parse(payload.deps).dependencies);
payload.bundled = Object.keys(JSON.parse(payload.bundled).dependencies);
} catch (err) {
return callback(err);
}
for (moduleName in payload.deps) {
if (isNbbModule.test(moduleName)) {
payload.bundled.push(moduleName);
}
}
payload.bundled = payload.bundled.filter(function (pkgName) {
return isNbbModule.test(pkgName);
});
payload.deps = payload.deps.filter(function (pkgName) {
return isNbbModule.test(pkgName);
});
// Whittle down deps to send back only extraneously installed plugins/themes/etc
payload.files.forEach(function (moduleName) {
try {
fs.accessSync(path.join(dirname, 'node_modules', moduleName, '.git'));
isGitRepo = true;
} catch (e) {
isGitRepo = false;
checklist = payload.deps.filter(function (pkgName) {
if (payload.bundled.includes(pkgName)) {
return false;
}
if (
payload.files.indexOf(moduleName) !== -1 && // found in `node_modules/`
payload.bundled.indexOf(moduleName) === -1 && // not found in `package.json`
!fs.lstatSync(path.join(dirname, 'node_modules', moduleName)).isSymbolicLink() && // is not a symlink
!isGitRepo // .git/ does not exist, so it is not a git repository
) {
payload.installed.push(moduleName);
// Ignore git repositories
try {
fs.accessSync(path.join(dirname, 'node_modules', pkgName, '.git'));
return false;
} catch (e) {
return true;
}
});
getModuleVersions(payload.installed, callback);
getModuleVersions(checklist, callback);
});
}
function getCurrentVersion(callback) {
fs.readFile(path.join(dirname, 'package.json'), { encoding: 'utf-8' }, function (err, pkg) {
fs.readFile(path.join(dirname, 'install/package.json'), { encoding: 'utf-8' }, function (err, pkg) {
if (err) {
return callback(err);
}
@@ -105,19 +110,19 @@ function getCurrentVersion(callback) {
function checkPlugins(standalone, callback) {
if (standalone) {
console.log('Checking installed plugins and themes for updates... ');
process.stdout.write('Checking installed plugins and themes for updates... ');
}
async.waterfall([
async.apply(async.parallel, {
plugins: async.apply(getInstalledPlugins),
version: async.apply(getCurrentVersion),
plugins: getInstalledPlugins,
version: getCurrentVersion,
}),
function (payload, next) {
var toCheck = Object.keys(payload.plugins);
if (!toCheck.length) {
console.log('OK'.green + ''.reset);
process.stdout.write(' OK'.green + ''.reset);
return next(null, []); // no extraneous plugins installed
}
@@ -127,10 +132,10 @@ function checkPlugins(standalone, callback) {
json: true,
}, function (err, res, body) {
if (err) {
console.log('error'.red + ''.reset);
process.stdout.write('error'.red + ''.reset);
return next(err);
}
console.log('OK'.green + ''.reset);
process.stdout.write(' OK'.green + ''.reset);
if (!Array.isArray(body) && toCheck.length === 1) {
body = [body];
@@ -172,11 +177,10 @@ function upgradePlugins(callback) {
}
if (found && found.length) {
console.log('\nA total of ' + String(found.length).bold + ' package(s) can be upgraded:');
process.stdout.write('\n\nA total of ' + String(found.length).bold + ' package(s) can be upgraded:\n\n');
found.forEach(function (suggestObj) {
console.log(' * '.yellow + suggestObj.name.reset + ' (' + suggestObj.current.yellow + ' -> '.reset + suggestObj.suggested.green + ')\n'.reset);
process.stdout.write(' * '.yellow + suggestObj.name.reset + ' (' + suggestObj.current.yellow + ' -> '.reset + suggestObj.suggested.green + ')\n'.reset);
});
console.log('');
} else {
if (standalone) {
console.log('\nAll packages up-to-date!'.green + ''.reset);
@@ -190,7 +194,7 @@ function upgradePlugins(callback) {
prompt.start();
prompt.get({
name: 'upgrade',
description: 'Proceed with upgrade (y|n)?'.reset,
description: '\nProceed with upgrade (y|n)?'.reset,
type: 'string',
}, function (err, result) {
if (err) {
@@ -199,15 +203,16 @@ function upgradePlugins(callback) {
if (['y', 'Y', 'yes', 'YES'].indexOf(result.upgrade) !== -1) {
console.log('\nUpgrading packages...');
var args = ['i'];
found.forEach(function (suggestObj) {
args.push(suggestObj.name + '@' + suggestObj.suggested);
});
var args = packageManagerInstallArgs.concat(found.map(function (suggestObj) {
return suggestObj.name + '@' + suggestObj.suggested;
}));
cproc.execFile((process.platform === 'win32') ? 'npm.cmd' : 'npm', args, { stdio: 'ignore' }, callback);
cproc.execFile(packageManagerExecutable, args, { stdio: 'ignore' }, function (err) {
callback(err, false);
});
} else {
console.log('Package upgrades skipped'.yellow + '. Check for upgrades at any time by running "'.reset + './nodebb upgrade-plugins'.green + '".'.reset);
callback();
console.log('Package upgrades skipped'.yellow + '. Check for upgrades at any time by running "'.reset + './nodebb upgrade -p'.green + '".'.reset);
callback(null, true);
}
});
});

View File

@@ -3,7 +3,7 @@
var async = require('async');
var nconf = require('nconf');
var packageInstall = require('../meta/package-install');
var packageInstall = require('./package-install');
var upgrade = require('../upgrade');
var build = require('../meta/build');
var db = require('../database');
@@ -22,7 +22,7 @@ var steps = {
install: {
message: 'Bringing base dependencies up to date...',
handler: function (next) {
packageInstall.npmInstallProduction();
packageInstall.installAll();
next();
},
},
@@ -53,10 +53,12 @@ var steps = {
function runSteps(tasks) {
tasks = tasks.map(function (key, i) {
return function (next) {
console.log(((i + 1) + '. ').bold + steps[key].message.yellow);
return steps[key].handler(function (err) {
process.stdout.write('\n' + ((i + 1) + '. ').bold + steps[key].message.yellow);
return steps[key].handler(function (err, inhibitOk) {
if (err) { return next(err); }
console.log(' OK'.green);
if (!inhibitOk) {
process.stdout.write(' OK'.green + '\n'.reset);
}
next();
});
};
@@ -73,7 +75,7 @@ function runSteps(tasks) {
var columns = process.stdout.columns;
var spaces = columns ? new Array(Math.floor(columns / 2) - (message.length / 2) + 1).join(' ') : ' ';
console.log('\n' + spaces + message.green.bold + '\n'.reset);
console.log('\n\n' + spaces + message.green.bold + '\n'.reset);
process.exit();
});
@@ -81,7 +83,7 @@ function runSteps(tasks) {
function runUpgrade(upgrades, options) {
console.log('\nUpdating NodeBB...'.cyan);
options = options || {};
// disable mongo timeouts during upgrade
nconf.set('mongo:options:socketTimeoutMS', 0);

View File

@@ -148,7 +148,10 @@ editController.uploadPicture = function (req, res, next) {
return helpers.notAllowed(req, res);
}
user.uploadPicture(updateUid, userPhoto, next);
user.uploadCroppedPicture({
uid: updateUid,
file: userPhoto,
}, next);
},
], function (err, image) {
file.delete(userPhoto.path);

View File

@@ -254,6 +254,10 @@ function getHomePageRoutes(userData, callback) {
route: 'recent',
name: 'Recent',
},
{
route: 'top',
name: 'Top',
},
{
route: 'popular',
name: 'Popular',
@@ -292,6 +296,3 @@ function getHomePageRoutes(userData, callback) {
},
], callback);
}
module.exports = settingsController;

View File

@@ -42,7 +42,7 @@ cacheController.get = function (req, res) {
dump: req.query.debug ? JSON.stringify(objectCache.dump(), null, 4) : false,
hits: utils.addCommas(String(objectCache.hits)),
misses: utils.addCommas(String(objectCache.misses)),
missRatio: (1 - (objectCache.hits / (objectCache.hits + objectCache.misses))).toFixed(4),
hitRatio: (objectCache.hits / (objectCache.hits + objectCache.misses)).toFixed(4),
};
}

View File

@@ -37,6 +37,10 @@ homePageController.get = function (req, res, next) {
route: 'recent',
name: 'Recent',
},
{
route: 'top',
name: 'Top',
},
{
route: 'popular',
name: 'Popular',

View File

@@ -30,7 +30,6 @@ apiController.loadConfig = function (req, callback) {
config.maximumTagsPerTopic = parseInt(meta.config.maximumTagsPerTopic || 5, 10);
config.minimumTagLength = meta.config.minimumTagLength || 3;
config.maximumTagLength = meta.config.maximumTagLength || 15;
config.hasImageUploadPlugin = plugins.hasListeners('filter:uploadImage');
config.useOutgoingLinksPage = parseInt(meta.config.useOutgoingLinksPage, 10) === 1;
config.allowGuestSearching = parseInt(meta.config.allowGuestSearching, 10) === 1;
config.allowGuestUserSearching = parseInt(meta.config.allowGuestUserSearching, 10) === 1;

View File

@@ -57,6 +57,10 @@ authenticationController.register = function (req, res) {
user.isPasswordValid(userData.password, next);
},
function (next) {
res.locals.processLogin = true; // set it to false in plugin if you wish to just register only
plugins.fireHook('filter:register.check', { req: req, res: res, userData: userData }, next);
},
function (result, next) {
registerAndLoginUser(req, res, userData, next);
},
], function (err, data) {
@@ -100,8 +104,7 @@ function registerAndLoginUser(req, res, userData, callback) {
user.shouldQueueUser(req.ip, next);
},
function (queue, next) {
res.locals.processLogin = true; // set it to false in plugin if you wish to just register only
plugins.fireHook('filter:register.check', { req: req, res: res, userData: userData, queue: queue }, next);
plugins.fireHook('filter:register.shouldQueue', { req: req, res: res, userData: userData, queue: queue }, next);
},
function (data, next) {
if (data.queue) {

View File

@@ -199,14 +199,17 @@ function addTags(categoryData, res) {
}
res.locals.linkTags = [
{
rel: 'alternate',
type: 'application/rss+xml',
href: categoryData.rssFeedUrl,
},
{
rel: 'up',
href: nconf.get('url'),
},
];
if (!categoryData['feeds:disableRSS']) {
res.locals.linkTags.push({
rel: 'alternate',
type: 'application/rss+xml',
href: categoryData.rssFeedUrl,
});
}
}

View File

@@ -19,6 +19,7 @@ Controllers.category = require('./category');
Controllers.unread = require('./unread');
Controllers.recent = require('./recent');
Controllers.popular = require('./popular');
Controllers.top = require('./top');
Controllers.tags = require('./tags');
Controllers.search = require('./search');
Controllers.user = require('./user');

84
src/controllers/top.js Normal file
View File

@@ -0,0 +1,84 @@
'use strict';
var async = require('async');
var nconf = require('nconf');
var querystring = require('querystring');
var user = require('../user');
var topics = require('../topics');
var meta = require('../meta');
var helpers = require('./helpers');
var pagination = require('../pagination');
var topController = module.exports;
topController.get = function (req, res, next) {
var page = parseInt(req.query.page, 10) || 1;
var stop = 0;
var settings;
var cid = req.query.cid;
var filter = req.params.filter || '';
var categoryData;
var rssToken;
if (!helpers.validFilters[filter]) {
return next();
}
async.waterfall([
function (next) {
async.parallel({
settings: function (next) {
user.getSettings(req.uid, next);
},
watchedCategories: function (next) {
helpers.getWatchedCategories(req.uid, cid, next);
},
rssToken: function (next) {
user.auth.getFeedToken(req.uid, next);
},
}, next);
},
function (results, next) {
rssToken = results.rssToken;
settings = results.settings;
categoryData = results.watchedCategories;
var start = Math.max(0, (page - 1) * settings.topicsPerPage);
stop = start + settings.topicsPerPage - 1;
topics.getTopTopics(cid, req.uid, start, stop, filter, next);
},
function (data) {
data.categories = categoryData.categories;
data.selectedCategory = categoryData.selectedCategory;
data.selectedCids = categoryData.selectedCids;
data.nextStart = stop + 1;
data.set = 'topics:votes';
data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1;
data.rssFeedUrl = nconf.get('relative_path') + '/top.rss';
if (req.uid) {
data.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken;
}
data.title = meta.config.homePageTitle || '[[pages:home]]';
data.filters = helpers.buildFilters('top', filter);
data.selectedFilter = data.filters.find(function (filter) {
return filter && filter.selected;
});
var pageCount = Math.max(1, Math.ceil(data.topicCount / settings.topicsPerPage));
data.pagination = pagination.create(page, pageCount, req.query);
if (req.originalUrl.startsWith(nconf.get('relative_path') + '/api/top') || req.originalUrl.startsWith(nconf.get('relative_path') + '/top')) {
data.title = '[[pages:top]]';
data.breadcrumbs = helpers.buildBreadcrumbs([{ text: '[[top:title]]' }]);
}
data.querystring = cid ? '?' + querystring.stringify({ cid: cid }) : '';
res.render('top', data);
},
], next);
};

View File

@@ -205,16 +205,11 @@ function buildBreadcrumbs(topicData, callback) {
}
function addTags(topicData, req, res) {
function findPost(index) {
for (var i = 0; i < topicData.posts.length; i += 1) {
if (parseInt(topicData.posts[i].index, 10) === parseInt(index, 10)) {
return topicData.posts[i];
}
}
}
var description = '';
var postAtIndex = findPost(Math.max(0, req.params.post_index - 1));
var postAtIndex = topicData.posts.find(function (postData) {
return parseInt(postData.index, 10) === parseInt(Math.max(0, req.params.post_index - 1), 10);
});
var description = '';
if (postAtIndex && postAtIndex.content) {
description = utils.stripHTMLTags(utils.decodeHTMLEntities(postAtIndex.content));
}
@@ -222,27 +217,8 @@ function addTags(topicData, req, res) {
if (description.length > 255) {
description = description.substr(0, 255) + '...';
}
var ogImageUrl = '';
if (topicData.thumb) {
ogImageUrl = topicData.thumb;
} else if (topicData.category.backgroundImage && (!postAtIndex || !postAtIndex.index)) {
ogImageUrl = topicData.category.backgroundImage;
} else if (postAtIndex && postAtIndex.user && postAtIndex.user.picture) {
ogImageUrl = postAtIndex.user.picture;
} else if (meta.config['og:image']) {
ogImageUrl = meta.config['og:image'];
} else if (meta.config['brand:logo']) {
ogImageUrl = meta.config['brand:logo'];
} else {
ogImageUrl = '/logo.png';
}
if (typeof ogImageUrl === 'string' && ogImageUrl.indexOf('http') === -1) {
ogImageUrl = nconf.get('url') + ogImageUrl;
}
description = description.replace(/\n/g, ' ');
res.locals.metaTags = [
{
name: 'title',
@@ -264,16 +240,6 @@ function addTags(topicData, req, res) {
property: 'og:type',
content: 'article',
},
{
property: 'og:image',
content: ogImageUrl,
noEscape: true,
},
{
property: 'og:image:url',
content: ogImageUrl,
noEscape: true,
},
{
property: 'article:published_time',
content: utils.toISOString(topicData.timestamp),
@@ -288,18 +254,23 @@ function addTags(topicData, req, res) {
},
];
addOGImageTags(res, topicData, postAtIndex);
res.locals.linkTags = [
{
rel: 'alternate',
type: 'application/rss+xml',
href: topicData.rssFeedUrl,
},
{
rel: 'canonical',
href: nconf.get('url') + '/topic/' + topicData.slug,
},
];
if (!topicData['feeds:disableRSS']) {
res.locals.linkTags.push({
rel: 'alternate',
type: 'application/rss+xml',
href: topicData.rssFeedUrl,
});
}
if (topicData.category) {
res.locals.linkTags.push({
rel: 'up',
@@ -308,6 +279,60 @@ function addTags(topicData, req, res) {
}
}
function addOGImageTags(res, topicData, postAtIndex) {
var ogImageUrl = '';
if (topicData.thumb) {
ogImageUrl = topicData.thumb;
} else if (topicData.category.backgroundImage && (!postAtIndex || !postAtIndex.index)) {
ogImageUrl = topicData.category.backgroundImage;
} else if (postAtIndex && postAtIndex.user && postAtIndex.user.picture) {
ogImageUrl = postAtIndex.user.picture;
} else if (meta.config['og:image']) {
ogImageUrl = meta.config['og:image'];
} else if (meta.config['brand:logo']) {
ogImageUrl = meta.config['brand:logo'];
} else {
ogImageUrl = '/logo.png';
}
addOGImageTag(res, ogImageUrl);
addOGImageTagsForPosts(res, topicData.posts);
}
function addOGImageTagsForPosts(res, posts) {
posts.forEach(function (postData) {
var regex = /src\s*=\s*"(.+?)"/g;
var match = regex.exec(postData.content);
while (match !== null) {
var image = match[1];
if (image.startsWith(nconf.get('url') + '/plugins')) {
return;
}
addOGImageTag(res, image);
match = regex.exec(postData.content);
}
});
}
function addOGImageTag(res, imageUrl) {
if (typeof imageUrl === 'string' && !imageUrl.startsWith('http')) {
imageUrl = nconf.get('url') + imageUrl;
}
res.locals.metaTags.push({
property: 'og:image',
content: imageUrl,
noEscape: true,
});
res.locals.metaTags.push({
property: 'og:image:url',
content: imageUrl,
noEscape: true,
});
}
topicsController.teaser = function (req, res, next) {
var tid = req.params.topic_id;

View File

@@ -66,7 +66,7 @@ module.exports = function (db, module) {
if (!key) {
return callback();
}
module.getObjectField(key, 'value', callback);
module.getObjectField(key, 'data', callback);
};
module.set = function (key, value, callback) {
@@ -74,7 +74,7 @@ module.exports = function (db, module) {
if (!key) {
return callback();
}
var data = { value: value };
var data = { data: value };
module.setObject(key, data, callback);
};
@@ -115,7 +115,7 @@ module.exports = function (db, module) {
return callback(null, 'set');
} else if (keys.length === 3 && data.hasOwnProperty('_key') && data.hasOwnProperty('array')) {
return callback(null, 'list');
} else if (keys.length === 3 && data.hasOwnProperty('_key') && data.hasOwnProperty('value')) {
} else if (keys.length === 3 && data.hasOwnProperty('_key') && data.hasOwnProperty('data')) {
return callback(null, 'string');
}
callback(null, 'hash');

View File

@@ -41,6 +41,22 @@ module.exports = function (db, module) {
key = { $in: key };
}
if (start < 0 && start > stop) {
return callback(null, []);
}
var reverse = false;
if (start === 0 && stop < -1) {
reverse = true;
sort *= -1;
start = Math.abs(stop + 1);
stop = -1;
} else if (start < 0 && stop > start) {
var tmp1 = Math.abs(stop + 1);
stop = Math.abs(start + 1);
start = tmp1;
}
var limit = stop - start + 1;
if (limit <= 0) {
limit = 0;
@@ -54,7 +70,9 @@ module.exports = function (db, module) {
if (err || !data) {
return callback(err);
}
if (reverse) {
data.reverse();
}
if (!withScores) {
data = data.map(function (item) {
return item.value;

View File

@@ -240,7 +240,7 @@ module.exports = function (Groups) {
old: oldName,
new: newName,
});
Groups.resetCache();
next();
},
], next);

View File

@@ -44,10 +44,25 @@ module.exports = function (Messaging) {
};
Messaging.canEdit = function (messageId, uid, callback) {
canEditDelete(messageId, uid, 'edit', callback);
};
Messaging.canDelete = function (messageId, uid, callback) {
canEditDelete(messageId, uid, 'delete', callback);
};
function canEditDelete(messageId, uid, type, callback) {
var durationConfig = '';
if (type === 'edit') {
durationConfig = 'chatEditDuration';
} else if (type === 'delete') {
durationConfig = 'chatDeleteDuration';
}
if (parseInt(meta.config.disableChat, 10) === 1) {
return callback(null, false);
return callback(new Error('[[error:chat-disabled]]'));
} else if (parseInt(meta.config.disableChatMessageEditing, 10) === 1) {
return callback(null, false);
return callback(new Error('[[error:chat-message-editing-disabled]]'));
}
async.waterfall([
@@ -56,25 +71,36 @@ module.exports = function (Messaging) {
},
function (userData, next) {
if (parseInt(userData.banned, 10) === 1) {
return callback(null, false);
return callback(new Error('[[error:user-banned]]'));
}
if (parseInt(meta.config.requireEmailConfirmation, 10) === 1 && parseInt(userData['email:confirmed'], 10) !== 1) {
return callback(null, false);
return callback(new Error('[[error:email-not-confirmed]]'));
}
async.parallel({
isAdmin: function (next) {
user.isAdministrator(uid, next);
},
messageData: function (next) {
Messaging.getMessageFields(messageId, ['fromuid', 'timestamp'], next);
},
}, next);
},
function (results, next) {
if (results.isAdmin) {
return callback();
}
var chatConfigDuration = parseInt(meta.config[durationConfig], 10);
if (chatConfigDuration && Date.now() - parseInt(results.messageData.timestamp, 10) > chatConfigDuration * 1000) {
return callback(new Error('[[error:chat-' + type + '-duration-expired, ' + meta.config[durationConfig] + ']]'));
}
Messaging.getMessageField(messageId, 'fromuid', next);
},
function (fromUid, next) {
if (parseInt(fromUid, 10) === parseInt(uid, 10)) {
return callback(null, true);
if (parseInt(results.messageData.fromuid, 10) === parseInt(uid, 10)) {
return callback();
}
user.isAdministrator(uid, next);
},
function (isAdmin, next) {
next(null, isAdmin);
next(new Error('[[error:cant-' + type + '-chat-message]]'));
},
], callback);
};
}
};

View File

@@ -99,6 +99,8 @@ function beforeBuild(targets, callback) {
var plugins = require('../plugins');
meta = require('../meta');
process.stdout.write(' started'.green + '\n'.reset);
async.series([
db.init,
meta.themes.setupPaths,
@@ -210,7 +212,7 @@ function build(targets, callback) {
}
winston.info('[build] Asset compilation successful. Completed in ' + totalTime + 'sec.');
callback();
callback(null, true);
});
}

View File

@@ -1,12 +1,12 @@
'use strict';
var nconf = require('nconf');
var validator = require('validator');
var async = require('async');
var winston = require('winston');
var plugins = require('../plugins');
var Meta = require('../meta');
var utils = require('../utils');
var Tags = module.exports;
@@ -66,7 +66,7 @@ Tags.parse = function (req, data, meta, link, callback) {
defaultLinks.push({
rel: 'search',
type: 'application/opensearchdescription+xml',
title: validator.escape(String(Meta.config.title || Meta.config.browserTitle || 'NodeBB')),
title: utils.escapeHTML(String(Meta.config.title || Meta.config.browserTitle || 'NodeBB')),
href: nconf.get('relative_path') + '/osd.xml',
});
}
@@ -116,7 +116,7 @@ Tags.parse = function (req, data, meta, link, callback) {
}
if (!tag.noEscape) {
tag.content = validator.escape(String(tag.content));
tag.content = utils.escapeHTML(String(tag.content));
}
return tag;
@@ -159,7 +159,7 @@ function addIfNotExists(meta, keyName, tagName, value) {
if (!exists && value) {
var data = {
content: validator.escape(String(value)),
content: utils.escapeHTML(String(value)),
};
data[keyName] = tagName;
meta.push(data);

View File

@@ -6,6 +6,8 @@ var jsesc = require('jsesc');
var db = require('../database');
var user = require('../user');
var topics = require('../topics');
var messaging = require('../messaging');
var meta = require('../meta');
var plugins = require('../plugins');
var navigation = require('../navigation');
@@ -109,10 +111,16 @@ module.exports = function (middleware) {
next(null, translated);
});
},
navigation: async.apply(navigation.get),
navigation: navigation.get,
tags: async.apply(meta.tags.parse, req, data, res.locals.metaTags, res.locals.linkTags),
banned: async.apply(user.isBanned, req.uid),
banReason: async.apply(user.getBannedReason, req.uid),
unreadTopicCount: async.apply(topics.getTotalUnread, req.uid),
unreadNewTopicCount: async.apply(topics.getTotalUnread, req.uid, 'new'),
unreadWatchedTopicCount: async.apply(topics.getTotalUnread, req.uid, 'watched'),
unreadChatCount: async.apply(messaging.getUnreadCount, req.uid),
unreadNotificationCount: async.apply(user.notifications.getUnreadCount, req.uid),
}, next);
},
function (results, next) {
@@ -131,8 +139,45 @@ module.exports = function (middleware) {
setBootswatchCSS(templateValues, res.locals.config);
var unreadCount = {
topic: results.unreadTopicCount || 0,
newTopic: results.unreadNewTopicCount || 0,
watchedTopic: results.unreadWatchedTopicCount || 0,
chat: results.unreadChatCount || 0,
notification: results.unreadNotificationCount || 0,
};
Object.keys(unreadCount).forEach(function (key) {
if (unreadCount[key] > 99) {
unreadCount[key] = '99+';
}
});
results.navigation = results.navigation.map(function (item) {
if (item.originalRoute === '/unread' && results.unreadTopicCount > 0) {
return Object.assign({}, item, {
content: unreadCount.topic,
iconClass: item.iconClass + ' unread-count',
});
}
if (item.originalRoute === '/unread/new' && results.unreadNewTopicCount > 0) {
return Object.assign({}, item, {
content: unreadCount.newTopic,
iconClass: item.iconClass + ' unread-count',
});
}
if (item.originalRoute === '/unread/watched' && results.unreadWatchedTopicCount > 0) {
return Object.assign({}, item, {
content: unreadCount.watchedTopic,
iconClass: item.iconClass + ' unread-count',
});
}
return item;
});
templateValues.browserTitle = results.browserTitle;
templateValues.navigation = results.navigation;
templateValues.unreadCount = unreadCount;
templateValues.metaTags = results.tags.meta;
templateValues.linkTags = results.tags.link;
templateValues.isAdmin = results.user.isAdmin;

View File

@@ -148,7 +148,7 @@ module.exports = function (middleware) {
},
function (userslug) {
if (!userslug) {
return res.status(401).send('not-authorized');
return controllers.helpers.notAllowed(req, res);
}
var path = req.path.replace(/^(\/api)?\/me/, '/user/' + userslug);
controllers.helpers.redirect(res, path);

View File

@@ -19,15 +19,16 @@ navigation.get = function (callback) {
data = data.filter(function (item) {
return item && item.enabled;
}).map(function (item) {
item.originalRoute = item.route;
if (!item.route.startsWith('http')) {
item.route = nconf.get('relative_path') + item.route;
}
for (var i in item) {
if (item.hasOwnProperty(i)) {
item[i] = translator.unescape(item[i]);
}
}
Object.keys(item).forEach(function (key) {
item[key] = translator.unescape(item[key]);
});
return item;
});

View File

@@ -82,20 +82,23 @@ module.exports = function (Plugins) {
var hookList = Plugins.loadedHooks[hook];
var hookType = hook.split(':')[0];
switch (hookType) {
case 'filter':
fireFilterHook(hook, hookList, params, callback);
break;
case 'action':
fireActionHook(hook, hookList, params, callback);
break;
case 'static':
fireStaticHook(hook, hookList, params, callback);
break;
default:
winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook);
break;
try {
switch (hookType) {
case 'filter':
fireFilterHook(hook, hookList, params, callback);
break;
case 'action':
fireActionHook(hook, hookList, params, callback);
break;
case 'static':
fireStaticHook(hook, hookList, params, callback);
break;
default:
winston.warn('[plugins] Unknown hookType: ' + hookType + ', hook : ' + hook);
break;
}
} catch (err) {
callback(err);
}
};

View File

@@ -13,6 +13,23 @@ var meta = require('../meta');
var pubsub = require('../pubsub');
var events = require('../events');
var packageManager = nconf.get('package_manager') === 'yarn' ? 'yarn' : 'npm';
var packageManagerExecutable = packageManager;
var packageManagerCommands = {
yarn: {
install: 'add',
uninstall: 'remove',
},
npm: {
install: 'install',
uninstall: 'uninstall',
},
};
if (process.platform === 'win32') {
packageManagerExecutable += '.cmd';
}
module.exports = function (Plugins) {
if (nconf.get('isPrimary') === 'true') {
pubsub.on('plugins:toggleInstall', function (data) {
@@ -95,7 +112,7 @@ module.exports = function (Plugins) {
setImmediate(next);
},
function (next) {
runNpmCommand(type, id, version || 'latest', next);
runPackageManagerCommand(type, id, version || 'latest', next);
},
function (next) {
Plugins.get(id, next);
@@ -107,8 +124,12 @@ module.exports = function (Plugins) {
], callback);
}
function runNpmCommand(command, pkgName, version, callback) {
cproc.execFile((process.platform === 'win32') ? 'npm.cmd' : 'npm', [command, pkgName + (command === 'install' ? '@' + version : ''), '--save'], function (err, stdout) {
function runPackageManagerCommand(command, pkgName, version, callback) {
cproc.execFile(packageManagerExecutable, [
packageManagerCommands[packageManager][command],
pkgName + (command === 'install' ? '@' + version : ''),
'--save',
], function (err, stdout) {
if (err) {
return callback(err);
}
@@ -125,7 +146,7 @@ module.exports = function (Plugins) {
function upgrade(id, version, callback) {
async.waterfall([
async.apply(runNpmCommand, 'install', id, version || 'latest'),
async.apply(runPackageManagerCommand, 'install', id, version || 'latest'),
function (next) {
Plugins.isActive(id, next);
},

View File

@@ -256,11 +256,27 @@ Posts.updatePostVoteCount = function (postData, callback) {
function (next) {
async.waterfall([
function (next) {
topics.getTopicField(postData.tid, 'mainPid', next);
topics.getTopicFields(postData.tid, ['mainPid', 'cid'], next);
},
function (mainPid, next) {
if (parseInt(mainPid, 10) === parseInt(postData.pid, 10)) {
return next();
function (topicData, next) {
if (parseInt(topicData.mainPid, 10) === parseInt(postData.pid, 10)) {
async.parallel([
function (next) {
topics.setTopicFields(postData.tid, {
upvotes: postData.upvotes,
downvotes: postData.downvotes,
}, next);
},
function (next) {
db.sortedSetAdd('topics:votes', postData.votes, postData.tid, next);
},
function (next) {
db.sortedSetAdd('cid:' + topicData.cid + ':tids:votes', postData.votes, postData.tid, next);
},
], function (err) {
next(err);
});
return;
}
db.sortedSetAdd('tid:' + postData.tid + ':posts:votes', postData.votes, postData.pid, next);
},
@@ -270,7 +286,10 @@ Posts.updatePostVoteCount = function (postData, callback) {
db.sortedSetAdd('posts:votes', postData.votes, postData.pid, next);
},
function (next) {
Posts.setPostFields(postData.pid, { upvotes: postData.upvotes, downvotes: postData.downvotes }, next);
Posts.setPostFields(postData.pid, {
upvotes: postData.upvotes,
downvotes: postData.downvotes,
}, next);
},
], function (err) {
callback(err);

View File

@@ -11,12 +11,11 @@ var dirname = require('./cli/paths').baseDir;
function setupWinston() {
winston.remove(winston.transports.Console);
winston.add(winston.transports.Console, {
colorize: true,
colorize: nconf.get('log-colorize') !== 'false',
timestamp: function () {
var date = new Date();
return nconf.get('json-logging') ? date.toJSON() :
date.getDate() + '/' + (date.getMonth() + 1) + ' ' +
date.toTimeString().substr(0, 8) + ' [' + global.process.pid + ']';
date.toISOString() + ' [' + global.process.pid + ']';
},
level: nconf.get('log-level') || (global.env === 'production' ? 'info' : 'verbose'),
json: !!nconf.get('json-logging'),

View File

@@ -19,6 +19,7 @@ module.exports = function (app, middleware) {
app.get('/topic/:topic_id.rss', middleware.maintenanceMode, generateForTopic);
app.get('/category/:category_id.rss', middleware.maintenanceMode, generateForCategory);
app.get('/recent.rss', middleware.maintenanceMode, generateForRecent);
app.get('/top.rss', middleware.maintenanceMode, generateForTop);
app.get('/popular.rss', middleware.maintenanceMode, generateForPopular);
app.get('/popular/:term.rss', middleware.maintenanceMode, generateForPopular);
app.get('/recentposts.rss', middleware.maintenanceMode, generateForRecentPosts);
@@ -209,6 +210,34 @@ function generateForRecent(req, res, next) {
], next);
}
function generateForTop(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
return controllers404.send404(req, res);
}
async.waterfall([
function (next) {
if (req.query.token && req.query.uid) {
db.getObjectField('user:' + req.query.uid, 'rss_token', next);
} else {
next(null, null);
}
},
function (token, next) {
next(null, token && token === req.query.token ? req.query.uid : req.uid);
},
function (uid, next) {
generateForTopics({
uid: uid,
title: 'Top Voted Topics',
description: 'A list of topics that have received the most votes',
feed_url: '/top.rss',
site_url: '/top',
}, 'topics:votes', req, res, next);
},
], next);
}
function generateForPopular(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
return controllers404.send404(req, res);

View File

@@ -65,6 +65,7 @@ function categoryRoutes(app, middleware, controllers) {
setupPageRoute(app, '/categories', middleware, [], controllers.categories.list);
setupPageRoute(app, '/popular/:term?', middleware, [], controllers.popular.get);
setupPageRoute(app, '/recent/:filter?', middleware, [], controllers.recent.get);
setupPageRoute(app, '/top/:filter?', middleware, [], controllers.top.get);
setupPageRoute(app, '/unread/:filter?', middleware, [middleware.authenticate], controllers.unread.get);
setupPageRoute(app, '/category/:category_id/:slug/:topic_index', middleware, [], controllers.category.get);

View File

@@ -209,7 +209,7 @@ function getMatchedPosts(pids, data, callback) {
db.getObjectsFields(cids, categoryFields, next);
},
tags: function (next) {
if (data.hasTags && data.hasTags.length) {
if (Array.isArray(data.hasTags) && data.hasTags.length) {
var tids = posts.map(function (post) {
return post && post.tid;
});
@@ -299,10 +299,10 @@ function filterByTimerange(posts, timeRange, timeFilter) {
}
function filterByTags(posts, hasTags) {
if (hasTags && hasTags.length) {
if (Array.isArray(hasTags) && hasTags.length) {
posts = posts.filter(function (post) {
var hasAllTags = false;
if (post && post.topic && post.topic.tags && post.topic.tags.length) {
if (post && post.topic && Array.isArray(post.topic.tags) && post.topic.tags.length) {
hasAllTags = hasTags.every(function (tag) {
return post.topic.tags.indexOf(tag) !== -1;
});

View File

@@ -183,7 +183,11 @@ User.search = function (socket, data, callback) {
var searchData;
async.waterfall([
function (next) {
user.search({ query: data.query, searchBy: data.searchBy, uid: socket.uid }, next);
user.search({
query: data.query,
searchBy: data.searchBy,
uid: socket.uid,
}, next);
},
function (_searchData, next) {
searchData = _searchData;

View File

@@ -246,10 +246,7 @@ SocketModules.chats.edit = function (socket, data, callback) {
function (next) {
Messaging.canEdit(data.mid, socket.uid, next);
},
function (allowed, next) {
if (!allowed) {
return next(new Error('[[error:cant-edit-chat-message]]'));
}
function (next) {
Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message, next);
},
], callback);
@@ -262,13 +259,9 @@ SocketModules.chats.delete = function (socket, data, callback) {
async.waterfall([
function (next) {
Messaging.canEdit(data.messageId, socket.uid, next);
Messaging.canDelete(data.messageId, socket.uid, next);
},
function (allowed, next) {
if (!allowed) {
return next(new Error('[[error:cant-delete-chat-message]]'));
}
function (next) {
Messaging.deleteMessage(data.messageId, data.roomId, next);
},
], callback);

View File

@@ -109,6 +109,17 @@ module.exports = function (SocketTopics) {
topics.getRecentTopics(data.cid, socket.uid, start, stop, data.filter, callback);
};
SocketTopics.loadMoreTopTopics = function (socket, data, callback) {
if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) {
return callback(new Error('[[error:invalid-data]]'));
}
var start = parseInt(data.after, 10);
var stop = start + Math.max(0, Math.min(meta.config.topicsPerPage || 20, parseInt(data.count, 10) || meta.config.topicsPerPage || 20) - 1);
topics.getTopTopics(data.cid, socket.uid, start, stop, data.filter, callback);
};
SocketTopics.loadMoreFromSet = function (socket, data, callback) {
if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0 || !data.set) {
return callback(new Error('[[error:invalid-data]]'));

View File

@@ -1,6 +1,7 @@
'use strict';
var async = require('async');
var winston = require('winston');
var user = require('../../user');
var meta = require('../../meta');
@@ -112,7 +113,12 @@ module.exports = function (SocketUser) {
reason: reason,
};
emailer.send('banned', uid, data, next);
emailer.send('banned', uid, data, function (err) {
if (err) {
winston.error('[emailer.send] ' + err.message);
}
next();
});
},
function (next) {
user.ban(uid, until, reason, next);

View File

@@ -49,23 +49,6 @@ module.exports = function (SocketUser) {
], callback);
};
SocketUser.uploadProfileImageFromUrl = function (socket, data, callback) {
if (!socket.uid || !data.url || !data.uid) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
function (next) {
user.isAdminOrSelf(socket.uid, data.uid, next);
},
function (next) {
user.uploadFromUrl(data.uid, data.url, next);
},
function (uploadedImage, next) {
next(null, uploadedImage ? uploadedImage.url : null);
},
], callback);
};
SocketUser.removeUploadedPicture = function (socket, data, callback) {
if (!socket.uid || !data || !data.uid) {
return callback(new Error('[[error:invalid-data]]'));

View File

@@ -21,6 +21,7 @@ require('./topics/delete')(Topics);
require('./topics/unread')(Topics);
require('./topics/recent')(Topics);
require('./topics/popular')(Topics);
require('./topics/top')(Topics);
require('./topics/user')(Topics);
require('./topics/fork')(Topics);
require('./topics/posts')(Topics);
@@ -165,6 +166,9 @@ Topics.getTopicsByTids = function (tids, uid, callback) {
topics[i].bookmark = results.bookmarks[i];
topics[i].unreplied = !topics[i].teaser;
topics[i].upvotes = parseInt(topics[i].upvotes, 10) || 0;
topics[i].downvotes = parseInt(topics[i].downvotes, 10) || 0;
topics[i].votes = topics[i].upvotes - topics[i].downvotes;
topics[i].icons = [];
}
}
@@ -226,6 +230,10 @@ Topics.getTopicWithPosts = function (topicData, set, uid, start, stop, reverse,
topicData.locked = parseInt(topicData.locked, 10) === 1;
topicData.pinned = parseInt(topicData.pinned, 10) === 1;
topicData.upvotes = parseInt(topicData.upvotes, 10) || 0;
topicData.downvotes = parseInt(topicData.downvotes, 10) || 0;
topicData.votes = topicData.upvotes - topicData.downvotes;
topicData.icons = [];
plugins.fireHook('filter:topic.get', { topic: topicData, uid: uid }, next);

View File

@@ -20,7 +20,12 @@ module.exports = function (Topics) {
}, next);
},
function (next) {
db.sortedSetsRemove(['topics:recent', 'topics:posts', 'topics:views'], tid, next);
db.sortedSetsRemove([
'topics:recent',
'topics:posts',
'topics:views',
'topics:votes',
], tid, next);
},
function (next) {
async.waterfall([
@@ -48,7 +53,7 @@ module.exports = function (Topics) {
var topicData;
async.waterfall([
function (next) {
Topics.getTopicFields(tid, ['cid', 'lastposttime', 'postcount', 'viewcount'], next);
Topics.getTopicData(tid, next);
},
function (_topicData, next) {
topicData = _topicData;
@@ -68,6 +73,11 @@ module.exports = function (Topics) {
function (next) {
db.sortedSetAdd('topics:views', topicData.viewcount, tid, next);
},
function (next) {
var upvotes = parseInt(topicData.upvotes, 10) || 0;
var downvotes = parseInt(topicData.downvotes, 10) || 0;
db.sortedSetAdd('topics:votes', upvotes - downvotes, tid, next);
},
function (next) {
async.waterfall([
function (next) {
@@ -138,7 +148,13 @@ module.exports = function (Topics) {
], next);
},
function (next) {
db.sortedSetsRemove(['topics:tid', 'topics:recent', 'topics:posts', 'topics:views'], tid, next);
db.sortedSetsRemove([
'topics:tid',
'topics:recent',
'topics:posts',
'topics:views',
'topics:votes',
], tid, next);
},
function (next) {
deleteTopicFromCategoryAndUser(tid, next);
@@ -196,6 +212,7 @@ module.exports = function (Topics) {
'cid:' + topicData.cid + ':tids:pinned',
'cid:' + topicData.cid + ':tids:posts',
'cid:' + topicData.cid + ':tids:lastposttime',
'cid:' + topicData.cid + ':tids:votes',
'cid:' + topicData.cid + ':recent_tids',
'cid:' + topicData.cid + ':uid:' + topicData.uid + ':tids',
'uid:' + topicData.uid + ':topics',

View File

@@ -253,6 +253,9 @@ module.exports = function (Topics) {
topicTags.forEach(function (tags, index) {
if (Array.isArray(tags)) {
topicTags[index] = tags.map(function (tag) { return tagData[tag]; });
topicTags[index].sort(function (tag1, tag2) {
return tag2.score - tag1.score;
});
}
});

View File

@@ -12,6 +12,8 @@ var plugins = require('../plugins');
var utils = require('../utils');
module.exports = function (Topics) {
var stripTeaserTags = utils.stripTags.concat(['img']);
Topics.getTeasers = function (topics, uid, callback) {
if (typeof uid === 'function') {
winston.warn('[Topics.getTeasers] this usage is deprecated please provide uid');
@@ -90,7 +92,7 @@ module.exports = function (Topics) {
if (tidToPost[topic.tid]) {
tidToPost[topic.tid].index = meta.config.teaserPost === 'first' ? 1 : counts[index];
if (tidToPost[topic.tid].content) {
tidToPost[topic.tid].content = utils.stripHTMLTags(tidToPost[topic.tid].content, utils.stripTags);
tidToPost[topic.tid].content = utils.stripHTMLTags(tidToPost[topic.tid].content, stripTeaserTags);
}
}
return tidToPost[topic.tid];

View File

@@ -179,12 +179,15 @@ module.exports = function (Topics) {
async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:pinned', Date.now(), tid),
async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids', tid),
async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:posts', tid),
async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:votes', tid),
], next);
} else {
var votes = (parseInt(topicData.upvotes, 10) || 0) - (parseInt(topicData.downvotes, 10) || 0);
async.parallel([
async.apply(db.sortedSetRemove, 'cid:' + topicData.cid + ':tids:pinned', tid),
async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids', topicData.lastposttime, tid),
async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:posts', topicData.postcount, tid),
async.apply(db.sortedSetAdd, 'cid:' + topicData.cid + ':tids:votes', votes, tid),
], next);
}
},

90
src/topics/top.js Normal file
View File

@@ -0,0 +1,90 @@
'use strict';
var async = require('async');
var db = require('../database');
var privileges = require('../privileges');
var user = require('../user');
var meta = require('../meta');
module.exports = function (Topics) {
Topics.getTopTopics = function (cid, uid, start, stop, filter, callback) {
var topTopics = {
nextStart: 0,
topics: [],
};
if (cid && !Array.isArray(cid)) {
cid = [cid];
}
async.waterfall([
function (next) {
var key = 'topics:votes';
if (cid) {
key = cid.map(function (cid) {
return 'cid:' + cid + ':tids:votes';
});
}
db.getSortedSetRevRange(key, 0, 199, next);
},
function (tids, next) {
filterTids(tids, uid, filter, cid, next);
},
function (tids, next) {
topTopics.topicCount = tids.length;
tids = tids.slice(start, stop + 1);
Topics.getTopicsByTids(tids, uid, next);
},
function (topicData, next) {
topTopics.topics = topicData;
topTopics.nextStart = stop + 1;
next(null, topTopics);
},
], callback);
};
function filterTids(tids, uid, filter, cid, callback) {
async.waterfall([
function (next) {
if (filter === 'watched') {
Topics.filterWatchedTids(tids, uid, next);
} else if (filter === 'new') {
Topics.filterNewTids(tids, uid, next);
} else if (filter === 'unreplied') {
Topics.filterUnrepliedTids(tids, next);
} else {
Topics.filterNotIgnoredTids(tids, uid, next);
}
},
function (tids, next) {
privileges.topics.filterTids('read', tids, uid, next);
},
function (tids, next) {
async.parallel({
ignoredCids: function (next) {
if (filter === 'watched' || parseInt(meta.config.disableRecentCategoryFilter, 10) === 1) {
return next(null, []);
}
user.getIgnoredCategories(uid, next);
},
topicData: function (next) {
Topics.getTopicsFields(tids, ['tid', 'cid'], next);
},
}, next);
},
function (results, next) {
cid = cid && cid.map(String);
tids = results.topicData.filter(function (topic) {
if (topic && topic.cid) {
return results.ignoredCids.indexOf(topic.cid.toString()) === -1 && (!cid || (cid.length && cid.indexOf(topic.cid.toString()) !== -1));
}
return false;
}).map(function (topic) {
return topic.tid;
});
next(null, tids);
},
], callback);
}
};

View File

@@ -189,7 +189,7 @@ Upgrade.process = function (files, skipCount, callback) {
}, next);
},
function (next) {
console.log('Upgrade complete!\n'.green);
console.log('Schema update complete!\n'.green);
setImmediate(next);
},
], callback);
@@ -205,7 +205,7 @@ Upgrade.incrementProgress = function (value) {
if (this.total) {
percentage = Math.floor((this.current / this.total) * 100) + '%';
filled = Math.floor((this.current / this.total) * 15);
unfilled = 15 - filled;
unfilled = Math.max(0, 15 - filled);
}
readline.cursorTo(process.stdout, 0);

View File

@@ -0,0 +1,67 @@
'use strict';
var async = require('async');
var db = require('../../database');
module.exports = {
name: 'Change the schema of simple keys so they don\'t use value field (mongodb only)',
timestamp: Date.UTC(2017, 11, 18),
method: function (callback) {
var configJSON = require('../../../config.json');
var isMongo = configJSON.hasOwnProperty('mongo') && configJSON.database === 'mongo';
var progress = this.progress;
if (!isMongo) {
return callback();
}
var client = db.client;
var cursor;
async.waterfall([
function (next) {
client.collection('objects').count({
_key: { $exists: true },
value: { $exists: true },
score: { $exists: false },
}, next);
},
function (count, next) {
progress.total = count;
cursor = client.collection('objects').find({
_key: { $exists: true },
value: { $exists: true },
score: { $exists: false },
}).batchSize(1000);
var done = false;
async.whilst(
function () {
return !done;
},
function (next) {
async.waterfall([
function (next) {
cursor.next(next);
},
function (item, next) {
progress.incr();
if (item === null) {
done = true;
return next();
}
if (Object.keys(item).length === 3 && item.hasOwnProperty('_key') && item.hasOwnProperty('value')) {
client.collection('objects').update({ _key: item._key }, { $rename: { value: 'data' } }, next);
} else {
next();
}
},
], function (err) {
next(err);
});
},
next
);
},
], callback);
},
};

View File

@@ -0,0 +1,60 @@
'use strict';
var async = require('async');
var batch = require('../../batch');
var db = require('../../database');
module.exports = {
name: 'Add votes to topics',
timestamp: Date.UTC(2017, 11, 8),
method: function (callback) {
var progress = this.progress;
batch.processSortedSet('topics:tid', function (tids, next) {
async.eachLimit(tids, 500, function (tid, _next) {
progress.incr();
var topicData;
async.waterfall([
function (next) {
db.getObjectFields('topic:' + tid, ['mainPid', 'cid'], next);
},
function (_topicData, next) {
topicData = _topicData;
if (!topicData.mainPid || !topicData.cid) {
return _next();
}
db.getObject('post:' + topicData.mainPid, next);
},
function (postData, next) {
if (!postData) {
return _next();
}
var upvotes = parseInt(postData.upvotes, 10) || 0;
var downvotes = parseInt(postData.downvotes, 10) || 0;
var data = {
upvotes: upvotes,
downvotes: downvotes,
};
var votes = upvotes - downvotes;
async.parallel([
function (next) {
db.setObject('topic:' + tid, data, next);
},
function (next) {
db.sortedSetAdd('topics:votes', votes, tid, next);
},
function (next) {
db.sortedSetAdd('cid:' + topicData.cid + ':tids:votes', votes, tid, next);
},
], function (err) {
next(err);
});
},
], _next);
}, next);
}, {
progress: progress,
batch: 500,
}, callback);
},
};

View File

@@ -239,6 +239,10 @@ User.isAdminOrGlobalModOrSelf = function (callerUid, uid, callback) {
isSelfOrMethod(callerUid, uid, User.isAdminOrGlobalMod, callback);
};
User.isPrivilegedOrSelf = function (callerUid, uid, callback) {
isSelfOrMethod(callerUid, uid, User.isPrivileged, callback);
};
function isSelfOrMethod(callerUid, uid, method, callback) {
if (parseInt(callerUid, 10) === parseInt(uid, 10)) {
return callback();

View File

@@ -129,10 +129,12 @@ module.exports = function (User) {
},
function (sessions, next) {
sessions = sessions.map(function (sessObj) {
sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString();
sessObj.meta.ip = validator.escape(String(sessObj.meta.ip));
if (sessObj.meta) {
sessObj.meta.datetimeISO = new Date(sessObj.meta.datetime).toISOString();
sessObj.meta.ip = validator.escape(String(sessObj.meta.ip));
}
return sessObj.meta;
});
}).filter(Boolean);
next(null, sessions);
},
], callback);

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