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%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%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%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%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 @@
+
+ + +
+ +
+ + +
+
diff --git a/src/views/admin/settings/user.tpl b/src/views/admin/settings/user.tpl index 1d2042e97d..8b2528624e 100644 --- a/src/views/admin/settings/user.tpl +++ b/src/views/admin/settings/user.tpl @@ -278,20 +278,6 @@
-
- -
- -
- -
-