diff --git a/README.md b/README.md index 485922d449..b08b6c3a84 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # NodeBB -[![Build Status](https://travis-ci.org/NodeBB/NodeBB.svg?branch=master)](https://travis-ci.org/nodebb/nodebb) +[![Build Status](https://travis-ci.org/NodeBB/NodeBB.svg?branch=master)](https://travis-ci.org/NodeBB/NodeBB) [![Dependency Status](https://david-dm.org/nodebb/nodebb.svg)](https://david-dm.org/nodebb/nodebb) [![Code Climate](https://codeclimate.com/github/designcreateplay/NodeBB.png)](https://codeclimate.com/github/designcreateplay/NodeBB) diff --git a/package.json b/package.json index 9440e3b6b3..01592c9035 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "validator": "~3.16.1", "winston": "~0.7.2", "xregexp": "~2.0.0", - "templates.js": "0.0.8" + "templates.js": "0.0.13" }, "devDependencies": { "mocha": "~1.13.0" @@ -67,7 +67,7 @@ "url": "https://github.com/NodeBB/NodeBB/issues" }, "engines": { - "node": ">=0.8" + "node": ">=0.10" }, "maintainers": [ { diff --git a/public/language/en_GB/modules.json b/public/language/en_GB/modules.json index fdaf14dd5b..9bf107bec6 100644 --- a/public/language/en_GB/modules.json +++ b/public/language/en_GB/modules.json @@ -13,7 +13,7 @@ "chat.pop-out": "Pop out chat", "chat.maximize": "Maximize", - "composer.user_said_in": "%1 said in %2:\n", - "composer.user_said": "%1 said:\n", + "composer.user_said_in": "%1 said in %2:", + "composer.user_said": "%1 said:", "composer.discard": "Are you sure you wish to discard this post?" } \ No newline at end of file diff --git a/public/language/en_GB/pages.json b/public/language/en_GB/pages.json index a91aec5e8e..3d444963e1 100644 --- a/public/language/en_GB/pages.json +++ b/public/language/en_GB/pages.json @@ -5,6 +5,7 @@ "recent": "Recent Topics", "users": "Registered Users", "notifications": "Notifications", + "tags": "Topics tagged under \"%1\"", "user.edit": "Editing \"%1\"", "user.following": "People %1 Follows", "user.followers": "People who Follow %1", diff --git a/public/language/en_GB/topic.json b/public/language/en_GB/topic.json index 8c77751419..c6fe5bc4ad 100644 --- a/public/language/en_GB/topic.json +++ b/public/language/en_GB/topic.json @@ -105,6 +105,7 @@ "more_users_and_guests": "%1 more user(s) and %2 guest(s)", "more_users": "%1 more user(s)", "more_guests": "%1 more guest(s)", + "users_and_others": "%1 and %2 others", "sort_by": "Sort by", "oldest_to_newest": "Oldest to Newest", diff --git a/public/language/et/global.json b/public/language/et/global.json index 45608e00bf..3cfa44bef3 100644 --- a/public/language/et/global.json +++ b/public/language/et/global.json @@ -50,9 +50,9 @@ "read_more": "loe veel", "posted_ago_by_guest": "postitatud %1 tagasi külalise poolt", "posted_ago_by": "postitatud %1 tagasi kasutaja %2 poolt", - "posted_ago": "postitatud %1 tagasi", + "posted_ago": "postitatud %1", "posted_in_ago_by_guest": "postitatud kategooriasse %1 %2 tagasi külalise poolt", - "posted_in_ago_by": "postitatud kategooriasse %1 %2 aega tagasi kasutaja %3 poolt", + "posted_in_ago_by": "postitatud kategooriasse %1 %2 kasutaja %3 poolt", "posted_in_ago": "postitatud kategooriasse %1 %2 tagasi", "replied_ago": "vastas %1", "user_posted_ago": "kasutaja %1 postitas %2 tagasi", diff --git a/public/language/et/topic.json b/public/language/et/topic.json index ff182da119..2bb35ffe5a 100644 --- a/public/language/et/topic.json +++ b/public/language/et/topic.json @@ -41,7 +41,7 @@ "thread_tools.pin": "Tõsta esile teema", "thread_tools.unpin": "Märgista teema", "thread_tools.lock": "Lukusta teema", - "thread_tools.unlock": "Eemalda märgistatud teema", + "thread_tools.unlock": "Taasava teema", "thread_tools.move": "Liiguta teema", "thread_tools.move_all": "Liiguta kõik", "thread_tools.fork": "Fork Topic", diff --git a/public/language/pt_BR/email.json b/public/language/pt_BR/email.json index 98e591ab02..92f058e6ff 100644 --- a/public/language/pt_BR/email.json +++ b/public/language/pt_BR/email.json @@ -1,20 +1,20 @@ { - "password-reset-requested": "Password Reset Requested - %1!", - "welcome-to": "Welcome to %1", - "greeting_no_name": "Hello", - "greeting_with_name": "Hello %1", - "welcome.text1": "Thank you for registering with %1!", + "password-reset-requested": "Recuperação de Senha Solicitada - %1!", + "welcome-to": "Bem vindo a %1", + "greeting_no_name": "Olá", + "greeting_with_name": "Olà %1", + "welcome.text1": "Obrigado por se registrar com %1!", "welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.", - "welcome.cta": "Click here to confirm your email address", + "welcome.cta": "Clique aqui para confirmar seu endereço de email", "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", "reset.text2": "To continue with the password reset, please click on the following link:", - "reset.cta": "Click here to reset your password", + "reset.cta": "Clique aqui para recuperar sua senha", "digest.notifications": "You have some unread notifications from %1:", - "digest.latest_topics": "Latest topics from %1", - "digest.cta": "Click here to visit %1", + "digest.latest_topics": "Últimos tópicos de %1", + "digest.cta": "Clique aqui para visitar %1", "digest.unsub.info": "This digest was sent to you due to your subscription settings.", - "digest.unsub.cta": "Click here to alter those settings", + "digest.unsub.cta": "Clique aqui para alterar suas configurações", "digest.daily.no_topics": "There have been no active topics in the past day", "test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.", - "closing": "Thanks!" + "closing": "Obrigado!" } \ No newline at end of file diff --git a/public/language/pt_BR/error.json b/public/language/pt_BR/error.json index 0f1f906336..9dca9b0ae2 100644 --- a/public/language/pt_BR/error.json +++ b/public/language/pt_BR/error.json @@ -25,7 +25,7 @@ "no-user": "Usuário não existe", "no-teaser": "Chamada não existe", "no-privileges": "Você não possui permissões para esta ação.", - "no-emailers-configured": "No email plugins were loaded, so a test email could not be sent", + "no-emailers-configured": "Nenhum plugin de email foi carregado, por isso o email de teste não pode ser enviado", "category-disabled": "Categoria desativada", "topic-locked": "Tópico trancado", "still-uploading": "Aguarde a conclusão dos uploads.", diff --git a/public/language/pt_BR/notifications.json b/public/language/pt_BR/notifications.json index 58e0bda322..08a4240c19 100644 --- a/public/language/pt_BR/notifications.json +++ b/public/language/pt_BR/notifications.json @@ -4,9 +4,9 @@ "see_all": "Visualizar todas as notificações", "back_to_home": "Voltar para %1", "outgoing_link": "Link Externo", - "outgoing_link_message": "You are now leaving %1.", - "continue_to": "Continue to %1", - "return_to": "Return to %1", + "outgoing_link_message": "Você deixou de seguir %1.", + "continue_to": "Continuar para %1", + "return_to": "Voltar para %1", "new_notification": "Nova notificação", "you_have_unread_notifications": "Você possui notificações não lidas.", "new_message_from": "Nova mensagem de %1", diff --git a/public/language/ru/email.json b/public/language/ru/email.json index 98e591ab02..d0fc8fabed 100644 --- a/public/language/ru/email.json +++ b/public/language/ru/email.json @@ -1,20 +1,20 @@ { - "password-reset-requested": "Password Reset Requested - %1!", - "welcome-to": "Welcome to %1", - "greeting_no_name": "Hello", - "greeting_with_name": "Hello %1", - "welcome.text1": "Thank you for registering with %1!", - "welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.", - "welcome.cta": "Click here to confirm your email address", - "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", - "reset.text2": "To continue with the password reset, please click on the following link:", - "reset.cta": "Click here to reset your password", - "digest.notifications": "You have some unread notifications from %1:", - "digest.latest_topics": "Latest topics from %1", - "digest.cta": "Click here to visit %1", - "digest.unsub.info": "This digest was sent to you due to your subscription settings.", - "digest.unsub.cta": "Click here to alter those settings", - "digest.daily.no_topics": "There have been no active topics in the past day", - "test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.", - "closing": "Thanks!" + "password-reset-requested": "Пароль сброшен - %1!", + "welcome-to": "Добро пожаловать на %1", + "greeting_no_name": "Здравствуйте!", + "greeting_with_name": "Здравствуйте, %1!", + "welcome.text1": "Благодарим за регистрацию %1! ", + "welcome.text2": "Для активации вашей учетной записи мы должны убедиться, что вы указали верный email адрес.", + "welcome.cta": "Перейдите по ссылке для подтверждения вашего email", + "reset.text1": "Мы получили запрос на изменение вашего пароля. Если не подавали запрос на изменение пароля, пожалуйста, проигнорируйте это сообщение.", + "reset.text2": "Для продолжения процедуры изменения пароля, пожалуйста, перейдите по ссылке:", + "reset.cta": "Кликните здесь для изменения пароля", + "digest.notifications": "У вас есть непрочитанные уведомления от %1:", + "digest.latest_topics": "Последние темы %1", + "digest.cta": "Кликните здесь для просмотра %1", + "digest.unsub.info": "Вам была выслана сводка новостей в соответствии с вашими настройками.", + "digest.unsub.cta": "Кликните здесь для изменения ваших настроек.", + "digest.daily.no_topics": "За минувший день новых тем нет.", + "test.text1": "Это тестовое сообщение для проверки почтового сервиса NodeBB.", + "closing": "Спасибо!" } \ No newline at end of file diff --git a/public/language/ru/error.json b/public/language/ru/error.json index e3afcd6dda..63a6a93355 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -15,7 +15,7 @@ "invalid-pagination-value": "Неверное значение пагинации", "username-taken": "Имя пользователя занято", "email-taken": "Email занят", - "email-not-confirmed": "Your email is not confirmed, please click here to confirm your email.", + "email-not-confirmed": "Ваш email не подтвержден, нажмите для подтверждения.", "username-too-short": "Слишком короткое имя пользователя", "user-banned": "Пользователь заблокирован", "no-category": "Несуществующая категория", @@ -25,10 +25,10 @@ "no-user": "Несуществующий пользователь", "no-teaser": "Несуществующее превью", "no-privileges": "У вас недостаточно прав, чтобы совершить данное действие.", - "no-emailers-configured": "No email plugins were loaded, so a test email could not be sent", + "no-emailers-configured": "Не подключен ни один плагин для отправки почты, поэтому тестовый email не может быть отправлен", "category-disabled": "Категория отключена", - "topic-locked": "Тема закрыт", - "still-uploading": "Пожалуйста, подождите завершения загрузки", + "topic-locked": "Тема закрыта", + "still-uploading": "Пожалуйста, подождите завершения загрузки.", "content-too-short": "Пост должен содержать минимум %1 символов.", "title-too-short": "Заголовок должен содержать минимум %1 символов.", "title-too-long": "Заголовок не может быть длиннее %1 символов.", diff --git a/public/language/ru/global.json b/public/language/ru/global.json index ced82c04eb..e83c49da26 100644 --- a/public/language/ru/global.json +++ b/public/language/ru/global.json @@ -13,7 +13,7 @@ "please_log_in": "Пожалуйста, войдите под своим аккаунтом", "logout": "Выйти", "posting_restriction_info": "Сообщения могут оставлять только зарегистрированные пользователи, нажмите сюда, чтобы войти", - "welcome_back": "Welcome Back", + "welcome_back": "С возвращением", "you_have_successfully_logged_in": "Вы вышли из аккаунта", "save_changes": "Сохранить изменения", "close": "Закрыть", diff --git a/public/language/ru/groups.json b/public/language/ru/groups.json index c00c111e11..a82a2bfc4b 100644 --- a/public/language/ru/groups.json +++ b/public/language/ru/groups.json @@ -1,7 +1,7 @@ { - "view_group": "View Group", - "details.title": "Group Details", - "details.members": "Member List", - "details.has_no_posts": "This group's members have not made any posts.", - "details.latest_posts": "Latest Posts" + "view_group": "Просмотр группы", + "details.title": "Информация о группе", + "details.members": "Список пользователей", + "details.has_no_posts": "Пользователями этой группы не публиковали никаких записей", + "details.latest_posts": "Последние записи" } \ No newline at end of file diff --git a/public/language/ru/modules.json b/public/language/ru/modules.json index 92d22ffa20..5cd840d3d0 100644 --- a/public/language/ru/modules.json +++ b/public/language/ru/modules.json @@ -1,18 +1,18 @@ { "chat.chatting_with": "Чат с ", - "chat.placeholder": "Type chat message here, press enter to send", + "chat.placeholder": "Введите сообщение, нажмите ENTER для отправки", "chat.send": "Отправить", "chat.no_active": "У вас нет активных чатов.", "chat.user_typing": "%1 печатает ...", "chat.user_has_messaged_you": "%1 отправил вам сообщение.", "chat.see_all": "Просмотр всех диалогов", - "chat.no-messages": "Please select a recipient to view chat message history", - "chat.recent-chats": "Recent Chats", - "chat.contacts": "Contacts", - "chat.message-history": "Message History", - "chat.pop-out": "Pop out chat", - "chat.maximize": "Maximize", - "composer.user_said_in": "%1 said in %2:", - "composer.user_said": "%1 said:", + "chat.no-messages": "Пожалуйста, выберите собеседника для просмотра истории сообщений", + "chat.recent-chats": "Последние переписки", + "chat.contacts": "Контакты", + "chat.message-history": "История сообщений", + "chat.pop-out": "Покинуть диалог", + "chat.maximize": "Развернуть", + "composer.user_said_in": "%1 сказал %2:", + "composer.user_said": "%1 сказал:", "composer.discard": "Вы уверены, что хотите отказаться от этого поста?" } \ No newline at end of file diff --git a/public/language/ru/notifications.json b/public/language/ru/notifications.json index fa840319ba..1648ebc8f8 100644 --- a/public/language/ru/notifications.json +++ b/public/language/ru/notifications.json @@ -4,9 +4,9 @@ "see_all": "Просмотреть все уведомления", "back_to_home": "Назад к %1", "outgoing_link": "Внешняя ссылка", - "outgoing_link_message": "You are now leaving %1.", - "continue_to": "Continue to %1", - "return_to": "Return to %1", + "outgoing_link_message": "Вы покидаете %1.", + "continue_to": "Перейти на %1", + "return_to": "Вернуться к %1", "new_notification": "Новое Уведомление", "you_have_unread_notifications": "У вас есть непрочитанные уведомления", "new_message_from": "Новое сообщение от %1", diff --git a/public/language/ru/recent.json b/public/language/ru/recent.json index 6e0ecb69ad..7dabef695e 100644 --- a/public/language/ru/recent.json +++ b/public/language/ru/recent.json @@ -3,6 +3,6 @@ "day": "День", "week": "Неделя", "month": "Месяц", - "year": "Year", + "year": "Год", "no_recent_topics": "Нет свежих тем." } \ No newline at end of file diff --git a/public/language/ru/search.json b/public/language/ru/search.json index d0ffc64f36..5f0a069bef 100644 --- a/public/language/ru/search.json +++ b/public/language/ru/search.json @@ -1,3 +1,3 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 результатов по фразе \"%2\", (%3 секунды) " } \ No newline at end of file diff --git a/public/language/ru/topic.json b/public/language/ru/topic.json index ba7e7662c2..554d1371de 100644 --- a/public/language/ru/topic.json +++ b/public/language/ru/topic.json @@ -54,7 +54,7 @@ "topic_move_success": "Эта тема успешно перемещена в %1", "post_delete_confirm": "Вы уверены, что хотите удалить этот пост?", "post_restore_confirm": "Вы уверены, что хотите восстановить этот пост?", - "post_purge_confirm": "Are you sure you want to purge this post?", + "post_purge_confirm": "Вы уверены, что хотите очистить эту запись?", "load_categories": "Загружаем Категории", "disabled_categories_note": "Отключенные категории затемненны", "confirm_move": "Перенести", diff --git a/public/language/ru/user.json b/public/language/ru/user.json index c3493e26ce..6f3e7c92e2 100644 --- a/public/language/ru/user.json +++ b/public/language/ru/user.json @@ -3,7 +3,7 @@ "offline": "Не в сети", "username": "Имя пользователя", "email": "Email", - "confirm_email": "Confirm Email", + "confirm_email": "Подтвердить Email", "fullname": "Полное имя", "website": "Сайт", "location": "Откуда", diff --git a/public/language/ru/users.json b/public/language/ru/users.json index b4121f10c8..46ebc41b69 100644 --- a/public/language/ru/users.json +++ b/public/language/ru/users.json @@ -6,5 +6,5 @@ "enter_username": "Введите имя пользователя для поиска", "load_more": "Загрузить еще", "user-not-found": "Пользователь не найден!", - "users-found-search-took": "Нашел %1 пользователя(ей)! Поиск занял %2 ms." + "users-found-search-took": "Нашел %1 пользователя(ей)! Поиск занял %2 мс." } \ No newline at end of file diff --git a/public/language/tr/email.json b/public/language/tr/email.json index 98e591ab02..96fd81431e 100644 --- a/public/language/tr/email.json +++ b/public/language/tr/email.json @@ -1,20 +1,20 @@ { - "password-reset-requested": "Password Reset Requested - %1!", - "welcome-to": "Welcome to %1", - "greeting_no_name": "Hello", - "greeting_with_name": "Hello %1", - "welcome.text1": "Thank you for registering with %1!", - "welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.", - "welcome.cta": "Click here to confirm your email address", - "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", - "reset.text2": "To continue with the password reset, please click on the following link:", - "reset.cta": "Click here to reset your password", - "digest.notifications": "You have some unread notifications from %1:", - "digest.latest_topics": "Latest topics from %1", - "digest.cta": "Click here to visit %1", - "digest.unsub.info": "This digest was sent to you due to your subscription settings.", - "digest.unsub.cta": "Click here to alter those settings", - "digest.daily.no_topics": "There have been no active topics in the past day", - "test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.", - "closing": "Thanks!" + "password-reset-requested": "Parola Değiştirme İsteği Gönderildi", + "welcome-to": "Hoşgeldiniz", + "greeting_no_name": "Merhaba", + "greeting_with_name": "Merhaba %1", + "welcome.text1": "Kaydolduğunuz için teşekkürler!", + "welcome.text2": "Hesabınızı aktif hale getirmek için, kaydolduğunuz e-posta adresinin size ait olduğunu onaylamamız gerekiyor.", + "welcome.cta": "E-posta adresinizi onaylamak için buraya tıklayın", + "reset.text1": "Şifrenizi değiştirmek istediğinize dair bir ileti aldık. Eğer böyle bir istek göndermediyseniz, lütfen bu e-postayı görmezden gelin.", + "reset.text2": "Parola değiştirme işlemine devam etmek için aşağıdaki bağlantıya tıklayın:", + "reset.cta": "Parolanızı değiştirmek için buraya tıklayın", + "digest.notifications": "Okunmamış bazı bildirimleriniz var", + "digest.latest_topics": "En güncel konular", + "digest.cta": "Ziyaret etmek için buraya tıklayın", + "digest.unsub.info": "Bu e-posta seçtiğiniz ayarlar nedeniyle gönderildi.", + "digest.unsub.cta": "Bu ayarları değiştirmek için buraya tıklayın", + "digest.daily.no_topics": "Geçtiğimiz gün içinde aktif bir konu yok.", + "test.text1": "Bu ileti NodeBB e-posta ayarlarınızın doğru çalışıp çalışmadığını kontrol etmek için gönderildi.", + "closing": "Teşekkürler!" } \ No newline at end of file diff --git a/public/language/tr/groups.json b/public/language/tr/groups.json index c00c111e11..804046d192 100644 --- a/public/language/tr/groups.json +++ b/public/language/tr/groups.json @@ -1,7 +1,7 @@ { - "view_group": "View Group", - "details.title": "Group Details", - "details.members": "Member List", - "details.has_no_posts": "This group's members have not made any posts.", - "details.latest_posts": "Latest Posts" + "view_group": "Grubu Gör", + "details.title": "Grup Detayları", + "details.members": "Üye Listesi", + "details.has_no_posts": "Bu grubun üyeleri henüz bir ileti göndermedi.", + "details.latest_posts": "En son iletiler" } \ No newline at end of file diff --git a/public/language/tr/search.json b/public/language/tr/search.json index d0ffc64f36..b384c82c72 100644 --- a/public/language/tr/search.json +++ b/public/language/tr/search.json @@ -1,3 +1,3 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 tane “%2“ bulundu (%3 saniye)" } \ No newline at end of file diff --git a/public/language/zh_CN/email.json b/public/language/zh_CN/email.json index 98e591ab02..ae359e04fb 100644 --- a/public/language/zh_CN/email.json +++ b/public/language/zh_CN/email.json @@ -1,20 +1,20 @@ { - "password-reset-requested": "Password Reset Requested - %1!", - "welcome-to": "Welcome to %1", - "greeting_no_name": "Hello", - "greeting_with_name": "Hello %1", - "welcome.text1": "Thank you for registering with %1!", - "welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.", - "welcome.cta": "Click here to confirm your email address", - "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", - "reset.text2": "To continue with the password reset, please click on the following link:", - "reset.cta": "Click here to reset your password", - "digest.notifications": "You have some unread notifications from %1:", - "digest.latest_topics": "Latest topics from %1", - "digest.cta": "Click here to visit %1", - "digest.unsub.info": "This digest was sent to you due to your subscription settings.", - "digest.unsub.cta": "Click here to alter those settings", - "digest.daily.no_topics": "There have been no active topics in the past day", - "test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.", - "closing": "Thanks!" + "password-reset-requested": "密码重设申请 - %1!", + "welcome-to": "欢迎来到 %1", + "greeting_no_name": "您好", + "greeting_with_name": "%1,您好", + "welcome.text1": "谢谢您使用 %1 注册帐户!", + "welcome.text2": "如需完全激活您的帐户,我们需要校验您注册的电子邮箱地址。", + "welcome.cta": "点击这里确认您的电子邮箱地址", + "reset.text1": "我们收到了重置您密码的申请,可能因为您忘记了密码。如果不是,请忽略这封邮件。", + "reset.text2": "如需继续重置密码,请点击下面的链接:", + "reset.cta": "点击这里重设您的密码", + "digest.notifications": "您有一些来自 %1 的未读通知:", + "digest.latest_topics": "来自 %1 的最新主题", + "digest.cta": "点击这里访问 %1", + "digest.unsub.info": "根据您的订阅设置,为您发送此摘要。", + "digest.unsub.cta": "点击这里修改这些设置", + "digest.daily.no_topics": "最近几天有一些未激活的主题", + "test.text1": "这是一封测试邮件,用来验证 NodeBB 的邮件配置是否设置正确。", + "closing": "谢谢!" } \ No newline at end of file diff --git a/public/language/zh_CN/groups.json b/public/language/zh_CN/groups.json index c00c111e11..42cd7e9ccd 100644 --- a/public/language/zh_CN/groups.json +++ b/public/language/zh_CN/groups.json @@ -1,7 +1,7 @@ { - "view_group": "View Group", - "details.title": "Group Details", - "details.members": "Member List", - "details.has_no_posts": "This group's members have not made any posts.", - "details.latest_posts": "Latest Posts" + "view_group": "查看用户组", + "details.title": "用户组详情", + "details.members": "会员列表", + "details.has_no_posts": "此用户组的会员尚未发表任何帖子。", + "details.latest_posts": "最新帖子" } \ No newline at end of file diff --git a/public/language/zh_CN/search.json b/public/language/zh_CN/search.json index d0ffc64f36..acbed61943 100644 --- a/public/language/zh_CN/search.json +++ b/public/language/zh_CN/search.json @@ -1,3 +1,3 @@ { - "results_matching": "%1 result(s) matching \"%2\", (%3 seconds)" + "results_matching": "%1 条结果,匹配 \"%2\",(耗时 %3 秒)" } \ No newline at end of file diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index fb02bd675c..06e5731983 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -96,12 +96,13 @@ var ajaxify = ajaxify || {}; translator.translate(template, function(translatedTemplate) { setTimeout(function() { $('#content').html(translatedTemplate); + + ajaxify.variables.parse(); + ajaxify.widgets.render(tpl_url, url, function() { $(window).trigger('action:ajaxify.end', {url: url}); }); - ajaxify.variables.parse(); - $(window).trigger('action:ajaxify.contentLoaded', {url: url}); ajaxify.loadScript(tpl_url); @@ -203,7 +204,6 @@ var ajaxify = ajaxify || {}; } var location = document.location || window.location, - api_url = (url === '' || url === '/') ? 'home' : url, tpl_url = ajaxify.getCustomTemplateMapping(url.split('?')[0]); if (!tpl_url) { @@ -211,7 +211,7 @@ var ajaxify = ajaxify || {}; } apiXHR = $.ajax({ - url: RELATIVE_PATH + '/api/' + api_url, + url: RELATIVE_PATH + '/api/' + url, cache: false, success: function(data) { if (!data) { @@ -329,9 +329,9 @@ var ajaxify = ajaxify || {}; templates.registerLoader(ajaxify.loadTemplate); - $.when($.getJSON(RELATIVE_PATH + '/templates/config.json'), $.getJSON(RELATIVE_PATH + '/api/get_templates_listing')).done(function (config_data, templates_data) { - templatesConfig = config_data[0]; - availableTemplates = templates_data[0]; + $.getJSON(RELATIVE_PATH + '/api/get_templates_listing', function (data) { + templatesConfig = data.templatesConfig; + availableTemplates = data.availableTemplates; app.load(); }); diff --git a/public/src/app.js b/public/src/app.js index eff176c266..64e5e3e15c 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -413,8 +413,8 @@ var socket, if (utils.findBootstrapEnvironment() === 'xs') { return; } - $('#header-menu li i[title]').each(function() { - $(this).parents('a').tooltip({ + $('#header-menu li [title]').each(function() { + $(this).tooltip({ placement: 'bottom', title: $(this).attr('title') }); @@ -441,7 +441,7 @@ var socket, searchButton.show(); } - searchButton.off().on('click', function(e) { + searchButton.on('click', function(e) { if (!config.loggedIn && !config.allowGuestSearching) { app.alert({ message:'[[error:search-requires-login]]', diff --git a/public/src/forum/admin/themes.js b/public/src/forum/admin/themes.js index c76fea0c9a..6a1b3db477 100644 --- a/public/src/forum/admin/themes.js +++ b/public/src/forum/admin/themes.js @@ -1,5 +1,5 @@ "use strict"; -/*global define, socket, app, bootbox, tabIndent, config, RELATIVE_PATH*/ +/*global define, socket, app, bootbox, tabIndent, config, RELATIVE_PATH, templates */ define('forum/admin/themes', ['forum/admin/settings'], function(Settings) { var Themes = {}; @@ -71,17 +71,17 @@ define('forum/admin/themes', ['forum/admin/settings'], function(Settings) { if (confirm) { socket.emit('admin.themes.set', { type: 'local', - id: 'nodebb-theme-cerulean' + id: 'nodebb-theme-vanilla' }, function(err) { if (err) { return app.alertError(err.message); } - highlightSelectedTheme('nodebb-theme-cerulean'); + highlightSelectedTheme('nodebb-theme-vanilla'); app.alert({ alert_id: 'admin:theme', type: 'success', title: 'Theme Changed', - message: 'You have successfully reverted your NodeBB back to it\'s default theme. Restarting your NodeBB ', + message: 'You have successfully reverted your NodeBB back to it\'s default theme.', timeout: 3500 }); }); @@ -95,34 +95,19 @@ define('forum/admin/themes', ['forum/admin/settings'], function(Settings) { return app.alertError(err.message); } - var instListEl = $('#installed_themes').empty(), liEl; + var instListEl = $('#installed_themes'); if (!themes.length) { instListEl.append($('
  • ').addClass('no-themes').html('No installed themes found')); return; + } else { + templates.parse('partials/admin/theme_list', { + themes: themes + }, function(html) { + instListEl.html(html); + highlightSelectedTheme(config['theme:id']); + }); } - - for (var x = 0, numThemes = themes.length; x < numThemes; x++) { - liEl = $('
  • ').attr({ - 'data-type': 'local', - 'data-theme': themes[x].id - }).html('' + - '
    ' + - '
    ' + - ' ' + - '
    ' + - '

    ' + themes[x].name + '

    ' + - '

    ' + - themes[x].description + - (themes[x].url ? ' (Homepage)' : '') + - '

    ' + - '
    ' + - '
    '); - - instListEl.append(liEl); - } - - highlightSelectedTheme(config['theme:id']); }); // Proper tabbing for "Custom CSS" field @@ -138,26 +123,23 @@ define('forum/admin/themes', ['forum/admin/settings'], function(Settings) { }; Themes.render = function(bootswatch) { - var themeContainer = $('#bootstrap_themes').empty(), - numThemes = bootswatch.themes.length, themeEl, theme; + var themeContainer = $('#bootstrap_themes'); - for (var x = 0; x < numThemes; x++) { - theme = bootswatch.themes[x]; - themeEl = $('
  • ').attr({ - 'data-type': 'bootswatch', - 'data-css': theme.cssCdn, - 'data-theme': theme.name - }).html('' + - '
    ' + - '
    ' + - ' ' + - '
    ' + - '

    ' + theme.name + '

    ' + - '

    ' + theme.description + '

    ' + - '
    ' + - '
    '); - themeContainer.append(themeEl); - } + templates.parse('partials/admin/theme_list', { + themes: bootswatch.themes.map(function(theme) { + return { + type: 'bootswatch', + id: theme.name, + name: theme.name, + description: theme.description, + screenshot_url: theme.thumbnail, + url: theme.preview, + css: theme.cssCdn + }; + }) + }, function(html) { + themeContainer.html(html); + }); }; Themes.prepareWidgets = function() { diff --git a/public/src/forum/login.js b/public/src/forum/login.js index 75754b04ca..4f4c9b6061 100644 --- a/public/src/forum/login.js +++ b/public/src/forum/login.js @@ -28,7 +28,7 @@ define('forum/login', function() { if (previousUrl) { app.previousUrl = previousUrl; } else if (!app.previousUrl) { - app.previousUrl = '/'; + app.previousUrl = RELATIVE_PATH || '/'; } if(app.previousUrl.indexOf('/reset/') !== -1) { diff --git a/public/src/forum/topic.js b/public/src/forum/topic.js index 7cf40d1fca..0300d58121 100644 --- a/public/src/forum/topic.js +++ b/public/src/forum/topic.js @@ -72,13 +72,11 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT $(window).trigger('action:topic.loaded'); - socket.emit('topics.markAsRead', tid); - socket.emit('topics.markTopicNotificationsRead', tid); - socket.emit('topics.increaseViewCount', tid); + socket.emit('topics.enter', tid); }; Topic.toTop = function() { - navigator.scrollTop(1); + navigator.scrollTop(0); }; Topic.toBottom = function() { @@ -353,7 +351,10 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT utils.makeNumbersHumanReadable(element.find('.human-readable-number')); element.find('span.timeago').timeago(); element.find('.post-content img:not(.emoji)').addClass('img-responsive').each(function() { - $(this).wrap(''); + var $this = $(this); + if (!$this.parent().is('a')) { + $this.wrap(''); + } }); postTools.updatePostCount(); showBottomPostBar(); @@ -364,7 +365,7 @@ define('forum/topic', dependencies, function(pagination, infinitescroll, threadT postHtml.find('.move').toggleClass('none', !privileges.move); postHtml.find('.reply, .quote').toggleClass('none', !$('.post_reply').length); var isSelfPost = parseInt(postHtml.attr('data-uid'), 10) === parseInt(app.uid, 10); - postHtml.find('.chat, .flag').toggleClass('none', isSelfPost); + postHtml.find('.chat, .flag').toggleClass('none', isSelfPost || !app.uid); } function loadMorePosts(direction) { diff --git a/public/src/forum/topic/postTools.js b/public/src/forum/topic/postTools.js index 24d7491fa5..5fe564c3db 100644 --- a/public/src/forum/topic/postTools.js +++ b/public/src/forum/topic/postTools.js @@ -36,13 +36,26 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com }; function addFavouriteHandler() { - $('#post-container').on('mouseenter', '.favourite-tooltip', function(e) { + $('#post-container').on('mouseenter', '.favourite-tooltip', function() { if (!$(this).data('users-loaded')) { - $(this).data('users-loaded', "true"); + $(this).data('users-loaded', 'true'); var pid = $(this).parents('.post-row').attr('data-pid'); var el = $(this).attr('title', "Loading..."); socket.emit('posts.getFavouritedUsers', pid, function(err, usernames) { - el.attr('title', usernames).tooltip('show'); + if (err) { + return; + } + if (usernames.length > 6) { + var otherCount = usernames.length - 5; + usernames = usernames.slice(0, 5).join(', ').replace(/,/g, '|'); + translator.translate('[[topic:users_and_others, ' + usernames + ', ' + otherCount + ']]', function(translated) { + translated = translated.replace(/\|/g, ','); + el.attr('title', translated).tooltip('show'); + }); + } else { + usernames = usernames.join(', '); + el.attr('title', usernames).tooltip('show'); + } }); } }); @@ -137,7 +150,7 @@ define('forum/topic/postTools', ['composer', 'share', 'navigator'], function(com if($('.composer').length) { composer.addQuote(tid, ajaxify.variables.get('topic_slug'), getData(button, 'data-index'), pid, topicName, username, quoted); } else { - composer.newReply(tid, pid, topicName, '[[modules:composer.user_said, ' + username + ']]' + quoted); + composer.newReply(tid, pid, topicName, '[[modules:composer.user_said, ' + username + ']]\n' + quoted); } }); } diff --git a/public/src/helpers.js b/public/src/helpers.js index 4dbd5419bc..a89aeae0e2 100644 --- a/public/src/helpers.js +++ b/public/src/helpers.js @@ -14,7 +14,7 @@ property = tag.property ? 'property="' + tag.property + '" ' : '', content = tag.content ? 'content="' + tag.content.replace(/\n/g, ' ') + '" ' : ''; - return ''; + return ''; }; if ('undefined' !== typeof window) { diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 78d5b6c546..cc4fbb1f42 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -249,7 +249,7 @@ define('chat', ['taskbar', 'string', 'sounds', 'forum/chats'], function(taskbar, module.center = function(chatModal) { chatModal.css("left", Math.max(0, (($(window).width() - $(chatModal).outerWidth()) / 2) + $(window).scrollLeft()) + "px"); - chatModal.css("top", $(window).height() / 4 - $(chatModal).outerHeight() / 2); + chatModal.css("top", Math.max(0, $(window).height() / 4 - $(chatModal).outerHeight() / 2)); chatModal.css("zIndex", 2000); chatModal.find('#chat-message-input').focus(); return chatModal; diff --git a/public/src/modules/composer.js b/public/src/modules/composer.js index 05b0f82c41..1ca81906de 100644 --- a/public/src/modules/composer.js +++ b/public/src/modules/composer.js @@ -119,7 +119,7 @@ define('composer', dependencies, function(taskbar, controls, uploads, formatting var uuid = composer.active; if (uuid === undefined) { - composer.newReply(tid, pid, title, '[[modules:composer.user_said, ' + username + ']]' + text); + composer.newReply(tid, pid, title, '[[modules:composer.user_said, ' + username + ']]\n' + text); return; } var postContainer = $('#cmp-uuid-' + uuid); @@ -127,9 +127,9 @@ define('composer', dependencies, function(taskbar, controls, uploads, formatting var prevText = bodyEl.val(); if (parseInt(tid, 10) !== parseInt(composer.posts[uuid].tid, 10)) { var link = '[' + title + '](/topic/' + topicSlug + '/' + (parseInt(postIndex, 10) + 1) + ')'; - translator.translate('[[modules:composer.user_said_in, ' + username + ', ' + link + ']]', onTranslated); + translator.translate('[[modules:composer.user_said_in, ' + username + ', ' + link + ']]\n', onTranslated); } else { - translator.translate('[[modules:composer.user_said, ' + username + ']]', onTranslated); + translator.translate('[[modules:composer.user_said, ' + username + ']]\n', onTranslated); } function onTranslated(translated) { @@ -360,33 +360,45 @@ define('composer', dependencies, function(taskbar, controls, uploads, formatting return composerAlert('[[error:content-too-short, ' + config.minimumPostLength + ']]'); } + var composerData = {}, action; + if (parseInt(postData.cid, 10) > 0) { - socket.emit('topics.post', { + composerData = { title: titleEl.val(), content: bodyEl.val(), topic_thumb: thumbEl.val() || '', category_id: postData.cid, tags: tags.getTags(post_uuid) - }, function(err, topic) { + }; + + action = 'topics.post'; + socket.emit(action, composerData, function(err, topic) { done(err); + if (!err) { ajaxify.go('topic/' + topic.slug); } }); } else if (parseInt(postData.tid, 10) > 0) { - socket.emit('posts.reply', { + composerData = { tid: postData.tid, content: bodyEl.val(), toPid: postData.toPid - }, done); + }; + + action = 'posts.reply'; + socket.emit(action, composerData, done); } else if (parseInt(postData.pid, 10) > 0) { - socket.emit('posts.edit', { + composerData = { pid: postData.pid, content: bodyEl.val(), title: titleEl.val(), topic_thumb: thumbEl.val() || '', tags: tags.getTags(post_uuid) - }, done); + }; + + action = 'posts.edit'; + socket.emit(action, composerData, done); } function done(err) { @@ -401,6 +413,8 @@ define('composer', dependencies, function(taskbar, controls, uploads, formatting discard(post_uuid); drafts.removeDraft(postData.save_id); + + $(window).trigger('action:composer.' + action, composerData); } } diff --git a/public/src/modules/navigator.js b/public/src/modules/navigator.js index bc1bf3e2b8..88e3934128 100644 --- a/public/src/modules/navigator.js +++ b/public/src/modules/navigator.js @@ -110,7 +110,7 @@ define('navigator', ['forum/pagination'], function(pagination) { navigator.scrollTop = function(index) { if ($('li[data-index="' + index + '"]').length) { - navigator.scrollUp(); + navigator.scrollToPost(index, true); } else { ajaxify.go(generateUrl()); } @@ -118,7 +118,7 @@ define('navigator', ['forum/pagination'], function(pagination) { navigator.scrollBottom = function(index) { if ($('li[data-index="' + index + '"]').length) { - navigator.scrollDown(); + navigator.scrollToPost(index, true); } else { index = parseInt(index, 10) + 1; ajaxify.go(generateUrl(index)); diff --git a/public/src/templates.js b/public/src/templates.js index 5cf10aeb4b..b629b03d0b 100644 --- a/public/src/templates.js +++ b/public/src/templates.js @@ -36,7 +36,7 @@ Please use the npm module instead - require('templates.js') } callback(parse(loaded, obj, bind)); - }); + }); } else { callback(parse(templates.cache[template], obj, bind)); } @@ -58,14 +58,15 @@ Please use the npm module instead - require('templates.js') }; templates.getBlock = function(template, block) { - return template.replace(new RegExp('[\\s\\S]*([\r\n]*[\\s\\S]*?[\r\n]*)[\\s\\S]*', 'g'), '$1'); + return template.replace(new RegExp('[\\s\\S]*([\\s\\S]*?)[\\s\\S]*', 'g'), '$1'); }; function express(filename, options, fn) { - console.log(filename, options, fn); var fs = require('fs'), tpl = filename.replace(options.settings.views + '/', ''); + options['_locals'] = null; + if (!templates.cache[tpl]) { fs.readFile(filename, function(err, html) { templates.cache[tpl] = (html || '').toString(); @@ -82,11 +83,11 @@ Please use the npm module instead - require('templates.js') } function makeRegex(block) { - return new RegExp('[\\s\\S]*?'); + return new RegExp('[\\t ]*[\\s\\S]*?'); } function makeBlockRegex(block) { - return new RegExp('([\\n]?[\\n]?)|([\\n]?[\\n]?)', 'g'); + return new RegExp('([\\t ]*[\\r\\n?|\\n]?)|()', 'g'); } function makeConditionalRegex(block) { @@ -94,7 +95,7 @@ Please use the npm module instead - require('templates.js') } function makeStatementRegex(key) { - return new RegExp('([\\s]*)|([\\s]*)', 'gi'); + return new RegExp('()|()', 'g'); } function registerGlobals(obj) { @@ -113,23 +114,23 @@ Please use the npm module instead - require('templates.js') if (matches !== null) { for (var i = 0, ii = matches.length; i < ii; i++) { var statement = makeStatementRegex(key), - nestedConditionals = matches[i].match(/[\s|\S][\s|\S]/), - match = matches[i].replace(statement, '').replace(/[\s|\S][\s|\S]/gi, ''), - conditionalBlock = match.split(/\s*\s*/); + nestedConditionals = matches[i].match(/(?!^)(?!$)/gi), + match = matches[i].replace(statement, '').replace(/(?!^)(?!$)/gi, ''), + conditionalBlock = match.split(/[\r\n?\n]*?[\r\n?\n]*?/); if (conditionalBlock[1]) { // there is an else statement - if (!value) { - template = template.replace(matches[i], conditionalBlock[1].replace(/(^[\r\n\t]*)|([\r\n\t]*$)/gi, '')); + if (!value) { // todo check second line break conditional, doesn't match. + template = template.replace(matches[i], conditionalBlock[1].replace(/(^[\r\n?|\n]*)|([\r\n\t]*$)/gi, '')); } else { - template = template.replace(matches[i], conditionalBlock[0].replace(/(^[\r\n\t]*)|([\r\n\t]*$)/gi, '')); + template = template.replace(matches[i], conditionalBlock[0].replace(/(^[\r\n?|\n]*)|([\r\n\t]*$)/gi, '')); } } else { // regular if statement if (!value) { template = template.replace(matches[i], ''); } else { - template = template.replace(matches[i], match.replace(/(^[\r\n\t]*)|([\r\n\t]*$)/gi, '')); + template = template.replace(matches[i], match.replace(/(^[\r\n?|\n]*)|([\r\n\t]*$)/gi, '')); } } @@ -181,40 +182,43 @@ Please use the npm module instead - require('templates.js') var regex = makeRegex(key), block; if (!array[key].length) { - return template; + return template.replace(regex, ''); } while (block = template.match(regex)) { block = block[0].replace(makeBlockRegex(key), ''); - + var numblocks = array[key].length - 1, iterator = 0, result = '', parsedBlock; do { - parsedBlock = parse(block, array[key][iterator], bind, namespace, {iterator: iterator, total: numblocks}) + ((iterator < numblocks) ? '\r\n':''); - + parsedBlock = parse(block, array[key][iterator], bind, namespace, {iterator: iterator, total: numblocks}); + result += (!bind) ? parsedBlock : setBindContainer(parsedBlock, bind + namespace + iterator); - result = parseFunctions(block, result, { - data: array[key][iterator], - iterator: iterator, - numblocks: numblocks - }); result = checkConditional(result, '@first', iterator === 0); result = checkConditional(result, '!@first', iterator !== 0); result = checkConditional(result, '@last', iterator === numblocks); result = checkConditional(result, '!@last', iterator !== numblocks); + result = result.replace(/^[\r\n?|\n|\t]*?|[\r\n?|\n|\t]*?$/g, ''); + + result = parseFunctions(block, result, { + data: array[key][iterator], + iterator: iterator, + numblocks: numblocks + }); + if (bind) { array[key][iterator].__template = block; } } while (iterator++ < numblocks); - template = template.replace(regex, result); + template = template.replace(regex, result.replace(/^[\r\n?|\n]|[\r\n?|\n]$/g, '')); } - + return template; } @@ -248,14 +252,14 @@ Please use the npm module instead - require('templates.js') this['__' + key] = value; var els = document.querySelectorAll('[data-binding="' + (this.__iterator !== false ? (bind + this.__namespace + this.__iterator) : bind) + '"]'); - + for (var el in els) { if (els.hasOwnProperty(el)) { if (this.__parent) { var parent = this.__parent(); els[el].innerHTML = parse(parent.template, parent.data, false); } else { - els[el].innerHTML = parse(this.__template, obj, false, this.__namespace); + els[el].innerHTML = parse(this.__template, obj, false, this.__namespace); } } } @@ -295,7 +299,7 @@ Please use the npm module instead - require('templates.js') template = parse(template, obj[key], bind, namespace + key + '.'); } else { template = parseValue(template, namespace + key, obj[key]); - + if (bind && obj[key]) { setupBindings({ obj: obj, diff --git a/src/categories.js b/src/categories.js index ceae7d8f01..bf42392596 100644 --- a/src/categories.js +++ b/src/categories.js @@ -93,7 +93,7 @@ var db = require('./database'), category.pageCount = results.pageCount; category.topic_row_size = 'col-md-9'; - callback(null, category); + plugins.fireHook('filter:category.get', category, uid, callback); }); }); }; @@ -237,10 +237,9 @@ var db = require('./database'), }; Categories.markAsUnreadForAll = function(cid, callback) { + callback = callback || function() {}; db.delete('cid:' + cid + ':read_by_uid', function(err) { - if(typeof callback === 'function') { - callback(err); - } + callback(err); }); }; @@ -295,6 +294,13 @@ var db = require('./database'), db.getObjectField('category:' + cid, field, callback); }; + Categories.getMultipleCategoryFields = function(cids, fields, callback) { + var keys = cids.map(function(cid) { + return 'category:' + cid; + }); + db.getObjectsFields(keys, fields, callback); + }; + Categories.getCategoryFields = function(cid, fields, callback) { db.getObjectFields('category:' + cid, fields, callback); }; diff --git a/src/controllers/accounts.js b/src/controllers/accounts.js index 2225373ee2..f2be462147 100644 --- a/src/controllers/accounts.js +++ b/src/controllers/accounts.js @@ -365,6 +365,8 @@ accountsController.accountSettings = function(req, res, next) { userData.settings = results.settings; userData.languages = results.languages; + userData.disableEmailSubscriptions = meta.config.disableEmailSubscriptions !== undefined && parseInt(meta.config.disableEmailSubscriptions, 10) === 1; + res.render('account/settings', userData); }); }); @@ -402,7 +404,11 @@ accountsController.uploadPicture = function (req, res, next) { image.resizeImage(req.files.userPhoto.path, extension, 128, 128, next); }, function(next) { - image.convertImageToPng(req.files.userPhoto.path, extension, next); + if (parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1) { + image.convertImageToPng(req.files.userPhoto.path, extension, next); + } else { + next(); + } }, function(next) { user.getUidByUserslug(req.params.userslug, next); @@ -448,7 +454,7 @@ accountsController.uploadPicture = function (req, res, next) { return plugins.fireHook('filter:uploadImage', req.files.userPhoto, done); } - var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10); + var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10) === 1; var filename = updateUid + '-profileimg' + (convertToPNG ? '.png' : extension); user.getUserField(updateUid, 'uploadedpicture', function (err, oldpicture) { diff --git a/src/controllers/categories.js b/src/controllers/categories.js index dffe49bcbc..d11567c4de 100644 --- a/src/controllers/categories.js +++ b/src/controllers/categories.js @@ -8,7 +8,8 @@ var categoriesController = {}, user = require('./../user'), categories = require('./../categories'), topics = require('./../topics'), - meta = require('./../meta'); + meta = require('./../meta'), + plugins = require('./../plugins'); categoriesController.recent = function(req, res, next) { var uid = req.user ? req.user.uid : 0; @@ -19,7 +20,9 @@ categoriesController.recent = function(req, res, next) { data['feeds:disableRSS'] = meta.config['feeds:disableRSS'] === '1' ? true : false; - res.render('recent', data); + plugins.fireHook('filter:category.get', data, uid, function(err, data) { + res.render('recent', data); + }); }); }; @@ -35,7 +38,9 @@ categoriesController.popular = function(req, res, next) { data['feeds:disableRSS'] = meta.config['feeds:disableRSS'] === '1' ? true : false; - res.render('popular', {topics: data}); + plugins.fireHook('filter:category.get', {topics: data}, uid, function(err, data) { + res.render('popular', data); + }); }); }; @@ -47,7 +52,9 @@ categoriesController.unread = function(req, res, next) { return next(err); } - res.render('unread', data); + plugins.fireHook('filter:category.get', data, uid, function(err, data) { + res.render('unread', data); + }); }); }; @@ -74,22 +81,16 @@ categoriesController.get = function(req, res, next) { }, function(disabled, next) { if (parseInt(disabled, 10) === 1) { - return next(new Error('category-disabled')); + return next(new Error('[[error:category-disabled]]')); } - privileges.categories.get(cid, uid, function(err, categoryPrivileges) { - if (err) { - return next(err); - } - - if (!categoryPrivileges.read) { - return next(new Error('[[error:no-privileges]]')); - } - - next(null, categoryPrivileges); - }); + privileges.categories.get(cid, uid, next); }, function (privileges, next) { + if (!privileges.read) { + return next(new Error('[[error:no-privileges]]')); + } + user.getSettings(uid, function(err, settings) { if (err) { return next(err); @@ -111,12 +112,6 @@ categoriesController.get = function(req, res, next) { return next(err); } - if (categoryData) { - if (parseInt(categoryData.disabled, 10) === 1) { - return next(new Error('[[error:category-disabled]]')); - } - } - categoryData.privileges = privileges; next(err, categoryData); }); @@ -189,7 +184,6 @@ categoriesController.get = function(req, res, next) { active: x === parseInt(page, 10) }); } - res.render('category', data); }); }; diff --git a/src/controllers/tags.js b/src/controllers/tags.js index 1b59483a4c..74c21a17fa 100644 --- a/src/controllers/tags.js +++ b/src/controllers/tags.js @@ -2,6 +2,7 @@ var tagsController = {}, async = require('async'), + nconf = require('nconf'), topics = require('./../topics'); tagsController.getTag = function(req, res, next) { @@ -13,7 +14,7 @@ tagsController.getTag = function(req, res, next) { return next(err); } - if (!tids || !tids.length) { + if (Array.isArray(tids) && !tids.length) { topics.deleteTag(tag); return res.render('tag', {topics: [], tag:tag}); } @@ -22,6 +23,22 @@ tagsController.getTag = function(req, res, next) { if (err) { return next(err); } + + res.locals.metaTags = [ + { + name: "title", + content: tag + }, + { + property: 'og:title', + content: tag + }, + { + property: "og:url", + content: nconf.get('url') + '/tags/' + tag + } + ]; + data.tag = tag; res.render('tag', data); }); diff --git a/src/database/level/sets.js b/src/database/level/sets.js index bc5aacf1b5..5d6ffabf6c 100644 --- a/src/database/level/sets.js +++ b/src/database/level/sets.js @@ -51,6 +51,10 @@ module.exports = function(db, module) { }); }; + module.getSetsMembers = function(keys, callback) { + throw new Error('not-implemented'); + }; + module.setCount = function(key, callback) { module.getListRange(key, 0, -1, function(err, set) { callback(err, set.length); diff --git a/src/database/mongo/sets.js b/src/database/mongo/sets.js index fb5f16343a..6b00ec5142 100644 --- a/src/database/mongo/sets.js +++ b/src/database/mongo/sets.js @@ -91,6 +91,24 @@ module.exports = function(db, module) { }); }; + module.getSetsMembers = function(keys, callback) { + db.collection('objects').find({_key: {$in: keys}}, {_key: 1, members: 1}).toArray(function(err, data) { + if (err) { + return callback(err); + } + var sets = {}; + data.forEach(function(set) { + sets[set._key] = set.members || []; + }); + + var returnData = new Array(keys.length); + for(var i=0; i=0; --i) { - if (!isMembers[i]) { - groupData.splice(i, 1); + async.map(uids, function(uid, next) { + db.isMemberOfSets(groupSets, uid, function(err, isMembers) { + if (err) { + return next(err); } - } - callback(null, groupData); - }); + var memberOf = []; + isMembers.forEach(function(isMember, index) { + if (isMember) { + memberOf.push(groupData[index]); + } + }); + + next(null, memberOf); + }); + }, callback); }); }); }; diff --git a/src/image.js b/src/image.js index 73bb85599d..0b2cea24cb 100644 --- a/src/image.js +++ b/src/image.js @@ -1,8 +1,7 @@ 'use strict'; var fs = require('fs'), - gm = require('gm').subClass({imageMagick: true}), - meta = require('./meta'); + gm = require('gm').subClass({imageMagick: true}); var image = {}; @@ -28,8 +27,7 @@ image.resizeImage = function(path, extension, width, height, callback) { }; image.convertImageToPng = function(path, extension, callback) { - var convertToPNG = parseInt(meta.config['profile:convertProfileImageToPNG'], 10); - if(convertToPNG && extension !== '.png') { + if(extension !== '.png') { gm(path).toBuffer('png', function(err, buffer) { if (err) { return callback(err); diff --git a/src/install.js b/src/install.js index 6793034515..0f30631354 100644 --- a/src/install.js +++ b/src/install.js @@ -266,12 +266,19 @@ function setOnEmpty(key1, key2) { function enableDefaultTheme(next) { var meta = require('./meta'); - winston.info('Enabling default theme: Lavender'); - meta.themes.set({ - type: 'local', - id: 'nodebb-theme-lavender' - }, next); + meta.configs.get('theme:id', function(err, id) { + if (err || id) { + winston.info('Previous theme detected, skipping enabling default theme'); + return next(err); + } + + winston.info('Enabling default theme: Lavender'); + meta.themes.set({ + type: 'local', + id: 'nodebb-theme-lavender' + }, next); + }); } function createAdministrator(next) { diff --git a/src/messaging.js b/src/messaging.js index 7d7018d388..d6cd44be5a 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -6,6 +6,7 @@ var db = require('./database'), user = require('./user'), plugins = require('./plugins'), meta = require('./meta'), + utils = require('../public/src/utils'), notifications = require('./notifications'), userNotifications = require('./user/notifications'); @@ -128,7 +129,7 @@ var db = require('./database'), var self = parseInt(message.fromuid, 10) === parseInt(fromuid, 10); message.fromUser = self ? userData[0] : userData[1]; message.toUser = self ? userData[1] : userData[0]; - message.timestampISO = new Date(parseInt(message.timestamp, 10)).toISOString(); + message.timestampISO = utils.toISOString(message.timestamp); message.self = self ? 1 : 0; message.newSet = false; @@ -193,7 +194,11 @@ var db = require('./database'), } }, function(mids, next) { - db.getObjects(['message:' + mids[0], 'message:' + mids[1]], next); + if (typeof mids !== 'boolean') { + db.getObjects(['message:' + mids[0], 'message:' + mids[1]], next); + } else { + next(null, mids); + } }, function(messages, next) { if (typeof messages !== 'boolean') { @@ -216,32 +221,27 @@ var db = require('./database'), return callback(err); } - async.parallel({ - unreadUids: async.apply(db.isSortedSetMembers, 'uid:' + uid + ':chats:unread', uids), - users: async.apply(user.getMultipleUserFields, uids, ['username', 'picture', 'uid']) - }, function(err, results) { + db.isSortedSetMembers('uid:' + uid + ':chats:unread', uids, function(err, unreadUids) { if (err) { return callback(err); } - var users = results.users; - for (var i=0; i parseInt(meta.config.maximumTitleLength, 10)) { + return callback(new Error('[[error:title-too-long, ' + meta.config.maximumTitleLength + ']]')); } else if (!data.content || data.content.length < parseInt(meta.config.minimumPostLength, 10)) { return callback(new Error('[[error:content-too-short, ' + meta.config.minimumPostLength + ']]')); } @@ -241,37 +243,18 @@ SocketPosts.getPrivileges = function(socket, pid, callback) { }; SocketPosts.getFavouritedUsers = function(socket, pid, callback) { - favourites.getFavouritedUidsByPids([pid], function(err, data) { - if(err) { return callback(err); } if(!Array.isArray(data) || !data.length) { - callback(null, ""); + callback(null, []); } - var max = 5; //hardcoded - var finalText = ""; - var pid_uids = data[0]; - var rest_amount = 0; - if (pid_uids.length > max) { - rest_amount = pid_uids.length - max; - pid_uids = pid_uids.slice(0, max); - } - - user.getUsernamesByUids(pid_uids, function(err, usernames) { - if(err) { - return callback(err); - } - - finalText = usernames.join(', ') + (rest_amount > 0 ? - (" and " + rest_amount + (rest_amount > 1 ? " others" : " other")) : ""); - callback(null, finalText); - }); + user.getUsernamesByUids(pid_uids, callback); }); }; diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index 8e71b9b98d..4c66db8cb1 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -54,6 +54,15 @@ SocketTopics.post = function(socket, data, callback) { }); }; +SocketTopics.enter = function(socket, tid, callback) { + if (!tid || !socket.uid) { + return; + } + SocketTopics.markAsRead(socket, tid); + topics.markTopicNotificationsRead(tid, socket.uid); + topics.increaseViewCount(tid); +}; + SocketTopics.postcount = function(socket, tid, callback) { topics.getTopicField(tid, 'postcount', callback); }; @@ -141,25 +150,58 @@ SocketTopics.markCategoryTopicsRead = function(socket, cid, callback) { }; SocketTopics.markAsUnreadForAll = function(socket, tids, callback) { - if(!Array.isArray(tids)) { + if (!Array.isArray(tids)) { return callback(new Error('[[error:invalid-tid]]')); } - async.each(tids, function(tid, next) { - topics.markAsUnreadForAll(tid, function(err) { - if(err) { - return next(err); - } + if (!socket.uid) { + return callback(new Error('[[error:no-privileges]]')); + } - db.sortedSetAdd('topics:recent', Date.now(), tid, function(err) { - if(err) { + user.isAdministrator(socket.uid, function(err, isAdmin) { + if (err) { + return callback(err); + } + + async.each(tids, function(tid, next) { + async.waterfall([ + function(next) { + threadTools.exists(tid, next); + }, + function(exists, next) { + if (!exists) { + return next(new Error('[[error:invalid-tid]]')); + } + topics.getTopicField(tid, 'cid', next); + }, + function(cid, next) { + user.isModerator(socket.uid, cid, next); + } + ], function(err, isMod) { + if (err) { return next(err); } - topics.pushUnreadCount(); - next(); + + if (!isAdmin && !isMod) { + return next(new Error('[[error:no-privileges]]')); + } + + topics.markAsUnreadForAll(tid, function(err) { + if(err) { + return next(err); + } + + db.sortedSetAdd('topics:recent', Date.now(), tid, function(err) { + if(err) { + return next(err); + } + topics.pushUnreadCount(); + next(); + }); + }); }); - }); - }, callback); + }, callback); + }); }; SocketTopics.delete = function(socket, data, callback) { @@ -333,20 +375,31 @@ SocketTopics.loadMore = function(socket, data, callback) { return callback(new Error('[[error:invalid-data]]')); } - user.getSettings(socket.uid, function(err, settings) { - if(err) { + async.parallel({ + settings: function(next) { + user.getSettings(socket.uid, next); + }, + privileges: function(next) { + privileges.topics.get(data.tid, socket.uid, next); + } + }, function(err, results) { + if (err) { return callback(err); } + if (!results.privileges.read) { + return callback(new Error('[[error:no-privileges]]')); + } + var start = Math.max(parseInt(data.after, 10) - 1, 0), - end = start + settings.postsPerPage - 1; + end = start + results.settings.postsPerPage - 1; var set = 'tid:' + data.tid + ':posts', reverse = false; - if (settings.topicPostSort === 'newest_to_oldest') { + if (results.settings.topicPostSort === 'newest_to_oldest') { reverse = true; - } else if (settings.topicPostSort === 'most_votes') { + } else if (results.settings.topicPostSort === 'most_votes') { reverse = true; set = 'tid:' + data.tid + ':posts:votes'; } @@ -356,7 +409,7 @@ SocketTopics.loadMore = function(socket, data, callback) { topics.getTopicPosts(data.tid, set, start, end, socket.uid, reverse, next); }, privileges: function(next) { - privileges.topics.get(data.tid, socket.uid, next); + next(null, results.privileges); }, 'reputation:disabled': function(next) { next(null, parseInt(meta.config['reputation:disabled'], 10) === 1); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 41bba06ba6..0f5e87c84e 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -5,6 +5,7 @@ var async = require('async'), groups = require('../groups'), topics = require('../topics'), messaging = require('../messaging'), + plugins = require('../plugins'), utils = require('./../../public/src/utils'), meta = require('../meta'), SocketUser = {}; @@ -79,7 +80,9 @@ SocketUser.reset.commit = function(socket, data, callback) { }; SocketUser.isOnline = function(socket, uid, callback) { - user.isOnline(uid, callback); + user.isOnline([uid], function(err, data) { + callback(err, Array.isArray(data) ? data[0] : null); + }); }; SocketUser.changePassword = function(socket, data, callback) { @@ -159,16 +162,30 @@ SocketUser.changePicture = function(socket, data, callback) { SocketUser.follow = function(socket, data, callback) { if (socket.uid && data) { - user.follow(socket.uid, data.uid, callback); + toggleFollow('follow', socket.uid, data.uid, callback); } }; SocketUser.unfollow = function(socket, data, callback) { if (socket.uid && data) { - user.unfollow(socket.uid, data.uid, callback); + toggleFollow('unfollow', socket.uid, data.uid, callback); } }; +function toggleFollow(method, uid, theiruid, callback) { + user[method](uid, theiruid, function(err) { + if (err) { + return callback(err); + } + + plugins.fireHook('action:user.' + method, { + fromUid: uid, + toUid: theiruid + }); + callback(); + }); +} + SocketUser.getSettings = function(socket, data, callback) { if (socket.uid) { if (socket.uid === parseInt(data.uid, 10)) { @@ -217,24 +234,23 @@ SocketUser.setTopicSort = function(socket, sort, callback) { } }; -SocketUser.getOnlineUsers = function(socket, data, callback) { +SocketUser.getOnlineUsers = function(socket, uids, callback) { var returnData = {}; - if(!data) { + if (!uids) { return callback(new Error('[[error:invalid-data]]')); } - function getUserStatus(uid, next) { - SocketUser.isOnline(socket, uid, function(err, data) { - if(err) { - return next(err); - } - returnData[uid] = data; - next(); - }); - } + user.isOnline(uids, function(err, userData) { + if (err) { + return callback(err); + } - async.each(data, getUserStatus, function(err) { - callback(err, returnData); + userData.forEach(function(user) { + if (user) { + returnData[user.uid] = user; + } + }); + callback(null, returnData); }); }; diff --git a/src/threadTools.js b/src/threadTools.js index 5e52d043c7..601ffc88ae 100644 --- a/src/threadTools.js +++ b/src/threadTools.js @@ -120,8 +120,7 @@ var winston = require('winston'), plugins.fireHook('action:topic.lock', { tid: tid, isLocked: lock, - uid: uid, - timestamp: Date.now() + uid: uid }); emitTo('topic_' + tid); @@ -165,8 +164,7 @@ var winston = require('winston'), plugins.fireHook('action:topic.pin', { tid: tid, isPinned: pin, - uid: uid, - timestamp: Date.now() + uid: uid }); emitTo('topic_' + tid); @@ -214,8 +212,7 @@ var winston = require('winston'), tid: tid, fromCid: oldCid, toCid: cid, - uid: uid, - timestamp: Date.now() + uid: uid }); }); }; diff --git a/src/topics.js b/src/topics.js index 0214f912bf..34fe471163 100644 --- a/src/topics.js +++ b/src/topics.js @@ -1,12 +1,11 @@ "use strict"; var async = require('async'), - gravatar = require('gravatar'), validator = require('validator'), db = require('./database'), posts = require('./posts'), - utils = require('./../public/src/utils'), + utils = require('../public/src/utils'), plugins = require('./plugins'), user = require('./user'), categories = require('./categories'), @@ -26,11 +25,7 @@ var async = require('async'), Topics.getTopicData = function(tid, callback) { Topics.getTopicsData([tid], function(err, topics) { - if (err) { - return callback(err); - } - - callback(null, topics ? topics[0] : null); + callback(err, Array.isArray(topics) && topics.length ? topics[0] : null); }); }; @@ -128,7 +123,7 @@ var async = require('async'), nextStart: 0 }; - if (!tids || !tids.length) { + if (!Array.isArray(tids) || !tids.length) { return callback(null, returnTopics); } @@ -170,96 +165,83 @@ var async = require('async'), }; Topics.getTopicsByTids = function(tids, uid, callback) { - if (!Array.isArray(tids) || tids.length === 0) { + if (!Array.isArray(tids) || !tids.length) { return callback(null, []); } - var categoryCache = {}, - privilegeCache = {}, - userCache = {}; - - - function loadTopicInfo(topicData, next) { - if (!topicData) { - return next(null, null); - } - - function isTopicVisible(topicData, topicInfo) { - if (parseInt(topicInfo.categoryData.disabled, 10) === 1) { - return false; - } - var deleted = parseInt(topicData.deleted, 10) !== 0; - return !deleted || (deleted && topicInfo.privileges.view_deleted) || parseInt(topicData.uid, 10) === parseInt(uid, 10); - } - - async.parallel({ - hasread: function(next) { - Topics.hasReadTopic(topicData.tid, uid, next); - }, - teaser: function(next) { - Topics.getTeaser(topicData.tid, next); - }, - privileges: function(next) { - if (privilegeCache[topicData.cid]) { - return next(null, privilegeCache[topicData.cid]); - } - privileges.categories.get(topicData.cid, uid, next); - }, - categoryData: function(next) { - if (categoryCache[topicData.cid]) { - return next(null, categoryCache[topicData.cid]); - } - categories.getCategoryFields(topicData.cid, ['name', 'slug', 'icon', 'bgColor', 'color', 'disabled'], next); - }, - user: function(next) { - if (userCache[topicData.uid]) { - return next(null, userCache[topicData.uid]); - } - user.getUserFields(topicData.uid, ['username', 'userslug', 'picture'], next); - }, - tags: function(next) { - Topics.getTopicTagsObjects(topicData.tid, next); - } - }, function(err, topicInfo) { - if(err) { - return next(err); - } - - privilegeCache[topicData.cid] = topicInfo.privileges; - categoryCache[topicData.cid] = topicInfo.categoryData; - userCache[topicData.uid] = topicInfo.user; - - if (!isTopicVisible(topicData, topicInfo)) { - return next(null, null); - } - - topicData.pinned = parseInt(topicData.pinned, 10) === 1; - topicData.locked = parseInt(topicData.locked, 10) === 1; - topicData.deleted = parseInt(topicData.deleted, 10) === 1; - topicData.unread = !(topicInfo.hasread && parseInt(uid, 10) !== 0); - topicData.unreplied = parseInt(topicData.postcount, 10) <= 1; - - topicData.category = topicInfo.categoryData; - topicData.teaser = topicInfo.teaser; - topicData.user = topicInfo.user; - topicData.tags = topicInfo.tags; - - next(null, topicData); - }); - } - Topics.getTopicsData(tids, function(err, topics) { + function mapFilter(array, field) { + return array.map(function(topic) { + return topic[field]; + }).filter(function(value, index, array) { + return array.indexOf(value) === index; + }); + } + if (err) { return callback(err); } - async.mapSeries(topics, loadTopicInfo, function(err, topics) { - if(err) { + var uids = mapFilter(topics, 'uid'); + var cids = mapFilter(topics, 'cid'); + + async.parallel({ + users: function(next) { + user.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture'], next); + }, + categories: function(next) { + categories.getMultipleCategoryFields(cids, ['cid', 'name', 'slug', 'icon', 'bgColor', 'color', 'disabled'], next); + }, + hasRead: function(next) { + Topics.hasReadTopics(tids, uid, next); + }, + isAdminOrMod: function(next) { + privileges.categories.isAdminOrMod(cids, uid, next); + }, + teasers: function(next) { + Topics.getTeasers(tids, next); + }, + tags: function(next) { + Topics.getTopicsTagsObjects(tids, next); + } + }, function(err, results) { + function arrayToObject(array, field) { + var obj = {}; + for (var i=0; i'"~()?\|]/g, ''); tag = tag.substr(0, meta.config.maximumTagLength || 15); var matches = tag.match(/^[.-]*(.+?)[.-]*$/); @@ -93,6 +92,25 @@ module.exports = function(Topics) { }); }; + Topics.getTopicsTagsObjects = function(tids, callback) { + var sets = tids.map(function(tid) { + return 'topic:' + tid + ':tags'; + }); + + db.getSetsMembers(sets, function(err, members) { + if (err) { + return callback(err); + } + + members.forEach(function(tags, index) { + if (Array.isArray(tags)) { + members[index] = mapToObject(tags); + } + }) + callback(null, members); + }); + }; + function mapToObject(tags) { if (!tags) { return tags; diff --git a/src/topics/unread.js b/src/topics/unread.js index 8bdc8a04e2..871d2043e7 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -23,26 +23,26 @@ module.exports = function(Topics) { done = false; uid = parseInt(uid, 10); - if(uid === 0) { + if (uid === 0) { return callback(null, unreadTids); } async.whilst(function() { return unreadTids.length < 21 && !done; - }, function(callback) { + }, function(next) { Topics.getLatestTids(start, stop, 'month', function(err, tids) { if (err) { - return callback(err); + return next(err); } if (tids && !tids.length) { done = true; - return callback(); + return next(); } Topics.hasReadTopics(tids, uid, function(err, read) { - if(err) { - return callback(err); + if (err) { + return next(err); } var newtids = tids.filter(function(tid, index) { @@ -50,16 +50,15 @@ module.exports = function(Topics) { }); privileges.topics.filter('read', newtids, uid, function(err, newtids) { - if(err) { - return callback(err); + if (err) { + return next(err); } - unreadTids.push.apply(unreadTids, newtids); start = stop + 1; stop = start + 19; - callback(); + next(); }); }); }); diff --git a/src/user.js b/src/user.js index f20686fc7c..c0e96960c6 100644 --- a/src/user.js +++ b/src/user.js @@ -68,8 +68,9 @@ var bcrypt = require('bcryptjs'), if (err) { return callback(err); } - - callback(null, modifyUserData(users, fieldsToRemove)); + plugins.fireHook('filter:user.removeFields', fieldsToRemove, function(err, fields) { + callback(err, modifyUserData(users, fields)); + }); }); }; @@ -94,7 +95,9 @@ var bcrypt = require('bcryptjs'), return callback(err); } - callback(null, modifyUserData(users, [])); + plugins.fireHook('filter:user.removeFields', [], function(err, fields) { + callback(err, modifyUserData(users, fields)); + }); }); }; @@ -239,36 +242,32 @@ var bcrypt = require('bcryptjs'), }; User.getUsers = function(uids, callback) { - function loadUserInfo(user, callback) { - if (!user) { - return callback(null, user); + async.parallel({ + userData: function(next) { + User.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'banned', 'postcount', 'reputation'], next); + }, + isAdmin: function(next) { + User.isAdministrator(uids, next); + }, + isOnline: function(next) { + db.isSortedSetMembers('users:online', uids, next); } - - async.waterfall([ - function(next) { - User.isAdministrator(user.uid, next); - }, - function(isAdmin, next) { - user.status = !user.status ? 'online' : user.status; - user.administrator = isAdmin; - user.banned = parseInt(user.banned, 10) === 1; - db.isSortedSetMember('users:online', user.uid, next); - }, - function(isMember, next) { - if (!isMember) { - user.status = 'offline'; - } - next(null, user); - } - ], callback); - } - - User.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'banned', 'postcount', 'reputation'], function(err, usersData) { + }, function(err, results) { if (err) { return callback(err); } - async.map(usersData, loadUserInfo, callback); + results.userData.forEach(function(user, index) { + if (!user) { + return; + } + user.status = !user.status ? 'online' : user.status; + user.status = !results.isOnline[index] ? 'offline' : user.status; + user.administrator = results.isAdmin[index]; + user.banned = parseInt(user.banned, 10) === 1; + }); + + callback(err, results.userData); }); }; @@ -410,29 +409,40 @@ var bcrypt = require('bcryptjs'), }; User.isAdministrator = function(uid, callback) { - groups.isMember(uid, 'administrators', callback); + if (Array.isArray(uid)) { + groups.isMembers(uid, 'administrators', callback); + } else { + groups.isMember(uid, 'administrators', callback); + } }; - User.isOnline = function(uid, callback) { - User.getUserFields(uid, ['username', 'userslug', 'picture', 'status', 'reputation', 'postcount'] , function(err, data) { - if(err) { + User.isOnline = function(uids, callback) { + if (!Array.isArray(uids)) { + uids = [uids]; + } + + User.getMultipleUserFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'reputation', 'postcount'] , function(err, userData) { + if (err) { return callback(err); } + var websockets = require('./socket.io'); - var online = websockets.isUserOnline(uid); - data.status = online ? (data.status || 'online') : 'offline'; + userData = userData.map(function(user) { + var online = websockets.isUserOnline(user.uid); + user.status = online ? (user.status || 'online') : 'offline'; - if(data.status === 'offline') { - online = false; - } + if (user.status === 'offline') { + online = false; + } - data.online = online; - data.uid = uid; - data.timestamp = Date.now(); - data.rooms = websockets.getUserRooms(uid); + user.online = online; + user.timestamp = Date.now(); + user.rooms = websockets.getUserRooms(user.uid); + return user; + }); - callback(null, data); + callback(null, userData); }); }; diff --git a/src/user/create.js b/src/user/create.js index b3b556edb7..80a5877d7f 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -12,19 +12,38 @@ var async = require('async'), module.exports = function(User) { - User.create = function(userData, callback) { - userData = userData || {}; + User.create = function(data, callback) { + var gravatar = User.createGravatarURLFromEmail(data.email); + var timestamp = Date.now(); + var password = data.password; + + var userData = { + 'username': data.username.trim(), + 'email': data.email, + 'joindate': timestamp, + 'picture': gravatar, + 'gravatarpicture': gravatar, + 'fullname': '', + 'location': '', + 'birthday': '', + 'website': '', + 'signature': '', + 'uploadedpicture': '', + 'profileviews': 0, + 'reputation': 0, + 'postcount': 0, + 'lastposttime': 0, + 'banned': 0, + 'status': 'online' + }; + userData.userslug = utils.slugify(userData.username); - userData.username = userData.username.trim(); if (userData.email !== undefined) { userData.email = userData.email.trim(); userData.email = validator.escape(userData.email); } - var password = userData.password; - userData.password = null; - async.parallel({ emailValid: function(next) { if (userData.email) { @@ -84,7 +103,7 @@ module.exports = function(User) { } }, customFields: function(next) { - plugins.fireHook('filter:user.custom_fields', userData, next); + plugins.fireHook('filter:user.custom_fields', [], next); }, userData: function(next) { plugins.fireHook('filter:user.create', userData, next); @@ -94,7 +113,14 @@ module.exports = function(User) { return callback(err); } - userData = utils.merge(results.userData, results.customFields); + var customData = {}; + results.customFields.forEach(function(customField) { + if (data[customField]) { + customData[customField] = data[customField]; + } + }); + + userData = utils.merge(results.userData, customData); var userNameChanged = !!results.renamedUsername; @@ -104,37 +130,14 @@ module.exports = function(User) { } db.incrObjectField('global', 'nextUid', function(err, uid) { - if(err) { + if (err) { return callback(err); } - var gravatar = User.createGravatarURLFromEmail(userData.email); - var timestamp = Date.now(); - - userData = utils.merge({ - 'uid': uid, - 'username': userData.username, - 'userslug': userData.userslug, - 'fullname': '', - 'location': '', - 'birthday': '', - 'website': '', - 'email': userData.email || '', - 'signature': '', - 'joindate': timestamp, - 'picture': gravatar, - 'gravatarpicture': gravatar, - 'uploadedpicture': '', - 'profileviews': 0, - 'reputation': 0, - 'postcount': 0, - 'lastposttime': 0, - 'banned': 0, - 'status': 'online' - }, userData); + userData.uid = uid; db.setObject('user:' + uid, userData, function(err) { - if(err) { + if (err) { return callback(err); } diff --git a/src/user/delete.js b/src/user/delete.js index 8ab14c4cea..7a91603438 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -18,14 +18,11 @@ module.exports = function(User) { }, function(next) { deleteTopics(uid, next); + }, + function(next) { + deleteAccount(uid, next); } - ], function(err) { - if (err) { - return callback(err); - } - - deleteAccount(uid, callback); - }); + ], callback); }; function deletePosts(uid, callback) { diff --git a/src/user/jobs.js b/src/user/jobs.js index 7659a87aea..160f3bb33c 100644 --- a/src/user/jobs.js +++ b/src/user/jobs.js @@ -22,6 +22,11 @@ module.exports = function(User) { }; User.sendDailyDigests = function() { + var digestsDisabled = meta.config.disableEmailSubscriptions !== undefined && parseInt(meta.config.disableEmailSubscriptions, 10) === 1; + if (digestsDisabled) { + return winston.log('[user/jobs] Did not send daily digests because subscription system is disabled.'); + } + async.parallel({ recent: function(next) { topics.getLatestTopics(0, 0, 10, 'day', next); diff --git a/src/user/notifications.js b/src/user/notifications.js index 591f799ca0..f6e85aa05f 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -55,6 +55,7 @@ var async = require('async'), } db.sortedSetRemove(set, nidsToUniqueIds[nid]); + db.deleteObjectField('uid:' + uid + ':notifications:uniqueId:nid', nidsToUniqueIds[nid]); return next(); } @@ -141,7 +142,7 @@ var async = require('async'), } notifs = notifs.filter(function(notif) { - return notif !== null; + return !!notif; }).sort(function(a, b) { return parseInt(b.datetime, 10) - parseInt(a.datetime, 10); }).map(function(notif) {