From 74b4d36a9cc7cbdb36d5ce759cb4e677360fb938 Mon Sep 17 00:00:00 2001 From: Panayotis Matsinopoulos Date: Wed, 18 Oct 2017 13:23:35 +0100 Subject: [PATCH 001/445] More accurate steps for preparing the database The existing prepare database instructions were not correct. I have updated them according to what existed in the other document (docs/DEVELOPMENT-ADVANCED.md). --- docs/DEVELOPMENT-OSX-NATIVE.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/DEVELOPMENT-OSX-NATIVE.md b/docs/DEVELOPMENT-OSX-NATIVE.md index abb88cb2aa..64902ff82d 100644 --- a/docs/DEVELOPMENT-OSX-NATIVE.md +++ b/docs/DEVELOPMENT-OSX-NATIVE.md @@ -257,10 +257,13 @@ bundle install ### Prepare your database ```sh -rake db:create -rake db:migrate -rake db:test:prepare -rake db:seed_fu +# run this if there was a pre-existing database +bundle exec rake db:drop +RAILS_ENV=test bundle exec rake db:drop + +# time to create the database and run migrations +bundle exec rake db:create db:migrate +RAILS_ENV=test bundle exec rake db:create db:migrate ``` ## Now, test it out! From e02ad4249e845437097d2bf7e8841965815e6914 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 30 Oct 2017 11:59:13 -0400 Subject: [PATCH 003/445] FIX: In case that `category` is `nil` it should not throw an error --- app/models/topic.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/topic.rb b/app/models/topic.rb index fe15c98f00..b2557df7b3 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -645,7 +645,7 @@ SQL Category.where(id: new_category.id).update_all("topic_count = topic_count + 1") CategoryFeaturedTopic.feature_topics_for(old_category) unless @import_mode - CategoryFeaturedTopic.feature_topics_for(new_category) unless @import_mode || old_category.id == new_category.id + CategoryFeaturedTopic.feature_topics_for(new_category) unless @import_mode || old_category.try(:id) == new_category.id end true From a0dd75ba887e9bb89af19bb87a08a8e67bf2436b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 30 Oct 2017 12:40:58 -0400 Subject: [PATCH 004/445] FEATURE: New API to create a custom formatter for displaying usernames This is not exhaustive right now, but a good start and we can add to it over time. --- .../discourse/helpers/format-username.js.es6 | 4 ++++ .../discourse/lib/link-mentions.js.es6 | 3 ++- .../discourse/lib/plugin-api.js.es6 | 23 +++++++++++++++++-- .../javascripts/discourse/lib/text.js.es6 | 4 +++- .../discourse/lib/utilities.js.es6 | 11 +++++++++ .../components/user-card-contents.hbs | 2 +- .../javascripts/discourse/templates/user.hbs | 2 +- .../widgets/notification-item.js.es6 | 6 ++--- .../discourse/widgets/poster-name.js.es6 | 10 ++++---- .../discourse/widgets/user-menu.js.es6 | 3 ++- .../pretty-text/pretty-text.js.es6 | 5 ++-- 11 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 app/assets/javascripts/discourse/helpers/format-username.js.es6 diff --git a/app/assets/javascripts/discourse/helpers/format-username.js.es6 b/app/assets/javascripts/discourse/helpers/format-username.js.es6 new file mode 100644 index 0000000000..dcb8be1840 --- /dev/null +++ b/app/assets/javascripts/discourse/helpers/format-username.js.es6 @@ -0,0 +1,4 @@ +import { registerUnbound } from 'discourse-common/lib/helpers'; +import { formatUsername } from 'discourse/lib/utilities'; + +export default registerUnbound('format-username', formatUsername); diff --git a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 index d8cd970299..688207c5b9 100644 --- a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 +++ b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 @@ -1,5 +1,6 @@ import { ajax } from 'discourse/lib/ajax'; import { userPath } from 'discourse/lib/url'; +import { formatUsername } from 'discourse/lib/utilities'; function replaceSpan($e, username, opts) { let extra = ""; @@ -16,7 +17,7 @@ function replaceSpan($e, username, opts) { extra = `data-name='${username}'`; extraClass = "cannot-see"; } - $e.replaceWith(`@${username}`); + $e.replaceWith(`@${formatUsername(username)}`); } } diff --git a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 index 38772203ae..03467421cb 100644 --- a/app/assets/javascripts/discourse/lib/plugin-api.js.es6 +++ b/app/assets/javascripts/discourse/lib/plugin-api.js.es6 @@ -20,10 +20,10 @@ import { addPostTransformCallback } from 'discourse/widgets/post-stream'; import { attachAdditionalPanel } from 'discourse/widgets/header'; import { registerIconRenderer, replaceIcon } from 'discourse-common/lib/icon-library'; import { addNavItem } from 'discourse/models/nav-item'; - +import { replaceFormatter } from 'discourse/lib/utilities'; // If you add any methods to the API ensure you bump up this number -const PLUGIN_API_VERSION = '0.8.11'; +const PLUGIN_API_VERSION = '0.8.12'; class PluginApi { constructor(version, container) { @@ -570,6 +570,25 @@ class PluginApi { addNavItem(item); } } + + + /** + * + * Registers a function that will format a username when displayed. This will not + * be applied when the username is used as an `id` or in URL strings. + * + * Example: + * + * ``` + * // display usernames in UPPER CASE + * api.formatUsername(username => username.toUpperCase()); + * + * ``` + * + **/ + formatUsername(fn) { + replaceFormatter(fn); + } } let _pluginv01; diff --git a/app/assets/javascripts/discourse/lib/text.js.es6 b/app/assets/javascripts/discourse/lib/text.js.es6 index 874f519010..09a7592828 100644 --- a/app/assets/javascripts/discourse/lib/text.js.es6 +++ b/app/assets/javascripts/discourse/lib/text.js.es6 @@ -3,6 +3,7 @@ import { performEmojiUnescape, buildEmojiUrl } from 'pretty-text/emoji'; import WhiteLister from 'pretty-text/white-lister'; import { sanitize as textSanitize } from 'pretty-text/sanitizer'; import loadScript from 'discourse/lib/load-script'; +import { formatUsername } from 'discourse/lib/utilities'; function getOpts(opts) { const siteSettings = Discourse.__container__.lookup('site-settings:main'), @@ -12,7 +13,8 @@ function getOpts(opts) { getURL: Discourse.getURLWithCDN, currentUser: Discourse.__container__.lookup('current-user:main'), censoredWords: site.censored_words, - siteSettings + siteSettings, + formatUsername }, opts); return buildOptions(opts); diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index a1e55234e2..fe3616abb5 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -21,6 +21,17 @@ export function escapeExpression(string) { return escape(string); } +let _usernameFormatDelegate = username => username; + +export function formatUsername(username) { + return _usernameFormatDelegate(username || ''); +} + +export function replaceFormatter(fn) { + _usernameFormatDelegate = fn; +} + + export function avatarUrl(template, size) { if (!template) { return ""; } const rawSize = getRawSize(translateSize(size)); diff --git a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs index 07c4effa3e..55190fb27f 100644 --- a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs @@ -16,7 +16,7 @@

- {{if nameFirst user.name username}} {{user-status user currentUser=currentUser}} + {{if nameFirst user.name (format-username username)}} {{user-status user currentUser=currentUser}}

{{#unless nameFirst}} diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs index 17677dd3d1..9307c32102 100644 --- a/app/assets/javascripts/discourse/templates/user.hbs +++ b/app/assets/javascripts/discourse/templates/user.hbs @@ -61,7 +61,7 @@
-

{{if nameFirst model.name model.username}} {{user-status model currentUser=currentUser}}

+

{{if nameFirst model.name (format-username model.username)}} {{user-status model currentUser=currentUser}}

{{#if nameFirst}}{{model.username}}{{else}}{{model.name}}{{/if}}

{{#if model.title}}

{{model.title}}

diff --git a/app/assets/javascripts/discourse/widgets/notification-item.js.es6 b/app/assets/javascripts/discourse/widgets/notification-item.js.es6 index f2878b5f12..83f0c368fa 100644 --- a/app/assets/javascripts/discourse/widgets/notification-item.js.es6 +++ b/app/assets/javascripts/discourse/widgets/notification-item.js.es6 @@ -4,7 +4,7 @@ import { createWidget } from 'discourse/widgets/widget'; import DiscourseURL from 'discourse/lib/url'; import { h } from 'virtual-dom'; import { emojiUnescape } from 'discourse/lib/text'; -import { postUrl, escapeExpression } from 'discourse/lib/utilities'; +import { postUrl, escapeExpression, formatUsername } from 'discourse/lib/utilities'; import { setTransientHeader } from 'discourse/lib/ajax'; import { userPath } from 'discourse/lib/url'; import { iconNode } from 'discourse-common/lib/icon-library'; @@ -79,11 +79,11 @@ createWidget('notification-item', { return I18n.t(scope, { count, group_name }); } - const username = data.display_username; + const username = formatUsername(data.display_username); const description = this.description(); if (notificationType === LIKED_TYPE && data.count > 1) { const count = data.count - 2; - const username2 = data.username2; + const username2 = formatUsername(data.username2); if (count===0) { return I18n.t('notifications.liked_2', {description, username, username2}); } else { diff --git a/app/assets/javascripts/discourse/widgets/poster-name.js.es6 b/app/assets/javascripts/discourse/widgets/poster-name.js.es6 index 2bf64d738d..363f769101 100644 --- a/app/assets/javascripts/discourse/widgets/poster-name.js.es6 +++ b/app/assets/javascripts/discourse/widgets/poster-name.js.es6 @@ -1,6 +1,7 @@ import { iconNode } from 'discourse-common/lib/icon-library'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; +import { formatUsername } from 'discourse/lib/utilities'; function sanitizeName(name){ return name.toLowerCase().replace(/[\s_-]/g,''); @@ -17,10 +18,11 @@ export default createWidget('poster-name', { }, userLink(attrs, text) { - return h('a', { attributes: { - href: attrs.usernameUrl, - 'data-user-card': attrs.username - } }, text); + return h( + 'a', + { attributes: { href: attrs.usernameUrl, 'data-user-card': attrs.username } }, + formatUsername(text) + ); }, html(attrs) { diff --git a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 index 6ba446b8be..9142695573 100644 --- a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 @@ -1,5 +1,6 @@ import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; +import { formatUsername } from 'discourse/lib/utilities'; let extraGlyphs; @@ -50,7 +51,7 @@ createWidget('user-menu-links', { model: currentUser, className: 'user-activity-link', icon: 'user', - rawLabel: currentUser.username + rawLabel: formatUsername(currentUser.username) }; if (currentUser.is_anonymous) { diff --git a/app/assets/javascripts/pretty-text/pretty-text.js.es6 b/app/assets/javascripts/pretty-text/pretty-text.js.es6 index 9c7a814940..3d3f85fc62 100644 --- a/app/assets/javascripts/pretty-text/pretty-text.js.es6 +++ b/app/assets/javascripts/pretty-text/pretty-text.js.es6 @@ -24,7 +24,8 @@ export function buildOptions(state) { lookupImageUrls, previewing, linkify, - censoredWords + censoredWords, + mentionLookup } = state; let features = { @@ -56,7 +57,7 @@ export function buildOptions(state) { getCurrentUser, currentUser, lookupAvatarByPostNumber, - mentionLookup: state.mentionLookup, + mentionLookup, emojiUnicodeReplacer, lookupInlineOnebox, lookupImageUrls, From fec569106418ede7fda4c7373db3adf49f2d7edc Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 30 Oct 2017 14:28:34 -0400 Subject: [PATCH 005/445] FIX: unsubscribe links in summary emails were missing subfolder --- app/views/user_notifications/digest.html.erb | 2 +- app/views/user_notifications/digest.text.erb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/user_notifications/digest.html.erb b/app/views/user_notifications/digest.html.erb index df8efadc04..15c82ada64 100644 --- a/app/views/user_notifications/digest.html.erb +++ b/app/views/user_notifications/digest.html.erb @@ -420,7 +420,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo <%=raw(t 'user_notifications.digest.unsubscribe', site_link: html_site_link(@anchor_color), email_settings_url: Discourse.base_url + '/my/preferences/emails', - unsubscribe_link: link_to(t('user_notifications.digest.click_here'), email_unsubscribe_url(host: Discourse.base_url_no_prefix, key: @unsubscribe_key), {:style=>"color: ##{@anchor_color}"})) %> + unsubscribe_link: link_to(t('user_notifications.digest.click_here'), email_unsubscribe_url(host: Discourse.base_url, key: @unsubscribe_key), {:style=>"color: ##{@anchor_color}"})) %>
<%= digest_custom_html("below_footer") %> diff --git a/app/views/user_notifications/digest.text.erb b/app/views/user_notifications/digest.text.erb index ec9278df89..5230597c6b 100644 --- a/app/views/user_notifications/digest.text.erb +++ b/app/views/user_notifications/digest.text.erb @@ -52,6 +52,7 @@ <%= digest_custom_text("above_footer") %> <%=raw(t :'user_notifications.digest.unsubscribe', site_link: site_link, + email_settings_url: Discourse.base_url + '/my/preferences/emails', unsubscribe_link: raw(@markdown_linker.create(t('user_notifications.digest.click_here'), email_unsubscribe_url(key: @unsubscribe_key, only_path: true)))) %> <%= raw(@markdown_linker.references) %> From 28bc5ac10ae74e84c1ae082fb17bb63ca11b7ad1 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 30 Oct 2017 14:34:12 -0400 Subject: [PATCH 006/445] FIX: link to about page on subfolder --- app/views/static/show.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/static/show.html.erb b/app/views/static/show.html.erb index ba7592c959..8ee6ca8b41 100644 --- a/app/views/static/show.html.erb +++ b/app/views/static/show.html.erb @@ -2,7 +2,7 @@
-
+
{{outlet}}
diff --git a/app/assets/javascripts/admin/templates/api.hbs b/app/assets/javascripts/admin/templates/api.hbs index f3407fe0cc..45bfbd9ca7 100644 --- a/app/assets/javascripts/admin/templates/api.hbs +++ b/app/assets/javascripts/admin/templates/api.hbs @@ -1,10 +1,8 @@ -
- {{#admin-nav}} - {{nav-item route='adminApiKeys' label='admin.api.title'}} - {{nav-item route='adminWebHooks' label='admin.web_hooks.title'}} - {{/admin-nav}} +{{#admin-nav}} + {{nav-item route='adminApiKeys' label='admin.api.title'}} + {{nav-item route='adminWebHooks' label='admin.web_hooks.title'}} +{{/admin-nav}} -
- {{outlet}} -
+
+ {{outlet}}
diff --git a/app/assets/javascripts/admin/templates/customize.hbs b/app/assets/javascripts/admin/templates/customize.hbs index 3065c09855..d5d3816310 100644 --- a/app/assets/javascripts/admin/templates/customize.hbs +++ b/app/assets/javascripts/admin/templates/customize.hbs @@ -1,16 +1,14 @@ -
- {{#admin-nav}} - {{nav-item route='adminCustomizeThemes' label='admin.customize.theme.title'}} - {{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}} - {{nav-item route='adminSiteText' label='admin.site_text.title'}} - {{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}} - {{nav-item route='adminUserFields' label='admin.user_fields.title'}} - {{nav-item route='adminEmojis' label='admin.emoji.title'}} - {{nav-item route='adminPermalinks' label='admin.permalink.title'}} - {{nav-item route='adminEmbedding' label='admin.embedding.title'}} - {{/admin-nav}} +{{#admin-nav}} + {{nav-item route='adminCustomizeThemes' label='admin.customize.theme.title'}} + {{nav-item route='adminCustomize.colors' label='admin.customize.colors.title'}} + {{nav-item route='adminSiteText' label='admin.site_text.title'}} + {{nav-item route='adminCustomizeEmailTemplates' label='admin.customize.email_templates.title'}} + {{nav-item route='adminUserFields' label='admin.user_fields.title'}} + {{nav-item route='adminEmojis' label='admin.emoji.title'}} + {{nav-item route='adminPermalinks' label='admin.permalink.title'}} + {{nav-item route='adminEmbedding' label='admin.embedding.title'}} +{{/admin-nav}} -
- {{outlet}} -
+
+ {{outlet}}
diff --git a/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 b/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 index c248ea5b95..58bddf795a 100644 --- a/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 +++ b/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 @@ -1,7 +1,7 @@ import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Component.extend({ - classNameBindings: ['containerClass', 'condition:visible'], + classNameBindings: [':loading-container', 'containerClass', 'condition:visible'], @computed('size') containerClass(size) { diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index bbcb624aa2..370ac30b7e 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -1,5 +1,5 @@ // Customise area -.customize { +.admin-customize { h1 { margin-bottom: 10px; input { diff --git a/app/assets/stylesheets/common/base/rtl.scss b/app/assets/stylesheets/common/base/rtl.scss index 67a9a7ade6..ad7e5c4af7 100644 --- a/app/assets/stylesheets/common/base/rtl.scss +++ b/app/assets/stylesheets/common/base/rtl.scss @@ -42,11 +42,11 @@ left: 27px; } -.rtl .customize .current-style .toggle-mobile { +.rtl .admin-customize .current-style .toggle-mobile { position: static !important; float: left !important; } -.rtl .customize .current-style .toggle-maximize { +.rtl .admin-customize .current-style .toggle-maximize { position: static !important; float: left !important; } From a5afc08363693716fad56e4ea1448063165c6103 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 30 Oct 2017 15:36:23 -0400 Subject: [PATCH 008/445] FIX: html links in text part of summary email --- app/views/user_notifications/digest.html.erb | 2 +- app/views/user_notifications/digest.text.erb | 2 +- config/locales/server.en.yml | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/views/user_notifications/digest.html.erb b/app/views/user_notifications/digest.html.erb index 15c82ada64..cdea397301 100644 --- a/app/views/user_notifications/digest.html.erb +++ b/app/views/user_notifications/digest.html.erb @@ -419,7 +419,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo diff --git a/app/views/user_notifications/digest.text.erb b/app/views/user_notifications/digest.text.erb index 5230597c6b..e3bbc1f818 100644 --- a/app/views/user_notifications/digest.text.erb +++ b/app/views/user_notifications/digest.text.erb @@ -52,7 +52,7 @@ <%= digest_custom_text("above_footer") %> <%=raw(t :'user_notifications.digest.unsubscribe', site_link: site_link, - email_settings_url: Discourse.base_url + '/my/preferences/emails', + email_preferences_link: raw(@markdown_linker.create(t('user_notifications.digest.your_email_settings'), '/my/preferences/emails')), unsubscribe_link: raw(@markdown_linker.create(t('user_notifications.digest.click_here'), email_unsubscribe_url(key: @unsubscribe_key, only_path: true)))) %> <%= raw(@markdown_linker.references) %> diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index cd740988d9..21fe80e59d 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2688,7 +2688,8 @@ en: popular_posts: "Popular Posts" more_new: "New for you" subject_template: "[%{email_prefix}] Summary" - unsubscribe: "This summary is sent from %{site_link} when we haven't seen you in a while. Change your email settings, or %{unsubscribe_link} to unsubscribe." + unsubscribe: "This summary is sent from %{site_link} when we haven't seen you in a while. Change %{email_preferences_link}, or %{unsubscribe_link} to unsubscribe." + your_email_settings: "your email settings" click_here: "click here" from: "%{site_name} summary" preheader: "A brief summary since your last visit on %{last_seen_at}" From d955af59600516cdc14a3bb940fc58dc28896392 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 30 Oct 2017 16:14:20 -0400 Subject: [PATCH 009/445] FEATURE: Allow widgets to call `_super()` when reopened --- .../javascripts/discourse/widgets/widget.js.es6 | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 index 022625168f..f5822f8fa9 100644 --- a/app/assets/javascripts/discourse/widgets/widget.js.es6 +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -133,7 +133,21 @@ export function reopenWidget(name, opts) { opts.html = opts.template; } - Object.keys(opts).forEach(k => existing.prototype[k] = opts[k]); + Object.keys(opts).forEach(k => { + let old = existing.prototype[k]; + + if (old) { + // Add support for `this._super()` to reopened widgets if the prototype exists in the + // base object + existing.prototype[k] = function(...args) { + let ctx = Object.create(this); + ctx._super = (...superArgs) => old.apply(this, superArgs); + return opts[k].apply(ctx, args); + }; + } else { + existing.prototype[k] = opts[k]; + } + }); return existing; } From bd1616d3d9f17d4daec74c4bf96855bcf61aea67 Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Mon, 30 Oct 2017 19:46:48 -0400 Subject: [PATCH 010/445] Add offline route and service worker to fix Android app install banner (#5217) * set up static offline.html route and service worker for Android Web App Banner * add viewport meta tag to offline view for android app banner * add i18n support for offline.html pages, cleanup * fix html syntax, add page title, remove license for service-worker.js --- app/controllers/offline_controller.rb | 8 +++ app/views/offline/offline.html.erb | 17 +++++ config/locales/server.en.yml | 4 ++ config/routes.rb | 1 + public/service-worker.js | 90 ++++++++++++++++++++++++++- 5 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 app/controllers/offline_controller.rb create mode 100644 app/views/offline/offline.html.erb diff --git a/app/controllers/offline_controller.rb b/app/controllers/offline_controller.rb new file mode 100644 index 0000000000..fb315862ba --- /dev/null +++ b/app/controllers/offline_controller.rb @@ -0,0 +1,8 @@ +class OfflineController < ApplicationController + layout false + skip_before_filter :preload_json, :check_xhr, :redirect_to_login_if_required + + def index + render :offline, content_type: 'text/html' + end +end diff --git a/app/views/offline/offline.html.erb b/app/views/offline/offline.html.erb new file mode 100644 index 0000000000..8e3115986a --- /dev/null +++ b/app/views/offline/offline.html.erb @@ -0,0 +1,17 @@ + + + + + + <%= SiteSetting.title %> + + +
+

<%= t 'offline.title' %>

+

+ <%= t 'offline.offline_page_message' %> +

+
+ + + diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 21fe80e59d..2ee6ce1c63 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2828,6 +2828,10 @@ en: search_title: "Search this site" search_google: "Google" + offline: + title: "Cannot load app" + offline_page_message: "It looks like you are offline! Please check your network connection and try again." + login_required: welcome_message: | ## [Welcome to %{title}](#welcome) diff --git a/config/routes.rb b/config/routes.rb index b3df2161ea..bb001d109e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -692,6 +692,7 @@ Discourse::Application.routes.draw do get "favicon/proxied" => "static#favicon", format: false get "robots.txt" => "robots_txt#index" + get "offline.html" => "offline#index" get "manifest.json" => "metadata#manifest", as: :manifest get "opensearch" => "metadata#opensearch", format: :xml diff --git a/public/service-worker.js b/public/service-worker.js index 3c6fecee60..50d9454459 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,4 +1,90 @@ /* - This service worker doesn't actually do anything! - I'm here just to support Google Chrome App Banner on Android + I'm here to support Google Chrome App Banner on Android */ + +'use strict'; + +// Incrementing CACHE_VERSION will kick off the install event and force previously cached +// resources to be cached again. +const CACHE_VERSION = 1; +let CURRENT_CACHES = { + offline: 'offline-v' + CACHE_VERSION +}; +const OFFLINE_URL = 'offline.html'; + +function createCacheBustedRequest(url) { + let request = new Request(url, {cache: 'reload'}); + // See https://fetch.spec.whatwg.org/#concept-request-mode + // This is not yet supported in Chrome as of M48, so we need to explicitly check to see + // if the cache: 'reload' option had any effect. + if ('cache' in request) { + return request; + } + + // If {cache: 'reload'} didn't have any effect, append a cache-busting URL parameter instead. + let bustedUrl = new URL(url, self.location.href); + bustedUrl.search += (bustedUrl.search ? '&' : '') + 'cachebust=' + Date.now(); + return new Request(bustedUrl); +} + +self.addEventListener('install', event => { + event.waitUntil( + // We can't use cache.add() here, since we want OFFLINE_URL to be the cache key, but + // the actual URL we end up requesting might include a cache-busting parameter. + fetch(createCacheBustedRequest(OFFLINE_URL)).then(function(response) { + return caches.open(CURRENT_CACHES.offline).then(function(cache) { + return cache.put(OFFLINE_URL, response); + }); + }) + ); +}); + +self.addEventListener('activate', event => { + // Delete all caches that aren't named in CURRENT_CACHES. + // While there is only one cache in this example, the same logic will handle the case where + // there are multiple versioned caches. + let expectedCacheNames = Object.keys(CURRENT_CACHES).map(function(key) { + return CURRENT_CACHES[key]; + }); + + event.waitUntil( + caches.keys().then(cacheNames => { + return Promise.all( + cacheNames.map(cacheName => { + if (expectedCacheNames.indexOf(cacheName) === -1) { + // If this cache name isn't present in the array of "expected" cache names, + // then delete it. + return caches.delete(cacheName); + } + }) + ); + }) + ); +}); + +self.addEventListener('fetch', event => { + // We only want to call event.respondWith() if this is a navigation request + // for an HTML page. + // request.mode of 'navigate' is unfortunately not supported in Chrome + // versions older than 49, so we need to include a less precise fallback, + // which checks for a GET request with an Accept: text/html header. + if (event.request.mode === 'navigate' || + (event.request.method === 'GET' && + event.request.headers.get('accept').includes('text/html'))) { + event.respondWith( + fetch(event.request).catch(error => { + // The catch is only triggered if fetch() throws an exception, which will most likely + // happen due to the server being unreachable. + // If fetch() returns a valid HTTP response with an response code in the 4xx or 5xx + // range, the catch() will NOT be called. If you need custom handling for 4xx or 5xx + // errors, see https://github.com/GoogleChrome/samples/tree/gh-pages/service-worker/fallback-response + return caches.match(OFFLINE_URL); + }) + ); + } + + // If our if() condition is false, then this fetch handler won't intercept the request. + // If there are any other fetch handlers registered, they will get a chance to call + // event.respondWith(). If no fetch handlers call event.respondWith(), the request will be + // handled by the browser as if there were no service worker involvement. +}); From 9cbb90c5ed1bf61ef55c02bcb3cd0ca81f22fb58 Mon Sep 17 00:00:00 2001 From: Jay Pfaffman Date: Mon, 30 Oct 2017 16:50:34 -0700 Subject: [PATCH 011/445] add importer for modx forum (#5239) --- script/import_scripts/modx.rb | 569 ++++++++++++++++++++++++++++++++++ 1 file changed, 569 insertions(+) create mode 100644 script/import_scripts/modx.rb diff --git a/script/import_scripts/modx.rb b/script/import_scripts/modx.rb new file mode 100644 index 0000000000..191135f6bf --- /dev/null +++ b/script/import_scripts/modx.rb @@ -0,0 +1,569 @@ +require 'mysql2' +require File.expand_path(File.dirname(__FILE__) + "/base.rb") +require 'htmlentities' + +class ImportScripts::Modx < ImportScripts::Base + BATCH_SIZE = 1000 + + # CHANGE THESE BEFORE RUNNING THE IMPORTER + + DB_HOST ||= ENV['DB_HOST'] || "localhost" + DB_NAME ||= ENV['DB_NAME'] || "modx" + DB_PW ||= ENV['DB_PW'] || "modex" + DB_USER ||= ENV['DB_USER'] || "modx" + TIMEZONE ||= ENV['TIMEZONE'] || "America/Los_Angeles" + TABLE_PREFIX ||= ENV['TABLE_PREFIX'] || "modx_" + ATTACHMENT_DIR ||= ENV['ATTACHMENT_DIR'] || '/path/to/your/attachment/folder' + RANDOM_CATEGORY_COLOR ||= !ENV['RANDOM_CATEGORY_COLOR'].nil? + SUSPEND_ALL_USERS ||= !ENV['SUSPEND_ALL_USERS'] + + # TODO: replace modx_ with #{TABLE_PREFIX} + + puts "#{DB_USER}:#{DB_PW}@#{DB_HOST} wants #{DB_NAME}" + + def initialize + super + + SiteSetting.disable_emails = true + + @old_username_to_new_usernames = {} + + @tz = TZInfo::Timezone.get(TIMEZONE) + + @htmlentities = HTMLEntities.new + + @client = Mysql2::Client.new( + host: DB_HOST, + username: DB_USER, + password: DB_PW, + database: DB_NAME + ) + rescue Exception => e + puts '=' * 50 + puts e.message + puts < #{last_user_id} + ORDER BY id + LIMIT #{BATCH_SIZE}; + SQL + ).to_a + + break if users.empty? + + last_user_id = users[-1]["userid"] + before = users.size + users.reject! { |u| @lookup.user_already_imported?(u["userid"].to_i) } + + create_users(users, total: user_count, offset: offset) do |user| + { + id: user["userid"], + name: user['name'], + username: user['username'], + email: user['email'], + website: user['website'], + created_at: parse_timestamp(user["created_at"]), + last_seen_at: parse_timestamp(user["last_seen_at"]), + date_of_birth: user['date_of_birth'], + password: "#{user['password']}:#{user['salt']}" # not tested + } + end + end + end + + def import_categories + # import modx_discuss_categories as categories + # import modx_discuss_boards as subcategories + puts "", "importing categories..." + + categories = mysql_query("select id, name, description from modx_discuss_categories").to_a + + create_categories(categories) do |category| + puts "Creating #{category['name']}" + puts category + { + id: "cat#{category['id']}", + name: category["name"], + color: RANDOM_CATEGORY_COLOR ? (0..2).map { "%0x" % (rand * 0x80) }.join : nil, + description: category["description"] + } + end + + puts "", "importing boards as subcategories..." + + boards = mysql_query("select id, category, name, description from modx_discuss_boards;").to_a + create_categories(boards) do |category| + puts category + parent_category_id = category_id_from_imported_category_id("cat#{category['category']}") + { + id: category["id"], + parent_category_id: parent_category_id.to_s, + name: category["name"], + color: RANDOM_CATEGORY_COLOR ? (0..2).map { "%0x" % (rand * 0x80) }.join : nil, + description: category["description"] + } + end + end + + def import_topics_and_posts + puts "", "creating topics and posts" + + total_count = mysql_query("SELECT count(id) count from #{TABLE_PREFIX}discuss_posts").first["count"] + + topic_first_post_id = {} + + batches(BATCH_SIZE) do |offset| + results = mysql_query(" + SELECT id, + thread topic_id, + board category_id, + title, + message raw, + parent, + author user_id, + createdon created_at + from modx_discuss_posts + ORDER BY createdon + LIMIT #{BATCH_SIZE} + OFFSET #{offset}; + ") + + break if results.size < 1 + + next if all_records_exist? :posts, results.map { |m| m['id'].to_i } + + create_posts(results, total: total_count, offset: offset) do |m| + skip = false + mapped = {} + + mapped[:id] = m['id'] + mapped[:user_id] = user_id_from_imported_user_id(m['user_id']) || -1 + mapped[:raw] = post_process_raw(m['raw']) + mapped[:created_at] = Time.zone.at(m['created_at']) + + if m['parent'] == 0 + mapped[:category] = category_id_from_imported_category_id(m['category_id']) + mapped[:title] = m['title'] + topic_first_post_id[m['topic_id']] = m['id'] + else + parent = topic_lookup_from_imported_post_id(topic_first_post_id[m['topic_id']]) + if parent + mapped[:topic_id] = parent[:topic_id] + else + puts "Parent post #{first_post_id} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" + skip = true + end + end + + skip ? nil : mapped + end + end + end + + def post_process_raw(raw) + # [QUOTE]...[/QUOTE] + raw = raw.gsub(/\[quote.*?\](.+?)\[\/quote\]/im) { |quote| + quote = quote.gsub(/\[quote author=(.*?) .+\]/i) { "\n[quote=\"#{$1}\"]\n" } + quote = quote.gsub(/[^\n]\[\/quote\]/im) { "\n[/quote]\n" } + } + + raw + end + + def not_mark_topics_as_solved + puts "", "Marking topics as solved..." + + PostAction.exec_sql <<-SQL + INSERT INTO topic_custom_fields (name, value, topic_id, created_at, updated_at) + SELECT 'accepted_answer_post_id', pcf.post_id, p.topic_id, p.created_at, p.created_at + FROM post_custom_fields pcf + JOIN posts p ON p.id = pcf.post_id + WHERE pcf.name = 'is_accepted_answer' + SQL + end + + # find the uploaded file information from the db + def not_find_upload(post, attachment_id) + sql = "SELECT a.attachmentid attachment_id, a.userid user_id, a.filedataid file_id, a.filename filename, + a.caption caption + FROM #{TABLE_PREFIX}attachment a + WHERE a.attachmentid = #{attachment_id}" + results = mysql_query(sql) + + unless row = results.first + puts "Couldn't find attachment record for post.id = #{post.id}, import_id = #{post.custom_fields['import_id']}" + return + end + + filename = File.join(ATTACHMENT_DIR, row['user_id'].to_s.split('').join('/'), "#{row['file_id']}.attach") + unless File.exists?(filename) + puts "Attachment file doesn't exist: #{filename}" + return + end + + real_filename = row['filename'] + real_filename.prepend SecureRandom.hex if real_filename[0] == '.' + upload = create_upload(post.user.id, filename, real_filename) + + if upload.nil? || !upload.valid? + puts "Upload not valid :(" + puts upload.errors.inspect if upload + return + end + + [upload, real_filename] + rescue Mysql2::Error => e + puts "SQL Error" + puts e.message + puts sql + end + + def not_import_private_messages + puts "", "importing private messages..." + + topic_count = mysql_query("SELECT COUNT(pmtextid) count FROM #{TABLE_PREFIX}pmtext").first["count"] + + last_private_message_id = -1 + + batches(BATCH_SIZE) do |offset| + private_messages = mysql_query(<<-SQL + SELECT pmtextid, fromuserid, title, message, touserarray, dateline + FROM #{TABLE_PREFIX}pmtext + WHERE pmtextid > #{last_private_message_id} + ORDER BY pmtextid + LIMIT #{BATCH_SIZE} + SQL + ).to_a + + break if private_messages.empty? + + last_private_message_id = private_messages[-1]["pmtextid"] + private_messages.reject! { |pm| @lookup.post_already_imported?("pm-#{pm['pmtextid']}") } + + title_username_of_pm_first_post = {} + + create_posts(private_messages, total: topic_count, offset: offset) do |m| + skip = false + mapped = {} + + mapped[:id] = "pm-#{m['pmtextid']}" + mapped[:user_id] = user_id_from_imported_user_id(m['fromuserid']) || Discourse::SYSTEM_USER_ID + mapped[:raw] = preprocess_post_raw(m['message']) rescue nil + mapped[:created_at] = Time.zone.at(m['dateline']) + title = @htmlentities.decode(m['title']).strip[0...255] + topic_id = nil + + next if mapped[:raw].blank? + + # users who are part of this private message. + target_usernames = [] + target_userids = [] + begin + to_user_array = PHP.unserialize(m['touserarray']) + rescue + puts "#{m['pmtextid']} -- #{m['touserarray']}" + skip = true + end + + begin + to_user_array.each do |to_user| + if to_user[0] == "cc" || to_user[0] == "bcc" # not sure if we should include bcc users + to_user[1].each do |to_user_cc| + user_id = user_id_from_imported_user_id(to_user_cc[0]) + username = User.find_by(id: user_id).try(:username) + target_userids << user_id || Discourse::SYSTEM_USER_ID + target_usernames << username if username + end + else + user_id = user_id_from_imported_user_id(to_user[0]) + username = User.find_by(id: user_id).try(:username) + target_userids << user_id || Discourse::SYSTEM_USER_ID + target_usernames << username if username + end + end + rescue + puts "skipping pm-#{m['pmtextid']} `to_user_array` is not properly serialized -- #{to_user_array.inspect}" + skip = true + end + + participants = target_userids + participants << mapped[:user_id] + begin + participants.sort! + rescue + puts "one of the participant's id is nil -- #{participants.inspect}" + end + + if title =~ /^Re:/ + + parent_id = title_username_of_pm_first_post[[title[3..-1], participants]] || + title_username_of_pm_first_post[[title[4..-1], participants]] || + title_username_of_pm_first_post[[title[5..-1], participants]] || + title_username_of_pm_first_post[[title[6..-1], participants]] || + title_username_of_pm_first_post[[title[7..-1], participants]] || + title_username_of_pm_first_post[[title[8..-1], participants]] + + if parent_id + if t = topic_lookup_from_imported_post_id("pm-#{parent_id}") + topic_id = t[:topic_id] + end + end + else + title_username_of_pm_first_post[[title, participants]] ||= m['pmtextid'] + end + + unless topic_id + mapped[:title] = title + mapped[:archetype] = Archetype.private_message + mapped[:target_usernames] = target_usernames.join(',') + + if mapped[:target_usernames].size < 1 # pm with yourself? + # skip = true + mapped[:target_usernames] = "system" + puts "pm-#{m['pmtextid']} has no target (#{m['touserarray']})" + end + else + mapped[:topic_id] = topic_id + end + + skip ? nil : mapped + end + end + end + + def not_import_attachments + puts '', 'importing attachments...' + + current_count = 0 + + total_count = mysql_query(<<-SQL + SELECT COUNT(postid) count + FROM #{TABLE_PREFIX}post p + JOIN #{TABLE_PREFIX}thread t ON t.threadid = p.threadid + WHERE t.firstpostid <> p.postid + SQL + ).first["count"] + + success_count = 0 + fail_count = 0 + + attachment_regex = /\[attach[^\]]*\](\d+)\[\/attach\]/i + + Post.find_each do |post| + current_count += 1 + print_status current_count, total_count + + new_raw = post.raw.dup + new_raw.gsub!(attachment_regex) do |s| + matches = attachment_regex.match(s) + attachment_id = matches[1] + + upload, filename = find_upload(post, attachment_id) + unless upload + fail_count += 1 + next + end + + html_for_upload(upload, filename) + end + + if new_raw != post.raw + PostRevisor.new(post).revise!(post.user, { raw: new_raw }, bypass_bump: true, edit_reason: 'Import attachments from modx') + end + + success_count += 1 + end + end + + def not_close_topics + puts "", "Closing topics..." + + # keep track of closed topics + closed_topic_ids = [] + + topics = mysql_query <<-MYSQL + SELECT t.threadid threadid, firstpostid, open + FROM #{TABLE_PREFIX}thread t + JOIN #{TABLE_PREFIX}post p ON p.postid = t.firstpostid + ORDER BY t.threadid + MYSQL + topics.each do |topic| + topic_id = "thread-#{topic["threadid"]}" + closed_topic_ids << topic_id if topic["open"] == 0 + end + + sql = <<-SQL + WITH closed_topic_ids AS ( + SELECT t.id AS topic_id + FROM post_custom_fields pcf + JOIN posts p ON p.id = pcf.post_id + JOIN topics t ON t.id = p.topic_id + WHERE pcf.name = 'import_id' + AND pcf.value IN (?) + ) + UPDATE topics + SET closed = true + WHERE id IN (SELECT topic_id FROM closed_topic_ids) + SQL + + Topic.exec_sql(sql, closed_topic_ids) + end + + def not_post_process_posts + puts "", "Postprocessing posts..." + + current = 0 + max = Post.count + + Post.find_each do |post| + begin + new_raw = postprocess_post_raw(post.raw) + if new_raw != post.raw + post.raw = new_raw + post.save + end + rescue PrettyText::JavaScriptError + nil + ensure + print_status(current += 1, max) + end + end + end + + def not_create_permalink_file + puts '', 'Creating Permalink File...', '' + + id_mapping = [] + + Topic.listable_topics.find_each do |topic| + pcf = topic.first_post.custom_fields + if pcf && pcf["import_id"] + id = pcf["import_id"].split('-').last + id_mapping.push("XXX#{id} YYY#{topic.id}") + end + end + + # Category.find_each do |cat| + # ccf = cat.custom_fields + # if ccf && ccf["import_id"] + # id = ccf["import_id"].to_i + # id_mapping.push("/forumdisplay.php?#{id} http://forum.quartertothree.com#{cat.url}") + # end + # end + + CSV.open(File.expand_path("../vb_map.csv", __FILE__), "w") do |csv| + id_mapping.each do |value| + csv << [value] + end + end + + end + + def deactivate_all_users + User.where("id > 0 and admin != true").update_all(active: true) + end + + def suspend_users + puts '', "updating blocked users" + + banned = 0 + failed = 0 + total = mysql_query("SELECT count(*) count FROM #{TABLE_PREFIX}user_attributes where blocked != 0").first['count'] + + system_user = Discourse.system_user + + mysql_query("SELECT id, blockedafter, blockeduntil FROM #{TABLE_PREFIX}user_attributes").each do |b| + user = User.find_by_id(user_id_from_imported_user_id(b['id'])) + if user + user.suspended_at = parse_timestamp(user["blockedafter"]) + user.suspended_till = parse_timestamp(user["blockeduntil"]) + if user.save + StaffActionLogger.new(system_user).log_user_suspend(user, "banned during initial import") + banned += 1 + else + puts "Failed to suspend user #{user.username}. #{user.errors.try(:full_messages).try(:inspect)}" + failed += 1 + end + else + puts "Not found: #{b['userid']}" + failed += 1 + end + + print_status banned + failed, total + end + end + + def parse_timestamp(timestamp) + Time.zone.at(@tz.utc_to_local(timestamp)) + end + + def mysql_query(sql) + @client.query(sql, cache_rows: true) + end + +end + +ImportScripts::Modx.new.perform From 85c749d6e4cb97fc95e52488a0bef3d8d5b9a1ef Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 31 Oct 2017 08:34:13 +0800 Subject: [PATCH 012/445] Fix eslint. --- app/assets/javascripts/admin/components/admin-nav.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/components/admin-nav.js.es6 b/app/assets/javascripts/admin/components/admin-nav.js.es6 index 55cb258d4f..9250c1ae73 100644 --- a/app/assets/javascripts/admin/components/admin-nav.js.es6 +++ b/app/assets/javascripts/admin/components/admin-nav.js.es6 @@ -1,3 +1,3 @@ export default Ember.Component.extend({ tagName: '' -}) +}); From d15068da707eba448b203722a421c2f7977ca44e Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 31 Oct 2017 10:15:22 +0800 Subject: [PATCH 013/445] Remove experimental logstash feature. * We'll most likely be extracting it into a plugin. --- config/initializers/100-lograge.rb | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/config/initializers/100-lograge.rb b/config/initializers/100-lograge.rb index 9e5dcfc916..565a29277b 100644 --- a/config/initializers/100-lograge.rb +++ b/config/initializers/100-lograge.rb @@ -4,8 +4,6 @@ if (Rails.env.production? && SiteSetting.logging_provider == 'lograge') || ENV[" Rails.application.configure do config.lograge.enabled = true - logstash_uri = ENV["LOGSTASH_URI"] - config.lograge.custom_options = lambda do |event| exceptions = %w(controller action format id) @@ -14,23 +12,6 @@ if (Rails.env.production? && SiteSetting.logging_provider == 'lograge') || ENV[" database: RailsMultisite::ConnectionManagement.current_db, time: event.time, } - - output[:type] = :rails if logstash_uri - output - end - - if logstash_uri - require 'logstash-logger' - - config.lograge.formatter = Lograge::Formatters::Logstash.new - - config.lograge.logger = LogStashLogger.new( - type: :multi_delegator, - outputs: [ - { uri: logstash_uri }, - { type: :file, path: "#{Rails.root}/log/#{Rails.env}.log", sync: true } - ] - ) end end end From 7c5a71e9299dca63f57dd4b70930551f6e2c0404 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 31 Oct 2017 13:48:47 +1100 Subject: [PATCH 014/445] DEV: allow queue_jobs = false in dev your mileage may vary --- app/jobs/base.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/jobs/base.rb b/app/jobs/base.rb index 05133bfc7e..fa16e3179b 100644 --- a/app/jobs/base.rb +++ b/app/jobs/base.rb @@ -200,7 +200,13 @@ module Jobs # Otherwise execute the job right away opts.delete(:delay_for) opts[:sync_exec] = true - klass.new.perform(opts) + if Rails.env == "development" + Scheduler::Defer.later("job") do + klass.new.perform(opts) + end + else + klass.new.perform(opts) + end end end From 9197feefb867fa1a028ae68b6ddc930119689600 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 31 Oct 2017 13:50:44 +1100 Subject: [PATCH 015/445] UX: onebox images no longer cause jiggle This stops pages from "jiggling" while oneboxes download. see: http://cssmojo.com/aspect-ratio-using-custom-properties-and-calc/ --- .../stylesheets/common/base/onebox.scss | 44 +++++++++++++++++++ lib/cooked_post_processor.rb | 11 ++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index b89a3efc78..dc1cb50afc 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -140,6 +140,50 @@ aside.onebox { margin-right: 10px; } + [style*="--aspect-ratio"] > :first-child { + width: 100%; + } + [style*="--aspect-ratio"] > img { + height: auto; + } + + // this allows us to load all onebox images without jiggle + // see: http://cssmojo.com/aspect-ratio-using-custom-properties-and-calc/ + @supports (--custom:property) { + + .aspect-image { + max-height: 170px; + max-width: 20%; + float: left; + margin-right: 10px; + width: 100%; + height: auto; + + img { + width: 100%; + height: inherit; + max-width: initial; + max-height: initial; + float: none; + margin-right: none; + } + } + [style*="--aspect-ratio"] { + position: relative; + } + [style*="--aspect-ratio"]::before { + content: ""; + display: block; + padding-bottom: calc(100% / (var(--aspect-ratio))); + } + [style*="--aspect-ratio"] > :first-child { + position: absolute; + top: 0; + left: 0; + height: 100%; + } + } + // tighten bottom margin on last para p:last-child { margin-bottom: 4px; diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 30f91d44e4..31da056aa5 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -323,7 +323,16 @@ class CookedPostProcessor end # make sure we grab dimensions for oneboxed images - oneboxed_images.each { |img| limit_size!(img) } + # and wrap in a div + oneboxed_images.each do |img| + limit_size!(img) + if (width = img["width"].to_i) > 0 && (height = img["height"].to_i) > 0 + img.delete('width') + img.delete('height') + new_parent = img.add_next_sibling("
") + new_parent.first.add_child(img) + end + end end def optimize_urls From 1bd9e64a36ebeed22e397e4ee2b815a99dfad348 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 31 Oct 2017 15:44:16 +1100 Subject: [PATCH 016/445] FIX: offline controller regression --- app/controllers/offline_controller.rb | 2 +- spec/controllers/offline_controller_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 spec/controllers/offline_controller_spec.rb diff --git a/app/controllers/offline_controller.rb b/app/controllers/offline_controller.rb index fb315862ba..9ca5ac84cc 100644 --- a/app/controllers/offline_controller.rb +++ b/app/controllers/offline_controller.rb @@ -1,6 +1,6 @@ class OfflineController < ApplicationController layout false - skip_before_filter :preload_json, :check_xhr, :redirect_to_login_if_required + skip_before_action :preload_json, :check_xhr, :redirect_to_login_if_required def index render :offline, content_type: 'text/html' diff --git a/spec/controllers/offline_controller_spec.rb b/spec/controllers/offline_controller_spec.rb new file mode 100644 index 0000000000..b583747b7d --- /dev/null +++ b/spec/controllers/offline_controller_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' + +describe OfflineController do + it "can hit index" do + get :index + expect(response.status).to eq(200) + end +end From 58bb3c14f66f4b4aa5f3bac878d1b8aaac8784b7 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 31 Oct 2017 14:13:54 +0800 Subject: [PATCH 017/445] Remove gem that is no longer used. --- Gemfile | 1 - Gemfile.lock | 4 ---- 2 files changed, 5 deletions(-) diff --git a/Gemfile b/Gemfile index befe82d775..05d0d66b4d 100644 --- a/Gemfile +++ b/Gemfile @@ -173,7 +173,6 @@ gem 'memory_profiler', require: false, platform: :mri gem 'cppjieba_rb', require: false gem 'lograge', require: false -gem 'logstash-logger', require: false gem 'logster' gem 'sassc', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 506f502ea2..59bc9e2574 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -155,9 +155,6 @@ GEM activesupport (>= 4, < 5.2) railties (>= 4, < 5.2) request_store (~> 1.0) - logstash-event (1.2.02) - logstash-logger (0.25.1) - logstash-event (~> 1.2) logster (1.2.8) loofah (2.1.1) crass (~> 1.0.2) @@ -441,7 +438,6 @@ DEPENDENCIES http_accept_language (~> 2.0.5) listen lograge - logstash-logger logster lru_redux mail From b3e61ebb38173bf1e378957b18379fbaf11237ee Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 31 Oct 2017 16:00:09 +0530 Subject: [PATCH 018/445] suppress print output when running specs --- spec/tasks/posts_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/tasks/posts_spec.rb b/spec/tasks/posts_spec.rb index acaf19c5dc..8215a08264 100644 --- a/spec/tasks/posts_spec.rb +++ b/spec/tasks/posts_spec.rb @@ -9,7 +9,7 @@ RSpec.describe "Post rake tasks" do before do Rake::Task.clear Discourse::Application.load_tasks - IO.any_instance.stubs(:puts) + STDOUT.stubs(:write) end describe 'remap' do From 8c27f28dcbeef57ec9e88b72ebe3ab731ae98d87 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 31 Oct 2017 12:08:34 +0100 Subject: [PATCH 019/445] add more logging to FinalDestination --- lib/file_helper.rb | 2 -- lib/final_destination.rb | 16 +++++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/file_helper.rb b/lib/file_helper.rb index 6de82ad7f2..10fa7bb000 100644 --- a/lib/file_helper.rb +++ b/lib/file_helper.rb @@ -33,8 +33,6 @@ class FileHelper url = "https:" + url if url.start_with?("//") raise Discourse::InvalidParameters.new(:url) unless url =~ /^https?:\/\// - uri = - dest = FinalDestination.new( url, max_redirects: follow_redirect ? 5 : 1, diff --git a/lib/final_destination.rb b/lib/final_destination.rb index dae0cea24b..357700ee44 100644 --- a/lib/final_destination.rb +++ b/lib/final_destination.rb @@ -93,6 +93,7 @@ class FinalDestination if @limit < 0 @status = :too_many_redirects + log(:warn, "FinalDestination could not resolve URL (too many redirects): #{@uri}") return nil end @@ -103,7 +104,11 @@ class FinalDestination end end - return nil unless validate_uri + unless validate_uri + log(:warn, "FinalDestination could not resolve URL (invalid URI): #{@uri}") + return nil + end + headers = request_headers response = Excon.public_send(@http_verb, @uri.to_s, @@ -175,8 +180,10 @@ class FinalDestination @status = :failure @status_code = response.status + log(:warn, "FinalDestination could not resolve URL (status #{response.status}): #{@uri}") nil rescue Excon::Errors::Timeout + log(:warn, "FinalDestination could not resolve URL (timeout): #{@uri}") nil end @@ -246,6 +253,13 @@ class FinalDestination SiteSetting.blacklist_ip_blocks.split('|').map { |r| IPAddr.new(r) rescue nil }.compact end + def log(log_level, message) + Rails.logger.public_send( + log_level, + "#{RailsMultisite::ConnectionManagement.current_db}: #{message}" + ) + end + def self.standard_private_ranges @private_ranges ||= [ IPAddr.new('127.0.0.1'), From 880d154381c13855bfc01cb138fe544d01eb9915 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 31 Oct 2017 15:13:23 +0100 Subject: [PATCH 020/445] FIX: deleting staged user of rejected email shouldn't delete incoming email --- lib/email/receiver.rb | 8 +++++++- spec/components/email/receiver_spec.rb | 5 +++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 6cb0adf8eb..2510aa1e00 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -756,7 +756,13 @@ module Email def delete_staged_users @staged_users.each do |user| - UserDestroyer.new(Discourse.system_user).destroy(user, quiet: true) if user.posts.count == 0 + if @incoming_email.user.id == user.id + @incoming_email.update_columns(user_id: nil) + end + + if user.posts.count == 0 + UserDestroyer.new(Discourse.system_user).destroy(user, quiet: true) + end end end end diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 69338c3b16..0054712aa4 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -760,6 +760,11 @@ describe Email::Receiver do end end + it "does not remove the incoming email record when staged users are deleted" do + expect { process(:bad_destinations) }.to change { IncomingEmail.count } + .and raise_error(Email::Receiver::BadDestinationAddress) + expect(IncomingEmail.last.message_id).to eq("9@foo.bar.mail") + end end end From d1f257d275adf53fc285273cb51af3f4598be606 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 31 Oct 2017 17:03:03 +0100 Subject: [PATCH 021/445] FinalDestination should only log when verbose is enabled --- lib/file_helper.rb | 3 ++- lib/final_destination.rb | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/file_helper.rb b/lib/file_helper.rb index 10fa7bb000..c8039bb9c5 100644 --- a/lib/file_helper.rb +++ b/lib/file_helper.rb @@ -36,7 +36,8 @@ class FileHelper dest = FinalDestination.new( url, max_redirects: follow_redirect ? 5 : 1, - skip_rate_limit: skip_rate_limit + skip_rate_limit: skip_rate_limit, + verbose: verbose ) uri = dest.resolve diff --git a/lib/final_destination.rb b/lib/final_destination.rb index 357700ee44..b290cf8376 100644 --- a/lib/final_destination.rb +++ b/lib/final_destination.rb @@ -51,6 +51,7 @@ class FinalDestination @http_verb = @force_get_hosts.any? { |host| hostname_matches?(host) } ? :get : :head @cookie = nil @limited_ips = [] + @verbose = @opts[:verbose] || false end def self.connection_timeout @@ -93,7 +94,7 @@ class FinalDestination if @limit < 0 @status = :too_many_redirects - log(:warn, "FinalDestination could not resolve URL (too many redirects): #{@uri}") + log(:warn, "FinalDestination could not resolve URL (too many redirects): #{@uri}") if @verbose return nil end @@ -105,7 +106,7 @@ class FinalDestination end unless validate_uri - log(:warn, "FinalDestination could not resolve URL (invalid URI): #{@uri}") + log(:warn, "FinalDestination could not resolve URL (invalid URI): #{@uri}") if @verbose return nil end @@ -180,10 +181,10 @@ class FinalDestination @status = :failure @status_code = response.status - log(:warn, "FinalDestination could not resolve URL (status #{response.status}): #{@uri}") + log(:warn, "FinalDestination could not resolve URL (status #{response.status}): #{@uri}") if @verbose nil rescue Excon::Errors::Timeout - log(:warn, "FinalDestination could not resolve URL (timeout): #{@uri}") + log(:warn, "FinalDestination could not resolve URL (timeout): #{@uri}") if @verbose nil end From d753adab84553949dc0b5864988cd0fc62aa18fc Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 31 Oct 2017 15:18:52 -0400 Subject: [PATCH 022/445] FIX: badge description links broken on subfolder --- app/models/badge.rb | 4 ++-- config/locales/server.en.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/models/badge.rb b/app/models/badge.rb index f8c41e7db1..96938ec4ea 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -197,7 +197,7 @@ class Badge < ActiveRecord::Base def long_description key = "badges.#{i18n_name}.long_description" - I18n.t(key, default: self[:long_description] || '') + I18n.t(key, default: self[:long_description] || '', base_uri: Discourse.base_uri) end def long_description=(val) @@ -207,7 +207,7 @@ class Badge < ActiveRecord::Base def description key = "badges.#{i18n_name}.description" - I18n.t(key, default: self[:description] || '') + I18n.t(key, default: self[:description] || '', base_uri: Discourse.base_uri) end def description=(val) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2ee6ce1c63..de77b20115 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -3265,9 +3265,9 @@ en: This badge is granted when you receive your first like on a post. Congratulations, you've posted something that your fellow community members found interesting, cool, or useful! autobiographer: name: Autobiographer - description: Filled out profile information + description: Filled out profile information long_description: | - This badge is granted for filling out your user profile and selecting a profile picture. Letting the community know a bit more about who you are and what you're interested in makes for a better, more connected community. Join us! + This badge is granted for filling out your user profile and selecting a profile picture. Letting the community know a bit more about who you are and what you're interested in makes for a better, more connected community. Join us! anniversary: name: Anniversary description: Active member for a year, posted at least once @@ -3360,9 +3360,9 @@ en: This badge is granted the first time you quote a post in your reply. Quoting relevant sections of earlier posts in your reply helps keep discussions connected together and on topic. The easiest way to quote is to highlight a section of a post, and then press any reply button. Quote generously! read_guidelines: name: Read Guidelines - description: Read the community guidelines + description: Read the community guidelines long_description: | - This badge is granted for reading the community guidelines. Following and sharing these simple guidelines helps build a safe, fun, and sustainable community for everyone. Always remember there's another human being, one very much like yourself, on the other side of that screen. Be nice! + This badge is granted for reading the community guidelines. Following and sharing these simple guidelines helps build a safe, fun, and sustainable community for everyone. Always remember there's another human being, one very much like yourself, on the other side of that screen. Be nice! reader: name: Reader description: Read every reply in a topic with more than 100 replies From 422f990615f64d3290f272dbdec8686ade04af89 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 31 Oct 2017 14:47:57 -0400 Subject: [PATCH 023/445] REFACTOR: Extract navigation into a component --- .../discourse/components/d-navigation.js.es6 | 22 ++++++++++++++++ .../controllers/navigation/default.js.es6 | 13 ---------- .../routes/build-category-route.js.es6 | 1 - .../routes/discovery-categories.js.es6 | 6 +---- .../templates/components/d-navigation.hbs | 26 +++++++++++++++++++ .../templates/navigation/categories.hbs | 15 +++++------ .../templates/navigation/category.hbs | 24 +++++------------ .../templates/navigation/default.hbs | 9 +++---- .../categories-admin-dropdown.js.es6 | 4 +-- 9 files changed, 68 insertions(+), 52 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/d-navigation.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/d-navigation.hbs diff --git a/app/assets/javascripts/discourse/components/d-navigation.js.es6 b/app/assets/javascripts/discourse/components/d-navigation.js.es6 new file mode 100644 index 0000000000..f2467ddcf6 --- /dev/null +++ b/app/assets/javascripts/discourse/components/d-navigation.js.es6 @@ -0,0 +1,22 @@ +import computed from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + tagName: '', + + @computed('category') + showCategoryNotifications(category) { + return category && this.currentUser; + }, + + @computed() + categories() { + return Discourse.Category.list(); + }, + + @computed("filterMode") + navItems(filterMode) { + // we don't want to show the period in the navigation bar since it's in a dropdown + if (filterMode.indexOf("top/") === 0) { filterMode = filterMode.replace("top/", ""); } + return Discourse.NavItem.buildList(null, { filterMode }); + } +}); diff --git a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 index 93ae014e47..57bb78d156 100644 --- a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 +++ b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 @@ -3,17 +3,4 @@ import computed from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend({ discovery: Ember.inject.controller(), discoveryTopics: Ember.inject.controller('discovery/topics'), - - @computed() - categories() { - return Discourse.Category.list(); - }, - - @computed("filterMode") - navItems(filterMode) { - // we don't want to show the period in the navigation bar since it's in a dropdown - if (filterMode.indexOf("top/") === 0) { filterMode = filterMode.replace("top/", ""); } - return Discourse.NavItem.buildList(null, { filterMode }); - } - }); diff --git a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 index 0f8fb98d39..4b29783e56 100644 --- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 @@ -49,7 +49,6 @@ export default (filterArg, params) => { category, filterMode: filterMode, noSubcategories: params && params.no_subcategories, - canEditCategory: category.get('can_edit') }); }, diff --git a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 index d254e0e6a2..0ba2194e50 100644 --- a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 @@ -12,10 +12,6 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { this.render("discovery/categories", { outlet: "list-container" }); }, - beforeModel() { - this.controllerFor("navigation/categories").set("filterMode", "categories"); - }, - model() { const style = !this.site.mobileView && this.siteSettings.desktop_category_page_style; const parentCategory = this.get("model.parentCategory"); @@ -81,7 +77,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { controller.set("model", model); this.controllerFor("navigation/categories").setProperties({ - canCreateCategory: model.get("can_create_category"), + showCategoryAdmin: model.get("can_create_category"), canCreateTopic: model.get("can_create_topic"), }); diff --git a/app/assets/javascripts/discourse/templates/components/d-navigation.hbs b/app/assets/javascripts/discourse/templates/components/d-navigation.hbs new file mode 100644 index 0000000000..8297a4f7e1 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/d-navigation.hbs @@ -0,0 +1,26 @@ +{{bread-crumbs categories=categories category=category noSubcategories=noSubcategories}} + +{{#if showCategoryAdmin}} + {{categories-admin-dropdown + create=createCategory + reorder=reorderCategories}} +{{/if}} + +{{navigation-bar navItems=navItems filterMode=filterMode category=category}} + +{{#if showCategoryNotifications}} + {{category-notifications-button category=category}} +{{/if}} + +{{create-topic-button + canCreateTopic=canCreateTopic + action=createTopic + disabled=createTopicDisabled}} + +{{#if category.can_edit}} + {{d-button + class="btn-default edit-category" + action=editCategory + icon="wrench" + label="category.edit_long"}} +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/navigation/categories.hbs b/app/assets/javascripts/discourse/templates/navigation/categories.hbs index 2a434f30f9..30495333df 100644 --- a/app/assets/javascripts/discourse/templates/navigation/categories.hbs +++ b/app/assets/javascripts/discourse/templates/navigation/categories.hbs @@ -1,10 +1,9 @@ {{#d-section bodyClass="navigation-categories" class="navigation-container"}} - {{bread-crumbs categories=categories}} - - {{navigation-bar navItems=navItems filterMode=filterMode}} - - {{#if canCreateCategory}} - {{categories-admin-dropdown}} - {{/if}} - {{create-topic-button canCreateTopic=canCreateTopic action=(route-action "createTopic")}} + {{d-navigation + filterMode="categories" + showCategoryAdmin=showCategoryAdmin + createCategory=(route-action "createCategory") + reorderCategories=(route-action "reorderCategories") + canCreateTopic=canCreateTopic + createTopic=(route-action "createTopic")}} {{/d-section}} diff --git a/app/assets/javascripts/discourse/templates/navigation/category.hbs b/app/assets/javascripts/discourse/templates/navigation/category.hbs index 8f1ab92d9b..d8dcac7040 100644 --- a/app/assets/javascripts/discourse/templates/navigation/category.hbs +++ b/app/assets/javascripts/discourse/templates/navigation/category.hbs @@ -11,24 +11,14 @@
- {{bread-crumbs categories=categories - category=category - noSubcategories=noSubcategories}} - - {{navigation-bar navItems=navItems filterMode=filterMode category=category}} - - {{#if currentUser}} - {{category-notifications-button category=category}} - {{/if}} - - {{create-topic-button + {{d-navigation + category=category + filterMode=filterMode + noSubcategories=noSubcategories canCreateTopic=canCreateTopic - disabled=cannotCreateTopicOnCategory - action=(route-action "createTopic")}} - - {{#if canEditCategory}} - {{d-button class="btn-default edit-category" action="editCategory" actionParam=category icon="wrench" label="category.edit_long"}} - {{/if}} + createTopic=(route-action "createTopic") + createTopicDisabled=cannotCreateTopicOnCategory + editCategory=(route-action "editCategory" category)}} {{plugin-outlet name="category-navigation" args=(hash category=category)}}
diff --git a/app/assets/javascripts/discourse/templates/navigation/default.hbs b/app/assets/javascripts/discourse/templates/navigation/default.hbs index e6b81e44b7..b9d9854295 100644 --- a/app/assets/javascripts/discourse/templates/navigation/default.hbs +++ b/app/assets/javascripts/discourse/templates/navigation/default.hbs @@ -1,7 +1,6 @@ {{#d-section bodyClass="navigation-topics" class="navigation-container" scrollTop="false"}} - {{bread-crumbs categories=categories}} - - {{navigation-bar navItems=navItems filterMode=filterMode}} - - {{create-topic-button canCreateTopic=canCreateTopic action=(route-action "createTopic")}} + {{d-navigation + filterMode=filterMode + canCreateTopic=canCreateTopic + createTopic=(route-action "createTopic")}} {{/d-section}} diff --git a/app/assets/javascripts/select-box-kit/components/categories-admin-dropdown.js.es6 b/app/assets/javascripts/select-box-kit/components/categories-admin-dropdown.js.es6 index f39d219db6..61ffa1c225 100644 --- a/app/assets/javascripts/select-box-kit/components/categories-admin-dropdown.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/categories-admin-dropdown.js.es6 @@ -5,7 +5,6 @@ import { on } from "ember-addons/ember-computed-decorators"; export default DropdownSelectBoxComponent.extend({ classNames: "categories-admin-dropdown", - actionNames: { create: "createCategory", reorder: "reorderCategories" }, @on("didReceiveAttrs") _setComponentOptions() { @@ -42,8 +41,7 @@ export default DropdownSelectBoxComponent.extend({ actions: { onSelect(value) { value = this.defaultOnSelect(value); - - this.sendAction(`actionNames.${value}`); + this.get(value)(); this.set("value", null); } } From 882cc9f992d33659a4a905d861e9bbf45e25d814 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 31 Oct 2017 15:42:12 -0400 Subject: [PATCH 024/445] Support appending a category id to a navigation item dynamically --- .../discourse/components/navigation-item.js.es6 | 13 ++++++++++++- .../templates/components/navigation-bar.hbs | 2 +- .../templates/mobile/components/navigation-bar.hbs | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/components/navigation-item.js.es6 b/app/assets/javascripts/discourse/components/navigation-item.js.es6 index 9ee2e38905..888eb3c28c 100644 --- a/app/assets/javascripts/discourse/components/navigation-item.js.es6 +++ b/app/assets/javascripts/discourse/components/navigation-item.js.es6 @@ -16,7 +16,18 @@ export default Ember.Component.extend(bufferedRender({ buildBuffer(buffer) { const content = this.get('content'); - buffer.push(""); + + let href = content.get('href'); + + // Include the category id if the option is present + if (content.get('includeCategoryId')) { + let categoryId = this.get('category.id'); + if (categoryId) { + href += `?category_id=${categoryId}`; + } + } + + buffer.push(``); if (content.get('hasIcon')) { buffer.push(""); } diff --git a/app/assets/javascripts/discourse/templates/components/navigation-bar.hbs b/app/assets/javascripts/discourse/templates/components/navigation-bar.hbs index 256ab2af04..0808efcc6d 100644 --- a/app/assets/javascripts/discourse/templates/components/navigation-bar.hbs +++ b/app/assets/javascripts/discourse/templates/components/navigation-bar.hbs @@ -1,5 +1,5 @@ {{#each navItems as |navItem|}} - {{navigation-item content=navItem filterMode=filterMode}} + {{navigation-item content=navItem filterMode=filterMode category=category}} {{/each}} {{custom-html name="extraNavItem" tagName="li"}} {{!- this is done to avoid DIV in the UL, originally {{plugin-outlet name="extra-nav-item"}} diff --git a/app/assets/javascripts/discourse/templates/mobile/components/navigation-bar.hbs b/app/assets/javascripts/discourse/templates/mobile/components/navigation-bar.hbs index f52a6b16ca..884ceb75d4 100644 --- a/app/assets/javascripts/discourse/templates/mobile/components/navigation-bar.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/components/navigation-bar.hbs @@ -7,7 +7,7 @@ {{#if expanded}}
    {{#each navItems as |navItem|}} - {{navigation-item content=navItem filterMode=filterMode}} + {{navigation-item content=navItem filterMode=filterMode category=category}} {{/each}}
{{/if}} From aee316c6ff65f76adfde418ad597465a33132fb8 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 31 Oct 2017 15:51:23 -0400 Subject: [PATCH 025/445] FIX: Unused variable is tripping up linter --- .../javascripts/discourse/controllers/navigation/default.js.es6 | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 index 57bb78d156..1fa8eedcc7 100644 --- a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 +++ b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 @@ -1,5 +1,3 @@ -import computed from "ember-addons/ember-computed-decorators"; - export default Ember.Controller.extend({ discovery: Ember.inject.controller(), discoveryTopics: Ember.inject.controller('discovery/topics'), From e7d8f5f9c8bba6bd5cbbca338291a923760aca6e Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 31 Oct 2017 15:46:03 -0400 Subject: [PATCH 026/445] FIX: support /my routes with subfolders --- app/assets/javascripts/discourse/lib/url.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index 1f989f297e..2fe1b79a8b 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -193,10 +193,10 @@ const DiscourseURL = Ember.Object.extend({ path = path.replace(/(https?\:)?\/\/[^\/]+/, ''); // Rewrite /my/* urls - if (path.indexOf('/my/') === 0) { + if (path.indexOf(Discourse.BaseUri + '/my/') === 0) { const currentUser = Discourse.User.current(); if (currentUser) { - path = path.replace('/my/', userPath(currentUser.get('username_lower') + "/")); + path = path.replace(Discourse.BaseUri + '/my/', userPath(currentUser.get('username_lower') + "/")); } else { document.location.href = "/404"; return; From ca8922e6f8fb5ae94f85739c6c4f2c79334b35e4 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 31 Oct 2017 16:02:20 -0400 Subject: [PATCH 027/445] UX: Autobiographer badge description should link to profile preferences --- config/locales/server.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index de77b20115..cbc605d584 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -3265,9 +3265,9 @@ en: This badge is granted when you receive your first like on a post. Congratulations, you've posted something that your fellow community members found interesting, cool, or useful! autobiographer: name: Autobiographer - description: Filled out
profile information + description: Filled out profile information long_description: | - This badge is granted for filling out your user profile and selecting a profile picture. Letting the community know a bit more about who you are and what you're interested in makes for a better, more connected community. Join us! + This badge is granted for filling out your user profile and selecting a profile picture. Letting the community know a bit more about who you are and what you're interested in makes for a better, more connected community. Join us! anniversary: name: Anniversary description: Active member for a year, posted at least once From 076df104dc4a49bf3b55400fdba8cd959b876596 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 31 Oct 2017 16:47:47 -0400 Subject: [PATCH 028/445] FEATURE: Support filtering of groups page by category if in url --- .../components/group-activity-filter.js.es6 | 3 ++ .../controllers/group-activity-posts.js.es6 | 5 +++- .../controllers/group-activity.js.es6 | 1 + .../javascripts/discourse/models/group.js.es6 | 5 ++-- .../routes/group-activity-posts.js.es6 | 5 ++-- .../components/group-activity-filter.hbs | 3 ++ .../discourse/templates/group/activity.hbs | 19 +++--------- app/controllers/groups_controller.rb | 30 +++++++++++++++---- app/models/group.rb | 28 +++++++++++++---- 9 files changed, 67 insertions(+), 32 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/group-activity-filter.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/group-activity-filter.hbs diff --git a/app/assets/javascripts/discourse/components/group-activity-filter.js.es6 b/app/assets/javascripts/discourse/components/group-activity-filter.js.es6 new file mode 100644 index 0000000000..d100e27e25 --- /dev/null +++ b/app/assets/javascripts/discourse/components/group-activity-filter.js.es6 @@ -0,0 +1,3 @@ +export default Ember.Component.extend({ + tagName: 'li' +}); diff --git a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 index c2434c83c6..991ce0b014 100644 --- a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 @@ -2,6 +2,7 @@ import { fmt } from 'discourse/lib/computed'; export default Ember.Controller.extend({ group: Ember.inject.controller(), + groupActivity: Ember.inject.controller(), loading: false, emptyText: fmt('type', 'groups.empty.%@'), @@ -14,7 +15,9 @@ export default Ember.Controller.extend({ const beforePostId = posts[posts.length-1].get('id'); const group = this.get('group.model'); - const opts = { beforePostId, type: this.get('type') }; + let categoryId = this.get('groupActivity.category_id'); + const opts = { beforePostId, type: this.get('type'), categoryId }; + group.findPosts(opts).then(newPosts => { posts.addObjects(newPosts); this.set('loading', false); diff --git a/app/assets/javascripts/discourse/controllers/group-activity.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity.js.es6 index 955822ffcf..de4bda4dcd 100644 --- a/app/assets/javascripts/discourse/controllers/group-activity.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group-activity.js.es6 @@ -2,6 +2,7 @@ import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Controller.extend({ application: Ember.inject.controller(), + queryParams: ['category_id'], @computed('model.is_group_user') showGroupMessages(isGroupUser) { diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6 index c7e5eed6b1..c26544e7c5 100644 --- a/app/assets/javascripts/discourse/models/group.js.es6 +++ b/app/assets/javascripts/discourse/models/group.js.es6 @@ -203,12 +203,13 @@ const Group = RestModel.extend({ findPosts(opts) { opts = opts || {}; - const type = opts['type'] || 'posts'; + const type = opts.type || 'posts'; var data = {}; if (opts.beforePostId) { data.before_post_id = opts.beforePostId; } + if (opts.categoryId) { data.category_id = parseInt(opts.categoryId); } - return ajax(`/groups/${this.get('name')}/${type}.json`, { data: data }).then(posts => { + return ajax(`/groups/${this.get('name')}/${type}.json`, { data }).then(posts => { return posts.map(p => { p.user = User.create(p.user); p.topic = Topic.create(p.topic); diff --git a/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 index 03cd7ec114..1127d36f97 100644 --- a/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 @@ -6,8 +6,9 @@ export function buildGroupPage(type) { return I18n.t(`groups.${type}`); }, - model() { - return this.modelFor("group").findPosts({ type }); + model(params, transition) { + let categoryId = Ember.get(transition, 'queryParams.category_id'); + return this.modelFor("group").findPosts({ type, categoryId }); }, setupController(controller, model) { diff --git a/app/assets/javascripts/discourse/templates/components/group-activity-filter.hbs b/app/assets/javascripts/discourse/templates/components/group-activity-filter.hbs new file mode 100644 index 0000000000..68623d09c8 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/group-activity-filter.hbs @@ -0,0 +1,3 @@ +{{#link-to (concat 'group.activity.' filter) (query-params category_id=categoryId)}} + {{i18n (concat 'groups.' filter)}} +{{/link-to}} diff --git a/app/assets/javascripts/discourse/templates/group/activity.hbs b/app/assets/javascripts/discourse/templates/group/activity.hbs index d03a14e84f..dcc8386bce 100644 --- a/app/assets/javascripts/discourse/templates/group/activity.hbs +++ b/app/assets/javascripts/discourse/templates/group/activity.hbs @@ -1,21 +1,10 @@
{{#mobile-nav class='group-activity-nav' desktopClass="pull-left nav nav-stacked" currentPath=application.currentPath}} -
  • - {{#link-to 'group.activity.posts'}}{{i18n 'groups.posts'}}{{/link-to}} -
  • - -
  • - {{#link-to 'group.activity.topics'}}{{i18n 'groups.topics'}}{{/link-to}} -
  • - -
  • - {{#link-to 'group.activity.mentions'}}{{i18n 'groups.mentions'}}{{/link-to}} -
  • - + {{group-activity-filter filter="posts" categoryId=category_id}} + {{group-activity-filter filter="topics" categoryId=category_id}} + {{group-activity-filter filter="mentions" categoryId=category_id}} {{#if showGroupMessages}} -
  • - {{#link-to 'group.activity.messages'}}{{i18n 'groups.messages'}}{{/link-to}} -
  • + {{group-activity-filter filter="messages"}} {{/if}} {{/mobile-nav}} diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 2785e69312..bd9f079547 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -69,13 +69,19 @@ class GroupsController < ApplicationController def posts group = find_group(:group_id) - posts = group.posts_for(guardian, params[:before_post_id]).limit(20) + posts = group.posts_for( + guardian, + params.permit(:before_post_id, :category_id) + ).limit(20) render_serialized posts.to_a, GroupPostSerializer end def posts_feed group = find_group(:group_id) - @posts = group.posts_for(guardian).limit(50) + @posts = group.posts_for( + guardian, + params.permit(:before_post_id, :category_id) + ).limit(50) @title = "#{SiteSetting.title} - #{I18n.t("rss_description.group_posts", group_name: group.name)}" @link = Discourse.base_url @description = I18n.t("rss_description.group_posts", group_name: group.name) @@ -84,19 +90,28 @@ class GroupsController < ApplicationController def topics group = find_group(:group_id) - posts = group.posts_for(guardian, params[:before_post_id]).where(post_number: 1).limit(20) + posts = group.posts_for( + guardian, + params.permit(:before_post_id, :category_id) + ).where(post_number: 1).limit(20) render_serialized posts.to_a, GroupPostSerializer end def mentions group = find_group(:group_id) - posts = group.mentioned_posts_for(guardian, params[:before_post_id]).limit(20) + posts = group.mentioned_posts_for( + guardian, + params.permit(:before_post_id, :category_id) + ).limit(20) render_serialized posts.to_a, GroupPostSerializer end def mentions_feed group = find_group(:group_id) - @posts = group.mentioned_posts_for(guardian).limit(50) + @posts = group.mentioned_posts_for( + guardian, + params.permit(:before_post_id, :category_id) + ).limit(50) @title = "#{SiteSetting.title} - #{I18n.t("rss_description.group_mentions", group_name: group.name)}" @link = Discourse.base_url @description = I18n.t("rss_description.group_mentions", group_name: group.name) @@ -106,7 +121,10 @@ class GroupsController < ApplicationController def messages group = find_group(:group_id) posts = if guardian.can_see_group_messages?(group) - group.messages_for(guardian, params[:before_post_id]).where(post_number: 1).limit(20).to_a + group.messages_for( + guardian, + params.permit(:before_post_id, :category_id) + ).where(post_number: 1).limit(20).to_a else [] end diff --git a/app/models/group.rb b/app/models/group.rb index 4083346854..bf2c2147b6 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -185,7 +185,8 @@ class Group < ActiveRecord::Base end end - def posts_for(guardian, before_post_id = nil) + def posts_for(guardian, opts = nil) + opts ||= {} user_ids = group_users.map { |gu| gu.user_id } result = Post.includes(:user, :topic, topic: :category) .references(:posts, :topics, :category) @@ -193,24 +194,35 @@ class Group < ActiveRecord::Base .where('topics.archetype <> ?', Archetype.private_message) .where(post_type: Post.types[:regular]) + if opts[:category_id].present? + result = result.where('topics.category_id = ?', opts[:category_id].to_i) + end + result = guardian.filter_allowed_categories(result) - result = result.where('posts.id < ?', before_post_id) if before_post_id + result = result.where('posts.id < ?', opts[:before_post_id].to_i) if opts[:before_post_id] result.order('posts.created_at desc') end - def messages_for(guardian, before_post_id = nil) + def messages_for(guardian, opts = nil) + opts ||= {} + result = Post.includes(:user, :topic, topic: :category) .references(:posts, :topics, :category) .where('topics.archetype = ?', Archetype.private_message) .where(post_type: Post.types[:regular]) .where('topics.id IN (SELECT topic_id FROM topic_allowed_groups WHERE group_id = ?)', self.id) + if opts[:category_id].present? + result = result.where('topics.category_id = ?', opts[:category_id].to_i) + end + result = guardian.filter_allowed_categories(result) - result = result.where('posts.id < ?', before_post_id) if before_post_id + result = result.where('posts.id < ?', opts[:before_post_id].to_i) if opts[:before_post_id] result.order('posts.created_at desc') end - def mentioned_posts_for(guardian, before_post_id = nil) + def mentioned_posts_for(guardian, opts = nil) + opts ||= {} result = Post.joins(:group_mentions) .includes(:user, :topic, topic: :category) .references(:posts, :topics, :category) @@ -218,8 +230,12 @@ class Group < ActiveRecord::Base .where(post_type: Post.types[:regular]) .where('group_mentions.group_id = ?', self.id) + if opts[:category_id].present? + result = result.where('topics.category_id = ?', opts[:category_id].to_i) + end + result = guardian.filter_allowed_categories(result) - result = result.where('posts.id < ?', before_post_id) if before_post_id + result = result.where('posts.id < ?', opts[:before_post_id].to_i) if opts[:before_post_id] result.order('posts.created_at desc') end From d888d3c54c4744d52b9d21d3728f5d6dbc132cde Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 1 Nov 2017 08:37:11 +0800 Subject: [PATCH 029/445] EXPERIMENTAL: Allow Logstash formatter to be enable for lograge logs. --- Gemfile | 1 + Gemfile.lock | 2 ++ config/initializers/100-lograge.rb | 9 ++++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 05d0d66b4d..ce14fdefda 100644 --- a/Gemfile +++ b/Gemfile @@ -173,6 +173,7 @@ gem 'memory_profiler', require: false, platform: :mri gem 'cppjieba_rb', require: false gem 'lograge', require: false +gem 'logstash-event', require: false gem 'logster' gem 'sassc', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 59bc9e2574..6bf55bdacd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -155,6 +155,7 @@ GEM activesupport (>= 4, < 5.2) railties (>= 4, < 5.2) request_store (~> 1.0) + logstash-event (1.2.02) logster (1.2.8) loofah (2.1.1) crass (~> 1.0.2) @@ -438,6 +439,7 @@ DEPENDENCIES http_accept_language (~> 2.0.5) listen lograge + logstash-event logster lru_redux mail diff --git a/config/initializers/100-lograge.rb b/config/initializers/100-lograge.rb index 565a29277b..304fde3ced 100644 --- a/config/initializers/100-lograge.rb +++ b/config/initializers/100-lograge.rb @@ -7,11 +7,18 @@ if (Rails.env.production? && SiteSetting.logging_provider == 'lograge') || ENV[" config.lograge.custom_options = lambda do |event| exceptions = %w(controller action format id) + params = event.payload[:params].except(*exceptions) + params[:files].map!(&:headers) if params[:files] + output = { - params: event.payload[:params].except(*exceptions), + params: params, database: RailsMultisite::ConnectionManagement.current_db, time: event.time, } end + + if ENV["LOGSTASH_FORMATTER"] + config.lograge.formatter = Lograge::Formatters::Logstash.new + end end end From 32b3847d5209dbbb9ecdd8150f267b889f1bd50d Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Wed, 1 Nov 2017 01:51:51 -0200 Subject: [PATCH 030/445] FIX: Update mobile logo resolution This makes Discourse compliant with latest Google PWA requirements, so we get the App Install banner back. Should bump our Lighthouse PWA Audit score to 11/11. --- app/controllers/metadata_controller.rb | 6 ++++-- config/locales/server.en.yml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/metadata_controller.rb b/app/controllers/metadata_controller.rb index 5785c33176..8c5886e917 100644 --- a/app/controllers/metadata_controller.rb +++ b/app/controllers/metadata_controller.rb @@ -13,6 +13,8 @@ class MetadataController < ApplicationController private def default_manifest + logo = SiteSetting.mobile_logo_url || SiteSetting.logo_small_url || SiteSetting.apple_touch_icon_url + manifest = { name: SiteSetting.title, short_name: SiteSetting.title, @@ -23,8 +25,8 @@ class MetadataController < ApplicationController theme_color: "##{ColorScheme.hex_for_name('header_background')}", icons: [ { - src: SiteSetting.apple_touch_icon_url, - sizes: "144x144", + src: logo, + sizes: "512x512", type: "image/png" } ] diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index cbc605d584..75cb0bb7ee 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1011,7 +1011,7 @@ en: digest_logo_url: "The alternate logo image used at the top of your site's email summary. Should be a wide rectangle shape. Should not be an SVG image. If left blank `logo_url` will be used." logo_small_url: "The small logo image at the top left of your site, should be a square shape, seen when scrolling down. If left blank a home glyph will be shown." favicon_url: "A favicon for your site, see http://en.wikipedia.org/wiki/Favicon, to work correctly over a CDN it must be a png" - mobile_logo_url: "The fixed position logo image used at the top left of your mobile site. Should be a square shape. If left blank, `logo_url` will be used. eg: http://example.com/uploads/default/logo.png" + mobile_logo_url: "The fixed position logo image used at the top left of your mobile site and as your logo/splash image on Android. Recommended size is 512px by 512px. If left blank, `logo_url` will be used. eg: http://example.com/uploads/default/logo.png" apple_touch_icon_url: "Icon used for Apple touch devices. Recommended size is 144px by 144px." notification_email: "The from: email address used when sending all essential system emails. The domain specified here must have SPF, DKIM and reverse PTR records set correctly for email to arrive." From 3c8b376e4ad1dc2e72c3a8429cc3f170794ff9ec Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Wed, 1 Nov 2017 02:28:09 -0200 Subject: [PATCH 031/445] FIX: Coalesce properly logos for the mobile manifest --- app/controllers/metadata_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/metadata_controller.rb b/app/controllers/metadata_controller.rb index 8c5886e917..e3494ceecc 100644 --- a/app/controllers/metadata_controller.rb +++ b/app/controllers/metadata_controller.rb @@ -13,7 +13,7 @@ class MetadataController < ApplicationController private def default_manifest - logo = SiteSetting.mobile_logo_url || SiteSetting.logo_small_url || SiteSetting.apple_touch_icon_url + logo = SiteSetting.mobile_logo_url.presence || SiteSetting.logo_small_url.presence || SiteSetting.apple_touch_icon_url.presence manifest = { name: SiteSetting.title, From f5cc28d740b2775b4b99a0f77d4d5fe6ba236531 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 1 Nov 2017 16:42:56 +1100 Subject: [PATCH 032/445] UX: correct regression with twitter onebox --- lib/cooked_post_processor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 31da056aa5..bf66b29e72 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -326,7 +326,7 @@ class CookedPostProcessor # and wrap in a div oneboxed_images.each do |img| limit_size!(img) - if (width = img["width"].to_i) > 0 && (height = img["height"].to_i) > 0 + if img.parent["class"].include?("onbox-body") && (width = img["width"].to_i) > 0 && (height = img["height"].to_i) > 0 img.delete('width') img.delete('height') new_parent = img.add_next_sibling("
    ") From deb79a8fff7b8d8dec85fb3af5c22bab90b7f6ee Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 1 Nov 2017 16:43:19 +1100 Subject: [PATCH 033/445] DEV: private means nothing for class methods --- lib/discourse_hub.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/discourse_hub.rb b/lib/discourse_hub.rb index 3ed120aaa9..cf6df955ee 100644 --- a/lib/discourse_hub.rb +++ b/lib/discourse_hub.rb @@ -18,8 +18,6 @@ module DiscourseHub $redis.set STATS_FETCHED_AT_KEY, time_with_zone.to_i end - private - def self.get_payload SiteSetting.share_anonymized_statistics && stats_fetched_at < 7.days.ago ? About.fetch_cached_stats.symbolize_keys : {} end From 9c5ad4648f1b760b982c609a4f4547468ab9ff47 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Tue, 31 Oct 2017 23:14:14 -0700 Subject: [PATCH 034/445] FIX: improves icon alignment the mobile icon has a higher height than other icons, given we want a total 30px height and we apply a 5px top and bottom padding, the icon can be at most 20px height --- .../stylesheets/common/admin/customize.scss | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index 370ac30b7e..91665d43b7 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -88,21 +88,27 @@ } .nav.target { + margin-top: 15px; + li { position: relative; + + a { + display: flex; + align-items: center; + justify-content: space-between; + } } - margin-top: 15px; + .fa { - margin-left: 3px; - } - li.mobile a { - padding-right: 25px; + margin-left: 8px; } + .d-icon-mobile { - position: absolute; - right: 10px; - top: 3px; + position: relative; + top: -3px; font-size: 1.5em; + max-height: 20px; } } From 6d8976e949dcc3245eb7ba8efe8afd2ad8c1a24c Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 1 Nov 2017 15:44:00 +0530 Subject: [PATCH 035/445] UX: support onebox labels --- app/assets/stylesheets/common/base/onebox.scss | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index dc1cb50afc..a96e156f07 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -89,7 +89,7 @@ a.loading-onebox { } aside.onebox { - border: 5px solid $primary-low; + border: 5px solid $primary-low; padding: 12px 25px 12px 12px; font-size: 1em; @@ -433,6 +433,16 @@ aside.onebox.stackexchange .onebox-body { } } +// whitelistedgeneric twitter labels +.onebox.whitelistedgeneric { + .label1 { + float: left; + } + .label2 { + float: right; + } +} + // mobile specific style .mobile-view article.onebox-body { border-top: none; From 6b5bb9d66452dc2e56c120413d643a0fbb0377b1 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 1 Nov 2017 15:50:23 +0530 Subject: [PATCH 036/445] bump onebox gem version --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index ce14fdefda..ba44402da7 100644 --- a/Gemfile +++ b/Gemfile @@ -35,7 +35,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.19' +gem 'onebox', '1.8.20' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 6bf55bdacd..a6a376a17e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -228,7 +228,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.19) + onebox (1.8.20) fast_blank (>= 1.0.0) htmlentities (~> 4.3) moneta (~> 1.0) @@ -464,7 +464,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.19) + onebox (= 1.8.20) openid-redis-store pg pry-nav From 2792c3c80e6939b2b1161a2b0b1e9b8114a7ba3b Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 1 Nov 2017 15:51:17 +0530 Subject: [PATCH 037/445] fix typo --- lib/cooked_post_processor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index bf66b29e72..03161dc6fd 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -326,7 +326,7 @@ class CookedPostProcessor # and wrap in a div oneboxed_images.each do |img| limit_size!(img) - if img.parent["class"].include?("onbox-body") && (width = img["width"].to_i) > 0 && (height = img["height"].to_i) > 0 + if img.parent["class"].include?("onebox-body") && (width = img["width"].to_i) > 0 && (height = img["height"].to_i) > 0 img.delete('width') img.delete('height') new_parent = img.add_next_sibling("
    ") From a00af4d85a39cd859d29bcd161a4eea2b00c55b1 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 27 Oct 2017 01:59:36 +0530 Subject: [PATCH 038/445] FEATURE: Rake task to export and import category structure --- lib/import_export/base_exporter.rb | 162 ++++++++++++++++++ lib/import_export/category_exporter.rb | 75 ++------ lib/import_export/category_importer.rb | 89 ---------- .../category_structure_exporter.rb | 30 ++++ lib/import_export/import_export.rb | 26 +-- lib/import_export/importer.rb | 160 +++++++++++++++++ lib/import_export/topic_exporter.rb | 83 ++------- lib/import_export/topic_importer.rb | 90 ---------- lib/tasks/export.rake | 7 + lib/tasks/import.rake | 8 + script/discourse | 4 +- spec/fabricators/group_user_fabricator.rb | 4 + spec/fixtures/json/import-export.json | 31 ++++ spec/import_export/category_exporter_spec.rb | 42 +++++ .../category_structure_exporter_spec.rb | 39 +++++ spec/import_export/importer_spec.rb | 68 ++++++++ spec/import_export/topic_exporter_spec.rb | 30 ++++ 17 files changed, 621 insertions(+), 327 deletions(-) create mode 100644 lib/import_export/base_exporter.rb delete mode 100644 lib/import_export/category_importer.rb create mode 100644 lib/import_export/category_structure_exporter.rb create mode 100644 lib/import_export/importer.rb delete mode 100644 lib/import_export/topic_importer.rb create mode 100644 lib/tasks/export.rake create mode 100644 spec/fabricators/group_user_fabricator.rb create mode 100644 spec/fixtures/json/import-export.json create mode 100644 spec/import_export/category_exporter_spec.rb create mode 100644 spec/import_export/category_structure_exporter_spec.rb create mode 100644 spec/import_export/importer_spec.rb create mode 100644 spec/import_export/topic_exporter_spec.rb diff --git a/lib/import_export/base_exporter.rb b/lib/import_export/base_exporter.rb new file mode 100644 index 0000000000..ff6cd2ed07 --- /dev/null +++ b/lib/import_export/base_exporter.rb @@ -0,0 +1,162 @@ +module ImportExport + class BaseExporter + + attr_reader :export_data, :categories + + CATEGORY_ATTRS = [:id, :name, :color, :created_at, :user_id, :slug, :description, :text_color, + :auto_close_hours, :parent_category_id, :auto_close_based_on_last_post, + :topic_template, :suppress_from_homepage, :all_topics_wiki, :permissions_params] + + GROUP_ATTRS = [ :id, :name, :created_at, :mentionable_level, :messageable_level, :visibility_level, + :automatic_membership_email_domains, :automatic_membership_retroactive, + :primary_group, :title, :grant_trust_level, :incoming_email] + + USER_ATTRS = [:id, :email, :username, :name, :created_at, :trust_level, :active, :last_emailed_at] + + TOPIC_ATTRS = [:id, :title, :created_at, :views, :category_id, :closed, :archived, :archetype] + + POST_ATTRS = [:id, :user_id, :post_number, :raw, :created_at, :reply_to_post_number, :hidden, + :hidden_reason_id, :wiki] + + def categories + @categories ||= Category.all.to_a + end + + def export_categories + data = [] + + categories.each do |cat| + data << CATEGORY_ATTRS.inject({}) { |h, a| h[a] = cat.send(a); h } + end + + data + end + + def export_categories! + @export_data[:categories] = export_categories + + self + end + + def export_category_groups + groups = [] + group_names = [] + auto_group_names = Group::AUTO_GROUPS.keys.map(&:to_s) + + @export_data[:categories].each do |c| + c[:permissions_params].each do |group_name, _| + group_names << group_name unless auto_group_names.include?(group_name.to_s) + end + end + + group_names.uniq! + return [] if group_names.empty? + + Group.where(name: group_names).find_each do |group| + attrs = GROUP_ATTRS.inject({}) { |h, a| h[a] = group.send(a); h } + attrs[:user_ids] = group.users.pluck(:id) + groups << attrs + end + + groups + end + + def export_category_groups! + @export_data[:groups] = export_category_groups + + self + end + + def export_group_users + user_ids = [] + + @export_data[:groups].each do |g| + user_ids += g[:user_ids] + end + + user_ids.uniq! + return [] if user_ids.empty? + + users = User.where(id: user_ids) + export_users(users.to_a) + end + + def export_group_users! + @export_data[:users] = export_group_users + + self + end + + def export_topics + data = [] + + @topics.each do |topic| + puts topic.title + + topic_data = TOPIC_ATTRS.inject({}) { |h, a| h[a] = topic.send(a); h; } + topic_data[:posts] = [] + + topic.ordered_posts.find_each do |post| + h = POST_ATTRS.inject({}) { |h, a| h[a] = post.send(a); h; } + h[:raw] = h[:raw].gsub('src="/uploads', "src=\"#{Discourse.base_url_no_prefix}/uploads") + topic_data[:posts] << h + end + + data << topic_data + end + + data + end + + def export_topics! + @export_data[:topics] = export_topics + + self + end + + def export_topic_users + return if @export_data[:topics].blank? + topic_ids = @export_data[:topics].pluck(:id) + + users = User.joins(:topics).where('topics.id IN (?)', topic_ids).to_a + users.uniq! + + export_users(users.to_a) + end + + def export_topic_users! + @export_data[:users] = export_topic_users + + self + end + + def export_users(users) + data = [] + users.reject! { |u| u.id == Discourse::SYSTEM_USER_ID } + + users.each do |u| + x = USER_ATTRS.inject({}) { |h, a| h[a] = u.send(a); h; } + x.merge(bio_raw: u.user_profile.bio_raw, + website: u.user_profile.website, + location: u.user_profile.location) + data << x + end + + data + end + + def default_filename_prefix + raise "Overwrite me!" + end + + def save_to_file(filename = nil) + output_basename = filename || File.join("#{default_filename_prefix}-#{Time.now.strftime("%Y-%m-%d-%H%M%S")}.json") + File.open(output_basename, "w:UTF-8") do |f| + f.write(@export_data.to_json) + end + puts "Export saved to #{output_basename}" + output_basename + end + + end +end diff --git a/lib/import_export/category_exporter.rb b/lib/import_export/category_exporter.rb index d40bd327c4..4ea7209c19 100644 --- a/lib/import_export/category_exporter.rb +++ b/lib/import_export/category_exporter.rb @@ -1,71 +1,32 @@ -module ImportExport - class CategoryExporter +require "import_export/base_exporter" +require "import_export/topic_exporter" - attr_reader :export_data +module ImportExport + class CategoryExporter < BaseExporter def initialize(category_id) @category = Category.find(category_id) - @subcategories = Category.where(parent_category_id: category_id) + @categories = Category.where(parent_category_id: category_id).to_a + @categories << @category @export_data = { - users: [], + categories: [], groups: [], - category: nil, - subcategories: [], - topics: [] + topics: [], + users: [] } end def perform puts "Exporting category #{@category.name}...", "" - export_categories + export_categories! + export_category_groups! export_topics_and_users self end - CATEGORY_ATTRS = [:id, :name, :color, :created_at, :user_id, :slug, :description, :text_color, - :auto_close_hours, :auto_close_based_on_last_post, - :topic_template, :suppress_from_homepage, :all_topics_wiki, :permissions_params] - - def export_categories - @export_data[:category] = CATEGORY_ATTRS.inject({}) { |h, a| h[a] = @category.send(a); h } - @subcategories.find_each do |subcat| - @export_data[:subcategories] << CATEGORY_ATTRS.inject({}) { |h, a| h[a] = subcat.send(a); h } - end - - # export groups that are mentioned in category permissions - group_names = [] - auto_group_names = Group::AUTO_GROUPS.keys.map(&:to_s) - - ([@export_data[:category]] + @export_data[:subcategories]).each do |c| - c[:permissions_params].each do |group_name, _| - group_names << group_name unless auto_group_names.include?(group_name.to_s) - end - end - - group_names.uniq! - export_groups(group_names) unless group_names.empty? - - self - end - - GROUP_ATTRS = [ :id, :name, :created_at, :mentionable_level, :messageable_level, :visible, - :automatic_membership_email_domains, :automatic_membership_retroactive, - :primary_group, :title, :grant_trust_level, :incoming_email] - - def export_groups(group_names) - group_names.each do |name| - group = Group.find_by_name(name) - group_attrs = GROUP_ATTRS.inject({}) { |h, a| h[a] = group.send(a); h } - group_attrs[:user_ids] = group.users.pluck(:id) - @export_data[:groups] << group_attrs - end - - self - end - def export_topics_and_users - all_category_ids = [@category.id] + @subcategories.pluck(:id) - description_topic_ids = Category.where(id: all_category_ids).pluck(:topic_id) + all_category_ids = @categories.pluck(:id) + description_topic_ids = @categories.pluck(:topic_id) topic_exporter = ImportExport::TopicExporter.new(Topic.where(category_id: all_category_ids).pluck(:id) - description_topic_ids) topic_exporter.perform @export_data[:users] = topic_exporter.export_data[:users] @@ -73,14 +34,8 @@ module ImportExport self end - def save_to_file(filename = nil) - require 'json' - output_basename = filename || File.join("category-export-#{Time.now.strftime("%Y-%m-%d-%H%M%S")}.json") - File.open(output_basename, "w:UTF-8") do |f| - f.write(@export_data.to_json) - end - puts "Export saved to #{output_basename}" - output_basename + def default_filename_prefix + "category-export" end end diff --git a/lib/import_export/category_importer.rb b/lib/import_export/category_importer.rb deleted file mode 100644 index b432bd078b..0000000000 --- a/lib/import_export/category_importer.rb +++ /dev/null @@ -1,89 +0,0 @@ -require File.join(Rails.root, 'script', 'import_scripts', 'base.rb') - -module ImportExport - class CategoryImporter < ImportScripts::Base - def initialize(export_data) - @export_data = export_data - @topic_importer = TopicImporter.new(@export_data) - end - - def perform - RateLimiter.disable - - import_users - import_groups - import_categories - import_topics - self - ensure - RateLimiter.enable - end - - def import_groups - return if @export_data[:groups].empty? - - @export_data[:groups].each do |group_data| - g = group_data.dup - user_ids = g.delete(:user_ids) - external_id = g.delete(:id) - new_group = Group.find_by_name(g[:name]) || Group.create!(g) - user_ids.each do |external_user_id| - new_group.add(User.find(@topic_importer.new_user_id(external_user_id))) rescue ActiveRecord::RecordNotUnique - end - end - end - - def import_users - @topic_importer.import_users - end - - def import_categories - id = @export_data[:category].delete(:id) - import_id = "#{id}#{import_source}" - - parent = CategoryCustomField.where(name: 'import_id', value: import_id).first.try(:category) - - unless parent - permissions = @export_data[:category].delete(:permissions_params) - parent = Category.new(@export_data[:category]) - parent.user_id = @topic_importer.new_user_id(@export_data[:category][:user_id]) # imported user's new id - parent.custom_fields["import_id"] = import_id - parent.permissions = permissions.present? ? permissions : { "everyone" => CategoryGroup.permission_types[:full] } - parent.save! - set_category_description(parent, @export_data[:category][:description]) - end - - @export_data[:subcategories].each do |cat_attrs| - id = cat_attrs.delete(:id) - import_id = "#{id}#{import_source}" - existing = CategoryCustomField.where(name: 'import_id', value: import_id).first.try(:category) - - unless existing - permissions = cat_attrs.delete(:permissions_params) - subcategory = Category.new(cat_attrs) - subcategory.parent_category_id = parent.id - subcategory.user_id = @topic_importer.new_user_id(cat_attrs[:user_id]) - subcategory.custom_fields["import_id"] = import_id - subcategory.permissions = permissions.present? ? permissions : { "everyone" => CategoryGroup.permission_types[:full] } - subcategory.save! - set_category_description(subcategory, cat_attrs[:description]) - end - end - end - - def set_category_description(c, description) - post = c.topic.ordered_posts.first - post.raw = description - post.save! - post.rebake! - end - - def import_topics - @topic_importer.import_topics - end - - def import_source - @_import_source ||= "#{ENV['IMPORT_SOURCE'] || ''}" - end - end -end diff --git a/lib/import_export/category_structure_exporter.rb b/lib/import_export/category_structure_exporter.rb new file mode 100644 index 0000000000..7c6f3c61db --- /dev/null +++ b/lib/import_export/category_structure_exporter.rb @@ -0,0 +1,30 @@ +require "import_export/base_exporter" + +module ImportExport + class CategoryStructureExporter < ImportExport::BaseExporter + + def initialize(include_group_users = false) + @include_group_users = include_group_users + + @export_data = { + groups: [], + categories: [] + } + @export_data[:users] = [] if @include_group_users + end + + def perform + puts "Exporting all the categories...", "" + export_categories! + export_category_groups! + export_group_users! if @include_group_users + + self + end + + def default_filename_prefix + "category-structure-export" + end + + end +end diff --git a/lib/import_export/import_export.rb b/lib/import_export/import_export.rb index fad560768f..53ed9f591c 100644 --- a/lib/import_export/import_export.rb +++ b/lib/import_export/import_export.rb @@ -1,26 +1,26 @@ +require "import_export/importer" +require "import_export/category_structure_exporter" require "import_export/category_exporter" -require "import_export/category_importer" require "import_export/topic_exporter" -require "import_export/topic_importer" require "json" module ImportExport + def self.import(filename) + data = ActiveSupport::HashWithIndifferentAccess.new(File.open(filename, "r:UTF-8") { |f| JSON.parse(f.read) }) + ImportExport::Importer.new(data).perform + end + + def self.export_categories(include_users, filename = nil) + ImportExport::CategoryStructureExporter.new(include_users).perform.save_to_file(filename) + end + def self.export_category(category_id, filename = nil) ImportExport::CategoryExporter.new(category_id).perform.save_to_file(filename) end - def self.import_category(filename) - export_data = ActiveSupport::HashWithIndifferentAccess.new(File.open(filename, "r:UTF-8") { |f| JSON.parse(f.read) }) - ImportExport::CategoryImporter.new(export_data).perform + def self.export_topics(topic_ids, filename = nil) + ImportExport::TopicExporter.new(topic_ids).perform.save_to_file(filename) end - def self.export_topics(topic_ids) - ImportExport::TopicExporter.new(topic_ids).perform.save_to_file - end - - def self.import_topics(filename) - export_data = ActiveSupport::HashWithIndifferentAccess.new(File.open(filename, "r:UTF-8") { |f| JSON.parse(f.read) }) - ImportExport::TopicImporter.new(export_data).perform - end end diff --git a/lib/import_export/importer.rb b/lib/import_export/importer.rb new file mode 100644 index 0000000000..9bc43ae506 --- /dev/null +++ b/lib/import_export/importer.rb @@ -0,0 +1,160 @@ +require File.join(Rails.root, 'script', 'import_scripts', 'base.rb') + +module ImportExport + class Importer < ImportScripts::Base + + def initialize(data) + @users = data[:users] + @groups = data[:groups] + @categories = data[:categories] + @topics = data[:topics] + + # To support legacy `category_export` script + if data[:category].present? + @categories = [] if @categories.blank? + @categories << data[:category] + end + end + + def perform + RateLimiter.disable + + import_users + import_groups + import_categories + import_topics + + self + ensure + RateLimiter.enable + end + + def import_users + return if @users.blank? + + @users.each do |u| + import_id = "#{u[:id]}#{import_source}" + existing = User.with_email(u[:email]).first + + if existing + if existing.custom_fields["import_id"] != import_id + existing.custom_fields["import_id"] = import_id + existing.save! + end + else + u = create_user(u, import_id) # see ImportScripts::Base + end + end + + self + end + + def import_groups + return if @groups.blank? + + @groups.each do |group_data| + g = group_data.dup + user_ids = g.delete(:user_ids) + external_id = g.delete(:id) + new_group = Group.find_by_name(g[:name]) || Group.create!(g) + user_ids.each do |external_user_id| + new_group.add(User.find(new_user_id(external_user_id))) rescue ActiveRecord::RecordNotUnique + end + end + + self + end + + def import_categories + return if @categories.blank? + + import_ids = @categories.collect { |c| "#{c[:id]}#{import_source}" } + existing_categories = CategoryCustomField.where("name = 'import_id' AND value IN (?)", import_ids).select(:category_id, :value).to_a + existing_category_ids = existing_categories.pluck(:value) + + @categories.reject! { |c| existing_category_ids.include? c[:id].to_s } + @categories.sort_by! { |c| c[:parent_category_id].presence || 0 } + + @categories.each do |cat_attrs| + id = cat_attrs.delete(:id) + permissions = cat_attrs.delete(:permissions_params) + + category = Category.new(cat_attrs) + category.parent_category_id = new_category_id(cat_attrs[:parent_category_id]) if cat_attrs[:parent_category_id].present? + category.user_id = new_user_id(cat_attrs[:user_id]) + import_id = "#{id}#{import_source}" + category.custom_fields["import_id"] = import_id + category.permissions = permissions.present? ? permissions : { "everyone" => CategoryGroup.permission_types[:full] } + category.save! + existing_categories << { category_id: category.id, value: import_id } + + if cat_attrs[:description].present? + post = category.topic.ordered_posts.first + post.raw = cat_attrs[:description] + post.save! + post.rebake! + end + end + + self + end + + def import_topics + return if @topics.blank? + + @topics.each do |t| + puts "" + print t[:title] + + first_post_attrs = t[:posts].first.merge(t.slice(*(TopicExporter::TOPIC_ATTRS - [:id, :category_id]))) + + first_post_attrs[:user_id] = new_user_id(first_post_attrs[:user_id]) + first_post_attrs[:category] = new_category_id(t[:category_id]) + + import_id = "#{first_post_attrs[:id]}#{import_source}" + first_post = PostCustomField.where(name: "import_id", value: import_id).first&.post + + unless first_post + first_post = create_post(first_post_attrs, import_id) + end + + topic_id = first_post.topic_id + + t[:posts].each_with_index do |post_data, i| + next if i == 0 + print "." + post_import_id = "#{post_data[:id]}#{import_source}" + existing = PostCustomField.where(name: "import_id", value: post_import_id).first&.post + unless existing + # see ImportScripts::Base + create_post( + post_data.merge( + topic_id: topic_id, + user_id: new_user_id(post_data[:user_id]) + ), + post_import_id + ) + end + end + end + + puts "" + + self + end + + def new_user_id(external_user_id) + ucf = UserCustomField.where(name: "import_id", value: "#{external_user_id}#{import_source}").first + ucf ? ucf.user_id : Discourse::SYSTEM_USER_ID + end + + def new_category_id(external_category_id) + CategoryCustomField.where(name: "import_id", value: "#{external_category_id}#{import_source}").first.category_id rescue nil + end + + def import_source + @_import_source ||= "#{ENV['IMPORT_SOURCE'] || ''}" + end + + end +end diff --git a/lib/import_export/topic_exporter.rb b/lib/import_export/topic_exporter.rb index 4c494003ed..4603e3d1b6 100644 --- a/lib/import_export/topic_exporter.rb +++ b/lib/import_export/topic_exporter.rb @@ -1,89 +1,26 @@ -module ImportExport - class TopicExporter +require "import_export/base_exporter" - attr_reader :exported_user_ids, :export_data +module ImportExport + class TopicExporter < ImportExport::BaseExporter def initialize(topic_ids) - @topic_ids = topic_ids - @exported_user_ids = [] + @topics = Topic.where(id: topic_ids).to_a @export_data = { - users: [], - topics: [] + topics: [], + users: [] } end def perform - export_users - export_topics + export_topics! + export_topic_users! # TODO: user actions self end - USER_ATTRS = [:id, :email, :username, :name, :created_at, :trust_level, :active, :last_emailed_at] - - def export_users - # TODO: avatar - - @exported_user_ids = [] - @topic_ids.each do |topic_id| - t = Topic.find(topic_id) - t.posts.includes(user: [:user_profile]).find_each do |post| - u = post.user - unless @exported_user_ids.include?(u.id) - x = USER_ATTRS.inject({}) { |h, a| h[a] = u.send(a); h; } - @export_data[:users] << x.merge(bio_raw: u.user_profile.bio_raw, - website: u.user_profile.website, - location: u.user_profile.location) - @exported_user_ids << u.id - end - end - end - - self - end - - def export_topics - @topic_ids.each do |topic_id| - t = Topic.find(topic_id) - puts t.title - export_topic(t) - end - puts "" - end - - TOPIC_ATTRS = [:id, :title, :created_at, :views, :category_id, :closed, :archived, :archetype] - POST_ATTRS = [:id, :user_id, :post_number, :raw, :created_at, :reply_to_post_number, - :hidden, :hidden_reason_id, :wiki] - - def export_topic(topic) - topic_data = {} - - TOPIC_ATTRS.each do |a| - topic_data[a] = topic.send(a) - end - - topic_data[:posts] = [] - - topic.ordered_posts.find_each do |post| - h = POST_ATTRS.inject({}) { |h, a| h[a] = post.send(a); h; } - h[:raw] = h[:raw].gsub('src="/uploads', "src=\"#{Discourse.base_url_no_prefix}/uploads") - topic_data[:posts] << h - end - - @export_data[:topics] << topic_data - - self - end - - def save_to_file(filename = nil) - require 'json' - output_basename = filename || File.join("topic-export-#{Time.now.strftime("%Y-%m-%d-%H%M%S")}.json") - File.open(output_basename, "w:UTF-8") do |f| - f.write(@export_data.to_json) - end - puts "Export saved to #{output_basename}" - output_basename + def default_filename_prefix + "topic-export" end end diff --git a/lib/import_export/topic_importer.rb b/lib/import_export/topic_importer.rb deleted file mode 100644 index 721536d722..0000000000 --- a/lib/import_export/topic_importer.rb +++ /dev/null @@ -1,90 +0,0 @@ -require File.join(Rails.root, 'script', 'import_scripts', 'base.rb') - -module ImportExport - class TopicImporter < ImportScripts::Base - def initialize(export_data) - @export_data = export_data - end - - def perform - RateLimiter.disable - - import_users - import_topics - self - ensure - RateLimiter.enable - end - - def import_users - @export_data[:users].each do |u| - import_id = "#{u[:id]}#{import_source}" - existing = User.with_email(u[:email]).first - if existing - if existing.custom_fields["import_id"] != import_id - existing.custom_fields["import_id"] = import_id - existing.save! - end - else - u = create_user(u, import_id) # see ImportScripts::Base - end - end - self - end - - def import_topics - @export_data[:topics].each do |t| - puts "" - print t[:title] - - first_post_attrs = t[:posts].first.merge(t.slice(*(TopicExporter::TOPIC_ATTRS - [:id, :category_id]))) - - first_post_attrs[:user_id] = new_user_id(first_post_attrs[:user_id]) - first_post_attrs[:category] = new_category_id(t[:category_id]) - - import_id = "#{first_post_attrs[:id]}#{import_source}" - first_post = PostCustomField.where(name: "import_id", value: import_id).first&.post - - unless first_post - first_post = create_post(first_post_attrs, import_id) - end - - topic_id = first_post.topic_id - - t[:posts].each_with_index do |post_data, i| - next if i == 0 - print "." - post_import_id = "#{post_data[:id]}#{import_source}" - existing = PostCustomField.where(name: "import_id", value: post_import_id).first&.post - unless existing - # see ImportScripts::Base - create_post( - post_data.merge( - topic_id: topic_id, - user_id: new_user_id(post_data[:user_id]) - ), - post_import_id - ) - end - end - end - - puts "" - - self - end - - def new_user_id(external_user_id) - ucf = UserCustomField.where(name: "import_id", value: "#{external_user_id}#{import_source}").first - ucf ? ucf.user_id : Discourse::SYSTEM_USER_ID - end - - def new_category_id(external_category_id) - CategoryCustomField.where(name: "import_id", value: "#{external_category_id}#{import_source}").first.category_id rescue nil - end - - def import_source - @_import_source ||= "#{ENV['IMPORT_SOURCE'] || ''}" - end - end -end diff --git a/lib/tasks/export.rake b/lib/tasks/export.rake new file mode 100644 index 0000000000..cfa5954027 --- /dev/null +++ b/lib/tasks/export.rake @@ -0,0 +1,7 @@ +desc 'Export all the categories' +task 'export:categories', [:include_group_users, :file_name] => [:environment] do |_, args| + require "import_export/import_export" + + ImportExport.export_categories(args[:include_group_users], args[:file_name]) + puts "", "Done", "" +end diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake index 129aeca234..f26e7b3025 100644 --- a/lib/tasks/import.rake +++ b/lib/tasks/import.rake @@ -500,3 +500,11 @@ task "import:create_vbulletin_permalinks" => :environment do log "Done!" end + +desc 'Import existing exported file' +task 'import:file', [:file_name] => [:environment] do |_, args| + require "import_export/import_export" + + ImportExport.import(args[:file_name]) + puts "", "Done", "" +end diff --git a/script/discourse b/script/discourse index 85b7fa491a..1f8fe1275f 100755 --- a/script/discourse +++ b/script/discourse @@ -198,7 +198,7 @@ class DiscourseCLI < Thor puts "Starting import from #{filename}..." load_rails load_import_export - ImportExport.import_category(filename) + ImportExport.import(filename) puts "", "Done", "" end @@ -218,7 +218,7 @@ class DiscourseCLI < Thor puts "Starting import from #{filename}..." load_rails load_import_export - ImportExport.import_topics(filename) + ImportExport.import(filename) puts "", "Done", "" end diff --git a/spec/fabricators/group_user_fabricator.rb b/spec/fabricators/group_user_fabricator.rb new file mode 100644 index 0000000000..9ed23e47eb --- /dev/null +++ b/spec/fabricators/group_user_fabricator.rb @@ -0,0 +1,4 @@ +Fabricator(:group_user) do + user + group +end diff --git a/spec/fixtures/json/import-export.json b/spec/fixtures/json/import-export.json new file mode 100644 index 0000000000..9af32b8e1e --- /dev/null +++ b/spec/fixtures/json/import-export.json @@ -0,0 +1,31 @@ +{ + "groups":[ + {"id":41,"name":"custom_group","created_at":"2017-10-26T15:33:46.328Z","mentionable_level":0,"messageable_level":0,"visibility_level":0,"automatic_membership_email_domains":"","automatic_membership_retroactive":false,"primary_group":false,"title":null,"grant_trust_level":null,"incoming_email":null,"user_ids":[1]}, + {"id":42,"name":"custom_group_import","created_at":"2017-10-26T15:33:46.328Z","mentionable_level":0,"messageable_level":0,"visibility_level":0,"automatic_membership_email_domains":"","automatic_membership_retroactive":false,"primary_group":false,"title":null,"grant_trust_level":null,"incoming_email":null,"user_ids":[2]} + ], + "categories":[ + {"id":8,"name":"Custom Category","color":"AB9364","created_at":"2017-10-26T15:32:44.083Z","user_id":1,"slug":"custom-category","description":null,"text_color":"FFFFFF","auto_close_hours":null,"parent_category_id":3,"auto_close_based_on_last_post":false,"topic_template":"","suppress_from_homepage":false,"all_topics_wiki":false,"permissions_params":{"custom_group":1,"everyone":2}}, + {"id":10,"name":"Site Feedback Import","color":"808281","created_at":"2017-10-26T17:12:39.995Z","user_id":-1,"slug":"site-feedback-import","description":"Discussion about this site, its organization, how it works, and how we can improve it.","text_color":"FFFFFF","auto_close_hours":null,"parent_category_id":null,"auto_close_based_on_last_post":false,"topic_template":null,"suppress_from_homepage":false,"all_topics_wiki":false,"permissions_params":{}}, + {"id":11,"name":"Uncategorized Import","color":"AB9364","created_at":"2017-10-26T17:12:32.359Z","user_id":-1,"slug":"uncategorized-import","description":"","text_color":"FFFFFF","auto_close_hours":null,"parent_category_id":null,"auto_close_based_on_last_post":false,"topic_template":null,"suppress_from_homepage":false,"all_topics_wiki":false,"permissions_params":{}}, + {"id":12,"name":"Lounge Import","color":"EEEEEE","created_at":"2017-10-26T17:12:39.490Z","user_id":-1,"slug":"lounge-import","description":"A category exclusive to members with trust level 3 and higher.","text_color":"652D90","auto_close_hours":null,"parent_category_id":null,"auto_close_based_on_last_post":false,"topic_template":null,"suppress_from_homepage":false,"all_topics_wiki":false,"permissions_params":{"trust_level_3":1}}, + {"id":13,"name":"Staff Import","color":"283890","created_at":"2017-10-26T17:12:42.806Z","user_id":2,"slug":"staff-import","description":"Private category for staff discussions. Topics are only visible to admins and moderators.","text_color":"FFFFFF","auto_close_hours":null,"parent_category_id":null,"auto_close_based_on_last_post":false,"topic_template":null,"suppress_from_homepage":false,"all_topics_wiki":false,"permissions_params":{"custom_group_import":1,"staff":1}}, + {"id":15,"name":"Custom Category Import","color":"AB9364","created_at":"2017-10-26T15:32:44.083Z","user_id":2,"slug":"custom-category-import","description":null,"text_color":"FFFFFF","auto_close_hours":null,"parent_category_id":10,"auto_close_based_on_last_post":false,"topic_template":"","suppress_from_homepage":false,"all_topics_wiki":false,"permissions_params":{"custom_group_import":1,"everyone":2}} + ], + "users":[ + {"id":1,"email":"vinothkannan@example.com","username":"example","name":"Example","created_at":"2017-10-07T15:01:24.597Z","trust_level":4,"active":true,"last_emailed_at":null}, + {"id":2,"email":"vinoth.kannan@discourse.org","username":"vinothkannans","name":"Vinoth Kannan","created_at":"2017-10-07T15:01:24.597Z","trust_level":4,"active":true,"last_emailed_at":null} + ], + "topics":[ + {"id":7,"title":"Assets for the site design","created_at":"2017-10-26T17:15:04.590Z","views":0,"category_id":8,"closed":false,"archived":false,"archetype":"regular", + "posts":[ + {"id":10,"user_id":-1,"post_number":1,"raw":"This topic, visible only to staff, is for storing images and files used in the site design.","created_at":"2017-10-26T17:15:04.720Z","reply_to_post_number":null,"hidden":false,"hidden_reason_id":null,"wiki":false} + ] + }, + {"id":6,"title":"Privacy Policy","created_at":"2017-10-26T17:15:04.009Z","views":0,"category_id":15,"closed":false,"archived":false,"archetype":"regular", + "posts":[ + {"id":8,"user_id":-1,"post_number":1,"raw":"[Third party links](#third-party)\n\nOccasionally, at our discretion, we may include or offer third party products or services on our site.","created_at":"2017-10-26T17:15:03.535Z","reply_to_post_number":null,"hidden":false,"hidden_reason_id":null,"wiki":false}, + {"id":7,"user_id":-1,"post_number":2,"raw":"Edit the first post in this topic to change the contents of the FAQ/Guidelines page.","created_at":"2017-10-26T17:15:03.822Z","reply_to_post_number":null,"hidden":false,"hidden_reason_id":null,"wiki":false} + ] + } + ] +} diff --git a/spec/import_export/category_exporter_spec.rb b/spec/import_export/category_exporter_spec.rb new file mode 100644 index 0000000000..5a6420712e --- /dev/null +++ b/spec/import_export/category_exporter_spec.rb @@ -0,0 +1,42 @@ +require "rails_helper" +require "import_export/category_exporter" + +describe ImportExport::CategoryExporter do + + let(:category) { Fabricate(:category) } + let(:group) { Fabricate(:group) } + let(:user) { Fabricate(:user) } + + context '.perform' do + it 'raises an error when the category is not found' do + expect { ImportExport::CategoryExporter.new(100).perform }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'export the category when it is found' do + data = ImportExport::CategoryExporter.new(category.id).perform.export_data + + expect(data[:categories].count).to eq(1) + expect(data[:groups].count).to eq(0) + end + + it 'export the category with permission groups' do + category_group = Fabricate(:category_group, category: category, group: group) + data = ImportExport::CategoryExporter.new(category.id).perform.export_data + + expect(data[:categories].count).to eq(1) + expect(data[:groups].count).to eq(1) + end + + it 'export the category with topics and users' do + topic1 = Fabricate(:topic, category: category, user_id: -1) + topic2 = Fabricate(:topic, category: category, user: user) + data = ImportExport::CategoryExporter.new(category.id).perform.export_data + + expect(data[:categories].count).to eq(1) + expect(data[:groups].count).to eq(0) + expect(data[:topics].count).to eq(2) + expect(data[:users].count).to eq(1) + end + end + +end diff --git a/spec/import_export/category_structure_exporter_spec.rb b/spec/import_export/category_structure_exporter_spec.rb new file mode 100644 index 0000000000..a840bf940c --- /dev/null +++ b/spec/import_export/category_structure_exporter_spec.rb @@ -0,0 +1,39 @@ +require "rails_helper" +require "import_export/category_structure_exporter" + +describe ImportExport::CategoryStructureExporter do + + it 'export all the categories' do + category = Fabricate(:category) + data = ImportExport::CategoryStructureExporter.new.perform.export_data + + expect(data[:categories].count).to eq(2) + expect(data[:groups].count).to eq(0) + expect(data[:users].blank?).to eq(true) + end + + it 'export all the categories with permission groups' do + category = Fabricate(:category) + group = Fabricate(:group) + category_group = Fabricate(:category_group, category: category, group: group) + data = ImportExport::CategoryStructureExporter.new.perform.export_data + + expect(data[:categories].count).to eq(2) + expect(data[:groups].count).to eq(1) + expect(data[:users].blank?).to eq(true) + end + + it 'export all the categories with permission groups and users' do + category = Fabricate(:category) + group = Fabricate(:group) + user = Fabricate(:user) + category_group = Fabricate(:category_group, category: category, group: group) + group_user = Fabricate(:group_user, group: group, user: user) + data = ImportExport::CategoryStructureExporter.new(true).perform.export_data + + expect(data[:categories].count).to eq(2) + expect(data[:groups].count).to eq(1) + expect(data[:users].count).to eq(1) + end + +end diff --git a/spec/import_export/importer_spec.rb b/spec/import_export/importer_spec.rb new file mode 100644 index 0000000000..3d5146b3ed --- /dev/null +++ b/spec/import_export/importer_spec.rb @@ -0,0 +1,68 @@ +require "rails_helper" +require "import_export/category_exporter" +require "import_export/category_structure_exporter" +require "import_export/importer" + +describe ImportExport::Importer do + + let(:import_data) do + import_file = Rack::Test::UploadedFile.new(file_from_fixtures("import-export.json", "json")) + data = ActiveSupport::HashWithIndifferentAccess.new(JSON.parse(import_file.read)) + end + + def import(data) + ImportExport::Importer.new(data).perform + end + + context '.perform' do + + it 'topics and users' do + data = import_data.dup + data[:categories] = nil + data[:groups] = nil + + expect { + import(data) + }.to change { Category.count }.by(0) + .and change { Group.count }.by(0) + .and change { Topic.count }.by(2) + .and change { User.count }.by(2) + end + + it 'categories and groups' do + data = import_data.dup + data[:topics] = nil + data[:users] = nil + + expect { + import(data) + }.to change { Category.count }.by(6) + .and change { Group.count }.by(2) + .and change { Topic.count }.by(6) + .and change { User.count }.by(0) + end + + it 'categories, groups and users' do + data = import_data.dup + data[:topics] = nil + + expect { + import(data) + }.to change { Category.count }.by(6) + .and change { Group.count }.by(2) + .and change { Topic.count }.by(6) + .and change { User.count }.by(2) + end + + it 'all' do + expect { + import(import_data) + }.to change { Category.count }.by(6) + .and change { Group.count }.by(2) + .and change { Topic.count }.by(8) + .and change { User.count }.by(2) + end + + end + +end diff --git a/spec/import_export/topic_exporter_spec.rb b/spec/import_export/topic_exporter_spec.rb new file mode 100644 index 0000000000..47a76f02bd --- /dev/null +++ b/spec/import_export/topic_exporter_spec.rb @@ -0,0 +1,30 @@ +require "rails_helper" +require "import_export/topic_exporter" + +describe ImportExport::TopicExporter do + + let(:user) { Fabricate(:user) } + let(:topic) { Fabricate(:topic, user: user) } + + context '.perform' do + it 'export a single topic' do + data = ImportExport::TopicExporter.new([topic.id]).perform.export_data + + expect(data[:categories].blank?).to eq(true) + expect(data[:groups].blank?).to eq(true) + expect(data[:topics].count).to eq(1) + expect(data[:users].count).to eq(1) + end + + it 'export multiple topics' do + topic2 = Fabricate(:topic, user: user) + data = ImportExport::TopicExporter.new([topic.id, topic2.id]).perform.export_data + + expect(data[:categories].blank?).to eq(true) + expect(data[:groups].blank?).to eq(true) + expect(data[:topics].count).to eq(2) + expect(data[:users].count).to eq(1) + end + end + +end From 7dc36714906be63288cfaf18a76300292125b10d Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 1 Nov 2017 11:41:43 -0400 Subject: [PATCH 039/445] FEATURE: remove obsolete settings ga_tracking_code and ga_domain_name. Use ga_universal_tracking_code and ga_universal_domain_name instead. --- app/helpers/common_helper.rb | 6 ------ app/views/common/_google_analytics.html.erb | 18 ------------------ app/views/layouts/application.html.erb | 2 -- app/views/layouts/crawler.html.erb | 1 - app/views/users/activate_account.html.erb | 2 -- app/views/users/password_reset.html.erb | 2 -- config/locales/server.en.yml | 2 -- config/site_settings.yml | 6 ------ test/javascripts/helpers/site-settings.js | 2 -- 9 files changed, 41 deletions(-) delete mode 100644 app/views/common/_google_analytics.html.erb diff --git a/app/helpers/common_helper.rb b/app/helpers/common_helper.rb index e610736903..1d8317554c 100644 --- a/app/helpers/common_helper.rb +++ b/app/helpers/common_helper.rb @@ -5,12 +5,6 @@ module CommonHelper end end - def render_google_analytics_code - if Rails.env.production? && SiteSetting.ga_tracking_code.present? - render partial: "common/google_analytics" - end - end - def render_google_tag_manager_code if Rails.env.production? && SiteSetting.gtm_container_id.present? render partial: "common/google_tag_manager" diff --git a/app/views/common/_google_analytics.html.erb b/app/views/common/_google_analytics.html.erb deleted file mode 100644 index c8a7f41740..0000000000 --- a/app/views/common/_google_analytics.html.erb +++ /dev/null @@ -1,18 +0,0 @@ - diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index aa81f4b41e..a6e1e70758 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -120,8 +120,6 @@ <%= render :partial => "common/discourse_javascript" %> - <%= render_google_analytics_code %> - <%- unless customization_disabled? %> <%= raw theme_lookup("body_tag") %> <%- end %> diff --git a/app/views/layouts/crawler.html.erb b/app/views/layouts/crawler.html.erb index b9c4ab0c46..ab98695c27 100644 --- a/app/views/layouts/crawler.html.erb +++ b/app/views/layouts/crawler.html.erb @@ -32,7 +32,6 @@

    <%= t 'powered_by_html' %>

    - <%= render_google_analytics_code %> <%= theme_lookup("body_tag") %> <%= yield :after_body %> diff --git a/app/views/users/activate_account.html.erb b/app/views/users/activate_account.html.erb index 44aac1f420..b481df62ae 100644 --- a/app/views/users/activate_account.html.erb +++ b/app/views/users/activate_account.html.erb @@ -40,5 +40,3 @@ }); })(); - -<%= render_google_analytics_code %> diff --git a/app/views/users/password_reset.html.erb b/app/views/users/password_reset.html.erb index db9a3d99e9..3a3c4338bf 100644 --- a/app/views/users/password_reset.html.erb +++ b/app/views/users/password_reset.html.erb @@ -24,5 +24,3 @@ <%- content_for(:head) do %> <%- end %> - -<%= render_google_analytics_code %> diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 75cb0bb7ee..1a51f1cd15 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1058,8 +1058,6 @@ en: must_approve_users: "Staff must approve all new user accounts before they are allowed to access the site. WARNING: enabling this for a live site will revoke access for existing non-staff users!" pending_users_reminder_delay: "Notify moderators if new users have been waiting for approval for longer than this many hours. Set to -1 to disable notifications." maximum_session_age: "User will remain logged in for n hours since last visit" - ga_tracking_code: "OBSOLETE: Google analytics (ga.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" - ga_domain_name: "OBSOLETE: Google analytics (ga.js) domain name, eg: mysite.com; see http://google.com/analytics" ga_universal_tracking_code: "Google Universal Analytics (analytics.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" ga_universal_domain_name: "Google Universal Analytics (analytics.js) domain name, eg: mysite.com; see http://google.com/analytics" ga_universal_auto_link_domains: "Enable Google Universal Analytics (analytics.js) cross-domain tracking. Outgoing links to these domains will have the client id added to them. See Google's Cross-Domain Tracking guide." diff --git a/config/site_settings.yml b/config/site_settings.yml index 1878e2ce41..2428262a1b 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -104,12 +104,6 @@ basic: ga_universal_auto_link_domains: default: '' type: list - ga_tracking_code: - client: true - default: '' - ga_domain_name: - client: true - default: '' gtm_container_id: client: true default: '' diff --git a/test/javascripts/helpers/site-settings.js b/test/javascripts/helpers/site-settings.js index 7f48f99947..90150b3920 100644 --- a/test/javascripts/helpers/site-settings.js +++ b/test/javascripts/helpers/site-settings.js @@ -10,8 +10,6 @@ Discourse.SiteSettingsOriginal = { "track_external_right_clicks":false, "ga_universal_tracking_code":"", "ga_universal_domain_name":"auto", - "ga_tracking_code":"UA-33736483-2", - "ga_domain_name":"", "top_menu":"latest|new|unread|categories|top", "post_menu":"like-count|like|share|flag|edit|bookmark|delete|admin|reply", "post_menu_hidden_items":"flag|edit|delete|admin", From f57d3c2315d3ddbca26707bdbd28b97aa2074458 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 1 Nov 2017 12:43:05 -0400 Subject: [PATCH 040/445] REFACTOR: Groups navigation to a component --- .../components/group-navigation.js.es6 | 15 ++++++++ .../discourse/controllers/group.js.es6 | 36 +++++-------------- .../templates/components/group-navigation.hbs | 11 ++++++ .../javascripts/discourse/templates/group.hbs | 14 ++------ 4 files changed, 37 insertions(+), 39 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/group-navigation.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/group-navigation.hbs diff --git a/app/assets/javascripts/discourse/components/group-navigation.js.es6 b/app/assets/javascripts/discourse/components/group-navigation.js.es6 new file mode 100644 index 0000000000..35f5f238e4 --- /dev/null +++ b/app/assets/javascripts/discourse/components/group-navigation.js.es6 @@ -0,0 +1,15 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + tagName: '', + + @computed('group') + availableTabs(group) { + return this.get('tabs').filter(t => { + if (t.admin) { + return this.currentUser ? this.currentUser.canManageGroup(group) : false; + } + return true; + }); + } +}); diff --git a/app/assets/javascripts/discourse/controllers/group.js.es6 b/app/assets/javascripts/discourse/controllers/group.js.es6 index 4734ac8db4..98e4687f89 100644 --- a/app/assets/javascripts/discourse/controllers/group.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group.js.es6 @@ -1,14 +1,11 @@ import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; -var Tab = Em.Object.extend({ - @computed('name') - location(name) { - return 'group.' + name; - }, - - @computed('name', 'i18nKey') - message(name, i18nKey) { - return I18n.t(`groups.${i18nKey || name}`); +const Tab = Ember.Object.extend({ + init() { + this._super(); + let name = this.get('name'); + this.set('route', this.get('route') || `group.` + name); + this.set('message', I18n.t(`groups.${this.get('i18nKey') || name}`)); } }); @@ -18,13 +15,13 @@ export default Ember.Controller.extend({ showing: 'members', tabs: [ - Tab.create({ name: 'members', 'location': 'group.index', icon: 'users' }), + Tab.create({ name: 'members', route: 'group.index', icon: 'users' }), Tab.create({ name: 'activity' }), Tab.create({ - name: 'edit', i18nKey: 'edit.title', icon: 'pencil', requiresGroupAdmin: true + name: 'edit', i18nKey: 'edit.title', icon: 'pencil', admin: true }), Tab.create({ - name: 'logs', i18nKey: 'logs.title', icon: 'list-alt', requiresGroupAdmin: true + name: 'logs', i18nKey: 'logs.title', icon: 'list-alt', admin: true }) ], @@ -58,21 +55,6 @@ export default Ember.Controller.extend({ this.get('tabs')[0].set('count', this.get('model.user_count')); }, - @computed('model.is_group_owner', 'model.automatic') - getTabs() { - return this.get('tabs').filter(t => { - let canSee = true; - - if (this.currentUser && t.requiresGroupAdmin) { - canSee = this.currentUser.canManageGroup(this.get('model')); - } else if (t.requiresGroupAdmin) { - canSee = false; - } - - return canSee; - }); - }, - actions: { messageGroup() { this.send('createNewMessageViaParams', this.get('model.name')); diff --git a/app/assets/javascripts/discourse/templates/components/group-navigation.hbs b/app/assets/javascripts/discourse/templates/components/group-navigation.hbs new file mode 100644 index 0000000000..8501e9e2f5 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/group-navigation.hbs @@ -0,0 +1,11 @@ +{{#mobile-nav class='group-nav' desktopClass="nav nav-pills" currentPath=currentPath}} + {{#each availableTabs as |tab|}} +
  • + {{#link-to tab.route group title=tab.message class=tab.name}} + {{#if tab.icon}}{{d-icon tab.icon}}{{/if}} + {{tab.message}} + {{#if tab.count}}({{tab.count}}){{/if}} + {{/link-to}} +
  • + {{/each}} +{{/mobile-nav}} diff --git a/app/assets/javascripts/discourse/templates/group.hbs b/app/assets/javascripts/discourse/templates/group.hbs index 777c0ea5c2..b6076c69e0 100644 --- a/app/assets/javascripts/discourse/templates/group.hbs +++ b/app/assets/javascripts/discourse/templates/group.hbs @@ -21,7 +21,7 @@
    {{#if model.bio_cooked}} -
    +

    {{{model.bio_cooked}}}

    @@ -31,17 +31,7 @@
    - {{#mobile-nav class='group-nav' desktopClass="nav nav-pills" currentPath=application.currentPath}} - {{#each getTabs as |tab|}} -
  • - {{#link-to tab.location model title=tab.message class=tab.name}} - {{#if tab.icon}}{{d-icon tab.icon}}{{/if}} - {{tab.message}} - {{#if tab.count}}({{tab.count}}){{/if}} - {{/link-to}} -
  • - {{/each}} - {{/mobile-nav}} + {{group-navigation group=model currentPath=application.currentPath tabs=tabs}} {{#if displayGroupMessageButton}} {{d-button From 5849bae9dfab90e2a49327d972dcc485dfa3cda9 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 1 Nov 2017 22:19:13 +0530 Subject: [PATCH 041/445] FIX: rescue error when importing category structure --- lib/import_export/importer.rb | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/lib/import_export/importer.rb b/lib/import_export/importer.rb index 9bc43ae506..2e982a089e 100644 --- a/lib/import_export/importer.rb +++ b/lib/import_export/importer.rb @@ -76,23 +76,27 @@ module ImportExport @categories.sort_by! { |c| c[:parent_category_id].presence || 0 } @categories.each do |cat_attrs| - id = cat_attrs.delete(:id) - permissions = cat_attrs.delete(:permissions_params) + begin + id = cat_attrs.delete(:id) + permissions = cat_attrs.delete(:permissions_params) - category = Category.new(cat_attrs) - category.parent_category_id = new_category_id(cat_attrs[:parent_category_id]) if cat_attrs[:parent_category_id].present? - category.user_id = new_user_id(cat_attrs[:user_id]) - import_id = "#{id}#{import_source}" - category.custom_fields["import_id"] = import_id - category.permissions = permissions.present? ? permissions : { "everyone" => CategoryGroup.permission_types[:full] } - category.save! - existing_categories << { category_id: category.id, value: import_id } + category = Category.new(cat_attrs) + category.parent_category_id = new_category_id(cat_attrs[:parent_category_id]) if cat_attrs[:parent_category_id].present? + category.user_id = new_user_id(cat_attrs[:user_id]) + import_id = "#{id}#{import_source}" + category.custom_fields["import_id"] = import_id + category.permissions = permissions.present? ? permissions : { "everyone" => CategoryGroup.permission_types[:full] } + category.save! + existing_categories << { category_id: category.id, value: import_id } - if cat_attrs[:description].present? - post = category.topic.ordered_posts.first - post.raw = cat_attrs[:description] - post.save! - post.rebake! + if cat_attrs[:description].present? + post = category.topic.ordered_posts.first + post.raw = cat_attrs[:description] + post.save! + post.rebake! + end + rescue + next end end From fdb342f0f0d6c9d5bc15e5f80cc6fe7dcc6fb240 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 1 Nov 2017 14:25:10 -0400 Subject: [PATCH 042/445] Add a plugin outlet before the group content --- app/assets/javascripts/discourse/templates/group.hbs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/discourse/templates/group.hbs b/app/assets/javascripts/discourse/templates/group.hbs index b6076c69e0..909c3f6e15 100644 --- a/app/assets/javascripts/discourse/templates/group.hbs +++ b/app/assets/javascripts/discourse/templates/group.hbs @@ -1,3 +1,5 @@ +{{plugin-outlet name="before-group-container" args=(hash group=model)}} +
    From 91438849d2b7bfd527d8a1b0afbbf82b098f265a Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 2 Nov 2017 00:51:21 +0530 Subject: [PATCH 043/445] bump onebox version --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index ba44402da7..9462490a73 100644 --- a/Gemfile +++ b/Gemfile @@ -35,7 +35,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.20' +gem 'onebox', '1.8.21' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index a6a376a17e..046407fc5d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -228,7 +228,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.20) + onebox (1.8.21) fast_blank (>= 1.0.0) htmlentities (~> 4.3) moneta (~> 1.0) @@ -464,7 +464,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.20) + onebox (= 1.8.21) openid-redis-store pg pry-nav From 0a69f2bc7741a2da39233bc8dac3c419fdf22030 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 1 Nov 2017 15:08:18 -0400 Subject: [PATCH 044/445] REFACTOR: Remove many `Discourse.Category.list()` calls --- .../javascripts/discourse/components/d-navigation.js.es6 | 5 ++++- .../discourse/components/edit-category-general.js.es6 | 8 +++----- .../javascripts/discourse/controllers/composer.js.es6 | 7 ++----- .../discourse/controllers/edit-category.js.es6 | 2 +- .../javascripts/discourse/controllers/tags-show.js.es6 | 6 ++---- .../javascripts/discourse/controllers/topic.js.es6 | 4 +--- app/assets/javascripts/discourse/models/category.js.es6 | 4 +--- app/assets/javascripts/discourse/models/composer.js.es6 | 4 ++-- app/assets/javascripts/discourse/models/nav-item.js.es6 | 1 + app/assets/javascripts/discourse/models/site.js.es6 | 8 ++++++++ app/assets/javascripts/discourse/models/topic.js.es6 | 2 +- .../pre-initializers/inject-discourse-objects.js.es6 | 8 ++++---- .../discourse/templates/components/d-navigation.hbs | 2 +- .../javascripts/discourse/widgets/hamburger-menu.js.es6 | 2 +- test/javascripts/helpers/qunit-helpers.js.es6 | 9 +++------ test/javascripts/test_helper.js | 7 ++++--- 16 files changed, 39 insertions(+), 40 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-navigation.js.es6 b/app/assets/javascripts/discourse/components/d-navigation.js.es6 index f2467ddcf6..9cfcad99bc 100644 --- a/app/assets/javascripts/discourse/components/d-navigation.js.es6 +++ b/app/assets/javascripts/discourse/components/d-navigation.js.es6 @@ -10,9 +10,12 @@ export default Ember.Component.extend({ @computed() categories() { - return Discourse.Category.list(); + return this.site.get('categoriesList'); }, + @computed('category.can_edit') + showCategoryEdit: canEdit => canEdit, + @computed("filterMode") navItems(filterMode) { // we don't want to show the period in the navigation bar since it's in a dropdown diff --git a/app/assets/javascripts/discourse/components/edit-category-general.js.es6 b/app/assets/javascripts/discourse/components/edit-category-general.js.es6 index e26c24a21e..693a48918a 100644 --- a/app/assets/javascripts/discourse/components/edit-category-general.js.es6 +++ b/app/assets/javascripts/discourse/components/edit-category-general.js.es6 @@ -9,13 +9,13 @@ export default buildCategoryPanel('general', { // background colors are available as a pipe-separated string backgroundColors: function() { - const categories = Discourse.Category.list(); + const categories = this.site.get('categoriesList'); return this.siteSettings.category_colors.split("|").map(function(i) { return i.toUpperCase(); }).concat( categories.map(function(c) { return c.color.toUpperCase(); }) ).uniq(); }.property(), usedBackgroundColors: function() { - const categories = Discourse.Category.list(); + const categories = this.site.get('categoriesList'); const category = this.get('category'); // If editing a category, don't include its color: @@ -25,9 +25,7 @@ export default buildCategoryPanel('general', { }.property('category.id', 'category.color'), parentCategories: function() { - return Discourse.Category.list().filter(function (c) { - return !c.get('parentCategory'); - }); + return this.site.get('categoriesList').filter(c => !c.get('parentCategory')); }.property(), categoryBadgePreview: function() { diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 61f381a239..506bee7e02 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -69,6 +69,7 @@ export default Ember.Controller.extend({ topic: null, linkLookup: null, whisperOrUnlistTopic: Ember.computed.or('model.whisper', 'model.unlistTopic'), + categories: Ember.computed.alias('site.categoriesList'), @computed('model.replyingToTopic', 'model.creatingPrivateMessage', 'model.targetUsernames') focusTarget(replyingToTopic, creatingPM, usernames) { @@ -396,10 +397,6 @@ export default Ember.Controller.extend({ }, - categories: function() { - return Discourse.Category.list(); - }.property(), - disableSubmit: Ember.computed.or("model.loading", "isUploading"), save(force) { @@ -654,7 +651,7 @@ export default Ember.Controller.extend({ if (!splitCategory[1]) { category = this.site.get('categories').findBy('nameLower', splitCategory[0].toLowerCase()); } else { - const categories = Discourse.Category.list(); + const categories = this.site.get('categories'); const mainCategory = categories.findBy('nameLower', splitCategory[0].toLowerCase()); category = categories.find(function(item) { return item && item.get('nameLower') === splitCategory[1].toLowerCase() && item.get('parent_category_id') === mainCategory.id; diff --git a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 index e05c37937b..2af5d3593d 100644 --- a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 @@ -65,7 +65,7 @@ export default Ember.Controller.extend(ModalFunctionality, { saveCategory() { const self = this, model = this.get('model'), - parentCategory = Discourse.Category.list().findBy('id', parseInt(model.get('parent_category_id'), 10)); + parentCategory = this.site.get('categories').findBy('id', parseInt(model.get('parent_category_id'), 10)); this.set('saving', true); model.set('parentCategory', parentCategory); diff --git a/app/assets/javascripts/discourse/controllers/tags-show.js.es6 b/app/assets/javascripts/discourse/controllers/tags-show.js.es6 index 467750926f..4658b09f38 100644 --- a/app/assets/javascripts/discourse/controllers/tags-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/tags-show.js.es6 @@ -58,6 +58,8 @@ export default Ember.Controller.extend(BulkTopicSelection, { max_posts: null, q: null, + categories: Ember.computed.alias('site.categoriesList'), + queryParams: ['order', 'ascending', 'status', 'state', 'search', 'max_posts', 'q'], navItems: function() { @@ -68,10 +70,6 @@ export default Ember.Controller.extend(BulkTopicSelection, { return Discourse.SiteSettings.show_filter_by_tag; }.property('category'), - categories: function() { - return Discourse.Category.list(); - }.property(), - showAdminControls: function() { return !this.get('additionalTags') && this.get('canAdminTag') && !this.get('category'); }.property('additionalTags', 'canAdminTag', 'category'), diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index fe2a3f4fb0..c609981c5c 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -752,9 +752,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { return selectedPostsUsername !== undefined; }, - categories: function() { - return Discourse.Category.list(); - }.property(), + categories: Ember.computed.alias('site.categoriesList'), canSelectAll: Em.computed.not('allPostsSelected'), diff --git a/app/assets/javascripts/discourse/models/category.js.es6 b/app/assets/javascripts/discourse/models/category.js.es6 index 16304f453e..6af4ff8750 100644 --- a/app/assets/javascripts/discourse/models/category.js.es6 +++ b/app/assets/javascripts/discourse/models/category.js.es6 @@ -205,9 +205,7 @@ Category.reopenClass({ }, list() { - return Discourse.SiteSettings.fixed_category_positions ? - Discourse.Site.currentProp('categories') : - Discourse.Site.currentProp('sortedCategories'); + return Discourse.Site.currentProp('categoriesList'); }, listByActivity() { diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 48e8e26196..6094ae285d 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -85,7 +85,7 @@ const Composer = RestModel.extend({ @computed("privateMessage", "archetype.hasOptions") showCategoryChooser(isPrivateMessage, hasOptions) { - const manyCategories = Discourse.Category.list().length > 1; + const manyCategories = this.site.get('categories').length > 1; return !isPrivateMessage && (hasOptions || manyCategories); }, @@ -481,7 +481,7 @@ const Composer = RestModel.extend({ this.set('categoryId', opts.categoryId || this.get('topic.category.id')); if (!this.get('categoryId') && this.get('creatingTopic')) { - const categories = Discourse.Category.list(); + const categories = this.site.get('categories'); if (categories.length === 1) { this.set('categoryId', categories[0].get('id')); } diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6 index f71842cd6e..7616644968 100644 --- a/app/assets/javascripts/discourse/models/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/models/nav-item.js.es6 @@ -111,6 +111,7 @@ NavItem.reopenClass({ opts = opts || {}; if (anonymous && !Discourse.Site.currentProp('anonymous_top_menu_items').includes(testName)) return null; + if (!Discourse.Category.list() && testName === "categories") return null; if (!Discourse.Site.currentProp('top_menu_items').includes(testName)) return null; diff --git a/app/assets/javascripts/discourse/models/site.js.es6 b/app/assets/javascripts/discourse/models/site.js.es6 index ace9f7176c..073f6b43f9 100644 --- a/app/assets/javascripts/discourse/models/site.js.es6 +++ b/app/assets/javascripts/discourse/models/site.js.es6 @@ -54,6 +54,14 @@ const Site = RestModel.extend({ return result; }, + // Returns it in the correct order, by setting + @computed + categoriesList() { + return this.siteSettings.fixed_category_positions ? + this.get('categories') : + this.get('sortedCategories'); + }, + postActionTypeById(id) { return this.get("postActionByIdLookup.action" + id); }, diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index 3727290485..df6fd563cc 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -135,7 +135,7 @@ const Topic = RestModel.extend({ const categoryName = this.get('categoryName'); let category; if (categoryName) { - category = Discourse.Category.list().findBy('name', categoryName); + category = this.site.get('categories').findBy('name', categoryName); } this.set('category', category); }.observes('categoryName'), diff --git a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 index ca60c3c311..453ee0612b 100644 --- a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 +++ b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 @@ -36,14 +36,14 @@ export default { app.register('topic-tracking-state:main', topicTrackingState, { instantiate: false }); ALL_TARGETS.forEach(t => app.inject(t, 'topicTrackingState', 'topic-tracking-state:main')); - const site = Discourse.Site.current(); - app.register('site:main', site, { instantiate: false }); - ALL_TARGETS.forEach(t => app.inject(t, 'site', 'site:main')); - const siteSettings = Discourse.SiteSettings; app.register('site-settings:main', siteSettings, { instantiate: false }); ALL_TARGETS.forEach(t => app.inject(t, 'siteSettings', 'site-settings:main')); + const site = Discourse.Site.current(); + app.register('site:main', site, { instantiate: false }); + ALL_TARGETS.forEach(t => app.inject(t, 'site', 'site:main')); + app.register('search-service:main', SearchService); ALL_TARGETS.forEach(t => app.inject(t, 'searchService', 'search-service:main')); diff --git a/app/assets/javascripts/discourse/templates/components/d-navigation.hbs b/app/assets/javascripts/discourse/templates/components/d-navigation.hbs index 8297a4f7e1..dd0fe03f3e 100644 --- a/app/assets/javascripts/discourse/templates/components/d-navigation.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-navigation.hbs @@ -17,7 +17,7 @@ action=createTopic disabled=createTopicDisabled}} -{{#if category.can_edit}} +{{#if showCategoryEdit}} {{d-button class="btn-default edit-category" action=editCategory diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index 2058a4b882..c9f40de4a2 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -130,7 +130,7 @@ export default createWidget('hamburger-menu', { const hideUncategorized = !this.siteSettings.allow_uncategorized_topics; const isStaff = Discourse.User.currentProp('staff'); - const categories = Discourse.Category.list().reject((c) => { + const categories = this.site.get('categoriesList').reject((c) => { if (c.get('parentCategory.show_subcategory_list')) { return true; } if (hideUncategorized && c.get('isUncategorizedCategory') && !isStaff) { return true; } return false; diff --git a/test/javascripts/helpers/qunit-helpers.js.es6 b/test/javascripts/helpers/qunit-helpers.js.es6 index f6a3a3848f..5a7c334704 100644 --- a/test/javascripts/helpers/qunit-helpers.js.es6 +++ b/test/javascripts/helpers/qunit-helpers.js.es6 @@ -1,7 +1,6 @@ -/* global QUnit, fixtures */ +/* global QUnit, resetSite */ import sessionFixtures from 'fixtures/session-fixtures'; -import siteFixtures from 'fixtures/site-fixtures'; import HeaderComponent from 'discourse/components/site-header'; import { forceMobile, resetMobile } from 'discourse/lib/mobile'; import { resetPluginApi } from 'discourse/lib/plugin-api'; @@ -60,7 +59,6 @@ export function acceptance(name, options) { HeaderComponent.reopen({examineDockHeader: function() { }}); resetExtraClasses(); - const siteJson = siteFixtures['site.json'].site; if (options.beforeEach) { options.beforeEach.call(this); } @@ -78,7 +76,7 @@ export function acceptance(name, options) { } if (options.site) { - Discourse.Site.resetCurrent(Discourse.Site.create(jQuery.extend(true, {}, siteJson, options.site))); + resetSite(Discourse.SiteSettings, options.site); } clearOutletCache(); @@ -93,8 +91,7 @@ export function acceptance(name, options) { } flushMap(); Discourse.User.resetCurrent(); - Discourse.Site.resetCurrent(Discourse.Site.create(jQuery.extend(true, {}, fixtures['site.json'].site))); - + resetSite(Discourse.SiteSettings); resetExtraClasses(); clearOutletCache(); clearHTMLCache(); diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index edc1191169..97f759b0dc 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -81,10 +81,11 @@ function dup(obj) { return jQuery.extend(true, {}, obj); } -function resetSite() { +function resetSite(siteSettings, extras) { var createStore = require('helpers/create-store').default; - var siteAttrs = dup(fixtures['site.json'].site); + var siteAttrs = $.extend({}, fixtures['site.json'].site, extras || {}); siteAttrs.store = createStore(); + siteAttrs.siteSettings = siteSettings; Discourse.Site.resetCurrent(Discourse.Site.create(siteAttrs)); } @@ -105,7 +106,7 @@ QUnit.testStart(function(ctx) { Discourse.BaseUrl = "localhost"; Discourse.Session.resetCurrent(); Discourse.User.resetCurrent(); - resetSite(); + resetSite(Discourse.SiteSettings); _DiscourseURL.redirectedTo = null; _DiscourseURL.redirectTo = function(url) { From a931d7ba5661f75d6df0a1ce0fa3800d36ee1d3a Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 1 Nov 2017 16:05:00 -0400 Subject: [PATCH 045/445] UX: Add new targetable class in staff-action-logs --- .../javascripts/admin/templates/logs/staff-action-logs.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs index c44edc1a3e..5df150c79f 100644 --- a/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs +++ b/app/assets/javascripts/admin/templates/logs/staff-action-logs.hbs @@ -1,6 +1,6 @@
    {{#if filtersExists}} -
    +
    {{i18n 'admin.logs.staff_actions.clear_filters'}} From 46b0c018237e25bcc84cf3a25c85e3055439967c Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 1 Nov 2017 16:28:23 -0400 Subject: [PATCH 046/445] FIX: Subcategory filters were not respecting the current category --- .../javascripts/discourse/components/d-navigation.js.es6 | 6 +++--- .../discourse/controllers/navigation/category.js.es6 | 5 ----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-navigation.js.es6 b/app/assets/javascripts/discourse/components/d-navigation.js.es6 index 9cfcad99bc..ff1e6ef40a 100644 --- a/app/assets/javascripts/discourse/components/d-navigation.js.es6 +++ b/app/assets/javascripts/discourse/components/d-navigation.js.es6 @@ -16,10 +16,10 @@ export default Ember.Component.extend({ @computed('category.can_edit') showCategoryEdit: canEdit => canEdit, - @computed("filterMode") - navItems(filterMode) { + @computed("filterMode", "category", 'noSubcategories') + navItems(filterMode, category, noSubcategories) { // we don't want to show the period in the navigation bar since it's in a dropdown if (filterMode.indexOf("top/") === 0) { filterMode = filterMode.replace("top/", ""); } - return Discourse.NavItem.buildList(null, { filterMode }); + return Discourse.NavItem.buildList(category, { filterMode, noSubcategories }); } }); diff --git a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 index 143812960b..5609131075 100644 --- a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 @@ -4,9 +4,4 @@ import NavigationDefaultController from 'discourse/controllers/navigation/defaul export default NavigationDefaultController.extend({ showingParentCategory: Em.computed.none('category.parentCategory'), showingSubcategoryList: Em.computed.and('category.show_subcategory_list', 'showingParentCategory'), - - @computed("showingSubcategoryList", "category", "noSubcategories") - navItems(showingSubcategoryList, category, noSubcategories) { - return Discourse.NavItem.buildList(category, { noSubcategories }); - } }); From 44eeb20c50873843ed939a7654713861e2566aac Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 2 Nov 2017 08:06:45 +0800 Subject: [PATCH 047/445] Make eslint happy. --- .../javascripts/discourse/controllers/navigation/category.js.es6 | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 index 5609131075..0d757e4bce 100644 --- a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 @@ -1,4 +1,3 @@ -import computed from "ember-addons/ember-computed-decorators"; import NavigationDefaultController from 'discourse/controllers/navigation/default'; export default NavigationDefaultController.extend({ From ab2a5cef389acf4cb3aabc004ca0277ca96eacda Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 2 Nov 2017 08:51:43 +0800 Subject: [PATCH 048/445] FIX: Can't edit membership request template on group page. --- app/controllers/groups_controller.rb | 3 ++- spec/requests/groups_controller_spec.rb | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index bd9f079547..324847f5bc 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -365,7 +365,8 @@ class GroupsController < ApplicationController :full_name, :public_admission, :public_exit, - :allow_membership_requests + :allow_membership_requests, + :membership_request_template, ) end diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb index 5e4f729a24..003b348f53 100644 --- a/spec/requests/groups_controller_spec.rb +++ b/spec/requests/groups_controller_spec.rb @@ -112,10 +112,11 @@ describe GroupsController do full_name: 'awesome team', public_admission: true, public_exit: true, - allow_membership_requests: true + allow_membership_requests: true, + membership_request_template: 'testing', } } - end.to change { GroupHistory.count }.by(8) + end.to change { GroupHistory.count }.by(9) expect(response).to be_success @@ -129,7 +130,8 @@ describe GroupsController do expect(group.public_admission).to eq(true) expect(group.public_exit).to eq(true) expect(group.allow_membership_requests).to eq(true) - expect(GroupHistory.last.subject).to eq('allow_membership_requests') + expect(group.membership_request_template).to eq('testing') + expect(GroupHistory.last.subject).to eq('membership_request_template') end end From edf4af608ea7e7aa1a486aa5f78582feb877cc32 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 2 Nov 2017 10:20:14 +0800 Subject: [PATCH 049/445] FIX: Better match when searching for groups. --- app/controllers/users_controller.rb | 18 ++++++------------ app/models/group.rb | 12 ++++++++---- spec/models/group_spec.rb | 18 +++++++++++------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index be74190f80..bec832bdbd 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -710,24 +710,18 @@ class UsersController < ApplicationController to_render = { users: results.as_json(only: user_fields, methods: [:avatar_template]) } - if params[:include_groups] == "true" - to_render[:groups] = Group.search_group(term).map do |m| - { name: m.name, full_name: m.full_name } - end - end - - if current_user - groups = + groups = + if current_user if params[:include_mentionable_groups] == 'true' Group.mentionable(current_user) elsif params[:include_messageable_groups] == 'true' Group.messageable(current_user) end - - if groups - to_render[:groups] = groups.where("name ILIKE :term_like", term_like: "#{term}%") - .map { |m| { name: m.name, full_name: m.full_name } } end + + if groups || params[:include_groups] == "true" + to_render[:groups] = Group.search_groups(term, groups: groups) + .map { |m| { name: m.name, full_name: m.full_name } } end render json: to_render diff --git a/app/models/group.rb b/app/models/group.rb index bf2c2147b6..226b0555b0 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -369,10 +369,14 @@ class Group < ActiveRecord::Base lookup_group(name) || refresh_automatic_group!(name) end - def self.search_group(name) - Group.where(visibility_level: visibility_levels[:public]).where( - "name ILIKE :term_like OR full_name ILIKE :term_like", term_like: "#{name}%" - ) + def self.search_groups(name, groups: nil) + query = groups || Group + + query + .where(visibility_level: visibility_levels[:public]) + .where( + "name ILIKE :term_like OR full_name ILIKE :term_like", term_like: "%#{name}%" + ) end def self.lookup_group(name) diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 3920a7da43..fbdb7c9505 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -556,17 +556,21 @@ describe Group do end end - describe '.search_group' do - let(:group) { Fabricate(:group, name: 'tEsT', full_name: 'eSTt') } + describe '.search_groups' do + let(:group) { Fabricate(:group, name: 'tEsT_more_things', full_name: 'Abc something awesome') } it 'should return the right groups' do group - expect(Group.search_group('te')).to eq([group]) - expect(Group.search_group('TE')).to eq([group]) - expect(Group.search_group('es')).to eq([group]) - expect(Group.search_group('ES')).to eq([group]) - expect(Group.search_group('test2')).to eq([]) + expect(Group.search_groups('te')).to eq([group]) + expect(Group.search_groups('TE')).to eq([group]) + expect(Group.search_groups('es')).to eq([group]) + expect(Group.search_groups('ES')).to eq([group]) + expect(Group.search_groups('ngs')).to eq([group]) + expect(Group.search_groups('sOmEthi')).to eq([group]) + expect(Group.search_groups('abc')).to eq([group]) + expect(Group.search_groups('sOmEthi')).to eq([group]) + expect(Group.search_groups('test2')).to eq([]) end end From 09cee4c3159ce8b2595f2217e880eee45477b54a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 2 Nov 2017 14:40:18 +0800 Subject: [PATCH 050/445] Don't add time to logs when using logstash formatter. --- config/initializers/100-lograge.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/config/initializers/100-lograge.rb b/config/initializers/100-lograge.rb index 304fde3ced..f7d199a9e8 100644 --- a/config/initializers/100-lograge.rb +++ b/config/initializers/100-lograge.rb @@ -4,6 +4,8 @@ if (Rails.env.production? && SiteSetting.logging_provider == 'lograge') || ENV[" Rails.application.configure do config.lograge.enabled = true + logstash_formatter = ENV["LOGSTASH_FORMATTER"] + config.lograge.custom_options = lambda do |event| exceptions = %w(controller action format id) @@ -13,11 +15,13 @@ if (Rails.env.production? && SiteSetting.logging_provider == 'lograge') || ENV[" output = { params: params, database: RailsMultisite::ConnectionManagement.current_db, - time: event.time, } + + output[:time] = event.time unless logstash_formatter + output end - if ENV["LOGSTASH_FORMATTER"] + if logstash_formatter config.lograge.formatter = Lograge::Formatters::Logstash.new end end From 4634935fe6e4bf7d91df8709e2ed87e2d5fd9ad5 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 2 Nov 2017 15:03:36 +0530 Subject: [PATCH 051/445] DEV: suppress puts output while running specs --- spec/import_export/category_exporter_spec.rb | 4 ++++ spec/import_export/category_structure_exporter_spec.rb | 4 ++++ spec/import_export/importer_spec.rb | 4 ++++ spec/import_export/topic_exporter_spec.rb | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/spec/import_export/category_exporter_spec.rb b/spec/import_export/category_exporter_spec.rb index 5a6420712e..3c22617570 100644 --- a/spec/import_export/category_exporter_spec.rb +++ b/spec/import_export/category_exporter_spec.rb @@ -7,6 +7,10 @@ describe ImportExport::CategoryExporter do let(:group) { Fabricate(:group) } let(:user) { Fabricate(:user) } + before do + STDOUT.stubs(:write) + end + context '.perform' do it 'raises an error when the category is not found' do expect { ImportExport::CategoryExporter.new(100).perform }.to raise_error(ActiveRecord::RecordNotFound) diff --git a/spec/import_export/category_structure_exporter_spec.rb b/spec/import_export/category_structure_exporter_spec.rb index a840bf940c..cb46123d07 100644 --- a/spec/import_export/category_structure_exporter_spec.rb +++ b/spec/import_export/category_structure_exporter_spec.rb @@ -3,6 +3,10 @@ require "import_export/category_structure_exporter" describe ImportExport::CategoryStructureExporter do + before do + STDOUT.stubs(:write) + end + it 'export all the categories' do category = Fabricate(:category) data = ImportExport::CategoryStructureExporter.new.perform.export_data diff --git a/spec/import_export/importer_spec.rb b/spec/import_export/importer_spec.rb index 3d5146b3ed..ae212046d2 100644 --- a/spec/import_export/importer_spec.rb +++ b/spec/import_export/importer_spec.rb @@ -5,6 +5,10 @@ require "import_export/importer" describe ImportExport::Importer do + before do + STDOUT.stubs(:write) + end + let(:import_data) do import_file = Rack::Test::UploadedFile.new(file_from_fixtures("import-export.json", "json")) data = ActiveSupport::HashWithIndifferentAccess.new(JSON.parse(import_file.read)) diff --git a/spec/import_export/topic_exporter_spec.rb b/spec/import_export/topic_exporter_spec.rb index 47a76f02bd..db767e4da5 100644 --- a/spec/import_export/topic_exporter_spec.rb +++ b/spec/import_export/topic_exporter_spec.rb @@ -3,6 +3,10 @@ require "import_export/topic_exporter" describe ImportExport::TopicExporter do + before do + STDOUT.stubs(:write) + end + let(:user) { Fabricate(:user) } let(:topic) { Fabricate(:topic, user: user) } From 54e4ff34f8d84150cd9405f33f1bab45850e59e4 Mon Sep 17 00:00:00 2001 From: Viktor Benei Date: Thu, 2 Nov 2017 12:33:35 +0100 Subject: [PATCH 052/445] Fix "duplicate method" issue Fixing http://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Lint/DuplicateMethods Readers are defined (https://github.com/discourse/discourse/blob/master/lib/single_sign_on.rb#L61), so only writers have to be generated. --- lib/single_sign_on.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/single_sign_on.rb b/lib/single_sign_on.rb index ef5a47d7b1..ba8a903594 100644 --- a/lib/single_sign_on.rb +++ b/lib/single_sign_on.rb @@ -8,7 +8,7 @@ class SingleSignOn NONCE_EXPIRY_TIME = 10.minutes attr_accessor(*ACCESSORS) - attr_accessor :sso_secret, :sso_url + attr_writer :sso_secret, :sso_url def self.sso_secret raise RuntimeError, "sso_secret not implemented on class, be sure to set it on instance" From d85ac97dc60f45c1a21c8e94b4dec2724ce52547 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 2 Nov 2017 17:11:25 +0100 Subject: [PATCH 053/445] FIX: clicking on button label didn't close popup --- .../javascripts/discourse/components/popup-menu.js.es6 | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/components/popup-menu.js.es6 b/app/assets/javascripts/discourse/components/popup-menu.js.es6 index 4f8de9b592..64ef71752d 100644 --- a/app/assets/javascripts/discourse/components/popup-menu.js.es6 +++ b/app/assets/javascripts/discourse/components/popup-menu.js.es6 @@ -11,11 +11,8 @@ export default Ember.Component.extend({ this.sendAction('hide'); }); - $('html').on(`mouseup.popup-menu-${this.get('elementId')}`, (e) => { - const $target = $(e.target); - if ($target.is("button") || this.$().has($target).length === 0) { - this.sendAction('hide'); - } + $('html').on(`mouseup.popup-menu-${this.get('elementId')}`, () => { + this.sendAction('hide'); }); }, From 5a55ce65f32f4940ce61614adb421f641836d456 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 2 Nov 2017 12:49:52 -0400 Subject: [PATCH 054/445] UX: Hide category badge colors if the style is none --- .../components/edit-category-general.js.es6 | 61 ++++++++++++------- .../components/edit-category-general.hbs | 34 ++++++----- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/app/assets/javascripts/discourse/components/edit-category-general.js.es6 b/app/assets/javascripts/discourse/components/edit-category-general.js.es6 index 693a48918a..906acf2ae8 100644 --- a/app/assets/javascripts/discourse/components/edit-category-general.js.es6 +++ b/app/assets/javascripts/discourse/components/edit-category-general.js.es6 @@ -2,54 +2,69 @@ import DiscourseURL from 'discourse/lib/url'; import { buildCategoryPanel } from 'discourse/components/edit-category-panel'; import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import Category from 'discourse/models/category'; +import computed from 'ember-addons/ember-computed-decorators'; export default buildCategoryPanel('general', { foregroundColors: ['FFFFFF', '000000'], canSelectParentCategory: Em.computed.not('category.isUncategorizedCategory'), // background colors are available as a pipe-separated string - backgroundColors: function() { + @computed + backgroundColors() { const categories = this.site.get('categoriesList'); return this.siteSettings.category_colors.split("|").map(function(i) { return i.toUpperCase(); }).concat( categories.map(function(c) { return c.color.toUpperCase(); }) ).uniq(); - }.property(), + }, - usedBackgroundColors: function() { + @computed + noCategoryStyle() { + return this.siteSettings.category_style === 'none'; + }, + + @computed('category.id', 'category.color') + usedBackgroundColors(categoryId, categoryColor) { const categories = this.site.get('categoriesList'); - const category = this.get('category'); // If editing a category, don't include its color: return categories.map(function(c) { - return (category.get('id') && category.get('color').toUpperCase() === c.color.toUpperCase()) ? null : c.color.toUpperCase(); + return (categoryId && categoryColor.toUpperCase() === c.color.toUpperCase()) ? null : c.color.toUpperCase(); }, this).compact(); - }.property('category.id', 'category.color'), + }, - parentCategories: function() { + @computed + parentCategories() { return this.site.get('categoriesList').filter(c => !c.get('parentCategory')); - }.property(), + }, - categoryBadgePreview: function() { + @computed( + 'category.parent_category_id', + 'category.categoryName', + 'category.color', + 'category.text_color' + ) + categoryBadgePreview(parentCategoryId, name, color, textColor) { const category = this.get('category'); const c = Category.create({ - name: category.get('categoryName'), - color: category.get('color'), - text_color: category.get('text_color'), - parent_category_id: parseInt(category.get('parent_category_id'),10), + name, + color, + text_color: textColor, + parent_category_id: parseInt(parentCategoryId), read_restricted: category.get('read_restricted') }); - return categoryBadgeHTML(c, {link: false}); - }.property('category.parent_category_id', 'category.categoryName', 'category.color', 'category.text_color'), - + return categoryBadgeHTML(c, { link: false }); + }, // We can change the parent if there are no children - subCategories: function() { - if (Ember.isEmpty(this.get('category.id'))) { return null; } - return Category.list().filterBy('parent_category_id', this.get('category.id')); - }.property('category.id'), + @computed('category.id') + subCategories(categoryId) { + if (Ember.isEmpty(categoryId)) { return null; } + return Category.list().filterBy('parent_category_id', categoryId); + }, - showDescription: function() { - return !this.get('category.isUncategorizedCategory') && this.get('category.id'); - }.property('category.isUncategorizedCategory', 'category.id'), + @computed('category.isUncategorizedCategory', 'category.id') + showDescription(isUncategorizedCategory, categoryId) { + return !isUncategorizedCategory && categoryId; + }, actions: { showCategoryTopic() { diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs index c988d53869..4f89ede604 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-general.hbs @@ -37,28 +37,30 @@ {{i18n 'category.no_description'}} {{/if}} {{#if category.topic_url}} -
    +
    {{d-button class="btn-small" action="showCategoryTopic" icon="pencil" label="category.change_in_category_topic"}} {{/if}} {{/if}} -
    - -
    - {{{categoryBadgePreview}}} + {{#unless noCategoryStyle}} +
    + +
    + {{{categoryBadgePreview}}} -
    - {{i18n 'category.background_color'}}: - #{{text-field value=category.color placeholderKey="category.color_placeholder" maxlength="6"}} - {{color-picker colors=backgroundColors usedColors=usedBackgroundColors value=category.color}} -
    +
    + {{i18n 'category.background_color'}}: + #{{text-field value=category.color placeholderKey="category.color_placeholder" maxlength="6"}} + {{color-picker colors=backgroundColors usedColors=usedBackgroundColors value=category.color}} +
    -
    - {{i18n 'category.foreground_color'}}: - #{{text-field value=category.text_color placeholderKey="category.color_placeholder" maxlength="6"}} - {{color-picker colors=foregroundColors value=category.text_color id='edit-text-color'}} +
    + {{i18n 'category.foreground_color'}}: + #{{text-field value=category.text_color placeholderKey="category.color_placeholder" maxlength="6"}} + {{color-picker colors=foregroundColors value=category.text_color id='edit-text-color'}} +
    -
    -
    +
    + {{/unless}} From 64cb8a3ce32b9e2fb881030f0ec636e348408a59 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 2 Nov 2017 14:11:20 -0400 Subject: [PATCH 055/445] FIX: Normalizer wasn't working with attributes without values --- lib/html_normalize.rb | 8 +++++--- spec/components/html_normalize_spec.rb | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/html_normalize.rb b/lib/html_normalize.rb index 9b5ebb5d58..1243a4426a 100644 --- a/lib/html_normalize.rb +++ b/lib/html_normalize.rb @@ -74,9 +74,11 @@ class HtmlNormalize attrs.each do |a| buffer << " " buffer << a.name - buffer << "='" - buffer << a.value - buffer << "'" + if a.value + buffer << "='" + buffer << a.value + buffer << "'" + end end end diff --git a/spec/components/html_normalize_spec.rb b/spec/components/html_normalize_spec.rb index ec6b422295..7c75d8a15b 100644 --- a/spec/components/html_normalize_spec.rb +++ b/spec/components/html_normalize_spec.rb @@ -7,6 +7,10 @@ describe HtmlNormalize do HtmlNormalize.normalize(html) end + it "handles attributes without values" do + expect(n "").to eq("") + end + it "handles self closing tags" do source = <<-HTML From e700068b17ce7e15e14cff88fc3ba04f2b76be8e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 2 Nov 2017 14:21:34 -0400 Subject: [PATCH 056/445] UX: Convert Group Posts to Flexbox --- .../templates/components/group-post.hbs | 37 ++++++++++++------- .../templates/group-activity-posts.hbs | 6 +-- app/assets/stylesheets/common/base/group.scss | 37 +++++++++++++++++++ .../common/components/user-stream-item.scss | 14 ------- app/assets/stylesheets/desktop/group.scss | 1 + 5 files changed, 64 insertions(+), 31 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/components/group-post.hbs b/app/assets/javascripts/discourse/templates/components/group-post.hbs index 66386d75ae..7521788ec3 100644 --- a/app/assets/javascripts/discourse/templates/components/group-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-post.hbs @@ -1,19 +1,28 @@ -
    {{!-- DEPRECATED: 'item' class --}} -
    -
    {{avatar post.user imageSize="large" extraClasses="actor" ignoreTitle="true"}}
    - {{format-date post.created_at leaveAgo="true"}} - {{expand-post item=post}} - - {{{post.topic.fancyTitle}}} - - {{category-link post.category}} -
    +
    +
    + + {{avatar post.user imageSize="large" extraClasses="actor" ignoreTitle="true"}} + +
    + -

    - {{{post.excerpt}}} -

    +
    +
    + {{{post.excerpt}}}
    diff --git a/app/assets/javascripts/discourse/templates/group-activity-posts.hbs b/app/assets/javascripts/discourse/templates/group-activity-posts.hbs index 298edaff83..061160d1ed 100644 --- a/app/assets/javascripts/discourse/templates/group-activity-posts.hbs +++ b/app/assets/javascripts/discourse/templates/group-activity-posts.hbs @@ -1,7 +1,7 @@ -{{#load-more selector=".user-stream .user-stream-item" action=(action "loadMore")}} -
    +{{#load-more selector=".group-post" action=(action "loadMore")}} +
    {{#each model as |post|}} - {{group-post post=post}} + {{group-post post=post class="group-post"}} {{else}}
    {{i18n emptyText}}
    {{/each}} diff --git a/app/assets/stylesheets/common/base/group.scss b/app/assets/stylesheets/common/base/group.scss index c476e892da..4b8f48d08b 100644 --- a/app/assets/stylesheets/common/base/group.scss +++ b/app/assets/stylesheets/common/base/group.scss @@ -4,6 +4,43 @@ margin-bottom: 15px; } +.group-post { + .group-post-header { + display: flex; + + .group-post-avatar { + margin-right: 0.5em; + } + + .time, + .delete-info { + color: lighten($primary, 40%); + font-size: 0.8em; + } + + .group-member-info { + display: flex; + color: lighten($primary, 40%); + } + } + + .group-post-info { + display: flex; + width: 100%; + justify-content: space-between; + } + + .group-post-excerpt { + margin: 1em 0; + font-size: 0.929em; + word-wrap: break-word; + color: $primary; + } + + padding: 1em 0; + border-bottom: 1px solid $primary-low; +} + .group-info { width: 100%; diff --git a/app/assets/stylesheets/common/components/user-stream-item.scss b/app/assets/stylesheets/common/components/user-stream-item.scss index fbdcc97e66..078d5fd2e4 100644 --- a/app/assets/stylesheets/common/components/user-stream-item.scss +++ b/app/assets/stylesheets/common/components/user-stream-item.scss @@ -111,20 +111,6 @@ word-wrap: break-word; color: $primary; } - - .group-member-info { - .name { - display: inline-block; - margin-top: 5px; - color: dark-light-choose($primary-medium,$secondary-medium); - } - - .title { - display: inline-block; - margin-top: 5px; - color: dark-light-choose($primary-medium, $secondary-medium); - } - } } .user-stream .child-actions, // DEPRECATED: '.user-stream .child-actions' selector diff --git a/app/assets/stylesheets/desktop/group.scss b/app/assets/stylesheets/desktop/group.scss index e287be2cb0..ea1378fd1e 100644 --- a/app/assets/stylesheets/desktop/group.scss +++ b/app/assets/stylesheets/desktop/group.scss @@ -1,3 +1,4 @@ + .group-nav { li { float: left; From 05a61fa8e15f0f4711bf8219e27a2d80ad493c2b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 2 Nov 2017 15:05:41 -0400 Subject: [PATCH 057/445] FIX: CSS class names changed on group posts --- test/javascripts/acceptance/groups-test.js.es6 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/javascripts/acceptance/groups-test.js.es6 b/test/javascripts/acceptance/groups-test.js.es6 index 65ead1b17d..4e098f3169 100644 --- a/test/javascripts/acceptance/groups-test.js.es6 +++ b/test/javascripts/acceptance/groups-test.js.es6 @@ -54,19 +54,19 @@ QUnit.test("Anonymous Viewing Group", assert => { click(".nav-pills li a[title='Activity']"); andThen(() => { - assert.ok(count('.user-stream .item') > 0, "it lists stream items"); + assert.ok(count('.group-post') > 0, "it lists stream items"); }); click(".group-activity-nav li a[href='/groups/discourse/activity/topics']"); andThen(() => { - assert.ok(count('.user-stream .item') > 0, "it lists stream items"); + assert.ok(count('.group-post') > 0, "it lists stream items"); }); click(".group-activity-nav li a[href='/groups/discourse/activity/mentions']"); andThen(() => { - assert.ok(count('.user-stream .item') > 0, "it lists stream items"); + assert.ok(count('.group-post') > 0, "it lists stream items"); }); andThen(() => { @@ -77,7 +77,7 @@ QUnit.test("Anonymous Viewing Group", assert => { ); assert.ok(find(".nav-pills li a[title='Edit Group']").length === 0, 'it should not show messages tab if user is not admin'); assert.ok(find(".nav-pills li a[title='Logs']").length === 0, 'it should not show Logs tab if user is not admin'); - assert.ok(count('.user-stream .item') > 0, "it lists stream items"); + assert.ok(count('.group-post') > 0, "it lists stream items"); }); }); From a6b0e627cdfdfde9dbc63ec6f5cc113fadfa3401 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 2 Nov 2017 20:24:59 +0100 Subject: [PATCH 058/445] exclude public directory from RuboCop --- .rubocop.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.rubocop.yml b/.rubocop.yml index a529980180..b897ad3b9e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,6 +6,7 @@ AllCops: - 'bundle/**/*' - 'vendor/**/*' - 'node_modules/**/*' + - 'public/**/*' # Prefer &&/|| over and/or. Style/AndOr: From 51e74cb66e465b2bef20fb0e14af4ac3b6291107 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 2 Nov 2017 15:51:49 -0400 Subject: [PATCH 059/445] UX: Plugin outlet for group nav --- app/assets/javascripts/discourse/templates/group/activity.hbs | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/discourse/templates/group/activity.hbs b/app/assets/javascripts/discourse/templates/group/activity.hbs index dcc8386bce..08df8ddb57 100644 --- a/app/assets/javascripts/discourse/templates/group/activity.hbs +++ b/app/assets/javascripts/discourse/templates/group/activity.hbs @@ -6,6 +6,7 @@ {{#if showGroupMessages}} {{group-activity-filter filter="messages"}} {{/if}} + {{plugin-outlet name="below-group-activity-nav" tagName='' connectorTagName='li'}} {{/mobile-nav}}
    From 24af9b7d971d477256ef7a277e1e42789fa93265 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 2 Nov 2017 15:33:40 -0400 Subject: [PATCH 060/445] FIX: when a topic is deleted, update the post count stats of all user who replied --- app/models/user_stat.rb | 1 + lib/post_destroyer.rb | 10 ++++++++++ spec/components/post_destroyer_spec.rb | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/app/models/user_stat.rb b/app/models/user_stat.rb index 51ca626c3a..f0b01fad49 100644 --- a/app/models/user_stat.rb +++ b/app/models/user_stat.rb @@ -52,6 +52,7 @@ class UserStat < ActiveRecord::Base ", seen_at: last_seen end + # topic_reply_count is a count of posts in other users' topics def update_topic_reply_count self.topic_reply_count = Topic diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 61595ab8f6..0b53a9b672 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -245,6 +245,16 @@ class PostDestroyer author.last_posted_at = author.posts.order('created_at DESC').first.try(:created_at) author.save! end + + if @post.is_first_post? && @post.topic && !@post.topic.private_message? + # Update stats of all people who replied + counts = Post.where(post_type: Post.types[:regular]).where(topic_id: @post.topic_id).group(:user_id).count + counts.each do |user_id, count| + if user_stat = UserStat.where(user_id: user_id).first + user_stat.update_attributes(post_count: user_stat.post_count - count) + end + end + end end end diff --git a/spec/components/post_destroyer_spec.rb b/spec/components/post_destroyer_spec.rb index 6b6ba44d89..857f4821f1 100644 --- a/spec/components/post_destroyer_spec.rb +++ b/spec/components/post_destroyer_spec.rb @@ -207,6 +207,7 @@ describe PostDestroyer do DiscourseEvent.on(:topic_destroyed, &topic_destroyed) @orig = post2.cooked + # Guardian.new(post2.user).can_delete_post?(post2) == false PostDestroyer.new(post2.user, post2).destroy post2.reload @@ -239,6 +240,26 @@ describe PostDestroyer do end end + it "when topic is destroyed, it updates user_stats correctly" do + post + user1 = post.user + user1.reload + user2 = Fabricate(:user) + reply = create_post(topic_id: post.topic_id, user: user2) + reply2 = create_post(topic_id: post.topic_id, user: user1) + expect(user1.user_stat.topic_count).to eq(1) + expect(user1.user_stat.post_count).to eq(2) + expect(user2.user_stat.topic_count).to eq(0) + expect(user2.user_stat.post_count).to eq(1) + PostDestroyer.new(Fabricate(:admin), post).destroy + user1.reload + user2.reload + expect(user1.user_stat.topic_count).to eq(0) + expect(user1.user_stat.post_count).to eq(0) + expect(user2.user_stat.topic_count).to eq(0) + expect(user2.user_stat.post_count).to eq(0) + end + it "accepts a delete_removed_posts_after option" do SiteSetting.delete_removed_posts_after = 0 From f29290ad11463aefe3d1ffec5c508348d7a5bd52 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 2 Nov 2017 16:08:42 -0400 Subject: [PATCH 061/445] FIX: don't count whispers in user stats post_count --- lib/post_creator.rb | 2 +- spec/components/post_creator_spec.rb | 37 +++++++++++++++++----------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/lib/post_creator.rb b/lib/post_creator.rb index be0e6a8026..1e1ae5f806 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -473,7 +473,7 @@ class PostCreator end unless @post.topic.private_message? - @user.user_stat.post_count += 1 + @user.user_stat.post_count += 1 if @post.post_type == Post.types[:regular] @user.user_stat.topic_count += 1 if @post.is_first_post? end diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index f3d2e8350a..4c753a0dc1 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -387,27 +387,34 @@ describe PostCreator do it 'whispers do not mess up the public view' do first = PostCreator.new(user, - topic_id: topic.id, - raw: 'this is the first post').create + topic_id: topic.id, + raw: 'this is the first post').create + + user_stat = user.user_stat whisper = PostCreator.new(user, - topic_id: topic.id, - reply_to_post_number: 1, - post_type: Post.types[:whisper], - raw: 'this is a whispered reply').create + topic_id: topic.id, + reply_to_post_number: 1, + post_type: Post.types[:whisper], + raw: 'this is a whispered reply').create + + # don't count whispers in user stats + expect(user_stat.reload.post_count).to eq(1) expect(whisper).to be_present expect(whisper.post_type).to eq(Post.types[:whisper]) whisper_reply = PostCreator.new(user, - topic_id: topic.id, - reply_to_post_number: whisper.post_number, - post_type: Post.types[:regular], - raw: 'replying to a whisper this time').create + topic_id: topic.id, + reply_to_post_number: whisper.post_number, + post_type: Post.types[:regular], + raw: 'replying to a whisper this time').create expect(whisper_reply).to be_present expect(whisper_reply.post_type).to eq(Post.types[:whisper]) + expect(user_stat.reload.post_count).to eq(1) + # date is not precise enough in db whisper_reply.reload @@ -423,10 +430,12 @@ describe PostCreator do expect(topic.posts_count).to eq(1) expect(topic.highest_staff_post_number).to eq(3) - topic.update_columns(highest_staff_post_number: 0, - highest_post_number: 0, - posts_count: 0, - last_posted_at: 1.year.ago) + topic.update_columns( + highest_staff_post_number: 0, + highest_post_number: 0, + posts_count: 0, + last_posted_at: 1.year.ago + ) Topic.reset_highest(topic.id) From 30689783db73faf80058e3929615661cc653e693 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 2 Nov 2017 16:43:09 -0400 Subject: [PATCH 062/445] don't decrement post_count for a post in a deleted topic that has already been uncounted --- lib/post_destroyer.rb | 4 +- spec/components/post_destroyer_spec.rb | 58 +++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 0b53a9b672..5c8830e96d 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -231,7 +231,9 @@ class PostDestroyer author.user_stat.first_post_created_at = author.posts.order('created_at ASC').first.try(:created_at) end - author.user_stat.post_count -= 1 + unless @topic.nil? && !@post.is_first_post? + author.user_stat.post_count -= 1 # TODO: deleting a regular post + end author.user_stat.topic_count -= 1 if @post.is_first_post? # We don't count replies to your own topics diff --git a/spec/components/post_destroyer_spec.rb b/spec/components/post_destroyer_spec.rb index 857f4821f1..4888ebb3b4 100644 --- a/spec/components/post_destroyer_spec.rb +++ b/spec/components/post_destroyer_spec.rb @@ -196,6 +196,7 @@ describe PostDestroyer do it "as the creator of the post, doesn't delete the post" do begin post2 = create_post + user_stat = post2.user.user_stat called = 0 topic_destroyed = -> (topic, user) do @@ -217,6 +218,7 @@ describe PostDestroyer do expect(post2.raw).to eq(I18n.t('js.post.deleted_by_author', count: 24)) expect(post2.version).to eq(2) expect(called).to eq(1) + expect(user_stat.reload.post_count).to eq(1) called = 0 topic_recovered = -> (topic, user) do @@ -234,6 +236,7 @@ describe PostDestroyer do expect(post2.user_deleted).to eq(false) expect(post2.cooked).to eq(@orig) expect(called).to eq(1) + expect(user_stat.reload.post_count).to eq(1) ensure DiscourseEvent.off(:topic_destroyed, &topic_destroyed) DiscourseEvent.off(:topic_recovered, &topic_recovered) @@ -351,6 +354,7 @@ describe PostDestroyer do context "deleting a post belonging to a deleted topic" do let!(:topic) { post.topic } + let(:author) { post.user } before do topic.trash!(admin) @@ -365,23 +369,65 @@ describe PostDestroyer do it "deletes the post" do expect(post.deleted_at).to be_present expect(post.deleted_by).to eq(moderator) + expect(author.user_stat.post_count).to eq(0) end end context "as an admin" do - before do - PostDestroyer.new(admin, post).destroy - end + subject { PostDestroyer.new(admin, post).destroy } it "deletes the post" do + subject expect(post.deleted_at).to be_present expect(post.deleted_by).to eq(admin) end it "creates a new user history entry" do - expect { - PostDestroyer.new(admin, post).destroy - }.to change { UserHistory.count }.by(1) + expect { subject }.to change { UserHistory.count }.by(1) + end + end + end + + context "deleting a reply belonging to a deleted topic" do + let!(:topic) { post.topic } + let!(:reply) { create_post(topic_id: topic.id, user: post.user) } + let(:author) { reply.user } + + before do + topic.trash!(admin) + post.reload + reply.reload + end + + context "as a moderator" do + subject { PostDestroyer.new(moderator, reply).destroy } + + it "deletes the reply" do + subject + expect(reply.deleted_at).to be_present + expect(reply.deleted_by).to eq(moderator) + end + + it "doesn't decrement post_count again" do + expect { subject }.to_not change { author.user_stat.post_count } + end + end + + context "as an admin" do + subject { PostDestroyer.new(admin, reply).destroy } + + it "deletes the post" do + subject + expect(reply.deleted_at).to be_present + expect(reply.deleted_by).to eq(admin) + end + + it "doesn't decrement post_count again" do + expect { subject }.to_not change { author.user_stat.post_count } + end + + it "creates a new user history entry" do + expect { subject }.to change { UserHistory.count }.by(1) end end end From 21dd2ccd43aa338f284f67d2cd0bd8f11d8e2ce2 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 2 Nov 2017 17:11:08 -0400 Subject: [PATCH 063/445] FIX: only count regular posts in user stats when deleting --- lib/post_destroyer.rb | 4 ++-- spec/components/post_destroyer_spec.rb | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index 5c8830e96d..a449d55255 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -231,8 +231,8 @@ class PostDestroyer author.user_stat.first_post_created_at = author.posts.order('created_at ASC').first.try(:created_at) end - unless @topic.nil? && !@post.is_first_post? - author.user_stat.post_count -= 1 # TODO: deleting a regular post + if @post.post_type == Post.types[:regular] && !(@topic.nil? && !@post.is_first_post?) + author.user_stat.post_count -= 1 end author.user_stat.topic_count -= 1 if @post.is_first_post? diff --git a/spec/components/post_destroyer_spec.rb b/spec/components/post_destroyer_spec.rb index 4888ebb3b4..9e168924c2 100644 --- a/spec/components/post_destroyer_spec.rb +++ b/spec/components/post_destroyer_spec.rb @@ -308,6 +308,21 @@ describe PostDestroyer do author.reload }.to change { author.post_count }.by(-1) end + + it "doesn't count whispers" do + user_stat = admin.user_stat + whisper = PostCreator.new( + admin, + topic_id: post.topic.id, + reply_to_post_number: 1, + post_type: Post.types[:whisper], + raw: 'this is a whispered reply' + ).create + expect(user_stat.reload.post_count).to eq(0) + expect { + PostDestroyer.new(admin, whisper).destroy + }.to_not change { user_stat.reload.post_count } + end end end From 18d65fe7e51bafedd22d6894596dad6ea67f8e39 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 2 Nov 2017 17:48:48 -0400 Subject: [PATCH 064/445] FIX: post counts in user stats when changing post owner --- lib/post_revisor.rb | 16 +++++++++------ spec/services/post_owner_changer_spec.rb | 26 +++++++++++++++++++++++- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb index 01f14a1f80..f3379aafdb 100644 --- a/lib/post_revisor.rb +++ b/lib/post_revisor.rb @@ -290,9 +290,11 @@ class PostRevisor private_message = @post.topic.private_message? prev_owner_user_stat = prev_owner.user_stat - prev_owner_user_stat.post_count -= 1 - prev_owner_user_stat.topic_count -= 1 if @post.is_first_post? - prev_owner_user_stat.likes_received -= likes if !private_message + unless private_message + prev_owner_user_stat.post_count -= 1 if @post.post_type == Post.types[:regular] + prev_owner_user_stat.topic_count -= 1 if @post.is_first_post? + prev_owner_user_stat.likes_received -= likes + end prev_owner_user_stat.update_topic_reply_count if @post.created_at == prev_owner.user_stat.first_post_created_at @@ -302,9 +304,11 @@ class PostRevisor prev_owner_user_stat.save! new_owner_user_stat = new_owner.user_stat - new_owner_user_stat.post_count += 1 - new_owner_user_stat.topic_count += 1 if @post.is_first_post? - new_owner_user_stat.likes_received += likes if !private_message + unless private_message + new_owner_user_stat.post_count += 1 if @post.post_type == Post.types[:regular] + new_owner_user_stat.topic_count += 1 if @post.is_first_post? + new_owner_user_stat.likes_received += likes + end new_owner_user_stat.update_topic_reply_count new_owner_user_stat.save! end diff --git a/spec/services/post_owner_changer_spec.rb b/spec/services/post_owner_changer_spec.rb index ea88731ea0..f4896ea197 100644 --- a/spec/services/post_owner_changer_spec.rb +++ b/spec/services/post_owner_changer_spec.rb @@ -131,19 +131,43 @@ describe PostOwnerChanger do expect(user_a_stat.likes_received).to eq(1) end + it "handles whispers" do + whisper = PostCreator.new( + editor, + topic_id: p1.topic_id, + reply_to_post_number: 1, + post_type: Post.types[:whisper], + raw: 'this is a whispered reply' + ).create + + user_stat = editor.user_stat + + expect { + described_class.new( + post_ids: [whisper.id], + topic_id: topic.id, + new_owner: Fabricate(:admin), + acting_user: editor + ).change_owner! + }.to_not change { user_stat.reload.post_count } + end + context 'private message topic' do let(:topic) { Fabricate(:private_message_topic) } it "should update users' counts" do PostAction.act(p2user, p1, PostActionType.types[:like]) - change_owners + expect { + change_owners + }.to_not change { p1user.user_stat.post_count } expect(p1user.user_stat.likes_received).to eq(0) user_a_stat = user_a.user_stat expect(user_a_stat.first_post_created_at).to be_present expect(user_a_stat.likes_received).to eq(0) + expect(user_a_stat.post_count).to eq(0) end end From c107e9913841878f20b7deb7537b9aff9ae4ade7 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 2 Nov 2017 23:38:51 +0100 Subject: [PATCH 065/445] clear localStorage after each test --- test/javascripts/helpers/qunit-helpers.js.es6 | 1 + 1 file changed, 1 insertion(+) diff --git a/test/javascripts/helpers/qunit-helpers.js.es6 b/test/javascripts/helpers/qunit-helpers.js.es6 index 5a7c334704..6cf586009f 100644 --- a/test/javascripts/helpers/qunit-helpers.js.es6 +++ b/test/javascripts/helpers/qunit-helpers.js.es6 @@ -90,6 +90,7 @@ export function acceptance(name, options) { options.afterEach.call(this); } flushMap(); + localStorage.clear(); Discourse.User.resetCurrent(); resetSite(Discourse.SiteSettings); resetExtraClasses(); From c7d7cb940c5c5a2bcaafa1036e6f9db0974c8046 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 2 Nov 2017 18:24:43 -0400 Subject: [PATCH 066/445] FIX: dashboard posts report was including posts in daily data, but not in totals --- app/models/post.rb | 1 + spec/models/report_spec.rb | 1 + 2 files changed, 2 insertions(+) diff --git a/app/models/post.rb b/app/models/post.rb index 1ae23deac5..4dc6999c2c 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -622,6 +622,7 @@ class Post < ActiveRecord::Base def self.public_posts_count_per_day(start_date, end_date, category_id = nil) result = public_posts.where('posts.created_at >= ? AND posts.created_at <= ?', start_date, end_date) + .where(post_type: Post.types[:regular]) result = result.where('topics.category_id = ?', category_id) if category_id result.group('date(posts.created_at)').order('date(posts.created_at)').count end diff --git a/spec/models/report_spec.rb b/spec/models/report_spec.rb index c88a72b9dc..c70f9a4fff 100644 --- a/spec/models/report_spec.rb +++ b/spec/models/report_spec.rb @@ -215,6 +215,7 @@ describe Report do post.topic.add_small_action(Fabricate(:admin), "invited_group", 'coolkids') r = Report.find('posts') expect(r.total).to eq(1) + expect(r.data[0][:y]).to eq(1) end end end From af01860a3de2759fa028729a41a5d01b70e67b32 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 3 Nov 2017 09:21:10 +0800 Subject: [PATCH 067/445] Clear all active connections after PostgreSQL failover tests. --- .../connection_adapters/postgresql_fallback_adapter_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb index 5d48ea2186..c58efa156e 100644 --- a/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb +++ b/spec/components/active_record/connection_adapters/postgresql_fallback_adapter_spec.rb @@ -36,6 +36,7 @@ describe ActiveRecord::ConnectionHandling do after do postgresql_fallback_handler.setup! + postgresql_fallback_handler.clear_connections end describe "#postgresql_fallback_connection" do @@ -118,8 +119,6 @@ describe ActiveRecord::ConnectionHandling do expect(ActiveRecord::Base.connection_pool.connections.count).to eq(0) expect(postgresql_fallback_handler.master_down?).to eq(nil) - skip("Only fails on Travis") - expect(ActiveRecord::Base.connection) .to be_an_instance_of(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) end From 56412adad5efb7f9b3c24fecd53b90b6f9a6c6be Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 3 Nov 2017 16:19:31 +1100 Subject: [PATCH 068/445] FEATURE: custom setting for large square site icon This icon is used for android splash screen --- app/controllers/metadata_controller.rb | 2 +- app/controllers/site_controller.rb | 2 +- config/locales/server.en.yml | 3 ++- config/site_settings.yml | 2 ++ spec/controllers/metadata_controller_spec.rb | 8 +++++++- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/controllers/metadata_controller.rb b/app/controllers/metadata_controller.rb index e3494ceecc..f8d75cfb3d 100644 --- a/app/controllers/metadata_controller.rb +++ b/app/controllers/metadata_controller.rb @@ -13,7 +13,7 @@ class MetadataController < ApplicationController private def default_manifest - logo = SiteSetting.mobile_logo_url.presence || SiteSetting.logo_small_url.presence || SiteSetting.apple_touch_icon_url.presence + logo = SiteSetting.large_icon_url.presence || SiteSetting.logo_small_url.presence || SiteSetting.apple_touch_icon_url.presence manifest = { name: SiteSetting.title, diff --git a/app/controllers/site_controller.rb b/app/controllers/site_controller.rb index 00ed49e502..1e622449c9 100644 --- a/app/controllers/site_controller.rb +++ b/app/controllers/site_controller.rb @@ -34,7 +34,7 @@ class SiteController < ApplicationController title: SiteSetting.title, description: SiteSetting.site_description } - results[:mobile_logo_url] = SiteSetting.mobile_logo_url if SiteSetting.mobile_logo_url.present? + results[:mobile_logo_url] = SiteSetting.mobile_logo_url.presence DiscourseHub.stats_fetched_at = Time.zone.now if request.user_agent == "Discourse Hub" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 1a51f1cd15..b31c87349a 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1011,7 +1011,8 @@ en: digest_logo_url: "The alternate logo image used at the top of your site's email summary. Should be a wide rectangle shape. Should not be an SVG image. If left blank `logo_url` will be used." logo_small_url: "The small logo image at the top left of your site, should be a square shape, seen when scrolling down. If left blank a home glyph will be shown." favicon_url: "A favicon for your site, see http://en.wikipedia.org/wiki/Favicon, to work correctly over a CDN it must be a png" - mobile_logo_url: "The fixed position logo image used at the top left of your mobile site and as your logo/splash image on Android. Recommended size is 512px by 512px. If left blank, `logo_url` will be used. eg: http://example.com/uploads/default/logo.png" + mobile_logo_url: "Custom logo url used on mobile version of your site. If left blank, `logo_url` will be used. eg: http://example.com/uploads/default/logo.png" + large_icon_url: "Image used as logo/splash image on Android. Recommended size is 512px by 512px." apple_touch_icon_url: "Icon used for Apple touch devices. Recommended size is 144px by 144px." notification_email: "The from: email address used when sending all essential system emails. The domain specified here must have SPF, DKIM and reverse PTR records set correctly for email to arrive." diff --git a/config/site_settings.yml b/config/site_settings.yml index 2428262a1b..5df79b5672 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -54,6 +54,8 @@ required: mobile_logo_url: client: true default: '' + large_icon_url: + default: '' favicon_url: client: true default: '/images/default-favicon.ico' diff --git a/spec/controllers/metadata_controller_spec.rb b/spec/controllers/metadata_controller_spec.rb index 681cbe0725..cf15a697de 100644 --- a/spec/controllers/metadata_controller_spec.rb +++ b/spec/controllers/metadata_controller_spec.rb @@ -3,11 +3,17 @@ require 'rails_helper' RSpec.describe MetadataController do describe 'manifest.json' do it 'returns the right output' do + title = 'MyApp' SiteSetting.title = title + SiteSetting.large_icon_url = "http://big.square/png" + get :manifest - expect(response.body).to include(title) expect(response.content_type).to eq('application/json') + manifest = JSON.parse(response.body) + + expect(manifest["name"]).to eq(title) + expect(manifest["icons"].first["src"]).to eq("http://big.square/png") end end From 470b1a5bc1b95e9d73b06fd2705d6296bc99b5f4 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 3 Nov 2017 16:57:48 +0800 Subject: [PATCH 069/445] Don't print Sidekiq starting message to STDERR. --- lib/demon/sidekiq.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/demon/sidekiq.rb b/lib/demon/sidekiq.rb index 85e343c987..0ca7ba9994 100644 --- a/lib/demon/sidekiq.rb +++ b/lib/demon/sidekiq.rb @@ -23,7 +23,7 @@ class Demon::Sidekiq < Demon::Base def after_fork Demon::Sidekiq.after_fork&.call - STDERR.puts "Loading Sidekiq in process id #{Process.pid}" + puts "Loading Sidekiq in process id #{Process.pid}" require 'sidekiq/cli' # CLI will close the logger, if we have one set we can be in big # trouble, if STDOUT is closed in our process all sort of weird From d320f4840d2b6803e0290d2ea8052e064d78c1a7 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 3 Nov 2017 21:39:55 +0800 Subject: [PATCH 070/445] FIX: Unable to invite groups that are not public visible into pms. https://meta.discourse.org/t/inviting-groups-broken-in-head/73346/6 --- app/controllers/users_controller.rb | 12 ++++++-- app/models/group.rb | 10 ++----- spec/requests/users_controller_spec.rb | 41 ++++++++++++++++++++++++-- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index bec832bdbd..ad304f72a6 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -719,9 +719,15 @@ class UsersController < ApplicationController end end - if groups || params[:include_groups] == "true" - to_render[:groups] = Group.search_groups(term, groups: groups) - .map { |m| { name: m.name, full_name: m.full_name } } + include_groups = params[:include_groups] == "true" + + if include_groups || groups + groups = Group.search_groups(term, groups: groups) + groups = groups.where(visibility_level: Group.visibility_levels[:public]) if include_groups + + to_render[:groups] = groups.map do |m| + { name: m.name, full_name: m.full_name } + end end render json: to_render diff --git a/app/models/group.rb b/app/models/group.rb index 226b0555b0..81c939fabc 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -370,13 +370,9 @@ class Group < ActiveRecord::Base end def self.search_groups(name, groups: nil) - query = groups || Group - - query - .where(visibility_level: visibility_levels[:public]) - .where( - "name ILIKE :term_like OR full_name ILIKE :term_like", term_like: "%#{name}%" - ) + (groups || Group).where( + "name ILIKE :term_like OR full_name ILIKE :term_like", term_like: "%#{name}%" + ) end def self.lookup_group(name) diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 002d3cc5b2..3b92888b32 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -167,14 +167,45 @@ RSpec.describe UsersController do end context 'groups' do - let!(:mentionable_group) { Fabricate(:group, mentionable_level: 99, messageable_level: 0) } - let!(:messageable_group) { Fabricate(:group, mentionable_level: 0, messageable_level: 99) } + let!(:mentionable_group) do + Fabricate(:group, + mentionable_level: 99, + messageable_level: 0, + visibility_level: 0 + ) + end + + let!(:mentionable_group_2) do + Fabricate(:group, + mentionable_level: 99, + messageable_level: 0, + visibility_level: 1 + ) + end + + let!(:messageable_group) do + Fabricate(:group, + mentionable_level: 0, + messageable_level: 99 + ) + end describe 'when signed in' do before do sign_in(user) end + it "only returns visible groups" do + get "/u/search/users.json", params: { include_groups: "true" } + + expect(response).to be_success + + groups = JSON.parse(response.body)["groups"] + + expect(groups.map { |group| group['name'] }) + .to_not include(mentionable_group_2.name) + end + it "doesn't search for groups" do get "/u/search/users.json", params: { include_mentionable_groups: 'false', @@ -202,7 +233,11 @@ RSpec.describe UsersController do } expect(response).to be_success - expect(JSON.parse(response.body)["groups"].first['name']).to eq(mentionable_group.name) + + groups = JSON.parse(response.body)["groups"] + + expect(groups.map { |group| group['name'] }) + .to eq([mentionable_group.name, mentionable_group_2.name]) end end From 93633865d90d9e1414691b101550d3096b7d3a6e Mon Sep 17 00:00:00 2001 From: ckeboss Date: Fri, 3 Nov 2017 06:51:40 -0700 Subject: [PATCH 071/445] Adds primary user group as a class to quote (#5285) * Adds primary user group as a class to quote This feature addition will add the class `group-PRIMARY_USER_GROUP` to the quote `aside`. `PRIMARY_USER_GROUP` will be the primary user group of the user being quoted. This is similar to the class that is added to a `topic-post`. * Remove trailing whitespace * Fix avatar in test * Address PR comments * Fix trailing whitespace --- .../components/composer-editor.js.es6 | 13 ++++++ .../engines/discourse-markdown/quotes.js.es6 | 42 ++++++++++++++----- .../pretty-text/pretty-text.js.es6 | 4 ++ lib/pretty_text.rb | 1 + lib/pretty_text/helpers.rb | 8 ++++ lib/pretty_text/shims.js | 4 ++ spec/components/pretty_text_spec.rb | 28 +++++++++++++ test/javascripts/lib/pretty-text-test.js.es6 | 11 ++++- 8 files changed, 99 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 8445b9f8a5..424a7bcd25 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -75,6 +75,19 @@ export default Ember.Component.extend({ return tinyAvatar(quotedPost.get('avatar_template')); } } + }, + + lookupPrimaryUserGroupByPostNumber: (postNumber, topicId) => { + const topic = this.get('topic'); + if (!topic) { return; } + + const posts = topic.get('postStream.posts'); + if (posts && topicId === topic.get('id')) { + const quotedPost = posts.findBy("post_number", postNumber); + if (quotedPost) { + return quotedPost.primary_group_name; + } + } } }; }, diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 index 70ce99c6fc..620d54135f 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/quotes.js.es6 @@ -9,7 +9,7 @@ const rule = { let options = state.md.options.discourse; let quoteInfo = attrs['_default']; - let username, postNumber, topicId, avatarImg, full; + let username, postNumber, topicId, avatarImg, primaryGroupName, full; if (quoteInfo) { let split = quoteInfo.split(/\,\s*/); @@ -34,9 +34,30 @@ const rule = { } } + if (options.lookupAvatarByPostNumber) { + // client-side, we can retrieve the avatar from the post + avatarImg = options.lookupAvatarByPostNumber(postNumber, topicId); + } else if (options.lookupAvatar) { + // server-side, we need to lookup the avatar from the username + avatarImg = options.lookupAvatar(username); + } - let token = state.push('bbcode_open', 'aside', 1); - token.attrs = [['class', 'quote']]; + if (options.lookupPrimaryUserGroupByPostNumber) { + // client-side, we can retrieve the primary user group from the post + primaryGroupName = options.lookupPrimaryUserGroupByPostNumber(postNumber, topicId); + } else if (options.lookupPrimaryUserGroup) { + // server-side, we need to lookup the primary user group from the username + primaryGroupName = options.lookupPrimaryUserGroup(username); + } + + let token = state.push('bbcode_open', 'aside', 1); + token.attrs = []; + + if (primaryGroupName && primaryGroupName.length !== 0) { + token.attrs.push(['class', `quote group-${primaryGroupName}`]); + } else { + token.attrs.push(['class', 'quote']); + } if (postNumber) { token.attrs.push(['data-post', postNumber]); @@ -50,14 +71,6 @@ const rule = { token.attrs.push(['data-full', 'true']); } - if (options.lookupAvatarByPostNumber) { - // client-side, we can retrieve the avatar from the post - avatarImg = options.lookupAvatarByPostNumber(postNumber, topicId); - } else if (options.lookupAvatar) { - // server-side, we need to lookup the avatar from the username - avatarImg = options.lookupAvatar(username); - } - if (username) { let offTopicQuote = options.topicId && postNumber && @@ -132,4 +145,11 @@ export function setup(helper) { }); helper.whiteList(['img[class=avatar]']); + helper.whiteList({ + custom(tag, name, value) { + if (tag === 'aside' && name === 'class') { + return !!/^quote group\-(.+)$/.exec(value); + } + } + }); } diff --git a/app/assets/javascripts/pretty-text/pretty-text.js.es6 b/app/assets/javascripts/pretty-text/pretty-text.js.es6 index 3d3f85fc62..7d70cbf0d9 100644 --- a/app/assets/javascripts/pretty-text/pretty-text.js.es6 +++ b/app/assets/javascripts/pretty-text/pretty-text.js.es6 @@ -12,6 +12,7 @@ export function buildOptions(state) { siteSettings, getURL, lookupAvatar, + lookupPrimaryUserGroup, getTopicInfo, topicId, categoryHashtagLookup, @@ -19,6 +20,7 @@ export function buildOptions(state) { getCurrentUser, currentUser, lookupAvatarByPostNumber, + lookupPrimaryUserGroupByPostNumber, emojiUnicodeReplacer, lookupInlineOnebox, lookupImageUrls, @@ -50,6 +52,7 @@ export function buildOptions(state) { getURL, features, lookupAvatar, + lookupPrimaryUserGroup, getTopicInfo, topicId, categoryHashtagLookup, @@ -57,6 +60,7 @@ export function buildOptions(state) { getCurrentUser, currentUser, lookupAvatarByPostNumber, + lookupPrimaryUserGroupByPostNumber, mentionLookup, emojiUnicodeReplacer, lookupInlineOnebox, diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 5af8c928a5..9b88f4eed2 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -158,6 +158,7 @@ module PrettyText __optInput.getURL = __getURL; __optInput.getCurrentUser = __getCurrentUser; __optInput.lookupAvatar = __lookupAvatar; + __optInput.lookupPrimaryUserGroup = __lookupPrimaryUserGroup; __optInput.getTopicInfo = __getTopicInfo; __optInput.categoryHashtagLookup = __categoryLookup; __optInput.mentionLookup = __mentionLookup; diff --git a/lib/pretty_text/helpers.rb b/lib/pretty_text/helpers.rb index 5d19b8ead1..db1cfc3914 100644 --- a/lib/pretty_text/helpers.rb +++ b/lib/pretty_text/helpers.rb @@ -25,6 +25,14 @@ module PrettyText UrlHelper.schemaless(UrlHelper.absolute(user.avatar_template)) end + def lookup_primary_user_group(username) + return "" unless username + user = User.find_by(username_lower: username.downcase) + return "" unless user.present? + + user.primary_group.try(:name) || "" + end + def mention_lookup(name) return false if name.blank? return "group" if Group.exists?(name: name) diff --git a/lib/pretty_text/shims.js b/lib/pretty_text/shims.js index 26741932a7..9b36f270ea 100644 --- a/lib/pretty_text/shims.js +++ b/lib/pretty_text/shims.js @@ -73,6 +73,10 @@ function __lookupAvatar(p) { return __utils.avatarImg({size: "tiny", avatarTemplate: __helpers.avatar_template(p) }, __getURL); } +function __lookupPrimaryUserGroup(p) { + return __helpers.lookup_primary_user_group(p); +} + function __getCurrentUser(userId) { return __helpers.get_current_user(userId); } diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index d9aa563971..1a60ffd741 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -113,6 +113,34 @@ describe PrettyText do end end + describe "with primary user group" do + let(:default_avatar) { "//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/{size}.png" } + let(:group) { Fabricate(:group) } + let!(:user) { Fabricate(:user, primary_group: group) } + + before do + User.stubs(:default_template).returns(default_avatar) + end + + it "adds primary group class to referenced users quote" do + + topic = Fabricate(:topic, title: "this is a test topic") + expected = <<~HTML + + HTML + + expect(cook("[quote=\"#{user.username}, post:2, topic:#{topic.id}\"]\nddd\n[/quote]", topic_id: 1)).to eq(n(expected)) + end + end + it "can handle inline block bbcode" do cooked = PrettyText.cook("[quote]te **s** t[/quote]") diff --git a/test/javascripts/lib/pretty-text-test.js.es6 b/test/javascripts/lib/pretty-text-test.js.es6 index d53fcc346e..bf2cf59427 100644 --- a/test/javascripts/lib/pretty-text-test.js.es6 +++ b/test/javascripts/lib/pretty-text-test.js.es6 @@ -244,7 +244,6 @@ QUnit.test("Quotes", assert => { `, "works with multiple lines"); - assert.cookedOptions("[quote=\"bob, post:1\"]\nmy quote\n[/quote]", { topicId: 2, lookupAvatar: function() { } }, ``, "handles nested quotes properly"); + assert.cookedOptions(`[quote="bob, post:1, topic:1"]\ntest quote\n[/quote]`, { lookupPrimaryUserGroupByPostNumber: () => "aUserGroup" }, + ``, + "quote has group class"); }); QUnit.test("Mentions", assert => { From 2f0c9793f1e9ec2877b971d7b70ba4664f999342 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 3 Nov 2017 11:32:32 -0400 Subject: [PATCH 072/445] FEATURE: Allow multiple html builders to be registered via plugins --- app/controllers/application_controller.rb | 4 ++-- lib/discourse_plugin_registry.rb | 6 ++++-- spec/components/discourse_plugin_registry_spec.rb | 7 +++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 75256ea49d..71d3e9e683 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -465,9 +465,9 @@ class ApplicationController < ActionController::Base data.merge! DiscoursePluginRegistry.custom_html end - DiscoursePluginRegistry.html_builders.each do |name, blk| + DiscoursePluginRegistry.html_builders.each do |name, _| if name.start_with?("client:") - data[name.sub(/^client:/, '')] = blk.call(self) + data[name.sub(/^client:/, '')] = DiscoursePluginRegistry.build_html(name, self) end end diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index 9e404c16c5..52ca1df2ed 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -137,11 +137,13 @@ class DiscoursePluginRegistry end def self.register_html_builder(name, &block) - html_builders[name] = block + html_builders[name] ||= [] + html_builders[name] << block end def self.build_html(name, ctx = nil) - html_builders[name]&.call(ctx) + builders = html_builders[name] || [] + builders.map { |b| b.call(ctx) }.join("\n") end def javascripts diff --git a/spec/components/discourse_plugin_registry_spec.rb b/spec/components/discourse_plugin_registry_spec.rb index b280939012..68542ac0eb 100644 --- a/spec/components/discourse_plugin_registry_spec.rb +++ b/spec/components/discourse_plugin_registry_spec.rb @@ -51,6 +51,13 @@ describe DiscoursePluginRegistry do DiscoursePluginRegistry.reset! expect(DiscoursePluginRegistry.build_html(:my_html)).to be_blank end + + it "can register multiple builders" do + DiscoursePluginRegistry.register_html_builder(:my_html) { "one" } + DiscoursePluginRegistry.register_html_builder(:my_html) { "two" } + expect(DiscoursePluginRegistry.build_html(:my_html)).to eq("one\ntwo") + DiscoursePluginRegistry.reset! + end end context '.register_css' do From 253de4116695c53632cc8b1b4c9a535c5f9bd148 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 3 Nov 2017 22:38:36 +0530 Subject: [PATCH 073/445] bump onebox gem version --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 9462490a73..ef79a66d79 100644 --- a/Gemfile +++ b/Gemfile @@ -35,7 +35,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.21' +gem 'onebox', '1.8.22' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 046407fc5d..00c92ebf4d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -228,7 +228,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.21) + onebox (1.8.22) fast_blank (>= 1.0.0) htmlentities (~> 4.3) moneta (~> 1.0) @@ -464,7 +464,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.21) + onebox (= 1.8.22) openid-redis-store pg pry-nav From 2097c60cc071315a194125d16e882162784182b6 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 3 Nov 2017 15:48:43 -0400 Subject: [PATCH 074/445] UX: Wrap columns on categories/latest if the browser is too small --- app/assets/stylesheets/desktop/category-list.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/desktop/category-list.scss b/app/assets/stylesheets/desktop/category-list.scss index b5d0a93ab0..3eab6a1076 100644 --- a/app/assets/stylesheets/desktop/category-list.scss +++ b/app/assets/stylesheets/desktop/category-list.scss @@ -116,10 +116,12 @@ .categories-and-latest { display: flex; - flex-direction: row; + flex-flow: row wrap; div.column { flex: 1; + flex-direction: row; + min-width: 300px; } div.column.categories { From 6b466cd1b42d51c0f8e1655b6483b2486672069e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 3 Nov 2017 16:09:43 -0400 Subject: [PATCH 075/445] UX: Dynamic margins on flexbox categories-and-latest --- app/assets/stylesheets/desktop/category-list.scss | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/desktop/category-list.scss b/app/assets/stylesheets/desktop/category-list.scss index 3eab6a1076..fe59aa77bf 100644 --- a/app/assets/stylesheets/desktop/category-list.scss +++ b/app/assets/stylesheets/desktop/category-list.scss @@ -125,7 +125,15 @@ } div.column.categories { - margin-right: 2em; + @media all and (max-width : 600px) { + margin-right: 0; + } + @media all and (min-width : 600px) { + margin-right: 1em; + } + @media all and (min-width : 700px) { + margin-right: 2em; + } } .discourse-tag { From acbf265cc15139f8da3fd1107938351ceb9b7cb9 Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 3 Nov 2017 17:07:56 -0400 Subject: [PATCH 076/445] fixing the "me" color in the directory --- app/assets/stylesheets/common/base/directory.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/directory.scss b/app/assets/stylesheets/common/base/directory.scss index 7b5f7bdc6c..6e874e14df 100644 --- a/app/assets/stylesheets/common/base/directory.scss +++ b/app/assets/stylesheets/common/base/directory.scss @@ -39,7 +39,7 @@ background-color: dark-light-choose($highlight-low, $highlight-medium); .username a, .name, .title, .number, .time-read { - color: dark-light-choose(scale-color($highlight, $lightness: -50%), $highlight-medium); + color: $primary-medium; } } } From 43d025b3124eba186e97586b86f1947fedd62f74 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 3 Nov 2017 17:09:30 -0400 Subject: [PATCH 077/445] FIX: All groups were redirecting to posts --- .../routes/{group-activity.js.es6 => group-activity-index.js.es6} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/assets/javascripts/discourse/routes/{group-activity.js.es6 => group-activity-index.js.es6} (100%) diff --git a/app/assets/javascripts/discourse/routes/group-activity.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-index.js.es6 similarity index 100% rename from app/assets/javascripts/discourse/routes/group-activity.js.es6 rename to app/assets/javascripts/discourse/routes/group-activity-index.js.es6 From 59529ea54a1093815fec94e8020be92b5c378867 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Sat, 4 Nov 2017 11:47:26 +0530 Subject: [PATCH 078/445] generalize spec fixture --- spec/fixtures/json/import-export.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/fixtures/json/import-export.json b/spec/fixtures/json/import-export.json index 9af32b8e1e..1bc4da072a 100644 --- a/spec/fixtures/json/import-export.json +++ b/spec/fixtures/json/import-export.json @@ -12,8 +12,8 @@ {"id":15,"name":"Custom Category Import","color":"AB9364","created_at":"2017-10-26T15:32:44.083Z","user_id":2,"slug":"custom-category-import","description":null,"text_color":"FFFFFF","auto_close_hours":null,"parent_category_id":10,"auto_close_based_on_last_post":false,"topic_template":"","suppress_from_homepage":false,"all_topics_wiki":false,"permissions_params":{"custom_group_import":1,"everyone":2}} ], "users":[ - {"id":1,"email":"vinothkannan@example.com","username":"example","name":"Example","created_at":"2017-10-07T15:01:24.597Z","trust_level":4,"active":true,"last_emailed_at":null}, - {"id":2,"email":"vinoth.kannan@discourse.org","username":"vinothkannans","name":"Vinoth Kannan","created_at":"2017-10-07T15:01:24.597Z","trust_level":4,"active":true,"last_emailed_at":null} + {"id":1,"email":"email@example.com","username":"example","name":"Example","created_at":"2017-10-07T15:01:24.597Z","trust_level":4,"active":true,"last_emailed_at":null}, + {"id":2,"email":"foo@bar.com","username":"foo","name":"Foo Bar","created_at":"2017-10-07T15:01:24.597Z","trust_level":4,"active":true,"last_emailed_at":null} ], "topics":[ {"id":7,"title":"Assets for the site design","created_at":"2017-10-26T17:15:04.590Z","views":0,"category_id":8,"closed":false,"archived":false,"archetype":"regular", From 9ebb1412d3586ef6e5c2fa946de6b00c4cdc7958 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Sat, 4 Nov 2017 09:30:17 -0400 Subject: [PATCH 079/445] FIX: Brittle, order dependent spec --- spec/requests/users_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb index 3b92888b32..94fb106ec9 100644 --- a/spec/requests/users_controller_spec.rb +++ b/spec/requests/users_controller_spec.rb @@ -237,7 +237,7 @@ RSpec.describe UsersController do groups = JSON.parse(response.body)["groups"] expect(groups.map { |group| group['name'] }) - .to eq([mentionable_group.name, mentionable_group_2.name]) + .to contain_exactly(mentionable_group.name, mentionable_group_2.name) end end From 7847002f90a3469f45d05aaebd1b0f7065e40fd3 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Sat, 4 Nov 2017 10:03:15 -0400 Subject: [PATCH 080/445] UX: Add more classes for customization purposes --- .../templates/mobile/components/navigation-bar.hbs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/mobile/components/navigation-bar.hbs b/app/assets/javascripts/discourse/templates/mobile/components/navigation-bar.hbs index 884ceb75d4..b510a920c7 100644 --- a/app/assets/javascripts/discourse/templates/mobile/components/navigation-bar.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/components/navigation-bar.hbs @@ -1,7 +1,7 @@ -
  • - - {{selectedNavItem.displayName}} - {{d-icon "caret-down"}} +
  • {{#if expanded}} From 4e618aa08fec9fa09d5026919e62b0ac1407612e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Sat, 4 Nov 2017 10:15:16 -0400 Subject: [PATCH 081/445] UX: Missing element id --- .../javascripts/discourse/components/navigation-bar.js.es6 | 2 +- app/assets/stylesheets/common/base/_topic-list.scss | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 b/app/assets/javascripts/discourse/components/navigation-bar.js.es6 index 5f9e1266f1..2487a0d50d 100644 --- a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 +++ b/app/assets/javascripts/discourse/components/navigation-bar.js.es6 @@ -5,7 +5,7 @@ import { renderedConnectorsFor } from 'discourse/lib/plugin-connectors'; export default Ember.Component.extend({ tagName: 'ul', classNameBindings: [':nav', ':nav-pills'], - id: 'navigation-bar', + elementId: 'navigation-bar', init() { this._super(); diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index 241494a1d9..bc0164c31b 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -8,13 +8,6 @@ } .list-controls { - #navigation-bar { - .has-icon span:before { - margin-right: 4px; - font: 1.071em/0.9 "FontAwesome"; - } - } - .select-box-kit { align-self: center; From 230fec68ca7b877039cbf3dfea873804a00a73b7 Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Thu, 2 Nov 2017 02:47:36 +0530 Subject: [PATCH 082/445] FIX: Topic links onebox differently if end in / --- lib/onebox/engine/discourse_local_onebox.rb | 8 ++++---- .../onebox/engine/discourse_local_onebox_spec.rb | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/onebox/engine/discourse_local_onebox.rb b/lib/onebox/engine/discourse_local_onebox.rb index 3ee243ffdd..a1ce9ebe82 100644 --- a/lib/onebox/engine/discourse_local_onebox.rb +++ b/lib/onebox/engine/discourse_local_onebox.rb @@ -12,8 +12,7 @@ module Onebox url = other.to_s return false unless url[Discourse.base_url] - path = url.sub(Discourse.base_url, "") - route = Rails.application.routes.recognize_path(path) + route = Discourse.route_for(url) !!(route[:controller] =~ /topics|uploads/) rescue ActionController::RoutingError @@ -21,8 +20,9 @@ module Onebox end def to_html - path = @url.sub(Discourse.base_url, "") - route = Rails.application.routes.recognize_path(path) + uri = URI(@url) + path = uri.path || "" + route = Discourse.route_for(uri) case route[:controller] when "uploads" then upload_html(path) diff --git a/spec/components/onebox/engine/discourse_local_onebox_spec.rb b/spec/components/onebox/engine/discourse_local_onebox_spec.rb index 22907e0e06..911ef40b31 100644 --- a/spec/components/onebox/engine/discourse_local_onebox_spec.rb +++ b/spec/components/onebox/engine/discourse_local_onebox_spec.rb @@ -35,6 +35,11 @@ describe Onebox::Engine::DiscourseLocalOnebox do expect(html).to include(post2.excerpt) expect(html).to include(post2.topic.title) + url = "#{Discourse.base_url}#{post2.url}/?source_topic_id=#{post2.topic_id + 1}" + html = Onebox.preview(url).to_s + expect(html).to include(post2.excerpt) + expect(html).to include(post2.topic.title) + html = Onebox.preview("#{Discourse.base_url}#{post2.url}").to_s expect(html).to include(post2.user.username) expect(html).to include(post2.excerpt) @@ -65,6 +70,10 @@ describe Onebox::Engine::DiscourseLocalOnebox do html = Onebox.preview(topic.url).to_s expect(html).to include(topic.ordered_posts.first.user.username) expect(html).to include("
    ") + + html = Onebox.preview("#{topic.url}/?u=codinghorror").to_s + expect(html).to include(topic.ordered_posts.first.user.username) + expect(html).to include("
    ") end end From abdfac9cb5acb8b0de8f71533194a240cfca7930 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 6 Nov 2017 10:30:58 +0800 Subject: [PATCH 083/445] FIX: Allow group members to freely exit group option incorrectly disabled. https://meta.discourse.org/t/cannot-check-allow-members-to-leave-group-freely-when-group-is-configured-to-allow-users-to-send-membership-requests-to-group-owners/73400 --- app/assets/javascripts/discourse/templates/group-edit.hbs | 3 +-- test/javascripts/acceptance/group-edit-test.js.es6 | 5 +++++ test/javascripts/fixtures/group-fixtures.js.es6 | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/group-edit.hbs b/app/assets/javascripts/discourse/templates/group-edit.hbs index 9f8624f13a..b149a4ad7f 100644 --- a/app/assets/javascripts/discourse/templates/group-edit.hbs +++ b/app/assets/javascripts/discourse/templates/group-edit.hbs @@ -33,8 +33,7 @@ diff --git a/test/javascripts/acceptance/group-edit-test.js.es6 b/test/javascripts/acceptance/group-edit-test.js.es6 index 47fe847cf5..4b1f3f987e 100644 --- a/test/javascripts/acceptance/group-edit-test.js.es6 +++ b/test/javascripts/acceptance/group-edit-test.js.es6 @@ -43,6 +43,11 @@ QUnit.test("Editing group", assert => { 'it should disable group public admission input' ); + assert.ok( + find('.group-edit-public-exit[disabled]').length === 0, + 'it should not disable group public exit input' + ); + assert.equal( find('.group-edit-membership-request-template').length, 1, 'it should display the membership request template field' diff --git a/test/javascripts/fixtures/group-fixtures.js.es6 b/test/javascripts/fixtures/group-fixtures.js.es6 index 78aab0abf8..07cb5ef9f5 100644 --- a/test/javascripts/fixtures/group-fixtures.js.es6 +++ b/test/javascripts/fixtures/group-fixtures.js.es6 @@ -9,7 +9,7 @@ export default { "alias_level":99, "visible":true, "public_admission":true, - "public_exit":true, + "public_exit":false, "flair_url": 'fa-adjust', "is_group_owner":true, "mentionable":true, From a97273e1a52e241508e9d3b80718a4c5417c23f0 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 6 Nov 2017 11:22:58 +0800 Subject: [PATCH 084/445] Update Unicorn to 5.3.1. --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 00c92ebf4d..4154eb620e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -282,7 +282,7 @@ GEM thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake - raindrops (0.18.0) + raindrops (0.19.0) rake (12.1.0) rake-compiler (1.0.4) rake @@ -386,7 +386,7 @@ GEM unf_ext unf_ext (0.0.7.4) unicode-display_width (1.3.0) - unicorn (5.3.0) + unicorn (5.3.1) kgio (~> 2.6) raindrops (~> 0.7) uniform_notifier (1.10.0) From c9df21e131f51b63125f7e5308b9da43f2e3409a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 6 Nov 2017 12:46:14 +0800 Subject: [PATCH 085/445] FEATURE: Allow Unicorn logs to be JSON formatted. --- config/unicorn.conf.rb | 7 +++++ lib/unicorn/unicorn_json_log_formatter.rb | 21 +++++++++++++ .../unicorn_json_log_formatter_spec.rb | 31 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 lib/unicorn/unicorn_json_log_formatter.rb create mode 100644 spec/components/unicorn/unicorn_json_log_formatter_spec.rb diff --git a/config/unicorn.conf.rb b/config/unicorn.conf.rb index f90911a1bb..9704fd3860 100644 --- a/config/unicorn.conf.rb +++ b/config/unicorn.conf.rb @@ -1,5 +1,12 @@ # See http://unicorn.bogomips.org/Unicorn/Configurator.html +if ENV["UNICORN_JSON_LOG_FORMAT"] + unicorn_logger = Logger.new($stderr) + require_relative '../lib/unicorn/unicorn_json_log_formatter' + unicorn_logger.formatter = UnicornJSONLogFormatter.new + logger unicorn_logger +end + # enable out of band gc out of the box, it is low risk and improves perf a lot ENV['UNICORN_ENABLE_OOBGC'] ||= "1" diff --git a/lib/unicorn/unicorn_json_log_formatter.rb b/lib/unicorn/unicorn_json_log_formatter.rb new file mode 100644 index 0000000000..4410c467bd --- /dev/null +++ b/lib/unicorn/unicorn_json_log_formatter.rb @@ -0,0 +1,21 @@ +require 'json' + +class UnicornJSONLogFormatter < Logger::Formatter + def call(severity, datetime, progname, message) + default = { + severity: severity, + datetime: datetime, + progname: progname, + pid: $$, + } + + default[:message] = + if message.is_a?(Exception) + "#{message.message}: #{message.backtrace.join("\n")}" + else + message + end + + "#{default.to_json}\n" + end +end diff --git a/spec/components/unicorn/unicorn_json_log_formatter_spec.rb b/spec/components/unicorn/unicorn_json_log_formatter_spec.rb new file mode 100644 index 0000000000..78505770c0 --- /dev/null +++ b/spec/components/unicorn/unicorn_json_log_formatter_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' +require 'unicorn/unicorn_json_log_formatter' + +RSpec.describe UnicornJSONLogFormatter do + context 'when message is an exception' do + it 'should include the backtrace' do + freeze_time do + begin + raise 'boom' + rescue => e + error = e + + output = described_class.new.call( + 'INFO', + Time.zone.now, + '', + e + ) + end + + output = JSON.parse(output) + + expect(output["severity"]).to eq('INFO') + expect(output["datetime"]).to be_present + expect(output["progname"]).to eq('') + expect(output["pid"]).to be_present + expect(output["message"]).to match(/boom:.*/) + end + end + end +end From c1926e6dd269b461bcd18f05c5e959d2abda406c Mon Sep 17 00:00:00 2001 From: Kyle Zhao Date: Mon, 6 Nov 2017 02:03:52 -0500 Subject: [PATCH 086/445] FIX: do not generate multiple detail blocks when the selected input (#5290) consists of multiple lines --- .../initializers/apply-details.js.es6 | 2 +- .../acceptance/details-button-test.js.es6 | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 b/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 index 222b76e291..e357aa574c 100644 --- a/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 +++ b/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 @@ -18,7 +18,7 @@ function initializeDetails(api) { "\n" + `[details="${I18n.t("composer.details_title")}"]` + "\n", "\n[/details]\n", "details_text", - { multiline: true } + { multiline: false } ); this.set('optionsVisible', false); } diff --git a/plugins/discourse-details/test/javascripts/acceptance/details-button-test.js.es6 b/plugins/discourse-details/test/javascripts/acceptance/details-button-test.js.es6 index 93ff83cc2e..b0f25283ec 100644 --- a/plugins/discourse-details/test/javascripts/acceptance/details-button-test.js.es6 +++ b/plugins/discourse-details/test/javascripts/acceptance/details-button-test.js.es6 @@ -90,3 +90,28 @@ test('details button', (assert) => { assert.equal(textarea.selectionEnd, 49, 'it should end highlighting at the right position'); }); }); + +test('details button surrounds all selected text in a single details block', (assert) => { + const multilineInput = 'first line\n\nsecond line\n\nthird line'; + + visit("/"); + click('#create-topic'); + fillIn('.d-editor-input', multilineInput); + + andThen(() => { + const textarea = findTextarea(); + textarea.selectionStart = 0; + textarea.selectionEnd = textarea.value.length; + }); + + click('button.options'); + click('.popup-menu .d-icon-caret-right'); + + andThen(() => { + assert.equal( + find(".d-editor-input").val(), + `\n[details="${I18n.t('composer.details_title')}"]\n${multilineInput}\n[/details]\n`, + 'it should contain the right output' + ); + }); +}); From 96a414d0a53db2d0c86c71ac5177b4fdb5e1571d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 6 Nov 2017 16:01:32 +0800 Subject: [PATCH 087/445] Log `progname` as empty string if `nil`. --- lib/unicorn/unicorn_json_log_formatter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/unicorn/unicorn_json_log_formatter.rb b/lib/unicorn/unicorn_json_log_formatter.rb index 4410c467bd..7d68d7368e 100644 --- a/lib/unicorn/unicorn_json_log_formatter.rb +++ b/lib/unicorn/unicorn_json_log_formatter.rb @@ -5,7 +5,7 @@ class UnicornJSONLogFormatter < Logger::Formatter default = { severity: severity, datetime: datetime, - progname: progname, + progname: progname || '', pid: $$, } From ffe823ed322ee685457c68e2daed40732ee9b1cd Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 6 Nov 2017 16:40:11 +0800 Subject: [PATCH 088/445] Ensure we log the datetime in the format that we want. --- lib/unicorn/unicorn_json_log_formatter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/unicorn/unicorn_json_log_formatter.rb b/lib/unicorn/unicorn_json_log_formatter.rb index 7d68d7368e..d0695d999a 100644 --- a/lib/unicorn/unicorn_json_log_formatter.rb +++ b/lib/unicorn/unicorn_json_log_formatter.rb @@ -4,7 +4,7 @@ class UnicornJSONLogFormatter < Logger::Formatter def call(severity, datetime, progname, message) default = { severity: severity, - datetime: datetime, + datetime: DateTime.parse(datetime).to_s, progname: progname || '', pid: $$, } From 6a47491afeb709bac8b0794e471ea74379565b88 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 6 Nov 2017 16:56:22 +0800 Subject: [PATCH 089/445] Fix the build. --- lib/unicorn/unicorn_json_log_formatter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/unicorn/unicorn_json_log_formatter.rb b/lib/unicorn/unicorn_json_log_formatter.rb index d0695d999a..0edc3d02da 100644 --- a/lib/unicorn/unicorn_json_log_formatter.rb +++ b/lib/unicorn/unicorn_json_log_formatter.rb @@ -4,7 +4,7 @@ class UnicornJSONLogFormatter < Logger::Formatter def call(severity, datetime, progname, message) default = { severity: severity, - datetime: DateTime.parse(datetime).to_s, + datetime: DateTime.parse(datetime.to_s).to_s, progname: progname || '', pid: $$, } From 7eb5f783438bc0ea86fe22ffe6f2bb6c7d369d73 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 6 Nov 2017 09:59:58 -0500 Subject: [PATCH 090/445] UX: increase max length of topic titles in summary email html by 40 characters --- app/views/user_notifications/digest.html.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/user_notifications/digest.html.erb b/app/views/user_notifications/digest.html.erb index cdea397301..b09bc3b736 100644 --- a/app/views/user_notifications/digest.html.erb +++ b/app/views/user_notifications/digest.html.erb @@ -125,7 +125,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo

    - <%= gsub_emoji_to_unicode(t.title.truncate(60, separator: /\s/)) -%> + <%= gsub_emoji_to_unicode(t.title.truncate(100, separator: /\s/)) -%>

    <%- if SiteSetting.show_topic_featured_link_in_digest && t.featured_link %> @@ -280,7 +280,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo

    - <%= gsub_emoji_to_unicode(post.topic.title.truncate(60, separator: /\s/)) -%> + <%= gsub_emoji_to_unicode(post.topic.title.truncate(100, separator: /\s/)) -%>

    <%=t 'user_notifications.digest.join_the_discussion' %> @@ -326,7 +326,7 @@ body, table, td, th, h1, h2, h3 {font-family: Helvetica, Arial, sans-serif !impo - <%= gsub_emoji_to_unicode(t.title.truncate(60, separator: /\s/)) -%> + <%= gsub_emoji_to_unicode(t.title.truncate(100, separator: /\s/)) -%> <%- if SiteSetting.show_topic_featured_link_in_digest && t.featured_link %> <%= raw topic_featured_link_domain(t.featured_link) %> From b3237d37f04f8d2daa12783f0a961d92ee938549 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 7 Nov 2017 10:06:42 +0800 Subject: [PATCH 091/445] Drop unused email column from users table. --- app/models/user.rb | 3 ++ db/fixtures/009_users.rb | 41 ++++++++++--------- .../20171107020512_drop_email_from_users.rb | 9 ++++ 3 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 db/migrate/20171107020512_drop_email_from_users.rb diff --git a/app/models/user.rb b/app/models/user.rb index 522081371f..881d162ec4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -19,6 +19,9 @@ class User < ActiveRecord::Base include Roleable include HasCustomFields + # TODO: Remove this after 7th Jan 2018 + self.ignored_columns = %w{email} + has_many :posts has_many :notifications, dependent: :destroy has_many :topic_users, dependent: :destroy diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb index 23d024fe19..f1bee9cd16 100644 --- a/db/fixtures/009_users.rb +++ b/db/fixtures/009_users.rb @@ -33,28 +33,29 @@ UserOption.where(user_id: -1).update_all( Group.user_trust_level_change!(-1, TrustLevel[4]) -# TODO drop email with ignored_columns pattern in rails 5.1 ColumnDropper.drop( table: 'users', - after_migration: 'CreateUserEmails', - columns: %w[ - email_always - mailing_list_mode - email_digests - email_direct - email_private_messages - external_links_in_new_tab - enable_quoting - dynamic_favicon - disable_jump_reply - edit_history_public - automatically_unpin_topics - digest_after_days - auto_track_topics_after_msecs - new_topic_duration_minutes - last_redirected_to_top_at - auth_token - auth_token_updated_at ], + after_migration: 'DropEmailFromUsers', + columns: %w[ + email + email_always + mailing_list_mode + email_digests + email_direct + email_private_messages + external_links_in_new_tab + enable_quoting + dynamic_favicon + disable_jump_reply + edit_history_public + automatically_unpin_topics + digest_after_days + auto_track_topics_after_msecs + new_topic_duration_minutes + last_redirected_to_top_at + auth_token + auth_token_updated_at + ], on_drop: ->() { STDERR.puts 'Removing superflous users columns!' } diff --git a/db/migrate/20171107020512_drop_email_from_users.rb b/db/migrate/20171107020512_drop_email_from_users.rb new file mode 100644 index 0000000000..f5697d3718 --- /dev/null +++ b/db/migrate/20171107020512_drop_email_from_users.rb @@ -0,0 +1,9 @@ +class DropEmailFromUsers < ActiveRecord::Migration[5.1] + def up + # Defer dropping of the columns until the new application code has been deployed. + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end From bf82b34f10f53d36a19eed18f541eba79d76d140 Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Tue, 7 Nov 2017 01:05:30 -0200 Subject: [PATCH 092/445] FIX: Remove scope in SW register, avoid duplicated SWs --- .../initializers/android-app-banner-service-worker.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/initializers/android-app-banner-service-worker.js.es6 b/app/assets/javascripts/discourse/initializers/android-app-banner-service-worker.js.es6 index 3d8c2f7fd2..e24cb2d6a2 100644 --- a/app/assets/javascripts/discourse/initializers/android-app-banner-service-worker.js.es6 +++ b/app/assets/javascripts/discourse/initializers/android-app-banner-service-worker.js.es6 @@ -10,7 +10,7 @@ export default { const isSecure = document.location.protocol === 'https:'; if (isSecure && caps.isAndroid && 'serviceWorker' in navigator) { - navigator.serviceWorker.register(Discourse.BaseUri + '/service-worker.js', {scope: './'}); + navigator.serviceWorker.register(Discourse.BaseUri + '/service-worker.js'); } } }; From d9602fe729ef15a9cf61f7517e3363e50898b9cf Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 7 Nov 2017 11:24:40 +0800 Subject: [PATCH 093/445] Fix incorrect severity field. --- lib/unicorn/unicorn_json_log_formatter.rb | 3 ++- spec/components/unicorn/unicorn_json_log_formatter_spec.rb | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/unicorn/unicorn_json_log_formatter.rb b/lib/unicorn/unicorn_json_log_formatter.rb index 0edc3d02da..b1d76ca181 100644 --- a/lib/unicorn/unicorn_json_log_formatter.rb +++ b/lib/unicorn/unicorn_json_log_formatter.rb @@ -3,7 +3,8 @@ require 'json' class UnicornJSONLogFormatter < Logger::Formatter def call(severity, datetime, progname, message) default = { - severity: severity, + severity: "Logger::Severity::#{severity}".constantize, + severity_name: severity, datetime: DateTime.parse(datetime.to_s).to_s, progname: progname || '', pid: $$, diff --git a/spec/components/unicorn/unicorn_json_log_formatter_spec.rb b/spec/components/unicorn/unicorn_json_log_formatter_spec.rb index 78505770c0..b304735bce 100644 --- a/spec/components/unicorn/unicorn_json_log_formatter_spec.rb +++ b/spec/components/unicorn/unicorn_json_log_formatter_spec.rb @@ -11,7 +11,7 @@ RSpec.describe UnicornJSONLogFormatter do error = e output = described_class.new.call( - 'INFO', + 'ERROR', Time.zone.now, '', e @@ -20,7 +20,8 @@ RSpec.describe UnicornJSONLogFormatter do output = JSON.parse(output) - expect(output["severity"]).to eq('INFO') + expect(output["severity"]).to eq(3) + expect(output["severity_name"]).to eq("ERROR") expect(output["datetime"]).to be_present expect(output["progname"]).to eq('') expect(output["pid"]).to be_present From 8970bdd4fa3a75cc1610f8a12462a178bd64dc5f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 7 Nov 2017 11:52:22 +0800 Subject: [PATCH 094/445] FIX: Undefined method before unicorn boots. --- lib/unicorn/unicorn_json_log_formatter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/unicorn/unicorn_json_log_formatter.rb b/lib/unicorn/unicorn_json_log_formatter.rb index b1d76ca181..5c0b252786 100644 --- a/lib/unicorn/unicorn_json_log_formatter.rb +++ b/lib/unicorn/unicorn_json_log_formatter.rb @@ -3,7 +3,7 @@ require 'json' class UnicornJSONLogFormatter < Logger::Formatter def call(severity, datetime, progname, message) default = { - severity: "Logger::Severity::#{severity}".constantize, + severity: Object.const_get("Logger::Severity::#{severity}"), severity_name: severity, datetime: DateTime.parse(datetime.to_s).to_s, progname: progname || '', From 95c891cf11b4f2cca9bdbd45f9d459e5c1efc3e1 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 7 Nov 2017 18:38:38 +0800 Subject: [PATCH 095/445] Raise error if sso record fails to create. --- app/models/discourse_single_sign_on.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/models/discourse_single_sign_on.rb b/app/models/discourse_single_sign_on.rb index 9894e7dfa7..78fde37faf 100644 --- a/app/models/discourse_single_sign_on.rb +++ b/app/models/discourse_single_sign_on.rb @@ -149,8 +149,15 @@ class DiscourseSingleSignOn < SingleSignOn sso_record.last_payload = unsigned_payload sso_record.external_id = external_id else - Jobs.enqueue(:download_avatar_from_url, url: avatar_url, user_id: user.id, override_gravatar: SiteSetting.sso_overrides_avatar) if avatar_url.present? - user.create_single_sign_on_record( + if avatar_url.present? + Jobs.enqueue(:download_avatar_from_url, + url: avatar_url, + user_id: user.id, + override_gravatar: SiteSetting.sso_overrides_avatar + ) + end + + user.create_single_sign_on_record!( last_payload: unsigned_payload, external_id: external_id, external_username: username, From 9355f92f78c085d87b79a564bdc553bde3a6eac0 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 7 Nov 2017 19:38:36 +0800 Subject: [PATCH 096/445] Add more verbose SSO logging. --- app/models/discourse_single_sign_on.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/discourse_single_sign_on.rb b/app/models/discourse_single_sign_on.rb index 78fde37faf..fbe2425ec9 100644 --- a/app/models/discourse_single_sign_on.rb +++ b/app/models/discourse_single_sign_on.rb @@ -142,6 +142,10 @@ class DiscourseSingleSignOn < SingleSignOn } user = User.create!(user_params) + + if SiteSetting.verbose_sso_logging + Rails.logger.warn("Verbose SSO log: New User (user_id: #{user.id}) Created with #{user_params}") + end end if user From 442d4bff85af4a58cf382e85f2ef3a38efa9c5eb Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 7 Nov 2017 15:41:10 +0800 Subject: [PATCH 097/445] Add onceoff job to remap bot images link. https://meta.discourse.org/t/discobot-tutorial-broken-elipsis-and-bookmark-png-images-appear-to-be-missing-on-my-site/73294/12 --- .../jobs/onceoff/remap_old_bot_images.rb | 36 ++++++++++++++++ plugins/discourse-narrative-bot/plugin.rb | 1 + .../jobs/onceoff/remap_old_bot_iamges_spec.rb | 42 +++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 plugins/discourse-narrative-bot/jobs/onceoff/remap_old_bot_images.rb create mode 100644 plugins/discourse-narrative-bot/spec/jobs/onceoff/remap_old_bot_iamges_spec.rb diff --git a/plugins/discourse-narrative-bot/jobs/onceoff/remap_old_bot_images.rb b/plugins/discourse-narrative-bot/jobs/onceoff/remap_old_bot_images.rb new file mode 100644 index 0000000000..d23f1c8d05 --- /dev/null +++ b/plugins/discourse-narrative-bot/jobs/onceoff/remap_old_bot_images.rb @@ -0,0 +1,36 @@ +module Jobs + module DiscourseNarrativeBot + class RemapOldBotImages < ::Jobs::Onceoff + def execute_onceoff(args) + paths = [ + "/images/font-awesome-link.png", + "/images/unicorn.png", + "/images/font-awesome-ellipsis.png", + "/images/font-awesome-bookmark.png", + "/images/font-awesome-smile.png", + "/images/font-awesome-flag.png", + "/images/font-awesome-search.png", + "/images/capybara-eating.gif", + "/images/font-awesome-pencil.png", + "/images/font-awesome-trash.png", + "/images/font-awesome-rotate-left.png", + "/images/font-awesome-gear.png", + ] + + Post.raw_match("/images/").where(user_id: -2).find_each do |post| + if (matches = post.raw.scan(/(? below and **bookmark this private message**. If you do, there may be a :gift: in your future!' + ) + end + + before do + post + end + + it 'should remap the links correctly' do + expected_raw = 'If you’d like to learn more, select below and **bookmark this private message**. If you do, there may be a :gift: in your future!' + + 2.times do + described_class.new.execute_onceoff({}) + expect(post.reload.raw).to eq(expected_raw) + end + end + + context 'subfolder' do + let(:post) do + Fabricate(:post, + user_id: -2, + raw: 'If you’d like to learn more, select below and **bookmark this private message**. If you do, there may be a :gift: in your future!' + ) + end + + it 'should remap the links correctly' do + described_class.new.execute_onceoff({}) + + expect(post.reload.raw).to eq( + 'If you’d like to learn more, select below and **bookmark this private message**. If you do, there may be a :gift: in your future!' + ) + end + end + end +end From 814502f7bb68815edc90673881fa4d284a6482ef Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 7 Nov 2017 10:41:12 -0500 Subject: [PATCH 098/445] FEATURE: add a Dismiss link to user menu that dismisses all notifications --- .../discourse/widgets/user-menu.js.es6 | 73 ++++++++++++++++--- .../widgets/user-notifications.js.es6 | 3 + .../stylesheets/common/base/menu-panel.scss | 7 ++ config/locales/client.en.yml | 1 + .../javascripts/widgets/user-menu-test.js.es6 | 1 + 5 files changed, 76 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 index 9142695573..ac99495c9c 100644 --- a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 @@ -1,6 +1,7 @@ import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; import { formatUsername } from 'discourse/lib/utilities'; +import { ajax } from 'discourse/lib/ajax'; let extraGlyphs; @@ -87,6 +88,46 @@ createWidget('user-menu-links', { } }); +createWidget('user-menu-dismiss-link', { + tagName: 'div.dismiss-link', + buildKey: () => 'user-menu-dismiss-link', + + defaultState() { + return { showDismiss: false }; + }, + + html() { + if (this.state.showDismiss) { + return h('ul.menu-links', + h('li', + this.attach('link', { + action: 'dismissNotifications', + className: 'dismiss', + icon: 'check', + label: 'user.dismiss' + }) + ) + ); + } else { + return ''; + } + }, + + showDismissLink() { + this.state.showDismiss = true; + this.scheduleRerender(); + }, + + dismissNotifications() { + ajax('/notifications/mark-read', { method: 'PUT' }).then(() => { + userNotifications.notificationsChanged(); + }); + } +}); + +let userNotifications = null, + dismissLink = null; + export default createWidget('user-menu', { tagName: 'div.user-menu', @@ -96,16 +137,26 @@ export default createWidget('user-menu', { panelContents() { const path = this.currentUser.get('path'); + userNotifications = this.attach('user-notifications', { path }); + dismissLink = this.attach('user-menu-dismiss-link'); - return [this.attach('user-menu-links', { path }), - this.attach('user-notifications', { path }), - h('div.logout-link', [ - h('ul.menu-links', - h('li', this.attach('link', { action: 'logout', - className: 'logout', - icon: 'sign-out', - label: 'user.log_out' }))) - ])]; + return [ + this.attach('user-menu-links', { path }), + userNotifications, + h('div.logout-link', [ + h('ul.menu-links', + h('li', + this.attach('link', { + action: 'logout', + className: 'logout', + icon: 'sign-out', + label: 'user.log_out' + }) + ) + ) + ]), + dismissLink + ]; }, html() { @@ -117,5 +168,9 @@ export default createWidget('user-menu', { clickOutside() { this.sendWidgetAction('toggleUserMenu'); + }, + + notificationsLoaded() { + dismissLink.showDismissLink(); } }); diff --git a/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 b/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 index c7570479cc..260bc36146 100644 --- a/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 @@ -50,6 +50,9 @@ export default createWidget('user-notifications', { }).finally(() => { state.loading = false; state.loaded = true; + if (state.notifications.get('length') > 0) { + this.sendWidgetAction('notificationsLoaded'); + } this.scheduleRerender(); }); }, diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 99397a8da3..ae2c954406 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -257,6 +257,13 @@ /* as a big ol' click target, don't let text inside be selected */ @include unselectable; } + + .logout-link, .dismiss-link { + display: inline-block; + } + .dismiss-link { + float: right; + } } .notifications .logout { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index fa1a8b1edb..6e2a7c6d1d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -590,6 +590,7 @@ en: enable: "Enable Notifications" currently_disabled: "" each_browser_note: "Note: You have to change this setting on every browser you use." + dismiss: 'Dismiss' dismiss_notifications: "Dismiss All" dismiss_notifications_tooltip: "Mark all unread notifications as read" first_notification: "Your first notification! Select it to begin." diff --git a/test/javascripts/widgets/user-menu-test.js.es6 b/test/javascripts/widgets/user-menu-test.js.es6 index c208d91b4b..8b26410e0f 100644 --- a/test/javascripts/widgets/user-menu-test.js.es6 +++ b/test/javascripts/widgets/user-menu-test.js.es6 @@ -11,6 +11,7 @@ widgetTest('basics', { assert.ok(this.$('.user-bookmarks-link').length); assert.ok(this.$('.user-preferences-link').length); assert.ok(this.$('.notifications').length); + assert.ok(this.$('.dismiss-link').length); } }); From 4f0bdec37084b5ab9d5439f7feebf31f59e302c9 Mon Sep 17 00:00:00 2001 From: discoursehosting Date: Tue, 7 Nov 2017 17:50:43 +0100 Subject: [PATCH 099/445] some improvements for importers (#5295) * decode html entities within code blocks * Only import users that actually participated in the bbpress part of Wordpress; import password hashes * create permalinks for topics * Better handling of [code] blocks --- script/import_scripts/bbpress.rb | 61 ++++++++++++++++--- .../phpbb3/support/text_processor.rb | 13 +++- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/script/import_scripts/bbpress.rb b/script/import_scripts/bbpress.rb index a9ecc0650b..6d27e950ea 100644 --- a/script/import_scripts/bbpress.rb +++ b/script/import_scripts/bbpress.rb @@ -22,6 +22,8 @@ class ImportScripts::Bbpress < ImportScripts::Base def initialize super + @he = HTMLEntities.new + @client = Mysql2::Client.new( host: BB_PRESS_HOST, username: BB_PRESS_USER, @@ -36,21 +38,32 @@ class ImportScripts::Bbpress < ImportScripts::Base import_categories import_topics_and_posts import_private_messages + create_permalinks end def import_users puts "", "importing users..." last_user_id = -1 - total_users = bbpress_query("SELECT COUNT(*) count FROM #{BB_PRESS_PREFIX}users WHERE user_email LIKE '%@%'").first["count"] + total_users = bbpress_query(<<-SQL + SELECT COUNT(DISTINCT(u.id)) AS cnt + FROM #{BB_PRESS_PREFIX}users u + LEFT JOIN #{BB_PRESS_PREFIX}posts p ON p.post_author = u.id + WHERE p.post_type IN ('forum', 'reply', 'topic') + AND user_email LIKE '%@%' + SQL + ).first["cnt"] batches(BATCH_SIZE) do |offset| users = bbpress_query(<<-SQL - SELECT id, user_nicename, display_name, user_email, user_registered, user_url - FROM #{BB_PRESS_PREFIX}users + SELECT u.id, user_nicename, display_name, user_email, user_registered, user_url, user_pass + FROM #{BB_PRESS_PREFIX}users u + LEFT JOIN #{BB_PRESS_PREFIX}posts p ON p.post_author = u.id WHERE user_email LIKE '%@%' - AND id > #{last_user_id} - ORDER BY id + AND p.post_type IN ('forum', 'reply', 'topic') + AND u.id > #{last_user_id} + GROUP BY u.id + ORDER BY u.id LIMIT #{BATCH_SIZE} SQL ).to_a @@ -86,6 +99,7 @@ class ImportScripts::Bbpress < ImportScripts::Base { id: u["id"].to_i, username: u["user_nicename"], + password: u["user_pass"], email: u["user_email"].downcase, name: u["display_name"].presence || u['user_nicename'], created_at: u["user_registered"], @@ -242,8 +256,7 @@ class ImportScripts::Bbpress < ImportScripts::Base } if post[:raw].present? - post[:raw].gsub!("
    ", "```\n")
    -          post[:raw].gsub!("
    ", "\n```") + post[:raw].gsub!(/\\(.*?)\<\/code\>\<\/pre\>/im) { "```\n#{@he.decode($2)}\n```" } end if p["post_type"] == "topic" @@ -264,6 +277,40 @@ class ImportScripts::Bbpress < ImportScripts::Base end end + def create_permalinks + puts "", "creating permalinks..." + + last_topic_id = -1 + total_topics = bbpress_query(<<-SQL + SELECT COUNT(*) count + FROM #{BB_PRESS_PREFIX}posts + WHERE post_status <> 'spam' + AND post_type IN ('topic') + SQL + ).first["count"] + + batches(BATCH_SIZE) do |offset| + topics = bbpress_query(<<-SQL + SELECT id, + guid + FROM #{BB_PRESS_PREFIX}posts + WHERE post_status <> 'spam' + AND post_type IN ('topic') + AND id > #{last_topic_id} + ORDER BY id + LIMIT #{BATCH_SIZE} + SQL + ).to_a + break if topics.empty? + + topics.each do |t| + topic = topic_lookup_from_imported_post_id(t['id']) + Permalink.create( url: URI.parse(t['guid']).path.chomp('/'), topic_id: topic[:topic_id] ) rescue nil + end + last_topic_id = topics[-1]["id"].to_i + end + end + def import_private_messages puts "", "importing private messages..." diff --git a/script/import_scripts/phpbb3/support/text_processor.rb b/script/import_scripts/phpbb3/support/text_processor.rb index 3eceff3f75..561b2c2f69 100644 --- a/script/import_scripts/phpbb3/support/text_processor.rb +++ b/script/import_scripts/phpbb3/support/text_processor.rb @@ -8,6 +8,7 @@ module ImportScripts::PhpBB3 @lookup = lookup @database = database @smiley_processor = smiley_processor + @he = HTMLEntities.new @settings = settings @new_site_prefix = settings.new_site_prefix @@ -25,7 +26,7 @@ module ImportScripts::PhpBB3 process_smilies(text) process_links(text) process_lists(text) - + process_code(text) text end @@ -48,6 +49,9 @@ module ImportScripts::PhpBB3 # [url=https://google.com:1qh1i7ky]click here[/url:1qh1i7ky] # [quote="cybereality":b0wtlzex]Some text.[/quote:b0wtlzex] text.gsub!(/:(?:\w{8})\]/, ']') + + # remove color tags + text.gsub!(/\[\/?color(=#[a-z0-9]*)?\]/i, "") end def bbcode_to_md(text) @@ -142,5 +146,12 @@ module ImportScripts::PhpBB3 @long_internal_link_regexp = Regexp.new(%Q||, Regexp::IGNORECASE) @short_internal_link_regexp = Regexp.new(link_regex, Regexp::IGNORECASE) end + + def process_code(text) + text.gsub!(//, "\n") + text + end end end From be0c7609f1d8e2db8a10cf8b9e504f1ddbe599d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 7 Nov 2017 19:17:33 +0100 Subject: [PATCH 100/445] FIX: validates attachments against current authorized extensions --- lib/email/receiver.rb | 2 +- spec/components/email/receiver_spec.rb | 4 +++ spec/fixtures/emails/attached_txt_file_2.eml | 30 ++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/emails/attached_txt_file_2.eml diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 2510aa1e00..094dda88cd 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -635,7 +635,7 @@ module Email # create the upload for the user opts = { for_group_message: options[:is_group_message] } upload = UploadCreator.new(tmp, attachment.filename, opts).create_for(user_id) - if upload && upload.errors.empty? + if upload&.valid? # try to inline images if attachment.content_type&.start_with?("image/") if raw[attachment.url] diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 0054712aa4..a11e3d9d96 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -381,6 +381,10 @@ describe Email::Receiver do SiteSetting.authorized_extensions = "txt" expect { process(:attached_txt_file) }.to change { topic.posts.count } expect(topic.posts.last.raw).to match(/text\.txt/) + + SiteSetting.authorized_extensions = "csv" + expect { process(:attached_txt_file_2) }.to change { topic.posts.count } + expect(topic.posts.last.raw).to_not match(/text\.txt/) end it "supports liking via email" do diff --git a/spec/fixtures/emails/attached_txt_file_2.eml b/spec/fixtures/emails/attached_txt_file_2.eml new file mode 100644 index 0000000000..4cb6cdc1df --- /dev/null +++ b/spec/fixtures/emails/attached_txt_file_2.eml @@ -0,0 +1,30 @@ +Return-Path: +From: Foo Bar +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +Date: Sat, 30 Jan 2016 01:10:11 +0100 +Message-ID: <38b@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary="--==_mimepart_56abff5d49749_ddf83fca6d033a28548ad"; + charset=UTF-8 +Content-Transfer-Encoding: 7bit + + +----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad +Content-Type: text/plain; + charset=UTF-8; + filename=text.txt +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename=text.txt +Content-ID: <56abff637aac_ddf83fca6d033a2855099@HAL.lan.mail> + +This is a txt file. + +----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad +Content-Type: text/plain; + charset=UTF-8 +Content-Transfer-Encoding: 7bit + +Please find the same text file attached. +----==_mimepart_56abff5d49749_ddf83fca6d033a28548ad-- From 234f0262b8a2e1991ac2eb402b634614f90421c9 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 7 Nov 2017 13:48:35 -0500 Subject: [PATCH 101/445] UX: use down chevron button instead of "show more" text to link to full list of notifications --- .../widgets/user-notifications.js.es6 | 16 +++++++++---- .../stylesheets/common/base/menu-panel.scss | 24 ++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 b/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 index 260bc36146..a334fce948 100644 --- a/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 @@ -1,6 +1,7 @@ import { createWidget } from 'discourse/widgets/widget'; import { headerHeight } from 'discourse/components/site-header'; import { h } from 'virtual-dom'; +import DiscourseURL from 'discourse/lib/url'; export default createWidget('user-notifications', { tagName: 'div.notifications', @@ -74,11 +75,14 @@ export default createWidget('user-notifications', { const items = [notificationItems]; if (notificationItems.length > 5) { - const href = `${attrs.path}/notifications`; - items.push( - h('li.read.last.heading', - h('a', { attributes: { href } }, [I18n.t('notifications.more'), '...'])), + h('li.read.last.heading.show-all', + this.attach('button', { + title: 'notifications.more', + icon: 'chevron-down', + action: 'showAllNotifications', + className: 'btn' + })), h('hr') ); } @@ -87,5 +91,9 @@ export default createWidget('user-notifications', { } return result; + }, + + showAllNotifications() { + DiscourseURL.routeTo(`${this.attrs.path}/notifications`); } }); diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index ae2c954406..6b66d5120e 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -210,12 +210,6 @@ .icon { color: dark-light-choose($primary-high, $secondary-low); } li { background-color: $tertiary-low; - padding: 0.25em 0.5em; - i { - float: left; - margin-right: 5px; - padding-top: 2px; - } // This is until other languages remove the HTML from within // notifications. It can then be removed @@ -231,6 +225,14 @@ overflow: hidden; } } + li:not(.show-all) { + padding: 0.25em 0.5em; + i { + float: left; + margin-right: 5px; + padding-top: 2px; + } + } .is-warning { .d-icon-envelope-o { &:before { @@ -254,6 +256,16 @@ border-width: 2px; margin: 0 auto; } + .show-all .btn { + width: 100%; + padding: 2px 0; + color: dark-light-choose($primary-medium, $secondary-high); + background: blend-primary-secondary(5%); + &:hover { + color: $primary; + background: dark-light-diff($primary, $secondary, 90%, -80%); + } + } /* as a big ol' click target, don't let text inside be selected */ @include unselectable; } From 667b025d123cde7869d6312f9aa4f39ae845492f Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 7 Nov 2017 14:20:46 -0500 Subject: [PATCH 102/445] make rubocop happy --- script/import_scripts/bbpress.rb | 10 +++++----- script/import_scripts/phpbb3/support/text_processor.rb | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/script/import_scripts/bbpress.rb b/script/import_scripts/bbpress.rb index 6d27e950ea..9059270f6f 100644 --- a/script/import_scripts/bbpress.rb +++ b/script/import_scripts/bbpress.rb @@ -47,9 +47,9 @@ class ImportScripts::Bbpress < ImportScripts::Base last_user_id = -1 total_users = bbpress_query(<<-SQL SELECT COUNT(DISTINCT(u.id)) AS cnt - FROM #{BB_PRESS_PREFIX}users u - LEFT JOIN #{BB_PRESS_PREFIX}posts p ON p.post_author = u.id - WHERE p.post_type IN ('forum', 'reply', 'topic') + FROM #{BB_PRESS_PREFIX}users u + LEFT JOIN #{BB_PRESS_PREFIX}posts p ON p.post_author = u.id + WHERE p.post_type IN ('forum', 'reply', 'topic') AND user_email LIKE '%@%' SQL ).first["cnt"] @@ -58,7 +58,7 @@ class ImportScripts::Bbpress < ImportScripts::Base users = bbpress_query(<<-SQL SELECT u.id, user_nicename, display_name, user_email, user_registered, user_url, user_pass FROM #{BB_PRESS_PREFIX}users u - LEFT JOIN #{BB_PRESS_PREFIX}posts p ON p.post_author = u.id + LEFT JOIN #{BB_PRESS_PREFIX}posts p ON p.post_author = u.id WHERE user_email LIKE '%@%' AND p.post_type IN ('forum', 'reply', 'topic') AND u.id > #{last_user_id} @@ -305,7 +305,7 @@ class ImportScripts::Bbpress < ImportScripts::Base topics.each do |t| topic = topic_lookup_from_imported_post_id(t['id']) - Permalink.create( url: URI.parse(t['guid']).path.chomp('/'), topic_id: topic[:topic_id] ) rescue nil + Permalink.create(url: URI.parse(t['guid']).path.chomp('/'), topic_id: topic[:topic_id]) rescue nil end last_topic_id = topics[-1]["id"].to_i end diff --git a/script/import_scripts/phpbb3/support/text_processor.rb b/script/import_scripts/phpbb3/support/text_processor.rb index 561b2c2f69..d89a6cc99f 100644 --- a/script/import_scripts/phpbb3/support/text_processor.rb +++ b/script/import_scripts/phpbb3/support/text_processor.rb @@ -49,7 +49,7 @@ module ImportScripts::PhpBB3 # [url=https://google.com:1qh1i7ky]click here[/url:1qh1i7ky] # [quote="cybereality":b0wtlzex]Some text.[/quote:b0wtlzex] text.gsub!(/:(?:\w{8})\]/, ']') - + # remove color tags text.gsub!(/\[\/?color(=#[a-z0-9]*)?\]/i, "") end @@ -148,7 +148,7 @@ module ImportScripts::PhpBB3 end def process_code(text) - text.gsub!(//, "\n") text From 3f2105db850d16a0a21db2c68bb64c4f252b7f26 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 8 Nov 2017 07:31:45 +1100 Subject: [PATCH 103/445] UX: iOS 11 modals now hide all background This fixes issues where login looks very buggy on iOS see also: https://meta.discourse.org/t/ios-11-makes-discourse-buggy/71140/7 --- .../discourse/lib/show-modal.js.es6 | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/app/assets/javascripts/discourse/lib/show-modal.js.es6 b/app/assets/javascripts/discourse/lib/show-modal.js.es6 index 10e7695b57..c13d390925 100644 --- a/app/assets/javascripts/discourse/lib/show-modal.js.es6 +++ b/app/assets/javascripts/discourse/lib/show-modal.js.es6 @@ -1,7 +1,37 @@ +import { isAppleDevice } from 'discourse/lib/safari-hacks'; + export default function(name, opts) { opts = opts || {}; const container = Discourse.__container__; + + // iOS 11 -> 11.1 have broken INPUTs on position fixed + // if for any reason there is a body higher than 100% behind them. + // What happens is that when INPUTs gets focus they shift the body + // which ends up moving the cursor to an invisible spot + // this makes the login experience on iOS painful, user thinks it is broken. + // + // Also, very little value in showing main outlet and header on iOS + // anyway, so just hide it. + if (isAppleDevice()) { + let pos = $(window).scrollTop(); + $(window) + .off('show.bs.modal.ios-hacks') + .on('show.bs.modal.ios-hacks', () => { + $('#main-outlet, header').hide(); + }); + + $(window) + .off('hide.bs.modal.ios-hacks') + .on('hide.bs.modal.ios-hacks', () => { + $('#main-outlet, header').show(); + $(window).scrollTop(pos); + + $(window).off('hide.bs.modal.ios-hacks'); + $(window).off('show.bs.modal.ios-hacks'); + }); + } + // We use the container here because modals are like singletons // in Discourse. Only one can be shown with a particular state. const route = container.lookup('route:application'); From d7880af0bb38c1eb19f2b509f807b48f47934a7d Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 7 Nov 2017 16:14:47 -0500 Subject: [PATCH 104/445] FIX: change password form validation should instruct admins to use min password length for admin accounts --- .../discourse/controllers/password-reset.js.es6 | 1 + .../discourse/mixins/password-validation.js.es6 | 13 ++++++------- app/controllers/users_controller.rb | 10 +++++++--- spec/controllers/users_controller_spec.rb | 2 +- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 index e7694a2301..91c62436d6 100644 --- a/app/assets/javascripts/discourse/controllers/password-reset.js.es6 +++ b/app/assets/javascripts/discourse/controllers/password-reset.js.es6 @@ -7,6 +7,7 @@ import { userPath } from 'discourse/lib/url'; export default Ember.Controller.extend(PasswordValidation, { isDeveloper: Ember.computed.alias('model.is_developer'), + admin: Ember.computed.alias('model.admin'), passwordRequired: true, errorMessage: null, successMessage: null, diff --git a/app/assets/javascripts/discourse/mixins/password-validation.js.es6 b/app/assets/javascripts/discourse/mixins/password-validation.js.es6 index 7e9caf62dc..b65272e03f 100644 --- a/app/assets/javascripts/discourse/mixins/password-validation.js.es6 +++ b/app/assets/javascripts/discourse/mixins/password-validation.js.es6 @@ -16,13 +16,13 @@ export default Ember.Mixin.create({ return I18n.t('user.password.instructions', {count: this.get('passwordMinLength')}); }, - @computed('isDeveloper') - passwordMinLength() { - return this.get('isDeveloper') ? this.siteSettings.min_admin_password_length : this.siteSettings.min_password_length; + @computed('isDeveloper', 'admin') + passwordMinLength(isDeveloper, admin) { + return (isDeveloper || admin) ? this.siteSettings.min_admin_password_length : this.siteSettings.min_password_length; }, - @computed('accountPassword', 'passwordRequired', 'rejectedPasswords.[]', 'accountUsername', 'accountEmail', 'isDeveloper') - passwordValidation(password, passwordRequired, rejectedPasswords, accountUsername, accountEmail, isDeveloper) { + @computed('accountPassword', 'passwordRequired', 'rejectedPasswords.[]', 'accountUsername', 'accountEmail', 'passwordMinLength') + passwordValidation(password, passwordRequired, rejectedPasswords, accountUsername, accountEmail, passwordMinLength) { if (!passwordRequired) { return InputValidation.create({ ok: true }); } @@ -40,8 +40,7 @@ export default Ember.Mixin.create({ } // If too short - const passwordLength = isDeveloper ? this.siteSettings.min_admin_password_length : this.siteSettings.min_password_length; - if (password.length < passwordLength) { + if (password.length < passwordMinLength) { return InputValidation.create({ failed: true, reason: I18n.t('user.password.too_short') diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index ad304f72a6..a8bea72d5f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -465,7 +465,10 @@ class UsersController < ApplicationController if @error render layout: 'no_ember' else - store_preloaded("password_reset", MultiJson.dump(is_developer: UsernameCheckerService.is_developer?(@user.email))) + store_preloaded( + "password_reset", + MultiJson.dump(is_developer: UsernameCheckerService.is_developer?(@user.email), admin: @user.admin?) + ) end return redirect_to(wizard_path) if request.put? && Wizard.user_requires_completion?(@user) end @@ -477,7 +480,8 @@ class UsersController < ApplicationController success: false, message: @error, errors: @user&.errors.to_hash, - is_developer: UsernameCheckerService.is_developer?(@user.email) + is_developer: UsernameCheckerService.is_developer?(@user.email), + admin: @user.admin? } else render json: { @@ -488,7 +492,7 @@ class UsersController < ApplicationController } end else - render json: { is_developer: UsernameCheckerService.is_developer?(@user.email) } + render json: { is_developer: UsernameCheckerService.is_developer?(@user.email), admin: @user.admin? } end end end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 03490e1b08..8ade5e48a9 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -342,7 +342,7 @@ describe UsersController do ) expect(response).to be_success - expect(response.body).to include('{"is_developer":false}') + expect(response.body).to include('{"is_developer":false,"admin":false}') user.reload From fc7dca58fe5bfa256e57fb15f8c9e1e0a1005f48 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 8 Nov 2017 11:50:01 +1100 Subject: [PATCH 105/445] UX: oneboxes with avatars now display consistently Onebox avatar size is reduced to 60px Also fixes regression with some oneboxes not cooking after post --- Gemfile | 2 +- Gemfile.lock | 6 +++--- app/assets/stylesheets/common/base/onebox.scss | 17 ++++++++++++----- lib/cooked_post_processor.rb | 6 +++++- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index ef79a66d79..aafaf0e805 100644 --- a/Gemfile +++ b/Gemfile @@ -35,7 +35,7 @@ gem 'redis-namespace' gem 'active_model_serializers', '~> 0.8.3' -gem 'onebox', '1.8.22' +gem 'onebox', '1.8.23' gem 'http_accept_language', '~>2.0.5', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 4154eb620e..9451fef5ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -228,7 +228,7 @@ GEM omniauth-twitter (1.3.0) omniauth-oauth (~> 1.1) rack - onebox (1.8.22) + onebox (1.8.23) fast_blank (>= 1.0.0) htmlentities (~> 4.3) moneta (~> 1.0) @@ -464,7 +464,7 @@ DEPENDENCIES omniauth-oauth2 omniauth-openid omniauth-twitter - onebox (= 1.8.22) + onebox (= 1.8.23) openid-redis-store pg pry-nav @@ -505,4 +505,4 @@ DEPENDENCIES webmock BUNDLED WITH - 1.15.4 + 1.16.0 diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index a96e156f07..eb78a65b4d 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -241,8 +241,8 @@ aside.onebox { aside.onebox .onebox-body .onebox-avatar { max-height: none; max-width: none; - height: 90px; - width: 90px; + height: 60px; + width: 60px; } blockquote { @@ -358,8 +358,7 @@ aside.onebox.twitterstatus .onebox-body { } .date { clear: left; - padding-top: 5px; - margin-left: 58px; + padding-top: 10px; } } @@ -397,7 +396,7 @@ aside.onebox.twitterstatus .onebox-body { // resize stackexchange onebox image aside.onebox.stackexchange .onebox-body { - img { + img:not(.onebox-avatar) { max-height: 60%; max-width: 10%; } @@ -443,6 +442,14 @@ aside.onebox.stackexchange .onebox-body { } } +.onebox.githubcommit { + pre.message { + clear: left; + padding: 0; + padding-top: 10px; + } +} + // mobile specific style .mobile-view article.onebox-body { border-top: none; diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 03161dc6fd..781c2cb038 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -326,7 +326,11 @@ class CookedPostProcessor # and wrap in a div oneboxed_images.each do |img| limit_size!(img) - if img.parent["class"].include?("onebox-body") && (width = img["width"].to_i) > 0 && (height = img["height"].to_i) > 0 + + next if img["class"]&.include?('onebox-avatar') + + parent_class = img.parent && img.parent["class"] + if parent_class&.include?("onebox-body") && (width = img["width"].to_i) > 0 && (height = img["height"].to_i) > 0 img.delete('width') img.delete('height') new_parent = img.add_next_sibling("
    ") From 8a8c14c6f7f74819bb5c58b34fdb44ce66ef134f Mon Sep 17 00:00:00 2001 From: ckeboss Date: Tue, 7 Nov 2017 17:04:21 -0800 Subject: [PATCH 106/445] Fix infinate loading of group posts (#5296) --- .../controllers/group-activity-posts.js.es6 | 13 +++++++++++++ .../discourse/routes/group-activity-posts.js.es6 | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 index 991ce0b014..be2dc575f0 100644 --- a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 @@ -1,13 +1,17 @@ +import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; import { fmt } from 'discourse/lib/computed'; export default Ember.Controller.extend({ group: Ember.inject.controller(), groupActivity: Ember.inject.controller(), + application: Ember.inject.controller(), + canLoadMore: true, loading: false, emptyText: fmt('type', 'groups.empty.%@'), actions: { loadMore() { + if (!this.get('canLoadMore')) { return; } if (this.get('loading')) { return; } this.set('loading', true); const posts = this.get('model'); @@ -20,9 +24,18 @@ export default Ember.Controller.extend({ group.findPosts(opts).then(newPosts => { posts.addObjects(newPosts); + if(newPosts.length === 0) { + this.set('canLoadMore', false); + } + }).finally(() => { this.set('loading', false); }); } } + }, + + @observes('canLoadMore') + _showFooter() { + this.set("application.showFooter", !this.get("canLoadMore")); } }); diff --git a/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 index 1127d36f97..827c8c468b 100644 --- a/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 +++ b/app/assets/javascripts/discourse/routes/group-activity-posts.js.es6 @@ -12,7 +12,7 @@ export function buildGroupPage(type) { }, setupController(controller, model) { - this.controllerFor('group-activity-posts').setProperties({ model, type }); + this.controllerFor('group-activity-posts').setProperties({ model, type, canLoadMore: true }); this.controllerFor("group").set("showing", type); }, From cd744d58f84c2bc799fe41db457bdd0397acaf21 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 7 Nov 2017 20:29:41 -0500 Subject: [PATCH 107/445] FIX: Lint error --- .../discourse/controllers/group-activity-posts.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 index be2dc575f0..4047450a1c 100644 --- a/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 +++ b/app/assets/javascripts/discourse/controllers/group-activity-posts.js.es6 @@ -1,4 +1,4 @@ -import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import { observes } from 'ember-addons/ember-computed-decorators'; import { fmt } from 'discourse/lib/computed'; export default Ember.Controller.extend({ From 4bb454d8892ee3bf5461f8a8fcaddc379278d968 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 8 Nov 2017 11:10:20 +0800 Subject: [PATCH 108/445] FIX: JSON custom fields incorrectly being converted to an array. https://meta.discourse.org/t/custom-fields-simultaneous-save-with-json-becomes-an-array/73647 --- app/models/concerns/has_custom_fields.rb | 15 +++++++------ .../concern/has_custom_fields_spec.rb | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/app/models/concerns/has_custom_fields.rb b/app/models/concerns/has_custom_fields.rb index 25a6422b0e..c6e929d8e0 100644 --- a/app/models/concerns/has_custom_fields.rb +++ b/app/models/concerns/has_custom_fields.rb @@ -161,7 +161,7 @@ module HasCustomFields array_fields = {} - _custom_fields.each do |f| + _custom_fields.reload.each do |f| if dup[f.name].is_a? Array # we need to collect Arrays fully before we can compare them if !array_fields.has_key?(f.name) @@ -171,7 +171,7 @@ module HasCustomFields end elsif dup[f.name].is_a? Hash if dup[f.name].to_json != f.value - f.destroy + f.destroy! else dup.delete(f.name) end @@ -180,7 +180,7 @@ module HasCustomFields self.class.append_custom_field(t, f.name, f.value) if dup[f.name] != t[f.name] - f.destroy + f.destroy! else dup.delete(f.name) end @@ -198,11 +198,12 @@ module HasCustomFields dup.each do |k, v| if v.is_a? Array - v.each { |subv| _custom_fields.create(name: k, value: subv) } - elsif v.is_a? Hash - _custom_fields.create(name: k, value: v.to_json) + v.each { |subv| _custom_fields.create!(name: k, value: subv) } else - _custom_fields.create(name: k, value: v) + _custom_fields.create!( + name: k, + value: v.is_a?(Hash) ? v.to_json : v + ) end end diff --git a/spec/components/concern/has_custom_fields_spec.rb b/spec/components/concern/has_custom_fields_spec.rb index fc1f1a43e8..f1bf5933cb 100644 --- a/spec/components/concern/has_custom_fields_spec.rb +++ b/spec/components/concern/has_custom_fields_spec.rb @@ -193,5 +193,27 @@ describe HasCustomFields do expect(fields[item1.id]['not_whitelisted']).to be_blank expect(fields[item2.id]['e']).to eq('hallo') end + + it "handles interleaving saving properly" do + field_type = 'deep-nest-test' + CustomFieldsTestItem.register_custom_field_type(field_type, :json) + test_item = CustomFieldsTestItem.create! + + test_item.custom_fields[field_type] ||= {} + test_item.custom_fields[field_type]['b'] ||= {} + test_item.custom_fields[field_type]['b']['c'] = 'd' + test_item.save_custom_fields(true) + + db_item = CustomFieldsTestItem.find(test_item.id) + db_item.custom_fields[field_type]['b']['e'] = 'f' + test_item.custom_fields[field_type]['b']['e'] = 'f' + expected = { field_type => { 'b' => { 'c' => 'd', 'e' => 'f' } } } + + db_item.save_custom_fields(true) + expect(db_item.reload.custom_fields).to eq(expected) + + test_item.save_custom_fields(true) + expect(test_item.reload.custom_fields).to eq(expected) + end end end From 7777a44673e27a81d5586d0b6dc0d14ee2c98b13 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 8 Nov 2017 11:35:53 +0800 Subject: [PATCH 109/445] FIX: Don't skip validations when updating user's email. --- lib/email_updater.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/email_updater.rb b/lib/email_updater.rb index ddb0627fef..e8515b8645 100644 --- a/lib/email_updater.rb +++ b/lib/email_updater.rb @@ -85,7 +85,7 @@ class EmailUpdater confirm_result = :authorizing_new when EmailChangeRequest.states[:authorizing_new] change_req.update_column(:change_state, EmailChangeRequest.states[:complete]) - user.primary_email.update_column(:email, token.email) + user.primary_email.update!(email: token.email) confirm_result = :complete end else From 0dec3269d84c2233d4fabe6001166d5615e4987a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 8 Nov 2017 11:43:41 +0800 Subject: [PATCH 110/445] Validates presence of `UserEmail#user_id` in AR. --- app/models/user_email.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/user_email.rb b/app/models/user_email.rb index 2186e6522e..8d019853a5 100644 --- a/app/models/user_email.rb +++ b/app/models/user_email.rb @@ -7,6 +7,7 @@ class UserEmail < ActiveRecord::Base before_validation :strip_downcase_email + validates :user_id, presence: true validates :email, presence: true, uniqueness: true validates :email, email: true, format: { with: EmailValidator.email_regex }, From 9abeaa471910bd3919b513077cca7ec329ac4db9 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 8 Nov 2017 11:54:18 +0800 Subject: [PATCH 111/445] Revert "Validates presence of `UserEmail#user_id` in AR." This reverts commit 0dec3269d84c2233d4fabe6001166d5615e4987a. --- app/models/user_email.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/user_email.rb b/app/models/user_email.rb index 8d019853a5..2186e6522e 100644 --- a/app/models/user_email.rb +++ b/app/models/user_email.rb @@ -7,7 +7,6 @@ class UserEmail < ActiveRecord::Base before_validation :strip_downcase_email - validates :user_id, presence: true validates :email, presence: true, uniqueness: true validates :email, email: true, format: { with: EmailValidator.email_regex }, From ed16cba77f3c53d9d21ebf254b7d2d3e130e27b0 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 8 Nov 2017 12:02:33 +0800 Subject: [PATCH 112/445] REFACTOR: Raise error if email token fails to create. --- app/controllers/users_controller.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index a8bea72d5f..22e7358419 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -637,9 +637,9 @@ class UsersController < ApplicationController @user = User.where(id: user_key.to_i).first end - raise Discourse::InvalidAccess.new unless @user.present? - raise Discourse::InvalidAccess.new if @user.active? - raise Discourse::InvalidAccess.new if current_user.present? + if @user.blank? || @user.active? || current_user.present? + raise Discourse::InvalidAccess.new + end User.transaction do primary_email = @user.primary_email @@ -648,7 +648,7 @@ class UsersController < ApplicationController primary_email.skip_validate_email = false if primary_email.save - @user.email_tokens.create(email: @user.email) + @user.email_tokens.create!(email: @user.email) enqueue_activation_email render json: success_json else @@ -688,7 +688,7 @@ class UsersController < ApplicationController end def enqueue_activation_email - @email_token ||= @user.email_tokens.create(email: @user.email) + @email_token ||= @user.email_tokens.create!(email: @user.email) Jobs.enqueue(:critical_user_email, type: :signup, user_id: @user.id, email_token: @email_token.token, to_address: @user.email) end From 6090994cdf00061711e3ecbe13d217e957e06359 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 8 Nov 2017 14:08:30 +0800 Subject: [PATCH 113/445] FEATURE: Retain the latest 30 days of WebHookEvent records by default. --- .../scheduled/purge_old_web_hook_events.rb | 9 +++++ app/models/web_hook_event.rb | 6 ++++ config/locales/client.en.yml | 1 + config/locales/server.en.yml | 2 ++ config/site_settings.yml | 4 +++ spec/models/web_hook_event_spec.rb | 35 +++++++++++++++---- 6 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 app/jobs/scheduled/purge_old_web_hook_events.rb diff --git a/app/jobs/scheduled/purge_old_web_hook_events.rb b/app/jobs/scheduled/purge_old_web_hook_events.rb new file mode 100644 index 0000000000..04e21d09b1 --- /dev/null +++ b/app/jobs/scheduled/purge_old_web_hook_events.rb @@ -0,0 +1,9 @@ +module Jobs + class PurgeOldWebHookEvents < Jobs::Scheduled + every 1.week + + def execute(_) + WebHookEvent.purge_old + end + end +end diff --git a/app/models/web_hook_event.rb b/app/models/web_hook_event.rb index ce2c134bc1..204efba8aa 100644 --- a/app/models/web_hook_event.rb +++ b/app/models/web_hook_event.rb @@ -5,6 +5,12 @@ class WebHookEvent < ActiveRecord::Base default_scope { order('created_at DESC') } + def self.purge_old + where( + 'created_at < ?', SiteSetting.retain_web_hook_events_period_days.days.ago + ).delete_all + end + def update_web_hook_delivery_status web_hook.last_delivery_status = case status diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 6e2a7c6d1d..8e9025d6a6 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3485,6 +3485,7 @@ en: developer: 'Developer' embedding: "Embedding" legal: "Legal" + api: 'API' user_api: 'User API' uncategorized: 'Other' backups: "Backups" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index b31c87349a..e4c0c8fe1f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1556,6 +1556,8 @@ en: default_categories_muted: "List of categories that are muted by default." default_categories_watching_first_post: "List of categories in which first post in each new topic will be watched by default." + retain_web_hook_events_period_days: "Number of days to retain web hook event records." + max_user_api_reqs_per_day: "Maximum number of user API requests per key per day" max_user_api_reqs_per_minute: "Maximum number of user API requests per key per minute" allow_user_api_keys: "Allow generation of user API keys" diff --git a/config/site_settings.yml b/config/site_settings.yml index 5df79b5672..175fb21ff0 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1427,6 +1427,10 @@ user_preferences: type: category_list default: '' +api: + retain_web_hook_events_period_days: + default: 30 + user_api: max_user_api_reqs_per_day: default: 2880 diff --git a/spec/models/web_hook_event_spec.rb b/spec/models/web_hook_event_spec.rb index 5f6a9046bc..b09b3c30ed 100644 --- a/spec/models/web_hook_event_spec.rb +++ b/spec/models/web_hook_event_spec.rb @@ -4,13 +4,36 @@ describe WebHookEvent do let(:event) { WebHookEvent.new(status: 200, web_hook: Fabricate(:web_hook)) } let(:failed_event) { WebHookEvent.new(status: 400, web_hook: Fabricate(:web_hook)) } - it 'update last delivery status for associated WebHook record' do - event.update_web_hook_delivery_status - expect(event.web_hook.last_delivery_status).to eq(WebHook.last_delivery_statuses[:successful]) + describe '.purge_old' do + before do + SiteSetting.retain_web_hook_events_period_days = 1 + end + + it "should be able to purge old web hook events" do + web_hook = Fabricate(:web_hook) + web_hook_event = WebHookEvent.create!(status: 200, web_hook: web_hook) + WebHookEvent.create!(status: 200, web_hook: web_hook, created_at: 2.days.ago) + + expect { described_class.purge_old } + .to change { WebHookEvent.count }.by(-1) + + expect(WebHookEvent.find(web_hook_event.id)).to eq(web_hook_event) + end end - it 'sets last delivery status to failed' do - failed_event.update_web_hook_delivery_status - expect(failed_event.web_hook.last_delivery_status).to eq(WebHook.last_delivery_statuses[:failed]) + describe '#update_web_hook_delivery_status' do + it 'update last delivery status for associated WebHook record' do + event.update_web_hook_delivery_status + + expect(event.web_hook.last_delivery_status) + .to eq(WebHook.last_delivery_statuses[:successful]) + end + + it 'sets last delivery status to failed' do + failed_event.update_web_hook_delivery_status + + expect(failed_event.web_hook.last_delivery_status) + .to eq(WebHook.last_delivery_statuses[:failed]) + end end end From 6def5a344aa30dc426298baa863bb20705910b39 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 8 Nov 2017 14:15:34 +0800 Subject: [PATCH 114/445] Let's be more aggressive with purging old WebHookEvent records. --- app/jobs/scheduled/purge_old_web_hook_events.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/scheduled/purge_old_web_hook_events.rb b/app/jobs/scheduled/purge_old_web_hook_events.rb index 04e21d09b1..4bb8c5b7ba 100644 --- a/app/jobs/scheduled/purge_old_web_hook_events.rb +++ b/app/jobs/scheduled/purge_old_web_hook_events.rb @@ -1,6 +1,6 @@ module Jobs class PurgeOldWebHookEvents < Jobs::Scheduled - every 1.week + every 1.day def execute(_) WebHookEvent.purge_old From b840971b7767a297cdbf66b6896756363259956f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 8 Nov 2017 15:19:45 +0800 Subject: [PATCH 115/445] Convert params hash to a query string instead of a hash when logging. --- config/initializers/100-lograge.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/100-lograge.rb b/config/initializers/100-lograge.rb index f7d199a9e8..eda1638de3 100644 --- a/config/initializers/100-lograge.rb +++ b/config/initializers/100-lograge.rb @@ -13,7 +13,7 @@ if (Rails.env.production? && SiteSetting.logging_provider == 'lograge') || ENV[" params[:files].map!(&:headers) if params[:files] output = { - params: params, + params: params.to_query, database: RailsMultisite::ConnectionManagement.current_db, } From 0bb07d395a9167aefe5d625f14178a4c19d3723a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 8 Nov 2017 10:39:26 +0100 Subject: [PATCH 116/445] REFACTOR: composer to use flexbox --- .../discourse/components/composer-body.js.es6 | 14 +- .../components/composer-editor.js.es6 | 82 +--- .../discourse/controllers/composer.js.es6 | 28 +- .../templates/components/composer-editor.hbs | 26 +- .../templates/components/d-editor.hbs | 26 +- .../discourse/templates/composer.hbs | 257 ++++++----- .../stylesheets/common/base/compose.scss | 267 ++++++++--- app/assets/stylesheets/common/d-editor.scss | 119 +++-- app/assets/stylesheets/desktop/compose.scss | 382 +--------------- app/assets/stylesheets/mobile/compose.scss | 427 +++++------------- 10 files changed, 604 insertions(+), 1024 deletions(-) diff --git a/app/assets/javascripts/discourse/components/composer-body.js.es6 b/app/assets/javascripts/discourse/components/composer-body.js.es6 index 1d193b5257..8df136d372 100644 --- a/app/assets/javascripts/discourse/components/composer-body.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-body.js.es6 @@ -15,6 +15,7 @@ export default Ember.Component.extend(KeyEnterEscape, { 'composer.createdPost:created-post', 'composer.creatingTopic:topic', 'composer.whisper:composing-whisper', + 'showPreview:show-preview:hide-preview', 'currentUserPrimaryGroupClass'], @computed("currentUser.primary_group_name") @@ -41,19 +42,6 @@ export default Ember.Component.extend(KeyEnterEscape, { const h = $('#reply-control').height() || 0; this.movePanels(h + "px"); - - // Figure out the size of the fields - const $fields = this.$('.composer-fields'); - const fieldPos = $fields.position(); - if (fieldPos) { - this.$('.wmd-controls').css('top', $fields.height() + fieldPos.top + 5); - } - - // get the submit panel height - const submitPos = this.$('.submit-panel').position(); - if (submitPos) { - this.$('.wmd-controls').css('bottom', h - submitPos.top + 7); - } }); }, diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 424a7bcd25..8c5dc77475 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -1,5 +1,5 @@ import userSearch from 'discourse/lib/user-search'; -import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; +import { default as computed, on } from 'ember-addons/ember-computed-decorators'; import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions'; import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags'; import { linkSeenTagHashtags, fetchUnseenTagHashtags } from 'discourse/lib/link-tag-hashtag'; @@ -18,11 +18,9 @@ import { lookupCachedUploadUrl, cacheShortUploadUrl } from 'pretty-text/image-short-url'; export default Ember.Component.extend({ - classNames: ['wmd-controls'], - classNameBindings: ['showToolbar:toolbar-visible', ':wmd-controls', 'showPreview', 'showPreview::hide-preview'], + classNameBindings: ['showToolbar:toolbar-visible', ':wmd-controls'], uploadProgress: 0, - showPreview: true, _xhr: null, @computed @@ -30,35 +28,6 @@ export default Ember.Component.extend({ return `[${I18n.t('uploading')}]() `; }, - @on('init') - _setupPreview() { - const val = (this.site.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true')); - this.set('showPreview', val === 'true'); - - this.appEvents.on('composer:show-preview', () => { - this.set('showPreview', true); - }); - - this.appEvents.on('composer:hide-preview', () => { - this.set('showPreview', false); - }); - }, - - @computed('site.mobileView', 'showPreview') - forcePreview(mobileView, showPreview) { - return mobileView && showPreview; - }, - - @computed('showPreview') - toggleText: function(showPreview) { - return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview'); - }, - - @observes('showPreview') - showPreviewChanged() { - this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') }); - }, - @computed markdownOptions() { return { @@ -363,7 +332,7 @@ export default Ember.Component.extend({ }); if (this.site.mobileView) { - this.$(".mobile-file-upload").on("click.uploader", function () { + $("#reply-control .mobile-file-upload").on("click.uploader", function () { // redirect the click on the hidden file input $("#mobile-uploader").click(); }); @@ -373,29 +342,28 @@ export default Ember.Component.extend({ }, _optionsLocation() { - // long term we want some smart positioning algorithm in popup-menu - // the problem is that positioning in a fixed panel is a nightmare - // cause offsetParent can end up returning a fixed element and then - // using offset() is not going to work, so you end up needing special logic - // especially since we allow for negative .top, provided there is room on screen - const myPos = this.$().position(); - const buttonPos = this.$('.options').position(); + const composer = $("#reply-control"); + const composerOffset = composer.offset(); + const composerPosition = composer.position(); - const popupHeight = $('#reply-control .popup-menu').height(); - const popupWidth = $('#reply-control .popup-menu').width(); + const buttonBarOffset = $('#reply-control .d-editor-button-bar').offset(); + const optionsButton = $('#reply-control .d-editor-button-bar .options'); - var top = myPos.top + buttonPos.top - 15; - var left = myPos.left + buttonPos.left - (popupWidth/2); + const popupMenu = $("#reply-control .popup-menu"); + const popupWidth = popupMenu.outerWidth(); + const popupHeight = popupMenu.outerHeight(); - const composerPos = $('#reply-control').position(); + const headerHeight = $(".d-header").outerHeight(); - if (composerPos.top + top - popupHeight < 0) { - top = top + popupHeight + this.$('.options').height() + 50; + let left = optionsButton.offset().left - composerOffset.left; + let top = buttonBarOffset.top - composerOffset.top - popupHeight + popupMenu.innerHeight(); + + if (top + composerPosition.top - headerHeight - popupHeight < 0) { + top += popupHeight + optionsButton.outerHeight(); } - var replyWidth = $('#reply-control').width(); - if (left + popupWidth > replyWidth) { - left = replyWidth - popupWidth - 40; + if (left + popupWidth > composer.width()) { + left -= popupWidth - optionsButton.outerWidth(); } return { position: "absolute", left, top }; @@ -493,7 +461,7 @@ export default Ember.Component.extend({ @on('willDestroyElement') _unbindUploadTarget() { this._validUploads = 0; - this.$(".mobile-file-upload").off("click.uploader"); + $("#reply-control .mobile-file-upload").off("click.uploader"); this.messageBus.unsubscribe("/uploads/composer"); const $uploadTarget = this.$(); try { $uploadTarget.fileupload("destroy"); } @@ -504,8 +472,6 @@ export default Ember.Component.extend({ @on('willDestroyElement') _composerClosed() { this.appEvents.trigger('composer:will-close'); - this.appEvents.off('composer:show-preview'); - this.appEvents.off('composer:hide-preview'); Ember.run.next(() => { $('#main-outlet').css('padding-bottom', 0); // need to wait a bit for the "slide down" transition of the composer @@ -541,12 +507,12 @@ export default Ember.Component.extend({ } }, - showUploadModal(toolbarEvent) { - this.sendAction('showUploadSelector', toolbarEvent); + togglePreview() { + this.sendAction('togglePreview'); }, - togglePreview() { - this.toggleProperty('showPreview'); + showUploadModal(toolbarEvent) { + this.sendAction('showUploadSelector', toolbarEvent); }, extraButtons(toolbar) { diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 506bee7e02..e33fe3a256 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -2,7 +2,7 @@ import DiscourseURL from 'discourse/lib/url'; import Quote from 'discourse/lib/quote'; import Draft from 'discourse/models/draft'; import Composer from 'discourse/models/composer'; -import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators'; import InputValidation from 'discourse/models/input-validation'; import { getOwner } from 'discourse-common/lib/get-owner'; import { escapeExpression } from 'discourse/lib/utilities'; @@ -68,9 +68,27 @@ export default Ember.Controller.extend({ isUploading: false, topic: null, linkLookup: null, + showPreview: true, + forcePreview: Ember.computed.and('site.mobileView', 'showPreview'), whisperOrUnlistTopic: Ember.computed.or('model.whisper', 'model.unlistTopic'), categories: Ember.computed.alias('site.categoriesList'), + @on('init') + _setupPreview() { + const val = (this.site.mobileView ? false : (this.keyValueStore.get('composer.showPreview') || 'true')); + this.set('showPreview', val === 'true'); + }, + + @computed('showPreview') + toggleText: function(showPreview) { + return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview'); + }, + + @observes('showPreview') + showPreviewChanged() { + this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') }); + }, + @computed('model.replyingToTopic', 'model.creatingPrivateMessage', 'model.targetUsernames') focusTarget(replyingToTopic, creatingPM, usernames) { if (this.capabilities.isIOS) { return "none"; } @@ -206,6 +224,10 @@ export default Ember.Controller.extend({ actions: { + togglePreview() { + this.toggleProperty('showPreview'); + }, + typed() { this.checkReplyLength(); this.get('model').typing(); @@ -291,10 +313,6 @@ export default Ember.Controller.extend({ return false; }, - togglePreview() { - this.get('model').togglePreview(); - }, - // Import a quote from the post importQuote(toolbarEvent) { const postStream = this.get('topic.postStream'); diff --git a/app/assets/javascripts/discourse/templates/components/composer-editor.hbs b/app/assets/javascripts/discourse/templates/components/composer-editor.hbs index 3d3f6fc8eb..bf6dcecd21 100644 --- a/app/assets/javascripts/discourse/templates/components/composer-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/composer-editor.hbs @@ -13,28 +13,6 @@ forcePreview=forcePreview composerEvents=true}} -
    - {{#if site.mobileView}} +{{#if site.mobileView}} - {{i18n 'upload'}} - - {{#if showPreview}} - {{d-button action='togglePreview' class='hide-preview' label='composer.hide_preview'}} - {{/if}} - {{else}} - {{{toggleText}}} - {{/if}} - - {{#if isUploading}} -
    - {{loading-spinner size="small"}} {{i18n 'upload_selector.uploading'}} - {{uploadProgress}}% - {{#if isCancellable}} - {{d-icon "times"}} - {{/if}} -
    - {{/if}} -
    - {{draftStatus}} -
    -
    +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs index 961b63c6ef..c928412ebc 100644 --- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs @@ -1,4 +1,5 @@ +
    {{#d-editor-modal class="insert-link" hidden=insertLinkHidden okAction="insertLink"}}

    {{i18n "composer.link_dialog_title"}}

    @@ -8,19 +9,18 @@
    -
    - {{#each toolbar.groups as |group|}} - {{#each group.buttons as |b|}} - {{d-button action=b.action actionParam=b translatedTitle=b.title label=b.label icon=b.icon class=b.className}} - {{/each}} - {{#unless group.lastGroup}} -
    - {{/unless}} - {{/each}} -
    -
    -
    +
    + {{#each toolbar.groups as |group|}} + {{#each group.buttons as |b|}} + {{d-button action=b.action actionParam=b translatedTitle=b.title label=b.label icon=b.icon class=b.className}} + {{/each}} + {{#unless group.lastGroup}} +
    + {{/unless}} + {{/each}} +
    + {{conditional-loading-spinner condition=loading}} {{textarea tabindex=tabindex value=value class="d-editor-input" placeholder=placeholderTranslated}} {{popup-input-tip validation=validation}} @@ -30,7 +30,7 @@
    {{{preview}}}
    - {{plugin-outlet name="editor-preview"}} + {{plugin-outlet name="editor-preview" classNames="d-editor-plugin"}}
    diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 98ad6257f1..4cf90be63b 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -1,11 +1,11 @@ -{{#composer-body - composer=model - openIfDraft="openIfDraft" - typed="typed" - cancelled="cancelled" - save="save"}} +{{#composer-body composer=model + showPreview=showPreview + openIfDraft="openIfDraft" + typed="typed" + cancelled="cancelled" + save="save"}} + {{#if visible}} -
    {{#if showPopupMenu}} {{#popup-menu visible=optionsVisible hide="hideOptions" title="composer.options"}} @@ -19,134 +19,169 @@ {{/popup-menu}} {{/if}} - {{composer-messages composer=model messageCount=messageCount addLinkLookup="addLinkLookup"}} + {{composer-messages composer=model + messageCount=messageCount + addLinkLookup="addLinkLookup"}} -
    - {{composer-toggles - composeState=model.composeState - toggleComposer=(action "toggle") - toggleToolbar=(action "toggleToolbar")}} + {{composer-toggles composeState=model.composeState + toggleComposer=(action "toggle") + toggleToolbar=(action "toggleToolbar")}} - {{#if model.viewOpen}} -
    -
    - {{plugin-outlet name="composer-open" args=(hash model=model)}} + {{#if model.viewOpen}} -
    - {{{model.actionTitle}}} - {{#unless site.mobileView}} +
    + +
    + + {{plugin-outlet name="composer-open" args=(hash model=model)}} + +
    + {{{model.actionTitle}}} + + {{#unless site.mobileView}} {{#if whisperOrUnlistTopicText}} ({{whisperOrUnlistTopicText}}) {{/if}} - {{/unless}} + {{/unless}} - {{#if canEdit}} - {{#if showEditReason}} -
    - {{text-field autofocus="true" value=editReason tabindex="7" id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}} -
    - {{else}} - {{i18n 'composer.show_edit_reason'}} - {{/if}} + {{#if canEdit}} + {{#if showEditReason}} + {{text-field autofocus="true" value=editReason tabindex="7" id="edit-reason" maxlength="255" placeholderKey="composer.edit_reason_placeholder"}} + {{else}} + {{i18n 'composer.show_edit_reason'}} {{/if}} -
    + {{/if}} +
    - {{#if model.canEditTitle}} -
    - {{#if model.creatingPrivateMessage}} - {{composer-user-selector topicId=topicModel.id - usernames=model.targetUsernames - hasGroups=model.hasTargetGroups - focusTarget=focusTarget}} - {{#if showWarning}} -
    - -
    - {{/if}} - {{/if}} - - {{composer-title composer=model lastValidatedAt=lastValidatedAt focusTarget=focusTarget}} - - {{#if model.showCategoryChooser}} -
    - {{category-chooser value=model.categoryId scopedCategoryId=scopedCategoryId tabindex="3"}} - {{popup-input-tip validation=categoryValidation}} -
    - {{#if model.archetype.hasOptions}} - {{d-button action="showOptions" label="topic.options"}} - {{/if}} + {{#if model.canEditTitle}} + {{#if model.creatingPrivateMessage}} +
    + {{composer-user-selector topicId=topicModel.id + usernames=model.targetUsernames + hasGroups=model.hasTargetGroups + focusTarget=focusTarget + class="users-input"}} + {{#if showWarning}} + {{/if}}
    {{/if}} - {{plugin-outlet name="composer-fields" args=(hash model=model)}} -
    - {{composer-editor topic=topic - composer=model - lastValidatedAt=lastValidatedAt - canWhisper=canWhisper - showPopupMenu=showPopupMenu - draftStatus=model.draftStatus - isUploading=isUploading - groupsMentioned="groupsMentioned" - cannotSeeMention="cannotSeeMention" - importQuote="importQuote" - showOptions="showOptions" - hideOptions="hideOptions" - optionsVisible=optionsVisible - showToolbar=showToolbar - showUploadSelector="showUploadSelector" - afterRefresh="afterRefresh"}} +
    - {{#if currentUser}} -
    - {{plugin-outlet name="composer-fields-below" args=(hash model=model)}} - {{#if canEditTags}} - {{tag-chooser tags=model.tags tabIndex="4" categoryId=model.categoryId}} + {{composer-title composer=model lastValidatedAt=lastValidatedAt focusTarget=focusTarget}} + + {{#if model.showCategoryChooser}} +
    + {{category-chooser value=model.categoryId scopedCategoryId=scopedCategoryId tabindex="3"}} + {{popup-input-tip validation=categoryValidation}} +
    + {{#if model.archetype.hasOptions}} + {{d-button action="showOptions" label="topic.options"}} + {{/if}} {{/if}} - {{composer-save-button - action=(action "save") - icon=model.saveIcon - label=model.saveLabel - disableSubmit=disableSubmit}} - {{i18n 'cancel'}} +
    + {{/if}} - {{#if site.mobileView}} + {{plugin-outlet name="composer-fields" args=(hash model=model)}} + +
    + + {{composer-editor topic=topic + composer=model + lastValidatedAt=lastValidatedAt + canWhisper=canWhisper + showPopupMenu=showPopupMenu + draftStatus=model.draftStatus + isUploading=isUploading + isCancellable=isCancellable + uploadProgress=uploadProgress + groupsMentioned="groupsMentioned" + cannotSeeMention="cannotSeeMention" + importQuote="importQuote" + showOptions="showOptions" + hideOptions="hideOptions" + optionsVisible=optionsVisible + togglePreview="togglePreview" + showToolbar=showToolbar + showUploadSelector="showUploadSelector" + afterRefresh="afterRefresh"}} + +
    + {{plugin-outlet name="composer-fields-below" args=(hash model=model)}} + + {{#if canEditTags}} + {{tag-chooser tags=model.tags tabIndex="4" categoryId=model.categoryId}} + {{/if}} + +
    + {{composer-save-button action=(action "save") + icon=model.saveIcon + label=model.saveLabel + disableSubmit=disableSubmit}} + + {{i18n 'cancel'}} + + {{#if site.mobileView}} {{#if whisperOrUnlistTopic}} {{d-icon "eye-slash"}} {{/if}} - {{/if}} -
    - {{/if}} -
    - {{else}} -
    -
    -
    - {{#if model.createdPost}} - {{i18n 'composer.saved'}} - {{else}} - {{i18n 'composer.saving'}} {{loading-spinner size="small"}} - {{/if}} -
    -
    - {{#if model.topic}} - {{d-icon "mail-forward"}} {{{draftTitle}}} - {{else}} - {{i18n "composer.saved_draft"}} - {{/if}} + {{/if}} + + + {{#if isUploading}} +
    + {{loading-spinner size="small"}} {{i18n 'upload_selector.uploading'}} + {{uploadProgress}}% + {{#if isCancellable}} + {{d-icon "times"}} + {{/if}} +
    + {{/if}} +
    + {{model.draftStatus}}
    -
    - {{/if}} -
    -
    +
    + {{#if site.mobileView}} + {{i18n 'upload'}} + + {{#if showPreview}} + {{d-button action='togglePreview' class='hide-preview' label='composer.hide_preview'}} + {{/if}} + {{else}} + {{{toggleText}}} + {{/if}} + +
    +
    +
    + + {{else}} +
    + {{#if model.createdPost}} + {{i18n 'composer.saved'}} + {{else}} + {{i18n 'composer.saving'}} {{loading-spinner size="small"}} + {{/if}} +
    + +
    + {{#if model.topic}} + {{d-icon "mail-forward"}} {{{draftTitle}}} + {{else}} + {{i18n "composer.saved_draft"}} + {{/if}} +
    + {{/if}} + {{/if}} + {{/composer-body}} diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 35bac3fdca..4d14bff26d 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -1,9 +1,196 @@ +#reply-control { + background-color: $primary-low; + border-top: 1px solid $primary-low; + bottom: 0; + height: 0; + position: fixed; + display: flex; + flex-direction: column; + transition: height 250ms ease; + width: 100%; + z-index: 999; + + .toggler, + .toggle-toolbar { + position: absolute; + color: $primary-medium; + } + + .toggler { + right: 1px; + } + + .toggle-toolbar { + right: 30px; + } + + .saving-text, + .draft-text { + display: none; + padding-left: 10px; + .spinner { + margin-left: 5px; + } + i { + color: $primary-medium; + } + } + + &.open { + height: 300px; + &.edit-title { + height: 400px; // more room when editing the title + } + } + + &.closed { + height: 0 !important; + } + + &.draft, + &.saving { + height: 40px !important; + justify-content: center; + } + + &.draft { + cursor: pointer; + display: flex; + .draft-text { + display: block; + } + .grippie, .saving-text { + display: none; + } + .toggler { + right: 10px; + } + } + + &.saving .saving-text { + display: flex; + } + + input[type="text"] { + border: 0; + } + + .reply-to { + color: $tertiary; + margin-bottom: 10px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + i { + color: $primary-medium; + } + } + + .whisper, + .display-edit-reason { + font-style: italic; + color: $primary-medium !important; + } + + #edit-reason { + margin: 4px; + } + + .user-selector, + .title-and-category { + display: flex; + align-items: center; + margin-bottom: 10px; + position: relative; + } + + .title-input { + position: relative; + .popup-tip {margin-top: -30px;} + } + + .add-warning { + margin-left: 1em; + } + + #reply-title { + width: 400px; + padding: 6px 10px; + margin: 0 10px 0 0; + } + + .tag-chooser { + margin: 5px 0 0 0; + flex-basis: 30%; + } + + .wmd-controls { + position: relative; + display: flex; + flex-direction: column; + flex-grow: 1; + min-height: 0; + } + + .d-editor-button-bar .btn { + padding: 0 6px; + } + + .save-or-cancel { + margin-top: 5px; + flex-basis: 50%; + display: flex; + align-items: center; + #draft-status, + #file-uploading { + margin-left: 25px; + } + #file-uploading { + display: flex; + .spinner { + margin-right: 5px; + } + } + } + + .composer-bottom-right { + flex-basis: 50%; + display: flex; + flex-direction: row-reverse; + .toggle-preview { + order: 2; + } + } + + .with-tags { + .composer-bottom-right { + flex-basis: 60%; + margin-left: auto; + } + .save-or-cancel { + order: 3; + } + } + + .create { + margin-right: 5px; + } + + #draft-status, #file-uploading { + color: dark-light-choose($primary-medium, $secondary-medium); + } +} + +.cooked > *:first-child { + margin-top: 0; +} + .autocomplete { z-index: 999999; position: absolute; width: 240px; background-color: $secondary; - border: 1px solid $primary-low; + border: 1px solid $primary-low; ul { list-style: none; padding: 0; @@ -15,7 +202,7 @@ padding: 0 2px; } - border-bottom: 1px solid $primary-low; + border-bottom: 1px solid $primary-low; a { padding: 5px; @@ -35,7 +222,7 @@ background-color: $tertiary-low; } @include hover { - background-color: dark-light-choose($highlight-low, $highlight-medium); + background-color: $highlight-low; text-decoration: none; } } @@ -43,36 +230,6 @@ } } -#reply-control { - .d-editor-textarea-wrapper .spinner { - z-index: 1000; - margin-top: 5em; - } - - .d-editor-button-bar { - -moz-box-sizing: border-box; - box-sizing: border-box; - margin: 0px; - padding: 5px; - border-bottom: 2px solid $primary-low; - height: 33px; - .btn:hover { - color: $primary-low; - } - } - - textarea { - box-shadow: none; - } -} - - -.saving-text .spinner { - display: inline-block; - left: 5px; - top: 4px; -} - div.ac-wrap.disabled { input { display:none; @@ -90,7 +247,7 @@ div.ac-wrap div.item a.remove, .remove-link { border-radius: 12px; width: 10px; display: inline-block; - border: 1px solid $primary-low; + border: 1px solid $primary-low; &:hover { background-color: $danger-low; border: 1px solid $danger-medium; @@ -103,7 +260,7 @@ div.ac-wrap { overflow: auto; max-height: 150px; background-color: $secondary; - border: 1px solid $primary-low; + border: 1px solid $primary-low; padding: 5px 4px 1px 4px; div.item { float: left; @@ -140,43 +297,3 @@ div.ac-wrap { color: lighten($primary, 40%); } } - -#reply-control { - .composer-loading { - position: absolute; - left: 48%; - top: 20%; - } - .whisper { - margin-left: 1em; - font-style: italic; - } -} - -#reply-control.topic-featured-link-only.open { - .wmd-controls { display: none; } -} - -#cancel-file-upload { - font-size: 1.6em; -} - -#draft-status, #file-uploading { - color: dark-light-choose($primary-medium, $secondary-medium); -} - -.composer-bottom-right { - .spinner.small { - width: 6px; - height: 6px; - } -} - -// this removes the topmost margin from the first element in the topic post -// if we don't do this, all posts would have extra space at the top -.d-editor-preview > *:first-child { - margin-top: 0; -} -.cooked > *:first-child { - margin-top: 0; -} diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index b1a97021b0..9d016e9a80 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -1,9 +1,6 @@ -.d-editor { - border: 1px solid $primary-low; -} - .d-editor-container { - padding: 0 10px 13px 10px; + display: flex; + flex-grow: 1; } .d-editor-overlay { @@ -18,6 +15,12 @@ z-index: 1001; } +.d-editor { + display: flex; + flex-grow: 1; + min-height: 0; +} + .d-editor .d-editor-modal { min-width: 400px; position: absolute; @@ -34,18 +37,35 @@ } } +.d-editor-textarea-wrapper, +.d-editor-preview-wrapper { + flex: 1; +} + +.d-editor-textarea-wrapper { + display: flex; + flex-direction: column; + background-color: $secondary; + position: relative; +} + +.d-editor-preview-wrapper { + margin-left: 5px; + max-width: 50%; + display: flex; + flex-direction: column; + background: $secondary; +} + .d-editor-button-bar { - margin: 5px; - padding: 0; - height: 20px; - overflow: hidden; + display: flex; + align-items: center; + border-bottom: 2px solid $primary-low; + min-height: 30px; button { background-color: transparent; - padding: 2px 4px; - float: left; - margin-right: 6px; - min-width: 20px; + color: $primary-medium; } .btn:not(.no-text) { @@ -59,61 +79,62 @@ .btn.italic { font-style: italic; } - - .btn.heading { - font-family: Palatino, Cambria, Georgia, "Times New Roman", serif; - font-weight: bold; - } } .d-editor-spacer { width: 1px; height: 20px; - margin-right: 8px; - margin-left: 5px; + margin: 0 5px; background-color: $primary-low; display: inline-block; - float: left; +} + +.d-editor-input, +.d-editor-preview { + box-shadow: none; + box-sizing: border-box; + height: 100%; + margin: 0; + min-height: auto; + width: 100%; + word-wrap: break-word; + flex-grow: 1; + &:focus { + box-shadow: none; + border: 0; + outline: 0; + } +} + +.d-editor-plugin { + display: flex; + overflow: auto; } .d-editor-input { - color: $primary; - width: 98%; - height: 200px; - - &:disabled { - background-color: $primary-low; - } + border: 0; + padding: 7px; + overflow-x: hidden; } .d-editor-preview { - color: $primary; border: 1px dashed $primary-low; overflow: auto; cursor: default; - margin-top: 8px; - padding: 8px 8px 0 8px; - video { - max-width: 100%; - max-height: 500px; - height: auto; - } - audio { - max-width: 100%; - } - &.hidden { - width: 0; - visibility: hidden; - } - + background-color: $secondary; + padding: 7px; } -.composing-whisper { - .d-editor-preview { - font-style: italic; - color: $primary-medium !important; - } + +.composing-whisper .d-editor-preview { + font-style: italic; + color: $primary-medium !important; } .d-editor-preview > *:first-child { margin-top: 0; } + +.hide-preview .d-editor-preview-wrapper { + display: none; + flex: 0; +} diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index ea905523cd..33f5b47e9a 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -1,19 +1,3 @@ -// styles that apply to the reply pane that slides up to compose replies - -// hack, this needs to be done cleaner -#private-message-users { - width: 400px; - float: left; -} - -.add-warning { - width: 300px; - display: inline-block; - position: relative; - top: -30px; - margin-left: 20px; -} - .composer-popup-container { max-width: 1500px; margin-left: auto; @@ -29,12 +13,11 @@ left: 50%; // The composer goes off-center at 1550px @media (min-width: 1550px) { - left: calc(50% - 15px); + left: calc(50%); } overflow-y: auto; z-index: 110; padding: 10px 10px 35px 10px; - box-shadow: 3px 3px 3px rgba(0,0,0, 0.34); background: $highlight-medium; @@ -59,7 +42,7 @@ right: 10px; bottom: 10px; color: $primary; - padding: 6px 4px 2px 4px; + opacity: 0.5; font-size: 1.071em; &:before { content: 'esc'; @@ -85,6 +68,10 @@ } } +.d-editor-container { + max-width: 100%; +} + .custom-body { background-color: $tertiary-low; p { @@ -123,354 +110,27 @@ width: calc(50% - 65px); } -// only disabled when the device is touch-only -.touch.mobile-device #reply-control.open .grippie { - display: none; -} - #reply-control { - .toggle-preview, #draft-status, #file-uploading { - position: absolute; - bottom: -31px; - margin-top: 0; - } - .toggle-preview { - right: 5px; - } - #draft-status, #file-uploading { - right: 51%; - } - #file-uploading { - font-size: 0.857em; - } - transition: height 0.4s ease; - width: 100%; - z-index: 999; - height: 0; - background-color: $primary-low; - bottom: 0; - font-size: 1em; - position: fixed; - .toggler { - right: 1px; - position: absolute; - i { font-size: 1.1em; } - color: dark-light-choose($primary-medium, $secondary-medium); - padding: 0 10px 5px 10px; - } - a.cancel { - padding-left: 7px; - } - .control-row { - margin: 0 5px 0 5px; - } - .saving-text { - display: none; - } - .draft-text { - display: none; - } - .grippie { - display: none; - } - // The various states - &.open { - height: 300px; - .grippie { - display: block; - } - } - &.closed { - height: 0 !important; - } - &.draft { - height: 40px !important; - cursor: pointer; - border-top: 1px solid $primary-low; - .draft-text { - display: block; - - i { - color: dark-light-choose($primary-medium, $secondary-medium); - } - } - } - &.saving { - height: 40px !important; - border-top: 1px solid $primary-low; - .saving-text { - display: block; - } - } .reply-area { max-width: 1500px; - margin-left: auto; - margin-right: auto; - float: none; - .ac-wrap { - padding-left: 10px; - } - } - - // When the post is new (new topic) the sizings are different - &.edit-title { - - &.open { - height: 400px; - } - .contents { - input#reply-title { - padding: 7px 10px; - margin-bottom: 0; - color: $primary; - width: 400px; - } - .wmd-controls { - transition: top 0.3s ease; - top: 100px; - } - } - } - .contents { - padding-left: 10px; - padding-top: 5px; - min-width: 1280px; - .form-element { - position: relative; - display: inline-block; - - .category-input .category-chooser { - width: 430px; - @include medium-width { width: 285px; } - @include small-width { width: 220px; } - - .select-box-kit-header { - padding: 7px 10px; - } - } - } - .edit-reason-input { - display: inline-block; - position: absolute; - margin-left: 10px; - top: 18px; - #edit-reason { - margin: -7px 0 0; - padding: 5px; - float: left; - width: 300px; - } - } - #reply-title { - color: $primary; - margin-right: 5px; - float: left; - &:disabled { - background-color: $primary-low; - } - } - #topic-featured-link { - padding: 7px 10px; - margin: 6px 10px 3px 0; - width: 400px; - } - .d-editor-input:disabled { - background-color: $primary-low; - } - .d-editor-input, .d-editor-preview { - color: $primary; - } - - .d-editor-preview { - border: 1px dashed $primary-low; - overflow: auto; - visibility: visible; - cursor: default; - video { - max-width: 100%; - max-height: 500px; - height: auto; - } - audio { - max-width: 100%; - } - &.hidden { - width: 0; - visibility: hidden; - } - } - .d-editor-input { - bottom: 35px; - } - - .submit-panel { - width: 28%; - position: absolute; - display: block; - bottom: 8px; - } - .future-date-input .examples { - margin-top: 0; - padding-bottom: 8px; - } - } - .title-input, .category-input, .show-admin-options { - display: inline; - float: left; - } - .show-admin-options { - vertical-align: top; - margin-top: 8px; - background: $primary-low; - &:hover { - color: $secondary; - background: $primary; - } - } - .title-input .popup-tip { - width: 300px; - margin-top: 8px; - left: 150px; - } - .category-input .popup-tip { - width: 240px; - left: 432px; - top: -19px; - } -} - -.reply-to { - margin-bottom: 10px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - color: $tertiary; - - @media (min-width: 1101px) { - max-width: 80%; - } - @media (max-width: 1100px) { - max-width: 500px; - } - @media (max-width: 600px) { - max-width: 300px; - } - - i { - color: dark-light-choose($primary-medium, $secondary-medium); - } -} - -#reply-control { - .wmd-controls.hide-preview { - .d-editor-input { - width: 100%; - } - .d-editor-button-bar { - width: 100%; - } - .d-editor-preview-wrapper { - display: none; - } - .d-editor-textarea-wrapper { - width: 100%; - } - } - - .wmd-controls { - left: 30px; - right: 30px; - position: absolute; - bottom: 48px; - top: 50px; - - - .d-editor-input, .d-editor-preview { - -moz-box-sizing: border-box; - box-sizing: border-box; - width: 100%; - height: 100%; - min-height: auto; - padding: 7px; - margin: 0; - background-color: $secondary; - word-wrap: break-word; - } - .d-editor-input, .d-editor-preview-header { - position: absolute; - left: 0; - top: 0; - border: 0px; - border-top: 30px solid transparent; - @include border-radius-all(0); - } - .d-editor-preview-header { - font-size: 0.929em; - line-height: 18px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - overflow: scroll; - .marker, .caret { - display: inline-block; - vertical-align: top; - } - } - .d-editor, .d-editor-container, .d-editor-textarea-wrapper, .d-editor-preview-wrapper { - position: relative; - -moz-box-sizing: border-box; - box-sizing: border-box; - height: 100%; - min-height: 100%; - margin: 0; - padding: 0; - } - .d-editor-textarea-wrapper { - width: 50%; - padding-right: 5px; - float: left; - .popup-tip { - margin-top: 3px; - right: 4px; - } - } - .d-editor-preview-wrapper { - width: 50%; - padding-left: 5px; - float: right; - } - } - .d-editor-button-bar { - top: 0; - position: absolute; - color: dark-light-choose($primary-medium, $secondary-medium); - background-color: $secondary; - z-index: 100; - overflow: hidden; - width: 50%; - - -moz-box-sizing: border-box; + margin: 0 auto; + padding: 0 10px 10px 10px; box-sizing: border-box; - - button { - color: dark-light-choose($primary-high, $secondary-high); + height: calc(100% - 11px); + width: 100%; + display: flex; + flex-direction: column; + .submit-panel { + display: flex; + flex-wrap: wrap; + align-items: center; + flex-shrink: 0; } } } -#reply-control.topic-featured-link-only.open { - height: 200px; -} - -.control-row.reply-area { - padding-left: 20px; - padding-right: 20px; -} - -@media all and (min-width: 1550px) { - #reply-control { - .wmd-controls { - width: 1450px; - left: auto; - right: auto; - } - } +#draft-status, +#file-uploading { + flex-grow: 1; + text-align: right; } diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index 00ff9c9c73..ff85b35ccd 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -1,10 +1,3 @@ -// styles that apply to the reply pane that slides up to compose replies - -// hack, this needs to be done cleaner -.private-message input.span8 { - width: 47%; -} - .composer-popup-container { display: none !important; // can be removed if inline JS CSS is removed from composer-popup } @@ -13,338 +6,142 @@ display: none !important; // can be removed if inline JS CSS is removed from composer-popup } -input { - background: $secondary; - color: $primary; - padding: 4px; - border-radius: 3px; - box-shadow: inset 0 1px 1px rgba(0,0,0, .3); - border: 1px solid $primary-low; -} - #reply-control { - input[type="text"] { - box-shadow: none; - -webkit-appearance: none; - } -} - -textarea { - box-shadow: none; - -webkit-appearance: none; -} - -input[type=radio], input[type=checkbox] { - box-shadow: none; -} - -#reply-control { - &.no-transition { - transition: none; - } -} - - -#reply-control { - // used for upload link - .composer-bottom-right { - float: right; - line-height: 3em; - } - #mobile-uploader { display: none; } - .mobile-file-upload.hidden { display: none; } - #draft-status, #file-uploading, .mobile-file-upload { display: inline-block; } - transition: height .4s ease-in-out; - width: 100%; - z-index: 1039; - height: 0; - background-color: $primary-low; - bottom: 0; - font-size: 1em; - position: fixed; - .toggle-toolbar, .toggler { - right: 1px; - position: absolute; - font-size: 1.071em; - color: dark-light-choose($primary-medium, $secondary-medium); - } - - .toggle-toolbar { - right: 30px; - } - - a.cancel { - padding-left: 7px; - line-height: 30px; - } - .control-row { - margin: 0 5px 0 5px; - .reply-to { - overflow: hidden; - max-width: 80%; - white-space: nowrap; - i { - color: dark-light-choose($primary-medium, $secondary-medium); - } + z-index: 1002; //d-header is 1001 + .reply-area { + padding: 0 5px; + display: flex; + flex-direction: column; + flex-grow: 1; } - } - .saving-text { - display: none; - } - .draft-text { - display: none; - } - .grippie { - display: none; - } - // The various states - &.open { - max-height: 100%; // ensure no overflow e.g. on small Android - height: 270px; - } - &.closed { - height: 0 !important; - } - &.draft { - height: 35px !important; - cursor: pointer; - border-top: 1px solid $primary-low; - .draft-text { - display: block; - position: absolute; - margin-right: 40px; - max-width: 80%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - i { - color: dark-light-choose($primary-medium, $secondary-medium); - } - } - } - &.saving { - height: 40px !important; - border-top: 1px solid $primary-low; - .saving-text { - display: block; - } - } - - // if this is a new topic, make room for the category field in the editor on - // a small screen mobile device - &.edit-title { &.open { - max-height: 100%; // ensure no overflow e.g. on small Android height: 250px; - } - .contents { - input#reply-title { - padding: 5px; - margin-top: 6px; - width: 100%; - box-sizing: border-box; - border: 1px solid $secondary; + &.edit-title { + height: 100%; } - .category-input { - margin-top: 3px; + } - .category-chooser { - width: 100%; + .reply-to { + margin: 5px; + max-width: calc(100% - 60px); + } + + .toggle-toolbar, + .toggler { + top: 2px; + } + + &.draft { + .toggle-toolbar, + .toggler { + top: 8px; + } + } + + .title-input { + margin-bottom: 5px; + } + + #reply-title { + width: calc(100% - 20px); + } + + .category-input { + margin-bottom: 5px; + .category-chooser { + width: 100% !important; + } + } + + .submit-panel { + margin-bottom: 5px; + display: flex; + align-items: baseline; + flex-shrink: 0; + .save-or-cancel { + flex-basis: 69%; + } + .composer-bottom-right { + flex-basis: 30%; + } + } + + .d-editor-textarea-wrapper { + width: 100%; + } + + &.show-preview { + .d-editor-preview-wrapper { + position: fixed; + z-index: 1000000; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: $secondary; + max-width: 100%; + margin: 0; + .d-editor-preview { + margin-bottom: 40px; + overflow: auto; } } - .wmd-controls { - transition: top 0.3s ease; - top: 110px; + .btn.hide-preview { + position: fixed; + right: 5px; + bottom: 5px; + z-index: 1000001; } } - } - .contents { - padding: 8px 5px 0 5px; - .edit-reason-input, .display-edit-reason { + &.hide-preview { + .d-editor-preview-wrapper { + display: none; + } + } + + .d-editor-button-bar { display: none; } - .edit-reason-input { - display: inline-block; - margin-left: 2px; - #edit-reason { + + .toolbar-visible .d-editor-button-bar { + display: flex; + } + + .d-editor-button-bar .btn { + @media all and ( max-width: 350px) { + padding: 2px 4px; + } + &.preview { margin: 0; - padding: 2px; } } - #reply-title { - margin-right: 10px; - &:disabled { - background-color: dark-light-choose($primary-low-mid, $secondary-high); - } + + #mobile-uploader { + display: none; } - .d-editor-input:disabled { - background-color: dark-light-choose($primary-low-mid, $secondary-high); + + .title-and-category, + .user-selector { + flex-direction: column; + margin: 0; } - .d-editor-input { - color: dark-light-choose(darken($primary, 40%), blend-primary-secondary(90%)); - bottom: 35px; + + .title-input, + .category-input, + .users-input, + .add-warning { width: 100%; - height: 100%; - padding: 7px; - margin: 0; - background-color: $secondary; - word-wrap: break-word; - box-sizing: border-box; - border: 1px solid $secondary; - } - .submit-panel { - // need minimum width that fits, or this'll wrap cancel under submit/create which is super bad - width: 180px; - position: absolute; - display: block; - bottom: 0; - white-space: nowrap; - .btn { - max-width: 120px; - overflow: hidden; - white-space: nowrap; - } - } - } - .popup-tip .close { - padding: 0 2px 2px 8px; // so my fingers can touch the little x - } - .title-input .popup-tip { - width: 240px; - right: 5px; - } - .category-input .popup-tip { - width: 240px; - right: 5px; - } - .d-editor-textarea-wrapper .popup-tip { - top: 28px; - } - button.btn.no-text { - margin: 7px 0 0 5px; - position: absolute; - } - .create.btn-primary { - margin-bottom: 8px; - } -} - -#reply-control.edit-title.private-message { - .wmd-controls { - transition: top 0.3s ease; - top: 120px; - } -} - -#reply-control { - - .d-editor { - height: 100%; - } - - .d-editor-container { - height: 100%; - } - - .wmd-controls { - left: 10px; - right: 10px; - position: absolute; - top: 40px; - bottom: 50px; - display: block; - .d-editor-container { - padding: 0; - } - .d-editor-preview-wrapper { - display: none; } - .btn.hide-preview { - position: fixed; - right: 5px; - bottom: 5px; - z-index: 1000001; + .add-warning { + margin: 5px 0 5px 5px; } - .d-editor-preview-wrapper.force-preview { - display: block; - position: fixed; - z-index: 1000000; - top: 0; - bottom: 0; - left: 0; - right: 0; - background-color: $secondary; + #draft-status { + color: $primary-medium; + } - .d-editor-preview { - height: 90%; - height: calc(100% - 60px); - border: 0; - overflow: auto; - } - } - .d-editor-textarea-wrapper { - position: relative; - box-sizing: border-box; - height: 100%; - min-height: 100%; - margin: 0; - padding: 0; - .popup-tip { - margin-top: 3px; - right: 4px; - } - } - } - .d-editor-button-bar { - display: none; } - - .wmd-controls.toolbar-visible .d-editor-input { - padding-top: 40px; - } - - .wmd-controls.toolbar-visible .d-editor-button-bar { - - .btn.link, .btn.upload, .btn.rule, .btn.bullet, .btn.list, .btn.heading { - display: none; - } - - display: block; - margin: 1px 4px; - position: absolute; - color: dark-light-choose($primary-medium, $secondary-medium); - background-color: $secondary; - z-index: 100; - overflow: hidden; - width: 100%; - width: calc(100% - 10px); - - -moz-box-sizing: border-box; - box-sizing: border-box; - - button { - color: dark-light-choose($primary-medium, $secondary-medium); - } - button.btn.no-text { - margin: 0 2px; - padding: 2px 5px; - position: static; - } - } -} - -// make sure the category selector *NEVER* gets focus by default on mobile anywhere -.select2-hidden, -.select2-search, -.select2-focusser { - display:none !important; -} - -#reply-control.edit-title.open { - height: 100%; -} From b21d5d3633b36ee0d2b1c4ee3f572775a022e43c Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 8 Nov 2017 20:25:15 +0530 Subject: [PATCH 117/445] FIX: SSO email match should be case insensitive --- app/models/discourse_single_sign_on.rb | 2 +- spec/models/discourse_single_sign_on_spec.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/app/models/discourse_single_sign_on.rb b/app/models/discourse_single_sign_on.rb index fbe2425ec9..3b976a57b3 100644 --- a/app/models/discourse_single_sign_on.rb +++ b/app/models/discourse_single_sign_on.rb @@ -176,7 +176,7 @@ class DiscourseSingleSignOn < SingleSignOn end def change_external_attributes_and_override(sso_record, user) - if SiteSetting.sso_overrides_email && user.email != email + if SiteSetting.sso_overrides_email && user.email != Email.downcase(email) user.email = email user.active = false if require_activation end diff --git a/spec/models/discourse_single_sign_on_spec.rb b/spec/models/discourse_single_sign_on_spec.rb index b4f4529f95..aa835ea5dc 100644 --- a/spec/models/discourse_single_sign_on_spec.rb +++ b/spec/models/discourse_single_sign_on_spec.rb @@ -266,6 +266,23 @@ describe DiscourseSingleSignOn do expect(user.active).to eq(false) end + it 'does not deactivate user if email provided is capitalized' do + SiteSetting.email_editable = false + SiteSetting.sso_overrides_email = true + sso.require_activation = true + + user = sso.lookup_or_create_user(ip_address) + expect(user.active).to eq(false) + + user.update_columns(active: true) + user = sso.lookup_or_create_user(ip_address) + expect(user.active).to eq(true) + + sso.email = "Test@example.com" + user = sso.lookup_or_create_user(ip_address) + expect(user.active).to eq(true) + end + it 'deactivates accounts that have updated email address' do SiteSetting.email_editable = false From 7134b11673e52e2d4e712e79c79e70fa28aa39e8 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 8 Nov 2017 10:45:41 -0500 Subject: [PATCH 118/445] better tooltip on the dismiss link --- app/assets/javascripts/discourse/widgets/user-menu.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 index ac99495c9c..123bbddf92 100644 --- a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 @@ -104,7 +104,8 @@ createWidget('user-menu-dismiss-link', { action: 'dismissNotifications', className: 'dismiss', icon: 'check', - label: 'user.dismiss' + label: 'user.dismiss', + title: 'user.dismiss_notifications_tooltip' }) ) ); From 3940e95227aabea7261abf2ffa7a107a4390d04b Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 8 Nov 2017 11:20:39 -0500 Subject: [PATCH 119/445] better way to show dismiss link based on notifications widget state --- .../discourse/widgets/user-menu.js.es6 | 15 +-------------- .../discourse/widgets/user-notifications.js.es6 | 3 --- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 index 123bbddf92..194ba4eb56 100644 --- a/app/assets/javascripts/discourse/widgets/user-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/user-menu.js.es6 @@ -92,12 +92,8 @@ createWidget('user-menu-dismiss-link', { tagName: 'div.dismiss-link', buildKey: () => 'user-menu-dismiss-link', - defaultState() { - return { showDismiss: false }; - }, - html() { - if (this.state.showDismiss) { + if (userNotifications.state.notifications.get('length') > 0) { return h('ul.menu-links', h('li', this.attach('link', { @@ -114,11 +110,6 @@ createWidget('user-menu-dismiss-link', { } }, - showDismissLink() { - this.state.showDismiss = true; - this.scheduleRerender(); - }, - dismissNotifications() { ajax('/notifications/mark-read', { method: 'PUT' }).then(() => { userNotifications.notificationsChanged(); @@ -169,9 +160,5 @@ export default createWidget('user-menu', { clickOutside() { this.sendWidgetAction('toggleUserMenu'); - }, - - notificationsLoaded() { - dismissLink.showDismissLink(); } }); diff --git a/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 b/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 index a334fce948..a6353a995f 100644 --- a/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/widgets/user-notifications.js.es6 @@ -51,9 +51,6 @@ export default createWidget('user-notifications', { }).finally(() => { state.loading = false; state.loaded = true; - if (state.notifications.get('length') > 0) { - this.sendWidgetAction('notificationsLoaded'); - } this.scheduleRerender(); }); }, From 2aadc42662dff605fd2a5eec0a849a3748ac4190 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 8 Nov 2017 15:25:56 -0500 Subject: [PATCH 120/445] FEATURE: show read time on user cards --- .../discourse/templates/components/user-card-contents.hbs | 1 + app/serializers/user_serializer.rb | 5 +++++ config/locales/client.en.yml | 1 + 3 files changed, 7 insertions(+) diff --git a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs index 55190fb27f..48e10c6dcb 100644 --- a/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-card-contents.hbs @@ -118,6 +118,7 @@

    {{i18n 'last_post'}} {{format-date user.last_posted_at leaveAgo="true"}}

    {{/if}}

    {{i18n 'joined'}} {{format-date user.created_at leaveAgo="true"}}

    +

    {{i18n 'time_read'}} {{user.time_read}}

    {{plugin-outlet name="user-card-metadata" args=(hash user=user)}}
    {{/if}} diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 9f3356f2d4..895a7a39a5 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -66,6 +66,7 @@ class UserSerializer < BasicUserSerializer :topic_post_count, :pending_count, :profile_view_count, + :time_read, :primary_group_name, :primary_group_flair_url, :primary_group_flair_bg_color, @@ -401,4 +402,8 @@ class UserSerializer < BasicUserSerializer object.user_profile.views end + def time_read + AgeWords.age_words(object.user_stat&.time_read) + end + end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8e9025d6a6..e75296f8f9 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1003,6 +1003,7 @@ en: mute: Mute unmute: Unmute last_post: Last post + time_read: Read time last_reply_lowercase: last reply replies_lowercase: one: reply From e5272949046dbe0e380e6e786ad6afd2ed4ac1d1 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 8 Nov 2017 13:48:30 -0500 Subject: [PATCH 121/445] UX: Allow collapsing of group posts after they've been expanded --- .../discourse/components/expand-post.js.es6 | 19 +++++++++++++++---- .../templates/components/expand-post.hbs | 12 +++++++++--- .../templates/components/group-post.hbs | 6 +++++- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/components/expand-post.js.es6 b/app/assets/javascripts/discourse/components/expand-post.js.es6 index 957750e92f..643781dba4 100644 --- a/app/assets/javascripts/discourse/components/expand-post.js.es6 +++ b/app/assets/javascripts/discourse/components/expand-post.js.es6 @@ -2,17 +2,28 @@ import { ajax } from 'discourse/lib/ajax'; export default Ember.Component.extend({ tagName: '', + expanded: null, + _loading: false, actions: { - expandItem() { + toggleItem() { + if (this._loading) { return false; } const item = this.get('item'); + + if (this.get('expanded')) { + this.set('expanded', false); + item.set('expandedExcerpt', null); + return; + } + const topicId = item.get('topic_id'); const postNumber = item.get('post_number'); + this._loading = true; return ajax(`/posts/by_number/${topicId}/${postNumber}.json`).then(result => { - item.set('truncated', false); - item.set('excerpt', result.cooked); - }); + this.set('expanded', true); + item.set('expandedExcerpt', result.cooked); + }).finally(() => this._loading = false); } } }); diff --git a/app/assets/javascripts/discourse/templates/components/expand-post.hbs b/app/assets/javascripts/discourse/templates/components/expand-post.hbs index 94ba1d6290..63716b696c 100644 --- a/app/assets/javascripts/discourse/templates/components/expand-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/expand-post.hbs @@ -1,5 +1,11 @@ {{#if item.truncated}} - - {{d-icon "chevron-down"}} - + {{#if expanded}} + + {{d-icon "chevron-up"}} + + {{else}} + + {{d-icon "chevron-down"}} + + {{/if}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/group-post.hbs b/app/assets/javascripts/discourse/templates/components/group-post.hbs index 7521788ec3..9f51a1584f 100644 --- a/app/assets/javascripts/discourse/templates/components/group-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/group-post.hbs @@ -24,5 +24,9 @@
    - {{{post.excerpt}}} + {{#if post.expandedExcerpt}} + {{{post.expandedExcerpt}}} + {{else}} + {{{post.excerpt}}} + {{/if}}
    From 1fb409e521a54f70e10bfa720358356cca6e6834 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 8 Nov 2017 16:34:35 -0500 Subject: [PATCH 122/445] FIX: Use `offset` to calculate eyeline, which is safer than `position` --- .../discourse/components/scrolling-post-stream.js.es6 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 index 87e59f60ba..d3595914c6 100644 --- a/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 +++ b/app/assets/javascripts/discourse/components/scrolling-post-stream.js.es6 @@ -4,13 +4,13 @@ import { cloak, uncloak } from 'discourse/widgets/post-stream'; import { isWorkaroundActive } from 'discourse/lib/safari-hacks'; import offsetCalculator from 'discourse/lib/offset-calculator'; -function findTopView($posts, viewportTop, min, max) { +function findTopView($posts, viewportTop, postsWrapperTop, min, max) { if (max < min) { return min; } while (max > min) { const mid = Math.floor((min + max) / 2); const $post = $($posts[mid]); - const viewBottom = $post.position().top + $post.height(); + const viewBottom = ($post.offset().top - postsWrapperTop) + $post.height(); if (viewBottom > viewportTop) { max = mid-1; @@ -71,9 +71,10 @@ export default MountWidget.extend({ const windowTop = $w.scrollTop(); + const postsWrapperTop = $('.posts-wrapper').offset().top; const $posts = this.$('.onscreen-post, .cloaked-post'); const viewportTop = windowTop - slack; - const topView = findTopView($posts, viewportTop, 0, $posts.length-1); + const topView = findTopView($posts, viewportTop, postsWrapperTop, 0, $posts.length-1); let windowBottom = windowTop + windowHeight; let viewportBottom = windowBottom + slack; From 0efed546a1eb2947ffe4d2a85048006115a1b1aa Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 8 Nov 2017 16:28:57 -0800 Subject: [PATCH 123/445] minor copyedit on new usercard read time --- config/locales/client.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index e75296f8f9..51e4ddfa80 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1003,7 +1003,7 @@ en: mute: Mute unmute: Unmute last_post: Last post - time_read: Read time + time_read: Read last_reply_lowercase: last reply replies_lowercase: one: reply From 86e6732f78300f3569d6324e049372723ad08bd8 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 9 Nov 2017 15:40:34 +1100 Subject: [PATCH 124/445] FEATURE: update rails multisite always allow /srv/status through even if host does not match --- Gemfile | 2 +- Gemfile.lock | 4 +- config/initializers/014-rails_multisite.rb | 19 ++++++ spec/integration/multisite_spec.rb | 67 ++++++++++++++++++++++ 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 config/initializers/014-rails_multisite.rb create mode 100644 spec/integration/multisite_spec.rb diff --git a/Gemfile b/Gemfile index aafaf0e805..291fac3d3c 100644 --- a/Gemfile +++ b/Gemfile @@ -46,7 +46,7 @@ gem 'barber' gem 'message_bus' -gem 'rails_multisite', '~> 1.1.0.rc4' +gem 'rails_multisite' gem 'fast_xs' diff --git a/Gemfile.lock b/Gemfile.lock index 9451fef5ae..c15ad160d7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -271,7 +271,7 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rails_multisite (1.1.0.rc4) + rails_multisite (1.1.1) activerecord (> 4.2, < 6) railties (> 4.2, < 6) railties (5.1.4) @@ -473,7 +473,7 @@ DEPENDENCIES r2 (~> 0.2.5) rack-mini-profiler rack-protection - rails_multisite (~> 1.1.0.rc4) + rails_multisite railties (~> 5.1) rake rb-fsevent diff --git a/config/initializers/014-rails_multisite.rb b/config/initializers/014-rails_multisite.rb new file mode 100644 index 0000000000..fba7ec79da --- /dev/null +++ b/config/initializers/014-rails_multisite.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class RailsMultisite::DiscoursePatches + def self.config + { + db_lookup: lambda do |env| + env["PATH_INFO"] == "/srv/status" ? "default" : nil + end + } + end +end + +if Rails.configuration.multisite + Rails.configuration.middleware.swap( + RailsMultisite::ConnectionManagement, + RailsMultisite::ConnectionManagement, + RailsMultisite::DiscoursePatches.config + ) +end diff --git a/spec/integration/multisite_spec.rb b/spec/integration/multisite_spec.rb new file mode 100644 index 0000000000..678052a046 --- /dev/null +++ b/spec/integration/multisite_spec.rb @@ -0,0 +1,67 @@ +require 'rails_helper' + +describe 'multisite' do + + class DBNameMiddleware + def initialize(app, config = {}) + @app = app + end + + def call(env) + # note current_db is already being ruined on boot cause its not multisite + [200, {}, [RailsMultisite::ConnectionManagement.current_hostname]] + end + end + + let :session do + RailsMultisite::ConnectionManagement.config_filename = "spec/fixtures/multisite/two_dbs.yml" + RailsMultisite::ConnectionManagement.load_settings! + + stack = ActionDispatch::MiddlewareStack.new + stack.use RailsMultisite::ConnectionManagement, RailsMultisite::DiscoursePatches.config + stack.use DBNameMiddleware + + routes = ActionDispatch::Routing::RouteSet.new + stack.build(routes) + end + + it "should always allow /srv/status through" do + headers = { + "HTTP_HOST" => "unknown.com", + "REQUEST_METHOD" => "GET", + "PATH_INFO" => "/srv/status", + "rack.input" => StringIO.new + } + + code, _, body = session.call(headers) + expect(code).to eq(200) + expect(body.join).to eq("test.localhost") + end + + it "should 404 on unknown routes" do + headers = { + "HTTP_HOST" => "unknown.com", + "REQUEST_METHOD" => "GET", + "PATH_INFO" => "/topics", + "rack.input" => StringIO.new + } + + code, _ = session.call(headers) + expect(code).to eq(404) + end + + it "should hit correct site elsewise" do + + headers = { + "HTTP_HOST" => "test2.localhost", + "REQUEST_METHOD" => "GET", + "PATH_INFO" => "/topics", + "rack.input" => StringIO.new + } + + code, _, body = session.call(headers) + expect(code).to eq(200) + expect(body.join).to eq("test2.localhost") + end + +end From 2d5bf0705acab3702f6bfd183ff71e63474a6078 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 9 Nov 2017 16:53:14 +1100 Subject: [PATCH 125/445] PERF: exact email match bypass instead of scanning full table when there is an exact email match ONLY return the actual user. --- lib/admin_user_index_query.rb | 21 +++++++++++++++---- .../components/admin_user_index_query_spec.rb | 12 +++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/lib/admin_user_index_query.rb b/lib/admin_user_index_query.rb index 2cd23d0b1c..6572fa2ccf 100644 --- a/lib/admin_user_index_query.rb +++ b/lib/admin_user_index_query.rb @@ -101,13 +101,26 @@ class AdminUserIndexQuery end end + def filter_by_user_with_bypass(filter) + if filter =~ /.+@.+/ + # probably an email so try the bypass + user_id = UserEmail.where(email: filter.downcase).pluck(:user_id).first + if user_id + return @query.where('users.id = ?', user_id) + end + end + + @query.where('username_lower ILIKE :filter OR user_emails.email ILIKE :filter', filter: "%#{params[:filter]}%") + end + def filter_by_search - if params[:filter].present? - params[:filter].strip! - if ip = IPAddr.new(params[:filter]) rescue nil + filter = params[:filter] + if filter.present? + filter.strip! + if ip = IPAddr.new(filter) rescue nil @query.where('ip_address <<= :ip OR registration_ip_address <<= :ip', ip: ip.to_cidr_s) else - @query.where('username_lower ILIKE :filter OR user_emails.email ILIKE :filter', filter: "%#{params[:filter]}%") + filter_by_user_with_bypass(filter) end end end diff --git a/spec/components/admin_user_index_query_spec.rb b/spec/components/admin_user_index_query_spec.rb index c124cc4662..4a11b97008 100644 --- a/spec/components/admin_user_index_query_spec.rb +++ b/spec/components/admin_user_index_query_spec.rb @@ -178,6 +178,18 @@ describe AdminUserIndexQuery do describe "filtering" do + context "exact email bypass" do + it "can correctly bypass expensive ilike query" do + user = Fabricate(:user, email: 'sam@Sam.com') + + query = AdminUserIndexQuery.new(filter: 'Sam@sam.com').find_users_query + expect(query.count).to eq(1) + expect(query.first.id).to eq(user.id) + + expect(query.to_sql.downcase).not_to include("ilike") + end + end + context "by email fragment" do before(:each) { Fabricate(:user, email: "test1@example.com") } From 06365023c4f215bb6867efe7e41bf740c919586b Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 9 Nov 2017 17:04:21 +1100 Subject: [PATCH 126/445] FEATURE: new API to search for a user by email --- lib/admin_user_index_query.rb | 4 ++++ spec/components/admin_user_index_query_spec.rb | 14 ++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/lib/admin_user_index_query.rb b/lib/admin_user_index_query.rb index 6572fa2ccf..7f3c4fca62 100644 --- a/lib/admin_user_index_query.rb +++ b/lib/admin_user_index_query.rb @@ -114,6 +114,10 @@ class AdminUserIndexQuery end def filter_by_search + if params[:email].present? + return @query.where('user_emails.email = ?', params[:email].downcase) + end + filter = params[:filter] if filter.present? filter.strip! diff --git a/spec/components/admin_user_index_query_spec.rb b/spec/components/admin_user_index_query_spec.rb index 4a11b97008..284f6389ac 100644 --- a/spec/components/admin_user_index_query_spec.rb +++ b/spec/components/admin_user_index_query_spec.rb @@ -188,6 +188,20 @@ describe AdminUserIndexQuery do expect(query.to_sql.downcase).not_to include("ilike") end + + it "can correctly bypass expensive ilike query" do + user = Fabricate(:user, email: 'sam2@Sam.com') + + query = AdminUserIndexQuery.new(email: 'Sam@sam.com').find_users_query + expect(query.count).to eq(0) + expect(query.to_sql.downcase).not_to include("ilike") + + query = AdminUserIndexQuery.new(email: 'Sam2@sam.com').find_users_query + expect(query.first.id).to eq(user.id) + expect(query.count).to eq(1) + expect(query.to_sql.downcase).not_to include("ilike") + + end end context "by email fragment" do From a3a0e365636d17eac3b2ee0af0c904ef5ba013ff Mon Sep 17 00:00:00 2001 From: Benjamin Borowski Date: Wed, 8 Nov 2017 16:36:50 -0800 Subject: [PATCH 127/445] [DEV] fixes sublime text 2 project loading issue --- discourse.sublime-project | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discourse.sublime-project b/discourse.sublime-project index 6ec5f18c58..82cb7955f7 100644 --- a/discourse.sublime-project +++ b/discourse.sublime-project @@ -16,7 +16,7 @@ { "path": "script" }, { "path": "spec" }, { "path": "vendor" }, - { "path": "test" }, + { "path": "test" } ], "settings": { From 5d8508c523b4109afc4e29e6d222b802f247f2ff Mon Sep 17 00:00:00 2001 From: Jon Bartlett Date: Fri, 24 Mar 2017 13:49:32 +1100 Subject: [PATCH 128/445] New yahoo groups importer --- script/import_scripts/yahoogroup.rb | 159 ++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 script/import_scripts/yahoogroup.rb diff --git a/script/import_scripts/yahoogroup.rb b/script/import_scripts/yahoogroup.rb new file mode 100644 index 0000000000..0b1a5456b1 --- /dev/null +++ b/script/import_scripts/yahoogroup.rb @@ -0,0 +1,159 @@ +require File.expand_path(File.dirname(__FILE__) + "/base.rb") +require 'mongo' + +# Import YahooGroups data as exported into MongoDB by: +# https://github.com/jonbartlett/yahoo-groups-export +# +# Optionally paste these lines into your shell before running this: +# +# =begin +# export CATEGORY_ID= +# =end + +class ImportScripts::YahooGroup < ImportScripts::Base + + MONGODB_HOST = '192.168.10.1:27017' + MONGODB_DB = 'syncro' + + def initialize + super + + client = Mongo::Client.new([ MONGODB_HOST ], database: MONGODB_DB) + db = client.database + Mongo::Logger.logger.level = Logger::FATAL + puts "connected to db...." + + @collection = client[:posts] + + @user_profile_map = {} + + end + + def execute + puts "", "Importing from Mongodb...." + + import_users + import_discussions + + puts "", "Done" + end + + def import_users + + puts '', "Importing users" + + # fetch distinct list of Yahoo "profile" names + profiles = @collection.aggregate( + [ + { "$group": { "_id": { profile: "$ygData.profile" } } } + ] + ) + + user_id = 0 + + create_users(profiles.to_a) do |u| + + user_id = user_id + 1 + + # fetch last message for profile to pickup latest user info as this may have changed + user_info = @collection.find("ygData.profile": u["_id"]["profile"]).sort("ygData.msgId": -1).limit(1).to_a[0] + + # Store user_id to profile lookup + @user_profile_map.store(user_info["ygData"]["profile"], user_id) + + puts "User created: #{user_info["ygData"]["profile"]}" + + user = + { + id: user_id, # yahoo "userId" sequence appears to have changed mid forum life so generate this + username: user_info["ygData"]["profile"], + name: user_info["ygData"]["authorName"], + email: user_info["ygData"]["from"], # mandatory + created_at: Time.now + } + user + end + + puts "#{user_id} users created" + + end + + def import_discussions + puts "", "Importing discussions" + + topics_count = 0 + posts_count = 0 + + topics = @collection.aggregate( + [ + { "$group": { "_id": { topicId: "$ygData.topicId" } } } + ] + ).to_a + + # for each distinct topicId found + topics.each_with_index do |t, tidx| + + # create "topic" post first. + # fetch topic document + topic_post = @collection.find("ygData.msgId": t["_id"]["topicId"]).to_a[0] + next if topic_post.nil? + + puts "Topic: #{tidx + 1} / #{topics.count()} (#{sprintf('%.2f', ((tidx + 1).to_f / topics.count().to_f) * 100)}%) Subject: #{topic_post["ygData"]["subject"]}" + + if topic_post["ygData"]["subject"].to_s.empty? + topic_title = "No Subject" + else + topic_title = topic_post["ygData"]["subject"] + end + + topic = { + id: tidx + 1, + user_id: @user_profile_map[topic_post["ygData"]["profile"]] || -1, + raw: topic_post["ygData"]["messageBody"], + created_at: Time.at(topic_post["ygData"]["postDate"].to_i), + cook_method: Post.cook_methods[:raw_html], + title: topic_title, + category: ENV['CATEGORY_ID'], + custom_fields: { import_id: topic_post["ygData"]["msgId"] } + } + + topics_count += 1 + + # create topic post + parent_post = create_post(topic, topic[:id]) + + # find all posts for topic id + posts = @collection.find("ygData.topicId": topic_post["ygData"]["topicId"]).to_a + + posts.each_with_index do |p, pidx| + + # skip over first post as this is created by topic above + next if p["ygData"]["msgId"] == topic_post["ygData"]["topicId"] + + puts " Post: #{pidx + 1} / #{posts.count()}" + + post = { + id: pidx + 1, + topic_id: parent_post[:topic_id], + user_id: @user_profile_map[p["ygData"]["profile"]] || -1, + raw: p["ygData"]["messageBody"], + created_at: Time.at(p["ygData"]["postDate"].to_i), + cook_method: Post.cook_methods[:raw_html], + custom_fields: { import_id: p["ygData"]["msgId"] } + } + + child_post = create_post(post, post[:id]) + + posts_count += 1 + + end + + end + + puts "", "Imported #{topics_count} topics with #{topics_count + posts_count} posts." + + end + +end + +ImportScripts::YahooGroup.new.perform From 16407dfc110228f258470fc5216b8690a6928f21 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 9 Nov 2017 10:49:12 -0500 Subject: [PATCH 129/445] Add a `failed_code` we can check for when using Auth::Result --- lib/auth/result.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/auth/result.rb b/lib/auth/result.rb index 0c18d3e1e5..35204bb7e3 100644 --- a/lib/auth/result.rb +++ b/lib/auth/result.rb @@ -6,8 +6,11 @@ class Auth::Result :admin_not_allowed_from_ip_address, :omit_username, :skip_email_validation - attr_accessor :failed, - :failed_reason + attr_accessor( + :failed, + :failed_reason, + :failed_code + ) def initialize @failed = false From e1e6f46d2632dee477a10b4b4c450f97a1fe7ef9 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Thu, 9 Nov 2017 07:56:44 -0800 Subject: [PATCH 130/445] Consistent copy for usercard (all past tense verbs) --- config/locales/client.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 51e4ddfa80..67be603d87 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1002,7 +1002,7 @@ en: first_post: First post mute: Mute unmute: Unmute - last_post: Last post + last_post: Posted time_read: Read last_reply_lowercase: last reply replies_lowercase: From 6e2853da536c8bb4e391057798cecd8d7d277b1a Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 9 Nov 2017 13:03:35 -0500 Subject: [PATCH 131/445] UX: Make flagged topics details button more clear --- .../javascripts/admin/templates/flags-topics-index.hbs | 3 ++- app/assets/stylesheets/common/admin/flagging.scss | 6 ++++++ config/locales/client.en.yml | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/templates/flags-topics-index.hbs b/app/assets/javascripts/admin/templates/flags-topics-index.hbs index d9fdf87066..b5a7eca133 100644 --- a/app/assets/javascripts/admin/templates/flags-topics-index.hbs +++ b/app/assets/javascripts/admin/templates/flags-topics-index.hbs @@ -37,7 +37,8 @@ ft.id class="btn d-button no-text btn-small btn-primary show-details" title=(i18n "admin.flags.show_details")}} - {{d-icon "search"}} + {{d-icon "list"}} + {{i18n "admin.flags.details"}} {{/link-to}} diff --git a/app/assets/stylesheets/common/admin/flagging.scss b/app/assets/stylesheets/common/admin/flagging.scss index 7cac376c71..e5fa38b9f5 100644 --- a/app/assets/stylesheets/common/admin/flagging.scss +++ b/app/assets/stylesheets/common/admin/flagging.scss @@ -196,3 +196,9 @@ } } } + +.show-details { + .d-icon { + padding-right: 0.25em; + } +} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 67be603d87..ec4fc40a9f 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2660,6 +2660,7 @@ en: was_edited: "Post was edited after the first flag" previous_flags_count: "This post has already been flagged {{count}} times." show_details: "Show flag details" + details: "details" flagged_topics: topic: "Topic" From 3093074398a2d7c3ba3d0318af0b9d94f0fc40be Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 9 Nov 2017 13:18:33 -0500 Subject: [PATCH 132/445] UX: Include the flagged person's username on the flagged post --- .../admin/templates/components/flagged-post.hbs | 4 ++++ app/assets/stylesheets/common/admin/flagging.scss | 9 +++++++++ test/javascripts/acceptance/admin-flags-test.js.es6 | 1 + 3 files changed, 14 insertions(+) diff --git a/app/assets/javascripts/admin/templates/components/flagged-post.hbs b/app/assets/javascripts/admin/templates/components/flagged-post.hbs index 4a832771a8..9bba9abfcd 100644 --- a/app/assets/javascripts/admin/templates/components/flagged-post.hbs +++ b/app/assets/javascripts/admin/templates/components/flagged-post.hbs @@ -20,6 +20,10 @@
    +
    + {{format-username flaggedPost.user.username}} +
    +
    {{#unless hideTitle}}

    diff --git a/app/assets/stylesheets/common/admin/flagging.scss b/app/assets/stylesheets/common/admin/flagging.scss index e5fa38b9f5..8867439f46 100644 --- a/app/assets/stylesheets/common/admin/flagging.scss +++ b/app/assets/stylesheets/common/admin/flagging.scss @@ -42,6 +42,14 @@ width: 100%; word-wrap: break-word; + .flagged-post-user-details { + .username { + font-weight: bold; + color: $primary; + } + margin-bottom: 0.5em; + } + .d-icon { display: inline-block; } @@ -202,3 +210,4 @@ padding-right: 0.25em; } } + diff --git a/test/javascripts/acceptance/admin-flags-test.js.es6 b/test/javascripts/acceptance/admin-flags-test.js.es6 index bd01fd0d36..0be7e93b0a 100644 --- a/test/javascripts/acceptance/admin-flags-test.js.es6 +++ b/test/javascripts/acceptance/admin-flags-test.js.es6 @@ -8,6 +8,7 @@ QUnit.test("flagged posts", assert => { assert.equal(find('.flagged-post .flag-user').length, 1, 'shows who flagged it'); assert.equal(find('.flagged-post-response').length, 2); assert.equal(find('.flagged-post-response:eq(0) img.avatar').length, 1); + assert.equal(find('.flagged-post-user-details .username').length, 1, 'shows the flagged username'); }); }); From 0da529010aad1d44c970a62bdd91342802271b67 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 9 Nov 2017 10:57:53 -0800 Subject: [PATCH 133/445] FEATURE: support for multi-combo-box --- .eslintrc | 10 +- .../components/admin-group-selector.js.es6 | 43 -- .../admin/components/list-setting.js.es6 | 52 -- .../components/admin-group-selector.hbs | 1 - .../templates/components/list-setting.hbs | 3 - .../components/admin-group-selector.js.es6 | 45 ++ .../categories-admin-dropdown.js.es6 | 13 +- .../components/category-chooser.js.es6 | 32 +- .../category-notifications-button.js.es6 | 8 +- .../components/combo-box.js.es6 | 4 +- .../components/dropdown-select-box.js.es6 | 13 +- .../group-notifications-button.js.es6 | 7 +- .../components/list-setting.js.es6 | 55 ++ .../components/multi-combo-box.js.es6 | 231 ++++++-- .../multi-combo-box-header.js.es6 | 32 +- .../multi-combo-box/selected-color.js.es6 | 8 + .../multi-combo-box/selected-name.js.es6 | 19 + .../components/notifications-button.js.es6 | 8 +- .../notifications-button-header.js.es6 | 6 +- .../components/pinned-options.js.es6 | 16 +- .../components/select-box-kit.js.es6 | 491 ++++++------------ .../select-box-kit-filter.js.es6 | 2 +- .../select-box-kit/select-box-kit-row.js.es6 | 20 +- .../tag-notifications-button.js.es6 | 8 +- .../topic-footer-mobile-dropdown.js.es6 | 52 +- .../topic-notifications-options.js.es6 | 16 +- .../select-box-kit/mixins/dom-helpers.js.es6 | 231 +++++++- .../select-box-kit/mixins/keyboard.js.es6 | 175 ++++--- .../select-box-kit/mixins/utils.js.es6 | 52 +- .../multi-combo-box-header.hbs | 29 +- .../multi-combo-box/selected-name.hbs | 9 + .../templates/components/select-box-kit.hbs | 13 +- .../select-box-kit-collection.hbs | 6 +- .../select-box-kit/select-box-kit-filter.hbs | 4 +- .../stylesheets/common/admin/admin_base.scss | 11 +- .../select-box-kit/dropdown-select-box.scss | 8 - .../common/select-box-kit/list-setting.scss | 13 + .../select-box-kit/multi-combo-box.scss | 147 ++++++ .../common/select-box-kit/multi-combobox.scss | 131 ----- .../common/select-box-kit/select-box-kit.scss | 15 +- config/locales/client.en.yml | 1 + .../acceptance/admin-suspend-user-test.js.es6 | 4 +- .../acceptance/category-chooser-test.js.es6 | 2 +- .../acceptance/category-edit-test.js.es6 | 4 +- .../acceptance/search-full-test.js.es6 | 12 +- .../javascripts/acceptance/search-test.js.es6 | 4 +- .../topic-notifications-button-test.js.es6 | 4 +- test/javascripts/acceptance/topic-test.js.es6 | 4 +- .../categories-admin-dropdown-test.js.es6 | 2 +- .../components/category-chooser-test.js.es6 | 18 +- .../components/combo-box-test.js.es6 | 38 +- .../components/list-setting-test.js.es6 | 74 +++ .../components/multi-combo-box-test.js.es6 | 94 +++- .../components/pinned-button-test.js.es6 | 2 +- ...test.js.es6 => select-box-kit-test.js.es6} | 43 +- .../topic-footer-mobile-dropdown-test.js.es6 | 4 +- ...box-helper.js => select-box-kit-helper.js} | 28 +- test/javascripts/test_helper.js | 2 +- 58 files changed, 1394 insertions(+), 985 deletions(-) delete mode 100644 app/assets/javascripts/admin/components/admin-group-selector.js.es6 delete mode 100644 app/assets/javascripts/admin/components/list-setting.js.es6 delete mode 100644 app/assets/javascripts/discourse/templates/components/admin-group-selector.hbs delete mode 100644 app/assets/javascripts/discourse/templates/components/list-setting.hbs create mode 100644 app/assets/javascripts/select-box-kit/components/admin-group-selector.js.es6 create mode 100644 app/assets/javascripts/select-box-kit/components/list-setting.js.es6 create mode 100644 app/assets/javascripts/select-box-kit/components/multi-combo-box/selected-color.js.es6 create mode 100644 app/assets/javascripts/select-box-kit/components/multi-combo-box/selected-name.js.es6 create mode 100644 app/assets/javascripts/select-box-kit/templates/components/multi-combo-box/selected-name.hbs create mode 100644 app/assets/stylesheets/common/select-box-kit/list-setting.scss create mode 100644 app/assets/stylesheets/common/select-box-kit/multi-combo-box.scss delete mode 100644 app/assets/stylesheets/common/select-box-kit/multi-combobox.scss create mode 100644 test/javascripts/components/list-setting-test.js.es6 rename test/javascripts/components/{select-box-test.js.es6 => select-box-kit-test.js.es6} (90%) rename test/javascripts/helpers/{select-box-helper.js => select-box-kit-helper.js} (75%) diff --git a/.eslintrc b/.eslintrc index 71e21b4381..6f7c9f0ecd 100644 --- a/.eslintrc +++ b/.eslintrc @@ -43,11 +43,11 @@ "asyncRender":true, "selectDropdown":true, "selectBox":true, - "expandSelectBox":true, - "collapseSelectBox":true, - "selectBoxSelectRow":true, - "selectBoxSelectNoneRow":true, - "selectBoxFillInFilter":true, + "expandSelectBoxKit":true, + "collapseSelectBoxKit":true, + "selectBoxKitSelectRow":true, + "selectBoxKitSelectNoneRow":true, + "selectBoxKitFillInFilter":true, "asyncTestDiscourse":true, "fixture":true, "find":true, diff --git a/app/assets/javascripts/admin/components/admin-group-selector.js.es6 b/app/assets/javascripts/admin/components/admin-group-selector.js.es6 deleted file mode 100644 index fd487640d0..0000000000 --- a/app/assets/javascripts/admin/components/admin-group-selector.js.es6 +++ /dev/null @@ -1,43 +0,0 @@ -export default Ember.Component.extend({ - tagName: 'div', - - _init: function(){ - this.$("input").select2({ - multiple: true, - width: '100%', - query: function(opts) { - opts.callback({ - results: this.get("available").filter(function(o) { - return -1 !== o.name.toLowerCase().indexOf(opts.term.toLowerCase()); - }).map(this._format) - }); - }.bind(this) - }).on("change", function(evt) { - if (evt.added){ - this.triggerAction({ - action: "groupAdded", - actionContext: this.get("available").findBy("id", evt.added.id) - }); - } else if (evt.removed) { - this.triggerAction({ - action:"groupRemoved", - actionContext: evt.removed.id - }); - } - }.bind(this)); - - this._refreshOnReset(); - }.on("didInsertElement"), - - _format(item) { - return { - "text": item.name, - "id": item.id, - "locked": item.automatic - }; - }, - - _refreshOnReset: function() { - this.$("input").select2("data", this.get("selected").map(this._format)); - }.observes("selected") -}); diff --git a/app/assets/javascripts/admin/components/list-setting.js.es6 b/app/assets/javascripts/admin/components/list-setting.js.es6 deleted file mode 100644 index 9a1d865133..0000000000 --- a/app/assets/javascripts/admin/components/list-setting.js.es6 +++ /dev/null @@ -1,52 +0,0 @@ -/** - Provide a nice GUI for a pipe-delimited list in the site settings. - - @param settingValue is a reference to SiteSetting.value. - @param choices is a reference to SiteSetting.choices -**/ -export default Ember.Component.extend({ - - _select2FormatSelection: function(selectedObject, jqueryWrapper, htmlEscaper) { - var text = selectedObject.text; - if (text.length <= 6) { - jqueryWrapper.closest('li.select2-search-choice').css({"border-bottom": '7px solid #'+text}); - } - return htmlEscaper(text); - }, - - _initializeSelect2: function(){ - var options = { - multiple: false, - separator: "|", - tokenSeparators: ["|"], - tags : this.get("choices") || [], - width: 'off', - dropdownCss: this.get("choices") ? {} : {display: 'none'}, - selectOnBlur: this.get("choices") ? false : true - }; - - var settingName = this.get('settingName'); - if (typeof settingName === 'string' && settingName.indexOf('colors') > -1) { - options.formatSelection = this._select2FormatSelection; - } - - var self = this; - this.$("input").select2(options).on("change", function(obj) { - self.set("settingValue", obj.val.join("|")); - self.refreshSortables(); - }); - - this.refreshSortables(); - }.on('didInsertElement'), - - refreshOnReset: function() { - this.$("input").select2("val", this.get("settingValue").split("|")); - }.observes("settingValue"), - - refreshSortables: function() { - var self = this; - this.$("ul.select2-choices").sortable().on('sortupdate', function() { - self.$("input").select2("onSortEnd"); - }); - } -}); diff --git a/app/assets/javascripts/discourse/templates/components/admin-group-selector.hbs b/app/assets/javascripts/discourse/templates/components/admin-group-selector.hbs deleted file mode 100644 index 4856de5179..0000000000 --- a/app/assets/javascripts/discourse/templates/components/admin-group-selector.hbs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/app/assets/javascripts/discourse/templates/components/list-setting.hbs b/app/assets/javascripts/discourse/templates/components/list-setting.hbs deleted file mode 100644 index 830982fc47..0000000000 --- a/app/assets/javascripts/discourse/templates/components/list-setting.hbs +++ /dev/null @@ -1,3 +0,0 @@ -
    - -
    diff --git a/app/assets/javascripts/select-box-kit/components/admin-group-selector.js.es6 b/app/assets/javascripts/select-box-kit/components/admin-group-selector.js.es6 new file mode 100644 index 0000000000..08248e72bd --- /dev/null +++ b/app/assets/javascripts/select-box-kit/components/admin-group-selector.js.es6 @@ -0,0 +1,45 @@ +import MultiComboBoxComponent from "select-box-kit/components/multi-combo-box"; + +export default MultiComboBoxComponent.extend({ + classNames: "admin-group-selector", + selected: null, + available: null, + allowAny: false, + + didReceiveAttrs() { + this._super(); + + this.set("value", this.get("selected").map(s => this._valueForContent(s))); + this.set("content", this.get("available")); + }, + + formatRowContent(content) { + let formatedContent = this._super(content); + formatedContent.locked = content.automatic; + return formatedContent; + }, + + didUpdateAttrs() { + this._super(); + + this.set("highlightedValue", null); + Ember.run.schedule("afterRender", () => { + this.autoHighlightFunction(); + }); + }, + + selectValuesFunction(values) { + values.forEach(value => { + this.triggerAction({ + action: "groupAdded", + actionContext: this.get("content").findBy("id", parseInt(value, 10)) + }); + }); + }, + + deselectValuesFunction(values) { + values.forEach(value => { + this.triggerAction({ action: "groupRemoved", actionContext: value }); + }); + } +}); diff --git a/app/assets/javascripts/select-box-kit/components/categories-admin-dropdown.js.es6 b/app/assets/javascripts/select-box-kit/components/categories-admin-dropdown.js.es6 index 61ffa1c225..187a710b88 100644 --- a/app/assets/javascripts/select-box-kit/components/categories-admin-dropdown.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/categories-admin-dropdown.js.es6 @@ -8,10 +8,10 @@ export default DropdownSelectBoxComponent.extend({ @on("didReceiveAttrs") _setComponentOptions() { - this.set("headerComponentOptions", Ember.Object.create({ + this.get("headerComponentOptions").setProperties({ shouldDisplaySelectedName: false, icon: `${iconHTML('bars')}${iconHTML('caret-down')}`.htmlSafe(), - })); + }); }, @computed @@ -38,11 +38,8 @@ export default DropdownSelectBoxComponent.extend({ return items; }, - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); - this.get(value)(); - this.set("value", null); - } + selectValueFunction(value) { + this.get(value)(); + this.set("value", null); } }); diff --git a/app/assets/javascripts/select-box-kit/components/category-chooser.js.es6 b/app/assets/javascripts/select-box-kit/components/category-chooser.js.es6 index ee6e9b158a..668f168b38 100644 --- a/app/assets/javascripts/select-box-kit/components/category-chooser.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/category-chooser.js.es6 @@ -12,24 +12,24 @@ export default ComboBoxComponent.extend({ castInteger: true, allowUncategorized: null, - filterFunction(computedContent) { - const _matchFunction = (filter, text) => { - return text.toLowerCase().indexOf(filter) > -1; - }; + filteredContentFunction(computedContent, computedValue, filter) { + if (isEmpty(filter)) { return computedContent; } - return (selectBox) => { - const filter = selectBox.get("filter").toLowerCase(); - return _.filter(computedContent, c => { - const category = Category.findById(get(c, "value")); - const text = get(c, "name"); - if (category && category.get("parentCategory")) { - const categoryName = category.get("parentCategory.name"); - return _matchFunction(filter, text) || _matchFunction(filter, categoryName); - } else { - return _matchFunction(filter, text); - } - }); + const _matchFunction = (f, text) => { + return text.toLowerCase().indexOf(f) > -1; }; + const lowerFilter = filter.toLowerCase(); + + return computedContent.filter(c => { + const category = Category.findById(get(c, "value")); + const text = get(c, "name"); + if (category && category.get("parentCategory")) { + const categoryName = category.get("parentCategory.name"); + return _matchFunction(lowerFilter, text) || _matchFunction(lowerFilter, categoryName); + } else { + return _matchFunction(lowerFilter, text); + } + }); }, @computed("rootNone", "rootNoneLabel") diff --git a/app/assets/javascripts/select-box-kit/components/category-notifications-button.js.es6 b/app/assets/javascripts/select-box-kit/components/category-notifications-button.js.es6 index d022c3c64c..0590884006 100644 --- a/app/assets/javascripts/select-box-kit/components/category-notifications-button.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/category-notifications-button.js.es6 @@ -7,11 +7,7 @@ export default NotificationOptionsComponent.extend({ value: Ember.computed.alias("category.notification_level"), headerComponent: "category-notifications-button/category-notifications-button-header", - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); - this.get("category").setNotification(value); - this.blur(); - } + selectValueFunction(value) { + this.get("category").setNotification(value); } }); diff --git a/app/assets/javascripts/select-box-kit/components/combo-box.js.es6 b/app/assets/javascripts/select-box-kit/components/combo-box.js.es6 index f5ff7d0305..26779a3894 100644 --- a/app/assets/javascripts/select-box-kit/components/combo-box.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/combo-box.js.es6 @@ -12,10 +12,10 @@ export default SelectBoxKitComponent.extend({ @on("didReceiveAttrs") _setComboBoxOptions() { - this.set("headerComponentOptions", Ember.Object.create({ + this.get("headerComponentOptions").setProperties({ caretUpIcon: this.get("caretUpIcon"), caretDownIcon: this.get("caretDownIcon"), clearable: this.get("clearable"), - })); + }); } }); diff --git a/app/assets/javascripts/select-box-kit/components/dropdown-select-box.js.es6 b/app/assets/javascripts/select-box-kit/components/dropdown-select-box.js.es6 index 8e2789aa1d..252ff2dd64 100644 --- a/app/assets/javascripts/select-box-kit/components/dropdown-select-box.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/dropdown-select-box.js.es6 @@ -9,16 +9,11 @@ export default SelectBoxKitComponent.extend({ headerComponent: "dropdown-select-box/dropdown-select-box-header", rowComponent: "dropdown-select-box/dropdown-select-box-row", - clickOutside() { - this.close(); - }, + clickOutside() { this.close(); }, - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); - this.set("value", value); + didSelectValue() { + this._super(); - this.blur(); - } + this.blur(); } }); diff --git a/app/assets/javascripts/select-box-kit/components/group-notifications-button.js.es6 b/app/assets/javascripts/select-box-kit/components/group-notifications-button.js.es6 index 81477d2e47..e30b4ec62f 100644 --- a/app/assets/javascripts/select-box-kit/components/group-notifications-button.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/group-notifications-button.js.es6 @@ -5,10 +5,7 @@ export default NotificationOptionsComponent.extend({ value: Ember.computed.alias("group.group_user.notification_level"), i18nPrefix: "groups.notifications", - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); - this.get("group").setNotification(value, this.get("user.id")); - } + selectValueFunction(value) { + this.get("group").setNotification(value, this.get("user.id")); } }); diff --git a/app/assets/javascripts/select-box-kit/components/list-setting.js.es6 b/app/assets/javascripts/select-box-kit/components/list-setting.js.es6 new file mode 100644 index 0000000000..94fa2fa66f --- /dev/null +++ b/app/assets/javascripts/select-box-kit/components/list-setting.js.es6 @@ -0,0 +1,55 @@ +import MultiComboBoxComponent from "select-box-kit/components/multi-combo-box"; +import { observes } from 'ember-addons/ember-computed-decorators'; + +export default MultiComboBoxComponent.extend({ + classNames: "list-setting", + tokenSeparator: "|", + settingValue: "", + choices: null, + filterable: true, + + init() { + const valuesFromString = this.get("settingValue").split(this.get("tokenSeparator")); + this.set("value", valuesFromString.reject(v => Ember.isEmpty(v))); + + if (Ember.isNone(this.get("choices"))) { + this.set("content", valuesFromString); + } else { + this.set("content", this.get("choices")); + } + + if (!Ember.isNone(this.get("settingName"))) { + this.set("nameProperty", this.get("settingName")); + } + + if (Ember.isEmpty(this.get("content"))) { + this.set("rowComponent", null); + this.set("noContentLabel", null); + } + + this._super(); + + if (this.get("nameProperty").indexOf("color") > -1) { + this.set("headerComponentOptions", Ember.Object.create({ + selectedNameComponent: "multi-combo-box/selected-color" + })); + } + }, + + @observes("value.[]") + setSettingValue() { + this.set("settingValue", this.get("value").join(this.get("tokenSeparator"))); + }, + + @observes("content.[]") + setChoices() { this.set("choices", this.get("content")); }, + + _handleTabOnKeyDown(event) { + if (this.$highlightedRow().length === 1) { + this._super(event); + } else { + this.close(); + return false; + } + } +}); diff --git a/app/assets/javascripts/select-box-kit/components/multi-combo-box.js.es6 b/app/assets/javascripts/select-box-kit/components/multi-combo-box.js.es6 index 6ce70b4e8e..bf08d7da1f 100644 --- a/app/assets/javascripts/select-box-kit/components/multi-combo-box.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/multi-combo-box.js.es6 @@ -1,20 +1,32 @@ -// Experimental import SelectBoxKitComponent from "select-box-kit/components/select-box-kit"; import computed from "ember-addons/ember-computed-decorators"; -const { get, isNone } = Ember; +const { get, isNone, isEmpty } = Ember; export default SelectBoxKitComponent.extend({ - classNames: "multi-combobox", + classNames: "multi-combo-box", headerComponent: "multi-combo-box/multi-combo-box-header", filterComponent: null, headerText: "select_box.default_header_text", - value: [], allowAny: true, + allowValueMutation: false, + autoSelectFirst: false, + autoFilterable: true, + selectedNameComponent: "multi-combo-box/selected-name", + + init() { + this._super(); + + if (isNone(this.get("value"))) { this.set("value", []); } + + this.set("headerComponentOptions", Ember.Object.create({ + selectedNameComponent: this.get("selectedNameComponent") + })); + }, @computed("filter") templateForCreateRow() { return (rowComponent) => { - return `Create: ${rowComponent.get("content.name")}`; + return I18n.t("select_box.create", { content: rowComponent.get("content.name")}); }; }, @@ -22,78 +34,207 @@ export default SelectBoxKitComponent.extend({ const keyCode = event.keyCode || event.which; const $filterInput = this.$filterInput(); - if (keyCode === 8) { - let $lastSelectedValue = $(this.$(".choices .selected-name").last()); + if (this.get("isFocused") === true && this.get("isExpanded") === false && keyCode === this.keys.BACKSPACE) { + this.expand(); + return; + } - if ($lastSelectedValue.is(":focus") || $(document.activeElement).is($lastSelectedValue)) { - this.send("onDeselect", $lastSelectedValue.data("value")); + // select all choices + if (event.metaKey === true && keyCode === 65 && isEmpty(this.get("filter"))) { + this.$(".choices .selected-name:not(.is-locked)").addClass("is-highlighted"); + return; + } + + // clear selection when multiple + if (Ember.isEmpty(this.get("filter")) && this.$(".selected-name.is-highlighted").length >= 1 && keyCode === this.keys.BACKSPACE) { + const highlightedValues = []; + $.each(this.$(".selected-name.is-highlighted"), (i, el) => { + highlightedValues.push($(el).attr("data-value")); + }); + + this.send("onDeselect", highlightedValues); + return; + } + + // try to remove last item from the list + if (Ember.isEmpty(this.get("filter")) && keyCode === this.keys.BACKSPACE) { + let $lastSelectedValue = $(this.$(".choices .selected-name:not(.is-locked)").last()); + + if ($lastSelectedValue.length === 0) { return; } + + if ($lastSelectedValue.hasClass("is-highlighted") || $(document.activeElement).is($lastSelectedValue)) { + this.send("onDeselect", this.get("selectedContent.lastObject.value")); $filterInput.focus(); return; } + if ($filterInput.not(":visible") && $lastSelectedValue.length > 0) { + $lastSelectedValue.click(); + return false; + } + if ($filterInput.val() === "") { if ($filterInput.is(":focus")) { - if ($lastSelectedValue.length > 0) { - $lastSelectedValue.focus(); - } + if ($lastSelectedValue.length > 0) { $lastSelectedValue.click(); } } else { if ($lastSelectedValue.length > 0) { - $lastSelectedValue.focus(); + $lastSelectedValue.click(); } else { $filterInput.focus(); } } } - } else { - $filterInput.focus(); - this._super(event); - } - }, - - @computed("none") - computedNone(none) { - if (!isNone(none)) { - this.set("none", { name: I18n.t(none), value: "" }); } }, @computed("value.[]") - computedValue(value) { - return value.map(v => this._castInteger(v)); - }, + computedValue(value) { return value.map(v => this._castInteger(v)); }, - @computed("computedValue.[]", "computedContent.[]") - selectedContent(computedValue, computedContent) { + @computed("value.[]", "computedContent.[]") + selectedContent(value, computedContent) { const contents = []; - computedValue.forEach(cv => { - contents.push(computedContent.findBy("value", cv)); + value.forEach(v => { + const content = computedContent.findBy("value", v); + if (!isNone(content)) { contents.push(content); } }); return contents; }, - filterFunction(content) { - return (selectBox, computedValue) => { - const filter = selectBox.get("filter").toLowerCase(); - return _.filter(content, c => { - return !computedValue.includes(get(c, "value")) && - get(c, "name").toLowerCase().indexOf(filter) > -1; - }); - }; + filteredContentFunction(computedContent, computedValue, filter) { + computedContent = computedContent.filter(c => { + return !computedValue.includes(get(c, "value")); + }); + + if (isEmpty(filter)) { return computedContent; } + + const lowerFilter = filter.toLowerCase(); + return computedContent.filter(c => { + return get(c, "name").toLowerCase().indexOf(lowerFilter) > -1; + }); + }, + + willCreateContent() { + this.set("highlightedValue", null); + }, + + didCreateContent() { + this.clearFilter(); + this.autoHighlightFunction(); + }, + + createContentFunction(input) { + if (!this.get("content").includes(input)) { + this.get("content").pushObject(input); + this.get("value").pushObject(input); + } + }, + + deselectValuesFunction(values) { + const contents = this._computeRemovableContentsForValues(values); + this.get("value").removeObjects(values); + this.get("content").removeObjects(contents); + }, + + highlightValueFunction(value) { + this.set("highlightedValue", value); + }, + + selectValuesFunction(values) { + this.get("value").pushObjects(values); + }, + + willSelectValues() { + this.expand(); + this.set("highlightedValue", null); + }, + + didSelectValues() { + this.focus(); + this.clearFilter(); + this.autoHighlightFunction(); + }, + + willDeselectValues() { + this.set("highlightedValue", null); + }, + + didDeselectValues() { + this.autoHighlightFunction(); + }, + + willHighlightValue() {}, + + didHighlightValue() {}, + + autoHighlightFunction() { + Ember.run.schedule("afterRender", () => { + if (this.get("isExpanded") === false) { return; } + if (this.get("renderedBodyOnce") === false) { return; } + if (!isNone(this.get("highlightedValue"))) { return; } + + if (isEmpty(this.get("filteredContent"))) { + if (!isEmpty(this.get("filter"))) { + this.send("onHighlight", this.get("filter")); + } else if (this.get("none") && !isEmpty(this.get("selectedContent"))) { + this.send("onHighlight", this.noneValue); + } + } else { + this.send("onHighlight", this.get("filteredContent.firstObject.value")); + } + }); }, actions: { onClearSelection() { - this.send("onSelect", []); + const values = this.get("selectedContent").map(c => get(c, "value")); + this.send("onDeselect", values); }, - onSelect(value) { - this.setProperties({ filter: "", highlightedValue: null }); - this.get("value").pushObject(value); + onHighlight(value) { + value = this._originalValueForValue(value); + this.willHighlightValue(value); + this.set("highlightedValue", value); + this.highlightValueFunction(value); + this.didHighlightValue(value); }, - onDeselect(value) { - this.defaultOnDeselect(value); - this.get("value").removeObject(value); + onCreateContent(input) { + this.willCreateContent(input); + this.createContentFunction(input); + this.didCreateContent(input); + }, + + onSelect(values) { + values = Ember.makeArray(values).map(v => this._originalValueForValue(v)); + this.willSelectValues(values); + this.selectValuesFunction(values); + this.didSelectValues(values); + }, + + onDeselect(values) { + values = Ember.makeArray(this._computeRemovableValues(values)); + this.willDeselectValues(values); + this.deselectValuesFunction(values); + this.didSelectValues(values); } + }, + + _computeRemovableContentsForValues(values) { + const removableContents = []; + values.forEach(v => { + if (!this.get("_initialValues").includes(v)) { + const content = this._contentForValue(v); + if (!isNone(content)) { removableContents.push(content); } + } + }); + return removableContents; + }, + + _computeRemovableValues(values) { + return Ember.makeArray(values) + .map(v => this._originalValueForValue(v)) + .filter(v => { + return get(this._computedContentForValue(v), "locked") !== true; + }); } }); diff --git a/app/assets/javascripts/select-box-kit/components/multi-combo-box/multi-combo-box-header.js.es6 b/app/assets/javascripts/select-box-kit/components/multi-combo-box/multi-combo-box-header.js.es6 index 0b5f302e7b..10e2d299ce 100644 --- a/app/assets/javascripts/select-box-kit/components/multi-combo-box/multi-combo-box-header.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/multi-combo-box/multi-combo-box-header.js.es6 @@ -4,37 +4,27 @@ import SelectBoxKitHeaderComponent from "select-box-kit/components/select-box-ki export default SelectBoxKitHeaderComponent.extend({ attributeBindings: ["names:data-name"], - classNames: "multi-combobox-header", + classNames: "multi-combo-box-header", layoutName: "select-box-kit/templates/components/multi-combo-box/multi-combo-box-header", - - @computed("filter", "selectedContent.[]", "isFocused", "selectBoxIsExpanded") - shouldDisplayFilterPlaceholder(filter, selectedContent, isFocused) { - if (Ember.isEmpty(selectedContent)) { - if (filter.length > 0) { return false; } - if (isFocused === true) { return false; } - return true; - } - - return false; - }, + selectedNameComponent: Ember.computed.alias("options.selectedNameComponent"), @on("didRender") _positionFilter() { - this.$(".filter").width(0); + if (this.get("shouldDisplayFilter") === false) { return; } + + const $filter = this.$(".filter"); + $filter.width(0); const leftHeaderOffset = this.$().offset().left; - const leftFilterOffset = this.$(".filter").offset().left; + const leftFilterOffset = $filter.offset().left; const offset = leftFilterOffset - leftHeaderOffset; const width = this.$().outerWidth(false); const availableSpace = width - offset; - - // TODO: avoid magic number 8 - // TODO: make sure the filter doesn’t end up being very small - this.$(".filter").width(availableSpace - 8); + const $choices = $filter.parent(".choices"); + const parentRightPadding = parseInt($choices.css("padding-right") , 10); + $filter.width(availableSpace - parentRightPadding * 4); }, @computed("selectedContent.[]") - names(selectedContent) { - return selectedContent.map(sc => sc.name).join(","); - } + names(selectedContent) { return selectedContent.map(sc => sc.name).join(","); } }); diff --git a/app/assets/javascripts/select-box-kit/components/multi-combo-box/selected-color.js.es6 b/app/assets/javascripts/select-box-kit/components/multi-combo-box/selected-color.js.es6 new file mode 100644 index 0000000000..240c3570d9 --- /dev/null +++ b/app/assets/javascripts/select-box-kit/components/multi-combo-box/selected-color.js.es6 @@ -0,0 +1,8 @@ +import SelectedNameComponent from "select-box-kit/components/multi-combo-box/selected-name"; + +export default SelectedNameComponent.extend({ + didRender() { + const name = this.get("content.name"); + this.$().css("border-bottom", Handlebars.Utils.escapeExpression(`7px solid #${name}`)); + } +}); diff --git a/app/assets/javascripts/select-box-kit/components/multi-combo-box/selected-name.js.es6 b/app/assets/javascripts/select-box-kit/components/multi-combo-box/selected-name.js.es6 new file mode 100644 index 0000000000..07c3ad0d64 --- /dev/null +++ b/app/assets/javascripts/select-box-kit/components/multi-combo-box/selected-name.js.es6 @@ -0,0 +1,19 @@ +export default Ember.Component.extend({ + attributeBindings: ["tabindex","content.name:data-name", "content.value:data-value"], + classNames: "selected-name", + classNameBindings: ["isHighlighted", "isLocked"], + layoutName: "select-box-kit/templates/components/multi-combo-box/selected-name", + tagName: "li", + tabindex: -1, + + isLocked: Ember.computed("content.locked", function() { + return this.getWithDefault("content.locked", false); + }), + + click() { + if (this.get("isLocked") === true) { return false; } + + this.toggleProperty("isHighlighted"); + return false; + } +}); diff --git a/app/assets/javascripts/select-box-kit/components/notifications-button.js.es6 b/app/assets/javascripts/select-box-kit/components/notifications-button.js.es6 index 88a1e71e98..d051553500 100644 --- a/app/assets/javascripts/select-box-kit/components/notifications-button.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/notifications-button.js.es6 @@ -22,15 +22,15 @@ export default DropdownSelectBoxComponent.extend({ @on("didReceiveAttrs", "didUpdateAttrs") _setComponentOptions() { - this.set("headerComponentOptions", Ember.Object.create({ + this.get("headerComponentOptions").setProperties({ i18nPrefix: this.get("i18nPrefix"), showFullTitle: this.get("showFullTitle"), - })); + }); - this.set("rowComponentOptions", Ember.Object.create({ + this.get("rowComponentOptions").setProperties({ i18nPrefix: this.get("i18nPrefix"), i18nPostfix: this.get("i18nPostfix") - })); + }); }, @computed("computedValue") diff --git a/app/assets/javascripts/select-box-kit/components/notifications-button/notifications-button-header.js.es6 b/app/assets/javascripts/select-box-kit/components/notifications-button/notifications-button-header.js.es6 index 96b1553492..268e8d7b31 100644 --- a/app/assets/javascripts/select-box-kit/components/notifications-button/notifications-button-header.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/notifications-button/notifications-button-header.js.es6 @@ -11,7 +11,7 @@ export default DropdownSelectBoxHeaderComponent.extend({ @computed("_selectedDetails.icon", "_selectedDetails.key") icon(icon, key) { - return iconHTML(icon, {class: key}).htmlSafe(); + return iconHTML(icon, { class: key }).htmlSafe(); }, @computed("_selectedDetails.key", "i18nPrefix") @@ -20,5 +20,7 @@ export default DropdownSelectBoxHeaderComponent.extend({ }, @computed("selectedContent.firstObject.value") - _selectedDetails(value) { return buttonDetails(value); } + _selectedDetails(value) { + return buttonDetails(value); + } }); diff --git a/app/assets/javascripts/select-box-kit/components/pinned-options.js.es6 b/app/assets/javascripts/select-box-kit/components/pinned-options.js.es6 index c1be2e6ab3..3c96f2ee22 100644 --- a/app/assets/javascripts/select-box-kit/components/pinned-options.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/pinned-options.js.es6 @@ -48,17 +48,13 @@ export default DropdownSelectBoxComponent.extend({ ]; }, - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); + selectValueFunction(value) { + const topic = this.get("topic"); - const topic = this.get("topic"); - - if (value === "unpinned") { - topic.clearPin(); - } else { - topic.rePin(); - } + if (value === "unpinned") { + topic.clearPin(); + } else { + topic.rePin(); } } }); diff --git a/app/assets/javascripts/select-box-kit/components/select-box-kit.js.es6 b/app/assets/javascripts/select-box-kit/components/select-box-kit.js.es6 index 486b39bd70..ffb118eba8 100644 --- a/app/assets/javascripts/select-box-kit/components/select-box-kit.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/select-box-kit.js.es6 @@ -1,5 +1,5 @@ const { get, isNone, isEmpty, isPresent } = Ember; -import { on, observes } from "ember-addons/ember-computed-decorators"; +import { on } from "ember-addons/ember-computed-decorators"; import computed from "ember-addons/ember-computed-decorators"; import UtilsMixin from "select-box-kit/mixins/utils"; import DomHelpersMixin from "select-box-kit/mixins/dom-helpers"; @@ -22,7 +22,8 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin isExpanded: false, isFocused: false, isHidden: false, - renderBody: false, + renderedBodyOnce: false, + renderedFilterOnce: false, tabindex: 0, scrollableParentSelector: ".modal-body", value: null, @@ -37,10 +38,12 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin filterPlaceholder: "select_box.filter_placeholder", filterIcon: "search", rowComponent: "select-box-kit/select-box-kit-row", + rowComponentOptions: null, noneRowComponent: "select-box-kit/select-box-kit-none-row", createRowComponent: "select-box-kit/select-box-kit-create-row", filterComponent: "select-box-kit/select-box-kit-filter", headerComponent: "select-box-kit/select-box-kit-header", + headerComponentOptions: null, collectionComponent: "select-box-kit/select-box-kit-collection", collectionHeight: 200, verticalOffset: 0, @@ -50,126 +53,92 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin allowAny: false, allowValueMutation: true, autoSelectFirst: true, + content: null, + _initialValues: null, init() { this._super(); + this.noneValue = "__none__"; + this._previousScrollParentOverflow = "auto"; + this._previousCSSContext = {}; + this.set("headerComponentOptions", Ember.Object.create()); + this.set("rowComponentOptions", Ember.Object.create()); + if ($(window).outerWidth(false) <= 420) { this.setProperties({ filterable: false, autoFilterable: false }); } - this._previousScrollParentOverflow = "auto"; - this._previousCSSContext = {}; + if (isNone(this.get("content"))) { this.set("content", []); } + this.set("value", this._castInteger(this.get("value"))); + + this.setInitialValues(); }, - click(event) { - event.stopPropagation(); + setInitialValues() { + this.set("_initialValues", this.getWithDefault("content", []).map((c) => { + return this._valueForContent(c); + })); }, - close() { - this.setProperties({ isExpanded: false, isFocused: false }); + @computed("computedContent.[]", "computedValue.[]", "filter") + filteredContent(computedContent, computedValue, filter) { + return this.filteredContentFunction(computedContent, computedValue, filter); }, - focus() { - Ember.run.schedule("afterRender", () => this.$offscreenInput().select() ); + filteredContentFunction(computedContent, computedValue, filter) { + if (isEmpty(filter)) { return computedContent; } + + const lowerFilter = filter.toLowerCase(); + return computedContent.filter(c => { + return get(c, "name").toLowerCase().indexOf(lowerFilter) > -1; + }); }, - blur() { - Ember.run.schedule("afterRender", () => this.$offscreenInput().blur() ); - }, + formatRowContent(content) { + let originalContent; - clickOutside(event) { - if ($(event.target).parents(".select-box-kit").length === 1) { - this.close(); - return; - } - - if (this.get("isExpanded") === true) { - this.set("isExpanded", false); - this.focus(); + if (typeof content === "string" || typeof content === "number") { + originalContent = {}; + originalContent[this.get("valueAttribute")] = content; + originalContent[this.get("nameProperty")] = content; } else { - this.close(); - } - }, - - createFunction(input) { - return (selectedBox) => { - const formatedContent = selectedBox.formatContent(input); - formatedContent.meta.generated = true; - return formatedContent; - }; - }, - - filterFunction(content) { - return selectBox => { - const filter = selectBox.get("filter").toLowerCase(); - return _.filter(content, c => { - return get(c, "name").toLowerCase().indexOf(filter) > -1; - }); - }; - }, - - nameForContent(content) { - if (isNone(content)) { - return null; + originalContent = content; } - if (typeof content === "object") { - return get(content, this.get("nameProperty")); - } - - return content; - }, - - valueForContent(content) { - switch (typeof content) { - case "string": - case "number": - return this._castInteger(content); - default: - return this._castInteger(get(content, this.get("valueAttribute"))); - } - }, - - formatContent(content) { return { - value: this.valueForContent(content), - name: this.nameForContent(content), - originalContent: content, - meta: { generated: false } + value: this._castInteger(this._valueForContent(content)), + name: this._nameForContent(content), + locked: false, + originalContent }; }, formatContents(contents) { - return contents.map(content => this.formatContent(content)); + return contents.map(content => this.formatRowContent(content)); }, - @computed("filter", "filterable", "autoFilterable") - computedFilterable(filter, filterable, autoFilterable) { - if (filterable === true) { - return true; - } - - if (filter.length > 0 && autoFilterable === true) { - return true; - } - + @computed("filter", "filterable", "autoFilterable", "renderedFilterOnce") + shouldDisplayFilter(filter, filterable, autoFilterable, renderedFilterOnce) { + if (renderedFilterOnce === true || filterable === true) { return true; } + if (filter.length > 0 && autoFilterable === true) { return true; } return false; }, - @computed("computedFilterable", "filter", "allowAny") - shouldDisplayCreateRow(computedFilterable, filter, allow) { - return computedFilterable === true && filter.length > 0 && allow === true; + @computed("filter") + shouldDisplayCreateRow(filter) { + if (this.get("allowAny") === true && filter.length > 0) { return true; } + return false; }, - @computed("filter", "allowAny") - createRowContent(filter, allow) { - if (allow === true) { + @computed("filter", "shouldDisplayCreateRow") + createRowContent(filter, shouldDisplayCreateRow) { + if (shouldDisplayCreateRow === true && !this.get("value").includes(filter)) { return Ember.Object.create({ value: filter, name: filter }); } }, - @computed("content.[]") + @computed("content.[]", "value.[]") computedContent(content) { this._mutateValue(); return this.formatContents(content || []); @@ -178,10 +147,10 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin @computed("value", "none", "computedContent.firstObject.value") computedValue(value, none, firstContentValue) { if (isNone(value) && isNone(none) && this.get("autoSelectFirst") === true) { - return this._castInteger(firstContentValue); + return firstContentValue; } - return this._castInteger(value); + return value; }, @computed @@ -195,90 +164,51 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin @computed("none") computedNone(none) { - if (isNone(none)) { - return null; - } + if (isNone(none)) { return null; } switch (typeof none) { case "string": - return Ember.Object.create({ name: I18n.t(none), value: "" }); + return Ember.Object.create({ name: I18n.t(none), value: this.noneValue }); default: - return this.formatContent(none); + return this.formatRowContent(none); } }, @computed("computedValue", "computedContent.[]") selectedContent(computedValue, computedContent) { - if (isNone(computedValue)) { - return []; - } - - return [ computedContent.findBy("value", this._castInteger(computedValue)) ]; - }, - - @on("didRender") - _configureSelectBoxDOM() { - if (this.get("isExpanded") === true) { - Ember.run.schedule("afterRender", () => { - this.$collection().css("max-height", this.get("collectionHeight")); - this._applyDirection(); - this._positionWrapper(); - }); - } - }, - - @on("willDestroyElement") - _cleanHandlers() { - $(window).off("resize.select-box-kit"); - this._removeFixedPosition(); + if (isNone(computedValue)) { return []; } + return [ computedContent.findBy("value", computedValue) ]; }, @on("didInsertElement") _setupResizeListener() { - $(window).on("resize.select-box-kit", () => this.set("isExpanded", false) ); + $(window).on("resize.select-box-kit", () => this.collapse() ); }, - @observes("filter", "filteredContent.[]", "shouldDisplayCreateRow") - _setHighlightedValue() { - const filteredContent = this.get("filteredContent"); - const display = this.get("shouldDisplayCreateRow"); - const none = this.get("computedNone"); - if (isNone(this.get("highlightedValue")) && !isEmpty(filteredContent)) { - this.set("highlightedValue", get(filteredContent, "firstObject.value")); - return; - } + autoHighlightFunction() { + Ember.run.schedule("afterRender", () => { + if (!isNone(this.get("highlightedValue"))) { return; } - if (display === true && isEmpty(filteredContent)) { - this.set("highlightedValue", this.get("filter")); - } - else if (!isEmpty(filteredContent)) { - this.set("highlightedValue", get(filteredContent, "firstObject.value")); - } - else if (isEmpty(filteredContent) && isPresent(none) && display === false) { - this.set("highlightedValue", get(none, "value")); - } - }, + const filteredContent = this.get("filteredContent"); + const display = this.get("shouldDisplayCreateRow"); + const none = this.get("computedNone"); - @observes("isExpanded") - _isExpandedChanged() { - if (this.get("isExpanded") === true) { - this._applyFixedPosition(); + if (isNone(this.get("highlightedValue")) && !isEmpty(filteredContent)) { + this.send("onHighlight", get(filteredContent, "firstObject.value")); + return; + } - this.setProperties({ - highlightedValue: this.get("computedValue"), - renderBody: true, - isFocused: true - }); - } else { - this._removeFixedPosition(); - } - }, - - @computed("filter", "computedFilterable", "computedContent.[]", "computedValue.[]") - filteredContent(filter, computedFilterable, computedContent, computedValue) { - if (computedFilterable === false) { return computedContent; } - return this.filterFunction(computedContent)(this, computedValue); + if (display === true && isEmpty(filteredContent)) { + this.send("onHighlight", this.get("filter")); + } + else if (!isEmpty(filteredContent)) { + this.send("onHighlight", get(filteredContent, "firstObject.value")); + } + else if (isEmpty(filteredContent) && isPresent(none) && display === false) { + this.send("onHighlight", get(none, "value")); + } + }); }, @computed("scrollableParentSelector") @@ -286,191 +216,100 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin return this.$().parents(scrollableParentSelector).first(); }, + willFilterContent() { + this.expand(); + this.set("highlightedValue", null); + }, + didFilterContent() { + this.set("renderedFilterOnce", true); + this.autoHighlightFunction(); + }, + + willCreateContent() { }, + createContentFunction(input) { + this.get("content").pushObject(input); + this.send("onSelect", input); + }, + didCreateContent() { + this.clearFilter(); + this.autoHighlightFunction(); + }, + + willHighlightValue() {}, + highlightValueFunction(value) { + this.set("highlightedValue", value); + }, + didHighlightValue() {}, + + willSelectValue() { + this.clearFilter(); + this.set("highlightedValue", null); + }, + selectValueFunction(value) { + this.set("value", value); + }, + didSelectValue() { + this.collapse(); + this.focus(); + }, + + willDeselectValue() { + this.set("highlightedValue", null); + }, + unsetValueFunction() { + this.set("value", null); + }, + didDeselectValue() { + this.focus(); + }, + actions: { onToggle() { - this.toggleProperty("isExpanded"); - - if (this.get("isExpanded") === true) { this.focus(); } - }, - - onCreateContent(input) { - const content = this.createFunction(input)(this); - this.get("computedContent").pushObject(content); - this.send("onSelect", content.value); - }, - - onFilterChange(filter) { - this.set("filter", filter); - }, - - onHighlight(value) { - this.set("highlightedValue", value); + this.get("isExpanded") === true ? this.collapse() : this.expand(); }, onClearSelection() { - this.send("onSelect", null); + this.send("onDeselect", this.get("value")); + }, + + onHighlight(value) { + value = this._originalValueForValue(value); + this.willHighlightValue(value); + this.set("highlightedValue", value); + this.highlightValueFunction(value); + this.didHighlightValue(value); + }, + + onCreateContent(input) { + this.willCreateContent(input); + this.createContentFunction(input); + this.didCreateContent(input); }, onSelect(value) { - value = this.defaultOnSelect(value); - this.set("value", value); + if (value === "") { value = null; } + this.willSelectValue(value); + this.selectValueFunction(value); + this.didSelectValue(value); }, - onDeselect() { - this.defaultOnDeselect(); - this.set("value", null); - } + onDeselect(value) { + value = this._originalValueForValue(value); + this.willDeselectValue(value); + this.unsetValueFunction(value); + this.didSelectValue(value); + }, + + onFilterChange(_filter) { + this.willFilterContent(_filter); + this.set("filter", _filter); + this.didFilterContent(_filter); + }, }, - defaultOnSelect(value) { - if (value === "") { value = null; } - - this.setProperties({ - highlightedValue: null, - isExpanded: false, - filter: "" - }); - - this.focus(); - - return value; - }, - - defaultOnDeselect(value) { - const content = this.get("computedContent").findBy("value", value); - if (!isNone(content) && get(content, "meta.generated") === true) { - this.get("computedContent").removeObject(content); - } - }, - - _applyDirection() { - let options = { left: "auto", bottom: "auto", top: "auto" }; - - const dHeader = $(".d-header")[0]; - const dHeaderBounds = dHeader ? dHeader.getBoundingClientRect() : {top: 0, height: 0}; - const dHeaderHeight = dHeaderBounds.top + dHeaderBounds.height; - const headerHeight = this.$header().outerHeight(false); - const headerWidth = this.$header().outerWidth(false); - const bodyHeight = this.$body().outerHeight(false); - const windowWidth = $(window).width(); - const windowHeight = $(window).height(); - const boundingRect = this.get("element").getBoundingClientRect(); - const offsetTop = boundingRect.top; - const offsetBottom = boundingRect.bottom; - - if (this.get("fullWidthOnMobile") && windowWidth <= 420) { - const margin = 10; - const relativeLeft = this.$().offset().left - $(window).scrollLeft(); - options.left = margin - relativeLeft; - options.width = windowWidth - margin * 2; - options.maxWidth = options.minWidth = "unset"; - } else { - const bodyWidth = this.$body().outerWidth(false); - - if ($("html").css("direction") === "rtl") { - const horizontalSpacing = boundingRect.right; - const hasHorizontalSpace = horizontalSpacing - (this.get("horizontalOffset") + bodyWidth) > 0; - if (hasHorizontalSpace) { - this.setProperties({ isLeftAligned: true, isRightAligned: false }); - options.left = bodyWidth + this.get("horizontalOffset"); - } else { - this.setProperties({ isLeftAligned: false, isRightAligned: true }); - options.right = - (bodyWidth - headerWidth + this.get("horizontalOffset")); - } - } else { - const horizontalSpacing = boundingRect.left; - const hasHorizontalSpace = (windowWidth - (this.get("horizontalOffset") + horizontalSpacing + bodyWidth) > 0); - if (hasHorizontalSpace) { - this.setProperties({ isLeftAligned: true, isRightAligned: false }); - options.left = this.get("horizontalOffset"); - } else { - this.setProperties({ isLeftAligned: false, isRightAligned: true }); - options.right = this.get("horizontalOffset"); - } - } - } - - const componentHeight = this.get("verticalOffset") + bodyHeight + headerHeight; - const hasBelowSpace = windowHeight - offsetBottom - componentHeight > 0; - const hasAboveSpace = offsetTop - componentHeight - dHeaderHeight > 0; - if (hasBelowSpace || (!hasBelowSpace && !hasAboveSpace)) { - this.setProperties({ isBelow: true, isAbove: false }); - options.top = headerHeight + this.get("verticalOffset"); - } else { - this.setProperties({ isBelow: false, isAbove: true }); - options.bottom = headerHeight + this.get("verticalOffset"); - } - - this.$body().css(options); - }, - - _applyFixedPosition() { - const width = this.$().outerWidth(false); - const height = this.$header().outerHeight(false); - - if (this.get("scrollableParent").length === 0) { return; } - - const $placeholder = $(`
    `); - - this._previousScrollParentOverflow = this.get("scrollableParent").css("overflow"); - this.get("scrollableParent").css({ overflow: "hidden" }); - - this._previousCSSContext = { - minWidth: this.$().css("min-width"), - maxWidth: this.$().css("max-width") - }; - - const componentStyles = { - position: "fixed", - "margin-top": -this.get("scrollableParent").scrollTop(), - width, - minWidth: "unset", - maxWidth: "unset" - }; - - if ($("html").css("direction") === "rtl") { - componentStyles.marginRight = -width; - } else { - componentStyles.marginLeft = -width; - } - - $placeholder.css({ display: "inline-block", width, height, "vertical-align": "middle" }); - - this.$().before($placeholder).css(componentStyles); - }, - - _removeFixedPosition() { - if (this.get("scrollableParent").length === 0) { - return; - } - - $(`.select-box-kit-fixed-placeholder-${this.elementId}`).remove(); - - const css = _.extend( - this._previousCSSContext, - { - top: "auto", - left: "auto", - "margin-left": "auto", - "margin-right": "auto", - "margin-top": "auto", - position: "relative" - } - ); - this.$().css(css); - - this.get("scrollableParent").css({ - overflow: this._previousScrollParentOverflow - }); - }, - - _positionWrapper() { - const headerHeight = this.$header().outerHeight(false); - - this.$(".select-box-kit-wrapper").css({ - width: this.$().width(), - height: headerHeight + this.$body().outerHeight(false) - }); + clearFilter() { + this.$filterInput().val(""); + this.setProperties({ filter: "" }); }, @on("didReceiveAttrs") diff --git a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-filter.js.es6 b/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-filter.js.es6 index 9717b59f78..aafaa5e136 100644 --- a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-filter.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-filter.js.es6 @@ -2,5 +2,5 @@ export default Ember.Component.extend({ layoutName: "select-box-kit/templates/components/select-box-kit/select-box-kit-filter", classNames: "select-box-kit-filter", classNameBindings: ["isFocused", "isHidden"], - isHidden: Ember.computed.not("filterable"), + isHidden: Ember.computed.not("shouldDisplayFilter") }); diff --git a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-row.js.es6 b/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-row.js.es6 index efdb299a56..0ef116bba9 100644 --- a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-row.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-row.js.es6 @@ -8,12 +8,15 @@ export default Ember.Component.extend(UtilsMixin, { layoutName: "select-box-kit/templates/components/select-box-kit/select-box-kit-row", classNames: "select-box-kit-row", tagName: "li", + tabIndex: -1, attributeBindings: [ + "tabIndex", "title", "content.value:data-value", "content.name:data-name" ], classNameBindings: ["isHighlighted", "isSelected"], + clicked: false, title: Ember.computed.alias("content.name"), @@ -23,17 +26,15 @@ export default Ember.Component.extend(UtilsMixin, { @on("didReceiveAttrs") _setSelectionState() { const contentValue = this.get("content.value"); + this.set("isSelected", this.get("value") === contentValue); - this.set("isHighlighted", this._castInteger(this.get("highlightedValue")) === this._castInteger(contentValue)); + this.set("isHighlighted", this.get("highlightedValue") === contentValue); }, @on("willDestroyElement") _clearDebounce() { const hoverDebounce = this.get("hoverDebounce"); - - if (isPresent(hoverDebounce)) { - run.cancel(hoverDebounce); - } + if (isPresent(hoverDebounce)) { run.cancel(hoverDebounce); } }, @computed("content.originalContent.icon", "content.originalContent.iconClass") @@ -50,7 +51,14 @@ export default Ember.Component.extend(UtilsMixin, { }, click() { - this.sendAction("onSelect", this.get("content.value")); + this._sendOnSelectAction(); + }, + + _sendOnSelectAction() { + if (this.get("clicked") === false) { + this.set("clicked", true); + this.sendAction("onSelect", this.get("content.value")); + } }, _sendOnHighlightAction() { diff --git a/app/assets/javascripts/select-box-kit/components/tag-notifications-button.js.es6 b/app/assets/javascripts/select-box-kit/components/tag-notifications-button.js.es6 index 2ab3ed375b..bc2d95ac4d 100644 --- a/app/assets/javascripts/select-box-kit/components/tag-notifications-button.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/tag-notifications-button.js.es6 @@ -6,11 +6,7 @@ export default NotificationOptionsComponent.extend({ showFullTitle: false, headerComponent: "tag-notifications-button/tag-notifications-button-header", - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); - this.sendAction("action", value); - this.blur(); - } + selectValueFunction(value) { + this.sendAction("action", value); } }); diff --git a/app/assets/javascripts/select-box-kit/components/topic-footer-mobile-dropdown.js.es6 b/app/assets/javascripts/select-box-kit/components/topic-footer-mobile-dropdown.js.es6 index 2fb8f3e317..2222fbc822 100644 --- a/app/assets/javascripts/select-box-kit/components/topic-footer-mobile-dropdown.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/topic-footer-mobile-dropdown.js.es6 @@ -39,38 +39,34 @@ export default ComboBoxComponent.extend({ return content; }, - actions: { - onSelect(value) { - value = this.defaultOnSelect(value); + selectValueFunction(value) { + const topic = this.get("topic"); - const topic = this.get("topic"); + // In case it"s not a valid topic + if (!topic.get("id")) { + return; + } - // In case it"s not a valid topic - if (!topic.get("id")) { - return; - } + this.set("value", value); - this.set("value", value); + const refresh = () => this.send("onDeselect", value); - const refresh = () => this.set("value", null); - - switch(value) { - case "invite": - this.attrs.showInvite(); - refresh(); - break; - case "bookmark": - topic.toggleBookmark().then(() => refresh() ); - break; - case "share": - this.appEvents.trigger("share:url", topic.get("shareUrl"), $("#topic-footer-buttons")); - refresh(); - break; - case "flag": - this.attrs.showFlagTopic(); - refresh(); - break; - } + switch(value) { + case "invite": + this.attrs.showInvite(); + refresh(); + break; + case "bookmark": + topic.toggleBookmark().then(() => refresh() ); + break; + case "share": + this.appEvents.trigger("share:url", topic.get("shareUrl"), $("#topic-footer-buttons")); + refresh(); + break; + case "flag": + this.attrs.showFlagTopic(); + refresh(); + break; } } }); diff --git a/app/assets/javascripts/select-box-kit/components/topic-notifications-options.js.es6 b/app/assets/javascripts/select-box-kit/components/topic-notifications-options.js.es6 index d1da27684a..34f63f4ea5 100644 --- a/app/assets/javascripts/select-box-kit/components/topic-notifications-options.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/topic-notifications-options.js.es6 @@ -24,17 +24,11 @@ export default NotificationOptionsComponent.extend({ this.appEvents.off("topic-notifications-button:changed"); }, - actions: { - onSelect(value) { - if (value !== this.get("computedValue")) { - this.get("topic.details").updateNotifications(value); - } - - this.set("value", value); - - this.defaultOnSelect(value); - - this.blur(); + selectValueFunction(value) { + if (value !== this.get("value")) { + this.get("topic.details").updateNotifications(value); } + + this.set("value", value); } }); diff --git a/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 b/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 index d8caacb501..ed70bcc59e 100644 --- a/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 +++ b/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 @@ -1,3 +1,5 @@ +import { on } from "ember-addons/ember-computed-decorators"; + export default Ember.Mixin.create({ init() { this._super(); @@ -8,46 +10,237 @@ export default Ember.Mixin.create({ this.collectionSelector = ".select-box-kit-collection"; this.headerSelector = ".select-box-kit-header"; this.bodySelector = ".select-box-kit-body"; + this.wrapperSelector = ".select-box-kit-wrapper"; }, - $findRowByValue(value) { - return this.$(`${this.rowSelector}[data-value='${value}']`); + $findRowByValue(value) { return this.$(`${this.rowSelector}[data-value='${value}']`); }, + + $header() { return this.$(this.headerSelector); }, + + $body() { return this.$(this.bodySelector); }, + + $collection() { return this.$(this.collectionSelector); }, + + $rows(withHidden) { + + if (withHidden === true) { + return this.$(`${this.rowSelector}:not(.no-content)`); + } else { + return this.$(`${this.rowSelector}:not(.no-content):not(.is-hidden)`); + } }, - $header() { - return this.$(this.headerSelector); + $highlightedRow() { return this.$rows().filter(".is-highlighted"); }, + + $selectedRow() { return this.$rows().filter(".is-selected"); }, + + $offscreenInput() { return this.$(this.offscreenInputSelector); }, + + $filterInput() { return this.$(this.filterInputSelector); }, + + @on("didRender") + _ajustPosition() { + $(`.select-box-kit-fixed-placeholder-${this.elementId}`).remove(); + this.$collection().css("max-height", this.get("collectionHeight")); + this._applyFixedPosition(); + this._applyDirection(); + this._positionWrapper(); }, - $body() { - return this.$(this.bodySelector); + @on("willDestroyElement") + _clearState() { + $(window).off("resize.select-box-kit"); + $(`.select-box-kit-fixed-placeholder-${this.elementId}`).remove(); }, - $collection() { - return this.$(this.collectionSelector); + // make sure we don’t propagate a click outside component + // to avoid closing a modal containing the component for example + click(event) { this._killEvent(event); }, + + // use to collapse and remove focus + close() { + this.collapse(); + this.setProperties({ isFocused: false }); }, - $rows() { - return this.$(this.rowSelector); + // force the component in a known default state + focus() { + Ember.run.schedule("afterRender", () => this.$offscreenInput().focus() ); }, - $highlightedRow() { - return this.$rows().filter(".is-highlighted"); + expand() { + if (this.get("isExpanded") === true) { return; } + this.setProperties({ isExpanded: true, renderedBodyOnce: true, isFocused: true }); + this.focus(); + this.autoHighlightFunction(); }, - $selectedRow() { - return this.$rows().filter(".is-selected"); + collapse() { + this.set("isExpanded", false); + Ember.run.schedule("afterRender", () => this._removeFixedPosition() ); }, - $offscreenInput() { - return this.$(this.offscreenInputSelector); + // make sure we close/unfocus the component when clicked outside + clickOutside(event) { + if ($(event.target).parents(".select-box-kit").length === 1) { + this.close(); + return false; + } + + this.unfocus(); + return; }, - $filterInput() { - return this.$(this.filterInputSelector); + // lose focus of the component in two steps + // first collapase and keep focus and then remove focus + unfocus() { + this.set("highlightedValue", null); + + if (this.get("isExpanded") === true) { + this.collapse(); + this.focus(); + } else { + this.close(); + } + }, + + blur() { + Ember.run.schedule("afterRender", () => this.$offscreenInput().blur() ); }, _killEvent(event) { event.preventDefault(); event.stopPropagation(); - } + }, + + _applyDirection() { + let options = { left: "auto", bottom: "auto", top: "auto" }; + + const dHeader = $(".d-header")[0]; + const dHeaderBounds = dHeader ? dHeader.getBoundingClientRect() : {top: 0, height: 0}; + const dHeaderHeight = dHeaderBounds.top + dHeaderBounds.height; + const headerHeight = this.$header().outerHeight(false); + const headerWidth = this.$header().outerWidth(false); + const bodyHeight = this.$body().outerHeight(false); + const windowWidth = $(window).width(); + const windowHeight = $(window).height(); + const boundingRect = this.get("element").getBoundingClientRect(); + const offsetTop = boundingRect.top; + const offsetBottom = boundingRect.bottom; + + if (this.get("fullWidthOnMobile") && windowWidth <= 420) { + const margin = 10; + const relativeLeft = this.$().offset().left - $(window).scrollLeft(); + options.left = margin - relativeLeft; + options.width = windowWidth - margin * 2; + options.maxWidth = options.minWidth = "unset"; + } else { + const bodyWidth = this.$body().outerWidth(false); + + if ($("html").css("direction") === "rtl") { + const horizontalSpacing = boundingRect.right; + const hasHorizontalSpace = horizontalSpacing - (this.get("horizontalOffset") + bodyWidth) > 0; + if (hasHorizontalSpace) { + this.setProperties({ isLeftAligned: true, isRightAligned: false }); + options.left = bodyWidth + this.get("horizontalOffset"); + } else { + this.setProperties({ isLeftAligned: false, isRightAligned: true }); + options.right = - (bodyWidth - headerWidth + this.get("horizontalOffset")); + } + } else { + const horizontalSpacing = boundingRect.left; + const hasHorizontalSpace = (windowWidth - (this.get("horizontalOffset") + horizontalSpacing + bodyWidth) > 0); + if (hasHorizontalSpace) { + this.setProperties({ isLeftAligned: true, isRightAligned: false }); + options.left = this.get("horizontalOffset"); + } else { + this.setProperties({ isLeftAligned: false, isRightAligned: true }); + options.right = this.get("horizontalOffset"); + } + } + } + + const componentHeight = this.get("verticalOffset") + bodyHeight + headerHeight; + const hasBelowSpace = windowHeight - offsetBottom - componentHeight > 0; + const hasAboveSpace = offsetTop - componentHeight - dHeaderHeight > 0; + if (hasBelowSpace || (!hasBelowSpace && !hasAboveSpace)) { + this.setProperties({ isBelow: true, isAbove: false }); + options.top = headerHeight + this.get("verticalOffset"); + } else { + this.setProperties({ isBelow: false, isAbove: true }); + options.bottom = headerHeight + this.get("verticalOffset"); + } + + this.$body().css(options); + }, + + _applyFixedPosition() { + if (this.get("scrollableParent").length === 0) { return; } + + const width = this.$().outerWidth(false); + const height = this.$().outerHeight(false); + const $placeholder = $(`
    `); + + this._previousScrollParentOverflow = this.get("scrollableParent").css("overflow"); + this.get("scrollableParent").css({ overflow: "hidden" }); + + this._previousCSSContext = { + minWidth: this.$().css("min-width"), + maxWidth: this.$().css("max-width") + }; + + const componentStyles = { + position: "fixed", + "margin-top": -this.get("scrollableParent").scrollTop(), + width, + minWidth: "unset", + maxWidth: "unset" + }; + + if ($("html").css("direction") === "rtl") { + componentStyles.marginRight = -width; + } else { + componentStyles.marginLeft = -width; + } + + $placeholder.css({ display: "inline-block", width, height, "vertical-align": "middle" }); + + this.$().before($placeholder).css(componentStyles); + }, + + _removeFixedPosition() { + $(`.select-box-kit-fixed-placeholder-${this.elementId}`).remove(); + + if (this.get("scrollableParent").length === 0) { + return; + } + + if (!this.element || this.isDestroying || this.isDestroyed) { return; } + + const css = jQuery.extend( + this._previousCSSContext, + { + top: "auto", + left: "auto", + "margin-left": "auto", + "margin-right": "auto", + "margin-top": "auto", + position: "relative" + } + ); + this.$().css(css); + + this.get("scrollableParent").css({ + overflow: this._previousScrollParentOverflow + }); + }, + + _positionWrapper() { + const headerHeight = this.$header().outerHeight(false); + + this.$(this.wrapperSelector).css({ + width: this.$().outerWidth(false), + height: headerHeight + this.$body().outerHeight(false) + }); + }, }); diff --git a/app/assets/javascripts/select-box-kit/mixins/keyboard.js.es6 b/app/assets/javascripts/select-box-kit/mixins/keyboard.js.es6 index 0de89dbb2e..e7973f5b0f 100644 --- a/app/assets/javascripts/select-box-kit/mixins/keyboard.js.es6 +++ b/app/assets/javascripts/select-box-kit/mixins/keyboard.js.es6 @@ -1,5 +1,3 @@ -const { isEmpty } = Ember; - export default Ember.Mixin.create({ init() { this._super(); @@ -35,9 +33,13 @@ export default Ember.Mixin.create({ .off("focus.select-box-kit") .off("focusin.select-box-kit") .off("blur.select-box-kit") + .off("keypress.select-box-kit") .off("keydown.select-box-kit"); - this.$filterInput().off("keydown.select-box-kit"); + this.$filterInput() + .off("change.select-box-kit") + .off("keypress.select-box-kit") + .off("keydown.select-box-kit"); }, didInsertElement() { @@ -70,58 +72,67 @@ export default Ember.Mixin.create({ .on("keydown.select-box-kit", (event) => { const keyCode = event.keyCode || event.which; + if (keyCode === this.keys.TAB) { this._handleTabOnKeyDown(event); } + if (keyCode === this.keys.ESC) { this._handleEscOnKeyDown(event); } + if (keyCode === this.keys.UP || keyCode === this.keys.DOWN) { + this._handleArrowKey(keyCode, event); + } + if (keyCode === this.keys.BACKSPACE) { + this.expand(); + + if (this.$filterInput().is(":visible")) { + this.$filterInput().focus().trigger(event).trigger("change"); + } + + return event; + } + + return true; + }) + .on("keypress.select-box-kit", (event) => { + const keyCode = event.keyCode || event.which; + switch (keyCode) { - case this.keys.UP: - case this.keys.DOWN: - if (this.get("isExpanded") === false) { - this.set("isExpanded", true); - } - - Ember.run.schedule("actions", () => { - this._handleArrowKey(keyCode); - }); - - this._killEvent(event); - - return; case this.keys.ENTER: if (this.get("isExpanded") === false) { - this.set("isExpanded", true); - } else { - this.send("onSelect", this.$highlightedRow().data("value")); + this.expand(); + } else if (this.$highlightedRow().length === 1) { + this.$highlightedRow().click(); } - - this._killEvent(event); - - return; - case this.keys.TAB: - if (this.get("isExpanded") === false) { - return true; - } else { - this.send("onSelect", this.$highlightedRow().data("value")); - return; - } - case this.keys.ESC: - this.close(); - this._killEvent(event); - return; + return false; case this.keys.BACKSPACE: - this._killEvent(event); - return; + return event; } if (this._isSpecialKey(keyCode) === false && event.metaKey === false) { - this.setProperties({ - isExpanded: true, - filter: String.fromCharCode(keyCode) - }); + this.expand(); - Ember.run.schedule("afterRender", () => this.$filterInput().focus() ); + if (this.get("filterable") === true || this.get("autoFilterable")) { + this.set("renderedFilterOnce", true); + } + + Ember.run.schedule("afterRender", () => { + this.$filterInput() + .focus() + .val(this.$filterInput().val() + String.fromCharCode(keyCode)); + }); } }); this.$filterInput() - .on(`keydown.select-box-kit`, (event) => { + .on("change.select-box-kit", (event) => { + this.send("onFilterChange", $(event.target).val()); + }) + .on("keydown.select-box-kit", (event) => { + const keyCode = event.keyCode || event.which; + + if (keyCode === this.keys.TAB) { this._handleTabOnKeyDown(event); } + if (keyCode === this.keys.ESC) { this._handleEscOnKeyDown(event); } + if (keyCode === this.keys.UP || keyCode === this.keys.DOWN) { + this._handleArrowKey(keyCode, event); + } + }) + .on("keypress.select-box-kit", (event) => { const keyCode = event.keyCode || event.which; if ([ @@ -133,67 +144,75 @@ export default Ember.Mixin.create({ return true; } + if (keyCode === this.keys.TAB && this.get("isExpanded") === false) { + return true; + } + if (this._isSpecialKey(keyCode) === true) { this.$offscreenInput().focus().trigger(event); + return false; } return true; }); }, - _handleArrowKey(keyCode) { - if (isEmpty(this.get("filteredContent"))) { + _handleEscOnKeyDown(event) { + this.unfocus(); + this._killEvent(event); + }, + + _handleTabOnKeyDown(event) { + if (this.get("isExpanded") === false) { + this.unfocus(); + return true; + } else if (this.$highlightedRow().length === 1) { + this._killEvent(event); + this.$highlightedRow().click(); + this.focus(); + } else { + this.unfocus(); + return true; + } + return false; + }, + + _handleArrowKey(keyCode, event) { + if (this.get("isExpanded") === false) { this.expand(); } + this._killEvent(event); + const $rows = this.$rows(); + + if ($rows.length <= 0) { return; } + if ($rows.length === 1) { + this._rowSelection($rows, 0); return; } - Ember.run.schedule("afterRender", () => { - switch (keyCode) { - case 38: - Ember.run.throttle(this, this._handleUpArrow, 32); - break; - default: - Ember.run.throttle(this, this._handleDownArrow, 32); - } - }); + const direction = keyCode === 38 ? -1 : 1; + + Ember.run.throttle(this, this._moveHighlight, direction, $rows, 32); }, - _moveHighlight(direction) { - const $rows = this.$rows(); + _moveHighlight(direction, $rows) { const currentIndex = $rows.index(this.$highlightedRow()); + let nextIndex = currentIndex + direction; - let nextIndex = 0; - - if (currentIndex < 0) { + if (nextIndex < 0) { + nextIndex = $rows.length - 1; + } else if (nextIndex >= $rows.length) { nextIndex = 0; - } else if (currentIndex + direction < $rows.length) { - nextIndex = currentIndex + direction; } this._rowSelection($rows, nextIndex); }, - _handleDownArrow() { this._moveHighlight(1); }, - - _handleUpArrow() { this._moveHighlight(-1); }, - _rowSelection($rows, nextIndex) { - const highlightableValue = $rows.eq(nextIndex).data("value"); + const highlightableValue = $rows.eq(nextIndex).attr("data-value"); const $highlightableRow = this.$findRowByValue(highlightableValue); - this.send("onHighlight", highlightableValue); Ember.run.schedule("afterRender", () => { - const $collection = this.$collection(); - const currentOffset = $collection.offset().top + - $collection.outerHeight(false); - const nextBottom = $highlightableRow.offset().top + - $highlightableRow.outerHeight(false); - const nextOffset = $collection.scrollTop() + nextBottom - currentOffset; - - if (nextIndex === 0) { - $collection.scrollTop(0); - } else if (nextBottom > currentOffset) { - $collection.scrollTop(nextOffset); - } + $highlightableRow.trigger("mouseover").focus(); + this.focus(); }); }, diff --git a/app/assets/javascripts/select-box-kit/mixins/utils.js.es6 b/app/assets/javascripts/select-box-kit/mixins/utils.js.es6 index 05d896a832..3ece47cc3b 100644 --- a/app/assets/javascripts/select-box-kit/mixins/utils.js.es6 +++ b/app/assets/javascripts/select-box-kit/mixins/utils.js.es6 @@ -1,9 +1,57 @@ +const { get, isNone } = Ember; + export default Ember.Mixin.create({ + _nameForContent(content) { + if (isNone(content)) { + return null; + } + + if (typeof content === "object") { + return get(content, this.get("nameProperty")); + } + + return content; + }, + _castInteger(value) { if (this.get("castInteger") === true && Ember.isPresent(value)) { return parseInt(value, 10); } - return Ember.isNone(value) ? value : value.toString(); - } + return value; + }, + + _valueForContent(content) { + switch (typeof content) { + case "string": + case "number": + return content; + default: + return get(content, this.get("valueAttribute")); + } + }, + + _contentForValue(value) { + return this.get("content").find(c => { + if (this._valueForContent(c) === value) { return true; } + }); + }, + + _computedContentForValue(value) { + const searchedValue = value.toString(); + return this.get("computedContent").find(c => { + if (c.value.toString() === searchedValue) { return true; } + }); + }, + + _originalValueForValue(value) { + if (isNone(value)) { return null; } + if (value === this.noneValue) { return this.noneValue; } + + const computedContent = this._computedContentForValue(value); + + if (isNone(computedContent)) { return value; } + + return get(computedContent.originalContent, this.get("valueAttribute")); + }, }); diff --git a/app/assets/javascripts/select-box-kit/templates/components/multi-combo-box/multi-combo-box-header.hbs b/app/assets/javascripts/select-box-kit/templates/components/multi-combo-box/multi-combo-box-header.hbs index 13cf6c3f6d..6245b4e8c7 100644 --- a/app/assets/javascripts/select-box-kit/templates/components/multi-combo-box/multi-combo-box-header.hbs +++ b/app/assets/javascripts/select-box-kit/templates/components/multi-combo-box/multi-combo-box-header.hbs @@ -1,30 +1,13 @@
      {{#each selectedContent as |selectedContent|}} -
    • - - {{d-icon "times"}} - - - {{selectedContent.name}} - -
    • - {{else}} - {{#if shouldDisplayFilterPlaceholder}} -
    • - {{text}} -
    • - {{/if}} + {{component selectedNameComponent onDeselect=onDeselect content=selectedContent}} {{/each}} -
    • - {{input - class="select-box-kit-filter-input" - key-up=onFilterChange - autocomplete="off" - autocorrect="off" - autocapitalize="off" - spellcheck=false - value=filter + {{component "select-box-kit/select-box-kit-filter" + onFilterChange=onFilterChange + shouldDisplayFilter=shouldDisplayFilter + isFocused=isFocused + filter=filter }}
    diff --git a/app/assets/javascripts/select-box-kit/templates/components/multi-combo-box/selected-name.hbs b/app/assets/javascripts/select-box-kit/templates/components/multi-combo-box/selected-name.hbs new file mode 100644 index 0000000000..54ef362ed3 --- /dev/null +++ b/app/assets/javascripts/select-box-kit/templates/components/multi-combo-box/selected-name.hbs @@ -0,0 +1,9 @@ + + {{#unless isLocked}} + + {{d-icon "times"}} + + {{/unless}} + + {{content.name}} + diff --git a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit.hbs b/app/assets/javascripts/select-box-kit/templates/components/select-box-kit.hbs index 6575daf884..9c9e7188ad 100644 --- a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit.hbs +++ b/app/assets/javascripts/select-box-kit/templates/components/select-box-kit.hbs @@ -19,6 +19,7 @@ onToggle=(action "onToggle") onFilterChange=(action "onFilterChange") onClearSelection=(action "onClearSelection") + shouldDisplayFilter=shouldDisplayFilter options=headerComponentOptions }} @@ -26,16 +27,14 @@ {{component filterComponent onFilterChange=(action "onFilterChange") icon=filterIcon - filter=filter - filterable=computedFilterable + shouldDisplayFilter=shouldDisplayFilter isFocused=isFocused placeholder=(i18n filterPlaceholder) - tabindex=tabindex + filter=filter }} - {{#if renderBody}} + {{#if renderedBodyOnce}} {{component collectionComponent - shouldDisplayCreateRow=shouldDisplayCreateRow none=computedNone createRowContent=createRowContent selectedContent=selectedContent @@ -43,13 +42,9 @@ rowComponent=rowComponent noneRowComponent=noneRowComponent createRowComponent=createRowComponent - iconForRow=iconForRow templateForRow=templateForRow templateForNoneRow=templateForNoneRow templateForCreateRow=templateForCreateRow - shouldHighlightRow=shouldHighlightRow - shouldSelectRow=shouldSelectRow - titleForRow=titleForRow onClearSelection=(action "onClearSelection") onSelect=(action "onSelect") onHighlight=(action "onHighlight") diff --git a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-collection.hbs b/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-collection.hbs index 73f80e2608..bf7311049c 100644 --- a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-collection.hbs +++ b/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-collection.hbs @@ -3,8 +3,6 @@ {{component noneRowComponent content=none templateForRow=templateForNoneRow - titleForRow=titleForRow - iconForRow=iconForRow highlightedValue=highlightedValue onClearSelection=onClearSelection onHighlight=onHighlight @@ -14,12 +12,11 @@ {{/if}} {{/if}} -{{#if shouldDisplayCreateRow}} +{{#if createRowContent}} {{component createRowComponent content=createRowContent templateForRow=templateForCreateRow titleForRow=titleForRow - iconForRow=iconForRow highlightedValue=highlightedValue onHighlight=onHighlight onCreateContent=onCreateContent @@ -33,7 +30,6 @@ content=content templateForRow=templateForRow titleForRow=titleForRow - iconForRow=iconForRow highlightedValue=highlightedValue onSelect=onSelect onHighlight=onHighlight diff --git a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-filter.hbs b/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-filter.hbs index 74d1288074..dc50d22504 100644 --- a/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-filter.hbs +++ b/app/assets/javascripts/select-box-kit/templates/components/select-box-kit/select-box-kit-filter.hbs @@ -1,8 +1,8 @@ {{input - tabindex=tabindex + tabindex=-1 class="select-box-kit-filter-input" placeholder=placeholder - key-down=onFilterChange + key-up=onFilterChange autocomplete="off" autocorrect="off" autocapitalize="off" diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 2f418ddad3..eb8eb427d8 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -215,9 +215,8 @@ $mobile-breakpoint: 700px; .select-box-kit { width: 350px; } - - .select-box-kit-header { - height: 28px; + .select-box-kit.multi-combo-box { + width: 500px; } } @@ -620,6 +619,10 @@ section.details { text-align: left; margin-left: 0; } + + .select-box-kit { + width: inherit; + } } .long-value { width: 800px; @@ -701,7 +704,7 @@ section.details { .ace_editor { pointer-events:none; - + .ace_cursor { visibility: hidden; } diff --git a/app/assets/stylesheets/common/select-box-kit/dropdown-select-box.scss b/app/assets/stylesheets/common/select-box-kit/dropdown-select-box.scss index 363af0d795..670a31e657 100644 --- a/app/assets/stylesheets/common/select-box-kit/dropdown-select-box.scss +++ b/app/assets/stylesheets/common/select-box-kit/dropdown-select-box.scss @@ -89,14 +89,6 @@ white-space: normal; } } - - &.is-highlighted { - background: $tertiary-low; - } - - &:hover { - background: $highlight-medium; - } } .select-box-kit-collection { diff --git a/app/assets/stylesheets/common/select-box-kit/list-setting.scss b/app/assets/stylesheets/common/select-box-kit/list-setting.scss new file mode 100644 index 0000000000..eea89e6cb5 --- /dev/null +++ b/app/assets/stylesheets/common/select-box-kit/list-setting.scss @@ -0,0 +1,13 @@ +.select-box-kit { + &.multi-combo-box { + &.list-setting { + .select-box-kit-row.create { + .square { + width: 12px; + height: 12px; + margin-left: 5px; + } + } + } + } +} diff --git a/app/assets/stylesheets/common/select-box-kit/multi-combo-box.scss b/app/assets/stylesheets/common/select-box-kit/multi-combo-box.scss new file mode 100644 index 0000000000..501afde4e6 --- /dev/null +++ b/app/assets/stylesheets/common/select-box-kit/multi-combo-box.scss @@ -0,0 +1,147 @@ +.select-box-kit { + &.multi-combo-box { + width: 300px; + background: $secondary; + border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + border-radius: 0; + + .select-box-kit-body { + width: 100%; + } + + .select-box-kit-row { + margin: 5px; + min-height: 1px; + padding: 5px; + border-radius: 0; + } + + .select-box-kit-filter { + border: 0; + } + + .multi-combo-box-header { + background: $secondary; + border: 0; + border-bottom: 1px solid transparent; + + &.is-focused { + box-shadow: $tertiary 0px 0px 6px 0px; + border-radius: 0; + } + } + + &.is-disabled { + .multi-combo-box-header { + background: #e9e9e9; + border-color: #ddd; + } + } + + &.is-highlighted { + .multi-combo-box-header { + border-radius: 0; + border-bottom: 1px solid transparent; + box-shadow: $tertiary 0px 0px 6px 0px; + } + } + + &.is-expanded { + .select-box-kit-wrapper { + display: block; + border: 1px solid $tertiary; + box-shadow: $tertiary 0px 0px 6px 0px; + border-radius: 0; + } + + .multi-combo-box-header { + border-bottom: 1px solid $primary-low; + border-radius: 0; + box-shadow: none; + } + + .select-box-kit-body { + border-radius: 0; + } + } + + .choices { + list-style: none; + margin: 0; + padding: 5px; + flex: 1; + min-height: 36px; + box-sizing: border-box; + + li { + display: inline-flex; + box-sizing: border-box; + padding: 0 5px; + margin-bottom: 4px; + border: 1px solid transparent; + } + + .filter { + align-items: center; + justify-content: flex-start; + white-space: nowrap; + min-width: 50px; + padding: 0; + + .select-box-kit-filter-input, .select-box-kit-filter-input:focus { + border: none; + background: none; + display: inline-block; + width: 100%; + outline: none; + min-width: auto; + padding: 0; + margin: 0; + outline: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + border-radius: 0; + } + } + + .selected-name { + align-items: center; + justify-content: flex-start; + color: $primary; + cursor: default; + border: 1px solid $primary-medium; + border-radius: 3px; + box-shadow: 0 0 2px $secondary inset, 0 1px 0 rgba(0,0,0,0.05); + background-clip: padding-box; + -webkit-touch-callout: none; + user-select: none; + background-color: $primary-low; + cursor: pointer; + outline: none; + padding: 0; + line-height: normal; + + .name { + padding: 0 5px; + line-height: 22px + } + + &.is-highlighted { + border-color: $danger; + } + + .d-icon { + margin-right: 5px; + color: $primary-medium; + cursor: pointer; + font-size: 12px; + + &:hover { + color: $primary; + } + } + } + } + } +} diff --git a/app/assets/stylesheets/common/select-box-kit/multi-combobox.scss b/app/assets/stylesheets/common/select-box-kit/multi-combobox.scss deleted file mode 100644 index 87235fa8f3..0000000000 --- a/app/assets/stylesheets/common/select-box-kit/multi-combobox.scss +++ /dev/null @@ -1,131 +0,0 @@ -.multi-combobox { - width: 300px; - background: $secondary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - - .select-box-kit-body { - width: 100%; - } - - .select-box-kit-row { - margin: 5px; - min-height: 1px; - padding: 5px; - } - - .select-box-kit-filter { - border-top: 1px solid $primary-low; - } - - .select-box-kit-header { - background: $secondary; - border-bottom: 1px solid transparent; - - &.is-focused { - border-bottom: 1px solid $primary-low; - box-shadow: $tertiary 0px 0px 6px 0px; - } - } - - &.is-disabled { - .select-box-kit-header { - background: #e9e9e9; - border-color: #ddd; - } - } - - &.is-highlighted { - .select-box-kit-header { - border: 1px solid $tertiary; - box-shadow: $tertiary 0px 0px 6px 0px; - } - } - - &.is-expanded { - .select-box-kit-wrapper { - display: block; - border: 1px solid $tertiary; - box-shadow: $tertiary 0px 0px 6px 0px; - } - - .select-box-kit-header { - border-radius: 3px 3px 0 0; - } - - .select-box-kit-body { - border-radius: 3px 3px 0 0; - } - } - - .choices { - list-style: none; - margin: 0; - padding: 5px; - } - - .choice-placeholder { - padding: 0 5px; - margin: 2px 0; - border: 1px solid transparent; - display: inline-flex; - flex: 1; - } - - .filter { - display: inline-flex; - align-items: center; - justify-content: flex-start; - margin: 0; - padding: 0; - white-space: nowrap; - } - - .select-box-kit-filter-input, .select-box-kit-filter-input:focus { - border: none; - background: none; - display: inline-block; - width: 100%; - outline: none; - min-width: auto; - padding: 0; - margin: 0; - outline: 0; - border: 0; - -webkit-box-shadow: none; - box-shadow: none; - border-radius: 0; - } - - .selected-name { - display: inline-flex; - align-items: center; - justify-content: flex-start; - padding: 0 5px; - margin: 2px 0; - color: $primary; - cursor: default; - border: 1px solid $primary-medium; - border-radius: 3px; - box-shadow: 0 0 2px $secondary inset, 0 1px 0 rgba(0,0,0,0.05); - background-clip: padding-box; - -webkit-touch-callout: none; - user-select: none; - background-color: $primary-low; - - &:focus { - border-color: $primary; - outline: none; - } - - .d-icon { - margin-right: 5px; - color: $primary-medium; - cursor: pointer; - font-size: 12px; - - &:hover { - color: $primary; - } - } - } -} diff --git a/app/assets/stylesheets/common/select-box-kit/select-box-kit.scss b/app/assets/stylesheets/common/select-box-kit/select-box-kit.scss index 56ccf609ab..05f7d620f5 100644 --- a/app/assets/stylesheets/common/select-box-kit/select-box-kit.scss +++ b/app/assets/stylesheets/common/select-box-kit/select-box-kit.scss @@ -3,6 +3,7 @@ } .select-box-kit { + border: 1px solid transparent; min-width: 220px; -webkit-box-sizing: border-box; box-sizing: border-box; @@ -65,6 +66,9 @@ } .select-box-kit-header { + border: 1px solid transparent; + box-sizing: border-box; + overflow: hidden; -webkit-transition: all .25s; -o-transition: all .25s; transition: all .25s; @@ -138,10 +142,13 @@ .select-box-kit-row { cursor: pointer; + line-height: normal; outline: none; display: -webkit-box; display: -ms-flexbox; display: flex; + -webkit-box-sizing: border-box; + box-sizing: border-box; -webkit-box-align: center; -ms-flex-align: center; align-items: center; @@ -172,10 +179,6 @@ &.is-selected.is-highlighted { background: $tertiary-low; } - - &.none:not(.is-highlighted) { - background: $primary-low; - } } .select-box-kit-collection { @@ -255,8 +258,8 @@ .select-box-kit-wrapper { position: absolute; - top: 0; - left: 0; + top: -1px; + left: -1px; background: none; display: none; -webkit-box-sizing: border-box; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index ec4fc40a9f..2b28082280 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1155,6 +1155,7 @@ en: default_header_text: Select... no_content: No matches found filter_placeholder: Search... + create: "Create {{content}}" emoji_picker: filter_placeholder: Search for emoji diff --git a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 index 4c3d43feeb..bd1fb80356 100644 --- a/test/javascripts/acceptance/admin-suspend-user-test.js.es6 +++ b/test/javascripts/acceptance/admin-suspend-user-test.js.es6 @@ -44,8 +44,8 @@ QUnit.test("suspend, then unsuspend a user", assert => { andThen(() => { assert.equal(find('.perform-suspend[disabled]').length, 1, 'disabled by default'); - expandSelectBox('.suspend-until .combobox'); - selectBoxSelectRow('tomorrow', { selector: '.suspend-until .combobox'}); + expandSelectBoxKit('.suspend-until .combobox'); + selectBoxKitSelectRow('tomorrow', { selector: '.suspend-until .combobox'}); }); fillIn('.suspend-reason', "for breaking the rules"); diff --git a/test/javascripts/acceptance/category-chooser-test.js.es6 b/test/javascripts/acceptance/category-chooser-test.js.es6 index de39c87094..9f645bc6b0 100644 --- a/test/javascripts/acceptance/category-chooser-test.js.es6 +++ b/test/javascripts/acceptance/category-chooser-test.js.es6 @@ -11,7 +11,7 @@ QUnit.test("does not display uncategorized if not allowed", assert => { visit("/"); click('#create-topic'); - expandSelectBox('.category-chooser'); + ('.category-chooser'); andThen(() => { assert.ok(selectBox('.category-chooser').rowByIndex(0).name() !== 'uncategorized'); diff --git a/test/javascripts/acceptance/category-edit-test.js.es6 b/test/javascripts/acceptance/category-edit-test.js.es6 index d3a99c3a35..24d1eceef4 100644 --- a/test/javascripts/acceptance/category-edit-test.js.es6 +++ b/test/javascripts/acceptance/category-edit-test.js.es6 @@ -75,9 +75,9 @@ QUnit.test("Subcategory list settings", assert => { click('.edit-category-general'); - expandSelectBox('.edit-category-tab-general .category-chooser'); + expandSelectBoxKit('.edit-category-tab-general .category-chooser'); - selectBoxSelectRow(3, {selector: '.edit-category-tab-general .category-chooser'}); + selectBoxKitSelectRow(3, {selector: '.edit-category-tab-general .category-chooser'}); click('.edit-category-settings'); andThen(() => { diff --git a/test/javascripts/acceptance/search-full-test.js.es6 b/test/javascripts/acceptance/search-full-test.js.es6 index b64ca86dec..24ad402b30 100644 --- a/test/javascripts/acceptance/search-full-test.js.es6 +++ b/test/javascripts/acceptance/search-full-test.js.es6 @@ -257,8 +257,8 @@ QUnit.test("update in filter through advanced search ui", assert => { fillIn('.search input.full-page-search', 'none'); click('.search-advanced-btn'); - expandSelectBox('.search-advanced-options .select-box-kit#in'); - selectBoxSelectRow('bookmarks', { selector: '.search-advanced-options .select-box-kit#in' }); + expandSelectBoxKit('.search-advanced-options .select-box-kit#in'); + selectBoxKitSelectRow('bookmarks', { selector: '.search-advanced-options .select-box-kit#in' }); fillIn('.search-advanced-options .select-box-kit#in', 'bookmarks'); andThen(() => { @@ -271,8 +271,8 @@ QUnit.test("update status through advanced search ui", assert => { visit("/search"); fillIn('.search input.full-page-search', 'none'); click('.search-advanced-btn'); - expandSelectBox('.search-advanced-options .select-box-kit#status'); - selectBoxSelectRow('closed', { selector: '.search-advanced-options .select-box-kit#status' }); + expandSelectBoxKit('.search-advanced-options .select-box-kit#status'); + selectBoxKitSelectRow('closed', { selector: '.search-advanced-options .select-box-kit#status' }); fillIn('.search-advanced-options .select-box-kit#status', 'closed'); andThen(() => { @@ -286,8 +286,8 @@ QUnit.test("update post time through advanced search ui", assert => { fillIn('.search input.full-page-search', 'none'); click('.search-advanced-btn'); fillIn('#search-post-date', '2016-10-05'); - expandSelectBox('.search-advanced-options .select-box-kit#postTime'); - selectBoxSelectRow('after', { selector: '.search-advanced-options .select-box-kit#postTime' }); + expandSelectBoxKit('.search-advanced-options .select-box-kit#postTime'); + selectBoxKitSelectRow('after', { selector: '.search-advanced-options .select-box-kit#postTime' }); fillIn('.search-advanced-options .select-box-kit#postTime', 'after'); andThen(() => { diff --git a/test/javascripts/acceptance/search-test.js.es6 b/test/javascripts/acceptance/search-test.js.es6 index c449d17ccc..a5ce94669b 100644 --- a/test/javascripts/acceptance/search-test.js.es6 +++ b/test/javascripts/acceptance/search-test.js.es6 @@ -89,7 +89,7 @@ QUnit.test("Search with context", assert => { QUnit.test("Right filters are shown to anonymous users", assert => { visit("/search?expanded=true"); - expandSelectBox(".select-box-kit#in"); + expandSelectBoxKit(".select-box-kit#in"); andThen(() => { assert.ok(exists('.select-box-kit#in .select-box-kit-row[data-value=first]')); @@ -115,7 +115,7 @@ QUnit.test("Right filters are shown to logged-in users", assert => { Discourse.reset(); visit("/search?expanded=true"); - expandSelectBox(".select-box-kit#in"); + expandSelectBoxKit(".select-box-kit#in"); andThen(() => { assert.ok(exists('.select-box-kit#in .select-box-kit-row[data-value=first]')); diff --git a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 index 353ac65150..fa21ef4e96 100644 --- a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 +++ b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 @@ -28,8 +28,8 @@ QUnit.test("Updating topic notification level", assert => { ); }); - expandSelectBox(notificationOptions); - selectBoxSelectRow("3", { selector: notificationOptions}); + expandSelectBoxKit(notificationOptions); + selectBoxKitSelectRow("3", { selector: notificationOptions}); andThen(() => { assert.equal( diff --git a/test/javascripts/acceptance/topic-test.js.es6 b/test/javascripts/acceptance/topic-test.js.es6 index b1f7d80d6d..72f8518070 100644 --- a/test/javascripts/acceptance/topic-test.js.es6 +++ b/test/javascripts/acceptance/topic-test.js.es6 @@ -53,9 +53,9 @@ QUnit.test("Updating the topic title and category", assert => { fillIn('#edit-title', 'this is the new title'); - expandSelectBox('.title-wrapper .category-chooser'); + expandSelectBoxKit('.title-wrapper .category-chooser'); - selectBoxSelectRow(4, {selector: '.title-wrapper .category-chooser'}); + selectBoxKitSelectRow(4, {selector: '.title-wrapper .category-chooser'}); click('#topic-title .submit-edit'); diff --git a/test/javascripts/components/categories-admin-dropdown-test.js.es6 b/test/javascripts/components/categories-admin-dropdown-test.js.es6 index 9816850677..07593db3e6 100644 --- a/test/javascripts/components/categories-admin-dropdown-test.js.es6 +++ b/test/javascripts/components/categories-admin-dropdown-test.js.es6 @@ -10,7 +10,7 @@ componentTest('default', { assert.equal($selectBox.el.find(".d-icon-bars").length, 1); assert.equal($selectBox.el.find(".d-icon-caret-down").length, 1); - expandSelectBox('.categories-admin-dropdown'); + expandSelectBoxKit(); andThen(() => { assert.equal($selectBox.rowByValue("create").name(), "New Category"); diff --git a/test/javascripts/components/category-chooser-test.js.es6 b/test/javascripts/components/category-chooser-test.js.es6 index f5ebfe2685..d6fca21fd0 100644 --- a/test/javascripts/components/category-chooser-test.js.es6 +++ b/test/javascripts/components/category-chooser-test.js.es6 @@ -6,7 +6,7 @@ componentTest('with value', { template: '{{category-chooser value=2}}', test(assert) { - expandSelectBox('.category-chooser'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.category-chooser').header.name(), "feature"); @@ -18,7 +18,7 @@ componentTest('with excludeCategoryId', { template: '{{category-chooser excludeCategoryId=2}}', test(assert) { - expandSelectBox('.category-chooser'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.category-chooser').rowByValue(2).el.length, 0); @@ -30,7 +30,7 @@ componentTest('with scopedCategoryId', { template: '{{category-chooser scopedCategoryId=2}}', test(assert) { - expandSelectBox('.category-chooser'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.category-chooser').rowByIndex(0).name(), "feature"); @@ -48,7 +48,7 @@ componentTest('with allowUncategorized=null', { }, test(assert) { - expandSelectBox('.category-chooser'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.category-chooser').header.name(), "Select a category…"); @@ -64,7 +64,7 @@ componentTest('with allowUncategorized=null rootNone=true', { }, test(assert) { - expandSelectBox('.category-chooser'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.category-chooser').header.name(), "Select a category…"); @@ -81,7 +81,7 @@ componentTest('with disallowed uncategorized, rootNone and rootNoneLabel', { }, test(assert) { - expandSelectBox('.category-chooser'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.category-chooser').header.name(), "Select a category…"); @@ -97,7 +97,7 @@ componentTest('with allowed uncategorized', { }, test(assert) { - expandSelectBox('.category-chooser'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.category-chooser').header.name(), "uncategorized"); @@ -113,7 +113,7 @@ componentTest('with allowed uncategorized and rootNone', { }, test(assert) { - expandSelectBox('.category-chooser'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.category-chooser').header.name(), "(no category)"); @@ -130,7 +130,7 @@ componentTest('with allowed uncategorized rootNone and rootNoneLabel', { }, test(assert) { - expandSelectBox('.category-chooser'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.category-chooser').header.name(), "root none label"); diff --git a/test/javascripts/components/combo-box-test.js.es6 b/test/javascripts/components/combo-box-test.js.es6 index dcaccdfc1b..c7f50b5da1 100644 --- a/test/javascripts/components/combo-box-test.js.es6 +++ b/test/javascripts/components/combo-box-test.js.es6 @@ -8,7 +8,7 @@ componentTest('default', { }, test(assert) { - expandSelectBox('.combobox'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.combobox').header.name(), "hello"); @@ -25,7 +25,7 @@ componentTest('with valueAttribute', { }, test(assert) { - expandSelectBox('.combobox'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.combobox').rowByValue(0).name(), "hello"); @@ -41,7 +41,7 @@ componentTest('with nameProperty', { }, test(assert) { - expandSelectBox('.combobox'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.combobox').rowByValue(0).name(), "hello"); @@ -57,7 +57,7 @@ componentTest('with an array as content', { }, test(assert) { - expandSelectBox('.combobox'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.combobox').rowByValue('evil').name(), "evil"); @@ -75,7 +75,7 @@ componentTest('with value and none as a string', { }, test(assert) { - expandSelectBox('.combobox'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.combobox').noneRow.name(), 'none'); @@ -85,7 +85,7 @@ componentTest('with value and none as a string', { assert.equal(this.get('value'), 'trout'); }); - selectBoxSelectRow('', {selector: '.combobox' }); + selectBoxKitSelectRow('__none__', {selector: '.combobox' }); andThen(() => { assert.equal(this.get('value'), null); @@ -102,7 +102,7 @@ componentTest('with value and none as an object', { }, test(assert) { - expandSelectBox('.combobox'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.combobox').noneRow.name(), 'none'); @@ -112,7 +112,7 @@ componentTest('with value and none as an object', { assert.equal(this.get('value'), 'evil'); }); - selectBoxSelectNoneRow({ selector: '.combobox' }); + selectBoxKitSelectNoneRow({ selector: '.combobox' }); andThen(() => { assert.equal(this.get('value'), null); @@ -130,7 +130,7 @@ componentTest('with no value and none as an object', { }, test(assert) { - expandSelectBox('.combobox'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.combobox').header.name(), 'none'); @@ -148,7 +148,7 @@ componentTest('with no value and none string', { }, test(assert) { - expandSelectBox('.combobox'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.combobox').header.name(), 'none'); @@ -164,7 +164,7 @@ componentTest('with no value and no none', { }, test(assert) { - expandSelectBox('.combobox'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.combobox').header.name(), 'evil', 'it sets the first row as value'); @@ -180,15 +180,15 @@ componentTest('with no value and no none', { // }, // // test(assert) { -// expandSelectBox(); +// (); // // andThen(() => assert.equal(find(".select-box-kit-filter-input").length, 1, "it has a search input")); // -// selectBoxFillInFilter("regis"); +// selectBoxKitFillInFilter("regis"); // // andThen(() => assert.equal(selectBox().rows.length, 1, "it filters results")); // -// selectBoxFillInFilter(""); +// selectBoxKitFillInFilter(""); // // andThen(() => { // assert.equal( @@ -207,17 +207,17 @@ componentTest('with no value and no none', { // }, // // test(assert) { -// expandSelectBox(); +// (); // -// selectBoxFillInFilter("rob"); +// selectBoxKitFillInFilter("rob"); // // andThen(() => assert.equal(selectBox().rows.length, 1) ); // -// collapseSelectBox(); +// collapseSelectBoxKit(); // // andThen(() => assert.notOk(selectBox().isExpanded) ); // -// expandSelectBox(); +// (); // // andThen(() => assert.equal(selectBox().rows.length, 1) ); // } @@ -232,7 +232,7 @@ componentTest('with empty string as value', { }, test(assert) { - expandSelectBox('.combobox'); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox('.combobox').header.name(), 'evil', 'it sets the first row as value'); diff --git a/test/javascripts/components/list-setting-test.js.es6 b/test/javascripts/components/list-setting-test.js.es6 new file mode 100644 index 0000000000..c3dd271fb1 --- /dev/null +++ b/test/javascripts/components/list-setting-test.js.es6 @@ -0,0 +1,74 @@ +import componentTest from 'helpers/component-test'; + +moduleForComponent('list-setting', {integration: true}); + +componentTest('default', { + template: '{{list-setting settingValue=settingValue choices=choices}}', + + beforeEach() { + this.set('settingValue', 'bold|italic'); + this.set('choices', ['bold', 'italic', 'underline']); + }, + + test(assert) { + expandSelectBoxKit(); + + andThen(() => { + assert.propEqual(selectBox().header.name(), 'bold,italic'); + }); + } +}); + +componentTest('with only setting value', { + template: '{{list-setting settingValue=settingValue}}', + + beforeEach() { + this.set('settingValue', 'bold|italic'); + }, + + test(assert) { + expandSelectBoxKit(); + + andThen(() => { + assert.propEqual(selectBox().header.name(), 'bold,italic'); + }); + } +}); + +componentTest('interactions', { + template: '{{list-setting settingValue=settingValue choices=choices}}', + + beforeEach() { + this.set('settingValue', 'bold|italic'); + this.set('choices', ['bold', 'italic', 'underline']); + }, + + test(assert) { + expandSelectBoxKit(); + + selectBoxKitSelectRow('underline'); + + andThen(() => { + assert.propEqual(selectBox().header.name(), 'bold,italic,underline'); + }); + + selectBoxKitFillInFilter('strike'); + + andThen(() => { + assert.equal(selectBox().highlightedRow.name(), 'strike'); + }); + + selectBox().keyboard.enter(); + + andThen(() => { + assert.propEqual(selectBox().header.name(), 'bold,italic,underline,strike'); + }); + + selectBox().keyboard.backspace(); + selectBox().keyboard.backspace(); + + andThen(() => { + assert.equal(this.get('choices').length, 3, 'it removes the created content from original list'); + }); + } +}); diff --git a/test/javascripts/components/multi-combo-box-test.js.es6 b/test/javascripts/components/multi-combo-box-test.js.es6 index 9dbe3b5ffe..81f8b209e7 100644 --- a/test/javascripts/components/multi-combo-box-test.js.es6 +++ b/test/javascripts/components/multi-combo-box-test.js.es6 @@ -11,7 +11,99 @@ componentTest('with objects and values', { test(assert) { andThen(() => { - assert.propEqual(selectBox(".multi-combobox").header.name(), 'hello,world'); + assert.propEqual(selectBox().header.name(), 'hello,world'); + }); + } +}); + +componentTest('interactions', { + template: '{{multi-combo-box none=none content=items value=value}}', + + beforeEach() { + I18n.translations[I18n.locale].js.test = {none: 'none'}; + this.set('items', [{id: 1, name: 'regis'}, {id: 2, name: 'sam'}, {id: 3, name: 'robin'}]); + this.set('value', [1, 2]); + }, + + test(assert) { + expandSelectBoxKit(); + + andThen(() => { + assert.equal(selectBox().highlightedRow.name(), 'robin', 'it highlights the first content row'); + }); + + this.set('none', 'test.none'); + + andThen(() => { + assert.equal(selectBox().noneRow.el.length, 1); + assert.equal(selectBox().highlightedRow.name(), 'robin', 'it highlights the first content row'); + }); + + selectBoxKitSelectRow(3); + + andThen(() => { + assert.equal(selectBox().highlightedRow.name(), 'none', 'it highlights none row if no content'); + }); + + selectBoxKitFillInFilter('joffrey'); + + andThen(() => { + assert.equal(selectBox().highlightedRow.name(), 'joffrey', 'it highlights create row when filling filter'); + }); + + selectBox().keyboard.enter(); + + andThen(() => { + assert.equal(selectBox().highlightedRow.name(), 'none', 'it highlights none row after creating content and no content left'); + }); + + selectBox().keyboard.backspace(); + + andThen(() => { + const $lastSelectedName = selectBox().header.el.find('.selected-name').last(); + assert.equal($lastSelectedName.attr('data-name'), 'joffrey'); + assert.ok($lastSelectedName.hasClass('is-highlighted'), 'it highlights the last selected name when using backspace'); + }); + + selectBox().keyboard.backspace(); + + andThen(() => { + const $lastSelectedName = selectBox().header.el.find('.selected-name').last(); + assert.equal($lastSelectedName.attr('data-name'), 'robin', 'it removes the previous highlighted selected content'); + assert.notOk(exists(selectBox().rowByValue('joffrey').el), 'generated content shouldn’t appear in content when removed'); + }); + + selectBox().keyboard.selectAll(); + + andThen(() => { + const $highlightedSelectedNames = selectBox().header.el.find('.selected-name.is-highlighted'); + assert.equal($highlightedSelectedNames.length, 3, 'it highlights each selected name'); + }); + + selectBox().keyboard.backspace(); + + andThen(() => { + const $selectedNames = selectBox().header.el.find('.selected-name'); + assert.equal($selectedNames.length, 0, 'it removed all selected content'); + }); + + andThen(() => { + assert.ok(this.$(".select-box-kit").hasClass("is-focused")); + assert.ok(this.$(".select-box-kit").hasClass("is-expanded")); + }); + + selectBox().keyboard.escape(); + + andThen(() => { + assert.ok(this.$(".select-box-kit").hasClass("is-focused")); + assert.notOk(this.$(".select-box-kit").hasClass("is-expanded")); + }); + + selectBox().keyboard.escape(); + + andThen(() => { + assert.notOk(this.$(".select-box-kit").hasClass("is-focused")); + assert.notOk(this.$(".select-box-kit").hasClass("is-expanded")); }); } }); diff --git a/test/javascripts/components/pinned-button-test.js.es6 b/test/javascripts/components/pinned-button-test.js.es6 index eba26d40eb..154d321a23 100644 --- a/test/javascripts/components/pinned-button-test.js.es6 +++ b/test/javascripts/components/pinned-button-test.js.es6 @@ -23,7 +23,7 @@ componentTest('updating the content refreshes the list', { test(assert) { andThen(() => assert.notOk(selectBox().isHidden) ); - expandSelectBox(); + expandSelectBoxKit(); andThen(() => assert.equal(selectBox().selectedRow.name(), "Pinned") ); diff --git a/test/javascripts/components/select-box-test.js.es6 b/test/javascripts/components/select-box-kit-test.js.es6 similarity index 90% rename from test/javascripts/components/select-box-test.js.es6 rename to test/javascripts/components/select-box-kit-test.js.es6 index d5ce466053..6e0f37a048 100644 --- a/test/javascripts/components/select-box-test.js.es6 +++ b/test/javascripts/components/select-box-kit-test.js.es6 @@ -10,7 +10,7 @@ componentTest('updating the content refreshes the list', { }, test(assert) { - expandSelectBox(); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox().rowByValue(1).name(), "robin"); @@ -29,7 +29,7 @@ componentTest('accepts a value by reference', { }, test(assert) { - expandSelectBox(); + expandSelectBoxKit(); andThen(() => { assert.equal( @@ -38,7 +38,7 @@ componentTest('accepts a value by reference', { ); }); - selectBoxSelectRow(1); + selectBoxKitSelectRow(1); andThen(() => { assert.equal(this.get("value"), 1, "it mutates the value"); @@ -58,7 +58,7 @@ componentTest('default search icon', { template: '{{select-box-kit filterable=true}}', test(assert) { - expandSelectBox(); + expandSelectBoxKit(); andThen(() => { assert.ok(exists(selectBox().filter.icon), "it has a the correct icon"); @@ -70,7 +70,7 @@ componentTest('with no search icon', { template: '{{select-box-kit filterable=true filterIcon=null}}', test(assert) { - expandSelectBox(); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox().filter.icon().length, 0, "it has no icon"); @@ -82,7 +82,7 @@ componentTest('custom search icon', { template: '{{select-box-kit filterable=true filterIcon="shower"}}', test(assert) { - expandSelectBox(); + expandSelectBoxKit(); andThen(() => { assert.ok(selectBox().filter.icon().hasClass("d-icon-shower"), "it has a the correct icon"); @@ -93,11 +93,11 @@ componentTest('custom search icon', { componentTest('select-box is expandable', { template: '{{select-box-kit}}', test(assert) { - expandSelectBox(); + expandSelectBoxKit(); andThen(() => assert.ok(selectBox().isExpanded) ); - collapseSelectBox(); + collapseSelectBoxKit(); andThen(() => assert.notOk(selectBox().isExpanded) ); } @@ -112,7 +112,7 @@ componentTest('accepts custom value/name keys', { }, test(assert) { - expandSelectBox(); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox().selectedRow.name(), "robin"); @@ -130,7 +130,7 @@ componentTest('doesn’t render collection content before first expand', { test(assert) { assert.notOk(exists(find(".select-box-kit-collection"))); - expandSelectBox(); + expandSelectBoxKit(); andThen(() => { assert.ok(exists(find(".select-box-kit-collection"))); @@ -146,7 +146,7 @@ componentTest('supports options to limit size', { }, test(assert) { - expandSelectBox(); + expandSelectBoxKit(); andThen(() => { const height = find(".select-box-kit-collection").height(); @@ -163,11 +163,11 @@ componentTest('dynamic headerText', { }, test(assert) { - expandSelectBox(); + expandSelectBoxKit(); andThen(() => assert.equal(selectBox().header.name(), "robin") ); - selectBoxSelectRow(2); + selectBoxKitSelectRow(2); andThen(() => { assert.equal(selectBox().header.name(), "regis", "it changes header text"); @@ -186,7 +186,7 @@ componentTest('supports custom row template', { }, test(assert) { - expandSelectBox(); + expandSelectBoxKit(); andThen(() => assert.equal(selectBox().rowByValue(1).el.html().trim(), "robin") ); } @@ -201,7 +201,7 @@ componentTest('supports converting select value to integer', { }, test(assert) { - expandSelectBox(); + expandSelectBoxKit(); andThen(() => assert.equal(selectBox().selectedRow.name(), "régis") ); @@ -224,7 +224,7 @@ componentTest('supports keyboard events', { }, test(assert) { - expandSelectBox(); + expandSelectBoxKit(); selectBox().keyboard.down(); @@ -251,7 +251,7 @@ componentTest('supports keyboard events', { assert.notOk(selectBox().isExpanded, "it collapses the select box when selecting a row"); }); - expandSelectBox(); + expandSelectBoxKit(); selectBox().keyboard.escape(); @@ -259,18 +259,13 @@ componentTest('supports keyboard events', { assert.notOk(selectBox().isExpanded, "it collapses the select box"); }); - expandSelectBox(); + expandSelectBoxKit(); - selectBoxFillInFilter("regis"); - - // andThen(() => { - // assert.equal(selectBox().highlightedRow.title(), "regis", "it highlights the first result"); - // }); + selectBoxKitFillInFilter("regis"); selectBox().keyboard.tab(); andThen(() => { - // assert.equal(selectBox().selectedRow.title(), "regis", "it selects the row when pressing tab"); assert.notOk(selectBox().isExpanded, "it collapses the select box when selecting a row"); }); } diff --git a/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 b/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 index fbd31aeafc..899bff4545 100644 --- a/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 +++ b/test/javascripts/components/topic-footer-mobile-dropdown-test.js.es6 @@ -17,7 +17,7 @@ componentTest('default', { }, test(assert) { - expandSelectBox(); + expandSelectBoxKit(); andThen(() => { assert.equal(selectBox().header.name(), "Topic Controls"); @@ -26,7 +26,7 @@ componentTest('default', { assert.equal(selectBox().selectedRow.el.length, 0, "it doesn’t preselect first row"); }); - selectBoxSelectRow("share"); + selectBoxKitSelectRow("share"); andThen(() => { assert.equal(this.get("value"), null, "it resets the value"); diff --git a/test/javascripts/helpers/select-box-helper.js b/test/javascripts/helpers/select-box-kit-helper.js similarity index 75% rename from test/javascripts/helpers/select-box-helper.js rename to test/javascripts/helpers/select-box-kit-helper.js index 546148531a..6388cff899 100644 --- a/test/javascripts/helpers/select-box-helper.js +++ b/test/javascripts/helpers/select-box-kit-helper.js @@ -10,7 +10,7 @@ function checkSelectBoxIsNotCollapsed(selectBoxSelector) { } } -Ember.Test.registerAsyncHelper('expandSelectBox', function(app, selectBoxSelector) { +Ember.Test.registerAsyncHelper('expandSelectBoxKit', function(app, selectBoxSelector) { selectBoxSelector = selectBoxSelector || '.select-box-kit'; checkSelectBoxIsNotExpanded(selectBoxSelector); @@ -18,7 +18,7 @@ Ember.Test.registerAsyncHelper('expandSelectBox', function(app, selectBoxSelecto click(selectBoxSelector + ' .select-box-kit-header'); }); -Ember.Test.registerAsyncHelper('collapseSelectBox', function(app, selectBoxSelector) { +Ember.Test.registerAsyncHelper('collapseSelectBoxKit', function(app, selectBoxSelector) { selectBoxSelector = selectBoxSelector || '.select-box-kit'; checkSelectBoxIsNotCollapsed(selectBoxSelector); @@ -26,7 +26,7 @@ Ember.Test.registerAsyncHelper('collapseSelectBox', function(app, selectBoxSelec click(selectBoxSelector + ' .select-box-kit-header'); }); -Ember.Test.registerAsyncHelper('selectBoxSelectRow', function(app, rowValue, options) { +Ember.Test.registerAsyncHelper('selectBoxKitSelectRow', function(app, rowValue, options) { options = options || {}; options.selector = options.selector || '.select-box-kit'; @@ -35,7 +35,7 @@ Ember.Test.registerAsyncHelper('selectBoxSelectRow', function(app, rowValue, opt click(options.selector + " .select-box-kit-row[data-value='" + rowValue + "']"); }); -Ember.Test.registerAsyncHelper('selectBoxSelectNoneRow', function(app, options) { +Ember.Test.registerAsyncHelper('selectBoxKitSelectNoneRow', function(app, options) { options = options || {}; options.selector = options.selector || '.select-box-kit'; @@ -44,7 +44,7 @@ Ember.Test.registerAsyncHelper('selectBoxSelectNoneRow', function(app, options) click(options.selector + " .select-box-kit-row.none"); }); -Ember.Test.registerAsyncHelper('selectBoxFillInFilter', function(app, filter, options) { +Ember.Test.registerAsyncHelper('selectBoxKitFillInFilter', function(app, filter, options) { options = options || {}; options.selector = options.selector || '.select-box-kit'; @@ -52,7 +52,6 @@ Ember.Test.registerAsyncHelper('selectBoxFillInFilter', function(app, filter, op var filterQuerySelector = options.selector + ' .select-box-kit-filter-input'; fillIn(filterQuerySelector, filter); - triggerEvent(filterQuerySelector, 'keyup'); }); function selectBox(selector) { // eslint-disable-line no-unused-vars @@ -88,23 +87,26 @@ function selectBox(selector) { // eslint-disable-line no-unused-vars } function keyboardHelper() { - function createEvent(target, keyCode) { + function createEvent(target, keyCode, options) { target = target || ".select-box-kit-filter-input"; selector = find(selector).find(target); andThen(function() { - var event = jQuery.Event('keydown'); + var event = jQuery.Event(options.type); event.keyCode = keyCode; + if (options && options.metaKey === true) { event.metaKey = true; } find(selector).trigger(event); }); } return { - down: function(target) { createEvent(target, 40); }, - up: function(target) { createEvent(target, 38); }, - escape: function(target) { createEvent(target, 27); }, - enter: function(target) { createEvent(target, 13); }, - tab: function(target) { createEvent(target, 9); } + down: function(target) { createEvent(target, 40, {type: 'keydown'}); }, + up: function(target) { createEvent(target, 38, {type: 'keydown'}); }, + escape: function(target) { createEvent(target, 27, {type: 'keydown'}); }, + enter: function(target) { createEvent(target, 13, {type: 'keypress'}); }, + tab: function(target) { createEvent(target, 9, {type: 'keydown'}); }, + backspace: function(target) { createEvent(target, 8, {type: 'keydown'}); }, + selectAll: function(target) { createEvent(target, 65, {metaKey: true, type: 'keydown'}); }, }; } diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 97f759b0dc..df242fa34e 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -32,7 +32,7 @@ //= require sinon-qunit-1.0.0 //= require helpers/assertions -//= require helpers/select-box-helper +//= require helpers/select-box-kit-helper //= require helpers/qunit-helpers //= require_tree ./fixtures From 162932114e6d7ac6847edf77cdf077fe4b22170d Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 9 Nov 2017 14:11:12 -0500 Subject: [PATCH 134/445] UX: Add an outlet to the user admin page --- app/assets/javascripts/admin/templates/user-index.hbs | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index ec086e3271..3e6e7d613a 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -17,6 +17,7 @@ {{d-button action="logOut" icon="power-off" label="admin.user.log_out"}} {{/if}} {{/if}} + {{plugin-outlet name="admin-user-controls-after" args=(hash model=model) tagName="" connectorTagName=""}}

    From 38b8d68c6887abc23f6b344c311ee248e8cb2ae7 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Thu, 9 Nov 2017 12:45:19 -0700 Subject: [PATCH 135/445] FEATURE: Allow the user to select a custom home page (#5268) * Add user_home configuration option * Use the new user_home preference to actually show the right home page * Fix trailing whitespace * Update user_option_serializer.rb * Fix JavaScript default homepage tests * Use an object instead of a giant switch * Remove trailing whitespace * Make the default `user_home` set to `null` instead of `0` * Rename user_home to homepage_id --- .../controllers/preferences/interface.js.es6 | 20 +++++++++++++++++++ .../discourse/lib/utilities.js.es6 | 20 +++++++++++++++++-- .../javascripts/discourse/models/user.js.es6 | 1 + .../templates/preferences/interface.hbs | 7 +++++++ app/controllers/application_controller.rb | 2 +- app/helpers/application_helper.rb | 4 ++++ app/models/user_option.rb | 12 +++++++++++ app/serializers/user_option_serializer.rb | 1 + app/services/user_updater.rb | 1 + app/views/layouts/application.html.erb | 1 + config/locales/client.en.yml | 1 + .../20171026014317_add_user_option_home.rb | 5 +++++ lib/homepage_constraint.rb | 2 +- test/javascripts/lib/utilities-test.js.es6 | 17 ++++++++++++++++ 14 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 db/migrate/20171026014317_add_user_option_home.rb diff --git a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 index 002eaba949..18dccd8685 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/interface.js.es6 @@ -1,8 +1,11 @@ import PreferencesTabController from "discourse/mixins/preferences-tab-controller"; +import { setDefaultHomepage } from "discourse/lib/utilities"; import { default as computed, observes } from "ember-addons/ember-computed-decorators"; import { currentThemeKey, listThemes, previewTheme, setLocalTheme } from 'discourse/lib/theme-selector'; import { popupAjaxError } from 'discourse/lib/ajax-error'; +const USER_HOMES = { 1: "latest", 2: "categories", 3: "unread", 4: "new", 5: "top" }; + export default Ember.Controller.extend(PreferencesTabController, { @computed("makeThemeDefault") @@ -14,6 +17,8 @@ export default Ember.Controller.extend(PreferencesTabController, { 'enable_quoting', 'disable_jump_reply', 'automatically_unpin_topics', + 'allow_private_messages', + 'homepage_id', ]; if (makeDefault) { @@ -51,6 +56,19 @@ export default Ember.Controller.extend(PreferencesTabController, { previewTheme(key); }, + homeChanged() { + const siteHome = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0]; + const userHome = USER_HOMES[this.get('model.user_option.homepage_id')]; + setDefaultHomepage(userHome || siteHome); + }, + + @computed() + userSelectableHome() { + return _.map(USER_HOMES, (name, num) => { + return {name: I18n.t('filters.' + name + '.title'), value: num}; + }); + }, + actions: { save() { this.set('saved', false); @@ -66,6 +84,8 @@ export default Ember.Controller.extend(PreferencesTabController, { setLocalTheme(this.get('themeKey'), this.get('model.user_option.theme_key_seq')); } + this.homeChanged(); + }).catch(popupAjaxError); } } diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6 index fe3616abb5..cba2761571 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js.es6 +++ b/app/assets/javascripts/discourse/lib/utilities.js.es6 @@ -1,5 +1,7 @@ import { escape } from 'pretty-text/sanitizer'; +const homepageSelector = 'meta[name=discourse_current_homepage]'; + export function translateSize(size) { switch (size) { case 'tiny': return 20; @@ -349,8 +351,22 @@ export function displayErrorForUpload(data) { } export function defaultHomepage() { - // the homepage is the first item of the 'top_menu' site setting - return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0]; + let homepage = null; + let elem = _.first($(homepageSelector)); + if (elem) { + homepage = elem.content; + } + if (!homepage) { + homepage = Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0]; + } + return homepage; +} + +export function setDefaultHomepage(homepage) { + let elem = _.first($(homepageSelector)); + if (elem) { + elem.content = homepage; + } } export function determinePostReplaceSelection({ selection, needle, replacement }) { diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 17e7f9bfc1..82171ada01 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -249,6 +249,7 @@ const User = RestModel.extend({ 'include_tl0_in_digests', 'theme_key', 'allow_private_messages', + 'homepage_id', ]; if (fields) { diff --git a/app/assets/javascripts/discourse/templates/preferences/interface.hbs b/app/assets/javascripts/discourse/templates/preferences/interface.hbs index 8e8d5852e9..f6db98b840 100644 --- a/app/assets/javascripts/discourse/templates/preferences/interface.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/interface.hbs @@ -23,6 +23,13 @@ {{/if}} +
    + +
    + {{combo-box content=userSelectableHome valueAttribute="value" value=model.user_option.homepage_id}} +
    +
    +
    diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 71d3e9e683..05fbfdb733 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -308,7 +308,7 @@ class ApplicationController < ActionController::Base end def current_homepage - current_user ? SiteSetting.homepage : SiteSetting.anonymous_homepage + current_user&.user_option&.homepage || SiteSetting.anonymous_homepage end def serialize_data(obj, serializer, opts = nil) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index de63356aa1..40bb824443 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -347,6 +347,10 @@ module ApplicationHelper end end + def current_homepage + current_user&.user_option&.homepage || SiteSetting.anonymous_homepage + end + def build_plugin_html(name) return "" unless allow_plugins? DiscoursePluginRegistry.build_html(name, controller) || "" diff --git a/app/models/user_option.rb b/app/models/user_option.rb index 17eed6baa7..a780a4015f 100644 --- a/app/models/user_option.rb +++ b/app/models/user_option.rb @@ -128,6 +128,17 @@ class UserOption < ActiveRecord::Base times.max end + def homepage + case homepage_id + when 1 then "latest" + when 2 then "categories" + when 3 then "unread" + when 4 then "new" + when 5 then "top" + else SiteSetting.homepage + end + end + private def update_tracked_topics @@ -165,6 +176,7 @@ end # theme_key :string # theme_key_seq :integer default(0), not null # allow_private_messages :boolean default(TRUE), not null +# homepage_id :integer default(null) # # Indexes # diff --git a/app/serializers/user_option_serializer.rb b/app/serializers/user_option_serializer.rb index 844f6c06b5..6bccbe8176 100644 --- a/app/serializers/user_option_serializer.rb +++ b/app/serializers/user_option_serializer.rb @@ -22,6 +22,7 @@ class UserOptionSerializer < ApplicationSerializer :theme_key, :theme_key_seq, :allow_private_messages, + :homepage_id, def auto_track_topics_after_msecs object.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs diff --git a/app/services/user_updater.rb b/app/services/user_updater.rb index 591a50a966..305f96fddc 100644 --- a/app/services/user_updater.rb +++ b/app/services/user_updater.rb @@ -36,6 +36,7 @@ class UserUpdater :include_tl0_in_digests, :theme_key, :allow_private_messages, + :homepage_id, ] def initialize(actor, user) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index a6e1e70758..83c7bccf1c 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -5,6 +5,7 @@ <%= content_for?(:title) ? yield(:title) : SiteSetting.title %> + <%= render partial: "layouts/head" %> <%= discourse_csrf_tags %> diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 2b28082280..a3ec45596d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -658,6 +658,7 @@ en: undo_revoke_access: "Undo Revoke Access" api_approved: "Approved:" theme: "Theme" + home: "Default Home Page" staff_counters: flags_given: "helpful flags" diff --git a/db/migrate/20171026014317_add_user_option_home.rb b/db/migrate/20171026014317_add_user_option_home.rb new file mode 100644 index 0000000000..8724ec40e7 --- /dev/null +++ b/db/migrate/20171026014317_add_user_option_home.rb @@ -0,0 +1,5 @@ +class AddUserOptionHome < ActiveRecord::Migration[5.1] + def change + add_column :user_options, :homepage_id, :integer, null: true, default: nil + end +end diff --git a/lib/homepage_constraint.rb b/lib/homepage_constraint.rb index a5cfb34758..586581425c 100644 --- a/lib/homepage_constraint.rb +++ b/lib/homepage_constraint.rb @@ -7,7 +7,7 @@ class HomePageConstraint return @filter == 'finish_installation' if SiteSetting.has_login_hint? provider = Discourse.current_user_provider.new(request.env) - homepage = provider.current_user ? SiteSetting.homepage : SiteSetting.anonymous_homepage + homepage = provider&.current_user&.user_option&.homepage || SiteSetting.anonymous_homepage homepage == @filter rescue Discourse::InvalidAccess false diff --git a/test/javascripts/lib/utilities-test.js.es6 b/test/javascripts/lib/utilities-test.js.es6 index e903b97792..8ef76a79fa 100644 --- a/test/javascripts/lib/utilities-test.js.es6 +++ b/test/javascripts/lib/utilities-test.js.es6 @@ -10,6 +10,7 @@ import { getRawSize, avatarImg, defaultHomepage, + setDefaultHomepage, validateUploadedFiles, getUploadMarkdown, caretRowCol, @@ -204,6 +205,22 @@ QUnit.test("allowsAttachments", assert => { QUnit.test("defaultHomepage", assert => { Discourse.SiteSettings.top_menu = "latest|top|hot"; assert.equal(defaultHomepage(), "latest", "default homepage is the first item in the top_menu site setting"); + var meta = document.createElement("meta"); + meta.name = "discourse_current_homepage"; + meta.content = "hot"; + document.body.appendChild(meta); + assert.equal(defaultHomepage(), "hot", "default homepage is pulled from "); + document.body.removeChild(meta); +}); + +QUnit.test("setDefaultHomepage", assert => { + var meta = document.createElement("meta"); + meta.name = "discourse_current_homepage"; + meta.content = "hot"; + document.body.appendChild(meta); + setDefaultHomepage("top"); + assert.equal(meta.content, "top", "default homepage set by setDefaultHomepage"); + document.body.removeChild(meta); }); QUnit.test("caretRowCol", assert => { From 90351348ec3c0b1872aa680c0c9ddd5271e3740b Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 10 Nov 2017 07:01:43 +1100 Subject: [PATCH 136/445] FIX: checkUsername, delete and dismissBanner not working with users with . --- app/assets/javascripts/discourse/models/user.js.es6 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 82171ada01..49f314373d 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -462,7 +462,7 @@ const User = RestModel.extend({ "delete": function() { if (this.get('can_delete_account')) { - return ajax(userPath(this.get('username')), { + return ajax(userPath(this.get('username') + ".json"), { type: 'DELETE', data: {context: window.location.pathname} }); @@ -473,7 +473,7 @@ const User = RestModel.extend({ dismissBanner(bannerKey) { this.set("dismissed_banner_key", bannerKey); - ajax(userPath(this.get('username')), { + ajax(userPath(this.get('username') + ".json"), { type: 'PUT', data: { dismissed_banner_key: bannerKey } }); @@ -552,7 +552,7 @@ User.reopenClass(Singleton, { }, checkUsername(username, email, for_user_id) { - return ajax(userPath('check_username'), { + return ajax(userPath('check_username') + ".json", { data: { username, email, for_user_id } }); }, From a62457bf29377674d44f13ae07fca767ecb6b0a4 Mon Sep 17 00:00:00 2001 From: Kris Date: Thu, 9 Nov 2017 15:03:19 -0500 Subject: [PATCH 137/445] fixing the input color issue on mobile (#5299) --- .../stylesheets/common/base/discourse.scss | 81 +++++++++++++++++++ app/assets/stylesheets/desktop/discourse.scss | 80 +----------------- 2 files changed, 82 insertions(+), 79 deletions(-) diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 077acd0c68..0d7cf2a725 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -142,6 +142,87 @@ input { } } +input, +select, +textarea { + color: $primary; + + &[class*="span"] { + float: none; + margin-left: 0; + } + + &[disabled], + &[readonly] { + cursor: not-allowed; + background-color: $primary-low; + border-color: $primary-low; + } + + &:focus:required:invalid { + color: $danger; + border-color: $danger; + } + + &:focus:required:invalid:focus { + border-color: $danger; + box-shadow: 0 0 6px $danger; + } +} + +input { + &[type="text"], + &[type="password"], + &[type="datetime"], + &[type="datetime-local"], + &[type="date"], + &[type="month"], + &[type="time"], + &[type="week"], + &[type="number"], + &[type="email"], + &[type="url"], + &[type="search"], + &[type="tel"], + &[type="color"] { + display: inline-block; + height: 18px; + padding: 4px; + margin-bottom: 9px; + font-size: 0.929em; + line-height: 18px; + color: $primary; + background-color: $secondary; + border: 1px solid $primary-low; + border-radius: 3px; + box-shadow: inset 0 1px 1px rgba(0,0,0, .3); + + &:focus { + border-color: $tertiary; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0,0,0, .3), 0 0 8px $tertiary; + } + } +} + +textarea { + height: auto; + background-color:$secondary; + border: 1px solid $primary-low; + border-radius: 3px; + box-shadow: inset 0 1px 1px rgba(0,0,0, .3); + + &:focus { + border-color: $tertiary; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0,0,0, .3), 0 0 8px $tertiary; + } +} + +select { + border: 1px solid $primary-low; +} + // Common Classes .radio, .checkbox { diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index 986ae3e4a5..c187528f84 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -127,34 +127,6 @@ textarea { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } -input, -select, -textarea { - color: $primary; - - &[class*="span"] { - float: none; - margin-left: 0; - } - - &[disabled], - &[readonly] { - cursor: not-allowed; - background-color: $primary-low; - border-color: $primary-low; - } - - &:focus:required:invalid { - color: $danger; - border-color: $danger; - } - - &:focus:required:invalid:focus { - border-color: $danger; - box-shadow: 0 0 6px $danger; - } -} - select, textarea { display: inline-block; @@ -165,56 +137,8 @@ textarea { color: $primary; } -input { +input, textarea { width: 210px; - - &[type="text"], - &[type="password"], - &[type="datetime"], - &[type="datetime-local"], - &[type="date"], - &[type="month"], - &[type="time"], - &[type="week"], - &[type="number"], - &[type="email"], - &[type="url"], - &[type="search"], - &[type="tel"], - &[type="color"] { - display: inline-block; - height: 18px; - padding: 4px; - margin-bottom: 9px; - font-size: 0.929em; - line-height: 18px; - color: $primary; - background-color: $secondary; - border: 1px solid $primary-low; - border-radius: 3px; - box-shadow: inset 0 1px 1px rgba(0,0,0, .3); - - &:focus { - border-color: $tertiary; - outline: 0; - box-shadow: inset 0 1px 1px rgba(0,0,0, .3), 0 0 8px $tertiary; - } - } -} - -textarea { - width: 210px; - height: auto; - background-color:$secondary; - border: 1px solid $primary-low; - border-radius: 3px; - box-shadow: inset 0 1px 1px rgba(0,0,0, .3); - - &:focus { - border-color: $tertiary; - outline: 0; - box-shadow: inset 0 1px 1px rgba(0,0,0, .3), 0 0 8px $tertiary; - } } select, @@ -224,8 +148,6 @@ input[type="file"] { select { width: 220px; - border: 1px solid $primary-low; - &[multiple], &[size] { height: auto; From 6d3ed966cdf23c55df1874304a6f031e3acc7452 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 9 Nov 2017 12:33:36 -0800 Subject: [PATCH 138/445] FEATURE: replace admin flags modal by a select-box --- .../admin/components/flagged-post.js.es6 | 4 + .../templates/components/flagged-post.hbs | 8 +- .../admin-agree-flag-dropdown.js.es6 | 86 +++++++++++++++++++ .../select-box-kit/select-box-kit-row.js.es6 | 5 +- .../admin-agree-flag-dropdown.scss | 10 +++ .../acceptance/admin-flags-test.js.es6 | 36 +++++--- .../components/select-box-kit-test.js.es6 | 2 +- 7 files changed, 129 insertions(+), 22 deletions(-) create mode 100644 app/assets/javascripts/select-box-kit/components/admin-agree-flag-dropdown.js.es6 create mode 100644 app/assets/stylesheets/common/select-box-kit/admin-agree-flag-dropdown.scss diff --git a/app/assets/javascripts/admin/components/flagged-post.js.es6 b/app/assets/javascripts/admin/components/flagged-post.js.es6 index 5bf1f9ab22..aa7c9ea8ae 100644 --- a/app/assets/javascripts/admin/components/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/components/flagged-post.js.es6 @@ -32,6 +32,10 @@ export default Ember.Component.extend({ }, actions: { + onRemoveAfterPromise(promise) { + this.removeAfter(promise); + }, + showAgreeFlagModal() { this._spawnModal('admin-agree-flag', this.get('flaggedPost'), 'agree-flag-modal'); }, diff --git a/app/assets/javascripts/admin/templates/components/flagged-post.hbs b/app/assets/javascripts/admin/templates/components/flagged-post.hbs index 9bba9abfcd..81b3195de4 100644 --- a/app/assets/javascripts/admin/templates/components/flagged-post.hbs +++ b/app/assets/javascripts/admin/templates/components/flagged-post.hbs @@ -111,13 +111,7 @@ {{#if canAct}}
    - {{d-button - title="admin.flags.agree_title" - class="agree-flag" - label="admin.flags.agree" - icon="thumbs-o-up" - action="showAgreeFlagModal" - ellipsis=true}} + {{admin-agree-flag-dropdown post=flaggedPost onRemoveAfterPromise=(action "onRemoveAfterPromise")}} {{#if flaggedPost.postHidden}} {{d-button diff --git a/app/assets/javascripts/select-box-kit/components/admin-agree-flag-dropdown.js.es6 b/app/assets/javascripts/select-box-kit/components/admin-agree-flag-dropdown.js.es6 new file mode 100644 index 0000000000..0645196dab --- /dev/null +++ b/app/assets/javascripts/select-box-kit/components/admin-agree-flag-dropdown.js.es6 @@ -0,0 +1,86 @@ +import { iconHTML } from 'discourse-common/lib/icon-library'; +import DropdownSelectBox from "select-box-kit/components/dropdown-select-box"; +import computed from "ember-addons/ember-computed-decorators"; +import { on } from "ember-addons/ember-computed-decorators"; + +export default DropdownSelectBox.extend({ + headerText: "admin.flags.agree", + headerIcon: "thumbs-o-up", + classNames: ["agree-flag", "admin-agree-flag-dropdown"], + adminTools: Ember.inject.service(), + nameProperty: "label", + + @on("didReceiveAttrs") + _setAdminAgreeDropdownOptions() { + this.set("headerComponentOptions.selectedName", `${I18n.t(this.get("headerText"))}...`); + this.set("headerComponentOptions.icon", iconHTML("thumbs-o-up")); + }, + + @computed("adminTools", "post.user") + spammerDetails(adminTools, user) { + return adminTools.spammerDetails(user); + }, + + canDeleteSpammer: Ember.computed.and("spammerDetails.canDelete", "post.flaggedForSpam"), + + @computed("post", "canDeleteSpammer") + content(post, canDeleteSpammer) { + const content = []; + + if (post.user_deleted === true) { + content.push({ + title: I18n.t("admin.flags.agree_flag_restore_post_title"), + icon: "eye", + id: "confirm-agree-restore", + action: () => this.send("perform", "restore"), + label: I18n.t("admin.flags.agree_flag_restore_post"), + }); + } else { + if (post.get("postHidden") !== true) { + content.push({ + title: I18n.t("admin.flags.agree_flag_hide_post_title"), + icon: "eye-slash", + action: () => this.send("perform", "hide"), + id: "confirm-agree-hide", + label: I18n.t("admin.flags.agree_flag_hide_post"), + }); + } + } + + content.push({ + title: I18n.t("admin.flags.agree_flag_title"), + icon: "thumbs-o-up", + id: "confirm-agree-keep", + action: () => this.send("perform", "keep"), + label: I18n.t("admin.flags.agree_flag"), + }); + + if (canDeleteSpammer) { + content.push({ + title: I18n.t("admin.flags.delete_spammer_title"), + icon: "exclamation-triangle", + id: "delete-spammer", + action: () => this.send("deleteSpammer"), + label: I18n.t("admin.flags.delete_spammer"), + }); + } + + return content; + }, + + selectValueFunction(value) { + Ember.get(this._contentForValue(value), "action")(); + }, + + actions: { + deleteSpammer() { + let spammerDetails = this.get("spammerDetails"); + this.sendAction("onRemoveAfterPromise", spammerDetails.deleteUser()); + }, + + perform(action) { + let flaggedPost = this.get("post"); + this.sendAction("onRemoveAfterPromise", flaggedPost.agreeFlags(action)); + }, + } +}); diff --git a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-row.js.es6 b/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-row.js.es6 index 0ef116bba9..c1bf0f2d5e 100644 --- a/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-row.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/select-box-kit/select-box-kit-row.js.es6 @@ -18,7 +18,10 @@ export default Ember.Component.extend(UtilsMixin, { classNameBindings: ["isHighlighted", "isSelected"], clicked: false, - title: Ember.computed.alias("content.name"), + @computed("content.originalContent.title", "content.name") + title(title, name) { + return title || name; + }, @computed("templateForRow") template(templateForRow) { return templateForRow(this); }, diff --git a/app/assets/stylesheets/common/select-box-kit/admin-agree-flag-dropdown.scss b/app/assets/stylesheets/common/select-box-kit/admin-agree-flag-dropdown.scss new file mode 100644 index 0000000000..271e4b1ad5 --- /dev/null +++ b/app/assets/stylesheets/common/select-box-kit/admin-agree-flag-dropdown.scss @@ -0,0 +1,10 @@ +.select-box-kit { + &.dropdown-select-box { + &.admin-agree-flag-dropdown { + .select-box-kit-row[data-value="delete-spammer"] .texts .name, + .select-box-kit-row[data-value="delete-spammer"] .icons .d-icon { + color: $danger; + } + } + } +} diff --git a/test/javascripts/acceptance/admin-flags-test.js.es6 b/test/javascripts/acceptance/admin-flags-test.js.es6 index 0be7e93b0a..80eacf7049 100644 --- a/test/javascripts/acceptance/admin-flags-test.js.es6 +++ b/test/javascripts/acceptance/admin-flags-test.js.es6 @@ -14,40 +14,50 @@ QUnit.test("flagged posts", assert => { QUnit.test("flagged posts - agree", assert => { visit("/admin/flags/active"); - click('.agree-flag'); + andThen(() => { - assert.equal(find('.agree-flag-modal:visible').length, 1); + expandSelectBoxKit('.agree-flag'); }); - click('.confirm-agree-keep'); + + andThen(() => { + selectBoxKitSelectRow('confirm-agree-keep', { selector: '.agree-flag'}); + }); + andThen(() => { - assert.equal(find('.agree-flag-modal:visible').length, 0, 'modal is closed'); assert.equal(find('.admin-flags .flagged-post').length, 0, 'post was removed'); }); }); QUnit.test("flagged posts - agree + hide", assert => { visit("/admin/flags/active"); - click('.agree-flag'); + andThen(() => { - assert.equal(find('.agree-flag-modal:visible').length, 1); + expandSelectBoxKit('.agree-flag'); }); - click('.confirm-agree-hide'); + + andThen(() => { + selectBoxKitSelectRow('confirm-agree-hide', { selector: '.agree-flag'}); + }); + andThen(() => { - assert.equal(find('.agree-flag-modal:visible').length, 0, 'modal is closed'); assert.equal(find('.admin-flags .flagged-post').length, 0, 'post was removed'); }); }); QUnit.test("flagged posts - agree + deleteSpammer", assert => { visit("/admin/flags/active"); - click('.agree-flag'); + andThen(() => { - assert.equal(find('.agree-flag-modal:visible').length, 1); + expandSelectBoxKit('.agree-flag'); }); - click('.delete-spammer'); - click('.confirm-delete'); + + andThen(() => { + selectBoxKitSelectRow('delete-spammer', { selector: '.agree-flag'}); + }); + + click('.confirm-delete'); + andThen(() => { - assert.equal(find('.agree-flag-modal:visible').length, 0, 'modal is closed'); assert.equal(find('.admin-flags .flagged-post').length, 0, 'post was removed'); }); }); diff --git a/test/javascripts/components/select-box-kit-test.js.es6 b/test/javascripts/components/select-box-kit-test.js.es6 index 6e0f37a048..904684cd8b 100644 --- a/test/javascripts/components/select-box-kit-test.js.es6 +++ b/test/javascripts/components/select-box-kit-test.js.es6 @@ -142,7 +142,7 @@ componentTest('supports options to limit size', { template: '{{select-box-kit collectionHeight=20 content=content}}', beforeEach() { - this.set("content", [{ id: 1, name: "robin" }]); + this.set("content", ["robin", "régis"]); }, test(assert) { From 16ff2a4715061d006ef608fc89d342adf66b34bd Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 9 Nov 2017 15:33:26 -0500 Subject: [PATCH 139/445] FIX: topic counts after converting topic to/from public and private --- app/models/topic_converter.rb | 19 +++++++++++++-- app/services/user_action_creator.rb | 4 ++++ spec/models/topic_converter_spec.rb | 36 ++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/app/models/topic_converter.rb b/app/models/topic_converter.rb index f46eaba6ce..5ebe651625 100644 --- a/app/models/topic_converter.rb +++ b/app/models/topic_converter.rb @@ -24,6 +24,21 @@ class TopicConverter @topic.archetype = Archetype.default @topic.save update_user_stats + + # TODO: Every post in a PRIVATE MESSAGE looks the same: each is a UserAction::NEW_PRIVATE_MESSAGE. + # So we need to remove all those user actions and re-log all the posts. + # Post counting depends on the correct UserActions (NEW_TOPIC, REPLY), so once a private topic + # becomes a public topic, post counts are wrong. The reverse is not so bad because + # we don't count NEW_PRIVATE_MESSAGE in any public stats. + # TBD: why do so many specs fail with this change? + + # UserAction.where(target_topic_id: @topic.id, action_type: [UserAction::GOT_PRIVATE_MESSAGE, UserAction::NEW_PRIVATE_MESSAGE]).find_each do |ua| + # UserAction.remove_action!(ua.attributes.symbolize_keys.slice(:action_type, :user_id, :acting_user_id, :target_topic_id, :target_post_id)) + # end + # @topic.posts.each do |post| + # UserActionCreator.log_post(post) unless post.post_number == 1 + # end + watch_topic(topic) end @topic @@ -45,7 +60,7 @@ class TopicConverter def update_user_stats @topic.posts.where(deleted_at: nil).each do |p| user = User.find(p.user_id) - # update posts count + # update posts count. NOTE that DirectoryItem.refresh will overwrite this by counting UserAction records. user.user_stat.post_count += 1 user.user_stat.save! end @@ -58,7 +73,7 @@ class TopicConverter @topic.posts.where(deleted_at: nil).each do |p| user = User.find(p.user_id) @topic.topic_allowed_users.build(user_id: user.id) unless @topic.topic_allowed_users.where(user_id: user.id).exists? - # update posts count + # update posts count. NOTE that DirectoryItem.refresh will overwrite this by counting UserAction records. user.user_stat.post_count -= 1 user.user_stat.save! end diff --git a/app/services/user_action_creator.rb b/app/services/user_action_creator.rb index 68a5d0847d..7b1c92562b 100644 --- a/app/services/user_action_creator.rb +++ b/app/services/user_action_creator.rb @@ -93,6 +93,10 @@ class UserActionCreator created_at: model.created_at } + UserAction.remove_action!(row.merge( + action_type: model.archetype == Archetype.private_message ? UserAction::NEW_TOPIC : UserAction::NEW_PRIVATE_MESSAGE + )) + rows = [row] if model.private_message? diff --git a/spec/models/topic_converter_spec.rb b/spec/models/topic_converter_spec.rb index 2e76cf9bba..05d28b10e6 100644 --- a/spec/models/topic_converter_spec.rb +++ b/spec/models/topic_converter_spec.rb @@ -6,7 +6,9 @@ describe TopicConverter do let(:admin) { Fabricate(:admin) } let(:author) { Fabricate(:user) } let(:category) { Fabricate(:category) } - let(:private_message) { Fabricate(:private_message_topic, user: author) } + let(:private_message) { Fabricate(:private_message_topic, user: author) } # creates a topic without a first post + let(:first_post) { Fabricate(:post, topic: private_message, user: author) } + let(:other_user) { private_message.topic_allowed_users.find { |u| u.user != author }.user } context 'success' do it "converts private message to regular topic" do @@ -48,12 +50,36 @@ describe TopicConverter do end it "updates user stats" do + first_post topic_user = TopicUser.create!(user_id: author.id, topic_id: private_message.id, posted: true) expect(private_message.user.user_stat.topic_count).to eq(0) private_message.convert_to_public_topic(admin) expect(private_message.reload.user.user_stat.topic_count).to eq(1) expect(topic_user.reload.notification_level).to eq(TopicUser.notification_levels[:watching]) end + + context "with a reply" do + before do + UserActionCreator.enable + first_post + create_post(topic: private_message, user: other_user) + private_message.reload + end + + after do + UserActionCreator.disable + end + + it "updates UserActions" do + TopicConverter.new(private_message, admin).convert_to_public_topic + expect(author.user_actions.where(action_type: UserAction::NEW_PRIVATE_MESSAGE).count).to eq(0) + expect(author.user_actions.where(action_type: UserAction::NEW_TOPIC).count).to eq(1) + # TODO: + # expect(other_user.user_actions.where(action_type: UserAction::NEW_PRIVATE_MESSAGE).count).to eq(0) + # expect(other_user.user_actions.where(action_type: UserAction::GOT_PRIVATE_MESSAGE).count).to eq(0) + # expect(other_user.user_actions.where(action_type: UserAction::REPLY).count).to eq(1) + end + end end end @@ -81,6 +107,14 @@ describe TopicConverter do expect(topic.reload.user.user_stat.topic_count).to eq(0) expect(topic_user.reload.notification_level).to eq(TopicUser.notification_levels[:watching]) end + + it "changes user_action type" do + UserActionCreator.enable + topic.convert_to_private_message(admin) + expect(author.user_actions.where(action_type: UserAction::NEW_TOPIC).count).to eq(0) + expect(author.user_actions.where(action_type: UserAction::NEW_PRIVATE_MESSAGE).count).to eq(1) + UserActionCreator.disable + end end context 'topic has replies' do From c53ddb7723720e4c5d98f830b731be4376c00d03 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 9 Nov 2017 15:34:46 -0500 Subject: [PATCH 140/445] FIX: Sometimes viewing a user's action logs would reset to view all --- .../controllers/admin-logs-staff-action-logs.js.es6 | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 index 4899c4b59e..626fe955ce 100644 --- a/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-logs-staff-action-logs.js.es6 @@ -30,7 +30,7 @@ export default Ember.Controller.extend({ showInstructions: Ember.computed.gt('model.length', 0), - refresh: function() { + _refresh() { this.set('loading', true); var filters = this.get('filters'), @@ -65,14 +65,18 @@ export default Ember.Controller.extend({ }); }, + scheduleRefresh() { + Ember.run.scheduleOnce('afterRender', this, this._refresh); + }, + resetFilters: function() { this.set('filters', Ember.Object.create()); - this.refresh(); + this.scheduleRefresh(); }.on('init'), _changeFilters: function(props) { this.get('filters').setProperties(props); - this.refresh(); + this.scheduleRefresh(); }, actions: { @@ -91,7 +95,7 @@ export default Ember.Controller.extend({ this._changeFilters(changed); }, - clearAllFilters: function() { + clearAllFilters() { this.set("filterActionId", null); this.resetFilters(); }, From 0b905e24ed83f42d765ee99d58355831ef80b185 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 10 Nov 2017 09:12:42 +1100 Subject: [PATCH 141/445] should not have changed this --- app/assets/javascripts/discourse/models/user.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 49f314373d..cc32dda927 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -552,7 +552,7 @@ User.reopenClass(Singleton, { }, checkUsername(username, email, for_user_id) { - return ajax(userPath('check_username') + ".json", { + return ajax(userPath('check_username'), { data: { username, email, for_user_id } }); }, From 73aa7edb8be19692a6141be9079692a8d7c9ebcd Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 9 Nov 2017 15:38:34 -0800 Subject: [PATCH 142/445] Temporarily skip multisite spec --- spec/integration/multisite_spec.rb | 134 ++++++++++++++--------------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/spec/integration/multisite_spec.rb b/spec/integration/multisite_spec.rb index 678052a046..647b925fce 100644 --- a/spec/integration/multisite_spec.rb +++ b/spec/integration/multisite_spec.rb @@ -1,67 +1,67 @@ -require 'rails_helper' - -describe 'multisite' do - - class DBNameMiddleware - def initialize(app, config = {}) - @app = app - end - - def call(env) - # note current_db is already being ruined on boot cause its not multisite - [200, {}, [RailsMultisite::ConnectionManagement.current_hostname]] - end - end - - let :session do - RailsMultisite::ConnectionManagement.config_filename = "spec/fixtures/multisite/two_dbs.yml" - RailsMultisite::ConnectionManagement.load_settings! - - stack = ActionDispatch::MiddlewareStack.new - stack.use RailsMultisite::ConnectionManagement, RailsMultisite::DiscoursePatches.config - stack.use DBNameMiddleware - - routes = ActionDispatch::Routing::RouteSet.new - stack.build(routes) - end - - it "should always allow /srv/status through" do - headers = { - "HTTP_HOST" => "unknown.com", - "REQUEST_METHOD" => "GET", - "PATH_INFO" => "/srv/status", - "rack.input" => StringIO.new - } - - code, _, body = session.call(headers) - expect(code).to eq(200) - expect(body.join).to eq("test.localhost") - end - - it "should 404 on unknown routes" do - headers = { - "HTTP_HOST" => "unknown.com", - "REQUEST_METHOD" => "GET", - "PATH_INFO" => "/topics", - "rack.input" => StringIO.new - } - - code, _ = session.call(headers) - expect(code).to eq(404) - end - - it "should hit correct site elsewise" do - - headers = { - "HTTP_HOST" => "test2.localhost", - "REQUEST_METHOD" => "GET", - "PATH_INFO" => "/topics", - "rack.input" => StringIO.new - } - - code, _, body = session.call(headers) - expect(code).to eq(200) - expect(body.join).to eq("test2.localhost") - end - -end +# require 'rails_helper' +# +# describe 'multisite' do +# +# class DBNameMiddleware +# def initialize(app, config = {}) +# @app = app +# end +# +# def call(env) +# # note current_db is already being ruined on boot cause its not multisite +# [200, {}, [RailsMultisite::ConnectionManagement.current_hostname]] +# end +# end +# +# let :session do +# RailsMultisite::ConnectionManagement.config_filename = "spec/fixtures/multisite/two_dbs.yml" +# RailsMultisite::ConnectionManagement.load_settings! +# +# stack = ActionDispatch::MiddlewareStack.new +# stack.use RailsMultisite::ConnectionManagement, RailsMultisite::DiscoursePatches.config +# stack.use DBNameMiddleware +# +# routes = ActionDispatch::Routing::RouteSet.new +# stack.build(routes) +# end +# +# it "should always allow /srv/status through" do +# headers = { +# "HTTP_HOST" => "unknown.com", +# "REQUEST_METHOD" => "GET", +# "PATH_INFO" => "/srv/status", +# "rack.input" => StringIO.new +# } +# +# code, _, body = session.call(headers) +# expect(code).to eq(200) +# expect(body.join).to eq("test.localhost") +# end +# +# it "should 404 on unknown routes" do +# headers = { +# "HTTP_HOST" => "unknown.com", +# "REQUEST_METHOD" => "GET", +# "PATH_INFO" => "/topics", +# "rack.input" => StringIO.new +# } +# +# code, _ = session.call(headers) +# expect(code).to eq(404) +# end +# +# it "should hit correct site elsewise" do +# +# headers = { +# "HTTP_HOST" => "test2.localhost", +# "REQUEST_METHOD" => "GET", +# "PATH_INFO" => "/topics", +# "rack.input" => StringIO.new +# } +# +# code, _, body = session.call(headers) +# expect(code).to eq(200) +# expect(body.join).to eq("test2.localhost") +# end +# +# end From 6cd69529a8f421385d1d39601b1cb63159aeda4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 10 Nov 2017 14:18:19 +0100 Subject: [PATCH 143/445] UX: change composer button text to 'whisper' when whispering --- .../discourse/models/composer.js.es6 | 17 +++++++++-------- config/locales/client.en.yml | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 6094ae285d..ed3c489e06 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -46,6 +46,12 @@ const CLOSED = 'closed', featuredLink: 'topic.featured_link' }; +const _saveLabels = {}; +_saveLabels[EDIT] = 'composer.save_edit'; +_saveLabels[REPLY] = 'composer.reply'; +_saveLabels[CREATE_TOPIC] = 'composer.create_topic'; +_saveLabels[PRIVATE_MESSAGE] = 'composer.create_pm'; + const Composer = RestModel.extend({ _categoryId: null, unlistTopic: false, @@ -250,14 +256,9 @@ const Composer = RestModel.extend({ } }, - @computed('action') - saveLabel(action) { - switch (action) { - case EDIT: return 'composer.save_edit'; - case REPLY: return 'composer.reply'; - case CREATE_TOPIC: return 'composer.create_topic'; - case PRIVATE_MESSAGE: return 'composer.create_pm'; - } + @computed('action', 'whisper') + saveLabel(action, whisper) { + return whisper ? 'composer.create_whisper' : _saveLabels[action]; }, hasMetaData: function() { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index a3ec45596d..bd9dbfda82 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1218,6 +1218,7 @@ en: cancel: "Cancel" create_topic: "Create Topic" create_pm: "Message" + create_whisper: "Whisper" title: "Or press Ctrl+Enter" users_placeholder: "Add a user" From 32be3f98c9f30bded933efc54d0429f7e682c91c Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 10 Nov 2017 10:53:57 -0500 Subject: [PATCH 144/445] UX: Widget options to disable FAQ and About --- .../discourse/widgets/hamburger-menu.js.es6 | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index c9f40de4a2..77e8b33186 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -35,7 +35,9 @@ export default createWidget('hamburger-menu', { settings: { showCategories: true, - maxWidth: 300 + maxWidth: 300, + showFAQ: true, + showAbout: true }, adminLinks() { @@ -141,9 +143,11 @@ export default createWidget('hamburger-menu', { footerLinks(prioritizeFaq, faqUrl) { const links = []; - links.push({ route: 'about', className: 'about-link', label: 'about.simple_title' }); + if (this.settings.showAbout) { + links.push({ route: 'about', className: 'about-link', label: 'about.simple_title' }); + } - if (!prioritizeFaq) { + if (this.settings.showFAQ && !prioritizeFaq) { links.push({ href: faqUrl, className: 'faq-link', label: 'faq' }); } @@ -171,7 +175,10 @@ export default createWidget('hamburger-menu', { faqUrl = Discourse.getURL('/faq'); } - const prioritizeFaq = this.currentUser && !this.currentUser.read_faq; + const prioritizeFaq = this.settings.showFAQ && + this.currentUser && + !this.currentUser.read_faq; + if (prioritizeFaq) { results.push(this.attach('menu-links', { name: 'faq-link', heading: true, contents: () => { return this.attach('priority-faq-link', { href: faqUrl }); From 31e2385316422a9d91ec118e73504143f3e4542e Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 10 Nov 2017 16:10:25 +0100 Subject: [PATCH 145/445] FEATURE: do not send notification emails to users who are included in the To and CC header of an incoming email --- app/services/post_alerter.rb | 14 ++++- spec/fabricators/incoming_email_fabricator.rb | 21 ++++++- spec/fabricators/post_fabricator.rb | 11 ++++ spec/services/post_alerter_spec.rb | 55 +++++++++++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index 56ed07fddd..ab86e75915 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -392,13 +392,20 @@ class PostAlerter notification_data[:group_name] = group.name end + if original_post.via_email && (incoming_email = original_post.incoming_email) + skip_send_email = contains_email_address?(incoming_email.to_addresses, user) || + contains_email_address?(incoming_email.cc_addresses, user) + else + skip_send_email = opts[:skip_send_email] + end + # Create the notification user.notifications.create(notification_type: type, topic_id: post.topic_id, post_number: post.post_number, post_action_id: opts[:post_action_id], data: notification_data.to_json, - skip_send_email: opts[:skip_send_email]) + skip_send_email: skip_send_email) if !existing_notification && NOTIFIABLE_TYPES.include?(type) && !user.suspended? # we may have an invalid post somehow, dont blow up @@ -422,6 +429,11 @@ class PostAlerter end + def contains_email_address?(addresses, user) + return false if addresses.blank? + addresses.split(";").include?(user.email) + end + def push_notification(user, payload) if SiteSetting.allow_user_api_key_scopes.split("|").include?("push") && SiteSetting.allowed_user_api_push_urls.present? clients = user.user_api_keys diff --git a/spec/fabricators/incoming_email_fabricator.rb b/spec/fabricators/incoming_email_fabricator.rb index 47e158addb..d91f6a9e39 100644 --- a/spec/fabricators/incoming_email_fabricator.rb +++ b/spec/fabricators/incoming_email_fabricator.rb @@ -1 +1,20 @@ -Fabricator(:incoming_email) +Fabricator(:incoming_email) do + message_id "12345@example.com" + subject "Hello world" + from_address "foo@example.com" + to_addresses "someone@else.com" + + raw <<~RAW + Return-Path: + From: Foo + To: someone@else.com + Subject: Hello world + Date: Fri, 15 Jan 2016 00:12:43 +0100 + Message-ID: <12345@example.com> + Mime-Version: 1.0 + Content-Type: text/plain; charset=UTF-8 + Content-Transfer-Encoding: quoted-printable + + The body contains "Hello world" too. + RAW +end diff --git a/spec/fabricators/post_fabricator.rb b/spec/fabricators/post_fabricator.rb index 0854ba0db1..e566d949b8 100644 --- a/spec/fabricators/post_fabricator.rb +++ b/spec/fabricators/post_fabricator.rb @@ -133,3 +133,14 @@ Fabricator(:private_message_post, from: :post) do end raw "Ssshh! This is our secret conversation!" end + +Fabricator(:post_via_email, from: :post) do + incoming_email + via_email true + + after_create do |post| + incoming_email.topic = post.topic + incoming_email.post = post + incoming_email.user = post.user + end +end diff --git a/spec/services/post_alerter_spec.rb b/spec/services/post_alerter_spec.rb index 4293108b9b..d7e655144b 100644 --- a/spec/services/post_alerter_spec.rb +++ b/spec/services/post_alerter_spec.rb @@ -572,6 +572,61 @@ describe PostAlerter do expect(admin1.notifications.where(notification_type: Notification.types[:replied]).count).to eq(1) end + + it "sends email notifications only to users not on CC list of incoming email" do + alice = Fabricate(:user, username: "alice", email: "alice@example.com") + bob = Fabricate(:user, username: "bob", email: "bob@example.com") + carol = Fabricate(:user,username: "carol", email: "carol@example.com", staged: true) + dave = Fabricate(:user, username: "dave", email: "dave@example.com", staged: true) + erin = Fabricate(:user, username: "erin", email: "erin@example.com") + + topic = Fabricate(:private_message_topic, topic_allowed_users: [ + Fabricate.build(:topic_allowed_user, user: alice), + Fabricate.build(:topic_allowed_user, user: bob), + Fabricate.build(:topic_allowed_user, user: carol), + Fabricate.build(:topic_allowed_user, user: dave), + Fabricate.build(:topic_allowed_user, user: erin) + ]) + post = Fabricate(:post, user: alice, topic: topic) + + TopicUser.change(alice.id, topic.id, notification_level: TopicUser.notification_levels[:watching]) + TopicUser.change(bob.id, topic.id, notification_level: TopicUser.notification_levels[:watching]) + TopicUser.change(erin.id, topic.id, notification_level: TopicUser.notification_levels[:watching]) + + email = Fabricate(:incoming_email, + raw: <<~RAW, + Return-Path: + From: Bob + To: meta+1234@discoursemail.com, dave@example.com + CC: carol@example.com, erin@example.com + Subject: Hello world + Date: Fri, 15 Jan 2016 00:12:43 +0100 + Message-ID: <12345@example.com> + Mime-Version: 1.0 + Content-Type: text/plain; charset=UTF-8 + Content-Transfer-Encoding: quoted-printable + + This post was created by email. + RAW + from_address: "bob@example.com", + to_addresses: "meta+1234@discoursemail.com;dave@example.com", + cc_addresses: "carol@example.com;erin@example.com") + reply = Fabricate(:post_via_email, user: bob, topic: topic, incoming_email: email, reply_to_post_number: 1) + + NotificationEmailer.expects(:process_notification).with { |n| n.user_id == alice.id }.once + NotificationEmailer.expects(:process_notification).with { |n| n.user_id == bob.id }.never + NotificationEmailer.expects(:process_notification).with { |n| n.user_id == carol.id }.never + NotificationEmailer.expects(:process_notification).with { |n| n.user_id == dave.id }.never + NotificationEmailer.expects(:process_notification).with { |n| n.user_id == erin.id }.never + + PostAlerter.post_created(reply) + + expect(alice.notifications.count).to eq(1) + expect(bob.notifications.count).to eq(0) + expect(carol.notifications.count).to eq(1) + expect(dave.notifications.count).to eq(1) + expect(erin.notifications.count).to eq(1) + end end context "watching" do From 482982dce8276c7fdbcedc000a9f11b11c6fadef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 10 Nov 2017 17:52:08 +0100 Subject: [PATCH 146/445] UX: fix user bio & category topic template editors --- .../templates/preferences/profile.hbs | 2 +- app/assets/stylesheets/common/d-editor.scss | 25 +++++++++++++++++++ .../common/foundation/variables.scss | 1 + 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/preferences/profile.hbs b/app/assets/javascripts/discourse/templates/preferences/profile.hbs index c94a78775d..39686f2b12 100644 --- a/app/assets/javascripts/discourse/templates/preferences/profile.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/profile.hbs @@ -1,7 +1,7 @@ {{#if canChangeBio}}
    -
    +
    {{d-editor value=model.bio_raw}}
    diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index 9d016e9a80..52e481abdc 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -138,3 +138,28 @@ display: none; flex: 0; } + +.user-preferences .bio-composer, +.edit-category-tab-topic-template { + textarea { + width: 100%; + } + .d-editor-container { + display: block; + } + .d-editor-textarea-wrapper { + border: 1px solid $primary-low; + } + .d-editor-preview-wrapper { + max-width: 100%; + margin: 10px 0 0 0; + } + .d-editor-preview { + background-color: dark-light-diff($primary, $secondary, 97%, -70%); + } +} + +.user-preferences .bio-composer { + padding: 10px; + border: 1px solid $primary-low; +} diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index 118725aab6..0cf84a490f 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -82,6 +82,7 @@ $base-font-family: Helvetica, Arial, sans-serif !default; // standard color transformations, use these if possible, and add any new dark-light-diffs here //primary +$primary-very-low: dark-light-diff($primary, $secondary, 97%, -75%); $primary-low: dark-light-diff($primary, $secondary, 90%, -65%); $primary-low-mid: dark-light-diff($primary, $secondary, 70%, -45%); $primary-medium: dark-light-diff($primary, $secondary, 50%, -20%); From 5135f739334b23e67788ad34c35e4023e1b28c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 10 Nov 2017 17:57:02 +0100 Subject: [PATCH 147/445] actually use -very-low color --- app/assets/stylesheets/common/d-editor.scss | 2 +- app/assets/stylesheets/common/foundation/variables.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/d-editor.scss b/app/assets/stylesheets/common/d-editor.scss index 52e481abdc..989576ce1b 100644 --- a/app/assets/stylesheets/common/d-editor.scss +++ b/app/assets/stylesheets/common/d-editor.scss @@ -155,7 +155,7 @@ margin: 10px 0 0 0; } .d-editor-preview { - background-color: dark-light-diff($primary, $secondary, 97%, -70%); + background-color: $primary-very-low; } } diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index 0cf84a490f..242ca1218e 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -82,7 +82,7 @@ $base-font-family: Helvetica, Arial, sans-serif !default; // standard color transformations, use these if possible, and add any new dark-light-diffs here //primary -$primary-very-low: dark-light-diff($primary, $secondary, 97%, -75%); +$primary-very-low: dark-light-diff($primary, $secondary, 97%, -80%); $primary-low: dark-light-diff($primary, $secondary, 90%, -65%); $primary-low-mid: dark-light-diff($primary, $secondary, 70%, -45%); $primary-medium: dark-light-diff($primary, $secondary, 50%, -20%); From 0ccefb036519e93c2cdd46ebd3757d7e29beb98e Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Fri, 10 Nov 2017 17:56:18 +0100 Subject: [PATCH 148/445] make RuboCop happy --- spec/services/post_alerter_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/post_alerter_spec.rb b/spec/services/post_alerter_spec.rb index d7e655144b..74c41d9a7a 100644 --- a/spec/services/post_alerter_spec.rb +++ b/spec/services/post_alerter_spec.rb @@ -576,7 +576,7 @@ describe PostAlerter do it "sends email notifications only to users not on CC list of incoming email" do alice = Fabricate(:user, username: "alice", email: "alice@example.com") bob = Fabricate(:user, username: "bob", email: "bob@example.com") - carol = Fabricate(:user,username: "carol", email: "carol@example.com", staged: true) + carol = Fabricate(:user, username: "carol", email: "carol@example.com", staged: true) dave = Fabricate(:user, username: "dave", email: "dave@example.com", staged: true) erin = Fabricate(:user, username: "erin", email: "erin@example.com") From 9dc9ca4ac0d4f46966d7ae16dbcfc545fdb22ad4 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 9 Nov 2017 18:05:53 -0500 Subject: [PATCH 149/445] FIX: be consistent with how first posts in topics are counted. do like DirectoryItem.refresh_period :all --- app/services/anonymous_shadow_creator.rb | 2 +- lib/guardian/user_guardian.rb | 2 +- lib/post_creator.rb | 2 +- lib/post_destroyer.rb | 24 +++++++--- spec/components/post_creator_spec.rb | 4 +- spec/components/post_destroyer_spec.rb | 53 ++++++++++++++++++----- spec/controllers/users_controller_spec.rb | 2 +- 7 files changed, 68 insertions(+), 21 deletions(-) diff --git a/app/services/anonymous_shadow_creator.rb b/app/services/anonymous_shadow_creator.rb index 08c1d6a338..651c155c76 100644 --- a/app/services/anonymous_shadow_creator.rb +++ b/app/services/anonymous_shadow_creator.rb @@ -17,7 +17,7 @@ class AnonymousShadowCreator if (shadow_id = user.custom_fields["shadow_id"].to_i) > 0 shadow = User.find_by(id: shadow_id) - if shadow && shadow.post_count > 0 && + if shadow && (shadow.post_count + shadow.topic_count) > 0 && shadow.last_posted_at < SiteSetting.anonymous_account_duration_minutes.minutes.ago shadow = nil end diff --git a/lib/guardian/user_guardian.rb b/lib/guardian/user_guardian.rb index 616020aa98..d066459e1c 100644 --- a/lib/guardian/user_guardian.rb +++ b/lib/guardian/user_guardian.rb @@ -9,7 +9,7 @@ module UserGuardian return false if (SiteSetting.sso_overrides_username? && SiteSetting.enable_sso?) return true if is_staff? return false if SiteSetting.username_change_period <= 0 - is_me?(user) && (user.post_count == 0 || user.created_at > SiteSetting.username_change_period.days.ago) + is_me?(user) && ((user.post_count + user.topic_count) == 0 || user.created_at > SiteSetting.username_change_period.days.ago) end def can_edit_email?(user) diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 1e1ae5f806..ba1d5915e5 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -473,7 +473,7 @@ class PostCreator end unless @post.topic.private_message? - @user.user_stat.post_count += 1 if @post.post_type == Post.types[:regular] + @user.user_stat.post_count += 1 if @post.post_type == Post.types[:regular] && !@post.is_first_post? @user.user_stat.topic_count += 1 if @post.is_first_post? end diff --git a/lib/post_destroyer.rb b/lib/post_destroyer.rb index a449d55255..6727211b9e 100644 --- a/lib/post_destroyer.rb +++ b/lib/post_destroyer.rb @@ -76,12 +76,26 @@ class PostDestroyer @post.recover! if author = @post.user - author.user_stat.post_count += 1 + if @post.is_first_post? + author.user_stat.topic_count += 1 + else + author.user_stat.post_count += 1 + end author.user_stat.save! end + if @post.is_first_post? && @post.topic && !@post.topic.private_message? + # Update stats of all people who replied + counts = Post.where(post_type: Post.types[:regular], topic_id: @post.topic_id).where('post_number > 1').group(:user_id).count + counts.each do |user_id, count| + if user_stat = UserStat.where(user_id: user_id).first + user_stat.update_attributes(post_count: user_stat.post_count + count) + end + end + end + @post.publish_change_to_clients! :recovered - TopicTrackingState.publish_recover(@post.topic) if @post.topic && @post.post_number == 1 + TopicTrackingState.publish_recover(@post.topic) if @post.topic && @post.is_first_post? end # When a post is properly deleted. Well, it's still soft deleted, but it will no longer @@ -231,12 +245,12 @@ class PostDestroyer author.user_stat.first_post_created_at = author.posts.order('created_at ASC').first.try(:created_at) end - if @post.post_type == Post.types[:regular] && !(@topic.nil? && !@post.is_first_post?) + if @post.post_type == Post.types[:regular] && !@post.is_first_post? && !@topic.nil? author.user_stat.post_count -= 1 end author.user_stat.topic_count -= 1 if @post.is_first_post? - # We don't count replies to your own topics + # We don't count replies to your own topics in topic_reply_count if @topic && author.id != @topic.user_id author.user_stat.update_topic_reply_count end @@ -250,7 +264,7 @@ class PostDestroyer if @post.is_first_post? && @post.topic && !@post.topic.private_message? # Update stats of all people who replied - counts = Post.where(post_type: Post.types[:regular]).where(topic_id: @post.topic_id).group(:user_id).count + counts = Post.where(post_type: Post.types[:regular], topic_id: @post.topic_id).where('post_number > 1').group(:user_id).count counts.each do |user_id, count| if user_stat = UserStat.where(user_id: user_id).first user_stat.update_attributes(post_count: user_stat.post_count - count) diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 4c753a0dc1..39d9ee4829 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -399,7 +399,7 @@ describe PostCreator do raw: 'this is a whispered reply').create # don't count whispers in user stats - expect(user_stat.reload.post_count).to eq(1) + expect(user_stat.reload.post_count).to eq(0) expect(whisper).to be_present expect(whisper.post_type).to eq(Post.types[:whisper]) @@ -413,7 +413,7 @@ describe PostCreator do expect(whisper_reply).to be_present expect(whisper_reply.post_type).to eq(Post.types[:whisper]) - expect(user_stat.reload.post_count).to eq(1) + expect(user_stat.reload.post_count).to eq(0) # date is not precise enough in db whisper_reply.reload diff --git a/spec/components/post_destroyer_spec.rb b/spec/components/post_destroyer_spec.rb index 9e168924c2..c4430c8d4f 100644 --- a/spec/components/post_destroyer_spec.rb +++ b/spec/components/post_destroyer_spec.rb @@ -167,26 +167,45 @@ describe PostDestroyer do before do post @user = post.user + @reply = create_post(topic: post.topic, user: @user) expect(@user.user_stat.post_count).to eq(1) end context "recovered by user" do it "should increment the user's post count" do - PostDestroyer.new(@user, post).destroy + PostDestroyer.new(@user, @reply).destroy + expect(@user.user_stat.topic_count).to eq(1) expect(@user.user_stat.post_count).to eq(1) - PostDestroyer.new(@user, post.reload).recover + PostDestroyer.new(@user, @reply.reload).recover + expect(@user.user_stat.topic_count).to eq(1) expect(@user.reload.user_stat.post_count).to eq(1) + + expect(UserAction.where(target_topic_id: post.topic_id, action_type: UserAction::NEW_TOPIC).count).to eq(1) + expect(UserAction.where(target_topic_id: post.topic_id, action_type: UserAction::REPLY).count).to eq(1) end end context "recovered by admin" do it "should increment the user's post count" do + PostDestroyer.new(moderator, @reply).destroy + expect(@user.reload.user_stat.topic_count).to eq(1) + expect(@user.user_stat.post_count).to eq(0) + + PostDestroyer.new(admin, @reply).recover + expect(@user.reload.user_stat.topic_count).to eq(1) + expect(@user.user_stat.post_count).to eq(1) + PostDestroyer.new(moderator, post).destroy + expect(@user.reload.user_stat.topic_count).to eq(0) expect(@user.user_stat.post_count).to eq(0) PostDestroyer.new(admin, post).recover - expect(@user.reload.user_stat.post_count).to eq(1) + expect(@user.reload.user_stat.topic_count).to eq(1) + expect(@user.user_stat.post_count).to eq(1) + + expect(UserAction.where(target_topic_id: post.topic_id, action_type: UserAction::NEW_TOPIC).count).to eq(1) + expect(UserAction.where(target_topic_id: post.topic_id, action_type: UserAction::REPLY).count).to eq(1) end end end @@ -218,7 +237,8 @@ describe PostDestroyer do expect(post2.raw).to eq(I18n.t('js.post.deleted_by_author', count: 24)) expect(post2.version).to eq(2) expect(called).to eq(1) - expect(user_stat.reload.post_count).to eq(1) + expect(user_stat.reload.post_count).to eq(0) + expect(user_stat.reload.topic_count).to eq(1) called = 0 topic_recovered = -> (topic, user) do @@ -236,7 +256,8 @@ describe PostDestroyer do expect(post2.user_deleted).to eq(false) expect(post2.cooked).to eq(@orig) expect(called).to eq(1) - expect(user_stat.reload.post_count).to eq(1) + expect(user_stat.reload.post_count).to eq(0) + expect(user_stat.reload.topic_count).to eq(1) ensure DiscourseEvent.off(:topic_destroyed, &topic_destroyed) DiscourseEvent.off(:topic_recovered, &topic_recovered) @@ -251,7 +272,7 @@ describe PostDestroyer do reply = create_post(topic_id: post.topic_id, user: user2) reply2 = create_post(topic_id: post.topic_id, user: user1) expect(user1.user_stat.topic_count).to eq(1) - expect(user1.user_stat.post_count).to eq(2) + expect(user1.user_stat.post_count).to eq(1) expect(user2.user_stat.topic_count).to eq(0) expect(user2.user_stat.post_count).to eq(1) PostDestroyer.new(Fabricate(:admin), post).destroy @@ -279,14 +300,15 @@ describe PostDestroyer do context "as a moderator" do it "deletes the post" do author = post.user + reply = create_post(topic_id: post.topic_id, user: author) post_count = author.post_count history_count = UserHistory.count - PostDestroyer.new(moderator, post).destroy + PostDestroyer.new(moderator, reply).destroy - expect(post.deleted_at).to be_present - expect(post.deleted_by).to eq(moderator) + expect(reply.deleted_at).to be_present + expect(reply.deleted_by).to eq(moderator) author.reload expect(author.post_count).to eq(post_count - 1) @@ -301,12 +323,23 @@ describe PostDestroyer do expect(post.deleted_by).to eq(admin) end - it "updates the user's post_count" do + it "updates the user's topic_count for first post" do author = post.user expect { PostDestroyer.new(admin, post).destroy author.reload + }.to change { author.topic_count }.by(-1) + expect(author.user_stat.post_count).to eq(0) + end + + it "updates the user's post_count for reply" do + author = post.user + reply = create_post(topic: post.topic, user: author) + expect { + PostDestroyer.new(admin, reply).destroy + author.reload }.to change { author.post_count }.by(-1) + expect(author.user_stat.topic_count).to eq(1) end it "doesn't count whispers" do diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 8ade5e48a9..bf5b32659a 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -2148,7 +2148,7 @@ describe UsersController do json = JSON.parse(response.body) expect(json["user_summary"]["topic_count"]).to eq(1) - expect(json["user_summary"]["post_count"]).to eq(1) + expect(json["user_summary"]["post_count"]).to eq(0) end end From ec3d80049226d208f661baa752916753a4cf7f5b Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 10 Nov 2017 12:30:20 -0500 Subject: [PATCH 150/445] add option to force DirectoryItem.refresh_period! to run even if user directory is disabled --- app/models/directory_item.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/directory_item.rb b/app/models/directory_item.rb index 4014f4d9c4..ad82c2a6ed 100644 --- a/app/models/directory_item.rb +++ b/app/models/directory_item.rb @@ -25,10 +25,10 @@ class DirectoryItem < ActiveRecord::Base period_types.each_key { |p| refresh_period!(p) } end - def self.refresh_period!(period_type) + def self.refresh_period!(period_type, force: false) # Don't calculate it if the user directory is disabled - return unless SiteSetting.enable_user_directory? + return unless SiteSetting.enable_user_directory? || force since = case period_type From 1f14350220b4df9ee0bdc3c6bf1e7794b7c494fc Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 10 Nov 2017 12:18:08 -0500 Subject: [PATCH 151/445] Rename "Blocked" to "Silenced" --- .../admin/controllers/admin-dashboard.js.es6 | 2 +- .../admin/controllers/admin-user-index.js.es6 | 4 +- .../admin/models/admin-user.js.es6 | 26 ++--- .../javascripts/admin/templates/dashboard.hbs | 4 +- .../admin/templates/user-index.hbs | 18 +-- .../admin/templates/users-list.hbs | 2 +- .../templates/components/queued-post.hbs | 4 +- .../admin/email_templates_controller.rb | 4 +- app/controllers/admin/users_controller.rb | 16 +-- app/jobs/regular/export_csv_file.rb | 4 +- .../notify_mailing_list_subscribers.rb | 2 +- .../scheduled/grant_anniversary_badges.rb | 2 +- app/models/admin_dashboard_data.rb | 2 +- app/models/directory_item.rb | 2 +- app/models/queued_post.rb | 2 +- app/models/user.rb | 8 +- app/models/user_history.rb | 8 +- app/serializers/admin_user_list_serializer.rb | 2 +- .../{auto_block.rb => auto_silence.rb} | 36 +++--- app/services/spam_rules_enforcer.rb | 2 +- app/services/staff_action_logger.rb | 8 +- .../{user_blocker.rb => user_silencer.rb} | 28 ++--- config/locales/client.en.yml | 34 +++--- config/locales/server.en.yml | 56 ++++----- config/routes.rb | 4 +- config/site_settings.yml | 16 +-- db/fixtures/009_users.rb | 11 ++ .../20171110174413_rename_blocked_silence.rb | 32 ++++++ lib/admin_user_index_query.rb | 2 +- lib/badge_queries.rb | 4 +- lib/email/processor.rb | 2 +- lib/email/receiver.rb | 4 +- lib/guardian.rb | 12 +- lib/guardian/post_guardian.rb | 2 +- lib/guardian/user_guardian.rb | 4 +- lib/new_post_manager.rb | 18 +-- .../components/admin_user_index_query_spec.rb | 8 +- spec/components/email/receiver_spec.rb | 6 +- spec/components/guardian_spec.rb | 16 +-- .../admin/users_controller_spec.rb | 26 ++--- spec/controllers/posts_controller_spec.rb | 12 +- spec/fixtures/emails/blocked_sender.eml | 9 -- spec/integration/spam_rules_spec.rb | 28 ++--- spec/jobs/grant_anniversary_badges_spec.rb | 4 +- .../notify_mailing_list_subscribers_spec.rb | 4 +- spec/models/topic_spec.rb | 2 +- ...uto_block_spec.rb => auto_silence_spec.rb} | 106 +++++++++--------- spec/services/group_message_spec.rb | 16 +-- spec/services/spam_rules_enforcer_spec.rb | 4 +- ..._blocker_spec.rb => user_silencer_spec.rb} | 66 +++++------ .../acceptance/queued-posts-test.js.es6 | 4 +- 51 files changed, 366 insertions(+), 332 deletions(-) rename app/services/spam_rule/{auto_block.rb => auto_silence.rb} (67%) rename app/services/{user_blocker.rb => user_silencer.rb} (55%) create mode 100644 db/migrate/20171110174413_rename_blocked_silence.rb delete mode 100644 spec/fixtures/emails/blocked_sender.eml rename spec/services/{auto_block_spec.rb => auto_silence_spec.rb} (75%) rename spec/services/{user_blocker_spec.rb => user_silencer_spec.rb} (61%) diff --git a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 index 2e39c15880..d06b8dc06e 100644 --- a/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-dashboard.js.es6 @@ -7,7 +7,7 @@ import computed from 'ember-addons/ember-computed-decorators'; const PROBLEMS_CHECK_MINUTES = 1; -const ATTRIBUTES = [ 'disk_space','admins', 'moderators', 'blocked', 'suspended', 'top_traffic_sources', +const ATTRIBUTES = [ 'disk_space','admins', 'moderators', 'silenced', 'suspended', 'top_traffic_sources', 'top_referred_topics', 'updated_at']; const REPORTS = [ 'global_reports', 'page_view_reports', 'private_message_reports', 'http_reports', diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 index 69c2bca9b8..0d06a8d0ad 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -58,8 +58,8 @@ export default Ember.Controller.extend(CanCheckEmails, { saveTrustLevel() { return this.get("model").saveTrustLevel(); }, restoreTrustLevel() { return this.get("model").restoreTrustLevel(); }, lockTrustLevel(locked) { return this.get("model").lockTrustLevel(locked); }, - unblock() { return this.get("model").unblock(); }, - block() { return this.get("model").block(); }, + unsilence() { return this.get("model").unsilence(); }, + silence() { return this.get("model").silence(); }, deleteAllPosts() { return this.get("model").deleteAllPosts(); }, anonymize() { return this.get('model').anonymize(); }, destroy() { return this.get('model').destroy(); }, diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index 893631b785..a162c0f319 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -299,32 +299,32 @@ const AdminUser = Discourse.User.extend({ }); }, - unblock() { - this.set('blockingUser', true); - return ajax('/admin/users/' + this.id + '/unblock', { + unsilence() { + this.set('silencingUser', true); + return ajax('/admin/users/' + this.id + '/unsilence', { type: 'PUT' }).then(function() { window.location.reload(); }).catch(function(e) { - var error = I18n.t('admin.user.unblock_failed', { error: "http: " + e.status + " - " + e.body }); + var error = I18n.t('admin.user.unsilence_failed', { error: "http: " + e.status + " - " + e.body }); bootbox.alert(error); }); }, - block() { + silence() { const user = this, - message = I18n.t("admin.user.block_confirm"); + message = I18n.t("admin.user.silence_confirm"); - const performBlock = function() { - user.set('blockingUser', true); - return ajax('/admin/users/' + user.id + '/block', { + const performSilence = function() { + user.set('silencingUser', true); + return ajax('/admin/users/' + user.id + '/silence', { type: 'PUT' }).then(function() { window.location.reload(); }).catch(function(e) { - var error = I18n.t('admin.user.block_failed', { error: "http: " + e.status + " - " + e.body }); + var error = I18n.t('admin.user.silence_failed', { error: "http: " + e.status + " - " + e.body }); bootbox.alert(error); - user.set('blockingUser', false); + user.set('silencingUser', false); }); }; @@ -333,9 +333,9 @@ const AdminUser = Discourse.User.extend({ "class": "cancel", "link": true }, { - "label": `${iconHTML('exclamation-triangle')} ` + I18n.t('admin.user.block_accept'), + "label": `${iconHTML('exclamation-triangle')} ` + I18n.t('admin.user.silence_accept'), "class": "btn btn-danger", - "callback": function() { performBlock(); } + "callback": function() { performSilence(); } }]; bootbox.dialog(message, buttons, { "classes": "delete-user-modal" }); diff --git a/app/assets/javascripts/admin/templates/dashboard.hbs b/app/assets/javascripts/admin/templates/dashboard.hbs index b8c0297d48..6b4dc88584 100644 --- a/app/assets/javascripts/admin/templates/dashboard.hbs +++ b/app/assets/javascripts/admin/templates/dashboard.hbs @@ -37,8 +37,8 @@ {{d-icon "shield"}} {{i18n 'admin.dashboard.moderators'}} {{#link-to 'adminUsersList.show' 'moderators'}}{{moderators}}{{/link-to}} - {{d-icon "ban"}} {{i18n 'admin.dashboard.blocked'}} - {{#link-to 'adminUsersList.show' 'blocked'}}{{blocked}}{{/link-to}} + {{d-icon "ban"}} {{i18n 'admin.dashboard.silenced'}} + {{#link-to 'adminUsersList.show' 'silenced'}}{{silenced}}{{/link-to}}
    diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index 3e6e7d613a..9eea948704 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -347,17 +347,17 @@
    {{/if}} -
    -
    {{i18n 'admin.user.blocked'}}
    -
    {{i18n-yes-no model.blocked}}
    +
    +
    {{i18n 'admin.user.silenced'}}
    +
    {{i18n-yes-no model.silenced}}
    - {{#conditional-loading-spinner size="small" condition=model.blockingUser}} - {{#if model.blocked}} - {{d-button action="unblock" icon="thumbs-o-up" label="admin.user.unblock"}} - {{i18n 'admin.user.block_explanation'}} + {{#conditional-loading-spinner size="small" condition=model.silencingUser}} + {{#if model.silenced}} + {{d-button action="unsilence" icon="thumbs-o-up" label="admin.user.unsilence"}} + {{i18n 'admin.user.silence_explanation'}} {{else}} - {{d-button action="block" icon="ban" label="admin.user.block"}} - {{i18n 'admin.user.block_explanation'}} + {{d-button action="silence" icon="ban" label="admin.user.silence"}} + {{i18n 'admin.user.silence_explanation'}} {{/if}} {{/conditional-loading-spinner}}
    diff --git a/app/assets/javascripts/admin/templates/users-list.hbs b/app/assets/javascripts/admin/templates/users-list.hbs index d37a9ae267..398732685b 100644 --- a/app/assets/javascripts/admin/templates/users-list.hbs +++ b/app/assets/javascripts/admin/templates/users-list.hbs @@ -8,7 +8,7 @@ {{/if}} {{nav-item route='adminUsersList.show' routeParam='staff' label='admin.users.nav.staff'}} {{nav-item route='adminUsersList.show' routeParam='suspended' label='admin.users.nav.suspended'}} - {{nav-item route='adminUsersList.show' routeParam='blocked' label='admin.users.nav.blocked'}} + {{nav-item route='adminUsersList.show' routeParam='silenced' label='admin.users.nav.silenced'}} {{nav-item route='adminUsersList.show' routeParam='suspect' label='admin.users.nav.suspect'}}
    diff --git a/app/assets/javascripts/discourse/templates/components/queued-post.hbs b/app/assets/javascripts/discourse/templates/components/queued-post.hbs index e0bbe42ac7..de2709ca4a 100644 --- a/app/assets/javascripts/discourse/templates/components/queued-post.hbs +++ b/app/assets/javascripts/discourse/templates/components/queued-post.hbs @@ -10,8 +10,8 @@ {{#user-link user=post.user}} {{post.user.username}} {{/user-link}} - {{#if post.user.blocked}} - {{d-icon "ban" title="user.blocked_tooltip"}} + {{#if post.user.silenced}} + {{d-icon "ban" title="user.silenced_tooltip"}} {{/if}}
    diff --git a/app/controllers/admin/email_templates_controller.rb b/app/controllers/admin/email_templates_controller.rb index a29aa0a5ed..3d83a71cd7 100644 --- a/app/controllers/admin/email_templates_controller.rb +++ b/app/controllers/admin/email_templates_controller.rb @@ -5,7 +5,7 @@ class Admin::EmailTemplatesController < Admin::AdminController "custom_invite_mailer", "custom_invite_forum_mailer", "new_version_mailer", "new_version_mailer_with_notes", "queued_posts_reminder", "system_messages.backup_failed", "system_messages.backup_succeeded", - "system_messages.blocked_by_staff", "system_messages.bulk_invite_failed", + "system_messages.silenced_by_staff", "system_messages.bulk_invite_failed", "system_messages.bulk_invite_succeeded", "system_messages.csv_export_failed", "system_messages.csv_export_succeeded", "system_messages.download_remote_images_disabled", "system_messages.email_error_notification", "system_messages.email_reject_auto_generated", @@ -19,7 +19,7 @@ class Admin::EmailTemplatesController < Admin::AdminController "system_messages.pending_users_reminder", "system_messages.post_hidden", "system_messages.restore_failed", "system_messages.restore_succeeded", "system_messages.spam_post_blocked", "system_messages.too_many_spam_flags", - "system_messages.unblocked", "system_messages.user_automatically_blocked", + "system_messages.unsilenced", "system_messages.user_automatically_silenced", "system_messages.welcome_invite", "system_messages.welcome_user", "test_mailer", "user_notifications.account_created", "user_notifications.admin_login", "user_notifications.confirm_new_email", "user_notifications.confirm_old_email", diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 06b3db551f..3228bb4e3a 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -15,8 +15,8 @@ class Admin::UsersController < Admin::AdminController :approve, :activate, :deactivate, - :block, - :unblock, + :silence, + :unsilence, :trust_level, :trust_level_lock, :add_group, @@ -272,15 +272,15 @@ class Admin::UsersController < Admin::AdminController render body: nil end - def block - guardian.ensure_can_block_user! @user - UserBlocker.block(@user, current_user, keep_posts: true) + def silence + guardian.ensure_can_silence_user! @user + UserSilencer.silence(@user, current_user, keep_posts: true) render body: nil end - def unblock - guardian.ensure_can_unblock_user! @user - UserBlocker.unblock(@user, current_user) + def unsilence + guardian.ensure_can_unsilence_user! @user + UserSilencer.unsilence(@user, current_user) render body: nil end diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb index ad2ac01b25..9354f4288d 100644 --- a/app/jobs/regular/export_csv_file.rb +++ b/app/jobs/regular/export_csv_file.rb @@ -10,7 +10,7 @@ module Jobs HEADER_ATTRS_FOR ||= HashWithIndifferentAccess.new( user_archive: ['topic_title', 'category', 'sub_category', 'is_pm', 'post', 'like_count', 'reply_count', 'url', 'created_at'], - user_list: ['id', 'name', 'username', 'email', 'title', 'created_at', 'last_seen_at', 'last_posted_at', 'last_emailed_at', 'trust_level', 'approved', 'suspended_at', 'suspended_till', 'blocked', 'active', 'admin', 'moderator', 'ip_address', 'staged'], + user_list: ['id', 'name', 'username', 'email', 'title', 'created_at', 'last_seen_at', 'last_posted_at', 'last_emailed_at', 'trust_level', 'approved', 'suspended_at', 'suspended_till', 'silenced', 'active', 'admin', 'moderator', 'ip_address', 'staged'], user_stats: ['topics_entered', 'posts_read_count', 'time_read', 'topic_count', 'post_count', 'likes_given', 'likes_received'], user_profile: ['location', 'website', 'views'], user_sso: ['external_id', 'external_email', 'external_username', 'external_name', 'external_avatar_url'], @@ -181,7 +181,7 @@ module Jobs def get_base_user_array(user) user_array = [] - user_array.push(user.id, escape_comma(user.name), user.username, user.email, escape_comma(user.title), user.created_at, user.last_seen_at, user.last_posted_at, user.last_emailed_at, user.trust_level, user.approved, user.suspended_at, user.suspended_till, user.blocked, user.active, user.admin, user.moderator, user.ip_address, user.staged, user.user_stat.topics_entered, user.user_stat.posts_read_count, user.user_stat.time_read, user.user_stat.topic_count, user.user_stat.post_count, user.user_stat.likes_given, user.user_stat.likes_received, escape_comma(user.user_profile.location), user.user_profile.website, user.user_profile.views) + user_array.push(user.id, escape_comma(user.name), user.username, user.email, escape_comma(user.title), user.created_at, user.last_seen_at, user.last_posted_at, user.last_emailed_at, user.trust_level, user.approved, user.suspended_at, user.suspended_till, user.silenced, user.active, user.admin, user.moderator, user.ip_address, user.staged, user.user_stat.topics_entered, user.user_stat.posts_read_count, user.user_stat.time_read, user.user_stat.topic_count, user.user_stat.post_count, user.user_stat.likes_given, user.user_stat.likes_received, escape_comma(user.user_profile.location), user.user_profile.website, user.user_profile.views) end def add_single_sign_on(user, user_info_array) diff --git a/app/jobs/regular/notify_mailing_list_subscribers.rb b/app/jobs/regular/notify_mailing_list_subscribers.rb index 015cb65fdc..055d7a5c61 100644 --- a/app/jobs/regular/notify_mailing_list_subscribers.rb +++ b/app/jobs/regular/notify_mailing_list_subscribers.rb @@ -15,7 +15,7 @@ module Jobs return if !post || post.trashed? || post.user_deleted? || !post.topic users = - User.activated.not_blocked.not_suspended.real + User.activated.not_silenced.not_suspended.real .joins(:user_option) .where('user_options.mailing_list_mode AND user_options.mailing_list_mode_frequency > 0') .where('NOT EXISTS ( diff --git a/app/jobs/scheduled/grant_anniversary_badges.rb b/app/jobs/scheduled/grant_anniversary_badges.rb index 04a38a9063..7a4da06d30 100644 --- a/app/jobs/scheduled/grant_anniversary_badges.rb +++ b/app/jobs/scheduled/grant_anniversary_badges.rb @@ -22,7 +22,7 @@ module Jobs ub.badge_id = #{Badge::Anniversary} AND ub.granted_at BETWEEN '#{fmt_start_date}' AND '#{fmt_end_date}' WHERE u.active AND - NOT u.blocked AND + NOT u.silenced AND NOT p.hidden AND p.deleted_at IS NULL AND t.visible AND diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index 20550a9fd0..57a4394c27 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -147,7 +147,7 @@ class AdminDashboardData admins: User.admins.count, moderators: User.moderators.count, suspended: User.suspended.count, - blocked: User.blocked.count, + silenced: User.silenced.count, top_referrers: IncomingLinksReport.find('top_referrers').as_json, top_traffic_sources: IncomingLinksReport.find('top_traffic_sources').as_json, top_referred_topics: IncomingLinksReport.find('top_referred_topics').as_json, diff --git a/app/models/directory_item.rb b/app/models/directory_item.rb index ad82c2a6ed..584ce6a709 100644 --- a/app/models/directory_item.rb +++ b/app/models/directory_item.rb @@ -82,7 +82,7 @@ class DirectoryItem < ActiveRecord::Base LEFT OUTER JOIN posts AS p ON ua.target_post_id = p.id LEFT OUTER JOIN categories AS c ON t.category_id = c.id WHERE u.active - AND NOT u.blocked + AND NOT u.silenced AND t.deleted_at IS NULL AND COALESCE(t.visible, true) AND p.deleted_at IS NULL diff --git a/app/models/queued_post.rb b/app/models/queued_post.rb index 86177137d7..dd6a3773f9 100644 --- a/app/models/queued_post.rb +++ b/app/models/queued_post.rb @@ -66,7 +66,7 @@ class QueuedPost < ActiveRecord::Base QueuedPost.transaction do change_to!(:approved, approved_by) - UserBlocker.unblock(user, approved_by) if user.blocked? + UserSilencer.unsilence(user, approved_by) if user.silenced? created_post = creator.create diff --git a/app/models/user.rb b/app/models/user.rb index 881d162ec4..7e3fe29d1e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -146,9 +146,9 @@ class User < ActiveRecord::Base # TODO-PERF: There is no indexes on any of these # and NotifyMailingListSubscribers does a select-all-and-loop - # may want to create an index on (active, blocked, suspended_till)? - scope :blocked, -> { where(blocked: true) } - scope :not_blocked, -> { where(blocked: false) } + # may want to create an index on (active, silence, suspended_till)? + scope :silenced, -> { where(silenced: true) } + scope :not_silenced, -> { where(silenced: false) } scope :suspended, -> { where('suspended_till IS NOT NULL AND suspended_till > ?', Time.zone.now) } scope :not_suspended, -> { where('suspended_till IS NULL OR suspended_till <= ?', Time.zone.now) } scope :activated, -> { where(active: true) } @@ -1141,7 +1141,7 @@ end # flag_level :integer default(0), not null # ip_address :inet # moderator :boolean default(FALSE) -# blocked :boolean default(FALSE) +# silenced :boolean default(FALSE) # title :string # uploaded_avatar_id :integer # locale :string(10) diff --git a/app/models/user_history.rb b/app/models/user_history.rb index c166587468..7c24db82c1 100644 --- a/app/models/user_history.rb +++ b/app/models/user_history.rb @@ -45,8 +45,8 @@ class UserHistory < ActiveRecord::Base delete_category: 27, create_category: 28, change_site_text: 29, - block_user: 30, - unblock_user: 31, + silence_user: 30, + unsilence_user: 31, grant_admin: 32, revoke_admin: 33, grant_moderation: 34, @@ -90,8 +90,8 @@ class UserHistory < ActiveRecord::Base :change_category_settings, :delete_category, :create_category, - :block_user, - :unblock_user, + :silence_user, + :unsilence_user, :grant_admin, :revoke_admin, :grant_moderation, diff --git a/app/serializers/admin_user_list_serializer.rb b/app/serializers/admin_user_list_serializer.rb index b4f4d60037..22d2e07879 100644 --- a/app/serializers/admin_user_list_serializer.rb +++ b/app/serializers/admin_user_list_serializer.rb @@ -22,7 +22,7 @@ class AdminUserListSerializer < BasicUserSerializer :suspended_at, :suspended_till, :suspended, - :blocked, + :silenced, :time_read, :staged diff --git a/app/services/spam_rule/auto_block.rb b/app/services/spam_rule/auto_silence.rb similarity index 67% rename from app/services/spam_rule/auto_block.rb rename to app/services/spam_rule/auto_silence.rb index fd2cb3357f..ac5173ef2c 100644 --- a/app/services/spam_rule/auto_block.rb +++ b/app/services/spam_rule/auto_silence.rb @@ -1,37 +1,37 @@ -class SpamRule::AutoBlock +class SpamRule::AutoSilence def initialize(user) @user = user end - def self.block?(user) - self.new(user).block? + def self.silence?(user) + self.new(user).silence? end def self.punish!(user) - self.new(user).block_user + self.new(user).silence_user end def perform - block_user if block? + silence_user if silence? end - def block? - return true if @user.blocked? + def silence? + return true if @user.silenced? return false if @user.staged? return false if @user.has_trust_level?(TrustLevel[1]) - if SiteSetting.num_spam_flags_to_block_new_user > (0) && - SiteSetting.num_users_to_block_new_user > (0) && - num_spam_flags_against_user >= (SiteSetting.num_spam_flags_to_block_new_user) && - num_users_who_flagged_spam_against_user >= (SiteSetting.num_users_to_block_new_user) + if SiteSetting.num_spam_flags_to_silence_new_user > (0) && + SiteSetting.num_users_to_silence_new_user > (0) && + num_spam_flags_against_user >= (SiteSetting.num_spam_flags_to_silence_new_user) && + num_users_who_flagged_spam_against_user >= (SiteSetting.num_users_to_silence_new_user) return true end - if SiteSetting.num_tl3_flags_to_block_new_user > (0) && - SiteSetting.num_tl3_users_to_block_new_user > (0) && - num_tl3_flags_against_user >= (SiteSetting.num_tl3_flags_to_block_new_user) && - num_tl3_users_who_flagged >= (SiteSetting.num_tl3_users_to_block_new_user) + if SiteSetting.num_tl3_flags_to_silence_new_user > (0) && + SiteSetting.num_tl3_users_to_silence_new_user > (0) && + num_tl3_flags_against_user >= (SiteSetting.num_tl3_flags_to_silence_new_user) && + num_tl3_users_who_flagged >= (SiteSetting.num_tl3_users_to_silence_new_user) return true end @@ -70,10 +70,10 @@ class SpamRule::AutoBlock .pluck(:id) end - def block_user + def silence_user Post.transaction do - if UserBlocker.block(@user, Discourse.system_user, message: :too_many_spam_flags) && SiteSetting.notify_mods_when_user_blocked - GroupMessage.create(Group[:moderators].name, :user_automatically_blocked, user: @user, limit_once_per: false) + if UserSilencer.silence(@user, Discourse.system_user, message: :too_many_spam_flags) && SiteSetting.notify_mods_when_user_silenced + GroupMessage.create(Group[:moderators].name, :user_automatically_silenced, user: @user, limit_once_per: false) end end end diff --git a/app/services/spam_rules_enforcer.rb b/app/services/spam_rules_enforcer.rb index f306cfdc57..ecfc2a1f6e 100644 --- a/app/services/spam_rules_enforcer.rb +++ b/app/services/spam_rules_enforcer.rb @@ -12,7 +12,7 @@ class SpamRulesEnforcer end def enforce! - SpamRule::AutoBlock.new(@user).perform if @user + SpamRule::AutoSilence.new(@user).perform if @user SpamRule::FlagSockpuppets.new(@post).perform if @post true end diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index 1a80f93250..7232490b3c 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -273,15 +273,15 @@ class StaffActionLogger context: category.url)) end - def log_block_user(user, opts = {}) + def log_silence_user(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create(params(opts).merge(action: UserHistory.actions[:block_user], + UserHistory.create(params(opts).merge(action: UserHistory.actions[:silence_user], target_user_id: user.id)) end - def log_unblock_user(user, opts = {}) + def log_unsilence_user(user, opts = {}) raise Discourse::InvalidParameters.new(:user) unless user - UserHistory.create(params(opts).merge(action: UserHistory.actions[:unblock_user], + UserHistory.create(params(opts).merge(action: UserHistory.actions[:unsilence_user], target_user_id: user.id)) end diff --git a/app/services/user_blocker.rb b/app/services/user_silencer.rb similarity index 55% rename from app/services/user_blocker.rb rename to app/services/user_silencer.rb index 8a35c5f239..63061f8b4b 100644 --- a/app/services/user_blocker.rb +++ b/app/services/user_silencer.rb @@ -1,26 +1,26 @@ -class UserBlocker +class UserSilencer def initialize(user, by_user = nil, opts = {}) @user, @by_user, @opts = user, by_user, opts end - def self.block(user, by_user = nil, opts = {}) - UserBlocker.new(user, by_user, opts).block + def self.silence(user, by_user = nil, opts = {}) + UserSilencer.new(user, by_user, opts).silence end - def self.unblock(user, by_user = nil, opts = {}) - UserBlocker.new(user, by_user, opts).unblock + def self.unsilence(user, by_user = nil, opts = {}) + UserSilencer.new(user, by_user, opts).unsilence end - def block + def silence hide_posts unless @opts[:keep_posts] - unless @user.blocked? - @user.blocked = true + unless @user.silenced? + @user.silenced = true if @user.save - message_type = @opts[:message] || :blocked_by_staff + message_type = @opts[:message] || :silenced_by_staff post = SystemMessage.create(@user, message_type) if post && @by_user - StaffActionLogger.new(@by_user).log_block_user(@user, context: "#{message_type}: '#{post.topic&.title rescue ''}' #{@opts[:reason]}") + StaffActionLogger.new(@by_user).log_silence_user(@user, context: "#{message_type}: '#{post.topic&.title rescue ''}' #{@opts[:reason]}") end end else @@ -36,11 +36,11 @@ class UserBlocker Topic.where(id: topic_ids).update_all(visible: false) unless topic_ids.empty? end - def unblock - @user.blocked = false + def unsilence + @user.silenced = false if @user.save - SystemMessage.create(@user, :unblocked) - StaffActionLogger.new(@by_user).log_unblock_user(@user) if @by_user + SystemMessage.create(@user, :unsilenced) + StaffActionLogger.new(@by_user).log_unsilence_user(@user) if @by_user end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index bd9dbfda82..5252945492 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -605,7 +605,7 @@ en: admin: "{{user}} is an admin" moderator_tooltip: "This user is a moderator" admin_tooltip: "This user is an admin" - blocked_tooltip: "This user is blocked" + silenced_tooltip: "This user is silenced" suspended_notice: "This user is suspended until {{date}}." suspended_permanently: "This user is suspended." suspended_reason: "Reason: " @@ -2575,7 +2575,7 @@ en: no_problems: "No problems were found." moderators: 'Moderators:' admins: 'Admins:' - blocked: 'Blocked:' + silenced: 'Silenced:' suspended: 'Suspended:' private_messages_short: "Msgs" private_messages_title: "Messages" @@ -3136,8 +3136,8 @@ en: change_category_settings: "change category settings" delete_category: "delete category" create_category: "create category" - block_user: "block user" - unblock_user: "unblock user" + silence_user: "silence user" + unsilence_user: "unsilence user" grant_admin: "grant admin" revoke_admin: "revoke admin" grant_moderation: "grant moderation" @@ -3235,7 +3235,7 @@ en: pending: "Pending" staff: 'Staff' suspended: 'Suspended' - blocked: 'Blocked' + silenced: 'Silenced' suspect: 'Suspect' approved: "Approved?" approved_selected: @@ -3256,7 +3256,7 @@ en: staff: "Staff" admins: 'Admin Users' moderators: 'Moderators' - blocked: 'Blocked Users' + silenced: 'Silenced Users' suspended: 'Suspended Users' suspect: 'Suspect Users' reject_successful: @@ -3287,12 +3287,12 @@ en: # keys ending with _MF use message format, see https://meta.discourse.org/t/message-format-support-for-localization/7035 for details delete_all_posts_confirm_MF: "You are about to delete {POSTS, plural, one {1 post} other {# posts}} and {TOPICS, plural, one {1 topic} other {# topics}}. Are you sure?" - suspend: "Suspend" - unsuspend: "Unsuspend" - suspended: "Suspended?" + silence: "Silence" + unsilence: "Unsilence" + silenced: "Silenced?" moderator: "Moderator?" admin: "Admin?" - blocked: "Blocked?" + suspended: "Suspended?" staged: "Staged?" show_admin_profile: "Admin" refresh_browsers: "Force browser refresh" @@ -3308,8 +3308,8 @@ en: grant_admin_confirm: "We've sent you an email to verify the new administrator. Please open it and follow the instructions." revoke_moderation: 'Revoke Moderation' grant_moderation: 'Grant Moderation' - unblock: 'Unblock' - block: 'Block' + unsuspend: 'Unsuspend' + suspend: 'Suspend' reputation: Reputation permissions: Permissions activity: Activity @@ -3356,10 +3356,10 @@ en: activate_failed: "There was a problem activating the user." deactivate_account: "Deactivate Account" deactivate_failed: "There was a problem deactivating the user." - unblock_failed: 'There was a problem unblocking the user.' - block_failed: 'There was a problem blocking the user.' - block_confirm: 'Are you sure you want to block this user? They will not be able to create any new topics or posts.' - block_accept: 'Yes, block this user' + unsilence_failed: 'There was a problem unsilencing the user.' + silence_failed: 'There was a problem unsilencing the user.' + silence_confirm: 'Are you sure you want to silence this user? They will not be able to create any new topics or posts.' + silence_accept: 'Yes, silence this user' bounce_score: "Bounce Score" reset_bounce_score: label: "Reset" @@ -3368,7 +3368,7 @@ en: deactivate_explanation: "A deactivated user must re-validate their email." suspended_explanation: "A suspended user can't log in." - block_explanation: "A blocked user can't post or start topics." + silence_explanation: "A silenced user can't post or start topics." staged_explanation: "A staged user can only post via email in specific topics." bounce_score_explanation: none: "No bounces were received recently from that email." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e4c0c8fe1f..2809c813b3 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -70,7 +70,7 @@ en: no_body_detected_error: "Happens when we couldn't extract a body and there were no attachments." no_sender_detected_error: "Happens when we couldn't find a valid email address in the From header." inactive_user_error: "Happens when the sender is not active." - blocked_user_error: "Happens when the sender has been blocked." + silenced_user_error: "Happens when the sender has been silenced." bad_destination_address: "Happens when none of the email addresses in To/Cc/Bcc fields matched a configured incoming email address." strangers_not_allowed_error: "Happens when a user tried to create a new topic in a category they're not a member of." insufficient_trust_level_error: "Happens when a user tried to create a new topic in a category they don't have the required trust level for." @@ -1046,11 +1046,11 @@ en: tl3_additional_likes_per_day_multiplier: "Increase limit of likes per day for tl3 (regular) by multiplying with this number" tl4_additional_likes_per_day_multiplier: "Increase limit of likes per day for tl4 (leader) by multiplying with this number" - num_spam_flags_to_block_new_user: "If a new user's posts get this many spam flags from num_users_to_block_new_user different users, hide all their posts and prevent future posting. 0 to disable." - num_users_to_block_new_user: "If a new user's posts get num_spam_flags_to_block_new_user spam flags from this many different users, hide all their posts and prevent future posting. 0 to disable." - num_tl3_flags_to_block_new_user: "If a new user's posts get this many flags from num_tl3_users_to_block_new_user different trust level 3 users, hide all their posts and prevent future posting. 0 to disable." - num_tl3_users_to_block_new_user: "If a new user's posts get num_tl3_flags_to_block_new_user flags from this many different trust level 3 users, hide all their posts and prevent future posting. 0 to disable." - notify_mods_when_user_blocked: "If a user is automatically blocked, send a message to all moderators." + num_spam_flags_to_silence_new_user: "If a new user's posts get this many spam flags from num_users_to_silence_new_user different users, hide all their posts and prevent future posting. 0 to disable." + num_users_to_silence_new_user: "If a new user's posts get num_spam_flags_to_silence_new_user spam flags from this many different users, hide all their posts and prevent future posting. 0 to disable." + num_tl3_flags_to_silence_new_user: "If a new user's posts get this many flags from num_tl3_users_to_silence_new_user different trust level 3 users, hide all their posts and prevent future posting. 0 to disable." + num_tl3_users_to_silence_new_user: "If a new user's posts get num_tl3_flags_to_silence_new_user flags from this many different trust level 3 users, hide all their posts and prevent future posting. 0 to disable." + notify_mods_when_user_silenced: "If a user is automatically silenced, send a message to all moderators." flag_sockpuppets: "If a new user replies to a topic from the same IP address as the new user who started the topic, flag both of their posts as potential spam." traditional_markdown_linebreaks: "Use traditional linebreaks in Markdown, which require two trailing spaces for a linebreak." @@ -1354,9 +1354,9 @@ en: auto_respond_to_flag_actions: "Enable automatic reply when disposing a flag." min_first_post_typing_time: "Minimum amount of time in milliseconds a user must type during first post, if threshold is not met post will automatically enter the needs approval queue. Set to 0 to disable (not recommended)" - auto_block_fast_typers_on_first_post: "Automatically block users that do not meet min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Maximum trust level to auto block fast typers" - auto_block_first_post_regex: "Case insensitive regex that if passed will cause first post by user to be blocked and sent to approval queue. Example: raging|a[bc]a , will cause all posts containing raging or aba or aca to be blocked on first. Only applies to first post." + auto_silence_fast_typers_on_first_post: "Automatically silence users that do not meet min_first_post_typing_time" + auto_silence_fast_typers_max_trust_level: "Maximum trust level to auto silence fast typers" + auto_silence_first_post_regex: "Case insensitive regex that if passed will cause first post by user to be silenced and sent to approval queue. Example: raging|a[bc]a , will cause all posts containing raging or aba or aca to be silenced on first. Only applies to first post." flags_default_topics: "Show flagged topics by default in the admin section" reply_by_email_enabled: "Enable replying to topics via email." @@ -2172,13 +2172,13 @@ en: Your account associated with this email address is not activated. Please activate your account before sending emails in. - email_reject_blocked_user: - title: "Email Reject Blocked User" - subject_template: "[%{email_prefix}] Email issue -- Blocked User" + email_reject_silenced_user: + title: "Email Reject Silenced User" + subject_template: "[%{email_prefix}] Email issue -- Silenced User" text_body_template: | We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. - Your account associated with this email address has been blocked. + Your account associated with this email address has been silenced. email_reject_reply_user_not_matching: title: "Email Reject User Not Matching" @@ -2324,7 +2324,7 @@ en: This is an automated message from %{site_name} to let you know that your posts have been temporarily hidden because they were flagged by the community. - As a precautionary measure, your new account has been blocked from creating new replies or topics until a staff member can review your account. We apologize for the inconvenience. + As a precautionary measure, your new account has been silenced from creating new replies or topics until a staff member can review your account. We apologize for the inconvenience. For additional guidance, please refer to our [community guidelines](%{base_url}/guidelines). too_many_tl3_flags: @@ -2335,11 +2335,11 @@ en: This is an automated message from %{site_name} to let you know you that your account has been placed on hold due to a large number of community flags. - As a precautionary measure, your new account has been blocked from creating new replies or topics until a staff member can review your account. We apologize for the inconvenience. + As a precautionary measure, your new account has been silenced from creating new replies or topics until a staff member can review your account. We apologize for the inconvenience. For additional guidance, please refer to our [community guidelines](%{base_url}/guidelines). - blocked_by_staff: - title: "Blocked by Staff" + silenced_by_staff: + title: "Silenced by Staff" subject_template: "Account temporarily on hold" text_body_template: | Hello, @@ -2350,32 +2350,32 @@ en: For additional guidance, refer to our [community guidelines](%{base_url}/guidelines). - user_automatically_blocked: - title: "User Automatically Blocked" - subject_template: "New user %{username} blocked by community flags" + user_automatically_silenced: + title: "User Automatically Silenced" + subject_template: "New user %{username} silenced by community flags" text_body_template: | This is an automated message. - The new user [%{username}](%{user_url}) was automatically blocked because multiple users flagged %{username}'s post(s). + The new user [%{username}](%{user_url}) was automatically silenced because multiple users flagged %{username}'s post(s). - Please [review the flags](%{base_url}/admin/flags). If %{username} was incorrectly blocked from posting, click the unblock button on [the admin page for this user](%{user_url}). + Please [review the flags](%{base_url}/admin/flags). If %{username} was incorrectly silenced from posting, click the unsilence button on [the admin page for this user](%{user_url}). This threshold can be changed via the `block_new_user` site settings. - spam_post_blocked: - title: "Spam Post Blocked" - subject_template: "New user %{username} posts blocked due to repeated links" + spam_post_silenced: + title: "Spam Post Silenced" + subject_template: "New user %{username} posts silenced due to repeated links" text_body_template: | This is an automated message. - The new user [%{username}](%{user_url}) tried to create multiple posts with links to %{domains}, but those posts were blocked to avoid spam. The user is still able to create new posts that do not link to %{domains}. + The new user [%{username}](%{user_url}) tried to create multiple posts with links to %{domains}, but those posts were silenced to avoid spam. The user is still able to create new posts that do not link to %{domains}. Please [review the user](%{user_url}). This can be modified via the `newuser_spam_host_threshold` and `white_listed_spam_host_domains` site settings. - unblocked: - title: "Unblocked" + unsilenced: + title: "Unsilenced" subject_template: "Account no longer on hold" text_body_template: | Hello, diff --git a/config/routes.rb b/config/routes.rb index bb001d109e..b9cd96d496 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -117,8 +117,8 @@ Discourse::Application.routes.draw do post "log_out", constraints: AdminConstraint.new put "activate" put "deactivate" - put "block" - put "unblock" + put "silence" + put "unsilence" put "trust_level" put "trust_level_lock" put "primary_group" diff --git a/config/site_settings.yml b/config/site_settings.yml index 175fb21ff0..142aea9470 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -968,11 +968,11 @@ spam: add_rel_nofollow_to_user_content: true flags_required_to_hide_post: 3 cooldown_minutes_after_hiding_posts: 10 - num_spam_flags_to_block_new_user: 3 - num_users_to_block_new_user: 3 - num_tl3_flags_to_block_new_user: 4 - num_tl3_users_to_block_new_user: 2 - notify_mods_when_user_blocked: false + num_spam_flags_to_silence_new_user: 3 + num_users_to_silence_new_user: 3 + num_tl3_flags_to_silence_new_user: 4 + num_tl3_users_to_silence_new_user: 2 + notify_mods_when_user_silenced: false flag_sockpuppets: false newuser_spam_host_threshold: 3 white_listed_spam_host_domains: @@ -992,9 +992,9 @@ spam: min: 1 auto_respond_to_flag_actions: true min_first_post_typing_time: 3000 - auto_block_fast_typers_on_first_post: true - auto_block_fast_typers_max_trust_level: 0 - auto_block_first_post_regex: "" + auto_silence_fast_typers_on_first_post: true + auto_silence_fast_typers_max_trust_level: 0 + auto_silence_first_post_regex: "" flags_default_topics: default: false client: true diff --git a/db/fixtures/009_users.rb b/db/fixtures/009_users.rb index f1bee9cd16..ae6588ce47 100644 --- a/db/fixtures/009_users.rb +++ b/db/fixtures/009_users.rb @@ -61,6 +61,17 @@ ColumnDropper.drop( } ) +ColumnDropper.drop( + table: 'users', + after_migration: 'RenameBlockedSilence', + columns: %w[ + blocked + ], + on_drop: ->() { + STDERR.puts 'Removing user blocked column!' + } +) + # User for the smoke tests if ENV["SMOKE"] == "1" UserEmail.seed do |ue| diff --git a/db/migrate/20171110174413_rename_blocked_silence.rb b/db/migrate/20171110174413_rename_blocked_silence.rb new file mode 100644 index 0000000000..d8c1613ff1 --- /dev/null +++ b/db/migrate/20171110174413_rename_blocked_silence.rb @@ -0,0 +1,32 @@ +class RenameBlockedSilence < ActiveRecord::Migration[5.1] + + def setting(old, new) + execute "UPDATE site_settings SET name='#{new}' where name='#{old}'" + end + + def up + add_column :users, :silenced, :boolean, default: false, null: false + execute "UPDATE users set silenced = blocked" + + setting :notify_mods_when_user_blocked, :notify_mods_when_user_silenced + setting :auto_block_fast_typers_on_first_post, :auto_silence_fast_typers_on_first_post + setting :auto_block_fast_typers_max_trust_level, :auto_silence_fast_typers_max_trust_level + setting :auto_block_first_post_regex, :auto_silence_first_post_regex + setting :num_spam_flags_to_block_new_user, :num_spam_flags_to_silence_new_user + setting :num_users_to_block_new_user, :num_users_to_silence_new_user + setting :num_tl3_flags_to_block_new_user, :num_tl3_flags_to_silence_new_user + setting :num_tl3_users_to_block_new_user, :num_tl3_users_to_silence_new_user + end + + def down + remove_column :users, :silenced + setting :notify_mods_when_user_silenced, :notify_mods_when_user_blocked + setting :auto_silence_fast_typers_on_first_post, :auto_block_fast_typers_on_first_post + setting :auto_silence_fast_typers_max_trust_level, :auto_block_fast_typers_max_trust_level + setting :auto_silence_first_post_regex, :auto_block_first_post_regex + setting :num_spam_flags_to_silence_new_user, :num_spam_flags_to_block_new_user + setting :num_users_to_silence_new_user, :num_users_to_block_new_user + setting :num_tl3_flags_to_silence_new_user, :num_tl3_flags_to_block_new_user + setting :num_tl3_users_to_silence_new_user, :num_tl3_users_to_block_new_user + end +end diff --git a/lib/admin_user_index_query.rb b/lib/admin_user_index_query.rb index 7f3c4fca62..b9d74e8a52 100644 --- a/lib/admin_user_index_query.rb +++ b/lib/admin_user_index_query.rb @@ -94,7 +94,7 @@ class AdminUserIndexQuery when 'staff' then @query.where("admin or moderator") when 'admins' then @query.where(admin: true) when 'moderators' then @query.where(moderator: true) - when 'blocked' then @query.blocked + when 'silenced' then @query.silenced when 'suspended' then @query.suspended when 'pending' then @query.not_suspended.where(approved: false, active: true) when 'suspect' then suspect_users diff --git a/lib/badge_queries.rb b/lib/badge_queries.rb index 9bba4dca8c..5878293a6d 100644 --- a/lib/badge_queries.rb +++ b/lib/badge_queries.rb @@ -141,10 +141,10 @@ SQL SELECT invited_by_id FROM invites i JOIN users u2 ON u2.id = i.user_id - WHERE i.deleted_at IS NULL AND u2.active AND u2.trust_level >= #{trust_level.to_i} AND not u2.blocked + WHERE i.deleted_at IS NULL AND u2.active AND u2.trust_level >= #{trust_level.to_i} AND not u2.silenced GROUP BY invited_by_id HAVING COUNT(*) >= #{count.to_i} - ) AND u.active AND NOT u.blocked AND u.id > 0 AND + ) AND u.active AND NOT u.silenced AND u.id > 0 AND (:backfill OR u.id IN (:user_ids) ) " end diff --git a/lib/email/processor.rb b/lib/email/processor.rb index 2e94811256..c93a2b92a1 100644 --- a/lib/email/processor.rb +++ b/lib/email/processor.rb @@ -43,7 +43,7 @@ module Email when Email::Receiver::EmailNotAllowed then :email_reject_not_allowed_email when Email::Receiver::AutoGeneratedEmailError then :email_reject_auto_generated when Email::Receiver::InactiveUserError then :email_reject_inactive_user - when Email::Receiver::BlockedUserError then :email_reject_blocked_user + when Email::Receiver::SilencedUserError then :email_reject_silenced_user when Email::Receiver::BadDestinationAddress then :email_reject_bad_destination_address when Email::Receiver::StrangersNotAllowedError then :email_reject_strangers_not_allowed when Email::Receiver::InsufficientTrustLevelError then :email_reject_insufficient_trust_level diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 094dda88cd..be5ae2f282 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -21,7 +21,7 @@ module Email class NoBodyDetectedError < ProcessingError; end class NoSenderDetectedError < ProcessingError; end class InactiveUserError < ProcessingError; end - class BlockedUserError < ProcessingError; end + class SilencedUserError < ProcessingError; end class BadDestinationAddress < ProcessingError; end class StrangersNotAllowedError < ProcessingError; end class InsufficientTrustLevelError < ProcessingError; end @@ -144,7 +144,7 @@ module Email @incoming_email.update_columns(user_id: user.id) raise InactiveUserError if !user.active && !user.staged - raise BlockedUserError if user.blocked + raise SilencedUserError if user.silenced? end def is_bounce? diff --git a/lib/guardian.rb b/lib/guardian.rb index ccf6a8b557..e36316a851 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -25,7 +25,7 @@ class Guardian def moderator?; false; end def approved?; false; end def staged?; false; end - def blocked?; false; end + def silenced?; false; end def secure_category_ids; []; end def topic_create_allowed_category_ids; []; end def has_trust_level?(level); false; end @@ -63,8 +63,8 @@ class Guardian @user.moderator? end - def is_blocked? - @user.blocked? + def is_silenced? + @user.silenced? end def is_developer? @@ -122,7 +122,7 @@ class Guardian end def can_moderate?(obj) - obj && authenticated? && !is_blocked? && (is_staff? || (obj.is_a?(Topic) && @user.has_trust_level?(TrustLevel[4]))) + obj && authenticated? && !is_silenced? && (is_staff? || (obj.is_a?(Topic) && @user.has_trust_level?(TrustLevel[4]))) end alias :can_move_posts? :can_moderate? alias :can_see_flags? :can_moderate? @@ -300,8 +300,8 @@ class Guardian (is_staff? || SiteSetting.enable_private_messages) && # Can't send PMs to suspended users (is_staff? || is_group || !target.suspended?) && - # Blocked users can only send PM to staff - (!is_blocked? || target.staff?) + # Silenced users can only send PM to staff + (!is_silenced? || target.staff?) end def cand_send_private_messages_to_email? diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index b5d4c43f67..d4ce94fdff 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -80,7 +80,7 @@ module PostGuardian # Creating Method def can_create_post?(parent) - (!SpamRule::AutoBlock.block?(@user) || (!!parent.try(:private_message?) && parent.allowed_users.include?(@user))) && ( + (!SpamRule::AutoSilence.silence?(@user) || (!!parent.try(:private_message?) && parent.allowed_users.include?(@user))) && ( !parent || !parent.category || Category.post_create_allowed(self).where(id: parent.category.id).count == 1 diff --git a/lib/guardian/user_guardian.rb b/lib/guardian/user_guardian.rb index d066459e1c..26a1056262 100644 --- a/lib/guardian/user_guardian.rb +++ b/lib/guardian/user_guardian.rb @@ -30,11 +30,11 @@ module UserGuardian is_me?(user) || is_admin? end - def can_block_user?(user) + def can_silence_user?(user) user && is_staff? && not(user.staff?) end - def can_unblock_user?(user) + def can_unsilence_user?(user) user && is_staff? end diff --git a/lib/new_post_manager.rb b/lib/new_post_manager.rb index 34919c01cb..39cf75cfaf 100644 --- a/lib/new_post_manager.rb +++ b/lib/new_post_manager.rb @@ -44,14 +44,14 @@ class NewPostManager is_first_post?(manager) && args[:typing_duration_msecs].to_i < SiteSetting.min_first_post_typing_time && - SiteSetting.auto_block_fast_typers_on_first_post && - manager.user.trust_level <= SiteSetting.auto_block_fast_typers_max_trust_level + SiteSetting.auto_silence_fast_typers_on_first_post && + manager.user.trust_level <= SiteSetting.auto_silence_fast_typers_max_trust_level end - def self.matches_auto_block_regex?(manager) + def self.matches_auto_silence_regex?(manager) args = manager.args - pattern = SiteSetting.auto_block_first_post_regex + pattern = SiteSetting.auto_silence_first_post_regex return false unless pattern.present? return false unless is_first_post?(manager) @@ -59,7 +59,7 @@ class NewPostManager begin regex = Regexp.new(pattern, Regexp::IGNORECASE) rescue => e - Rails.logger.warn "Invalid regex in auto_block_first_post_regex #{e}" + Rails.logger.warn "Invalid regex in auto_silence_first_post_regex #{e}" return false end @@ -80,7 +80,7 @@ class NewPostManager (user.trust_level < SiteSetting.approve_unless_trust_level.to_i) || (manager.args[:title].present? && user.trust_level < SiteSetting.approve_new_topics_unless_trust_level.to_i) || is_fast_typer?(manager) || - matches_auto_block_regex?(manager) || + matches_auto_silence_regex?(manager) || WordWatcher.new("#{manager.args[:title]} #{manager.args[:raw]}").requires_approval? end @@ -110,9 +110,9 @@ class NewPostManager result = manager.enqueue('default') if is_fast_typer?(manager) - UserBlocker.block(manager.user, Discourse.system_user, keep_posts: true, reason: I18n.t("user.new_user_typed_too_fast")) - elsif matches_auto_block_regex?(manager) - UserBlocker.block(manager.user, Discourse.system_user, keep_posts: true, reason: I18n.t("user.content_matches_auto_block_regex")) + UserSilencer.silence(manager.user, Discourse.system_user, keep_posts: true, reason: I18n.t("user.new_user_typed_too_fast")) + elsif matches_auto_silence_regex?(manager) + UserSilencer.silence(manager.user, Discourse.system_user, keep_posts: true, reason: I18n.t("user.content_matches_auto_silence_regex")) end result diff --git a/spec/components/admin_user_index_query_spec.rb b/spec/components/admin_user_index_query_spec.rb index 284f6389ac..de124a5be8 100644 --- a/spec/components/admin_user_index_query_spec.rb +++ b/spec/components/admin_user_index_query_spec.rb @@ -165,12 +165,12 @@ describe AdminUserIndexQuery do end - describe "with a blocked user" do + describe "with a silenced user" do - let!(:user) { Fabricate(:user, blocked: true) } + let!(:user) { Fabricate(:user, silenced: true) } - it "finds the blocked user" do - query = ::AdminUserIndexQuery.new(query: 'blocked') + it "finds the silenced user" do + query = ::AdminUserIndexQuery.new(query: 'silenced') expect(query.find_users.count).to eq(1) end diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index a11e3d9d96..51e655a6b2 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -56,9 +56,9 @@ describe Email::Receiver do expect { process(:inactive_sender) }.to raise_error(Email::Receiver::InactiveUserError) end - it "raises a BlockedUserError when the sender has been blocked" do - Fabricate(:user, email: "blocked@bar.com", blocked: true) - expect { process(:blocked_sender) }.to raise_error(Email::Receiver::BlockedUserError) + it "raises a SilencedUserError when the sender has been silenced" do + Fabricate(:user, email: "silenced@bar.com", silenced: true) + expect { process(:silenced_sender) }.to raise_error(Email::Receiver::SilencedUserError) end it "doesn't raise an InactiveUserError when the sender is staged" do diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index 692fa1ad89..277e2f408d 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -193,9 +193,9 @@ describe Guardian do end end - context "author is blocked" do + context "author is silenced" do before do - user.blocked = true + user.silenced = true user.save end @@ -852,14 +852,14 @@ describe Guardian do expect(Guardian.new(user).can_create?(Post, private_message)).to be_falsey end - it "allows new posts from blocked users included in the pm" do - user.update_attribute(:blocked, true) + it "allows new posts from silenced users included in the pm" do + user.update_attribute(:silenced, true) private_message.topic_allowed_users.create!(user_id: user.id) expect(Guardian.new(user).can_create?(Post, private_message)).to be_truthy end - it "doesn't allow new posts from blocked users not invited to the pm" do - user.update_attribute(:blocked, true) + it "doesn't allow new posts from silenced users not invited to the pm" do + user.update_attribute(:silenced, true) expect(Guardian.new(user).can_create?(Post, private_message)).to be_falsey end end @@ -1374,9 +1374,9 @@ describe Guardian do expect(Guardian.new(user).can_moderate?(nil)).to be_falsey end - context 'when user is blocked' do + context 'when user is silenced' do it 'returns false' do - user.toggle!(:blocked) + user.toggle!(:silenced) expect(Guardian.new(user).can_moderate?(post)).to be(false) expect(Guardian.new(user).can_moderate?(topic)).to be(false) end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 64dbb1060b..629f12c80a 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -583,48 +583,48 @@ describe Admin::UsersController do end end - context 'block' do + context 'silence' do before do @reg_user = Fabricate(:user) end it "raises an error when the user doesn't have permission" do - Guardian.any_instance.expects(:can_block_user?).with(@reg_user).returns(false) - UserBlocker.expects(:block).never - put :block, params: { user_id: @reg_user.id }, format: :json + Guardian.any_instance.expects(:can_silence_user?).with(@reg_user).returns(false) + UserSilencer.expects(:silence).never + put :silence, params: { user_id: @reg_user.id }, format: :json expect(response).to be_forbidden end it "returns a 403 if the user doesn't exist" do - put :block, params: { user_id: 123123 }, format: :json + put :silence, params: { user_id: 123123 }, format: :json expect(response).to be_forbidden end it "punishes the user for spamming" do - UserBlocker.expects(:block).with(@reg_user, @user, anything) - put :block, params: { user_id: @reg_user.id }, format: :json + UserSilencer.expects(:silence).with(@reg_user, @user, anything) + put :silence, params: { user_id: @reg_user.id }, format: :json end end - context 'unblock' do + context 'unsilence' do before do @reg_user = Fabricate(:user) end it "raises an error when the user doesn't have permission" do - Guardian.any_instance.expects(:can_unblock_user?).with(@reg_user).returns(false) - put :unblock, params: { user_id: @reg_user.id }, format: :json + Guardian.any_instance.expects(:can_unsilence_user?).with(@reg_user).returns(false) + put :unsilence, params: { user_id: @reg_user.id }, format: :json expect(response).to be_forbidden end it "returns a 403 if the user doesn't exist" do - put :unblock, params: { user_id: 123123 }, format: :json + put :unsilence, params: { user_id: 123123 }, format: :json expect(response).to be_forbidden end it "punishes the user for spamming" do - UserBlocker.expects(:unblock).with(@reg_user, @user, anything) - put :unblock, params: { user_id: @reg_user.id }, format: :json + UserSilencer.expects(:unsilence).with(@reg_user, @user, anything) + put :unsilence, params: { user_id: @reg_user.id }, format: :json end end diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index 5a1850792e..eda2fa09c8 100644 --- a/spec/controllers/posts_controller_spec.rb +++ b/spec/controllers/posts_controller_spec.rb @@ -695,7 +695,7 @@ describe PostsController do context "fast typing" do before do SiteSetting.min_first_post_typing_time = 3000 - SiteSetting.auto_block_fast_typers_max_trust_level = 1 + SiteSetting.auto_silence_fast_typers_max_trust_level = 1 end it 'queues the post if min_first_post_typing_time is not met' do @@ -710,7 +710,7 @@ describe PostsController do expect(parsed["action"]).to eq("enqueued") user.reload - expect(user.blocked).to eq(true) + expect(user.silenced).to eq(true) qp = QueuedPost.first @@ -718,7 +718,7 @@ describe PostsController do qp.approve!(mod) user.reload - expect(user.blocked).to eq(false) + expect(user.silenced).to eq(false) end it "doesn't enqueue replies when the topic is closed" do @@ -749,8 +749,8 @@ describe PostsController do end end - it 'blocks correctly based on auto_block_first_post_regex' do - SiteSetting.auto_block_first_post_regex = "I love candy|i eat s[1-5]" + it 'silences correctly based on auto_silence_first_post_regex' do + SiteSetting.auto_silence_first_post_regex = "I love candy|i eat s[1-5]" post :create, params: { raw: 'this is the test content', @@ -763,7 +763,7 @@ describe PostsController do expect(parsed["action"]).to eq("enqueued") user.reload - expect(user.blocked).to eq(true) + expect(user.silenced).to eq(true) end it "can send a message to a group" do diff --git a/spec/fixtures/emails/blocked_sender.eml b/spec/fixtures/emails/blocked_sender.eml deleted file mode 100644 index 19bab36237..0000000000 --- a/spec/fixtures/emails/blocked_sender.eml +++ /dev/null @@ -1,9 +0,0 @@ -Return-Path: -From: Foo Bar -Date: Fri, 15 Jan 2016 00:12:43 +0100 -Message-ID: <8@foo.bar.mail> -Mime-Version: 1.0 -Content-Type: text/plain -Content-Transfer-Encoding: 7bit - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. diff --git a/spec/integration/spam_rules_spec.rb b/spec/integration/spam_rules_spec.rb index adec648d23..1f8d98f9ea 100644 --- a/spec/integration/spam_rules_spec.rb +++ b/spec/integration/spam_rules_spec.rb @@ -4,7 +4,7 @@ require 'rails_helper' describe SpamRulesEnforcer do - describe 'auto-blocking users based on flagging' do + describe 'auto-silenceing users based on flagging' do let!(:admin) { Fabricate(:admin) } # needed to send a system message let!(:moderator) { Fabricate(:moderator) } let(:user1) { Fabricate(:user) } @@ -12,8 +12,8 @@ describe SpamRulesEnforcer do before do SiteSetting.flags_required_to_hide_post = 0 - SiteSetting.num_spam_flags_to_block_new_user = 2 - SiteSetting.num_users_to_block_new_user = 2 + SiteSetting.num_spam_flags_to_silence_new_user = 2 + SiteSetting.num_users_to_silence_new_user = 2 end context 'spammer is a new user' do @@ -37,7 +37,7 @@ describe SpamRulesEnforcer do expect(spam_post.reload).to_not be_hidden expect(spam_post2.reload).to_not be_hidden - expect(spammer.reload).to_not be_blocked + expect(spammer.reload).to_not be_silenced end end @@ -55,7 +55,7 @@ describe SpamRulesEnforcer do end it 'should hide the posts' do - expect(spammer.reload).to be_blocked + expect(spammer.reload).to be_silenced expect(spam_post.reload).to be_hidden expect(spam_post2.reload).to be_hidden expect(spammer.reload.private_topics_count).to eq(private_messages_count + 1) @@ -63,35 +63,35 @@ describe SpamRulesEnforcer do # The following cases describe when a staff user takes some action, but the user # still won't be able to make posts. - # A staff user needs to clear the blocked flag from the user record. + # A staff user needs to clear the silenced flag from the user record. context "a post's flags are cleared" do - it 'should block the spammer' do + it 'should silence the spammer' do PostAction.clear_flags!(spam_post, admin); spammer.reload - expect(spammer.reload).to be_blocked + expect(spammer.reload).to be_silenced end end context "a post is deleted" do - it 'should block the spammer' do + it 'should silence the spammer' do spam_post.trash!(moderator); spammer.reload - expect(spammer.reload).to be_blocked + expect(spammer.reload).to be_silenced end end context "spammer becomes trust level 1" do - it 'should block the spammer' do + it 'should silence the spammer' do spammer.change_trust_level!(TrustLevel[1]); spammer.reload - expect(spammer.reload).to be_blocked + expect(spammer.reload).to be_silenced end end end context 'flags_required_to_hide_post takes effect too' do - it 'should block the spammer' do + it 'should silence the spammer' do SiteSetting.flags_required_to_hide_post = 2 PostAction.act(user2, spam_post, PostActionType.types[:spam]) - expect(spammer.reload).to be_blocked + expect(spammer.reload).to be_silenced expect(Guardian.new(spammer).can_create_topic?(nil)).to be false end end diff --git a/spec/jobs/grant_anniversary_badges_spec.rb b/spec/jobs/grant_anniversary_badges_spec.rb index 2ca7348097..d1664eaa2c 100644 --- a/spec/jobs/grant_anniversary_badges_spec.rb +++ b/spec/jobs/grant_anniversary_badges_spec.rb @@ -23,8 +23,8 @@ describe Jobs::GrantAnniversaryBadges do expect(badge).to be_blank end - it "doesn't award to a blocked user" do - user = Fabricate(:user, created_at: 400.days.ago, blocked: true) + it "doesn't award to a silenced user" do + user = Fabricate(:user, created_at: 400.days.ago, silenced: true) Fabricate(:post, user: user, created_at: 1.week.ago) granter.execute({}) diff --git a/spec/jobs/notify_mailing_list_subscribers_spec.rb b/spec/jobs/notify_mailing_list_subscribers_spec.rb index fa2576be73..131dd780c2 100644 --- a/spec/jobs/notify_mailing_list_subscribers_spec.rb +++ b/spec/jobs/notify_mailing_list_subscribers_spec.rb @@ -65,8 +65,8 @@ describe Jobs::NotifyMailingListSubscribers do include_examples "no emails" end - context "to a blocked user" do - before { mailing_list_user.update(blocked: true) } + context "to a silenced user" do + before { mailing_list_user.update(silenced: true) } include_examples "no emails" end diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 23021afbb6..0e527e92eb 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -677,7 +677,7 @@ describe Topic do context "when moderator post fails to be created" do before do - user.toggle!(:blocked) + user.toggle!(:silenced) end it "should not increment moderator_posts_count" do diff --git a/spec/services/auto_block_spec.rb b/spec/services/auto_silence_spec.rb similarity index 75% rename from spec/services/auto_block_spec.rb rename to spec/services/auto_silence_spec.rb index f64d89d3f3..3ee3269e4c 100644 --- a/spec/services/auto_block_spec.rb +++ b/spec/services/auto_silence_spec.rb @@ -1,32 +1,32 @@ require 'rails_helper' -describe SpamRule::AutoBlock do +describe SpamRule::AutoSilence do before do SiteSetting.flags_required_to_hide_post = 0 # never - SiteSetting.num_spam_flags_to_block_new_user = 2 - SiteSetting.num_users_to_block_new_user = 2 + SiteSetting.num_spam_flags_to_silence_new_user = 2 + SiteSetting.num_users_to_silence_new_user = 2 end describe 'perform' do let(:post) { Fabricate.build(:post, user: Fabricate.build(:user, trust_level: TrustLevel[0])) } subject { described_class.new(post.user) } - it 'takes no action if user should not be blocked' do - subject.stubs(:block?).returns(false) - subject.expects(:block_user).never + it 'takes no action if user should not be silenced' do + subject.stubs(:silence?).returns(false) + subject.expects(:silence_user).never subject.perform end - it 'delivers punishment when user should be blocked' do - subject.stubs(:block?).returns(true) - subject.expects(:block_user) + it 'delivers punishment when user should be silenced' do + subject.stubs(:silence?).returns(true) + subject.expects(:silence_user) subject.perform end end describe 'num_spam_flags_against_user' do - before { described_class.any_instance.stubs(:block_user) } + before { described_class.any_instance.stubs(:silence_user) } let(:post) { Fabricate(:post) } let(:enforcer) { described_class.new(post.user) } subject { enforcer.num_spam_flags_against_user } @@ -53,7 +53,7 @@ describe SpamRule::AutoBlock do end describe 'num_users_who_flagged_spam_against_user' do - before { described_class.any_instance.stubs(:block_user) } + before { described_class.any_instance.stubs(:silence_user) } let(:post) { Fabricate(:post) } let(:enforcer) { described_class.new(post.user) } subject { enforcer.num_users_who_flagged_spam_against_user } @@ -124,85 +124,85 @@ describe SpamRule::AutoBlock do end end - describe 'block_user' do + describe 'silence_user' do let!(:admin) { Fabricate(:admin) } # needed for SystemMessage let(:user) { Fabricate(:user) } let!(:post) { Fabricate(:post, user: user) } subject { described_class.new(user) } before do - described_class.stubs(:block?).with { |u| u.id != user.id }.returns(false) - described_class.stubs(:block?).with { |u| u.id == user.id }.returns(true) - subject.stubs(:block?).returns(true) + described_class.stubs(:silence?).with { |u| u.id != user.id }.returns(false) + described_class.stubs(:silence?).with { |u| u.id == user.id }.returns(true) + subject.stubs(:silence?).returns(true) end - context 'user is not blocked' do + context 'user is not silenced' do before do - UserBlocker.expects(:block).with(user, Discourse.system_user, message: :too_many_spam_flags).returns(true) + UserSilencer.expects(:silence).with(user, Discourse.system_user, message: :too_many_spam_flags).returns(true) end it 'prevents the user from making new posts' do - subject.block_user + subject.silence_user expect(Guardian.new(user).can_create_post?(nil)).to be_falsey end it 'sends private message to moderators' do - SiteSetting.notify_mods_when_user_blocked = true + SiteSetting.notify_mods_when_user_silenced = true moderator = Fabricate(:moderator) GroupMessage.expects(:create).with do |group, msg_type, params| - group == (Group[:moderators].name) && msg_type == (:user_automatically_blocked) && params[:user].id == (user.id) + group == (Group[:moderators].name) && msg_type == (:user_automatically_silenced) && params[:user].id == (user.id) end - subject.block_user + subject.silence_user end - it "doesn't send a pm to moderators if notify_mods_when_user_blocked is false" do - SiteSetting.notify_mods_when_user_blocked = false + it "doesn't send a pm to moderators if notify_mods_when_user_silenced is false" do + SiteSetting.notify_mods_when_user_silenced = false GroupMessage.expects(:create).never - subject.block_user + subject.silence_user end end - context 'user is already blocked' do + context 'user is already silenced' do before do - UserBlocker.expects(:block).with(user, Discourse.system_user, message: :too_many_spam_flags).returns(false) + UserSilencer.expects(:silence).with(user, Discourse.system_user, message: :too_many_spam_flags).returns(false) end - it "doesn't send a pm to moderators if the user is already blocked" do + it "doesn't send a pm to moderators if the user is already silenced" do GroupMessage.expects(:create).never - subject.block_user + subject.silence_user end end end - describe 'block?' do + describe 'silence?' do - context 'never been blocked' do - shared_examples "can't be blocked" do + context 'never been silenced' do + shared_examples "can't be silenced" do it "returns false" do enforcer = described_class.new(user) enforcer.expects(:num_spam_flags_against_user).never enforcer.expects(:num_users_who_flagged_spam_against_user).never enforcer.expects(:num_flags_against_user).never enforcer.expects(:num_users_who_flagged).never - expect(enforcer.block?).to eq(false) + expect(enforcer.silence?).to eq(false) end end (1..4).each do |trust_level| context "user has trust level #{trust_level}" do let(:user) { Fabricate(:user, trust_level: trust_level) } - include_examples "can't be blocked" + include_examples "can't be silenced" end end context "user is an admin" do let(:user) { Fabricate(:admin) } - include_examples "can't be blocked" + include_examples "can't be silenced" end context "user is a moderator" do let(:user) { Fabricate(:moderator) } - include_examples "can't be blocked" + include_examples "can't be silenced" end end @@ -213,73 +213,73 @@ describe SpamRule::AutoBlock do it 'returns false if there are no spam flags' do subject.stubs(:num_spam_flags_against_user).returns(0) subject.stubs(:num_users_who_flagged_spam_against_user).returns(0) - expect(subject.block?).to be_falsey + expect(subject.silence?).to be_falsey end it 'returns false if there are not received enough flags' do subject.stubs(:num_spam_flags_against_user).returns(1) subject.stubs(:num_users_who_flagged_spam_against_user).returns(2) - expect(subject.block?).to be_falsey + expect(subject.silence?).to be_falsey end it 'returns false if there have not been enough users' do subject.stubs(:num_spam_flags_against_user).returns(2) subject.stubs(:num_users_who_flagged_spam_against_user).returns(1) - expect(subject.block?).to be_falsey + expect(subject.silence?).to be_falsey end - it 'returns false if num_spam_flags_to_block_new_user is 0' do - SiteSetting.num_spam_flags_to_block_new_user = 0 + it 'returns false if num_spam_flags_to_silence_new_user is 0' do + SiteSetting.num_spam_flags_to_silence_new_user = 0 subject.stubs(:num_spam_flags_against_user).returns(100) subject.stubs(:num_users_who_flagged_spam_against_user).returns(100) - expect(subject.block?).to be_falsey + expect(subject.silence?).to be_falsey end - it 'returns false if num_users_to_block_new_user is 0' do - SiteSetting.num_users_to_block_new_user = 0 + it 'returns false if num_users_to_silence_new_user is 0' do + SiteSetting.num_users_to_silence_new_user = 0 subject.stubs(:num_spam_flags_against_user).returns(100) subject.stubs(:num_users_who_flagged_spam_against_user).returns(100) - expect(subject.block?).to be_falsey + expect(subject.silence?).to be_falsey end it 'returns true when there are enough flags from enough users' do subject.stubs(:num_spam_flags_against_user).returns(2) subject.stubs(:num_users_who_flagged_spam_against_user).returns(2) - expect(subject.block?).to be_truthy + expect(subject.silence?).to be_truthy end context "all types of flags" do before do - SiteSetting.num_tl3_flags_to_block_new_user = 3 - SiteSetting.num_tl3_users_to_block_new_user = 2 + SiteSetting.num_tl3_flags_to_silence_new_user = 3 + SiteSetting.num_tl3_users_to_silence_new_user = 2 end it 'returns false if there are not enough flags' do subject.stubs(:num_tl3_flags_against_user).returns(1) subject.stubs(:num_tl3_users_who_flagged).returns(1) - expect(subject.block?).to be_falsey + expect(subject.silence?).to be_falsey end it 'returns false if enough flags but not enough users' do subject.stubs(:num_tl3_flags_against_user).returns(3) subject.stubs(:num_tl3_users_who_flagged).returns(1) - expect(subject.block?).to be_falsey + expect(subject.silence?).to be_falsey end it 'returns true if enough flags and users' do subject.stubs(:num_tl3_flags_against_user).returns(3) subject.stubs(:num_tl3_users_who_flagged).returns(2) - expect(subject.block?).to eq(true) + expect(subject.silence?).to eq(true) end end end - context "blocked, but has higher trust level now" do - let(:user) { Fabricate(:user, blocked: true, trust_level: TrustLevel[1]) } + context "silenced, but has higher trust level now" do + let(:user) { Fabricate(:user, silenced: true, trust_level: TrustLevel[1]) } subject { described_class.new(user) } it 'returns false' do - expect(subject.block?).to be_truthy + expect(subject.silence?).to be_truthy end end end diff --git a/spec/services/group_message_spec.rb b/spec/services/group_message_spec.rb index 206ea4a8e4..94957e0bc9 100644 --- a/spec/services/group_message_spec.rb +++ b/spec/services/group_message_spec.rb @@ -11,7 +11,7 @@ describe GroupMessage do Discourse.stubs(:system_user).returns(admin) end - subject(:send_group_message) { GroupMessage.create(moderators_group, :user_automatically_blocked, user: user) } + subject(:send_group_message) { GroupMessage.create(moderators_group, :user_automatically_silenced, user: user) } describe 'not sent recently' do before { GroupMessage.any_instance.stubs(:sent_recently?).returns(false) } @@ -42,7 +42,7 @@ describe GroupMessage do describe 'sent recently' do before { GroupMessage.any_instance.stubs(:sent_recently?).returns(true) } - subject { GroupMessage.create(moderators_group, :user_automatically_blocked, user: user) } + subject { GroupMessage.create(moderators_group, :user_automatically_silenced, user: user) } it { is_expected.to eq(false) } @@ -61,8 +61,8 @@ describe GroupMessage do end end - context 'user_automatically_blocked' do - subject { GroupMessage.new(moderators_group, :user_automatically_blocked, user: user).message_params } + context 'user_automatically_silenced' do + subject { GroupMessage.new(moderators_group, :user_automatically_silenced, user: user).message_params } include_examples 'common message params for group messages' end @@ -74,7 +74,7 @@ describe GroupMessage do describe 'methods that use redis' do let(:user) { Fabricate.build(:user, id: 123123) } - subject(:group_message) { GroupMessage.new(moderators_group, :user_automatically_blocked, user: user) } + subject(:group_message) { GroupMessage.new(moderators_group, :user_automatically_silenced, user: user) } before do PostCreator.stubs(:create).returns(stub_everything) group_message.stubs(:sent_recently_key).returns('the_key') @@ -92,7 +92,7 @@ describe GroupMessage do end it 'always returns false if limit_once_per is false' do - gm = GroupMessage.new(moderators_group, :user_automatically_blocked, user: user, limit_once_per: false) + gm = GroupMessage.new(moderators_group, :user_automatically_silenced, user: user, limit_once_per: false) gm.stubs(:sent_recently_key).returns('the_key') $redis.stubs(:get).with(gm.sent_recently_key).returns('1') expect(gm.sent_recently?).to be_falsey @@ -107,12 +107,12 @@ describe GroupMessage do it 'can use a given expiry time' do $redis.expects(:setex).with(anything, 30 * 60, anything).returns('OK') - GroupMessage.new(moderators_group, :user_automatically_blocked, user: user, limit_once_per: 30.minutes).remember_message_sent + GroupMessage.new(moderators_group, :user_automatically_silenced, user: user, limit_once_per: 30.minutes).remember_message_sent end it 'can be disabled' do $redis.expects(:setex).never - GroupMessage.new(moderators_group, :user_automatically_blocked, user: user, limit_once_per: false).remember_message_sent + GroupMessage.new(moderators_group, :user_automatically_silenced, user: user, limit_once_per: false).remember_message_sent end end end diff --git a/spec/services/spam_rules_enforcer_spec.rb b/spec/services/spam_rules_enforcer_spec.rb index 0bb0a8790b..12ee2e2ce2 100644 --- a/spec/services/spam_rules_enforcer_spec.rb +++ b/spec/services/spam_rules_enforcer_spec.rb @@ -19,8 +19,8 @@ describe SpamRulesEnforcer do context 'user argument' do subject(:enforce) { described_class.enforce!(Fabricate.build(:user)) } - it 'performs the AutoBlock' do - SpamRule::AutoBlock.any_instance.expects(:perform).once + it 'performs the AutoSilence' do + SpamRule::AutoSilence.any_instance.expects(:perform).once enforce end end diff --git a/spec/services/user_blocker_spec.rb b/spec/services/user_silencer_spec.rb similarity index 61% rename from spec/services/user_blocker_spec.rb rename to spec/services/user_silencer_spec.rb index 16980610a1..9b23057563 100644 --- a/spec/services/user_blocker_spec.rb +++ b/spec/services/user_silencer_spec.rb @@ -1,39 +1,39 @@ require 'rails_helper' -describe UserBlocker do +describe UserSilencer do before do SystemMessage.stubs(:create) end - describe 'block' do + describe 'silence' do let(:user) { stub_everything(save: true) } - let(:blocker) { UserBlocker.new(user) } - subject(:block_user) { blocker.block } + let(:silencer) { UserSilencer.new(user) } + subject(:silence_user) { silencer.silence } - it 'blocks the user' do + it 'silences the user' do u = Fabricate(:user) - expect { UserBlocker.block(u) }.to change { u.reload.blocked? } + expect { UserSilencer.silence(u) }.to change { u.reload.silenced? } end it 'hides posts' do - blocker.expects(:hide_posts) - block_user + silencer.expects(:hide_posts) + silence_user end context 'given a staff user argument' do - it 'sends the correct message to the blocked user' do + it 'sends the correct message to the silenced user' do SystemMessage.unstub(:create) - SystemMessage.expects(:create).with(user, :blocked_by_staff).returns(true) - UserBlocker.block(user, Fabricate.build(:admin)) + SystemMessage.expects(:create).with(user, :silenced_by_staff).returns(true) + UserSilencer.silence(user, Fabricate.build(:admin)) end end context 'not given a staff user argument' do it 'sends a default message to the user' do SystemMessage.unstub(:create) - SystemMessage.expects(:create).with(user, :blocked_by_staff).returns(true) - UserBlocker.block(user, Fabricate.build(:admin)) + SystemMessage.expects(:create).with(user, :silenced_by_staff).returns(true) + UserSilencer.silence(user, Fabricate.build(:admin)) end end @@ -41,7 +41,7 @@ describe UserBlocker do it 'sends that message to the user' do SystemMessage.unstub(:create) SystemMessage.expects(:create).with(user, :the_custom_message).returns(true) - UserBlocker.block(user, Fabricate.build(:admin), message: :the_custom_message) + UserSilencer.silence(user, Fabricate.build(:admin), message: :the_custom_message) end end @@ -49,50 +49,50 @@ describe UserBlocker do user.stubs(:save).returns(false) SystemMessage.unstub(:create) SystemMessage.expects(:create).never - block_user + silence_user end - it "doesn't send a pm if the user is already blocked" do - user.stubs(:blocked?).returns(true) + it "doesn't send a pm if the user is already silenced" do + user.stubs(:silenced?).returns(true) SystemMessage.unstub(:create) SystemMessage.expects(:create).never - expect(block_user).to eq(false) + expect(silence_user).to eq(false) end it "logs it with context" do SystemMessage.stubs(:create).returns(Fabricate.build(:post)) expect { - UserBlocker.block(user, Fabricate(:admin)) + UserSilencer.silence(user, Fabricate(:admin)) }.to change { UserHistory.count }.by(1) expect(UserHistory.last.context).to be_present end end - describe 'unblock' do + describe 'unsilence' do let(:user) { stub_everything(save: true) } - subject(:unblock_user) { UserBlocker.unblock(user, Fabricate.build(:admin)) } + subject(:unsilence_user) { UserSilencer.unsilence(user, Fabricate.build(:admin)) } - it 'unblocks the user' do - u = Fabricate(:user, blocked: true) - expect { UserBlocker.unblock(u) }.to change { u.reload.blocked? } + it 'unsilences the user' do + u = Fabricate(:user, silenced: true) + expect { UserSilencer.unsilence(u) }.to change { u.reload.silenced? } end it 'sends a message to the user' do SystemMessage.unstub(:create) - SystemMessage.expects(:create).with(user, :unblocked).returns(true) - unblock_user + SystemMessage.expects(:create).with(user, :unsilenced).returns(true) + unsilence_user end it "doesn't send a pm if save fails" do user.stubs(:save).returns(false) SystemMessage.unstub(:create) SystemMessage.expects(:create).never - unblock_user + unsilence_user end it "logs it" do expect { - unblock_user + unsilence_user }.to change { UserHistory.count }.by(1) end end @@ -100,28 +100,28 @@ describe UserBlocker do describe 'hide_posts' do let(:user) { Fabricate(:user, trust_level: 0) } let!(:post) { Fabricate(:post, user: user) } - subject { UserBlocker.new(user) } + subject { UserSilencer.new(user) } it "hides all the user's posts" do - subject.block + subject.silence expect(post.reload).to be_hidden end it "hides the topic if the post was the first post" do - subject.block + subject.silence expect(post.topic.reload).to_not be_visible end it "doesn't hide posts if user is TL1" do user.trust_level = 1 - subject.block + subject.silence expect(post.reload).to_not be_hidden expect(post.topic.reload).to be_visible end it "only hides posts from the past 24 hours" do old_post = Fabricate(:post, user: user, created_at: 2.days.ago) - subject.block + subject.silence expect(post.reload).to be_hidden expect(post.topic.reload).to_not be_visible old_post.reload diff --git a/test/javascripts/acceptance/queued-posts-test.js.es6 b/test/javascripts/acceptance/queued-posts-test.js.es6 index 8c41899fc0..c2349ce01a 100644 --- a/test/javascripts/acceptance/queued-posts-test.js.es6 +++ b/test/javascripts/acceptance/queued-posts-test.js.es6 @@ -10,7 +10,7 @@ QUnit.test("For topics: body of post, title, category and tags are all editbale" return [ 200, {"Content-Type": "application/json"}, - {"users":[{"id":3,"username":"test_user","avatar_template":"/letter_avatar_proxy/v2/letter/t/eada6e/{size}.png","active":true,"admin":false,"moderator":false,"last_seen_at":"2017-08-11T20:48:05.405Z","last_emailed_at":null,"created_at":"2017-08-07T02:23:33.309Z","last_seen_age":"1d","last_emailed_age":null,"created_at_age":"6d","username_lower":"test_user","trust_level":0,"trust_level_locked":false,"flag_level":0,"title":null,"suspended_at":null,"suspended_till":null,"suspended":null,"blocked":false,"time_read":"19m","staged":false,"days_visited":4,"posts_read_count":12,"topics_entered":6,"post_count":2}],"queued_posts":[{"id":22,"queue":"default","user_id":3,"state":1,"topic_id":null,"approved_by_id":null,"rejected_by_id":null,"raw":"some content","post_options":{"archetype":"regular","category":"1","typing_duration_msecs":"3200","composer_open_duration_msecs":"19007","visible":true,"is_warning":false,"title":"a new topic that needs to be reviewed","ip_address":"172.17.0.1","first_post_checks":true,"is_poll":true},"created_at":"2017-08-11T20:43:41.115Z","category_id":1,"can_delete_user":true}],"__rest_serializer":"1","refresh_queued_posts":"/queued_posts?status=new"} + {"users":[{"id":3,"username":"test_user","avatar_template":"/letter_avatar_proxy/v2/letter/t/eada6e/{size}.png","active":true,"admin":false,"moderator":false,"last_seen_at":"2017-08-11T20:48:05.405Z","last_emailed_at":null,"created_at":"2017-08-07T02:23:33.309Z","last_seen_age":"1d","last_emailed_age":null,"created_at_age":"6d","username_lower":"test_user","trust_level":0,"trust_level_locked":false,"flag_level":0,"title":null,"suspended_at":null,"suspended_till":null,"suspended":null,"silenced":false,"time_read":"19m","staged":false,"days_visited":4,"posts_read_count":12,"topics_entered":6,"post_count":2}],"queued_posts":[{"id":22,"queue":"default","user_id":3,"state":1,"topic_id":null,"approved_by_id":null,"rejected_by_id":null,"raw":"some content","post_options":{"archetype":"regular","category":"1","typing_duration_msecs":"3200","composer_open_duration_msecs":"19007","visible":true,"is_warning":false,"title":"a new topic that needs to be reviewed","ip_address":"172.17.0.1","first_post_checks":true,"is_poll":true},"created_at":"2017-08-11T20:43:41.115Z","category_id":1,"can_delete_user":true}],"__rest_serializer":"1","refresh_queued_posts":"/queued_posts?status=new"} ]; }); @@ -31,7 +31,7 @@ QUnit.test("For replies: only the body of post is editbale", assert => { return [ 200, {"Content-Type": "application/json"}, - {"users":[{"id":3,"username":"test_user","avatar_template":"/letter_avatar_proxy/v2/letter/t/eada6e/{size}.png","active":true,"admin":false,"moderator":false,"last_seen_at":"2017-08-11T20:48:05.405Z","last_emailed_at":null,"created_at":"2017-08-07T02:23:33.309Z","last_seen_age":"1d","last_emailed_age":null,"created_at_age":"6d","username_lower":"test_user","trust_level":0,"trust_level_locked":false,"flag_level":0,"title":null,"suspended_at":null,"suspended_till":null,"suspended":null,"blocked":false,"time_read":"19m","staged":false,"days_visited":4,"posts_read_count":12,"topics_entered":6,"post_count":2}],"topics":[{"id":11,"title":"This is a topic","fancy_title":"This is a topic","slug":"this-is-a-topic","posts_count":2}],"queued_posts":[{"id":4,"queue":"default","user_id":3,"state":1,"topic_id":11,"approved_by_id":null,"rejected_by_id":null,"raw":"edited haahaasdfasdfasdfasdf","post_options":{"archetype":"regular","category":"3","reply_to_post_number":"2","typing_duration_msecs":"1900","composer_open_duration_msecs":"12096","visible":true,"is_warning":false,"featured_link":"","ip_address":"172.17.0.1","first_post_checks":true,"is_poll":true},"created_at":"2017-08-07T19:11:52.018Z","category_id":3,"can_delete_user":true}],"__rest_serializer":"1","refresh_queued_posts":"/queued_posts?status=new"} + {"users":[{"id":3,"username":"test_user","avatar_template":"/letter_avatar_proxy/v2/letter/t/eada6e/{size}.png","active":true,"admin":false,"moderator":false,"last_seen_at":"2017-08-11T20:48:05.405Z","last_emailed_at":null,"created_at":"2017-08-07T02:23:33.309Z","last_seen_age":"1d","last_emailed_age":null,"created_at_age":"6d","username_lower":"test_user","trust_level":0,"trust_level_locked":false,"flag_level":0,"title":null,"suspended_at":null,"suspended_till":null,"suspended":null,"silenced":false,"time_read":"19m","staged":false,"days_visited":4,"posts_read_count":12,"topics_entered":6,"post_count":2}],"topics":[{"id":11,"title":"This is a topic","fancy_title":"This is a topic","slug":"this-is-a-topic","posts_count":2}],"queued_posts":[{"id":4,"queue":"default","user_id":3,"state":1,"topic_id":11,"approved_by_id":null,"rejected_by_id":null,"raw":"edited haahaasdfasdfasdfasdf","post_options":{"archetype":"regular","category":"3","reply_to_post_number":"2","typing_duration_msecs":"1900","composer_open_duration_msecs":"12096","visible":true,"is_warning":false,"featured_link":"","ip_address":"172.17.0.1","first_post_checks":true,"is_poll":true},"created_at":"2017-08-07T19:11:52.018Z","category_id":3,"can_delete_user":true}],"__rest_serializer":"1","refresh_queued_posts":"/queued_posts?status=new"} ]; }); From 4270c93666fb3903e1320ef22813cd3ea3a6c4a6 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 10 Nov 2017 14:29:36 -0500 Subject: [PATCH 152/445] FIX: Missing yml file --- spec/fixtures/emails/silenced_sender.eml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 spec/fixtures/emails/silenced_sender.eml diff --git a/spec/fixtures/emails/silenced_sender.eml b/spec/fixtures/emails/silenced_sender.eml new file mode 100644 index 0000000000..47295f480b --- /dev/null +++ b/spec/fixtures/emails/silenced_sender.eml @@ -0,0 +1,9 @@ +Return-Path: +From: Foo Bar +Date: Fri, 15 Jan 2016 00:12:43 +0100 +Message-ID: <8@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. From bd8f8ea1f906c749163dbc0ed856446884a3813d Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 10 Nov 2017 15:38:54 -0500 Subject: [PATCH 153/445] FIX: don't show Create Topic button on full search page to users who can't create topics --- .../discourse/controllers/full-page-search.js.es6 | 6 +++--- app/serializers/grouped_search_result_serializer.rb | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 index ef661f1928..e4c86c0dcb 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -159,9 +159,9 @@ export default Ember.Controller.extend({ return this.currentUser && this.currentUser.staff && hasResults; }, - @computed('expanded') - canCreateTopic(expanded) { - return this.currentUser && !this.site.mobileView && !expanded; + @computed('expanded', 'model.grouped_search_result.can_create_topic') + canCreateTopic(expanded, userCanCreateTopic) { + return this.currentUser && userCanCreateTopic && !this.site.mobileView && !expanded; }, @computed('expanded') diff --git a/app/serializers/grouped_search_result_serializer.rb b/app/serializers/grouped_search_result_serializer.rb index 2ce8d6143f..b9dcb5b62c 100644 --- a/app/serializers/grouped_search_result_serializer.rb +++ b/app/serializers/grouped_search_result_serializer.rb @@ -3,7 +3,7 @@ class GroupedSearchResultSerializer < ApplicationSerializer has_many :users, serializer: SearchResultUserSerializer has_many :categories, serializer: BasicCategorySerializer has_many :tags, serializer: TagSerializer - attributes :more_posts, :more_users, :more_categories, :term, :search_log_id, :more_full_page_results + attributes :more_posts, :more_users, :more_categories, :term, :search_log_id, :more_full_page_results, :can_create_topic def search_log_id object.search_log_id @@ -17,4 +17,8 @@ class GroupedSearchResultSerializer < ApplicationSerializer SiteSetting.tagging_enabled end + def can_create_topic + scope.can_create?(Topic) + end + end From 75dad26fa5194c5d5838148e144636fb71d38686 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 10 Nov 2017 15:55:48 -0500 Subject: [PATCH 154/445] Rename confusing action name --- app/assets/javascripts/admin/components/flagged-post.js.es6 | 2 +- .../javascripts/admin/templates/components/flagged-post.hbs | 4 +++- .../components/admin-agree-flag-dropdown.js.es6 | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/admin/components/flagged-post.js.es6 b/app/assets/javascripts/admin/components/flagged-post.js.es6 index aa7c9ea8ae..132e1c80b3 100644 --- a/app/assets/javascripts/admin/components/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/components/flagged-post.js.es6 @@ -32,7 +32,7 @@ export default Ember.Component.extend({ }, actions: { - onRemoveAfterPromise(promise) { + removeAfter(promise) { this.removeAfter(promise); }, diff --git a/app/assets/javascripts/admin/templates/components/flagged-post.hbs b/app/assets/javascripts/admin/templates/components/flagged-post.hbs index 81b3195de4..dc89d3b83c 100644 --- a/app/assets/javascripts/admin/templates/components/flagged-post.hbs +++ b/app/assets/javascripts/admin/templates/components/flagged-post.hbs @@ -111,7 +111,9 @@ {{#if canAct}}
    - {{admin-agree-flag-dropdown post=flaggedPost onRemoveAfterPromise=(action "onRemoveAfterPromise")}} + {{admin-agree-flag-dropdown + post=flaggedPost + removeAfter=(action "removeAfter") }} {{#if flaggedPost.postHidden}} {{d-button diff --git a/app/assets/javascripts/select-box-kit/components/admin-agree-flag-dropdown.js.es6 b/app/assets/javascripts/select-box-kit/components/admin-agree-flag-dropdown.js.es6 index 0645196dab..f18f311f7c 100644 --- a/app/assets/javascripts/select-box-kit/components/admin-agree-flag-dropdown.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/admin-agree-flag-dropdown.js.es6 @@ -75,12 +75,12 @@ export default DropdownSelectBox.extend({ actions: { deleteSpammer() { let spammerDetails = this.get("spammerDetails"); - this.sendAction("onRemoveAfterPromise", spammerDetails.deleteUser()); + this.attrs.removeAfter(spammerDetails.deleteUser()); }, perform(action) { let flaggedPost = this.get("post"); - this.sendAction("onRemoveAfterPromise", flaggedPost.agreeFlags(action)); + this.attrs.removeAfter(flaggedPost.agreeFlags(action)); }, } }); From 94764399ae8e78f01d217998fe60cbe4f273708b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 10 Nov 2017 16:02:31 -0500 Subject: [PATCH 155/445] Delete unused files --- .../admin/components/flagged-post.js.es6 | 4 --- .../modals/admin-agree-flag.js.es6 | 21 ----------- .../templates/modal/admin-agree-flag.hbs | 35 ------------------- .../stylesheets/common/admin/flagging.scss | 2 +- app/assets/stylesheets/desktop/modal.scss | 2 +- app/assets/stylesheets/mobile/modal.scss | 2 +- 6 files changed, 3 insertions(+), 63 deletions(-) delete mode 100644 app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 delete mode 100644 app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs diff --git a/app/assets/javascripts/admin/components/flagged-post.js.es6 b/app/assets/javascripts/admin/components/flagged-post.js.es6 index 132e1c80b3..3019a1aae0 100644 --- a/app/assets/javascripts/admin/components/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/components/flagged-post.js.es6 @@ -36,10 +36,6 @@ export default Ember.Component.extend({ this.removeAfter(promise); }, - showAgreeFlagModal() { - this._spawnModal('admin-agree-flag', this.get('flaggedPost'), 'agree-flag-modal'); - }, - showDeleteFlagModal() { this._spawnModal('admin-delete-flag', this.get('flaggedPost'), 'delete-flag-modal'); }, diff --git a/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 deleted file mode 100644 index 07fbe0763c..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 +++ /dev/null @@ -1,21 +0,0 @@ -import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import DeleteSpammerModal from 'admin/mixins/delete-spammer-modal'; - -export default Ember.Controller.extend(ModalFunctionality, DeleteSpammerModal, { - removeAfter: null, - - actions: { - agreeDeleteSpammer(user) { - return this.removeAfter(user.deleteAsSpammer()).then(() => { - this.send('closeModal'); - }); - }, - - perform(action) { - let flaggedPost = this.get('model'); - return this.removeAfter(flaggedPost.agreeFlags(action)).then(() => { - this.send('closeModal'); - }); - }, - } -}); diff --git a/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs b/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs deleted file mode 100644 index b9d5a88383..0000000000 --- a/app/assets/javascripts/admin/templates/modal/admin-agree-flag.hbs +++ /dev/null @@ -1,35 +0,0 @@ -{{#d-modal-body title="admin.flags.agree_flag_modal_title"}} - {{#if model.user_deleted}} - {{d-button - title="admin.flags.agree_flag_restore_post_title" - class="confirm-agree-restore" - action=(action "perform" "restore") - icon="eye" - label="admin.flags.agree_flag_restore_post"}} - {{else}} - {{#unless model.postHidden}} - {{d-button - title="admin.flags.agree_flag_hide_post_title" - action=(action "perform" "hide") - class="confirm-agree-hide" - icon="eye-slash" - label="admin.flags.agree_flag_hide_post"}} - {{/unless}} - {{/if}} - - {{d-button - title="admin.flags.agree_flag_title" - action=(action "perform" "keep") - class="confirm-agree-keep" - icon="thumbs-o-up" - label="admin.flags.agree_flag"}} - - {{#if canDeleteSpammer}} - {{d-button - title="admin.flags.delete_spammer_title" - action="deleteSpammer" - class="btn-danger delete-spammer" - icon="exclamation-triangle" - label="admin.flags.delete_spammer"}} - {{/if}} -{{/d-modal-body}} diff --git a/app/assets/stylesheets/common/admin/flagging.scss b/app/assets/stylesheets/common/admin/flagging.scss index 8867439f46..f2132ed893 100644 --- a/app/assets/stylesheets/common/admin/flagging.scss +++ b/app/assets/stylesheets/common/admin/flagging.scss @@ -188,7 +188,7 @@ margin-bottom: 2em; } -.delete-flag-modal, .agree-flag-modal { +.delete-flag-modal { button { margin: 10px 0 10px 10px; padding: 10px 15px; diff --git a/app/assets/stylesheets/desktop/modal.scss b/app/assets/stylesheets/desktop/modal.scss index 53e18769a5..06d9484471 100644 --- a/app/assets/stylesheets/desktop/modal.scss +++ b/app/assets/stylesheets/desktop/modal.scss @@ -220,7 +220,7 @@ margin-bottom: 10px; } -.delete-flag-modal, .agree-flag-modal { +.delete-flag-modal { .modal-inner-container { width: 400px; } diff --git a/app/assets/stylesheets/mobile/modal.scss b/app/assets/stylesheets/mobile/modal.scss index 2adf1c67cc..0643a007d1 100644 --- a/app/assets/stylesheets/mobile/modal.scss +++ b/app/assets/stylesheets/mobile/modal.scss @@ -169,7 +169,7 @@ } } -.delete-flag-modal, .agree-flag-modal { +.delete-flag-modal { .modal-inner-container { width: 300px; } From 9716e3ddf3c0e55d810b1f9eeba8cb8b75dbda95 Mon Sep 17 00:00:00 2001 From: Kris Date: Fri, 10 Nov 2017 16:45:06 -0500 Subject: [PATCH 156/445] bringing mobile topic statuses to parity with desktop --- .../stylesheets/common/base/topic-post.scss | 7 ++++++ .../stylesheets/desktop/topic-post.scss | 9 ------- app/assets/stylesheets/mobile/topic-post.scss | 25 ------------------- 3 files changed, 7 insertions(+), 34 deletions(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 84b7aeac1c..70e9a1bc34 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -321,6 +321,8 @@ blockquote > *:last-child { } .small-action { + max-width: 755px; + border-top: 1px solid $primary-low; .topic-avatar { padding: 5px 0 3px; border-top: none; @@ -332,10 +334,15 @@ blockquote > *:last-child { color: dark-light-choose($primary-low-mid, $secondary-high); } } + + .small-action.deleted { + background-color: dark-light-diff(rgba($danger,.7), $secondary, 50%, -60%); + } .small-action-desc.timegap { color: dark-light-choose($primary-medium, $secondary-high); } + .small-action-desc { padding: 0.25em 0 0.5em 4.3em; margin-top: 6px; diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index a85d624486..f36bfcf05a 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -660,15 +660,6 @@ $topic-avatar-width: 45px; width: calc(#{$topic-avatar-width} + #{$topic-body-width} + 2 * #{$topic-body-width-padding}); } -.small-action { - max-width: 755px; - border-top: 1px solid $primary-low; -} - -.small-action.deleted { - background-color: dark-light-diff(rgba($danger,.7), $secondary, 50%, -60%); -} - /* hide the reply border above the time gap notices */ .time-gap + .topic-post .topic-body, .time-gap + .topic-post .topic-avatar { diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 57d2302749..ab1312fa26 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -1,27 +1,3 @@ -.gap { - /* may not need this */ -} - -.small-action { - border-top: 1px solid $primary-low; - color: lighten($primary, 50%); - padding-bottom: 3px; - text-transform: uppercase; - font-weight: bold; - - .topic-avatar { - margin-top: 5px; - } - - .small-action-desc { - padding-top: 1em; - - button { - padding-top: 0px; - } - } -} - /* hide the reply border above the time gap notices */ .time-gap + .topic-post article { border-top: none; @@ -36,7 +12,6 @@ padding-bottom: 30px; } - span.badge-posts { margin-right: 5px; } From d9823f69c6b3b4ee91f6611bd021c3affd56652e Mon Sep 17 00:00:00 2001 From: Vinoth Kanan Date: Sat, 11 Nov 2017 18:52:22 +0530 Subject: [PATCH 157/445] FEATURE: Option to export multiple categories using export_category method --- lib/import_export/category_exporter.rb | 7 ++----- lib/import_export/import_export.rb | 6 +++--- lib/tasks/export.rake | 12 ++++++++++-- script/discourse | 16 +++++++++++----- spec/import_export/category_exporter_spec.rb | 19 ++++++++++++------- 5 files changed, 38 insertions(+), 22 deletions(-) diff --git a/lib/import_export/category_exporter.rb b/lib/import_export/category_exporter.rb index 4ea7209c19..5568fb5180 100644 --- a/lib/import_export/category_exporter.rb +++ b/lib/import_export/category_exporter.rb @@ -4,10 +4,8 @@ require "import_export/topic_exporter" module ImportExport class CategoryExporter < BaseExporter - def initialize(category_id) - @category = Category.find(category_id) - @categories = Category.where(parent_category_id: category_id).to_a - @categories << @category + def initialize(category_ids) + @categories = Category.where(id: category_ids).or(Category.where(parent_category_id: category_ids)).to_a @export_data = { categories: [], groups: [], @@ -17,7 +15,6 @@ module ImportExport end def perform - puts "Exporting category #{@category.name}...", "" export_categories! export_category_groups! export_topics_and_users diff --git a/lib/import_export/import_export.rb b/lib/import_export/import_export.rb index 53ed9f591c..4523bb779d 100644 --- a/lib/import_export/import_export.rb +++ b/lib/import_export/import_export.rb @@ -11,12 +11,12 @@ module ImportExport ImportExport::Importer.new(data).perform end - def self.export_categories(include_users, filename = nil) + def self.export_category_structure(include_users, filename = nil) ImportExport::CategoryStructureExporter.new(include_users).perform.save_to_file(filename) end - def self.export_category(category_id, filename = nil) - ImportExport::CategoryExporter.new(category_id).perform.save_to_file(filename) + def self.export_categories(category_ids, filename = nil) + ImportExport::CategoryExporter.new(category_ids).perform.save_to_file(filename) end def self.export_topics(topic_ids, filename = nil) diff --git a/lib/tasks/export.rake b/lib/tasks/export.rake index cfa5954027..d0b80c8819 100644 --- a/lib/tasks/export.rake +++ b/lib/tasks/export.rake @@ -1,7 +1,15 @@ desc 'Export all the categories' -task 'export:categories', [:include_group_users, :file_name] => [:environment] do |_, args| +task 'export:categories', [:category_ids] => [:environment] do |_, args| require "import_export/import_export" - ImportExport.export_categories(args[:include_group_users], args[:file_name]) + ImportExport.export_categories(args[:category_ids]) + puts "", "Done", "" +end + +desc 'Export only the structure of all categories' +task 'export:category_structure', [:include_group_users, :file_name] => [:environment] do |_, args| + require "import_export/import_export" + + ImportExport.export_category_structure(args[:include_group_users], args[:file_name]) puts "", "Done", "" end diff --git a/script/discourse b/script/discourse index 1f8fe1275f..67d4bfec6e 100755 --- a/script/discourse +++ b/script/discourse @@ -181,16 +181,22 @@ class DiscourseCLI < Thor puts 'Requests sent. Clients will refresh on next navigation.' end - desc "export_category", "Export a category, all its topics, and all users who posted in those topics" - def export_category(category_id, filename = nil) - raise "Category id argument is missing!" unless category_id - + desc "export_categories", "Export categories, all its topics, and all users who posted in those topics" + def export_categories(*category_ids) + puts "Starting export of categories...", "" load_rails load_import_export - ImportExport.export_category(category_id, filename) + ImportExport.export_categories(category_ids) puts "", "Done", "" end + desc "export_category", "Export a category, all its topics, and all users who posted in those topics" + def export_category(category_id) + raise "Category id argument is missing!" unless category_id + + export_categories([category_id]) + end + desc "import_category", "Import a category, its topics and the users from the output of the export_category command" def import_category(filename) raise "File name argument missing!" unless filename diff --git a/spec/import_export/category_exporter_spec.rb b/spec/import_export/category_exporter_spec.rb index 3c22617570..785f520c85 100644 --- a/spec/import_export/category_exporter_spec.rb +++ b/spec/import_export/category_exporter_spec.rb @@ -12,12 +12,8 @@ describe ImportExport::CategoryExporter do end context '.perform' do - it 'raises an error when the category is not found' do - expect { ImportExport::CategoryExporter.new(100).perform }.to raise_error(ActiveRecord::RecordNotFound) - end - it 'export the category when it is found' do - data = ImportExport::CategoryExporter.new(category.id).perform.export_data + data = ImportExport::CategoryExporter.new([category.id]).perform.export_data expect(data[:categories].count).to eq(1) expect(data[:groups].count).to eq(0) @@ -25,16 +21,25 @@ describe ImportExport::CategoryExporter do it 'export the category with permission groups' do category_group = Fabricate(:category_group, category: category, group: group) - data = ImportExport::CategoryExporter.new(category.id).perform.export_data + data = ImportExport::CategoryExporter.new([category.id]).perform.export_data expect(data[:categories].count).to eq(1) expect(data[:groups].count).to eq(1) end + it 'export multiple categories' do + category2 = Fabricate(:category) + category_group = Fabricate(:category_group, category: category, group: group) + data = ImportExport::CategoryExporter.new([category.id, category2.id]).perform.export_data + + expect(data[:categories].count).to eq(2) + expect(data[:groups].count).to eq(1) + end + it 'export the category with topics and users' do topic1 = Fabricate(:topic, category: category, user_id: -1) topic2 = Fabricate(:topic, category: category, user: user) - data = ImportExport::CategoryExporter.new(category.id).perform.export_data + data = ImportExport::CategoryExporter.new([category.id]).perform.export_data expect(data[:categories].count).to eq(1) expect(data[:groups].count).to eq(0) From 9219786a2420abdb5fbd9a7c7d3e3eb94dcf22d7 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Sat, 11 Nov 2017 10:33:37 -0800 Subject: [PATCH 158/445] FIX: makes sure fixed positioning is correctly applied and removed --- .../select-box-kit/components/dropdown-select-box.js.es6 | 5 ++++- .../javascripts/select-box-kit/mixins/dom-helpers.js.es6 | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/select-box-kit/components/dropdown-select-box.js.es6 b/app/assets/javascripts/select-box-kit/components/dropdown-select-box.js.es6 index 252ff2dd64..1044765582 100644 --- a/app/assets/javascripts/select-box-kit/components/dropdown-select-box.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/dropdown-select-box.js.es6 @@ -9,7 +9,10 @@ export default SelectBoxKitComponent.extend({ headerComponent: "dropdown-select-box/dropdown-select-box-header", rowComponent: "dropdown-select-box/dropdown-select-box-row", - clickOutside() { this.close(); }, + clickOutside() { + if (this.get("isExpanded") === false) { return; } + this.close(); + }, didSelectValue() { this._super(); diff --git a/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 b/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 index ed70bcc59e..9286ec10e8 100644 --- a/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 +++ b/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 @@ -175,13 +175,14 @@ export default Ember.Mixin.create({ }, _applyFixedPosition() { + if (this.get("isExpanded") !== true) { return; } if (this.get("scrollableParent").length === 0) { return; } const width = this.$().outerWidth(false); const height = this.$().outerHeight(false); const $placeholder = $(`
    `); - this._previousScrollParentOverflow = this.get("scrollableParent").css("overflow"); + this._previousScrollParentOverflow = this._previousScrollParentOverflow || this.get("scrollableParent").css("overflow"); this.get("scrollableParent").css({ overflow: "hidden" }); this._previousCSSContext = { From 0f0b2907a7e138d05d8372406898bfa7ffbfc7ea Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Sat, 11 Nov 2017 11:46:26 -0800 Subject: [PATCH 159/445] FIX: makes fixed positioning more resilient --- .../components/select-box-kit.js.es6 | 5 ----- .../select-box-kit/mixins/dom-helpers.js.es6 | 21 +++++++++---------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/select-box-kit/components/select-box-kit.js.es6 b/app/assets/javascripts/select-box-kit/components/select-box-kit.js.es6 index ffb118eba8..a3371d0249 100644 --- a/app/assets/javascripts/select-box-kit/components/select-box-kit.js.es6 +++ b/app/assets/javascripts/select-box-kit/components/select-box-kit.js.es6 @@ -211,11 +211,6 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin }); }, - @computed("scrollableParentSelector") - scrollableParent(scrollableParentSelector) { - return this.$().parents(scrollableParentSelector).first(); - }, - willFilterContent() { this.expand(); this.set("highlightedValue", null); diff --git a/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 b/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 index 9286ec10e8..def896b373 100644 --- a/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 +++ b/app/assets/javascripts/select-box-kit/mixins/dom-helpers.js.es6 @@ -176,14 +176,16 @@ export default Ember.Mixin.create({ _applyFixedPosition() { if (this.get("isExpanded") !== true) { return; } - if (this.get("scrollableParent").length === 0) { return; } + + const scrollableParent = this.$().parents(this.get("scrollableParentSelector")); + if (scrollableParent.length === 0) { return; } const width = this.$().outerWidth(false); const height = this.$().outerHeight(false); const $placeholder = $(`
    `); - this._previousScrollParentOverflow = this._previousScrollParentOverflow || this.get("scrollableParent").css("overflow"); - this.get("scrollableParent").css({ overflow: "hidden" }); + this._previousScrollParentOverflow = this._previousScrollParentOverflow || scrollableParent.css("overflow"); + scrollableParent.css({ overflow: "hidden" }); this._previousCSSContext = { minWidth: this.$().css("min-width"), @@ -192,7 +194,7 @@ export default Ember.Mixin.create({ const componentStyles = { position: "fixed", - "margin-top": -this.get("scrollableParent").scrollTop(), + "margin-top": -scrollableParent.scrollTop(), width, minWidth: "unset", maxWidth: "unset" @@ -212,12 +214,11 @@ export default Ember.Mixin.create({ _removeFixedPosition() { $(`.select-box-kit-fixed-placeholder-${this.elementId}`).remove(); - if (this.get("scrollableParent").length === 0) { - return; - } - if (!this.element || this.isDestroying || this.isDestroyed) { return; } + const scrollableParent = this.$().parents(this.get("scrollableParentSelector")); + if (scrollableParent.length === 0) { return; } + const css = jQuery.extend( this._previousCSSContext, { @@ -231,9 +232,7 @@ export default Ember.Mixin.create({ ); this.$().css(css); - this.get("scrollableParent").css({ - overflow: this._previousScrollParentOverflow - }); + scrollableParent.css("overflow", this._previousScrollParentOverflow); }, _positionWrapper() { From 4af7881cb7fc6600c40f0cbb40670e8b5657affc Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Sat, 11 Nov 2017 12:16:00 -0800 Subject: [PATCH 160/445] FIX: removes clip causing UI glitches on safari --- .../stylesheets/common/select-box-kit/select-box-kit.scss | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/common/select-box-kit/select-box-kit.scss b/app/assets/stylesheets/common/select-box-kit/select-box-kit.scss index 05f7d620f5..fc8fb103f7 100644 --- a/app/assets/stylesheets/common/select-box-kit/select-box-kit.scss +++ b/app/assets/stylesheets/common/select-box-kit/select-box-kit.scss @@ -242,11 +242,10 @@ } &.is-hidden { - clip: rect(0 0 0 0); width: 1px; height: 1px; border: 0; - margin: 0; + margin: -1px; padding: 0; overflow: hidden; position: fixed; @@ -269,11 +268,10 @@ } .select-box-kit-offscreen, .select-box-kit-offscreen:focus { - clip: rect(0 0 0 0); + margin: -1px; width: 1px; height: 1px; border: 0; - margin: 0; padding: 0; overflow: hidden; position: fixed; From 4dc4bc70c805548458ee4c50749dda7b7bd40523 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Sun, 12 Nov 2017 01:43:18 +0100 Subject: [PATCH 161/445] FIX: ignore_by_title should match case-insensitive --- lib/email/receiver.rb | 2 +- spec/components/email/receiver_spec.rb | 3 +-- spec/components/validators/email_validator_spec.rb | 3 +++ spec/fixtures/emails/ignored.eml | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index be5ae2f282..0a5ad7f7cb 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -66,7 +66,7 @@ module Email def is_blacklisted? return false if SiteSetting.ignore_by_title.blank? - Regexp.new(SiteSetting.ignore_by_title) =~ @mail.subject + Regexp.new(SiteSetting.ignore_by_title, Regexp::IGNORECASE) =~ @mail.subject end def create_incoming_email diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 51e655a6b2..6f6e7d50a3 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -568,11 +568,10 @@ describe Email::Receiver do expect { process(:tl4_user) }.to change(Topic, :count) end - it "ignores by title" do + it "ignores by case-insensitive title" do SiteSetting.ignore_by_title = "foo" expect { process(:ignored) }.to_not change(Topic, :count) end - end context "new topic in a category that allows strangers" do diff --git a/spec/components/validators/email_validator_spec.rb b/spec/components/validators/email_validator_spec.rb index 71f6bb746a..42ff521375 100644 --- a/spec/components/validators/email_validator_spec.rb +++ b/spec/components/validators/email_validator_spec.rb @@ -21,11 +21,13 @@ describe EmailValidator do it "adds an error when email matches a blocked email" do ScreenedEmail.create!(email: 'sam@sam.com', action_type: ScreenedEmail.actions[:block]) expect(blocks?('sam@sam.com')).to eq(true) + expect(blocks?('SAM@sam.com')).to eq(true) end it "blocks based on email_domains_blacklist" do SiteSetting.email_domains_blacklist = "email.com|mail.com|e-mail.com" expect(blocks?('sam@email.com')).to eq(true) + expect(blocks?('sam@EMAIL.com')).to eq(true) expect(blocks?('sam@bob.email.com')).to eq(true) expect(blocks?('sam@e-mail.com')).to eq(true) expect(blocks?('sam@googlemail.com')).to eq(false) @@ -34,6 +36,7 @@ describe EmailValidator do it "blocks based on email_domains_whitelist" do SiteSetting.email_domains_whitelist = "googlemail.com|email.com" expect(blocks?('sam@email.com')).to eq(false) + expect(blocks?('sam@EMAIL.com')).to eq(false) expect(blocks?('sam@bob.email.com')).to eq(false) expect(blocks?('sam@e-mail.com')).to eq(true) expect(blocks?('sam@googlemail.com')).to eq(false) diff --git a/spec/fixtures/emails/ignored.eml b/spec/fixtures/emails/ignored.eml index 1eafc6a6d2..153a9efb41 100644 --- a/spec/fixtures/emails/ignored.eml +++ b/spec/fixtures/emails/ignored.eml @@ -1,7 +1,7 @@ Return-Path: From: Foo Bar To: category@foo.com -Subject: This is a foo topic +Subject: This is a FoO topic Date: Fri, 15 Jan 2016 00:12:43 +0100 Message-ID: <53@foo.bar.mail> Mime-Version: 1.0 From be1bc2706b1a24d71ff62bbc9af75df37ec206b7 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Sun, 12 Nov 2017 14:51:23 +0530 Subject: [PATCH 162/445] FIX: redirect /admin/users to Admin Users List --- app/assets/javascripts/admin/routes/admin-users-index.js.es6 | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/assets/javascripts/admin/routes/admin-users-index.js.es6 diff --git a/app/assets/javascripts/admin/routes/admin-users-index.js.es6 b/app/assets/javascripts/admin/routes/admin-users-index.js.es6 new file mode 100644 index 0000000000..8bb7adc055 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-users-index.js.es6 @@ -0,0 +1,5 @@ +export default Discourse.Route.extend({ + redirect: function() { + this.transitionTo('adminUsersList'); + } +}); From fc6de6863bd87d8684447149e32376fdce41b099 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Sat, 11 Nov 2017 17:27:28 +0100 Subject: [PATCH 163/445] WIP --- spec/components/email/receiver_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 6f6e7d50a3..cdfe82fc9e 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -396,6 +396,9 @@ describe Email::Receiver do expect(topic.posts.last.created_at).to be_within(1.minute).of(DateTime.now) end + it "accepts emails with wrong reply key if the system knows about the forwareded email" do + + end end context "new message to a group" do From 5210e3e744995ba94ba69fdf9a64adcb571be58d Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Sun, 12 Nov 2017 23:44:22 +0100 Subject: [PATCH 164/445] FEATURE: accept incoming email with reply_key mismatch when original email was forwarded --- lib/email/receiver.rb | 36 ++++++++++++++++++- spec/components/email/receiver_spec.rb | 22 ++++++++++++ .../reply_user_not_matching_but_known.eml | 11 ++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 spec/fixtures/emails/reply_user_not_matching_but_known.eml diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 0a5ad7f7cb..2570a0bf3a 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -429,7 +429,7 @@ module Email when :reply email_log = destination[:obj] - if email_log.user_id != user.id + if email_log.user_id != user.id && !forwareded_reply_key?(email_log, user) raise ReplyUserNotMatchingError, "email_log.user_id => #{email_log.user_id.inspect}, user.id => #{user.id.inspect}" end @@ -442,6 +442,40 @@ module Email end end + def forwareded_reply_key?(email_log, user) + incoming_emails = IncomingEmail + .joins(:post) + .where('posts.topic_id = ?', email_log.topic_id) + .where('incoming_emails.to_addresses ILIKE :email OR incoming_emails.cc_addresses ILIKE :email', email: "%#{email_log.reply_key}%") + .where('incoming_emails.to_addresses ILIKE :email OR incoming_emails.cc_addresses ILIKE :email', email: "%#{user.email}%") + + incoming_emails.each do |email| + next unless contains_email_address?(email.to_addresses, user.email) || + contains_email_address?(email.cc_addresses, user.email) + + return true if contains_reply_by_email_address(email.to_addresses, email_log.reply_key) || + contains_reply_by_email_address(email.cc_addresses, email_log.reply_key) + end + + false + end + + def contains_email_address?(addresses, email) + return false if addresses.blank? + addresses.split(";").include?(email) + end + + def contains_reply_by_email_address(addresses, reply_key) + return false if addresses.blank? + + addresses.split(";").each do |address| + match = Email::Receiver.reply_by_email_address_regex.match(address) + return true if match && match.captures&.include?(reply_key) + end + + false + end + def has_been_forwarded? subject[/^[[:blank:]]*(fwd?|tr)[[:blank:]]?:/i] && embedded_email_raw.present? end diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index cdfe82fc9e..75ef299f93 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -397,7 +397,29 @@ describe Email::Receiver do end it "accepts emails with wrong reply key if the system knows about the forwareded email" do + Fabricate(:incoming_email, + raw: <<~RAW, + Return-Path: + From: Alice + To: dave@bar.com, reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com + CC: carol@bar.com, bob@bar.com + Subject: Hello world + Date: Fri, 15 Jan 2016 00:12:43 +0100 + Message-ID: <10@foo.bar.mail> + Mime-Version: 1.0 + Content-Type: text/plain; charset=UTF-8 + Content-Transfer-Encoding: quoted-printable + This post was created by email. + RAW + from_address: "discourse@bar.com", + to_addresses: "dave@bar.com;reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com", + cc_addresses: "carol@bar.com;bob@bar.com", + topic: topic, + post: post, + user: user) + + expect { process(:reply_user_not_matching_but_known) }.to change { topic.posts.count } end end diff --git a/spec/fixtures/emails/reply_user_not_matching_but_known.eml b/spec/fixtures/emails/reply_user_not_matching_but_known.eml new file mode 100644 index 0000000000..f0c3c7d724 --- /dev/null +++ b/spec/fixtures/emails/reply_user_not_matching_but_known.eml @@ -0,0 +1,11 @@ +Return-Path: +From: Bob +To: reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com +CC: Alice , carol@bar.com +Date: Fri, 15 Jan 2016 02:12:43 +0100 +Message-ID: <11@foo.bar.mail> +Mime-Version: 1.0 +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. From 3ac7d041ae1605a2d5c7b57321c773f5b230de53 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 13 Nov 2017 11:19:06 +1100 Subject: [PATCH 165/445] UX: generic onebox treats all square images as avatars and renders them smaller --- .../javascripts/pretty-text/oneboxer.js.es6 | 44 +++++++++++++++++-- lib/cooked_post_processor.rb | 21 ++++++++- lib/final_destination.rb | 10 +++-- spec/components/cooked_post_processor_spec.rb | 41 ++++++++++++++--- 4 files changed, 102 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/pretty-text/oneboxer.js.es6 b/app/assets/javascripts/pretty-text/oneboxer.js.es6 index ac84ca0d1a..7dbc43af65 100644 --- a/app/assets/javascripts/pretty-text/oneboxer.js.es6 +++ b/app/assets/javascripts/pretty-text/oneboxer.js.es6 @@ -3,6 +3,38 @@ const loadingQueue = []; const localCache = {}; const failedCache = {}; +function resolveSize(img) { + $(img).addClass('size-resolved'); + + if (img.width > 0 && img.width === img.height) { + $(img).addClass('onebox-avatar'); + } +} + +// Detect square images and apply smaller onebox-avatar class +function applySquareGenericOnebox($elem, normalizedUrl) { + if (!$elem.hasClass('whitelistedgeneric')) { + return; + } + + let $img = $elem.find('.onebox-body img.thumbnail'); + let img = $img[0]; + + // already resolved... skip + if ($img.length !== 1 || $img.hasClass('size-resolved')) { + return; + } + + if (img.complete) { + resolveSize(img, $elem, normalizedUrl); + } else { + $img.on('load.onebox', () => { + resolveSize(img, $elem, normalizedUrl); + $img.off('load.onebox'); + }); + } +} + function loadNext(ajax) { if (loadingQueue.length === 0) { timeout = null; @@ -19,8 +51,11 @@ function loadNext(ajax) { data: { url, refresh, user_id: userId }, cache: true }).then(html => { - localCache[normalize(url)] = html; - $elem.replaceWith(html); + let $html = $(html); + + localCache[normalize(url)] = $html; + $elem.replaceWith($html); + applySquareGenericOnebox($html, normalize(url)); }, result => { if (result && result.jqXHR && result.jqXHR.status === 429) { timeoutMs = 2000; @@ -53,7 +88,7 @@ export function load(e, refresh, ajax, userId, synchronous) { if (!refresh) { // If we have it in our cache, return it. const cached = localCache[normalize(url)]; - if (cached) return cached; + if (cached) return cached.prop('outerHTML'); // If the request failed, don't do anything const failed = failedCache[normalize(url)]; @@ -81,5 +116,6 @@ function normalize(url) { } export function lookupCache(url) { - return localCache[normalize(url)]; + const cached = localCache[normalize(url)]; + return cached && cached.prop('outerHTML'); } diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 781c2cb038..1f413c07ec 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -7,7 +7,7 @@ require_dependency 'pretty_text' class CookedPostProcessor include ActionView::Helpers::NumberHelper - attr_reader :cooking_options + attr_reader :cooking_options, :doc def initialize(post, opts = {}) @dirty = false @@ -180,7 +180,6 @@ class CookedPostProcessor # FastImage fails when there's no scheme absolute_url = SiteSetting.scheme + ":" + absolute_url if absolute_url.start_with?("//") - return unless is_valid_image_url?(absolute_url) # we can *always* crawl our own images @@ -331,6 +330,24 @@ class CookedPostProcessor parent_class = img.parent && img.parent["class"] if parent_class&.include?("onebox-body") && (width = img["width"].to_i) > 0 && (height = img["height"].to_i) > 0 + + # special instruction for width == height, assume we are dealing with an avatar + if (img["width"].to_i == img["height"].to_i) + found = false + parent = img + while parent = parent.parent + if parent["class"].include? "whitelistedgeneric" + found = true + break + end + end + + if found + img["class"] = img["class"].to_s + " onebox-avatar" + next + end + end + img.delete('width') img.delete('height') new_parent = img.add_next_sibling("
    ") diff --git a/lib/final_destination.rb b/lib/final_destination.rb index b290cf8376..ed821d2465 100644 --- a/lib/final_destination.rb +++ b/lib/final_destination.rb @@ -40,9 +40,7 @@ class FinalDestination @opts[:max_redirects] ||= 5 @opts[:lookup_ip] ||= lambda do |host| begin - IPSocket::getaddress(host) - rescue SocketError - nil + FinalDestination.lookup_ip(host) end end @ignored = [Discourse.base_url_no_prefix] + (@opts[:ignore_redirects] || []) @@ -272,7 +270,13 @@ class FinalDestination end def self.lookup_ip(host) + # TODO clean this up in the test suite, cause it is a mess + # if Rails.env == "test" + # STDERR.puts "WARNING FinalDestination.lookup_ip was called with host: #{host}, this is network call that should be mocked" + # end IPSocket::getaddress(host) + rescue SocketError + nil end end diff --git a/spec/components/cooked_post_processor_spec.rb b/spec/components/cooked_post_processor_spec.rb index 7247649417..0034212b9a 100644 --- a/spec/components/cooked_post_processor_spec.rb +++ b/spec/components/cooked_post_processor_spec.rb @@ -431,14 +431,45 @@ describe CookedPostProcessor do .returns("
    GANGNAM STYLE
    ") cpp.post_process_oneboxes end - - it "is dirty" do - expect(cpp).to be_dirty - end - it "inserts the onebox without wrapping p" do + expect(cpp).to be_dirty expect(cpp.html).to match_html "
    GANGNAM STYLE
    " end + end + + context ".post_process_oneboxes with square image" do + + it "generates a onebox-avatar class" do + SiteSetting.crawl_images = true + + url = 'https://square-image.com/onebox' + + body = <<~HTML + + + + + + + + HTML + + stub_request(:head, url).to_return(status: 200) + stub_request(:get , url).to_return(status: 200, body: body) + FinalDestination.stubs(:lookup_ip).returns('1.2.3.4') + + # not an ideal stub but shipping the whole image to fast image can add + # a lot of cost to this test + FastImage.stubs(:size).returns([200, 200]) + + post = Fabricate.build(:post, raw: url) + cpp = CookedPostProcessor.new(post, invalidate_oneboxes: true) + + cpp.post_process_oneboxes + + expect(cpp.doc.to_s).not_to include('aspect-image') + expect(cpp.doc.to_s).to include('onebox-avatar') + end end From 232311aa8c8e879c98dc061ac1755ee4d0dd9b0f Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 13 Nov 2017 12:08:11 +1100 Subject: [PATCH 166/445] FIX: missing short image resolution on queued posts --- .../components/composer-editor.js.es6 | 36 ++----------------- .../discourse/components/cook-text.js.es6 | 12 ++++++- .../pretty-text/image-short-url.js.es6 | 32 +++++++++++++++++ 3 files changed, 46 insertions(+), 34 deletions(-) diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 8c5dc77475..9a9b33aba0 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -13,9 +13,7 @@ import { tinyAvatar, displayErrorForUpload, getUploadMarkdown, validateUploadedFiles } from 'discourse/lib/utilities'; -import { lookupCachedUploadUrl, - lookupUncachedUploadUrls, - cacheShortUploadUrl } from 'pretty-text/image-short-url'; +import { cacheShortUploadUrl, resolveAllShortUrls } from 'pretty-text/image-short-url'; export default Ember.Component.extend({ classNameBindings: ['showToolbar:toolbar-visible', ':wmd-controls'], @@ -180,24 +178,6 @@ export default Ember.Component.extend({ $oneboxes.each((_, o) => load(o, refresh, ajax, this.currentUser.id)); }, - _loadShortUrls($images) { - const urls = _.map($images, img => $(img).data('orig-src')); - lookupUncachedUploadUrls(urls, ajax).then(() => this._loadCachedShortUrls($images)); - }, - - _loadCachedShortUrls($images) { - $images.each((idx, image) => { - let $image = $(image); - let url = lookupCachedUploadUrl($image.data('orig-src')); - if (url) { - $image.removeAttr('data-orig-src'); - if (url !== "missing") { - $image.attr('src', url); - } - } - }); - }, - _warnMentionedGroups($preview) { Ember.run.scheduleOnce('afterRender', () => { var found = this.get('warnedGroupMentions') || []; @@ -584,18 +564,8 @@ export default Ember.Component.extend({ Ember.run.debounce(this, this._loadOneboxes, $oneboxes, 450); } - // Short upload urls - let $shortUploadUrls = $('img[data-orig-src]'); - - if ($shortUploadUrls.length > 0) { - this._loadCachedShortUrls($shortUploadUrls); - - $shortUploadUrls = $('img[data-orig-src]'); - if ($shortUploadUrls.length > 0) { - // this is carefully batched so we can do an leading debounce (trigger right away) - Ember.run.debounce(this, this._loadShortUrls, $shortUploadUrls, 450, true); - } - } + // Short upload urls need resolution + resolveAllShortUrls(ajax); let inline = {}; $('a.inline-onebox-loading', $preview).each(function(index, link) { diff --git a/app/assets/javascripts/discourse/components/cook-text.js.es6 b/app/assets/javascripts/discourse/components/cook-text.js.es6 index 80ed693563..60a96295c8 100644 --- a/app/assets/javascripts/discourse/components/cook-text.js.es6 +++ b/app/assets/javascripts/discourse/components/cook-text.js.es6 @@ -1,4 +1,5 @@ import { cookAsync } from 'discourse/lib/text'; +import { ajax } from 'discourse/lib/ajax'; const CookText = Ember.Component.extend({ tagName: '', @@ -6,7 +7,16 @@ const CookText = Ember.Component.extend({ didReceiveAttrs() { this._super(...arguments); - cookAsync(this.get('rawText')).then(cooked => this.set('cooked', cooked)); + cookAsync(this.get('rawText')).then( + cooked => { + this.set('cooked', cooked); + // no choice but to defer this cause + // pretty text may only be loaded now + Em.run.next(() => + window.requireModule('pretty-text/image-short-url').resolveAllShortUrls(ajax) + ); + } + ); } }); diff --git a/app/assets/javascripts/pretty-text/image-short-url.js.es6 b/app/assets/javascripts/pretty-text/image-short-url.js.es6 index d815b46696..4efcefaec3 100644 --- a/app/assets/javascripts/pretty-text/image-short-url.js.es6 +++ b/app/assets/javascripts/pretty-text/image-short-url.js.es6 @@ -16,3 +16,35 @@ export function lookupUncachedUploadUrls(urls, ajax) { export function cacheShortUploadUrl(shortUrl, url) { _cache[shortUrl] = url; } + +function _loadCachedShortUrls($images) { + $images.each((idx, image) => { + let $image = $(image); + let url = lookupCachedUploadUrl($image.data('orig-src')); + if (url) { + $image.removeAttr('data-orig-src'); + if (url !== "missing") { + $image.attr('src', url); + } + } + }); +} + +function _loadShortUrls($images, ajax) { + const urls = _.map($images, img => $(img).data('orig-src')); + lookupUncachedUploadUrls(urls, ajax).then(() => _loadCachedShortUrls($images)); +} + +export function resolveAllShortUrls(ajax) { + let $shortUploadUrls = $('img[data-orig-src]'); + + if ($shortUploadUrls.length > 0) { + _loadCachedShortUrls($shortUploadUrls); + + $shortUploadUrls = $('img[data-orig-src]'); + if ($shortUploadUrls.length > 0) { + // this is carefully batched so we can do a leading debounce (trigger right away) + Ember.run.debounce(null, () => { _loadShortUrls($shortUploadUrls, ajax); }, 450, true); + } + } +} From 4f28c71b5082d8194129f10084738775b36b8ed3 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 13 Nov 2017 15:36:45 +1100 Subject: [PATCH 167/445] FIX: error setting tombstone bucket when set to old version --- lib/s3_helper.rb | 14 ++++++++- spec/components/s3_helper_spec.rb | 52 +++++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 3 deletions(-) diff --git a/lib/s3_helper.rb b/lib/s3_helper.rb index d1e5a0aa78..9dadd71de3 100644 --- a/lib/s3_helper.rb +++ b/lib/s3_helper.rb @@ -100,12 +100,24 @@ class S3Helper # skip trying to merge end + # in the past we has a rule that was called purge-tombstone vs purge_tombstone + # just go ahead and normalize for our bucket rules.delete_if do |r| - r.id == id + r.id.gsub('_', '-') == id.gsub('_', '-') end rules << rule + # normalize filter in rules, due to AWS library bug + rules = rules.map do |r| + r = r.to_h + prefix = r.delete(:prefix) + if prefix + r[:filter] = { prefix: prefix } + end + r + end + s3_resource.client.put_bucket_lifecycle_configuration( bucket: @s3_bucket_name, lifecycle_configuration: { diff --git a/spec/components/s3_helper_spec.rb b/spec/components/s3_helper_spec.rb index 0ee326b65b..412f0805e2 100644 --- a/spec/components/s3_helper_spec.rb +++ b/spec/components/s3_helper_spec.rb @@ -3,6 +3,54 @@ require "rails_helper" describe "S3Helper" do - # we should test something here, perhaps in a smoke test that uploads a real image - # to s3 + it "can correctly set the purge policy" do + + stub_request(:get, "http://169.254.169.254/latest/meta-data/iam/security-credentials/"). + to_return(status: 404, body: "", headers: {}) + + lifecycle = <<~XML + + + + old_rule + projectdocs/ + Enabled + + 3650 + + + + purge-tombstone + test/ + Enabled + + 3650 + + + + XML + + stub_request(:get, "https://bob.s3.amazonaws.com/?lifecycle"). + to_return(status: 200, body: lifecycle, headers: {}) + + stub_request(:put, "https://bob.s3.amazonaws.com/?lifecycle"). + with do |req| + + hash = Hash.from_xml(req.body.to_s) + rules = hash["LifecycleConfiguration"]["Rule"] + + expect(rules.length).to eq(2) + + # fixes the bad filter + expect(rules[0]["Filter"]["Prefix"]).to eq("projectdocs/") + end.to_return(status: 200, body: "", headers: {}) + + SiteSetting.enable_s3_uploads = true + SiteSetting.s3_access_key_id = "abc" + SiteSetting.s3_secret_access_key = "def" + + helper = S3Helper.new('bob', 'tomb') + helper.update_tombstone_lifecycle(100) + end + end From dfe9f70747791bc1d0e211f2d483d3d42ac2d3d2 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 13 Nov 2017 15:59:51 +1100 Subject: [PATCH 168/445] UX: warn that something must be selected with safe mode --- app/controllers/safe_mode_controller.rb | 1 + app/views/safe_mode/index.html.erb | 11 ++++++++--- config/locales/server.en.yml | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/controllers/safe_mode_controller.rb b/app/controllers/safe_mode_controller.rb index 864d8408f7..0a0b6ab4ed 100644 --- a/app/controllers/safe_mode_controller.rb +++ b/app/controllers/safe_mode_controller.rb @@ -14,6 +14,7 @@ class SafeModeController < ApplicationController if safe_mode.length > 0 redirect_to path("/?safe_mode=#{safe_mode.join("%2C")}") else + flash[:must_select] = true redirect_to safe_mode_path end end diff --git a/app/views/safe_mode/index.html.erb b/app/views/safe_mode/index.html.erb index 4908bf23b3..4a9700cf9a 100644 --- a/app/views/safe_mode/index.html.erb +++ b/app/views/safe_mode/index.html.erb @@ -6,24 +6,29 @@

    <%= submit_tag t('safe_mode.enter'), class: 'btn btn-danger' %> + <%- if flash[:must_select] %> + + <%= t 'safe_mode.must_select' %> + + <%- end %>

    <% end %>
    diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2809c813b3..c6649fd47a 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -3503,6 +3503,7 @@ en: only_official: "Disable unofficial plugins" no_plugins: "Disable all plugins" enter: "Enter Safe Mode" + must_select: "You must select at least one option to enter safe mode." wizard: title: "Discourse Setup" step: From ed0751f288d2b00a862b20c2b4ef72793f93e0db Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 13 Nov 2017 16:06:18 +1100 Subject: [PATCH 169/445] always double check there is a class first --- lib/cooked_post_processor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 1f413c07ec..077b878f22 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -336,7 +336,7 @@ class CookedPostProcessor found = false parent = img while parent = parent.parent - if parent["class"].include? "whitelistedgeneric" + if parent["class"] && parent["class"].include?("whitelistedgeneric") found = true break end From 5427ca13b726d0639777803d3447043b93fbac2f Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 13 Nov 2017 16:37:25 +1100 Subject: [PATCH 170/445] FEATURE: update rails multisite so we error out if RAILS_DB is invalid --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index c15ad160d7..ac52302970 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -271,7 +271,7 @@ GEM nokogiri (>= 1.6) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rails_multisite (1.1.1) + rails_multisite (1.1.2) activerecord (> 4.2, < 6) railties (> 4.2, < 6) railties (5.1.4) From 281e430a89bafa3640abe91a2ee3078fa437d576 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 13 Nov 2017 16:56:20 +1100 Subject: [PATCH 171/445] UX: expand parent category in full screen search results --- .../javascripts/discourse/templates/full-page-search.hbs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index 1ce6a3f4c8..163aac03fc 100644 --- a/app/assets/javascripts/discourse/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -82,7 +82,10 @@
    - {{category-link result.topic.category}} + {{#if result.topic.category.parentCategory}} + {{category-link result.topic.category.parentCategory}} + {{/if}} + {{category-link result.topic.category hideParent=true}} {{#each result.topic.tags as |tag|}} {{discourse-tag tag}} {{/each}} From cb14da4d90af2a397e0eca9393cfe1c24f43cd61 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 13 Nov 2017 17:26:02 +1100 Subject: [PATCH 172/445] FIX: stop stripping "undefined" from fullpage search --- .../discourse/controllers/full-page-search.js.es6 | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 index e4c86c0dcb..5fe60501c4 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -104,11 +104,13 @@ export default Ember.Controller.extend({ cleanTerm(term) { if (term) { SortOrders.forEach(order => { - let matches = term.match(new RegExp(`${order.term}\\b`)); - if (matches) { - this.set('sortOrder', order.id); - term = term.replace(new RegExp(`${order.term}\\b`, 'g'), ""); - term = term.trim(); + if (order.term) { + let matches = term.match(new RegExp(`${order.term}\\b`)); + if (matches) { + this.set('sortOrder', order.id); + term = term.replace(new RegExp(`${order.term}\\b`, 'g'), ""); + term = term.trim(); + } } }); } From 8a6644684924fd5388aec1bc2125f958afe3de1e Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 13 Nov 2017 17:52:15 +1100 Subject: [PATCH 173/445] FEATURE: add overflow-y auto to Markdown tables --- .../engines/discourse-markdown/table.js.es6 | 14 ++++++++++++-- app/assets/stylesheets/common/base/compose.scss | 4 ++++ spec/components/pretty_text_spec.rb | 2 ++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 index c3760799e0..27620d20bc 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 @@ -1,6 +1,15 @@ export function setup(helper) { - // this is built in now - // TODO: sanitizer needs fixing, does not properly support this yet + + helper.registerPlugin(md => { + + md.renderer.rules.table_open = function(){ + return '
    \n'; + }; + + md.renderer.rules.table_close = function(){ + return '
    \n
    '; + }; + }); // we need a custom callback for style handling helper.whiteList({ @@ -24,5 +33,6 @@ export function setup(helper) { 'tr', 'th', 'td', + 'div.md-table' ]); } diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 4d14bff26d..9cec8bcfe5 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -297,3 +297,7 @@ div.ac-wrap { color: lighten($primary, 40%); } } + +.md-table { + overflow-y: auto; +} diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 1a60ffd741..fef584a617 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -965,6 +965,7 @@ HTML MD expected = <<~HTML +
    @@ -981,6 +982,7 @@ HTML
    +
    HTML expect(PrettyText.cook(markdown)).to eq(expected.strip) From 38c103c75e2167b7fb372848c89f5dd9db18ee90 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 13 Nov 2017 18:09:24 +1100 Subject: [PATCH 174/445] correct spec --- .../pretty-text/engines/discourse-markdown/table.js.es6 | 2 +- test/javascripts/lib/pretty-text-test.js.es6 | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 index 27620d20bc..3506e66ceb 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 @@ -3,7 +3,7 @@ export function setup(helper) { helper.registerPlugin(md => { md.renderer.rules.table_open = function(){ - return '
    \n'; + return '
    \n
    \n'; }; md.renderer.rules.table_close = function(){ diff --git a/test/javascripts/lib/pretty-text-test.js.es6 b/test/javascripts/lib/pretty-text-test.js.es6 index bf2cf59427..f68de877be 100644 --- a/test/javascripts/lib/pretty-text-test.js.es6 +++ b/test/javascripts/lib/pretty-text-test.js.es6 @@ -812,7 +812,8 @@ QUnit.test("enable/disable features", assert => { assert.cookedOptions('|a|\n--\n|a|', { features: {table: false} }, ''); assert.cooked('|a|\n--\n|a|', -`
    +`
    +
    @@ -823,7 +824,8 @@ QUnit.test("enable/disable features", assert => { -
    aa
    `); + +
    `); }); QUnit.test("emoji", assert => { From 7370adeae3f539c75f5446f39b13d058bac0eb8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 13 Nov 2017 15:01:31 +0100 Subject: [PATCH 175/445] FIX: don't delete uploads referenced in drafts or queued posts when using the short_url --- app/jobs/scheduled/clean_up_uploads.rb | 5 +++-- spec/jobs/clean_up_uploads_spec.rb | 9 +++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/jobs/scheduled/clean_up_uploads.rb b/app/jobs/scheduled/clean_up_uploads.rb index 08e29d202e..46691265b7 100644 --- a/app/jobs/scheduled/clean_up_uploads.rb +++ b/app/jobs/scheduled/clean_up_uploads.rb @@ -50,8 +50,9 @@ module Jobs result = result.where("uploads.url NOT IN (?)", ignore_urls) if ignore_urls.present? result.find_each do |upload| - next if QueuedPost.where("raw LIKE '%#{upload.sha1}%'").exists? - next if Draft.where("data LIKE '%#{upload.sha1}%'").exists? + encoded_sha = Base62.encode(upload.sha1.hex) + next if QueuedPost.where("raw LIKE '%#{upload.sha1}%' OR raw LIKE '%#{encoded_sha}%'").exists? + next if Draft.where("data LIKE '%#{upload.sha1}%' OR data LIKE '%#{encoded_sha}%'").exists? upload.destroy end end diff --git a/spec/jobs/clean_up_uploads_spec.rb b/spec/jobs/clean_up_uploads_spec.rb index d04a9eda8b..73b778d014 100644 --- a/spec/jobs/clean_up_uploads_spec.rb +++ b/spec/jobs/clean_up_uploads_spec.rb @@ -127,12 +127,13 @@ describe Jobs::CleanUpUploads do it "does not delete uploads in a queued post" do upload = fabricate_upload + upload2 = fabricate_upload QueuedPost.create( queue: "uploads", state: QueuedPost.states[:new], user_id: Fabricate(:user).id, - raw: upload.sha1, + raw: "#{upload.sha1}\n#{upload2.short_url}", post_options: {} ) @@ -140,16 +141,20 @@ describe Jobs::CleanUpUploads do expect(Upload.find_by(id: @upload.id)).to eq(nil) expect(Upload.find_by(id: upload.id)).to eq(upload) + expect(Upload.find_by(id: upload2.id)).to eq(upload2) end it "does not delete uploads in a draft" do upload = fabricate_upload - Draft.set(Fabricate(:user), "test", 0, upload.sha1) + upload2 = fabricate_upload + + Draft.set(Fabricate(:user), "test", 0, "#{upload.sha1}\n#{upload2.short_url}") Jobs::CleanUpUploads.new.execute(nil) expect(Upload.find_by(id: @upload.id)).to eq(nil) expect(Upload.find_by(id: upload.id)).to eq(upload) + expect(Upload.find_by(id: upload2.id)).to eq(upload2) end it "does not delete custom emojis" do From d3baae53659f51ec4a1ca08fce118a4ce838a7b4 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Mon, 13 Nov 2017 15:20:36 +0100 Subject: [PATCH 176/445] removes whitespaces and uses scope --- app/models/incoming_email.rb | 2 ++ lib/email/receiver.rb | 4 ++-- spec/components/email/receiver_spec.rb | 22 +++++++++++----------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/app/models/incoming_email.rb b/app/models/incoming_email.rb index 2b33700a66..be5bfdce60 100644 --- a/app/models/incoming_email.rb +++ b/app/models/incoming_email.rb @@ -4,6 +4,8 @@ class IncomingEmail < ActiveRecord::Base belongs_to :post scope :errored, -> { where("NOT is_bounce AND error IS NOT NULL") } + + scope :addressed_to, -> (email) { where('incoming_emails.to_addresses ILIKE :email OR incoming_emails.cc_addresses ILIKE :email', email: "%#{email}%") } end # == Schema Information diff --git a/lib/email/receiver.rb b/lib/email/receiver.rb index 2570a0bf3a..fbf4e7f84d 100644 --- a/lib/email/receiver.rb +++ b/lib/email/receiver.rb @@ -446,8 +446,8 @@ module Email incoming_emails = IncomingEmail .joins(:post) .where('posts.topic_id = ?', email_log.topic_id) - .where('incoming_emails.to_addresses ILIKE :email OR incoming_emails.cc_addresses ILIKE :email', email: "%#{email_log.reply_key}%") - .where('incoming_emails.to_addresses ILIKE :email OR incoming_emails.cc_addresses ILIKE :email', email: "%#{user.email}%") + .addressed_to(email_log.reply_key) + .addressed_to(user.email) incoming_emails.each do |email| next unless contains_email_address?(email.to_addresses, user.email) || diff --git a/spec/components/email/receiver_spec.rb b/spec/components/email/receiver_spec.rb index 75ef299f93..39451cb49b 100644 --- a/spec/components/email/receiver_spec.rb +++ b/spec/components/email/receiver_spec.rb @@ -399,18 +399,18 @@ describe Email::Receiver do it "accepts emails with wrong reply key if the system knows about the forwareded email" do Fabricate(:incoming_email, raw: <<~RAW, - Return-Path: - From: Alice - To: dave@bar.com, reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com - CC: carol@bar.com, bob@bar.com - Subject: Hello world - Date: Fri, 15 Jan 2016 00:12:43 +0100 - Message-ID: <10@foo.bar.mail> - Mime-Version: 1.0 - Content-Type: text/plain; charset=UTF-8 - Content-Transfer-Encoding: quoted-printable + Return-Path: + From: Alice + To: dave@bar.com, reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com + CC: carol@bar.com, bob@bar.com + Subject: Hello world + Date: Fri, 15 Jan 2016 00:12:43 +0100 + Message-ID: <10@foo.bar.mail> + Mime-Version: 1.0 + Content-Type: text/plain; charset=UTF-8 + Content-Transfer-Encoding: quoted-printable - This post was created by email. + This post was created by email. RAW from_address: "discourse@bar.com", to_addresses: "dave@bar.com;reply+4f97315cc828096c9cb34c6f1a0d6fe8@bar.com", From c958d8c7e1e5051c6fbd76a7746ea578251c360b Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 13 Nov 2017 09:55:04 -0500 Subject: [PATCH 177/445] string should say silencing instead of unsilencing --- config/locales/client.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 5252945492..80a49840fd 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -3357,7 +3357,7 @@ en: deactivate_account: "Deactivate Account" deactivate_failed: "There was a problem deactivating the user." unsilence_failed: 'There was a problem unsilencing the user.' - silence_failed: 'There was a problem unsilencing the user.' + silence_failed: 'There was a problem silencing the user.' silence_confirm: 'Are you sure you want to silence this user? They will not be able to create any new topics or posts.' silence_accept: 'Yes, silence this user' bounce_score: "Bounce Score" From 55e5cfcc679acac94e802feba7ab2cc65969f2bc Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 13 Nov 2017 12:29:18 -0500 Subject: [PATCH 178/445] UX: Collapse button displayed in the wrong place --- app/assets/stylesheets/common/components/user-stream-item.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/components/user-stream-item.scss b/app/assets/stylesheets/common/components/user-stream-item.scss index 078d5fd2e4..57fd7ab4da 100644 --- a/app/assets/stylesheets/common/components/user-stream-item.scss +++ b/app/assets/stylesheets/common/components/user-stream-item.scss @@ -43,7 +43,7 @@ font-size: 1em; } - .expand-item { + .expand-item, .collapse-item { float: right; margin-right: 0.5em; color: lighten($primary, 40%); From 13c91fc7ec2a51f4ee08549575f32c3477cac3ee Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Mon, 13 Nov 2017 15:02:53 -0500 Subject: [PATCH 179/445] Update translations --- config/locales/client.ar.yml | 20 +-- config/locales/client.bs_BA.yml | 17 +- config/locales/client.cs.yml | 21 +-- config/locales/client.da.yml | 21 +-- config/locales/client.de.yml | 43 +++-- config/locales/client.el.yml | 43 +++-- config/locales/client.es.yml | 15 +- config/locales/client.et.yml | 25 +-- config/locales/client.fa_IR.yml | 21 +-- config/locales/client.fi.yml | 20 +-- config/locales/client.fr.yml | 37 ++-- config/locales/client.gl.yml | 21 +-- config/locales/client.he.yml | 21 +-- config/locales/client.id.yml | 59 ++++++- config/locales/client.it.yml | 54 +++--- config/locales/client.ja.yml | 20 +-- config/locales/client.ko.yml | 20 +-- config/locales/client.lv.yml | 21 +-- config/locales/client.nb_NO.yml | 46 +++-- config/locales/client.nl.yml | 20 +-- config/locales/client.pl_PL.yml | 20 +-- config/locales/client.pt.yml | 21 +-- config/locales/client.pt_BR.yml | 21 +-- config/locales/client.ro.yml | 21 +-- config/locales/client.ru.yml | 24 +-- config/locales/client.sk.yml | 21 +-- config/locales/client.sq.yml | 21 +-- config/locales/client.sv.yml | 23 +-- config/locales/client.te.yml | 16 +- config/locales/client.th.yml | 11 +- config/locales/client.tr_TR.yml | 165 ++++++++++++++---- config/locales/client.uk.yml | 20 +-- config/locales/client.ur.yml | 21 +-- config/locales/client.vi.yml | 21 +-- config/locales/client.zh_CN.yml | 98 ++++++----- config/locales/client.zh_TW.yml | 21 +-- config/locales/server.ar.yml | 3 - config/locales/server.bs_BA.yml | 3 - config/locales/server.cs.yml | 1 - config/locales/server.da.yml | 75 -------- config/locales/server.de.yml | 90 +++++----- config/locales/server.el.yml | 111 +++++------- config/locales/server.es.yml | 77 -------- config/locales/server.et.yml | 1 - config/locales/server.fa_IR.yml | 73 -------- config/locales/server.fi.yml | 96 ++-------- config/locales/server.fr.yml | 75 -------- config/locales/server.he.yml | 76 -------- config/locales/server.id.yml | 3 - config/locales/server.it.yml | 92 +++++----- config/locales/server.ja.yml | 6 - config/locales/server.ko.yml | 21 --- config/locales/server.nb_NO.yml | 15 +- config/locales/server.nl.yml | 30 +--- config/locales/server.pl_PL.yml | 78 +-------- config/locales/server.pt.yml | 71 -------- config/locales/server.pt_BR.yml | 13 -- config/locales/server.ro.yml | 71 -------- config/locales/server.ru.yml | 12 -- config/locales/server.sk.yml | 13 -- config/locales/server.sq.yml | 6 - config/locales/server.sv.yml | 73 -------- config/locales/server.th.yml | 1 - config/locales/server.tr_TR.yml | 39 ++--- config/locales/server.uk.yml | 2 - config/locales/server.vi.yml | 29 --- config/locales/server.zh_CN.yml | 143 ++++++--------- config/locales/server.zh_TW.yml | 78 --------- .../config/locales/server.tr_TR.yml | 84 +++++++++ .../config/locales/server.zh_CN.yml | 16 +- plugins/poll/config/locales/client.tr_TR.yml | 1 + 71 files changed, 773 insertions(+), 1915 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index f86a4b07b0..38f66f9550 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -654,7 +654,6 @@ ar: admin: "{{user}} هو مدير" moderator_tooltip: "هذا المستخدم مشرف" admin_tooltip: "هذا المستخدم مدير" - blocked_tooltip: "هذا المستخدم محظور" suspended_notice: "هذا المستخدم موقوف حتى تاريخ {{date}}" suspended_permanently: "هذا العضو موقوف." suspended_reason: "السبب:" @@ -2764,7 +2763,6 @@ ar: no_problems: "لا مشاكل." moderators: 'المشرفون:' admins: 'المدراء:' - blocked: 'محظورون:' suspended: 'موقوفون:' private_messages_short: "الرسائل" private_messages_title: "الرسائل" @@ -3310,8 +3308,6 @@ ar: change_category_settings: "تغيير إعدادات القسم" delete_category: "حذف القسم" create_category: "أنشئ قسم" - block_user: "حظر" - unblock_user: "رفع الحظر" grant_admin: "منح صلاحيات ادارية" revoke_admin: "سحب الصلاحيات الادارية" grant_moderation: "عين كمشرف" @@ -3398,7 +3394,6 @@ ar: pending: "قيد الانتظار" staff: 'الطاقم' suspended: 'موقوف' - blocked: 'محظور' suspect: 'المريبون' approved: "موافقة؟" approved_selected: @@ -3427,7 +3422,6 @@ ar: staff: "الطاقم" admins: 'الأعضاء المدراء' moderators: 'المشرفون' - blocked: 'الأعضاء المحظورين' suspended: 'أعضاء موقوفين' suspect: 'الأعضاء المريبون' reject_successful: @@ -3463,12 +3457,9 @@ ar: cant_suspend: "لا يمكن ايقاف هذا العضو" delete_all_posts: "احذف كل مشاركاته" delete_all_posts_confirm_MF: "أنت على وشك {POSTS, plural, zero {عدم حذف شيء} one {حذف مشاركة واحدة} two {حذف مشاركتين} few {حذف # مشاركات} many {حذف # مشاركة} other {حذف # مشاركة}}{TOPICS, plural, zero {} one { وموضوع واحد} two { وموضوعين} few { و# مواضيع} many {و# موضوعا} other {و# موضوع}}. أمتأكد؟" - suspend: "علّق" - unsuspend: "إلقاء التعليق" - suspended: "معلّق؟" moderator: "مراقب؟" admin: "مدير؟" - blocked: "محظور؟" + suspended: "معلّق؟" staged: "تنظيم؟" show_admin_profile: "مدير" refresh_browsers: "أجبر إنعاش المتصفح" @@ -3484,8 +3475,8 @@ ar: grant_admin_confirm: "لقد ارسلنا بريداً الكترونياً لتأكيد المدير الجديد. رجاء افتح الرسالة و اتبع التعلميات." revoke_moderation: 'سحب المراقبة' grant_moderation: 'منحة مراقبة' - unblock: 'إلغاء حظر' - block: 'حظر' + unsuspend: 'إلقاء التعليق' + suspend: 'علّق' reputation: شهرة permissions: التّصاريح activity: أنشطة @@ -3544,17 +3535,12 @@ ar: activate_failed: "حدث خطأ عند تفعيل هذا المستخدم." deactivate_account: "تعطيل الحساب" deactivate_failed: "حدث خطأ عند تعطيل هذا المستخدم." - unblock_failed: 'حدثت مشكلة في رفع المنع عن المستخدم.' - block_failed: 'حدثت مشكلة في منع المستخدم.' - block_confirm: 'هل انت متأكد من حظر هذا المستخدم؟ لن يستطيع انشاء مواضيع او ردود جديدة' - block_accept: 'نعم، امنع هذا المستخدم' bounce_score: "سجل الترويج" reset_bounce_score: label: "اعادة تعيين" title: "اعادة تعيين سجل الترويج الى 0" deactivate_explanation: "المستخدم الغير نشط يحب أن يتأكد من البريد الالكتروني" suspended_explanation: "المستخدم الموقوف لايملك صلاحية تسجيل الدخول" - block_explanation: "المستخدم الموقوف لايستطيع أن يشارك" staged_explanation: "العضو المنظم يمكنه فقط النشر عن طريق البريد الالكتروني في المواضيع المخصصه ." bounce_score_explanation: none: "لا ترويجات تم استلامها من هذا البريد الالكتروني" diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index 7f66870fa7..1206888b66 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -505,7 +505,6 @@ bs_BA: admin: "{{user}} je admin" moderator_tooltip: "This user is a moderator" admin_tooltip: "This user is an admin" - blocked_tooltip: "Korisnik je blokiran" suspended_notice: "This user is suspended until {{date}}." suspended_reason: "Reason: " github_profile: "Github" @@ -1667,7 +1666,6 @@ bs_BA: no_problems: "No problems were found." moderators: 'Moderators:' admins: 'Admins:' - blocked: 'Blocked:' suspended: 'Suspended:' private_messages_short: "PP" private_messages_title: "Privatne Poruke" @@ -2006,7 +2004,6 @@ bs_BA: active: "Active" pending: "Pending" suspended: 'Suspended' - blocked: 'Blocked' approved: "Approved?" titles: active: 'Active Users' @@ -2017,7 +2014,6 @@ bs_BA: staff: "Zaposleni" admins: 'Admin Users' moderators: 'Moderators' - blocked: 'Blocked Users' suspended: 'Suspended Users' not_verified: "Not verified" check_email: @@ -2027,17 +2023,13 @@ bs_BA: suspend_failed: "Something went wrong suspending this user {{error}}" unsuspend_failed: "Something went wrong unsuspending this user {{error}}" suspend_duration: "How long will the user be suspended for?" - suspend_duration_units: "(days)" suspend_reason_label: "Why are you suspending? This text will be visible to everyone on this user's profile page, and will be shown to the user when they try to log in. Keep it short." suspend_reason: "Reason" suspended_by: "Suspended by" delete_all_posts: "Delete all posts" - suspend: "Suspend" - unsuspend: "Unsuspend" - suspended: "Suspended?" moderator: "Moderator?" admin: "Admin?" - blocked: "Blocked?" + suspended: "Suspended?" show_admin_profile: "Admin" refresh_browsers: "Force browser refresh" refresh_browsers_message: "Message sent to all clients!" @@ -2050,8 +2042,8 @@ bs_BA: grant_admin: 'Grant Admin' revoke_moderation: 'Revoke Moderation' grant_moderation: 'Grant Moderation' - unblock: 'Unblock' - block: 'Block' + unsuspend: 'Unsuspend' + suspend: 'Suspend' reputation: Reputation permissions: Permissions activity: Activity @@ -2084,11 +2076,8 @@ bs_BA: activate_failed: "There was a problem activating the user." deactivate_account: "Deactivate Account" deactivate_failed: "There was a problem deactivating the user." - unblock_failed: 'There was a problem unblocking the user.' - block_failed: 'There was a problem blocking the user.' deactivate_explanation: "A deactivated user must re-validate their email." suspended_explanation: "A suspended user can't log in." - block_explanation: "A blocked user can't post or start topics." trust_level_change_failed: "There was a problem changing the user's trust level." suspend_modal_title: "Suspend User" trust_level_2_users: "Trust Level 2 Users" diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index 5373cd0f46..5f4d997d3a 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -527,7 +527,6 @@ cs: admin: "{{user}} je administrátor" moderator_tooltip: "Tento uživatel je moderátor" admin_tooltip: "Tento uživatel je admi" - blocked_tooltip: "Tento uživatel je zablokován." suspended_notice: "Uživatel je suspendován do {{date}}." suspended_reason: "Důvod: " github_profile: "Github" @@ -2223,7 +2222,6 @@ cs: no_problems: "Nenalezeny žádné problémy." moderators: 'Moderátoři:' admins: 'Administrátoři:' - blocked: 'Blokováno:' suspended: 'Zakázáno:' private_messages_short: "Zpráv" private_messages_title: "Zprávy" @@ -2653,8 +2651,6 @@ cs: change_category_settings: "změnit nastavení kategorie" delete_category: "smazat kategorii" create_category: "vytvořit kategorii" - block_user: "zablokuj uživatele" - unblock_user: "odblokuj uživatele" grant_admin: "udělit administrátorská práva" revoke_admin: "odebrat administrátorská práva" grant_moderation: "udělit moderátorská práva" @@ -2711,7 +2707,6 @@ cs: pending: "Čeká na schválení" staff: 'Redakce' suspended: 'Zakázaní' - blocked: 'Blokovaní' suspect: 'Podezřelí' approved: "Schválen?" approved_selected: @@ -2734,7 +2729,6 @@ cs: staff: "Redakce" admins: 'Administrátoři' moderators: 'Moderátoři' - blocked: 'Blokovaní uživatelé' suspended: 'Zakázaní uživatelé' suspect: 'Podezřelí uživatelé' reject_successful: @@ -2753,18 +2747,14 @@ cs: suspend_failed: "Nastala chyba při zakazování uživatele {{error}}" unsuspend_failed: "Nastala chyba při povolování uživatele {{error}}" suspend_duration: "Jak dlouho má zákaz platit? (dny)" - suspend_duration_units: "(days)" suspend_reason_label: "Why are you suspending? This text will be visible to everyone on this user's profile page, and will be shown to the user when they try to log in. Keep it short." suspend_reason: "Reason" suspended_by: "Suspended by" delete_all_posts: "Smazat všechny příspěvky" delete_all_posts_confirm_MF: "Chystáte se smazat {POSTS, plural, one {1 příspěvek} other {# příspěvků}} and {TOPICS, plural, one {1 téma} other {# témat}}. Jste si jistí?" - suspend: "Zakázat" - unsuspend: "Povolit" - suspended: "Zakázán?" moderator: "Moderátor?" admin: "Administrátor?" - blocked: "Zablokovaný?" + suspended: "Zakázán?" show_admin_profile: "Administrace" refresh_browsers: "Vynutit obnovení prohlížeče" refresh_browsers_message: "Zpráva odeslána všem klientům!" @@ -2777,8 +2767,8 @@ cs: grant_admin: 'Udělit administrátorská práva' revoke_moderation: 'Odebrat moderátorská práva' grant_moderation: 'Udělit moderátorská práva' - unblock: 'Odblokovat' - block: 'Zablokovat' + unsuspend: 'Povolit' + suspend: 'Zakázat' reputation: Reputace permissions: Oprávnění activity: Aktivita @@ -2828,17 +2818,12 @@ cs: activate_failed: "Nasstal problém při aktivování tohoto uživatele." deactivate_account: "Deaktivovat účet" deactivate_failed: "Nastal problém při deaktivování tohoto uživatele." - unblock_failed: 'Nastal problém při odblokování uživatele.' - block_failed: 'Nastal problém při blokování uživatele.' - block_confirm: 'Jste si jistí, že chcete zablokovat tohoto uživatele? Nebudou poté oprávněni vytvářet žádné témata ani příspěvky.' - block_accept: 'Ano, zablokuj tohoto uživatele:' bounce_score: "Bounce skóre" reset_bounce_score: label: "obnovit výchozí" title: "Resetovat bounce skóre zpět na 0" deactivate_explanation: "Uživatel bude muset znovu potvrdit emailovou adresu." suspended_explanation: "Zakázaný uživatel se nemůže přihlásit." - block_explanation: "Zablokovaný uživatel nemůže přispívat nebo vytvářet nová témata." trust_level_change_failed: "Nastal problém při změně důveryhodnosti uživatele." suspend_modal_title: "Suspend User" trust_level_2_users: "Uživatelé důvěryhodnosti 2" diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 87837d6bcc..173272feae 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -515,7 +515,6 @@ da: admin: "{{user}} er admin" moderator_tooltip: "Denne bruger er moderator" admin_tooltip: "Denne bruger er administrator" - blocked_tooltip: "Brugeren er blokeret" suspended_notice: "Denne bruger er suspenderet indtil {{date}}." suspended_reason: "Begrundelse: " github_profile: "Github" @@ -2216,7 +2215,6 @@ da: no_problems: "Ingen problemer fundet." moderators: 'Moderatorer:' admins: 'Admins:' - blocked: 'Blokeret:' suspended: 'Suspenderet:' private_messages_short: "Beskeder" private_messages_title: "Beskeder" @@ -2745,8 +2743,6 @@ da: change_category_settings: "ret kategori-indstillinger" delete_category: "slet kategori" create_category: "opret kategori" - block_user: "bloker bruger" - unblock_user: "fjern blokering af bruger" grant_admin: "tildel admin" revoke_admin: "fjern admin" grant_moderation: "tildel moderation" @@ -2813,7 +2809,6 @@ da: pending: "Afventer" staff: 'Personale' suspended: 'Suspenderet' - blocked: 'Blokeret' suspect: 'Mistænkt' approved: "Godkendt?" approved_selected: @@ -2834,7 +2829,6 @@ da: staff: "Personale" admins: 'Admin-brugere' moderators: 'Moderatorer' - blocked: 'Blokerede brugere' suspended: 'Suspenderede brugere' suspect: 'Mistænkte brugere' reject_successful: @@ -2851,18 +2845,14 @@ da: suspend_failed: "Noget gik galt ved suspenderingen af denne bruger {{error}}" unsuspend_failed: "Noget gik galt ved ophævningen af denne brugers suspendering {{error}}" suspend_duration: "Hvor lang tid skal brugeren være suspenderet?" - suspend_duration_units: "(dage)" suspend_reason_label: "Hvorfor suspenderer du? Denne tekst er synlig for alle på brugerens profilside, og vises til brugeren når de prøver at logge ind. Fat dig i korthed." suspend_reason: "Begrundelse" suspended_by: "Suspenderet af" delete_all_posts: "Slet alle indlæg" delete_all_posts_confirm_MF: "Du er ved at slette {POSTS, plural, one {1 post} other {# posts}} og {TOPICS, plural, one {1 topic} other {# topics}}. Er du sikker?" - suspend: "Suspendér" - unsuspend: "Ophæv suspendering" - suspended: "Suspenderet?" moderator: "Moderator?" admin: "Admin?" - blocked: "Blokeret?" + suspended: "Suspenderet?" staged: "Midlertidig?" show_admin_profile: "Admin" refresh_browsers: "Gennemtving browser refresh" @@ -2878,8 +2868,8 @@ da: grant_admin_confirm: "Vi har sendt dig en e-mail for at bekræfte den ny administrator. Åbn den venligst og følg instrukserne." revoke_moderation: 'Fratag moderation' grant_moderation: 'Tildel moderation' - unblock: 'Ophæv blokering' - block: 'Blokér' + unsuspend: 'Ophæv suspendering' + suspend: 'Suspendér' reputation: Omdømme permissions: Tilladelser activity: Aktivitet @@ -2926,17 +2916,12 @@ da: activate_failed: "Der opstod et problem ved aktivering af brugeren." deactivate_account: "Deaktivér konto" deactivate_failed: "Der opstod et problem ved deaktivering af brugeren." - unblock_failed: 'Der opstod et problem ved ophævelsen af brugerens blokering.' - block_failed: 'Der opstod et problem ved blokering af brugeren.' - block_confirm: 'Er du sikker på, at du vil blokere brugeren? Bruger kan ikke længere oprette emner eller indlæg.' - block_accept: 'Ja, bloker brugeren' bounce_score: "Bounce Score" reset_bounce_score: label: "Nulstil" title: "Nulstil \"bounce score\" til 0" deactivate_explanation: "En deaktiveret bruger skal genvalidere deres e-mail." suspended_explanation: "En suspenderet bruger kan ikke logge ind." - block_explanation: "En blokeret bruger kan ikke oprette indlæg eller starte emner." staged_explanation: "En midlertidig bruger kan kun svare via email i bestemte emner." bounce_score_explanation: none: "Ingen \"bounces\" er for nyligt modtaget fra denne email." diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index 1e768b1179..2a97f303f9 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -526,6 +526,7 @@ de: disable: "Benachrichtigungen deaktivieren" enable: "Benachrichtigungen aktivieren" each_browser_note: "Hinweis: Du musst diese Einstellung in jedem von dir verwendeten Browser ändern." + dismiss: 'Ablehnen' dismiss_notifications: "Alles ausblenden" dismiss_notifications_tooltip: "Alle ungelesenen Benachrichtigungen als gelesen markieren" first_notification: "Deine erste Benachrichtigung! Wähle sie aus um fortzufahren." @@ -540,7 +541,7 @@ de: admin: "{{user}} ist ein Administrator" moderator_tooltip: "Dieser Benutzer ist ein Moderator" admin_tooltip: "Dieser Benutzer ist ein Administrator" - blocked_tooltip: "Dieser Benutzer wird blockiert." + silenced_tooltip: "Der Benutzer ist stummgeschaltet" suspended_notice: "Dieser Benutzer ist bis zum {{date}} gesperrt." suspended_permanently: "Der Benutzer ist gesperrt." suspended_reason: "Grund: " @@ -592,6 +593,7 @@ de: undo_revoke_access: "Entziehe Zugriffsrecht widerrufen" api_approved: "Genehmigt:" theme: "Design" + home: "Standard-Startseite" staff_counters: flags_given: "hilfreiche Meldungen" flagged_posts: "gemeldete Beiträge" @@ -904,7 +906,8 @@ de: first_post: Erster Beitrag mute: Stummschalten unmute: Stummschaltung aufheben - last_post: Letzter Beitrag + last_post: Geschrieben + time_read: Gelesen last_reply_lowercase: letzte Antwort replies_lowercase: one: Antwort @@ -1041,6 +1044,7 @@ de: default_header_text: Auswählen… no_content: Keine Treffer gefunden filter_placeholder: Suchen… + create: "Erstelle {{content}}" emoji_picker: filter_placeholder: Emoji suchen people: Personen @@ -1096,6 +1100,7 @@ de: cancel: "Abbrechen" create_topic: "Thema erstellen" create_pm: "Nachricht" + create_whisper: "Flüstern" title: "Oder drücke Strg+Eingabetaste" users_placeholder: "Benutzer hinzufügen" title_placeholder: "Um was geht es in dieser Diskussion? Schreib einen kurzen Satz." @@ -2343,7 +2348,7 @@ de: no_problems: "Es wurden keine Probleme gefunden." moderators: 'Moderatoren:' admins: 'Administratoren:' - blocked: 'Blockiert:' + silenced: 'Stummgeschaltet:' suspended: 'Gesperrt:' private_messages_short: "Nachr." private_messages_title: "Nachrichten" @@ -2425,6 +2430,7 @@ de: was_edited: "Beitrag wurde nach der ersten Meldung bearbeitet" previous_flags_count: "Dieses Thema wurde bereits {{count}} mal gemeldet." show_details: "Zeige Meldungsdetails" + details: "Details" flagged_topics: topic: "Thema" type: "Art" @@ -2885,8 +2891,8 @@ de: change_category_settings: "Kategorieeinstellungen ändern" delete_category: "Kategorie löschen" create_category: "Kategorie erstellen" - block_user: "Benutzer blockieren" - unblock_user: "Blockierung von Benutzer aufheben" + silence_user: "Benutzer stummschalten" + unsilence_user: "Benutzer nicht mehr stummschalten" grant_admin: "Administration gewähren" revoke_admin: "Administration entziehen" grant_moderation: "Moderation gewähren" @@ -2981,7 +2987,7 @@ de: pending: "Genehmigung" staff: 'Team' suspended: 'Gesperrt' - blocked: 'Blockiert' + silenced: 'Stummgeschaltet' suspect: 'Verdächtig' approved: "Genehmigt?" approved_selected: @@ -3002,7 +3008,7 @@ de: staff: "Team" admins: 'Administratoren' moderators: 'Moderatoren' - blocked: 'Blockierte Benutzer' + silenced: 'Stummgeschaltete Benutzer' suspended: 'Gesperrte Benutzer' suspect: 'Verdächtige Benutzer' reject_successful: @@ -3030,12 +3036,12 @@ de: cant_suspend: "Der Benutzer kann nicht gesperrt werden." delete_all_posts: "Lösche alle Beiträge" delete_all_posts_confirm_MF: "Du wirst {POSTS, plural, one {einen Beitrag} other {# Beiträge}} und {TOPICS, plural, one {ein Thema} other {# Themen}} löschen. Bist du dir sicher?" - suspend: "Sperren" - unsuspend: "Entsperren" - suspended: "Gesperrt?" + silence: "Stummschalten" + unsilence: "Stummschaltung aufheben" + silenced: "Stummgeschaltet?" moderator: "Moderator?" admin: "Administrator?" - blocked: "Geblockt?" + suspended: "Gesperrt?" staged: "Vorbereitet?" show_admin_profile: "Administration" refresh_browsers: "Aktualisierung im Browser erzwingen" @@ -3051,8 +3057,8 @@ de: grant_admin_confirm: "Wir haben dir eine E-Mail geschickt, um den neuen Administrator zu überprüfen. Bitte öffne sie und folge den Anweisungen." revoke_moderation: 'Moderationsrechte entziehen' grant_moderation: 'Moderationsrechte vergeben' - unblock: 'Blockierung aufheben' - block: 'Blockieren' + unsuspend: 'Entsperren' + suspend: 'Sperren' reputation: Reputation permissions: Berechtigungen activity: Aktivität @@ -3099,10 +3105,10 @@ de: activate_failed: "Beim Aktivieren des Benutzers ist ein Fehler aufgetreten." deactivate_account: "Benutzer deaktivieren" deactivate_failed: "Beim Deaktivieren des Benutzers ist ein Fehler aufgetreten." - unblock_failed: 'Beim Aufheben der Blockierung des Benutzers ist ein Fehler aufgetreten.' - block_failed: 'Beim Blocken des Benutzers ist ein Fehler aufgetreten.' - block_confirm: 'Bist du sicher, dass du diesen Benutzer blockieren willst? Der Benutzer wird keine Möglichkeit mehr haben, Themen oder Beiträge zu erstellen.' - block_accept: 'Ja, diesen Benutzer blockieren.' + unsilence_failed: 'Beim Aufheben der Stummschaltung ist ein Fehler aufgetreten.' + silence_failed: 'Beim Aufheben der Stummschaltung ist ein Fehler aufgetreten.' + silence_confirm: 'Bist du sicher, dass du diesen Benutzer stummschalten möchtest? Der Benutzer wird keine Möglichkeit mehr haben, Themen oder Beiträge zu erstellen.' + silence_accept: 'Ja, diesen Benutzer stummschalten' bounce_score: "Anzahl unzustellbarer E-Mails" reset_bounce_score: label: "Zurücksetzen" @@ -3110,7 +3116,7 @@ de: visit_profile: "Besuche die Benutzer-Einstellungen, um dieses Profil zu bearbeiten" deactivate_explanation: "Ein deaktivierter Benutzer muss seine E-Mail-Adresse erneut bestätigen." suspended_explanation: "Ein gesperrter Benutzer kann sich nicht anmelden." - block_explanation: "Ein geblockter Benutzer kann keine Themen erstellen oder Beiträge veröffentlichen." + silence_explanation: "Ein stummgeschalteter Benutzer kann keine Beiträge schreiben oder Themen beginnen." staged_explanation: "Ein vorbereiteter Benutzer kann nur per E-Mail und nur in bestimmten Themen schreiben." bounce_score_explanation: none: "Keine unzustellbaren E-Mails an diese Adresse in letzter Zeit." @@ -3228,6 +3234,7 @@ de: developer: 'Entwickler' embedding: "Einbettung" legal: "Rechtliches" + api: 'API-Schnittstelle' user_api: 'Benutzer API' uncategorized: 'Sonstiges' backups: "Backups" diff --git a/config/locales/client.el.yml b/config/locales/client.el.yml index d2d6eb2429..6ce5ecf9da 100644 --- a/config/locales/client.el.yml +++ b/config/locales/client.el.yml @@ -526,6 +526,7 @@ el: disable: "Απενεργοποίηση Ειδοποιήσεων" enable: "Ενεργοποίηση Ειδοποιήσεων" each_browser_note: "Σημείωση: Θα πρέπει να αλλάξετε αυτή τη ρύθμιση σε κάθε browser που χρησιμοποιείτε." + dismiss: 'Απόρριψη' dismiss_notifications: "Απόρριψη Όλων" dismiss_notifications_tooltip: "Όλες οι αδιάβαστες ειδοποιήσεις να χαρακτηριστούν διαβασμένες" first_notification: "Η πρώτη σου ειδοποίηση! Επίλεξε την για να ξεκινήσεις." @@ -540,7 +541,7 @@ el: admin: "Ο/Η {{user}} είναι διαχειριστής" moderator_tooltip: "Αυτός ο χρήστης είναι συντονιστής" admin_tooltip: "Αυτός ο χρήστης είναι διαχειριστής" - blocked_tooltip: "Ο χρήστης είναι μπλοκαρισμένος" + silenced_tooltip: "Χρήστης σε σιγή" suspended_notice: "Αυτός ο χρήστης είναι σε αποβολή μέχρι τις {{date}}." suspended_permanently: "Ο χρήστης είναι αποβλημένος." suspended_reason: "Αιτιολογία:" @@ -592,6 +593,7 @@ el: undo_revoke_access: "Απενεργοποίηση Ανάκλησης Πρόσβασης" api_approved: "Εγκεκριμένο:" theme: "Θέμα" + home: "Προεπιλεγμένη Αρχική Σελίδα" staff_counters: flags_given: "χρήσιμες σημάνσεις" flagged_posts: "επισημασμένες αναρτήσεις" @@ -904,7 +906,8 @@ el: first_post: Πρώτη ανάρτηση mute: Σίγαση unmute: Αναίρεση σίγασης - last_post: Τελευταία ανάρτηση + last_post: Αναρτήθηκε + time_read: Διαβάστηκε last_reply_lowercase: τελευταία απάντηση replies_lowercase: one: απάντηση @@ -1041,6 +1044,7 @@ el: default_header_text: Επίλεξε... no_content: Δεν βρέθηκαν αποτελέσματα filter_placeholder: Αναζήτηση... + create: "Δημιουργία {{content}}" emoji_picker: filter_placeholder: Αναζήτηση για emoji people: Άνθρωποι @@ -1096,6 +1100,7 @@ el: cancel: "Ακύρωση" create_topic: "Δημιουργία Νήματος" create_pm: "Μήνυμα" + create_whisper: "Ψυθίρισμα" title: "Ή πάτα Ctrl+Enter" users_placeholder: "Προσθήκη χρήστη" title_placeholder: "Τι αφορά αυτή η συζήτησης σε μία σύντομη πρόταση;" @@ -2347,7 +2352,7 @@ el: no_problems: "Δεν βρέθηκε κανένα πρόβλημα." moderators: 'Συντονιστές: ' admins: 'Διαχειριστές:' - blocked: 'Αποκλεισμένοι:' + silenced: 'Σιγήθηκαν:' suspended: 'Αποβλημένοι:' private_messages_short: "Μνμτ." private_messages_title: "Μηνύματα" @@ -2429,6 +2434,7 @@ el: was_edited: "Η ανάρτηση έχει υποστεί επεξεργασία μετά την πρώτη επισήμανση" previous_flags_count: "Αυτό το νήμα έχει επισημανθεί ήδη {{count}} φορές" show_details: "Εμφάνιση λεπτομερειών επισήμανσης" + details: "λεπτομέρειες" flagged_topics: topic: "Νήμα" type: "Τύπος" @@ -2889,8 +2895,8 @@ el: change_category_settings: "αλλαγή ρυθμίσεων κατηγορίας" delete_category: "διαγραφή κατηγορίας" create_category: "δημιουργία κατηγορίας" - block_user: "αποκλεισμός χρήστη" - unblock_user: "κατάργηση αποκλεισμού χρήστη" + silence_user: "σίγαση χρήστη" + unsilence_user: "αναίρεση σίγασης" grant_admin: "χορήγηση διαχειριστή" revoke_admin: "ανάκληση διαχειριστή" grant_moderation: "χορήγηση συντονιστή" @@ -2985,7 +2991,7 @@ el: pending: "Εκκρεμή" staff: 'Συνεργάτες' suspended: 'Αποβλημένοι' - blocked: 'Αποκλεισμένο' + silenced: 'Σιγήθηκαν' suspect: 'Ύποπτο' approved: "Εγκεκριμένο;" approved_selected: @@ -3006,7 +3012,7 @@ el: staff: "Συνεργάτες" admins: 'Διαχειριστές' moderators: 'Συντονιστές' - blocked: 'Αποκλεισμένοι Χρήστες' + silenced: 'Σιγημένοι Χρήστες' suspended: 'Αποβλημένοι Χρήστες' suspect: 'Ύποπτοι χρήστες' reject_successful: @@ -3034,12 +3040,12 @@ el: cant_suspend: "Ο χρήστης αυτός δεν μπορεί να αποβληθεί." delete_all_posts: "Σβήσε όλες τις αναρτήσεις" delete_all_posts_confirm_MF: "Πρόκειται να διαγράψεις {POSTS, plural, one {1 ανάρτηση} other {# αναρτήσεις}} and {TOPICS, plural, one {1 νήμα} other {# νήματα}}. Είσαι σίγουρος;" - suspend: "Αποβολή" - unsuspend: "Αναίρεση αποβολής" - suspended: "Αποβλημένος;" + silence: "Σίγαση" + unsilence: "Αναίρεση Σίγασης" + silenced: "Σε Σιγή;" moderator: "Συντονιστής;" admin: "Διαχειριστής;" - blocked: "Αποκλείστηκε; " + suspended: "Αποβλημένος;" staged: "Στάδιο Μετάβασης;" show_admin_profile: "Διαχείριση" refresh_browsers: "Να ανανεωθεί αναγκαστικά η σελίδα" @@ -3055,8 +3061,8 @@ el: grant_admin_confirm: "Έχουμε στείλει ένα email για να επιβεβαιώσουμε τον νέο διαχειριστή. Παρακαλούμε ανοίξτε το και ακολουθήστε τις οδηγίες." revoke_moderation: 'Ανάκληση δικαιωμάτων συντονιστή' grant_moderation: 'Απόδοση δικαιωμάτων συντονιστή' - unblock: 'Αναίρεση αποκλεισμού' - block: 'Αποκλεισμός' + unsuspend: 'Αναίρεση αποβολής' + suspend: 'Αποβολή' reputation: Φήμη permissions: Δικαιώματα activity: Δραστηριότητα @@ -3103,10 +3109,10 @@ el: activate_failed: "Παρουσιάστηκε πρόβλημα κατά την ενεργοποίηση του χρήστη." deactivate_account: "Απενεργοποίηση Λογαριασμού" deactivate_failed: "Παρουσιάστηκε πρόβλημα κατά την απενεργοποίηση του χρήστη." - unblock_failed: 'Παρουσιάστηκε πρόβλημα κατά την άρση αποκλεισμού του χρήστη.' - block_failed: 'Παρουσιάστηκε πρόβλημα κατά τον αποκλεισμό του χρήστη.' - block_confirm: 'Είσαι βέβαιος πως θέλεις να αποκλείσεις αυτόν τον χρήστη; Δε θα μπορεί να δημιουργήσει καινούρια νήματα ή αναρτήσεις.' - block_accept: 'Ναι, αποκλεισμός του χρήστη' + unsilence_failed: 'Υπήρξε ένα πρόβλημα κατά την αναίρεση της σίγασης του χρήστη.' + silence_failed: 'Υπήρξε ένα πρόβλημα κατά την σίγαση του χρήστη' + silence_confirm: 'Θέλετε σίγουρα να σιγήσετε τον χρήστη; Δεν θα μπορεί να δημιουργήσει νέα νήματα ή αναρτήσεις.' + silence_accept: 'Ναι, σίγαση του χρήστη' bounce_score: "Bounce Score" reset_bounce_score: label: "Επαναφορά" @@ -3114,7 +3120,7 @@ el: visit_profile: "Επισκεφθείτε την σελίδα προτιμήσεων του χρήστη για να επεξεργαστείτε το προφίλ του" deactivate_explanation: "Ένας απενεργοποιημένος χρήστης πρέπει να επιβεβαιώσει ξανά τη διεύθυνση του ηλεκτρονικού του ταχυδρομείου." suspended_explanation: "Ένας χρήστης που έχει αποβληθεί δεν γίνεται να συνδεθεί." - block_explanation: "Ένας απενεργοποιημένος χρήστης δεν μπορεί να αναρτήσει ή να δημιουργεί νήματα" + silence_explanation: "Ένας χρήστης σε σιγή δεν μπορεί να αναρτήσει ή να ξεκινήσει νήματα." staged_explanation: "Ένας χρήστης σε στάδιο μετάβασης μπορεί να κάνει αναρτήσεις μέσω email και σε συγκεκριμένα θέματα. " bounce_score_explanation: none: "Κανένα επιστρεφόμενο μήνυμα δεν ελήφθη από αυτή τη διεύθυνση email." @@ -3232,6 +3238,7 @@ el: developer: 'Developer' embedding: "Ενσωμάτωση" legal: "Νομικά" + api: 'API' user_api: 'User API' uncategorized: 'Άλλα' backups: "Αντίγραφα ασφαλέιας" diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 8def12123a..a93159b604 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -528,7 +528,6 @@ es: admin: "{{user}} es un administrador" moderator_tooltip: "Este usuario es un moderador" admin_tooltip: "Este usuario es un administrador" - blocked_tooltip: "El usuario está bloqueado" suspended_notice: "Este usuario ha sido suspendido hasta {{date}}." suspended_permanently: "Este usuario está suspendido." suspended_reason: "Causa: " @@ -3001,12 +3000,9 @@ es: cant_suspend: "Este usuario no puede ser suspendido." delete_all_posts: "Eliminar todos los posts" delete_all_posts_confirm_MF: "Estás a punto de eliminar {POSTS, plural, one {1 post} other {# posts}} y {TOPICS, plural, one {1 topic} other {# topics}}. ¿Seguro?" - suspend: "Suspender" - unsuspend: "Quitar suspensión" - suspended: "¿Suspendido?" moderator: "¿Moderador?" admin: "¿Administrador?" - blocked: "¿Bloqueado?" + suspended: "¿Suspendido?" staged: "¿Provisional?" show_admin_profile: "Administrador" refresh_browsers: "Forzar recarga del navegador" @@ -3022,8 +3018,8 @@ es: grant_admin_confirm: "Te hemos enviado un e-mail para verificar el nuevo administrador. Por favor abre el correo y sigue las instrucciones." revoke_moderation: 'Revocar moderación' grant_moderation: 'Conceder moderación' - unblock: 'Desbloquear' - block: 'Bloquear' + unsuspend: 'Quitar suspensión' + suspend: 'Suspender' reputation: Reputación permissions: Permisos activity: Actividad @@ -3070,10 +3066,6 @@ es: activate_failed: "Ha habido un problem activando el usuario." deactivate_account: "Desactivar cuenta" deactivate_failed: "Ha habido un problema desactivando el usuario." - unblock_failed: 'Ha habido un problema desbloqueando el usuario.' - block_failed: 'Ha habido un problema bloqueando el usuario.' - block_confirm: '¿Seguro que quieres bloquear a este usuario? No podrá crear nuevos temas ni publicar posts.' - block_accept: 'Sí, bloquear este usuario' bounce_score: "Puntuación de rebote" reset_bounce_score: label: "Restablecer" @@ -3081,7 +3073,6 @@ es: visit_profile: "Visita esta página de preferencias del usuario para editar el perfil" deactivate_explanation: "Un usuario desactivado debe rehabilitar su dirección de correo." suspended_explanation: "Un usuario suspendido no puede ingresar al sitio." - block_explanation: "Un usuario bloqueado no puede publicar posts ni crear temas." staged_explanation: "Un usuario provisional solo puede publicar por email en temas específicos." bounce_score_explanation: none: "No se han recibido rebotes recientemente desde ese email." diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index 5f1b56a94c..22d06c211c 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -506,7 +506,6 @@ et: admin: "{{user}} on admin" moderator_tooltip: "See kasutaja on moderaator" admin_tooltip: "See kasutaja on admin" - blocked_tooltip: "See kasutaja on blokeeritud" suspended_notice: "Selle kasutaja ligipääs on ajutiselt peatatud kuni {{date}}." suspended_reason: "Põhjus:" github_profile: "Github" @@ -2127,7 +2126,6 @@ et: no_problems: "Probleeme ei tuvastatud." moderators: 'Moderaatorid:' admins: 'Adminnid:' - blocked: 'Blokeeritud:' suspended: 'Peatatud:' private_messages_short: "Sõnumid" private_messages_title: "Sõnumid" @@ -2160,8 +2158,6 @@ et: by: "autor" flags: title: "Lipud" - old: "Vana" - active: "Aktiivne" agree: "Nõustun" agree_title: "Kinnita see tähis kui korrektne ja kehtiv" agree_flag_modal_title: "Nõustun ning..." @@ -2171,11 +2167,9 @@ et: agree_flag_restore_post_title: "Taasta see postitus" agree_flag: "Nõustu tähisega" agree_flag_title: "Nõustu tähisega ja ära postitust muuda" - defer_flag: "Lükka edasi" defer_flag_title: "Eemalda see tähis: praegu ei ole vaja sekkuda." delete: "Kustuta" delete_title: "Kustuta postitus, millele see tähis viitab." - delete_post_defer_flag: "Kustuta postitus ja lükka tähis edasi" delete_post_defer_flag_title: "Kustuta postitus; kui on ainus, kustuta teema" delete_post_agree_flag: "Kustuta postitus ja nõustu tähisega" delete_post_agree_flag_title: "Kustuta postitus; kui on ainus, kustuta teema" @@ -2586,8 +2580,6 @@ et: change_category_settings: "muuda foorumi sätteid" delete_category: "kustuta foorum" create_category: "loo foorum" - block_user: "blokeeri kasutaja" - unblock_user: "vabasta kasutaja blokeeringust" grant_admin: "anna admin-õigus" revoke_admin: "võta admin-õigus ära" grant_moderation: "anna modereerimisõigus" @@ -2654,7 +2646,6 @@ et: pending: "Ootel" staff: 'Meeskond' suspended: 'Peatatud' - blocked: 'Blokeeritud' suspect: 'Kahtlane' approved: "Heakskiidetud?" approved_selected: @@ -2675,7 +2666,6 @@ et: staff: "Meeskond" admins: 'Admin-kasutajad' moderators: 'Moderaatorid' - blocked: 'Blokeeritud kasutajad' suspended: 'Peatatud kasutajad' suspect: 'Kahtlased kasutajad' reject_successful: @@ -2692,18 +2682,14 @@ et: suspend_failed: "Selle kasutaja peatamisel tekkis viga {{error}}" unsuspend_failed: "Selle kasutaja taaslubamisel tekkis viga {{error}}" suspend_duration: "Kauaks see kasutaja peatatakse?" - suspend_duration_units: "(päeva)" suspend_reason_label: "Peatamise põhjus? See tekst on nähtav kõigile selle kasutaja profiililehel, samuti kuvatakse kasutajale, kui ta üritab sisse logida. Tee lühidalt." suspend_reason: "Põhjus" suspended_by: "Peatati kasutaja poolt" delete_all_posts: "Kustuta kõik postitused" delete_all_posts_confirm_MF: "Sa oled kustutamas {POSTS, plural, one {1 post} other {# posts}} and {TOPICS, plural, one {1 topic} other {# topics}}. Oled Sa kindel?" - suspend: "Peata" - unsuspend: "Taasluba" - suspended: "Peatatud?" moderator: "Moderaator?" admin: "Admin?" - blocked: "Blokeeritud?" + suspended: "Peatatud?" staged: "Ettevalmistamisel?" show_admin_profile: "Admin" refresh_browsers: "Sunni brauserit värskendama" @@ -2718,8 +2704,8 @@ et: grant_admin: 'Anna admini õigused' revoke_moderation: 'Eemalda modereerimise õigused' grant_moderation: 'Anna modereerimise õigused' - unblock: 'Eemalda blokeering' - block: 'Blokeeri' + unsuspend: 'Taasluba' + suspend: 'Peata' reputation: Maine permissions: Õigused activity: Tegevused @@ -2766,17 +2752,12 @@ et: activate_failed: "Viga kasutaja aktiveerimisel." deactivate_account: "Deaktiveeri konto" deactivate_failed: "Viga kasutaja deaktiveerimisel." - unblock_failed: 'Viga kasutaja blokeeringu eemaldamisel.' - block_failed: 'Viga kasutaja blokeerimisel.' - block_confirm: 'Oled Sa kindel, et soovid seda kasutajat blokeerida? Kasutaja ei saa peale seda luua ühtegi teemat ega postitust.' - block_accept: 'Jah, blokeeri see kasutaja' bounce_score: "Väljaviskamiste skoor" reset_bounce_score: label: "Lähtesta" title: "Lähtesta väljaviskamiste skoor tagasi 0" deactivate_explanation: "Deaktiveeritud kasutaja peab valideerima oma meiliaadressi." suspended_explanation: "Peatatud kasutaja ei saa sisse logida." - block_explanation: "Blokeeritud kasutaja ei saa postitada ega alustada teemasid." staged_explanation: "Ettevalmistamisel oleval kasutajal on lubatud postitada ettemääratud teemadesse vaid meili teel." bounce_score_explanation: none: "Sellelt meiliaadressilt pole viimasel ajal tagasipõrgatusi saabunud." diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index e54fe0fe4a..d5c32a495d 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -502,7 +502,6 @@ fa_IR: admin: "{{user}} یک مدیر ارشد است" moderator_tooltip: "این کاربر یک مدیر است" admin_tooltip: "این کاربر یک ادمین است" - blocked_tooltip: "این کاربر مسدود شده است" suspended_notice: "این کاربر تا {{date}} در وضعیت معلق است." suspended_permanently: "این کاربر معلق شده." suspended_reason: "دلیل: " @@ -2167,7 +2166,6 @@ fa_IR: no_problems: "هیچ مشکلات پیدا نشد." moderators: 'مدیران:' admins: 'مدیران ارشد:' - blocked: 'مسدود شده‌ها:' suspended: 'تعلیق شده:' private_messages_short: "پیام‌ها" private_messages_title: "پیام‌ها" @@ -2698,8 +2696,6 @@ fa_IR: change_category_settings: "تغییر تنظیمات دسته‌بندی" delete_category: "حذف دسته‌بندی" create_category: "ساخت دسته‌بندی" - block_user: "بستن دسترسی کاربر" - unblock_user: "باز کردن دسترسی کاربر" grant_admin: "اعطای مقام مدیر ارشد" revoke_admin: "برداشتن مقام مدیر ارشد" grant_moderation: "اعطای مقام مدیریت" @@ -2766,7 +2762,6 @@ fa_IR: pending: "در انتظار" staff: 'همکاران' suspended: 'تعلیق شد ' - blocked: 'مسدود شده' suspect: 'مشکوک' approved: "تایید شده ؟" approved_selected: @@ -2785,7 +2780,6 @@ fa_IR: staff: "همکار" admins: 'مدیران ارشد' moderators: 'مدیران' - blocked: 'کاربران مسدود شده' suspended: 'کاربران تعلیق شده' suspect: 'کاربران مشکوک' reject_successful: @@ -2800,18 +2794,14 @@ fa_IR: suspend_failed: "در جریان به تعلیق درآوردن این کاربر اشتباهی رخ داد. {{error}}" unsuspend_failed: "در جریان خارج کردن این کاربر از تعلیق، اشتباهی رخ داد {{error}}" suspend_duration: "کاربر چه مدت در تعلیق خواهد بود؟" - suspend_duration_units: "(روزا)" suspend_reason_label: "شما چرا معلق شده‌اید؟ این متن بر روی صفحه‌ی نمایه‌ی کاربر برای همه قابل مشاهده خواهد بود، و در هنگام ورود به سیستم نیز به خود کاربر نشان داده خواهد شد. لطفاً خلاصه بنویسید." suspend_reason: "دلیل" suspended_by: "تعلیق شده توسط" delete_all_posts: "پاک کردن همه‌ی نوشته‌ها" delete_all_posts_confirm_MF: "شما در حال حذف {POSTS, plural, one {1 نوشته} other {# نوشته}} and {TOPICS, plural, one {1 نوشته} other {# موضوع}} هستید. ادامه می‌دهید؟" - suspend: "تعلیق" - unsuspend: "خارج کردن از تعلیق" - suspended: "تعلیق شد؟" moderator: "مدیر؟ " admin: "مدیر ارشد؟" - blocked: "مسدود شد؟" + suspended: "تعلیق شد؟" staged: "کاربر خودکار؟" show_admin_profile: "مدیر ارشد" refresh_browsers: "تازه کردن اجباری مرورگر" @@ -2827,8 +2817,8 @@ fa_IR: grant_admin_confirm: "برای تایید مدیر ارشد جدید ایمیلی ارسال کردیم. لطفا ایمیل را باز کرده و دستور‌العمل آن را دنبال کنید." revoke_moderation: 'پس گرفتن مدیریت' grant_moderation: 'اعطای مدیریت' - unblock: 'باز کردن' - block: 'بستن' + unsuspend: 'خارج کردن از تعلیق' + suspend: 'تعلیق' reputation: ' اعتبار' permissions: دسترسی‌ها activity: فعالیت @@ -2872,17 +2862,12 @@ fa_IR: activate_failed: "در فعال‌سازی این کاربر مشکلی پیش آمد." deactivate_account: "غیرفعال‌کردن حساب‌کاربری" deactivate_failed: "در غیرفعال کردن این کاربر مشکلی وجود دارد." - unblock_failed: 'در رفع مسدودی این کاربر مشکلی وجود دارد.' - block_failed: 'در مسدودی این کاربر مشکلی وجود دارد.' - block_confirm: 'آیا از بستن دسترسی کاربر اطمینان دارید؟ بعد از انجام این کار امکان ایجاد موضوع یا ارسال پست نخواهند داشت.' - block_accept: 'بله، این کاربر را مسدود کن' bounce_score: "امتیاز ویژه" reset_bounce_score: label: "بازنشانی" title: "بازنشانی امتیاز ویژه به 0" deactivate_explanation: "کاربر غیر فعال باید دوباره ایمیل خود را تایید کند." suspended_explanation: "کاربر تعلیق شده نمی‌تواند وارد سیستم شود." - block_explanation: "کاربر مسدود شده نمی‌تواند نوشته‌ای بگذارد یا موضوعی آغاز کند." staged_explanation: "کاربر خودکار فقط در بخش های خاص و با ایمیل می‌تواند پست ارسال کند." bounce_score_explanation: none: "بازتابی به تازگی از آن ایمیل دریافت نشده." diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index b9548257a5..8749733268 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -539,7 +539,6 @@ fi: admin: "{{user}} on ylläpitäjä" moderator_tooltip: "Tämä käyttäjä on valvoja" admin_tooltip: "Tämä käyttäjä on ylläpitäjä" - blocked_tooltip: "Tämä käyttäjä on estetty" suspended_notice: "Tämä käyttäjätili on hyllytetty {{date}} asti." suspended_permanently: "Käyttäjä on hyllytetty." suspended_reason: "Syy:" @@ -2347,7 +2346,6 @@ fi: no_problems: "Ongelmia ei löytynyt." moderators: 'Valvojat:' admins: 'Ylläpitäjät:' - blocked: 'Estetyt:' suspended: 'Hyllytetyt:' private_messages_short: "YV:t" private_messages_title: "Viestit" @@ -2887,8 +2885,6 @@ fi: change_category_settings: "muuta alueen asetuksia" delete_category: "poista alue" create_category: "luo alue" - block_user: "estä käyttäjä" - unblock_user: "poista esto" grant_admin: "myönnä ylläpitäjän oikeudet" revoke_admin: "peru ylläpitäjän oikeudet" grant_moderation: "myönnä valvojan oikeudet" @@ -2983,7 +2979,6 @@ fi: pending: "Odottaa" staff: 'Henkilökunta' suspended: 'Hyllytetyt' - blocked: 'Estetyt' suspect: 'Epäilty' approved: "Hyväksytty?" approved_selected: @@ -3004,7 +2999,6 @@ fi: staff: "Henkilökunta" admins: 'Ylläpitäjät' moderators: 'Valvojat' - blocked: 'Estetyt käyttäjät' suspended: 'Hyllytetyt käyttäjät' suspect: 'Epäillyt käyttäjät' reject_successful: @@ -3032,12 +3026,9 @@ fi: cant_suspend: "Käyttäjää ei voi hyllyttää." delete_all_posts: "Poista kaikki viestit" delete_all_posts_confirm_MF: "Olet poistamassa {POSTS, plural, one {1 viestin} other {# viestiä}} ja {TOPICS, plural, one {1 ketjun} other {# ketjua}}. Oletko varma?" - suspend: "Hyllytä" - unsuspend: "Poista hyllytys" - suspended: "Hyllytetty?" moderator: "Valvoja?" admin: "Ylläpitäjä?" - blocked: "Estetty?" + suspended: "Hyllytetty?" staged: "Esikäyttäjä?" show_admin_profile: "Ylläpito" refresh_browsers: "Pakota sivun uudelleen lataus" @@ -3053,8 +3044,8 @@ fi: grant_admin_confirm: "Vahvistaaksemme uuden ylläpitäjän lähetimme sinulle sähköpostin. Avaa se ja noudata ohjeita." revoke_moderation: 'Peru valvojan oikeudet' grant_moderation: 'Myönnä valvojan oikeudet' - unblock: 'Poista esto' - block: 'Estä' + unsuspend: 'Poista hyllytys' + suspend: 'Hyllytä' reputation: Maine permissions: Oikeudet activity: Toiminta @@ -3101,10 +3092,6 @@ fi: activate_failed: "Käyttäjätilin vahvistaminen ei onnistunut." deactivate_account: "Poista käyttäjätili käytöstä" deactivate_failed: "Käyttäjätilin poistaminen käytöstä ei onnistunut." - unblock_failed: 'Käyttäjätilin eston poistaminen ei onnistunut.' - block_failed: 'Käyttäjätilin estäminen ei onnistunut.' - block_confirm: 'Oletko varma, että haluat estää tämän käyttäjän? He eivät voi luoda uusia aiheita tai viestejä.' - block_accept: 'Kyllä, estä tämä käyttäjä' bounce_score: "Palautuspisteet" reset_bounce_score: label: "Palauta" @@ -3112,7 +3099,6 @@ fi: visit_profile: "Muokkaa tämän käyttäjän käyttäjäasetuksia hänen profiilissaan" deactivate_explanation: "Käytöstä poistetun käyttäjän täytyy uudelleen vahvistaa sähköpostiosoitteensa." suspended_explanation: "Hyllytetty käyttäjä ei voi kirjautua sisään." - block_explanation: "Estetty käyttäjä ei voi luoda viestejä tai ketjuja." staged_explanation: "Esikäyttäjä voi kirjoittaa vain tiettyihin ketjuihin sähköpostin välityksellä." bounce_score_explanation: none: "Tästä sähköpostiosoitteesta ei ole tullut palautuksia viime aikoina" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 40f3559beb..dc5879e7e9 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -540,7 +540,6 @@ fr: admin: "{{user}} est un administrateur" moderator_tooltip: "Cet utilisateur est un modérateur" admin_tooltip: "Cet utilisateur est un administrateur" - blocked_tooltip: "Cet utilisateur est bloqué" suspended_notice: "L'utilisateur est suspendu jusqu'au {{date}}." suspended_permanently: "Cet utilisateur est suspendu." suspended_reason: "Raison :" @@ -1226,7 +1225,7 @@ fr: no_more_results: "Aucun résultat supplémentaire." searching: "Recherche en cours…" post_format: "#{{post_number}} par {{username}}" - results_page: "Résultats de votre recherche" + results_page: "Résultats de recherche pour « {{term}} »" more_results: "Il y a d'autres résultats. Merci de préciser vos critères de recherche." cant_find: "Vous ne trouvez pas ce que vous cherchez ?" start_new_topic: "Voulez-vous commencer un nouveau sujet ?" @@ -1420,6 +1419,9 @@ fr: next_week: "Semaine prochaine" two_weeks: "Deux semaines" next_month: "Mois prochai" + three_months: "Trois mois" + six_months: "Six mois" + one_year: "Une année" forever: "Toujours" pick_date_and_time: "Sélectionner une date et heure" set_based_on_last_post: "Fermer par rapport au dernier message" @@ -1743,8 +1745,8 @@ fr: actions: flag: 'Signaler' defer_flags: - one: "Reporter le signalement" - other: "Reporter les signalements" + one: "Ignorer le signalement" + other: "Ignorer les signalements" undo: off_topic: "Annuler le signalement" spam: "Annuler le signalement" @@ -2344,7 +2346,6 @@ fr: no_problems: "Aucun problème n'a été trouvé." moderators: 'Modérateurs :' admins: 'Administateurs :' - blocked: 'Bloqués :' suspended: 'Suspendus :' private_messages_short: "Msgs" private_messages_title: "Messages" @@ -2389,11 +2390,11 @@ fr: agree_flag_restore_post_title: "Restaurer ce message" agree_flag: "Accepter le signalement" agree_flag_title: "Accepter le signalement et garder le message inchangé" - defer_flag: "Reporter" + defer_flag: "Ignorer" defer_flag_title: "Retirer le signalement ; il ne nécessite aucune action pour le moment." delete: "Supprimer" delete_title: "Supprimer le message signalé." - delete_post_defer_flag: "Supprimer le message et reporter le signalement" + delete_post_defer_flag: "Supprimer le message et ignorer le signalement" delete_post_defer_flag_title: "Supprimer le message ; si c'est le premier message, le sujet sera supprimé" delete_post_agree_flag: "Supprimer le message et accepter le signalement" delete_post_agree_flag_title: "Supprimer le message ; si c'est le premier message, le sujet sera supprimé" @@ -2412,7 +2413,7 @@ fr: dispositions: agreed: "accepté" disagreed: "refusé" - deferred: "reporté" + deferred: "ignoré" flagged_by: "Signalé par" resolved_by: "Résolu par" took_action: "Intervenu" @@ -2435,6 +2436,8 @@ fr: off_topic: "hors-sujet" inappropriate: "inapproprié" spam: "spam" + notify_user: "personnalisé" + notify_moderators: "personnalisé" groups: primary: "Groupe principal" no_primary: "(pas de groupe principal)" @@ -2884,8 +2887,6 @@ fr: change_category_settings: "modifier les paramètres de la catégorie" delete_category: "supprimer la catégorie" create_category: "créer une catégorie" - block_user: "bloquer l'utilisateur" - unblock_user: "débloquer l'utilisateur" grant_admin: "accorder les droits d'administration" revoke_admin: "révoquer les droits d'administration" grant_moderation: "Accorder les droits de modération" @@ -2980,7 +2981,6 @@ fr: pending: "En attente" staff: 'Responsables' suspended: 'Suspendus' - blocked: 'Bloqués' suspect: 'Suspects' approved: "Approuvé ?" approved_selected: @@ -3001,7 +3001,6 @@ fr: staff: "Responsables" admins: 'Administrateurs' moderators: 'Modérateurs' - blocked: 'Utilisateurs bloqués' suspended: 'Utilisateurs suspendus' suspect: 'Utilisateurs suspects' reject_successful: @@ -3029,12 +3028,9 @@ fr: cant_suspend: "Cet utilisateur ne peut pas être suspendu." delete_all_posts: "Supprimer tous les messages" delete_all_posts_confirm_MF: "Vous êtes sur le point de supprimer {POSTS, plural, one {1 message} other {# messages}} et {TOPICS, plural, one {1 sujet} other {# sujets}}. Êtes-vous sûr ?" - suspend: "Suspendre" - unsuspend: "Annuler la suspension" - suspended: "Suspendu ?" moderator: "Modérateur ?" admin: "Administrateur ?" - blocked: "Bloqué ?" + suspended: "Suspendu ?" staged: "En attente ?" show_admin_profile: "Administration" refresh_browsers: "Forcer le rafraîchissement du navigateur" @@ -3050,8 +3046,8 @@ fr: grant_admin_confirm: "Nous vous avons envoyé un courriel pour vérifier le nouvel administrateur. Veuillez le lire et suivre les instructions." revoke_moderation: 'Révoquer les droits de modération' grant_moderation: 'Accorder les droits de modération' - unblock: 'Débloquer' - block: 'Bloquer' + unsuspend: 'Annuler la suspension' + suspend: 'Suspendre' reputation: Réputation permissions: Permissions activity: Activité @@ -3098,10 +3094,6 @@ fr: activate_failed: "Il y a eu un problème lors de l'activation du compte." deactivate_account: "Désactiver le compte" deactivate_failed: "Il y a eu un problème lors de la désactivation du compte." - unblock_failed: 'Il y a eu un problème lors du déblocage de l''utilisateur.' - block_failed: 'Il y a eu un problème lors du blocage de l''utilisateur.' - block_confirm: 'Êtes-vous sûr de vouloir bloquer cet utilisateur ? Il ne pourra plus créer de sujets ou messages.' - block_accept: 'Oui, bloquer cet utilisateur' bounce_score: "Score de rejet" reset_bounce_score: label: "Réinitialiser" @@ -3109,7 +3101,6 @@ fr: visit_profile: "Visiter la page de préférence de cet utilisateur pour éditer son profil" deactivate_explanation: "Un utilisateur désactivé doit revalider son adresse de courriel." suspended_explanation: "Un utilisateur suspendu ne peut pas se connecter." - block_explanation: "Un utilisateur bloqué ne peut pas écrire de messages ni créer de sujets." staged_explanation: "Un utilisateur en attente ne peut envoyer des messages par courriel que pour des sujets spécifiques." bounce_score_explanation: none: "Aucune rejet récent pour cette adresse courriel." diff --git a/config/locales/client.gl.yml b/config/locales/client.gl.yml index 5c61cdf2f3..207cb4b7ba 100644 --- a/config/locales/client.gl.yml +++ b/config/locales/client.gl.yml @@ -432,7 +432,6 @@ gl: admin: "{{user}} é administrador" moderator_tooltip: "Este usuario é un moderador" admin_tooltip: "Este usuario é un administrador" - blocked_tooltip: "Este usuario está bloqueado" suspended_notice: "Este usuario está suspendido até o {{date}}." suspended_reason: "Razón:" github_profile: "Github" @@ -1616,7 +1615,6 @@ gl: no_problems: "Non se atoparon problemas." moderators: 'Moderadores:' admins: 'Administradores:' - blocked: 'Bloqueados:' suspended: 'Suspendidos:' private_messages_short: "Mensaxes" private_messages_title: "Mensaxes" @@ -1989,8 +1987,6 @@ gl: change_category_settings: "cambiar axustes da categoría" delete_category: "eliminar categoría" create_category: "crear categoría" - block_user: "bloquear usuario" - unblock_user: "desbloquear usuario" grant_admin: "conceder administración" revoke_admin: "revogar administración" grant_moderation: "conceder moderación" @@ -2046,7 +2042,6 @@ gl: pending: "Pendente" staff: 'Equipo' suspended: 'Suspendido' - blocked: 'Bloqueado' suspect: 'Sospeitoso' approved: "Aprobado?" approved_selected: @@ -2067,7 +2062,6 @@ gl: staff: "Equipo" admins: 'Usuarios administradores' moderators: 'Moderadores' - blocked: 'Usuarios bloqueados' suspended: 'Usuarios suspendidos' suspect: 'Usuarios sospeitosos' reject_successful: @@ -2084,17 +2078,13 @@ gl: suspend_failed: "Algo fallou rexeitando este usuario {{error}}" unsuspend_failed: "Algo foi mal levantando a suspensión deste usuario {{error}}" suspend_duration: "Canto tempo estará suspendido o usuario?" - suspend_duration_units: "(días)" suspend_reason_label: "Cal é o motivo da suspensión? Este texto será visíbel para todo o mundo na páxina do perfil deste usuario e amosaráselle ao usuario cando tente iniciar sesión. Procura ser breve." suspend_reason: "Razón" suspended_by: "Suspendido por" delete_all_posts: "Eliminar todas as publicacións" - suspend: "Suspender" - unsuspend: "Non suspender" - suspended: "Suspendido?" moderator: "Moderador?" admin: "Administrador?" - blocked: "Bloqueado?" + suspended: "Suspendido?" staged: "Transitorio?" show_admin_profile: "Administración" refresh_browsers: "Forzar a actualización do navegador" @@ -2108,8 +2098,8 @@ gl: grant_admin: 'Facer administrador' revoke_moderation: 'Revogar moderación' grant_moderation: 'Conceder moderación' - unblock: 'Desbloquear' - block: 'Bloquear' + unsuspend: 'Non suspender' + suspend: 'Suspender' reputation: Reputación permissions: Permisos activity: Actividade @@ -2156,13 +2146,8 @@ gl: activate_failed: "Produciuse un problema activando o usuario." deactivate_account: "Desactivar a conta" deactivate_failed: "Produciuse un problema desactivando o usuario." - unblock_failed: 'Produciuse un problema desbloqueando o usuario.' - block_failed: 'Produciuse un problema bloqueando o usuario.' - block_confirm: 'Confirmas o bloqueo deste usuario? Non poderá crear máis temas nin publicacións.' - block_accept: 'Si, bloquealo' deactivate_explanation: "Un usuario desactivado debe revalidar o seu correo-e." suspended_explanation: "Un usuario suspendido non pode iniciar sesión." - block_explanation: "Un usuario bloquedo non pode publicar nin iniciar temas." trust_level_change_failed: "Produciuse un problema cambiando o nivel de confianza do usuario." suspend_modal_title: "Suspender usuario" trust_level_2_users: "Usuarios con nivel 2 de confianza" diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index a4e08a8814..10cb046cba 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -527,7 +527,6 @@ he: admin: "{{user}} הוא מנהל מערכת" moderator_tooltip: "משתמש זה הינו מנחה (Moderator)" admin_tooltip: "משתמש זה הינו אדמיניסטרטור" - blocked_tooltip: "משתמש זה חסום" suspended_notice: "המשתמש הזה מושעה עד לתאריך: {{date}}." suspended_reason: "הסיבה: " github_profile: "גיטהאב" @@ -2276,7 +2275,6 @@ he: no_problems: "לא נמצאו בעיות." moderators: 'מנהלים:' admins: 'מנהלים ראשיים:' - blocked: 'חסומים:' suspended: 'מושעים:' private_messages_short: "הודעות" private_messages_title: "הודעות" @@ -2818,8 +2816,6 @@ he: change_category_settings: "שינוי הגדרות קטגוריה" delete_category: "מחיקת קטגוריה" create_category: "יצירת קטגוריה" - block_user: "חסום משתמש" - unblock_user: "בטל חסימת משתמש" grant_admin: "הענקת ניהול" revoke_admin: "שללו אדמיניסטרציה" grant_moderation: "הענקת הנחיה" @@ -2909,7 +2905,6 @@ he: pending: "ממתין" staff: 'צוות' suspended: 'מושעים' - blocked: 'חסומים' suspect: 'חשודים' approved: "מאושר?" approved_selected: @@ -2930,7 +2925,6 @@ he: staff: "צוות" admins: 'מנהלים ראשיים' moderators: 'מנהלים' - blocked: 'משתמשים חסומים' suspended: 'משתמשים מושעים' suspect: 'משתמשים חשודים' reject_successful: @@ -2947,18 +2941,14 @@ he: suspend_failed: "משהו נכשל בהשעיית המשתמש הזה {{error}}" unsuspend_failed: "משהו נכשל בביטול השהיית המשתמש הזה {{error}}" suspend_duration: "למשך כמה זמן יהיה המשתמש מושעה?" - suspend_duration_units: "(ימים)" suspend_reason_label: "מדוע אתם משעים? הטקסט הזה יהיה נראה לכולם בעמוד המשתמש הזה, ויוצג למשתמש כשינסה להתחבר. נסו לשמור עליו קצר." suspend_reason: "סיבה" suspended_by: "הושעה על ידי" delete_all_posts: "מחק את כל הפוסטים" delete_all_posts_confirm_MF: "אתם עומדים למחוק {POSTS, plural, one {פוסט אחד} other {# פסוטים}} ו{TOPICS, plural, one {נושא אחד} other {# נושאים}}. האם אתם בטוחים?" - suspend: "השעה" - unsuspend: "בטל השעייה" - suspended: "מושעה?" moderator: "מנהל?" admin: "מנהל ראשי?" - blocked: "חסום?" + suspended: "מושעה?" staged: "מועמד?" show_admin_profile: "מנהל ראשי" refresh_browsers: "הכרח רענון דפדפן" @@ -2974,8 +2964,8 @@ he: grant_admin_confirm: "שלחנו אליכם מייל כדי לוודא את האדמיניסטרטור החדש. בבקשה פיתחו אותו ועיקבו אחר ההוראות." revoke_moderation: 'שללו הנחיה' grant_moderation: 'הענקת הנחיה' - unblock: 'ביטול חסימה' - block: 'חסום' + unsuspend: 'בטל השעייה' + suspend: 'השעה' reputation: מוניטין permissions: הרשאות activity: פעילות @@ -3022,10 +3012,6 @@ he: activate_failed: "הייתה בעיה בהפעלת המשתמש." deactivate_account: "נטרל חשבון" deactivate_failed: "הייתה בעיה בנטרול חשבון המשתמש." - unblock_failed: 'הייתה בעיה בביטול חסימת המשתמש.' - block_failed: 'הייתה בעיה בחסימת המשתמש.' - block_confirm: 'האם אתם בטוחים שאתם מעוניינים לחסום משתמש זה? הם לא יוכלו ליצור נושאים חדשים או פוסטים.' - block_accept: 'כן, חסום משתמש זה' bounce_score: "ניקוד-החזר" reset_bounce_score: label: "איפוס" @@ -3033,7 +3019,6 @@ he: visit_profile: "בקרו בדף ההעדפות של משתמש/ת זה/זו כדי לערוך את הפרופיל שלהם" deactivate_explanation: "חשבון משתמש מנוטרל נדרש לוודא דואר אלקטרוני מחדש." suspended_explanation: "משתמש מושעה לא יכול להתחבר." - block_explanation: "משתמש חסום לא יכול לפרסם או להתחיל נושאים." staged_explanation: "משתמש מועמד יכול לפרסם רק באמצעות מייל בנושאים ספציפיים." bounce_score_explanation: none: "לא התקבלו החזרים לאחרונה מהמייל הזה." diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml index 2b6a676711..37826a680d 100644 --- a/config/locales/client.id.yml +++ b/config/locales/client.id.yml @@ -96,6 +96,7 @@ id: private_topic: "membuat topik ini pribadi %{when}" split_topic: "pisahkan topik ini %{when}" invited_user: "mengundang %{who} %{when}" + user_left: "%{who}meninggalkan pesan %{when}" removed_user: "menghapus %{who} %{when}" autoclosed: enabled: 'ditutup %{when}' @@ -191,6 +192,7 @@ id: like_count: "Suka" topic_count: "Topik" post_count: "Pos" + user_count: "Pengguna" active_user_count: "Pengguna Aktif" contact: "Kontak kami" contact_info: "Pada kegiatan dari sebuah isu kritikal atau penting terjadi pada situs ini, silahkan kontak kami di %{contact_info}." @@ -289,13 +291,42 @@ id: total_rows: other: "%{count} pengguna" groups: + logs: + action: "Aksi" + details: "Detil" + edit: + full_name: 'Nama Lengkap' + add_members: "Tambah Anggota" + delete_member_confirm: "Hapus '%{username}' dari grup '%{group}'?" + name_placeholder: "Nama grup, tidak boleh menggunakan spasi, sama seperti aturan username" + empty: + members: "Tidak ada anggota dalam kelompok ini." + messages: "Tidak ada pesan untuk kelompok ini." + topics: "Tidak ada topik oleh anggota kelompok ini." add: "Menambahkan" + message: "Pesan" + automatic_group: Kelompok Otomatis + closed_group: Kelompok Tertutup + is_group_user: "Anda adalah anggota dari kelompok ini." + membership: "Keanggotaan" + name: "Nama" + user_count: "Jumlah Anggota" + bio: "Tentang Kelompok" selector_placeholder: "Menambahkan anggota" owner: "pemilik" + members: "Anggota" topics: "Topik-topik" mentions: "Balasan-balasan" messages: "Pesan-pesan" + visibility_levels: + title: "Siapa yang bisa melihat kelompok ini?" + public: "Semua orang" + members: "Pemilik kelompok, anggota, dan admin" + staff: "Pemilik kelompok dan staf" + owners: "Pemilik grup dan admin" alias_levels: + messageable: "Siapa yang bisa mengirim pesan pada kelompok ini?" + only_admins: "Hanya admin" mods_and_admins: "Hanya moderator dan Admin" members_mods_and_admins: "Hanya anggota grup, moderator dan admin" everyone: "Semua orang" @@ -374,6 +405,8 @@ id: profile: "Profil" mute: "Redam" edit: "Ubah Preferensi" + download_archive: + button_text: "Unduh Semua" new_private_message: "Pesan Baru" private_message: "pesan" private_messages: "Messages" @@ -405,7 +438,6 @@ id: admin: "{{user}} adalah admin" moderator_tooltip: "Pengguna ini adalah moderator" admin_tooltip: "Pengguna ini adalah admin" - blocked_tooltip: "Pengguna ini yang diblokir" suspended_notice: "Pengguna ini ditangguhkan sampai {{date}}" suspended_reason: "Alasan:" github_profile: "Github" @@ -643,10 +675,35 @@ id: blank_username_or_password: "Silahkan masukkan email atau nama pengguna dan kata sandi anda" reset_password: 'Atur Ulang Kata Sandi' or: "atau" + submit_new_email: "Perbarui Alamat Email" + google: + title: "dengan Google" + message: "Autentikasi dengan Google (pastikan pop up blockers tidak dinyalakan)" + google_oauth2: + title: "dengan Google" + message: "Autentikasi dengan Google (pastikan pop up blockers tidak dinyalakan)" twitter: title: "dengan Twitter" + message: "Autentikasi dengan Twitter (pastikan pop up blockers tidak dinyalakan)" + instagram: + title: "dengan Instagram" + message: "Autentikasi dengan Instagram (pastikan pop up blockers tidak dinyalakan)" facebook: title: "dengan Facebook" + message: "Autentikasi dengan Facebook (pastikan pop up blockers tidak dinyalakan)" + yahoo: + title: "dengan Yahoo" + message: "Autentikasi dengan Yahoo (pastikan pop up blockers tidak dinyalakan)" + github: + title: "dengan GitHub" + message: "Autentikasi dengan GitHub (pastikan pop up blockers tidak dinyalakan)" + invites: + accept_title: "Undangan" + welcome_to: "Selamat datang di %{site_name}!" + invited_by: "Anda telah diundang oleh:" + accept_invite: "Terima Undangan" + name_label: "Nama" + password_label: "Atur Kata Sandi" composer: reply_here: "Balas Disini" reply: "Balas" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index e02452b0a0..1dc4f87280 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -526,6 +526,7 @@ it: disable: "Disabilita Notifiche" enable: "Abilita Notifiche" each_browser_note: "Nota: devi modificare questa impostazione per ogni browser che utilizzi." + dismiss: 'Chiudi' dismiss_notifications: "Chiudi Tutti" dismiss_notifications_tooltip: "Imposta tutte le notifiche non lette come lette " first_notification: "La tua prima notifica! Selezionala per iniziare." @@ -540,7 +541,7 @@ it: admin: "{{user}} è un amministratore" moderator_tooltip: "Questo utente è un moderatore" admin_tooltip: "Questo utente è un amministratore" - blocked_tooltip: "Questo utente è bloccato" + silenced_tooltip: "Questo utente è silenziato" suspended_notice: "Questo utente è sospeso fino al {{date}}." suspended_permanently: "Questo utente è sospeso." suspended_reason: "Motivo: " @@ -592,6 +593,7 @@ it: undo_revoke_access: "Annullare Revoca Accesso" api_approved: "Approvato:" theme: "Tema" + home: "Home Page Predefinita" staff_counters: flags_given: "segnalazioni utili" flagged_posts: "messaggi segnalati" @@ -905,6 +907,7 @@ it: mute: Ignora unmute: Attiva last_post: Ultimo messaggio + time_read: Letti last_reply_lowercase: ultima risposta replies_lowercase: one: risposta @@ -1041,6 +1044,7 @@ it: default_header_text: Selezione... no_content: Nessun risultato trovato filter_placeholder: Ricerca... + create: "Crea {{content}}" emoji_picker: filter_placeholder: Ricerca per emoji people: Persone @@ -1096,6 +1100,7 @@ it: cancel: "Annulla" create_topic: "Crea Argomento" create_pm: "Messaggio" + create_whisper: "Sussurro" title: "O premi Ctrl+Enter" users_placeholder: "Aggiunti un utente" title_placeholder: "In breve, di cosa tratta questo argomento?" @@ -1226,7 +1231,7 @@ it: no_more_results: "Nessun altro risultato trovato." searching: "Ricerca in corso..." post_format: "#{{post_number}} da {{username}}" - results_page: "Risultati Ricerca" + results_page: "Risultati della ricerca per '{{term}}'" more_results: "Ci sono più risultati. Restringi i criteri di ricerca." cant_find: "Non riesci a trovare quello che stai cercando?" start_new_topic: "Forse vuoi avviare un nuovo argomento?" @@ -1420,6 +1425,9 @@ it: next_week: "La prossima settimana" two_weeks: "Due Settimane" next_month: "Il prossimo mese" + three_months: "Tre Mesi" + six_months: "Sei Mesi" + one_year: "Un Anno" forever: "Per sempre" pick_date_and_time: "Scegli la data e l'ora" set_based_on_last_post: "Chiudi in base all'ultimo messaggio" @@ -1743,8 +1751,8 @@ it: actions: flag: 'Segnala' defer_flags: - one: "Ignora segnalazione" - other: "Annulla segnalazioni" + one: "Ignore flag" + other: "Ignora le segnalazioni" undo: off_topic: "Rimuovi segnalazione" spam: "Rimuovi segnalazione" @@ -2340,7 +2348,7 @@ it: no_problems: "Nessun problema rilevato." moderators: 'Moderatori:' admins: 'Amministratori:' - blocked: 'Bloccati:' + silenced: 'Silenziati:' suspended: 'Sospesi: ' private_messages_short: "MP" private_messages_title: "Messaggi" @@ -2389,7 +2397,7 @@ it: defer_flag_title: "Rimuovi segnalazione; non è necessaria alcuna azione questa volta." delete: "Cancella" delete_title: "Cancella il messaggio a cui si riferisce la segnalazione." - delete_post_defer_flag: "Cancella il messaggio e Ignora la segnalazione" + delete_post_defer_flag: "Elimina il messaggio e Ignora la segnalazione" delete_post_defer_flag_title: "Cancella il messaggio: se è il primo, cancella l'argomento" delete_post_agree_flag: "Elimina messaggio e Accetta la segnalazione" delete_post_agree_flag_title: "Cancella il messaggio: se è il primo, cancella l'argomento" @@ -2422,6 +2430,7 @@ it: was_edited: "Il messaggio è stato modificato dopo la prima segnalazione" previous_flags_count: "Questo messaggio è stato già segnalato {{count}} volte." show_details: "Mostra i dettagli della segnalazione" + details: "dettagli" flagged_topics: topic: "Argomento" type: "Tipo" @@ -2431,6 +2440,8 @@ it: off_topic: "fuori tema" inappropriate: "inappropriato" spam: "spam" + notify_user: "personalizzato" + notify_moderators: "personalizzato" groups: primary: "Gruppo Primario" no_primary: "(nessun gruppo primario)" @@ -2880,8 +2891,8 @@ it: change_category_settings: "cambia le impostazioni della categoria" delete_category: "cancella categoria" create_category: "crea categoria" - block_user: "blocca utente" - unblock_user: "sblocca utente" + silence_user: "utente in silenzio" + unsilence_user: "utente non silenziato" grant_admin: "assegna amministrazione" revoke_admin: "revoca amministrazione" grant_moderation: "assegna moderazione" @@ -2976,7 +2987,7 @@ it: pending: "In attesa" staff: 'Staff' suspended: 'Sospesi' - blocked: 'Bloccati' + silenced: 'Silenziato' suspect: 'Sospetti' approved: "Approvato?" approved_selected: @@ -2997,7 +3008,7 @@ it: staff: "Staff" admins: 'Utenti Amministratori' moderators: 'Moderatori' - blocked: 'Utenti Bloccati' + silenced: 'Utenti Silenziati' suspended: 'Utenti Sospesi' suspect: 'Utenti Sospetti' reject_successful: @@ -3025,12 +3036,12 @@ it: cant_suspend: "Questo utente non può essere sospeso." delete_all_posts: "Cancella tutti i messaggi" delete_all_posts_confirm_MF: "Stai per cancellare {POSTS, plural, one {1 messaggio} other {# messaggi}} e {TOPICS, plural, one {1 argomento} other {# argomenti}}. Sei sicuro?" - suspend: "Sospendi" - unsuspend: "Riabilita" - suspended: "Sospeso?" + silence: "Silenzio" + unsilence: "Non in silenzio" + silenced: "Silenziato?" moderator: "Moderatore?" admin: "Amministratore?" - blocked: "Bloccato?" + suspended: "Sospeso?" staged: "Temporaneo?" show_admin_profile: "Amministratore" refresh_browsers: "Forza l'aggiornamento del browser" @@ -3046,8 +3057,8 @@ it: grant_admin_confirm: "Ti abbiamo inviato una email per verificare il nuovo amministratore. Per favore aprila e segui le istruzioni." revoke_moderation: 'Revoca privilegi di moderazione' grant_moderation: 'Assegna diritti di moderazione' - unblock: 'Sblocca' - block: 'Blocca' + unsuspend: 'Riabilita' + suspend: 'Sospendi' reputation: Reputazione permissions: Permessi activity: Attività @@ -3094,10 +3105,10 @@ it: activate_failed: "Si è verificato un problema nell'attivazione dell'utente." deactivate_account: "Disattiva Account" deactivate_failed: "Si è verificato un errore durante la disattivazione dell'utente." - unblock_failed: 'Si è verificato un errore durante lo sblocco dell''utente.' - block_failed: 'Si è verificato un errore durante il blocco dell''utente.' - block_confirm: 'Sei sicuro di voler bloccare questo utente? Non sarà più in grado di creare alcun nuovo argomento o messaggio.' - block_accept: 'Sì, blocca questo utente' + unsilence_failed: 'Si è verificato un errore mentre si de-silenziava l''utente.' + silence_failed: 'Si è verificato un errore mentre si de-silenziava l''utente.' + silence_confirm: 'Sei sicuro di voler silenziare questo utente? Non sarà più in grado di creare alcun nuovo argomento o messaggio.' + silence_accept: 'Sì, silenzia questo utente' bounce_score: "Errori di Ritorno" reset_bounce_score: label: "Azzera" @@ -3105,7 +3116,7 @@ it: visit_profile: "Visita la pagina delle preferenze di questo utente per modificare il suo profilo" deactivate_explanation: "Un utente disattivato deve riconvalidare la propria email." suspended_explanation: "Un utente sospeso non può connettersi." - block_explanation: "Un utente bloccato non può pubblicare messaggi o iniziare argomenti." + silence_explanation: "Un utente silenziato non può pubblicare messaggi o iniziare argomenti. " staged_explanation: "Un utente temporaneo può pubblicare via email solo in alcuni argomenti." bounce_score_explanation: none: "L'indirizzo email non ha causato nessun errore di ritorno recentemente." @@ -3223,6 +3234,7 @@ it: developer: 'Sviluppatore' embedding: "Incorporo" legal: "Legale" + api: 'API' user_api: 'API Utente' uncategorized: 'Altro' backups: "Backup" diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index 0b8bba78ce..20e98796a2 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -509,7 +509,6 @@ ja: admin: "{{user}} は管理者です" moderator_tooltip: "このユーザーはモデレータです" admin_tooltip: "このユーザーは管理者です" - blocked_tooltip: "このユーザーはブロックされています" suspended_notice: "このユーザーは {{date}} まで凍結状態です。" suspended_reason: "理由: " github_profile: "Github" @@ -2099,7 +2098,6 @@ ja: no_problems: "問題は見つかりませんでした。" moderators: 'モデレータ:' admins: '管理者:' - blocked: 'ブロック中:' suspended: '停止中:' private_messages_short: "メッセージ" private_messages_title: "メッセージ" @@ -2504,8 +2502,6 @@ ja: change_category_settings: "カテゴリ設定を変更" delete_category: "カテゴリを削除する" create_category: "カテゴリを作る" - block_user: "ユーザをブロック" - unblock_user: "ユーザをブロック解除" grant_admin: "管理者権限を付与" revoke_admin: "管理者権限を剥奪" grant_moderation: "モデレータ権限を付与" @@ -2580,7 +2576,6 @@ ja: pending: "保留中" staff: 'スタッフ' suspended: '凍結中' - blocked: 'ブロック中' suspect: 'マーク中' approved: "承認?" approved_selected: @@ -2599,7 +2594,6 @@ ja: staff: "スタッフ" admins: '管理者ユーザ' moderators: 'モデレータ' - blocked: 'ブロック中のユーザ' suspended: '凍結中のユーザ' suspect: 'マーク中のユーザ' reject_successful: @@ -2619,12 +2613,9 @@ ja: suspend_reason_placeholder: "凍結理由" suspended_by: "凍結したユーザ" delete_all_posts: "全ての投稿を削除" - suspend: "凍結" - unsuspend: "凍結解除" - suspended: "凍結状態" moderator: "モデレータ権限の所有" admin: "管理者権限の所有" - blocked: "ブロック状態" + suspended: "凍結状態" staged: "ステージドモードの状態" show_admin_profile: "アカウントの管理" refresh_browsers: "ブラウザを強制更新" @@ -2639,8 +2630,8 @@ ja: grant_admin: '管理者権限を付与' revoke_moderation: 'モデレータ権限を剥奪' grant_moderation: 'モデレータ権限を付与' - unblock: 'ブロック解除' - block: 'ブロック' + unsuspend: '凍結解除' + suspend: '凍結' reputation: レピュテーション permissions: パーミッション activity: アクティビティ @@ -2684,10 +2675,6 @@ ja: activate_failed: "ユーザのアクティベートに失敗しました。" deactivate_account: "アカウントのアクティベート解除" deactivate_failed: "ユーザのアクティベート解除に失敗しました。" - unblock_failed: 'ユーザのブロック解除に失敗しました。' - block_failed: 'ユーザのブロックに失敗しました。' - block_confirm: 'このユーザをブロックしてもよろしいですか? ユーザは新しくトピックを作成したり、投稿する事ができなくなります。' - block_accept: 'はい。このユーザーをブロックします。' bounce_score: "バウンススコア" reset_bounce_score: label: "リセット" @@ -2695,7 +2682,6 @@ ja: visit_profile: "設定ページからプロフィールの変更ができます" deactivate_explanation: "アクティベート解除されたユーザは、メールで再アクティベートする必要があります。" suspended_explanation: "凍結中のユーザはログインできません。" - block_explanation: "ブロックされているユーザは投稿およびトピックの作成ができません。" staged_explanation: "ステージドユーザは特定のトピック宛に、メールを経由して投稿する事が出来ます。" bounce_score_explanation: none: "直近ではバウンスメールは受け取っていません。" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 6100c40805..078091609e 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -511,7 +511,6 @@ ko: admin: "{{user}}님은 관리자 입니다" moderator_tooltip: "이 회원은 운영자 입니다" admin_tooltip: "이 회원은 관리자입니다." - blocked_tooltip: "이 회원은 차단되었습니다" suspended_notice: "이 회원은 {{date}}까지 접근 금지 되었습니다." suspended_permanently: "이 회원은 일시정지되었습니다." suspended_reason: "사유: " @@ -2237,7 +2236,6 @@ ko: no_problems: "아무런 문제가 발견되지 않았습니다." moderators: '운영자:' admins: '관리자:' - blocked: '블락됨:' suspended: '접근금지:' private_messages_short: "메시지" private_messages_title: "메시지" @@ -2775,8 +2773,6 @@ ko: change_category_settings: "카테고리 설정 변경" delete_category: "카테고리 지우기" create_category: "카테고리 만들기" - block_user: "사용자 차단" - unblock_user: "사용자 차단 해제" grant_admin: "관리자권한 부여" revoke_admin: "관리자권한 회수" grant_moderation: "운영자권한 부여" @@ -2870,7 +2866,6 @@ ko: pending: "보류된 사용자" staff: '스태프' suspended: '접근 금지 사용자' - blocked: '블락된 사용자' suspect: '의심스러운 사용자' approved: "승인?" approved_selected: @@ -2889,7 +2884,6 @@ ko: staff: "스태프" admins: '관리자' moderators: '운영자' - blocked: '블락된 사용자들' suspended: '접근 금지된 사용자들' suspect: '의심스러운 사용자들' reject_successful: @@ -2915,12 +2909,9 @@ ko: cant_suspend: "이 사용자는 일시정지할 수 없습니다." delete_all_posts: "모든 글을 삭제합니다" delete_all_posts_confirm_MF: " {POSTS, plural, one {1개의 포스트} other {#개의 포스트}} 와 {TOPICS, plural, one {1개의 토픽} other {#개의 토픽}}을 삭제하려고 합니다. 정말로 삭제할까요?" - suspend: "접근 금지" - unsuspend: "접근 허용" - suspended: "접근 금지?" moderator: "운영자?" admin: "관리자?" - blocked: "블락" + suspended: "접근 금지?" staged: "격리조치?" show_admin_profile: "관리자" refresh_browsers: "브라우저 새로 고침" @@ -2936,8 +2927,8 @@ ko: grant_admin_confirm: "새로운 관리자를 인증하기 위해 메일을 보냈습니다. 메일을 열어서 메일에 적힌 지시에 따라 주세요." revoke_moderation: '운영자 권한 회수' grant_moderation: '운영자 권한 부여' - unblock: '언블락' - block: '블락' + unsuspend: '접근 허용' + suspend: '접근 금지' reputation: 평판 permissions: 권한 activity: 활동 @@ -2981,10 +2972,6 @@ ko: activate_failed: "사용자 활성화에 문제가 있습니다." deactivate_account: "계정 비활성화" deactivate_failed: "사용자 비활성에 문제가 있습니다." - unblock_failed: '사용자 언블락에 문제가 있습니다.' - block_failed: '사용자 블락에 문제가 있습니다.' - block_confirm: '정말로 이 사용자를 차단하겠습니까? 차단된 사용자는 토픽, 포스트 작성이 금지됩니다.' - block_accept: '네, 이 사용자를 차단합니다.' bounce_score: "반송 스코어" reset_bounce_score: label: "리셋" @@ -2992,7 +2979,6 @@ ko: visit_profile: "사용자 환경설정 페이지로 가서 프로필 수정하기" deactivate_explanation: "비활성화 사용자는 이메일 인증을 다시 받아야합니다." suspended_explanation: "접근 금지된 유저는 로그인 할 수 없습니다." - block_explanation: "차단된 사용자는 글을 작성하거나 토픽을 작성할 수 없습니다." staged_explanation: "격리조치된 회원은 특정 토픽에 한해서 이메일로만 글을 쓸 수 있습니다." bounce_score_explanation: none: "최근 해당 이메일로부터 반송메일을 받은 내역이 없습니다." diff --git a/config/locales/client.lv.yml b/config/locales/client.lv.yml index 66c0b482d6..5cc5c735de 100644 --- a/config/locales/client.lv.yml +++ b/config/locales/client.lv.yml @@ -555,7 +555,6 @@ lv: admin: "{{user}} ir administrators" moderator_tooltip: "Šis lietotājs ir moderators" admin_tooltip: "Šis lietotājs ir administrators" - blocked_tooltip: "Šis lietotājs ir bloķēts" suspended_notice: "Lietotājs ir īslaicīgi bloķēts līdz {{date}}." suspended_reason: "Iemesls:" github_profile: "Github" @@ -2369,7 +2368,6 @@ lv: no_problems: "Problēmas nav atrastas." moderators: 'Moderatori:' admins: 'Administratori:' - blocked: 'Bloķētie:' suspended: 'Īslaicīgi bloķētie:' private_messages_short: "Ziņas" private_messages_title: "Ziņās" @@ -2814,8 +2812,6 @@ lv: change_category_settings: "Mainīt kategorijas iestatījumus" delete_category: "Dzēst kategoriju" create_category: "Radīt kategoriju" - block_user: "bloķēt lietotāju" - unblock_user: "atbloķēt lietotāju" grant_admin: "piešķirt admina tiesības" revoke_admin: "atsaukt adminu" grant_moderation: "piešķirt moderātora tiesības" @@ -2909,7 +2905,6 @@ lv: pending: "Neizlemts" staff: 'Personāls' suspended: 'Apturēts' - blocked: 'Bloķēts' suspect: 'Novērot' approved: "Apstiprināt?" approved_selected: @@ -2932,7 +2927,6 @@ lv: staff: "Personāls" admins: 'Administratori' moderators: 'Moderātori' - blocked: 'Bloķētie lietotāji' suspended: 'Atceltie lietotāji' suspect: 'Novēroti (suspect) lietotāji' reject_successful: @@ -2951,18 +2945,14 @@ lv: suspend_failed: "Kaut kas nenostrādāj apturot šo lietotāju {{error}}" unsuspend_failed: "Kaut kas nenostrādāj atjaunojot šo lietotāju {{error}}" suspend_duration: "Uz cik ilgu laiku lietotājs tiks atstādināts?" - suspend_duration_units: "(dienas)" suspend_reason_label: "Kādēļ apturēt? Šis texts būt redzams visiem šajā lietotāja profila lapā, un tiks rādīts visiem, kas \"ielogojas\"." suspend_reason: "Iemesls" suspended_by: "Apturēja" delete_all_posts: "Dzēst visus ierakstus" delete_all_posts_confirm_MF: "Jūs grasaties dzēst šos ierakstus. Vai est drošs?" - suspend: "Apturēt" - unsuspend: "Neapturēt" - suspended: "Neapturēts?" moderator: "Moderātors?" admin: "Administrators?" - blocked: "Bloķēts?" + suspended: "Neapturēts?" staged: "Izveidots?" show_admin_profile: "Administrators" refresh_browsers: "Piespiest pārlūka atsvaidzināšanu (Force browser refresh)" @@ -2978,8 +2968,8 @@ lv: grant_admin_confirm: "Mēs nosūtījām jums e-pastu, lai jūs apstiprinātu jauno administratoru. Lūdzu atver e-pastu un seko instrukcijām." revoke_moderation: 'Atsaukt Moderātora pakāpi' grant_moderation: 'Piešķirt Moderātora pakāpi' - unblock: 'Atbloķēt' - block: 'Bloķēt' + unsuspend: 'Neapturēt' + suspend: 'Apturēt' reputation: Reputācija permissions: Atļaujas / Pieejas activity: Aktivitāte @@ -3029,10 +3019,6 @@ lv: activate_failed: "Radās problēma aktivizējot šo lietotāju." deactivate_account: "Deaktivizēts konts" deactivate_failed: "Radās problēma deaktivizējot šo lietotāju." - unblock_failed: 'Radās problēma atbloķējot šo lietotāju.' - block_failed: 'Radās problēma bloķējot šo lietotāju.' - block_confirm: 'Vai esi drošs, ka vēlies bloķēt šo lietotāju? Viņs nevarēs veikt jaunus ierakstus un tēmas.' - block_accept: 'Jā, bloķēt šo lietotāju.' bounce_score: "Pieprasījumu \"Score\"" reset_bounce_score: label: "Atlikt" @@ -3040,7 +3026,6 @@ lv: visit_profile: "Apskatilietotāja iestatījumu lapu lai labotu viņu profilu" deactivate_explanation: "Deaktivizētam lietotājam jā-apstiprina sava e-pasta adrese." suspended_explanation: "Apturēts lietotājs nevar pieslēgties." - block_explanation: "Bloķēts lietotājs nevar veikt ierakstus vai uzsākt tēmas." staged_explanation: "Lietotājs caur iepastu var veikt ierakstus tikai specifiskās tēmās." bounce_score_explanation: none: "Neviens pieprasījums nesen nav saņemts." diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index ca30afe0b7..831a1665d6 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -526,6 +526,7 @@ nb_NO: disable: "Slå av varslinger" enable: "Slå på varslinger" each_browser_note: "Merk: Du må endre denne innstillinger for hver nettleser du bruker." + dismiss: 'Avslå' dismiss_notifications: "Avslå alt" dismiss_notifications_tooltip: "Merk alle uleste varslinger som lest" first_notification: "Ditt første varsel! Velg det for å komme i gang." @@ -540,7 +541,6 @@ nb_NO: admin: "{{user}} er en admin" moderator_tooltip: "Denne brukeren er en moderator" admin_tooltip: "Denne brukeren er en administrator" - blocked_tooltip: "Denne brukeren er blokkert" suspended_notice: "Denne brukeren er bannlyst til {{date}}." suspended_permanently: "Brukeren er bannlyst." suspended_reason: "Begrunnelse:" @@ -592,6 +592,7 @@ nb_NO: undo_revoke_access: "Angre \"trekk tilbake tilgang\"" api_approved: "Godkjent:" theme: "Drakt" + home: "Forvalgt hjemmeside" staff_counters: flags_given: "nyttige rapporteringer" flagged_posts: "rapporterte innlegg" @@ -904,7 +905,8 @@ nb_NO: first_post: Første innlegg mute: Ignorer unmute: Fjern ignorering - last_post: Siste innlegg + last_post: Postet + time_read: Les last_reply_lowercase: siste svar replies_lowercase: one: svar @@ -1041,6 +1043,7 @@ nb_NO: default_header_text: Velg… no_content: Ingen treff funnet filter_placeholder: Søk… + create: "Opprett {{content}}" emoji_picker: filter_placeholder: Søk etter emoji people: Folk @@ -1062,7 +1065,7 @@ nb_NO: emoji: "Emoji :)" more_emoji: "mer…" options: "Alternativer" - whisper: "hvisker" + whisper: "hvisk" unlist: "skjult" blockquote_text: "Sitatramme" add_warning: "Dette er en offisiell advarsel." @@ -1096,6 +1099,7 @@ nb_NO: cancel: "Avbryt" create_topic: "Opprett emne" create_pm: "Melding" + create_whisper: "Hvisk" title: "Eller trykk Ctrl+Enter" users_placeholder: "Legg til en bruker" title_placeholder: "Oppsummert i en setning, hva handler denne diskusjonen om?" @@ -1226,7 +1230,7 @@ nb_NO: no_more_results: "Ingen flere resultater funnet." searching: "Søker…" post_format: "#{{post_number}} av {{username}}" - results_page: "Søkeresultater" + results_page: "Søkeresultater for \"{{term}}\"" more_results: "Det er flere resultater. Begrens søket ditt." cant_find: "Finner du ikke det du leter etter?" start_new_topic: "Kanskje du ønsker å starte et nytt emne?" @@ -1420,6 +1424,9 @@ nb_NO: next_week: "Neste uke" two_weeks: "To uker" next_month: "Neste måned" + three_months: "Tre måneder" + six_months: "Seks måneder" + one_year: "Ett år" forever: "For alltid" pick_date_and_time: "Velg dato og tid" set_based_on_last_post: "Lukk basert på siste post" @@ -1743,8 +1750,8 @@ nb_NO: actions: flag: 'Rapportering' defer_flags: - one: "Utsett rapportering" - other: "Utsett rapporteringer" + one: "Ignorer flagg" + other: "Ignorer flagg" undo: off_topic: "Angre rapportering" spam: "Angre rapportering" @@ -2344,7 +2351,6 @@ nb_NO: no_problems: "Ingen problemer ble funnet." moderators: 'Moderatorer:' admins: 'Adminer:' - blocked: 'sperret:' suspended: 'Bannlyst:' private_messages_short: "Meldinger" private_messages_title: "Meldinger" @@ -2389,11 +2395,11 @@ nb_NO: agree_flag_restore_post_title: "Gjenopprett dette innlegget" agree_flag: "Si deg enig med rapportering" agree_flag_title: "Si deg enig med rapportering og la innlegget stå urørt" - defer_flag: "Utsett" + defer_flag: "Ignorer" defer_flag_title: "Fjern denne rapporteringen; den krever ingen handling på dette tidspunktet." delete: "Slett" delete_title: "Fjern innlegget denne rapporteringen refererer til." - delete_post_defer_flag: "Slett innlegg og utsett rapportering" + delete_post_defer_flag: "Slett innlegg og ignorer flagg" delete_post_defer_flag_title: "Slett innlegg; hvis det er første innlegg, slett emnet" delete_post_agree_flag: "Slett innlegg og si deg enig med rapportering" delete_post_agree_flag_title: "Slett innlegg; hvis det er første innlegg, slett emnet" @@ -2412,7 +2418,7 @@ nb_NO: dispositions: agreed: "enig" disagreed: "uenig" - deferred: "utsatt" + deferred: "ignorert" flagged_by: "Rapportert av" resolved_by: "Løst av" took_action: "Tok Handling" @@ -2426,6 +2432,7 @@ nb_NO: was_edited: "Innlegget ble redigert etter første rapportering" previous_flags_count: "Dette innlegget har allerede blitt rapportert {{count}} ganger." show_details: "Vis rapportdetaljer" + details: "detaljer" flagged_topics: topic: "Emne" type: "Type" @@ -2886,8 +2893,6 @@ nb_NO: change_category_settings: "endre kategori-innstillinger" delete_category: "slett kategori" create_category: "opprett kategori" - block_user: "blokker bruker" - unblock_user: "fjern blokkering av bruker" grant_admin: "innvilg admin" revoke_admin: "tilbakekall admin" grant_moderation: "innvilg moderering" @@ -2982,7 +2987,6 @@ nb_NO: pending: "Ventende" staff: 'Stab' suspended: 'Bannlyst' - blocked: 'Blokkert' suspect: 'Mistenkt' approved: "Godkjent?" approved_selected: @@ -3003,7 +3007,6 @@ nb_NO: staff: "Stab" admins: 'Admin-brukere' moderators: 'Moderatorer' - blocked: 'Blokkerte brukere' suspended: 'Bannlyste brukere' suspect: 'Mistenkte brukere' reject_successful: @@ -3031,12 +3034,9 @@ nb_NO: cant_suspend: "Brukeren kan ikke bannlyses." delete_all_posts: "Slett alle innlegg" delete_all_posts_confirm_MF: "Du er i ferd med å slette {INNLEGG, flertall, ett {ett innlegg} andre {# innlegg}} og {INNLEGG, flertall, ett {ett emne} andre {# emner}}. Er du sikker?" - suspend: "Bannlyst" - unsuspend: "Gjeninnsett" - suspended: "Bannlyst?" moderator: "Moderator?" admin: "Admin?" - blocked: "Blokkert?" + suspended: "Bannlyst?" staged: "Arrangert?" show_admin_profile: "Admin" refresh_browsers: "Tving gjenoffrisking av nettleser" @@ -3052,8 +3052,8 @@ nb_NO: grant_admin_confirm: "Vi har sendt deg en e-post for å bekrefte den nye administratoren. Følg instruksjonene i meldingen." revoke_moderation: 'Tilbakedra moderering' grant_moderation: 'Innvilg moderering' - unblock: 'Opphev blokkering' - block: 'Blokker' + unsuspend: 'Gjeninnsett' + suspend: 'Bannlyst' reputation: Rykte permissions: Tillatelser activity: Aktivitet @@ -3100,10 +3100,6 @@ nb_NO: activate_failed: "Det oppstod et problem ved aktiveringen av den brukeren." deactivate_account: "Skru av konto" deactivate_failed: "Det oppstod et problem ved deaktiveringen av den brukeren." - unblock_failed: 'Det oppstod et problem med å oppheve blokkeringen av brukeren.' - block_failed: 'Det oppstod et problem med blokkeringen av brukeren.' - block_confirm: 'Er du sikker på at du vil blokkere denne brukeren? Vedkommende vil ikke kunne opprette nye emner eller innlegg.' - block_accept: 'Ja, blokker denne brukeren' bounce_score: "Antall tilbakesendinger" reset_bounce_score: label: "Tilbakestill" @@ -3111,7 +3107,6 @@ nb_NO: visit_profile: "Besøk denne brukerinnstillingssiden for å redigere profilen deres" deactivate_explanation: "En deaktivert bruker må re-validere sin e-post." suspended_explanation: "En bannlyst bruker kan ikke logge inn." - block_explanation: "En blokkert bruker kan ikke poste eller starte emner." staged_explanation: "En arrangert bruker kan bare skrive innlegg via e-post i gitte emner." bounce_score_explanation: none: "Ingen tilbakesendinger ble mottatt nylig fra den e-posten." @@ -3229,6 +3224,7 @@ nb_NO: developer: 'Utvikler' embedding: "Innebygging" legal: "Juridisk" + api: 'API' user_api: 'Bruker-API' uncategorized: 'Annet' backups: "Sikkerhetskopier" diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index b891cbadb8..07fbf32c80 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -538,7 +538,6 @@ nl: admin: "{{user}} is een beheerder" moderator_tooltip: "Deze gebruiker is een moderator" admin_tooltip: "Deze gebruiker is een beheerder" - blocked_tooltip: "Deze gebruiker is geblokkeerd" suspended_notice: "Deze gebruiker is geschorst tot {{date}}." suspended_permanently: "Deze gebruiker is geschorst." suspended_reason: "Reden: " @@ -2340,7 +2339,6 @@ nl: no_problems: "Er zijn geen problemen gevonden." moderators: 'Moderators:' admins: 'Beheerders:' - blocked: 'Geblokkeerd:' suspended: 'Geschorst:' private_messages_short: "PB's" private_messages_title: "Berichten" @@ -2892,8 +2890,6 @@ nl: change_category_settings: "categorie-instellingen wijzigen" delete_category: "categorie verwijderen" create_category: "categorie maken" - block_user: "gebruiker blokkeren" - unblock_user: "gebruiker deblokkeren" grant_admin: "beheerdersrechten toekennen" revoke_admin: "beheerdersrechten intrekken" grant_moderation: "moderatierechten toekennen" @@ -2987,7 +2983,6 @@ nl: pending: "Te beoordelen" staff: 'Stafleden' suspended: 'Geschorst' - blocked: 'Geblokkeerd' suspect: 'Verdacht' approved: "Goedgekeurd?" approved_selected: @@ -3008,7 +3003,6 @@ nl: staff: "Stafleden" admins: 'Beheerders' moderators: 'Moderators' - blocked: 'Geblokkeerde gebruikers' suspended: 'Geschorste gebruikers' suspect: 'Verdachte gebruikers' reject_successful: @@ -3036,12 +3030,9 @@ nl: cant_suspend: "Deze gebruiker kan niet worden geschorst." delete_all_posts: "Alle berichten verwijderen" delete_all_posts_confirm_MF: "U gaat {POSTS, plural, one {1 bericht} other {# berichten}} en {TOPICS, plural, one {1 topic} other {# topics}} verwijderen. Weet u het zeker?" - suspend: "Schorsen" - unsuspend: "Schorsing opheffen" - suspended: "Geschorst?" moderator: "Moderator?" admin: "Beheerder?" - blocked: "Geblokkeerd?" + suspended: "Geschorst?" staged: "Staged?" show_admin_profile: "Beheerder" refresh_browsers: "Vernieuwen in browser forceren" @@ -3057,8 +3048,8 @@ nl: grant_admin_confirm: "We hebben u een e-mail gestuurd om de nieuwe beheerder te verifiëren. Open deze en volg de instructies." revoke_moderation: 'Moderatierechten intrekken' grant_moderation: 'Moderatierechten toekennen' - unblock: 'Deblokkeren' - block: 'Blokkeren' + unsuspend: 'Schorsing opheffen' + suspend: 'Schorsen' reputation: Reputatie permissions: Toestemmingen activity: Activiteit @@ -3105,10 +3096,6 @@ nl: activate_failed: "Er is een probleem opgetreden bij het activeren van de gebruiker." deactivate_account: "Deactiveer account" deactivate_failed: "Er is een probleem opgetreden bij het deactiveren van de gebruiker." - unblock_failed: 'Er is een probleem opgetreden bij het deblokkeren van de gebruiker.' - block_failed: 'Er is een probleem opgetreden bij het blokkeren van de gebruiker.' - block_confirm: 'Weet u zeker dat u deze gebruiker wilt blokkeren? De gebruiker zal dan geen nieuwe topics of berichten kunnen plaatsen.' - block_accept: 'Ja, blokkeer deze gebruiker' bounce_score: "Bouncescore" reset_bounce_score: label: "Terugzetten" @@ -3116,7 +3103,6 @@ nl: visit_profile: "Ga naar de instellingen van deze gebruiker om hun profiel te bewerken" deactivate_explanation: "Een gedeactiveerde gebruiker moet zijn of haar e-mailadres opnieuw valideren." suspended_explanation: "Een geschorste gebruiker kan zich niet aanmelden." - block_explanation: "Een geblokkeerde gebruiker kan geen berichten plaatsen of topics maken." staged_explanation: "Een staged gebruiker kan alleen in bepaalde topics berichten plaatsen via e-mail." bounce_score_explanation: none: "Er zijn onlangs geen bounceberichten van dat e-mailadres ontvangen." diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index ae4ec076a2..07c52c7fa0 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -596,7 +596,6 @@ pl_PL: admin: "{{user}} jest adminem" moderator_tooltip: "Ten użytkownik jest moderatorem" admin_tooltip: "Ten użytkownik jest administratorem" - blocked_tooltip: "Ten użytkownik jest zablokowany" suspended_notice: "ten użytkownik jest zawieszony do {{date}}." suspended_reason: "Powód: " github_profile: "Github" @@ -2541,7 +2540,6 @@ pl_PL: no_problems: "Nie znaleziono problemów." moderators: 'Moderatorzy:' admins: 'Adminstratorzy:' - blocked: 'Zablokowani:' suspended: 'Zawieszeni:' private_messages_short: "Wiad." private_messages_title: "Wiadomości" @@ -3109,8 +3107,6 @@ pl_PL: change_category_settings: "zmiana ustawień kategorii" delete_category: "Usuń kategorię" create_category: "Dodaj nową kategorię" - block_user: "zablokuj użytkownika" - unblock_user: "odblokuj użytkownika" grant_admin: "nadaj prawa admina" revoke_admin: "odbierz prawa admina" grant_moderation: "Przyznaj status moderatora" @@ -3207,7 +3203,6 @@ pl_PL: pending: "Oczekujący" staff: 'Zespół' suspended: 'Zawieszeni' - blocked: 'Zablokowani' suspect: 'Podejrzani' approved: "Zatwierdzam?" approved_selected: @@ -3232,7 +3227,6 @@ pl_PL: staff: "Zespół" admins: 'Administratorzy' moderators: 'Moderatoratorzy' - blocked: 'Zablokowane konta' suspended: 'Zawieszone konta' suspect: 'Podejrzani użytkownicy' reject_successful: @@ -3264,12 +3258,9 @@ pl_PL: cant_suspend: "Nie można zawiesić tego użytkownika." delete_all_posts: "Usuń wszystkie wpisy" delete_all_posts_confirm_MF: "Zamierzasz usunąć {POSTS, plural, one {1 post} few {# posty} many {# postów} other {# postów}} i {TOPICS, plural, one {1 temat} few {# tematy} many {# tematów} other {# tematów}}. Czy jesteś pewien?" - suspend: "Zawieś" - unsuspend: "Odwieś" - suspended: "Zawieszony?" moderator: "Moderator?" admin: "Admin?" - blocked: "Zablokowany?" + suspended: "Zawieszony?" staged: "Wystawiony?" show_admin_profile: "Admin" refresh_browsers: "Wymuś odświeżenie przeglądarki" @@ -3285,8 +3276,8 @@ pl_PL: grant_admin_confirm: "Wysłaliśmy adres email, aby zweryfikować nowego administratora. Zapoznaj się z instrukcjami w nim zawartymi." revoke_moderation: 'Odbierz status moderatora' grant_moderation: 'Przyznaj status moderatora' - unblock: 'Odblokuj' - block: 'Blokuj' + unsuspend: 'Odwieś' + suspend: 'Zawieś' reputation: Reputacja permissions: Uprawnienia activity: Aktywność @@ -3339,10 +3330,6 @@ pl_PL: activate_failed: "Wystąpił problem przy aktywacji konta użytkownika." deactivate_account: "Deaktywuj konto" deactivate_failed: "Wystąpił problem przy deaktywacji konta użytkownika." - unblock_failed: 'Wystąpił problem podczaj odblokowania użytkownika.' - block_failed: 'Wystąpił problem podczas blokowania użytkownika.' - block_confirm: 'Czy na pewno chcesz zablokować tego użytkownika ? Nie będzie już mógł tworzyć nowych tematów ani pisać postów.' - block_accept: 'Tak, zablokuj tego użytkownika' bounce_score: "Wskaźnik odbić" reset_bounce_score: label: "Przywróć" @@ -3350,7 +3337,6 @@ pl_PL: visit_profile: "Odwiedź ustawienia tego użytkownika, aby zedytować jego profil." deactivate_explanation: "Wymusza ponowne potwierdzenie adresu email tego konta." suspended_explanation: "Zawieszony użytkownik nie może się logować." - block_explanation: "Zablokowany użytkownik nie może tworzyć wpisów ani zaczynać tematów." staged_explanation: "Etapowy użytkownik może pisać tylko za pośrednictwem poczty email w konkretnych tematach." bounce_score_explanation: none: "Z tego adresu email nie otrzymano ostatnio żadnych wiadomości zwrotnych." diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index 90551d82a1..8684f7b1d6 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -524,7 +524,6 @@ pt: admin: "{{user}} é um administrador" moderator_tooltip: "Este utilizador é um moderador" admin_tooltip: "Este utilizador é um administrador" - blocked_tooltip: "Este utilizador está bloqueado" suspended_notice: "Este utilizador está suspenso até {{date}}." suspended_reason: "Motivo: " github_profile: "Github" @@ -2184,7 +2183,6 @@ pt: no_problems: "Nenhum problema encontrado." moderators: 'Moderadores:' admins: 'Administradores:' - blocked: 'Bloqueado:' suspended: 'Suspenso: ' private_messages_short: "Msgs" private_messages_title: "Mensagens" @@ -2641,8 +2639,6 @@ pt: change_category_settings: "alterar configurações de categoria" delete_category: "eliminar categoria" create_category: "criar categoria" - block_user: "utilizador bloqueado" - unblock_user: "Desbloquear utilizador" grant_admin: "conceder administração" revoke_admin: "revogar administração" grant_moderation: "conceder moderação" @@ -2703,7 +2699,6 @@ pt: pending: "Pendente" staff: 'Pessoal' suspended: 'Suspenso' - blocked: 'Bloqueado' suspect: 'Suspeito' approved: "Aprovado?" approved_selected: @@ -2724,7 +2719,6 @@ pt: staff: "Pessoal" admins: 'Utilizadores da Administração' moderators: 'Moderadores' - blocked: 'Utilizadores Bloqueados' suspended: 'Utilizadores Suspensos' suspect: 'Utilizadores Suspeitos' reject_successful: @@ -2741,18 +2735,14 @@ pt: suspend_failed: "Ocorreu um erro ao suspender este utilizador {{error}}" unsuspend_failed: "Ocorreu um erro ao retirar a suspensão deste utilizador {{error}}" suspend_duration: "Durante quanto tempo o utilizador estará suspenso?" - suspend_duration_units: "(dias)" suspend_reason_label: "Qual é o motivo da sua suspensão? Este texto estará visível para todos na página do perfil deste utilizador, e será mostrada ao utilizador quando tentar iniciar sessão. Mantenha-o breve." suspend_reason: "Motivo" suspended_by: "Suspendido por" delete_all_posts: "Eliminar todas as publicações" delete_all_posts_confirm_MF: "Está prestes a apagar {POSTS, plural, one {1 publicação} other {# publicações}} e {TOPICS, plural, one {1 tópico} other {# tópicos}}. Tem a certeza de que quer continuar?" - suspend: "Suspender" - unsuspend: "Retirar a suspensão" - suspended: "Suspendido?" moderator: "Moderador?" admin: "Administração?" - blocked: "Bloqueado?" + suspended: "Suspendido?" staged: "Temporário?" show_admin_profile: "Administração" refresh_browsers: "Forçar atualização da página no browser" @@ -2766,8 +2756,8 @@ pt: grant_admin: 'Conceder Administração' revoke_moderation: 'Revogar Moderação' grant_moderation: 'Conceder Moderação' - unblock: 'Desbloquear' - block: 'Bloquear' + unsuspend: 'Retirar a suspensão' + suspend: 'Suspender' reputation: Reputação permissions: Permissões activity: Atividade @@ -2814,17 +2804,12 @@ pt: activate_failed: "Ocorreu um problema ao ativar o utilizador." deactivate_account: "Desativar Conta" deactivate_failed: "Ocorreu um problema ao desativar o utilizador." - unblock_failed: 'Ocorreu um problema ao desbloquear o utilizador.' - block_failed: 'Ocorreu um problema ao bloquear o utilizador.' - block_confirm: 'Tem a certeza que pretende bloquear este utilizador? Este não será capaz de criar novos tópicos ou publicações.' - block_accept: 'Sim, bloquear este utilizador' bounce_score: "Pontuação de Redirecionamento" reset_bounce_score: label: "Repor" title: "Repor pontuação de redirecionamento a 0" deactivate_explanation: "Um utilizador desativado deve revalidar o seu email." suspended_explanation: "Um utilizador suspenso não pode iniciar sessão." - block_explanation: "Um utilizador bloqueado não pode publicar publicações ou iniciar tópicos." staged_explanation: "Um utilizador temporário pode apenas publicar por email em tópicos específicos." bounce_score_explanation: none: "Nenhum redirecionamento foi recebido recentemente desse email." diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index 62f52ccb59..2075ac2f24 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -521,7 +521,6 @@ pt_BR: admin: "{{user}} é um administrador" moderator_tooltip: "Esse usuário é da moderação" admin_tooltip: "Esse usuário é da administração" - blocked_tooltip: "Esse usuário está bloqueado." suspended_notice: "Esse usuário está suspenso até {{date}}." suspended_reason: "Motivo:" github_profile: "Github" @@ -2209,7 +2208,6 @@ pt_BR: no_problems: "Nenhum problema encontrado." moderators: 'Moderadores:' admins: 'Admins:' - blocked: 'Bloqueado:' suspended: 'Suspenso:' private_messages_short: "Msgs" private_messages_title: "Mensagens" @@ -2686,8 +2684,6 @@ pt_BR: change_category_settings: "mudas configurações da categoria" delete_category: "apagar a categoria" create_category: "criar uma categoria" - block_user: "bloquear usuário" - unblock_user: "desbloquear usuário" grant_admin: "conceder admin" revoke_admin: "revogar admin" grant_moderation: "conceder moderação" @@ -2754,7 +2750,6 @@ pt_BR: pending: "Pendentes" staff: 'Equipe' suspended: 'Suspenso' - blocked: 'Bloqueados' suspect: 'Suspeito' approved: "Aprovado?" approved_selected: @@ -2775,7 +2770,6 @@ pt_BR: staff: "Equipe de apoio" admins: 'Usuários Administradores' moderators: 'Moderadores' - blocked: 'Usuários Boqueados' suspended: 'Usuários Suspensos' suspect: 'Usuários suspeitos' reject_successful: @@ -2792,18 +2786,14 @@ pt_BR: suspend_failed: "Algo deu errado suspendendo este usuário {{error}}" unsuspend_failed: "Algo deu errado reativando este usuário {{error}}" suspend_duration: "Por quanto tempo o usuário deverá ser suspenso?" - suspend_duration_units: "(dias)" suspend_reason_label: "Por que você está suspendendo? Esse texto será visível para todos na página de perfil desse usuário, e será mostrado ao usuário quando ele tentar se logar. Seja breve." suspend_reason: "Motivo" suspended_by: "Suspenso por" delete_all_posts: "Apagar todas mensagens" delete_all_posts_confirm_MF: "Você está prestes a apagar {POSTS, plural, one {1 publicação} other {# publicações}} and {TOPICS, plural, one {1 tópico} other {# tópicos}}. Você tem certeza?" - suspend: "Suspender" - unsuspend: "Readmitir" - suspended: "Suspenso?" moderator: "Moderador?" admin: "Admin?" - blocked: "Bloqueado?" + suspended: "Suspenso?" staged: "Testado?" show_admin_profile: "Admin" refresh_browsers: "Forçar atualização da página no browser" @@ -2819,8 +2809,8 @@ pt_BR: grant_admin_confirm: "Nós o enviamos um email para verificar o novo administrador. Por favor abra-o e siga as instruções." revoke_moderation: 'Revogar Moderação' grant_moderation: 'Conceder Moderação' - unblock: 'Desbloquear' - block: 'Bloquear' + unsuspend: 'Readmitir' + suspend: 'Suspender' reputation: Reputação permissions: Permissões activity: Atividade @@ -2867,17 +2857,12 @@ pt_BR: activate_failed: "Houve um problema ao tornar o usuário ativo." deactivate_account: "Desativar Conta" deactivate_failed: "Houve um problema ao desativar o usuário." - unblock_failed: 'Houve um problema ao desbloquear o usuário.' - block_failed: 'Houve um problema ao bloquear o usuário.' - block_confirm: 'Você tem certeza que quer bloquear este usuário? Ele não vai poder criar nenhum tópico ou postagem nova.' - block_accept: 'Sim, bloquear este usuário' bounce_score: "Pontuação de Devolução" reset_bounce_score: label: "Redefinir" title: "Redefinir pontuação de devolução para 0" deactivate_explanation: "Um usuário desativado deve revalidar seu email." suspended_explanation: "Um usuário suspenso não pode entrar." - block_explanation: "Um usuário bloqueado não pode postar ou iniciar tópicos." staged_explanation: "Um usuário de teste só pode postar por email em tópicos específicos." bounce_score_explanation: none: "Nenhuma devolução foi recebida recentemente daquele email." diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index 8a1d7b4e74..b88659b240 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -535,7 +535,6 @@ ro: admin: "{{user}} este admin" moderator_tooltip: "Acest utilizator este moderator" admin_tooltip: "Acest utilizator este admin" - blocked_tooltip: "Acest utilizator este blocat." suspended_notice: "Acest utilizator este suspendat până la {{date}}." suspended_reason: "Motiv: " github_profile: "Github" @@ -2194,7 +2193,6 @@ ro: no_problems: "Nu a apărut nicio problemă." moderators: 'Moderatori:' admins: 'Admini:' - blocked: 'Blocați:' suspended: 'Suspendați:' private_messages_short: "Mesje" private_messages_title: "Mesaje" @@ -2657,8 +2655,6 @@ ro: change_category_settings: "schimbă setările categoriei" delete_category: "șterge categorie" create_category: "creează categorie" - block_user: "blochează utilizator" - unblock_user: "deblochează utilizator" grant_admin: "Acordă titlul de Admin" revoke_admin: "Revocă titlul de Admin" grant_moderation: "Acordă titlul de Moderator" @@ -2717,7 +2713,6 @@ ro: pending: "În așteptare" staff: 'Membrii echipei' suspended: 'Suspendați' - blocked: 'Blocați' suspect: 'Suspecți' approved: "Aprobați?" approved_selected: @@ -2740,7 +2735,6 @@ ro: staff: "Echipa" admins: 'Administratori' moderators: 'Moderatori' - blocked: 'Utilizatori blocați' suspended: 'Utilizatori suspendați' suspect: 'Utilizatori suspecţi' reject_successful: @@ -2759,18 +2753,14 @@ ro: suspend_failed: "Ceva nu a funcționat la suspendarea acestui utilizator {{error}}" unsuspend_failed: "Ceva nu a funcționat la activarea acestui utilizator {{error}}" suspend_duration: "Pentru cât timp va fi suspendat utilizatorul?" - suspend_duration_units: "(zile)" suspend_reason_label: "Motivul suspendării? Acest text va fi vizibil pe pagina de profil a utilizatorului, și va fi arătat utilizatorului atunci când încearcă să se autentifice. Încearcă să fii succint." suspend_reason: "Motiv" suspended_by: "Suspendat de" delete_all_posts: "Șterge toate postările" delete_all_posts_confirm_MF: "Ești pe cale să ștergi {POSTS, plural, one {1 postare} other {# postări}} și {TOPICS, plural, one {1 subiect} other {# subiecte}}. Ești sigur?" - suspend: "Suspendă" - unsuspend: "Reactivează" - suspended: "Suspendat?" moderator: "Moderator?" admin: "Admin?" - blocked: "Blocat?" + suspended: "Suspendat?" staged: "În așteptare?" show_admin_profile: "Admin" refresh_browsers: "Forțează reîmprospătarea browserului" @@ -2784,8 +2774,8 @@ ro: grant_admin: 'Acordă titlu de admin' revoke_moderation: 'Revocă titlu de moderator' grant_moderation: 'Acordă titlu de moderator' - unblock: 'Deblochează' - block: 'Blochează' + unsuspend: 'Reactivează' + suspend: 'Suspendă' reputation: Reputație permissions: Permisiuni activity: Activitate @@ -2835,17 +2825,12 @@ ro: activate_failed: "A apărut o problemă la activarea utilizatorului." deactivate_account: "Dezactivează cont" deactivate_failed: "A apărut o problemă la dezactivarea contului." - unblock_failed: 'A apărut o problemă la deblocarea contului.' - block_failed: 'A apărut o problemă la blocarea contului.' - block_confirm: 'Ești sigur că vrei să blochezi acest utilizator? Nu va mai putea să creeze niciun subiect sau postare.' - block_accept: 'Blochează utilizatorul' bounce_score: "Rata de ricoșeu" reset_bounce_score: label: "Resetează" title: "Resetează rata de ricoșeu înapoi la 0" deactivate_explanation: "Un utilizator dezactivat va trebui să-și revalideze email-ul." suspended_explanation: "Un utilizator suspendat nu se poate autentifica" - block_explanation: "Un utilizator blocat nu poate posta sau scrie un subiect." staged_explanation: "Un utilizator în așteptare poate posta numai prin email la subiectele specifice." bounce_score_explanation: none: "În ultimul timp nu au fost primite ricoșeuri de acest email." diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 1f5f183c18..56e57730c9 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -152,6 +152,7 @@ ru: split_topic: "Разделил эту тему %{when}" invited_user: "Пригласил %{who} %{when}" invited_group: "Пригласил %{who} %{when}" + user_left: "%{who} написал(а) это сообщение %{when}" removed_user: "Исключил %{who} %{when}" removed_group: "Исключил %{who} %{when}" autoclosed: @@ -172,6 +173,8 @@ ru: visible: enabled: 'Включил в списки %{when}' disabled: 'Исключил из списков %{when}' + banner: + enabled: 'создал(а) из этого баннер %{when}. Он будет виден вверху каждой страницы пока пользователь не закроет его.' topic_admin_menu: "действия администратора над темой" wizard_required: "Добро пожаловать в ваш новый Discourse! Начните с мастера настройки ✨" emails_are_disabled: "Все исходящие письма были глобально отключены администратором. Уведомления любого вида не будут отправляться на почту." @@ -577,7 +580,6 @@ ru: admin: "{{user}} - админ" moderator_tooltip: "Этот пользователь модератор" admin_tooltip: "{{user}} - админ" - blocked_tooltip: "Этот пользователь заблокирован" suspended_notice: "Пользователь заморожен до {{date}}." suspended_permanently: "Этот пользователь заморожен." suspended_reason: "Причина:" @@ -1196,6 +1198,7 @@ ru: empty: "Уведомления не найдены." more: "посмотреть более ранние уведомления" total_flagged: "всего сообщений с жалобами" + granted_badge: "Заслужил(а) '{{description}}'" alt: mentioned: "Упомянуто" quoted: "Процитировано пользователем" @@ -2442,7 +2445,6 @@ ru: no_problems: "Проблем не обнаружено." moderators: 'Модераторы:' admins: 'Администраторы:' - blocked: 'Заблокированы:' suspended: 'Заморожен:' private_messages_short: "Сообщ." private_messages_title: "Сообщений" @@ -2993,8 +2995,6 @@ ru: change_category_settings: "изменена настройка раздела" delete_category: "удален раздел" create_category: "создан раздел" - block_user: "пользователь заблокирован" - unblock_user: "пользователь разблокирован" grant_admin: "выданы права администратора" revoke_admin: "отозваны права администратора" grant_moderation: "выданы права модератора" @@ -3061,7 +3061,6 @@ ru: pending: "Ожидает одобрения" staff: 'Персонал' suspended: 'Замороженные' - blocked: 'Заблокированные' suspect: 'Подозрительные' approved: "Подтвердить?" approved_selected: @@ -3086,7 +3085,6 @@ ru: staff: "Персонал" admins: 'Администраторы' moderators: 'Модераторы' - blocked: 'Заблокированные пользователи' suspended: 'Замороженные пользователи' suspect: 'Подозрительные пользователи' reject_successful: @@ -3113,12 +3111,9 @@ ru: cant_suspend: "Этого пользователя нельзя заморозить." delete_all_posts: "Удалить все сообщения" delete_all_posts_confirm_MF: "Вы собираетесь удалить {POSTS, plural, one {1 сообщение} other {# сообщений}} и {TOPICS, plural, one {1 тему} other {# тем}}. Вы уверены?" - suspend: "Заморозить" - unsuspend: "Разморозить" - suspended: "Заморожен?" moderator: "Модератор?" admin: "Администратор?" - blocked: "Заблокирован?" + suspended: "Заморожен?" staged: "Имитация?" show_admin_profile: "Администратор" refresh_browsers: "Выполнить перезагрузку браузера" @@ -3134,8 +3129,8 @@ ru: grant_admin_confirm: "Мы отправили вам письмо с инструкциями для активации нового администратора." revoke_moderation: 'Лишить прав Модератора' grant_moderation: 'Выдать права Модератора' - unblock: 'Разблокировать' - block: 'Заблокировать' + unsuspend: 'Разморозить' + suspend: 'Заморозить' reputation: Репутация permissions: Права activity: Активность @@ -3188,17 +3183,12 @@ ru: activate_failed: "Во время активации пользователя произошла ошибка." deactivate_account: "Деактивировать" deactivate_failed: "Во время деактивации пользователя произошла ошибка." - unblock_failed: 'Не удалось разблокировать пользователя.' - block_failed: 'Не удалось заблокировать пользователя.' - block_confirm: 'Вы уверены что хотите заблокировать этого пользователя? Он больше не сможет создавать темы и отправлять сообщения.' - block_accept: 'Подтвердить блокировку' bounce_score: "Возвратов Писем" reset_bounce_score: label: "Сбросить" title: "сбросить карму к 0" deactivate_explanation: "Дезактивированные пользователи должны заново подтвердить свой e-mail." suspended_explanation: "Замороженный пользователь не может войти (авторизоваться)." - block_explanation: "Заблокированный не может отвечать и создавать новые темы." staged_explanation: "Имитированный пользователь может отправлять сообщения только по эл.почте в определённые темы." bounce_score_explanation: none: "Нет возвратов полученных недавно от этой эл.почты." diff --git a/config/locales/client.sk.yml b/config/locales/client.sk.yml index 2d8d67a54b..efc0917fab 100644 --- a/config/locales/client.sk.yml +++ b/config/locales/client.sk.yml @@ -504,7 +504,6 @@ sk: admin: "{{user}} je administrátor" moderator_tooltip: "Tento používateľ je moderátor" admin_tooltip: "Tento používateľ je administrátor" - blocked_tooltip: "Tento používateľ je zablokovaný" suspended_notice: "Tento používateľ je suspendovaný do {{date}}" suspended_reason: "Dôvod:" github_profile: "Github" @@ -2050,7 +2049,6 @@ sk: no_problems: "Nenašli sa žiadne problémy." moderators: 'Moderátori:' admins: 'Administrátori:' - blocked: 'Zablokované:' suspended: 'Odobraté:' private_messages_short: "Správy" private_messages_title: "Správy" @@ -2434,8 +2432,6 @@ sk: change_category_settings: "zmeniť nastavenia kategórie" delete_category: "odstrániť kategóriu" create_category: "vytvoriť kategóriu" - block_user: "blokovať používateľa" - unblock_user: "odblokovať používateľa" grant_admin: "udeliť administrátorské práva" revoke_admin: "odobrať admin" grant_moderation: "udeliť moderátorské práva" @@ -2493,7 +2489,6 @@ sk: pending: "Čakajúci" staff: 'Zamestnanci' suspended: 'Odobrate práva' - blocked: 'Zablokovaný' suspect: 'Podozrivý' approved: "Schválený?" approved_selected: @@ -2516,7 +2511,6 @@ sk: staff: "Zamestnanci" admins: 'Admin používatelia' moderators: 'Moderátori' - blocked: 'Zablokovaní užívatelia' suspended: 'Užívatelia s odobratými právami' suspect: 'Podozriví užívatelia' reject_successful: @@ -2535,17 +2529,13 @@ sk: suspend_failed: "Niečo sa pokazilo pri odoberaní práv tomuto používateľovi {{error}}" unsuspend_failed: "Niečo sa pokazilo pri obnovovaní práv tomuto používateľovi {{error}}" suspend_duration: "Ako dlho budú používateľovi odobrate práva?" - suspend_duration_units: "(dni)" suspend_reason_label: "Prečo mu odoberáte práva? Tento text sa zobrazí každému na stránke profilu používateľa a bude zobrazený užívateľovi pri pokuse o prihlásenie. Buďte strucný." suspend_reason: "Dôvod" suspended_by: "Práva odobraté" delete_all_posts: "Zmazať všetky príspevky" - suspend: "Odobrať" - unsuspend: "Obnoviť" - suspended: "Odobrate práva?" moderator: "Moderátor?" admin: "Admin?" - blocked: "Blokovaný?" + suspended: "Odobrate práva?" staged: "Dočasný?" show_admin_profile: "Admin" refresh_browsers: "Vynútiť refresh browsera." @@ -2559,8 +2549,8 @@ sk: grant_admin: 'Udeliť admin' revoke_moderation: 'Odobrať moderovanie' grant_moderation: 'Udeliť moderovanie' - unblock: 'Odblokovať' - block: 'Blokovať' + unsuspend: 'Obnoviť' + suspend: 'Odobrať' reputation: Reputácia permissions: Práva activity: Aktivita @@ -2610,13 +2600,8 @@ sk: activate_failed: "Počas aktivácie používateľa nastala chyba." deactivate_account: "Deaktivovať účet" deactivate_failed: "Počas deaktivácie používateľa nastala chyba." - unblock_failed: 'Nastala chyba pri odblokovaní používateľa.' - block_failed: 'Nastala chyba pri zablokovaní používateľa.' - block_confirm: 'Ste si istý tým, že chcete zablokovať tohoto používateľa? Nebude môcť vytvárať žiadne nové témy alebo príspevky.' - block_accept: 'Ano, zablokovať používateľa' deactivate_explanation: "Deaktivovaý používateľ musí znovu overiť svoj email" suspended_explanation: "Suspendovaní užívatelia sa nemôžu prihlasovať." - block_explanation: "Zablokovaní používatelia nemôžu zakladať témy ani pridávať príspevky." staged_explanation: "Dočasný používateľ môže iba prispievať emailom do vybraných tém." trust_level_change_failed: "Nastala chyba pri zmene stupňa dôvery používateľa." suspend_modal_title: "Zruš práva používateľovi" diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index 36a04370c4..6187236ca2 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -492,7 +492,6 @@ sq: admin: "{{user}} është admin" moderator_tooltip: "Ky anëtar është moderator" admin_tooltip: "Ky anëtar është administrator" - blocked_tooltip: "Ky anëtar është i bllokuar" suspended_notice: "Ky anëtar është përjashtuar deri më {{date}}." suspended_reason: "Arsyeja:" github_profile: "Github" @@ -2027,7 +2026,6 @@ sq: no_problems: "Nuk u gjet asnjë gabim." moderators: 'Moderatorët:' admins: 'Administratorët:' - blocked: 'Bllokuar:' suspended: 'Përjashtuar:' private_messages_short: "Msgs" private_messages_title: "Mesazhet" @@ -2361,8 +2359,6 @@ sq: change_category_settings: "ndrysho parametrat e kategorisë" delete_category: "fshi kategorinë" create_category: "krijo një kategori" - block_user: "blloko anëtarin" - unblock_user: "çblloko anëtarin" grant_admin: "jepi rolin admin" revoke_admin: "hiq rolin admin" grant_moderation: "jepi rolin moderator" @@ -2412,7 +2408,6 @@ sq: pending: "Pezulluar" staff: 'Stafi' suspended: 'Të pezulluar' - blocked: 'Të bllokuar' suspect: 'Të dyshimtë' approved: "Aprovuar?" approved_selected: @@ -2433,7 +2428,6 @@ sq: staff: "Stafi" admins: 'Administratorë' moderators: 'Moderatorë' - blocked: 'Përdorues të Bllokuar' suspended: 'Përdorues të Pezulluar' suspect: 'Përdorues të Dyshimtë' reject_successful: @@ -2450,17 +2444,13 @@ sq: suspend_failed: "Pati një problem gjatë pezullimit të anëtarit. {{error}}" unsuspend_failed: "Pati një problem gjatë çpezullimit të anëtarit. {{error}}" suspend_duration: "Për sa kohë do të pezullohet anëtari?" - suspend_duration_units: "(ditë)" suspend_reason_label: "Pse po e pezulloni? Ky tekst do të shfaqet publikisht në profilin e këtij anëtari, dhe do t'i shfaqet vetë anëtarit si mesazh kur ky i fundit do tentojë të futet në faqe. Shkruani shkurt." suspend_reason: "Arsye" suspended_by: "Përjashtuar nga:" delete_all_posts: "Fshi gjithë postimet" - suspend: "Pezullo" - unsuspend: "Çpezullo" - suspended: "Pezulluar?" moderator: "Moderator?" admin: "Admin?" - blocked: "Bllokuar?" + suspended: "Pezulluar?" staged: "Staged?" show_admin_profile: "Admin" refresh_browsers: "Forco rifreskimin e shfletuesit" @@ -2474,8 +2464,8 @@ sq: grant_admin: 'Jepi rolin admin' revoke_moderation: 'Hiqi rolin moderator' grant_moderation: 'Jepi rolin moderator' - unblock: 'Çblloko' - block: 'Blloko' + unsuspend: 'Çpezullo' + suspend: 'Pezullo' reputation: Reputacioni permissions: Të drejtat activity: Aktiviteti @@ -2509,17 +2499,12 @@ sq: activate_failed: "Pati një problem gjatë aktivizimit të anëtarit." deactivate_account: "Çaktivizo llogarinë" deactivate_failed: "Pati një problem gjatë çaktivizimit të anëtarit." - unblock_failed: 'Pati një problem gjatë çbllokimit të anëtarit.' - block_failed: 'Pati një problem gjatë bllokimit të anëtarit.' - block_confirm: 'A jeni të sigurtë që doni të bllokoni këtë anëtar? Anëtari nuk do të ketë më të drejtën të krijojë postime ose tema të reja.' - block_accept: 'Po, blloko anëtarin' bounce_score: "Rezultati \"bounce\"" reset_bounce_score: label: "Rivendos" title: "Rivendos në 0 rezultatin \"bounce\"" deactivate_explanation: "Një anëtar i çaktivizuar duhet të ri-verifikojë adresën email." suspended_explanation: "Një anëtar i pezulluar nuk ka të drejtë të identifikohet. " - block_explanation: "Një anëtar i bllokuar nuk mund të përgjigjet apo të hapë tema të reja. " staged_explanation: "Një anëtar \"staged\" mund të postojë vetëm me email në tema të caktuara. " bounce_score_explanation: none: "Nuk ka patur mesazhe email të kthyera për këtë anëtar." diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index 2e8ccc07a3..1b04795277 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -509,7 +509,6 @@ sv: admin: "{{user}} är en admin" moderator_tooltip: "Den här användaren är moderator" admin_tooltip: "Den här användaren är administrator" - blocked_tooltip: "Den här användaren är blockerad" suspended_notice: "Den här användaren är avstängd till {{date}}." suspended_reason: "Anledning:" github_profile: "Github" @@ -1517,7 +1516,7 @@ sv: reply_as_new_private_message: "Svara som nytt meddelande till samma mottagare" continue_discussion: "Fortsätter diskussionen från {{postLink}}:" follow_quote: "gå till det citerade inlägget" - show_full: "Via hela inlägget" + show_full: "Visa hela inlägget" show_hidden: 'Visa dolt innehåll.' deleted_by_author: one: "(inlägg tillbakadraget av skaparen, kommer att raderas automatiskt om 1 timme om det inte flaggas)" @@ -2174,7 +2173,6 @@ sv: no_problems: "Inga problem upptäcktes." moderators: 'Moderatorer:' admins: 'Administratörer:' - blocked: 'Blockerad:' suspended: 'Avstängd:' private_messages_short: "Meddelanden" private_messages_title: "Meddelanden" @@ -2636,8 +2634,6 @@ sv: change_category_settings: "ändra kategori-inställningarna" delete_category: "radera kategori" create_category: "skapa kategori" - block_user: "blockera användare" - unblock_user: "häv blockering av användare" grant_admin: "bevlija administratör" revoke_admin: "återkalla administratör" grant_moderation: "bevilja moderering" @@ -2704,7 +2700,6 @@ sv: pending: "Avvaktar" staff: 'Medarbetare' suspended: 'Avstängd' - blocked: 'Blockerad' suspect: 'Misstänkt' approved: "Godkänd?" approved_selected: @@ -2725,7 +2720,6 @@ sv: staff: "Medarbetare" admins: 'Admin-användare' moderators: 'Moderatorer' - blocked: 'Blockerade användare' suspended: 'Avstängda användare' suspect: 'Misstänkta användare' reject_successful: @@ -2742,18 +2736,14 @@ sv: suspend_failed: "Någonting gick fel under avstängningen av denna användare {{error}}" unsuspend_failed: "Någonting gick fel under upplåsningen av denna användare {{error}}" suspend_duration: "Hur länge ska användaren vara avstängd?" - suspend_duration_units: "(dagar)" suspend_reason_label: "Varför stänger du av användaren? Denna text kommer att vara synlig för alla på användarens profilsida, och kommer att visas för användaren när han/hon försöker logga in. Håll den kort." suspend_reason: "Anledning" suspended_by: "Avstängd av" delete_all_posts: "Radera alla inlägg" delete_all_posts_confirm_MF: "Du håller på att radera {POSTS, plural, one {1 inlägg} other {# inlägg}} och {TOPICS, plural, one {1 ämne} other {# ämnen}}. Är du säker?" - suspend: "Stäng av användare" - unsuspend: "Lås upp användare" - suspended: "Avstängd?" moderator: "Moderator?" admin: "Administratör?" - blocked: "Blockerad?" + suspended: "Avstängd?" staged: "Arrangerad?" show_admin_profile: "Administratör" refresh_browsers: "Tvinga webbläsaruppdatering" @@ -2769,8 +2759,8 @@ sv: grant_admin_confirm: "Vi har skickat ett mail till dig för att bekräfta den nya administratören. Vänligen öppna det och följ instruktionerna." revoke_moderation: 'Återkalla Moderering' grant_moderation: 'Bevilja Moderering' - unblock: 'Avblockera' - block: 'Blockera' + unsuspend: 'Lås upp användare' + suspend: 'Stäng av användare' reputation: Rykte permissions: Rättigheter activity: Aktivitet @@ -2817,17 +2807,12 @@ sv: activate_failed: "Ett problem uppstod då användaren skulle aktiveras." deactivate_account: "Inaktivera Konto" deactivate_failed: "Det uppstod ett problem vid inaktiveringen av användaren." - unblock_failed: 'Ett problem uppstod vid avblockeringen av användaren.' - block_failed: 'Ett problem uppstod vid blockering av användaren.' - block_confirm: 'Är du säker på att du vill blockera den här användaren? Användaren kommer inte att kunna skapa några nya ämnen eller inlägg.' - block_accept: 'Ja, blockera den här användaren' bounce_score: "Antal studs" reset_bounce_score: label: "Återställ" title: "Återställ studsantalet till 0" deactivate_explanation: "En avaktiverad användare måste bekräfta sin e-postadress igen." suspended_explanation: "En avstängd användare kan inte logga in." - block_explanation: "En blockerad användare kan inte skriva inlägg eller starta ämnen." staged_explanation: "En arrangerad användare kan endast skriva inlägg via e-post i specifika ämnen." bounce_score_explanation: none: "Inga studsar har mottagits nyligen från den e-posten." diff --git a/config/locales/client.te.yml b/config/locales/client.te.yml index 46aa61838b..bd22b8caba 100644 --- a/config/locales/client.te.yml +++ b/config/locales/client.te.yml @@ -1096,7 +1096,6 @@ te: no_problems: "ఎటువంటి సమస్యలూ కనిపించలేదు" moderators: 'నిర్వాహకులు:' admins: 'అధికారులు:' - blocked: 'నిలిపిన:' suspended: 'సస్పెండయిన:' space_free: "{{size}} ఖాలీ" uploads: "ఎగుమతులు" @@ -1441,7 +1440,6 @@ te: pending: "పెండింగు" staff: 'సిబ్బంది' suspended: 'సస్పెడయ్యాడు' - blocked: 'నిలిపాడు' suspect: 'అనుమానించు' approved: "అంగీకరించు" approved_selected: @@ -1459,7 +1457,6 @@ te: staff: "సిబ్బంది" admins: 'అధికారి సభ్యులు' moderators: 'నిర్వాహకులు' - blocked: 'నిలిపిన సభ్యులు' suspended: 'సస్పెండయిన సభ్యులు' suspect: 'అనుమానిత సభ్యులు' reject_successful: @@ -1475,17 +1472,13 @@ te: suspend_failed: "ఈ సభ్యుడిని సస్పెండ్ చేసేప్పుడు ఏదో తేడా జరిగింది. {{error}}" unsuspend_failed: "ఈ వినియోగదారు వలన ఏదో తొలగింపబడని తప్పు జరిగింది {{error}}" suspend_duration: "వినియోగదారు ఎంతకాలం నిలిపివేయబడ్డాడు?" - suspend_duration_units: "(రోజులు)" suspend_reason_label: "మీరు ఎందుకు తొలగించబడ్డారు? ఈ పాఠ్యం వినియోగదారును ప్రొఫైల్ పుట మీద ప్రతివారికి కనబడుతుంది, మరియు వినియోగదారుడు లాగిన్‌కు ప్రయత్నించినపుడు చూస్తారు.చిన్నదిగా ఉంచండి." suspend_reason: "కారణం" suspended_by: "సస్పెండు చేసినవారు" delete_all_posts: "అన్ని టపాలూ తొలగించు" - suspend: "సస్పెండు" - unsuspend: "సస్పెండు తొలగించు" - suspended: "సస్పెండయ్యాడా? " moderator: "నిర్వాహకుడు?" admin: "అధికారి?" - blocked: "నిలిపిన?" + suspended: "సస్పెండయ్యాడా? " show_admin_profile: "అధికారి" refresh_browsers: "బ్రౌజరు తాజాకరణ బలవంతంచేయి" refresh_browsers_message: "అన్ని క్లైంటులకు సందేశం పంపబడింది!" @@ -1498,8 +1491,8 @@ te: grant_admin: 'నిర్వాహకులు సమ్మతించారు' revoke_moderation: 'సమన్వయం నిలిపివేశారు' grant_moderation: 'సమన్వయం అనుమతించారు' - unblock: 'అడ్డగింపలేదు' - block: 'నిలుపు' + unsuspend: 'సస్పెండు తొలగించు' + suspend: 'సస్పెండు' reputation: ప్రసిధ్ధ permissions: అనుమతులు activity: కలాపం @@ -1533,11 +1526,8 @@ te: activate_failed: "సభ్యుడిని చేతనం చేయుటలో దోషం" deactivate_account: "ఖాతా అక్రియాశీలం చేయి" deactivate_failed: "వినియోగదారుని నిర్వీర్యం చేసే ఒక సమస్య ఉంది." - unblock_failed: 'వినియోగదారుని అనుమతించడంలో ఒక సమస్య ఉంది.' - block_failed: 'వినియోగదారుని ఒక సమస్య నిరోధిస్తుంది.' deactivate_explanation: "క్రియారహిత వినియోగదారు తప్పనిసరిగా వారి ఈ-మెయిల్ ను సరిదిద్దాలి." suspended_explanation: "నిలిపివేయబడ్డ వినియోగదారు లాగిన్ కాలేరు." - block_explanation: "అడ్డగింపబడ్డ వినియోగదారు టపాలు చేయలేరు లేదా విషయాలు మొదలుపెట్టలేరు." trust_level_change_failed: "వినియోగదారు నమ్మకపు స్థాయి మార్చడానికి సమస్య ఉంది." suspend_modal_title: "సభ్యుడిని సస్పెండు చేయి" trust_level_2_users: "నమ్మకం స్థాయి 2 సభ్యులు" diff --git a/config/locales/client.th.yml b/config/locales/client.th.yml index 814b6648aa..2310a2548f 100644 --- a/config/locales/client.th.yml +++ b/config/locales/client.th.yml @@ -444,7 +444,6 @@ th: admin: "{{user}} เป็นผู้ดูแลระบบ" moderator_tooltip: "ผู้ใช้นี้เป็นผู้ตรวจสอบ" admin_tooltip: "ผู้ใช้นี้เป็นผู้ดูแลระบบ" - blocked_tooltip: "ผู้ใช้นี้ถูกระงับ" suspended_notice: "ผู้ใช้นี้ถูกระงับตั้งแต่ {{date}}" suspended_reason: "เหตุผล:" github_profile: "Github" @@ -1349,7 +1348,6 @@ th: no_problems: "ไม่พบปัญหา" moderators: 'ผู้ดูแล:' admins: 'ผู้ดูแลระบบ:' - blocked: 'ถูกบล็อก:' suspended: 'ถูกปิด:' private_messages_short: "ข้อความ" private_messages_title: "ข้อความ" @@ -1500,7 +1498,6 @@ th: pending: "กำลังรอ" staff: 'ทีมงาน' suspended: 'ระงับการใช้งาน' - blocked: 'ถูกบล็อก' suspect: 'ต้องสงสัย' approved: "ยืนยันหรือไม่" approved_selected: @@ -1517,7 +1514,6 @@ th: regular: 'ผู้ใช้ที่ระดับความไว้ใจ 3 (ทั่วไป)' leader: 'ผู้ใช้ที่ระดับความไว้ใจ 4 (ผู้นำ)' staff: "ทีมงาน" - blocked: 'ผู้ใช้งานที่ถูกบล็อก' suspended: 'ผู้ใช้งานที่ถูกระงับ' suspect: 'ผู้ใช้งานที่ต้องสงสัย' not_verified: "ยังไม่ได้รับการตรวจสอบ" @@ -1528,15 +1524,12 @@ th: suspend_failed: "มีข้อผิดพลาดในการระงับการใช้งาน {{error}}" unsuspend_failed: "มีข้อผิดพลาดในเปิดการใช้งาน {{error}}" suspend_duration: "ระงับการใช้งานถึงเมื่อไร" - suspend_duration_units: "(วัน)" suspended_by: "ระงับการใช้งานโดย" delete_all_posts: "ลบโพสทั้งหมด" - suspend: "ระงับการใช้งาน" - unsuspend: "เปิดการใช้งาน" suspended: "ระงับการใช้งานหรือไม่" - blocked: "บล็อกหรือไม่" ip_lookup: "ค้นหา IP" - unblock: 'ปลดบล็อก' + unsuspend: 'เปิดการใช้งาน' + suspend: 'ระงับการใช้งาน' last_100_days: 'ใน 100 วันที่ผ่านมา' private_topics_count: หัวข้อส่วนตัว posts_read_count: อ่านโพส diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 4202b1b896..2a0c007044 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -98,6 +98,7 @@ tr_TR: split_topic: "bu konuyu %{when} ayırdı" invited_user: "%{when} %{who} davet edildi" invited_group: "%{who} %{when} davet edildi" + user_left: "%{who} bu iletiden ayrıldı %{when}" removed_user: "%{when} %{who} silindi" removed_group: "%{who} %{when} kaldırıldı" autoclosed: @@ -119,6 +120,7 @@ tr_TR: enabled: '%{when} listelendi' disabled: '%{when} listelenmedi' topic_admin_menu: "konuyla alakalı yönetici eylemleri" + wizard_required: "Yeni Discourse'unuza hoşgeldiniz! Hadi kuruluma başlayalım! Kurulum Sihirbazı ✨" emails_are_disabled: "Tüm giden e-postalar yönetici tarafından genel olarak devre dışı bırakıldı. Herhangi bir e-posta bildirimi gönderilmeyecek." bootstrap_mode_enabled: "Yeni sitenizi kolayca çalıştırmak için bootstrap modundasınız. Tüm yeni kullanıcılar güven seviyesi 1den başlar ve e-posta uyarıcıları açıksa günlük mail alırlar. Bu özellik kullanıcı sayısı %{min_users} rakamına ulaştığında otomatik kapatılacaktır." bootstrap_mode_disabled: "Bootstrap modu önümüzdeki 24 saat içinde edilgen olacaktır." @@ -134,8 +136,10 @@ tr_TR: cn_north_1: "China (Beijing)" eu_central_1: "EU (Frankfurt)" eu_west_1: "EU (Ireland)" + eu_west_2: "AB (Londra)" sa_east_1: "South America (Sao Paulo)" us_east_1: "US East (N. Virginia)" + us_east_2: "ABD Doğu (Ohio)" us_gov_west_1: "AWS GovCloud (US)" us_west_1: "US West (N. California)" us_west_2: "US West (Oregon)" @@ -321,6 +325,8 @@ tr_TR: add_members: "Üyeleri ekle" delete_member_confirm: "'%{username}' adlı kullanıcıyı '%{group}' grubundan çıkart?" name_placeholder: "Grup adı, kullanıcı adındaki gibi boşluksuz olmalı" + public_admission: "Kullanıcıların gruba özgürce katılmalarına izin ver (Herkese açık olarak görünür grup gerektirir)" + public_exit: "Kullanıcıların grubu özgürce terk etmesine izin ver" empty: posts: "Bu grubun üyelerinden konu yok." members: "Bu grupta üye bulunmamaktır." @@ -336,8 +342,11 @@ tr_TR: automatic_group: Otomatik Grup closed_group: Kapanmış Grup is_group_user: "Bu grubun bir üyesisiniz." + allow_membership_requests: "Kullanıcıların grup sahiplerine üyelik talepleri göndermesine izin ver" membership_request: submit: "İstek Gönderin" + title: "@%{group_name} için Katılma isteği " + reason: "Grup sahiplerine bu gruba neden üye olduğunuzu bildirin" membership: "Üyelik" name: "İsim" user_count: "Grup Sayısı" @@ -347,13 +356,23 @@ tr_TR: index: title: "Gruplar" empty: "Görünen hiç bir grup bulunmamaktadır." + title: + other: "Gruplar" activity: "Etkinlik" members: "Üyeler" topics: "Konular" posts: "Gönderiler" mentions: "Bahsetmeler" messages: "İletiler" + notification_level: "Grup mesajları için varsayılan bildirim düzeyi" + visibility_levels: + title: "Bu grubu kim görebilir?" + public: "Herkes" + members: "Grup sahipleri, üyeler ve yöneticiler" + staff: "Grup sahipleri ve yetkili" + owners: "Grup sahipleri ve yöneticileri" alias_levels: + messageable: "Bu gruba kimler ileti gönderebilir?" nobody: " Hiç Kimse" only_admins: "Sadece Yöneticiler" mods_and_admins: "Sadece Moderatörler ve Yöneticiler" @@ -478,6 +497,7 @@ tr_TR: disable_jump_reply: "Cevapladıktan sonra gönderime atlama" dynamic_favicon: "Tarayıcı simgesinde yeni / güncellenen konu sayısını göster" theme_default_on_all_devices: "Seçilen temayı bütün cihazlarımda varsayılan yap" + allow_private_messages: "Diğer kullanıcıların bana özel mesaj göndermelerine izin ver" external_links_in_new_tab: "Tüm dış bağlantıları yeni sekmede aç" enable_quoting: "Vurgulanan yazıyı alıntılayarak cevaplama özelliğini etkinleştir" change: "değiştir" @@ -485,8 +505,8 @@ tr_TR: admin: "{{user}} bir yöneticidir" moderator_tooltip: "Bu kullanıcı bir moderatör" admin_tooltip: "Bu kullanıcı bir yönetici." - blocked_tooltip: "Bu kullanıcı engellendi" suspended_notice: "Bu kullanıcı {{tarih}} tarihine kadar uzaklaştırıldı." + suspended_permanently: "Bu kullanıcı askıya alındı." suspended_reason: "Neden:" github_profile: "Github" email_activity_summary: "Etkinlik özeti" @@ -517,6 +537,7 @@ tr_TR: watched_first_post_tags_instructions: "Bu etiketlerdeki her yeni konudaki ilk gönderi için bildirim alacaksınız." muted_categories: "Susturuldu" muted_categories_instructions: "Bu kategorilerdeki yeni konular hakkında herhangi bir bildiri almayacaksınız ve en son gönderilerde gözükmeyecekler. " + no_category_access: "Bir moderatör olarak kategori erişiminiz sınırlıdır, kaydetme devre dışıdır." delete_account: "Hesabımı Sil" delete_account_confirm: "Hesabınızı kalıcı olarak silmek istediğinize emin misiniz? Bu eylemi geri alamazsınız!" deleted_yourself: "Hesabınız başarıyla silindi." @@ -528,11 +549,13 @@ tr_TR: muted_users_instructions: "Bu kullanıcılardan gelen tüm bildirileri kapa." muted_topics_link: "Susturulmuş konuları göster" watched_topics_link: "Gözlenen konuları göster" + tracked_topics_link: "İzlenen konuları göster" automatically_unpin_topics: "En alta ulaşınca otomatik olarak başlıkların tutturulmasını kaldır." apps: "Uygulamalar" revoke_access: "Erişimi İptal Et" undo_revoke_access: "Erişim İptalini Geri Al" api_approved: "Onaylanmış:" + theme: "Tema" staff_counters: flags_given: "yardımcı bildirimler" flagged_posts: "bildirilen gönderiler" @@ -552,9 +575,13 @@ tr_TR: select_all: "Tümünü seç" preferences_nav: account: "Hesap" + profile: "Profil" + emails: "E-postalar" notifications: "Bildirimler" categories: "Kategoriler" + tags: "Etiketler" interface: "Arayüz" + apps: "Uygulamalar" change_password: success: "(e-posta gönderildi)" in_progress: "(e-posta yollanıyor)" @@ -703,8 +730,12 @@ tr_TR: expired: "Bu davetin süresi doldu." rescind: "Kaldır" rescinded: "Davet kaldırıldı" + rescind_all: "Tüm davetiyeleri kaldır" + rescinded_all: "Tüm davetiyeler kaldırıldı!" + rescind_all_confirm: "Tüm davetiyeleri kaldırmak istediğinizden emin misiniz?" reinvite: "Davetiyeyi Tekrar Yolla" reinvite_all: "Tüm davetleri tekrar gönder" + reinvite_all_confirm: "Tüm davetiyeleri tekrar göndermek istediğinizden emin misiniz?" reinvited: "Davetiye tekrar yollandı" reinvited_all: "Tüm davetler tekrar gönderildi!" time_read: "Okunma Zamanı" @@ -736,9 +767,9 @@ tr_TR: post_count: other: "oluşturulan gönderiler" likes_given: - other: " verilen" + other: "verilen" likes_received: - other: " alınan " + other: "alınan" days_visited: other: "ziyaret edilen gün" posts_read: @@ -851,6 +882,7 @@ tr_TR: private_message_info: title: "İleti" invite: "Diğerlerini Davet Et..." + leave_message: "Bu mesajı gerçekten bırakmak mı istiyorsun?" remove_allowed_user: "Bu iletiden {{name}} isimli kullanıcıyı çıkarmak istediğinize emin misiniz?" remove_allowed_group: "{{name}} bunu gerçekten iletiden kaldırmak istiyor musunuz?" email: 'E-posta' @@ -875,6 +907,8 @@ tr_TR: complete_email_found: "%{email} adresi ile eşleşen bir hesap bulduk, kısa bir süre içerisinde parolanızı nasıl sıfırlayacağınızı açıklayan bir e-posta alacaksınız." complete_username_not_found: "Hiçbir hesap kullanıcı adı %{username} ile eşleşmiyor" complete_email_not_found: "Hiçbir hesap %{email} adresi ile eşleşmiyor" + button_ok: "Tamam" + button_help: "Yardım" login: title: "Giriş Yap" username: "Kullanıcı" @@ -888,6 +922,7 @@ tr_TR: logging_in: "Oturum açılıyor..." or: "ya da" authenticating: "Kimliğiniz doğrulanıyor..." + awaiting_activation: "Hesabınız etkinleştirme işlemini bekliyor, başka bir etkinleştirme e-postası göndermek için şifremi unuttum bağlantısını kullanın." awaiting_approval: "Hesabınız henüz bir görevli tarafından onaylanmadı. Onaylandığında e-posta ile haberdar edileceksiniz." requires_invite: "Üzgünüz, bu foruma sadece davetliler erişebilir." not_activated: "Henüz oturum açamazsınız. Hesabınızı etkinleştirmek için lütfen daha önceden {{sentTo}} adresine yollanan etkinleştirme e-postasındaki açıklamaları okuyun." @@ -896,6 +931,8 @@ tr_TR: resend_activation_email: "Etkinleştirme e-postasını tekrar yollamak için buraya tıklayın. " resend_title: "Etkinleştirme epostasını tekrar gönder" change_email: "Eposta adresini değiştir" + provide_new_email: "Yeni bir e posta adresi girerek aktivasyon maili gönderin." + submit_new_email: "Eposta adresi güncelle" sent_activation_email_again: "{{currentEmail}} adresine yeni bir etkinleştirme e-postası yolladık. Bu e-postanın size ulaşması bir kaç dakika sürebilir; istenmeyen klasörüzü kontrol etmeyi unutmayın." to_continue: "Lütfen Giriş Yap" preferences: "Tercihlerinizi değiştirebilmek için giriş yapmanız gerekiyor." @@ -926,8 +963,12 @@ tr_TR: accept_title: "Davet" welcome_to: "%{site_name} topluluğuna hoş geldiniz!" invited_by: "Davet gönderen:" + your_email: "Hesap e-posta adresiniz %{email}." accept_invite: "Daveti kabul et" + success: "Hesabınız oluşturuldu ve şimdi giriş yaptınız." name_label: "İsim" + password_label: "Parola Belirle" + optional_description: "(isteğe bağlı)" password_reset: continue: "%{site_name} devam edin" emoji_set: @@ -936,6 +977,8 @@ tr_TR: twitter: "Twitter" emoji_one: "Emoji One" win10: "Win10" + google_classic: "Google Classic" + facebook_messenger: "Facebook Messenger" category_page_style: categories_only: "Yalnızca Kategoriler" categories_with_featured_topics: "Kategoriler ve Öne Çıkarılan Konular" @@ -944,12 +987,26 @@ tr_TR: shift: 'Shift' ctrl: 'Ctrl' alt: 'Alt' + select_box: + default_header_text: Seç... + no_content: Hiçbir sonuç bulunamadı + filter_placeholder: Ara... + emoji_picker: + filter_placeholder: Emoji ara + people: İnsanlar + nature: Doğa + food: Gıda + activity: Etkinlik + travel: Seyahat + celebration: Kutlama + recent: Son zamanlarda kullanılmış composer: emoji: "Emoji :)" more_emoji: "dahası..." options: "Seçenekler" whisper: "fısıltı" unlist: "listelenmedi" + blockquote_text: "Blok-alıntı" add_warning: "Bu resmi bir uyarıdır." toggle_whisper: "Fısıldamayı Aç/Kapa" toggle_unlisted: "Listelenmeme aç/kapa" @@ -1072,6 +1129,7 @@ tr_TR: sort_by: "Sırala" relevance: "Alaka" latest_post: "Son Gönderi" + latest_topic: "Güncel konu" most_viewed: "En Çok Görüntülenen" most_liked: "En Çok Beğenilen" select_all: "Tümünü Seç" @@ -1082,6 +1140,9 @@ tr_TR: no_more_results: "Başka sonuç yok." searching: "Aranıyor..." post_format: "{{username}} tarafından #{{post_number}}" + more_results: "Daha fazla sonuç var. Lütfen arama kriterlerini daraltın." + search_google_button: "Google" + search_google_title: "Bu sitede ara" context: user: "@{{username}} kullancısına ait gönderilerde ara" category: "#{{category}} kategorisini ara" @@ -1110,7 +1171,11 @@ tr_TR: first: en ilk gönderidir. pinned: tutturulmuş unpinned: tutturulmamış + seen: Okudum + unseen: Okumadım wiki: wiki olan + images: resim(ler) aktar + all_tags: Tüm etiketleri içerir statuses: label: Şöyle olan konular open: açık @@ -1136,6 +1201,7 @@ tr_TR: select_all: "Tümünü Seç" clear_all: "Tümünü Temizle" unlist_topics: "Konuları Listeleme" + relist_topics: "Konuları sırala" reset_read: "Okunmuşları Sıfırla" delete: "Konuları Sil" dismiss: "Yoksay" @@ -1146,12 +1212,15 @@ tr_TR: dismiss_new: "Yenileri Yoksay" toggle: "konuların toplu seçimini aç/kapa" actions: "Toplu Eylemler" + change_category: "Kategori düzenle" close_topics: "Konuları Kapat" archive_topics: "Konuları Arşivle" notification_level: "Bildirimler" choose_new_category: "Konular için yeni bir kategori seçin:" selected: other: "{{count}} konu seçtiniz." + change_tags: "Etiketleri Değiştir" + append_tags: "Etiketleri Ekle" choose_new_tags: "Konular için yeni etiketler seçin:" changed_tags: "Bu konuların etiketleri değiştirildi." none: @@ -1233,6 +1302,18 @@ tr_TR: jump_reply_up: Daha önceki cevaba geç jump_reply_down: Daha sonraki cevaba geç deleted: "Konu silindi " + topic_status_update: + when: "Ne zaman:" + public_timer_types: Konu Zamanlayıcıları + private_timer_types: Kullanıcı Konusu Zamanlayıcılar + auto_update_input: + none: "Bir zaman çerçevesi seçin" + one_year: "Bir Yıl" + forever: "Sonsuza dek" + auto_close: + error: "Lütfen geçerli bir değer giriniz." + reminder: + title: "Bana hatırlat" auto_close_title: 'Otomatik Kapatma Ayarları' auto_close_immediate: other: "Konudaki son gönderi zaten %{count} saat eski, bu yüzden konu hemen kapatılacak." @@ -1484,6 +1565,7 @@ tr_TR: has_liked: "bu gönderiyi beğendiniz" undo_like: "beğenmekten vazgeç" edit: "bu gönderiyi düzenle" + edit_action: "Düzenle" edit_anonymous: "Üzgünüz, ama bu gönderiyi düzenleyebilmek için oturum açmalısınız." flag: "bu gönderiyi kontrol edilmesi için özel olarak bildirin ya da bununla ilgili özel bir bildirim yollayın" delete: "bu gönderiyi sil" @@ -2034,7 +2116,6 @@ tr_TR: no_problems: "Herhangi bir sorun bulunamadı." moderators: 'Moderatörler:' admins: 'Yöneticiler:' - blocked: 'Engellenmiş:' suspended: 'Uzaklaştırılmışlar:' private_messages_short: "İletiler" private_messages_title: "İletiler" @@ -2099,29 +2180,26 @@ tr_TR: dispositions: agreed: "onaylandı" disagreed: "onaylanmadı" - deferred: "ertelendi" flagged_by: "Bildiren" resolved_by: "Çözen" took_action: "İşlem uygulandı" system: "Sistem" error: "Bir şeyler ters gitti" reply_message: "Cevapla" - no_results: "Bildirilen içerik yok." topic_flagged: "Bu konu bildirildi." visit_topic: "Harekete geçmek için konuyu ziyaret edin" was_edited: "İlk bildirimden sonra gönderi düzenlendi" previous_flags_count: "Bu gönderi daha önce {{count}} defa bildirilmiş." - summary: - action_type_3: - other: "konu dışı x{{count}}" - action_type_4: - other: "uygunsuz x{{count}}" - action_type_6: - other: "özel x{{count}}" - action_type_7: - other: "özel x{{count}}" - action_type_8: - other: "istenmeyen x{{count}}" + flagged_topics: + topic: "Konu" + type: "Tür" + users: "Kullanıcılar" + short_names: + off_topic: "konu-dışı" + inappropriate: "uygunsuz" + spam: "istenmeyen" + notify_user: "özel" + notify_moderators: "özel" groups: primary: "Ana Grup" no_primary: "(ana grup yok)" @@ -2320,6 +2398,7 @@ tr_TR: new_style: "Yeni Biçim" import: "İçeri Aktar" delete: "Sil" + delete_confirm: "Bu tema silinsin mi?" about: "Sitedeki CSS biçim sayfalarını ve HTML başlıklarını değiştir. Özelleştirme ekleyerek başla." color: "Renk" opacity: "Opaklık" @@ -2332,6 +2411,29 @@ tr_TR: none_selected: "Düzenlemeye başlamak için bir e-posta şablonu seçin. " revert: "Değişiklikleri Geri Al" revert_confirm: "Değişikliklerinizi geri almak istediğinize emin misiniz?" + theme: + customize_desc: "Özelleştir:" + title: "Temalar" + edit: "Düzenle" + common: "Ortak" + desktop: "Masaüstü" + mobile: "Mobil" + preview: "Önizleme" + is_default: "Tema varsayılan olarak etkinleştirildi" + uploads: "Yüklemeler" + upload: "Yükle" + about_theme: "Tema Hakkında" + check_for_updates: "Güncellemeleri kontrol et" + updating: "Güncelleniyor..." + add: "Ekle" + scss: + text: "CSS" + header: + text: "Başlık" + after_header: + text: "Başlıktan Sonra" + footer: + text: "Alt Kısım" colors: title: "Renkler" long_title: "Renk Düzenleri" @@ -2452,6 +2554,7 @@ tr_TR: block: "engelle" do_nothing: "hiçbir şey yapma" staff_actions: + all: "hepsi" title: "Görevli Eylemleri" clear_filters: "Hepsini Göster" staff_user: "Görevli Kullanıcı" @@ -2486,8 +2589,6 @@ tr_TR: change_category_settings: "kategori ayarlarını değiştir" delete_category: "kategoriyi sil" create_category: "kategori oluştur" - block_user: "kullanıcıyı engelle" - unblock_user: "kullanıcı engelini kaldır" grant_admin: "yönetici yetkisi ver" revoke_admin: "yönetici yetkisini kaldır" grant_moderation: "moderasyon yetkisi ver" @@ -2536,10 +2637,15 @@ tr_TR: logster: title: "Hata Kayıtları" watched_words: + search: "ara" + clear_filter: "Temizle" + actions: + flag: 'Bildir' form: label: 'Yeni Kelime: ' add: 'Ekle' upload: "Yükle" + upload_successful: "Yükleme başarılı oldu. Kelimeler eklendi." impersonate: title: "Rolüne gir" help: "Hata bulma ve giderme amaçları için, bu aracı kullanarak kullanıcının rolüne girin. İşiniz bitince sistemdne çıkış yapmanız gerekecek." @@ -2559,7 +2665,6 @@ tr_TR: pending: "Bekleyen" staff: 'Görevli' suspended: 'Uzaklaştırılmış' - blocked: 'Engellenmiş' suspect: 'Kuşkulanılan' approved: "Onaylanmış mı?" approved_selected: @@ -2578,7 +2683,6 @@ tr_TR: staff: "Görevli" admins: 'Yöneticiler' moderators: 'Moderatörler' - blocked: 'Engellenen Kullanıcılar' suspended: 'Uzaklaştırılmış Kullanıcılar' suspect: 'Kuşkulanılan Kullanıcılar' reject_successful: @@ -2595,22 +2699,22 @@ tr_TR: suspend_duration: "Kullanıcı ne kadar uzun bir süre için uzaklaştırılacak?" suspend_reason_label: "Neden uzaklaştırıyorsunuz? Buraya yazdıklarınız bu kullanıcının profil sayfasında herkese gözükecek ve sistemde oturum açtığı anda kullanıcıya gösterilecek. Lütfen yazıyı kısa tutun." suspend_reason: "Neden" + suspend_reason_placeholder: "Askıya alma sebebi" suspend_message: "E Posta Mesajı" suspended_by: "Uzaklaştıran" + cant_suspend: "Bu kullanıcı askıya alınamaz." delete_all_posts: "Tüm gönderileri sil" delete_all_posts_confirm_MF: "{POSTS, plural, one {1 gönderi} other {# gönderi}} ve {TOPICS, plural, one {1 konu} other {# konu}} silmek üzeresiniz. Emin misiniz?" - suspend: "Uzaklaştır" - unsuspend: "Uzaklaştırmayı geri al" - suspended: "Uzaklaştırıldı mı?" moderator: "Moderatör mü?" admin: "Yönetici mi?" - blocked: "Engellendi mi?" + suspended: "Uzaklaştırıldı mı?" staged: "Aşamalı?" show_admin_profile: "Yönetici" refresh_browsers: "Tarayıcıyı sayfa yenilemesine zorla" refresh_browsers_message: "İleti tüm kullanıcılara gönderildi!" show_public_profile: "Herkese Açık Profili Görüntüle" impersonate: 'Rolüne gir' + action_logs: "İşlem Kayıtları" ip_lookup: "IP Arama" log_out: "Çıkış Yap" logged_out: "Kullanıcının tüm cihazlarda oturumu kapatılmış" @@ -2618,8 +2722,8 @@ tr_TR: grant_admin: 'Yönetici Yetkisi Ver' revoke_moderation: 'Moderasyonu İptal Et' grant_moderation: 'Moderasyon Yetkisi Ver' - unblock: 'Engeli Kaldır' - block: 'Engelle' + unsuspend: 'Uzaklaştırmayı geri al' + suspend: 'Uzaklaştır' reputation: İtibar permissions: İzinler activity: Etkinlik @@ -2663,17 +2767,13 @@ tr_TR: activate_failed: "Kullanıcı etkinleştirilirken bir sorun yaşandı." deactivate_account: "Hesabı Pasifleştir" deactivate_failed: "Kullanıcı deaktive edilirken bir sorun yaşandı." - unblock_failed: 'Kullanıcının engeli kaldırılırken bir sorun yaşandı.' - block_failed: 'Kullanıcı engellenirken bir sorun yaşandı.' - block_confirm: 'Bu kullanıcıyı engellemek istediğinize emin misiniz? Bunu yaparsanız yeni başlık ya da gönderi oluşturamayacak.' - block_accept: 'Evet, bu kullanıcıyı engelle' bounce_score: "Geri Sekme Skoru" reset_bounce_score: label: "Sıfırla" title: "Geri sekme skorunu 0'a çek" + visit_profile: "Profilini güncellemek için kullanıcı tercihleri sayfası 'nı ziyaret edebilirsin" deactivate_explanation: "Deaktive edilmiş bir kullanıcı e-postasını tekrar doğrulamalı." suspended_explanation: "Uzaklaştırılmış kullanıcılar sistemde oturum açamaz." - block_explanation: "Engellenmiş bir kullanıcı gönderi oluşturamaz veya konu başlatamaz." staged_explanation: "Aşamalı bir kullanıcı sadece belirli konularda e-posta ile gönderide bulunabilir." bounce_score_explanation: none: "Bu e-posta adresinden yakın zamanda hiç geri sekme alınmadı." @@ -2879,6 +2979,7 @@ tr_TR: sample: "Discourse konuları oluşturmak ve gömmek için aşağıdaki HTML kodunu sitenizde kullanın. REPLACE_ME'yi Discourse'u gömdüğünüz sayfanın tam URL'i ile değiştirin." title: "Gömme" host: "İzin Verilen Sunucular" + class_name: "Sınıf adı" path_whitelist: "Kabul edilen yol listesi" edit: "düzenle" category: "Kategoriye Gönder" diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 5a164f2499..343b67fd18 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -339,7 +339,6 @@ uk: admin: "{{user}} є адміном" moderator_tooltip: "Цей користувач є модератором" admin_tooltip: "Цей користувач є адміністратором" - blocked_tooltip: "Цього користувача заблоковано" suspended_notice: "Цього користувача призупинено до {{date}}." suspended_reason: "Причина: " github_profile: "Github" @@ -1137,7 +1136,6 @@ uk: no_problems: "Не виявлено жодних проблем." moderators: 'Модератори:' admins: 'Адміни:' - blocked: 'Заблоковані:' suspended: 'Призупинені:' private_messages_short: "Пвдмл" private_messages_title: "Повідомлення" @@ -1457,8 +1455,6 @@ uk: change_category_settings: "змінити налаштування категорії" delete_category: "видалити категорію" create_category: "створити категорію" - block_user: "заблокувати користувача" - unblock_user: "розблокувати користувача" grant_admin: "зробити адміністратором" revoke_admin: "відкликати адміністраторство" grant_moderation: "зробити модератором" @@ -1513,7 +1509,6 @@ uk: pending: "Очікують" staff: 'Персонал' suspended: 'Призупинені' - blocked: 'Заблоковані' approved: "Схвалено?" titles: active: 'Активні користувачі' @@ -1524,7 +1519,6 @@ uk: staff: "Персонал" admins: 'Адміністратори' moderators: 'Модератори' - blocked: 'Заблоковані користувачі' suspended: 'Призупинені користувачі' not_verified: "Не підтверджений" check_email: @@ -1537,12 +1531,9 @@ uk: suspend_reason: "Причина" suspended_by: "Призупинено" delete_all_posts: "Видалити всі дописи" - suspend: "Призупинити" - unsuspend: "Скасувати призупинення" - suspended: "Призупинено?" moderator: "Модератор?" admin: "Адмін?" - blocked: "Заблоковано?" + suspended: "Призупинено?" show_admin_profile: "Адмін" refresh_browsers: "Примусово оновити оглядач" refresh_browsers_message: "Повідомлення надіслано всім клієнтам!" @@ -1555,8 +1546,8 @@ uk: grant_admin: 'Зробити адміністратором' revoke_moderation: 'Відкликати модераторство' grant_moderation: 'Зробити модератором' - unblock: 'Розблокувати' - block: 'Заблокувати' + unsuspend: 'Скасувати призупинення' + suspend: 'Призупинити' reputation: Репутація permissions: Дозволи activity: Активність @@ -1587,15 +1578,10 @@ uk: activate_failed: "Під час активації користувача сталася помилка." deactivate_account: "Деактивувати обліковий запис" deactivate_failed: "Під час деактивації користувача сталася помилка." - unblock_failed: 'Під час розблокування користувача сталася помилка.' - block_failed: 'Під час блокування користувача сталася помилка.' - block_confirm: 'Ви впевнені, що хочете заблокувати цього користувача? Вони не зможуть створювати будь-які нові теми або дописи.' - block_accept: 'Так, заблокувати цього користувача' reset_bounce_score: label: "Скинути" deactivate_explanation: "Деактивований користувач має повторно підтвердити свою електронну скриньку." suspended_explanation: "Призупинений користувач не може увійти." - block_explanation: "Заблокований користувач не може дописувати і розпочинати теми." trust_level_change_failed: "Під час зміни Рівня довіри користувача трапилася помилка." suspend_modal_title: "Призупинити користувача" trust_level_2_users: "Користувачі з Рівнем довіри 2" diff --git a/config/locales/client.ur.yml b/config/locales/client.ur.yml index 76610872fd..091c7019c4 100644 --- a/config/locales/client.ur.yml +++ b/config/locales/client.ur.yml @@ -496,7 +496,6 @@ ur: admin: "{{user}} ایک ایڈمِن ہے" moderator_tooltip: "یہ صارف ایک ماڈریٹر ہے" admin_tooltip: "یہ صارف ایک ایڈمِن ہے" - blocked_tooltip: "یہ صارف بلاک کیا ہوا ہے" suspended_notice: "ہ صارف {{date}} تک معطل ہے۔" suspended_reason: "وجہ:" github_profile: "گِٹ حَب" @@ -2136,7 +2135,6 @@ ur: no_problems: "کوئی مسائل نہیں پائے گئے۔" moderators: 'ماڈریٹرز:' admins: 'ایڈمن:' - blocked: 'بلاک کردہ:' suspended: 'معطل کردہ:' private_messages_short: "پغم" private_messages_title: "پیغامات" @@ -2599,8 +2597,6 @@ ur: change_category_settings: "زمرہ جات کی سیٹِنگ تبدیل کریں" delete_category: "زمرہ حذف کریں" create_category: "زمرہ بنائیں" - block_user: "صارف بلاک کریں" - unblock_user: "صارف پر بلاک ختم کریں" grant_admin: "اَیڈمِن عطا کریں" revoke_admin: "اَیڈمِن منسوخ کریں" grant_moderation: "ماڈرَیشَن عطا کریں" @@ -2667,7 +2663,6 @@ ur: pending: "زیرِاِلتوَاء" staff: 'سٹاف' suspended: 'معطل کردہ' - blocked: 'بلاک کردہ' suspect: 'مشتبہ' approved: "منظورشدہ؟" approved_selected: @@ -2688,7 +2683,6 @@ ur: staff: "سٹاف" admins: 'ایڈمِن صارفین' moderators: 'ماڈریٹرز' - blocked: 'بلاک کردہ صارفین' suspended: 'معطل کردہ صارفین' suspect: 'مشتبہ صارفین' reject_successful: @@ -2705,18 +2699,14 @@ ur: suspend_failed: "اِس صارف کو معطل کرتے ہوے کچھ غلط ہو گیا {{error}}" unsuspend_failed: "اِس صارف کی معطلی ختم کرتے ہوے کچھ غلط ہو گیا {{error}}" suspend_duration: "صارف کب تک معطل رہے گا؟" - suspend_duration_units: "(دن)" suspend_reason_label: "آپ معطل کیوں کر رہے ہیں؟ یہ ٹَیکسٹ اِس صارف کے پروفائل پیج پر ہر کسی کے لیے ظاہر ہو گا، اور جب صارف لاگ اِن کرنے کی کوشش کریں گے تو اُنہیں دکھایا جائے گا۔ اِسے مختصر رکھیں۔" suspend_reason: "وجہ" suspended_by: "کی طرف سے معطل کیا گیا" delete_all_posts: "تمام پوسٹس حذف کریں" delete_all_posts_confirm_MF: "آپ {POSTS، جمع، ایک {1 پوسٹ} دیگر {# posts}} اور {TOPICS، جمع، ایک {1 ٹاپک} دیگر {# topics}} حذف کرنے والے ہیں۔ کیا آپ کو یقین ہے؟" - suspend: "معطل کریں" - unsuspend: "معطلی ختم کریں" - suspended: "معطل کردہ؟" moderator: "ماڈریٹر؟" admin: "ایڈمن؟" - blocked: "بلاک کردہ؟" + suspended: "معطل کردہ؟" staged: "سٹَیجڈ؟" show_admin_profile: "ایڈمن" refresh_browsers: "براؤزر رِیفریش فورس کریں" @@ -2731,8 +2721,8 @@ ur: grant_admin: 'اَیڈمِن عطا کریں' revoke_moderation: 'ماڈرَیشَن منسوخ کریں' grant_moderation: 'ماڈرَیشَن عطا کریں' - unblock: 'بلاک ختم کریں' - block: 'بلاک کریں' + unsuspend: 'معطلی ختم کریں' + suspend: 'معطل کریں' reputation: ساکھ permissions: اجازتیں activity: سرگرمی @@ -2779,17 +2769,12 @@ ur: activate_failed: "صارف فعال کرنے میں ایک مسئلہ پیش آیا۔" deactivate_account: "اکاؤنٹ غیر فعال کریں" deactivate_failed: "صارف کو غیر فعال کرنے میں ایک مسئلہ پیش آیا۔" - unblock_failed: 'صارف کو بلاک سے ہٹانے میں ایک مسئلہ پیش آیا۔' - block_failed: 'صارف کو بلاک کرنے میں ایک مسئلہ پیش آیا۔' - block_confirm: 'کیا آپ واقعی اِس صارف کو بلاک کرنا چاہتے ہیں؟ وہ کوئی بھی نئے ٹاپک یا پوسٹ نہیں بنا سکیں گے۔' - block_accept: 'جی ہاں، اِس صارف کو بلاک کریں' bounce_score: "بائونس سکور" reset_bounce_score: label: "رِی سَیٹ" title: "بائونس سکور واپس 0 پر رِی سَیٹ کریں" deactivate_explanation: "ایک غیر فعال صارف کو اپنا اِیمیل دوبارہ تصدیق کرنا ضروری ہے۔" suspended_explanation: "کوئی معطل صارف لاگ اِن نہیں کر سکتا۔" - block_explanation: "ایک بلاک ہوا صارف پوسٹ شائع یا ٹاپک شروع نہیں کر سکتا۔" staged_explanation: "ایک سٹَیجڈ صارف صرف مخصوص ٹاپکس پر اِیمیل کے ذریعے پوسٹ کر سکتا ہے۔" bounce_score_explanation: none: "اس اِیمیل سے حال میں کوئی باؤنسِز موصول نہیں ہوئے۔" diff --git a/config/locales/client.vi.yml b/config/locales/client.vi.yml index b9f13c557b..cca5bc36ea 100644 --- a/config/locales/client.vi.yml +++ b/config/locales/client.vi.yml @@ -497,7 +497,6 @@ vi: admin: "{{user}} là người điều hành" moderator_tooltip: "Thành viên này là MOD" admin_tooltip: "Thành viên này là admin" - blocked_tooltip: "Tài khoản này bị khóa" suspended_notice: "Thành viên này bị đình chỉ cho đến ngày {{date}}. " suspended_reason: "Lý do: " github_profile: "Github" @@ -1931,7 +1930,6 @@ vi: no_problems: "Không phát hiện vấn đề" moderators: 'Điều hành:' admins: 'Quản trị:' - blocked: 'Đã khóa:' suspended: 'Đã tạm khóa:' private_messages_short: "Tin nhắn" private_messages_title: "Tin nhắn" @@ -2308,8 +2306,6 @@ vi: change_category_settings: "thay đổi cấu hình danh mục" delete_category: "xóa danh mục" create_category: "tạo danh mục" - block_user: "khóa tài khoản" - unblock_user: "mở khóa tài khoản" grant_admin: "cấp quản trị" revoke_admin: "hủy bỏ quản trị" grant_moderation: "cấp điều hành" @@ -2365,7 +2361,6 @@ vi: pending: "Đang chờ xử lý" staff: 'Nhân viên' suspended: 'Đã tạm khóa' - blocked: 'Đã khóa' suspect: 'Nghi ngờ' approved: "Đã duyệt?" approved_selected: @@ -2384,7 +2379,6 @@ vi: staff: "Nhân viên" admins: 'Tài khoản Quản trị' moderators: 'Điều hành viên' - blocked: 'Tài khoản Khóa' suspended: 'Tài khoản Tạm khóa' suspect: 'Tài khoản đáng ngờ' reject_successful: @@ -2399,17 +2393,13 @@ vi: suspend_failed: "Có gì đó đã sai khi đình chỉ tài khoản này {{error}}" unsuspend_failed: "Có gì đó sai khi gỡ bỏ đình chỉ tài khoản này {{error}}" suspend_duration: "Tài khoản này sẽ bị đình chỉ bao lâu?" - suspend_duration_units: "(ngày)" suspend_reason_label: "Tại sao bạn bị đình chỉ? Dòng chữ hiển thị cho tất cả mọi người sẽ hiển thị trên trang hồ sơ tài khoản của người dùng này, và sẽ hiển thị cho thành viên khi họ đăng nhập, hãy viết ngắn." suspend_reason: "Lý do" suspended_by: "Tạm khóa bởi" delete_all_posts: "Xóa tất cả bài viết" - suspend: "Tạm khóa" - unsuspend: "Đã mở khóa" - suspended: "Đã tạm khóa?" moderator: "Mod?" admin: "Quản trị?" - blocked: "Đã khóa?" + suspended: "Đã tạm khóa?" staged: "Cấp bậc?" show_admin_profile: "Quản trị" refresh_browsers: "Bắt buộc làm mới trình duyệt" @@ -2423,8 +2413,8 @@ vi: grant_admin: 'Cấp quản trị' revoke_moderation: 'Thu hồi điều hành' grant_moderation: 'Cấp điều hành' - unblock: 'Mở khóa' - block: 'Khóa' + unsuspend: 'Đã mở khóa' + suspend: 'Tạm khóa' reputation: Danh tiếng permissions: Quyền activity: Hoạt động @@ -2468,13 +2458,8 @@ vi: activate_failed: "Có vấn đề khi kích hoạt thành viên này." deactivate_account: "Vô hiệu hóa Tài khoản" deactivate_failed: "Có vấn đề khi bỏ kích hoạt thành viên này." - unblock_failed: 'Có vẫn đề khi gỡ khóa thành viên này.' - block_failed: 'Có vấn đề khi khóa thành viên này.' - block_confirm: 'Bạn có chắc chắn muốn chặn người dùng này? Họ sẽ không thể tạo bất kỳ chủ đề hoặc bài viết mới nào.' - block_accept: 'Có, chặn người dùng này' deactivate_explanation: "Tài khoản chờ kích hoạt phải xác thực email của họ." suspended_explanation: "Tài khoản tạm khóa không thể đăng nhập." - block_explanation: "Tài khoản bị khóa không thể đăng bài hoặc tạo chủ đề." trust_level_change_failed: "Có lỗi xảy ra khi thay đổi mức độ tin tưởng của tài khoản." suspend_modal_title: "Tạm khóa Thành viên" trust_level_2_users: "Độ tin cậy tài khoản mức 2" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 2e913e6389..13dba3dfcc 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -98,6 +98,7 @@ zh_CN: split_topic: "于%{when}分割了该主题" invited_user: "于%{when}邀请了%{who}" invited_group: "于%{when}邀请了%{who}" + user_left: "%{who}于%{when}写了该消息" removed_user: "于%{when}移除了%{who}" removed_group: "于%{when}移除了%{who}" autoclosed: @@ -502,6 +503,7 @@ zh_CN: disable_jump_reply: "回复后不跳转至新帖子" dynamic_favicon: "在浏览器图标中显示主题更新数量" theme_default_on_all_devices: "将其设为我所有设备上的默认主题" + allow_private_messages: "允许其他用户发送私信给我" external_links_in_new_tab: "在新标签页打开外部链接" enable_quoting: "在选择文字时显示引用回复按钮" change: "修改" @@ -509,8 +511,8 @@ zh_CN: admin: "{{user}}是管理员" moderator_tooltip: "用户是版主" admin_tooltip: "用户是管理员" - blocked_tooltip: "这个用户已被封禁" suspended_notice: "该用户将被禁止登录,直至 {{date}}。" + suspended_permanently: "该用户被封禁了。" suspended_reason: "原因:" github_profile: "Github" email_activity_summary: "活动摘要" @@ -751,7 +753,7 @@ zh_CN: link_generated: "邀请链接生成成功!" valid_for: "邀请链接只对这个邮件地址有效:%{email}" bulk_invite: - none: "你还没有邀请任何人。你可以单独邀请,也可以通过 上传CSV文件批量邀请。" + none: "你还没有邀请任何人。你可以单独邀请用户,也可以通过上传CSV文件批量邀请。" text: "通过文件批量邀请" success: "文件上传成功,当操作完成时将通过私信通知你。" error: "抱歉,文件必须是CSV格式。" @@ -772,9 +774,9 @@ zh_CN: post_count: other: "发表帖子" likes_given: - other: " 送出赞" + other: "送出" likes_received: - other: " 获得赞" + other: "收到" days_visited: other: "访问天数" posts_read: @@ -887,6 +889,7 @@ zh_CN: private_message_info: title: "私信" invite: "邀请其他人..." + leave_message: "你真的想要发送消息么?" remove_allowed_user: "确定将 {{name}} 从本条私信中移除?" remove_allowed_group: "确定将 {{name}} 从本条私信中移除?" email: '邮箱' @@ -1178,7 +1181,7 @@ zh_CN: no_more_results: "没有找到更多结果。" searching: "搜索中..." post_format: "#{{post_number}} 来自于 {{username}}" - results_page: "搜索结果" + results_page: "关于“{{term}}”的搜索结果" more_results: "还有更多结果。请增加您的搜索条件。" cant_find: "找不到你要找的内容?" start_new_topic: "不如创建一个新主题?" @@ -1347,19 +1350,27 @@ zh_CN: jump_reply_down: 转到更新的回复 deleted: "此主题已被删除" topic_status_update: - title: "设置主题计时器" + title: "主题计时器" save: "设置计时器" num_of_hours: "小时数:" remove: "撤销计时器" publish_to: "发布至:" when: "时间:" + public_timer_types: 主题计时器 + private_timer_types: 用户主题计时器 auto_update_input: + none: "选择时间范围" later_today: "今天的某个时候" tomorrow: "明天" later_this_week: "这周的某个时候" this_weekend: "周末" next_week: "下个星期" + two_weeks: "两周" next_month: "下个月" + three_months: "三个月" + six_months: "六个月" + one_year: "一年" + forever: "永远" pick_date_and_time: "选择日期和时间" set_based_on_last_post: "按照最新帖子关闭" publish_to_category: @@ -1667,7 +1678,7 @@ zh_CN: actions: flag: '标记' defer_flags: - other: "推迟处理标记" + other: "忽略标记" undo: off_topic: "撤回标记" spam: "撤回标记" @@ -2063,7 +2074,7 @@ zh_CN: hamburger_menu: '= 打开汉堡菜单' user_profile_menu: 'p 打开用户菜单' show_incoming_updated_topics: '. 显示更新主题' - search: '/ctrl+shift+s 搜索' + search: '/ctrl+alt+f 搜索' help: '? 打开键盘帮助' dismiss_new_posts: 'x 然后 r 解除新/帖子提示' dismiss_topics: 'x 然后 t 解除主题提示' @@ -2132,7 +2143,9 @@ zh_CN: tags: "标签" choose_for_topic: "为此主题选择可选标签" delete_tag: "删除标签" - delete_confirm: "确定要删除这个标签吗?" + delete_confirm: + other: "你确定你想要删除这个标签以及撤销在{{count}}个主题中的关联么?" + delete_confirm_no_topics: "你确定你想要删除这个标签吗?" rename_tag: "重命名标签" rename_instructions: "标签的新名称:" sort_by: "排序方式:" @@ -2227,7 +2240,6 @@ zh_CN: no_problems: "找不到问题." moderators: '版主:' admins: '管理员:' - blocked: '禁止参与讨论:' suspended: '禁止登录' private_messages_short: "私信" private_messages_title: "私信" @@ -2260,8 +2272,9 @@ zh_CN: by: "来自" flags: title: "标记" - old: "历史" - active: "待处理" + active_posts: "被标记帖子" + old_posts: "过去被标记帖子" + topics: "被标记主题" agree: "确认标记" agree_title: "确认这个标记有效且正确" agree_flag_modal_title: "确认标记并执行..." @@ -2271,11 +2284,11 @@ zh_CN: agree_flag_restore_post_title: "还原这篇帖子" agree_flag: "确认标记" agree_flag_title: "确认标记,不对帖子进行操作" - defer_flag: "推迟处理" + defer_flag: "忽略" defer_flag_title: "移除标记;这次不处理。" delete: "删除" delete_title: "删除标记指向的帖子。" - delete_post_defer_flag: "删除帖子并推迟处理标记" + delete_post_defer_flag: "删除帖子并忽略标记" delete_post_defer_flag_title: "删除此帖;如果这是这个主题内的第一篇帖子则删除主题" delete_post_agree_flag: "删除帖子并确认标记" delete_post_agree_flag_title: "删除此帖;如果这是这个主题内的第一篇帖子则删除主题" @@ -2289,32 +2302,36 @@ zh_CN: clear_topic_flags: "完成" clear_topic_flags_title: "主题的问题已经已被调查且提案已被解决。单击完成以删除报告。" more: "(更多回复...)" + suspend_user: "禁用用户" + suspend_user_title: "因该贴禁用用户" dispositions: agreed: "已确认" disagreed: "被否决" - deferred: "已推迟" + deferred: "忽略" flagged_by: "标记者" resolved_by: "处理者" took_action: "立即执行" system: "系统" error: "出错了" reply_message: "回复" - no_results: "当前没有标记。" + no_results: "没有被标记的主题。" topic_flagged: "主题已经被标记。" + show_full: "显示全部帖子" visit_topic: "浏览主题才能操作" was_edited: "帖子在第一次标记后被编辑" previous_flags_count: "这篇帖子已经被标记了 {{count}} 次。" - summary: - action_type_3: - other: "偏离主题 x{{count}}" - action_type_4: - other: "不当内容 x{{count}}" - action_type_6: - other: "自定义 x{{count}}" - action_type_7: - other: "自定义 x{{count}}" - action_type_8: - other: "广告 x{{count}}" + show_details: "显示标记细节" + flagged_topics: + topic: "主题" + type: "类型" + users: "用户" + last_flagged: "最后标记" + short_names: + off_topic: "离题" + inappropriate: "不恰当" + spam: "广告" + notify_user: "自定义" + notify_moderators: "自定义" groups: primary: "主要群组" no_primary: "(无主要群组)" @@ -2761,8 +2778,6 @@ zh_CN: change_category_settings: "更改分类设置" delete_category: "删除分类" create_category: "创建分类" - block_user: "封禁用户" - unblock_user: "解封用户" grant_admin: "授予管理员权限" revoke_admin: "撤销管理员权限" grant_moderation: "授予版主权限" @@ -2832,6 +2847,7 @@ zh_CN: form: label: '新敏感词:' placeholder: '完整字词或 * 作为通配符' + placeholder_regexp: "正则表达式" add: '新增' success: '成功' upload: "上传" @@ -2855,7 +2871,6 @@ zh_CN: pending: "未审核" staff: '管理人员' suspended: '禁止登录' - blocked: '禁止参与讨论' suspect: '怀疑' approved: "已批准?" approved_selected: @@ -2874,7 +2889,6 @@ zh_CN: staff: "管理人员" admins: '管理员' moderators: '版主' - blocked: '被封用户' suspended: '被禁用户' suspect: '嫌疑用户' reject_successful: @@ -2889,18 +2903,20 @@ zh_CN: suspend_failed: "禁止此用户时发生了错误 {{error}}" unsuspend_failed: "解禁此用户时发生了错误 {{error}}" suspend_duration: "该用户将被封禁多久?" - suspend_duration_units: "(天)" suspend_reason_label: "为什么封禁该用户?该理由将公开显示在用户个人页面上,当其尝试登录时,也看到这条理由。尽量简洁。" + suspend_reason_hidden_label: "你为什么被封禁了?该理由将在用户尝试登录时显示。尽量简洁。" suspend_reason: "封禁的理由" + suspend_reason_placeholder: "禁用理由" + suspend_message: "邮件消息" + suspend_message_placeholder: "(可选)你可以提供更多封禁的理由并邮件用户该理由。" suspended_by: "封禁操作者:" + suspended_until: "(直到%{until})" + cant_suspend: "该用户不能被封禁。" delete_all_posts: "删除所有帖子" delete_all_posts_confirm_MF: "你将要删除 {POSTS, plural, one {1 个帖子} other {# 个帖子}}和 {TOPICS, plural, one {1 个主题} other {# 个主题}}。确定吗?" - suspend: "禁止" - unsuspend: "解禁" - suspended: "已禁止?" moderator: "版主?" admin: "管理员?" - blocked: "已封?" + suspended: "已禁止?" staged: "暂存?" show_admin_profile: "管理员" refresh_browsers: "强制浏览器刷新" @@ -2916,8 +2932,8 @@ zh_CN: grant_admin_confirm: "我们已经发送了一封邮件以验证新管理员。请打开邮件并按照指示完成确认。" revoke_moderation: '吊销论坛版主资格' grant_moderation: '赋予论坛版主资格' - unblock: '解封' - block: '封号' + unsuspend: '解禁' + suspend: '禁止' reputation: 声誉 permissions: 权限 activity: 活动 @@ -2961,10 +2977,6 @@ zh_CN: activate_failed: "在激活用户帐号时发生了错误。" deactivate_account: "停用帐号" deactivate_failed: "在停用用户帐号时发生了错误。" - unblock_failed: '在解除用户帐号封禁时发生了错误。' - block_failed: '在封禁用户帐号时发生了错误。' - block_confirm: '你确定要封禁用户吗?他们将没有办法创建任何主题或者帖子。' - block_accept: '是的,封禁用户' bounce_score: "累计退信分值" reset_bounce_score: label: "重置" @@ -2972,7 +2984,6 @@ zh_CN: visit_profile: "访问用户设置页面编辑他们的个人页" deactivate_explanation: "已停用的用户必须重新验证他们的电子邮件。" suspended_explanation: "一个被封禁的用户不能登录。" - block_explanation: "被封禁的用户不能发表主题或者评论。" staged_explanation: "暂存用户只能通过邮件回复指定主题。" bounce_score_explanation: none: "近期该邮箱没有退信。" @@ -3089,6 +3100,7 @@ zh_CN: developer: '开发者' embedding: "嵌入" legal: "法律信息" + api: 'API' user_api: '用户 API' uncategorized: '未分类' backups: "备份" diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 6de9387ed2..a486505281 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -478,7 +478,6 @@ zh_TW: admin: "{{user}} 是管理員" moderator_tooltip: "此用戶為板主" admin_tooltip: "此用戶為管理員" - blocked_tooltip: "此用戶被屏蔽" suspended_notice: "此用戶已被停權至 {{date}}。" suspended_reason: "原因: " github_profile: "Github" @@ -2034,7 +2033,6 @@ zh_TW: no_problems: "未發現任何問題。" moderators: '板主:' admins: '管理員:' - blocked: '已封鎖:' suspended: '已停權:' private_messages_short: "訊息" private_messages_title: "訊息" @@ -2486,8 +2484,6 @@ zh_TW: change_category_settings: "變更分類設定" delete_category: "刪除分類" create_category: "建立分類" - block_user: "封鎖用戶" - unblock_user: "解除封鎖" grant_admin: "授予管理員權限" revoke_admin: "撤銷管理員權限" grant_moderation: "授予板主權限" @@ -2554,7 +2550,6 @@ zh_TW: pending: "申請中" staff: '管理員' suspended: '已停權' - blocked: '已封鎖' suspect: '嫌疑' approved: "已批准?" approved_selected: @@ -2573,7 +2568,6 @@ zh_TW: staff: "管理員" admins: '管理員' moderators: '板主' - blocked: '已封鎖的用戶' suspended: '已停權的用戶' suspect: '嫌疑使用者' reject_successful: @@ -2588,18 +2582,14 @@ zh_TW: suspend_failed: "將此用戶停權時發生錯誤 {{error}}" unsuspend_failed: "恢復此用戶的權限時發生錯誤 {{error}}" suspend_duration: "你想將此用戶停權多久?" - suspend_duration_units: "(天)" suspend_reason_label: "你為什麼要將此用戶停權?你輸入的原因將在此用戶登入時顯示,及顯示在此用戶的基本資料頁面,且任何人都可以看見,請簡短說明原因。" suspend_reason: "原因" suspended_by: "將其停權者" delete_all_posts: "刪除所有文章" delete_all_posts_confirm_MF: "你將要刪除 {POSTS, plural, one {1 個帖子} other {# 個帖子}}和 {TOPICS, plural, one {1 個主題} other {# 個主題}}。確定嗎?" - suspend: "停權" - unsuspend: "恢復權限" - suspended: "已停權?" moderator: "板主?" admin: "管理員?" - blocked: "已封鎖?" + suspended: "已停權?" staged: "暫存?" show_admin_profile: "管理員" refresh_browsers: "強制瀏覽器重新整理" @@ -2614,8 +2604,8 @@ zh_TW: grant_admin: '授予管理員權限' revoke_moderation: '撤銷板主權限' grant_moderation: '授予板主權限' - unblock: '解除封鎖' - block: '封鎖' + unsuspend: '恢復權限' + suspend: '停權' reputation: 聲望 permissions: 權限 activity: 活動 @@ -2659,17 +2649,12 @@ zh_TW: activate_failed: "啟用此帳號時發生錯誤。" deactivate_account: "取消帳號的啟用狀態" deactivate_failed: "取消此帳號的啟用狀態時發生錯誤。" - unblock_failed: '解除此用戶的封鎖狀態時發生錯誤。' - block_failed: '封鎖此用戶時發生錯誤。' - block_confirm: '你確定要封禁用戶嗎?他們將沒有辦法創建任何主題或者帖子。' - block_accept: '是的,封禁用戶' bounce_score: "累計退信分值" reset_bounce_score: label: "重置" title: "重置累計退信分值為 0" deactivate_explanation: "帳號的啟用狀態被取消的用戶必需重新啟用帳號。" suspended_explanation: "被停權的用戶無法登入。" - block_explanation: "被封鎖的用戶無法發表文章或建立討論話題。" staged_explanation: "暫存用戶只能通過郵件回覆指定主題。" bounce_score_explanation: none: "近期該郵箱沒有退信。" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 57e78d175e..8d3f152199 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -46,7 +46,6 @@ ar: no_message_id_error: "يحدث عندما لا يحتوي البريد ترويسة ’Message-Id‘." no_body_detected_error: "يحدث عندما يتعذّر استخراج المتن وعدم وجود مرفقات." inactive_user_error: "يحدث عندما يكون المرسل خاملًا." - blocked_user_error: "يحدث عندما يكون المرسل ممنوعًا." bad_destination_address: "يحدث عندما لا تتطابق عناوين البريد في حقول ”إلى/ن‌ك/ن‌ك‌م“ أحد عناوين البريد الوارد المضبوطة." strangers_not_allowed_error: "يحدث عندما يحاول العضو إنشاء موضوع جديد في قسم و هو ليس عضوًا به." insufficient_trust_level_error: "يحدث عندما يحاول العضو إنشاء موضوع جديد في قسم و هو لا يملك مستوى الثّقة المطلوب لذلك." @@ -1554,8 +1553,6 @@ ar: عذرا, لكن رسالتك الإلكترونية إلي %{destination} ( بعنوان %{former_title}) مرفوضة. القسم الذي ارسلت له يسمح فقط لعناوين البريد الإلكتروني المعروفة و الاعضاء الحاصلين علي صلاحية مناسبة بعمل رد. إذا كنت تعتقد ان هذا خطا [تواصل مع طاقم العمل](%{base_url}/about). - spam_post_blocked: - subject_template: "العضو الجديد %{username} تم حجب مشاركاته بسبب تكرار الروابط " pending_users_reminder: subject_template: zero: "لا يوجد أعضاء تنتظر الموافقة." diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index d8671adf0d..cef76d0cc9 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -397,7 +397,6 @@ bs_BA: polling_interval: "How often should logged in user clients poll in milliseconds" anon_polling_interval: "How often should anonymous clients poll in milliseconds" cooldown_minutes_after_hiding_posts: "Number of minutes a user must wait before they can edit a post hidden via community flagging" - notify_mods_when_user_blocked: "If a user is automatically blocked, send a message to all moderators." flag_sockpuppets: "If a new user replies to a topic from the same IP address as the new user who started the topic, flag both of their posts as potential spam." traditional_markdown_linebreaks: "Use traditional linebreaks in Markdown, which require two trailing spaces for a linebreak." post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)." @@ -679,8 +678,6 @@ bs_BA: subject_template: "Data Export completed successfully" csv_export_failed: subject_template: "Export failed" - spam_post_blocked: - subject_template: "New user %{username} posts blocked due to repeated links" pending_users_reminder: text_body_template: | There are new user signups waiting to be approved (or rejected) before they can access this forum. diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml index b029c05677..ea0ddc9d3b 100644 --- a/config/locales/server.cs.yml +++ b/config/locales/server.cs.yml @@ -598,7 +598,6 @@ cs: email_custom_headers: "Seznam vlastních hlaviček emailů, oddělený svislítkem" enable_long_polling: "'Message bus' smí používat dlouhé výzvy" anon_polling_interval: "Jak často se mají zasílat výzvy anonymním uživatelům v milisekundách" - notify_mods_when_user_blocked: "If a user is automatically blocked, send a message to all moderators." ga_universal_tracking_code: "Kód Google Universal Analytics (analytics.js); například UA–12345678-9; více na http://google.com/analytics" ga_universal_domain_name: "Doménové jméno Google Universal Analytics (analytics.js); například mojeforum.cz; více na http://google.com/analytics" allow_moderators_to_create_categories: "Povolit moderátorům vytváření nových kategorií." diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index b59e34d9f8..00b4a61dde 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -47,7 +47,6 @@ da: auto_generated_email_error: "Dette sker når headeren 'precedence' er angivet til: list, junk, bulk eller auto_reply, eller når enhver anden header indeholder: auto-submitted, auto-replied eller auto-generated." no_body_detected_error: "Dette sker når vi ikke kunne trække noget indhold ud og der heller ikke var nogen vedhæftninger." inactive_user_error: "Dette sker når afsenderen ikke er aktiv." - blocked_user_error: "Dette sker når afsenderen er blevet blokeret." bad_destination_address: "Dette sker når ingen af email adresserne i To/Cc/Bcc felterne matcher en opsat indgående email adresse." strangers_not_allowed_error: "Dette sker når en bruger prøver at oprette et nyt emne i en kategori som de ikke er medlem af." insufficient_trust_level_error: "Dette sker når en bruger prøvede at oprette et nyt emne i en kategori som de ikke har den krævne \"trust level\" til." @@ -893,19 +892,12 @@ da: tl2_additional_likes_per_day_multiplier: "Øg antallet af likes pr dag for tl2 (member) ved at gange med dette tal" tl3_additional_likes_per_day_multiplier: "Øg antallet af likes pr. dag for tl3 (regular) ved at gange med dette tal" tl4_additional_likes_per_day_multiplier: "Øg antallet af likes pr. dag for tl4 (leader) ved at gange med dette tal" - num_spam_flags_to_block_new_user: "Hvis indlæg fra en ny bruger får så mange spam flag fra forskellige brugere num_users_to_block_new_user gem da deres indlæg og forhindre at vedkommende kan udgive flere indlæg. 0 for at slå fra." - num_users_to_block_new_user: "Hvis en ny bruger får num_spam_flags_to_block_new_user spam flag fra dette antal forskellige brugere, gem da alle indlæg og forhindr yderligere posteringer 0 for at slå fra." - num_tl3_flags_to_block_new_user: "Hvis en ny brugers indlæg får dette antal flag fra num_tl3_users_to_block_new_user fra Trust Level 3 brugere, gem da alle deres indlæg og bloker yderligere posteringer. 0 for at slå fra." - num_tl3_users_to_block_new_user: "Hvis en brugers indlæg får num_tl3_flags_to_block_new_user flag fra mange forskellige \"trust level 3\" brugere, så gem alle deres indlæg og fjern muligheden for nye indlæg. Skriv 0 for at deaktivere." - notify_mods_when_user_blocked: "Send en besked til alle moderatorer hvis en bruger blokeres automatisk" flag_sockpuppets: "Hvis en ny bruger svarer på et emne fra den samme IP adresse som den der startede emnet, så rapporter begge at deres indlæg potentielt er spam." traditional_markdown_linebreaks: "Brug traditionelle linjeskift i Markdown, som kræver 2 mellemrum i slutningen af sætningen." post_undo_action_window_mins: "Antal minutter som brugere er tilladt at fortryde handlinger på et indlæg (like, flag, etc)." must_approve_users: "Personale skal godkende alle nye bruger konti inden de kan tilgå sitet. ADVARSEL: aktivering af dette for et live site vil medføre en ophævning af adgang for eksisterende ikke-personale brugere." pending_users_reminder_delay: "Underret moderatorer hvis nye brugere har ventet på godkendelse i længere end så mange timer. Skriv -1 for at deaktivere notifikationer." maximum_session_age: "Bruger vil forblive logget in for n hours siden sidste besøg" - ga_tracking_code: "OBSOLETE: Google analytics (ga.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" - ga_domain_name: "OBSOLETE: Google analytics (ga.js) domain name, eg: mysite.com; see http://google.com/analytics" ga_universal_tracking_code: "Google Universal Analytics (analytics.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" ga_universal_domain_name: "Google Universal Analytics (analytics.js) domain name, eg: mysite.com; see http://google.com/analytics" gtm_container_id: "Google Tag Manager container id. eg: GTM-ABCDEF" @@ -978,8 +970,6 @@ da: num_flaggers_to_close_topic: "Minimum antal flagmarkeringer fra unikke brugere, der skal til for automatisk at pause et emne til ingriben" num_flags_to_close_topic: "Minimum antal aktive flagmarkeringer, der skal til for automatisk at pause et emne til indgriben" auto_respond_to_flag_actions: "Aktivér automatisk besvarelse, når en flagmarkering fjernes." - auto_block_fast_typers_on_first_post: "Blokér automatisk brugere som ikke opfylder min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Maksimal trust level for at blokere \"fast typers\"" reply_by_email_enabled: "Aktivér muligheden for at svare til emner via email." reply_by_email_address: "Skabelon for e-mail-adressen i formularen, for eksempel: %{reply_key}@reply.myforum.com." disable_emails: "Stop Discourse i at sende nogen form for emails" @@ -1216,12 +1206,6 @@ da: Den konto, der er knyttet til denne mailadresse, er ikke aktiveret. Aktiver din konto, før du sendere flere mails ind. - email_reject_blocked_user: - text_body_template: |+ - Vi beklager, men din mailbesked til %{destination} (titled %{former_title}) fungerede ikke. - - Den konto, der er knyttet til denne mailadresse, er blokeret. - email_reject_empty: text_body_template: |+ Vi beklager, men din mailbesked til %{destination} (titled %{former_title}) fungerede ikke. @@ -1258,68 +1242,9 @@ da: too_many_spam_flags: title: "For Mange Flagmarkeringer for Spam" subject_template: "Nyoprettet konto sat på hold" - text_body_template: | - Hej, - - Dette er en automatisk besked fra %{site_name} for at lade dig vide, at dine indlæg midlertidigt er blevet skjult, fordi de er blevet flagmarkeret af andre brugere. - - For en sikkerheds skyld er din nyoprettede konto blevet spærret, så du ikke kan oprette nye svar eller emner, indtil en administrator kan gennemgå din konto. Vi beklager ulejligheden. - - Se venligst vores [Retningslinjer](%{base_url}/guidelines) for yderligere vejledning. too_many_tl3_flags: title: "For Mange TL3 Flagmarkeringer" subject_template: "Nyoprettet konto sat på hold" - text_body_template: | - Hej, - - Dette er en automatisk besked fra %{site_name} for at lade dig vide, at din konto er blevet sat i venteposition på grund af et stort antal flagmarkeringer fra andre brugere. - - For en sikkerheds skyld er din nyoprettede konto blevet spærret, så du ikke kan oprette nye svar eller emner, indtil en administrator kan gennemgå din konto. Vi beklager ulejligheden. - - Se venligst vores [Retningslinjer](%{base_url}/guidelines) for yderligere vejledning. - blocked_by_staff: - title: "Blokeret af hjælperteamet" - subject_template: "Konto midlertidigt sat på hold" - text_body_template: | - Hej, - - Dette er en automatisk besked fra %{site_name} for at lade dig vide, at din konto midlertidigt er blevet sat på hold af sikkerhedsmæssige årsager. - - Du kan fortsat kigge dig omkring på sitet, men du kan ikke oprette indlæg eller emner, intil en [administrator](%{base_url}/about) har gennemgået dine seneste indlæg. Vi beklager ulejligheden. - - Se venligst vores [Retningslinjer](%{base_url}/guidelines) for yderligere vejledning. - user_automatically_blocked: - title: "Bruger automatisk spærret" - subject_template: "Den nye bruger %{username} er blokeret pga. flagmakeringer" - text_body_template: | - Dette er en automatisk besked. - - Den nye bruger [%{username}](%{user_url}) er automatisk blevet spærret, fordi adskillige brugere har flagmarkeret %{username}s indlæg. - - [Gennemgå venligst flagmarkeringerne](%{base_url}/admin/flags). Hvis %{username} uretmæssigt er blevet forhindret i oprette indlæg, kan du klikke på ophæv spærring på [admin-siden for denne bruger](%{user_url}). - - Grænseværdien kan ændres via indstillingen `block_new_user`. - spam_post_blocked: - title: "Spam-indlæg blokeret" - subject_template: "Ny bruger %{username} indlæg blokeret på grund af gentagne links" - text_body_template: | - Dette er en automatisk besked. - - Den nye bruger [%{username}](%{user_url}) har forsøgt at oprette flere indlæg med links til %{domains}, men indlæggene blev blokeret for at forhindre spam. Brugeren kan stadig oprette nye indlæg, som ikke linker til %{domains}. - - [Tjek denne bruger](%{user_url}). - - Indstillingen kan ændres via `newuser_spam_host_threshold` og `white_listed_spam_host_domains`. - unblocked: - title: "Spærring ophævet" - subject_template: "Konto ikke længere sat på hold" - text_body_template: |+ - Hej, - - Dette er en automatisk besked fra %{site_name} for at lade dig vide, at din konto ikke længere er på hold, efter at den er blevet gennemgået af administrator. - - Du kan nu oprette nye indlæg og emner igen. Tak for din tålmodighed. - pending_users_reminder: title: "Påmindelse om afventende brugere" subject_template: diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 96774ae04c..fab8dac1aa 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -48,7 +48,7 @@ de: no_body_detected_error: "Passiert, wenn wir keinen Textkörper extrahieren konnten und keine Anhänge gefunden wurden." no_sender_detected_error: "Kommt vor, wenn wir keine gültige E-Mail-Adresse im From-Header finden konnten." inactive_user_error: "Passiert wenn der Sender nicht aktiv ist." - blocked_user_error: "Passiert wenn der Sender geblockt wurde." + silenced_user_error: "Kommt vor, wenn der Absender stummgeschaltet wurde." bad_destination_address: "Passiert wenn keine der E-Mail Adressen in den Feldern To/Cc/Bcc zu einer konfigurierten Eingangs-E-Mail-Adresse passt." strangers_not_allowed_error: "Passiert wenn ein Benutzer versuchte ein neues Thema in einer Kategorie ohne Mitgliedschaft zu erstellen." insufficient_trust_level_error: "Passiert wenn ein Benutzer versuchte ein neues Thema in einer Kategorie zu erstellen, ohne die erforderliche Vertrauensstufe dafür zu haben." @@ -665,7 +665,7 @@ de: email_title: 'Das Thema "%{title}" benötigt die Aufmerksamkeit eines Moderators' email_body: "%{link}\n\n%{message}" flagging: - you_must_edit: '

    Dein Beitrag wurde von der Community gemeldet. Bitte lies deine Nachrichten.

    ' + you_must_edit: '

    Dein Beitrag wurde von der Community gemeldet. Bitte überprüfe deine Nachrichten.

    ' user_must_edit: '

    Dieser Beitrag wurde von der Community gemeldet und ist vorübergehend ausgeblendet.

    ' archetypes: regular: @@ -925,7 +925,8 @@ de: digest_logo_url: "Das alternative Logo oben in den E-Mail-Zusammenfassungen deiner Site. Sollte eine breite, rechteckige Form haben. Sollte kein SVG-Bild sein. Wenn leer, wird `logo_url` verwendet." logo_small_url: "Dein Logo in klein für die obere linke Seite deiner Site, in Form eines rechteckigen Quadrates. Es wird angezeigt, wenn der Benutzer scrollt. Wenn du dieses Feld frei lässt, wird stattdessen ein Haus-Symbol angezeigt." favicon_url: "Das Favicon deiner Site. Besuche http://de.wikipedia.org/wiki/Favicon um weitere Informationen zu erhalten. Damit das Favicon korrekt über einen CDN-Service funktioniert, muss es eine .png Datei sein." - mobile_logo_url: "Das fixierte Logo in der oberen linken Hälfte der mobilen Siteversion. Es sollte eine quadratische Form haben. Wenn du dieses Feld frei lässt, wird `logo_url` benutzt. Z.B. http://example.com/uploads/default/logo.png" + mobile_logo_url: "Benutzerdefiniertes Logo für die mobile Seitenversion. Falls leer, wird `logo_url` verwendet. Beispiel: http://example.com/uploads/default/logo.png" + large_icon_url: "Bild wird als Logo/Startbild auf Android verwendet. Empfohlene Größe beträgt 512px mal 512px." apple_touch_icon_url: "Icon für berührungsempfindliche Apple-Geräte. Empfohlene Grösse ist 144px auf 144px." notification_email: "Die E-Mail-Adresse die als \"From:\" Absender aller wichtiger System-E-Mails benutzt wird. Die benutzte Domain sollte über korrekte SPF, DKIM und PTR Einträge verfügen, damit E-Mails sicher zugestellt werden können." email_custom_headers: "Eine durch senkrechte Striche getrennte Liste von eigenen E-Mail Headerzeilen" @@ -952,11 +953,11 @@ de: tl2_additional_likes_per_day_multiplier: "Limit für \"Gefällt mir\" für Benutzer auf Vertrauensstufe 2 (Mitglied) mit diesem Faktor multiplizieren" tl3_additional_likes_per_day_multiplier: "Limit für \"Gefällt mir\" für Benutzer auf Vertrauensstufe 3 (Stammgast) mit diesem Faktor multiplizieren" tl4_additional_likes_per_day_multiplier: "Limit für \"Gefällt mir\" für Benutzer auf Vertrauensstufe 4 (Anführer) mit diesem Faktor multiplizieren" - num_spam_flags_to_block_new_user: "Verstecke alle Beiträge eines neuen Benutzers und erlaube keine neuen Beiträge mehr, wenn der neue Benutzer die hier angegebene Zahl an Meldungen bezüglich Spam von num_users_to_block_new_user verschiedenen anderen Benutzern erhalten hat. 0 deaktiviert diese Funktion." - num_users_to_block_new_user: "Wenn Beiträge eines neuen Benutzers so viele Spam-Meldungen von num_spam_flags_to_block_new_user verschiedenen Benutzern mit Vertrauensstufe 3 erhält, werden alle Beiträge des Benutzers ausgeblendet und weitere Beiträge verhindert. 0, um die Funktion zu deaktivieren." - num_tl3_flags_to_block_new_user: "Wenn Beiträge eines neuen Benutzers so viele Meldungen von num_tl3_users_to_block_new_user verschiedenen Benutzern mit Vertrauensstufe 3 erhält, werden alle Beiträge des Benutzers ausgeblendet und weitere Beiträge verhindert. 0, um die Funktion zu deaktivieren." - num_tl3_users_to_block_new_user: "Wenn Beiträge eines neuen Benutzers so viele Meldungen von num_tl3_users_to_block_new_user verschiedenen Benutzern mit Vertrauensstufe 3 erhält, werden alle Beiträge des Benutzers ausgeblendet und weitere Beiträge verhindert. 0, um die Funktion zu deaktivieren." - notify_mods_when_user_blocked: "Wenn ein Benutzer automatisch gesperrt wird, sende eine Nachricht an alle Moderatoren." + num_spam_flags_to_silence_new_user: "Wenn Beiträge eines neuen Benutzers von num_users_to_silence_new_user verschiedenen Benutzern gemeldet wird, verstecke alle Beiträge des Benutzers und erlaube keine weiteren Beiträge. Ein Wert von 0 deaktiviert diese Funktion." + num_users_to_silence_new_user: "Wenn Beiträge eines neuen Benutzers Spam-Meldungen von num_spam_flags_to_block_new_user verschiedenen Benutzern erhalten, werden alle Beiträge des Benutzers ausgeblendet und weitere Beiträge verhindert. Ein Wert von 0 deaktiviert die Funktion." + num_tl3_flags_to_silence_new_user: "Wenn Beiträge eines neuen Benutzers von num_tl3_users_to_silence_new_user verschiedenen Benutzern mit Vertrauensstufe 3 gemeldet werden, verstecke alle Beiträge des Benutzers und erlaube keine weiteren Beiträge. Ein Wert von 0 deaktiviert diese Funktion." + num_tl3_users_to_silence_new_user: "Wenn Beiträge eines neuen Benutzers von num_tl3_users_to_silence_new_user verschiedenen Benutzern mit Vertrauensstufe 3 gemeldet werden, verstecke alle Beiträge des Benutzers und erlaube keine weiteren Beiträge. Ein Wert von 0 deaktiviert diese Funktion." + notify_mods_when_user_silenced: "Wenn ein Benutzer automatisch stummgeschaltet wird, sende eine Nachricht an alle Moderatoren." flag_sockpuppets: "Wenn ein neuer Benutzer auf ein Thema antwortet, das von einem anderen neuen Benutzer aber mit der gleichen IP-Adresse begonnen wurde, markiere beide Beiträge als potenziellen Spam." traditional_markdown_linebreaks: "Traditionelle Zeilenumbrüche in Markdown, die zwei nachfolgende Leerzeichen für einen Zeilenumbruch benötigen." enable_markdown_typographer: "Verwende grundlegende typografische Regeln, um die Lesbarkeit von Absätzen zu erhöhen, ersetzt (c) (tm) usw. durch Symbole, reduziert die Anzahl von Lesezeichen und so weiter" @@ -964,8 +965,6 @@ de: must_approve_users: "Team-Mitglieder müssen alle neuen Benutzerkonten freischalten, bevor diese Zugriff auf die Website erhalten. ACHTUNG: Das Aktivieren dieser Option für eine Live-Site entfernt den Zugriff auch für alle existierenden Benutzer außer für Team-Mitglieder!" pending_users_reminder_delay: "Benachrichtige die Moderatoren, falls neue Benutzer mehr als so viele Stunden auf ihre Genehmigung gewartet haben. Stelle -1 ein, um diese Benachrichtigungen zu deaktivieren." maximum_session_age: "Benutzer bleiben (n) Stunden nach ihrem letzten Besuch angemeldet" - ga_tracking_code: "VERALTET: Google Analytics (analytics.js) tracking code code, z.B.: UA-12345678-9; siehe http://google.com/analytics" - ga_domain_name: "VERALTET: Google Analytics (analytics.js) Domain-Name, z.B.: mysite.com; siehe http://google.com/analytics" ga_universal_tracking_code: "Google Universal Analytics (analytics.js) tracking code code, beispielsweise: UA-12345678-9; Siehe http://google.com/analytics" ga_universal_domain_name: "Google Universal Analytics (analytics.js) domain name, eg: mysite.com; Siehe http://google.com/analytics" ga_universal_auto_link_domains: "Aktiviere Google Universal Analytics (analytics.js) Cross-Domain-Tracking. Ausgehenden Links zu diesen Domains wird eine Client-ID hinzugefügt. Siehe Googles Anleitung zu Cross-Domain Tracking." @@ -1197,9 +1196,9 @@ de: num_hours_to_close_topic: "Anzahl Stunden, um ein Thema als Maßnahme zu pausieren." auto_respond_to_flag_actions: "Automatische Antwort auf abgearbeitete Meldungen aktivieren." min_first_post_typing_time: "Minimale Zeit die ein Benutzer mindestens aufwenden muss, um einen Beitrag zu schreiben. Wenn diese Zeit unterschritten wird, wird der Beitrag automatisch in die Warteschlange für freizuschaltende Beiträge verschoben. Setze diesen Wert auf 0 um dieses Verhalten zu deaktivieren (nicht empfohlen)." - auto_block_fast_typers_on_first_post: "Blockiere Benutzer automatisch, welche unterhalb der min_first_post_typing_time liegen." - auto_block_fast_typers_max_trust_level: "Maximale Vertrauensstufe um \"Schnelltipper\" automatisch zu blockieren." - auto_block_first_post_regex: "Regulärer Ausdruck der dafür sorgt dass passende erste Beiträge von Benutzern genehmigt werden müssen. Groß- und Kleinschreibung wird nicht beachtet.\nBeispiel: raging|a[bc]a blockiert alle ersten Beiträge, die raging, aba oder aca beinhalten. Wird nur auf den ersten Beitrag angewendet." + auto_silence_fast_typers_on_first_post: "Benutzer automatisch stummschalten, die nicht der min_first_post_typing_time entsprechen." + auto_silence_fast_typers_max_trust_level: "Maximale Vertrauensstufe, um „Schnelltipper“ stummzuschalten." + auto_silence_first_post_regex: "Regulärer Ausdruck (ohne Groß- und Kleinschreibung), der dafür sorgt dass entsprechende erste Beiträge von Benutzern stummgeschaltet und in die Genehmigungswarteschlange verschoben werden. Beispiel: raging|a[bc]a sorgt dafür, dass Beiträge, die raging, aba oder aca enthalten, zunächst stummgeschaltet werden. Dies betrifft jedoch nur den ersten Beitrag." flags_default_topics: "Zeige gemeldete Themen standardmäßig im Administrationsbereich" reply_by_email_enabled: "Aktviere das Antworten auf Themen via E-Mail." reply_by_email_address: "Vorlage für die Antwort einer per E-Mail eingehender E-Mail-Adresse, zum Beispiel: %{reply_key}@reply.example.com oder replies+%{reply_key}@example.com" @@ -1349,6 +1348,7 @@ de: default_categories_tracking: "Liste der standardmäßig gefolgten Kategorien." default_categories_muted: "Liste der standardmäßig stummgeschalteten Kategorien." default_categories_watching_first_post: "Liste von Kategorien, in denen der erste Beiträge in jedem neuen Thema automatisch beobachtet wird." + retain_web_hook_events_period_days: "Anzahl an Tagen, die Web-Hook-Ereigniseinträge aufbewahrt werden" max_user_api_reqs_per_day: "Maximale Zahl der Benutzer API Anfragen pro Schlüssel pro Tag" max_user_api_reqs_per_minute: "Maximale Zahl der Benutzer API Anfragen pro Schlüssel pro Minute" allow_user_api_keys: "Erlaube das Generieren von Benutzer-API-Schlüsseln" @@ -1914,13 +1914,13 @@ de: Es tut uns leid, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) hat nicht funktioniert. Dein mit dieser E-Mail-Adresse verbundenes Benutzerkonto ist nicht aktiviert. Bitte aktiviere dein Konto bevor du E-Mails sendest. - email_reject_blocked_user: - title: "E-Mail abgelehnt weil Benutzer gesperrt" - subject_template: "[%{email_prefix}] E-Mail-Problem -- Benutzer blockiert" + email_reject_silenced_user: + title: "E-Mail abgelehnt weil Benutzer stummgeschaltet" + subject_template: "[%{email_prefix}] E-Mail-Problem -- Benutzer stummgeschaltet" text_body_template: | - Es tut uns leid, aber deine E-Mail-Nachricht an %{destination} (betitelt mit %{former_title}) hat nicht funktioniert. + Es tut uns leid, aber deine E-Mail-Nachricht an %{destination} (mit dem Betreff %{former_title}) hat nicht funktioniert. - Dein mit dieser E-Mail-Adresse verbundenes Konto wurde blockiert. + Dein mit dieser E-Mail-Adresse verbundenes Konto wurde stummgeschaltet. email_reject_reply_user_not_matching: title: "E-Mail abgelehnt weil E-Mail-Adresse abweichend" subject_template: "[%{email_prefix}] E-Mail-Problem -- Antwortadresse unerwartet" @@ -2047,9 +2047,9 @@ de: text_body_template: | Hallo, - dies ist eine automatisch erzeugte Nachricht von %{site_name}, um dich darüber zu informieren, dass deine Beitrage vorübergehend ausgeblendet wurden, weil sie von der Community gemeldet wurden. + dies ist eine automatisch erzeugte Nachricht von %{site_name}, um dich darüber zu informieren, dass dein Beitrag vorübergehend versteckt wurde, weil er von der Community gemeldet wurde.. - Als Vorsichtsmaßnahme wurde dein neues Konto für das Erstellen neuer Antworten und Themen gesperrt, bis ein Team-Mitglied dein Konto überprüfen kann. Wir bitten um Entschuldigung für die Unannehmlichkeiten. + Als Vorsichtsmaßnahme wurde dein neues Konto für das Erstellen neuer Antworten und Themen stummgeschaltet, bis ein Team-Mitglied dein Konto überprüfen kann. Wir bitten um Entschuldigung für die Unannehmlichkeiten. Weitere Hinweise findest du in unseren [Community-Richtlinien](%{base_url}/guidelines). too_many_tl3_flags: @@ -2058,46 +2058,46 @@ de: text_body_template: | Hallo, - dies ist eine automatisch erzeugte Nachricht von %{site_name}, um dich darüber zu informieren, dass dein Konto aufgrund einer großen Anzahl von Community-Meldungen vorübergehend gesperrt wurde. + dies ist eine automatisch erzeugte Nachricht von %{site_name}, um dich darüber zu informieren, dass dein Konto vorübergehend gesperrt wurde aufgrund einer großen Anzahl an Community-Meldungen. - Als Vorsichtsmaßnahme wurde dein neues Konto für das Erstellen neuer Antworten und Themen gesperrt, bis ein Team-Mitglied dein Konto überprüfen kann. Wir bitten um Entschuldigung für die Unannehmlichkeiten. + Als Vorsichtsmaßnahme wurde dein neues Konto für das Erstellen neuer Antworten und Themen stummgeschaltet, bis ein Team-Mitglied dein Konto überprüfen kann. Wir bitten um Entschuldigung für die Unannehmlichkeiten. Weitere Hinweise findest du in unseren [Community-Richtlinien](%{base_url}/guidelines). - blocked_by_staff: - title: "Gesperrt vom Team" + silenced_by_staff: + title: "Stummgeschaltet vom Team" subject_template: "Konto vorübergehend gesperrt" text_body_template: | Hallo, dies ist eine automatisch erzeugte Nachricht von %{site_name}, um dich darüber zu informieren, dass dein Konto als Vorsichtsmaßnahme vorübergehend gesperrt wurde. - Bitte schaue dir weiter Inhalte an, aber du kannst zunächst keine Antworten oder Themen erstellen, bis ein [Team-Mitglied](%{base_url}/about) deine aktuellesten Beiträge überprüft hat. Wir bitten um Entschuldigung für die Unannehmlichkeiten. + Bitte schaue dir weiter Inhalte an, aber du kannst zunächst keine Antworten oder Themen erstellen, bis ein [Team-Mitglied](%{base_url}/about) deine aktuellsten Beiträge überprüft hat. Wir bitten um Entschuldigung für die Unannehmlichkeiten. Weitere Hinweise findest du in unseren [Community-Richtlinien](%{base_url}/guidelines). - user_automatically_blocked: - title: "Benutzer automatisch gesperrt" - subject_template: "Neuer Benutzer %{username} gesperrt wegen Community-Meldungen" + user_automatically_silenced: + title: "Benutzer automatisch stummgeschaltet" + subject_template: "Neuer Benutzer %{username} stummgeschaltet wegen Community-Meldungen" text_body_template: | Dies ist eine automatisierte Nachricht. - Der neue Benutzer [%{username}](%{user_url}) wurde automatisch blockiert, weil mehrere Benutzer Beiträge von %{username} gemeldet haben. + Der neue Benutzer [%{username}](%{user_url}) wurde automatisch stummgeschaltet, weil mehrere Benutzer Beiträge von %{username} gemeldet haben. - Bitte [überprüfe die Meldungen](%{base_url}/admin/flags). Wenn %{username} fälschlich blockiert wurde, so klicke die „Entsperren“-Schaltfläche auf der [Administrationsseite dieses Benutzers](%{user_url}). + Bitte [überprüfe die Meldungen](%{base_url}/admin/flags). Wenn %{username} fälschlich stummgeschaltet wurde, so klicke die „Stummschaltung aufheben“-Schaltfläche auf der [Administrationsseite dieses Benutzers](%{user_url}). Der Schwellenwert kann über die Einstellung `block_new_user` geändert werden. - spam_post_blocked: - title: "Spam-Beitrag gesperrt" - subject_template: "Beiträge des neuen Benutzers ${username} wegen mehrfacher Verlinkung blockiert" + spam_post_silenced: + title: "Spam-Beitrag stummgeschaltet" + subject_template: "Beiträge des neuen Benutzers %{username} stummgeschaltet wegen wiederholten Links" text_body_template: | Dies ist eine automatisierte Nachricht. - Der neue Benutzer [%{username}](%{user_url}) hat versucht, mehrere Beiträge mit Links zur Domain %{domains} zu erstellen, aber diese Beiträge wuden blockiert, um Spam zu vermeiden. Der Benutzer kann weiter neue Beiträge erstellen, die nicht auf %{domains} verlinken. + Der neue Benutzer [%{username}](%{user_url}) hat versucht, mehrere Beiträge mit Links zur Domain %{domains} zu erstellen, aber diese Beiträge wurden stummgeschaltet, um Spam zu vermeiden. Der Benutzer kann weiter neue Beiträge erstellen, die nicht auf %{domains} verlinken. Bitte [überprüfe den Benutzer](%{user_url}). - Dies kann über die Einstellungen `newuser_spam_host_threshold` und `white_listed_spam_host_domains` geändert werden. - unblocked: - title: "Entsperrt" + Dieses Verhalten kann über die Einstellungen `newuser_spam_host_threshold` und `white_listed_spam_host_domains` geändert werden. + unsilenced: + title: "Stummschaltung aufgehoben" subject_template: "Konto nicht mehr gesperrt" text_body_template: | Hallo, @@ -2131,7 +2131,7 @@ de: text_body_template: | Glückwunsch, du hast die Auszeichnung **Neuer Benutzer des Monats %{month_year}** erhalten. :trophy: - Diese Auszeichnung wird nur zwei neuen Benutzern pro Monat verliehen, und wird dauerhaft sichtbar sein auf [deiner Profilseite](/my/badges). + Diese Auszeichnung wird nur zwei neuen Benutzern pro Monat verliehen, und wird dauerhaft sichtbar sein auf [deiner Abzeichen-Seite](%{url}). Du bist schnell ein wertvolles Mitglied unserer Community geworden. Danke, dass du dazugekommen bist, und mache weiter mit der großartigen Arbeit! queued_posts_reminder: @@ -2374,7 +2374,8 @@ de: popular_posts: "Beliebte Beiträge" more_new: "Neu für dich" subject_template: "[%{email_prefix}] Zusammenfassung" - unsubscribe: "Diese Zusammenfassung wird von %{site_link} versendet, wenn wir dich einige Zeit lang nicht gesehen haben. Ändere deine E-Mail-Einstellungen. Abbestellen unter %{unsubscribe_link}." + unsubscribe: "Diese Zusammenfassung wird von %{site_link} versendet, wenn wir dich einige Zeit lang nicht gesehen haben. Ändere %{email_preferences_link} oder %{unsubscribe_link}zum Abbestellen." + your_email_settings: "deine E-Mail-Einstellungen" click_here: "klicke hier" from: "Zusammenfassung für %{site_name}" preheader: "Eine kurze Zusammenfassung seit deinem letzten Besuch am %{last_seen_at}" @@ -2476,6 +2477,9 @@ de: see_more: "Mehr" search_title: "Diese Site durchsuchen" search_google: "Google" + offline: + title: "App kann nicht geladen werden" + offline_page_message: "Sieht so aus als wärst du offline! Bitte überprüfe deine Netzwerkverbindung und probiere es nochmal." login_required: welcome_message: | #[Wilkommen bei %{title}](#welcome) @@ -2894,9 +2898,9 @@ de: Das Abzeichen wird verliehen, wenn du den ersten Like auf einen Beitrag erhältst. Glückwunsch, du hast einen Beitrag geschrieben, den deine Mitbenutzer interessant, cool oder nützlich finden! autobiographer: name: Autobiograf - description: Hat Benutzerprofil ausgefüllt + description: Hat Benutzerprofil ausgefüllt long_description: | - Das Abzeichen wird verliehen, wenn du dein Benutzerprofil ausfüllst und ein Profilbild hochlädst. Die Community mehr über dich wissen zu lassen und darüber, was dich interessiert, trägt zu einer besseren, verbundeneren Community bei. Mach mit! + Dieses Abzeichen erhältst du für das Ausfüllen von deinem Benutzerprofil und für das Hochladen eines Profilbildes erhalten. Die Community ein bisschen mehr wissen zu lassen über dich und darüber, was du gerne tust, macht die gesamte Community besser. Mach mit! anniversary: name: Jubiläum description: Ist ein aktives Mitglied für ein Jahr und hat mindestens einen Beitrag verfasst @@ -2989,9 +2993,9 @@ de: Das Abzeichen wird verliehen, wenn du das erste Mal einen Beitrag in deiner Antwort zitierst. Das Zitieren relevanter Abschnitte früherer Beiträge in deiner Antwort hilft dabei, Diskussionen verbunden zu halten und beim Thema zu bleiben. Der einfachste Weg zu zitieren besteht darin, den Abschnitt eines Beitrags zu markieren und dann die Antwort-Schaltfläche zu drücken. Zitiere weiter großzügig! read_guidelines: name: Richtlinien gelesen - description: Hat die Community-Richtlinien gelesen + description: Hat die Community-Richtlinien gelesen long_description: | - Das Abzeichen wird verliehen, wenn du die Community-Richtlinien liest. Das Befolgen und Teilen dieser einfachen Richtlinien hilft dabei, eine sichere, lustige und nachhaltige Community für alle aufzubauen. Denke immer daran, dass dort ein anderer Mensch, sehr ähnlich zu dir, auf der anderen Seite des Bildschirms ist. Sei nett! + Dieses Abzeichen wird verliehen für das Lesen der Communtiy-Richtlinien. Ein Befolgen und Teilen dieser einfachen Richtlinien hilft uns dabei, eine sichere, lustige und nachhaltige Gemeinschaft für alle zu schaffen. Denke immer daran, dass dort ein anderer Mensch, genauso wie du, auf der anderen Seite des Bildschirms ist. Sei nett! reader: name: Leser description: Hat in einem Thema mit mehr als 100 Beiträgen jeden Beitrag gelesen diff --git a/config/locales/server.el.yml b/config/locales/server.el.yml index 9167f54ca4..c1d3d09e79 100644 --- a/config/locales/server.el.yml +++ b/config/locales/server.el.yml @@ -48,7 +48,7 @@ el: no_body_detected_error: "Συμβαίνει όταν είναι αδύνατο να βρεθεί το κύριο μέρος του μηνύματος και όταν δεν υπάρχουν επισυναπτόμενα." no_sender_detected_error: "Συμβαίνει όταν δεν μπορεί να βρεθεί μία έγκυρη διεύθυνση email στο From header." inactive_user_error: "Συμβαίνει όταν ο αποστολέας είναι ανενεργός" - blocked_user_error: "Συμβαίνει όταν ο αποστολέας έχει μπλοκαριστεί" + silenced_user_error: "Συμβαίνει όταν ο αποστολέας έχει σιγηθεί." bad_destination_address: "Συμβαίνει όταν καμία από τις διευθύνσεις email στα πεδία To/Cc/Bcc δεν αντιστοιχίζεται σε κάποια ρυθμισμένη διεύθυνση εισερχόμενων email." strangers_not_allowed_error: "Συμβαίνει όταν ο χρήστης προσπαθεί να δημιουργήσει ένα νέο θέμα σε κατηγορία που δεν είναι μέλος." insufficient_trust_level_error: "Συμβαίνει όταν ο χρήστης προσπαθεί να δημιουργήσει ένα νέο θέμα σε κατηγορία που δεν έχει το απαιτούμενο επίπεδο εμπιστοσύνης. " @@ -673,7 +673,7 @@ el: email_title: 'Το θέμα "%{title}" χρειάζεται έλεγχο από συντονιστή' email_body: "%{link}\n\n%{message}" flagging: - you_must_edit: '

    Η ανάρτησή σου έχει επισημανθεί από την κοινότητα. Παρακαλώ δες τα μηνύματά σου.

    ' + you_must_edit: '

    Η ανάρτησή σου έχει επισημανθεί από την κοινότητα. Παρακαλώ δες τα μηνύματά σου.

    ' user_must_edit: '

    Αυτή η ανάρτηση έχει επισημανθεί από την κοινότητα και είναι προσωρινά κρυφή.

    ' archetypes: regular: @@ -935,7 +935,8 @@ el: digest_logo_url: "Το εναλλακτικό λογότυπο εικόνας που χρησιμοποιείται στην κορυφή του συνοπτικού email του ιστότοπου σας. Θα πρέπει να είναι ένα ευρύ ορθογώνιο σχήμα. Δεν πρέπει να είναι μια εικόνα SVG. Αν μείνει κενό τότε το `logo_url` θα χρησιμοποιηθεί." logo_small_url: "Η μικρή εικόνα του λογοτύπου στο πάνω αριστερό μέρος του site σας, θα πρέπει να έχει τετράγωνο σχήμα, το οποίο εμφανίζεται κατά την κύλιση προς τα κάτω. Αν μείνει κενό, θα εμφανίζεται ένα home glyph." favicon_url: "Μια εικόνα για την ιστοσελίδα σας, βλέπε http://en.wikipedia.org/wiki/Favicon, για να δουλέψει σωστά πάνω απο ένα CDN πρέπει να είναι png" - mobile_logo_url: "Το λογότυπο σταθερής θέσης που χρησιμοποιείται πάνω αριστερά σε κινητά τηλέφωνα. Θα πρέπει να έχει τετράγωνο σχήμα. Αν μείνει κενό, θα χρησιμοποιηθεί το `logo_url`. Π.χ.: http://example.com/uploads/default/logo.png" + mobile_logo_url: "Το url του λογοτύπου που χρησιμοποιείται στην έκδοση της ιστοσελίδας για κινητά τηλέφωνα. Αν μείνει κενό, θα χρησιμοποιηθεί το `logo_url`. Π.χ.: http://example.com/uploads/default/logo.png" + large_icon_url: "Εικόνα που χρησιμοποιείται ως logo/splash εικόνα στο Android. Η προτεινόμενη διάσταση είναι 512px επί 512px." apple_touch_icon_url: "Εικονίδιο που χρησιμοποιείται για συσκευές αφής της Apple. Το συνιστώμενο μέγεθος είναι 144px από 144px." notification_email: "Η από: διεύθυνση email που χρησιμοποιείται για την αποστολή όλων των απαραιτήτων μηνυμάτων του συστήματος. Ο τομέας που καθορίζεται εδώ πρέπει να έχει SPF, DKIM και reverse PTR αρχεία ρυθμισμένα σωστά για να φθάσει το μήνυμα." email_custom_headers: "Μια pipe-delimited λίστα που περιέχει τις προσαρμοσμένες κεφαλίδες των μηνυμάτων email. " @@ -962,11 +963,11 @@ el: tl2_additional_likes_per_day_multiplier: "Αύξησε το όριο των ''Μου αρέσει'' καθημερινά για tl2 (member) πολλαπλασιάζοντας με αυτό τον αριθμό." tl3_additional_likes_per_day_multiplier: "Αύξησε το όριο των ''Μου αρέσει'' καθημερινά για tl3 (regular) πολλαπλασιάζοντας με αυτό τον αριθμό." tl4_additional_likes_per_day_multiplier: "Αύξησε το όριο των ''Μου αρέσει'' καθημερινά για tl4 (leader) πολλαπλασιάζοντας με αυτό τον αριθμό" - num_spam_flags_to_block_new_user: "Αν οι αναρτήσεις ενός καινούριου χρήστη λάβουν τόσες αναφορές για spam από num_users_to_block_new_user διαφορετικούς χρήστες, κρύψε όλες τις αναρτήσεις του χρήστη και εμπόδισε κάθε μελλοντική ανάρτηση. 0 για απενεργοποιήσει." - num_users_to_block_new_user: "Αν οι αναρτήσεις ενός καινούριου χρήστη πάρουν num_spam_flags_to_block_new_user σήμανση ανεπιθύμητου από τόσους χρηστες, κρύψε όλες τις αναρτήσεις του χρήστη και εμπόδισε κάθε μελλοντική ανάρτηση. 0 για απενεργοποίηση." - num_tl3_flags_to_block_new_user: "Αν οι αναρτήσεις ενός καινούριου χρήστη πάρουν τόσες πολλές σημάνσεις από num_tl3_users_to_block_new_user, διαφορετικούς χρήστες επιπέδου εμπιστοσύνης 3, κρύψε όλες τις αναρτήσεις του χρήστη και εμπόδισε κάθε μελλοντική ανάρτηση. 0 για απενεργοποίηση." - num_tl3_users_to_block_new_user: "Αν οι αναρτήσεις ενός καινούριου χρήστη πάρουν num_tl3_flags_to_block_new_user σημάνσεις από τόσους χρηστες επιπέδου εμπιστοσύνης 3, κρύψε όλες τις αναρτήσεις του χρήστη και εμπόδισε κάθε μελλοντική ανάρτηση. 0 για απενεργοποίηση." - notify_mods_when_user_blocked: "Εάν ένας χρήστης μπλοκαριστει αυτόματα, στείλε μήνυμα σε όλους τους συντονιστές." + num_spam_flags_to_silence_new_user: "Αν η ανάρτηση ενός νέου χρήστη λάβει αυτόν τον αριθμό σημάνσεων spam από num_users_to_silence_new_user διαφορετικούς χρήστες, κρύψε όλες τις υπάρχουσες αναρτησεις του χρήστη και απαγόρευσε κάθε μελλοντική ανάρτηση. 0 για ακύρωση." + num_users_to_silence_new_user: "Αν η ανάρτηση ενός νέου χρήστη λάβει num_spam_flags_to_silence_new_user σημάνσεις spam από αυτόν τον αριθμό διαφορετικών χρηστών, κρύψε όλες τις υπάρχουσες αναρτησεις του χρήστη και απαγόρευσε κάθε μελλοντική ανάρτηση. 0 για ακύρωση." + num_tl3_flags_to_silence_new_user: "Αν η ανάρτηση ενός νέου χρήστη λάβει αυτόν τον αριθμό σημάνσεων από num_tl3_users_to_silence_new_user διαφορετικούς χρήστες επιπέδου 3, κρύψε όλες τις υπάρχουσες αναρτησεις του χρήστη και απαγόρευσε κάθε μελλοντική ανάρτηση. 0 για ακύρωση." + num_tl3_users_to_silence_new_user: "Αν η ανάρτηση ενός νέου χρήστη λάβει num_tl3_flags_to_silence_new_user flags από αυτόν τον αριθμό διαφορετικών χρηστών επιπέδου 3, κρύψε όλες τις υπάρχουσες αναρτησεις του χρήστη και απαγόρευσε κάθε μελλοντική ανάρτηση. 0 για ακύρωση." + notify_mods_when_user_silenced: "Αν ένας χρήστης σιγηθεί αυτόματα, στείλε ένα μήνυμα σε όλους τους συντονιστές." flag_sockpuppets: "Εάν ένας νέος χρήστης απαντήσει σε ένα νήμα από την ίδια διεύθυνση ΙP όπως ο νέος χρήστης, ο οποίος ξεκίνησε το νήμα, και οι δυο δημοσιεύσεις τους θα επισημανθούν ως δυνητικά ανεπιθύμητες." traditional_markdown_linebreaks: "Χρήση παραδοσιακών αλλαγών γραμμών στη Markdown, η οποία απαιτεί δύο κενά διαστήματα για μια αλλαγή γραμμής." enable_markdown_typographer: "Χρησιμοποίησε βασικούς τυπογραφικούς κανόνες για να βελτιώσεις την αναγνωσιμότητα του κειμένου στις παραγράφους, αντικαθιστά (c) (tm) κλπ με σύμβολα, μειώνει τον αριθμό των ερωτηματικών και άλλα" @@ -974,8 +975,6 @@ el: must_approve_users: "Το προσωπικό πρέπει να εγκρίνει όλους τους λογαριασμούς των νέων χρηστών προτού τους επιτραπεί να έχουν πρόσβαση στην ιστοσελίδα. Προειδοποίηση: ενεργοποιώντας το για μια ζωντανή ιστοσελίδα θα έχει ως αποτέλεσμα την ανάκληση για τους υπάρχοντες χρήστες που δεν ανήκουν στο προσωπικό!" pending_users_reminder_delay: "Ειδοποίηση των συντονιστών αν υπάρχουν νέοι χρήστες που περιμένουν για αποδοχή του λογαριασμού τους για μεγαλύτερο απο αυτό το χρονικό διάστημα. Όρισέ το στο -1 για να απενεργοποιηθούν οι ειδοποιήσεις." maximum_session_age: "Ο χρήστης θα παραμείνει συνδεδεμένος για n ώρες από την τελευταία του επίσκεψη" - ga_tracking_code: "ΠΑΡΩΧΗΜΕΝΟ: Google analytics (ga.js) tracking code code, eg: UA-12345678-9; βλέπε http://google.com/analytics" - ga_domain_name: "ΠΑΡΩΧΗΜΕΝΟ: Google analytics (ga.js) όνομα τομέα , π.χ. eg: mysite.com, βλέπε http://google.com/analytics" ga_universal_tracking_code: "Google Universal Analytics (analytics.js) tracking code code, eg: UA-12345678-9; βλέπε http://google.com/analytics" ga_universal_domain_name: "Google Universal Analytics (analytics.js) όνομα τομέα, eg: mysite.com; βλέπε http://google.com/analytics" ga_universal_auto_link_domains: "Ενεργοποίησε τα Google Universal Analytics (analytics.js) cross-domain tracking. Στα εξερχόμενα links προς αυτά τα domains θα προστεθεί αυτόματα το client id. Δες τον οδηγό Google's Cross-Domain Tracking." @@ -1207,9 +1206,8 @@ el: num_hours_to_close_topic: "Αριθμός ωρών παύσης ενός νήματος για παρέμβαση" auto_respond_to_flag_actions: "Ενεργοποίηση αυτόματης απάντησης όταν αφαιρείται μια επισήμανση." min_first_post_typing_time: "Ελάχιστος χρόνος σε χιλιοστά του δευτερολέπτου που ένας χρήστης θα πρέπει να πληκτρολογεί κατά τη διάρκεια της πρώτης ανάρτησης, εάν το όριο δεν επιτευχθεί τότε η ανάρτηση αυτόματα εισέρχεται στην ουρά αυτών που απαιτούν έγκριση. Θέτοντας τον ίσο με 0 απενεργοποιείται το όριο (δεν συνιστάται) " - auto_block_fast_typers_on_first_post: "Αυτόματος αποκλεισμός στους χρήστες που δεν πληρούν min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Μέγιστο επίπεδο εμπιστοσύνης για τον αυτόματο αποκλεισμό γρήγορων δακτυλογράφων" - auto_block_first_post_regex: "Το regex χωρίς διάκριση πεζών-κεφαλαίων το οποίο αν περάσει θα προκαλέσει την πρώτη ανάρτηση από το χρήστη να μπλοκάρει και να σταλθεί στην ουρά για έγκριση. Παράδειγμα: raging|a[bc]a, θα προκαλέσει όλα τα μηνύματα που περιέχουν raging ή ΑΒΑ ή ACA να μπλοκάρουν. Ισχύει μόνο για την πρώτη ανάρτηση." + auto_silence_fast_typers_on_first_post: "Αυτόματη σίγηση των χρηστών που δεν πληρούν το κριτήριο min_first_post_typing_time" + auto_silence_fast_typers_max_trust_level: "Μέγιστο επίπεδο εμπιστοσύνης για την αυτόματη σίγηση σε περίπτωση γρήγορης πληκτρολόγησης" flags_default_topics: "Προβολή επισημασμένων νημάτων από προεπιλογή στην σελίδα διαχείρισης" reply_by_email_enabled: "Ενεργοποίηση απάντησης στα νήματα μέσω email." reply_by_email_address: "Πρότυπο για την απάντηση μέσω email της διεύθυνσης email εισερχομένων μηνυμάτων, για παράδειγμα: %{reply_key}@reply.example.com or replies+%{reply_key}@example.com" @@ -1359,6 +1357,7 @@ el: default_categories_tracking: "Λίστα κατηγοριών που παρακολουθούνται από προεπιλογή." default_categories_muted: "Λίστα κατηγοριών που βρίσκονται σε σίγαση από προεπιλογή." default_categories_watching_first_post: "Λίστα κατηγοριών στις οποίες η πρώτη ανάρτηση σε κάθε νέο νήμα θα επιτηρείται από προεπιλογή." + retain_web_hook_events_period_days: "Number of days to retain web hook event records." max_user_api_reqs_per_day: "Μέγιστος αριθμός από user API requests ανά κλειδί ανά μέρα" max_user_api_reqs_per_minute: "Μέγιστος αριθμός από user API requests ανά κλειδί ανά λεπτό" allow_user_api_keys: "Επιτρέψτε την δημιουργία των user API keys" @@ -1925,14 +1924,13 @@ el: Λυπούμαστε, αλλά το email σου προς %{destination} (με τίτλο %{former_title}) απέτυχε. Ο λογαριασμός σου που σχετίζεται με αυτή τη διεύθυνση email δεν είναι ενεργοποιημένος. Παρακαλώ ενεργοποίησε τον λογαριασμό σου προτού στείλεις email. - email_reject_blocked_user: - title: "Απόρριψη email Ο χρήστης είναι αποκλεισμένος" - subject_template: "[%{email_prefix}] Πρόβλημα Email -- Αποκλεισμένος Χρήστης" - text_body_template: |+ - Λυπούμαστε, αλλά το email σου προς %{destination} (με τίτλο %{former_title}) απέτυχε. - - Ο λογαριασμός σου που σχετίζεται με αυτή τη διεύθυνση email έχει αποκλειστεί. + email_reject_silenced_user: + title: "Απόρριψη Email Χρήστης σε Σιγή" + subject_template: "[%{email_prefix}] Πρόβλημα Email -- Χρήστης σε Σιγή" + text_body_template: | + Λυπούμαστε, αλλά το email προς %{destination} (με τίτλο %{former_title}) δεν λειτούργησε. + Ο λογαριασμός χρήστη που είναι συνδεμένος με αυτό το email είναι σε σιγή. email_reject_reply_user_not_matching: title: "Απόρριψη email Ο χρήστης δεν ταιριάζει" subject_template: "[%{email_prefix}] Πρόβλημα Email -- Απροσδόκητη Διεύθυνση Απάντησης" @@ -2061,7 +2059,7 @@ el: Αυτό είναι αυτοματοποιημένο μήνυμα από %{site_name} για να σε ενημερώσει ότι οι αναρτήσεις σου έχουν αποκρυφθεί επειδή επισημάνθηκαν από την κοινότητα. - Ως μέτρο προστασίας, ο νέος σου λογαριασμός έχει μπλοκαριστεί και δε μπορείς να δημιουργήσεις νέες απαντήσεις ή νήματα έως ότου ένας συνεργάτης μπορέσει να εξετάσει το λογαριασμό σου. Ζητούμε συγνώμη για την ταλαιπωρία. + Ως μέτρο προστασίας, ο νέος σου λογαριασμός είναι σε σιγή και δε μπορείς να δημιουργήσεις νέες απαντήσεις ή νήματα έως ότου ένας συνεργάτης μπορέσει να εξετάσει το λογαριασμό σου. Ζητούμε συγνώμη για την ταλαιπωρία. Για επιπλέον καθοδήγηση, παρακαλούμε ανάτρεξε στον [οδηγό χρήσης](%{base_url}/guidelines). too_many_tl3_flags: @@ -2072,52 +2070,21 @@ el: Αυτό είναι αυτοματοποιημένο μήνυμα από %{site_name} για να σε ενημερώσει ότι ο λογαριασμός σου έχει ανασταλλεί εξαιτίας του μεγάλου αριθμού επισημάνσεων από την κοινότητα. - Ως μέτρο προστασίας, ο νέος σου λογαριασμός έχει μπλοκαριστεί και δε μπορείς να δημιουργήσεις νέες απαντήσεις ή νήματα έως ότου ένας συνεργάτης μπορέσει να εξετάσει το λογαριασμό σου. Ζητούμε συγνώμη για την ταλαιπωρία. + Ως μέτρο προστασίας, ο νέος σου λογαριασμός είναι σε σιγή και δε μπορείς να δημιουργήσεις νέες απαντήσεις ή νήματα έως ότου ένας συνεργάτης μπορέσει να εξετάσει το λογαριασμό σου. Ζητούμε συγνώμη για την ταλαιπωρία. Για επιπλέον καθοδήγηση, παρακαλούμε ανάτρεξε στον [οδηγό χρήσης](%{base_url}/guidelines). - blocked_by_staff: - title: "Μπλοκάρισμα από το προσωπικό" - subject_template: "Λογαριασμός προσωρινά σε αναμονή" - text_body_template: | - Γεια σου, - - Αυτό είναι αυτοματοποιημένο μήνυμα από %{site_name} για να σε ενημερώσει ότι ο λογαριασμός σου έχει προσωρινά ανασταλλεί ως μέτρο προστασίας. - - Παρακαλούμε συνέχισε την περιήγηση, αλλά δεν θα είσαι σε θέση να απαντήσεις ή να δημιουργήσεις νήματα έως ότου ένας [συνεργάτης](%{base_url}/about) αναθεωρήσει τις πιο πρόσφατες αναρτήσεις σου. Ζητούμε συγνώμη για την ταλαιπωρία. - - Για επιπλέον καθοδήγηση, ανάτρεξε στον [οδηγό χρήσης της κοινότητας](%{base_url}/guidelines). - user_automatically_blocked: - title: "Ο χρήστης μπλοκαρίστηκε αυτόματα" - subject_template: "Νέος χρήστης %{username} μπλοκαρίστηκε από τις σημάνσεις της κοινότητας" - text_body_template: | - Αυτό είναι αυτοματοποιημένο μήνυμα. - - Ο νέος χρήστης [%{username}](%{user_url}) μπλοκαρίστηκε αυτόματα επειδή πολλαπλοί χρήστες επισήμαναν την ανάρτηση(σεις) του %{username}. - - Παρακαλούμε [αναθεώρησε τις επισημάνσεις](%{base_url}/admin/flags). Εάν ο χρήστης %{username} λανθασμένα μπλοκαρίστηκε από το να αναρτεί, πάτησε το κουμπί απεμπλοκής στην [σελίδα διαχείρισης χρήστη](%{user_url}). - - Αυτό το κατώφλι μπορεί να αλλαχθεί μέσω των ρυθμίσεων `block_new_user`. - spam_post_blocked: - title: " Ανεπιθύμητη Ανάρτηση Μπλοκαρίστηκε" - subject_template: "Οι αναρτήσεις του νέου χρήστη %{username} μπλοκαρίστηκαν εξαιτίας των επαναλαμβανόμενων συνδέσμων" - text_body_template: | - Αυτό είναι αυτοματοποιημένο μήνυμα. - - Ο νέος χρήστης [%{username}](%{user_url}) προσπάθησε να δημιουργήσει πολλαπλές αναρτήσεις με συνδέσμους σε %{domains}, όμως αυτές οι αναρτήσεις μπλοκαρίστηκαν για να αποφευχθεί ανεπιθύμητη αλληλογραφία. Ο χρήστης ακόμα μπορεί να δημιουργεί νέες αναρτήσεις που δεν περιέχουν συνδέσμους σε %{domains}. - - Παρακαλούμε [αναθεώρησε το χρήστη](%{user_url}). - - Μπορείς να το τροποποιήσεις μέσω των ρυθμίσεων ιστοτόπου `newuser_spam_host_threshold` και `white_listed_spam_host_domains`. - unblocked: - title: "Αναιρέθηκε ο αποκλεισμός" + silenced_by_staff: + title: "Σιγήθηκε από Συνεργάτες" + subject_template: "Λογαριασμός σε αναστολή" + user_automatically_silenced: + title: "Χρήστης Αυτόματα σε Σιγή" + subject_template: "Νέος χρήστης %{username} σιγήθηκε από σημάνσεις της κοινότητας" + spam_post_silenced: + title: "Σιγήθηκε Ανάρτηση Spam" + subject_template: "Οι αναρτήσεις του νέου χρήστη %{username} σιγήθηκαν λόγω επαναλαμβανόμενων συνδέσμων" + unsilenced: + title: "Εκτός Σιγής" subject_template: "Λογαριασμός όχι πια σε αναστολή" - text_body_template: |+ - Γεια σου, - - αυτό είναι αυτοματοποιημένο μήνυμα από %{site_name} για να σε ενημερώσει ότι ο λογαριασμός σου δεν είναι πλέον σε αναστολή έπειτα από αναθεώρηση απο τους συνεργάτες. - - Μπορείς τώρα να δημιουργήσεις νέες απαντήσεις και νήματα ξανά. Ευχαριστούμε για την υπομονή σου. - pending_users_reminder: title: "Υπενθύμηση για χρήστες που εκκρεμούν" subject_template: @@ -2144,7 +2111,7 @@ el: text_body_template: | Συγχαρητήρια, κέρδισες το βραβείο **Χρήστης του Μήνα για %{month_year}**. :trophy: - Αυτό το βραβείο επονέμεται σε δυο νέους χρήστες κάθε μήνα και θα είναι για πάντα ορατό στην [σελίδα χρήστη](%{base_url}/my/badges). + Αυτό το βραβείο επονέμεται σε δυο νέους χρήστες κάθε μήνα και θα είναι για πάντα ορατό στην [σελίδα χρήστη](%{url}). Γίνατε γρήγορα ένα πολύτιμο μέλος της κοινότητάς μας. Σας ευχαριστούμε για την συμμετοχή σας και ελπίζουμε να συνεχίσετε έτσι και στο μέλλον! queued_posts_reminder: @@ -2389,7 +2356,8 @@ el: popular_posts: "Δημοφιλείς Αναρτήσεις" more_new: "Νέα για σένα" subject_template: "[%{email_prefix}] Σύνοψη" - unsubscribe: "Λάβατε αυτή την σύνοψη από την %{site_link} επειδή δεν σας είδαμε εδώ και μερικές μέρες. Αν δεν θέλετε να λαμβάνετε αυτή την σύνοψη, αλλάξτε τις ρυθμίσεις σας ή %{unsubscribe_link} για διαγραφή από την λίστα." + unsubscribe: "Λάβατε αυτή την σύνοψη από την %{site_link} επειδή δεν σας είδαμε εδώ και μερικές μέρες. Αν δεν θέλετε να λαμβάνετε αυτή την σύνοψη, αλλάξτε τις %{email_preferences_link} ή %{unsubscribe_link} για διαγραφή από την λίστα." + your_email_settings: "ρυθμίσεις email" click_here: "κάνε κλικ εδώ" from: "%{site_name} σύνοψη" preheader: "Μία σύντομη σύνοψη από την τελευταία σου επίσκεψη στις %{last_seen_at}" @@ -2495,6 +2463,9 @@ el: see_more: "Περισσότερα" search_title: "Αναζήτηση στην ιστοσελίδα" search_google: "Google" + offline: + title: "Cannot load app" + offline_page_message: "Πιθανώς βρίσκεσαι εκτός σύνδεσης! Έλεγξε την σύνδεσή σου και δοκίμασε ξανά." login_required: welcome_message: | ## [Καλώς ήρθατε στην %{title}](#welcome) @@ -2934,9 +2905,9 @@ el: autobiographer: name: Αυτοβιογράφος - description: Συμπλήρωσε τις πληροφορίες του προφίλ + description: Συμπλήρωσε τις πληροφορίες του προφίλ long_description: | - Αυτό το παράσημο χορηγείται όταν συμπληρώσεις το προφίλ σου και επιλέξεις μια φωτογραφία προφίλ. Επιτρέποντας στην κοινότητα να μάθει λίγα περισσότερα για σένα και τα ενδιαφέροντά σου, βοηθάει στην δημιουργία μιας καλύτερης, πιο διασυνδεμένης κοινότητας. + Αυτό το παράσημο χορηγείται όταν συμπληρώσεις το προφίλ σου και επιλέξεις μια φωτογραφία προφίλ. Επιτρέποντας στην κοινότητα να μάθει λίγα περισσότερα για σένα και τα ενδιαφέροντά σου, βοηθάει στην δημιουργία μιας καλύτερης, πιο διασυνδεμένης κοινότητας. anniversary: name: Επέτειος description: Ενεργό μέλος για ένα χρόνο με τουλάχιστον μια ανάρτηση @@ -3041,9 +3012,9 @@ el: Αυτό το παράσημο δίνεται την πρώτη φορά που παραθέτεις μια ανάρτηση σε μια απάντησή σου. Παραθέτοντας σχετικά τμήματα από προηγούμενες αναρτήσεις στην απάντησή σου, βοηθάει να παραμένουν οι συζητήσεις συνδεδεμένες και εντός θέματος. Ο πιο εύκολος τρόπος να παραθέσεις, είναι να μαρκάρεις ένα τμήμα από μια ανάρτηση και έπειτα να πατήσεις κάποιο πλήκτρο απάντησης. Παραθέστε ελεύθερα! read_guidelines: name: Διάβασε τις Οδηγίες - description: Διάβασε τις οδηγίες χρήσης της κοινότητας + description: Διάβασε τις οδηγίες χρήσης της κοινότητας long_description: |+ - Το παράσημο χορηγείται για την ανάγνωση των οδηγιών χρήσης της κοινότητας. Ακολουθώντας και κοινοποιώντας αυτές τις απλές οδηγίες σε βοηθά να χτίσεις μια ασφαλή, διασκεδαστική και βιώσιμη κοινότητα για όλους. Να θυμάσαι πάντα ότι υπάρχει κάποιος άλλος άνθρωπος, πολύ όμοιος με εσένα, στην άλλη πλευρά της οθόνης. Να είσαι καλός! + Το παράσημο χορηγείται για την ανάγνωση των οδηγιών χρήσης της κοινότητας. Ακολουθώντας και κοινοποιώντας αυτές τις απλές οδηγίες σε βοηθά να χτίσεις μια ασφαλή, διασκεδαστική και βιώσιμη κοινότητα για όλους. Να θυμάσαι πάντα ότι υπάρχει κάποιος άλλος άνθρωπος, πολύ όμοιος με εσένα, στην άλλη πλευρά της οθόνης. Να είσαι καλός! reader: name: Αναγνώστης diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 2b73d7fde9..ebdae90117 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -47,7 +47,6 @@ es: auto_generated_email_error: "Sucede cuando la 'prioridad' en el encabezado está establecida en: lista, basura, en masa o auto_respuesta, o cuando algún otro encabezado contiene: auto-enviado, auto-respondido o auto-generado." no_body_detected_error: "Sucede cuando no podemos extraer el cuerpo del mensaje y no hay archivos adjuntos." inactive_user_error: "Sucede cuando el emisor no está activo." - blocked_user_error: "Sucede cuando el emisor ha sido bloqueado." bad_destination_address: "Sucede cuando ninguna de las direcciones de email en los campos Para/Cc/Bcc coincide con un email configurado como dirección de correo entrante." strangers_not_allowed_error: "Sucede cuando un usuario intentó crear un nuevo tema en una categoría de la que no forma parte." insufficient_trust_level_error: "Sucede cuando un usuario intentó crear un nuevo tema en una categoría para la que no tiene el nivel de confianza requerido." @@ -928,11 +927,6 @@ es: tl2_additional_likes_per_day_multiplier: "Incrementar el límite de likes por día para el nivel de confianza 2 (miembro) multiplicando por este número" tl3_additional_likes_per_day_multiplier: "Incrementar el límite de likes por día para el nivel de confianza 3 (habitual) multiplicando por este número" tl4_additional_likes_per_day_multiplier: "Incrementar el límite de likes por día para el nivel de confianza 4 (líder) multiplicando por este número" - num_spam_flags_to_block_new_user: "Si los posts de un nuevo usuario obtienen este número de reportes por spam de num_users_to_block_new_user diferentes usuarios, se ocultarán todos sus posts y no podrá publicar más en un futuro. Pon 0 para deshabilitar esto." - num_users_to_block_new_user: "Si los posts de un nuevo usuario son reportados como spam por num_spam_flags_to_block_new_user usuarios diferentes, ocultar todos sus posts y evitar que publique en el futuro. Poner 0 para desactivar." - num_tl3_flags_to_block_new_user: "Si los posts de un nuevo usuario obtienen este número de reportes por spam de num_users_to_block_new_user diferentes usuarios, se ocultarán todos sus posts y no podrá publicar más en un futuro. Pon 0 para deshabilitar esto." - num_tl3_users_to_block_new_user: "Si los posts de un nuevo usuario reciben num_tl3_flags_to_block_new_user reportes de este número de usuarios de nivel 3 de confianza, ocultar todos sus posts y prevenir futuras publicaciones. 0 para desactivar esta opción." - notify_mods_when_user_blocked: "Si un usuario es bloqueado automáticamente, enviar un mensaje a todos los moderadores." flag_sockpuppets: "Si un nuevo usuario responde a un tema desde la misma dirección de IP que el nuevo usuario que inició el tema, reportar los posts de los dos como spam en potencia." traditional_markdown_linebreaks: "Utiliza saltos de línea tradicionales en Markdown, que requieren dos espacios al final para un salto de línea." enable_markdown_typographer: "Utilice reglas básicas de tipografía para mejorar la legibilidad de texto de los párrafos de texto, reemplaza (c) (tm) etc, con símbolos, reduce el número de signos de interrogación y así sucesivamente" @@ -940,8 +934,6 @@ es: must_approve_users: "Los miembros administración deben aprobar todas las nuevas cuentas antes de que se les permita el acceso al sitio. AVISO: ¡habilitar esta opción en un sitio activo revocará el acceso a los usuarios que no sean moderadores o admin!" pending_users_reminder_delay: "Notificar a los moderadores si hay nuevos usuarios que hayan estado esperando aprbación durante más estas horas. Usa -1 para desactivar estas notificaciones." maximum_session_age: "El usuario permanecerá n horas con su sesión iniciada desde su última visita" - ga_tracking_code: "El código \"Google analytics (ga.js)\", por ejemplo: UA-12345678-9 es OBSOLETO; leer http://google.com/analytics" - ga_domain_name: "El nombre del dominio para \"Google analytics (ga.js)\" es OBSOLETO, ejemplo: mysite.com; ver http://google.com/analytics" ga_universal_tracking_code: "Código de seguimiento de Google Universal Analytics (analytics.js), ejemplo: UA-12345678-9; visita http://google.com/analytics" ga_universal_domain_name: "Nombre del dominio establecido en Google Universal Analytics (analytics.js), ejemplo: misitio.com; visita http://google.com/analytics" gtm_container_id: "Google Tag Manager container id. ejempo: GTM-ABCDEF" @@ -1167,9 +1159,6 @@ es: num_hours_to_close_topic: "Número de horas a pausar un tema para invervención." auto_respond_to_flag_actions: "Activar la respuesta automática al deshacer un reporte." min_first_post_typing_time: "Mínimo período de tiempo en milisegundos en el que un usuario debe estar tecleando durante su primer post, si no se llega a este umbral entrará automáticamente a la cola de moderación. Establece esta opción a 0 para desactivarla (no recomendado)." - auto_block_fast_typers_on_first_post: "Bloquear automáticamente a usuarios que no cumplan el umbral establecido en min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Máximo nivel de confianza por debajo del cual se podrán bloquearán usuarios que escriban demasiado rápido en su primer post" - auto_block_first_post_regex: "Expresión regular que no distingue mayúsculas y minúsculas que si es detectada hará que el primer post de un usuario sea bloqueado y enviado a la cola de aprobacion. Ejemplo: raging|a[bc]a hará que todos los posts que contengan gaging, aba o aca sean bloqueados. Sólo tiene efecto en el primer mensaje de un usuario." reply_by_email_enabled: "Habilitar la respuesta a temas por email." reply_by_email_address: "Plantilla para la dirección de email que aparecerá al recibir correos con la función de respuesta por email: %{reply_key}@respuesta.ejemplo.com o respuestas+%{reply_key}@ejemplo.com" alternative_reply_by_email_addresses: "Lista de plantillas alternativa para las direcciones de respuesta por email. Ejemplo: %{reply_key}@reply.example.com|replies+%{reply_key}@example.com" @@ -1840,13 +1829,6 @@ es: Lo sentimos, pero tu email para %{destination} (titulado %{former_title}) no se entregó. La cuenta asociada a esta dirección de email no ha sido activada. Por favor activa tu cuenta antes de enviar emails. - email_reject_blocked_user: - title: "Email rechazado, usuario bloqueado" - subject_template: "[%{email_prefix}] Problema con el email -- Usuario bloqueado" - text_body_template: | - Lo sentimos, pero tu email para %{destination} (titulado %{former_title}) no se entregó. - - La cuenta asociada a esta dirección de email ha sido bloqueada. email_reject_reply_user_not_matching: title: "Email rechazado, el usuario no coincide" subject_template: "[%{email_prefix}] Problema con el correo -- Email de respuesta no esperado" @@ -1928,68 +1910,9 @@ es: too_many_spam_flags: title: "Demasiadas banderas por Spam" subject_template: "Nueva cuenta retenida" - text_body_template: | - Hola, - - Esto es un mensaje automático de %{site_name} para hacerte saber que tus mensajes han sido ocultados temporalmente porque han sido reportados por la comunidad. - - Como medida cautelar, tu cuenta nueva ha sido bloqueada y no puede crear nuevas respuestas o temas hasta que un miembro del staff la revise. Sentimos las molestias ocasionadas. - - Para más información sobre lo que consideramos adecuado, lee las [directrices de la comunidad](%{base_url}/guidelines). too_many_tl3_flags: title: "Demasiadas banderas NC3" subject_template: "Nueva cuenta retenida" - text_body_template: | - Hola, - - Esto es un mensaje automático de %{site_name} para hacerte saber que tus mensajes han sido ocultados temporalmente porque han sido reportados muchas veces por la comunidad. - - Como medida cautelar, tu cuenta nueva ha sido bloqueada y no puede crear nuevas respuestas o temas hasta que un miembro del staff la revise. Sentimos las molestias ocasionadas. - - Para más información sobre lo que consideramos adecuado, lee las [directrices de la comunidad](%{base_url}/guidelines). - blocked_by_staff: - title: "Bloqueado por Staff" - subject_template: "Cuenta temporalmente retenida" - text_body_template: | - Hola, - - Esto es un mensaje automático de %{site_name} para hacerte saber que tu cuenta ha sido temporalmente retenida como medida cautelar. - - Por favor, sigue leyendo, pero no podrás responder o crear temas hasta que un [miembro del staff](%{base_url}/about) revise tus últimos temas. Sentimos las molestias ocasionadas. - - - Para más información sobre lo que consideramos adecuado, lee las [directrices de la comunidad](%{base_url}/guidelines). - user_automatically_blocked: - title: "Usuario Bloqueado Automaticamente" - subject_template: "Nuevo usuario %{username} bloqueado por las \"banderas\" de la comunidad." - text_body_template: | - Este es un mensaje automático. - - El usuario nuevo [%{username}](%{user_url}) ha sido automáticamente bloqueado porque varios usuarios han reportado los mensajes de %{username}. - - Por favor, [mira los reportes](%{base_url}/admin/flags). Si %{username} hubiera sido bloqueado incorrectamente, click haz clic en el botón de desbloquear en la [página de administración del usuario](%{user_url}). - - Esto puede ser cambiado con el ajuste `block_new_user`. - spam_post_blocked: - title: "Post con Spam Bloqueado" - subject_template: "El nuevo usuario %{username} tiene posts bloqueados debido a repetición de enlaces." - text_body_template: | - Este es un mensaje automático. - - El usuario nuevo [%{username}](%{user_url}) ha intentado crear múltiples temas con enlaces a %{domains}, pero han sido bloqueados para evitar spam. El usuario todavía puede crear temas, siempre y cuando no contengan enlaces a %{domains}. - - Por favor, [echa un vistazo al usuario](%{user_url}). - - Esto puede ser modificado con los ajustes`newuser_spam_host_threshold` y`white_listed_spam_host_domains`. - unblocked: - title: "Desbloqueado" - subject_template: "Cuenta no retenida" - text_body_template: | - Hola, - - Este es un mensaje automático desde %{site_name} para informarte que tu cuenta no se encuentra retenida debido a la revisión de miembros del staff. - - Ahora puedes crear temas, y responder a otros usuarios. Gracias por tu paciencia. pending_users_reminder: title: "Recordatorio de usuarios pendientes de revisión" subject_template: diff --git a/config/locales/server.et.yml b/config/locales/server.et.yml index 60af56afc3..a7fa24e09c 100644 --- a/config/locales/server.et.yml +++ b/config/locales/server.et.yml @@ -43,7 +43,6 @@ et: auto_generated_email_error: "Juhtub, kui 'precedence' päis on seatud väärtusele: list, junk, bulk või auto_reply või kui mingi muu päis sisaldab väärtusi: auto-submitted, auto-replied või auto-generated." no_body_detected_error: "Juhtub, kui meil ei õnnestunud keha eraldada ja manused puudusid." inactive_user_error: "Juhtub, kui saatja ei ole aktiveeritud." - blocked_user_error: "Juhtub, kui saatja on blokeeritud." bad_destination_address: "Juhtub, kui ükski To/Cc/Bcc meiliaadresidest ei lange kokku seadistatud sissetulavate meiliaadressidega." strangers_not_allowed_error: "Juhtub, kui kasutaja proovib teha teemat liiki, millesse ta ise ei kuulu." insufficient_trust_level_error: "Juhtub, kui kasutaja proovib teha teemat liiki, millele tal puudub vajalik usaldustase." diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 64516e72f3..a63c001419 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -47,7 +47,6 @@ fa_IR: auto_generated_email_error: "زمانی که 'اولویت' سربرگ به: فهرست، شکسته، انبوه یا پاسخ خودکار اتفاق می‌افتد. یا وقتی سربرگ دیگری شامل: ثبت خودکار، پاسخ خودکار یا ایجاد خودکار باشد." no_body_detected_error: "وقتی که نمی‌توانیم متن را استخراج کنیم و ضمیمه‌ای وجود ندارد اتفاق می‌افتد." inactive_user_error: "هنگامی که ارسال کننده فعال نباشد." - blocked_user_error: "هنگامی که ارسال کننده مسدود شده باشد، رخ می‌دهد." bad_destination_address: "وقتی ایمیل‌‌های To/Cc/Bcc با یکی از ایمیل‌های پیکربندی شده ورودی هماهنگ نیستند، اتفاق می‌افتد" strangers_not_allowed_error: "هنگامی که که کاربر سعی در ایجاد موضوع در دسته‌بندی می کند که در آن عضو نیست، رخ می‌دهد." insufficient_trust_level_error: "وقتی اتفاق می‌افتد که کاربر قصد ایجاد موضوع در دسته‌بندی را دارد که سطح اعتمادش با آن دسته‌بندی هماهنگ نیست." @@ -892,19 +891,12 @@ fa_IR: tl2_additional_likes_per_day_multiplier: "کنترل بالا رفتن پسند‌ها در روز برای tl2 (اعضا) از طریق حاصل ضرب این شماره" tl3_additional_likes_per_day_multiplier: "کنترل بالا رفتن پسند ها در روز برای tl3 (معمولی) از طریق حاصل ضرب این شماره" tl4_additional_likes_per_day_multiplier: "کنترل بالا رفتن پسند ها در روز برای tl4 ( راهبران) از طریق حاصل ضرب این شماره" - num_spam_flags_to_block_new_user: "اگر نوشته های کاربران جدید num_flags_to_block_new_user از کاربران متفاوت پرچم های هرزنامه گرفت٬ نوشته های آن‌ها را مخفی کن و از نوشتن آنها در آینده جلوگیری کن. 0 برای غیر فعال." - num_users_to_block_new_user: "اگر نوشته های کاربران جدید num_flags_to_block_new_user از کاربران متفاوت پرچم های هرزنامه گرفت٬ نوشته های آنها را مخفی کن و از نوشتن آنها در آینده جلوگیری کن. 0 برای از کار انداختن. " - num_tl3_flags_to_block_new_user: "اگر کاربر جدید این تعداد پرچم هرزنامه از کاربران مختلف سطح اعتماد 3 num_tl3_users_to_block_new_user دریافت کرد تمام نوشته‌هایش را مخفی کن و اجازه ارسال نوشته جدید نده. 0 برای غیر فعال." - num_tl3_users_to_block_new_user: "اگر کاربر جدید این تعداد پرچم هرزنامه از کاربران مختلف سطح اعتماد 3 num_tl3_users_to_block_new_user دریافت کرد تمام نوشته‌هایش را مخفی کن و اجازه ارسال نوشته جدید نده. 0 برای غیر فعال." - notify_mods_when_user_blocked: "اگر کاربر به‌طور خودکار مسدود شد، به تمام مدیران پیام بفرست." flag_sockpuppets: "اگر کاربری به موضوع با ای پی برابر با کاربری که نوشته را شروع کرده ٬ آنها را به عنوان هرزنامه پرچم گزاری کن." traditional_markdown_linebreaks: "در مدل‌های نشانه گزاری از خط جدید سنتی استفاده کن،‌ که برای linebreak نیاز به دو فضای انتهایی دارد ." post_undo_action_window_mins: "تعداد دقایقی که کاربران اجازه دارند اقدامی را که در نوشته انجام داده اند باز گردانند. (پسند، پرچم گذاری،‌ چیزهای دیگر)." must_approve_users: "همکاران باید تمامی حساب‌های کاربری را قبل از اجازه دسترسی به سایت تایید کنند. اخطار: فعال‌سازی این گزینه ممکن است باعث جلوگیری از دسترسی کاربرانی که قبلا عضو شده‌اند نیز بشود!" pending_users_reminder_delay: "اگر کاربر‌ها بیشتر از این مقدار ساعت منتظر تایید بودند به مدیران اعلام کن. مقدار -1 برای غیرفعال‌سازی." maximum_session_age: "کاربر برای n ساعت از آخرین بازدید در حالت وارد شده می‌ماند." - ga_tracking_code: "منسوخ: کد پیگیری آمارگیر گوگل (ga.js) برای مثال: UA-12345678-9; این لینک را ببینید http://google.com/analytics" - ga_domain_name: "منسوخ: نام دامنه آمار‌گیر گوگل (ga.js) برای مثال: mysite.com; این لینک را ببینید http://google.com/analytics" ga_universal_tracking_code: "کد آمارگیر گوگل (analytics.js) UA-12345678-9؛ اینجا را ببینید http://google.com/analytics" ga_universal_domain_name: "کد آمارگیر گوگل (analytics.js) اسم دامنه٬‌ مثلا: mysite.com؛ اینجا را ببینید http://google.com/analytics" gtm_container_id: "مدیریت برچسب گوگل شامل شناسه است برای مثال: GTM-ABCDEF" @@ -1129,9 +1121,6 @@ fa_IR: num_hours_to_close_topic: "زمان توقف مورد نیاز برای مداخله در یک موضوع" auto_respond_to_flag_actions: "فعال کردن پاسخ خودکار برای زمانی که پرچم‌گذاری دفع می شود." min_first_post_typing_time: "حداقل زمان تایپ برای اولین نوشته کاربر به واحد میلی ثانیه، اگر این زمان رعایت نشود نوشته نیازمند تایید خواهد بود. 0 برای غیر فعال (توصیه نمی‌شود)" - auto_block_fast_typers_on_first_post: "کاربرانی که حداقل زمان نوشتن متن را رعایت نمی‌کنند به صورت خودکار مسدود کن" - auto_block_fast_typers_max_trust_level: "حداکثر سطح اعتماد برای مسدود کردن اسپمر‌ها" - auto_block_first_post_regex: "regex غیر حساس به اندازه حروف که اگر در نوشته اول کاربر باشد باعث جلوگیری از ارسال خواهد شد و نیاز به تایید توسط مدیران دارد. برای مثال raging|a[bc]a باعث می‌شود نوشته های شامل raging یا aba یا aca در ابتدا بسته شوند. فقط به نوشته اول اثر دارد." reply_by_email_enabled: "Enable replying to topics via email." reply_by_email_address: "نمونه برای پاسخ به ایمیل، آدرس‌های ایمیل آمده برای مثال: %{reply_key}@reply.example.com یا replies+%{reply_key}@example.com" alternative_reply_by_email_addresses: "لیست قالب‌های مشابه برای پاسخ به ایمیل‌های دریافتی. مثال:\n%{reply_key}@reply.example.com|replies+%{reply_key}@example.com" @@ -1709,13 +1698,6 @@ fa_IR: با عرض پوزش، ولی پیام ایمیلی شما به {destination} (titled %{former_title}) ارسال نشد. حساب کاربری شما که مربوط به این ایمیل است، فعال نشده. لطفا قبل از ارسال ایمیل حساب کاربری خود را فعال کنید. - email_reject_blocked_user: - title: "ایمیل رد شده کاربر مسدود" - subject_template: "[%{email_prefix}] مشکل ایمیل -- کاربر مسدود" - text_body_template: | - با عرض پوزش، ولی پیام ایمیلی شما به {destination} (titled %{former_title}) ارسال نشد. - - حساب کاربری مربوط به این ایمیل شما مسدود شده است. email_reject_reply_user_not_matching: title: "ایمیل رد شده کاربر تطابق ندارد" subject_template: "[%{email_prefix}] مشکل ایمیل -- آدرس پاسخ غیر منتظره" @@ -1799,64 +1781,9 @@ fa_IR: too_many_spam_flags: title: "پرچم هرز‌نامه بسیار زیاد است" subject_template: "حساب‌کاربری جدید در انتظار" - text_body_template: | - سلام، - این یک پیام خودکار از %{site_name} است که به شما درباره نوشته‌هایی که به دلیل دریافت پرچم از انجمن مخفی شده‌اند اطلاع می‌دهد. - - به عنوان یک اقدام حیاتی، حساب کاربری شما از ایجاد نوشته‌ها و موضوعات جدید تا زمانی که یکی از همکاران حساب کاربریتان را بررسی نماید، مسدود خواهد بود. برای ناراحتی پیش آمده عذر خواهی می‌کنیم. - - برای راهنمایی بیشتر لطفا [راهنمای انجمن](%{base_url}/guidelines) را ببینید. too_many_tl3_flags: title: "تعداد پرچم‌های TL3 بالاست" subject_template: "حساب‌کاربری جدید در انتظار" - text_body_template: | - سلام، - - این یک پیام خودکار از %{site_name} جهت اطلاع از در انتظار نگه داشتن حساب‌کاربری شما به دلیل دریافت پرچم‌های زیاد است. - - به عنوان یک اقدام حیاتی، حساب‌کاربری شما تا زمانی که همکاران آن را بازبینی نکنند اجازه ایجاد پاسخ یا موضوع جدید را نخواهد داشت. از اینکه به زحمت افتادید پوزش میخواهیم. - - برای راهنمایی بیشتر، [راهنمای انجمن](%{base_url}/guidelines) را ببینید. - blocked_by_staff: - title: "مسدود شده توسط همکاران" - subject_template: "حساب کاربری به صورت موقت در انتظار است" - text_body_template: | - سلام، - - این یک پیام خودکار از %{site_name} جهت اطلاع از در انتظار نگه داشتن حساب‌کاربری شما به عنوان یک اقدام حیاتی است. - - لطفا به مرور انجمن ادامه دهید، ولی تا زمانی که [همکاران](%{base_url}/about) نوشته‌های اخیر شما را بررسی نکنند، نمی‌توانید پاسخ یا موضوع جدید ایجاد کنید. از اینکه به زحمت افتادید پوزش میخواهیم. - - برای راهنمایی بیشتر، [راهنمای انجمن](%{base_url}/guidelines) را ببینید. - user_automatically_blocked: - title: "کاربر به صورت خودکار مسدود شده" - subject_template: "کاربر جدید %{username} توسط پرچم‌های اعضا مسدود شده" - text_body_template: | - این یک پیام خودکار است. - - کاربر جدید [%{username}](%{user_url}) به دلیل پرچم گذاری چندکاربره نوشته‌(ها)ی %{username} به صورت خودکار مسدود شد. - - لطفا [پرچم‌ها را بازبینی کنید](%{base_url}/admin/flags). اگر %{username} به درستی مسدود نشده، روی گزینه خروج از مسدودیت در [صفحه مدیریت این کاربر](%{user_url}) کلیک کنید. - - آستانه می‌توانید از طریق تنظیمات `block_new_user` تغییر کند. - spam_post_blocked: - title: "نوشته هرزنامه مسدود شد" - subject_template: "کاربر جدید%{username} به‌دلیل پیوند‌های تکراری نوشته مسدود شده است" - text_body_template: | - این یک پیام خودکار است. - - کاربر جدید [%{username}](%{user_url}) تلاش کرده چند نوشته با لینک به %{domains} ایجاد کند، ولی لینک‌ها برای جلوگیری از هرزنامه شدن مسدود شدند. کاربر همچنان می‌تواند نوشته‌های جدیدی ایجاد کند که در آن لینک %{domains} وجود ندارد. - - لطفا [کاربر را بازبینی کنید](%{user_url}). - unblocked: - title: "رفع مسدودیت شد" - subject_template: "حساب کاربری در انتظار نیست" - text_body_template: | - سلام، - - این یک پیام خودکار از %{site_name} است که شما را متوجه آن کند که حساب کاربریتان از حالت انتظار و تایید همکاران خارج شده. - - شما می‌توانید دوباره پاسخ‌ها و موضوعات جدید ایجاد کنید. با تشکر از صبر شما. pending_users_reminder: title: "یاداور کاربران در انتظار" subject_template: diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 533f20f2d5..9da2b3f8b6 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -48,7 +48,6 @@ fi: no_body_detected_error: "Näin käy, kun leipätekstin poiminta epäonnistuu eikä liitteitä ole." no_sender_detected_error: "Näin käy, kun emme tunnista käypää sähköpostiosoitetta viestin From-otsikkotiedosta." inactive_user_error: "Näin käy, kun lähettäjä ei ole aktiivinen." - blocked_user_error: "Näin käy, kun lähettäjä on estetty." bad_destination_address: "Näin käy, kun viestin vastaanottaja/kopio/piilokopio -kenttien osoitteet eivät täsmää asetettuihin saapuvan sähköpostin osoitteiden kanssa." strangers_not_allowed_error: "Näin käy, kun käyttäjä yrittää luoda ketjun alueelle, jonka jäsen ei ole." insufficient_trust_level_error: "Näin käy, kun käyttäjä yrittää luoda ketjun alueelle, jonka vähimmäisluottamustasovaatimusta ei täytä." @@ -950,11 +949,6 @@ fi: tl2_additional_likes_per_day_multiplier: "Nosta tykkäysten päivittäistä rajaa tasolla lt2 (konkari) kertomalla tällä luvulla" tl3_additional_likes_per_day_multiplier: "Nosta tykkäysten päivittäistä rajaa tasolla lt3 (mestari) kertomalla tällä luvulla" tl4_additional_likes_per_day_multiplier: "Nosta tykkäysten päivittäistä rajaa tasolla lt4 (johtaja) kertomalla tällä luvulla" - num_spam_flags_to_block_new_user: "Jos uuden käyttäjän viestit saavat näin monta roskapostiliputusta num_users_to_block_new_user eri käyttäjältä, piilota kaikki viestit ja jäädytä kirjoitusoikeudet. Aseta 0 poistaaksesi käytöstä." - num_users_to_block_new_user: "Jos uuden käyttäjän viestit saavat num_spam_flags_to_block_new_user roskapostiliputusta näin monelta eri käyttäjältä, piilota kaikki viestit ja jäädytä kirjoitusoikeudet. Aseta 0 poistaaksesi käytöstä." - num_tl3_flags_to_block_new_user: "Jos uuden käyttäjän viesti saa näin monta lippua num_tl3_users_to_block_new_user eri lt3 käyttäjältä, piilota kaikki hänen viestinsä ja estä uusien viestien kirjoittaminen. 0 poistaa käytöstä." - num_tl3_users_to_block_new_user: "Jos uuden käyttäjän viesti saa num_tl3_flags_to_block_new_user lippua näin monelta lt3 käyttäjältä, piilota kaikki hänen viestinsä ja estä uusien viestien kirjoittaminen. 0 poistaa käytöstä." - notify_mods_when_user_blocked: "Jos käyttäjä estetään automaattisesti, lähetä viesti kaikille valvojille." flag_sockpuppets: "Jos uuden käyttäjän luomaan ketjuun vastaa toinen uusi käyttäjä samasta IP-osoitteesta, liputa molemmat viestit mahdolliseksi roskapostiksi." traditional_markdown_linebreaks: "Käytä perinteisiä rivinvaihtoja Markdownissa, joka vaatii kaksi perättäistä välilyöntiä rivin vaihtoon." enable_markdown_typographer: "Käytetään tavanomaisia typografisia sääntöjä parantamaan tekstikappaleiden luettavuutta, (c), (tm) ym. korvataan symboleilla, kysymysmerkkien määrää vähennetään jne." @@ -962,8 +956,6 @@ fi: must_approve_users: "Henkilökunnan täytyy hyväksyä kaikki uudet tilit, ennen uusien käyttäjien päästämistä sivustolle. VAROITUS: tämän asetuksen valitseminen poistaa pääsyn kaikilta jo olemassa olevilta henkilökuntaan kuulumattomilta käyttäjiltä." pending_users_reminder_delay: "Ilmoita valvojille, jos uusi käyttäjä on odottanut hyväksyntää kauemmin kuin näin monta tuntia. Aseta -1, jos haluat kytkeä ilmoitukset pois päältä." maximum_session_age: "Käyttäjä pysyy sisäänkirjautuneena n tuntia vierailunsa jälkeen" - ga_tracking_code: "VANHENTUNUT: Google Analytics (ga.js) -seurantatunnus, esim. UA-12345678-9; ks. http://google.com/analytics" - ga_domain_name: "VANHENTUNUT: Google Analytics (ga.js) -verkkotunnus, esim. mysite.com; ks. http://google.com/analytics" ga_universal_tracking_code: "Google Universal analytics (analytics.js) seurantakoodi, esim.: UA-12345678-9; katso http://google.com/analytics" ga_universal_domain_name: "Google Universal analytics (analytics.js) verkkotunnus, esim.: osoite.fi; katso http://google.com/analytics" gtm_container_id: "Google Tag Manager -säiliön ID. Esim: GTM-ABCDEF" @@ -1191,9 +1183,6 @@ fi: num_hours_to_close_topic: "Kuinka moneksi tunniksi ketju menee tauolle puuttumistoimia odottamaan." auto_respond_to_flag_actions: "Ota käyttöön automaattinen vastaus lippua poistettaessa." min_first_post_typing_time: "Minimimäärä aikaa millisekunneissa, joka käyttäjän täytyy kirjoittaa ensimmäistä viestiään. Jos rajaa ei saavuteta, viesti lisätään automaattisesti hyväksyttävien jonoon. Aseta 0 ottaaksesi pois käytöstä (ei suositella)." - auto_block_fast_typers_on_first_post: "Estä automaattisesti käyttäjät, jotka eivät saavuta min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Suurin luottamustaso, jolla automaattisesti estetään nopeat kirjoittajat" - auto_block_first_post_regex: "Merkkikokoriippumaton regex joka aiheuttaa käyttäjän ensimmäisen viestin estämisen ja lisäämisen hyväksymisjonoon. Esimerkiksi raivoaminen|a[bc]a aiheuttaa kaikkien viestien, joissa on sanat raivoava, aba tai aca estämisen. Vaikuttaa vain ensimmäiseen viestiin." reply_by_email_enabled: "Ota käyttöön vastaukset sähköpostin avulla." reply_by_email_address: "Saapuvien sähköpostivastausten sähköpostiosoitekaava, esimerkiksi: %{reply_key}@reply.example.com tai replies+%{reply_key}@example.com" alternative_reply_by_email_addresses: "Lista vaihtoehtoisista saapuvien sähköpostivastausten sähköpostiosoitekaavoista, esimerkiksi: %{reply_key}@reply.example.com tai replies+%{reply_key}@example.com" @@ -1903,13 +1892,6 @@ fi: Pahoittelemme, mutta sähköpostin lähettäminen tänne %{destination} (otsikolla %{former_title}) ei onnistunut. Käyttäjätiliä tällä sähköpostiosoitteella ei ole aktivoitu. Aktivoi käyttäjätili ennen sähköpostien lähettämistä. - email_reject_blocked_user: - title: "Sähköposti hylätty - estetty käyttäjä" - subject_template: "[%{email_prefix}] Sähköpostiongelma-- Estetty käyttäjä" - text_body_template: | - Pahoittelemme, mutta sähköpostiviesetisi tänne %{destination} (otsikolla %{former_title}) ei onnistunut. - - Käyttäjätili tällä sähköpostiosoitteella on estetty. email_reject_reply_user_not_matching: title: "Sähköposti hylätty - käyttäjä ei täsmää" subject_template: "[%{email_prefix}] Sähköpostiongelma -- Vastaus odottamattomasta osoitteesta" @@ -2030,67 +2012,9 @@ fi: too_many_spam_flags: title: "Liian monta roskapostiliputusta" subject_template: "Uusi tili on estetty" - text_body_template: | - Hei, - - Tämä automaattinen viesti on lähetetty sivustolta %{site_name} kertoaksemme, että viestejäsi on väliakaisesti piilotettu yhteisön liputusten perusteella. - - Varotoimena uudelta tililtäsi ei voi vastata tai luoda uusia ketjuja ennen kuin henkilökunnan jäsen tarkastaa sen. Pahoittelemme tästä aiheutuvaa vaivaa. - - Saadaksesi lisätietoa, tutustu [yhteisön ohjeisiin](%{base_url}/guidelines). too_many_tl3_flags: title: "Liian monta lt3-lippua" subject_template: "Uusi tili on estetty" - text_body_template: | - Hei, - - Tämä automaattinen viesti on lähetetty sivustolta %{site_name} kertoaksemme, että tilisi on jäädytetty yhteisön useiden liputusten perusteella. - - Varotoimena uudelta tililtäsi ei voi vastata tai luoda uusia ketjuja ennen kuin henkilökunnan jäsen tarkastaa sen. Pahoittelemme tästä aiheutuvaa vaivaa. - - Saadaksesi lisätietoa, tutustu [yhteisön ohjeisiin](%{base_url}/guidelines). - blocked_by_staff: - title: "Henkilökunta esti" - subject_template: "Tili on väliaikaisesti estetty" - text_body_template: | - Hei, - - Tämä automaattinen viesti on lähetetty sivustolta %{site_name} kertoaksemme, että tilisi on jäädytetty väliaikaisesti varotoimenpiteenä. - - Voit jatkaa selailua, muttet voi vastata tai luoda uusia ketjuja ennen kuin [henkilökunnan jäsen](%{base_url}/about) tarkastaa viimeisimmät viestisi. Pahoittelemme tästä aiheutuvaa vaivaa. - - Saadaksesi lisätietoa, tutustu [yhteisön ohjeisiin](%{base_url}/guidelines). - user_automatically_blocked: - title: "Käyttäjä estetty automaattisesti" - subject_template: "Uusi käyttäjä %{username} on estetty yhteisön liputusten perusteella" - text_body_template: | - Tämä on automaattinen viesti. - - Uusi käyttäjätili [%{username}](%{user_url}) estettiin, koska monet liputtivat käyttäjän %{username} viestin tai viestejä. - - [Tarkastele liputettuja viestejä](%{base_url}/admin/flags). Jos käyttäjä %{username} estettiin aiheettomasti, klikkaa poista esto -painiketta [tähän käyttäjään liittyvällä ylläpitäjän sivulla](%{user_url}). - - Kynnysarvoa voi muuttaa säätämällä asetuksen `block_new_user` arvoa sivuston asetuksissa. - spam_post_blocked: - title: "Roskapostiviesti estetty" - subject_template: "Uuden käyttäjän %{username} viestit on estetty toistuvien linkkien vuoksi" - text_body_template: | - Tämä on automaattisesti luotu viesti. - - Uusi käyttäjä [%{username}](%{user_url}) yritti luoda useita viestejä, jotka sisälsivät linkkejä osoitteeseen %{domains}, mutta jotka estettiin roskapostin välttämiseksi. Käyttäjä voi edelleen luoda viesteijä, joissa ei ole linkkejä näihin osoitteisiin %{domains}. - - [Tarkasta käyttäjätili](%{user_url}). - - Tätä toimintoa voi muokata vaihtamalla asetuksia `newuser_spam_host_threshold` `white_listed_spam_host_domains` sivuston asetuksissa. - unblocked: - title: "Ei enää estetty" - subject_template: "Tili avattu" - text_body_template: | - Hei, - - Tämä automaattinen viesti on lähetetty sivustolta %{site_name} kertoaksemme, että tilisi on avattu henkilökunnan tarkastettua tilanteen. - - Voit jälleen luoda uusia viestejä ja ketjuja. Kiitos kärsivällisyydestänne. pending_users_reminder: title: "Käyttäjiä jonossa" subject_template: @@ -2115,9 +2039,9 @@ fi: title: "Olet kuukauden tulokas!" subject_template: "Olet kuukauden tulokas!" text_body_template: | - Onneksi olkoon, olet ansainnut Kuukauden tulokas -palkinnon %{month_year}** :trophy: + Onneksi olkoon, olet ansainnut **Kuukauden tulokas -palkinnon %{month_year}** :trophy: - Palkinto myönnetään vain kahdelle käyttäjälle joka kuukausi, ja se näkyy pysyvästi [käyttäjäsivullasi](%{base_url}/my/badges). + Palkinto myönnetään vain kahdelle käyttäjälle joka kuukausi, ja se näkyy pysyvästi [käyttäjäsivullasi](%{url}). Sinusta on äkkiä tullut tärkeä osa yhteisöä. Kiitos kun liityit, ja jatka samaa rataa! queued_posts_reminder: @@ -2360,8 +2284,9 @@ fi: popular_posts: "Suosittuja viestejä" more_new: "Uutta sinulle" subject_template: "[%{email_prefix}] Kooste" - unsubscribe: "Tämä kooste lähetettiin sivustolta %{site_link}, koska sinua ei ole näkynyt vähään aikaan. Voit perua tilauksen muuttamalla sähköpostiasetuksiasi tai %{unsubscribe_link}." - click_here: "klikkaa tästä" + unsubscribe: "Tämä kooste lähetettiin sivustolta %{site_link}, koska sinua ei ole näkynyt vähään aikaan. Voit perua tilauksen muuttamalla %{email_preferences_link} tai %{unsubscribe_link} peruaksesi tilauksen." + your_email_settings: "sähköpostiasetuksiasi" + click_here: "klikkaa tätä" from: "%{site_name} kooste" preheader: "Lyhyt yhteenveto tapahtumista viime vierailusi (%{last_seen_at}) jälkeen" forgot_password: @@ -2461,6 +2386,9 @@ fi: see_more: "Lisää" search_title: "Etsi tältä sivustolta" search_google: "Google" + offline: + title: "Sovellusta ei voida ladata" + offline_page_message: "Yhteydessäsi vaikuttaa olevan vikaa! Tarkista verkkoyhteytesi ja yritä uudelleen." login_required: welcome_message: | ## [Tervetuloa sivustolle %{title}](#welcome) @@ -2742,9 +2670,9 @@ fi: Tämä ansiomerkki myönnetään, kun viestistäsi tykätään ensi kertaa. Onnittelut, olet tuottanut jotakin, mitä toinen käyttäjä on pitänyt mielenkiintoisena, hauskana tai hyödyllisenä! autobiographer: name: Omaelämäkerta - description: Täytti käyttäjätiedot + description: Täytti käyttäjätiedot long_description: | - Tämä ansiomerkki myönnetään , kun täytät käyttäjätiedot ja valitset profiilikuvan. Kertomalla hieman itsestäsi ja kiinnostuksen kohteistasi edistät yhteisöllisyyttä. Liity meihin! + Tämä ansiomerkki myönnetään, kun täytät käyttäjätiedot ja valitset profiilikuvan. Kertomalla hieman itsestäsi ja kiinnostuksen kohteistasi edistät yhteisöllisyyttä. Liity meihin! anniversary: name: Vuosipäivä description: Aktiivinen jäsen vuoden ajan, kirjoittanut ainakin yhden viestin @@ -2839,9 +2767,9 @@ fi: Tämä ansiomerkki myönnetään, kun ensi kerran lainaat viestiä vastauksessasi. Edellisten viestien olennaisten osien lainaaminen auttaa pitämään keskustelun yhtenäisenä ja aiheen mukaisena. Helpoin tapa on maalata viestin osa ja sitten painaa vastaa-painiketta. Lainaa runsaasti! read_guidelines: name: Luki ohjeet - description: Luki palstan säännöt + description: Luki yhteisön säännöt long_description: | - Tämä ansiomerkki myönnetään, kun luet yhteisön ohjeet . Noudattamalla ja jakamalla näitä yksinkertaisia ohjeita edistät yhteisön turvallisuutta, viihtyisyyttä ja jatkuvuutta. Muista aina, että toisessa päässä on toinen ihminen, hyvin itsesi kaltainen. Ole kohtelias! + Tämä ansiomerkki myönnetään, kun luet yhteisön säännöt. Noudattamalla ja jakamalla näitä yksinkertaisia ohjeita edistät yhteisön turvallisuutta, viihtyisyyttä ja jatkuvuutta. Muista aina, että toisessa päässä on toinen ihminen, hyvin itsesi kaltainen. Ole kohtelias! reader: name: Lukutoukka description: Luki jokaisen viestin ketjusta, jossa on enemmän kuin 100 vastausta diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index f49326be4a..c06f2c23d9 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -48,7 +48,6 @@ fr: no_body_detected_error: "Se produit quand il est impossible d'extraire le corps du message et qu'il n'y a pas de pièces-jointes." no_sender_detected_error: "Se produit lorsque nous n'avons pas trouvé une adresse courriel valide dans l'entête From." inactive_user_error: "Se produit quand l'expéditeur n'est pas actif." - blocked_user_error: "Se produit quand l'expéditeur a été bloqué." bad_destination_address: "Se produit quand aucune des adresses de courriel des champs To/Cc/Bcc ne correspondent à une adresse de courriel entrante configurée." strangers_not_allowed_error: "Se produit quand un utilisateur a essayé de créer un nouveau sujet dans une catégorie dans laquelle il n'est pas membre." insufficient_trust_level_error: "Se produit quand un utilisateur a essayé de créer un nouveau sujet dans une catégorie pour laquelle il n'a pas le niveau de confiance nécessaire." @@ -943,19 +942,12 @@ fr: tl2_additional_likes_per_day_multiplier: "Augmenter la limite de J'aime par jour pour les utilisateurs de niveau 2 (membres) en la multipliant par ce nombre" tl3_additional_likes_per_day_multiplier: "Augmenter la limite de J'aime par jour pour les utilisateurs de niveau 3 (réguliers) en la multipliant par ce nombre" tl4_additional_likes_per_day_multiplier: "Augmenter la limite de J'aime par jour pour les utilisateurs de niveau 4 (meneurs) en la multipliant par ce nombre" - num_spam_flags_to_block_new_user: "Si les messages d'un nouvel utilisateur obtiennent ce nombre de signalements pour spam de la part de num_users_to_block_new_user utilisateurs différents, masquer tous ses messages et l'empêcher de poster à l'avenir. 0 désactive cette fonctionnalité." - num_users_to_block_new_user: "Si les messages d'un nouvel utilisateur obtiennent num_spam_flags_to_block_new_user signalements d'autant d'utilisateurs différents, masquer tous ses messages et l'empêcher de poster à l'avenir. 0 désactive cette fonctionnalité." - num_tl3_flags_to_block_new_user: "Si les messages d'un nouvel utilisateur obtiennent\nce nombre de signalements de la part de num_tl3_users_to_block_new_user utilisateurs de niveau de confiance 3 différents, masquer tous ses messages et l'empêcher de poster à l'avenir. 0 désactive cette fonctionnalité." - num_tl3_users_to_block_new_user: "Si les messages d'un nouvel utilisateur obtiennent num_tl3_flags_to_block_new_user signalements d'autant d'utilisateurs de niveau de confiance 3 différents, masquer tous ses messages et l'empêcher de poster à l'avenir. 0 désactive cette fonctionnalité." - notify_mods_when_user_blocked: "Si un utilisateur est bloqué automatiquement, envoyer un message à tous les modérateurs." flag_sockpuppets: "Si un nouvel utilisateur répond à un sujet avec la même adresse IP que le nouvel utilisateur qui a commencé le sujet, alors leurs messages seront automatiquement marqués comme spam." traditional_markdown_linebreaks: "Utiliser le retour à la ligne traditionnel dans Markdown, qui nécessite deux espaces pour un saut de ligne." post_undo_action_window_mins: "Nombre de minutes pendant lesquelles un utilisateur peut annuler une action sur un message (J'aime, signaler, etc.)" must_approve_users: "Les responsables doivent approuver les nouveaux utilisateurs afin qu'ils puissent accéder au site. ATTENTION : activer cette option sur un site en production suspendra l'accès des utilisateurs existants qui ne sont pas des responsables !" pending_users_reminder_delay: "Avertir les modérateurs si des nouveaux utilisateurs sont en attente d'approbation depuis x heures. Mettre -1 pour désactiver les notifications." maximum_session_age: "L'utilisateur restera connecté pour n heures après la dernière visite" - ga_tracking_code: "Obsolète : code de suivi Google Analytics (ga.js), par exemple : UA-12345678-9 ; voir http://google.com/analytics" - ga_domain_name: "Obsolète : nom de domaine Google Analytics (ga.js), par exemple : monsite.com ; voir http://google.com/analytics" ga_universal_tracking_code: "Code de suivi Google Universal Analytics (analytics.js), par exemple : UA-12345678-9 ; voir http://google.com/analytics" ga_universal_domain_name: "Nom de domaine Google Universal Analytics (analytics.js), par exemple : monsite.com ; voir http://google.com/analytics" gtm_container_id: "ID du conteneur Google Tag Manager, par exemple : GTM-ABCDEF" @@ -1181,9 +1173,6 @@ fr: num_hours_to_close_topic: "Nombre d'heures de fermeture d'un sujet pour intervention." auto_respond_to_flag_actions: "Activer la réponse automatique lors du traitement d'un signalement." min_first_post_typing_time: "Minimum de temps en millisecondes qu'un utilisateur doit passer à la saisie de son premier commentaire, si le seuil n'est pas atteint, il rejoindra automatiquement la file des commentaires en cours d'approbation. Mettre à 0 pour désactiver (non recommandé)" - auto_block_fast_typers_on_first_post: "Bloque automatiquement les utilisateurs qui n'ont pas respecté min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Niveau de confiance maximum pour bloquer automatiquement les 'dactylo rapides'" - auto_block_first_post_regex: "Regex non sensible à la casse qui, si elle est déclenchée, bloquera le premier message de l'utilisateur et l'enverra dans la file d'attente d'approbation.\nExemple: rageux|a[bc]a bloquera les premiers messages contenant rageux ou aba ou aca." reply_by_email_enabled: "Activer les réponses aux sujets via courriel." reply_by_email_address: "Modèle pour la réponse par courriel entrant; exemple : %{reply_key}@reply.example.com ou replies+%{reply_key}@example.com" alternative_reply_by_email_addresses: "Liste des templates alternatifs pour les adresses des courriels entrants de la réponse par courriel. Exemple : %{reply_key}@reply.example.com|replies+%{reply_key}@example.com" @@ -1819,12 +1808,6 @@ fr: Malheureusement votre courriel à %{destination} (titled %{former_title}) a échoué. Le compte associé avec cette adresse existe n'est pas activé. Veuillez activer votre compte avant d'envoyer des courriels. - email_reject_blocked_user: - subject_template: "[%{email_prefix}] Problème de courriel -- Utilisateur bloqué" - text_body_template: | - Malheureusement votre courriel à %{destination} (titled %{former_title}) a échoué. - - Le compte associé avec cette adresse a été bloqué. email_reject_reply_user_not_matching: subject_template: "[%{email_prefix}] Problème de courriel -- Adresse de réponse inattendue" email_reject_no_account: @@ -1890,66 +1873,8 @@ fr: too_many_spam_flags: title: "Trop de signalements de spam" subject_template: "Nouveau compte bloqué" - text_body_template: | - Bonjour, - - Ceci est un message automatique de %{site_name} pour vous informer que vos messages ont été temporairement masqués suite à des signalements de la communauté. - - Par mesure de précaution, votre nouveau compte a été bloqué et vous ne pourrez plus créer de nouvelles réponses et sujets tant qu'un responsable n'a pas vérifié votre compte. Veuillez nous excuser pour la gêne occasionnée. - - Pour plus d'informations, merci de vous référer à la [charte de la communauté](%{base_url}/guidelines). too_many_tl3_flags: subject_template: "Nouveau compte bloqué" - text_body_template: | - Bonjour, - - Ceci est un message automatique de %{site_name} pour vous informer que votre compte a été bloqué suite à un grand nombre de signalements de la communauté. - - Par mesure de précaution, votre nouveau compte a été bloqué et vous ne pourrez par créer de nouvelles réponses ou sujets tant qu'un responsable n'a pas vérifié votre compte. - - Pour plus d'informations, merci de vous en référer à la [charte de la communauté](%{base_url}/guidelines). - blocked_by_staff: - title: "Bloqué par un responsable" - subject_template: "Compte bloqué temporairement" - text_body_template: | - Bonjour, - - Ceci est un message automatique de %{site_name} pour vous informer que votre compte a été temporairement bloqué par mesure de précaution. - - Vous pouvez continuer à naviguer mais vous ne pourrez plus répondre ou créer de sujets tant qu'un [responsable](%{base_url}/about) n'a pas vérifié vos messages les plus récents. Veuillez nous excuser pour la gêne occasionnée. - - Pour plus d'informations, merci de vous référer à la [charte de la communauté](%{base_url}/guidelines). - user_automatically_blocked: - title: "Utilisateur bloqué automatiquement" - subject_template: "Nouvel utilisateur %{username} bloqué suite à des signalements de la communauté" - text_body_template: | - Ceci est un message automatique - - Le nouvel utilisateur [%{username}](%{user_url}) a été bloqué automatiquement par plusieurs utilisateurs ayant signalés ses (%{username}) messages. - - Merci de [vérifier les signalements](%{base_url}/admin/flags). Si %{username} a été bloqué injustement, cliquez sur le bouton débloquer sur la [page d'administration de cet utilisateur](%{user_url}). - - Cette limite peut être changée par le paramètre du site `block_new_user`. - spam_post_blocked: - title: "Message spam bloqué" - subject_template: "Les messages du nouvel utilisateur %{username} sont bloqués pour des liens répétés" - text_body_template: | - Ceci est un message automatique. - - Le nouvel utilisateur [%{username}](%{user_url}) tente de créer de multiples messages avec des liens vers %{domains}, mais que ces messages ont été bloqués pour éviter le spam. Cet utilisateur est toujours capable de créer des messages qui ne contiennent pas de liens vers %{domains}. - - Merci de [review the user](%{user_url}). - - Ceci peut être modifier via les options `newuser_spam_host_threshold` et white_listed_spam_host_domains`. - unblocked: - title: "Débloqué" - subject_template: "Compte débloqué" - text_body_template: | - Bonjour, - - Ceci est un message automatique de %{site_name} pour vous informer que votre compte a été débloqué après vérification d'un responsable. - - Vous pouvez à nouveau créer des nouveaux messages et sujets. Merci pour votre patience. pending_users_reminder: title: "Rappel d'utilisateurs en attente" subject_template: diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 6fbc8674b4..33432a2566 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -48,7 +48,6 @@ he: no_body_detected_error: "קורה כאשר לא הצלחנו לחלץ גוף ולא היו קבצים מצורפים." no_sender_detected_error: "קורה כשאנחנו לא מצליחים כתובת מייל תקינה בכותרת ״From״." inactive_user_error: "קורה כאשר השולח אינו פעיל." - blocked_user_error: "קורה כאשר השולח נחסם." bad_destination_address: "קורה כאשר אף אחת מהכתובות ב To/CC/Bcc לא מתאימה לאף כתובת מייל נכנסת." strangers_not_allowed_error: "מתרחש כאשר משתמשים מנסים ליצור נושא חדש בקטגוריה בה הם אינם חברים." insufficient_trust_level_error: "מתרחש כאשר משתמשים מנסים ליצור נושא חדש בקטגוריה בה אין להם את רמת האמון/הרשאה הנדרשת." @@ -932,19 +931,12 @@ he: tl2_additional_likes_per_day_multiplier: "להגדיל את כמות הלייקים האפשרית ביום עבור tl2 (משתמש) באמצעות הכפלה במספר זה. " tl3_additional_likes_per_day_multiplier: "להגדיל את כמות הלייקים האפשרית ביום עבור רמת-אמון 3 (רגיל) באמצעות הכפלה במספר זה." tl4_additional_likes_per_day_multiplier: "להגדיל את כמות הלייקים האפשרית ביום עבור tl4 (מנהיג) באמצעות הכפלה במספר זה. " - num_spam_flags_to_block_new_user: "אם פוסטים של משתמשים חדשים מקבלים כמות זו של דגלי ספאם מ num_users_to_block_new_user משתמשים אחרים, הסתירו את הפוסטים שלהם ומנעו מהם פרסומים בעתיד. 0 לניטרול." - num_users_to_block_new_user: "אם פוסטים של משתמשים חדשים מקבלים num_spam_flags_to_block_new_user דגלי ספאם מכמות זו של משתמשים שונים, הסתירו את הפוסטים שלהם ומנעו פרסומים עתידיים. 0 לניטרול." - num_tl3_flags_to_block_new_user: "אם פוסטים של משתמשים חדשים מקבלים כמות זו של דגלים מ num_tl3_users_to_block_new_user משתמשים שונים ברמת אמון 3, הסתירו את כל הפוסטים שלהם ומנעו פרסומים עתידיים. 0 לניטרול." - num_tl3_users_to_block_new_user: "אם פוסטים של משתמשים חדשים מקבלים num_tl3_flags_to_block_new_user דגלים מכמות זו של משתמשים ברמת אמון 3, הסתירו את הפוסטים שלהם ומנעו פרסומים עתידיים. 0 לניטרול." - notify_mods_when_user_blocked: "אם משתמש נחסם אוטומטית, שילחו הודעה לכל המנחים." flag_sockpuppets: "אם משתמשים חדשים מגיבים לנושא מכתובת IP זהה לזו של מי שהחל את הנושא, סמנו את הפוסטים של שניהם כספאם פוטנציאלי." traditional_markdown_linebreaks: "שימוש בשבירת שורות מסורתית בסימון, מה שדורש שני רווחים עוקבים למעבר שורה." post_undo_action_window_mins: "מספר הדקות בהן מתאפשר למשתמשים לבטל פעולות אחרות בפוסט (לייק, סימון, וכו')." must_approve_users: "על הצוות לאשר את כל המשתמשים החדשים לפני שהם מקבלים גישה לאתר. אזהרה: בחירה זו עבור אתר קיים תשלול גישה ממשתמשים קיימים שאינם מנהלים." pending_users_reminder_delay: "הודיעו למנחים אם משתמשים חדשים ממתינים לאישור למעלה מכמות זו של שעות. קבעו ל -1 כדי לנטרל התראות." maximum_session_age: "משתמשים ישארו מחוברים ל n שעות מאז ביקורם האחרון" - ga_tracking_code: "מיושן: קוד גוגל אנליטיקס (ga.js) למעקב, כגון: UA-12345678-9ֿ; ראו http://google.com/analytics" - ga_domain_name: "מיושן: שם מתחם (דומיין) לגוגל אנליטיקס (ga.js), למשל: mysite.com; ראו http://google.com/analytics" ga_universal_tracking_code: "קוד מעקב של (Google Universal Analytics (analytics.js, למשל: UA-12345678-9; ראו http://google.com/analytics" ga_universal_domain_name: "שם המתחם של (Google Universal Analytics analytics.js), למשל: mysite.com; ראו http://google.com/analytics" gtm_container_id: "מזהה קונטיינר של מנהל תגיות גוגל. למשל: GTM-ABCDEF" @@ -1170,9 +1162,6 @@ he: num_hours_to_close_topic: "מספר שעות לעצירת נושא לצורך התערבות." auto_respond_to_flag_actions: "אפשרו תגובה אוטמטית עם הסרת דגל." min_first_post_typing_time: "זמן מינימלי במילי-שניות שמשתמש חייב להקיש בזמן הפוסט הראשון, אם הסף לא נעבר הפוסט אוטומטית יכנס לתור של אלו שצריכים אישור. קיבעו 0 כדי לנטרל (לא מומלץ)" - auto_block_fast_typers_on_first_post: "חסימה אוטומטית של משתמשים שלא עונים על min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "רמת אמון מקסימלית שבה לחסום אוטומטית מקלידים מהירים" - auto_block_first_post_regex: "ביטוי רגולרי case insensitive שאם הוא מתקיים יגרום לפוסט הראשון מהמשתמש להחסם ולהשלח לתור האישורים. למשל raging|a[bc]a , יגרום לכל הפוסטים שמכילים raging או aba או aca קודם כל להחסם. רלוונטי רק לפוסט הראשון." reply_by_email_enabled: "אפשרו תגובה לנושאים באמצעות הדוא\"ל." reply_by_email_address: "תבנית עבור כתובות מייל של מענה באמצעות אימייל, למשל: {reply_key}@reply.example.com או replies+%{reply_key}@example.com" alternative_reply_by_email_addresses: "רשימה של כמה תבניות לתגובות במייל באמצעות כתובות מייל נכנסות. למשל: %{reply_key}@reply.example.com|replies+%{reply_key}@example.com" @@ -1796,13 +1785,6 @@ he: אנחנו מצטערים, אך הודעת המייל שלכם ל %{destination} (עם הכותרת %{former_title}) לא עבדה. החשבון שלכם שמקושר לכתובת המייל הזו לא הופעל. אנא הפעילו את החשבון שלכם לפני שאתם שולחים מיילים למערכת. - email_reject_blocked_user: - title: "מייל נדחה, משתמש חסום" - subject_template: "[%{email_prefix}] בעיית מייל -- משתמש חסום" - text_body_template: | - אנחנו מצטערים, אך הודעת המייל שלכם ל %{destination} (עם הכותרת %{former_title}) לא עבדה. - - חשבונכם שקשור לכתובת מייל זו נחסם. email_reject_reply_user_not_matching: title: "דחיית מייל משתמש/ת לא מתאימים" subject_template: "[%{email_prefix}] בעיית מייל -- נמען לא תואם" @@ -1885,67 +1867,9 @@ he: too_many_spam_flags: title: "יותר מידי דיגלי ספאם" subject_template: "חשבון חדש בהשהיה" - text_body_template: | - שלום, - - זוהי הודעה אוטומטית מ %{site_name} כדי ליידע אתכם שהפוסטים שלכם מוחבאים זמנית כיוון שהם דוגלו על ידי הקהילה. - - כאמצעי זהירות, חשבונכם החדש נחסם מליצור תגובות או נושאים חדשים עד שחבר צוות יסקור את חשבונכם. אנחנו מצטערים על חוסר הנוחות. - - להנחיות נוספות, אנא פנו ל[הנחיות הקהילה](%{base_url}/guidelines). too_many_tl3_flags: title: "יותר מידי דגלי רמת-אמון 3" subject_template: "חשבון חדש בהשהיה" - text_body_template: | - שלום, - - זוהי הודעה אוטומטית מ %{site_name} כדי ליידע אתכם שחשבונכם הושהה בגלל דגלים רבים מהקהילה. - - כאמצעי זהירות, חשבונכם החדש נחסם מליצור תגובות או נושאים חדשים עד שחבר צוות יוכל לסקור את חשבונכם. אנחנו מתנצלים על חוסר הנוחות. - - לפרטים נוספים, אנא פנו ל[הנחיות הקהילה](%{base_url}/guidelines). - blocked_by_staff: - title: "נחסם על ידי הצוות" - subject_template: "חשבון זמנית בהשהיה" - text_body_template: | - שלום, - - זוהי הודעה אוטומטית מ %{site_name} כדי ליידע אתכם שהחשבון שלכם הושהה באופן זמני כאמצעי זהירות. - - אנא המשיכו לבקר, אך לא תוכלו לענות או ליצור נושאים עד ש[חבר צוות](%{base_url}/about) יסקור את הפוסטים האחרונים שלכם. אנחנו מתנצלים על אי הנוחות. - - להנחיות נוספות, אנא פנו ל[הנחיות הקהילה](%{base_url}/guidelines). - user_automatically_blocked: - title: "משתמש נחסם אוטומטית" - subject_template: "המשתמש החדש %{username} נחסם על ידי דגלים של הקהילה" - text_body_template: | - זוהי הודעה אוטומטית. - - המשתמש החדש [%{username}](%{user_url}) נחסם אוטומטית כיוון שמספר משתמשים דיגלו פוסטים שלו. - - אנא [סיקרו את הדגלים](%{base_url}/admin/flags). אם %{username} נחסם בטעות מלפרסם, לחצו על כפתור שחרור הנעילה ב [דף הניהול למשתמש זה](%{user_url}). - - סף זה ניתן לשינוי באמצעות הגדרת האתר `block_new_user`. - spam_post_blocked: - title: "פוסט ספאם נחסם" - subject_template: "פוסים של המשתמש/ת החדש/ה %{username} נחסמו בשל קישורים חוזרים." - text_body_template: | - זוהי הודעה אוטומטית. - - המשתמש החדש [%{username}](%{user_url}) ניסה ליצור מספר פוסטים עם קישורים ל %{domains}, אבל הפוסטים האלו נחסמו כדי למנוע ספאם. המשתמש עדיין יכול ליצור פוסטים חדשים שלא מקשרים ל %{domains}. - - אנא [סיקרו את המשתמש](%{user_url}). - - ניתן לכוון זאת בהגדרות האתר `newuser_spam_host_threshold` ו`white_listed_spam_host_domains`. - unblocked: - title: "לא-חסום" - subject_template: "החשבון כבר לא מושהה" - text_body_template: | - שלום, - - זוהי הודעה אוטומטית מ %{site_name} כדי ליידע אתכם שהחשבון שלכם כבר לא מושהה לאחר סקירה של הצוות. - - אתם יכולים עכשיו ליצור נושאים חדשים, ושוב להגיב לנושאים קיימים. תודה על הסבלנות. pending_users_reminder: title: "תזכורת משתמשים ממתינים" subject_template: diff --git a/config/locales/server.id.yml b/config/locales/server.id.yml index 1ca7b879aa..040029a506 100644 --- a/config/locales/server.id.yml +++ b/config/locales/server.id.yml @@ -428,7 +428,6 @@ id: allow_user_locale: "Perbolehkan pengguna untuk memilih bahasa sendiri" allow_duplicate_topic_titles: "Perbolehkan topik dengan judul yang sama." contact_url: "URL kontak untuk situs ini. Digunakan dalam formulir kontak pada /about untuk hal penting." - notify_mods_when_user_blocked: "Jika pengguna diblokir secara otomatis, kirimkan pesan kepada semua moderator." maximum_backups: "Jumlah maksimum backup yang disimpan dalam disk. Backup lama akan dihapus secara otomatis." email_editable: "Perbolehkan pengguna untuk mengganti alamat email setelah registrasi." errors: @@ -446,8 +445,6 @@ id: blocked: "Registrasi baru dari alamat IP Anda tidak diijinkan." max_new_accounts_per_registration_ip: "Registrasi baru dari alamat IP Anda tidak diijinkan (telah mencapai batas maksimum). Silahkan hubungi staff." system_messages: - spam_post_blocked: - subject_template: "Pengguna baru %{username} disuspensi atas penulisan tautan yang berulang-ulang kali" pending_users_reminder: subject_template: other: "%{count} pengguna sedang menunggu persetujuan" diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index e0f4e97c05..b42e7be125 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -48,7 +48,7 @@ it: no_body_detected_error: "Succede quando non si riesce ad estrapolare il corpo del messaggio e non ci sono allegati. " no_sender_detected_error: "Succede quando non abbiamo trovato un indirizzo email valido nell'intestazione From." inactive_user_error: "Succede quando il mittente non è attivo." - blocked_user_error: "Succede quando il mittente è stato bloccato." + silenced_user_error: "Succede quando il mittente è stato silenziato." bad_destination_address: "Succede quando nessuno degli indirizzi email presente nei campi A/Cc/Bcc corrisponde ad alcun indirizzo email entrante tra quelli configurati." strangers_not_allowed_error: "Succede quando un utente tenta di creare uno nuovo argomento in una categoria della quale non è membro." insufficient_trust_level_error: "Succede quando un utente tenta di creare un nuovo argomento in una categoria per la quale non ha il livello di esperienza richiesto." @@ -668,7 +668,7 @@ it: email_title: 'L''argomento "%{title}" richiede l''attenzione di un moderatore' email_body: "%{link}\n\n%{message}" flagging: - you_must_edit: '

    Il tuo messaggio è stato segnalato dalla comunità. vedi i tuoi messaggi.

    ' + you_must_edit: '

    Il tuo messaggio è stato segnalato dalla comunità. Per favore controlla i tuoi messaggi.

    ' user_must_edit: '

    Questo messaggio è stato segnalato dalla comunità ed è stato temporaneamente nascosto.

    ' archetypes: regular: @@ -928,7 +928,8 @@ it: digest_logo_url: "Il logo alternativo usato in cima alla email di riepilogo. Dovrebbe essere a forma di un ampio rettangolo. Non dovrebbe essere un'immagine SVG. Se lasciato vuoto, sarà utilizzato il campo `logo_url`. " logo_small_url: "Il piccolo logo in alto a sinistra del tuo sito, dovrebbe essere a forma di quadrato. Se non impostato, verrà mostrata l'icona di una casa." favicon_url: "La favicon del tuo sito, vedi http://it.wikipedia.org/wiki/Favicon, per funzionare correttamente su una CDN deve essere una png" - mobile_logo_url: "L'immagine del logo a posizione fissa usata in alto a sinistra del sito mobile. Dovrebbe essere di forma quadrata. Se non impostato, verrà usato `logo_url`. Es: http://example.com/uploads/default/logo.png" + mobile_logo_url: "Url del logo personalizzato usato nella versione mobile del tuo sito. Se non impostato verrà usato `logo_url`. Es: http://esempio.com/uploads/default/logo.png " + large_icon_url: "Immagine usata come logo/splash su Android. La dimensione consigliata è di 512px per 512px." apple_touch_icon_url: "Icona usata per dispositivi touch Apple. La dimensione consigliata è 144 x 144 pixel." notification_email: "L'indirizzo presente nel campo from: usato per inviare tutte le email essenziali di sistema. Il dominio indicato deve avere i record SPF, DKIM e reverse PTR impostati correttamente perché l'email arrivi." email_custom_headers: "Una lista di intestazioni email personalizzate delimitata da una barra verticale (pipe |)" @@ -955,11 +956,11 @@ it: tl2_additional_likes_per_day_multiplier: "Aumenta il limite di \"Mi piace\" al giorno ammessi per gli utenti con livello di esperienza 2 (Assiduo), moltiplicando per questo numero" tl3_additional_likes_per_day_multiplier: "Aumenta il limite di \"Mi piace\" al giorno ammessi per gli utenti con livello di esperienza 3 (Esperto) moltiplicando per questo numero" tl4_additional_likes_per_day_multiplier: "Aumenta il limite di \"Mi piace\" al giorno ammessi per gli utenti con livello di esperienza 4 (Veterano), moltiplicando per questo numero" - num_spam_flags_to_block_new_user: "Se i messaggi di un nuovo utente ricevono questo numero di segnalazioni spam da num_users_to_block_new_user diversi utenti, nascondi tutti i suoi messaggi ed impediscigli di crearne altri. 0 per disabilitare." - num_users_to_block_new_user: "Se i messaggi di un nuovo utente ricevono num_flags_to_block_new_user segnalazioni di spam da così tanti utenti diversi, nascondi tutti i suoi messaggi e impediscigli ulteriori invii. 0 per disabilitare." - num_tl3_flags_to_block_new_user: "Se i messaggi di un nuovo utente ricevono questo numero di segnalazioni da num_tl3_users_to_block_new_user differenti utenti a livello di esperienza 3, nascondi tutti i messaggi e previenine la futura pubblicazione. 0 per disabilitare." - num_tl3_users_to_block_new_user: "Se i messaggi di un nuovo utente ricevono num_tl3_flags_to_block_new_user segnalazioni da questo numero di utenti a livello di esperienza 3, nascondi tutti i messaggi e previenine la futura pubblicazione. 0 per disabilitare." - notify_mods_when_user_blocked: "Se un utente è bloccato automaticamente, manda un messaggio ai moderatori." + num_spam_flags_to_silence_new_user: "Se i messaggi di un nuovo utente ricevono segnalazioni di spam da num_users_to_silence_new_user diversi utenti, nascondi tutti i messaggi dell'utente e impediscigli ulteriori invii. 0 per disabilitare questa funzionalità. " + num_users_to_silence_new_user: "Se i messaggi di un nuovo utente ricevono num_spam_flags_to_silence_new_user segnalazioni di spam da così tanti utenti diversi, nascondi tutti i suoi messaggi e impediscigli ulteriori invii. 0 per disabilitare. " + num_tl3_flags_to_silence_new_user: "Se i messaggi di un nuovo utente ricevono questo numero di segnalazioni da num_tl3_users_to_silence_new_user differenti utenti a livello di esperienza 3, nascondi tutti i messaggi e previenine la futura pubblicazione. 0 per disabilitare. " + num_tl3_users_to_silence_new_user: "Se i messaggi di un nuovo utente ricevono num_tl3_flags_to_silence_new_user segnalazioni da questo numero di utenti a livello di esperienza 3, nascondi tutti i messaggi e previenine la futura pubblicazione. 0 per disabilitare." + notify_mods_when_user_silenced: "Se un utente è silenziato automaticamente, manda un messaggio ai moderatori." flag_sockpuppets: "Se un nuovo utente risponde ad un argomento dallo stesso indirizzo IP dell'utente che ha aperto l'argomento stesso, segnala entrambi i messaggi come potenziale spam." traditional_markdown_linebreaks: "Usa l'accapo tradizionale in Markdown, cioè due spazi a fine riga per andare a capo." enable_markdown_typographer: "Utilizza regole tipografiche di base per migliorare la leggibilità dei paragrafi di testo, sostituire (c) (tm) ecc, con simboli, ridurre il numero di punti interrogativi e così via" @@ -967,8 +968,6 @@ it: must_approve_users: "Lo staff deve approvare tutti i nuovi account utente prima che essi possano accedere al sito. ATTENZIONE: abilitare l'opzione per un sito live revocherà l'accesso per tutti gli utenti non-staff esistenti!" pending_users_reminder_delay: "Notifica i moderatori se nuovi utenti sono in attesa di approvazione per più di queste ore. Imposta a -1 per disabilitare le notifiche." maximum_session_age: "L'utente resterà connesso per n ore dall'ultima visita" - ga_tracking_code: "OBSOLETO: codice di tracciamento di Google Analitycs (ga.js), es UA-12345678-9; vedi http://google.com/analytics" - ga_domain_name: "OBSOLETO: nome del dominio Google analytics (ga.js), esempio: miosito.com; vedi http://google.com/analytics" ga_universal_tracking_code: "Codice di tracking Google Universal Analytics (analytics.js), es: UA-12345678-9; vedi http://google.com/analytics" ga_universal_domain_name: "Nome di dominio Google Universal Analytics (analytics.js), es: miosito.com; vedi http://google.com/analytics" ga_universal_auto_link_domains: "Attiva Il monitoraggio interdominio di Google Universal Analytics (analytics.js). Ai collegamenti in uscita a questi domini verrà aggiunto l'id del client. Vedi la guida di Google Cross-domain Tracking." @@ -1200,9 +1199,9 @@ it: num_hours_to_close_topic: "Numero di ore per mettere in pausa un argomento in attesa dell'intervento di un moderatore." auto_respond_to_flag_actions: "Attiva risposta automatica quando viene fatta una segnalazione." min_first_post_typing_time: "Quantità minima di tempo in millisecondi che un utente deve impiegare per la digitazione del primo messaggio, se non viene raggiunta la soglia il messaggio verrà automaticamente inserito nella coda dei messaggi da approvare. Impostare su 0 per disabilitare (non consigliato)" - auto_block_fast_typers_on_first_post: "Blocca automaticamente gli utenti che non soddisfano min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Livello di esperienza massimo per bloccare automaticamente i digitatori veloci" - auto_block_first_post_regex: "Regex senza distinzione tra maiuscole e minuscole che, se passato, causerà il blocco del primo messaggio dell'utente e che sarà inviato nella coda dei messaggi da approvare. Esempio: rabbia|a[bc]a, fa in modo che tutti i messaggi contenenti le parole rabbia o aba o aca siano bloccati in un primo momento. Si applica solo al primo messaggio." + auto_silence_fast_typers_on_first_post: "Silenzia automaticamente gli utenti che non soddisfano min_first_post_typing_time" + auto_silence_fast_typers_max_trust_level: "Livello di esperienza massimo per silenziare automaticamente i digitatori veloci" + auto_silence_first_post_regex: "Regex senza distinzione tra maiuscole e minuscole che, se passato, causerà il silenziamento del primo messaggio dell'utente e che sarà inviato nella coda dei messaggi da approvare. Esempio: rabbia|a[bc]a, fa in modo che tutti i messaggi contenenti le parole rabbia o aba o aca siano silenziati in un primo momento. Si applica solo al primo messaggio." flags_default_topics: "Di default, mostra gli argomenti segnalati nella sezione di amministrazione" reply_by_email_enabled: "Abilita la possibilità di rispondere ai messaggi tramite email." reply_by_email_address: "Modello per rispondere via email, per esempio: %{reply_key}@risposta.esempio.com o risposte+%{reply_key}@esempio.com" @@ -1296,7 +1295,7 @@ it: topic_page_title_includes_category: "Il titolo dell'argomento sulla pagina include il nome della categoria." native_app_install_banner: "Domanda ai visitatori ricorrenti di installare l'app nativa di Discourse." share_anonymized_statistics: "Condividi statistiche anonime di utilizzo." - auto_handle_queued_age: "Gestisci automaticamente i record che sono in attesa di essere rivisti dopo tale numero di giorni. Le segnalazioni saranno ignorate. I messaggi in attesa e gli utenti saranno rifiutati. Imposta a 0 per disattivare questa funzionalità." + auto_handle_queued_age: "Gestisci automaticamente i record che sono in attesa di essere rivisti dopo tale numero di giorni. Le segnalazioni saranno ignorate. I messaggi in attesa e gli utenti saranno rifiutati. Impostare su 0 per disattivare questa funzionalità." max_prints_per_hour_per_user: "Numero massimo di impressioni /pagine di stampa (imposta a 0 per disattivare)" full_name_required: "Il nome completo è un campo obbligatorio del profilo utente." enable_names: "Mostra il nome completo dell'utente su profilo, scheda utente e email. Disabilita per nascondere il nome completo ovunque." @@ -1352,6 +1351,7 @@ it: default_categories_tracking: "Elenco di categorie seguite per default." default_categories_muted: "Elenco di categorie silenziate per default." default_categories_watching_first_post: "Elenco di categorie in cui il primo messaggio di ogni nuovo argomento verrà osservato per impostazione predefinita." + retain_web_hook_events_period_days: "Numero di giorni per mantenere i record degli eventi di web hook." max_user_api_reqs_per_day: "Numero massimo di richieste giornaliere per la chiave API utente " max_user_api_reqs_per_minute: "Numero massimo di richieste al minuto per la chiave API utente" allow_user_api_keys: "Consenti la generazione di chiavi API utente" @@ -1409,6 +1409,7 @@ it: category: 'Categorie' topic: 'Risultati' user: 'Utenti' + results_page: "Risultati della ricerca per '%{term}'" sso: login_error: "Errore Login" not_found: "Impossibile trovare il tuo account. Si prega di contattare l'amministratore del sito." @@ -1916,13 +1917,13 @@ it: Spiacenti, ma il tuo messaggio email per %{destination} (intitolato %{former_title}) non ha funzionato. Il tuo account associato a questo indirizzo email non è stato attivato. Attiva il tuo account prima di inviare email. - email_reject_blocked_user: - title: "Email Rifiutata Utente Bloccato" - subject_template: "[%{email_prefix}] Problema email -- Utente Bloccato" + email_reject_silenced_user: + title: "Email Rifiutata Utente Silenziato" + subject_template: "[%{email_prefix}] Problema email -- Utente Silenziato" text_body_template: | Spiacenti, ma il tuo messaggio email per %{destination} (intitolato %{former_title}) non ha funzionato. - Il tuo account associato a questo indirizzo email è stato bloccato. + Il tuo account associato a questo indirizzo email è stato silenziato. email_reject_reply_user_not_matching: title: "Email Rifiutata Utente Non Corrispondente" subject_template: "[%{email_prefix}] Problema email -- Indirizzo Risposta Imprevisto" @@ -2047,13 +2048,13 @@ it: title: "Troppe Segnalazioni Spam" subject_template: "Nuovo account sospeso" text_body_template: | - Salve, + Ciao, - questo è un messaggio automatico da %{site_name} per informarti che i tuoi messaggi sono stati temporaneamente nascosti perché segnalati dalla comunità. + questo è un messaggio automatico da %{site_name} per informarti che il tuo account è stato bloccato a causa di un gran numero di segnalazioni da parte della comunità. - Come misura precauzionale, il tuo nuovo account non può creare altre risposte o argomenti finché il tuo account non verrà revisionato da un membro dello staff. Ci scusiamo per il disagio. + Come misura precauzionale, il tuo nuovo account è stato silenziato e non può creare altre risposte o argomenti finché non verrà revisionato da un membro dello staff. Ci scusiamo per l'inconveniente - Per ulteriori informazioni, ti rimandiamo alle [linee guida della comunità](%{base_url}/guidelines). + Per maggiori dettagli, fai riferimento alle [linee guida della comunità](%{base_url}/guidelines). too_many_tl3_flags: title: "Troppe Segnalazioni Da TL3" subject_template: "Nuovo account sospeso" @@ -2062,11 +2063,11 @@ it: questo è un messaggio automatico da %{site_name} per informarti che il tuo account è stato bloccato a causa di un gran numero di segnalazioni da parte della comunità. - Come misura precauzionale, il tuo nuovo account non può creare altre risposte o argomenti finché non verrà revisionato da un membro dello staff. Ci scusiamo per l'inconveniente + Come misura precauzionale, il tuo nuovo account è stato silenziato e non può creare altre risposte o argomenti finché non verrà revisionato da un membro dello staff. Ci scusiamo per l'inconveniente Per maggiori dettagli, fai riferimento alle [linee guida della comunità](%{base_url}/guidelines). - blocked_by_staff: - title: "Bloccato dallo Staff" + silenced_by_staff: + title: "Silenziato dallo Staff" subject_template: "Account temporaneamente sospeso" text_body_template: | Ciao, @@ -2076,35 +2077,35 @@ it: Per favore continua pure a navigare, anche se non potrai rispondere o creare argomenti fino a che un [membro dello staff](%{base_url}/about) avrà revisionato i tuoi messaggi più recenti. Ci scusiamo per l'inconveniente. Per maggiori dettagli, fai riferimento alle [linee guida della comunità](%{base_url}/guidelines). - user_automatically_blocked: - title: "Utente Bloccato Automaticamente" - subject_template: "Nuovo utente %{username} bloccato dalle segnalazioni della comunità" + user_automatically_silenced: + title: "Utente Silenziato Automaticamente" + subject_template: "Nuovo utente %{username} silenziato dalle segnalazioni della comunità" text_body_template: | Questo è un messaggio automatico. - Il nuovo utente [%{username}](%{user_url}) è stato automaticamente bloccato perché più utenti hanno contrassegnato i messaggi di %{username}. + Il nuovo utente [%{username}](%{user_url}) è stato automaticamente silenziato perché più utenti hanno contrassegnato i messaggi di %{username}. - Per favore [controlla le segnalazioni](%{base_url}/admin/flags). Se %{username} è stato bloccato per errore, clicca sul tasto di sblocco nella [pagina di amministrazione di questo utente](%{user_url}). + Per favore [controlla le segnalazioni](%{base_url}/admin/flags). Se %{username} è stato silenziato per errore, clicca sul tasto di sblocco nella [pagina di amministrazione di questo utente](%{user_url}). Questa impostazione può essere modificata tramite l'impostazione del sito `block_new_user`. - spam_post_blocked: - title: "Messaggio Spam Bloccato" - subject_template: "Il messaggi del nuovo utente %{username} sono stati bloccati a causa dell'invio di ripetuti collegamenti." + spam_post_silenced: + title: "Messaggio Spam Silenziato" + subject_template: "I messaggi del nuovo utente %{username} sono stati silenziati a causa dell'invio di ripetuti collegamenti." text_body_template: | Questo è un messaggio automatico. - Il nuovo utente [%{username}](%{user_url}) ha creato diversi messaggi con collegamenti a %{domains}, ma tali messaggi sono stati bloccati per evitare spam. L'utente è ancora abilitato alla creazione di messaggi ma senza collegamenti verso %{domains}. + Il nuovo utente [%{username}](%{user_url}) ha creato diversi messaggi con collegamenti a %{domains}, ma tali messaggi sono stati silenziati per evitare spam. L'utente è ancora abilitato alla creazione di messaggi ma senza collegamenti verso %{domains}. Per favore [controlla questo utente](%{user_url}). Questa impostazione può essere modificata tramite le impostazioni del sito `newuser_spam_host_threshold` e `white_listed_spam_host_domains`. - unblocked: - title: "Sbloccato" + unsilenced: + title: "Non silenziato" subject_template: "Account non più sospeso" text_body_template: | Ciao, - questo è un messaggio automatico da %{site_name} per informarti che il tuo account è stato sbloccato a seguito di revisione da parte dello staff. + questo è un messaggio automatico da %{site_name} per informarti che il tuo account non è più silenziato a seguito di revisione da parte dello staff. Ora puoi nuovamente creare argomenti e risposte. Grazie della pazienza. pending_users_reminder: @@ -2133,7 +2134,7 @@ it: text_body_template: | Congratulazioni, ti sei meritato il premio come **Nuovo Utente del Mese per %{month_year}**. :trophy: - Questo premio viene assegnato solo a due nuovi utenti al mese e sarà visibile permanentemente sulla [tua pagina utente](%{base_url}/my/badges). + Questo premio viene assegnato solo a due nuovi utenti al mese e sarà visibile permanentemente sulla [pagina dei distintivi](%{url}). Sei diventato velocemente un membro prezioso della nostra comunità. Grazie per l'adesione e continua a fare un buon lavoro! queued_posts_reminder: @@ -2376,7 +2377,8 @@ it: popular_posts: "Messaggi di Successo" more_new: "Nuovo per te" subject_template: "[%{email_prefix}] Riepilogo" - unsubscribe: "Questo riepilogo viene inviato da %{site_link} se non ti si vede da un po'. Modifica le tue impostazioni email, o %{unsubscribe_link} annulla l'iscrizione." + unsubscribe: "Questo riepilogo viene inviato da %{site_link} se non ti si vede da un po'. Modifica %{email_preferences_link}, o %{unsubscribe_link} per annullare l'iscrizione." + your_email_settings: "le tue impostazioni email" click_here: "clicca qui" from: "%{site_name} riepilogo" preheader: "Un breve riepilogo dall'ultima tua visita il %{last_seen_at}" @@ -2481,6 +2483,9 @@ it: see_more: "Altro" search_title: "Cerca nel sito" search_google: "Google" + offline: + title: "Impossibile caricare l'app" + offline_page_message: "Sembra che tu sia offline! Controlla la tua connessione di rete e riprova." login_required: welcome_message: | ## [Benvenuto su %{title}](#welcome) @@ -2897,9 +2902,9 @@ it: Questo distintivo è assegnato quando ricevi il tuo primo mi piace su un messaggio. Congratulazioni, hai pubblicato qualcosa che i membri della tua comunità hanno trovato interessante, eccezionale o utile! autobiographer: name: Autobiografo - description: Ha compilato le informazioni sul profilo + description: Ha compilato le informazioni del profilo long_description: | - Questo distintivo è assegnato per aver compilato il tuo profilo utente e selezionato un'immagine del profilo. Lasciare che la comunità sappia di più su chi sei e a cosa sei interessato rende la comunità migliore e più connessa. Unisciti a noi! + Questo distintivo è assegnato per aver compilato il tuo profilo utente e selezionato un'immagine per il profilo. Lasciare che gli altri sappiano di più su chi sei e a cosa sei interessato rende la comunità migliore e più connessa. Unisciti a noi! anniversary: name: Compleanno description: Membro attivo per un anno, ha scritto almeno una volta @@ -2992,9 +2997,9 @@ it: Questo distintivo è assegnato la prima volta che citi un messaggio nella tua risposta. Citare le sezioni rilevanti di messaggi precedenti nella tua risposta aiuta a mantenere le discussioni connesse insieme e in argomento. Il modo più semplice per citare è evidenziare una sezione di un messaggio e quindi premere il pulsante rispondi. Cita generosamente! read_guidelines: name: Linee Guida - description: Ha letto le linee guida della comunità + description: Ha letto le linee guida della comunità long_description: | - Questo distintivo è assegnato per aver letto le linee guida della comunità. Seguendo e condividendo queste semplici linee guida aiuti a costruire una comunità sicura, divertente e sostenibile per tutti. Ricorda sempre che c'è un altro essere umano, molto simile a te, dall'altro lato dello schermo. Sii piacevole! + Questo distintivo è assegnato per aver letto le linee guida della comunità. Seguendo e condividendo queste semplici linee guida aiuti a costruire una comunità sicura, divertente e sostenibile per tutti. Ricorda sempre che c'è un altro essere umano, molto simile a te, dall'altro lato dello schermo. Sii carino! reader: name: Lettore description: Ha letto ogni risposta in un argomento con più di 100 risposte @@ -3129,6 +3134,7 @@ it: only_official: "Disabilita i plugin non ufficiali" no_plugins: "Disabilita tutti i plugin" enter: "Avvia Modalità Sicura" + must_select: "È necessario selezionare almeno un'opzione per accedere alla modalità sicura." wizard: title: "Configurazione Discourse" step: diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index 72dc258089..40aef30185 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -606,7 +606,6 @@ ja: tl2_additional_likes_per_day_multiplier: "この数字を掛けると TL2 (メンバー) の1日あたりの「いいね!」の上限を増やします" tl3_additional_likes_per_day_multiplier: "この数字を掛けると TL3 (レギュラー) の1日あたりの「いいね!」の上限を増やします" tl4_additional_likes_per_day_multiplier: "この数字を掛けると TL4 (リーダー) の1日あたりの「いいね!」の上限を増やします" - notify_mods_when_user_blocked: "ユーザが自動的にブロックされた際に、すべてのモデレータにメッセージを送信する。" flag_sockpuppets: "トピックを作成したユーザーと同じIPアドレスで、新規ユーザーがトピックに回答した場合、両者を潜在的なスパムとしてフラグを立てるか" traditional_markdown_linebreaks: "Markdown の従来形式のラインブレーク (行の終わりにダブルスペース) を使う" post_undo_action_window_mins: "投稿に対するアクション (「いいね!」、通報等) 取り消しを許可する時間 (秒)" @@ -770,9 +769,6 @@ ja: num_flags_to_close_topic: "トピックの介入のため、自動的にトピックを停止するために必要なフラグの数" auto_respond_to_flag_actions: "フラグを解除時の自動返信を有効にする" min_first_post_typing_time: "最初の投稿に必要なタイピング時間。閾値以下の場合、投稿は自動的に承認キューに追加されます。0を設定すると無効化されます(推奨されません)" - auto_block_fast_typers_on_first_post: "min_first_post_typing_timeを満たしていないユーザーを自動的にブロック" - auto_block_fast_typers_max_trust_level: "自動的にブロックされるfast typerに対する最大のトラストレベル" - auto_block_first_post_regex: "この正規表現に一致すると、ユーザーの最初の投稿をブロックし、承認キューに送られる。最初の投稿にのみ適用されます" reply_by_email_enabled: "メールでのトピックへの返信を有効化する" reply_by_email_address: "メールアドレスによる回答のテンプレート。例: %{reply_key}@reply.myforum.com" disable_emails: "Discourseからの全ての種類のメール送信を止める" @@ -976,8 +972,6 @@ ja: text_body_template: | 申し訳ありません,あなたの %{destination} へのメール(titled %{former_title}) は動きませんでした メールから回答を見つける事ができませんでした。返信はメールのトップにあることを確認してください。インライン回答を見つける事はできません。 - spam_post_blocked: - subject_template: "同一リンクの連続投稿による新規ユーザ %{username} のブロック" pending_users_reminder: subject_template: other: "%{count} 人のユーザか承認を待っています。" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 196825c24d..d673cf488d 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -47,7 +47,6 @@ ko: auto_generated_email_error: "'precedence'헤더가 list, junk, bulk, auto_reply로 설정되어 있거나 다른 헤더에 auto-submitted, auto-replied, auto-generated가 있을 때 발생합니다." no_body_detected_error: "body를 추출할 수 없거나 첨부파일이 없을 때 발생합니다." inactive_user_error: "발신자(sender)가 비활성상태일 때 발생합니다." - blocked_user_error: "발신자(sender)가 차단되었을 때 발생합니다." bad_destination_address: "수신/참조/비밀참조 필드에 있는 이메일 주소가 설정된 수신 이메일 주소와 하나도 매칭되지 않을 때 발생합니다." strangers_not_allowed_error: "사용자가 자신이 속하지 않은 카테고리에 토픽 생성을 시도할 때 발생합니다." insufficient_trust_level_error: "사용자가 더 높은 신뢰 레벨이 필요한 카테고리에 토픽 생성을 시도할 때 발생합니다." @@ -879,11 +878,6 @@ ko: tl2_additional_likes_per_day_multiplier: "tl2 (회원)의 일별 좋아요 제한을 올릴 배수" tl3_additional_likes_per_day_multiplier: "tl3 (정회원)의 일별 좋아요 제한을 올릴 배수" tl4_additional_likes_per_day_multiplier: "tl4 (리더)의 일별 좋아요 제한을 올릴 배수" - num_spam_flags_to_block_new_user: "신규 사용자가 num_users_to_block_new_user 명의 다른 사용자들로부터 이 수치에 해당하는 신고를 받으면, 작성한 모든 글을 숨기고 더 이상의 글 게시를 금지함(0 = 기능해제)" - num_users_to_block_new_user: "만약 신규 사용자의 포스트가 num_spam_flags_to_block_new_user개의 스팸 신고를 설정된 숫자 만큼의 사용자에게 받으면, 포스트를 모두 숨기고 앞으로의 포스팅을 금지합니다. 0으로 설정하면 해제됩니다." - num_tl3_flags_to_block_new_user: "만약 신규 사용자의 포스트가 num_tl3_users_to_block_new_user명 이상의 신뢰도3 사용자로부터 설정된 숫자만큼의 신고를 받으면 해당 사용자의 포스트를 모두 숨기고 앞으로의 포스팅을 금지합니다. 0으로 설정하면 해제됩니다." - num_tl3_users_to_block_new_user: "만약 신규 사용자의 포스트가 설정된 숫자 이상의 신뢰도 3 사용자로부터 num_tl3_flags_to_block_new_user개의 신고를 받으면 해당 사용자의 포스트를 모두 숨기고 앞으로의 포스팅을 금지합니다. 0으로 설정하면 해제합니다." - notify_mods_when_user_blocked: "만약 사용자가 자동 블락되면 중간 운영자에게 메시지 보내기" flag_sockpuppets: "어떤 신규 사용자(예:24이내 가입자)가 글타래를 생성하고 같은 IP주소의 또 다른 신규 사용자가 댓글을 쓰면 자동 스팸 신고" traditional_markdown_linebreaks: "Markdown에서 전통적인 linebreak를 사용, linebreak시 두개의 trailing space를 사용하는 것." enable_markdown_typographer: "문단의 가독성을 높이기 위해서 기본 타이포그라피 룰을 사용합니다. (c) (tm), 기타 기호를 교체하고 연달아 나오는 물음표의 갯수를 줄입니다." @@ -891,8 +885,6 @@ ko: must_approve_users: "스태프는 반드시 사이트 엑세스권한을 허용하기 전에 모든 신규가입계정을 승인해야 합니다. 경고: 이것을 활성화하면 기존 스태프 아닌 회원들의 엑세스권한이 회수됩니다." pending_users_reminder_delay: "새로운 사용자가 승인을 기다리는 시간이 여기에 지정된 시간횟수보다 더 길어길경우 운영자에게 알려줍니다. 알림을 해제하려면 -1로 설정하세요." maximum_session_age: "마지막 방문으로 부터 n시간 동안 사용자의 로그인이 유지됩니다" - ga_tracking_code: "노후기술경고: Google 애널리틱스 (ga.js) 추적 코드, 예: UA-12345678-9; http://google.com/analytics를 참고하세요." - ga_domain_name: "노후기술경고: Google 애널리틱스 (ga.js) 도메인 명, 예: mysite.com; http://google.com/analytics을 참조하세요." ga_universal_tracking_code: "Google Universal Analytics (analytics.js)의 트래킹 코드, 예: UA-12345678-9; 참고 http://google.com/analytics" ga_universal_domain_name: "Google Universal Analytics (analytics.js)의 도메인 이름, 예: mysite.com; 참고 http://google.com/analytics" gtm_container_id: "Google Tag Manager 컨테이너 id. 예: GTM-ABCDEF" @@ -1118,9 +1110,6 @@ ko: num_hours_to_close_topic: "운영진 중재를 위하여 토픽을 중지시킬 시간" auto_respond_to_flag_actions: "신고 기각 시 자동 답변 활성화" min_first_post_typing_time: "사용자가 첫번째 포스트를 작성할 때 타이핑에 소요해야 하는 밀리초. 설정값에 도달하지 않았을 때는 자동으로 승인 큐에 포함됩니다. 승인 0으로 설정하면 해제됩니다.(해제는 권장하지 않습니다)" - auto_block_fast_typers_on_first_post: " min_first_post_typing_time에 도달하지 않은 사용자를 자동으로 차단" - auto_block_fast_typers_max_trust_level: "빠른 타이핑이 감지되면 자동 차단을 적용할 최대 신뢰도" - auto_block_first_post_regex: "첫번째 포스트를 검사하여 통과하면 사용자를 차단하고 승인 대기열에 포함시키는 대소문자 구분이 없는 정규표현식. 예: raging|a[bc]a, 는 raging, aba, aca를 포함한 첫번째 포스트를 차단합니다. 첫번째 포스트에만 적용됩니다." reply_by_email_enabled: "이메일을 통해 주제에 답글을 달 수 있음." reply_by_email_address: "이메일 주소로 답글을 다는 템플릿. 예: %{reply_key}@reply.myforum.com" alternative_reply_by_email_addresses: "수신된 이메일에 대한 답장메일의 대체 템플릿 목록. 예: %{reply_key}@reply.example.com|replies+%{reply_key}@example.com" @@ -1393,16 +1382,6 @@ ko: too_many_tl3_flags: title: "TL3 Flag가 너무 많음" subject_template: "대기중인 신규 계정" - blocked_by_staff: - title: "운영진에 의해 차단됨" - user_automatically_blocked: - title: "사용자 자동차단됨" - spam_post_blocked: - title: "스팸 게시글 차단됨" - subject_template: "%{username} 신규가입자가 연속 링크로 블락됨" - unblocked: - title: "차단해제됨" - subject_template: "계정의 대기가 해제되었습니다" pending_users_reminder: subject_template: other: "%{count}명의 새로운 사용자가 가입 승인 대기중 입니다." diff --git a/config/locales/server.nb_NO.yml b/config/locales/server.nb_NO.yml index fd24476343..ee18cf5523 100644 --- a/config/locales/server.nb_NO.yml +++ b/config/locales/server.nb_NO.yml @@ -46,7 +46,6 @@ nb_NO: no_body_detected_error: "Skjer når vi ikke kunne trekke ut meldingskroppen eller det ikke var noen vedlegg." no_sender_detected_error: "Skjer når vi ikke kunne finne en gyldig adresse i Fra-feltet" inactive_user_error: "Skjer når senderen ikke er aktiv." - blocked_user_error: "Skjer når avsenderen har blitt blokkert." bad_destination_address: "Skjer når ingen av adressene i To-, Cc- eller Bcc-feltene passer med en konfigurert innkommende e-postadresse." strangers_not_allowed_error: "Skjer når en bruker forsøket å opprette et nytt emne i en kategori som han eller hun ikke er medlem av." insufficient_trust_level_error: "Skjer når en bruker forsøket å opprette et nytt emne i en kategori som han eller hun ikke har tillitsnivå for." @@ -259,6 +258,7 @@ nb_NO: no_user_selected: "Du må velge en gyldig bruker. " featured_link: invalid: "er ugyldig. Nettadressen bør inneholde http:// eller https://" + invalid_category: "kan ikke redigeres i denne kategorien." user: attributes: password: @@ -273,10 +273,21 @@ nb_NO: attributes: hex: invalid: "er ikke en gyldig farge" + post_reply: + base: + different_topic: "Innlegg ogg svar må tilhøre samme emne." web_hook: attributes: payload_url: invalid: "URL-adresse er ikke gyldig. Adressen skal starte med http:// eller https:// og ingen blanke tegn er tillatt." + custom_emoji: + attributes: + name: + taken: er allerede i bruk av en annen emoji + topic_timer: + attributes: + execute_at: + in_the_past: "må være i fremtiden." <<: *errors user_profile: no_info_other: "
    %{name} har ikke skrevet noe i Om meg-feltet på sin profil ennå
    " @@ -851,8 +862,6 @@ nb_NO: subject_template: "Ny konto på vent" too_many_tl3_flags: subject_template: "Ny konto på vent" - unblocked: - subject_template: "Kontoen er ikke lenger på vent" unsubscribe_mailing_list: | Du mottar dette fordi du har slått på e-postlistemodus. diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 504e5edce3..65470cf79f 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -47,7 +47,6 @@ nl: auto_generated_email_error: "Gebeurt wanneer de 'precedence'-header is ingesteld op list, junk, bulk of auto_reply, of wanneer een andere header het volgende bevat: auto-submitted, auto-replied of auto-generated." no_body_detected_error: "Gebeurt wanneer er geen hoofdtekst uit de mail kon worden gehaald en er geen bijlagen waren." inactive_user_error: "Gebeurt wanneer de afzender niet actief is." - blocked_user_error: "Gebeurt wanneer de afzender is geblokkeerd." bad_destination_address: "Gebeurt wanneer geen van de e-mailadressen in de Aan, CC-, of BCC-velden gelijk zijn aan de geconfigureerde inkomende e-mailadressen." strangers_not_allowed_error: "Gebeurt wanneer een gebruiker een nieuwe topic probeerde aan te maken in een categorie waar hij/zij geen lid van is." insufficient_trust_level_error: "Gebeurt wanneer een gebruiker een nieuwe topic probeerde aan te maken in een categorie waarvoor hij/zij niet het vereiste vertrouwensniveau heeft." @@ -647,7 +646,6 @@ nl: email_title: 'Het topic ''%{title}'' vereist aandacht van een moderator' email_body: "%{link}\n\n%{message}" flagging: - you_must_edit: '

    Uw bericht is gemarkeerd door de gemeenschap. Bekijk uw berichten.

    ' user_must_edit: '

    Uw bericht is gemarkeerd door de gemeenschap en is tijdelijk verborgen.

    ' archetypes: regular: @@ -922,7 +920,6 @@ nl: tl2_additional_likes_per_day_multiplier: "Verhoog de limiet van likes per dag voor tl2 (lid) door te vermenigvuldigen met dit getal" tl3_additional_likes_per_day_multiplier: "Verhoog de limiet van likes per dag voor tl3 (vaste bezoeker) door te vermenigvuldigen met dit getal" tl4_additional_likes_per_day_multiplier: "Verhoog de limiet van likes per dag voor tl4 (leider) door te vermenigvuldigen met dit getal" - notify_mods_when_user_blocked: "Als een gebruiker automatisch geblokkeerd is, stuur dan een bericht naar alle moderatoren." flag_sockpuppets: "Als een nieuwe gebruiker antwoord op een topic vanaf hetzelfde ip-adres als de nieuwe gebruiker die het topic opende, markeer dan beide berichten als potentiële spam." traditional_markdown_linebreaks: "Gebruik traditionele regeleinden in Markdown, welke 2 spaties aan het einde van een regel nodig heeft voor een regeleinde." post_undo_action_window_mins: "Het aantal minuten waarin gebruikers hun recente acties op een bericht nog terug kunnen draaien (liken, markeren, etc)." @@ -1118,9 +1115,6 @@ nl: num_flags_to_close_topic: "Minimun aantal actieve markeerders benodigd om geautomatiseerd een topic te pauzeren." auto_respond_to_flag_actions: "Automatisch antwoord Inschakelen op moment van weggooien van een markering." min_first_post_typing_time: "Minimum tijd in milliseconden dat een gebruiker moet typen tijdens een eerste post, als de drempelwaarde niet wordt bereikt zal de post automatisch in de wachtrij voor goedkeuring gezet worden. Zet op 0 om uit te schakelen (niet aanbevolen)" - auto_block_fast_typers_on_first_post: "Blokkeer gebruikers automatisch als ze niet voldoen aan min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Maximale vertrouwensniveau om snelle typers automatisch te blokkeren " - auto_block_first_post_regex: "Hoofdlettergevoelige regex die bij overeenkomst het eerste bericht van een gebruiker blokkeert en in de wachtrij voor goedkeuring zet. Voorbeeld: raging|a[bc]a zorgt ervoor dat alle eerste berichten die raging, aba of aca bevatten worden geblokkeerd. Alleen van toepassing op het eerste bericht." reply_by_email_enabled: "Mogelijk inschakelen om te antwoorden per e-mail." reply_by_email_address: "Template voor antwoorden per e-mail, bijvoorbeeld: %{reply_key}@reageer.mijnforum.nl" incoming_email_prefer_html: "De HTML gebruiken in plaats van de tekst voor inkomende e-mail" @@ -1422,11 +1416,6 @@ nl: Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. Het account dat bij dit e-mailadres hoort, is niet geactiveerd. Je moet je account activeren voordat je e-mails instuurt. - email_reject_blocked_user: - text_body_template: | - Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. - - Het account dat bij dit e-mailadres hoort, is geblokkeerd. email_reject_empty: text_body_template: |+ Het spijt ons, maar het plaatsen van uw e-mailbericht naar %{destination} (getiteld %{former_title}) is niet gelukt. @@ -1460,23 +1449,6 @@ nl: Het spijt ons, maar het plaatsen van je e-mail op %{destination} (met onderwerp %{former_title}) is niet gelukt. De plaatsingsactie werd niet herkend. Probeer het opnieuw of plaats je bericht via de website als het nog steeds niet lukt. - blocked_by_staff: - subject_template: "Account tijdelijk geblokkeerd" - user_automatically_blocked: - title: "Gebruiker Automatisch Geblokkeerd" - subject_template: "Nieuwe gebruiker %{username} geblokkeerd wegens meldingen van andere gebruikers" - spam_post_blocked: - title: "Spam bericht geblokkeerd" - subject_template: "Berichten van nieuwe gebruiker %{username} geblokkeerd vanwege herhaalde links" - unblocked: - title: "Gedeblokkeerd" - subject_template: "Account niet langer geblokkeerd" - text_body_template: | - Hallo, - - Dit is een automatisch bericht van %{site_name} om u te laten weten dat uw account na beoordeling door een staflid niet meer is geblokkeerd. - - U kunt nu weer nieuwe antwoorden en topics aanmaken. Bedankt voor uw geduld. pending_users_reminder: subject_template: one: "1 gebruiker wacht op goedkeuring" @@ -1588,6 +1560,8 @@ nl: privacy_topic: title: "Privacy Voorwaarden" badges: + autobiographer: + name: Autobiograaf read_guidelines: name: Richtlijnen gelezen admin_login: diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 784d9d9ffc..fee4fa3c7f 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -47,7 +47,6 @@ pl_PL: auto_generated_email_error: "Zdarza się, gdy \"pierwszeństwo\" nagłówka jest ustawione na: listę, śmieci, luzem lub auto odpowiedź lub gdy inny nagłówek zawiera: auto-składane, auto-odpowiadanie lub auto-generowane." no_body_detected_error: "Zdarza się kiedy nie może rozpakować całości wiadomości, a zawarte są w niej załączniki." inactive_user_error: "Zdarza się kiedy nadawca jest nie aktywny." - blocked_user_error: "Zdarza się kiedy nadawca jest zablokowany." bad_destination_address: "Zdarza się gdy żaden adres email nie pasuje do pól To/Cc/Bcc skonfigurowanych jako przychodzący adres email." strangers_not_allowed_error: "Zdarza się, gdy użytkownik próbuje stworzyć temat w kategorii, której nie jest członkiem." insufficient_trust_level_error: "Zdarza się, gdy użytkownik próbuje stworzyć temat w kategorii, której nie ma odpowiedniego poziomu zaufania.." @@ -1016,11 +1015,6 @@ pl_PL: tl2_additional_likes_per_day_multiplier: "Zwiększ limit lajków na dzień dla tl2 (członek), mnożąc przez tę liczbę" tl3_additional_likes_per_day_multiplier: "Zwiększ limit lajków na dzień dla tl3 (użytkownik stały), mnożąc przez tę liczbę" tl4_additional_likes_per_day_multiplier: "Zwiększ limit lajków na dzień dla tl4 (lider), mnożąc przez tę liczbę" - num_spam_flags_to_block_new_user: "Jeżeli post nowego użytkownika otrzyma tyle spam flag od num_users_to_block_new_user różnych użytkowników, ukryj ich wszystkie posty i zablokuj możliwość tworzenia kolejnych. 0 żeby wyłączyć." - num_users_to_block_new_user: "Jeżeli post nowego użytkownika otrzyma num_spam_flags_to_block_new_user flag spamu od tylu różnych użytkowników, ukryj jego wszystkie posty i zablokuj możliwość pisania nowych. 0 żeby wyłączyć." - num_tl3_flags_to_block_new_user: "Jeżeli post nowego użytkownika otrzyma num_tl3_users_to_block_new_user flag od tylu różnych użytkowników poziomu zaufania 3, ukryj jego wszystkie posty i zablokuj możliwość pisania nowych. 0 żeby wyłączyć." - num_tl3_users_to_block_new_user: "Jeżeli post nowego użytkownika otrzyma num_tl3_users_to_block_new_user flag spamu od tylu różnych użytkowników poziomu zaufania 3, ukryj jego wszystkie posty i zablokuj możliwość pisania nowych. 0 żeby wyłączyć." - notify_mods_when_user_blocked: "If a user is automatically blocked, send a message to all moderators." flag_sockpuppets: "Jeśli nowy użytkownik odpowiada na dany temat z tego samego adresu IP co nowy użytkownik, który założył temat, oznacz ich posty jako potencjalny spam." traditional_markdown_linebreaks: "Używaj tradycyjnych znaków końca linii w Markdown, to znaczy dwóch spacji na końcu linii." enable_markdown_typographer: "Używaj podstawowych zasad typografii, aby poprawić czytelność poszczególnych paragrafów tekstu, zastąpienie (c)(tm) itd, z symbolami, zmniejsza liczbę znaków zapytania itd" @@ -1028,8 +1022,6 @@ pl_PL: must_approve_users: "Zespół musi zaakceptować wszystkie nowe konta zanim uzyskają dostęp do serwisu. UWAGA: włączenie tego dla już udostępnionej strony sprawi, że zostanie odebrany dostęp wszystkim istniejącym użytkownikom spoza zespołu." pending_users_reminder_delay: "Powiadomić moderatorów jeżeli nowi użytkownicy czekali na zatwierdzenie dłużej niż his mamy godzin. Ustaw -1 aby wyłączyć powiadomienia. " maximum_session_age: "Użytkownik zostanie zalogowany przez n godzin od czasu ostatniej wizyty." - ga_tracking_code: "Identyfikator Google Universal Analytics (ga.js) tracking code code, eg: UA-12345678-9; zobacz http://google.com/analytics" - ga_domain_name: "Identyfikator Google Analytics (ga.js) nazwa domeny, eg: UA-12345678-9; zobacz http://google.com/analytics" ga_universal_tracking_code: "Identyfikator Google Universal Analytics (analytics.js), np: UA-12345678-9; zobacz http://google.com/analytics" ga_universal_domain_name: "Domena dla Google Universal Analytics (analytics.js), np: mysite.com; zobacz http://google.com/analytics" ga_universal_auto_link_domains: "Włącz Google Universal Analytics (analytics.js) śledzenie wielu domen. Linki wychodzące do tych domen będą miały dodane id klienta. Zobacz poradnik Google's Cross-Domain Tracking." @@ -1257,9 +1249,6 @@ pl_PL: num_hours_to_close_topic: "Liczba godzin zatrzymanie tematu dla interwencji." auto_respond_to_flag_actions: "Włącz automatyczną odpowiedź, w czasie pozbywania się flagi." min_first_post_typing_time: "Minimalny czas w milisekundach, w którym użytkownik musi napisać post, jeśli próg nie zostanie osiągnięty post zostanie automatycznie dostosowany w zależności od kolejki zatwierdzania potrzeb. Ustaw 0, aby wyłączyć (nie polecane)" - auto_block_fast_typers_on_first_post: "Automatycznie blokuj użytkowników, którzy nie spełniają min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Maksymalny poziom zaufania do blokowania użytkowników piszących zbyt szybko" - auto_block_first_post_regex: "Regex niewrażliwy na wielkość liter, jeśli przejdzie to pierwszy post użytkownika zostanie zablokowany i wysłany do kolejki oczekujących na akceptację. Na przykład: ragingla[bc]a, spowoduje, że wszystkie posty zawierające ranging lub aba lub aca będą blokowane w pierwszej kolejności. Ma zastosowanie tylko do pierwszego posta." reply_by_email_enabled: "Włącz możliwość odpowiadania na wpisy poprzez email." reply_by_email_address: "Template for reply by email incoming email address, for example: %{reply_key}@reply.example.com or replies+%{reply_key}@example.com" alternative_reply_by_email_addresses: "Lista alternatywnych szablonów do odpowiedzi przez adres email. Przykład: %{reply_key}@reply.example.com|replies+%{reply_key}@example.com" @@ -1719,7 +1708,7 @@ pl_PL: text_body_template: | Proszę potwierdzić dodanie **%{target_username}** jako administratora do twojego forum. - [Potwierdź Konto Administratora](2%{admin_confirm_url}) + [Potwierdź Konto Administratora](%{admin_confirm_url}) test_mailer: title: "Test Powiadomień" subject_template: "[%{site_name}] Test dostarczania poczty" @@ -1970,13 +1959,6 @@ pl_PL: Przepraszamy, ale twoja wiadomość email do %{destination}(zatytułowana %{former_title}) nie została wysłana. Twoja konto powiązane z tym adresem email nie jest aktywne. Proszę aktywować konto przed wysyłaniem emaili. - email_reject_blocked_user: - title: "Email odrzucony Użytkownik zablokowany" - subject_template: "[%{email_prefix}] Problem z pocztą elektroniczną - zablokowany użytkownik" - text_body_template: | - Przepraszamy, ale twoja wiadomość email do %{destination}(zatytułowana %{former_title}) nie została wysłana. - - Twoje konto połączone z tym adresem email zostało zablokowane. email_reject_reply_user_not_matching: title: "Email odrzucony Użytkownik nie pasuje" subject_template: "[%{email_prefix}] Problem z pocztą - Nieoczekiwany adres odpowiedzi" @@ -2096,67 +2078,9 @@ pl_PL: too_many_spam_flags: title: "Zbyt wiele spamowych flag." subject_template: "Nowe konto zawieszone" - text_body_template: | - Witaj, - - Jest to automatyczna wiadomość z forum %{site_name} wysłana by poinformować Cię że Twoje wpisy zostały automatycznie ukryte z powodu oflagowania przez społeczność. - - Jako środek zapobiegawczy, na Twoim nowym koncie zablokowano tworzenie nowych odpowiedzi i tematów do momentu gdy członek zespołu będzie mógł zweryfikować Twoje konto. Przepraszamy za niedogodności. - - Dodatkowe wskazówki możesz znaleźć w naszych [wytycznych dla społeczności](%{base_url}/guidelines). too_many_tl3_flags: title: "Zbyt wiele flag TL3" subject_template: "Nowe konto zawieszone" - text_body_template: | - Dzień dobry, - - To jest automatyczna wiadomość od%{site_name}, aby cię poinformować, że twoje konto zostało zablokowane ze względu na dużą liczbę oflagowań przez społeczność. - - Jako środek zapobiegawczy, twoje nowe konto zostało zablokowane i nie ma możliwości dodawania odpowiedzi oraz nowych wątków dopóki obsługa przeglądanie twoje konto. Przepraszamy za niedogodności. - - W celu uzyskania dodatkowych wskazówek, proszę przejść do naszych [wskazówek społeczności] %{base_url}/guidelines). - blocked_by_staff: - title: "Zablokowany prze moderatora" - subject_template: "Konto tymczasowo zawieszone" - text_body_template: | - Dzień dobry, - - To jest automatyczna wiadomość od %{site_name}, aby poinformować cię, że twoje konto zostało tymczasowo zablokowane jako środek zapobiegawczy. - - Możesz kontynuować przeglądanie, ale nie będzie mógł dodawać odpowiedzi lub tworzyć wątków dopóki [członek obsługi](%{base_url}/o nas) przejrzy twoje ostatnie posty. Przepraszamy za niedogodności. - - W celu uzyskania dodatkowych wskazówek, proszę przejść do naszych [wskazówek społeczności](%{base_url}/about). - user_automatically_blocked: - title: "Użytkownik Zablokowany Automatycznie" - subject_template: "Nowy użytkownik %{username} został zablokowany z powodu oflagowania przez społeczność" - text_body_template: | - Jest to wiadomość wysłana automatycznie. - - Nowy użytkownik [%{username}](%{base_url}%{user_url}) został automatycznie zablokowany ponieważ wielu użytkowników oflagowało jego posty. - - [Przejrzyj flagi](%{base_url}/admin/flags). Jeśli %{username} został mylnie zablokowany, kliknij na przycisk odblokuj na [administracyjnej stronie tego użytkownika](%{base_url}%{user_url}). - - Próg blokowania może być zmieniony przez ustawienie serwisu `block_new_user`. - spam_post_blocked: - title: "Spam Zablokowany" - subject_template: "Posty nowego użytkownika %{username} zablokowane za powtarzanie linków" - text_body_template: | - Jest to wiadomość wysłana automatycznie. - - Nowy użytkownik [%{username}](%{base_url}%{user_url}) spróbował utworzyć wiele postów z linkami do %{domains}, lecz posty te zostały zablokowane by uniknąć spamu. Użytkownik nadal może tworzyć nowe posty nie linkujące do %{domains}. - - [Przejrzyj tego użytkownika](%{user_url}). - - Można dokonać modyfikacji poprzez strony ustawień `newuser_spam_host_threshold` i `white_listed_spam_host_domains'. - unblocked: - title: "Odblokowany" - subject_template: "Konto odblokowane" - text_body_template: | - Witaj, - - Wysyłamy tę automatyczną wiadomość z %{site_name} aby poinformować Cię, że Twoje konto zostało odblokowane po interwencji zespołu. - - Możesz znowu tworzyć wpisy i tematy. Dziękujemy za cierpliwość. pending_users_reminder: title: "Oczekujący użytkownicy przypomnienie" subject_template: diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index ace016fe07..56844fd0e1 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -47,7 +47,6 @@ pt: auto_generated_email_error: "Acontece quando o cabeçalho 'precedence' é definida como: 'list', 'junk' ou 'auto_reply', ou quando algum cabeçalho contém: 'auto-submitted', 'auto-replied' ou 'auto-generated'." no_body_detected_error: "Acontece quando nós não conseguimos extrair um corpo e não existiam anexos." inactive_user_error: "Acontece quando o remetente não está ativo." - blocked_user_error: "Acontece quando o remetente foi bloqueado." bad_destination_address: "Acontece quando nenhum dos endereços de e-mail nos campos Para/Cc/Bcc coincidem com um endereço de e-mail de 'A Receber' configurado." strangers_not_allowed_error: "Acontece quando um utilizador tentou criar um novo tópico numa categoria em que não é membro." insufficient_trust_level_error: "Acontece quando um utilizador tentou criar um novo tópico numa categoria em que não tem o nível de confiança necessário." @@ -854,19 +853,12 @@ pt: tl2_additional_likes_per_day_multiplier: "Aumentar limite de gostos por dia para nc2 (membro) ao multiplicar por este número" tl3_additional_likes_per_day_multiplier: "Aumentar limite de gostos por dia para nc3 (habitual) ao multiplicar por este número" tl4_additional_likes_per_day_multiplier: "Aumentar limite de gostos por dia para nc4 (líder) ao multiplicar por este número" - num_spam_flags_to_block_new_user: "Se as publicações de um novo utilizador receberem estas tantas denúncias de num_users_to_block_new_user diferentes utilizadores, esconder todos as suas publicações e prevenir publicação futura. 0 para desactivar." - num_users_to_block_new_user: "Se as publicações de um novo utilizador receberem num_spam_flags_to_block_new_user denúncias de spam de estes tantos diferentes utilizadores, esconder todos as suas publicações e prevenir publicação futura. 0 para desactivar." - num_tl3_flags_to_block_new_user: "Se as publicações de um novo utilizador receberem estas tantas denúncias de num_tl3_users_to_block_new_user diferentes utilizadores de nível de confiança 3, esconder todos as suas publicações e prevenir publicação futura. 0 para desactivar." - num_tl3_users_to_block_new_user: "Se as publicações de um novo utilizador receberem num_tl3_flags_to_block_new_user denúncias de estes tantos diferentes utilizadores de nível de confiança 3, esconder todos as suas publicações e prevenir publicação futura. 0 para desactivar." - notify_mods_when_user_blocked: "Se um utilizador for bloqueado de forma automática, enviar uma mensagem a todos os moderadores." flag_sockpuppets: "Se um novo utilizador responde a um tópico a partir do mesmo endereço IP do novo utilizador que iniciou o tópico, sinalizar ambas as mensagens como potencial spam." traditional_markdown_linebreaks: "Utilize tradicionais quebras de linha no Markdown, que requer dois espaços no final para uma quebra de linha." post_undo_action_window_mins: "Número de minutos durante o qual os utilizadores têm permissão para desfazer ações numa mensagem (gostos, sinalizações, etc)." must_approve_users: "O pessoal deve aprovar todas as novas contas de utilizador antes destas terem permissão para aceder ao sítio. AVISO: ativar isto para um sítio ativo irá revogar o acesso aos utilizadores existentes que não fazem parte do pessoal!" pending_users_reminder_delay: "Notificar moderadores se novos utilizadores estiverem à espera de aprovação por mais que esta quantidade de horas. Configurar com -1 para desativar notificações." maximum_session_age: "Utilizador permanecerá ligado durante n horas desde a última visita" - ga_tracking_code: "OBSOLETO: Google analytics (ga.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" - ga_domain_name: "OBSOLETO: Google analytics (ga.js) domain name, eg: mysite.com; see http://google.com/analytics" ga_universal_tracking_code: "Código de acompanhamento do Google Universal Analytics (analytics.js), ex: UA-12345678-9; ver http://google.com/analytics" ga_universal_domain_name: "Nome de domínio do Google Universal Analytics (analytics.js), ex: omeusitio.com; ver http://google.com/analytics" gtm_container_id: "id de contentor do Google Tag Manager. eg: GTM-ABCDEF" @@ -1078,9 +1070,6 @@ pt: num_hours_to_close_topic: "Número de horas para pausar um tópico para intervenção." auto_respond_to_flag_actions: "Ativar respostas automáticas ao eliminar uma bandeira." min_first_post_typing_time: "Quantidade mínima de tempo em milissegundos que um utilizador deve digitar durante a sua primeira mensagem, se o limite não for atingido a mensagem será automaticamente inserida na fila de aprovação. Configurar a 0 para desativar (não recomendado)" - auto_block_fast_typers_on_first_post: "Bloquear automaticamente utilizadores que não cumprem min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Nível de confiança máximo para bloquear automaticamente escritores rápidos" - auto_block_first_post_regex: "Expressão regular sensível a maiúsculas e minúsculas que se passar irá fazer com que a primeira mensagem do utilizador seja bloqueada e enviada para a fila de aprovação. Exemplo: raging|a[bc]a, irá fazer com que todas as mensagens que contenham raging ou aba ou aca sejam bloqueadas primeiro. Aplicável apenas à primeira mensagem." reply_by_email_enabled: "Ativar respostas aos tópicos por email." reply_by_email_address: "Modelo para endereços emails recebidos com função resposta por email, por exemplo: %{reply_key}@resposta.exemplo.com ou resposta+%{reply_key}@exemplo.com" alternative_reply_by_email_addresses: "Lista de modelos alternativos para respostas via email. Exemplo: %{reply_key}@resposta.exemplo.com ou resposta+%{reply_key}@exemplo.com" @@ -1570,12 +1559,6 @@ pt: Pedimos desculpa mas a sua mensagem de email para %{destination} (intitulada %{former_title}) não funcionou. A sua conta associada com este email não está activa. Por favor active a sua conta antes de enviar emails. - email_reject_blocked_user: - text_body_template: |+ - Pedimos desculpa mas a sua mensagem de email para %{destination} (intitulada %{former_title}) não funcionou. - - A sua conta associada com este endereço de email foi bloqueada. - email_reject_empty: text_body_template: | Pedimos desculpa, mas a sua mensagem de email para %{destination} (com título%{former_title}) não funcionou. @@ -1617,62 +1600,8 @@ pt: Se existe uma interface web para a sua conta de POP de email, pode precisar de iniciar sessão na web e verificar a suas configurações aí. too_many_spam_flags: subject_template: "Nova conta suspensa" - text_body_template: | - Olá, - - Esta é uma mensagem automática de %{site_name} para informá-lo de que as suas publicações foram temporariamente ocultadas porque foram denunciadas pela comunidade. - - Como medida precaucionária, a sua conta nova foi bloqueada de criar novas respostas ou tópicos até um membro da equipa de apoio poder rever a sua conta. Pedimos desculpa pela inconveniência. - - Para orientação adicional, por favor consulte as [orientações da comunidade](%{base_url}/guidelines). too_many_tl3_flags: subject_template: "Nova conta suspensa" - text_body_template: | - Olá, - - Esta é uma mensagem automática de %{site_name} para informá-lo de que a sua conta está temporariamente suspensa devido a um número elevado de denúncias pela comunidade. - - Como medida precaucionária, a sua conta nova foi bloqueada de criar novas respostas ou tópicos até um membro da equipa de apoio poder rever a sua conta. Pedimos desculpa pela inconveniência. - - Para orientação adicional, por favor consulte as [orientações da comunidade](%{base_url}/guidelines). - blocked_by_staff: - subject_template: "Conta temporariamente suspensa" - text_body_template: | - Olá, - - Esta é uma mensagem automática de %{site_name} para informá-lo de que a sua conta está temporariamente suspensa como medida precaucionária. - - Por favor continue a navegar, mas não será capaz de criar novas respostas ou tópicos até um [membro da equipa de apoio](%{base_url}/about) poder rever a sua conta. Pedimos desculpa pela inconveniência. - - Para orientação adicional, por favor consulte as [orientações da comunidade](%{base_url}/guidelines). - user_automatically_blocked: - subject_template: "Novo utilizador %{username} bloqueado devido a denúncias da comunidade." - text_body_template: | - Esta é uma mensagem automática. - - O novo utilizador [%{username}](%{user_url}) foi automaticamente bloqueado porque múltiplos utilizadores marcaram mensagem(ns) de %{username}. - - Por favor [reveja as sinalizações](%{base_url}/admin/flags). Se %{username} foi incorretamente bloqueado de publicar, carregue no botão de desbloqueio em [a página de administração para este utilizador](%{user_url}) - - Este limite pode ser alterado através de `block_new_user` nas configurações do sítio. - spam_post_blocked: - subject_template: "Novas mensagens do novo utilizador %{username} foram bloqueadas devido a hiperligações repetidas" - text_body_template: | - Esta é uma mensagem automática. - - O novo utilizador [%{username}](%{user_url}) tentou criar múltiplas mensagens com hiperligações para %{domains}, mas estas foram bloqueadas para evitar spam. O utilizador ainda pode criar novas mensagens sem hiperligação a %{domains}. - - Por favor [reveja o utilizador](%{user_url}). - - Isto pode ser modificado através de `newuser_spam_host_threshold` e `white_listed_spam_host_domains` nas configurações do sítio. - unblocked: - subject_template: "Conta deixou de estar suspensa" - text_body_template: | - Olá, - - Esta é uma mensagem automática de %{site_name} para informar que a sua conta deixou de estar suspensa após avaliação da staff. - - Pode agora voltar a responder e criar novos tópicos. pending_users_reminder: subject_template: one: "1 utilizador espera a aprovação" diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 252743bddc..e0ce135a7c 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -43,7 +43,6 @@ pt_BR: auto_generated_email_error: "Ocorre quando o cabeçalho 'precedência' estiver definido como: lista, lixo eletrônico, lote ou resposta_automática, ou quando outro cabeçalho contiver: enviado_automaticamente, respondido_automaticamente ou gerado_automaticamente." no_body_detected_error: "Ocorre quando não conseguimos extrair um corpo e não há anexos." inactive_user_error: "Ocorre quando o remetente não está ativo." - blocked_user_error: "Ocorre quando o remetente foi bloqueado." bad_destination_address: "Ocorre quando nenhum dos endereços de email nos campos Para/Cc/Cco era correspondente ao endereço de email recebido." strangers_not_allowed_error: "Ocorre quando um usuário tentou criar um novo tópico em uma categoria da qual não é membro." insufficient_trust_level_error: "Ocorre quando um usuário tenta criar um novo tópico em uma categoria onde não possui nível de confiança necessário." @@ -845,17 +844,11 @@ pt_BR: tl2_additional_likes_per_day_multiplier: "Aumentar limite de likes por dia para tl2 (membros) multiplicando por esse número" tl3_additional_likes_per_day_multiplier: "Aumentar limite de likes por dia para tl3 (regular) multiplicando por esse número" tl4_additional_likes_per_day_multiplier: "Aumentar limite de likes por dia para tl4 (lideres) multiplicando por esse número" - num_spam_flags_to_block_new_user: "Se as publicações de um novo usuário receberem essa quantidade de sinalizações de spam de num_users_to_block_new_user usuários diferentes, esconder todos os seus posts e impedir publicação futura. 0 para desabilitar. " - num_users_to_block_new_user: "Se as publicações de um usuário novo receberem num_spam_flags_to_block_new_user sinalizações de spam dessa quantidade de usuários diferentes, esconder todos as suas publicações e impedir publicações futuras. 0 para desabilitar." - num_tl3_flags_to_block_new_user: "Se as publicações de um usuário novo tiverem essa quantidade de sinalizações de num_tl3_users_to_block_new_user usuários de nível de confiança 3 diferentes, esconder todas as publicações do usuário e prevenir postagem futura. 0 para desabilitar." - notify_mods_when_user_blocked: "Se um usuário for bloqueado de forma automática, enviar uma mensagem para todos os moderadores." flag_sockpuppets: "Se um novo usuário responder um tópico usando o mesmo endereço de IP do novo usário que começou o tópico, sinalize as duas publicações como um potencial spam." traditional_markdown_linebreaks: "Use quebras de linhas tradicionais em Markdown, as quais requerem dois espaços à direita para uma quebra de linha." post_undo_action_window_mins: "Número de minutos permitidos aos usuários para desfazerem uma ação recente em uma publicação (curtir, sinalizar, etc)" must_approve_users: "Os moderadores devem aprovar todas novas contas antes que elas sejam permitidas acessarem o site. CUIDADO: habilitar esta opção para um site já em funcionamento irá revogar o acesso para todos usuários já cadastrados que não são moderadores." maximum_session_age: "Usuário continuará logado por n horas desde a última visita" - ga_tracking_code: "OBSOLETO: Código de monitoramento do Google analytics (ga.js), por ex: UA-12345678-9; veja http://google.com/analytics" - ga_domain_name: "OBSOLETO: Nome de domínio do Google Analytics (ga.js), por ex: meusite.com; veja http://google.com/analytics" ga_universal_tracking_code: "Código de monitoramento Google Universal Analytics (analytics.js), ex: UA-12345678-9; veja http://google.com/analytics" ga_universal_domain_name: "Nome de domínio do Google Universal Analytics (analytics.js), ex: meusite.com; veja http://google.com/analytics" gtm_container_id: "ID do container do Gerenciador de Tags do Google. eg: GTM-ABCDEF" @@ -1288,12 +1281,6 @@ pt_BR: subject_template: "Nova conta suspensa" too_many_tl3_flags: subject_template: "Nova conta suspensa" - blocked_by_staff: - subject_template: "Conta temporariamente suspensa" - spam_post_blocked: - subject_template: "Novo usuário %{username} teve postagem bloqueada por repetidos links" - unblocked: - subject_template: "Conta não está mais suspensa" pending_users_reminder: subject_template: one: "1 usuário aguardando aprovação" diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index aa71025a2f..81379337b2 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -43,7 +43,6 @@ ro: auto_generated_email_error: "Apare când header-ul de 'precedență' este setat pe: list, junk, bulk sau auto_reply, sau atunci când orice alt header conține: auto-submitted, auto-replied sau auto-generated." no_body_detected_error: "Apare când nu am putut extrage corpul mesajului și nu existau fișiere atașate." inactive_user_error: "Apare când expeditorul nu este activ." - blocked_user_error: "Apare când expeditorul a fost blocat." bad_destination_address: "Apare când niciuna dintre adresele de email din câmpurile To/Cc/Bcc nu se potrivește cu o adresa de email expeditor configurată." strangers_not_allowed_error: "Apare când un utilizator a încercat să creeze un nou subiect într-o categorie în care nu este membru. " insufficient_trust_level_error: "Apare atunci când un utilizator a încercat să creeze un subiect nou într-o categorie pentru care nu are nivelul de încredere necesar." @@ -868,19 +867,12 @@ ro: tl2_additional_likes_per_day_multiplier: "Crește numărul maxim de aprecieri pe zi pentru (membrii cu) nivelul de încredere 2, multiplicând cu această valoare" tl3_additional_likes_per_day_multiplier: "Crește numărul maxim de aprecieri pe zi pentru (membrii cu) nivelul de încredere 3, multiplicând cu această valoare" tl4_additional_likes_per_day_multiplier: "Crește numărul maxim de aprecieri pe zi pentru (membrii cu) nivelul de încredere 4, multiplicând cu această valoare" - num_spam_flags_to_block_new_user: "Dacă postările unui nou utilizator primesc atâtea marcaje de avertizare ca spam de la num_users_to_block_new_user utilizatori diferiți, ascunde-i toate postările și împiedică-l să mai posteze. 0 pentru dezactivare." - num_users_to_block_new_user: "Dacă mesajele unui nou utilizator obțin num_spam_flags_to_block_new_user marcaje de avertizare de la tot atâția utilizatori diferiți, ascunde toate mesajele sale și blochează-i posibilitatea de a posta. 0 pentru dezactivare." - num_tl3_flags_to_block_new_user: "Dacă postările unui nou utilizator primesc atâtea marcaje de avertizare de la num_tl3_users_to_block_new_user de utilizatori diferiți cu nivel de încredere 3, ascunde toate postările sale și blochează-i posibilitatea de a posta pe viitor. 0 pentru dezactivare." - num_tl3_users_to_block_new_user: "Dacă postările unui utilizator nou primesc num_tl3_flags_to_block_new_user marcaje de avertizare de la tot atâția utilizatori diferiți cu nivelul de încredere 3, ascunde-i toate postările și blochează-i posibilitatea de a posta pe viitor. 0 pentru dezactivare." - notify_mods_when_user_blocked: "Dacă un utilizator este blocat automat, trimite un mesaj tuturor moderatorilor." flag_sockpuppets: "Dacă un utilizator nou răspunde unui subiect de la același IP ca utilizatorul ce a pornit subiectul, marchează ambele postări ca potențial spam." traditional_markdown_linebreaks: "Folosește întreruperi de rând tradiționale în Markdown, ceea ce necesită două spații pentru un capăt de rând. " post_undo_action_window_mins: "Numărul de minute în care utilizatorii pot anula acțiunile recente asupra unei postări (aprecieri, marcări cu marcaje de avertizare, etc)." must_approve_users: "Membrii echipei trebuie să aprobe toate conturile noilor utilizatori înainte ca aceștia să poată accesa site-ul. ATENȚIE: activarea acestei opțiuni pentru un site în producție va revoca accesul tuturor utilizatorilor care nu sunt membri ai echipei!" pending_users_reminder_delay: "Notifică moderatorii dacă noii utilizatori sunt în așteptarea aprobării de mai mult de atâtea ore. Setează la -1 pentru a dezactiva notificările." maximum_session_age: "Utilizatorul va rămâne autentificat pentru n ore de la ultima vizită" - ga_tracking_code: "ÎNVECHIT: Google analytics (ga.js) cod de urmărire, eg: UA-12345678-9; see http://google.com/analytics" - ga_domain_name: "ÎNVECHIT: numele domeniului Google Analytics (ga.js), de exemplu: siteulmeu.com ; vezi http://google.com/analytics" ga_universal_tracking_code: "Codul de urmărire Google Universal Analytics (analytics.js), ex: UA-12345678-9; vezi http://google.com/analytics" ga_universal_domain_name: "Numele de domeniu Google Universal Analytics (analytics.js), ex: mysite.com; vezi http://google.com/analytics" gtm_container_id: "ID-ul containerului Google Tag Manager. De ex: GTM-ABCDEF" @@ -1092,9 +1084,6 @@ ro: num_flags_to_close_topic: "Numărul minim de marcaje de avertizare active care este necesar pentru a suspenda un subiect în vederea intervenției" auto_respond_to_flag_actions: "Activează răspunsul automat la gestionarea unui marcaj de avertizare." min_first_post_typing_time: "Durata de timp minimă în milisecunde cât este nevoie ca un utilizator să tasteze la prima postare, dacă acest prag nu este atins postarea va intra automat în coada mesajelor care necesită aprobare. Valoarea 0 dezactivează (nerecomandat)" - auto_block_fast_typers_on_first_post: "Blochează automat utilizatorii care nu întrunesc min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Nivelul maxim de încredere pentru a-i bloca automat pe cei care scriu rapid la tastatură" - auto_block_first_post_regex: "Un regex care nu ține cont de majuscule și minuscule va determina prima postare a utilizatorului să fie blocată și trimisă în coada de aprobare. Exemplu: raging|a[bc]a va determina toate postările care conțin raging sau aba sau aca să fie blocate din prima. Se aplică doar la prima postare." reply_by_email_enabled: "Activează răspunsurile către subiecte prin email." reply_by_email_address: "șablon pentru răspuns prin email de intrare, de exemplu: %{reply_key}@reply.example.com sau răspunsuri+%{reply_key}@example.com" alternative_reply_by_email_addresses: "Listă de șabloane alternative pentru adresele de email primite cu răspunsuri prin email. Exemplu: %{reply_key}@reply.example.com|replies+%{reply_key}@example.com" @@ -1562,11 +1551,6 @@ ro: Ne pare rău dar mesajul tău către %{destination} (cu titlul %{former_title}) nu a mers. Contul tău asociat acestei adrese de email nu este activat. Te rugăm să îți activezi contul înainte de a trimite emailuri. - email_reject_blocked_user: - text_body_template: | - Ne pare rău dar mesajul tău către %{destination} (cu titlul %{former_title}) nu a mers. - - Contul asociat acestei adrese de email este blocat. email_reject_empty: text_body_template: | Ne pare rău dar mesajul tău către %{destination} (cu titlul %{former_title}) nu a mers. @@ -1607,63 +1591,8 @@ ro: Dacă există interfață utilizator web pentru contul tău POP, ar trebui să te autentifici pe ea și să îți verifici acolo setările. too_many_spam_flags: subject_template: "Cont nou suspendat" - text_body_template: | - Salut, - - Acesta este un mesaj automat de pe %{site_name} pentru a te informa că postările tale au fost temporar ascunse pentru că au fost marcate cu marcaje de avertizare de către comunitate.. - - Ca măsura de precauție, noului tău cont i s-a blocat posibilitatea de a crea noi răspunsuri sau subiecte până ce un un membru al echipei nu îl va verifica. Ne cerem scuze pentru inconveniență. - - Pentru informații suplimentare, te rugăm să citești [ghidul comunității](%{base_url}/guidelines). too_many_tl3_flags: subject_template: "Cont nou suspendat" - text_body_template: | - Salut, - - Acesta este un mesaj automat de pe %{site_name} pentru a te informa că ți-a fost suspendat contul din cauza unui număr mare de marcaje de avertizare primite de la comunitate. - - Ca măsura de precauție, noului tău cont i s-a blocat posibilitatea de a crea noi răspunsuri sau subiecte până ce un un membru al echipei nu îl va verifica. Ne cerem scuze pentru inconveniență. - - Pentru informații suplimentare, te rugăm să citești [ghidul comunității](%{base_url}/guidelines). - blocked_by_staff: - subject_template: "Cont suspendat temporar" - text_body_template: |+ - Salut, - - Acesta este un mesaj automat de pe %{site_name} pentru a te informa că ți-a fost suspendat temporar contul, ca măsură de precauție. - - Poți să continui să răsfoiești, dar nu vei mai putea să răspunzi sau să creezi subiecte până când un [membru al echipei](%{base_url}/about) nu îți verifică postările recente. Ne cerem scuze pentru inconveniență. - - Pentru informații suplimentare, te rugăm să citești [ghidul comunității](%{base_url}/guidelines). - - user_automatically_blocked: - subject_template: "Noul utilizator %{username} a fost blocat de marcajele de avertizare ale comunității." - text_body_template: | - Acesta este un mesaj automat. - - Noul utilizator [%{username}](%{user_url}) a fost blocat în mod automat din cauza că mai mulți utilizatori au marcat postarea(ările) lui %{username} cu cu marcaje de avertizare. - - Te rugăm [verifică marcajele de avertizare](%{base_url}/admin/flags). Dacă %{username} a fost blocat în mod incorect de la postare, dă click pe butonul deblocare din [pagina de administrare pentru acest utilizator](%{user_url}). - - Pragul poate fi schimbat prin `block_new_user` din setările site-ului. - spam_post_blocked: - subject_template: "Utilizatorul nou %{username} are postările blocate datorită adreselor repetate" - text_body_template: | - Acesta este un mesaj automat. - - Noul utilizator [%{username}](%{user_url}) a încercat să creeze postări multiple cu link-uri la %{domains}, dar aceste postări au fost blocate pentru a evita spam-ul. Utilizatorul poate încă să creeze noi subiecte dar care să nu link-uiască la %{domains}. - - Te rugăam [verifică acest utilizator](%{user_url}). - - Această setare poate fi schimbată prin `newuser_spam_host_threshold` și `white_listed_spam_host_domains` din setările site-ului. - unblocked: - subject_template: "Contul nu mai este în așteptare" - text_body_template: | - Salut, - - Acesta este un mesaj automat de pe %{site_name} pentru a te informa că - în urma verificărilor echipei - contul tău nu mai este suspendat. - - Acum poți să creezi din nou subiecte și postări. Îți mulțumim pentru răbdare. pending_users_reminder: subject_template: one: "un utilizator în așteptarea aprobării" diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index ad4cc23a68..dac234a494 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -46,7 +46,6 @@ ru: auto_generated_email_error: "Происходит когда заголовок \"приоритет\" установлен в: 'bulk', 'list', 'auto_reply', 'junk' или когда другой заголовок содержит флаги 'auto-submitted', 'auto-replied' или 'auto-generated'." no_body_detected_error: "Случается, когда мы не смогли извлечь текст письма и не было никаких вложений." inactive_user_error: "Происходит, когда отправитель неактивен." - blocked_user_error: "Происходит, когда отправитель заблокирован." bad_destination_address: "Происходит, когда ни один из адресатов, указанных в полях To/Cc/Bcc не совпадает со входящим адресом электронной почты." strangers_not_allowed_error: "Происходит когда пользователь пытается создать новую тему в разделе, участником которого он не является." insufficient_trust_level_error: "Происходит когда пользователь пытается создать новую тему в категории, недоступной ему по уровню доступа." @@ -883,7 +882,6 @@ ru: tl2_additional_likes_per_day_multiplier: "Увеличить лимит лайков в день для tl2 (member) до" tl3_additional_likes_per_day_multiplier: "Увеличить лимит лайков в день для tl3 (member) до" tl4_additional_likes_per_day_multiplier: "Увеличить лимит лайков в день для tl4 (leader) до" - notify_mods_when_user_blocked: "Отправить сообщение всем модераторам, если пользователь заблокирован автоматически." flag_sockpuppets: "Если новый пользователь отвечает на тему с того же IP адреса как и новый пользователь, кто начал тему, отметить оба их сообщения как возможный спам." traditional_markdown_linebreaks: "Использовать стандартный способ переноса строки в Markdown: строка должна заканчиваться двумя пробелами, чтобы перенос строки после нее сработал." post_undo_action_window_mins: "Количество минут, в течение которых пользователь может отменить действия, связанные с сообщениями: 'Мне нравится', 'Жалоба' и др." @@ -1363,16 +1361,6 @@ ru: К сожалению, ваше письмо к %{destination} с заголовком %{former_title} не может быть обработано. Система не смогла обнаружить тект сообщения в теле письма. Советуем убедится в том, что вы написали желаемый текст **в самом верху письма**. - blocked_by_staff: - title: "Заблокировано Персоналом" - subject_template: "Учётная запись временно заблокирована" - user_automatically_blocked: - title: "Автоматическая Блокировка Пользователя" - spam_post_blocked: - subject_template: "Сообщения нового пользователя %{username} заблокированы из-за повторяющихся ссылок" - unblocked: - title: "Разблокировано" - subject_template: "Учётная запись больше не заблокирована" pending_users_reminder: subject_template: one: "1 пользователь ожидает рассмотрение" diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index b8c9158b5d..8d4d6df900 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -40,7 +40,6 @@ sk: empty_email_error: "Nastane, keď prijmeme úplne prázdny mail." no_message_id_error: "Nastane, keď v maili chýba hlavička 'Message-Id'." inactive_user_error: "Nastane, keď odosielateľ nie je aktívny." - blocked_user_error: "Nastane, keď odosielateľ bol zablokovaný." insufficient_trust_level_error: "Nastane, keď sa používateľ pokúsi vytvoriť novú tému v kategórii, na ktorú nemajú dostatočný stupeň dôvery." topic_closed_error: "Nastane, keď príde odpoveď na uzavretú tému." errors: &errors @@ -755,13 +754,11 @@ sk: tl2_additional_likes_per_day_multiplier: "Zvýšiť počet páči sa mi na deň pre úroveň dôvery 2 (člen) vynásobením týmto číslom" tl3_additional_likes_per_day_multiplier: "Zvýšiť počet páči sa mi na deň pre úroveň dôvery 3 (bežný) vynásobením týmto číslom" tl4_additional_likes_per_day_multiplier: "Zvýšiť počet páči sa mi na deň pre úroveň dôvery 4 (vedúci) vynásobením týmto číslom" - notify_mods_when_user_blocked: "Ak je používateľ automaticky zablokovaný, pošli správu všetkým moderátorom." flag_sockpuppets: "Ak nový používateľ odpovedá na tému z rovnakej IP adresy, ako nový používateľ, ktorý danú tému vytvoril, označ oba ich príspevky ako potencionálny spam." traditional_markdown_linebreaks: "V Markdown použiť tradičné oddeľovače riadkov, čo vyžaduje dve koncové medzery ako oddeľovač riadku." post_undo_action_window_mins: "Počet minút počas ktorých môžu používatelia zrušiť poslednú akciu na príspevku (\"Páči sa\", označenie, atď..)." must_approve_users: "Obsluha musí povoliť účty všetkým novým používateľom skôr než im bude povolený prístup na stránku. UPOZORNENIE: zapnutie na živej stránke spôsobí zrušenie prístupu pre existujúcich používateľov, okrem obsluhy!" pending_users_reminder_delay: "Upozorni moderátora ak nový používateľ čaká na schválenie dlhšie ako tento počet hodín. Nastavte -1 pre vypnutie upozornenia." - ga_tracking_code: "ZASTARALÉ: Google analytics (ga.js) sledovací kód, napr.: UA-12345678-9; pozri http://google.com/analytics" ga_universal_tracking_code: "Google Universal Analytics (analytics.js) sledovací kód, napr: UA-12345678-9; pozri http://google.com/analytics" ga_universal_domain_name: "Google Universal Analytics (analytics.js) názov domény, napr: mysite.com; pozri http://google.com/analytics" allow_moderators_to_create_categories: "Povoliť moderátorom vytváranie nových kategórií" @@ -937,9 +934,6 @@ sk: num_flags_to_close_topic: "Minimálny počet aktívnych príznakov potrebný na automatické pozastavenie témy pre intervenciu." auto_respond_to_flag_actions: "Zapnúť automatickú odpoveď pri odstránení označenia." min_first_post_typing_time: "Minimalny čas písania prvého príspevku v milisekundách, ak je čas kratší ako je limit, príspevok bude automaticky zaradený do fronty na schválenie. Nastav 0 pre vypnutie (Neoporúča sa)" - auto_block_fast_typers_on_first_post: "Automaticky blokovať užívateľov, ktorí nesplnili podmienku minimálneho času pre prvý príspevok min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Maximálny level dôvery pre automatické blokovanie rýchlo-písačov" - auto_block_first_post_regex: "Regulárny výraz bez rozlíšenia veľkých a malých písmen, ktorého splnenie zablokuje prvý príspevok užívateľa a pošle ho do fronty na schválenie. Napriklad: raging|a[bc]a , zablokuje príspevky obsahujúce raging alebo aba alebo aca. Aplikuje sa len na prvý príspevok. " reply_by_email_enabled: "Povoliť odpoveď na tému prostredníctvom emailu." reply_by_email_address: "Šablóna pre príchodziu emailovú adresu emailových odpovedí. Napríklad : %{reply_key}@reply.example.com or replies+%{reply_key}@example.com" disable_emails: "Zabrániť Discoursu v odosielaní akýchkoľvek e-mailov" @@ -1203,11 +1197,6 @@ sk: Ľutujeme, ale Vaša emailová správa na %{destination} (titled %{former_title}) nefungovala. Váš účet asociovaný s touto emailovou adresou nie je aktivovaný. Prosím aktivujte svoj účet pred posielaním emailov. - email_reject_blocked_user: - text_body_template: | - Ľutujeme, ale Vaša emailová správa na %{destination} (titled %{former_title}) nefungovala. - - Váš účet asociovaný s touto emailovou adresou bol zablokovaný. email_reject_empty: text_body_template: | Ľutujeme, ale Vaša emailová správa na %{destination} (titled %{former_title}) nefungovala. @@ -1239,8 +1228,6 @@ sk: Ľutujeme, ale Vaša emailová správa na %{destination} (titled %{former_title}) nefungovala. Akcia uverejnenia nebola rozpoznaná. Prosíme skúste znovu alebo, ak sa chovanie opakuje, uverejnite pomocou webstránky. - spam_post_blocked: - subject_template: "Nový uživateľ %{username} je zablokovaný kôli opakovaným odkazom" pending_users_reminder: subject_template: one: "1 užívateľ čaká na schválenie" diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index f92b0e3f92..49c82d02ec 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -599,7 +599,6 @@ sq: tl2_additional_likes_per_day_multiplier: "Increase limit of likes per day for tl2 (member) by multiplying with this number" tl3_additional_likes_per_day_multiplier: "Increase limit of likes per day for tl3 (regular) by multiplying with this number" tl4_additional_likes_per_day_multiplier: "Increase limit of likes per day for tl4 (leader) by multiplying with this number" - notify_mods_when_user_blocked: "If a user is automatically blocked, send a message to all moderators." flag_sockpuppets: "If a new user replies to a topic from the same IP address as the new user who started the topic, flag both of their posts as potential spam." traditional_markdown_linebreaks: "Use traditional linebreaks in Markdown, which require two trailing spaces for a linebreak." post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)." @@ -760,9 +759,6 @@ sq: num_flags_to_close_topic: "Minimum number of active flags that is required to automatically pause a topic for intervention" auto_respond_to_flag_actions: "Enable automatic reply when disposing a flag." min_first_post_typing_time: "Minimum amount of time in milliseconds a user must type during first post, if threshold is not met post will automatically enter the needs approval queue. Set to 0 to disable (not recommended)" - auto_block_fast_typers_on_first_post: "Automatically block users that do not meet min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Maximum trust level to auto block fast typers" - auto_block_first_post_regex: "Case insensitive regex that if passed will cause first post by user to be blocked and sent to approval queue. Example: raging|a[bc]a , will cause all posts containing raging or aba or aca to be blocked on first. Only applies to first post." reply_by_email_enabled: "Enable replying to topics via email." reply_by_email_address: "Template for reply by email incoming email address, for example: %{reply_key}@reply.example.com or replies+%{reply_key}@example.com" disable_emails: "Prevent Discourse from sending any kind of emails" @@ -983,8 +979,6 @@ sq: We're sorry, but your email message to %{destination} (titled %{former_title}) didn't work. We couldn't find your reply in the email. **Make sure your reply is at the top of the email** -- we can't process inline replies. - spam_post_blocked: - subject_template: "New user %{username} posts blocked due to repeated links" pending_users_reminder: subject_template: one: "1 user waiting for approval" diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 8cc2c66904..229beda454 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -43,7 +43,6 @@ sv: auto_generated_email_error: "Händer när 'precedence'-rubriken är satt till: list, skräp, mass eller auto-reply, eller när någon annan rubrik innehåller: auto-submitted, auto-replied eller auto-generated." no_body_detected_error: "Händer när vi inte kunde extrahera någon huvuddel och det inte fanns några bilagor." inactive_user_error: "Händer när sändaren inte är aktiv." - blocked_user_error: "Händer när sändare har blivit blockerad." bad_destination_address: "Händer när inga av e-postadresserna i Till/Cc/Bcc-fälten matchar en konfigurerad inkommande e-postadress." strangers_not_allowed_error: "Händer när en användare försöker skapa ett nytt ämne i en kategori som användare inte är en medlem av." insufficient_trust_level_error: "Händer när en användare försöker skapa ett nytt ämne i en kategori som användare saknar förtroendenivå för." @@ -808,11 +807,6 @@ sv: tl2_additional_likes_per_day_multiplier: "Höj gränsen för antal gillningar per dag för användare med förtroendenivå 2 (medlem) genom att multiplicera med det här numret " tl3_additional_likes_per_day_multiplier: "Höj gränsen för antal gillningar per dag för användare med förtroendenivå 3 (regelbundna) genom att multiplicera med det här numret" tl4_additional_likes_per_day_multiplier: "Höj gränsen för antal gillningar per dag för användare med förtroendenivå 4 (ledare) genom att multiplicera med det här numret" - num_spam_flags_to_block_new_user: "Dölj alla inlägg från en ny användare och förhindra framtida inlägg om ett av användarens inlägg får så här många flaggningar för spam från num_users_to_block_new_user olika användare. Ange 0 för att inaktivera." - num_users_to_block_new_user: "Dölj alla inlägg och förhindra framtida inlägg från en ny användare om ett av användarens inlägg får num_spam_flags_to_block_new_user flaggningar för spam från så här många olika användare. Ange 0 för att inaktivera." - num_tl3_flags_to_block_new_user: "Dölj alla inlägg från en ny användare och förhindra framtida inlägg om ett av användarens inlägg får så här många flaggningar från num_tl3_users_to_block_new_user olika användare med förtroendenivå 3. Ange 0 för att inaktivera." - num_tl3_users_to_block_new_user: "Dölj alla inlägg och förhindra framtida inlägg från en ny användare om ett av användarens inlägg får num_tl3_flags_to_block_new_user flaggningar från så här många olika användare med förtroendenivå 3. Ange 0 för att inaktivera." - notify_mods_when_user_blocked: "Om en användare blockeras automatiskt, skicka ett meddelande till alla moderatorer." flag_sockpuppets: "Flagga båda användarnas inlägg som potentiell skräppost om en ny användare svarar på ett ämne från samma IP-adress som den andra nya användaren som skapade ämnet." traditional_markdown_linebreaks: "Använd vanliga radmatningar i Markdown, vilka kräver 2 avslutande mellanslag för en radmatning." post_undo_action_window_mins: "Antal minuter som en användare tillåts att upphäva handlingar på ett inlägg som gjorts nyligen (gillning, flaggning osv)." @@ -1026,9 +1020,6 @@ sv: num_flags_to_close_topic: "Minsta antal aktiva flaggor som krävs för att automatiskt pausa ett ämne för ingripande" auto_respond_to_flag_actions: "Aktivera automatiskt svar vid hantering av en flagga." min_first_post_typing_time: "Minsta tid i millisekunder en användare måste skriva vid komponerandet av sitt första inlägg. Om gränsen inte är uppnådd så kommer inlägget automatiskt att hamna i kön av saker som behöver granskas. Ange 0 för att inaktivera (rekommenderas ej)" - auto_block_fast_typers_on_first_post: "Blockera automatiskt användare som inte når upp till min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Högsta förtroendenivå för att automatiskt blockera snabba författare" - auto_block_first_post_regex: "Skriftlägeskänsligt regex som om godkänt kommer att orsaka att första inlägget av en användare blockeras och skickas till kön för saker som behöver granskas. Exempel: raging|a[bc]a kommer att orsaka att alla inlägg som börjar med raging eller aba eller aca blockeras vid första försöket. Gäller endast användarens första inlägg. " reply_by_email_enabled: "Aktivera möjlighet att svara på ämnen via e-post." reply_by_email_address: "Mall för inkommande e-postadress för svar via e-post, till exempel: %{reply_key}@svar.exempel.se eller svar+%{reply_key}@exempel.se" disable_emails: "Förhindra Discourse från att skicka någon form av e-post" @@ -1399,12 +1390,6 @@ sv: Tyvärr har ditt e-postmeddelande till %{destination} (med ämnet %{former_title}) inte kunnat skickas. Kontot associerat till den här e-postadressen är inte aktiverat. Var god se till att ditt konto är aktiverat innan du skickar e-post. - email_reject_blocked_user: - title: "Email avslås blockerad användare" - text_body_template: | - Tyvärr har ditt e-postmeddelande till %{destination} (med ämnet %{former_title}) inte kunnat skickas. - - Kontot associerat till den här e-postadressen har blockerats. email_reject_reply_user_not_matching: title: "Emailet avslås användare hittas ej" email_reject_no_account: @@ -1469,66 +1454,8 @@ sv: too_many_spam_flags: title: "För många skräppost flaggningar" subject_template: "Nytt konto pausat" - text_body_template: | - Hej! - - Det här är ett automatiserat meddelande från %{site_name} för att informera dig om att dina inlägg temporärt har dolts på grund av att de flaggades av medlemmar. - - Som en förebyggande åtgärd har ditt nya konto blockerats från att skapa nya inlägg eller ämnen tills det har granskats av personal. Vi ber om ursäkt för besväret. - - För ytterligare information, se våra [riktlinjer](%{base_url}/guidelines). too_many_tl3_flags: subject_template: "Nytt konto pausat" - text_body_template: | - Hej! - - Det här är ett automatiserat meddelande från %{site_name} för att informera dig om att dina inlägg temporärt har dolts på grund av att de flaggades av ett stort antal medlemmar. - - Som en förebyggande åtgärd har ditt nya konto blockerats från att skapa nya inlägg eller ämnen tills det har granskats av personal. Vi ber om ursäkt för besväret. - - För ytterligare information, se våra [riktlinjer](%{base_url}/guidelines). - blocked_by_staff: - title: "Blockerat av personal" - subject_template: "Konto tillfälligt pausat" - text_body_template: | - Hej! - - Det här är ett automatiserat meddelande från %{site_name} för att informera dig om att ditt konto tillfälligt har pausats som en försiktighetsåtgärd. - - Det går bra att fortsätta besöka sidan men du kommer inte att kunna skriva inlägg eller skapa nya ämnen förrän [personalen](%{base_url}/about) granskar dina senaste inlägg. Vi ber om ursäkt för besväret. - - För mer information, se våra [riktlinjer](%{base_url}/guidelines). - user_automatically_blocked: - title: "Användare blir automatiskt blockerad" - subject_template: "Ny användare %{username} blockerad på grund av flaggningar från forumet" - text_body_template: | - Det här är ett automatiserat meddelande. - - Den nya användaren [%{username}](%{user_url}) blockerades automatiskt på grund av att ett flertal medlemmar flaggat användaren %{username}s inlägg. - - Var vänlig och [granska inläggen](%{base_url}/admin/flags). Om %{username} har blockerats på felaktig grund, var god och klicka på avblockeringsknappen [på administrationssidan för den här användaren](%{user_url}). - - Du kan också modifiera inställningen `block_new_user` via Webbplatsinställningar. - spam_post_blocked: - title: "Skräppost är blockerat" - subject_template: "Ny användare %{username} har fått meddelanden blockerade pga. upprepade länkar " - text_body_template: | - Det här är ett automatiserat meddelande. - - Den nya användaren [%{username}](%{user_url}) försökte skriva flera inlägg med länkar till %{domains}, men inläggen blockerades för att undvika spam. Användaren kan fortfarande skriva nya inlägg som inte länkar till %{domains}. - - Var vänlig och [granska användaren](%{user_url}). - - Du kan också modifiera webbplatsinställningarna `newuser_spam_host_threshold`och `white_listed_spam_host_domains`. - unblocked: - title: "Avblockerat" - subject_template: "Konto ej längre pausat" - text_body_template: | - Hej! - - Det här är ett automatiserat meddelande från %{site_name} för att informera dig om att ditt konto inte längre är pausat efter att personal har granskat ärendet. - - Du kan nu skriva inlägg och skapa nya ämnen igen. Tack för ditt tålamod. pending_users_reminder: title: "Påminnelse om väntande användare" subject_template: diff --git a/config/locales/server.th.yml b/config/locales/server.th.yml index 8b8a4de555..e17b6d18d1 100644 --- a/config/locales/server.th.yml +++ b/config/locales/server.th.yml @@ -33,7 +33,6 @@ th: incoming: errors: inactive_user_error: "เกิดขึ้นเมื่อผู้ส่งยังไม่มีการใช้งาน" - blocked_user_error: "เกิดขึ้นเมื่อผู้ใช้งานโดนบล็อค" errors: &errors messages: invalid: ไม่ถูกต้อง diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index fbd877771a..b463ead79e 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -32,6 +32,9 @@ tr_TR: disable_remote_images_download_reason: "Yeterli disk alanı kalmaması sebebiyle uzaktan görüntü indirme devre dışı bırakıldı." anonymous: "Anonim" remove_posts_deleted_by_author: "Yazar tarafından silindi" + themes: + bad_color_scheme: "Temayı güncelleyemiyorum, geçersiz renk şeması" + other_error: "Temanın güncellenmesi sırasında bir sorun oluştu" emails: incoming: default_subject: "Konuya bir başlık konulmalı" @@ -40,7 +43,6 @@ tr_TR: empty_email_error: "Bize ulaşan ham posta boş olduğunda olur." no_message_id_error: "E-postanın 'Message-Id' başlığı olmadığında olur." inactive_user_error: "Gönderici etkin olmadığında olur." - blocked_user_error: "Gönderici engellendiğinde olur." errors: &errors format: '%{attribute} %{message}' messages: @@ -107,6 +109,7 @@ tr_TR: embed: start_discussion: "Tartışma Başlat" continue: "Tartışmaya Devam Et" + referer: "Yönlendiren:" more_replies: other: "%{count} cevap daha" loading: "Tartışma Yükleniyor..." @@ -151,6 +154,12 @@ tr_TR: latest: "En son konular" hot: "Sıcak konular" top: "En iyi konular" + top_all: "Tüm zamanlarda en iyi konular" + top_yearly: "Yıllık en iyi konular" + top_quarterly: "Üç aylık en iyi konular" + top_monthly: "Aylık en iyi konular" + top_weekly: "Haftalık en iyi konular" + top_daily: "Günlük en iyi konular" posts: "Son gönderiler" private_posts: "Son özel iletiler" group_posts: "%{group_name} tarafından son gönderiler" @@ -700,15 +709,12 @@ tr_TR: tl2_additional_likes_per_day_multiplier: "Güvenlik seviyesi 2 (Üye) olanlar için günlük beğeni limitini bu rakamla çarparak artır" tl3_additional_likes_per_day_multiplier: "Güven seviyesi 3 (Müdavim) olanlar için günlük beğeni limitini bu rakamla çarparak artır" tl4_additional_likes_per_day_multiplier: "Güvenlik seviyesi 4 (Lider) olanlar için günlük beğeni limitini bu rakamla çarparak artır" - notify_mods_when_user_blocked: "Eğer bir kullanıcı otomatik olarak engellendiyse, tüm moderatörlere ileti yolla." flag_sockpuppets: "Eğer, yeni kullanıcı konuya, konuyu başlatan yeni kullanıcı ile aynı IP adresinden cevap yazarsa, her iki gönderiyi de potansiyel istenmeyen olarak bildir. " traditional_markdown_linebreaks: "Markdown'da, satır sonundan önce yazının sağında iki tane boşluk gerektiren, geleneksel satır sonu metodunu kullan." post_undo_action_window_mins: "Bir gönderide yapılan yeni eylemlerin (beğenme, bildirme vb) geri alınabileceği zaman, dakika olarak" must_approve_users: "Siteye erişimlerine izin verilmeden önce tüm yeni kullanıcı hesaplarının görevliler tarafından onaylanması gerekir. UYARI: yayındaki bir site için bunu etkinleştirmek görevli olmayan hesapların erişimini iptal edecek." pending_users_reminder_delay: "Belirtilen saatten daha uzun bir süredir onay bekleyen yeni kullanıcılar mevcutsa moderatörleri bilgilendir. Bilgilendirmeyi devre dışı bırakmak için -1 girin." maximum_session_age: "Kullanıcı son ziyaretinden bu yana n saat boyunca giriş yapmış olarak kalacak" - ga_tracking_code: "ESKİ: Google analiz (ga.js) takip kodu, ör:UA-12345678-9; bakınız http://google.com/analytics" - ga_domain_name: "ESKİ: Google analiz (ga.js) alan adı, ör: benimsitem.com; bakınız http://google.com/analytics" ga_universal_tracking_code: "Google Universal Analytics (analytics.js) takip kodu, ör: UA-12345678-9; bakınız http://google.com/analytics" ga_universal_domain_name: "Google Universal Analytics (analytics.js) alan adı, ör: mysite.com; bakınız http://google.com/analytics" enable_escaped_fragments: "Eğer bir ağ gezgini tespit edilmezse Google'ın Ajax-Crawling API'sine dönün. Dahası için şuraya bakın : https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" @@ -899,9 +905,6 @@ tr_TR: num_flags_to_close_topic: "Bir konunun moderatör müdahalesi için otomatik olarak durdurulmadan önce alması gereken en az etkin bildirim sayısı " auto_respond_to_flag_actions: "Bir bildirim kaldırırken otomatik olarak cevaplamayı etkinleştir" min_first_post_typing_time: "Milisaniye olarak bir kullanıcının ilk gönderisini yazarken geçmesi gereken en küçük süre, bu süre geçmezse gönderi otomatik olarak onaylanma kuyruğuna girer. Devre dışı bırakmak için 0'a ayarlayın (tavsiye edilmez)." - auto_block_fast_typers_on_first_post: "min_first_post_typing_time'ı karşılamayan kullanıcıları otomatik olarak engelle." - auto_block_fast_typers_max_trust_level: "Hızlı yazıcıları otomatik engellemek için en büyük güven seviyesi." - auto_block_first_post_regex: "Eğer eşleşirse kullanıcının ilk gönderisinin engellenmesi ve onaylanma kuyruğuna gitmesine neden olan harf büyüklüğü duyarsız düzenli ifade. Örnek: reklam|a[bc]a düzenli ifadesi reklam, aba ya da aca içeren ilk gönderilerin engellenmesi ve onaylanma kuyruğuna gitmesine neden olacaktır. Sadece ilk gönderiler için geçerlidir." reply_by_email_enabled: "Konulara e-posta üzerinden cevap yazmayı etkinleştir." reply_by_email_address: "Email ile cevapla özelliği için gelen e-posta adresi şablonu, örnek: %{reply_key}@reply.example.com or replies+%{reply_key}@example.com" disable_emails: "Discourse'un herhangi bir e-posta göndermesine izin verme" @@ -1249,30 +1252,8 @@ tr_TR: Üzgünüz, ama %{destination} (titled %{former_title}) adresine göndermeye çalıştığınız e-posta başırısız oldu. E-postada herhangi bir cevap bulamadık. **Cevabınızın e-posta'nın en üstünde yer aldığından emin olun** -- aralardaki cevapları işleyemiyoruz. too_many_spam_flags: subject_template: "Yeni hesap askıda" - text_body_template: | - Merhaba, - - Bu, %{site_name} adresinden size gönderilen, topluluk tarafından bildirildiği için kontrol edilmek üzere gizlenen gönderinizi bildiren otomatik bir iletidir. - - Bir önlem olarak, bir yetkili incelemesine kadar yeni hesabınızdan yeni cevaplar veya konular oluşturmanız engellendi. Bu rahatsızlık için üzgünüz. - - Ek bilgi için, [topluluk yönergelerine](%{base_url}/guidelines) başvurun. too_many_tl3_flags: subject_template: "Yeni hesap askıda" - blocked_by_staff: - subject_template: "Hesap geçiçi olarak askıda" - user_automatically_blocked: - subject_template: "Yeni kullanıcı %{username} topluluk bildirimleri tarafından engellendi" - spam_post_blocked: - subject_template: "Üst üste aynı bağlantıların paylaşılmasından ötürü %{username} adlı yeni kullanıcının gönderileri engelledi" - unblocked: - subject_template: "Hesap artık askıda değil" - text_body_template: | - Merhaba, - - Bu otomatik ileti %{site_name} forumunda, görevli incelemesinin ardından hesabınızın artık askıda olmadığını bilmeniz amacıyla gönderilmiştir. - - Şimdi tekrar gönderi ve konular oluşturabilirsiniz. Sabrınız için teşekkürler. pending_users_reminder: subject_template: other: "%{count} kullanıcı onay bekliyor" diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index 92fa7153c4..a2bc6a3671 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -19,7 +19,6 @@ uk: incoming: errors: inactive_user_error: "Таке трапляється, якщо відправник не активний" - blocked_user_error: "Таке трапляється, якщо відправника було заблоковано" errors: &errors messages: accepted: має бути прийнятий @@ -311,7 +310,6 @@ uk: email_custom_headers: "Список заголовків електронної пошти, розділених вертикальною рискою" enable_long_polling: "Message bus used for notification can use long polling" anon_polling_interval: "How often should anonymous clients poll in milliseconds" - notify_mods_when_user_blocked: "Якщо користувача автоматично заблоковано, надіслати повідомлення всім модераторам." ga_universal_tracking_code: "Код відстеження Google Universal Analytics (analytics.js), напр.: UA-12345678-9; див. http://google.com/analytics" ga_universal_domain_name: "Доменне ім'я Google Universal Analytics (analytics.js), напр.: mysite.com; див. http://google.com/analytics" post_menu: "Визначає, які елементи з'являються в меню допису, та в якому порядку. Наприклад, like|edit|flag|delete|share|bookmark|reply" diff --git a/config/locales/server.vi.yml b/config/locales/server.vi.yml index b99d7016ef..5241eb852d 100644 --- a/config/locales/server.vi.yml +++ b/config/locales/server.vi.yml @@ -37,7 +37,6 @@ vi: no_message_id_error: "Xảy ra khi thư không có tiêu đề." auto_generated_email_error: "Xảy ra khi tiêu đề 'ưu tiên' được đặt là: danh sách, rác, theo lô hoặc tự động trả lời, hoặc khi có tiêu đề khác bao gồm: tự động gửi, tự động trả lời hoặc tự động tạo." inactive_user_error: "Xảy ra khi người gửi không hoạt động." - blocked_user_error: "Xảy ra khi người gửi đã bị chặn." bad_destination_address: "Xảy ra khi không có địa chỉ email trong các trường Tới/Cc/Bcc khớp với địa chỉ email đến đã được cấu hình." strangers_not_allowed_error: "Xảy ra khi người dùng cố tạo chủ đề mới trong một danh mục mà họ không phải là thành viên." insufficient_trust_level_error: "Xảy ra khi người dùng cố tạo chủ đề mới trong một danh mục mà họ chưa đủ cấp độ tin cậy." @@ -675,7 +674,6 @@ vi: tl2_additional_likes_per_day_multiplier: "Tăng giới hạn thích mỗi ngày cho mức độ tin tưởng 2 (thành viên) bằng cách nhân với số này" tl3_additional_likes_per_day_multiplier: "Tăng giới hạn thích mỗi ngày cho mức độ tin tưởng 3 (bình thường) bằng cách nhân với số này" tl4_additional_likes_per_day_multiplier: "Tăng giới hạn thích mỗi ngày cho mức độ tin tưởng 4 (dẫn đầu) bằng cách nhân với số này" - notify_mods_when_user_blocked: "Nếu một thành viên được khóa tự động, gửi tin nhắn đến tất cả các điều hành viên." flag_sockpuppets: "Nếu thành viên mới trả lời chủ đề có cùng địa chỉ IP với thành viên mới tạo chủ đề, đánh dấu các bài viết của họ là spam tiềm năng." traditional_markdown_linebreaks: "Sử dụng ngắt dòng truyền thống trong Markdown, đòi hỏi hai khoảng trống kế tiếp cho một ngắt dòng." post_undo_action_window_mins: "Số phút thành viên được phép làm lại các hành động gần đây với bài viết (like, đánh dấu...)." @@ -872,9 +870,6 @@ vi: num_flags_to_close_topic: "Số lượng tối thiểu đánh dấu hoạt động để tự động tạm dừng một chủ đề để can thiệp." auto_respond_to_flag_actions: "Kích hoạt tính năng trả lời tự động khi xử lý một đánh dấu." min_first_post_typing_time: "Số lượng thời gian tối thiểu theo phần ngàn giây mà thành viên phải nhập trong bài viết đầu tiên, nếu ngưỡng không được đáp ứng sẽ tự động nhập vào hàng chờ duyệt. Đặt là 0 để tắt (không khuyến nghị)" - auto_block_fast_typers_on_first_post: "Tự động chặn người dùng không đáp ứng min_first_post_typing_time" - auto_block_fast_typers_max_trust_level: "Cấp độ tin cậy tối đa để tự động chặn những người đánh máy quá nhanh" - auto_block_first_post_regex: "Regex trường hợp không nhạy cảm để nếu vượt qua sẽ khóa bài viết đầu tiên của thành viên và gửi tới chờ duyệt. Ví dụ: raging|a[bc]a , sẽ khóa tất cả các bài viết đầu tiên có chứa raging, aba hoặc aca. Chỉ áp dụng cho các bài viết đầu tiên." reply_by_email_enabled: "Cho phép trả lời chủ đề qua email." reply_by_email_address: "Mẫu để trả lời cho các địa chỉ email đến, ví dụ: %{reply_key}@reply.example.com hoặc replies+%{reply_key}@example.com" disable_emails: "Ngăn chặn Discourse gửi thêm bất kỳ email nào" @@ -1128,11 +1123,6 @@ vi: Xin lỗi, tin nhắn email bạn gửi tới %{destination} (tiêu đề %{former_title}) không được thực hiện. Tài khoản của bạn liên kết với địa chỉ email này chưa được kích hoạt. Xin hãy kích hoạt tài khoản của bạn trước khi gửi email. - email_reject_blocked_user: - text_body_template: | - Xin lỗi, tin nhắn email bạn gửi tới %{destination} (tiêu đề %{former_title}) không được thực hiện. - - Tài khoản của bạn liên kết với địa chỉ email này đã bị chặn. email_reject_empty: text_body_template: | Xin lỗi, tin nhắn email bạn gửi tới %{destination} (tiêu đề %{former_title}) không được thực hiện. @@ -1171,25 +1161,6 @@ vi: Hãy chắc chắn rằng bạn đã cấu hình đúng các thông tin POP trong [thiết lập website](%{base_url}/admin/site_settings/category/email). Nếu có một giao diện web cho các tài khoản email POP, bạn có thể cần phải đăng nhập vào website và kiểm tra các thiết lập này. - user_automatically_blocked: - text_body_template: | - Đây là tin nhắn tự động. - - Tài khoản mới [%{username}](%{user_url}) đã được tự động chặn do nhiều thành viên đã gắn cờ các bài viết của %{username}. - - Hãy [xem lại các cờ](%{base_url}/admin/flags). Nếu %{username} không đáng bị chặn gửi bài, click nút gỡ bỏ trên [trang quản trị thành viên](%{user_url}). - - Ngưỡng này có thể thay đổi trong thiết lập `block_new_user` của website. - spam_post_blocked: - subject_template: "Các bài viết của tài khoản mới %{username} đã bị chặn do lặp đi lặp lại các liên kết" - text_body_template: | - Đây là tin nhắn tự động. - - Tài khoản mới [%{username}](%{user_url}) đã nhiều lần tạo bài viết chứa các liên kết tới %{domains}, nhưng những bài viết này đã bị chặn để tránh spam. Thành viên vẫn có thể tạo bài viết mới mà không liên kết tới %{domains}. - - Hãy [xem xét thành viên](%{user_url}). - - Có thể thay đổi điều này trong thiết lập `newuser_spam_host_threshold` và `white_listed_spam_host_domains` của website. pending_users_reminder: subject_template: other: "%{count} thành viên đang chờ duyệt" diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 223772bffa..4822f2164b 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -48,7 +48,6 @@ zh_CN: no_body_detected_error: "无法解析正文并且没有附件。" no_sender_detected_error: "当无法在来源信息中找到有效邮件地址时发生。" inactive_user_error: "发送者未激活。" - blocked_user_error: "发送者被封禁。" bad_destination_address: "发至/抄送/密送栏中的所有邮件都不匹配进站邮件地址。" strangers_not_allowed_error: "用户尝试在非成员分类中创建主题。" insufficient_trust_level_error: "用户尝试在用户等级不满足分类要求时发帖。" @@ -128,6 +127,7 @@ zh_CN: not_logged_in: "你需要登录后才能这么做。" not_found: "找不到请求的地址或资源。" invalid_access: "你没有权限查看请求的资源。" + invalid_api_credentials: "你没有权限查看请求的资源。API 用户名或者秘钥无效。" read_only_mode_enabled: "这个站点正处于只读模式。交互功能已禁用。" reading_time: "阅读时间" likes: "赞" @@ -597,6 +597,9 @@ zh_CN: short_description: '为该贴投票' long_form: '已给本帖投票' user_activity: + no_default: + self: "你还没有活动。" + others: "无活动。" no_bookmarks: self: "你没有收藏帖子,收藏帖子能让你之后轻松找到他们。" others: "没有收藏。" @@ -622,7 +625,7 @@ zh_CN: email_title: '“{title}”主题需要你的关注' email_body: "%{link}\n\n%{message}" flagging: - you_must_edit: '

    你的帖子被社区成员标记了。请查看你的私信

    ' + you_must_edit: '

    你的帖子被社区标记了。请查看你的消息

    ' user_must_edit: '

    你的帖子已经被社区标记并被临时隐藏。

    ' archetypes: regular: @@ -881,7 +884,8 @@ zh_CN: digest_logo_url: "邮件摘要中使用的标志图像。应为长方形。不能使用 SVG 图片。如果留空,将使用 `logo_url`。" logo_small_url: "在你站点左上角的小号标志图片,应为正方形,当滚动时才可见。如果留空,将显示主页图标。" favicon_url: "你的站点图标(favicon),参考 http://zh.wikipedia.org/wiki/Favicon,如果要正确地通过 CDN 分发,它必须是 png 图片" - mobile_logo_url: "移动站点左上角的固定标志图片。应为正方形。如果留空,`logo_url` 将被使用。如:http://example.com/uploads/default/logo.png" + mobile_logo_url: "站点移动版本的自定义标志 url。如果留空,则使用`logo_url`。例如: http://example.com/uploads/default/logo.png" + large_icon_url: "Android 上用于标志和启动屏图标。推荐 512px x 512px 大小。" apple_touch_icon_url: "Apple 触摸设备使用的图标。推荐 144x144 大小。" notification_email: "这个表格:被用于发送所有重要系统邮件的邮箱地址。指定的域名必须正确设置 SPF、DKIM 和反向 PTR 记录以发送邮件。" email_custom_headers: "一个逗号分离的自定义邮件头部" @@ -908,11 +912,6 @@ zh_CN: tl2_additional_likes_per_day_multiplier: "增加信任等级2(成员)的赞限制,可提供一个乘数与原始值相乘" tl3_additional_likes_per_day_multiplier: "增加信任等级3(常规)的每日赞限制,将此设置项与原始值相乘" tl4_additional_likes_per_day_multiplier: "增加信任等级4(资深)的赞限制,可提供一个乘数与原始值相乘" - num_spam_flags_to_block_new_user: "如果新用户的帖子被 num_users_to_block_new_user 个不同用户标记,隐藏他们的所有帖子并不让其继续发表内容。0 为禁用。" - num_users_to_block_new_user: "如果新用户的帖子被不同用户 num_spam_flags_to_block_new_user 次标记为垃圾信息,隐藏他们的所有帖子并不让其继续发表内容。0 为禁用。" - num_tl3_flags_to_block_new_user: "如果新用户的帖子被 num_tl3_users_to_block_new_user 个不同的信任等级3的用户标记,隐藏他们的所有帖子并不让其继续发表内容。0 为禁用。" - num_tl3_users_to_block_new_user: "如果新用户的帖子被不同的信任等级3的用户标记 num_tl3_flags_to_block_new_user 次,隐藏他们的所有帖子并不让其继续发表内容。0 为禁用。" - notify_mods_when_user_blocked: "如果一个用户被自动封禁了,发送一个私信给所有管理员。" flag_sockpuppets: "如果一个新用户开始了一个主题,并且同时另一个新用户以同一个 IP 在该主题回复,他们所有的帖子都将被自动标记为垃圾。" traditional_markdown_linebreaks: "在 Markdown 中使用传统换行符,即用两个尾随空格来换行" enable_markdown_typographer: "使用基本的排版规则来提高文本段落的可读性,用符号替换 (c) (tm) 等,减少问号等等" @@ -920,14 +919,13 @@ zh_CN: must_approve_users: "新用户在被允许访问站点前需要由管理人员批准。警告:在运行的站点中启用将解除所有非管理人员用户的访问权限!" pending_users_reminder_delay: "如果新用户等待批准时间超过此小时设置则通知版主。设置 -1 关闭通知。" maximum_session_age: "用户自访问后可维持登录 n 小时" - ga_tracking_code: "废弃:Google 分析追踪代码(ga.js),例如:UA-12345678-9。参考 http://google.com/analytics" - ga_domain_name: "废弃:Google 分析域名(ga.js),例如:mysite.com;参考 http://google.com/analytics" ga_universal_tracking_code: "Google 通用分析追踪代码(analytics.js)追踪代码,例如:UA-12345678-9;参考 http://google.com/analytics" ga_universal_domain_name: "Google 通用分析域名(analytics.js)追踪代码,例如:mysite.com;参考 http://google.com/analytics" ga_universal_auto_link_domains: "启用Google通用数据分析(analytics.js)跨域跟踪。发送到这些域的链接将添加客户端标识。请参阅Google的跨域跟踪指南。" gtm_container_id: "Google Tag Manager 容器 id。例如:GTM-ABCDEF" enable_escaped_fragments: "如未侦测到爬虫回退到使用 Google Ajax-Crawling API。参见 https://developers.google.com/webmasters/ajax-crawling/docs/learn-more" allow_moderators_to_create_categories: "允许版主创建新的分类" + crawler_user_agents: "被当做爬虫的用户代理列表,将向他们提供静态 HTML 而非 JavaScript payload" cors_origins: "允许跨源请求(CORS)。每个源必须包括 http:// 或 https://。DISCOURSE_ENABLE_CORS 环境变量必须设置为 true 才能启用 CORS 政策。" use_admin_ip_whitelist: "当目前的IP刚好处于IP段禁止名单(Admin > Logs > Screened Ips)中的时候,只有管理员才可登入。" blacklist_ip_blocks: "Discourse 不会访问抓取的保密 IP 封禁列表" @@ -956,7 +954,7 @@ zh_CN: allow_index_in_robots_txt: "在 robots.txt 中详细指出这个站点允许被网页搜索引擎检索。" email_domains_blacklist: "用管道符“|”分隔的邮箱域名黑名单列表,其中的域名将不能用来注册账户,例如:mailinator.com|trashmail.net" email_domains_whitelist: "用管道符“|”分隔的电子邮箱域名的列表,用户必须使用这些邮箱域名注册。警告:用户使用不包含在这个列表里的邮箱域名,将无法成功注册。" - hide_email_address_taken: "用户找回密码时不提示帐户是否存在。" + hide_email_address_taken: "通过邮件地址注册账户或忘记密码时不反馈用户账户是否存在的信息。" log_out_strict: "退出时,退出用户所有设备的会话" version_checks: "访问 Discourse Hub 来检查版本更新,并在管理面板 /admin 显示新版本信息" new_version_emails: "当新版本发布时,发送一封邮件至 contact_email 设置的地址。" @@ -1152,9 +1150,7 @@ zh_CN: num_hours_to_close_topic: "暂停主题以介入处理的小时数" auto_respond_to_flag_actions: "当处理标记时启用自动回复。" min_first_post_typing_time: "用户发表第一帖时打字的最小时间(以毫秒计),如果没有达到该阈值,帖子将自动进入需要批准队列。设置为 0 禁用(不推荐)" - auto_block_fast_typers_on_first_post: "自动封禁没有达到首贴输入时间 min_first_post_typing_time 要求的用户" - auto_block_fast_typers_max_trust_level: "自动封禁输入过于快速的用户的最高信任等级" - auto_block_first_post_regex: "大小写不相关的正则表达式,如果匹配成功,会导致该用户不能发表第一帖,而是进入审核队列。例如:求下载|谢谢楼[主猪],会阻止包含求下载、谢谢楼主或谢谢楼猪的帖子。该选项只适用于首贴。" + flags_default_topics: "在管理栏目中默认显示被标记的主题" reply_by_email_enabled: "启用通过邮件回复。" reply_by_email_address: "通过邮件回复的回复地址模板,例如:%{reply_key}@reply.example.com 或 replies+%{reply_key}@example.com" alternative_reply_by_email_addresses: "通过邮件回复的回复地址模板,例如:%{reply_key}@reply.example.com|replies+%{reply_key}@example.com" @@ -1217,7 +1213,7 @@ zh_CN: suppress_digest_email_after_days: "停止给(n)天未登陆网站的用户发送摘要邮件。" digest_suppress_categories: "不在摘要邮件中显示这些分类的内容。" disable_digest_emails: "禁用所有用户摘要邮件功能。" - email_accent_bg_color: "HTML 邮件中某些元素的背景使用的强调颜色。输入色彩名(“red”)或十六进制值(“#FF0000”)。" + email_accent_bg_color: "HTML 邮件中用于背景的强调颜色。输入色彩名字(“red”)或十六进制值(“#0000FF”)。" email_accent_fg_color: "HTML 邮件中背景使用的字体颜色。输入色彩名(“white”)或十六进制值(“#FFFFFF”)。" email_link_color: "HTML 邮件中的链接颜色。输入色彩名字(“blue”)或十六进制值(“#0000FF”)。" detect_custom_avatars: "检测用户是否上传了自定义个人头像。" @@ -1230,6 +1226,7 @@ zh_CN: anonymous_posting_min_trust_level: "启用匿名发帖所需的最小信任等级" anonymous_account_duration_minutes: "为了匿名性,为每个用户每 N 分钟创建一个匿名账户。例如:如果设置为 600,只要发帖后 600 分钟到了,并且用户切换至了匿名模式,就会创建一个新的匿名账户。" hide_user_profiles_from_public: "不对来访用户显示用户信息卡、用户资料和用户目录。" + hide_suspension_reasons: "不在用户页面公开显示封禁原因。" user_website_domains_whitelist: "用户网站要属于这些域名中。用 | 分割。" allow_profile_backgrounds: "允许用户上传个人资料背景图片。" sequential_replies_threshold: "在被提醒回复了太多连续的回复前,用户在主题中可以连续回复的帖子的数量。" @@ -1246,7 +1243,7 @@ zh_CN: topic_page_title_includes_category: "主题页面标题包含分类名。" native_app_install_banner: "邀請常客安裝 Discourse 本机应用程序" share_anonymized_statistics: "分享匿名使用数据。" - auto_handle_queued_age: "此设定天数后自动处理待审核的记录。标记将被推迟。队列中的帖子及用户将被拒绝。设为 0 将禁用此功能。" + auto_handle_queued_age: "此设定天数后自动处理待审核的记录。标记将被忽略。队列中的帖子及用户将被拒绝。设为 0 将禁用此功能。" max_prints_per_hour_per_user: "/print 页面的每小时最大展示量(设置为 0 禁用)" full_name_required: "全名是用户个人信息的必填项。" enable_names: "在用户的个人信息、用户卡片和邮件中显示全名。禁用将在所有地方隐藏全名。" @@ -1278,6 +1275,7 @@ zh_CN: auto_close_topics_post_count: "主题中贴子数上限,达到后自动关闭主题 ( 0 为禁用 )" code_formatting_style: "编辑器中的代码格式化按钮设置的默认格式" max_allowed_message_recipients: "私信允许的最大收件人数。" + watched_words_regular_expressions: "监视词是正则表达式。" default_email_digest_frequency: "用户收到摘要邮件的默认频率。" default_include_tl0_in_digests: "在摘要邮件中默认包含新用户帖子。用户可以自行在参数设置中更改这个设置。" default_email_private_messages: "默认在有人发私信给用户时发送一封邮件通知。" @@ -1358,6 +1356,7 @@ zh_CN: category: '分类' topic: '结果' user: '用户' + results_page: "关于“%{term}”的搜索结果" sso: login_error: "登录错误" not_found: "无法找到你的账户。请联系站点管理人员。" @@ -1463,7 +1462,7 @@ zh_CN: username: short: "必须超过 %{min} 个字" long: "必须不超过 %{max} 个字" - characters: "必须只包含字母、数字和下划线" + characters: "必须只包括数字、字母、连字号或下划线" unique: "已被使用" blank: "必须存在" must_begin_with_alphanumeric_or_underscore: "必须以字母、数字或下划线开头" @@ -1831,6 +1830,13 @@ zh_CN: text_body_template: | 我们非常抱歉,但是你发送至%{destination}(名为%{former_title})的邮件出问题了。 + 你发送回复的邮件地址是已被封禁的邮件地址。试试从另外一个邮件地址发送,或者[联系管理人员](%{base_url}/about)。 + email_reject_not_allowed_email: + title: "邮件被拒绝:不被允许的邮件地址" + subject_template: "[%{email_prefix}] 邮件问题 -- 被封禁的邮件地址" + text_body_template: | + 我们非常抱歉,但是你发送至%{destination}(名为%{former_title})的邮件出问题了。 + 你发送回复的邮件地址是已被封禁的邮件地址。试试从另外一个邮件地址发送,或者[联系管理人员](%{base_url}/about)。 email_reject_inactive_user: title: "Email被拒绝:未激活的用户" @@ -1839,13 +1845,6 @@ zh_CN: 我们非常抱歉,但是你发送至 %{destination}(名为%{former_title}) 的邮件出问题了。 与你账户关联的邮件地址没有激活,请先激活你的帐号再发送邮件。 - email_reject_blocked_user: - title: "Email被拒绝:被禁用的用户" - subject_template: "[%{email_prefix}] 邮件问题 -- 被封禁的用户" - text_body_template: | - 我们非常抱歉,但是你发送至 %{destination}(名为%{former_title}) 的邮件出问题了。 - - 与你账户关联的邮件地址已经被封禁。 email_reject_reply_user_not_matching: title: "Email被拒绝:用户未找到" subject_template: "[%{email_prefix}] 邮件问题 -- 未预期的回复地址" @@ -1970,67 +1969,9 @@ zh_CN: too_many_spam_flags: title: "太多垃圾标记" subject_template: "新账户被暂时封禁" - text_body_template: | - 你好, - - 这是%{site_name}自动发出的邮件。你的帖子因社区标记而被临时隐藏。 - - 出于谨慎的考虑,在管理人员审核通过前,你的新账户不能再发表新的回复或者主题。我们对此带来的不便表示歉意。 - - 要查看更多指引,请参考我们的[社区指引](%{base_url}/guidelines)。 too_many_tl3_flags: title: "太多信任等级3用户标记" subject_template: "新账户被暂时封禁" - text_body_template: | - 你好, - - 这是%{site_name}自动发出的邮件。你的帐户因大量社区标记而被暂停使用。 - - 出于谨慎的考虑,在管理人员审核通过前,你的新账户不能再发表新的回复或者主题。我们对此带来的不便表示歉意。 - - 要查看更多指引,请参考我们的[社区指引](%{base_url}/guidelines)。 - blocked_by_staff: - title: "已被管理人员封禁" - subject_template: "账户被暂时封禁" - text_body_template: | - 你好, - - 这是%{site_name}自动发出的邮件。出于谨慎的考虑,你的帐户临时暂停使用。 - - 请继续浏览,但在管理人员审核通过前,你的账户不能再发表新的回复或者主题。我们对此带来的不便表示歉意。 - - 要查看更多指引,请参考我们的[社区指引](%{base_url}/guidelines)。 - user_automatically_blocked: - title: "用户被自动封禁" - subject_template: "新用户%{username}因社区标记而被封禁" - text_body_template: | - 这是自动发出的邮件。 - - 因多位用户标记%{username}的帖子,新用户[%{username}](%{base_url}%{user_url})已被自动封禁。 - - 请[查看这些标记](%{base_url}/admin/flags)。如果%{username}被意外禁用了编辑功能,点击[该用户管理页面](%{base_url}%{user_url})内的解封按钮。 - - 该阈值可以通过站点设置中的 `block_new_user` 更改。 - spam_post_blocked: - title: "垃圾帖子被封锁" - subject_template: "新用户 %{username} 因重复发布链接而被禁止发表相关帖子" - text_body_template: | - 这是自动发出的邮件。 - - 新用户[%{username}](%{base_url}%{user_url})试图创建多个链接至 %{domains} 的帖子,但这些帖子因为反垃圾策略而被阻挡了。用户仍能够发表不包含到 %{domains} 的帖子。 - - 请[审核该用户](%{base_url}%{user_url})。 - - 该阈值可以通过站点设置中的 `newuser_spam_host_threshold` 和 `white_listed_spam_host_domains` 更改。 - unblocked: - title: "解锁" - subject_template: "账户已解除封禁" - text_body_template: | - 你好, - - 这是%{site_name}自动发出的邮件。管理人员审核你的帐户后,你的帐户不再受到限制。 - - 你现在可以创建新回复和主题了。感谢你的耐心等待。 pending_users_reminder: title: "待审核用户提醒" subject_template: @@ -2054,11 +1995,11 @@ zh_CN: title: "你是这个月的最佳新用户!" subject_template: "你是这个月的最佳新用户!" text_body_template: | - 恭喜,你获得了**%{month_year}月最佳新用户**。 :trophy: + 恭喜,你是**%{month_year}月最佳新用户**。 :trophy: 该奖项每月只授予给两位新用户,并且你永远都可以在[你的用户页](%{base_url}/my/badges)找到。 - 你迅速成为了我们社区中的宝贵一员。感谢你参与,未来继续努力! + 你迅速成为了我们社区中的宝贵一员。感谢你的参与,未来继续努力! queued_posts_reminder: title: "待处理帖子提醒" subject_template: @@ -2262,6 +2203,26 @@ zh_CN: text_body_template: |2 %{message} + account_suspended: + title: "账户被禁用" + subject_template: "[%{email_prefix}] 你的账户已经被禁用" + text_body_template: | + 你的账户已经被禁用至%{suspended_till}。 + + %{reason} + + %{message} + account_exists: + title: "账户已经存在" + subject_template: "[%{email_prefix}] 账户已经存在" + text_body_template: | + 你刚试着在%{site_name}创建账户或者试着将账户的邮箱改为%{email}。然而,%{email}已经绑定于另一账户了。 + + 如果你忘记了密码,[立即重置](%{base_url}/password-reset)。 + + 如果你没有试着用%{email}创建账户或者更改邮件,不要担心——你可以安全地忽略这封邮件。 + + 如果你有任何问题,[联系我们友善的工作人员](%{base_url}/about)。 digest: why: "在你上次于%{last_seen_at}访问后,%{site_link}的新内容摘要。" since_last_visit: "自从你上次访问" @@ -2277,7 +2238,8 @@ zh_CN: popular_posts: "流行帖子" more_new: "你关注的新帖" subject_template: "[%{email_prefix}] 摘要" - unsubscribe: "这是一封来自%{site_link}的摘要邮件,因为你长时间没有访问而发送。修改你的邮件设置,或%{unsubscribe_link}取消订阅。" + unsubscribe: "这是来自%{site_link}的摘要邮件,因为你长时间没有访问站点而发送。修改你的邮件设置%{email_preferences_link},或%{unsubscribe_link}取消订阅。" + your_email_settings: "你的邮件设置" click_here: "点击此处" from: "%{site_name}的摘要" preheader: "在你上次于%{last_seen_at}访问后的新内容摘要" @@ -2379,6 +2341,9 @@ zh_CN: see_more: "更多" search_title: "搜索该网页" search_google: "Google" + offline: + title: "无法载入应用" + offline_page_message: "看起来你掉线了!请检查网络连接并重试。" login_required: welcome_message: | ##[欢迎来到 %{title}](#welcome) @@ -2794,9 +2759,9 @@ zh_CN: 该徽章授予给帖子收到了第一个赞的成员。恭喜,有社区成员发现你发表的内容有意思、酷炫或者有用! autobiographer: name: 自传作者 - description: 已填写用户资料信息 + description: 填写用户资料信息 long_description: | - 该徽章授予给填写了用户页面并选择了用户头像的你。让社区成员们多了解你一点以及你感兴趣的内容,能帮助我们打造一个更好、更团结的社区。让我们一起努力! + 该徽章授予给填写了用户资料页并设置了用户头像的你。让社群成员们多了解你一点以及你感兴趣的内容,能帮助我们打造一个更好、更团结的社群。让我们一起努力! anniversary: name: 年度纪念日 description: 一年活跃用户,至少发了一个帖子 @@ -2891,9 +2856,9 @@ zh_CN: 该徽章授予给第一次在回复中引用帖子的你。在你的回复中引用之前帖子中的相关的段落将把主题关联在一起并切题。只要选中别的帖子中的任何文字,然后点击回复按钮将可以引用啦。多引用一些吧! read_guidelines: name: 阅读指引 - description: 阅读社区指引 + description: 阅读社群指引 long_description: | - 该徽章授予给阅读了社区指引的你。遵守和分享简单的指引能让我们打造一个稳定、有趣和可持续的社区。永远记住别人,一个和你有很多共同点的人,就在屏幕的那一侧。友好一些! + 该徽章授予给阅读了社群指引的你。遵守和分享简单的指引能让我们打造一个稳定、有趣和可持续的社群。永远记住别人,一个和你有很多共同点的人,就在屏幕的那一侧。友好一些! reader: name: 读者 description: 阅读了有至少 100 个回复的主题中的所有回复 diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 353eaf8fdb..81b711268e 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -44,7 +44,6 @@ zh_TW: auto_generated_email_error: "郵件頭部的“precedence”為:list、junk、bulk 或 auto_reply,或者頭部包含了:auto-submitted、auto-replied 或 auto-generated。" no_body_detected_error: "無法解析正文並且沒有附件。" inactive_user_error: "當寄件者處於非啟動狀態時發生。" - blocked_user_error: "當寄件者已被封鎖時發生。" bad_destination_address: "發至/抄送/密送欄中的所有郵件都不匹配進站郵件地址。" strangers_not_allowed_error: "當用戶試圖在尚未晉身為成員之一的分類中新建討論話題時發生。" insufficient_trust_level_error: "用戶嘗試在用戶等級不滿足分類要求時發帖。" @@ -847,19 +846,12 @@ zh_TW: tl2_additional_likes_per_day_multiplier: "增加信任等級2(成員)的贊限制,可提供一個乘數與原始值相乘" tl3_additional_likes_per_day_multiplier: "增加信任等級3(常規)的每日贊限制,將此設置項與原始值相乘" tl4_additional_likes_per_day_multiplier: "增加信任等級4(資深)的贊限制,可提供一個乘數與原始值相乘" - num_spam_flags_to_block_new_user: "如果新用戶的帖子被 num_users_to_block_new_user 個不同用戶標記,隱藏他們的所有帖子並不讓其繼續發表內容。0 為禁用。" - num_users_to_block_new_user: "如果新用戶的帖子被不同用戶 num_spam_flags_to_block_new_user 次標記為垃圾信息,隱藏他們的所有帖子並不讓其繼續發表內容。0 為禁用。" - num_tl3_flags_to_block_new_user: "如果新用戶的帖子被 num_tl3_users_to_block_new_user 個不同的信任等級3的用戶標記,隱藏他們的所有帖子並不讓其繼續發表內容。0 為禁用。" - num_tl3_users_to_block_new_user: "如果新用戶的帖子被不同的信任等級3的用戶標記 num_tl3_flags_to_block_new_user 次,隱藏他們的所有帖子並不讓其繼續發表內容。0 為禁用。" - notify_mods_when_user_blocked: "若有用戶被自動封鎖,將發送訊息給所有板主。" flag_sockpuppets: "如果一個新用戶開始了一個主題,並且同時另一個新用戶以同一個 IP 在該主題回復,他們所有的帖子都將被自動標記為垃圾。" traditional_markdown_linebreaks: "在 Markdown 中使用傳統的換行符號,即用兩個行末空格來換行" post_undo_action_window_mins: "允許使用者還原文章上近期動作(讚、標記等)的時間長度(分鐘數)。" must_approve_users: "新用戶在被允許訪問站點前需要由管理人員批准。警告:在運行的站點中啟用將解除所有非管理人員用戶的訪問權限!" pending_users_reminder_delay: "如果新用戶等待批准時間超過此小時設置則通知版主。設置 -1 關閉通知。" maximum_session_age: "用戶自訪問後可維持登錄 n 小時" - ga_tracking_code: "廢棄:Google 分析追蹤代碼(ga.js),例如:UA-12345678-9。參考 http://google.com/analytics" - ga_domain_name: "廢棄:Google 分析域名(ga.js),例如:mysite.com;參考 http://google.com/analytics" ga_universal_tracking_code: "Google 通用 Analytics (分析) 追蹤代碼(analytics.js)追蹤代碼,例如:UA-12345678-9;參考 http://google.com/analytics" ga_universal_domain_name: "Google 通用 Analytics (分析) 域名(analytics.js)追踪代码,例如:mysite.com;参考 http://google.com/analytics" gtm_container_id: "Google Tag Manager 容器 id。例如:GTM-ABCDEF" @@ -1075,9 +1067,6 @@ zh_TW: num_flags_to_close_topic: "要自動終止一個主題的討論並介入時所需的最小數量的有效標記" auto_respond_to_flag_actions: "啟用自動回覆當加入一個標籤。" min_first_post_typing_time: "用戶發表第一帖時打字的最小時間(以毫秒計),如果沒有達到該閾值,帖子將自動進入需要批准隊列。設置為 0 禁用(不推薦)" - auto_block_fast_typers_on_first_post: "自動封禁沒有達到首貼輸入時間 min_first_post_typing_time 要求的用戶" - auto_block_fast_typers_max_trust_level: "自動封禁輸入過于快速的用戶的最高信任等級" - auto_block_first_post_regex: "大小寫不相關的正則表達式,如果匹配成功,會導致該用戶不能發表第一帖,而是進入審核隊列。例如:求下載|謝謝樓[主豬],會阻止包含求下載、謝謝樓主或謝謝樓豬的帖子。該選項只適用於首貼。" reply_by_email_enabled: "啟用通過電郵回覆。" reply_by_email_address: "通過郵件回覆的回覆地址模板,例如:%{reply_key}@reply.example.com 或 replies+%{reply_key}@example.com" alternative_reply_by_email_addresses: "通過郵件回覆的回覆地址模板,例如:%{reply_key}@reply.example.com|replies+%{reply_key}@example.com" @@ -1686,12 +1675,6 @@ zh_TW: 我們非常抱歉,但是你發送至 %{destination}(名為%{former_title}) 的郵件出問題了。 與你賬戶關聯的郵件地址沒有激活,請先激活你的賬號再發送郵件。 - email_reject_blocked_user: - title: "電子郵件拒絕 已封鎖的用戶" - text_body_template: | - 我們非常抱歉,但是你發送至 %{destination}(名為%{former_title}) 的郵件出問題了。 - - 與你賬戶關聯的郵件地址已經被封禁。 email_reject_reply_user_not_matching: title: "點子郵件拒絕 未找到符合的用戶" email_reject_no_account: @@ -1759,70 +1742,9 @@ zh_TW: too_many_spam_flags: title: "過多垃圾文章投訴" subject_template: "新帳號被暫時停用" - text_body_template: | - 你好, - - 這是 %{site_name} 自動發出的郵件。你的文章因社群投訴而被臨時隱藏。 - - 出於謹慎的考慮,在管理人員審核通過前,你的新帳號不能再回覆或者發表文章。我們對此帶來的不便表示歉意。 - - 要查看更多指引,請參考我們的[社群準則](%{base_url}/guidelines)。 too_many_tl3_flags: title: "過多 TL3 投訴" subject_template: "新帳號被暫時停用" - text_body_template: |+ - 你好, - - 這是 %{site_name} 自動發出的郵件。你的帳號因大量社群投訴,而被暫時停用。 - - 出於謹慎的考慮,在管理人員審核通過前,你的新帳號不能再回覆或者發表文章。我們對此帶來的不便表示歉意。 - - 要查看更多指引,請參考我們的[社群準則](%{base_url}/guidelines)。 - - blocked_by_staff: - title: "被管理員封鎖" - subject_template: "帳號已暫時停用" - text_body_template: | - 你好, - - 這是%{site_name}自動發出的郵件。出於謹慎的考慮,你的帳號已被暫時停用。 - - 請繼續瀏覽,但在[管理成員](/about)審核前,你將無法回應或發表文章。我們對帶來的不便表示歉意。 - - 要查看更多指引,請參考我們的[社群準則](%{base_url}/guidelines)。 - user_automatically_blocked: - title: "用戶自動封鎖" - subject_template: "新使用者 %{username} 因社群投訴而被阻擋" - text_body_template: | - 這是自動發出的訊息。 - - 因多位使用者投訴 %{username} 的文章,新使用者 [%{username}](%{base_url}) 已被自動阻擋。 - - 請[查看這些投訴](%{base_url}/admin/flags)。如果 %{username} 被不合理地阻止貼文,點擊[該用戶管理頁面](%{base_url}%{user_url})內的解除封鎖按鈕。 - - 您可以調整網站設定中的 `block_new_user` 以變更封鎖門檻值。 - spam_post_blocked: - title: "因垃圾貼文而被封鎖" - subject_template: "新用戶 %{username} 由於重複貼連結,文章已被封鎖" - text_body_template: | - 這是自動發出的訊息。 - - 新使用者 [%{username}](%{base_url}%{user_url}) 試圖張貼多重文章連結至 %{domains},但這些文章因為反垃圾策略而被阻擋了。使用者仍能夠發表不包含 %{domains} 連結的新文章。 - - 請[審核該使用者](%{base_url}%{user_url})。 - - 該閾值可以通過站點設置中的 `newuser_spam_host_threshold` 和 `white_listed_spam_host_domains` 更改。 - - 您可以調整網站設定中的 `newuser_spam_host_threshold` 和 `white_listed_spam_host_domains` 以變更封鎖門檻值。 - unblocked: - title: "解除封鎖" - subject_template: "帳號已解除封鎖" - text_body_template: | - 你好, - - 這是 %{site_name} 自動發出的郵件。經管理成員審核後,你的帳號已恢復正常狀態。 - - 你現在可以發表新文章與回覆討論了。感謝你的耐心等待。 pending_users_reminder: title: "待審用戶提醒" subject_template: diff --git a/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml b/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml index db42d8e667..93028b5210 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.tr_TR.yml @@ -11,3 +11,87 @@ tr_TR: badges: certified: name: Sertifikalı + description: "Yeni kullanıcı eğitimi tamamlandı" + licensed: + name: Lisanslı + description: "Gelişmiş kullanıcı eğitimi tamamlandı" + discourse_narrative_bot: + quote: + trigger: "alıntıla" + '1': + quote: "Her zorluğun ortasında fırsat yatar." + author: "Albert Einstein" + '2': + quote: "Dünyada görmek istediğiniz değişim olmalı." + author: "Mahatma Gandhi" + '3': + quote: "Ağlama çünkü bitti, gülümse çünkü gerçekleşti." + author: "Dr Seuss" + '4': + quote: "Doğru yapılan bir şey istiyorsanız, kendiniz yapın." + author: "Charles-Guillaume Étienne" + '5': + author: "Theodore Roosevelt" + '6': + quote: "Hayat bir kutu çikolata gibidir. Ne bulacağını asla bilemezsin." + author: "Forrest Gump’ın Annesi" + '7': + quote: "Bir insan için küçük, insanlık için dev bir adım." + author: "Neil Armstrong" + '8': + quote: "Her gün seni korkutan bir şey yap." + author: "Eleanor Roosevelt" + '9': + quote: "Onları kabul etme cesareti varsa, hatalar daima affedilebilirdir." + author: "Bruce Lee" + '10': + quote: "İnsan zihni ne olursa olsun düşünebilir ve inanabilir, başarabilir." + author: "Napoleon Hill" + magic_8_ball: + trigger: 'kısmet' + answers: + '1': "Bu kesin" + '2': "Kesinlikle öyle" + '3': "Şüphesiz" + '4': "Evet kesinlikle" + '5': "Güvenebilirsin" + '6': "Gördüğüm gibi, evet" + '7': "Büyük ihtimalle" + '8': "İyi görünüm" + '9': "Evet" + '10': "İşaretler evet'i gösteriyor" + '11': "Cevaplar bulanık, tekrar deneyin" + '12': "Sonra tekrar sor" + '13': "En iyisi şimdi söylememek" + '14': "Şimdi tahmin edemiyorum" + '15': "Konsantre ol ve tekrar sor" + '16': "Güvenmeyin" + '17': "Cevabım hayır" + '18': "Kaynaklarım hayır diyor" + '19': "Görünüm o kadar iyi değil" + '20': "Çok şüpheli" + track_selector: + reset_trigger: 'başla' + skip_trigger: 'atla' + help_trigger: 'yardımı görüntüle' + new_user_narrative: + reset_trigger: "yeni kullanıcı" + hello: + title: ":robot: Selamlar!" + quoting: + reply: |- + Güzel iş, benim en favori alıntımı aldınız! :left_speech_bubble: + emoji: + reply: |- + Bu :sparkles: _emojitastic!_ :sparkles: + certificate: + alt: 'Başarı Belgesi' + advanced_user_narrative: + reset_trigger: 'gelişmiş kullanıcı' + title: ':arrow_up: Gelişmiş kullanıcı özellikleri' + edit: + bot_created_post_raw: "@%{discobot_username} bugüne kadar, bildiğim en havalı bot :wink:" + recover: + deleted_post_raw: 'Neden @%{discobot_username} gönderimi sildin? :anguished:' + certificate: + alt: 'İleri Kullanıcı İzlemesi Başarı Sertifikası' diff --git a/plugins/discourse-narrative-bot/config/locales/server.zh_CN.yml b/plugins/discourse-narrative-bot/config/locales/server.zh_CN.yml index 9505a9e95d..553efb134e 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.zh_CN.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.zh_CN.yml @@ -162,7 +162,7 @@ zh_CN: - https://en.wikipedia.org/wiki/Death_by_coconut - https://en.wikipedia.org/wiki/Calculator_spelling reply: |- - 酷!这将会对大多数链接起作用。请记住,它必须是单独的一行链接,在它之前和之后都没有内容。 + 酷!这对许多链接都管用。请记住,它必须占**单独的一行**,前后都没东西。 not_found: |- 抱歉,我没在你的回复中找到链接! :cry: @@ -222,20 +222,20 @@ zh_CN: 选择我帖子中的任何文字都会弹出**引用**按钮。然后选中文字的同时按下**回复**按钮也可以!你可以再试一次吗? bookmark: instructions: |- - 如果你想了解更多,请选择下方**收藏此私信**。如果你这样做, 未来你可能会有 :gift: ! + 如果你想了解更多,请选择下方**收藏此私信**。如果你这样做,你一会可能收到份 :gift: ! reply: |- 太棒了!现在你可以轻松地找到我们的私信了。它就在[你个人页面的收藏栏](%{profile_page_url}/activity/bookmarks)中。选择右上方你的个人头像 ↗ not_found: |- 啊哦,我没有在这个主题中看到任何收藏。你在每个帖子下找到了收藏按钮吗?如果需要的话,使用显示更多展开更多操作。 emoji: instructions: |- - 你可能看到我在回复中用了些小图片 :blue_car::dash: 他们叫[emoji](https://zh.wikipedia.org/wiki/Emoji)。你能在你的回复中**添加Emoji**吗?以下任意方式都可以: + 你可能已经看到我在回复中用了很多小图片 :blue_car::dash: 这些叫做[绘文字(Emoji)](https://zh.wikipedia.org/wiki/%E7%B9%AA%E6%96%87%E5%AD%97)。你能在回复中**使用一个 Emoji **吗?这些都可以用: - 输入 `:) ;) :D :P :O` - - 输入半角冒号 : 跟上 emoji 的名字 `:tada:` + - 输入冒号 : 然后输入完整的 Emoji 名字 `:tada:` - - 在编辑器中或者你移动端的键盘上按下 emoji 按钮 + - 点击编辑器中的 Emoji 按钮,或者使用移动设备的键盘 reply: |- 这是 :sparkles: **Emoji 精神!** :sparkles: not_found: |- @@ -267,14 +267,14 @@ zh_CN: reply: |- [我们的管理人员](/groups/staff)将会私下里通知关于你标记的后续。如果许多社区成员标记了帖子,出于谨慎,帖子将自动被隐藏。(我没有真的写很糟糕的帖子 :angel:,我先把这个标记删除了) not_found: |- - 啊不,我糟糕的帖子还没被标记。 :worried: 你能**标记**其为不合适的吗?不要忘记用显示更多按钮来显示每个帖子的更多操作。 + 啊不,我糟糕的帖子还没被标记。 :worried: 你能**标记**其为不合适的吗?不要忘记用显示更多按钮来显示每个帖子的更多操作。 search: instructions: |- - 呼…我已经在这个主题中隐藏了一个惊喜。如果你想挑战的话,点击右上角↗的**搜索按钮** 来找到它。 + 呼…我已经在这个主题中隐藏了一个惊喜。如果你想挑战的话,点击右上角↗的**搜索按钮** 来找到它。 试试在这个主题中搜索关键词“capy​bara” hidden_message: |- - 你怎么错过这个 capybara 哒 :wink: + 你怎么错过这个 capybara 哒? :wink: diff --git a/plugins/poll/config/locales/client.tr_TR.yml b/plugins/poll/config/locales/client.tr_TR.yml index e313bed0a5..5487fbc571 100644 --- a/plugins/poll/config/locales/client.tr_TR.yml +++ b/plugins/poll/config/locales/client.tr_TR.yml @@ -49,6 +49,7 @@ tr_TR: insert: Anket Ekle help: options_count: En az 2 seçenek ekleyin + invalid_values: Minimum değer maksimum değerden küçük olmalıdır. poll_type: label: Tür regular: Tekli seçim From 4dc29e5f9eb47fba77a28567aa0fb12fd331bb83 Mon Sep 17 00:00:00 2001 From: Joshua Rosenfeld Date: Mon, 13 Nov 2017 15:35:52 -0500 Subject: [PATCH 180/445] Missed a spot renaming block to silence --- config/locales/server.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index c6649fd47a..cb518bac1b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2360,7 +2360,7 @@ en: Please [review the flags](%{base_url}/admin/flags). If %{username} was incorrectly silenced from posting, click the unsilence button on [the admin page for this user](%{user_url}). - This threshold can be changed via the `block_new_user` site settings. + This threshold can be changed via the `silence_new_user` site settings. spam_post_silenced: title: "Spam Post Silenced" From a9fd42f91ca5eeb72099ee696eaf74ade04ad7d5 Mon Sep 17 00:00:00 2001 From: Kris Date: Mon, 13 Nov 2017 17:09:20 -0500 Subject: [PATCH 181/445] fixing category delete modal background color --- app/assets/stylesheets/common/base/modal.scss | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index b9f3e8b217..1183ad70f7 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -331,8 +331,8 @@ .cannot_delete_reason { position: absolute; - background: dark-light-choose($primary, $secondary); - color: dark-light-choose($secondary, $secondary); + background: $primary; + color: $secondary; text-align: center; border-radius: 2px; padding: 12px 8px; @@ -343,14 +343,13 @@ border: solid transparent; content: " "; position: absolute; - border-top-color: dark-light-choose($primary, $secondary); + border-top-color: $primary; border-width: 8px; } } } } - .incoming-email-modal { .btn { transition: none; From e4464351a2cf61d945abb0873785479dc536aef7 Mon Sep 17 00:00:00 2001 From: Quim Gil Date: Tue, 14 Nov 2017 00:37:20 +0100 Subject: [PATCH 182/445] Changing misattributed Mahatma Gandhi's quote As reported at https://meta.discourse.org/t/gandhi-misattributed/74022 --- plugins/discourse-narrative-bot/config/locales/server.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/discourse-narrative-bot/config/locales/server.en.yml b/plugins/discourse-narrative-bot/config/locales/server.en.yml index 42fc2f271f..da2fd0c677 100644 --- a/plugins/discourse-narrative-bot/config/locales/server.en.yml +++ b/plugins/discourse-narrative-bot/config/locales/server.en.yml @@ -51,7 +51,7 @@ en: quote: "In the middle of every difficulty lies opportunity" author: "Albert Einstein" "2": - quote: "You must be the change you wish to see in the world." + quote: "Freedom is not worth having if it does not connote freedom to err." author: "Mahatma Gandhi" "3": quote: "Don’t cry because it’s over, smile because it happened." From e0bee3a3bc0fe27c81ba0ce30588254eab6a6002 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 13 Nov 2017 18:51:19 -0800 Subject: [PATCH 183/445] FIX: mutate value if numeric in enums --- .../admin/templates/components/site-settings/enum.hbs | 2 +- app/assets/javascripts/select-box-kit/mixins/utils.js.es6 | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs b/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs index 765a0e20d1..9b3da3178e 100644 --- a/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs +++ b/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs @@ -1,4 +1,4 @@ -{{combo-box valueAttribute="value" content=setting.validValues value=value none=setting.allowsNone}} +{{combo-box castInteger=true valueAttribute="value" content=setting.validValues value=value none=setting.allowsNone}} {{preview}} {{setting-validation-message message=validationMessage}}
    {{{unbound setting.description}}}
    diff --git a/app/assets/javascripts/select-box-kit/mixins/utils.js.es6 b/app/assets/javascripts/select-box-kit/mixins/utils.js.es6 index 3ece47cc3b..475cf726fc 100644 --- a/app/assets/javascripts/select-box-kit/mixins/utils.js.es6 +++ b/app/assets/javascripts/select-box-kit/mixins/utils.js.es6 @@ -13,8 +13,12 @@ export default Ember.Mixin.create({ return content; }, + _isNumeric(input) { + return !isNaN(parseFloat(input)) && isFinite(input); + }, + _castInteger(value) { - if (this.get("castInteger") === true && Ember.isPresent(value)) { + if (this.get("castInteger") === true && Ember.isPresent(value) && this._isNumeric(value)) { return parseInt(value, 10); } From bf5ba5fbd1ab17e23a347d4cd2d575bf95f03e75 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 14 Nov 2017 11:18:38 +0800 Subject: [PATCH 184/445] Remove `readonly alert` smoke test. --- spec/phantom_js/smoke_test.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/phantom_js/smoke_test.js b/spec/phantom_js/smoke_test.js index 50926f3338..55e48cd9a8 100644 --- a/spec/phantom_js/smoke_test.js +++ b/spec/phantom_js/smoke_test.js @@ -178,11 +178,7 @@ var runTests = function() { return $("#user-card .names").length; }); - if (system.env["READONLY_TESTS"]) { - test("readonly alert is present", function() { - return $(".alert-read-only").length; - }); - } else { + if (!system.env["READONLY_TESTS"]) { exec("open login modal", function() { $(".login-button").click(); }); From 075a4584896897d10389b40fa345e45df043f641 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 14 Nov 2017 15:22:59 +1100 Subject: [PATCH 185/445] FIX: child theme component vars not resolved in parent --- app/models/theme.rb | 12 ++++++++++++ lib/stylesheet/importer.rb | 4 +--- spec/models/theme_spec.rb | 20 ++++++++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/app/models/theme.rb b/app/models/theme.rb index 793c098888..280fa6847f 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -270,6 +270,18 @@ class Theme < ActiveRecord::Base end end + def all_theme_variables + fields = {} + ([self] + (included_themes || [])).each do |theme| + theme&.theme_fields.each do |field| + next unless ThemeField.theme_var_type_ids.include?(field.type_id) + next if fields.key?(field.name) + fields[field.name] = field + end + end + fields.values + end + def add_child_theme!(theme) child_theme_relation.create!(child_theme_id: theme.id) @included_themes = nil diff --git a/lib/stylesheet/importer.rb b/lib/stylesheet/importer.rb index f1ed26e7e2..93e4c479c8 100644 --- a/lib/stylesheet/importer.rb +++ b/lib/stylesheet/importer.rb @@ -41,9 +41,7 @@ module Stylesheet colors.each do |n, hex| contents << "$#{n}: ##{hex} !default;\n" end - theme&.theme_fields&.each do |field| - next unless ThemeField.theme_var_type_ids.include?(field.type_id) - + theme&.all_theme_variables&.each do |field| if field.type_id == ThemeField.types[:theme_upload_var] if upload = field.upload url = upload_cdn_path(upload.url) diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb index 5ff7368462..817326a1ad 100644 --- a/spec/models/theme_spec.rb +++ b/spec/models/theme_spec.rb @@ -155,6 +155,26 @@ HTML end context 'theme vars' do + + it 'works in parent theme' do + + theme = Theme.new(name: 'theme', user_id: -1) + theme.set_field(target: :common, name: :scss, value: 'body {color: $magic; }') + theme.set_field(target: :common, name: :magic, value: 'red', type: :theme_var) + theme.set_field(target: :common, name: :not_red, value: 'red', type: :theme_var) + theme.save + + parent_theme = Theme.new(name: 'parent theme', user_id: -1) + parent_theme.set_field(target: :common, name: :scss, value: 'body {background-color: $not_red; }') + parent_theme.set_field(target: :common, name: :not_red, value: 'blue', type: :theme_var) + parent_theme.save + parent_theme.add_child_theme!(theme) + + scss, _map = Stylesheet::Compiler.compile('@import "theme_variables"; @import "desktop_theme"; ', "theme.scss", theme_id: parent_theme.id) + expect(scss).to include("color:red") + expect(scss).to include("background-color:blue") + end + it 'can generate scss based off theme vars' do theme = Theme.new(name: 'theme', user_id: -1) theme.set_field(target: :common, name: :scss, value: 'body {color: $magic; content: quote($content)}') From 47e4c9bb468fca10f4818df3bc44a67990eb75aa Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 14 Nov 2017 16:30:23 +1100 Subject: [PATCH 186/445] FIX: import/export theme should work with uploads --- app/controllers/admin/themes_controller.rb | 26 ++++++++++++-- app/serializers/theme_serializer.rb | 27 +++++++++++++++ .../admin/themes_controller_spec.rb | 34 +++++++++++++++++++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/themes_controller.rb b/app/controllers/admin/themes_controller.rb index 1ba250abf3..5416f10d50 100644 --- a/app/controllers/admin/themes_controller.rb +++ b/app/controllers/admin/themes_controller.rb @@ -1,4 +1,5 @@ require_dependency 'upload_creator' +require 'base64' class Admin::ThemesController < Admin::AdminController @@ -31,7 +32,28 @@ class Admin::ThemesController < Admin::AdminController @theme = Theme.new(name: theme["name"], user_id: current_user.id) theme["theme_fields"]&.each do |field| - @theme.set_field(target: field["target"], name: field["name"], value: field["value"]) + + if field["raw_upload"] + begin + tmp = Tempfile.new + tmp.binmode + file = Base64.decode64(field["raw_upload"]) + tmp.write(file) + tmp.rewind + upload = UploadCreator.new(tmp, field["filename"]).create_for(current_user.id) + field["upload_id"] = upload.id + ensure + tmp.unlink + end + end + + @theme.set_field( + target: field["target"], + name: field["name"], + value: field["value"], + type_id: field["type_id"], + upload_id: field["upload_id"] + ) end if @theme.save @@ -168,7 +190,7 @@ class Admin::ThemesController < Admin::AdminController response.headers['Content-Disposition'] = "attachment; filename=#{@theme.name.parameterize}.dcstyle.json" response.sending_file = true - render json: ThemeSerializer.new(@theme) + render json: ThemeWithEmbeddedUploadsSerializer.new(@theme, root: 'theme') end end diff --git a/app/serializers/theme_serializer.rb b/app/serializers/theme_serializer.rb index 4f8864158a..78532e399a 100644 --- a/app/serializers/theme_serializer.rb +++ b/app/serializers/theme_serializer.rb @@ -1,3 +1,5 @@ +require 'base64' + class ThemeFieldSerializer < ApplicationSerializer attributes :name, :target, :value, :error, :type_id, :upload_id, :url, :filename @@ -68,3 +70,28 @@ class ThemeSerializer < ChildThemeSerializer object.child_themes.order(:name) end end + +class ThemeFieldWithEmbeddedUploadsSerializer < ThemeFieldSerializer + attributes :raw_upload + + def include_raw_upload? + object.upload + end + + def raw_upload + filename = Discourse.store.path_for(object.upload) + raw = nil + + if filename + raw = File.read(filename) + else + raw = Discourse.store.download(object.upload).read + end + + Base64.encode64(raw) + end +end + +class ThemeWithEmbeddedUploadsSerializer < ThemeSerializer + has_many :theme_fields, serializer: ThemeFieldWithEmbeddedUploadsSerializer, embed: :objects +end diff --git a/spec/controllers/admin/themes_controller_spec.rb b/spec/controllers/admin/themes_controller_spec.rb index ee4dc41930..1418f165a8 100644 --- a/spec/controllers/admin/themes_controller_spec.rb +++ b/spec/controllers/admin/themes_controller_spec.rb @@ -1,4 +1,5 @@ require 'rails_helper' +require_dependency 'theme_serializer' describe Admin::ThemesController do @@ -34,6 +35,39 @@ describe Admin::ThemesController do Rack::Test::UploadedFile.new(file_from_fixtures("sam-s-simple-theme.dcstyle.json", "json")) end + let :image do + file_from_fixtures("logo.png") + end + + it 'can import a theme with an upload' do + upload = Fabricate(:upload) + theme = Theme.new(name: 'with-upload', user_id: -1) + upload = UploadCreator.new(image, "logo.png").create_for(-1) + theme.set_field(target: :common, name: :logo, upload_id: upload.id, type: :theme_upload_var) + theme.save + + json = ThemeWithEmbeddedUploadsSerializer.new(theme, root: 'theme').to_json + theme.destroy + + temp = Tempfile.new + temp.write(json) + temp.rewind + + uploaded_json = Rack::Test::UploadedFile.new(temp) + upload.destroy + + post :import, params: { theme: uploaded_json }, format: :json + expect(response).to be_success + temp.unlink + + theme = Theme.last + expect(theme.theme_fields.count).to eq(1) + expect(theme.theme_fields.first.upload).not_to eq(nil) + expect(theme.theme_fields.first.upload.filesize).to eq(upload.filesize) + expect(theme.theme_fields.first.upload.sha1).to eq(upload.sha1) + expect(theme.theme_fields.first.upload.original_filename).to eq(upload.original_filename) + end + it 'imports a theme' do post :import, params: { theme: theme_file }, format: :json expect(response).to be_success From 4b42a0abc9f0fc106db7c928caca593c6052266f Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 14 Nov 2017 16:52:00 +1100 Subject: [PATCH 187/445] FIX: add error for suspended users attempting to login via sso --- app/controllers/session_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index f05d0976af..d8a2b66457 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -107,6 +107,11 @@ class SessionController < ApplicationController begin if user = sso.lookup_or_create_user(request.remote_ip) + if user.suspended? + render_sso_error(text: I18n.t("login.suspended", date: user.suspended_till), status: 403) + return + end + if SiteSetting.must_approve_users? && !user.approved? if SiteSetting.sso_not_approved_url.present? redirect_to SiteSetting.sso_not_approved_url From ddff25d7ff4b53d15742e3a5cb38d93cbcd9e7e2 Mon Sep 17 00:00:00 2001 From: James Kiesel Date: Tue, 14 Nov 2017 21:46:31 +1300 Subject: [PATCH 188/445] Fix placeholder for show more images (#5312) --- .../javascripts/discourse/templates/modal/upload-selector.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/modal/upload-selector.hbs b/app/assets/javascripts/discourse/templates/modal/upload-selector.hbs index 532f3770d1..0720195f8b 100644 --- a/app/assets/javascripts/discourse/templates/modal/upload-selector.hbs +++ b/app/assets/javascripts/discourse/templates/modal/upload-selector.hbs @@ -22,7 +22,7 @@ {{#if showMore}}
    - {{input value=imageLink laceholder="http://example.com"}} + {{input value=imageLink placeholder="http://example.com"}} {{i18n 'upload_selector.image_link'}}
    From ba2209f7d79aa4da46271e629a7d4a805731aeb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 14 Nov 2017 10:56:10 +0100 Subject: [PATCH 189/445] FIX: always clean up uploads with no sha1 --- app/jobs/scheduled/clean_up_uploads.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/jobs/scheduled/clean_up_uploads.rb b/app/jobs/scheduled/clean_up_uploads.rb index 46691265b7..aa6728ec16 100644 --- a/app/jobs/scheduled/clean_up_uploads.rb +++ b/app/jobs/scheduled/clean_up_uploads.rb @@ -50,9 +50,11 @@ module Jobs result = result.where("uploads.url NOT IN (?)", ignore_urls) if ignore_urls.present? result.find_each do |upload| - encoded_sha = Base62.encode(upload.sha1.hex) - next if QueuedPost.where("raw LIKE '%#{upload.sha1}%' OR raw LIKE '%#{encoded_sha}%'").exists? - next if Draft.where("data LIKE '%#{upload.sha1}%' OR data LIKE '%#{encoded_sha}%'").exists? + if upload.sha1.present? + encoded_sha = Base62.encode(upload.sha1.hex) + next if QueuedPost.where("raw LIKE '%#{upload.sha1}%' OR raw LIKE '%#{encoded_sha}%'").exists? + next if Draft.where("data LIKE '%#{upload.sha1}%' OR data LIKE '%#{encoded_sha}%'").exists? + end upload.destroy end end From 4be8f17e66d95e786edd91cdade334ff5da8ee04 Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Tue, 14 Nov 2017 11:38:54 +0100 Subject: [PATCH 190/445] FIX: counting invites didn't work PostgreSQL reported the following error: "for SELECT DISTINCT, ORDER BY expressions must appear in select list" --- app/models/invite.rb | 4 ++-- spec/models/invite_spec.rb | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/models/invite.rb b/app/models/invite.rb index 8f9cf8ebd9..984e3cb180 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -183,11 +183,11 @@ class Invite < ActiveRecord::Base end def self.find_pending_invites_count(inviter) - find_all_invites_from(inviter, 0, nil).where('invites.user_id IS NULL').count + find_all_invites_from(inviter, 0, nil).where('invites.user_id IS NULL').reorder(nil).count end def self.find_redeemed_invites_count(inviter) - find_all_invites_from(inviter, 0, nil).where('invites.user_id IS NOT NULL').count + find_all_invites_from(inviter, 0, nil).where('invites.user_id IS NOT NULL').reorder(nil).count end def self.filter_by(email_or_username) diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index b36afc144d..552a953194 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -402,6 +402,8 @@ describe Invite do expect(invites.length).to eq(1) expect(invites.first).to eq pending_invite + + expect(Invite.find_pending_invites_count(inviter)).to eq(1) end end @@ -426,6 +428,8 @@ describe Invite do expect(invites.length).to eq(1) expect(invites.first).to eq redeemed_invite + + expect(Invite.find_redeemed_invites_count(inviter)).to eq(1) end end From 680696fc04ebea252095540a87133e3044257c66 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Tue, 14 Nov 2017 07:55:08 -0800 Subject: [PATCH 191/445] Replaces delete flag modal by a dropdown --- .../admin/components/flagged-post.js.es6 | 4 - .../modals/admin-delete-flag.js.es6 | 22 ------ .../admin/mixins/delete-spammer-modal.js.es6 | 23 ------ .../templates/components/flagged-post.hbs | 7 +- .../templates/modal/admin-delete-flag.hbs | 24 ------ .../admin-delete-flag-dropdown.js.es6 | 78 +++++++++++++++++++ .../stylesheets/common/admin/admin_base.scss | 3 + .../stylesheets/common/admin/flagging.scss | 14 +--- .../admin-delete-flag-dropdown.scss | 16 ++++ app/assets/stylesheets/desktop/modal.scss | 6 -- app/assets/stylesheets/mobile/modal.scss | 9 --- .../acceptance/admin-flags-test.js.es6 | 36 +++++---- 12 files changed, 125 insertions(+), 117 deletions(-) delete mode 100644 app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 delete mode 100644 app/assets/javascripts/admin/mixins/delete-spammer-modal.js.es6 delete mode 100644 app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs create mode 100644 app/assets/javascripts/select-box-kit/components/admin-delete-flag-dropdown.js.es6 create mode 100644 app/assets/stylesheets/common/select-box-kit/admin-delete-flag-dropdown.scss diff --git a/app/assets/javascripts/admin/components/flagged-post.js.es6 b/app/assets/javascripts/admin/components/flagged-post.js.es6 index 3019a1aae0..c8583199b0 100644 --- a/app/assets/javascripts/admin/components/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/components/flagged-post.js.es6 @@ -36,10 +36,6 @@ export default Ember.Component.extend({ this.removeAfter(promise); }, - showDeleteFlagModal() { - this._spawnModal('admin-delete-flag', this.get('flaggedPost'), 'delete-flag-modal'); - }, - disagree() { this.removeAfter(this.get('flaggedPost').disagreeFlags()); }, diff --git a/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 deleted file mode 100644 index e8038edbca..0000000000 --- a/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 +++ /dev/null @@ -1,22 +0,0 @@ -import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import DeleteSpammerModal from 'admin/mixins/delete-spammer-modal'; - -export default Ember.Controller.extend(ModalFunctionality, DeleteSpammerModal, { - removeAfter: null, - - actions: { - deletePostDeferFlag() { - let flaggedPost = this.get('model'); - this.removeAfter(flaggedPost.deferFlags(true)).then(() => { - this.send('closeModal'); - }); - }, - - deletePostAgreeFlag() { - let flaggedPost = this.get('model'); - this.removeAfter(flaggedPost.agreeFlags('delete')).then(() => { - this.send('closeModal'); - }); - } - } -}); diff --git a/app/assets/javascripts/admin/mixins/delete-spammer-modal.js.es6 b/app/assets/javascripts/admin/mixins/delete-spammer-modal.js.es6 deleted file mode 100644 index 138fd5422a..0000000000 --- a/app/assets/javascripts/admin/mixins/delete-spammer-modal.js.es6 +++ /dev/null @@ -1,23 +0,0 @@ -export default Ember.Mixin.create({ - adminTools: Ember.inject.service(), - spammerDetails: null, - - onShow() { - let adminTools = this.get('adminTools'); - let spammerDetails = adminTools.spammerDetails(this.get('model.user')); - - this.setProperties({ - spammerDetails, - canDeleteSpammer: spammerDetails.canDelete && this.get('model.flaggedForSpam') - }); - }, - - actions: { - deleteSpammer() { - let spammerDetails = this.get('spammerDetails'); - this.removeAfter(spammerDetails.deleteUser()).then(() => { - this.send('closeModal'); - }); - } - } -}); diff --git a/app/assets/javascripts/admin/templates/components/flagged-post.hbs b/app/assets/javascripts/admin/templates/components/flagged-post.hbs index dc89d3b83c..29e7213189 100644 --- a/app/assets/javascripts/admin/templates/components/flagged-post.hbs +++ b/app/assets/javascripts/admin/templates/components/flagged-post.hbs @@ -138,12 +138,7 @@ icon="external-link" label="admin.flags.defer_flag"}} - {{d-button - class="btn-danger delete-flag" - title="admin.flags.delete_title" - action="showDeleteFlagModal" - icon="trash-o" - label="admin.flags.delete"}} + {{admin-delete-flag-dropdown post=flaggedPost removeAfter=(action "removeAfter")}} {{#unless suspended}} {{d-button diff --git a/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs b/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs deleted file mode 100644 index 571f332dbf..0000000000 --- a/app/assets/javascripts/admin/templates/modal/admin-delete-flag.hbs +++ /dev/null @@ -1,24 +0,0 @@ -{{#d-modal-body title="admin.flags.delete_flag_modal_title"}} - {{d-button - class="delete-defer" - title="admin.flags.delete_post_defer_flag_title" - action="deletePostDeferFlag" - icon="external-link" - label="admin.flags.delete_post_defer_flag"}} - - {{d-button - class="delete-agree" - title="admin.flags.delete_post_agree_flag_title" - action="deletePostAgreeFlag" - icon="thumbs-o-up" - label="admin.flags.delete_post_agree_flag"}} - - {{#if canDeleteSpammer}} - {{d-button - class="btn-danger delete-spammer" - title="admin.flags.delete_spammer_title" - action="deleteSpammer" - icon="exclamation-triangle" - label="admin.flags.delete_spammer"}} - {{/if}} -{{/d-modal-body}} diff --git a/app/assets/javascripts/select-box-kit/components/admin-delete-flag-dropdown.js.es6 b/app/assets/javascripts/select-box-kit/components/admin-delete-flag-dropdown.js.es6 new file mode 100644 index 0000000000..ba63430b1d --- /dev/null +++ b/app/assets/javascripts/select-box-kit/components/admin-delete-flag-dropdown.js.es6 @@ -0,0 +1,78 @@ +import { iconHTML } from 'discourse-common/lib/icon-library'; +import DropdownSelectBox from "select-box-kit/components/dropdown-select-box"; +import computed from "ember-addons/ember-computed-decorators"; +import { on } from "ember-addons/ember-computed-decorators"; + +export default DropdownSelectBox.extend({ + headerText: "admin.flags.delete", + classNames: ["delete-flag", "admin-delete-flag-dropdown"], + adminTools: Ember.inject.service(), + nameProperty: "label", + + @on("didReceiveAttrs") + _setAdminDeleteDropdownOptions() { + this.set("headerComponentOptions.selectedName", I18n.t(this.get("headerText"))); + this.set("headerComponentOptions.icon", iconHTML("trash-o")); + }, + + @computed("adminTools", "post.user") + spammerDetails(adminTools, user) { + return adminTools.spammerDetails(user); + }, + + canDeleteSpammer: Ember.computed.and("spammerDetails.canDelete", "post.flaggedForSpam"), + + @computed("post", "canDeleteSpammer") + content(post, canDeleteSpammer) { + const content = []; + + content.push({ + title: I18n.t("admin.flags.delete_post_defer_flag_title"), + icon: "external-link", + id: "delete-defer", + action: () => this.send("deletePostDeferFlag"), + label: I18n.t("admin.flags.delete_post_defer_flag") + }); + + content.push({ + title: I18n.t("admin.flags.delete_post_agree_flag_title"), + icon: "thumbs-o-up", + id: "delete-agree", + action: () => this.send("deletePostAgreeFlag"), + label: I18n.t("admin.flags.delete_post_agree_flag") + }); + + if (canDeleteSpammer) { + content.push({ + title: I18n.t("admin.flags.delete_post_agree_flag_title"), + icon: "exclamation-triangle", + id: "delete-spammer", + action: () => this.send("deleteSpammer"), + label: I18n.t("admin.flags.delete_spammer") + }); + } + + return content; + }, + + selectValueFunction(value) { + Ember.get(this._contentForValue(value), "action")(); + }, + + actions: { + deleteSpammer() { + let spammerDetails = this.get("spammerDetails"); + this.attrs.removeAfter(spammerDetails.deleteUser()); + }, + + deletePostDeferFlag() { + let flaggedPost = this.get('post'); + this.attrs.removeAfter(flaggedPost.deferFlags(true)); + }, + + deletePostAgreeFlag() { + let flaggedPost = this.get('post'); + this.attrs.removeAfter(flaggedPost.agreeFlags('delete')); + } + } +}); diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index eb8eb427d8..838b5c6c0a 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -218,6 +218,9 @@ $mobile-breakpoint: 700px; .select-box-kit.multi-combo-box { width: 500px; } + .select-box-kit.dropdown-select-box { + width: auto; + } } .admin-container .controls { diff --git a/app/assets/stylesheets/common/admin/flagging.scss b/app/assets/stylesheets/common/admin/flagging.scss index f2132ed893..0bcdd190b2 100644 --- a/app/assets/stylesheets/common/admin/flagging.scss +++ b/app/assets/stylesheets/common/admin/flagging.scss @@ -151,10 +151,12 @@ .flagged-post-controls { display: flex; flex-wrap: wrap; + align-items: center; + justify-content: flex-start; - button { - margin-right: 0.5em; + button, .select-box-kit { margin-bottom: 0.5em; + margin-right: 0.5em; } } } @@ -188,13 +190,6 @@ margin-bottom: 2em; } -.delete-flag-modal { - button { - margin: 10px 0 10px 10px; - padding: 10px 15px; - } -} - .mobile-view { .flagged-posts { .flagged-post { @@ -210,4 +205,3 @@ padding-right: 0.25em; } } - diff --git a/app/assets/stylesheets/common/select-box-kit/admin-delete-flag-dropdown.scss b/app/assets/stylesheets/common/select-box-kit/admin-delete-flag-dropdown.scss new file mode 100644 index 0000000000..ffdf62634b --- /dev/null +++ b/app/assets/stylesheets/common/select-box-kit/admin-delete-flag-dropdown.scss @@ -0,0 +1,16 @@ +.select-box-kit { + &.dropdown-select-box { + width: auto; + &.admin-delete-flag-dropdown { + .dropdown-select-box-header .btn { + background: $danger; + color: white; + } + + .select-box-kit-row[data-value="delete-spammer"] .texts .name, + .select-box-kit-row[data-value="delete-spammer"] .icons .d-icon { + color: $danger; + } + } + } +} diff --git a/app/assets/stylesheets/desktop/modal.scss b/app/assets/stylesheets/desktop/modal.scss index 06d9484471..f4ce2021b1 100644 --- a/app/assets/stylesheets/desktop/modal.scss +++ b/app/assets/stylesheets/desktop/modal.scss @@ -220,12 +220,6 @@ margin-bottom: 10px; } -.delete-flag-modal { - .modal-inner-container { - width: 400px; - } -} - .change-timestamp { min-height: 300px; diff --git a/app/assets/stylesheets/mobile/modal.scss b/app/assets/stylesheets/mobile/modal.scss index 0643a007d1..ebf8f68ca5 100644 --- a/app/assets/stylesheets/mobile/modal.scss +++ b/app/assets/stylesheets/mobile/modal.scss @@ -169,15 +169,6 @@ } } -.delete-flag-modal { - .modal-inner-container { - width: 300px; - } - .btn { - float: none !important; - } -} - #search-help { max-width: 98%; } diff --git a/test/javascripts/acceptance/admin-flags-test.js.es6 b/test/javascripts/acceptance/admin-flags-test.js.es6 index 80eacf7049..3d44818433 100644 --- a/test/javascripts/acceptance/admin-flags-test.js.es6 +++ b/test/javascripts/acceptance/admin-flags-test.js.es6 @@ -80,40 +80,50 @@ QUnit.test("flagged posts - defer", assert => { QUnit.test("flagged posts - delete + defer", assert => { visit("/admin/flags/active"); - click('.delete-flag'); + andThen(() => { - assert.equal(find('.delete-flag-modal:visible').length, 1); + expandSelectBoxKit('.delete-flag'); }); - click('.delete-defer'); + + andThen(() => { + selectBoxKitSelectRow('delete-defer', { selector: '.delete-flag'}); + }); + andThen(() => { - assert.equal(find('.delete-flag-modal:visible').length, 0); assert.equal(find('.admin-flags .flagged-post').length, 0); }); }); QUnit.test("flagged posts - delete + agree", assert => { visit("/admin/flags/active"); - click('.delete-flag'); + andThen(() => { - assert.equal(find('.delete-flag-modal:visible').length, 1); + expandSelectBoxKit('.delete-flag'); }); - click('.delete-agree'); + + andThen(() => { + selectBoxKitSelectRow('delete-agree', { selector: '.delete-flag'}); + }); + andThen(() => { - assert.equal(find('.delete-flag-modal:visible').length, 0); assert.equal(find('.admin-flags .flagged-post').length, 0); }); }); QUnit.test("flagged posts - delete + deleteSpammer", assert => { visit("/admin/flags/active"); - click('.delete-flag'); + andThen(() => { - assert.equal(find('.delete-flag-modal:visible').length, 1); + expandSelectBoxKit('.delete-flag'); }); - click('.delete-spammer'); - click('.confirm-delete'); + + andThen(() => { + selectBoxKitSelectRow('delete-spammer', { selector: '.delete-flag'}); + }); + + click('.confirm-delete'); + andThen(() => { - assert.equal(find('.delete-flag-modal:visible').length, 0); assert.equal(find('.admin-flags .flagged-post').length, 0); }); }); From 52480d554abec6ef59329daa89d8f5b06ad7b824 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 14 Nov 2017 11:57:17 -0500 Subject: [PATCH 192/445] UX: Support for custom 404 pages --- app/views/exceptions/not_found.html.erb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/exceptions/not_found.html.erb b/app/views/exceptions/not_found.html.erb index 32d1532fa0..0e7e8057cf 100644 --- a/app/views/exceptions/not_found.html.erb +++ b/app/views/exceptions/not_found.html.erb @@ -1,7 +1,7 @@ -<% local_domain = "#{request.protocol}#{request.host_with_port}" %> -

    <%= t 'page_not_found.title' %>

    +<%= raw build_plugin_html 'server:not-found-before-topics' %> + <% unless SiteSetting.login_required? && current_user.nil? %>