diff --git a/install/package.json b/install/package.json
index add6f1e85a..ed6e6a8b2a 100644
--- a/install/package.json
+++ b/install/package.json
@@ -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",
diff --git a/public/language/ar/admin/menu.json b/public/language/ar/admin/menu.json
index 2b836ed0f7..07fe387d20 100644
--- a/public/language/ar/admin/menu.json
+++ b/public/language/ar/admin/menu.json
@@ -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",
diff --git a/public/language/bg/topic.json b/public/language/bg/topic.json
index 3e83e70b12..c09c9927d8 100644
--- a/public/language/bg/topic.json
+++ b/public/language/bg/topic.json
@@ -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": "Отхвърляне",
diff --git a/public/language/de/admin/appearance/customise.json b/public/language/de/admin/appearance/customise.json
index b3c9f4e1ba..342552e881 100644
--- a/public/language/de/admin/appearance/customise.json
+++ b/public/language/de/admin/appearance/customise.json
@@ -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 <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.",
+ "custom-header.description": "Füge dein benutzerdefiniertes HTML hier ein (wie z.B. meta Tags etc.), welche in die <head> Sektion im Markup des Forums eingefügt werden. script Tags sind erlaubt, es wird aber davon abgeraten, da es den Benutzerdefiniertes Javascript Tab gibt.",
"custom-header.enable": "Benutzerdefinierten Header aktivieren",
"custom-css.livereload": "Live-Aktualisierung aktivieren",
diff --git a/public/language/de/admin/menu.json b/public/language/de/admin/menu.json
index 32c4fa2797..965724e772 100644
--- a/public/language/de/admin/menu.json
+++ b/public/language/de/admin/menu.json
@@ -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",
diff --git a/public/language/de/admin/settings/notifications.json b/public/language/de/admin/settings/notifications.json
index 6fda1fb5b0..e66499b081 100644
--- a/public/language/de/admin/settings/notifications.json
+++ b/public/language/de/admin/settings/notifications.json
@@ -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)"
}
\ No newline at end of file
diff --git a/public/language/de/admin/settings/post.json b/public/language/de/admin/settings/post.json
index b9076f8cee..99c65998c4 100644
--- a/public/language/de/admin/settings/post.json
+++ b/public/language/de/admin/settings/post.json
@@ -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",
diff --git a/public/language/de/admin/settings/user.json b/public/language/de/admin/settings/user.json
index 6e2c89bbce..316a66ffae 100644
--- a/public/language/de/admin/settings/user.json
+++ b/public/language/de/admin/settings/user.json
@@ -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)",
diff --git a/public/language/de/email.json b/public/language/de/email.json
index 8e4368dd6c..f110343626 100644
--- a/public/language/de/email.json
+++ b/public/language/de/email.json
@@ -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.",
diff --git a/public/language/de/error.json b/public/language/de/error.json
index 36ab2d796f..096dc01623 100644
--- a/public/language/de/error.json
+++ b/public/language/de/error.json
@@ -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!"
}
\ No newline at end of file
diff --git a/public/language/de/notifications.json b/public/language/de/notifications.json
index 481cee2149..93858808b7 100644
--- a/public/language/de/notifications.json
+++ b/public/language/de/notifications.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/language/de/topic.json b/public/language/de/topic.json
index 1505a77485..4a01b115ac 100644
--- a/public/language/de/topic.json
+++ b/public/language/de/topic.json
@@ -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",
diff --git a/public/language/en-GB/admin/manage/users.json b/public/language/en-GB/admin/manage/users.json
index f1651a814b..5b68fcdc91 100644
--- a/public/language/en-GB/admin/manage/users.json
+++ b/public/language/en-GB/admin/manage/users.json
@@ -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",
diff --git a/public/language/en-GB/admin/settings/chat.json b/public/language/en-GB/admin/settings/chat.json
index 0b22127341..c538790b95 100644
--- a/public/language/en-GB/admin/settings/chat.json
+++ b/public/language/en-GB/admin/settings/chat.json
@@ -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)"
}
\ No newline at end of file
diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json
index 66516bd3c5..7700006520 100644
--- a/public/language/en-GB/error.json
+++ b/public/language/en-GB/error.json
@@ -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.",
diff --git a/public/language/en-GB/global.json b/public/language/en-GB/global.json
index 741fb4d2b8..b61d8f27b9 100644
--- a/public/language/en-GB/global.json
+++ b/public/language/en-GB/global.json
@@ -66,6 +66,7 @@
"topics": "Topics",
"posts": "Posts",
"best": "Best",
+ "votes": "Votes",
"upvoters": "Upvoters",
"upvoted": "Upvoted",
"downvoters": "Downvoters",
diff --git a/public/language/en-GB/pages.json b/public/language/en-GB/pages.json
index c89266b661..201d10ef0a 100644
--- a/public/language/en-GB/pages.json
+++ b/public/language/en-GB/pages.json
@@ -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",
diff --git a/public/language/en-GB/top.json b/public/language/en-GB/top.json
new file mode 100644
index 0000000000..b8a05bfa5f
--- /dev/null
+++ b/public/language/en-GB/top.json
@@ -0,0 +1,4 @@
+{
+ "title": "Top",
+ "no_top_topics": "No top topics"
+}
\ No newline at end of file
diff --git a/public/language/fa-IR/error.json b/public/language/fa-IR/error.json
index 29b36ecae9..27ca06d761 100644
--- a/public/language/fa-IR/error.json
+++ b/public/language/fa-IR/error.json
@@ -135,5 +135,5 @@
"invalid-home-page-route": "مسیر صفحه اصلی نامعتبر است",
"invalid-session": "عدم تطابق جلسه",
"invalid-session-text": "به نظر میرسد این جلسه برای ورود دیگر فعال نیست و یا با سرور هماهنگ نیست. لطفا این صفحه را رفرش کنید.",
- "no-topics-selected": "No topics selected!"
+ "no-topics-selected": "هیچ موضوعی انتخاب نشده است !"
}
\ No newline at end of file
diff --git a/public/language/fa-IR/topic.json b/public/language/fa-IR/topic.json
index 3102da6b99..4c04d49e00 100644
--- a/public/language/fa-IR/topic.json
+++ b/public/language/fa-IR/topic.json
@@ -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": "دور بیانداز",
diff --git a/public/language/fa-IR/user.json b/public/language/fa-IR/user.json
index ae8014ee56..54cd114210 100644
--- a/public/language/fa-IR/user.json
+++ b/public/language/fa-IR/user.json
@@ -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": "پیوندهای به بیرون را در برگ جدید باز کن",
diff --git a/public/language/pl/admin/settings/pagination.json b/public/language/pl/admin/settings/pagination.json
index 37200ef559..dafdc1acd3 100644
--- a/public/language/pl/admin/settings/pagination.json
+++ b/public/language/pl/admin/settings/pagination.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/language/pl/admin/settings/post.json b/public/language/pl/admin/settings/post.json
index 41a53c1882..efb08114ba 100644
--- a/public/language/pl/admin/settings/post.json
+++ b/public/language/pl/admin/settings/post.json
@@ -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",
diff --git a/public/language/pl/unread.json b/public/language/pl/unread.json
index 392bc03530..01888a3b22 100644
--- a/public/language/pl/unread.json
+++ b/public/language/pl/unread.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/language/sr/error.json b/public/language/sr/error.json
index e5e02dba37..c44fef981b 100644
--- a/public/language/sr/error.json
+++ b/public/language/sr/error.json
@@ -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": "Нема одабраних тема!"
}
\ No newline at end of file
diff --git a/public/language/sr/topic.json b/public/language/sr/topic.json
index 53f860c3d3..787aa27a4b 100644
--- a/public/language/sr/topic.json
+++ b/public/language/sr/topic.json
@@ -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": "Одбаци",
diff --git a/public/language/vi/admin/admin.json b/public/language/vi/admin/admin.json
index 726cef0e8b..8fdf8c210a 100644
--- a/public/language/vi/admin/admin.json
+++ b/public/language/vi/admin/admin.json
@@ -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",
diff --git a/public/language/vi/admin/advanced/cache.json b/public/language/vi/admin/advanced/cache.json
index 505b1a4510..d8b94de7a9 100644
--- a/public/language/vi/admin/advanced/cache.json
+++ b/public/language/vi/admin/advanced/cache.json
@@ -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",
diff --git a/public/language/vi/admin/settings/email.json b/public/language/vi/admin/settings/email.json
index 50ad2e06ea..96fb624791 100644
--- a/public/language/vi/admin/settings/email.json
+++ b/public/language/vi/admin/settings/email.json
@@ -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",
diff --git a/public/language/vi/category.json b/public/language/vi/category.json
index 494fa8b153..f9cf6a500a 100644
--- a/public/language/vi/category.json
+++ b/public/language/vi/category.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/language/vi/email.json b/public/language/vi/email.json
index 5199f136a5..0dc0cc8be6 100644
--- a/public/language/vi/email.json
+++ b/public/language/vi/email.json
@@ -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!"
}
\ No newline at end of file
diff --git a/public/language/vi/error.json b/public/language/vi/error.json
index 894ab8a3c6..490b661dd3 100644
--- a/public/language/vi/error.json
+++ b/public/language/vi/error.json
@@ -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!"
}
\ No newline at end of file
diff --git a/public/language/vi/global.json b/public/language/vi/global.json
index bca009cd8c..5049b7bed2 100644
--- a/public/language/vi/global.json
+++ b/public/language/vi/global.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/language/vi/language.json b/public/language/vi/language.json
index f0787f748a..e6b933b8af 100644
--- a/public/language/vi/language.json
+++ b/public/language/vi/language.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/language/vi/notifications.json b/public/language/vi/notifications.json
index 1f3e674954..3605d52d4a 100644
--- a/public/language/vi/notifications.json
+++ b/public/language/vi/notifications.json
@@ -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ừ %1",
"upvoted_your_post_in": "%1 đã bình chọn bài của bạn trong %2.",
"upvoted_your_post_in_dual": "%1 và %2 đã tán thành với bài viết của bạn trong %3.",
@@ -29,9 +29,9 @@
"user_flagged_post_in": "%1 gắn cờ 1 bài trong %2",
"user_flagged_post_in_dual": "%1 và %2 đã gắn cờ một bài viết trong %3",
"user_flagged_post_in_multiple": "%1 và %2 người khác đã gắn cờ bài viết của bạn trong %3",
- "user_flagged_user": "%1 flagged a user profile (%2)",
- "user_flagged_user_dual": "%1 and %2 flagged a user profile (%3)",
- "user_flagged_user_multiple": "%1 and %2 others flagged a user profile (%3)",
+ "user_flagged_user": "%1 đã cảnh báo một người dùng (%2)",
+ "user_flagged_user_dual": "%1 và %2 đã cảnh báo một người dùng (%3)",
+ "user_flagged_user_multiple": "%1 và %2 người khác đã cảnh báo người dùng (%3)",
"user_posted_to": "%1 đã trả lời %2",
"user_posted_to_dual": "%1 và %2 đã trả lời: %3",
"user_posted_to_multiple": "%1 và %2 người khác đã trả lời: %3",
@@ -41,24 +41,24 @@
"user_started_following_you_multiple": "%1 và %2 người khác đã bắt đầu theo dõi bạn.",
"new_register": "%1 đã gửi một yêu cầu tham gia.",
"new_register_multiple": "Có %1 đơn đăng ký đang chờ xem xét.",
- "flag_assigned_to_you": "Flag %1 has been assigned to you",
- "post_awaiting_review": "Post awaiting review",
+ "flag_assigned_to_you": "Cảnh báo %1 đã đượ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"
}
\ No newline at end of file
diff --git a/public/language/vi/pages.json b/public/language/vi/pages.json
index febfef2060..c70ca5e399 100644
--- a/public/language/vi/pages.json
+++ b/public/language/vi/pages.json
@@ -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",
diff --git a/public/language/vi/search.json b/public/language/vi/search.json
index 7e4211c2c8..fc4e78954b 100644
--- a/public/language/vi/search.json
+++ b/public/language/vi/search.json
@@ -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",
diff --git a/public/language/vi/success.json b/public/language/vi/success.json
index aff07d84ad..5b5e438697 100644
--- a/public/language/vi/success.json
+++ b/public/language/vi/success.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/language/vi/topic.json b/public/language/vi/topic.json
index 6f8290fa75..c59553269d 100644
--- a/public/language/vi/topic.json
+++ b/public/language/vi/topic.json
@@ -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.
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.
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ỏ",
diff --git a/public/language/vi/unread.json b/public/language/vi/unread.json
index e0609d5e04..3c9d149bac 100644
--- a/public/language/vi/unread.json
+++ b/public/language/vi/unread.json
@@ -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"
}
\ No newline at end of file
diff --git a/public/language/vi/user.json b/public/language/vi/user.json
index fb7413330f..03fb99398e 100644
--- a/public/language/vi/user.json
+++ b/public/language/vi/user.json
@@ -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à %1",
"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ủ đề mà 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ú"
}
\ No newline at end of file
diff --git a/public/language/zh-CN/error.json b/public/language/zh-CN/error.json
index 68b4c0a035..d0f3a62642 100644
--- a/public/language/zh-CN/error.json
+++ b/public/language/zh-CN/error.json
@@ -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": "没有主题被选中!"
}
\ No newline at end of file
diff --git a/public/language/zh-CN/topic.json b/public/language/zh-CN/topic.json
index 6687b7b58c..92dfcc59f2 100644
--- a/public/language/zh-CN/topic.json
+++ b/public/language/zh-CN/topic.json
@@ -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": "撤销",
diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js
index 8c22a65720..b6c7b7aa03 100644
--- a/public/src/admin/manage/users.js
+++ b/public/src/admin/manage/users.js
@@ -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;
diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js
index 35c30ab1a1..39cbb48c48 100644
--- a/public/src/ajaxify.js
+++ b/public/src/ajaxify.js
@@ -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');
diff --git a/public/src/client/account/edit.js b/public/src/client/account/edit.js
index 7fa2026823..027e328037 100644
--- a/public/src/client/account/edit.js
+++ b/public/src/client/account/edit.js
@@ -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;
});
});
diff --git a/public/src/client/chats.js b/public/src/client/chats.js
index 600331dc6f..f82623fd5d 100644
--- a/public/src/client/chats.js
+++ b/public/src/client/chats.js
@@ -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,
diff --git a/public/src/client/footer.js b/public/src/client/footer.js
index 7dcdade78b..7bc187921e 100644
--- a/public/src/client/footer.js
+++ b/public/src/client/footer.js
@@ -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) {
diff --git a/public/src/client/top.js b/public/src/client/top.js
new file mode 100644
index 0000000000..9e80cb668a
--- /dev/null
+++ b/public/src/client/top.js
@@ -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;
+});
diff --git a/public/src/client/topic.js b/public/src/client/topic.js
index b3fa02b509..54052d47c1 100644
--- a/public/src/client/topic.js
+++ b/public/src/client/topic.js
@@ -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);
diff --git a/public/src/modules/alerts.js b/public/src/modules/alerts.js
index 62f6da940e..0094c88f96 100644
--- a/public/src/modules/alerts.js
+++ b/public/src/modules/alerts.js
@@ -117,7 +117,6 @@ define('alerts', ['translator', 'components', 'benchpress'], function (translato
alert
.on('mouseenter', function () {
$(this).css('transition-duration', 0);
- console.log(this);
});
}
diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js
index a215c19475..8ce876eebe 100644
--- a/public/src/modules/notifications.js
+++ b/public/src/modules/notifications.js
@@ -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);
+ });
});
});
};
diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js
index 817f6095b6..6376d9e4d0 100644
--- a/public/src/modules/translator.js
+++ b/public/src/modules/translator.js
@@ -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')) {
diff --git a/src/categories/topics.js b/src/categories/topics.js
index 2a3bd71e5a..32022b7a96 100644
--- a/src/categories/topics.js
+++ b/src/categories/topics.js
@@ -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,
diff --git a/src/cli/index.js b/src/cli/index.js
index 0bc95a7c6d..aa7ef2c257 100644
--- a/src/cli/index.js
+++ b/src/cli/index.js
@@ -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);
diff --git a/src/meta/package-install.js b/src/cli/package-install.js
similarity index 85%
rename from src/meta/package-install.js
rename to src/cli/package-install.js
index 2ae93612a0..5f6f9917a5 100644
--- a/src/meta/package-install.js
+++ b/src/cli/package-install.js
@@ -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
diff --git a/src/cli/upgrade-plugins.js b/src/cli/upgrade-plugins.js
index 4011546fd3..3be00cb5d1 100644
--- a/src/cli/upgrade-plugins.js
+++ b/src/cli/upgrade-plugins.js
@@ -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);
}
});
});
diff --git a/src/cli/upgrade.js b/src/cli/upgrade.js
index 179970192b..e5ab2b6c0c 100644
--- a/src/cli/upgrade.js
+++ b/src/cli/upgrade.js
@@ -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);
diff --git a/src/controllers/accounts/edit.js b/src/controllers/accounts/edit.js
index 61f4cad452..b5bd4cf3a5 100644
--- a/src/controllers/accounts/edit.js
+++ b/src/controllers/accounts/edit.js
@@ -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);
diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js
index 0bb3ef1744..cadb7c12f8 100644
--- a/src/controllers/accounts/settings.js
+++ b/src/controllers/accounts/settings.js
@@ -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;
diff --git a/src/controllers/admin/cache.js b/src/controllers/admin/cache.js
index 4de2045518..baa461a21b 100644
--- a/src/controllers/admin/cache.js
+++ b/src/controllers/admin/cache.js
@@ -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),
};
}
diff --git a/src/controllers/admin/homepage.js b/src/controllers/admin/homepage.js
index bc0971622f..45fabeb2d4 100644
--- a/src/controllers/admin/homepage.js
+++ b/src/controllers/admin/homepage.js
@@ -37,6 +37,10 @@ homePageController.get = function (req, res, next) {
route: 'recent',
name: 'Recent',
},
+ {
+ route: 'top',
+ name: 'Top',
+ },
{
route: 'popular',
name: 'Popular',
diff --git a/src/controllers/api.js b/src/controllers/api.js
index 8034f958f6..4f9430826a 100644
--- a/src/controllers/api.js
+++ b/src/controllers/api.js
@@ -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;
diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js
index c6b418ff9b..b2d46dcd70 100644
--- a/src/controllers/authentication.js
+++ b/src/controllers/authentication.js
@@ -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) {
diff --git a/src/controllers/category.js b/src/controllers/category.js
index f231349a49..89f924e479 100644
--- a/src/controllers/category.js
+++ b/src/controllers/category.js
@@ -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,
+ });
+ }
}
diff --git a/src/controllers/index.js b/src/controllers/index.js
index 5539cae729..bddab21a11 100644
--- a/src/controllers/index.js
+++ b/src/controllers/index.js
@@ -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');
diff --git a/src/controllers/top.js b/src/controllers/top.js
new file mode 100644
index 0000000000..7b500533d5
--- /dev/null
+++ b/src/controllers/top.js
@@ -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);
+};
diff --git a/src/controllers/topics.js b/src/controllers/topics.js
index c75f5c3602..13228f5779 100644
--- a/src/controllers/topics.js
+++ b/src/controllers/topics.js
@@ -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;
diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js
index eff7459a69..278ae6c413 100644
--- a/src/database/mongo/main.js
+++ b/src/database/mongo/main.js
@@ -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');
diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js
index 49b5862fe2..22c7e44196 100644
--- a/src/database/mongo/sorted.js
+++ b/src/database/mongo/sorted.js
@@ -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;
diff --git a/src/groups/update.js b/src/groups/update.js
index 73196700ca..e2fc4772e4 100644
--- a/src/groups/update.js
+++ b/src/groups/update.js
@@ -240,7 +240,7 @@ module.exports = function (Groups) {
old: oldName,
new: newName,
});
-
+ Groups.resetCache();
next();
},
], next);
diff --git a/src/messaging/edit.js b/src/messaging/edit.js
index f9c664d67f..b118ca03c5 100644
--- a/src/messaging/edit.js
+++ b/src/messaging/edit.js
@@ -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);
- };
+ }
};
diff --git a/src/meta/build.js b/src/meta/build.js
index df68e93375..2beb5f8af9 100644
--- a/src/meta/build.js
+++ b/src/meta/build.js
@@ -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);
});
}
diff --git a/src/meta/tags.js b/src/meta/tags.js
index e5a6a10c9c..babd1d5f9d 100644
--- a/src/meta/tags.js
+++ b/src/meta/tags.js
@@ -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);
diff --git a/src/middleware/header.js b/src/middleware/header.js
index 3824ff6fc3..5a896fcdd7 100644
--- a/src/middleware/header.js
+++ b/src/middleware/header.js
@@ -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;
diff --git a/src/middleware/user.js b/src/middleware/user.js
index d7b70377f6..e3123e6942 100644
--- a/src/middleware/user.js
+++ b/src/middleware/user.js
@@ -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);
diff --git a/src/navigation/index.js b/src/navigation/index.js
index 9aec34dd25..0712ce79f5 100644
--- a/src/navigation/index.js
+++ b/src/navigation/index.js
@@ -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;
});
diff --git a/src/plugins/hooks.js b/src/plugins/hooks.js
index 71b91beeef..020ea4e024 100644
--- a/src/plugins/hooks.js
+++ b/src/plugins/hooks.js
@@ -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);
}
};
diff --git a/src/plugins/install.js b/src/plugins/install.js
index 7bd407ca08..da03fd8d71 100644
--- a/src/plugins/install.js
+++ b/src/plugins/install.js
@@ -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);
},
diff --git a/src/posts.js b/src/posts.js
index f6b22b89ed..cf30bd4c64 100644
--- a/src/posts.js
+++ b/src/posts.js
@@ -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);
diff --git a/src/prestart.js b/src/prestart.js
index 9b592cf4af..dbc904b3a8 100644
--- a/src/prestart.js
+++ b/src/prestart.js
@@ -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'),
diff --git a/src/routes/feeds.js b/src/routes/feeds.js
index 5aaf3590c2..eba2fee4ee 100644
--- a/src/routes/feeds.js
+++ b/src/routes/feeds.js
@@ -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);
diff --git a/src/routes/index.js b/src/routes/index.js
index e9f9b26c0a..ce0a4045e8 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -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);
diff --git a/src/search.js b/src/search.js
index 71f212632e..4d2560e6cf 100644
--- a/src/search.js
+++ b/src/search.js
@@ -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;
});
diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js
index 3fb4dfb9ff..d8d9a0f282 100644
--- a/src/socket.io/admin/user.js
+++ b/src/socket.io/admin/user.js
@@ -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;
diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js
index e277c5c5e7..d58fb7fa59 100644
--- a/src/socket.io/modules.js
+++ b/src/socket.io/modules.js
@@ -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);
diff --git a/src/socket.io/topics/infinitescroll.js b/src/socket.io/topics/infinitescroll.js
index 17d44712ea..18be5f6341 100644
--- a/src/socket.io/topics/infinitescroll.js
+++ b/src/socket.io/topics/infinitescroll.js
@@ -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]]'));
diff --git a/src/socket.io/user/ban.js b/src/socket.io/user/ban.js
index a61a9b83ee..66f98eb061 100644
--- a/src/socket.io/user/ban.js
+++ b/src/socket.io/user/ban.js
@@ -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);
diff --git a/src/socket.io/user/picture.js b/src/socket.io/user/picture.js
index 682cd59239..8f8c0577c3 100644
--- a/src/socket.io/user/picture.js
+++ b/src/socket.io/user/picture.js
@@ -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]]'));
diff --git a/src/topics.js b/src/topics.js
index 44c263210d..f20c069d98 100644
--- a/src/topics.js
+++ b/src/topics.js
@@ -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);
diff --git a/src/topics/delete.js b/src/topics/delete.js
index 1c6f261f73..6121868c00 100644
--- a/src/topics/delete.js
+++ b/src/topics/delete.js
@@ -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',
diff --git a/src/topics/tags.js b/src/topics/tags.js
index f936d7da4a..4561c950fa 100644
--- a/src/topics/tags.js
+++ b/src/topics/tags.js
@@ -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;
+ });
}
});
diff --git a/src/topics/teaser.js b/src/topics/teaser.js
index 2827e74d41..28fbeb9a35 100644
--- a/src/topics/teaser.js
+++ b/src/topics/teaser.js
@@ -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];
diff --git a/src/topics/tools.js b/src/topics/tools.js
index 566fb62841..be87b8826f 100644
--- a/src/topics/tools.js
+++ b/src/topics/tools.js
@@ -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);
}
},
diff --git a/src/topics/top.js b/src/topics/top.js
new file mode 100644
index 0000000000..b4f10b9340
--- /dev/null
+++ b/src/topics/top.js
@@ -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);
+ }
+};
diff --git a/src/upgrade.js b/src/upgrade.js
index a0ceb5b7df..f30a5f43d4 100644
--- a/src/upgrade.js
+++ b/src/upgrade.js
@@ -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);
diff --git a/src/upgrades/1.7.3/key_value_schema_change.js b/src/upgrades/1.7.3/key_value_schema_change.js
new file mode 100644
index 0000000000..4e747f6846
--- /dev/null
+++ b/src/upgrades/1.7.3/key_value_schema_change.js
@@ -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);
+ },
+};
diff --git a/src/upgrades/1.7.3/topic_votes.js b/src/upgrades/1.7.3/topic_votes.js
new file mode 100644
index 0000000000..76a4d0900c
--- /dev/null
+++ b/src/upgrades/1.7.3/topic_votes.js
@@ -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);
+ },
+};
diff --git a/src/user.js b/src/user.js
index 2ad441c5ce..6661dca8a7 100644
--- a/src/user.js
+++ b/src/user.js
@@ -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();
diff --git a/src/user/auth.js b/src/user/auth.js
index 6d0002939e..195e14ce50 100644
--- a/src/user/auth.js
+++ b/src/user/auth.js
@@ -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);
diff --git a/src/user/digest.js b/src/user/digest.js
index 0ee46deba1..dba9c48f16 100644
--- a/src/user/digest.js
+++ b/src/user/digest.js
@@ -139,6 +139,7 @@ Digest.send = function (data, callback) {
notifications: notifications,
recent: data.topics,
interval: data.interval,
+ showUnsubscribe: true,
}, function (err) {
if (err) {
winston.error('[user/jobs] Could not send digest email', err);
diff --git a/src/user/picture.js b/src/user/picture.js
index 60991aa39c..d1a4dac7b0 100644
--- a/src/user/picture.js
+++ b/src/user/picture.js
@@ -1,8 +1,6 @@
'use strict';
var async = require('async');
-var request = require('request');
-var mime = require('mime');
var winston = require('winston');
var plugins = require('../plugins');
@@ -12,47 +10,6 @@ var meta = require('../meta');
var db = require('../database');
module.exports = function (User) {
- User.uploadPicture = function (uid, picture, callback) {
- User.uploadCroppedPicture({ uid: uid, file: picture }, callback);
- };
-
- User.uploadFromUrl = function (uid, url, callback) {
- if (!plugins.hasListeners('filter:uploadImage')) {
- return callback(new Error('[[error:no-plugin]]'));
- }
-
- async.waterfall([
- function (next) {
- request.head(url, next);
- },
- function (res, body, next) {
- var uploadSize = parseInt(meta.config.maximumProfileImageSize, 10) || 256;
- var size = res.headers['content-length'];
- var type = res.headers['content-type'];
- var extension = mime.getExtension(type);
-
- if (['png', 'jpeg', 'jpg', 'gif'].indexOf(extension) === -1) {
- return callback(new Error('[[error:invalid-image-extension]]'));
- }
-
- if (size > uploadSize * 1024) {
- return callback(new Error('[[error:file-too-big, ' + uploadSize + ']]'));
- }
-
- plugins.fireHook('filter:uploadImage', {
- uid: uid,
- image: {
- url: url,
- name: '',
- },
- }, next);
- },
- function (image, next) {
- next(null, image);
- },
- ], callback);
- };
-
User.updateCoverPosition = function (uid, position, callback) {
// Reject anything that isn't two percentages
if (!/^[\d.]+%\s[\d.]+%$/.test(position)) {
diff --git a/src/user/search.js b/src/user/search.js
index 37549232cd..198b027266 100644
--- a/src/user/search.js
+++ b/src/user/search.js
@@ -14,17 +14,19 @@ module.exports = function (User) {
var uid = data.uid || 0;
var paginate = data.hasOwnProperty('paginate') ? data.paginate : true;
- if (searchBy === 'ip') {
- return searchByIP(query, uid, callback);
- }
-
var startTime = process.hrtime();
var searchResult = {};
async.waterfall([
function (next) {
- var searchMethod = data.findUids || findUids;
- searchMethod(query, searchBy, data.hardCap, next);
+ if (searchBy === 'ip') {
+ searchByIP(query, next);
+ } else if (searchBy === 'uid') {
+ next(null, [query]);
+ } else {
+ var searchMethod = data.findUids || findUids;
+ searchMethod(query, searchBy, data.hardCap, next);
+ }
},
function (uids, next) {
filterAndSortUids(uids, data, next);
@@ -153,20 +155,7 @@ module.exports = function (User) {
}
}
- function searchByIP(ip, uid, callback) {
- var start = process.hrtime();
- async.waterfall([
- function (next) {
- db.getSortedSetRevRange('ip:' + ip + ':uid', 0, -1, next);
- },
- function (uids, next) {
- User.getUsers(uids, uid, next);
- },
- function (users, next) {
- var diff = process.hrtime(start);
- var timing = ((diff[0] * 1e3) + (diff[1] / 1e6)).toFixed(1);
- next(null, { timing: timing, users: users });
- },
- ], callback);
+ function searchByIP(ip, callback) {
+ db.getSortedSetRevRange('ip:' + ip + ':uid', 0, -1, callback);
}
};
diff --git a/src/user/settings.js b/src/user/settings.js
index df22ff5c42..df5ed93d71 100644
--- a/src/user/settings.js
+++ b/src/user/settings.js
@@ -120,8 +120,6 @@ module.exports = function (User) {
userLang: data.userLang || meta.config.defaultLang,
followTopicsOnCreate: data.followTopicsOnCreate,
followTopicsOnReply: data.followTopicsOnReply,
- sendChatNotifications: data.sendChatNotifications,
- sendPostNotifications: data.sendPostNotifications,
restrictChat: data.restrictChat,
topicSearchEnabled: data.topicSearchEnabled,
delayImageLoading: data.delayImageLoading,
diff --git a/src/views/admin/advanced/cache.tpl b/src/views/admin/advanced/cache.tpl
index 1c2d98ca93..0a07425a78 100644
--- a/src/views/admin/advanced/cache.tpl
+++ b/src/views/admin/advanced/cache.tpl
@@ -42,7 +42,7 @@
{objectCache.hits}
{objectCache.misses}
- {objectCache.missRatio}
+ {objectCache.hitRatio}
{objectCache.dump}
diff --git a/src/views/admin/manage/users.tpl b/src/views/admin/manage/users.tpl
index bebded1a5e..3eca4f998d 100644
--- a/src/views/admin/manage/users.tpl
+++ b/src/views/admin/manage/users.tpl
@@ -49,6 +49,9 @@