diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f00d5d078..98aa05e4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,129 +1,27 @@ # Contributing to Discourse -## Before You Start +## Important note for Developers -Anyone wishing to contribute to the **[Discourse/Discourse](https://github.com/discourse/discourse)** project **MUST read & sign the [Electronic Discourse Forums Contribution License Agreement](http://www.discourse.org/cla)**. The Discourse team is legally prevented from accepting any pull requests from users who have not signed the CLA first. +Anyone wishing to contribute to the [github.com/discourse/discourse](https://github.com/discourse/discourse) project **must read & sign our [Contributor License Agreement](http://www.discourse.org/cla)**. The Discourse team is legally prevented from accepting any pull requests from users who have not signed the CLA first. -## Reporting Bugs +For more information on -1. Always update to the most recent master release; the bug may already be resolved. +- how to set up your development environment +- first-time project suggestions +- code conventions +- step-by-step guide for GitHub commits -2. Search for similar issues on the [Discourse meta forum][m]; it may already be an identified problem. +**please read our [Discourse Development Contribution Guidelines](https://meta.discourse.org/t/discourse-development-contribution-guidelines/3823)** -3. Make sure you can reproduce your problem on our sandbox at [try.discourse.org](http://try.discourse.org) +## Everything Else -4. If this is a bug or problem that **requires any kind of extended discussion -- open [a topic on meta][m] about it**. +There are many other ways to contribute to Discourse besides code. We've outlined the most common ones below. -5. If possible, submit a Pull Request with a failing test. If you'd rather take matters into your own hands, fix the bug yourself (jump down to the "Contributing (Step-by-step)" section). +- [Reporting Bugs](https://meta.discourse.org/t/how-to-make-bug-reports-for-discourse/33070) +- [Requesting Features](https://meta.discourse.org/t/how-to-request-new-features-for-discourse/32986) +- [Translation](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882) +- Documentation (TBA) -6. When the bug is fixed, we will do our best to update the Discourse topic. +For anything else, just start a new topic on [Meta](https://meta.discourse.org/) and let us know what you're interested in working on. -## Requesting New Features - -1. Do not submit a feature request on GitHub; all feature requests on GitHub will be closed. Instead, visit the **[Discourse meta forum, features category](http://meta.discourse.org/category/feature)**, and search this list for similar feature requests. It's possible somebody has already asked for this feature or provided a pull request that we're still discussing. - -2. Provide a clear and detailed explanation of the feature you want and why it's important to add. The feature must apply to a wide array of users of Discourse; for smaller, more targeted "one-off" features, you might consider writing a plugin for Discourse. You may also want to provide us with some advance documentation on the feature, which will help the community to better understand where it will fit. - -3. If you're a Rock Star programmer, build the feature yourself (refer to the "Contributing (Step-by-step)" section below). - -## Contributing (Step-by-step) - -1. Clone the Repo: - - git clone git://github.com/discourse/discourse.git - -2. Create a new Branch: - - cd discourse - git checkout -b new_discourse_branch - - > Please keep your code clean: one feature or bug-fix per branch. If you find another bug, you want to fix while being in a new branch, please fix it in a separated branch instead. - -3. Code - * Adhere to common conventions you see in the existing code - * Include tests, and ensure they pass - * Search to see if your new functionality has been discussed on [the Discourse meta forum](http://meta.discourse.org), and include updates as appropriate - -4. Follow the Coding Conventions - * two spaces, no tabs - * no trailing whitespaces, blank lines should have no spaces - * use spaces around operators, after commas, colons, semicolons, around `{` and before `}` - * no space after `(`, `[` or before `]`, `)` - * use Ruby 1.9 hash syntax: prefer `{ a: 1 }` over `{ :a => 1 }` - * prefer `class << self; def method; end` over `def self.method` for class methods - * prefer `{ ... }` over `do ... end` for single-line blocks, avoid using `{ ... }` for multi-line blocks - * avoid `return` when not required - - > However, please note that **pull requests consisting entirely of style changes are not welcome on this project**. Style changes in the context of pull requests that also refactor code, fix bugs, improve functionality *are* welcome. - -5. Commit - - For every commit please write a short (max 72 characters) summary in the first line followed with a blank line and then more detailed descriptions of the change. Use markdown syntax for simple styling. - - **NEVER leave the commit message blank!** Provide a detailed, clear, and complete description of your commit! - - -6. Update your branch - - ``` - git fetch origin - git rebase origin/master - ``` - -7. Fork - - ``` - git remote add mine git@github.com:<your user name>/discourse.git - ``` - -8. Push to your remote - - ``` - git push mine new_discourse_branch - ``` - -9. Issue a Pull Request - - Before submitting a pull-request, clean up the history, go over your commits and squash together minor changes and fixes into the corresponding commits. You can squash commits with the interactive rebase command: - - ``` - git fetch origin - git checkout new_discourse_branch - git rebase origin/master - git rebase -i - - < the editor opens and allows you to change the commit history > - < follow the instructions on the bottom of the editor > - - git push -f mine new_discourse_branch - ``` - - - In order to make a pull request, - * Navigate to the Discourse repository you just pushed to (e.g. https://github.com/your-user-name/discourse) - * Click "Pull Request". - * Write your branch name in the branch field (this is filled with "master" by default) - * Click "Update Commit Range". - * Ensure the changesets you introduced are included in the "Commits" tab. - * Ensure that the "Files Changed" incorporate all of your changes. - * Fill in some details about your potential patch including a meaningful title. - * Click "Send pull request". - - Thanks for that -- we'll get to your pull request ASAP, we love pull requests! - -10. Responding to Feedback - - The Discourse team may recommend adjustments to your code. Part of interacting with a healthy open-source community requires you to be open to learning new techniques and strategies; *don't get discouraged!* Remember: if the Discourse team suggest changes to your code, **they care enough about your work that they want to include it**, and hope that you can assist by implementing those revisions on your own. - - > Though we ask you to clean your history and squash commit before submitting a pull-request, please do not change any commits you've submitted already (as other work might be build on top). - -## Translations - -Translators can do their work in our [Transifex project](https://www.transifex.com/projects/p/discourse-org/). For more information, please see these how-to topics: - -* [Contributing a translation to Discourse](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882) -* [How to add a new language](https://meta.discourse.org/t/how-to-add-a-new-language/14970) - - - -[m]: http://meta.discourse.org +*Thanks for contributing!* diff --git a/Gemfile b/Gemfile index 98cdfdd79..233268918 100644 --- a/Gemfile +++ b/Gemfile @@ -63,7 +63,8 @@ gem 'email_reply_parser' # note: for image_optim to correctly work you need to follow # https://github.com/toy/image_optim -gem 'image_optim' +# pinned due to https://github.com/toy/image_optim/pull/75, docker image must be upgraded to upgrade +gem 'image_optim', '0.20.2' gem 'multi_json' gem 'mustache' gem 'nokogiri' @@ -90,6 +91,7 @@ gem 'rinku' gem 'sanitize' gem 'sass' gem 'sidekiq' +gem 'sidekiq-statistic' # for sidekiq web gem 'sinatra', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 607e318db..e33b7a10e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -209,7 +209,7 @@ GEM omniauth-twitter (1.0.1) multi_json (~> 1.3) omniauth-oauth (~> 1.0) - onebox (1.5.24) + onebox (1.5.25) moneta (~> 0.8) multi_json (~> 1.11) mustache @@ -333,6 +333,8 @@ GEM json (~> 1.0) redis (~> 3.2, >= 3.2.1) redis-namespace (~> 1.5, >= 1.5.2) + sidekiq-statistic (1.1.0) + sidekiq (~> 3.3, >= 3.3.4) simple-rss (1.3.1) simplecov (0.9.1) docile (~> 1.1.0) @@ -420,7 +422,7 @@ DEPENDENCIES highline hiredis htmlentities - image_optim + image_optim (= 0.20.2) librarian (>= 0.0.25) listen (= 0.7.3) logster @@ -474,6 +476,7 @@ DEPENDENCIES seed-fu (~> 2.3.3) shoulda sidekiq + sidekiq-statistic simple-rss simplecov sinatra @@ -485,3 +488,6 @@ DEPENDENCIES uglifier unf unicorn + +BUNDLED WITH + 1.10.6 diff --git a/app/assets/javascripts/admin/components/ip-lookup.js.es6 b/app/assets/javascripts/admin/components/ip-lookup.js.es6 index bf46c9a2d..f1fe7a391 100644 --- a/app/assets/javascripts/admin/components/ip-lookup.js.es6 +++ b/app/assets/javascripts/admin/components/ip-lookup.js.es6 @@ -22,7 +22,7 @@ export default Ember.Component.extend({ this.set("show", true); if (!this.get("location")) { - Discourse.ajax("/admin/users/ip-info.json", { + Discourse.ajax("/admin/users/ip-info", { data: { ip: this.get("ip") } }).then(function (location) { self.set("location", Em.Object.create(location)); @@ -38,7 +38,7 @@ export default Ember.Component.extend({ "order": "trust_level DESC" }; - Discourse.ajax("/admin/users/total-others-with-same-ip.json", { data: data }).then(function (result) { + Discourse.ajax("/admin/users/total-others-with-same-ip", { data }).then(function (result) { self.set("totalOthersWithSameIP", result.total); }); diff --git a/app/assets/javascripts/admin/controllers/admin-group.js.es6 b/app/assets/javascripts/admin/controllers/admin-group.js.es6 index f16151ed2..2ee95299a 100644 --- a/app/assets/javascripts/admin/controllers/admin-group.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-group.js.es6 @@ -39,7 +39,7 @@ export default Ember.Controller.extend({ if (this.get("showingLast")) { return; } const group = this.get("model"), - offset = Math.min(group.get("offset") + group.get("model.limit"), group.get("user_count")); + offset = Math.min(group.get("offset") + group.get("limit"), group.get("user_count")); group.set("offset", offset); @@ -50,7 +50,7 @@ export default Ember.Controller.extend({ if (this.get("showingFirst")) { return; } const group = this.get("model"), - offset = Math.max(group.get("offset") - group.get("model.limit"), 0); + offset = Math.max(group.get("offset") - group.get("limit"), 0); group.set("offset", offset); diff --git a/app/assets/javascripts/admin/routes/admin-plugins.js.es6 b/app/assets/javascripts/admin/routes/admin-plugins.js.es6 index f1092e500..820c5207d 100644 --- a/app/assets/javascripts/admin/routes/admin-plugins.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-plugins.js.es6 @@ -6,7 +6,7 @@ export default Ember.Route.extend({ actions: { showSettings(plugin) { const controller = this.controllerFor('adminSiteSettings'); - this.transitionTo('adminSiteSettingsCategory', 'plugins').then(function() { + this.transitionTo('adminSiteSettingsCategory', 'plugins').then(() => { if (plugin) { const match = /^(.*)_enabled/.exec(plugin.get('enabled_setting')); if (match[1]) { diff --git a/app/assets/javascripts/admin/templates/email_preview_digest.hbs b/app/assets/javascripts/admin/templates/email_preview_digest.hbs index 6d95eb066..83d99db35 100644 --- a/app/assets/javascripts/admin/templates/email_preview_digest.hbs +++ b/app/assets/javascripts/admin/templates/email_preview_digest.hbs @@ -8,9 +8,9 @@ <div class="toggle"> <label>{{i18n 'admin.email.format'}}</label> {{#if showHtml}} - <span>{{i18n 'admin.email.html'}}</span> | <a href='#' {{action "toggleShowHtml"}}>{{i18n 'admin.email.text'}}</a> + <span>{{i18n 'admin.email.html'}}</span> | <a href {{action "toggleShowHtml"}}>{{i18n 'admin.email.text'}}</a> {{else}} - <a href='#' {{action "toggleShowHtml"}}>{{i18n 'admin.email.html'}}</a> | <span>{{i18n 'admin.email.text'}}</span> + <a href {{action "toggleShowHtml"}}>{{i18n 'admin.email.html'}}</a> | <span>{{i18n 'admin.email.text'}}</span> {{/if}} </div> </div> diff --git a/app/assets/javascripts/admin/templates/group.hbs b/app/assets/javascripts/admin/templates/group.hbs index 3da80d9af..ac1f97969 100644 --- a/app/assets/javascripts/admin/templates/group.hbs +++ b/app/assets/javascripts/admin/templates/group.hbs @@ -13,9 +13,9 @@ <div> <label>{{i18n 'admin.groups.group_members'}} ({{model.user_count}})</label> <div> - <a {{bind-attr class=":previous showingFirst:disabled"}} {{action "previous"}}>{{fa-icon "fast-backward"}}</a> + <a class="previous {{if showingFirst 'disabled'}}" {{action "previous"}}>{{fa-icon "fast-backward"}}</a> {{currentPage}}/{{totalPages}} - <a {{bind-attr class=":next showingLast:disabled"}} {{action "next"}}>{{fa-icon "fast-forward"}}</a> + <a class="next {{if showingLast 'disabled'}}" {{action "next"}}>{{fa-icon "fast-forward"}}</a> </div> <div class="ac-wrap clearfix"> {{#each model.members as |member|}} @@ -28,7 +28,7 @@ <div> <label for="user-selector">{{i18n 'admin.groups.add_members'}}</label> {{user-selector usernames=model.usernames placeholderKey="admin.groups.selector_placeholder" id="user-selector"}} - <button {{action "addMembers"}} class='btn add'>{{fa-icon "plus"}} {{i18n 'admin.groups.add'}}</button> + {{d-button action="addMembers" class="add" icon="plus" label="admin.groups.add"}} </div> {{/unless}} {{/if}} diff --git a/app/assets/javascripts/admin/templates/logs/site_customization_change_modal.hbs b/app/assets/javascripts/admin/templates/logs/site_customization_change_modal.hbs index 2c995713b..556f6310d 100644 --- a/app/assets/javascripts/admin/templates/logs/site_customization_change_modal.hbs +++ b/app/assets/javascripts/admin/templates/logs/site_customization_change_modal.hbs @@ -1,10 +1,10 @@ <div> <ul class="nav nav-pills"> <li {{bind-attr class="newSelected:active"}}> - <a href="#" {{action "selectNew"}}>{{i18n 'admin.logs.staff_actions.new_value'}}</a> + <a href {{action "selectNew"}}>{{i18n 'admin.logs.staff_actions.new_value'}}</a> </li> <li {{bind-attr class="previousSelected:active"}}> - <a href="#" {{action "selectPrevious"}}>{{i18n 'admin.logs.staff_actions.previous_value'}}</a> + <a href {{action "selectPrevious"}}>{{i18n 'admin.logs.staff_actions.previous_value'}}</a> </li> </ul> <div class="modal-body"> diff --git a/app/assets/javascripts/admin/templates/plugins-index.hbs b/app/assets/javascripts/admin/templates/plugins-index.hbs index 95b979de4..5236d9d99 100644 --- a/app/assets/javascripts/admin/templates/plugins-index.hbs +++ b/app/assets/javascripts/admin/templates/plugins-index.hbs @@ -37,7 +37,7 @@ {{i18n "admin.plugins.not_enabled"}} {{/if}} {{else}} - {{i18n "admin.plugins.cant_disable"}} + {{i18n "admin.plugins.is_enabled"}} {{/if}} </td> <td> diff --git a/app/assets/javascripts/discourse/components/actions-summary.js.es6 b/app/assets/javascripts/discourse/components/actions-summary.js.es6 index 21c2076aa..bac5779d4 100644 --- a/app/assets/javascripts/discourse/components/actions-summary.js.es6 +++ b/app/assets/javascripts/discourse/components/actions-summary.js.es6 @@ -19,7 +19,7 @@ export default Ember.Component.extend(StringBuffer, { const renderActionIf = function(property, dataAttribute, text) { if (!c.get(property)) { return; } - buffer.push(" <span class='action-link " + dataAttribute +"-action'><a href='#' data-" + dataAttribute + "='" + c.get('id') + "'>" + text + "</a>.</span>"); + buffer.push(" <span class='action-link " + dataAttribute +"-action'><a href data-" + dataAttribute + "='" + c.get('id') + "'>" + text + "</a>.</span>"); }; // TODO multi line expansion for flags diff --git a/app/assets/javascripts/discourse/components/auto-close-form.js.es6 b/app/assets/javascripts/discourse/components/auto-close-form.js.es6 index fdad14df5..f6ab041c7 100644 --- a/app/assets/javascripts/discourse/components/auto-close-form.js.es6 +++ b/app/assets/javascripts/discourse/components/auto-close-form.js.es6 @@ -1,26 +1,32 @@ +import computed from "ember-addons/ember-computed-decorators"; +import { observes } from "ember-addons/ember-computed-decorators"; + export default Ember.Component.extend({ - autoCloseValid: false, limited: false, + autoCloseValid: false, - autoCloseUnits: function() { - var key = this.get("limited") ? "composer.auto_close.limited.units" - : "composer.auto_close.all.units"; + @computed("limited") + autoCloseUnits(limited) { + const key = limited ? "composer.auto_close.limited.units" : "composer.auto_close.all.units"; return I18n.t(key); - }.property("limited"), + }, - autoCloseExamples: function() { - var key = this.get("limited") ? "composer.auto_close.limited.examples" - : "composer.auto_close.all.examples"; + @computed("limited") + autoCloseExamples(limited) { + const key = limited ? "composer.auto_close.limited.examples" : "composer.auto_close.all.examples"; return I18n.t(key); - }.property("limited"), + }, - _updateAutoCloseValid: function() { - var isValid = this._isAutoCloseValid(this.get("autoCloseTime"), this.get("limited")); + @observes("autoCloseTime", "limited") + _updateAutoCloseValid() { + const limited = this.get("limited"), + autoCloseTime = this.get("autoCloseTime"), + isValid = this._isAutoCloseValid(autoCloseTime, limited); this.set("autoCloseValid", isValid); - }.observes("autoCloseTime", "limited"), + }, - _isAutoCloseValid: function(autoCloseTime, limited) { - var t = (autoCloseTime || "").toString().trim(); + _isAutoCloseValid(autoCloseTime, limited) { + const t = (autoCloseTime || "").toString().trim(); if (t.length === 0) { // "empty" is always valid return true; diff --git a/app/assets/javascripts/discourse/components/autofocus-text-field.js.es6 b/app/assets/javascripts/discourse/components/autofocus-text-field.js.es6 index 29ecbb0d3..dd1ccd987 100644 --- a/app/assets/javascripts/discourse/components/autofocus-text-field.js.es6 +++ b/app/assets/javascripts/discourse/components/autofocus-text-field.js.es6 @@ -1,7 +1,12 @@ +import { on } from "ember-addons/ember-computed-decorators"; + export default Ember.TextField.extend({ - becomeFocused: function() { - var input = this.get("element"); + + @on("didInsertElement") + becomeFocused() { + const input = this.get("element"); input.focus(); input.selectionStart = input.selectionEnd = input.value.length; - }.on('didInsertElement') + } + }); diff --git a/app/assets/javascripts/discourse/components/avatar-uploader.js.es6 b/app/assets/javascripts/discourse/components/avatar-uploader.js.es6 index 837960646..539171bc9 100644 --- a/app/assets/javascripts/discourse/components/avatar-uploader.js.es6 +++ b/app/assets/javascripts/discourse/components/avatar-uploader.js.es6 @@ -1,3 +1,4 @@ +import computed from "ember-addons/ember-computed-decorators"; import UploadMixin from "discourse/mixins/upload"; export default Em.Component.extend(UploadMixin, { @@ -5,21 +6,23 @@ export default Em.Component.extend(UploadMixin, { tagName: "span", imageIsNotASquare: false, - uploadButtonText: function() { - return this.get("uploading") ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture"); - }.property("uploading"), + @computed("uploading") + uploadButtonText(uploading) { + return uploading ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture"); + }, uploadDone(upload) { this.setProperties({ imageIsNotASquare: upload.width !== upload.height, uploadedAvatarTemplate: upload.url, - custom_avatar_upload_id: upload.id, + uploadedAvatarId: upload.id, }); this.sendAction("done"); }, - data: function() { - return { user_id: this.get("user_id") }; - }.property("user_id") + @computed("user_id") + data(user_id) { + return { user_id }; + } }); diff --git a/app/assets/javascripts/discourse/components/categories-admin-dropdown.js.es6 b/app/assets/javascripts/discourse/components/categories-admin-dropdown.js.es6 new file mode 100644 index 000000000..b2fea03ff --- /dev/null +++ b/app/assets/javascripts/discourse/components/categories-admin-dropdown.js.es6 @@ -0,0 +1,39 @@ +import { iconHTML } from 'discourse/helpers/fa-icon'; +import DropdownButton from 'discourse/components/dropdown-button'; +import computed from "ember-addons/ember-computed-decorators"; + +export default DropdownButton.extend({ + buttonExtraClasses: 'no-text', + title: '', + text: iconHTML('bars') + ' ' + iconHTML('caret-down'), + classNames: ['category-notification-menu', 'category-admin-menu'], + + @computed() + dropDownContent() { + const includeReorder = this.get('siteSettings.fixed_category_positions'); + const items = [ + { id: 'create', + title: I18n.t('category.create'), + description: I18n.t('category.create_long'), + styleClasses: 'fa fa-plus' } + ]; + if (includeReorder) { + items.push({ + id: 'reorder', + title: I18n.t('categories.reorder.title'), + description: I18n.t('categories.reorder.title_long'), + styleClasses: 'fa fa-random' + }); + } + return items; + }, + + actionNames: { + create: 'createCategory', + reorder: 'reorderCategories' + }, + + clicked(id) { + this.sendAction('actionNames.' + id); + } +}); diff --git a/app/assets/javascripts/discourse/components/category-chooser.js.es6 b/app/assets/javascripts/discourse/components/category-chooser.js.es6 index 57a7ed969..df61b75dd 100644 --- a/app/assets/javascripts/discourse/components/category-chooser.js.es6 +++ b/app/assets/javascripts/discourse/components/category-chooser.js.es6 @@ -1,5 +1,7 @@ import ComboboxView from 'discourse/components/combo-box'; import { categoryBadgeHTML } from 'discourse/helpers/category-link'; +import computed from 'ember-addons/ember-computed-decorators'; +import { observes, on } from 'ember-addons/ember-computed-decorators'; export default ComboboxView.extend({ classNames: ['combobox category-combobox'], @@ -8,46 +10,34 @@ export default ComboboxView.extend({ valueBinding: Ember.Binding.oneWay('source'), castInteger: true, - content: function() { - let scopedCategoryId = this.get('scopedCategoryId'); - + @computed("scopedCategoryId", "categories") + content(scopedCategoryId, categories) { // Always scope to the parent of a category, if present if (scopedCategoryId) { const scopedCat = Discourse.Category.findById(scopedCategoryId); scopedCategoryId = scopedCat.get('parent_category_id') || scopedCat.get('id'); } - return this.get('categories').filter(function(c) { - if (scopedCategoryId && (c.get('id') !== scopedCategoryId) && (c.get('parent_category_id') !== scopedCategoryId)) { - return false; - } - return c.get('permission') === Discourse.PermissionType.FULL && !c.get('isUncategorizedCategory'); + return categories.filter(c => { + if (scopedCategoryId && c.get('id') !== scopedCategoryId && c.get('parent_category_id') !== scopedCategoryId) { return false; } + if (c.get('isUncategorizedCategory')) { return false; } + return c.get('permission') === Discourse.PermissionType.FULL; }); - }.property('scopedCategoryId', 'categories'), + }, - _setCategories: function() { + @on("init") + @observes("site.sortedCategories") + _updateCategories() { + const categories = Discourse.SiteSettings.fixed_category_positions_on_create ? + Discourse.Category.list() : + Discourse.Category.listByActivity(); + this.set('categories', categories); + }, - if (!this.get('categories')) { - this.set('automatic', true); - } - - this._updateCategories(); - - }.on('init'), - - _updateCategories: function() { - - if (this.get('automatic')) { - this.set('categories', - Discourse.SiteSettings.fixed_category_positions_on_create ? - Discourse.Category.list() : Discourse.Category.listByActivity() - ); - } - }.observes('automatic', 'site.sortedCategories'), - - none: function() { + @computed("rootNone") + none(rootNone) { if (Discourse.User.currentProp('staff') || Discourse.SiteSettings.allow_uncategorized_topics) { - if (this.get('rootNone')) { + if (rootNone) { return "category.none"; } else { return Discourse.Category.findUncategorized(); @@ -55,10 +45,9 @@ export default ComboboxView.extend({ } else { return 'category.choose'; } - }.property(), + }, comboTemplate(item) { - let category; // If we have no id, but text with the uncategorized name, we can use that badge. @@ -79,16 +68,14 @@ export default ComboboxView.extend({ result = categoryBadgeHTML(Discourse.Category.findById(parentCategoryId), {link: false}) + " " + result; } - result += " <span class='topic-count'>× " + category.get('topic_count') + "</span>"; + result += ` <span class='topic-count'>× ${category.get('topic_count')}</span>`; const description = category.get('description'); // TODO wtf how can this be null?; if (description && description !== 'null') { - result += '<div class="category-desc">' + - description.substr(0,200) + - (description.length > 200 ? '…' : '') + - '</div>'; + result += `<div class="category-desc">${description.substr(0, 200)}${description.length > 200 ? '…' : ''}</div>`; } + return result; } diff --git a/app/assets/javascripts/discourse/components/create-topics-notice.js.es6 b/app/assets/javascripts/discourse/components/create-topics-notice.js.es6 index 9f502ec7c..9481bfbd4 100644 --- a/app/assets/javascripts/discourse/components/create-topics-notice.js.es6 +++ b/app/assets/javascripts/discourse/components/create-topics-notice.js.es6 @@ -54,23 +54,17 @@ export default Ember.Component.extend({ })); }, - @computed() - topicTrackingState() { - return Discourse.TopicTrackingState.current(); - }, - @observes('topicTrackingState.incomingCount') fetchLiveStats() { if (!this.get('enabled')) { return; } - var self = this; - LivePostCounts.find().then(function(stats) { + LivePostCounts.find().then((stats) => { if(stats) { - self.set('publicTopicCount', stats.get('public_topic_count')); - self.set('publicPostCount', stats.get('public_post_count')); - if (self.get('publicTopicCount') >= self.get('requiredTopics') - && self.get('publicPostCount') >= self.get('requiredPosts')) { - self.set('enabled', false); // No more checks + this.set('publicTopicCount', stats.get('public_topic_count')); + this.set('publicPostCount', stats.get('public_post_count')); + if (this.get('publicTopicCount') >= this.get('requiredTopics') + && this.get('publicPostCount') >= this.get('requiredPosts')) { + this.set('enabled', false); // No more checks } } }); diff --git a/app/assets/javascripts/discourse/components/d-button.js.es6 b/app/assets/javascripts/discourse/components/d-button.js.es6 index 5fcc00a0f..068e814ca 100644 --- a/app/assets/javascripts/discourse/components/d-button.js.es6 +++ b/app/assets/javascripts/discourse/components/d-button.js.es6 @@ -8,9 +8,9 @@ export default Ember.Component.extend({ noText: Ember.computed.empty('translatedLabel'), - @computed("title", "translatedLabel") - translatedTitle(title, translatedLabel) { - return title ? I18n.t(title) : translatedLabel; + @computed("title") + translatedTitle(title) { + if (title) return I18n.t(title); }, @computed("label") diff --git a/app/assets/javascripts/discourse/components/d-link.js.es6 b/app/assets/javascripts/discourse/components/d-link.js.es6 index a0babf304..79a55a9fe 100644 --- a/app/assets/javascripts/discourse/components/d-link.js.es6 +++ b/app/assets/javascripts/discourse/components/d-link.js.es6 @@ -1,9 +1,10 @@ import computed from 'ember-addons/ember-computed-decorators'; import { iconHTML } from 'discourse/helpers/fa-icon'; -import DiscourseURL from 'discourse/lib/url'; +import interceptClick from 'discourse/lib/intercept-click'; export default Ember.Component.extend({ tagName: 'a', + classNames: ['d-link'], attributeBindings: ['translatedTitle:title', 'translatedTitle:aria-title', 'href'], @computed('path') @@ -14,7 +15,13 @@ export default Ember.Component.extend({ if (route) { const router = this.container.lookup('router:main'); if (router && router.router) { - return router.router.generate(route, this.get('model')); + const params = [route]; + const model = this.get('model'); + if (model) { + params.push(model); + } + + return router.router.generate.apply(router.router, params); } } @@ -27,18 +34,14 @@ export default Ember.Component.extend({ if (text) return I18n.t(text); }, - click() { + click(e) { const action = this.get('action'); if (action) { this.sendAction('action'); return false; } - const href = this.get('href'); - if (href) { - DiscourseURL.routeTo(href); - return false; - } - return false; + + return interceptClick(e); }, render(buffer) { @@ -55,7 +58,8 @@ export default Ember.Component.extend({ if (label) { if (icon) { buffer.push(" "); } - buffer.push(I18n.t(label)); + const count = this.get('count'); + buffer.push(I18n.t(label, { count })); } } diff --git a/app/assets/javascripts/discourse/components/dropdown-button.js.es6 b/app/assets/javascripts/discourse/components/dropdown-button.js.es6 index 99d211bbe..0037ec17f 100644 --- a/app/assets/javascripts/discourse/components/dropdown-button.js.es6 +++ b/app/assets/javascripts/discourse/components/dropdown-button.js.es6 @@ -29,9 +29,7 @@ export default Ember.Component.extend(StringBuffer, { buffer.push("<h4 class='title'>" + title + "</h4>"); } - buffer.push("<button class='btn standard dropdown-toggle' data-toggle='dropdown'>"); - buffer.push(this.get('text')); - buffer.push("</button>"); + buffer.push(`<button class='btn standard dropdown-toggle ${this.get('buttonExtraClasses')}' data-toggle='dropdown'>${this.get('text')}</button>`); buffer.push("<ul class='dropdown-menu'>"); const contents = this.get('dropDownContent'); 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 5d323ce06..e26c24a21 100644 --- a/app/assets/javascripts/discourse/components/edit-category-general.js.es6 +++ b/app/assets/javascripts/discourse/components/edit-category-general.js.es6 @@ -1,6 +1,7 @@ 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'; export default buildCategoryPanel('general', { foregroundColors: ['FFFFFF', '000000'], @@ -31,7 +32,7 @@ export default buildCategoryPanel('general', { categoryBadgePreview: function() { const category = this.get('category'); - const c = Discourse.Category.create({ + const c = Category.create({ name: category.get('categoryName'), color: category.get('color'), text_color: category.get('text_color'), @@ -45,7 +46,7 @@ export default buildCategoryPanel('general', { // We can change the parent if there are no children subCategories: function() { if (Ember.isEmpty(this.get('category.id'))) { return null; } - return Discourse.Category.list().filterBy('parent_category_id', this.get('category.id')); + return Category.list().filterBy('parent_category_id', this.get('category.id')); }.property('category.id'), showDescription: function() { diff --git a/app/assets/javascripts/discourse/components/edit-category-security.js.es6 b/app/assets/javascripts/discourse/components/edit-category-security.js.es6 index 593a604e7..60c3f4ddc 100644 --- a/app/assets/javascripts/discourse/components/edit-category-security.js.es6 +++ b/app/assets/javascripts/discourse/components/edit-category-security.js.es6 @@ -7,16 +7,24 @@ export default buildCategoryPanel('security', { actions: { editPermissions() { - this.set('editingPermissions', true); + if (!this.get('category.is_special')) { + this.set('editingPermissions', true); + } }, addPermission(group, id) { - this.get('category').addPermission({group_name: group + "", - permission: Discourse.PermissionType.create({id})}); + if (!this.get('category.is_special')) { + this.get('category').addPermission({ + group_name: group + "", + permission: Discourse.PermissionType.create({id}) + }); + } }, removePermission(permission) { - this.get('category').removePermission(permission); + if (!this.get('category.is_special')) { + this.get('category').removePermission(permission); + } }, } }); diff --git a/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 index 903c705c6..910b8d661 100644 --- a/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/components/hamburger-menu.js.es6 @@ -2,6 +2,12 @@ import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Component.extend({ classNames: ['hamburger-panel'], + @computed('currentUser.read_faq') + prioritizeFaq(readFaq) { + // If it's a custom FAQ never prioritize it + return Ember.isEmpty(this.siteSettings.faq_url) && !readFaq; + }, + @computed() showKeyboardShortcuts() { return !Discourse.Mobile.mobileView && !this.capabilities.touch; @@ -22,6 +28,21 @@ export default Ember.Component.extend({ return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq'); }, + _lookupCount(type) { + const state = this.get('topicTrackingState'); + return state ? state.lookupCount(type) : 0; + }, + + @computed('topicTrackingState.messageCount') + newCount() { + return this._lookupCount('new'); + }, + + @computed('topicTrackingState.messageCount') + unreadCount() { + return this._lookupCount('unread'); + }, + @computed() categories() { const hideUncategorized = !this.siteSettings.allow_uncategorized_topics; diff --git a/app/assets/javascripts/discourse/components/header-dropdown.js.es6 b/app/assets/javascripts/discourse/components/header-dropdown.js.es6 index 4990aa107..02843970a 100644 --- a/app/assets/javascripts/discourse/components/header-dropdown.js.es6 +++ b/app/assets/javascripts/discourse/components/header-dropdown.js.es6 @@ -1,11 +1,24 @@ +import computed from 'ember-addons/ember-computed-decorators'; + export default Ember.Component.extend({ tagName: 'li', classNameBindings: [':header-dropdown-toggle', 'active'], + @computed('showUser') + href(showUser) { + return showUser ? this.currentUser.get('path') : ''; + }, + active: Ember.computed.alias('toggleVisible'), actions: { toggle() { + + if (Discourse.Mobile.mobileView && this.get('mobileAction')) { + this.sendAction('mobileAction'); + return; + } + if (this.siteSettings.login_required && !this.currentUser) { this.sendAction('loginAction'); } else { diff --git a/app/assets/javascripts/discourse/components/menu-panel.js.es6 b/app/assets/javascripts/discourse/components/menu-panel.js.es6 index 6d1edefb2..02304afee 100644 --- a/app/assets/javascripts/discourse/components/menu-panel.js.es6 +++ b/app/assets/javascripts/discourse/components/menu-panel.js.es6 @@ -1,4 +1,5 @@ import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; +import { headerHeight } from 'discourse/views/header'; const PANEL_BODY_MARGIN = 30; const mutationSupport = !!window['MutationObserver']; @@ -21,36 +22,39 @@ export default Ember.Component.extend({ const viewMode = this.get('viewMode'); const $panelBody = this.$('.panel-body'); + let contentHeight = parseInt(this.$('.panel-body-contents').height()); if (viewMode === 'drop-down') { const $buttonPanel = $('header ul.icons'); if ($buttonPanel.length === 0) { return; } - const buttonPanelPos = $buttonPanel.offset(); - - const posTop = parseInt(buttonPanelPos.top + $buttonPanel.height() - $('header.d-header').offset().top); - const posLeft = parseInt(buttonPanelPos.left + $buttonPanel.width() - width); - - this.$().css({ left: posLeft + "px", top: posTop + "px" }); + // These values need to be set here, not in the css file - this is to deal with the + // possibility of the window being resized and the menu changing from .slide-in to .drop-down. + this.$().css({ top: '100%', height: 'auto' }); // adjust panel height - let contentHeight = parseInt(this.$('.panel-body-contents').height()); const fullHeight = parseInt($window.height()); - const offsetTop = this.$().offset().top; const scrollTop = $window.scrollTop(); + if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) { contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN; } $panelBody.height(contentHeight); $('body').addClass('drop-down-visible'); } else { - $panelBody.height('auto'); - const $header = $('header.d-header'); - const headerOffset = $header.offset(); - const headerOffsetTop = (headerOffset) ? headerOffset.top : 0; - const headerHeight = parseInt($header.height() + headerOffsetTop - $window.scrollTop() + 3); - this.$().css({ left: "auto", top: headerHeight + "px" }); + + const menuTop = headerHeight(); + + let height; + if ((menuTop + contentHeight) < ($(window).height() - 20)) { + height = contentHeight + "px"; + } else { + height = $(window).height() - menuTop; + } + + $panelBody.height('100%'); + this.$().css({ top: menuTop + "px", height }); $('body').removeClass('drop-down-visible'); } @@ -82,7 +86,11 @@ export default Ember.Component.extend({ }); this.performLayout(); this._watchSizeChanges(); - $(window).on('scroll.discourse-menu-panel', () => this.performLayout()); + + // iOS does not handle scroll events well + if (!this.capabilities.touch) { + $(window).on('scroll.discourse-menu-panel', () => this.performLayout()); + } } else { Ember.run.scheduleOnce('afterRender', () => this.sendAction('onHidden')); $('html').off('click.close-menu-panel'); @@ -124,9 +132,13 @@ export default Ember.Component.extend({ clearInterval(this._resizeInterval); this._resizeInterval = setInterval(() => { Ember.run(() => { - const contentHeight = parseInt(this.$('.panel-body-contents').height()); - if (contentHeight !== this._lastHeight) { this.performLayout(); } - this._lastHeight = contentHeight; + const $panelBodyContents = this.$('.panel-body-contents'); + + if ($panelBodyContents.length) { + const contentHeight = parseInt($panelBodyContents.height()); + if (contentHeight !== this._lastHeight) { this.performLayout(); } + this._lastHeight = contentHeight; + } }); }, 500); } @@ -142,7 +154,8 @@ export default Ember.Component.extend({ @on('didInsertElement') _bindEvents() { - this.$().on('click.discourse-menu-panel', 'a', (e) => { + this.$().on('click.discourse-menu-panel', 'a', e => { + if (e.metaKey) { return; } if ($(e.target).data('ember-action')) { return; } this.hide(); }); @@ -150,7 +163,7 @@ export default Ember.Component.extend({ this.appEvents.on('dropdowns:closeAll', this, this.hide); this.appEvents.on('dom:clean', this, this.hide); - $('body').on('keydown.discourse-menu-panel', (e) => { + $('body').on('keydown.discourse-menu-panel', e => { if (e.which === 27) { this.hide(); } @@ -178,7 +191,7 @@ export default Ember.Component.extend({ $('body').off('keydown.discourse-menu-panel'); $('html').off('click.close-menu-panel'); $(window).off('resize.discourse-menu-panel'); - $(window).off('scroll.discourse-menu-panel'); + $(window).off('scroll.discourse-menu-panel'); }, hide() { diff --git a/app/assets/javascripts/discourse/components/nav-item.js.es6 b/app/assets/javascripts/discourse/components/nav-item.js.es6 index 87ab1007a..69c797bd3 100644 --- a/app/assets/javascripts/discourse/components/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/components/nav-item.js.es6 @@ -1,24 +1,28 @@ /* You might be looking for navigation-item. */ +import computed from "ember-addons/ember-computed-decorators"; + export default Ember.Component.extend({ tagName: 'li', classNameBindings: ['active'], - router: function() { + @computed() + router() { return this.container.lookup('router:main'); - }.property(), + }, - fullPath: function() { - return Discourse.getURL(this.get('path')); - }.property('path'), + @computed("path") + fullPath(path) { + return Discourse.getURL(path); + }, - active: function() { - const route = this.get('route'); + @computed("route", "router.url") + active(route) { if (!route) { return; } const routeParam = this.get('routeParam'), router = this.get('router'); return routeParam ? router.isActive(route, routeParam) : router.isActive(route); - }.property('router.url', 'route') + } }); diff --git a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 b/app/assets/javascripts/discourse/components/navigation-bar.js.es6 index 46fc846db..7bc237c90 100644 --- a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 +++ b/app/assets/javascripts/discourse/components/navigation-bar.js.es6 @@ -1,27 +1,25 @@ +import { default as computed, observes } from "ember-addons/ember-computed-decorators"; import DiscourseURL from 'discourse/lib/url'; export default Ember.Component.extend({ tagName: 'ul', classNameBindings: [':nav', ':nav-pills'], id: 'navigation-bar', - selectedNavItem: function(){ - const filterMode = this.get('filterMode'), - navItems = this.get('navItems'); - - var item = navItems.find(function(i){ - return i.get('filterMode').indexOf(filterMode) === 0; - }); + @computed("filterMode", "navItems") + selectedNavItem(filterMode, navItems){ + var item = navItems.find(i => i.get('filterMode').indexOf(filterMode) === 0); return item || navItems[0]; - }.property('filterMode'), + }, - closedNav: function(){ + @observes("expanded") + closedNav() { if (!this.get('expanded')) { this.ensureDropClosed(); } - }.observes('expanded'), + }, - ensureDropClosed: function(){ + ensureDropClosed() { if (!this.get('expanded')) { this.set('expanded',false); } @@ -30,25 +28,23 @@ export default Ember.Component.extend({ }, actions: { - toggleDrop: function(){ + toggleDrop() { this.set('expanded', !this.get('expanded')); - var self = this; - if (this.get('expanded')) { + if (this.get('expanded')) { DiscourseURL.appEvents.on('dom:clean', this, this.ensureDropClosed); - Em.run.next(function() { + Em.run.next(() => { + if (!this.get('expanded')) { return; } - if (!self.get('expanded')) { return; } - - self.$('.drop a').on('click', function(){ - self.$('.drop').hide(); - self.set('expanded', false); + this.$('.drop a').on('click', () => { + this.$('.drop').hide(); + this.set('expanded', false); return true; }); - $(window).on('click.navigation-bar', function() { - self.set('expanded', false); + $(window).on('click.navigation-bar', () => { + this.set('expanded', false); return true; }); }); diff --git a/app/assets/javascripts/discourse/components/navigation-item.js.es6 b/app/assets/javascripts/discourse/components/navigation-item.js.es6 index 075919ca7..758e4a385 100644 --- a/app/assets/javascripts/discourse/components/navigation-item.js.es6 +++ b/app/assets/javascripts/discourse/components/navigation-item.js.es6 @@ -1,3 +1,4 @@ +import computed from "ember-addons/ember-computed-decorators"; import StringBuffer from 'discourse/mixins/string-buffer'; export default Ember.Component.extend(StringBuffer, { @@ -7,22 +8,23 @@ export default Ember.Component.extend(StringBuffer, { hidden: Em.computed.not('content.visible'), rerenderTriggers: ['content.count'], - title: function() { - var categoryName = this.get('content.categoryName'), - name = this.get('content.name'), - extra = {}; + @computed("content.categoryName", "content.name") + title(categoryName, name) { + const extra = {}; if (categoryName) { name = "category"; extra.categoryName = categoryName; } - return I18n.t("filters." + name.replace("/", ".") + ".help", extra); - }.property("content.{categoryName,name}"), - active: function() { - return this.get('content.filterMode') === this.get('filterMode') || - this.get('filterMode').indexOf(this.get('content.filterMode')) === 0; - }.property('content.filterMode', 'filterMode'), + return I18n.t("filters." + name.replace("/", ".") + ".help", extra); + }, + + @computed("content.filterMode", "filterMode") + active(contentFilterMode, filterMode) { + return contentFilterMode === filterMode || + filterMode.indexOf(contentFilterMode) === 0; + }, renderString(buffer) { const content = this.get('content'); diff --git a/app/assets/javascripts/discourse/components/notification-item.js.es6 b/app/assets/javascripts/discourse/components/notification-item.js.es6 index f0add4c2f..2aa93e141 100644 --- a/app/assets/javascripts/discourse/components/notification-item.js.es6 +++ b/app/assets/javascripts/discourse/components/notification-item.js.es6 @@ -4,17 +4,19 @@ export default Ember.Component.extend({ tagName: 'li', classNameBindings: ['notification.read', 'notification.is_warning'], - scope: function() { + name: function() { var notificationType = this.get("notification.notification_type"); var lookup = this.site.get("notificationLookup"); - var name = lookup[notificationType]; + return lookup[notificationType]; + }.property("notification.notification_type"), - if (name === "custom") { + scope: function() { + if (this.get("name") === "custom") { return this.get("notification.data.message"); } else { - return "notifications." + name; + return "notifications." + this.get("name"); } - }.property("notification.notification_type"), + }.property("name"), url: function() { const it = this.get('notification'); @@ -57,7 +59,7 @@ export default Ember.Component.extend({ const url = this.get('url'); if (url) { - buffer.push('<a href="' + url + '">' + text + '</a>'); + buffer.push('<a href="' + url + '" alt="' + I18n.t('notifications.alt.' + this.get("name")) + '">' + text + '</a>'); } else { buffer.push(text); } diff --git a/app/assets/javascripts/discourse/components/number-field.js.es6 b/app/assets/javascripts/discourse/components/number-field.js.es6 new file mode 100644 index 000000000..5104138c0 --- /dev/null +++ b/app/assets/javascripts/discourse/components/number-field.js.es6 @@ -0,0 +1,29 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.TextField.extend({ + + classNameBindings: ['invalid'], + + @computed('number') + value: { + get(number) { + return parseInt(number); + }, + set(value) { + const num = parseInt(value); + if (isNaN(num)) { + this.set('invalid', true); + return value; + } else { + this.set('invalid', false); + this.set('number', num); + return num.toString(); + } + } + }, + + @computed("placeholderKey") + placeholder(key) { + return key ? I18n.t(key) : ""; + } +}); diff --git a/app/assets/javascripts/discourse/components/popup-input-tip.js.es6 b/app/assets/javascripts/discourse/components/popup-input-tip.js.es6 index 48d49e610..51725abc0 100644 --- a/app/assets/javascripts/discourse/components/popup-input-tip.js.es6 +++ b/app/assets/javascripts/discourse/components/popup-input-tip.js.es6 @@ -17,8 +17,8 @@ export default Ember.Component.extend(StringBuffer, { good: Ember.computed.not("bad"), @observes("shownAt") - bounce(shownAt) { - if (shownAt) { + bounce() { + if (this.get("shownAt")) { var $elem = this.$(); if (!this.animateAttribute) { this.animateAttribute = $elem.css('left') === 'auto' ? 'right' : 'left'; diff --git a/app/assets/javascripts/discourse/components/post-gap.js.es6 b/app/assets/javascripts/discourse/components/post-gap.js.es6 index 3c3209a89..159d047f0 100644 --- a/app/assets/javascripts/discourse/components/post-gap.js.es6 +++ b/app/assets/javascripts/discourse/components/post-gap.js.es6 @@ -3,8 +3,8 @@ export default Ember.Component.extend({ initGaps: function(){ this.set('loading', false); - var before = this.get('before') === 'true', - gaps = before ? this.get('postStream.gaps.before') : this.get('postStream.gaps.after'); + const before = this.get('before') === 'true'; + const gaps = before ? this.get('postStream.gaps.before') : this.get('postStream.gaps.after'); if (gaps) { this.set('gap', gaps[this.get('post.id')]); @@ -16,29 +16,27 @@ export default Ember.Component.extend({ this.rerender(); }.observes('post.hasGap'), - render: function(buffer) { + render(buffer) { if (this.get('loading')) { buffer.push(I18n.t('loading')); } else { - var gapLength = this.get('gap.length'); + const gapLength = this.get('gap.length'); if (gapLength) { buffer.push(I18n.t('post.gap', {count: gapLength})); } } }, - click: function() { + click() { if (this.get('loading') || (!this.get('gap'))) { return false; } this.set('loading', true); this.rerender(); - var self = this, - postStream = this.get('postStream'), - filler = this.get('before') === 'true' ? postStream.fillGapBefore : postStream.fillGapAfter; + const postStream = this.get('postStream'); + const filler = this.get('before') === 'true' ? postStream.fillGapBefore : postStream.fillGapAfter; - filler.call(postStream, this.get('post'), this.get('gap')).then(function() { - // hide this control after the promise is resolved - self.set('gap', null); + filler.call(postStream, this.get('post'), this.get('gap')).then(() => { + this.set('gap', null); }); return false; diff --git a/app/assets/javascripts/discourse/components/search-menu.js.es6 b/app/assets/javascripts/discourse/components/search-menu.js.es6 index 4786747d9..413525280 100644 --- a/app/assets/javascripts/discourse/components/search-menu.js.es6 +++ b/app/assets/javascripts/discourse/components/search-menu.js.es6 @@ -1,4 +1,4 @@ -import searchForTerm from 'discourse/lib/search-for-term'; +import {searchForTerm, searchContextDescription, isValidSearchTerm } from 'discourse/lib/search'; import DiscourseURL from 'discourse/lib/url'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; import showModal from 'discourse/lib/show-modal'; @@ -48,18 +48,7 @@ export default Ember.Component.extend({ @computed('searchService.searchContext') searchContextDescription(ctx) { - if (ctx) { - switch(Em.get(ctx, 'type')) { - case 'topic': - return I18n.t('search.context.topic'); - case 'user': - return I18n.t('search.context.user', {username: Em.get(ctx, 'user.username')}); - case 'category': - return I18n.t('search.context.category', {category: Em.get(ctx, 'category.name')}); - case 'private_messages': - return I18n.t('search.context.private_messages'); - } - } + return searchContextDescription(Em.get(ctx, 'type'), Em.get(ctx, 'user.username') || Em.get(ctx, 'category.name')); }, @observes('searchService.searchContextEnabled') @@ -72,8 +61,8 @@ export default Ember.Component.extend({ @observes('searchService.term', 'typeFilter') newSearchNeeded() { this.set('noResults', false); - const term = (this.get('searchService.term') || '').trim(); - if (term.length >= Discourse.SiteSettings.min_search_term_length) { + const term = this.get('searchService.term') + if (isValidSearchTerm(term)) { this.set('loading', true); Ember.run.debounce(this, 'searchTerm', term, this.get('typeFilter'), 400); } else { @@ -145,7 +134,7 @@ export default Ember.Component.extend({ }, showedSearch() { - $('#search-term').focus(); + $('#search-term').focus().select(); }, showSearchHelp() { @@ -165,8 +154,7 @@ export default Ember.Component.extend({ }, keyDown(e) { - const term = this.get('searchService.term'); - if (e.which === 13 && term && term.length >= this.siteSettings.min_search_term_length) { + if (e.which === 13 && isValidSearchTerm(this.get('searchService.term'))) { this.set('visible', false); this.send('fullSearch'); } diff --git a/app/assets/javascripts/discourse/components/search-text-field.js.es6 b/app/assets/javascripts/discourse/components/search-text-field.js.es6 index bf46ddf33..0b7b3d0e3 100644 --- a/app/assets/javascripts/discourse/components/search-text-field.js.es6 +++ b/app/assets/javascripts/discourse/components/search-text-field.js.es6 @@ -1,9 +1,19 @@ import computed from 'ember-addons/ember-computed-decorators'; +import { on } from 'ember-addons/ember-computed-decorators'; import TextField from 'discourse/components/text-field'; export default TextField.extend({ @computed('searchService.searchContextEnabled') - placeholder: function(searchContextEnabled) { + placeholder(searchContextEnabled) { return searchContextEnabled ? "" : I18n.t('search.title'); + }, + + focusIn() { + Em.run.later(() => this.$().select()); + }, + + @on("didInsertElement") + becomeFocused() { + if (this.get('hasAutofocus')) this.$().focus(); } }); diff --git a/app/assets/javascripts/discourse/components/text-field.js.es6 b/app/assets/javascripts/discourse/components/text-field.js.es6 index 40560b8c8..a9246efa7 100644 --- a/app/assets/javascripts/discourse/components/text-field.js.es6 +++ b/app/assets/javascripts/discourse/components/text-field.js.es6 @@ -1,19 +1,10 @@ -/** - This is a custom text field that allows i18n placeholders +import computed from "ember-addons/ember-computed-decorators"; - @class TextField - @extends Ember.TextField - @namespace Discourse - @module Discourse -**/ export default Ember.TextField.extend({ attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus', 'maxLength'], - placeholder: function() { - if (this.get('placeholderKey')) { - return I18n.t(this.get('placeholderKey')); - } else { - return ''; - } - }.property('placeholderKey') + @computed("placeholderKey") + placeholder(placeholderKey) { + return placeholderKey ? I18n.t(placeholderKey) : ""; + } }); diff --git a/app/assets/javascripts/discourse/components/time-gap.js.es6 b/app/assets/javascripts/discourse/components/time-gap.js.es6 index 3cd887ec4..9e410c4b7 100644 --- a/app/assets/javascripts/discourse/components/time-gap.js.es6 +++ b/app/assets/javascripts/discourse/components/time-gap.js.es6 @@ -2,6 +2,8 @@ import SmallActionComponent from 'discourse/components/small-action'; export default SmallActionComponent.extend({ classNames: ['time-gap'], + classNameBindings: ['hideTimeGap::hidden'], + hideTimeGap: Em.computed.alias('postStream.hasNoFilters'), icon: 'clock-o', description: function() { diff --git a/app/assets/javascripts/discourse/components/user-menu.js.es6 b/app/assets/javascripts/discourse/components/user-menu.js.es6 index a6a7e46be..b1c14d2d7 100644 --- a/app/assets/javascripts/discourse/components/user-menu.js.es6 +++ b/app/assets/javascripts/discourse/components/user-menu.js.es6 @@ -1,5 +1,6 @@ import { url } from 'discourse/lib/computed'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import { headerHeight } from 'discourse/views/header'; export default Ember.Component.extend({ classNames: ['user-menu'], @@ -17,8 +18,8 @@ export default Ember.Component.extend({ showDisableAnon(allowAnon, isAnon) { return allowAnon && isAnon; }, @observes('visible') - _loadNotifications(visible) { - if (visible) { + _loadNotifications() { + if (this.get("visible")) { this.refreshNotifications(); } }, @@ -43,13 +44,30 @@ export default Ember.Component.extend({ refreshNotifications() { if (this.get('loadingNotifications')) { return; } + // estimate (poorly) the amount of notifications to return + var limit = Math.round(($(window).height() - headerHeight()) / 50); + // we REALLY don't want to be asking for negative counts of notifications + // less than 5 is also not that useful + if (limit < 5) { limit = 5; } + if (limit > 40) { limit = 40; } + // TODO: It's a bit odd to use the store in a component, but this one really // wants to reach out and grab notifications const store = this.container.lookup('store:main'); - const stale = store.findStale('notification', {recent: true}); + const stale = store.findStale('notification', {recent: true, limit }, {storageKey: 'recent-notifications'}); if (stale.hasResults) { - this.set('notifications', stale.results); + const results = stale.results; + var content = results.get('content'); + + // we have to truncate to limit, otherwise we will render too much + if (content && (content.length > limit)) { + content = content.splice(0, limit); + results.set('content', content); + results.set('totalRows', limit); + } + + this.set('notifications', results); } else { this.set('loadingNotifications', true); } diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index d3bed034e..5b0be280c 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -25,7 +25,7 @@ export default TextField.extend({ dataSource: function(term) { return userSearch({ - term: term.replace(/[^a-zA-Z0-9_]/, ''), + term: term.replace(/[^a-zA-Z0-9_\-\.]/, ''), topicId: self.get('topicId'), exclude: excludedUsernames(), includeGroups, diff --git a/app/assets/javascripts/discourse/components/who-liked.js.es6 b/app/assets/javascripts/discourse/components/who-liked.js.es6 index 34ba67223..5c12a91d9 100644 --- a/app/assets/javascripts/discourse/components/who-liked.js.es6 +++ b/app/assets/javascripts/discourse/components/who-liked.js.es6 @@ -13,7 +13,7 @@ export default Ember.Component.extend(StringBuffer, { iconsHtml += "<a href=\"" + Discourse.getURL("/users/") + u.get('username_lower') + "\" data-user-card=\"" + u.get('username_lower') + "\">"; iconsHtml += Discourse.Utilities.avatarImg({ size: 'small', - avatarTemplate: u.get('avatarTemplate'), + avatarTemplate: u.get('avatar_template'), title: u.get('username') }); iconsHtml += "</a>"; diff --git a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 index f0bb9fcb3..29aae8bf6 100644 --- a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 @@ -1,21 +1,29 @@ -import ModalFunctionality from 'discourse/mixins/modal-functionality'; +import computed from "ember-addons/ember-computed-decorators"; +import ModalFunctionality from "discourse/mixins/modal-functionality"; export default Ember.Controller.extend(ModalFunctionality, { - uploadedAvatarTemplate: null, - saveDisabled: Em.computed.alias("uploading"), - hasUploadedAvatar: Em.computed.or('uploadedAvatarTemplate', 'custom_avatar_upload_id'), - - selectedUploadId: function() { - switch (this.get("selected")) { - case "system": return this.get("system_avatar_upload_id"); - case "gravatar": return this.get("gravatar_avatar_upload_id"); - default: return this.get("custom_avatar_upload_id"); + @computed("selected", "system_avatar_upload_id", "gravatar_avatar_upload_id", "custom_avatar_upload_id") + selectedUploadId(selected, system, gravatar, custom) { + switch (selected) { + case "system": return system; + case "gravatar": return gravatar; + default: return custom; } - }.property('selected', 'system_avatar_upload_id', 'gravatar_avatar_upload_id', 'custom_avatar_upload_id'), + }, - allowImageUpload: function() { + @computed("selected", "system_avatar_template", "gravatar_avatar_template", "custom_avatar_template") + selectedAvatarTemplate(selected, system, gravatar, custom) { + switch (selected) { + case "system": return system; + case "gravatar": return gravatar; + default: return custom; + } + }, + + @computed() + allowImageUpload() { return Discourse.Utilities.allowsImages(); - }.property(), + }, actions: { useUploadedAvatar() { this.set("selected", "uploaded"); }, @@ -25,8 +33,11 @@ export default Ember.Controller.extend(ModalFunctionality, { refreshGravatar() { this.set("gravatarRefreshDisabled", true); return Discourse - .ajax("/user_avatar/" + this.get("username") + "/refresh_gravatar.json", { method: 'POST' }) - .then(result => this.set("gravatar_avatar_upload_id", result.upload_id)) + .ajax(`/user_avatar/${this.get("username")}/refresh_gravatar.json`, { method: "POST" }) + .then(result => this.setProperties({ + gravatar_avatar_template: result.gravatar_avatar_template, + gravatar_upload_id: result.gravatar_upload_id, + })) .finally(() => this.set("gravatarRefreshDisabled", false)); } } diff --git a/app/assets/javascripts/discourse/controllers/change-owner.js.es6 b/app/assets/javascripts/discourse/controllers/change-owner.js.es6 index 39ae9cddc..211e27a7e 100644 --- a/app/assets/javascripts/discourse/controllers/change-owner.js.es6 +++ b/app/assets/javascripts/discourse/controllers/change-owner.js.es6 @@ -39,11 +39,11 @@ export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, { username: this.get('new_user') }; - Discourse.Topic.changeOwners(this.get('topicController.model.id'), saveOpts).then(function(result) { + Discourse.Topic.changeOwners(this.get('topicController.model.id'), saveOpts).then(function() { // success self.send('closeModal'); self.get('topicController').send('toggleMultiSelect'); - Em.run.next(function() { DiscourseURL.routeTo(result.url); }); + Em.run.next(() => { DiscourseURL.routeTo(self.get("topicController.model.url")); }); }, function() { // failure self.flash(I18n.t('topic.change_owner.error'), 'alert-error'); diff --git a/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6 b/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6 index 1d55e74cf..1cd859cfa 100644 --- a/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6 +++ b/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6 @@ -1,5 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import computed from 'ember-addons/ember-computed-decorators'; +import DiscourseURL from 'discourse/lib/url'; // Modal related to changing the timestamp of posts export default Ember.Controller.extend(ModalFunctionality, { @@ -40,14 +41,16 @@ export default Ember.Controller.extend(ModalFunctionality, { actions: { changeTimestamp: function() { this.set('saving', true); - const self = this; + const self = this, + topic = this.get('topicController.model'); Discourse.Topic.changeTimestamp( - this.get('topicController.model.id'), + topic.get('id'), this.get('createdAt').unix() ).then(function() { self.send('closeModal'); self.setProperties({ date: '', time: '', saving: false }); + Em.run.next(() => { DiscourseURL.routeTo(topic.get('url')); }); }).catch(function() { self.flash(I18n.t('topic.change_timestamp.error'), 'alert-error'); self.set('saving', false); diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 8dfd77ba3..9e5b2206d 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -2,6 +2,45 @@ import { setting } from 'discourse/lib/computed'; 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 computed from 'ember-addons/ember-computed-decorators'; + +function loadDraft(store, opts) { + opts = opts || {}; + + let draft = opts.draft; + const draftKey = opts.draftKey; + const draftSequence = opts.draftSequence; + + try { + if (draft && typeof draft === 'string') { + draft = JSON.parse(draft); + } + } catch (error) { + draft = null; + Draft.clear(draftKey, draftSequence); + } + if (draft && ((draft.title && draft.title !== '') || (draft.reply && draft.reply !== ''))) { + const composer = store.createRecord('composer'); + composer.open({ + draftKey, + draftSequence, + action: draft.action, + title: draft.title, + categoryId: draft.categoryId || opts.categoryId, + postId: draft.postId, + archetypeId: draft.archetypeId, + reply: draft.reply, + metaData: draft.metaData, + usernames: draft.usernames, + draft: true, + composerState: Composer.DRAFT, + composerTime: draft.composerTime, + typingTime: draft.typingTime + }); + return composer; + } +} export default Ember.Controller.extend({ needs: ['modal', 'topic', 'composer-messages', 'application'], @@ -26,6 +65,12 @@ export default Ember.Controller.extend({ this.set('similarTopics', []); }.on('init'), + @computed('model.action') + canWhisper(action) { + const currentUser = this.currentUser; + return currentUser && currentUser.get('staff') && this.siteSettings.enable_whispers && action === Composer.REPLY; + }, + showWarning: function() { if (!Discourse.User.currentProp('staff')) { return false; } @@ -94,7 +139,6 @@ export default Ember.Controller.extend({ }, hitEsc() { - const messages = this.get('controllers.composer-messages.model'); if (messages.length) { messages.popObject(); @@ -438,7 +482,7 @@ export default Ember.Controller.extend({ // Given a potential instance and options, set the model for this composer. _setModel(composerModel, opts) { if (opts.draft) { - composerModel = Discourse.Composer.loadDraft(opts); + composerModel = loadDraft(this.store, opts); if (composerModel) { composerModel.set('topic', opts.topic); } diff --git a/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6 index ebda2c04a..c4d639f36 100644 --- a/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery/categories.js.es6 @@ -6,6 +6,9 @@ export default DiscoveryController.extend({ withLogo: Em.computed.filterBy('model.categories', 'logo_url'), showPostsColumn: Em.computed.empty('withLogo'), + // this makes sure the composer isn't scoping to a specific category + category: null, + actions: { refresh() { @@ -19,8 +22,8 @@ export default DiscoveryController.extend({ this.set('controllers.discovery.loading', true); const parentCategory = this.get('model.parentCategory'); - const promise = parentCategory ? Discourse.CategoryList.listForParent(parentCategory) : - Discourse.CategoryList.list(); + const promise = parentCategory ? Discourse.CategoryList.listForParent(this.store, parentCategory) : + Discourse.CategoryList.list(this.store); const self = this; promise.then(function(list) { diff --git a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 index 772c8e4c5..28a663e38 100644 --- a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 @@ -29,8 +29,8 @@ const controllerOpts = { }, // Show newly inserted topics - showInserted: function() { - const tracker = Discourse.TopicTrackingState.current(); + showInserted() { + const tracker = this.topicTrackingState; // Move inserted into topics this.get('content').loadBefore(tracker.get('newIncoming')); @@ -38,9 +38,8 @@ const controllerOpts = { return false; }, - refresh: function() { - const filter = this.get('model.filter'), - self = this; + refresh() { + const filter = this.get('model.filter'); this.setProperties({ order: 'default', ascending: false }); @@ -52,36 +51,27 @@ const controllerOpts = { // Lesson learned: Don't call `loading` yourself. this.set('controllers.discovery.loading', true); - this.store.findFiltered('topicList', {filter}).then(function(list) { - Discourse.TopicList.hideUniformCategory(list, self.get('category')); + this.store.findFiltered('topicList', {filter}).then((list) => { + Discourse.TopicList.hideUniformCategory(list, this.get('category')); - self.setProperties({ model: list }); - self.resetSelected(); + this.setProperties({ model: list }); + this.resetSelected(); - const tracking = Discourse.TopicTrackingState.current(); - if (tracking) { - tracking.sync(list, filter); + if (this.topicTrackingState) { + this.topicTrackingState.sync(list, filter); } - self.send('loadingComplete'); + this.send('loadingComplete'); }); }, - resetNew: function() { - const self = this; - - Discourse.TopicTrackingState.current().resetNew(); - Discourse.Topic.resetNew().then(function() { - self.send('refresh'); - }); + resetNew() { + this.topicTrackingState.resetNew(); + Discourse.Topic.resetNew().then(() => this.send('refresh')); } }, - topicTrackingState: function() { - return Discourse.TopicTrackingState.current(); - }.property(), - isFilterPage: function(filter, filterType) { if (!filter) { return false; } return filter.match(new RegExp(filterType + '$', 'gi')) ? true : false; @@ -95,10 +85,6 @@ const controllerOpts = { return this.get('model.filter') === 'new' && this.get('model.topics.length') > 0; }.property('model.filter', 'model.topics.length'), - tooManyTracked: function(){ - return Discourse.TopicTrackingState.current().tooManyTracked(); - }.property(), - showDismissAtTop: function() { return (this.isFilterPage(this.get('model.filter'), 'new') || this.isFilterPage(this.get('model.filter'), 'unread')) && diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 b/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 index 6958ff446..0c8795dd1 100644 --- a/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 @@ -1,3 +1,4 @@ +import { observes } from "ember-addons/ember-computed-decorators"; import ModalFunctionality from 'discourse/mixins/modal-functionality'; // Modal related to auto closing of topics @@ -5,31 +6,32 @@ export default Ember.Controller.extend(ModalFunctionality, { auto_close_valid: true, auto_close_invalid: Em.computed.not('auto_close_valid'), - setAutoCloseTime: function() { - var autoCloseTime = null; + @observes("model.details.auto_close_at", "model.details.auto_close_hours") + setAutoCloseTime() { + let autoCloseTime = null; if (this.get("model.details.auto_close_based_on_last_post")) { autoCloseTime = this.get("model.details.auto_close_hours"); } else if (this.get("model.details.auto_close_at")) { - var closeTime = new Date(this.get("model.details.auto_close_at")); + const closeTime = new Date(this.get("model.details.auto_close_at")); if (closeTime > new Date()) { autoCloseTime = moment(closeTime).format("YYYY-MM-DD HH:mm"); } } this.set("model.auto_close_time", autoCloseTime); - }.observes("model.details.{auto_close_at,auto_close_hours}"), - - actions: { - saveAutoClose: function() { this.setAutoClose(this.get("model.auto_close_time")); }, - removeAutoClose: function() { this.setAutoClose(null); } }, - setAutoClose: function(time) { - var self = this; + actions: { + saveAutoClose() { this.setAutoClose(this.get("model.auto_close_time")); }, + removeAutoClose() { this.setAutoClose(null); } + }, + + setAutoClose(time) { + const self = this; this.send('hideModal'); Discourse.ajax({ - url: '/t/' + this.get('model.id') + '/autoclose', + url: `/t/${this.get('model.id')}/autoclose`, type: 'PUT', dataType: 'json', data: { @@ -37,15 +39,15 @@ export default Ember.Controller.extend(ModalFunctionality, { auto_close_based_on_last_post: this.get("model.details.auto_close_based_on_last_post"), timezone_offset: (new Date().getTimezoneOffset()) } - }).then(function(result){ + }).then(result => { if (result.success) { - self.send('closeModal'); - self.set('model.details.auto_close_at', result.auto_close_at); - self.set('model.details.auto_close_hours', result.auto_close_hours); + this.send('closeModal'); + this.set('model.details.auto_close_at', result.auto_close_at); + this.set('model.details.auto_close_hours', result.auto_close_hours); } else { bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } ); } - }, function () { + }).catch(() => { bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } ); }); } 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 7a5f031d2..f4fb1e787 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -1,45 +1,125 @@ -import { translateResults } from "discourse/lib/search-for-term"; +import { translateResults, searchContextDescription, getSearchKey, isValidSearchTerm } from "discourse/lib/search"; +import showModal from 'discourse/lib/show-modal'; +import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import Category from 'discourse/models/category'; export default Ember.Controller.extend({ needs: ["application"], loading: Em.computed.not("model"), - queryParams: ["q"], + queryParams: ["q", "context_id", "context", "skip_context"], q: null, selected: [], + context_id: null, + context: null, - modelChanged: function() { + @computed('q') + hasAutofocus(q) { + return Em.isEmpty(q); + }, + + @computed('skip_context', 'context') + searchContextEnabled: { + get(skip,context){ + return (!skip && context) || skip === "false"; + }, + set(val) { + this.set('skip_context', val ? "false" : "true" ) + } + }, + + @computed('context', 'context_id') + searchContextDescription(context, id){ + var name = id; + if (context === 'category') { + var category = Category.findById(id); + if (!category) {return;} + + name = category.get('name'); + } + return searchContextDescription(context, name); + }, + + @computed('q') + searchActive(q){ + return isValidSearchTerm(q); + }, + + @computed('searchTerm') + isNotValidSearchTerm(searchTerm) { + return !isValidSearchTerm(searchTerm); + }, + + @observes('model') + modelChanged() { if (this.get("searchTerm") !== this.get("q")) { this.set("searchTerm", this.get("q")); } - }.observes("model"), + }, - qChanged: function() { + @observes('q') + qChanged() { const model = this.get("model"); if (model && this.get("model.q") !== this.get("q")) { this.set("searchTerm", this.get("q")); this.send("search"); } - }.observes("q"), + }, - _showFooter: function() { + @observes('loading') + _showFooter() { this.set("controllers.application.showFooter", !this.get("loading")); - }.observes("loading"), + }, canBulkSelect: Em.computed.alias('currentUser.staff'), search(){ + if (this._searching) { + return; + } + this._searching = true; + + const router = Discourse.__container__.lookup('router:main'); + this.set("q", this.get("searchTerm")); this.set("model", null); - Discourse.ajax("/search", { data: { q: this.get("searchTerm") } }).then(results => { - this.set("model", translateResults(results) || {}); - this.set("model.q", this.get("q")); - }); + var args = { q: this.get("searchTerm") }; + + const skip = this.get("skip_context"); + if ((!skip && this.get('context')) || skip==="false"){ + args.search_context = { + type: this.get('context'), + id: this.get('context_id') + }; + } + + const searchKey = getSearchKey(args); + + Discourse.ajax("/search", { data: args }).then(results => { + const model = translateResults(results) || {}; + router.transientCache('lastSearch', { searchKey, model }, 5); + this.set("model", model); + }).finally(() => {this._searching = false}); }, actions: { + selectAll() { + this.get('selected').addObjects(this.get('model.posts').map(r => r.topic)); + // Doing this the proper way is a HUGE pain, + // we can hack this to work by observing each on the array + // in the component, however, when we select ANYTHING, we would force + // 50 traversals of the list + // This hack is cheap and easy + $('.fps-result input[type=checkbox]').prop('checked', true); + }, + + clearAll() { + this.get('selected').clear() + $('.fps-result input[type=checkbox]').prop('checked', false); + }, + toggleBulkSelect() { this.toggleProperty('bulkSelectEnabled'); this.get('selected').clear(); @@ -51,7 +131,15 @@ export default Ember.Controller.extend({ this.search(); }, + showSearchHelp() { + // TODO: dupe code should be centralized + Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then((model) => { + showModal('searchHelp', { model }); + }); + }, + search() { + if (this.get("isNotValidSearchTerm")) return; this.search(); } } diff --git a/app/assets/javascripts/discourse/controllers/header.js.es6 b/app/assets/javascripts/discourse/controllers/header.js.es6 index 813e9e717..0460830bc 100644 --- a/app/assets/javascripts/discourse/controllers/header.js.es6 +++ b/app/assets/javascripts/discourse/controllers/header.js.es6 @@ -1,3 +1,5 @@ +import DiscourseURL from 'discourse/lib/url'; + const HeaderController = Ember.Controller.extend({ topic: null, showExtraInfo: null, @@ -18,6 +20,24 @@ const HeaderController = Ember.Controller.extend({ actions: { + showUserMenu() { + if (!this.get('userMenuVisible')) { + this.appEvents.trigger('dropdowns:closeAll'); + this.set('userMenuVisible', true); + } + }, + + fullPageSearch() { + const searchService = this.container.lookup('search-service:main'); + const context = searchService.get('searchContext'); + var params = ""; + + if (context) { + params = `?context=${context.type}&context_id=${context.id}`; + } + + DiscourseURL.routeTo('/search' + params); + }, toggleMenuPanel(visibleProp) { this.toggleProperty(visibleProp); this.appEvents.trigger('dropdowns:closeAll'); diff --git a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 index c21ed8134..35b11cd30 100644 --- a/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/navigation/category.js.es6 @@ -1,3 +1,4 @@ +import computed from "ember-addons/ember-computed-decorators"; import NavigationDefaultController from 'discourse/controllers/navigation/default'; import { setting } from 'discourse/lib/computed'; @@ -6,8 +7,9 @@ export default NavigationDefaultController.extend({ showingParentCategory: Em.computed.none('category.parentCategory'), showingSubcategoryList: Em.computed.and('subcategoryListSetting', 'showingParentCategory'), - navItems: function() { - if (this.get('showingSubcategoryList')) { return []; } - return Discourse.NavItem.buildList(this.get('category'), { noSubcategories: this.get('noSubcategories') }); - }.property('category', 'noSubcategories') + @computed("showingSubcategoryList", "category", "noSubcategories") + navItems(showingSubcategoryList, category, noSubcategories) { + if (showingSubcategoryList) { return []; } + return Discourse.NavItem.buildList(category, { noSubcategories }); + } }); diff --git a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 index c2a48c4b7..1d23c1fb2 100644 --- a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 +++ b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 @@ -1,12 +1,18 @@ +import computed from "ember-addons/ember-computed-decorators"; + export default Ember.Controller.extend({ needs: ['discovery', 'discovery/topics'], - categories: function() { + @computed() + categories() { return Discourse.Category.list(); - }.property(), + }, - navItems: function() { - return Discourse.NavItem.buildList(null, {filterMode: this.get('filterMode')}); - }.property('filterMode') + @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/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index ee3e86e9d..a7aa333fb 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -1,6 +1,7 @@ import { setting } from 'discourse/lib/computed'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; import { popupAjaxError } from 'discourse/lib/ajax-error'; +import computed from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend(CanCheckEmails, { @@ -10,18 +11,18 @@ export default Ember.Controller.extend(CanCheckEmails, { allowBackgrounds: setting('allow_profile_backgrounds'), editHistoryVisible: setting('edit_history_visible_to_public'), - selectedCategories: function(){ - return [].concat(this.get("model.watchedCategories"), - this.get("model.trackedCategories"), - this.get("model.mutedCategories")); - }.property("model.watchedCategories", "model.trackedCategories", "model.mutedCategories"), + @computed("model.watchedCategories", "model.trackedCategories", "model.mutedCategories") + selectedCategories(watched, tracked, muted) { + return [].concat(watched, tracked, muted); + }, // By default we haven't saved anything saved: false, newNameInput: null, - userFields: function() { + @computed("model.user_fields.@each.value") + userFields() { let siteUserFields = this.site.get('user_fields'); if (!Ember.isEmpty(siteUserFields)) { const userFields = this.get('model.user_fields'); @@ -35,34 +36,37 @@ export default Ember.Controller.extend(CanCheckEmails, { return Ember.Object.create({ value, field }); }); } - }.property('model.user_fields.@each.value'), + }, cannotDeleteAccount: Em.computed.not('can_delete_account'), deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'), canEditName: setting('enable_names'), - nameInstructions: function() { + @computed() + nameInstructions() { return I18n.t(Discourse.SiteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions'); - }.property(), + }, - canSelectTitle: function() { - return this.siteSettings.enable_badges && this.get('model.has_title_badges'); - }.property('model.badge_count'), + @computed("model.has_title_badges") + canSelectTitle(hasTitleBadges) { + return this.siteSettings.enable_badges && hasTitleBadges; + }, - canChangePassword: function() { + @computed() + canChangePassword() { return !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins; - }.property(), + }, - canReceiveDigest: function() { + @computed() + canReceiveDigest() { return !this.siteSettings.disable_digest_emails; - }.property(), + }, - availableLocales: function() { - return this.siteSettings.available_locales.split('|').map( function(s) { - return {name: s, value: s}; - }); - }.property(), + @computed() + availableLocales() { + return this.siteSettings.available_locales.split('|').map(s => ({ name: s, value: s })); + }, digestFrequencies: [{ name: I18n.t('user.email_digests.daily'), value: 1 }, { name: I18n.t('user.email_digests.every_three_days'), value: 3 }, @@ -86,16 +90,16 @@ export default Ember.Controller.extend(CanCheckEmails, { { name: I18n.t('user.new_topic_duration.after_2_weeks'), value: 2 * 7 * 60 * 24 }, { name: I18n.t('user.new_topic_duration.last_here'), value: -2 }], - saveButtonText: function() { - return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('save'); - }.property('model.isSaving'), + @computed("model.isSaving") + saveButtonText(isSaving) { + return isSaving ? I18n.t('saving') : I18n.t('save'); + }, passwordProgress: null, actions: { save() { - const self = this; this.set('saved', false); const model = this.get('model'); @@ -113,28 +117,27 @@ export default Ember.Controller.extend(CanCheckEmails, { // Cook the bio for preview model.set('name', this.get('newNameInput')); - return model.save().then(function() { + return model.save().then(() => { if (Discourse.User.currentProp('id') === model.get('id')) { Discourse.User.currentProp('name', model.get('name')); } model.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(model.get('bio_raw')))); - self.set('saved', true); + this.set('saved', true); }).catch(popupAjaxError); }, changePassword() { - const self = this; if (!this.get('passwordProgress')) { this.set('passwordProgress', I18n.t("user.change_password.in_progress")); - return this.get('model').changePassword().then(function() { + return this.get('model').changePassword().then(() => { // password changed - self.setProperties({ + this.setProperties({ changePasswordProgress: false, passwordProgress: I18n.t("user.change_password.success") }); - }, function() { + }).catch(() => { // password failed to change - self.setProperties({ + this.setProperties({ changePasswordProgress: false, passwordProgress: I18n.t("user.change_password.error") }); diff --git a/app/assets/javascripts/discourse/controllers/reorder-categories.js.es6 b/app/assets/javascripts/discourse/controllers/reorder-categories.js.es6 new file mode 100644 index 000000000..0debb1e70 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/reorder-categories.js.es6 @@ -0,0 +1,94 @@ +import ModalFunctionality from 'discourse/mixins/modal-functionality'; +const BufferedProxy = window.BufferedProxy; // import BufferedProxy from 'ember-buffered-proxy/proxy'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; +import computed from "ember-addons/ember-computed-decorators"; +import Ember from 'ember'; + +const SortableArrayProxy = Ember.ArrayProxy.extend(Ember.SortableMixin); + +export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, { + + @computed("site.categories") + categoriesBuffered(categories) { + const bufProxy = Ember.ObjectProxy.extend(BufferedProxy); + return categories.map(c => bufProxy.create({ content: c })); + }, + + categoriesOrdered: function() { + return SortableArrayProxy.create({ + sortProperties: ['content.position'], + content: this.get('categoriesBuffered') + }); + }.property('categoriesBuffered'), + + showFixIndices: function() { + const cats = this.get('categoriesOrdered'); + const len = cats.get('length'); + for (let i = 0; i < len; i++) { + if (cats.objectAt(i).get('position') !== i) { + return true; + } + } + return false; + }.property('categoriesOrdered.@each.position'), + + showApplyAll: function() { + let anyChanged = false; + this.get('categoriesBuffered').forEach(bc => { anyChanged = anyChanged || bc.get('hasBufferedChanges') }); + return anyChanged; + }.property('categoriesBuffered.@each.hasBufferedChanges'), + + saveDisabled: Ember.computed.or('showApplyAll', 'showFixIndices'), + + moveDir(cat, dir) { + const cats = this.get('categoriesOrdered'); + const curIdx = cats.indexOf(cat); + const desiredIdx = curIdx + dir; + if (desiredIdx >= 0 && desiredIdx < cats.get('length')) { + const curPos = cat.get('position'); + cat.set('position', curPos + dir); + const otherCat = cats.objectAt(desiredIdx); + otherCat.set('position', curPos - dir); + this.send('commit'); + } + }, + + actions: { + + moveUp(cat) { + this.moveDir(cat, -1); + }, + moveDown(cat) { + this.moveDir(cat, 1); + }, + + fixIndices() { + const cats = this.get('categoriesOrdered'); + const len = cats.get('length'); + for (let i = 0; i < len; i++) { + cats.objectAt(i).set('position', i); + } + this.send('commit'); + }, + + commit() { + this.get('categoriesBuffered').forEach(bc => { + if (bc.get('hasBufferedChanges')) { + bc.applyBufferedChanges(); + } + }); + this.propertyDidChange('categoriesBuffered'); + }, + + saveOrder() { + const data = {}; + this.get('categoriesBuffered').forEach((cat) => { + data[cat.get('id')] = cat.get('position'); + }); + Discourse.ajax('/categories/reorder', + {type: 'POST', data: {mapping: JSON.stringify(data)}}). + then(() => this.send("closeModal")). + catch(popupAjaxError); + } + } +}); diff --git a/app/assets/javascripts/discourse/controllers/static.js.es6 b/app/assets/javascripts/discourse/controllers/static.js.es6 index c22ce3c26..16b4d731e 100644 --- a/app/assets/javascripts/discourse/controllers/static.js.es6 +++ b/app/assets/javascripts/discourse/controllers/static.js.es6 @@ -3,8 +3,11 @@ export default Ember.Controller.extend({ actions: { markFaqRead() { - if (this.currentUser) { - Discourse.ajax("/users/read-faq", { method: "POST" }); + const currentUser = this.currentUser; + if (currentUser) { + Discourse.ajax("/users/read-faq", { method: "POST" }).then(() => { + currentUser.set('read_faq', true); + }); } } } diff --git a/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 b/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 index 4f453b98f..4a7fdf61c 100644 --- a/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 @@ -1,7 +1,7 @@ import DiscourseURL from 'discourse/lib/url'; function entranceDate(dt, showTime) { - var today = new Date(); + const today = new Date(); if (dt.toDateString() === today.toDateString()) { return moment(dt).format(I18n.t("dates.time")); @@ -44,7 +44,7 @@ export default Ember.Controller.extend({ }.property('bumpedDate'), actions: { - show: function(data) { + show(data) { // Show the chooser but only if the model changes if (this.get('model') !== data.topic) { this.set('model', data.topic); @@ -52,11 +52,11 @@ export default Ember.Controller.extend({ } }, - enterTop: function() { + enterTop() { DiscourseURL.routeTo(this.get('model.url')); }, - enterBottom: function() { + enterBottom() { DiscourseURL.routeTo(this.get('model.lastPostUrl')); } } diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 2a6d0ffaf..80ac93739 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -428,20 +428,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { }, toggleWiki(post) { - // the request to the server is made in an observer in the post class - post.toggleProperty('wiki'); + post.updatePostField('wiki', !post.get('wiki')); }, togglePostType(post) { - // the request to the server is made in an observer in the post class - const regular = this.site.get('post_types.regular'), - moderator = this.site.get('post_types.moderator_action'); + const regular = this.site.get('post_types.regular'); + const moderator = this.site.get('post_types.moderator_action'); - if (post.get("post_type") === moderator) { - post.set("post_type", regular); - } else { - post.set("post_type", moderator); - } + post.updatePostField('post_type', post.get('post_type') === moderator ? regular : moderator); }, rebakePost(post) { diff --git a/app/assets/javascripts/discourse/controllers/user-card.js.es6 b/app/assets/javascripts/discourse/controllers/user-card.js.es6 index 1a4c3ad6d..a7ade2d7f 100644 --- a/app/assets/javascripts/discourse/controllers/user-card.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-card.js.es6 @@ -37,7 +37,7 @@ export default Ember.Controller.extend({ show(username, postId, target) { // XSS protection (should be encapsulated) - username = username.toString().replace(/[^A-Za-z0-9_]/g, ""); + username = username.toString().replace(/[^A-Za-z0-9_\.\-]/g, ""); // Don't show on mobile if (Discourse.Mobile.mobileView) { diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index 6df5a32d6..83c7c4ec9 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -1,5 +1,6 @@ import { exportUserArchive } from 'discourse/lib/export-csv'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; +import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Controller.extend(CanCheckEmails, { indexStream: false, @@ -11,7 +12,10 @@ export default Ember.Controller.extend(CanCheckEmails, { return this.get('content.username') === Discourse.User.currentProp('username'); }.property('content.username'), - collapsedInfo: Em.computed.not('indexStream'), + @computed('indexStream', 'viewingSelf', 'forceExpand') + collapsedInfo(indexStream, viewingSelf, forceExpand){ + return (!indexStream || viewingSelf) && !forceExpand; + }, linkWebsite: Em.computed.not('model.isBasic'), @@ -19,7 +23,11 @@ export default Ember.Controller.extend(CanCheckEmails, { return this.get('model.trust_level') > 2 && !this.siteSettings.tl3_links_no_follow; }.property('model.trust_level'), - canSeePrivateMessages: Ember.computed.or('viewingSelf', 'currentUser.admin'), + @computed('viewSelf', 'currentUser.admin') + canSeePrivateMessages(viewingSelf, isAdmin) { + return this.siteSettings.enable_private_messages && (viewingSelf || isAdmin); + }, + canSeeNotificationHistory: Em.computed.alias('canSeePrivateMessages'), showBadges: function() { @@ -59,6 +67,9 @@ export default Ember.Controller.extend(CanCheckEmails, { privateMessagesUnreadActive: Em.computed.equal('pmView', 'unread'), actions: { + expandProfile: function() { + this.set('forceExpand', true); + }, adminDelete: function() { Discourse.AdminUser.find(this.get('model.username').toLowerCase()).then(function(user){ user.destroy({deletePosts: true}); diff --git a/app/assets/javascripts/discourse/dialects/code_dialect.js b/app/assets/javascripts/discourse/dialects/code_dialect.js index e7322a4ab..2594cd831 100644 --- a/app/assets/javascripts/discourse/dialects/code_dialect.js +++ b/app/assets/javascripts/discourse/dialects/code_dialect.js @@ -33,6 +33,7 @@ function codeFlattenBlocks(blocks) { Discourse.Dialect.replaceBlock({ start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm, stop: /^```$/gm, + withoutLeading: /\[quote/gm, //if leading text contains a quote this should not match emitter: function(blockContents, matches) { var klass = Discourse.SiteSettings.default_code_lang; diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js index e80e5e967..1614c1c0f 100644 --- a/app/assets/javascripts/discourse/dialects/dialect.js +++ b/app/assets/javascripts/discourse/dialects/dialect.js @@ -501,6 +501,12 @@ Discourse.Dialect = { var pos = args.start.lastIndex - match[0].length, leading = block.slice(0, pos), trailing = match[2] ? match[2].replace(/^\n*/, "") : ""; + + if(args.withoutLeading && args.withoutLeading.test(leading)) { + //The other leading block should be processed first! eg a code block wrapped around a code block. + return; + } + // just give up if there's no stop tag in this or any next block args.stop.lastIndex = block.length - trailing.length; if (!args.stop.exec(block) && lastChance()) { return; } diff --git a/app/assets/javascripts/discourse/dialects/mention_dialect.js b/app/assets/javascripts/discourse/dialects/mention_dialect.js index 4832d436b..4f1b8b8da 100644 --- a/app/assets/javascripts/discourse/dialects/mention_dialect.js +++ b/app/assets/javascripts/discourse/dialects/mention_dialect.js @@ -7,7 +7,7 @@ Discourse.Dialect.inlineRegexp({ start: '@', // NOTE: we really should be using SiteSettings here, but it loads later in process // also, if we do, we must ensure serverside version works as well - matcher: /^(@[A-Za-z0-9][A-Za-z0-9_]{0,40})/, + matcher: /^(@[A-Za-z0-9][A-Za-z0-9_\.\-]{0,40}[A-Za-z0-9])/, wordBoundary: true, emitter: function(matches) { diff --git a/app/assets/javascripts/discourse/helpers/application.js.es6 b/app/assets/javascripts/discourse/helpers/application.js.es6 index cac2ea1f0..2f4ea292c 100644 --- a/app/assets/javascripts/discourse/helpers/application.js.es6 +++ b/app/assets/javascripts/discourse/helpers/application.js.es6 @@ -1,37 +1,29 @@ import registerUnbound from 'discourse/helpers/register-unbound'; -import avatarTemplate from 'discourse/lib/avatar-template'; import { longDate, autoUpdatingRelativeAge, number } from 'discourse/lib/formatter'; const safe = Handlebars.SafeString; -Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) { +Em.Handlebars.helper('bound-avatar', (user, size) => { if (Em.isEmpty(user)) { return new safe("<div class='avatar-placeholder'></div>"); } - const username = Em.get(user, 'username'); - if (arguments.length < 4) { uploadId = Em.get(user, 'uploaded_avatar_id'); } - const avatar = Em.get(user, 'avatar_template') || avatarTemplate(username, uploadId); - + const avatar = Em.get(user, 'avatar_template'); return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar })); -}, 'username', 'uploaded_avatar_id', 'avatar_template'); +}, 'username', 'avatar_template'); /* * Used when we only have a template */ -Em.Handlebars.helper('bound-avatar-template', function(at, size) { +Em.Handlebars.helper('bound-avatar-template', (at, size) => { return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: at })); }); -registerUnbound('raw-date', function(dt) { - return longDate(new Date(dt)); -}); +registerUnbound('raw-date', dt => longDate(new Date(dt))); -registerUnbound('age-with-tooltip', function(dt) { - return new safe(autoUpdatingRelativeAge(new Date(dt), {title: true})); -}); +registerUnbound('age-with-tooltip', dt => new safe(autoUpdatingRelativeAge(new Date(dt), {title: true}))); -registerUnbound('number', function(orig, params) { +registerUnbound('number', (orig, params) => { orig = parseInt(orig, 10); if (isNaN(orig)) { orig = 0; } diff --git a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 index 6e8bbcc0d..d9e76878b 100644 --- a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 +++ b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 @@ -47,6 +47,13 @@ **/ +// TODO: Add all plugin-outlet names dynamically +const rewireableOutlets = [ + 'hamburger-admin' +]; + +const _rewires = {}; + let _connectorCache, _rawCache; function findOutlets(collection, callback) { @@ -63,9 +70,17 @@ function findOutlets(collection, callback) { } const segments = res.split("/"); - const outletName = segments[segments.length-2]; + let outletName = segments[segments.length-2]; const uniqueName = segments[segments.length-1]; + const outletRewires = _rewires[outletName]; + if (outletRewires) { + const newOutlet = outletRewires[uniqueName]; + if (newOutlet) { + outletName = newOutlet; + } + } + const dashedName = outletName.replace(/_/g, '-'); if (dashedName !== outletName) { Ember.warn("DEPRECATION: You need to use dashes in outlet names, not underscores"); @@ -179,4 +194,11 @@ Ember.HTMLBars._registerHelper('plugin-outlet', function(params, hash, options, } }); - +// Allow plugins to rewire outlets to new outlets if they exist. For example, the akismet +// plugin will use `hamburger-admin` if it exists, otherwise `site-menu-links` +export function rewire(uniqueName, outlet, wantedOutlet) { + if (rewireableOutlets.indexOf(wantedOutlet) !== -1) { + _rewires[outlet] = _rewires[outlet] || {}; + _rewires[outlet][uniqueName] = wantedOutlet; + } +} diff --git a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 index 1ab668ffc..c5eac31ad 100644 --- a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 +++ b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 @@ -1,24 +1,23 @@ import registerUnbound from 'discourse/helpers/register-unbound'; -import avatarTemplate from 'discourse/lib/avatar-template'; function renderAvatar(user, options) { options = options || {}; if (user) { - var username = Em.get(user, 'username'); - if (!username) { - if (!options.usernamePath) { return ''; } - username = Em.get(user, options.usernamePath); - } - var title; + const username = Em.get(user, options.usernamePath || 'username'); + const avatarTemplate = Em.get(user, options.avatarTemplatePath || 'avatar_template'); + + if (!username || !avatarTemplate) { return ''; } + + let title; if (!options.ignoreTitle) { // first try to get a title title = Em.get(user, 'title'); // if there was no title provided if (!title) { // try to retrieve a description - var description = Em.get(user, 'description'); + const description = Em.get(user, 'description'); // if a description has been provided if (description && description.length > 0) { // preprend the username before the description @@ -27,14 +26,11 @@ function renderAvatar(user, options) { } } - // this is simply done to ensure we cache images correctly - var uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id'); - return Discourse.Utilities.avatarImg({ size: options.imageSize, extraClasses: Em.get(user, 'extras') || options.extraClasses, title: title || username, - avatarTemplate: avatarTemplate(username, uploadedAvatarId) + avatarTemplate: avatarTemplate }); } else { return ''; diff --git a/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 b/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 index 5c04f67d0..763a06b2e 100644 --- a/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 +++ b/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 @@ -1,37 +1,10 @@ +import interceptClick from 'discourse/lib/intercept-click'; import DiscourseURL from 'discourse/lib/url'; -/** - Discourse does some server side rendering of HTML, such as the `cooked` contents of - posts. The downside of this in an Ember app is the links will not go through the router. - This jQuery code intercepts clicks on those links and routes them properly. -**/ export default { name: "click-interceptor", - initialize: function() { - $('#main').on('click.discourse', 'a', function(e) { - if (e.isDefaultPrevented() || e.shiftKey || e.metaKey || e.ctrlKey) { return; } - - var $currentTarget = $(e.currentTarget), - href = $currentTarget.attr('href'); - - if (!href || - href === '#' || - $currentTarget.attr('target') || - $currentTarget.data('ember-action') || - $currentTarget.data('auto-route') || - $currentTarget.data('share-url') || - $currentTarget.data('user-card') || - $currentTarget.hasClass('mention') || - $currentTarget.hasClass('ember-view') || - $currentTarget.hasClass('lightbox') || - href.indexOf("mailto:") === 0 || - (href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i")))) { - return; - } - - e.preventDefault(); - DiscourseURL.routeTo(href); - return false; - }); + initialize() { + $('#main').on('click.discourse', 'a', interceptClick); + $(window).on('hashchange', () => DiscourseURL.routeTo(document.location.hash)); } }; diff --git a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 index c3a5dd46b..2511dd3eb 100644 --- a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 +++ b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 @@ -6,6 +6,9 @@ export default { initialize(container) { + const cache = {}; + var transitionCount = 0; + // Tell our AJAX system to track a page transition const router = container.lookup('router:main'); router.on('willTransition', function() { @@ -14,8 +17,23 @@ export default { router.on('didTransition', function() { Em.run.scheduleOnce('afterRender', Ember.Route, cleanDOM); + transitionCount++; + _.each(cache, (v,k) => { + if (v && v.target && v.target < transitionCount) { + delete cache[k]; + } + }); }); + + router.transientCache = function(key, data, count) { + if (data === undefined) { + return cache[key]; + } else { + return cache[key] = {data, target: transitionCount + count}; + } + }; + const pageTracker = PageTracker.current(); pageTracker.start(); diff --git a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 index 19c64a61b..92ab828c6 100644 --- a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 @@ -8,7 +8,12 @@ export default { const user = container.lookup('current-user:main'), site = container.lookup('site:main'), siteSettings = container.lookup('site-settings:main'), - bus = container.lookup('message-bus:main'); + bus = container.lookup('message-bus:main'), + keyValueStore = container.lookup('key-value-store:main'); + + // clear old cached notifications + // they could be a week old for all we know + keyValueStore.remove('recent-notifications'); if (user) { @@ -38,6 +43,32 @@ export default { if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) { user.set('lastNotificationChange', new Date()); } + + var stale = keyValueStore.getObject('recent-notifications'); + const lastNotification = data.last_notification && data.last_notification.notification; + + if (stale && stale.notifications && lastNotification) { + + const oldNotifications = stale.notifications; + const staleIndex = _.findIndex(oldNotifications, {id: lastNotification.id}); + + if (staleIndex > -1) { + oldNotifications.splice(staleIndex, 1); + } + + // this gets a bit tricky, uread pms are bumped to front + var insertPosition = 0; + if (lastNotification.notification_type !== 6) { + insertPosition = _.findIndex(oldNotifications, function(n){ + return n.notification_type !== 6 || n.read; + }); + insertPosition = insertPosition === -1 ? oldNotifications.length - 1 : insertPosition; + } + + oldNotifications.splice(insertPosition, 0, lastNotification); + keyValueStore.setItem('recent-notifications', JSON.stringify(stale)); + + } }, user.notification_channel_position); bus.subscribe("/categories", function(data) { diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 index 7750475c3..cbaffc2b8 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -91,7 +91,7 @@ export default function(options) { transformed = _.isArray(transformedItem) ? transformedItem : [transformedItem || item]; var divs = transformed.map(function(itm) { - var d = $("<div class='item'><span>" + itm + "<a class='remove' href='#'><i class='fa fa-times'></i></a></span></div>"); + var d = $("<div class='item'><span>" + itm + "<a class='remove' href><i class='fa fa-times'></i></a></span></div>"); var prev = me.parent().find('.item:last'); if (prev.length === 0) { me.parent().prepend(d); @@ -220,6 +220,13 @@ export default function(options) { vOffset = div.height(); } + if (Discourse.Mobile.mobileView && !isInput) { + div.css('width', 'auto'); + + if ((me.height() / 2) >= pos.top) { vOffset = -23; } + if ((me.width() / 2) <= pos.left) { hOffset = -div.width(); } + } + var mePos = me.position(); var borderTop = parseInt(me.css('border-top-width'), 10) || 0; div.css({ diff --git a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 deleted file mode 100644 index 542e97959..000000000 --- a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 +++ /dev/null @@ -1,32 +0,0 @@ -import { hashString } from 'discourse/lib/hash'; - -let _splitAvatars; - -function defaultAvatar(username) { - const defaultAvatars = Discourse.SiteSettings.default_avatars; - if (defaultAvatars && defaultAvatars.length) { - _splitAvatars = _splitAvatars || defaultAvatars.split("\n"); - - if (_splitAvatars.length) { - const hash = hashString(username); - return _splitAvatars[Math.abs(hash) % _splitAvatars.length]; - } - } - - return Discourse.getURLWithCDN("/letter_avatar/" + - username.toLowerCase() + - "/{size}/" + - Discourse.LetterAvatarVersion + ".png"); -} - -export default function(username, uploadedAvatarId) { - if (uploadedAvatarId) { - return Discourse.getURLWithCDN("/user_avatar/" + - Discourse.BaseUrl + - "/" + - username.toLowerCase() + - "/{size}/" + - uploadedAvatarId + ".png"); - } - return defaultAvatar(username); -} diff --git a/app/assets/javascripts/discourse/lib/binary-search.js.es6 b/app/assets/javascripts/discourse/lib/binary-search.js.es6 new file mode 100644 index 000000000..03675866e --- /dev/null +++ b/app/assets/javascripts/discourse/lib/binary-search.js.es6 @@ -0,0 +1,29 @@ +// The binarySearch() function is licensed under the UNLICENSE +// https://github.com/Olical/binary-search + +// Modified for use in Discourse + +export default function binarySearch(list, target, keyProp) { + var min = 0; + var max = list.length - 1; + var guess; + var keyProperty = keyProp || "id"; + + while (min <= max) { + guess = Math.floor((min + max) / 2); + + if (Em.get(list[guess], keyProperty) === target) { + return guess; + } + else { + if (Em.get(list[guess], keyProperty) < target) { + min = guess + 1; + } + else { + max = guess - 1; + } + } + } + + return -Math.floor((min + max) / 2); +} diff --git a/app/assets/javascripts/discourse/lib/emoji/emoji.js.erb b/app/assets/javascripts/discourse/lib/emoji/emoji.js.erb index e662df804..8a8fceb25 100644 --- a/app/assets/javascripts/discourse/lib/emoji/emoji.js.erb +++ b/app/assets/javascripts/discourse/lib/emoji/emoji.js.erb @@ -77,7 +77,8 @@ function imageFor(code) { code = code.toLowerCase(); var url = urlFor(code); if (url) { - return ['img', { href: url, title: ':' + code + ':', 'class': 'emoji', alt: code }]; + var code = ':' + code + ':'; + return ['img', { href: url, title: code, 'class': 'emoji', alt: code }]; } } diff --git a/app/assets/javascripts/discourse/lib/intercept-click.js.es6 b/app/assets/javascripts/discourse/lib/intercept-click.js.es6 new file mode 100644 index 000000000..6ce7a300f --- /dev/null +++ b/app/assets/javascripts/discourse/lib/intercept-click.js.es6 @@ -0,0 +1,32 @@ +import DiscourseURL from 'discourse/lib/url'; + +/** + Discourse does some server side rendering of HTML, such as the `cooked` contents of + posts. The downside of this in an Ember app is the links will not go through the router. + This jQuery code intercepts clicks on those links and routes them properly. +**/ +export default function interceptClick(e) { + if (e.isDefaultPrevented() || e.shiftKey || e.metaKey || e.ctrlKey) { return; } + + const $currentTarget = $(e.currentTarget), + href = $currentTarget.attr('href'); + + if (!href || + href === '#' || + $currentTarget.attr('target') || + $currentTarget.data('ember-action') || + $currentTarget.data('auto-route') || + $currentTarget.data('share-url') || + $currentTarget.data('user-card') || + $currentTarget.hasClass('mention') || + (!$currentTarget.hasClass('d-link') && $currentTarget.hasClass('ember-view')) || + $currentTarget.hasClass('lightbox') || + href.indexOf("mailto:") === 0 || + (href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i")))) { + return; + } + + e.preventDefault(); + DiscourseURL.routeTo(href); + return false; +} diff --git a/app/assets/javascripts/discourse/lib/key-value-store.js.es6 b/app/assets/javascripts/discourse/lib/key-value-store.js.es6 index 8c1e13515..cc3427d76 100644 --- a/app/assets/javascripts/discourse/lib/key-value-store.js.es6 +++ b/app/assets/javascripts/discourse/lib/key-value-store.js.es6 @@ -32,6 +32,7 @@ KeyValueStore.prototype = { }, remove(key) { + if (!safeLocalStorage) { return; } return safeLocalStorage.removeItem(this.context + key); }, @@ -51,6 +52,13 @@ KeyValueStore.prototype = { const result = parseInt(this.get(key)); if (!isFinite(result)) { return def; } return result; + }, + + getObject(key) { + if (!safeLocalStorage) { return null; } + try { + return JSON.parse(safeLocalStorage[this.context + key]); + } catch(e) {} } }; diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index be0f13800..0172e8167 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -1,61 +1,53 @@ import DiscourseURL from 'discourse/lib/url'; -const PATH_BINDINGS = { - 'g h': '/', - 'g l': '/latest', - 'g n': '/new', - 'g u': '/unread', - 'g c': '/categories', - 'g t': '/top', - 'g b': '/bookmarks', - 'g p': '/my/activity' - }, - - SELECTED_POST_BINDINGS = { - 'd': 'deletePost', - 'e': 'editPost', - 'l': 'toggleLike', - 'r': 'replyToPost', - '!': 'showFlags', - 't': 'replyAsNewTopic' - }, - - CLICK_BINDINGS = { - 'm m': 'div.notification-options li[data-id="0"] a', // mark topic as muted - 'm r': 'div.notification-options li[data-id="1"] a', // mark topic as regular - 'm t': 'div.notification-options li[data-id="2"] a', // mark topic as tracking - 'm w': 'div.notification-options li[data-id="3"] a', // mark topic as watching - 'x r': '#dismiss-new,#dismiss-new-top,#dismiss-posts,#dismiss-posts-top', // dismiss new/posts - 'x t': '#dismiss-topics,#dismiss-topics-top', // dismiss topics - '.': '.alert.alert-info.clickable', // show incoming/updated topics - 'o,enter': '.topic-list tr.selected a.title', // open selected topic - 'shift+s': '#topic-footer-buttons button.share', // share topic - 's': '.topic-post.selected a.post-date' // share post - }, - - FUNCTION_BINDINGS = { - 'c': 'createTopic', // create new topic - 'home': 'goToFirstPost', - '#': 'toggleProgress', - 'end': 'goToLastPost', - 'shift+j': 'nextSection', - 'j': 'selectDown', - 'shift+k': 'prevSection', - 'shift+p': 'pinUnpinTopic', - 'k': 'selectUp', - 'u': 'goBack', - '/': 'showSearch', - '=': 'toggleHamburgerMenu', - 'p': 'showCurrentUser', // open current user menu - 'ctrl+f': 'showBuiltinSearch', - 'command+f': 'showBuiltinSearch', - '?': 'showHelpModal', // open keyboard shortcut help - 'q': 'quoteReply', - 'b': 'toggleBookmark', - 'f': 'toggleBookmarkTopic', - 'shift+r': 'replyToTopic', - 'shift+z shift+z': 'logout' - }; +const bindings = { + '!': {postAction: 'showFlags'}, + '#': {handler: 'toggleProgress', anonymous: true}, + '/': {handler: 'showSearch', anonymous: true}, + '=': {handler: 'toggleHamburgerMenu', anonymous: true}, + '?': {handler: 'showHelpModal', anonymous: true}, + '.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics + 'b': {handler: 'toggleBookmark'}, + 'c': {handler: 'createTopic'}, + 'ctrl+f': {handler: 'showBuiltinSearch', anonymous: true}, + 'command+f': {handler: 'showBuiltinSearch', anonymous: true}, + 'd': {postAction: 'deletePost'}, + 'e': {postAction: 'editPost'}, + 'end': {handler: 'goToLastPost', anonymous: true}, + 'f': {handler: 'toggleBookmarkTopic'}, + 'g h': {path: '/', anonymous: true}, + 'g l': {path: '/latest', anonymous: true}, + 'g n': {path: '/new'}, + 'g u': {path: '/unread'}, + 'g c': {path: '/categories', anonymous: true}, + 'g t': {path: '/top', anonymous: true}, + 'g b': {path: '/bookmarks'}, + 'g p': {path: '/my/activity'}, + 'g m': {path: '/my/messages'}, + 'home': {handler: 'goToFirstPost', anonymous: true}, + 'j': {handler: 'selectDown', anonymous: true}, + 'k': {handler: 'selectUp', anonymous: true}, + 'l': {postAction: 'toggleLike'}, + 'm m': {click: 'div.notification-options li[data-id="0"] a'}, // mark topic as muted + 'm r': {click: 'div.notification-options li[data-id="1"] a'}, // mark topic as regular + 'm t': {click: 'div.notification-options li[data-id="2"] a'}, // mark topic as tracking + 'm w': {click: 'div.notification-options li[data-id="3"] a'}, // mark topic as watching + 'o,enter': {click: '.topic-list tr.selected a.title', anonymous: true}, // open selected topic + 'p': {handler: 'showCurrentUser'}, + 'q': {handler: 'quoteReply'}, + 'r': {postAction: 'replyToPost'}, + 's': {click: '.topic-post.selected a.post-date', anonymous: true}, // share post + 'shift+j': {handler: 'nextSection', anonymous: true}, + 'shift+k': {handler: 'prevSection', anonymous: true}, + 'shift+p': {handler: 'pinUnpinTopic'}, + 'shift+r': {handler: 'replyToTopic'}, + 'shift+s': {click: '#topic-footer-buttons button.share', anonymous: true}, // share topic + 'shift+z shift+z': {handler: 'logout'}, + 't': {postAction: 'replyAsNewTopic'}, + 'u': {handler: 'goBack', anonymous: true}, + 'x r': {click: '#dismiss-new,#dismiss-new-top,#dismiss-posts,#dismiss-posts-top'}, // dismiss new/posts + 'x t': {click: '#dismiss-topics,#dismiss-topics-top'} // dismiss topics +}; export default { @@ -64,14 +56,24 @@ export default { this.container = container; this._stopCallback(); - this.searchService = this.container.lookup('search-service:main'); this.appEvents = this.container.lookup('app-events:main'); + this.currentUser = this.container.lookup('current-user:main'); - _.each(PATH_BINDINGS, this._bindToPath, this); - _.each(CLICK_BINDINGS, this._bindToClick, this); - _.each(SELECTED_POST_BINDINGS, this._bindToSelectedPost, this); - _.each(FUNCTION_BINDINGS, this._bindToFunction, this); + Object.keys(bindings).forEach(key => { + const binding = bindings[key]; + if (!binding.anonymous && !this.currentUser) { return; } + + if (binding.path) { + this._bindToPath(binding.path, key); + } else if (binding.handler) { + this._bindToFunction(binding.handler, key); + } else if (binding.postAction) { + this._bindToSelectedPost(binding.postAction, key); + } else if (binding.click) { + this._bindToClick(binding.click, key); + } + }); }, toggleBookmark() { @@ -222,17 +224,11 @@ export default { }, _bindToSelectedPost(action, binding) { - const self = this; - - this.keyTrapper.bind(binding, function() { - self.sendToSelectedPost(action); - }); + this.keyTrapper.bind(binding, () => this.sendToSelectedPost(action)); }, - _bindToPath(path, binding) { - this.keyTrapper.bind(binding, function() { - DiscourseURL.routeTo(path); - }); + _bindToPath(path, key) { + this.keyTrapper.bind(key, () => DiscourseURL.routeTo(path)); }, _bindToClick(selector, binding) { diff --git a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 index a73d88020..647f9a6e0 100644 --- a/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 +++ b/app/assets/javascripts/discourse/lib/safari-hacks.js.es6 @@ -1,11 +1,8 @@ function applicable() { - // CriOS is Chrome on iPad / iPhone, OPiOS is Opera (they need no patching) - // Dolphin has a wierd user agent, rest seem a bit nitch + // This will apply hack on all iDevices return navigator.userAgent.match(/(iPad|iPhone|iPod)/g) && - navigator.userAgent.match(/Safari/g) && - !navigator.userAgent.match(/CriOS/g) && - !navigator.userAgent.match(/OPiOS/g); + navigator.userAgent.match(/Safari/g); } // per http://stackoverflow.com/questions/29001977/safari-in-ios8-is-scrolling-screen-when-fixed-elements-get-focus/29064810 @@ -17,6 +14,7 @@ function positioningWorkaround($fixedElement) { const fixedElement = $fixedElement[0]; var done = false; + var originalScrollTop = 0; var blurredNow = function(evt) { if (!done && _.include($(document.activeElement).parents(), fixedElement)) { @@ -25,8 +23,16 @@ function positioningWorkaround($fixedElement) { } done = true; + + fixedElement.parentElement.style.height = ''; + $('#main-outlet').show(); + $('header').show(); + fixedElement.style.position = ''; fixedElement.style.top = ''; + fixedElement.style.height = ''; + $(window).scrollTop(originalScrollTop); + if (evt) { evt.target.removeEventListener('blur', blurred); } @@ -50,31 +56,23 @@ function positioningWorkaround($fixedElement) { return; } + originalScrollTop = $(window).scrollTop(); + + // take care of body + $('#main-outlet').hide(); + $('header').hide(); + + fixedElement.style.position = 'absolute'; // get out of the way while opening keyboard fixedElement.style.top = '0px'; + fixedElement.style.height = parseInt(window.innerHeight*0.6) + "px"; + fixedElement.parentElement.style.height = window.innerHeight + "px"; + $(window).scrollTop(0); + // great ... iOS positions this yet again + // so lets take over if this happens + setTimeout(()=>$(window).scrollTop(0),500); - var iPadOffset = 0; - if (window.innerHeight > window.innerWidth && navigator.userAgent.match(/iPad/)) { - // there is no way to get virtual keyboard height - iPadOffset = 640 - $(fixedElement).height(); - } - - var oldScrollY = 0; - - var positionElement = function(){ - if (done) { - return; - } - if (Math.abs(oldScrollY - window.scrollY) < 20) { - return; - } - oldScrollY = window.scrollY; - fixedElement.style.top = window.scrollY + iPadOffset + 'px'; - }; - - // position once, correctly, after keyboard is shown - setTimeout(positionElement, 500); evt.preventDefault(); self.focus(); diff --git a/app/assets/javascripts/discourse/lib/screen-track.js.es6 b/app/assets/javascripts/discourse/lib/screen-track.js.es6 index 8fe233401..82f5cb55c 100644 --- a/app/assets/javascripts/discourse/lib/screen-track.js.es6 +++ b/app/assets/javascripts/discourse/lib/screen-track.js.es6 @@ -10,6 +10,9 @@ const ScreenTrack = Ember.Object.extend({ init() { this.reset(); + + // TODO: Move `ScreenTrack` to injection and remove this + this.set('topicTrackingState', Discourse.__container__.lookup('topic-tracking-state:main')); }, start(topicId, topicController) { @@ -110,7 +113,7 @@ const ScreenTrack = Ember.Object.extend({ highestSeenByTopic[topicId] = highestSeen; } - Discourse.TopicTrackingState.current().updateSeen(topicId, highestSeen); + this.topicTrackingState.updateSeen(topicId, highestSeen); if (!$.isEmptyObject(newTimings)) { if (Discourse.User.current()) { diff --git a/app/assets/javascripts/discourse/lib/search-for-term.js.es6 b/app/assets/javascripts/discourse/lib/search.js.es6 similarity index 73% rename from app/assets/javascripts/discourse/lib/search-for-term.js.es6 rename to app/assets/javascripts/discourse/lib/search.js.es6 index 2d029fdf9..9d7bec2d6 100644 --- a/app/assets/javascripts/discourse/lib/search-for-term.js.es6 +++ b/app/assets/javascripts/discourse/lib/search.js.es6 @@ -86,4 +86,32 @@ function searchForTerm(term, opts) { return promise; } -export default searchForTerm; +const searchContextDescription = function(type, name){ + if (type) { + switch(type) { + case 'topic': + return I18n.t('search.context.topic'); + case 'user': + return I18n.t('search.context.user', {username: name}); + case 'category': + return I18n.t('search.context.category', {category: name}); + case 'private_messages': + return I18n.t('search.context.private_messages'); + } + } +}; + +const getSearchKey = function(args){ + return args.q + "|" + ((args.searchContext && args.searchContext.type) || "") + "|" + + ((args.searchContext && args.searchContext.id) || "") +}; + +const isValidSearchTerm = function(searchTerm) { + if (searchTerm) { + return searchTerm.trim().length >= Discourse.SiteSettings.min_search_term_length; + } else { + return false; + } +}; + +export { searchForTerm, searchContextDescription, getSearchKey, isValidSearchTerm }; diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index f94c4c2bd..ddc964cba 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -105,7 +105,7 @@ const DiscourseURL = Ember.Object.createWithMixins({ It contains the logic necessary to route within a topic using replaceState to keep the history intact. **/ - routeTo: function(path, opts) { + routeTo(path, opts) { if (Em.isEmpty(path)) { return; } if (Discourse.get('requiresRefresh')) { @@ -122,6 +122,7 @@ const DiscourseURL = Ember.Object.createWithMixins({ // Scroll to the same page, different anchor if (path.indexOf('#') === 0) { this.scrollToId(path); + history.replaceState(undefined, undefined, path); return; } @@ -271,7 +272,7 @@ const DiscourseURL = Ember.Object.createWithMixins({ // This has been extracted so it can be tested. origin: function() { - return window.location.origin; + return window.location.origin + (Discourse.BaseUri === "/" ? '' : Discourse.BaseUri); }, /** diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6 index 790a00ff0..3600814bb 100644 --- a/app/assets/javascripts/discourse/lib/user-search.js.es6 +++ b/app/assets/javascripts/discourse/lib/user-search.js.es6 @@ -89,7 +89,7 @@ export default function userSearch(options) { return new Ember.RSVP.Promise(function(resolve) { // TODO site setting for allowed regex in username - if (term.match(/[^a-zA-Z0-9_\.]/)) { + if (term.match(/[^a-zA-Z0-9_\.\-]/)) { resolve([]); return; } diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js index ac55bdaf4..20d9c53f5 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js +++ b/app/assets/javascripts/discourse/lib/utilities.js @@ -97,7 +97,10 @@ Discourse.Utilities = { // Strip out any .click elements from the HTML before converting it to text var div = document.createElement('div'); div.innerHTML = html; - $('.clicks', $(div)).remove(); + var $div = $(div); + // Find all emojis and replace with its title attribute. + $div.find('img.emoji').replaceWith(function() { return this.title }); + $('.clicks', $div).remove(); var text = div.textContent || div.innerText || ""; return String(text).trim(); @@ -212,6 +215,10 @@ Discourse.Utilities = { } }, + getUploadPlaceholder: function(filename) { + return "[" + I18n.t("uploading_filename", { filename: filename }) + "]() "; + }, + isAnImage: function(path) { return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp)$/i).test(path); }, diff --git a/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6 b/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6 index a30cba1f9..c0e7650d8 100644 --- a/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6 +++ b/app/assets/javascripts/discourse/mixins/bulk-topic-selection.js.es6 @@ -36,7 +36,7 @@ export default Ember.Mixin.create({ } promise.then(function(result) { if (result && result.topic_ids) { - const tracker = Discourse.TopicTrackingState.current(); + const tracker = self.topicTrackingState; result.topic_ids.forEach(function(t) { tracker.removeTopic(t); }); diff --git a/app/assets/javascripts/discourse/mixins/load-more.js.es6 b/app/assets/javascripts/discourse/mixins/load-more.js.es6 index d0e63e4ba..415c43b5e 100644 --- a/app/assets/javascripts/discourse/mixins/load-more.js.es6 +++ b/app/assets/javascripts/discourse/mixins/load-more.js.es6 @@ -1,5 +1,6 @@ import Eyeline from 'discourse/lib/eyeline'; import Scrolling from 'discourse/mixins/scrolling'; +import { on } from 'ember-addons/ember-computed-decorators'; // Provides the ability to load more items for a view which is scrolled to the bottom. export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, { @@ -9,15 +10,23 @@ export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, { if (eyeline) { eyeline.update(); } }, - _bindEyeline: function() { + loadMoreUnlessFull() { + if (this.screenNotFull()) { + this.send("loadMore"); + } + }, + + @on("didInsertElement") + _bindEyeline() { const eyeline = new Eyeline(this.get('eyelineSelector') + ":last"); this.set('eyeline', eyeline); eyeline.on('sawBottom', () => this.send('loadMore')); this.bindScrolling(); - }.on('didInsertElement'), + }, - _removeEyeline: function() { + @on("willDestroyElement") + _removeEyeline() { this.unbindScrolling(); - }.on('willDestroyElement') + } }); diff --git a/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 b/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 index e73b685d4..42a279b80 100644 --- a/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 +++ b/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 @@ -3,7 +3,7 @@ export default Em.Mixin.create({ needs: ['modal'], - flash: function(message, messageClass) { + flash(message, messageClass) { this.set('flashMessage', Em.Object.create({ message, messageClass })); } }); diff --git a/app/assets/javascripts/discourse/mixins/scrolling.js.es6 b/app/assets/javascripts/discourse/mixins/scrolling.js.es6 index ffa40294f..e25af304e 100644 --- a/app/assets/javascripts/discourse/mixins/scrolling.js.es6 +++ b/app/assets/javascripts/discourse/mixins/scrolling.js.es6 @@ -6,16 +6,20 @@ import debounce from 'discourse/lib/debounce'; easier. **/ const ScrollingDOMMethods = { - bindOnScroll: function(onScrollMethod, name) { + bindOnScroll(onScrollMethod, name) { name = name || 'default'; - $(document).bind('touchmove.discourse-' + name, onScrollMethod); - $(window).bind('scroll.discourse-' + name, onScrollMethod); + $(document).bind(`touchmove.discourse-${name}`, onScrollMethod); + $(window).bind(`scroll.discourse-${name}`, onScrollMethod); }, - unbindOnScroll: function(name) { + unbindOnScroll(name) { name = name || 'default'; - $(window).unbind('scroll.discourse-' + name); - $(document).unbind('touchmove.discourse-' + name); + $(window).unbind(`scroll.discourse-${name}`); + $(document).unbind(`touchmove.discourse-${name}`); + }, + + screenNotFull() { + return $(window).height() >= $(document).height(); } }; @@ -23,16 +27,15 @@ const Scrolling = Ember.Mixin.create({ // Begin watching for scroll events. By default they will be called at max every 100ms. // call with {debounce: N} for a diff time - bindScrolling: function(opts) { - opts = opts || {debounce: 100}; + bindScrolling(opts) { + opts = opts || { debounce: 100 }; // So we can not call the scrolled event while transitioning const router = Discourse.__container__.lookup('router:main').router; - const self = this; - var onScrollMethod = function() { + let onScrollMethod = () => { if (router.activeTransition) { return; } - return Em.run.scheduleOnce('afterRender', self, 'scrolled'); + return Ember.run.scheduleOnce('afterRender', this, 'scrolled'); }; if (opts.debounce) { @@ -40,10 +43,11 @@ const Scrolling = Ember.Mixin.create({ } ScrollingDOMMethods.bindOnScroll(onScrollMethod, opts.name); - Em.run.scheduleOnce('afterRender', onScrollMethod); }, - unbindScrolling: function(name) { + screenNotFull: () => ScrollingDOMMethods.screenNotFull(), + + unbindScrolling(name) { ScrollingDOMMethods.unbindOnScroll(name); } }); diff --git a/app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6 b/app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6 index 0d4baae0c..07dcd548a 100644 --- a/app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6 +++ b/app/assets/javascripts/discourse/mixins/stale-local-storage.js.es6 @@ -8,10 +8,11 @@ export default { return `${type}_${hashedArgs}`; }, - findStale(store, type, findArgs) { + findStale(store, type, findArgs, opts) { const staleResult = new StaleResult(); + const key = (opts && opts.storageKey) || this.storageKey(type, findArgs) try { - const stored = this.keyValueStore.getItem(this.storageKey(type, findArgs)); + const stored = this.keyValueStore.getItem(key); if (stored) { const parsed = JSON.parse(stored); staleResult.setResults(parsed); @@ -22,9 +23,11 @@ export default { return staleResult; }, - find(store, type, findArgs) { + find(store, type, findArgs, opts) { + const key = (opts && opts.storageKey) || this.storageKey(type, findArgs) + return this._super(store, type, findArgs).then((results) => { - this.keyValueStore.setItem(this.storageKey(type, findArgs), JSON.stringify(results)); + this.keyValueStore.setItem(key, JSON.stringify(results)); return results; }); } diff --git a/app/assets/javascripts/discourse/models/category-list.js.es6 b/app/assets/javascripts/discourse/models/category-list.js.es6 new file mode 100644 index 000000000..b5e709705 --- /dev/null +++ b/app/assets/javascripts/discourse/models/category-list.js.es6 @@ -0,0 +1,60 @@ +const CategoryList = Ember.ArrayProxy.extend({ + init() { + this.set('content', []); + this._super(); + } +}); + +CategoryList.reopenClass({ + categoriesFrom(store, result) { + const categories = Discourse.CategoryList.create(); + const users = Discourse.Model.extractByKey(result.featured_users, Discourse.User); + const list = Discourse.Category.list(); + + result.category_list.categories.forEach(function(c) { + if (c.parent_category_id) { + c.parentCategory = list.findBy('id', c.parent_category_id); + } + + if (c.subcategory_ids) { + c.subcategories = c.subcategory_ids.map(scid => list.findBy('id', parseInt(scid, 10))); + } + + if (c.featured_user_ids) { + c.featured_users = c.featured_user_ids.map(u => users[u]); + } + + if (c.topics) { + c.topics = c.topics.map(t => Discourse.Topic.create(t)); + } + + categories.pushObject(store.createRecord('category', c)); + }); + return categories; + }, + + listForParent(store, category) { + return Discourse.ajax(`/categories.json?parent_category_id=${category.get("id")}`).then(result => { + return Discourse.CategoryList.create({ + categories: this.categoriesFrom(store, result), + parentCategory: category + }); + }); + }, + + list(store) { + const getCategories = () => Discourse.ajax("/categories.json"); + return PreloadStore.getAndRemove("categories_list", getCategories).then(result => { + return Discourse.CategoryList.create({ + categories: this.categoriesFrom(store, result), + can_create_category: result.category_list.can_create_category, + can_create_topic: result.category_list.can_create_topic, + draft_key: result.category_list.draft_key, + draft: result.category_list.draft, + draft_sequence: result.category_list.draft_sequence + }); + }); + } +}); + +export default CategoryList; diff --git a/app/assets/javascripts/discourse/models/category.js b/app/assets/javascripts/discourse/models/category.js.es6 similarity index 70% rename from app/assets/javascripts/discourse/models/category.js rename to app/assets/javascripts/discourse/models/category.js.es6 index 7c3cefc1e..01e281c62 100644 --- a/app/assets/javascripts/discourse/models/category.js +++ b/app/assets/javascripts/discourse/models/category.js.es6 @@ -1,17 +1,24 @@ -Discourse.Category = Discourse.Model.extend({ +import RestModel from 'discourse/models/rest'; +import { on } from 'ember-addons/ember-computed-decorators'; - init: function() { - this._super(); - var availableGroups = Em.A(this.get("available_groups")); +const Category = RestModel.extend({ + @on('init') + setupGroupsAndPermissions() { + const availableGroups = this.get('available_groups'); + if (!availableGroups) { return; } this.set("availableGroups", availableGroups); - this.set("permissions", Em.A(_.map(this.group_permissions, function(elem){ - availableGroups.removeObject(elem.group_name); - return { - group_name: elem.group_name, - permission: Discourse.PermissionType.create({id: elem.permission_type}) - }; - }))); + + const groupPermissions = this.get('group_permissions'); + if (groupPermissions) { + this.set('permissions', groupPermissions.map((elem) => { + availableGroups.removeObject(elem.group_name); + return { + group_name: elem.group_name, + permission: Discourse.PermissionType.create({id: elem.permission_type}) + }; + })); + } }, availablePermissions: function(){ @@ -26,7 +33,7 @@ Discourse.Category = Discourse.Model.extend({ }.property('id'), url: function() { - return Discourse.getURL("/c/") + Discourse.Category.slugFor(this); + return Discourse.getURL("/c/") + Category.slugFor(this); }.property('name'), fullSlug: function() { @@ -77,7 +84,8 @@ Discourse.Category = Discourse.Model.extend({ background_url: this.get('background_url'), allow_badges: this.get('allow_badges'), custom_fields: this.get('custom_fields'), - topic_template: this.get('topic_template') + topic_template: this.get('topic_template'), + suppress_from_homepage: this.get('suppress_from_homepage'), }, type: this.get('id') ? 'PUT' : 'POST' }); @@ -128,16 +136,12 @@ Discourse.Category = Discourse.Model.extend({ } }.property('topics'), - topicTrackingState: function(){ - return Discourse.TopicTrackingState.current(); - }.property(), - - unreadTopics: function(){ - return this.get('topicTrackingState').countUnread(this.get('id')); + unreadTopics: function() { + return this.topicTrackingState.countUnread(this.get('id')); }.property('topicTrackingState.messageCount'), - newTopics: function(){ - return this.get('topicTrackingState').countNew(this.get('id')); + newTopics: function() { + return this.topicTrackingState.countNew(this.get('id')); }.property('topicTrackingState.messageCount'), topicStatsTitle: function() { @@ -192,83 +196,78 @@ Discourse.Category = Discourse.Model.extend({ var _uncategorized; -Discourse.Category.reopenClass({ +Category.reopenClass({ - findUncategorized: function() { - _uncategorized = _uncategorized || Discourse.Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id')); + findUncategorized() { + _uncategorized = _uncategorized || Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id')); return _uncategorized; }, - slugFor: function(category) { + slugFor(category) { if (!category) return ""; - var parentCategory = Em.get(category, 'parentCategory'), - result = ""; + const parentCategory = Em.get(category, 'parentCategory'); + let result = ""; if (parentCategory) { - result = Discourse.Category.slugFor(parentCategory) + "/"; + result = Category.slugFor(parentCategory) + "/"; } - var id = Em.get(category, 'id'), - slug = Em.get(category, 'slug'); + const id = Em.get(category, 'id'), + slug = Em.get(category, 'slug'); - if (!slug || slug.trim().length === 0) return result + id + "-category"; - return result + slug; + return !slug || slug.trim().length === 0 ? `${result}${id}-category` : result + slug; }, - list: function() { - if (Discourse.SiteSettings.fixed_category_positions) { - return Discourse.Site.currentProp('categories'); - } else { - return Discourse.Site.currentProp('sortedCategories'); - } + list() { + return Discourse.SiteSettings.fixed_category_positions ? + Discourse.Site.currentProp('categories') : + Discourse.Site.currentProp('sortedCategories'); }, - listByActivity: function() { + listByActivity() { return Discourse.Site.currentProp('sortedCategories'); }, - idMap: function() { + idMap() { return Discourse.Site.currentProp('categoriesById'); }, - findSingleBySlug: function(slug) { - return Discourse.Category.list().find(function(c) { - return Discourse.Category.slugFor(c) === slug; - }); + findSingleBySlug(slug) { + return Category.list().find(c => Category.slugFor(c) === slug); }, - findById: function(id) { + findById(id) { if (!id) { return; } - return Discourse.Category.idMap()[id]; + return Category.idMap()[id]; }, - findByIds: function(ids){ - var categories = []; - _.each(ids, function(id){ - var found = Discourse.Category.findById(id); - if(found){ + findByIds(ids) { + const categories = []; + _.each(ids, id => { + const found = Category.findById(id); + if (found) { categories.push(found); } }); return categories; }, - findBySlug: function(slug, parentSlug) { - var categories = Discourse.Category.list(), - category; + findBySlug(slug, parentSlug) { + const categories = Category.list(); + let category; if (parentSlug) { - var parentCategory = Discourse.Category.findSingleBySlug(parentSlug); + const parentCategory = Category.findSingleBySlug(parentSlug); if (parentCategory) { if (slug === 'none') { return parentCategory; } - category = categories.find(function(item) { - return item && item.get('parentCategory') === parentCategory && Discourse.Category.slugFor(item) === (parentSlug + "/" + slug); + category = categories.find(item => { + return item && item.get('parentCategory') === parentCategory && Category.slugFor(item) === (parentSlug + "/" + slug); }); } } else { - category = Discourse.Category.findSingleBySlug(slug); + category = Category.findSingleBySlug(slug); // If we have a parent category, we need to enforce it if (category && category.get('parentCategory')) return; @@ -282,9 +281,9 @@ Discourse.Category.reopenClass({ return category; }, - reloadById: function(id) { - return Discourse.ajax("/c/" + id + "/show.json").then(function (result) { - return Discourse.Category.create(result.category); - }); + reloadById(id) { + return Discourse.ajax(`/c/${id}/show.json`); } }); + +export default Category; diff --git a/app/assets/javascripts/discourse/models/category_list.js b/app/assets/javascripts/discourse/models/category_list.js deleted file mode 100644 index bd1e1711f..000000000 --- a/app/assets/javascripts/discourse/models/category_list.js +++ /dev/null @@ -1,68 +0,0 @@ -Discourse.CategoryList = Ember.ArrayProxy.extend({ - init: function() { - this.set('content', []); - this._super(); - } -}); - -Discourse.CategoryList.reopenClass({ - categoriesFrom: function(result) { - var categories = Discourse.CategoryList.create(), - users = Discourse.Model.extractByKey(result.featured_users, Discourse.User), - list = Discourse.Category.list(); - - result.category_list.categories.forEach(function(c) { - - if (c.parent_category_id) { - c.parentCategory = list.findBy('id', c.parent_category_id); - } - - if (c.subcategory_ids) { - c.subcategories = c.subcategory_ids.map(function(scid) { return list.findBy('id', parseInt(scid, 10)); }); - } - - if (c.featured_user_ids) { - c.featured_users = c.featured_user_ids.map(function(u) { - return users[u]; - }); - } - if (c.topics) { - c.topics = c.topics.map(function(t) { - return Discourse.Topic.create(t); - }); - } - - categories.pushObject(Discourse.Category.create(c)); - - }); - return categories; - }, - - listForParent: function(category) { - var self = this; - return Discourse.ajax('/categories.json?parent_category_id=' + category.get('id')).then(function(result) { - return Discourse.CategoryList.create({ - categories: self.categoriesFrom(result), - parentCategory: category - }); - }); - }, - - list: function() { - var self = this; - - return PreloadStore.getAndRemove("categories_list", function() { - return Discourse.ajax("/categories.json"); - }).then(function(result) { - return Discourse.CategoryList.create({ - categories: self.categoriesFrom(result), - can_create_category: result.category_list.can_create_category, - can_create_topic: result.category_list.can_create_topic, - draft_key: result.category_list.draft_key, - draft: result.category_list.draft, - draft_sequence: result.category_list.draft_sequence - }); - }); - } - -}); diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index ad4115c8b..2b5241d29 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -3,6 +3,7 @@ import Topic from 'discourse/models/topic'; import { throwAjaxError } from 'discourse/lib/ajax-error'; import Quote from 'discourse/lib/quote'; import Draft from 'discourse/models/draft'; +import computed from 'ember-addons/ember-computed-decorators'; const CLOSED = 'closed', SAVING = 'saving', @@ -23,6 +24,7 @@ const CLOSED = 'closed', category: 'categoryId', topic_id: 'topic.id', is_warning: 'isWarning', + whisper: 'whisper', archetype: 'archetypeId', target_usernames: 'targetUsernames', typing_duration_msecs: 'typingTime', @@ -35,15 +37,42 @@ const CLOSED = 'closed', }; const Composer = RestModel.extend({ + _categoryId: null, archetypes: function() { return this.site.get('archetypes'); }.property(), + + @computed + categoryId: { + get() { return this._categoryId; }, + + // We wrap categoryId this way so we can fire `applyTopicTemplate` with + // the previous value as well as the new value + set(categoryId) { + const oldCategoryId = this._categoryId; + + if (Ember.isEmpty(categoryId)) { categoryId = null; } + this._categoryId = categoryId; + + if (oldCategoryId !== categoryId) { + this.applyTopicTemplate(oldCategoryId, categoryId); + } + return categoryId; + } + }, + creatingTopic: Em.computed.equal('action', CREATE_TOPIC), creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE), notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'), + showCategoryChooser: function(){ + const manyCategories = Discourse.Category.list().length > 1; + const hasOptions = this.get('archetype.hasOptions'); + return !this.get('privateMessage') && (hasOptions || manyCategories); + }.property('privateMessage'), + privateMessage: function(){ return this.get('creatingPrivateMessage') || this.get('topic.archetype') === 'private_message'; }.property('creatingPrivateMessage', 'topic'), @@ -56,6 +85,7 @@ const Composer = RestModel.extend({ viewOpen: Em.computed.equal('composeState', OPEN), viewDraft: Em.computed.equal('composeState', DRAFT), + composeStateChanged: function() { var oldOpen = this.get('composerOpened'); @@ -339,20 +369,24 @@ const Composer = RestModel.extend({ this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') }); }, - applyTopicTemplate: function() { + applyTopicTemplate(oldCategoryId, categoryId) { if (this.get('action') !== CREATE_TOPIC) { return; } - if (!Ember.isEmpty(this.get('reply'))) { return; } + let reply = this.get('reply'); - const categoryId = this.get('categoryId'); - const category = this.site.categories.find((c) => c.get('id') === categoryId); - if (category) { - const topicTemplate = category.get('topic_template'); - if (!Ember.isEmpty(topicTemplate)) { - this.set('reply', topicTemplate); + // If the user didn't change the template, clear it + if (oldCategoryId) { + const oldCat = this.site.categories.findProperty('id', oldCategoryId); + if (oldCat && (oldCat.get('topic_template') === reply)) { + reply = ""; } } - }.observes('categoryId'), + if (!Ember.isEmpty(reply)) { return; } + const category = this.site.categories.findProperty('id', categoryId); + if (category) { + this.set('reply', category.get('topic_template') || ""); + } + }, /* Open a composer @@ -397,14 +431,22 @@ const Composer = RestModel.extend({ } } - const categoryId = opts.categoryId || this.get('topic.category.id'); this.setProperties({ - categoryId, archetypeId: opts.archetypeId || this.site.get('default_archetype'), metaData: opts.metaData ? Em.Object.create(opts.metaData) : null, reply: opts.reply || this.get("reply") || "" }); + // We set the category id separately for topic templates on opening of composer + this.set('categoryId', opts.categoryId || this.get('topic.category.id')); + + if (!this.get('categoryId') && this.get('creatingTopic')) { + const categories = Discourse.Category.list(); + if (categories.length === 1) { + this.set('categoryId', categories[0].get('id')); + } + } + if (opts.postId) { this.set('loading', true); this.store.find('post', opts.postId).then(function(post) { @@ -529,6 +571,9 @@ const Composer = RestModel.extend({ let addedToStream = false; + const postTypes = this.site.get('post_types'); + const postType = this.get('whisper') ? postTypes.whisper : postTypes.regular; + // Build the post object const createdPost = this.store.createRecord('post', { imageSizes: opts.imageSizes, @@ -539,9 +584,9 @@ const Composer = RestModel.extend({ username: user.get('username'), user_id: user.get('id'), user_title: user.get('title'), - uploaded_avatar_id: user.get('uploaded_avatar_id'), + avatar_template: user.get('avatar_template'), user_custom_fields: user.get('custom_fields'), - post_type: this.site.get('post_types.regular'), + post_type: postType, actions_summary: [], moderator: user.get('moderator'), admin: user.get('admin'), @@ -559,7 +604,7 @@ const Composer = RestModel.extend({ reply_to_post_number: post.get('post_number'), reply_to_user: { username: post.get('username'), - uploaded_avatar_id: post.get('uploaded_avatar_id') + avatar_template: post.get('avatar_template') } }); } @@ -690,47 +735,6 @@ const Composer = RestModel.extend({ Composer.reopenClass({ - open(opts) { - const composer = Composer.create(); - composer.open(opts); - return composer; - }, - - loadDraft(opts) { - opts = opts || {}; - - let draft = opts.draft; - const draftKey = opts.draftKey; - const draftSequence = opts.draftSequence; - - try { - if (draft && typeof draft === 'string') { - draft = JSON.parse(draft); - } - } catch (error) { - draft = null; - Draft.clear(draftKey, draftSequence); - } - if (draft && ((draft.title && draft.title !== '') || (draft.reply && draft.reply !== ''))) { - return this.open({ - draftKey, - draftSequence, - action: draft.action, - title: draft.title, - categoryId: draft.categoryId || opts.categoryId, - postId: draft.postId, - archetypeId: draft.archetypeId, - reply: draft.reply, - metaData: draft.metaData, - usernames: draft.usernames, - draft: true, - composerState: DRAFT, - composerTime: draft.composerTime, - typingTime: draft.typingTime - }); - } - }, - // TODO: Replace with injection create(args) { args = args || {}; diff --git a/app/assets/javascripts/discourse/models/nav-item.js.es6 b/app/assets/javascripts/discourse/models/nav-item.js.es6 index d4e5ef228..ee81b705e 100644 --- a/app/assets/javascripts/discourse/models/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/models/nav-item.js.es6 @@ -20,10 +20,6 @@ const NavItem = Discourse.Model.extend({ return I18n.t("filters." + name.replace("/", ".") + ".title", extra); }.property('categoryName', 'name', 'count'), - topicTrackingState: function() { - return Discourse.TopicTrackingState.current(); - }.property(), - categoryName: function() { var split = this.get('name').split('/'); return split[0] === 'category' ? split[1] : null; @@ -100,26 +96,24 @@ NavItem.reopenClass({ extra = cb.call(self, text, opts); _.merge(args, extra); }); - return Discourse.NavItem.create(args); + + const store = Discourse.__container__.lookup('store:main'); + return store.createRecord('nav-item', args); }, buildList(category, args) { args = args || {}; + if (category) { args.category = category } - var items = Discourse.SiteSettings.top_menu.split("|"); + let items = Discourse.SiteSettings.top_menu.split("|"); - if (args.filterMode && !_.some(items, function(i){ - return i.indexOf(args.filterMode) !== -1; - })) { + if (args.filterMode && !_.some(items, i => i.indexOf(args.filterMode) !== -1)) { items.push(args.filterMode); } - return items.map(function(i) { - return Discourse.NavItem.fromText(i, args); - }).filter(function(i) { - return i !== null && !(category && i.get("name").indexOf("categor") === 0); - }); + return items.map(i => Discourse.NavItem.fromText(i, args)) + .filter(i => i !== null && !(category && i.get("name").indexOf("categor") === 0)); } }); diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index 9f625f347..2e2dcf079 100644 --- a/app/assets/javascripts/discourse/models/post-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/post-stream.js.es6 @@ -5,18 +5,13 @@ function calcDayDiff(p1, p2) { if (!p1) { return; } const date = p1.get('created_at'); - if (date) { - if (p2) { - const numDiff = p1.get('post_number') - p2.get('post_number'); - if (numDiff === 1) { - const lastDate = p2.get('created_at'); - if (lastDate) { - const delta = new Date(date).getTime() - new Date(lastDate).getTime(); - const days = Math.round(delta / (1000 * 60 * 60 * 24)); + if (date && p2) { + const lastDate = p2.get('created_at'); + if (lastDate) { + const delta = new Date(date).getTime() - new Date(lastDate).getTime(); + const days = Math.round(delta / (1000 * 60 * 60 * 24)); - p1.set('daysSincePrevious', days); - } - } + p1.set('daysSincePrevious', days); } } } @@ -281,14 +276,13 @@ const PostStream = RestModel.extend({ // Fill in a gap of posts after a particular post fillGapAfter(post, gap) { const postId = post.get('id'), - stream = this.get('stream'), - idx = stream.indexOf(postId), - self = this; + stream = this.get('stream'), + idx = stream.indexOf(postId); if (idx !== -1) { stream.pushObjects(gap); - return this.appendMore().then(function() { - self.get('stream').enumerableContentDidChange(); + return this.appendMore().then(() => { + this.get('stream').enumerableContentDidChange(); }); } return Ember.RSVP.resolve(); @@ -296,24 +290,18 @@ const PostStream = RestModel.extend({ // Appends the next window of posts to the stream. Call it when scrolling downwards. appendMore() { - const self = this; - // Make sure we can append more posts - if (!self.get('canAppendMore')) { return Ember.RSVP.resolve(); } + if (!this.get('canAppendMore')) { return Ember.RSVP.resolve(); } - const postIds = self.get('nextWindow'); + const postIds = this.get('nextWindow'); if (Ember.isEmpty(postIds)) { return Ember.RSVP.resolve(); } - self.set('loadingBelow', true); + this.set('loadingBelow', true); - const stopLoading = function() { - self.set('loadingBelow', false); - }; + const stopLoading = () => this.set('loadingBelow', false); - return self.findPostsByIds(postIds).then(function(posts) { - posts.forEach(function(p) { - self.appendPost(p); - }); + return this.findPostsByIds(postIds).then((posts) => { + posts.forEach(p => this.appendPost(p)); stopLoading(); }, stopLoading); }, @@ -685,6 +673,12 @@ const PostStream = RestModel.extend({ const postIdentityMap = this.get('postIdentityMap'), existing = postIdentityMap.get(post.get('id')); + // Update the `highest_post_number` if this post is higher. + const postNumber = post.get('post_number'); + if (postNumber && postNumber > (this.get('topic.highest_post_number') || 0)) { + this.set('topic.highest_post_number', postNumber); + } + if (existing) { // If the post is in the identity map, update it and return the old reference. existing.updateFromPost(post); @@ -693,12 +687,6 @@ const PostStream = RestModel.extend({ post.set('topic', this.get('topic')); postIdentityMap.set(post.get('id'), post); - - // Update the `highest_post_number` if this post is higher. - const postNumber = post.get('post_number'); - if (postNumber && postNumber > (this.get('topic.highest_post_number') || 0)) { - this.set('topic.highest_post_number', postNumber); - } } return post; }, diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index 3719dc3d0..6ba2606aa 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -1,7 +1,7 @@ import RestModel from 'discourse/models/rest'; import { popupAjaxError } from 'discourse/lib/ajax-error'; import ActionSummary from 'discourse/models/action-summary'; -import { url, fmt, propertyEqual } from 'discourse/lib/computed'; +import { url, propertyEqual } from 'discourse/lib/computed'; import Quote from 'discourse/lib/quote'; import computed from 'ember-addons/ember-computed-decorators'; @@ -77,29 +77,18 @@ const Post = RestModel.extend({ topicOwner: propertyEqual('topic.details.created_by.id', 'user_id'), hasHistory: Em.computed.gt('version', 1), - postElementId: fmt('post_number', 'post_%@'), canViewRawEmail: function() { return this.get("user_id") === Discourse.User.currentProp("id") || Discourse.User.currentProp('staff'); }.property("user_id"), - wikiChanged: function() { - const data = { wiki: this.get("wiki") }; - this._updatePost("wiki", data); - }.observes('wiki'), + updatePostField(field, value) { + const data = {}; + data[field] = value; - postTypeChanged: function () { - const data = { post_type: this.get("post_type") }; - this._updatePost("post_type", data); - }.observes("post_type"), - - _updatePost(field, data) { - const self = this; - Discourse.ajax("/posts/" + this.get("id") + "/" + field, { - type: "PUT", - data: data - }).then(function () { - self.incrementProperty("version"); + Discourse.ajax(`/posts/${this.get('id')}/${field}`, { type: 'PUT', data }).then(() => { + this.set(field, value); + this.incrementProperty("version"); }).catch(popupAjaxError); }, diff --git a/app/assets/javascripts/discourse/models/rest.js.es6 b/app/assets/javascripts/discourse/models/rest.js.es6 index 60d011bfc..0c595bb73 100644 --- a/app/assets/javascripts/discourse/models/rest.js.es6 +++ b/app/assets/javascripts/discourse/models/rest.js.es6 @@ -78,13 +78,10 @@ RestModel.reopenClass({ create(args) { args = args || {}; - if (!args.store || !args.keyValueStore) { + if (!args.store) { const container = Discourse.__container__; // Ember.warn('Use `store.createRecord` to create records instead of `.create()`'); args.store = container.lookup('store:main'); - - // TODO: Remove this when composer is using the store fully - args.keyValueStore = container.lookup('key-value-store:main'); } args.__munge = this.munge; diff --git a/app/assets/javascripts/discourse/models/site.js.es6 b/app/assets/javascripts/discourse/models/site.js.es6 index 226e1ed81..988ba1eac 100644 --- a/app/assets/javascripts/discourse/models/site.js.es6 +++ b/app/assets/javascripts/discourse/models/site.js.es6 @@ -1,35 +1,37 @@ +import computed from "ember-addons/ember-computed-decorators"; import Archetype from 'discourse/models/archetype'; import PostActionType from 'discourse/models/post-action-type'; import Singleton from 'discourse/mixins/singleton'; +import RestModel from 'discourse/models/rest'; -const Site = Discourse.Model.extend({ +const Site = RestModel.extend({ isReadOnly: Em.computed.alias('is_readonly'), - notificationLookup: function() { + @computed("notification_types") + notificationLookup(notificationTypes) { const result = []; - _.each(this.get('notification_types'), function(v,k) { - result[v] = k; - }); + _.each(notificationTypes, (v, k) => result[v] = k); return result; - }.property('notification_types'), + }, - flagTypes: function() { + @computed("post_action_types.@each") + flagTypes() { const postActionTypes = this.get('post_action_types'); if (!postActionTypes) return []; return postActionTypes.filterProperty('is_flag', true); - }.property('post_action_types.@each'), + }, topicCountDesc: ['topic_count:desc'], categoriesByCount: Ember.computed.sort('categories', 'topicCountDesc'), // Sort subcategories under parents - sortedCategories: function() { - const cats = this.get('categoriesByCount'), - result = [], - remaining = {}; + @computed("categoriesByCount", "categories.@each") + sortedCategories(cats) { + const result = [], + remaining = {}; - cats.forEach(function(c) { + cats.forEach(c => { const parentCategoryId = parseInt(c.get('parent_category_id'), 10); if (!parentCategoryId) { result.pushObject(c); @@ -39,17 +41,17 @@ const Site = Discourse.Model.extend({ } }); - Ember.keys(remaining).forEach(function(parentCategoryId) { + Ember.keys(remaining).forEach(parentCategoryId => { const category = result.findBy('id', parseInt(parentCategoryId, 10)), - index = result.indexOf(category); + index = result.indexOf(category); if (index !== -1) { - result.replace(index+1, 0, remaining[parentCategoryId]); + result.replace(index + 1, 0, remaining[parentCategoryId]); } }); return result; - }.property("categories.@each"), + }, postActionTypeById(id) { return this.get("postActionByIdLookup.action" + id); @@ -80,7 +82,7 @@ const Site = Discourse.Model.extend({ existingCategory.setProperties(newCategory); } else { // TODO insert in right order? - newCategory = Discourse.Category.create(newCategory); + newCategory = this.store.createRecord('category', newCategory); categories.pushObject(newCategory); this.get('categoriesById')[categoryId] = newCategory; } @@ -91,20 +93,20 @@ Site.reopenClass(Singleton, { // The current singleton will retrieve its attributes from the `PreloadStore`. createCurrent() { - return Site.create(PreloadStore.get('site')); + const store = Discourse.__container__.lookup('store:main'); + return store.createRecord('site', PreloadStore.get('site')); }, create() { const result = this._super.apply(this, arguments); + const store = result.store; if (result.categories) { result.categoriesById = {}; - result.categories = _.map(result.categories, function(c) { - return result.categoriesById[c.id] = Discourse.Category.create(c); - }); + result.categories = _.map(result.categories, c => result.categoriesById[c.id] = store.createRecord('category', c)); // Associate the categories with their parents - result.categories.forEach(function (c) { + result.categories.forEach(c => { if (c.get('parent_category_id')) { c.set('parentCategory', result.categoriesById[c.get('parent_category_id')]); } @@ -112,16 +114,13 @@ Site.reopenClass(Singleton, { } if (result.trust_levels) { - result.trustLevels = result.trust_levels.map(function (tl) { - return Discourse.TrustLevel.create(tl); - }); - + result.trustLevels = result.trust_levels.map(tl => Discourse.TrustLevel.create(tl)); delete result.trust_levels; } if (result.post_action_types) { result.postActionByIdLookup = Em.Object.create(); - result.post_action_types = _.map(result.post_action_types,function(p) { + result.post_action_types = _.map(result.post_action_types, p => { const actionType = PostActionType.create(p); result.postActionByIdLookup.set("action" + p.id, actionType); return actionType; @@ -130,7 +129,7 @@ Site.reopenClass(Singleton, { if (result.topic_flag_types) { result.topicFlagByIdLookup = Em.Object.create(); - result.topic_flag_types = _.map(result.topic_flag_types,function(p) { + result.topic_flag_types = _.map(result.topic_flag_types, p => { const actionType = PostActionType.create(p); result.topicFlagByIdLookup.set("action" + p.id, actionType); return actionType; @@ -138,16 +137,14 @@ Site.reopenClass(Singleton, { } if (result.archetypes) { - result.archetypes = _.map(result.archetypes,function(a) { + result.archetypes = _.map(result.archetypes, a => { a.site = result; return Archetype.create(a); }); } if (result.user_fields) { - result.user_fields = result.user_fields.map(function(uf) { - return Ember.Object.create(uf); - }); + result.user_fields = result.user_fields.map(uf => Ember.Object.create(uf)); } return result; diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index 851b712a6..57d60b0bb 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -71,18 +71,18 @@ export default Ember.Object.extend({ // See if the store can find stale data. We sometimes prefer to show stale data and // refresh it in the background. - findStale(type, findArgs) { - const stale = this.adapterFor(type).findStale(this, type, findArgs); + findStale(type, findArgs, opts) { + const stale = this.adapterFor(type).findStale(this, type, findArgs, opts); if (stale.hasResults) { stale.results = this._hydrateFindResults(stale.results, type, findArgs); } - stale.refresh = () => this.find(type, findArgs); + stale.refresh = () => this.find(type, findArgs, opts); return stale; }, - find(type, findArgs) { - return this.adapterFor(type).find(this, type, findArgs).then((result) => { - return this._hydrateFindResults(result, type, findArgs); + find(type, findArgs, opts) { + return this.adapterFor(type).find(this, type, findArgs, opts).then((result) => { + return this._hydrateFindResults(result, type, findArgs, opts); }); }, @@ -157,6 +157,10 @@ export default Ember.Object.extend({ obj.__type = type; obj.__state = obj.id ? "created" : "new"; + // TODO: Have injections be automatic + obj.topicTrackingState = this.container.lookup('topic-tracking-state:main'); + obj.keyValueStore = this.container.lookup('key-value-store:main'); + const klass = this.container.lookupFactory('model:' + type) || RestModel; const model = klass.create(obj); diff --git a/app/assets/javascripts/discourse/models/topic-list.js.es6 b/app/assets/javascripts/discourse/models/topic-list.js.es6 index 782cb9fd0..a600221e6 100644 --- a/app/assets/javascripts/discourse/models/topic-list.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-list.js.es6 @@ -147,9 +147,6 @@ TopicList.reopenClass({ json.per_page = json.topic_list.per_page; json.topics = topicsFrom(json, store); - if (json.topic_list.filtered_category) { - json.category = Discourse.Category.create(json.topic_list.filtered_category); - } return json; }, @@ -163,10 +160,9 @@ TopicList.reopenClass({ return this.find(filter); }, - // Sets `hideCategory` if all topics in the last have a particular category + // hide the category when it has no children hideUniformCategory(list, category) { - const hideCategory = !list.get('topics').any(function (t) { return t.get('category') !== category; }); - list.set('hideCategory', hideCategory); + list.set('hideCategory', category && !category.get("has_children")); } }); diff --git a/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 b/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 index cf5f89037..11c8d67fe 100644 --- a/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 @@ -1,4 +1,6 @@ import NotificationLevels from 'discourse/lib/notification-levels'; +import computed from "ember-addons/ember-computed-decorators"; +import { on } from "ember-addons/ember-computed-decorators"; function isNew(topic) { return topic.last_read_post_number === null && @@ -15,24 +17,25 @@ function isUnread(topic) { const TopicTrackingState = Discourse.Model.extend({ messageCount: 0, - _setup: function() { + @on("init") + _setup() { this.unreadSequence = []; this.newSequence = []; this.states = {}; - }.on('init'), + }, establishChannels() { const tracker = this; - const process = function(data){ + const process = data => { if (data.message_type === "delete") { tracker.removeTopic(data.topic_id); tracker.incrementMessageCount(); } if (data.message_type === "new_topic" || data.message_type === "latest") { - const ignored_categories = Discourse.User.currentProp("muted_category_ids"); - if(_.include(ignored_categories, data.payload.category_id)){ + const muted_category_ids = Discourse.User.currentProp("muted_category_ids"); + if (_.include(muted_category_ids, data.payload.category_id)) { return; } } @@ -45,7 +48,7 @@ const TopicTrackingState = Discourse.Model.extend({ tracker.notify(data); const old = tracker.states["t" + data.topic_id]; - if(!_.isEqual(old, data.payload)){ + if (!_.isEqual(old, data.payload)) { tracker.states["t" + data.topic_id] = data.payload; tracker.incrementMessageCount(); } @@ -60,20 +63,27 @@ const TopicTrackingState = Discourse.Model.extend({ }, updateSeen(topicId, highestSeen) { - if(!topicId || !highestSeen) { return; } + if (!topicId || !highestSeen) { return; } const state = this.states["t" + topicId]; - if(state && (!state.last_read_post_number || state.last_read_post_number < highestSeen)) { + if (state && (!state.last_read_post_number || state.last_read_post_number < highestSeen)) { state.last_read_post_number = highestSeen; this.incrementMessageCount(); } }, - notify(data){ + notify(data) { if (!this.newIncoming) { return; } const filter = this.get("filter"); - if ((filter === "all" || filter === "latest" || filter === "new") && data.message_type === "new_topic" ) { + if (filter === Discourse.Utilities.defaultHomepage()) { + const suppressed_from_homepage_category_ids = Discourse.Site.currentProp("suppressed_from_homepage_category_ids"); + if (_.include(suppressed_from_homepage_category_ids, data.payload.category_id)) { + return; + } + } + + if ((filter === "all" || filter === "latest" || filter === "new") && data.message_type === "new_topic") { this.addIncoming(data.topic_id); } @@ -84,7 +94,7 @@ const TopicTrackingState = Discourse.Model.extend({ } } - if(filter === "latest" && data.message_type === "latest") { + if (filter === "latest" && data.message_type === "latest") { this.addIncoming(data.topic_id); } @@ -92,12 +102,12 @@ const TopicTrackingState = Discourse.Model.extend({ }, addIncoming(topicId) { - if(this.newIncoming.indexOf(topicId) === -1){ + if (this.newIncoming.indexOf(topicId) === -1) { this.newIncoming.push(topicId); } }, - resetTracking(){ + resetTracking() { this.newIncoming = []; this.set("incomingCount", 0); }, @@ -109,10 +119,10 @@ const TopicTrackingState = Discourse.Model.extend({ this.set("incomingCount", 0); }, - hasIncoming: function(){ - const count = this.get('incomingCount'); - return count && count > 0; - }.property('incomingCount'), + @computed("incomingCount") + hasIncoming(incomingCount) { + return incomingCount && incomingCount > 0; + }, removeTopic(topic_id) { delete this.states["t" + topic_id]; @@ -124,7 +134,7 @@ const TopicTrackingState = Discourse.Model.extend({ if (Em.isEmpty(topics)) { return; } const states = this.states; - topics.forEach(function(t) { + topics.forEach(t => { const state = states['t' + t.get('id')]; if (state) { @@ -135,9 +145,7 @@ const TopicTrackingState = Discourse.Model.extend({ unread = postsCount - state.last_read_post_number; if (newPosts < 0) { newPosts = 0; } - if (!state.last_read_post_number) { - unread = 0; - } + if (!state.last_read_post_number) { unread = 0; } if (unread < 0) { unread = 0; } t.setProperties({ @@ -154,7 +162,7 @@ const TopicTrackingState = Discourse.Model.extend({ sync(list, filter) { const tracker = this, - states = tracker.states; + states = tracker.states; if (!list || !list.topics) { return; } @@ -166,8 +174,8 @@ const TopicTrackingState = Discourse.Model.extend({ if (filter === "new") { list.topics.splice(i, 1); } else { - list.topics[i].unseen = false; - list.topics[i].dont_sync = true; + list.topics[i].set('unseen', false); + list.topics[i].set('dont_sync', true); } } } @@ -198,14 +206,12 @@ const TopicTrackingState = Discourse.Model.extend({ }); // Correct missing states, safeguard in case message bus is corrupt - if((filter === "new" || filter === "unread") && !list.more_topics_url){ + if ((filter === "new" || filter === "unread") && !list.more_topics_url) { const ids = {}; - list.topics.forEach(function(r){ - ids["t" + r.id] = true; - }); + list.topics.forEach(r => ids["t" + r.id] = true); - _.each(tracker.states, function(v, k){ + _.each(tracker.states, (v, k) => { // we are good if we are on the list if (ids[k]) { return; } @@ -229,33 +235,28 @@ const TopicTrackingState = Discourse.Model.extend({ this.set("messageCount", this.get("messageCount") + 1); }, - countNew(category_id){ + countNew(category_id) { return _.chain(this.states) - .where(isNew) - .where(function(topic){ return topic.category_id === category_id || !category_id;}) - .value() - .length; - }, - - tooManyTracked() { - return this.initialStatesLength >= Discourse.SiteSettings.max_tracked_new_unread; + .where(isNew) + .where(topic => topic.category_id === category_id || !category_id) + .value() + .length; }, resetNew() { - const self = this; - Object.keys(this.states).forEach(function (id) { - if (self.states[id].last_read_post_number === null) { - delete self.states[id]; + Object.keys(this.states).forEach(id => { + if (this.states[id].last_read_post_number === null) { + delete this.states[id]; } }); }, - countUnread(category_id){ + countUnread(category_id) { return _.chain(this.states) - .where(isUnread) - .where(function(topic){ return topic.category_id === category_id || !category_id;}) - .value() - .length; + .where(isUnread) + .where(topic => topic.category_id === category_id || !category_id) + .value() + .length; }, countCategory(category_id) { @@ -269,54 +270,50 @@ const TopicTrackingState = Discourse.Model.extend({ return sum; }, - lookupCount(name, category){ - + lookupCount(name, category) { if (name === "latest") { return this.lookupCount("new", category) + this.lookupCount("unread", category); } let categoryName = category ? Em.get(category, "name") : null; - if(name === "new") { + if (name === "new") { return this.countNew(categoryName); - } else if(name === "unread") { + } else if (name === "unread") { return this.countUnread(categoryName); } else { categoryName = name.split("/")[1]; - if(categoryName) { + if (categoryName) { return this.countCategory(categoryName); } } }, loadStates(data) { - // not exposed const states = this.states; - - if(data) { - _.each(data,function(topic){ - states["t" + topic.topic_id] = topic; - }); + if (data) { + _.each(data,topic => states["t" + topic.topic_id] = topic); } } }); TopicTrackingState.reopenClass({ - createFromStates(data) { + createFromStates(data) { // TODO: This should be a model that does injection automatically const container = Discourse.__container__, messageBus = container.lookup('message-bus:main'), currentUser = container.lookup('current-user:main'), - instance = Discourse.TopicTrackingState.create({ messageBus, currentUser }); + instance = TopicTrackingState.create({ messageBus, currentUser }); instance.loadStates(data); instance.initialStatesLength = data && data.length; instance.establishChannels(); return instance; }, - current(){ + + current() { if (!this.tracker) { const data = PreloadStore.get('topicTrackingStates'); this.tracker = this.createFromStates(data); diff --git a/app/assets/javascripts/discourse/models/user-action.js.es6 b/app/assets/javascripts/discourse/models/user-action.js.es6 index 2d273c43e..f03d81908 100644 --- a/app/assets/javascripts/discourse/models/user-action.js.es6 +++ b/app/assets/javascripts/discourse/models/user-action.js.es6 @@ -1,5 +1,7 @@ import RestModel from 'discourse/models/rest'; import { url } from 'discourse/lib/computed'; +import { on } from 'ember-addons/ember-computed-decorators'; +import computed from 'ember-addons/ember-computed-decorators'; const UserActionTypes = { likes_given: 1, @@ -17,21 +19,22 @@ const UserActionTypes = { }; const InvertedActionTypes = {}; -_.each(UserActionTypes, function (k, v) { +_.each(UserActionTypes, (k, v) => { InvertedActionTypes[k] = v; }); const UserAction = RestModel.extend({ - _attachCategory: function() { + @on("init") + _attachCategory() { const categoryId = this.get('category_id'); if (categoryId) { this.set('category', Discourse.Category.findById(categoryId)); } - }.on('init'), + }, - descriptionKey: function() { - const action = this.get('action_type'); + @computed("action_type") + descriptionKey(action) { if (action === null || Discourse.UserAction.TO_SHOW.indexOf(action) >= 0) { if (this.get('isPM')) { return this.get('sameUser') ? 'sent_by_you' : 'sent_by_user'; @@ -59,34 +62,39 @@ const UserAction = RestModel.extend({ return this.get('targetUser') ? 'user_mentioned_you' : 'user_mentioned_user'; } } - }.property('action_type'), + }, - sameUser: function() { - return this.get('username') === Discourse.User.currentProp('username'); - }.property('username'), + @computed("username") + sameUser(username) { + return username === Discourse.User.currentProp('username'); + }, - targetUser: function() { - return this.get('target_username') === Discourse.User.currentProp('username'); - }.property('target_username'), + @computed("target_username") + targetUser(targetUsername) { + return targetUsername === Discourse.User.currentProp('username'); + }, presentName: Em.computed.any('name', 'username'), targetDisplayName: Em.computed.any('target_name', 'target_username'), actingDisplayName: Em.computed.any('acting_name', 'acting_username'), targetUserUrl: url('target_username', '/users/%@'), - usernameLower: function() { - return this.get('username').toLowerCase(); - }.property('username'), + @computed("username") + usernameLower(username) { + return username.toLowerCase(); + }, userUrl: url('usernameLower', '/users/%@'), - postUrl: function() { + @computed() + postUrl() { return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number')); - }.property(), + }, - replyUrl: function() { + @computed() + replyUrl() { return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('reply_to_post_number')); - }.property(), + }, replyType: Em.computed.equal('action_type', UserActionTypes.replies), postType: Em.computed.equal('action_type', UserActionTypes.posts), @@ -99,7 +107,7 @@ const UserAction = RestModel.extend({ postReplyType: Em.computed.or('postType', 'replyType'), removableBookmark: Em.computed.and('bookmarkType', 'sameUser'), - addChild: function(action) { + addChild(action) { let groups = this.get("childGroups"); if (!groups) { groups = { @@ -143,22 +151,21 @@ const UserAction = RestModel.extend({ "childGroups.edits.items", "childGroups.edits.items.@each", "childGroups.bookmarks.items", "childGroups.bookmarks.items.@each"), - switchToActing: function() { + switchToActing() { this.setProperties({ username: this.get('acting_username'), - uploaded_avatar_id: this.get('acting_uploaded_avatar_id'), name: this.get('actingDisplayName') }); } }); UserAction.reopenClass({ - collapseStream: function(stream) { + collapseStream(stream) { const uniq = {}; const collapsed = []; let pos = 0; - stream.forEach(function(item) { + stream.forEach(item => { const key = "" + item.topic_id + "-" + item.post_number; const found = uniq[key]; if (found === void 0) { diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 3f5941bc7..bd0e36c5f 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -1,11 +1,11 @@ import { url } from 'discourse/lib/computed'; import RestModel from 'discourse/models/rest'; -import avatarTemplate from 'discourse/lib/avatar-template'; import UserStream from 'discourse/models/user-stream'; import UserPostsStream from 'discourse/models/user-posts-stream'; import Singleton from 'discourse/mixins/singleton'; import { longDate } from 'discourse/lib/formatter'; import computed from 'ember-addons/ember-computed-decorators'; +import { observes } from 'ember-addons/ember-computed-decorators'; import Badge from 'discourse/models/badge'; import UserBadge from 'discourse/models/user-badge'; @@ -18,13 +18,15 @@ const User = RestModel.extend({ hasNotPosted: Em.computed.not("hasPosted"), canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"), - stream: function() { + @computed() + stream() { return UserStream.create({ user: this }); - }.property(), + }, - postsStream: function() { + @computed() + postsStream() { return UserPostsStream.create({ user: this }); - }.property(), + }, staff: Em.computed.or('admin', 'moderator'), @@ -32,27 +34,22 @@ const User = RestModel.extend({ return Discourse.ajax(`/session/${this.get('username')}`, { type: 'DELETE'}); }, - searchContext: function() { + @computed("username_lower") + searchContext(username) { return { type: 'user', - id: this.get('username_lower'), + id: username, user: this }; - }.property('username_lower'), + }, - /** - This user's display name. Returns the name if possible, otherwise returns the - username. - - @property displayName - @type {String} - **/ - displayName: function() { - if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(this.get('name'))) { - return this.get('name'); + @computed("username", "name") + displayName(username, name) { + if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(name)) { + return name; } - return this.get('username'); - }.property('username', 'name'), + return username; + }, @computed('profile_background') profileBackground(bgUrl) { @@ -60,44 +57,23 @@ const User = RestModel.extend({ return ('background-image: url(' + Discourse.getURLWithCDN(bgUrl) + ')').htmlSafe(); }, - /** - Path to this user. - - @property path - @type {String} - **/ - path: function(){ - return Discourse.getURL('/users/' + this.get('username_lower')); + @computed() + path() { // no need to observe, requires a hard refresh to update - }.property(), + return Discourse.getURL(`/users/${this.get('username_lower')}`); + }, - /** - Path to this user's administration - - @property adminPath - @type {String} - **/ adminPath: url('username_lower', "/admin/users/%@"), - /** - This user's username in lowercase. + @computed("username") + username_lower(username) { + return username.toLowerCase(); + }, - @property username_lower - @type {String} - **/ - username_lower: function() { - return this.get('username').toLowerCase(); - }.property('username'), - - /** - This user's trust level. - - @property trustLevel - @type {Integer} - **/ - trustLevel: function() { - return Discourse.Site.currentProp('trustLevels').findProperty('id', parseInt(this.get('trust_level'), 10)); - }.property('trust_level'), + @computed("trust_level") + trustLevel(trustLevel) { + return Discourse.Site.currentProp('trustLevels').findProperty('id', parseInt(trustLevel, 10)); + }, isBasic: Em.computed.equal('trust_level', 0), isLeader: Em.computed.equal('trust_level', 3), @@ -106,61 +82,36 @@ const User = RestModel.extend({ isSuspended: Em.computed.equal('suspended', true), - suspended: function() { - return this.get('suspended_till') && moment(this.get('suspended_till')).isAfter(); - }.property('suspended_till'), + @computed("suspended_till") + suspended(suspendedTill) { + return suspendedTill && moment(suspendedTill).isAfter(); + }, - suspendedTillDate: function() { - return longDate(this.get('suspended_till')); - }.property('suspended_till'), + @computed("suspended_till") + suspendedTillDate(suspendedTill) { + return longDate(suspendedTill); + }, - /** - Changes this user's username. - - @method changeUsername - @param {String} newUsername The user's new username - @returns Result of ajax call - **/ - changeUsername: function(newUsername) { - return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/username", { + changeUsername(new_username) { + return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/username`, { type: 'PUT', - data: { new_username: newUsername } + data: { new_username } }); }, - /** - Changes this user's email address. - - @method changeEmail - @param {String} email The user's new email address\ - @returns Result of ajax call - **/ - changeEmail: function(email) { - return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/email", { + changeEmail(email) { + return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/email`, { type: 'PUT', - data: { email: email } + data: { email } }); }, - /** - Returns a copy of this user. - - @method copy - @returns {User} - **/ - copy: function() { + copy() { return Discourse.User.create(this.getProperties(Ember.keys(this))); }, - /** - Save's this user's properties over AJAX via a PUT request. - - @method save - @returns {Promise} the result of the operation - **/ - save: function() { - const self = this, - data = this.getProperties( + save() { + const data = this.getProperties( 'auto_track_topics_after_msecs', 'bio_raw', 'website', @@ -185,10 +136,10 @@ const User = RestModel.extend({ 'card_background' ); - ['muted','watched','tracked'].forEach(function(s){ - var cats = self.get(s + 'Categories').map(function(c){ return c.get('id')}); + ['muted','watched','tracked'].forEach(s => { + let cats = this.get(s + 'Categories').map(c => c.get('id')); // HACK: denote lack of categories - if(cats.length === 0) { cats = [-1]; } + if (cats.length === 0) { cats = [-1]; } data[s + '_category_ids'] = cats; }); @@ -198,26 +149,19 @@ const User = RestModel.extend({ // TODO: We can remove this when migrated fully to rest model. this.set('isSaving', true); - return Discourse.ajax("/users/" + this.get('username_lower'), { + return Discourse.ajax(`/users/${this.get('username_lower')}`, { data: data, type: 'PUT' - }).then(function(result) { - self.set('bio_excerpt', result.user.bio_excerpt); - - const userProps = self.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); + }).then(result => { + this.set('bio_excerpt', result.user.bio_excerpt); + const userProps = this.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); Discourse.User.current().setProperties(userProps); }).finally(() => { this.set('isSaving', false); }); }, - /** - Changes the password and calls the callback function on AJAX.complete. - - @method changePassword - @returns {Promise} the result of the change password operation - **/ - changePassword: function() { + changePassword() { return Discourse.ajax("/session/forgot_password", { dataType: 'json', data: { login: this.get('username') }, @@ -225,73 +169,63 @@ const User = RestModel.extend({ }); }, - /** - Loads a single user action by id. - - @method loadUserAction - @param {Integer} id The id of the user action being loaded - @returns A stream of the user's actions containing the action of id - **/ - loadUserAction: function(id) { - var self = this, - stream = this.get('stream'); - return Discourse.ajax("/user_actions/" + id + ".json", { cache: 'false' }).then(function(result) { + loadUserAction(id) { + const stream = this.get('stream'); + return Discourse.ajax(`/user_actions/${id}.json`, { cache: 'false' }).then(result => { if (result && result.user_action) { - var ua = result.user_action; + const ua = result.user_action; - if ((self.get('stream.filter') || ua.action_type) !== ua.action_type) return; - if (!self.get('stream.filter') && !self.inAllStream(ua)) return; + if ((this.get('stream.filter') || ua.action_type) !== ua.action_type) return; + if (!this.get('stream.filter') && !this.inAllStream(ua)) return; - var action = Discourse.UserAction.collapseStream([Discourse.UserAction.create(ua)]); + const action = Discourse.UserAction.collapseStream([Discourse.UserAction.create(ua)]); stream.set('itemsLoaded', stream.get('itemsLoaded') + 1); stream.get('content').insertAt(0, action[0]); } }); }, - inAllStream: function(ua) { + inAllStream(ua) { return ua.action_type === Discourse.UserAction.TYPES.posts || ua.action_type === Discourse.UserAction.TYPES.topics; }, // The user's stat count, excluding PMs. - statsCountNonPM: function() { - var self = this; - + @computed("statsExcludingPms.@each.count") + statsCountNonPM() { if (Ember.isEmpty(this.get('statsExcludingPms'))) return 0; - var count = 0; - _.each(this.get('statsExcludingPms'), function(val) { - if (self.inAllStream(val)){ + let count = 0; + _.each(this.get('statsExcludingPms'), val => { + if (this.inAllStream(val)) { count += val.count; } }); return count; - }.property('statsExcludingPms.@each.count'), + }, // The user's stats, excluding PMs. - statsExcludingPms: function() { + @computed("stats.@each.isPM") + statsExcludingPms() { if (Ember.isEmpty(this.get('stats'))) return []; return this.get('stats').rejectProperty('isPM'); - }.property('stats.@each.isPM'), + }, - findDetails: function(options) { - var user = this; + findDetails(options) { + const user = this; - return PreloadStore.getAndRemove("user_" + user.get('username'), function() { - return Discourse.ajax("/users/" + user.get('username') + '.json', {data: options}); - }).then(function (json) { + return PreloadStore.getAndRemove(`user_${user.get('username')}`, () => { + return Discourse.ajax(`/users/${user.get('username')}.json`, { data: options }); + }).then(json => { if (!Em.isEmpty(json.user.stats)) { - json.user.stats = Discourse.User.groupStats(_.map(json.user.stats,function(s) { + json.user.stats = Discourse.User.groupStats(_.map(json.user.stats, s => { if (s.count) s.count = parseInt(s.count, 10); return Discourse.UserActionStat.create(s); })); } if (!Em.isEmpty(json.user.custom_groups)) { - json.user.custom_groups = json.user.custom_groups.map(function (g) { - return Discourse.Group.create(g); - }); + json.user.custom_groups = json.user.custom_groups.map(g => Discourse.Group.create(g)); } if (json.user.invited_by) { @@ -300,12 +234,10 @@ const User = RestModel.extend({ if (!Em.isEmpty(json.user.featured_user_badge_ids)) { const userBadgesMap = {}; - UserBadge.createFromJson(json).forEach(function(userBadge) { + UserBadge.createFromJson(json).forEach(userBadge => { userBadgesMap[ userBadge.get('id') ] = userBadge; }); - json.user.featured_user_badges = json.user.featured_user_badge_ids.map(function(id) { - return userBadgesMap[id]; - }); + json.user.featured_user_badges = json.user.featured_user_badge_ids.map(id => userBadgesMap[id]); } if (json.user.card_badge) { @@ -317,81 +249,62 @@ const User = RestModel.extend({ }); }, - findStaffInfo: function() { + findStaffInfo() { if (!Discourse.User.currentProp("staff")) { return Ember.RSVP.resolve(null); } - var self = this; - return Discourse.ajax("/users/" + this.get("username_lower") + "/staff-info.json").then(function(info) { - self.setProperties(info); + return Discourse.ajax(`/users/${this.get("username_lower")}/staff-info.json`).then(info => { + this.setProperties(info); }); }, - avatarTemplate: function() { - return avatarTemplate(this.get('username'), this.get('uploaded_avatar_id')); - }.property('uploaded_avatar_id', 'username'), - - /* - Change avatar selection - */ - pickAvatar: function(uploadId) { - var self = this; - - return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/avatar/pick", { + pickAvatar(upload_id, type, avatar_template) { + return Discourse.ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, { type: 'PUT', - data: { upload_id: uploadId } - }).then(function(){ - self.set('uploaded_avatar_id', uploadId); - }); + data: { upload_id, type } + }).then(() => this.setProperties({ + avatar_template, + uploaded_avatar_id: upload_id + })); }, - /** - Determines whether the current user is allowed to upload a file. - - @method isAllowedToUploadAFile - @param {String} type The type of the upload (image, attachment) - @returns true if the current user is allowed to upload a file - **/ - isAllowedToUploadAFile: function(type) { + isAllowedToUploadAFile(type) { return this.get('staff') || this.get('trust_level') > 0 || Discourse.SiteSettings['newuser_max_' + type + 's'] > 0; }, - /** - Invite a user to the site - - @method createInvite - @param {String} email The email address of the user to invite to the site - @returns {Promise} the result of the server call - **/ - createInvite: function(email, groupNames) { + createInvite(email, group_names) { return Discourse.ajax('/invites', { type: 'POST', - data: {email: email, group_names: groupNames} + data: { email, group_names } }); }, - generateInviteLink: function(email, groupNames, topicId) { + generateInviteLink(email, group_names, topic_id) { return Discourse.ajax('/invites/link', { type: 'POST', - data: {email: email, group_names: groupNames, topic_id: topicId} + data: { email, group_names, topic_id } }); }, - updateMutedCategories: function() { + @observes("muted_category_ids") + updateMutedCategories() { this.set("mutedCategories", Discourse.Category.findByIds(this.muted_category_ids)); - }.observes("muted_category_ids"), + }, - updateTrackedCategories: function() { + @observes("tracked_category_ids") + updateTrackedCategories() { this.set("trackedCategories", Discourse.Category.findByIds(this.tracked_category_ids)); - }.observes("tracked_category_ids"), + }, - updateWatchedCategories: function() { + @observes("watched_category_ids") + updateWatchedCategories() { this.set("watchedCategories", Discourse.Category.findByIds(this.watched_category_ids)); - }.observes("watched_category_ids"), + }, - canDeleteAccount: function() { - return !Discourse.SiteSettings.enable_sso && this.get('can_delete_account') && ((this.get('reply_count')||0) + (this.get('topic_count')||0)) <= 1; - }.property('can_delete_account', 'reply_count', 'topic_count'), + @computed("can_delete_account", "reply_count", "topic_count") + canDeleteAccount(canDeleteAccount, replyCount, topicCount) { + return !Discourse.SiteSettings.enable_sso && canDeleteAccount && ((replyCount || 0) + (topicCount || 0)) <= 1; + }, "delete": function() { if (this.get('can_delete_account')) { @@ -404,27 +317,26 @@ const User = RestModel.extend({ } }, - dismissBanner: function (bannerKey) { + dismissBanner(bannerKey) { this.set("dismissed_banner_key", bannerKey); - Discourse.ajax("/users/" + this.get('username'), { + Discourse.ajax(`/users/${this.get('username')}`, { type: 'PUT', data: { dismissed_banner_key: bannerKey } }); }, - checkEmail: function () { - var self = this; - return Discourse.ajax("/users/" + this.get("username_lower") + "/emails.json", { + checkEmail() { + return Discourse.ajax(`/users/${this.get("username_lower")}/emails.json`, { type: "PUT", data: { context: window.location.pathname } - }).then(function (result) { + }).then(result => { if (result) { - self.setProperties({ + this.setProperties({ email: result.email, associated_accounts: result.associated_accounts }); } - }, function () {}); + }); } }); @@ -432,14 +344,14 @@ const User = RestModel.extend({ User.reopenClass(Singleton, { // Find a `Discourse.User` for a given username. - findByUsername: function(username, options) { + findByUsername(username, options) { const user = User.create({username: username}); return user.findDetails(options); }, // TODO: Use app.register and junk Singleton - createCurrent: function() { - var userJson = PreloadStore.get('currentUser'); + createCurrent() { + const userJson = PreloadStore.get('currentUser'); if (userJson) { const store = Discourse.__container__.lookup('store:main'); return store.createRecord('user', userJson); @@ -447,56 +359,38 @@ User.reopenClass(Singleton, { return null; }, - /** - Checks if given username is valid for this email address - - @method checkUsername - @param {String} username A username to check - @param {String} email An email address to check - @param {Number} forUserId user id - provide when changing username - **/ - checkUsername: function(username, email, forUserId) { + checkUsername(username, email, for_user_id) { return Discourse.ajax('/users/check_username', { - data: { username: username, email: email, for_user_id: forUserId } + data: { username, email, for_user_id } }); }, - /** - Groups the user's statistics - - @method groupStats - @param {Array} stats Given stats - @returns {Object} - **/ - groupStats: function(stats) { - var responses = Discourse.UserActionStat.create({ + groupStats(stats) { + const responses = Discourse.UserActionStat.create({ count: 0, action_type: Discourse.UserAction.TYPES.replies }); - stats.filterProperty('isResponse').forEach(function (stat) { + stats.filterProperty('isResponse').forEach(stat => { responses.set('count', responses.get('count') + stat.get('count')); }); - var result = Em.A(); + const result = Em.A(); result.pushObjects(stats.rejectProperty('isResponse')); - var insertAt = 0; - result.forEach(function(item, index){ - if(item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts){ + let insertAt = 0; + result.forEach((item, index) => { + if (item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts) { insertAt = index + 1; } }); - if(responses.count > 0) { + if (responses.count > 0) { result.insertAt(insertAt, responses); } - return(result); + return result; }, - /** - Creates a new account - **/ - createAccount: function(attrs) { + createAccount(attrs) { return Discourse.ajax("/users", { data: { name: attrs.accountName, diff --git a/app/assets/javascripts/discourse/pre-initializers/dynamic-route-builders.js.es6 b/app/assets/javascripts/discourse/pre-initializers/dynamic-route-builders.js.es6 index a01c73def..812e2aed9 100644 --- a/app/assets/javascripts/discourse/pre-initializers/dynamic-route-builders.js.es6 +++ b/app/assets/javascripts/discourse/pre-initializers/dynamic-route-builders.js.es6 @@ -3,6 +3,7 @@ import buildTopicRoute from 'discourse/routes/build-topic-route'; import DiscoverySortableController from 'discourse/controllers/discovery-sortable'; export default { + after: 'inject-discourse-objects', name: 'dynamic-route-builders', initialize(container, app) { 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 59e7202b3..ed2577e23 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 @@ -5,6 +5,7 @@ import Store from 'discourse/models/store'; import DiscourseURL from 'discourse/lib/url'; import DiscourseLocation from 'discourse/lib/discourse-location'; import SearchService from 'discourse/services/search'; +import TopicTrackingState from 'discourse/models/topic-tracking-state'; function inject() { const app = arguments[0], @@ -30,6 +31,12 @@ export default { app.register('store:main', Store); inject(app, 'store', 'route', 'controller'); + app.register('message-bus:main', window.MessageBus, { instantiate: false }); + injectAll(app, 'messageBus'); + + app.register('topic-tracking-state:main', TopicTrackingState.current(), { instantiate: false }); + injectAll(app, 'topicTrackingState'); + const site = Discourse.Site.current(); app.register('site:main', site, { instantiate: false }); injectAll(app, 'site'); @@ -46,9 +53,6 @@ export default { app.register('current-user:main', Discourse.User.current(), { instantiate: false }); inject(app, 'currentUser', 'component', 'route', 'controller'); - app.register('message-bus:main', window.MessageBus, { instantiate: false }); - injectAll(app, 'messageBus'); - app.register('location:discourse-location', DiscourseLocation); const keyValueStore = new KeyValueStore("discourse_"); diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index 5944ccbd2..17023a74a 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -2,6 +2,7 @@ import { setting } from 'discourse/lib/computed'; import logout from 'discourse/lib/logout'; import showModal from 'discourse/lib/show-modal'; import OpenComposer from "discourse/mixins/open-composer"; +import Category from 'discourse/models/category'; function unlessReadOnly(method) { return function() { @@ -126,11 +127,12 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, { }, editCategory(category) { - const self = this; - Discourse.Category.reloadById(category.get('id')).then(function (model) { - self.site.updateCategory(model); + Category.reloadById(category.get('id')).then((atts) => { + const model = this.store.createRecord('category', atts.category); + model.setupGroupsAndPermissions(); + this.site.updateCategory(model); showModal('editCategory', { model }); - self.controllerFor('editCategory').set('selectedTab', 'general'); + this.controllerFor('editCategory').set('selectedTab', 'general'); }); }, 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 eaf26cc18..58f945ac9 100644 --- a/app/assets/javascripts/discourse/routes/build-category-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-category-route.js.es6 @@ -1,15 +1,15 @@ import { queryParams, filterQueryParams, findTopicList } from 'discourse/routes/build-topic-route'; // A helper function to create a category route with parameters -export default function(filter, params) { +export default (filter, params) => { return Discourse.Route.extend({ queryParams: queryParams, - model: function(modelParams) { + model(modelParams) { return Discourse.Category.findBySlug(modelParams.slug, modelParams.parentSlug); }, - afterModel: function(model, transition) { + afterModel(model, transition) { if (!model) { this.replaceWith('/404'); return; @@ -20,9 +20,9 @@ export default function(filter, params) { this._retrieveTopicList(model, transition)]); }, - _setupNavigation: function(model) { - var noSubcategories = params && !!params.no_subcategories, - filterMode = "c/" + Discourse.Category.slugFor(model) + (noSubcategories ? "/none" : "") + "/l/" + filter; + _setupNavigation(model) { + const noSubcategories = params && !!params.no_subcategories, + filterMode = `c/${Discourse.Category.slugFor(model)}${noSubcategories ? "/none" : ""}/l/${filter}`; this.controllerFor('navigation/category').setProperties({ category: model, @@ -32,42 +32,38 @@ export default function(filter, params) { }); }, - _createSubcategoryList: function(model) { + _createSubcategoryList(model) { this._categoryList = null; if (Em.isNone(model.get('parentCategory')) && Discourse.SiteSettings.show_subcategory_list) { - var self = this; - return Discourse.CategoryList.listForParent(model).then(function(list) { - self._categoryList = list; - }); + return Discourse.CategoryList.listForParent(this.store, model) + .then(list => this._categoryList = list); } // If we're not loading a subcategory list just resolve return Em.RSVP.resolve(); }, - _retrieveTopicList: function(model, transition) { - var listFilter = "c/" + Discourse.Category.slugFor(model) + "/l/" + filter, - self = this; + _retrieveTopicList(model, transition) { + const listFilter = `c/${Discourse.Category.slugFor(model)}/l/${filter}`, + findOpts = filterQueryParams(transition.queryParams, params), + extras = { cached: this.isPoppedState(transition) }; - var findOpts = filterQueryParams(transition.queryParams, params), - extras = { cached: this.isPoppedState(transition) }; - - return findTopicList(this.store, listFilter, findOpts, extras).then(function(list) { + return findTopicList(this.store, this.topicTrackingState, listFilter, findOpts, extras).then(list => { Discourse.TopicList.hideUniformCategory(list, model); - self.set('topics', list); + this.set('topics', list); }); }, - titleToken: function() { - var filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title', {count: 0}), - model = this.currentModel; + titleToken() { + const filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title', { count: 0 }), + model = this.currentModel; return I18n.t('filters.with_category', { filter: filterText, category: model.get('name') }); }, - setupController: function(controller, model) { - var topics = this.get('topics'), - periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''); + setupController(controller, model) { + const topics = this.get('topics'), + periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''); this.controllerFor('navigation/category').set('canCreateTopic', topics.get('can_create_topic')); this.controllerFor('discovery/topics').setProperties({ @@ -87,7 +83,7 @@ export default function(filter, params) { this.openTopicDraft(topics); }, - renderTemplate: function() { + renderTemplate() { this.render('navigation/category', { outlet: 'navigation-bar' }); if (this._categoryList) { @@ -96,13 +92,13 @@ export default function(filter, params) { this.render('discovery/topics', { controller: 'discovery/topics', outlet: 'list-container' }); }, - deactivate: function() { + deactivate() { this._super(); this.searchService.set('searchContext', null); }, actions: { - setNotification: function(notification_level){ + setNotification(notification_level) { this.currentModel.setNotification(notification_level); } } diff --git a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 index 185c2fd52..36d54fd84 100644 --- a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 @@ -12,9 +12,7 @@ function filterQueryParams(params, defaultParams) { return findOpts; } -function findTopicList(store, filter, filterParams, extras) { - const tracking = Discourse.TopicTrackingState.current(); - +function findTopicList(store, tracking, filter, filterParams, extras) { extras = extras || {}; return new Ember.RSVP.Promise(function(resolve) { @@ -40,7 +38,6 @@ function findTopicList(store, filter, filterParams, extras) { session.setProperties({topicList: null, topicListScrollPosition: null}); } - // Clean up any string parameters that might slip through filterParams = filterParams || {}; Ember.keys(filterParams).forEach(function(k) { @@ -50,17 +47,7 @@ function findTopicList(store, filter, filterParams, extras) { } }); - const findParams = {}; - Discourse.SiteSettings.top_menu.split('|').forEach(function (i) { - if (i.indexOf(filter) === 0) { - const exclude = i.split("-"); - if (exclude && exclude.length === 2) { - findParams.exclude_category = exclude[1]; - } - } - }); - return resolve(store.findFiltered('topicList', { filter, params:_.extend(findParams, filterParams || {})})); - + return resolve(store.findFiltered('topicList', { filter, params: filterParams || {} })); }).then(function(list) { list.set('listParams', filterParams); if (tracking) { @@ -88,7 +75,7 @@ export default function(filter, extras) { const findOpts = filterQueryParams(transition.queryParams), findExtras = { cached: this.isPoppedState(transition) }; - return findTopicList(this.store, filter, findOpts, findExtras); + return findTopicList(this.store, this.topicTrackingState, filter, findOpts, findExtras); }, titleToken() { diff --git a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 index 5bbd7aa66..072f33e47 100644 --- a/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 @@ -16,8 +16,8 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { // if default page is categories PreloadStore.remove("topic_list"); - return Discourse.CategoryList.list("categories").then(function(list) { - const tracking = Discourse.TopicTrackingState.current(); + return Discourse.CategoryList.list(this.store, 'categories').then(list => { + const tracking = this.topicTrackingState; if (tracking) { tracking.sync(list, "categories"); tracking.trackIncoming("categories"); @@ -34,9 +34,10 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { setupController(controller, model) { controller.set("model", model); - // Only show either the Create Category or Create Topic button - this.controllerFor("navigation/categories").set("canCreateCategory", model.get("can_create_category")); - this.controllerFor("navigation/categories").set("canCreateTopic", model.get("can_create_topic") && !model.get("can_create_category")); + this.controllerFor("navigation/categories").setProperties({ + canCreateCategory: model.get("can_create_category"), + canCreateTopic: model.get("can_create_topic"), + }); this.openTopicDraft(model); }, @@ -46,7 +47,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { const groups = this.site.groups, everyoneName = groups.findBy("id", 0).name; - const model = Discourse.Category.create({ + const model = this.store.createRecord('category', { color: "AB9364", text_color: "FFFFFF", group_permissions: [{group_name: everyoneName, permission_type: 1}], available_groups: groups.map(g => g.name), allow_badges: true @@ -56,12 +57,16 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { this.controllerFor("editCategory").set("selectedTab", "general"); }, + reorderCategories() { + showModal("reorderCategories"); + }, + createTopic() { this.openComposer(this.controllerFor("discovery/categories")); }, didTransition() { - this.controllerFor("application").set("showFooter", true); + Ember.run.next(() => this.controllerFor("application").set("showFooter", true)); return true; } } diff --git a/app/assets/javascripts/discourse/routes/full-page-search.js.es6 b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 index 482ebf1fc..3464250a7 100644 --- a/app/assets/javascripts/discourse/routes/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 @@ -1,14 +1,36 @@ -import { translateResults } from "discourse/lib/search-for-term"; +import { translateResults, getSearchKey, isValidSearchTerm } from "discourse/lib/search"; export default Discourse.Route.extend({ - queryParams: { q: {} }, + queryParams: { q: {}, context_id: {}, context: {} }, model(params) { + const router = Discourse.__container__.lookup('router:main'); + var cached = router.transientCache('lastSearch'); + var args = { q: params.q }; + if (params.context_id && !args.skip_context) { + args.search_context = { + type: params.context, + id: params.context_id + } + } + + const searchKey = getSearchKey(args); + + if (cached && cached.data.searchKey === searchKey) { + // extend expiry + router.transientCache('lastSearch', { searchKey, model: cached.data.model }, 5); + return cached.data.model; + } + return PreloadStore.getAndRemove("search", function() { - return Discourse.ajax("/search", { data: { q: params.q } }); + if (isValidSearchTerm(params.q)) { + return Discourse.ajax("/search", { data: args }); + } else { + return null; + } }).then(results => { - const model = translateResults(results) || {}; - model.q = params.q; + const model = (results && translateResults(results)) || {}; + router.transientCache('lastSearch', { searchKey, model }, 5); return model; }); }, diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index d748689f4..3ae6aed00 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -18,50 +18,52 @@ export default RestrictedUserRoute.extend({ showModal('avatar-selector'); // all the properties needed for displaying the avatar selector modal - const controller = this.controllerFor('avatar-selector'), - props = this.modelFor('user').getProperties( + const props = this.modelFor('user').getProperties( 'id', 'email', 'username', - 'uploaded_avatar_id', + 'avatar_template', + 'system_avatar_template', + 'gravatar_avatar_template', + 'custom_avatar_template', 'system_avatar_upload_id', 'gravatar_avatar_upload_id', 'custom_avatar_upload_id' ); - switch (props.uploaded_avatar_id) { - case props.system_avatar_upload_id: + switch (props.avatar_template) { + case props.system_avatar_template: props.selected = "system"; break; - case props.gravatar_avatar_upload_id: + case props.gravatar_avatar_template: props.selected = "gravatar"; break; default: props.selected = "uploaded"; } - controller.setProperties(props); + this.controllerFor('avatar-selector').setProperties(props); }, saveAvatarSelection() { const user = this.modelFor('user'), - avatarSelector = this.controllerFor('avatar-selector'); + controller = this.controllerFor('avatar-selector'), + selectedUploadId = controller.get("selectedUploadId"), + selectedAvatarTemplate = controller.get("selectedAvatarTemplate"), + type = controller.get("selected"); - // sends the information to the server if it has changed - if (avatarSelector.get('selectedUploadId') !== user.get('uploaded_avatar_id')) { - user.pickAvatar(avatarSelector.get('selectedUploadId')) - .then(() => { - user.setProperties(avatarSelector.getProperties( - 'system_avatar_upload_id', - 'gravatar_avatar_upload_id', - 'custom_avatar_upload_id' - )); - bootbox.alert(I18n.t("user.change_avatar.cache_notice")); - }); - } + user.pickAvatar(selectedUploadId, type, selectedAvatarTemplate) + .then(() => { + user.setProperties(controller.getProperties( + 'system_avatar_template', + 'gravatar_avatar_template', + 'custom_avatar_template' + )); + bootbox.alert(I18n.t("user.change_avatar.cache_notice")); + }); // saves the data back - avatarSelector.send('closeModal'); + controller.send('closeModal'); }, } diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 5c9a97733..759ec4bbb 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -56,7 +56,7 @@ const TopicRoute = Discourse.Route.extend({ }, showAutoClose() { - showModal('edit-topic-auto-close', { model: this.modelFor('topic'), title: 'topic.auto_close_title' }); + showModal('edit-topic-auto-close', { model: this.modelFor('topic') }); this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal'); }, @@ -216,7 +216,7 @@ const TopicRoute = Discourse.Route.extend({ this.controllerFor('topic-admin-menu').set('model', model); this.controllerFor('composer').set('topic', model); - Discourse.TopicTrackingState.current().trackIncoming('all'); + this.topicTrackingState.trackIncoming('all'); controller.subscribe(); this.controllerFor('topic-progress').set('model', model); diff --git a/app/assets/javascripts/discourse/templates/components/auto-close-form.hbs b/app/assets/javascripts/discourse/templates/components/auto-close-form.hbs index 4234d0673..34d53e2f7 100644 --- a/app/assets/javascripts/discourse/templates/components/auto-close-form.hbs +++ b/app/assets/javascripts/discourse/templates/components/auto-close-form.hbs @@ -12,7 +12,7 @@ </div> <div> <label> - {{input type="checkbox" name="autoCloseBasedOnLastPost" checked=autoCloseBasedOnLastPost}} + {{input type="checkbox" checked=autoCloseBasedOnLastPost}} {{i18n 'composer.auto_close.based_on_last_post'}} </label> </div> diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs index 02361e3cf..1b323792a 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs @@ -1,4 +1,7 @@ <section class='field'> + {{#if category.is_special}} + <p class="warning">{{i18n 'category.special_warning'}}</p> + {{/if}} <ul class='permission-list'> {{#each category.permissions as |p|}} <li> @@ -16,6 +19,8 @@ {{view 'select' class="permission-selector" optionValuePath="content.id" optionLabelPath="content.description" content=category.availablePermissions value=selectedPermission}} <button {{action "addPermission" selectedGroup selectedPermission}} class="btn btn-small">{{i18n 'category.add_permission'}}</button> {{else}} - <button {{action "editPermissions"}} class="btn btn-small">{{i18n 'category.edit_permissions'}}</button> + {{#unless category.is_special}} + <button {{action "editPermissions"}} class="btn btn-small">{{i18n 'category.edit_permissions'}}</button> + {{/unless}} {{/if}} </section> diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs index 9dab35610..722a9f5e1 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs @@ -1,47 +1,61 @@ <section class='field'> {{auto-close-form autoCloseTime=category.auto_close_hours autoCloseBasedOnLastPost=category.auto_close_based_on_last_post + autoCloseExamples="" limited="true" }} </section> <section class='field'> - <div class="allow-badges"> - <div> - {{input type="checkbox" checked=category.allow_badges}} - {{i18n 'category.allow_badges_label'}} - </div> - </div> + <label> + {{input type="checkbox" checked=category.allow_badges}} + {{i18n 'category.allow_badges_label'}} + </label> </section> -<section class='field'> - <div class="email-in-fields"> - {{#if emailInEnabled}} - <div> - <i class="fa fa-envelope-o"></i> - {{i18n 'category.email_in'}} - {{text-field value=category.email_in}} - </div> - <div> - <label class="checkbox-label"> - {{input type="checkbox" checked=category.email_in_allow_strangers}} - {{i18n 'category.email_in_allow_strangers'}} - </label> - </div> - {{else}} - {{i18n 'category.email_in_disabled'}} - <a href="/admin/site_settings/category/email">{{i18n 'category.email_in_disabled_click'}}</a> - {{/if}} - </div> +<section class="field"> + <label> + {{input type="checkbox" checked=category.suppress_from_homepage}} + {{i18n "category.suppress_from_homepage"}} + </label> </section> -<section class='field'> - {{#if showPositionInput}} - <label>{{i18n 'category.position'}}</label> - {{text-field value=category.position class="position-input"}} - {{else}} +{{#if emailInEnabled}} + <section class='field'> + <label> + {{input type="checkbox" checked=category.email_in_allow_strangers}} + {{i18n 'category.email_in_allow_strangers'}} + </label> + </section> + <section class='field'> + <label> + {{fa-icon "envelope-o"}} + {{i18n 'category.email_in'}} + {{text-field value=category.email_in}} + </label> + </section> +{{/if}} + +{{#if showPositionInput}} + <section class='field'> + <label> + {{i18n 'category.position'}} + {{text-field value=category.position class="position-input"}} + </label> + </section> +{{/if}} + +{{#unless emailInEnabled}} + <section class='field'> + {{i18n 'category.email_in_disabled'}} + <a href="/admin/site_settings/category/email">{{i18n 'category.email_in_disabled_click'}}</a> + </section> +{{/unless}} + +{{#unless showPositionInput}} + <section class='field'> {{i18n 'category.position_disabled'}} <a href="/admin/site_settings/category/basic">{{i18n 'category.position_disabled_click'}}</a> - {{/if}} -</section> + </section> +{{/unless}} {{plugin-outlet "category-custom-settings"}} diff --git a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs b/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs index 098d31015..fe7050955 100644 --- a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs +++ b/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs @@ -1,73 +1,96 @@ {{#menu-panel visible=visible}} - <ul class="menu-links"> - {{#if currentUser.staff}} - <li> - {{#link-to "admin" class="admin-link"}} - {{fa-icon "wrench"}} {{i18n 'admin_title'}} - {{/link-to}} + {{#if prioritizeFaq}} + {{#menu-links}} + <li class='heading'> + {{#d-link path=faqUrl class="faq-link"}} + {{i18n "faq"}} + <span class='badge badge-notification'>{{i18n "new_item"}}</span> + {{/d-link}} </li> + {{/menu-links}} + {{/if}} + + {{#if currentUser.staff}} + {{#menu-links}} + <li>{{d-link route="admin" class="admin-link" icon="wrench" label="admin_title"}}</li> <li> - {{#link-to "adminFlags" class="flagged-posts-link"}} + {{#d-link route="adminFlags" class="flagged-posts-link"}} {{fa-icon "flag"}} {{i18n 'flags_title'}} {{#if currentUser.site_flagged_posts_count}} <span title={{i18n 'notifications.total_flagged'}} class='badge-notification flagged-posts'>{{currentUser.site_flagged_posts_count}}</span> {{/if}} - {{/link-to}} + {{/d-link}} + </li> + + {{#if currentUser.show_queued_posts}} + <li> + {{#d-link route='queued-posts'}} + {{i18n "queue.title"}} + {{#if currentUser.post_queue_new_count}} + <span class='badge-notification flagged-posts'>{{currentUser.post_queue_new_count}}</span> + {{/if}} + {{/d-link}} + </li> + {{/if}} + <li>{{d-link route="adminSiteSettings" icon="gear" label="admin.site_settings.title"}}</li> + + {{plugin-outlet "hamburger-admin"}} + {{/menu-links}} + {{/if}} + + {{#menu-links}} + <li>{{d-link route="discovery.latest" class="latest-topics-link" label="filters.latest.title.zero"}}</li> + + {{#if currentUser}} + <li> + {{d-link route="discovery.new" class="new-topics-link" label="filters.new.title" count=newCount}} + </li> + <li> + {{d-link route="discovery.unread" class="unread-topics-link" label="filters.unread.title" count=unreadCount}} </li> {{/if}} - <li> - {{#link-to "discovery.latest" class="latest-topics-link"}} - {{i18n 'filters.latest.title.zero'}} - {{/link-to}} - </li> + <li>{{d-link route="discovery.top" class="top-topics-link" label="filters.top.title"}}</li> + {{#if siteSettings.enable_badges}} - <li> - {{#link-to 'badges' class="badge-link"}}{{i18n 'badges.title'}}{{/link-to}} - </li> + <li>{{d-link route="badges" class="badge-link" label="badges.title"}}</li> {{/if}} {{#if siteSettings.enable_user_directory}} - <li>{{#link-to 'users' class="user-directory-link"}}{{i18n "directory.title"}}{{/link-to}}</li> - {{/if}} - - {{#if currentUser.show_queued_posts}} - <li> - {{#link-to 'queued-posts'}} - {{i18n "queue.title"}} - {{#if currentUser.post_queue_new_count}} - <span class='badge-notification flagged-posts'>{{currentUser.post_queue_new_count}}</span> - {{/if}} - {{/link-to}} - </li> + <li>{{d-link route="users" class="user-directory-link" label="directory.title"}}</li> {{/if}} {{plugin-outlet "site-map-links"}} - {{#if showKeyboardShortcuts}} - <li><a href {{action "keyboardShortcuts"}} class="keyboard-shortcuts-link">{{i18n 'keyboard_shortcuts_help.title'}}</a></li> - {{/if}} - <li> - <a href={{faqUrl}} class="faq-link">{{i18n 'faq'}}</a> - </li> - <li> - {{#link-to 'about' class="about-link"}}{{i18n 'about.simple_title'}}{{/link-to}} - </li> - {{#if showMobileToggle}} - <li><a href class="mobile-toggle-link" {{action "toggleMobileView"}}>{{boundI18n mobileViewLinkTextKey}}</a></li> - {{/if}} - {{plugin-outlet "site-map-links-last"}} - </ul> + {{/menu-links}} {{#if categories}} <ul class="category-links clearfix"> - <li class="heading" title={{i18n 'filters.categories.help'}}> - {{#link-to "discovery.categories" class="categories-link"}}{{i18n 'filters.categories.title'}}{{/link-to}} + <li class='heading'> + {{d-link class="heading" + route="discovery.categories" + class="categories-link" + label="filters.categories.title"}} </li> - {{#each categories as |c|}} {{hamburger-category category=c}} {{/each}} </ul> {{/if}} + <hr> + + {{#menu-links omitRule="true"}} + <li>{{d-link route="about" class="about-link" label="about.simple_title"}}</li> + {{#unless prioritizeFaq}} + <li>{{d-link path=faqUrl class="faq-link" label="faq"}}</li> + {{/unless}} + + {{#if showKeyboardShortcuts}} + <li>{{d-link action="keyboardShortcuts" class="keyboard-shortcuts-link" label="keyboard_shortcuts_help.title"}}</li> + {{/if}} + + {{#if showMobileToggle}} + <li>{{d-link action="toggleMobileView" class="mobile-toggle-link" label=mobileViewLinkTextKey}}</li> + {{/if}} + {{/menu-links}} {{/menu-panel}} diff --git a/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs b/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs index 732e31e06..6cb4af742 100644 --- a/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs +++ b/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs @@ -1,4 +1,4 @@ -<a {{action "toggle"}} class='icon' href title={{i18n title}} aria-label={{i18n title}} id={{iconId}}> +<a {{action "toggle"}} class='icon' href={{href}} title={{i18n title}} aria-label={{i18n title}} id={{iconId}}> {{#if showUser}} {{bound-avatar currentUser "medium"}} {{else}} diff --git a/app/assets/javascripts/discourse/templates/components/menu-links.hbs b/app/assets/javascripts/discourse/templates/components/menu-links.hbs new file mode 100644 index 000000000..fe9b872fc --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/menu-links.hbs @@ -0,0 +1,7 @@ +<ul class="menu-links columned"> + {{yield}} + <div class="clearfix"></div> +</ul> +{{#unless omitRule}} + <hr> +{{/unless}} diff --git a/app/assets/javascripts/discourse/templates/components/stream-item.hbs b/app/assets/javascripts/discourse/templates/components/stream-item.hbs index c84082519..22900bbb2 100644 --- a/app/assets/javascripts/discourse/templates/components/stream-item.hbs +++ b/app/assets/javascripts/discourse/templates/components/stream-item.hbs @@ -23,7 +23,7 @@ {{fa-icon 'times'}} {{i18n "bookmarks.remove"}} </button> {{else}} - <a href={{grandChild.userUrl}} data-user-card={{grandChild.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a> + <a href={{grandChild.userUrl}} data-user-card={{grandChild.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true" avatarTemplatePath="acting_avatar_template"}}</div></a> {{#if grandChild.edit_reason}} — <span class="edit-reason">{{grandChild.edit_reason}}</span>{{/if}} {{/if}} {{/each}} diff --git a/app/assets/javascripts/discourse/templates/components/topic-map.hbs b/app/assets/javascripts/discourse/templates/components/topic-map.hbs index b851f03bc..915c3f0e7 100644 --- a/app/assets/javascripts/discourse/templates/components/topic-map.hbs +++ b/app/assets/javascripts/discourse/templates/components/topic-map.hbs @@ -14,7 +14,7 @@ </a> </li> <li> - <a {{bind-attr href="topic.lastPostUrl"}}> + <a href={{topic.lastPostUrl}}> <h4>{{i18n 'last_reply_lowercase'}}</h4> {{avatar details.last_poster imageSize="tiny"}} {{format-date topic.last_posted_at}} diff --git a/app/assets/javascripts/discourse/templates/components/topic-participant.hbs b/app/assets/javascripts/discourse/templates/components/topic-participant.hbs index 8233edb58..dc8f810b0 100644 --- a/app/assets/javascripts/discourse/templates/components/topic-participant.hbs +++ b/app/assets/javascripts/discourse/templates/components/topic-participant.hbs @@ -1,4 +1,4 @@ -<a href='#' {{bind-attr class=":poster toggled"}} {{action "toggle"}} title="{{unbound participant.username}}"> +<a href {{bind-attr class=":poster toggled"}} {{action "toggle"}} title="{{unbound participant.username}}"> {{#if showPostCount}} <span class='post-count'>{{unbound participant.post_count}}</span> {{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/user-menu.hbs b/app/assets/javascripts/discourse/templates/components/user-menu.hbs index 213366641..0fa99d740 100644 --- a/app/assets/javascripts/discourse/templates/components/user-menu.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-menu.hbs @@ -4,11 +4,13 @@ <li>{{d-link route='user' model=currentUser class="user-activity-link" icon="user" label="user.profile"}}</li> {{#if showDisableAnon}} - <li>{{d-link action="toggleAnon" label="switch_from_anon"}}</li> + <li>{{d-link action="toggleAnon" label="switch_from_anon"}}</li> {{/if}} <li class='glyphs'> {{d-link path=bookmarksPath title="user.bookmarks" icon="bookmark"}} - {{d-link path=messagesPath title="user.private_messages" icon="envelope"}} + {{#if siteSettings.enable_private_messages}} + {{d-link path=messagesPath title="user.private_messages" icon="envelope"}} + {{/if}} {{#if showEnableAnon}} {{d-link action="toggleAnon" title="switch_to_anon" icon="user-secret"}} {{/if}} @@ -25,7 +27,7 @@ {{#each notifications as |n|}} {{notification-item notification=n}} {{/each}} - <li class="read last"> + <li class="read last heading"> {{#d-link path=notificationsPath}} {{i18n 'notifications.more'}}… {{/d-link}} @@ -37,6 +39,8 @@ {{plugin-outlet "user-menu-bottom"}} {{#if siteSettings.show_logout_in_header}} <hr> - {{d-link action="logout" class="logout" icon="sign-out" label="user.log_out"}} + <ul class='menu-links'> + <li>{{d-link action="logout" class="logout" icon="sign-out" label="user.log_out"}}</li> + </ul> {{/if}} {{/menu-panel}} diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index cbbe7653f..01234ff0e 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -48,7 +48,7 @@ {{popup-input-tip validation=view.titleValidation shownAt=view.showTitleTip}} </div> - {{#unless model.privateMessage}} + {{#if model.showCategoryChooser}} <div class="category-input"> {{category-chooser valueAttribute="id" value=model.categoryId scopedCategoryId=scopedCategoryId tabindex="3"}} {{popup-input-tip validation=view.categoryValidation shownAt=view.showCategoryTip}} @@ -57,9 +57,19 @@ <button class='btn' {{action "showOptions"}}>{{i18n 'topic.options'}}</button> {{/if}} {{render "additional-composer-buttons" model}} - {{/unless}} + {{/if}} </div> {{/if}} + + {{#if canWhisper}} + <div class='form-element clearfix'> + <label> + {{input type="checkbox" checked=model.whisper tabindex="3"}} + {{i18n "composer.add_whisper"}} + </label> + </div> + {{/if}} + {{plugin-outlet "composer-fields"}} </div> @@ -93,7 +103,7 @@ <div class='submit-panel'> {{plugin-outlet "composer-fields-below"}} <button {{action "save"}} tabindex="5" {{bind-attr class=":btn :btn-primary :create disableSubmit:disabled"}} title="{{i18n 'composer.title'}}">{{{model.saveIcon}}}{{model.saveText}}</button> - <a href='#' {{action "cancel"}} class='cancel' tabindex="6">{{i18n 'cancel'}}</a> + <a href {{action "cancel"}} class='cancel' tabindex="6">{{i18n 'cancel'}}</a> </div> {{/if}} diff --git a/app/assets/javascripts/discourse/templates/composer/education.hbs b/app/assets/javascripts/discourse/templates/composer/education.hbs index bb818e700..b59da973c 100644 --- a/app/assets/javascripts/discourse/templates/composer/education.hbs +++ b/app/assets/javascripts/discourse/templates/composer/education.hbs @@ -1,2 +1,2 @@ -<a href='#' {{action "closeMessage" this}} class='close'><i class='fa fa-times-circle'></i></a> +<a href {{action "closeMessage" this}} class='close'><i class='fa fa-times-circle'></i></a> {{{body}}} diff --git a/app/assets/javascripts/discourse/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/discovery/topics.hbs index e95136eac..f3e452a4a 100644 --- a/app/assets/javascripts/discourse/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/topics.hbs @@ -1,7 +1,3 @@ -{{#if tooManyTracked}} - <div class="alert alert-info">{{i18n 'topics.too_many_tracked'}}</div> -{{/if}} - {{#if redirectedReason}} <div class="alert alert-info">{{redirectedReason}}</div> {{/if}} diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index 832e68148..8f1874345 100644 --- a/app/assets/javascripts/discourse/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -1,6 +1,6 @@ <div class="search row clearfix"> - {{input type="text" value=searchTerm class="input-xxlarge search no-blur" action="search"}} - {{d-button action="search" icon="search" class="btn-primary"}} + {{search-text-field value=searchTerm class="input-xxlarge search no-blur" action="search" hasAutofocus=hasAutofocus}} + {{d-button action="search" icon="search" class="btn-primary" disabled=isNotValidSearchTerm}} {{#if canBulkSelect}} {{#if model.posts}} {{d-button icon="list" class="bulk-select" title="topics.bulk.toggle" action="toggleBulkSelect"}} @@ -9,11 +9,31 @@ {{/if}} </div> +{{#if model.posts}} + {{#if bulkSelectEnabled}} + <div class='fps-select'> + <a href {{action "selectAll"}}>{{i18n "search.select_all"}}</a> + <a href {{action "clearAll"}}>{{i18n "search.clear_all"}}</a> + </div> + {{/if}} +{{/if}} + +{{#if context}} +<div class='fps-search-context'> +<label> + {{input type="checkbox" name="searchContext" checked=searchContextEnabled}} {{searchContextDescription}} +</label> +</div> +{{/if}} + {{#conditional-loading-spinner condition=loading}} {{#unless model.posts}} <h3> - {{i18n "search.no_results"}} <a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "search.search_help"}}</a> + {{#if searchActive}} + {{i18n "search.no_results"}} + {{/if}} + <a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "search.search_help"}}</a> </h3> {{/unless}} diff --git a/app/assets/javascripts/discourse/templates/header.hbs b/app/assets/javascripts/discourse/templates/header.hbs index d09c4209f..ab92b1951 100644 --- a/app/assets/javascripts/discourse/templates/header.hbs +++ b/app/assets/javascripts/discourse/templates/header.hbs @@ -1,7 +1,3 @@ -{{user-menu visible=userMenuVisible logoutAction="logout"}} -{{hamburger-menu visible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}} -{{search-menu visible=searchVisible}} - <div class='wrap'> <div class='contents clearfix'> {{home-logo minimized=showExtraInfo}} @@ -22,6 +18,7 @@ {{#header-dropdown iconId="search-button" icon="search" toggleVisible=searchVisible + mobileAction="fullPageSearch" loginAction="showLogin" title="search.title"}} {{/header-dropdown}} @@ -44,15 +41,18 @@ loginAction="showLogin" title="user.avatar.header_title"}} {{#if currentUser.unread_notifications}} - <a href class='badge-notification unread-notifications'>{{currentUser.unread_notifications}}</a> + <a href {{action "showUserMenu"}} class='badge-notification unread-notifications'>{{currentUser.unread_notifications}}</a> {{/if}} {{#if currentUser.unread_private_messages}} - <a href class='badge-notification unread-private-messages'>{{currentUser.unread_private_messages}}</a> + <a href {{action "showUserMenu"}} class='badge-notification unread-private-messages'>{{currentUser.unread_private_messages}}</a> {{/if}} {{/header-dropdown}} {{/if}} - {{plugin-outlet "header-before-dropdowns"}} </ul> + {{plugin-outlet "header-before-dropdowns"}} + {{user-menu visible=userMenuVisible logoutAction="logout"}} + {{hamburger-menu visible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}} + {{search-menu visible=searchVisible}} </div> {{#if showExtraInfo}} diff --git a/app/assets/javascripts/discourse/templates/list/post-count-or-badges.raw.hbs b/app/assets/javascripts/discourse/templates/list/post-count-or-badges.raw.hbs new file mode 100644 index 000000000..88dfe8f00 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/list/post-count-or-badges.raw.hbs @@ -0,0 +1,8 @@ +{{#if view.showBadges}} + {{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}} +{{else}} + {{#if topic.unseen}} + <span class="badge-notification new-topic"></span> + {{/if}} + {{raw "list/posts-count-column" topic=topic tagName="div"}} +{{/if}} \ No newline at end of file diff --git a/app/assets/javascripts/discourse/templates/list/posters-column.raw.hbs b/app/assets/javascripts/discourse/templates/list/posters-column.raw.hbs index 1b837fb5a..5adbfd3d7 100644 --- a/app/assets/javascripts/discourse/templates/list/posters-column.raw.hbs +++ b/app/assets/javascripts/discourse/templates/list/posters-column.raw.hbs @@ -1,5 +1,5 @@ <td class='posters'> {{#each poster in posters}} -<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extras}}">{{avatar poster usernamePath="user.username" imageSize="small"}}</a> +<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extras}}">{{avatar poster avatarTemplatePath="user.avatar_template" usernamePath="user.username" imageSize="small"}}</a> {{/each}} </td> diff --git a/app/assets/javascripts/discourse/templates/mobile/components/basic-topic-list.hbs b/app/assets/javascripts/discourse/templates/mobile/components/basic-topic-list.hbs index 2dfeb9ad4..06af1df5d 100644 --- a/app/assets/javascripts/discourse/templates/mobile/components/basic-topic-list.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/components/basic-topic-list.hbs @@ -5,13 +5,9 @@ {{#each t in topics}} <tr {{bind-attr class="t.archived"}}> <td> - <div class='main-link clearfix'> + <div class='main-link'> {{topic-status topic=t}} {{topic-link t}} - {{topic-post-badges unread=t.unread - newPosts=t.new_posts - unseen=t.unseen - url=t.lastUnreadUrl}} {{#if t.hasExcerpt}} <div class="topic-excerpt"> @@ -20,15 +16,19 @@ {{#unless t.canClearPin}}<a href="{{unbound t.url}}">{{i18n 'read_more'}}</a>{{/unless}} {{/if}} {{#if t.canClearPin}} - <a href="#" {{action "clearPin" t}} title="{{i18n 'topic.clear_pin.help'}}">{{i18n 'topic.clear_pin.title'}}</a> + <a href {{action "clearPin" t}} title="{{i18n 'topic.clear_pin.help'}}">{{i18n 'topic.clear_pin.title'}}</a> {{/if}} </div> {{/if}} </div> + <div class='pull-right'> + {{raw "list/post-count-or-badges" topic=t postBadgesEnabled="true"}} + </div> + <div class='clearfix'></div> <div class="topic-item-stats clearfix"> <div class="pull-right"> - {{raw "list/posts-count-column" topic=t tagName="div"}} {{raw "list/activity-column" topic=t tagName="div" class="num activity last"}} + <a href="{{t.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date t.bumped_at}}}'>{{t.last_poster_username}}</a> </div> {{#unless controller.hideCategory}} <div class='category'> diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs index 349cabd1a..dc7db78da 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs @@ -30,7 +30,7 @@ {{#unless t.canClearPin}}<a href="{{unbound t.url}}">{{i18n 'read_more'}}</a>{{/unless}} {{/if}} {{#if t.canClearPin}} - <a href="#" {{action "clearPin" t}} title="{{unbound i18n topic.clear_pin.help}}">{{i18n 'topic.clear_pin.title'}}</a> + <a href {{action "clearPin" t}} title="{{unbound i18n topic.clear_pin.help}}">{{i18n 'topic.clear_pin.title'}}</a> {{/if}} </div> {{/if}} @@ -60,7 +60,7 @@ <figure title="{{i18n 'week_desc'}}">{{number c.topics_week}} <figcaption>{{i18n 'week'}}</figcaption></figure> {{#if controller.canEdit}} - <a href='#' {{action "editCategory" c}} class='btn btn-small'>{{i18n 'category.edit'}}</a> + <a href {{action "editCategory" c}} class='btn btn-small'>{{i18n 'category.edit'}}</a> {{/if}} </footer> diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs index 7a02b2a77..22cbceea9 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs @@ -38,7 +38,7 @@ </div> <h3> {{footerMessage}} - {{#if model.can_create_topic}}<a href='#' {{action "createTopic"}}>{{i18n 'topic.suggest_create_topic'}}</a>{{/if}} + {{#if model.can_create_topic}}<a href {{action "createTopic"}}>{{i18n 'topic.suggest_create_topic'}}</a>{{/if}} </h3> {{else}} {{#if top}} diff --git a/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs b/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs index 972acfa16..aa4370c96 100644 --- a/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs @@ -1,12 +1,13 @@ <td> -<div class='main-link clearfix'> +<div class='main-link'> {{raw "topic-status" topic=content}} {{topic-link content}} - {{#if controller.showTopicPostBadges}} - {{raw "topic-post-badges" unread=content.unread newPosts=content.displayNewPosts unseen=content.unseen url=content.lastUnreadUrl}} - {{/if}} {{raw "list/topic-excerpt" topic=content}} </div> +<div class='pull-right'> + {{raw "list/post-count-or-badges" topic=content postBadgesEnabled=controller.showTopicPostBadges}} +</div> +<div class="clearfix"></div> <div class="topic-item-stats clearfix"> {{#unless controller.hideCategory}} @@ -17,10 +18,9 @@ {{plugin-outlet "topic-list-tags"}} <div class="pull-right"> - {{raw "list/posts-count-column" topic=content tagName="div"}} <div class='num activity last'> - <a href="{{content.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date content.bumped_at}}}'>{{content.last_poster_username}}</a> {{raw "list/activity-column" topic=content tagName="span" class="age"}} + <a href="{{content.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date content.bumped_at}}}'>{{content.last_poster_username}}</a> </div> </div> <div class="clearfix"></div> diff --git a/app/assets/javascripts/discourse/templates/modal/avatar_selector.hbs b/app/assets/javascripts/discourse/templates/modal/avatar_selector.hbs index 12f0309d6..e7d070d81 100644 --- a/app/assets/javascripts/discourse/templates/modal/avatar_selector.hbs +++ b/app/assets/javascripts/discourse/templates/modal/avatar_selector.hbs @@ -2,32 +2,27 @@ <div> <div> <input type="radio" id="system-avatar" name="avatar" value="system" {{action "useSystem"}}> - <label class="radio" for="system-avatar">{{bound-avatar controller "large" system_avatar_upload_id}} {{{i18n 'user.change_avatar.letter_based'}}}</label> + <label class="radio" for="system-avatar">{{bound-avatar-template system_avatar_template "large"}} {{{i18n 'user.change_avatar.letter_based'}}}</label> </div> <div> <input type="radio" id="gravatar" name="avatar" value="gravatar" {{action "useGravatar"}}> - <label class="radio" for="gravatar">{{bound-avatar controller "large" gravatar_avatar_upload_id}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label> + <label class="radio" for="gravatar">{{bound-avatar-template gravatar_avatar_template "large"}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label> {{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}} </div> {{#if allowImageUpload}} <div> <input type="radio" id="uploaded_avatar" name="avatar" value="uploaded" {{action "useUploadedAvatar"}}> <label class="radio" for="uploaded_avatar"> - {{#if hasUploadedAvatar}} - {{#if uploadedAvatarTemplate}} - {{bound-avatar-template uploadedAvatarTemplate "large"}} - {{else}} - {{bound-avatar controller "large" custom_avatar_upload_id}} - {{/if}} + {{#if custom_avatar_template}} + {{bound-avatar-template custom_avatar_template "large"}} {{i18n 'user.change_avatar.uploaded_avatar'}} {{else}} {{i18n 'user.change_avatar.uploaded_avatar_empty'}} {{/if}} </label> - {{avatar-uploader username=username - user_id=id - uploadedAvatarTemplate=uploadedAvatarTemplate - custom_avatar_upload_id=custom_avatar_upload_id + {{avatar-uploader user_id=id + uploadedAvatarTemplate=custom_avatar_template + uploadedAvatarId=custom_avatar_upload_id uploading=uploading done="useUploadedAvatar"}} </div> @@ -36,6 +31,6 @@ </div> <div class="modal-footer"> - {{d-button action="saveAvatarSelection" class="btn-primary" disabled=saveDisabled label="save"}} + {{d-button action="saveAvatarSelection" class="btn-primary" disabled=uploading label="save"}} <a {{action "closeModal"}}>{{i18n 'cancel'}}</a> </div> diff --git a/app/assets/javascripts/discourse/templates/modal/edit-topic-auto-close.hbs b/app/assets/javascripts/discourse/templates/modal/edit-topic-auto-close.hbs index 4ad1e97e4..fcf524920 100644 --- a/app/assets/javascripts/discourse/templates/modal/edit-topic-auto-close.hbs +++ b/app/assets/javascripts/discourse/templates/modal/edit-topic-auto-close.hbs @@ -1,4 +1,4 @@ -<form {{action "saveAutoClose" on="submit"}}> +<form> <div class="modal-body"> {{auto-close-form autoCloseTime=model.auto_close_time autoCloseValid=auto_close_valid @@ -6,8 +6,8 @@ limited=model.details.auto_close_based_on_last_post }} </div> <div class="modal-footer"> - <button class='btn btn-primary' type='submit' {{bind-attr disabled="auto_close_invalid"}}>{{i18n 'topic.auto_close_save'}}</button> + {{d-button class="btn-primary" disabled=auto_close_invalid label="topic.auto_close_save" action="saveAutoClose"}} <a {{action "closeModal"}}>{{i18n 'cancel'}}</a> - <button class='btn pull-right' {{action "removeAutoClose"}}>{{i18n 'topic.auto_close_remove'}}</button> + {{d-button class="pull-right" action="removeAutoClose" label="topic.auto_close_remove"}} </div> </form> diff --git a/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs b/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs index 7581daf46..4b1053397 100644 --- a/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs +++ b/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs @@ -11,6 +11,7 @@ <li>{{{i18n 'keyboard_shortcuts_help.jump_to.top'}}}</li> <li>{{{i18n 'keyboard_shortcuts_help.jump_to.bookmarks'}}}</li> <li>{{{i18n 'keyboard_shortcuts_help.jump_to.profile'}}}</li> + <li>{{{i18n 'keyboard_shortcuts_help.jump_to.messages'}}}</li> </ul> <h4>{{i18n 'keyboard_shortcuts_help.navigation.title'}}</h4> <ul> diff --git a/app/assets/javascripts/discourse/templates/modal/not-activated.hbs b/app/assets/javascripts/discourse/templates/modal/not-activated.hbs index 4266126c8..f8df2639e 100644 --- a/app/assets/javascripts/discourse/templates/modal/not-activated.hbs +++ b/app/assets/javascripts/discourse/templates/modal/not-activated.hbs @@ -3,7 +3,7 @@ {{{i18n 'login.sent_activation_email_again' currentEmail=currentEmail}}} {{else}} {{{i18n 'login.not_activated' sentTo=sentTo}}} - <a href="#" {{action "sendActivationEmail"}}>{{i18n 'login.resend_activation_email'}}</a> + <a href {{action "sendActivationEmail"}}>{{i18n 'login.resend_activation_email'}}</a> {{/if}} </div> <div class="modal-footer"> diff --git a/app/assets/javascripts/discourse/templates/modal/reorder-categories.hbs b/app/assets/javascripts/discourse/templates/modal/reorder-categories.hbs new file mode 100644 index 000000000..64c36b959 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/modal/reorder-categories.hbs @@ -0,0 +1,33 @@ +<div class="modal-body reorder-categories full-height-modal"> + <div id="rc-scroll-anchor"></div> + <table> + <thead> + <th class="th-pos">{{i18n "categories.reorder.position"}}</th> + <th class="th-cat">{{i18n "categories.category"}}</th> + </thead> + {{#each categoriesOrdered as |cat|}} + <tr data-category-id="{{cat.id}}"> + <td> + {{number-field number=cat.position}} + {{d-button class="no-text" action="moveUp" actionParam=cat icon="arrow-up"}} + {{d-button class="no-text" action="moveDown" actionParam=cat icon="arrow-down"}} + {{#if cat.hasBufferedChanges}} + {{d-button class="no-text" action="commit" icon="check"}} + {{/if}} + </td> + <td>{{category-badge cat allowUncategorized="true"}}</td> + </tr> + {{/each}} + </table> + <div id="rc-scroll-bottom"></div> +</div> + +<div class="modal-footer"> + {{#if showFixIndices}} + {{d-button action="fixIndices" icon="random" label="categories.reorder.fix_order" title="categories.reorder.fix_order_tooltip"}} + {{/if}} + {{#if showApplyAll}} + {{d-button action="commit" icon="check" label="categories.reorder.apply_all"}} + {{/if}} + {{d-button class="btn-primary" disabled=saveDisabled action="saveOrder" label="categories.reorder.save"}} +</div> diff --git a/app/assets/javascripts/discourse/templates/navigation/categories.hbs b/app/assets/javascripts/discourse/templates/navigation/categories.hbs index 6ba5509e9..e6a650cfb 100644 --- a/app/assets/javascripts/discourse/templates/navigation/categories.hbs +++ b/app/assets/javascripts/discourse/templates/navigation/categories.hbs @@ -3,7 +3,7 @@ {{navigation-bar navItems=navItems filterMode=filterMode}} {{#if canCreateCategory}} - <button class='btn btn-default' {{action "createCategory"}}><i class='fa fa-plus'></i>{{i18n 'category.create'}}</button> + {{categories-admin-dropdown}} {{/if}} {{#if canCreateTopic}} <button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i>{{i18n 'topic.create'}}</button> diff --git a/app/assets/javascripts/discourse/templates/post.hbs b/app/assets/javascripts/discourse/templates/post.hbs index 679035302..abdde5fbc 100644 --- a/app/assets/javascripts/discourse/templates/post.hbs +++ b/app/assets/javascripts/discourse/templates/post.hbs @@ -1,14 +1,14 @@ {{post-gap post=this postStream=controller.model.postStream before="true"}} {{#if hasTimeGap}} - {{time-gap daysAgo=daysSincePrevious}} + {{time-gap daysAgo=daysSincePrevious postStream=controller.model.postStream}} {{/if}} <div class='row'> {{view 'reply-history' content=replyHistory}} </div> -<article {{bind-attr class=":boxed via_email" id="postElementId" data-post-id="id" data-user-id="user_id"}}> +<article class="boxed {{if via_email 'via-email'}}" id={{postElementId}} data-post-id={{id}} data-user-id={{user_id}}> <div class='row'> <div class='topic-avatar'> @@ -45,15 +45,20 @@ </div> {{/if}} {{#if wiki}} - <div class="post-info wiki" title="{{i18n 'post.wiki.about'}}" {{action "editPost" this}}>{{fa-icon "pencil-square-o"}}</div> + <div class="post-info wiki" title={{i18n 'post.wiki.about'}} {{action "editPost" this}}>{{fa-icon "pencil-square-o"}}</div> {{/if}} {{#if via_email}} {{#if canViewRawEmail}} - <div class="post-info via-email raw-email" title="{{i18n 'post.via_email'}}" {{action "showRawEmail" this}}>{{fa-icon "envelope-o"}}</div> + <div class="post-info via-email raw-email" title={{i18n 'post.via_email'}} {{action "showRawEmail" this}}>{{fa-icon "envelope-o"}}</div> {{else}} - <div class="post-info via-email" title="{{i18n 'post.via_email'}}">{{fa-icon "envelope-o"}}</div> + <div class="post-info via-email" title={{i18n 'post.via_email'}}>{{fa-icon "envelope-o"}}</div> {{/if}} {{/if}} + + {{#if view.whisper}} + <div class="post-info whisper" title={{i18n 'post.whisper'}}>{{fa-icon "eye-slash"}}</div> + {{/if}} + {{#if showUserReplyTab}} <a href {{action "toggleReplyHistory" this target="view"}} class='reply-to-tab'> {{#if loadingReplyHistory}} @@ -77,6 +82,7 @@ <div {{bind-attr class="showUserReplyTab:avoid-tab view.repliesShown::contents :regular view.extraClass"}}> <div class='cooked'> {{{cooked}}} + {{plugin-outlet "post-after-cooked"}} {{#if firstPost}} {{plugin-outlet "topic-after-cooked"}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/user-card.hbs b/app/assets/javascripts/discourse/templates/user-card.hbs index ce4fdb928..a6b30ca57 100644 --- a/app/assets/javascripts/discourse/templates/user-card.hbs +++ b/app/assets/javascripts/discourse/templates/user-card.hbs @@ -16,7 +16,7 @@ {{#if user.title}} <h2>{{user.title}}</h2> {{/if}} - + {{plugin-outlet "user-card-post-names"}} </span> </div> diff --git a/app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs index f24fea4f5..e0f26deae 100644 --- a/app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs +++ b/app/assets/javascripts/discourse/templates/user-selector-autocomplete.raw.hbs @@ -2,7 +2,8 @@ <ul> {{#each user in options.users}} <li> - <a href='#'>{{avatar user imageSize="tiny"}} + <a href> + {{avatar user imageSize="tiny"}} <span class='username'>{{user.username}}</span> <span class='name'>{{user.name}}</span> </a> @@ -12,7 +13,7 @@ {{#if options.users}}<hr>{{/if}} {{#each group in options.groups}} <li> - <a href=''> + <a href> <i class='icon-group'></i> <span class='username'>{{group.name}}</span> <span class='name'>{{max-usernames group.usernames max="3"}}</span> diff --git a/app/assets/javascripts/discourse/templates/user/preferences.hbs b/app/assets/javascripts/discourse/templates/user/preferences.hbs index 8b622f650..b8e42444c 100644 --- a/app/assets/javascripts/discourse/templates/user/preferences.hbs +++ b/app/assets/javascripts/discourse/templates/user/preferences.hbs @@ -72,7 +72,7 @@ <div class="control-group pref-password"> <label class="control-label">{{i18n 'user.password.title'}}</label> <div class="controls"> - <a href="#" {{action "changePassword"}} class='btn'> + <a href {{action "changePassword"}} class='btn'> {{fa-icon "envelope"}} {{#if model.no_password}} {{i18n 'user.change_password.set_password'}} @@ -246,6 +246,9 @@ </div> <div class="instructions">{{i18n 'user.muted_users_instructions'}}</div> </div> + + {{plugin-outlet "user-custom-controls"}} + <div class="control-group"> <div class="controls"> {{partial 'user/preferences/save-button'}} diff --git a/app/assets/javascripts/discourse/templates/user/user.hbs b/app/assets/javascripts/discourse/templates/user/user.hbs index 586ef838b..28634a17f 100644 --- a/app/assets/javascripts/discourse/templates/user/user.hbs +++ b/app/assets/javascripts/discourse/templates/user/user.hbs @@ -1,4 +1,4 @@ -<div class="container"> +<div class="container{{if viewingSelf ' viewing-self'}}"> <section class='user-main'> <section {{bind-attr class="collapsedInfo :about model.profileBackground:has-background:no-background"}} style={{model.profileBackground}}> <div class='staff-counters'> @@ -52,6 +52,11 @@ {{#if canInviteToForum}} <li>{{#link-to 'userInvited' class="btn right"}}{{fa-icon "user-plus"}}{{i18n 'user.invited.title'}}{{/link-to}}</li> {{/if}} + {{#if collapsedInfo}} + {{#if viewingSelf}} + <li><a {{action "expandProfile"}} href class="btn right">{{fa-icon "angle-double-down"}}{{i18n 'user.expand_profile'}}</a></li> + {{/if}} + {{/if}} </ul> </section> @@ -165,7 +170,6 @@ {{#link-to 'user.notifications'}} {{fa-icon "comment" class="glyph"}} {{i18n 'user.notifications'}} - <span class='count'>({{model.notification_count}})</span> {{/link-to}} {{/link-to}} {{/if}} diff --git a/app/assets/javascripts/discourse/views/avatar-selector.js.es6 b/app/assets/javascripts/discourse/views/avatar-selector.js.es6 index 15b8541ef..6fcc5c9bc 100644 --- a/app/assets/javascripts/discourse/views/avatar-selector.js.es6 +++ b/app/assets/javascripts/discourse/views/avatar-selector.js.es6 @@ -1,3 +1,4 @@ +import { on, observes } from "ember-addons/ember-computed-decorators"; import ModalBodyView from "discourse/views/modal-body"; export default ModalBodyView.extend({ @@ -6,11 +7,14 @@ export default ModalBodyView.extend({ title: I18n.t('user.change_avatar.title'), // *HACK* used to select the proper radio button, because {{action}} stops the default behavior - selectedChanged: function() { + @on("didInsertElement") + @observes("controller.selected") + selectedChanged() { Em.run.next(() => $('input:radio[name="avatar"]').val([this.get('controller.selected')])); - }.observes('controller.selected').on("didInsertElement"), + }, - _focusSelectedButton: function() { + @on("didInsertElement") + _focusSelectedButton() { Em.run.next(() => $('input:radio[value="' + this.get('controller.selected') + '"]').focus()); - }.on("didInsertElement") + } }); diff --git a/app/assets/javascripts/discourse/views/choose-topic.js.es6 b/app/assets/javascripts/discourse/views/choose-topic.js.es6 index da4cbac2f..cb2a34661 100644 --- a/app/assets/javascripts/discourse/views/choose-topic.js.es6 +++ b/app/assets/javascripts/discourse/views/choose-topic.js.es6 @@ -1,5 +1,5 @@ import debounce from 'discourse/lib/debounce'; -import searchForTerm from 'discourse/lib/search-for-term'; +import { searchForTerm } from 'discourse/lib/search'; export default Ember.View.extend({ templateName: 'choose_topic', diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index 22ab46f0f..5d62db416 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -1,10 +1,10 @@ import userSearch from 'discourse/lib/user-search'; import afterTransition from 'discourse/lib/after-transition'; import loadScript from 'discourse/lib/load-script'; -import avatarTemplate from 'discourse/lib/avatar-template'; import positioningWorkaround from 'discourse/lib/safari-hacks'; import debounce from 'discourse/lib/debounce'; import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions'; +import { headerHeight } from 'discourse/views/header'; const ComposerView = Ember.View.extend(Ember.Evented, { _lastKeyTimeout: null, @@ -60,23 +60,22 @@ const ComposerView = Ember.View.extend(Ember.Evented, { }, resize: function() { - const self = this; - Ember.run.scheduleOnce('afterRender', function() { - const h = $('#reply-control').height() || 0; - self.movePanels.apply(self, [h + "px"]); + Ember.run.scheduleOnce('afterRender', () => { + let h = $('#reply-control').height() || 0; + this.movePanels(h + "px"); // Figure out the size of the fields - const $fields = self.$('.composer-fields'); + const $fields = this.$('.composer-fields'); let pos = $fields.position(); if (pos) { - self.$('.wmd-controls').css('top', $fields.height() + pos.top + 5); + this.$('.wmd-controls').css('top', $fields.height() + pos.top + 5); } // get the submit panel height - pos = self.$('.submit-panel').position(); + pos = this.$('.submit-panel').position(); if (pos) { - self.$('.wmd-controls').css('bottom', h - pos.top + 7); + this.$('.wmd-controls').css('bottom', h - pos.top + 7); } }); @@ -117,20 +116,21 @@ const ComposerView = Ember.View.extend(Ember.Evented, { }, _enableResizing: function() { - const $replyControl = $('#reply-control'), - self = this; + const $replyControl = $('#reply-control'); - const resizer = function() { - Ember.run(function() { - self.resize(); - }); + const runResize = () => { + Ember.run(() => this.resize()); }; $replyControl.DivResizer({ - resize: resizer, - onDrag(sizePx) { self.movePanels.apply(self, [sizePx]); } + maxHeight(winHeight) { + return winHeight - headerHeight(); + }, + resize: runResize, + onDrag: (sizePx) => this.movePanels(sizePx) }); - afterTransition($replyControl, resizer); + + afterTransition($replyControl, runResize); this.set('controller.view', this); positioningWorkaround(this.$()); @@ -250,10 +250,7 @@ const ComposerView = Ember.View.extend(Ember.Evented, { if (posts && topicId === self.get('controller.controllers.topic.model.id')) { const quotedPost = posts.findProperty("post_number", postNumber); if (quotedPost) { - const username = quotedPost.get('username'), - uploadId = quotedPost.get('uploaded_avatar_id'); - - return Discourse.Utilities.tinyAvatar(avatarTemplate(username, uploadId)); + return Discourse.Utilities.tinyAvatar(quotedPost.get('avatar_template')); } } } @@ -321,8 +318,9 @@ const ComposerView = Ember.View.extend(Ember.Evented, { this.messageBus.subscribe("/uploads/composer", upload => { if (!cancelledByTheUser) { if (upload && upload.url) { - const markdown = Discourse.Utilities.getUploadMarkdown(upload); - this.addMarkdown(markdown + " "); + const old = Discourse.Utilities.getUploadPlaceholder(upload.original_filename), + markdown = Discourse.Utilities.getUploadMarkdown(upload); + this.replaceMarkdown(old, markdown); } else { Discourse.Utilities.displayErrorForUpload(upload); } @@ -349,6 +347,10 @@ const ComposerView = Ember.View.extend(Ember.Evented, { this.get("controller").send("closeModal"); // deal with cancellation cancelledByTheUser = false; + // add upload placeholder + const markdown = Discourse.Utilities.getUploadPlaceholder(data.files[0].name); + this.addMarkdown(markdown); + // if (data["xhr"]) { const jqHXR = data.xhr(); if (jqHXR) { @@ -501,6 +503,11 @@ const ComposerView = Ember.View.extend(Ember.Evented, { }); }, + replaceMarkdown(old, text) { + const reply = this.get("model.reply"); + this.set("model.reply", reply.replace(old, text)); + }, + // Uses javascript to get the image sizes from the preview, if present imageSizes() { const result = {}; diff --git a/app/assets/javascripts/discourse/views/discovery-topics.js.es6 b/app/assets/javascripts/discourse/views/discovery-topics.js.es6 index 448db126e..af4a5f8d7 100644 --- a/app/assets/javascripts/discourse/views/discovery-topics.js.es6 +++ b/app/assets/javascripts/discourse/views/discovery-topics.js.es6 @@ -1,5 +1,6 @@ import UrlRefresh from 'discourse/mixins/url-refresh'; import LoadMore from "discourse/mixins/load-more"; +import { on, observes, default as computed } from "ember-addons/ember-computed-decorators"; export default Ember.View.extend(LoadMore, UrlRefresh, { eyelineSelector: '.topic-list-item', @@ -8,30 +9,32 @@ export default Ember.View.extend(LoadMore, UrlRefresh, { loadMore() { const self = this; Discourse.notifyTitle(0); - this.get('controller').loadMoreTopics().then(function (hasMoreResults) { - Em.run.schedule('afterRender', function() { - self.saveScrollPosition(); - }); + this.get('controller').loadMoreTopics().then(hasMoreResults => { + Ember.run.schedule('afterRender', () => self.saveScrollPosition()); if (!hasMoreResults) { - self.get('eyeline').flushRest(); + this.get('eyeline').flushRest(); + } else if ($(window).height() >= $(document).height()) { + this.send("loadMore"); } }); } }, - _readjustScrollPosition: function() { + @on("didInsertElement") + @observes("controller.model") + _readjustScrollPosition() { const scrollTo = this.session.get('topicListScrollPosition'); - - if (typeof scrollTo !== "undefined") { - Em.run.schedule('afterRender', function() { - $(window).scrollTop(scrollTo+1); - }); + if (scrollTo && scrollTo >= 0) { + Ember.run.schedule('afterRender', () => $(window).scrollTop(scrollTo + 1)); + } else { + this.loadMoreUnlessFull(); } - }.on('didInsertElement'), + }, - _updateTitle: function() { - Discourse.notifyTitle(this.get('controller.topicTrackingState.incomingCount')); - }.observes('controller.topicTrackingState.incomingCount'), + @computed("controller.topicTrackingState.incomingCount") + _updateTitle(incomingCount) { + Discourse.notifyTitle(incomingCount); + }, // Remember where we were scrolled to saveScrollPosition() { diff --git a/app/assets/javascripts/discourse/views/edit-topic-auto-close.js.es6 b/app/assets/javascripts/discourse/views/edit-topic-auto-close.js.es6 new file mode 100644 index 000000000..6265d41b8 --- /dev/null +++ b/app/assets/javascripts/discourse/views/edit-topic-auto-close.js.es6 @@ -0,0 +1,7 @@ +import ModalBodyView from "discourse/views/modal-body"; + +export default ModalBodyView.extend({ + templateName: "modal/edit-topic-auto-close", + title: I18n.t("topic.auto_close_title"), + focusInput: false +}); diff --git a/app/assets/javascripts/discourse/views/full-page-search.js.es6 b/app/assets/javascripts/discourse/views/full-page-search.js.es6 index 98225e4f7..f0b71073a 100644 --- a/app/assets/javascripts/discourse/views/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/views/full-page-search.js.es6 @@ -1,3 +1 @@ -import ScrollTop from "discourse/mixins/scroll-top"; - -export default Ember.View.extend(ScrollTop, {}); +export default Ember.View.extend({}); diff --git a/app/assets/javascripts/discourse/views/header.js.es6 b/app/assets/javascripts/discourse/views/header.js.es6 index d7088739c..add2ce107 100644 --- a/app/assets/javascripts/discourse/views/header.js.es6 +++ b/app/assets/javascripts/discourse/views/header.js.es6 @@ -1,52 +1,55 @@ +import { on } from 'ember-addons/ember-computed-decorators'; + export default Ember.View.extend({ tagName: 'header', classNames: ['d-header', 'clearfix'], classNameBindings: ['editingTopic'], templateName: 'header', - examineDockHeader: function() { - var headerView = this; - + examineDockHeader() { // Check the dock after the current run loop. While rendering, // it's much slower to calculate `outlet.offset()` - Em.run.next(function () { - if (!headerView.docAt) { - var outlet = $('#main-outlet'); + Ember.run.next(() => { + if (!this.docAt) { + const outlet = $('#main-outlet'); if (!(outlet && outlet.length === 1)) return; - headerView.docAt = outlet.offset().top; + this.docAt = outlet.offset().top; } - var offset = window.pageYOffset || $('html').scrollTop(); - if (offset >= headerView.docAt) { - if (!headerView.dockedHeader) { + const offset = window.pageYOffset || $('html').scrollTop(); + if (offset >= this.docAt) { + if (!this.dockedHeader) { $('body').addClass('docked'); - headerView.dockedHeader = true; + this.dockedHeader = true; } } else { - if (headerView.dockedHeader) { + if (this.dockedHeader) { $('body').removeClass('docked'); - headerView.dockedHeader = false; + this.dockedHeader = false; } } }); }, - _tearDown: function() { + @on('willDestroyElement') + _tearDown() { $(window).unbind('scroll.discourse-dock'); $(document).unbind('touchmove.discourse-dock'); this.$('a.unread-private-messages, a.unread-notifications, a[data-notifications]').off('click.notifications'); $('body').off('keydown.header'); - }.on('willDestroyElement'), + }, - _setup: function() { - const self = this; - - $(window).bind('scroll.discourse-dock', function() { - self.examineDockHeader(); - }); - $(document).bind('touchmove.discourse-dock', function() { - self.examineDockHeader(); - }); - self.examineDockHeader(); - }.on('didInsertElement') + @on('didInsertElement') + _setup() { + $(window).bind('scroll.discourse-dock', () => this.examineDockHeader()); + $(document).bind('touchmove.discourse-dock', () => this.examineDockHeader()); + this.examineDockHeader(); + } }); + +export function headerHeight() { + const $header = $('header.d-header'); + const headerOffset = $header.offset(); + const headerOffsetTop = (headerOffset) ? headerOffset.top : 0; + return parseInt($header.outerHeight() + headerOffsetTop - $(window).scrollTop()); +} diff --git a/app/assets/javascripts/discourse/views/list/post-count-or-badges.js.es6 b/app/assets/javascripts/discourse/views/list/post-count-or-badges.js.es6 new file mode 100644 index 000000000..40faddcde --- /dev/null +++ b/app/assets/javascripts/discourse/views/list/post-count-or-badges.js.es6 @@ -0,0 +1,4 @@ +export default Ember.Object.extend({ + postCountsPresent: Ember.computed.or('topic.unread', 'topic.displayNewPosts'), + showBadges: Ember.computed.and('postBadgesEnabled', 'postCountsPresent') +}); diff --git a/app/assets/javascripts/discourse/views/modal-body.js.es6 b/app/assets/javascripts/discourse/views/modal-body.js.es6 index c5807ea32..f164c8dbb 100644 --- a/app/assets/javascripts/discourse/views/modal-body.js.es6 +++ b/app/assets/javascripts/discourse/views/modal-body.js.es6 @@ -1,7 +1,10 @@ +import { observes, on } from "ember-addons/ember-computed-decorators"; + export default Ember.View.extend({ focusInput: true, - _setupModal: function() { + @on("didInsertElement") + _setupModal() { $('#modal-alert').hide(); $('#discourse-modal').modal('show'); @@ -14,9 +17,10 @@ export default Ember.View.extend({ if (title) { this.set('controller.controllers.modal.title', title); } - }.on('didInsertElement'), + }, - flashMessageChanged: function() { + @observes("controller.flashMessage") + flashMessageChanged() { const flashMessage = this.get('controller.flashMessage'); if (flashMessage) { const messageClass = flashMessage.get('messageClass') || 'success'; @@ -25,6 +29,6 @@ export default Ember.View.extend({ .addClass("alert alert-" + messageClass).html(flashMessage.get('message')) .fadeIn(); } - }.observes('controller.flashMessage') + } }); diff --git a/app/assets/javascripts/discourse/views/modal.js.es6 b/app/assets/javascripts/discourse/views/modal.js.es6 index e69cd3c51..07f2bbeb1 100644 --- a/app/assets/javascripts/discourse/views/modal.js.es6 +++ b/app/assets/javascripts/discourse/views/modal.js.es6 @@ -1,3 +1,5 @@ +import { on } from "ember-addons/ember-computed-decorators"; + export default Ember.View.extend({ elementId: 'discourse-modal', templateName: 'modal/modal', @@ -7,17 +9,19 @@ export default Ember.View.extend({ // We handle ESC ourselves 'data-keyboard': 'false', - _bindOnInsert: function() { + @on("didInsertElement") + setUp() { $('html').on('keydown.discourse-modal', e => { if (e.which === 27) { Em.run.next(() => $('.modal-header a.close').click()); } }); - }.on('didInsertElement'), + }, - _bindOnDestroy: function() { + @on("willDestroyElement") + cleanUp() { $('html').off('keydown.discourse-modal'); - }.on('willDestroyElement'), + }, click(e) { const $target = $(e.target); diff --git a/app/assets/javascripts/discourse/views/post.js.es6 b/app/assets/javascripts/discourse/views/post.js.es6 index bc555c678..e2c663177 100644 --- a/app/assets/javascripts/discourse/views/post.js.es6 +++ b/app/assets/javascripts/discourse/views/post.js.es6 @@ -1,6 +1,8 @@ import ScreenTrack from 'discourse/lib/screen-track'; import { number } from 'discourse/lib/formatter'; import DiscourseURL from 'discourse/lib/url'; +import computed from 'ember-addons/ember-computed-decorators'; +import { fmt } from 'discourse/lib/computed'; const DAY = 60 * 50 * 1000; @@ -12,10 +14,18 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, { 'post.deleted:deleted', 'post.topicOwner:topic-owner', 'groupNameClass', - 'post.wiki:wiki'], + 'post.wiki:wiki', + 'whisper'], post: Ember.computed.alias('content'), + postElementId: fmt('post.post_number', 'post_%@'), + + @computed('post.post_type') + whisper(postType) { + return postType === this.site.get('post_types.whisper'); + }, + templateName: function() { return (this.get('post.post_type') === this.site.get('post_types.small_action')) ? 'post-small-action' : 'post'; }.property('post.post_type'), @@ -145,11 +155,10 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, { topicId = parseInt(topicId, 10); Discourse.ajax("/posts/by_number/" + topicId + "/" + postId).then(function (result) { - // slightly double escape the cooked html to prevent jQuery from unescaping it - const escaped = result.cooked.replace(/&[^gla]/, "&"); - const parsed = $(escaped); - parsed.replaceText(originalText, "<span class='highlighted'>" + originalText + "</span>"); - $blockQuote.showHtml(parsed, 'fast', finished); + const div = $("<div class='expanded-quote'></div>"); + div.html(result.cooked); + div.highlight(originalText, {caseSensitive: true, element: 'span', className: 'highlighted'}); + $blockQuote.showHtml(div, 'fast', finished); }); } else { // Hide expanded quote diff --git a/app/assets/javascripts/discourse/views/reorder-categories.js.es6 b/app/assets/javascripts/discourse/views/reorder-categories.js.es6 new file mode 100644 index 000000000..570c9ec96 --- /dev/null +++ b/app/assets/javascripts/discourse/views/reorder-categories.js.es6 @@ -0,0 +1,80 @@ +import ModalBodyView from "discourse/views/modal-body"; + +export default ModalBodyView.extend({ + title: I18n.t('categories.reorder.title'), + templateName: 'modal/reorder-categories', + + _setup: function() { + this.get('controller').on('scrollIntoView', this, this.scrollIntoView); + }.on('didInsertElement'), + _teardown: function() { + this.get('controller').off('scrollIntoView', this, this.scrollIntoView); + this.set('prevScrollElem', null); + }.on('willClearRender'), + + scrollIntoView() { + const elem = this.$('tr[data-category-id="' + this.get('controller.scrollIntoViewId') + '"]'); + const scrollParent = this.$('.modal-body'); + const eoff = elem.position(); + const poff = $(document.getElementById('rc-scroll-anchor')).position(); + const currHeight = scrollParent.height(); + + elem[0].className = "highlighted"; + + const goal = eoff.top - poff.top - currHeight / 2, + current = scrollParent.scrollTop(); + scrollParent.scrollTop(9999999); + const max = scrollParent.scrollTop(); + scrollParent.scrollTop(current); + + const doneTimeout = setTimeout(function() { + elem[0].className = "highlighted done"; + setTimeout(function() { + elem[0].className = ""; + }, 2000); + }, 0); + + if (goal > current - currHeight / 4 && goal < current + currHeight / 4) { + // Too close to goal + return; + } + if (max - current < 10 && goal > current) { + // Too close to bottom + return; + } + if (current < 10 && goal < current) { + // Too close to top + return; + } + + if (!window.requestAnimationFrame) { + scrollParent.scrollTop(goal); + } else { + clearTimeout(doneTimeout); + const startTime = performance.now(); + const duration = 100; + + function doScroll(timestamp) { + let progress = (timestamp - startTime) / duration; + if (progress > 1) { + progress = 1; + setTimeout(function() { + elem[0].className = "highlighted done"; + setTimeout(function() { + elem[0].className = ""; + }, 2000); + }, 0); + } else if (progress < 0) { + progress = 0; + } + if (progress < 1) { + window.requestAnimationFrame(doScroll); + } + + const iprogress = 1 - progress; + scrollParent.scrollTop(goal * progress + current * iprogress); + } + window.requestAnimationFrame(doScroll); + } + } +}); diff --git a/app/assets/javascripts/discourse/views/static.js.es6 b/app/assets/javascripts/discourse/views/static.js.es6 index 12dbfdf60..35ce3d674 100644 --- a/app/assets/javascripts/discourse/views/static.js.es6 +++ b/app/assets/javascripts/discourse/views/static.js.es6 @@ -1,24 +1,28 @@ import isElementInViewport from "discourse/lib/is-element-in-viewport"; import ScrollTop from 'discourse/mixins/scroll-top'; - -var readFaq = false; +import { on } from 'ember-addons/ember-computed-decorators'; export default Ember.View.extend(ScrollTop, { - _checkRead: function() { - const path = this.get('controller.model.path'); - if (path === "faq" || path === "guidelines") { - const controller = this.get('controller'); - $(window).on('load.faq resize.faq scroll.faq', function() { - if (!readFaq && isElementInViewport($(".contents p").last())) { - readFaq = true; - controller.send('markFaqRead'); - } - }); + @on('didInsertElement') + _checkRead() { + const currentUser = this.get('controller.currentUser'); + if (currentUser) { + const path = this.get('controller.model.path'); + if (path === "faq" || path === "guidelines") { + const controller = this.get('controller'); + $(window).on('load.faq resize.faq scroll.faq', function() { + const faqUnread = !currentUser.get('read_faq'); + if (faqUnread && isElementInViewport($(".contents p").last())) { + controller.send('markFaqRead'); + } + }); + } } - }.on('didInsertElement'), + }, - _stopChecking: function(){ + @on('willDestroyElement') + _stopChecking() { $(window).off('load.faq resize.faq scroll.faq'); - }.on('willDestroyElement') + } }); diff --git a/app/assets/javascripts/discourse/views/topic-footer-buttons.js.es6 b/app/assets/javascripts/discourse/views/topic-footer-buttons.js.es6 index 1598eb1ae..3b70d394d 100644 --- a/app/assets/javascripts/discourse/views/topic-footer-buttons.js.es6 +++ b/app/assets/javascripts/discourse/views/topic-footer-buttons.js.es6 @@ -9,6 +9,32 @@ import PinnedButton from 'discourse/components/pinned-button'; import TopicNotificationsButton from 'discourse/components/topic-notifications-button'; import DiscourseContainerView from 'discourse/views/container'; +const MainPanel = Discourse.ContainerView.extend({ + elementId: 'topic-footer-main-buttons', + topicBinding: 'controller.content', + + init() { + this._super(); + + const topic = this.get('topic'); + if (!topic.get('isPrivateMessage')) { + // We hide some controls from private messages + if (this.get('topic.details.can_invite_to')) { + this.attachViewClass(InviteReplyButton); + } + this.attachViewClass(BookmarkButton); + this.attachViewClass(ShareButton); + if (this.get('topic.details.can_flag_topic')) { + this.attachViewClass(FlagTopicButton); + } + } + if (this.get('topic.details.can_create_post')) { + this.attachViewClass(ReplyButton); + } + } + +}); + export default DiscourseContainerView.extend({ elementId: 'topic-footer-buttons', topicBinding: 'controller.content', @@ -26,20 +52,9 @@ export default DiscourseContainerView.extend({ if (Discourse.User.currentProp("staff")) { this.attachViewClass(TopicAdminMenuButton); } - if (!topic.get('isPrivateMessage')) { - // We hide some controls from private messages - if (this.get('topic.details.can_invite_to')) { - this.attachViewClass(InviteReplyButton); - } - this.attachViewClass(BookmarkButton); - this.attachViewClass(ShareButton); - if (this.get('topic.details.can_flag_topic')) { - this.attachViewClass(FlagTopicButton); - } - } - if (this.get('topic.details.can_create_post')) { - this.attachViewClass(ReplyButton); - } + + this.attachViewWithArgs(viewArgs, MainPanel); + this.attachViewWithArgs(viewArgs, PinnedButton); this.attachViewWithArgs(viewArgs, TopicNotificationsButton); diff --git a/app/assets/javascripts/discourse/views/topic.js.es6 b/app/assets/javascripts/discourse/views/topic.js.es6 index bd6265ace..8a1828236 100644 --- a/app/assets/javascripts/discourse/views/topic.js.es6 +++ b/app/assets/javascripts/discourse/views/topic.js.es6 @@ -121,10 +121,6 @@ const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolli this.appEvents.trigger('topic:scrolled', offset); }, - topicTrackingState: function() { - return Discourse.TopicTrackingState.current(); - }.property(), - browseMoreMessage: function() { var opts = { latestLink: "<a href=\"" + Discourse.getURL("/latest") + "\">" + I18n.t("topic.view_latest_topics") + "</a>" }, category = this.get('controller.content.category'); diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 25238bf92..757f7308e 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -14,7 +14,6 @@ //= require ./discourse/lib/load-script //= require ./discourse/lib/notification-levels //= require ./discourse/lib/app-events -//= require ./discourse/lib/avatar-template //= require ./discourse/lib/url //= require ./discourse/lib/debounce //= require ./discourse/lib/quote @@ -31,16 +30,16 @@ //= require ./discourse/models/rest //= require ./discourse/models/badge-grouping //= require ./discourse/models/badge +//= require ./discourse/models/category //= require_tree ./discourse/mixins //= require ./discourse/lib/ajax-error //= require ./discourse/lib/markdown -//= require ./discourse/lib/search-for-term +//= require ./discourse/lib/search //= require ./discourse/lib/user-search //= require ./discourse/lib/export-csv //= require ./discourse/lib/autocomplete //= require ./discourse/lib/after-transition //= require ./discourse/lib/debounce -//= require ./discourse/lib/avatar-template //= require ./discourse/lib/safari-hacks //= require_tree ./discourse/adapters //= require ./discourse/models/rest @@ -73,6 +72,7 @@ //= require ./discourse/components/notifications-button //= require ./discourse/components/topic-notifications-button //= require ./discourse/lib/link-mentions +//= require ./discourse/views/header //= require ./discourse/views/composer //= require ./discourse/lib/show-modal //= require ./discourse/lib/screen-track @@ -92,13 +92,11 @@ //= require ./discourse/lib/export-result //= require ./discourse/dialects/dialect //= require ./discourse/lib/emoji/emoji -//= require ./discourse/lib/sharing -//= require discourse/lib/desktop-notifications +//= require_tree ./discourse/lib //= require ./discourse/router //= require_tree ./discourse/dialects //= require_tree ./discourse/controllers -//= require_tree ./discourse/lib //= require_tree ./discourse/models //= require_tree ./discourse/components //= require_tree ./discourse/views diff --git a/app/assets/javascripts/preload_store.js b/app/assets/javascripts/preload_store.js index b4b0e8b0d..4911d4e54 100644 --- a/app/assets/javascripts/preload_store.js +++ b/app/assets/javascripts/preload_store.js @@ -42,7 +42,7 @@ window.PreloadStore = { var result = finder(); // If the finder returns a promise, we support that too - if (result.then) { + if (result && result.then) { result.then(function(result) { return resolve(result); }, function(result) { diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js index 5c0788f8c..1a52785b5 100644 --- a/app/assets/javascripts/vendor.js +++ b/app/assets/javascripts/vendor.js @@ -22,7 +22,6 @@ //= require div_resizer //= require caret_position //= require favcount.js -//= require jquery.ba-replacetext.js //= require jquery.ba-resize.min.js //= require jquery.color.js //= require jquery.cookie.js diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index d1de50da4..498dae55e 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -1 +1 @@ -@import "common/admin/admin_base" +@import "common/admin/admin_base"; diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index d7686805a..6007fd830 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -198,7 +198,7 @@ td.flaggers td { } // Hide the search checkbox for very small screens // Todo: find somewhere to display it - probably requires switching its order in the html - @media (max-width: 450px) { + @media (max-width: 550px) { display: none; } } @@ -279,14 +279,14 @@ td.flaggers td { .admin-detail.mobile-open { @media (max-width: $mobile-breakpoint) { transition: transform 0.3s ease; - transform: (translateX(50%)); + @include transform(translateX(50%)); } } .admin-detail.mobile-closed { @media (max-width: $mobile-breakpoint) { transition: transform 0.3s ease; - transform: (translateX(0)); + @include transform(translateX(0)); } } diff --git a/app/assets/stylesheets/common/base/cat_reorder.scss b/app/assets/stylesheets/common/base/cat_reorder.scss new file mode 100644 index 000000000..c1f1ea994 --- /dev/null +++ b/app/assets/stylesheets/common/base/cat_reorder.scss @@ -0,0 +1,31 @@ +.reorder-categories { + input { + width: 4em; + } + .th-pos { + width: calc(4em + 150px); + } + tbody tr { + background-color: transparent; + transition: background 0s ease; + &.highlighted { + background-color: rgba($highlight, 0.4); + &.done { + background-color: transparent; + transition-duration: 1s; + } + } + &:first-child td { + padding-top: 7px; + } + } + tbody { + border-bottom: 1px solid blend-primary-secondary(50%); + } + table { + padding-bottom: 150px; + } +} +.category-admin-menu ul { + width: 320px; +} diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 4ebe91a7e..844a9e32d 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -130,6 +130,9 @@ div.ac-wrap { input[type=text] { width: 50px; } + label { + font-size: .929em; + } } } diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index c8cedb4a6..d8b473980 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -144,6 +144,9 @@ body { } } +input[type].invalid { + background-color: dark-light-choose(scale-color($danger, $lightness: 80%), scale-color($danger, $lightness: -60%)); +} .wmd-input { resize: none; diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index bf718f19a..85a9869a4 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -1,8 +1,9 @@ .menu-panel.slide-in { position: fixed; + // positions are relative to the .d-header .panel div right: 0; top: 0; - height: 100%; + .panel-body { position: absolute; top: 3px; @@ -13,6 +14,9 @@ .menu-panel.drop-down { position: absolute; + // positions are relative to the .d-header .panel div + top: 100%; // directly underneath .panel + right: -10px; // 10px to the right of .panel - adjust as needed } .menu-panel { @@ -20,10 +24,13 @@ box-shadow: 0 2px 2px rgba(0,0,0, .25); background-color: $secondary; z-index: 1100; - overflow: none; padding: 0.5em; width: 300px; + hr { + margin: 3px 0; + } + .panel-header { position: absolute; right: 20px; @@ -42,6 +49,13 @@ } +.menu-links.columned { + li { + width: 50%; + float: left; + } +} + .menu-panel { ul.menu-links li, ul li.heading { a { @@ -51,6 +65,13 @@ background-color: dark-light-diff($highlight, $secondary, 50%, -55%); } } + + .new { + font-size: 0.8em; + margin-left: 0.5em; + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + } + } li.category-link { @@ -62,9 +83,8 @@ .badge-notification { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); background-color: transparent; - vertical-align: top; - padding: 5px 5px 2px 5px; display: inline; + padding: 0; } } @@ -74,6 +94,13 @@ font-weight: normal; font-size: 11px; } + + span.badge-category { + max-width: 90px; + overflow: hidden; + text-overflow: ellipsis; + } + } .search-menu { @@ -90,9 +117,9 @@ } input[type='text'] { - margin: 0.5em 0 0.5em; + margin: 0.5em 3px; box-sizing: border-box; - width: 100%; + width: calc(100% - 6px); height: 32px; padding: 5px; } @@ -268,6 +295,3 @@ div.menu-links-header { margin-right: 0.2em; } } - - - diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index 7674cd42f..74291ab40 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -129,6 +129,9 @@ } .modal-body { + &.full-height-modal { + max-height: calc(100vh - 150px); + } textarea { width: 99%; height: 80px; diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index ec75223b5..d77d57ea1 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -18,10 +18,20 @@ top: -3px; margin-right: 4px; } + a.search-link:visited .topic-title { + color: scale-color($tertiary, $lightness: 15%); + } .search-link { .topic-statuses, .topic-title { font-size: 1.25em; } + + .topic-statuses { + float: none; + display: inline-block; + color: $primary; + font-size: 1.0em; + } } .blurb { font-size: 1.0em; @@ -39,6 +49,20 @@ } } +.fps-select { + margin-top: -15px; + margin-bottom: 15px; + a:hover { + color: $secondary; + background-color: $tertiary; + } + a { + margin-right: 15px; + font-size: 12px; + padding: 2px 5px; + } +} + .search.row { margin-bottom: 15px; input { @@ -50,3 +74,7 @@ .search-footer { margin-bottom: 30px; } + +.panel-body-contents .search-context label { + float: left; +} diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 3b0d62008..dab4f08f1 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -147,7 +147,7 @@ aside.quote { } .post-info { - &.wiki, &.via-email { + &.wiki, &.via-email, &.whisper { margin-right: 5px; i.fa { font-size: 1em; @@ -317,3 +317,12 @@ table.md-table { clear: both; } + +.whisper { + .topic-body { + .cooked { + font-style: italic; + color: dark-light-diff($primary, $secondary, 55%, -40%); + } + } +} diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index 47fb1a1cb..96eefe157 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -59,6 +59,8 @@ span.badge-category { color: $primary !important; display: inline-block; + overflow: hidden; + text-overflow: ellipsis; #search-dropdown & { margin-top: -2px; @@ -101,6 +103,8 @@ span { display: block; + overflow: hidden; + text-overflow: ellipsis; &.badge-category-bg, &.badge-category-parent-bg { position: absolute; @@ -126,17 +130,6 @@ } } } - - span.badge-category { - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - - .menu-panel & { - max-width: 90px; - } - } - } // Category badge dropdown diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index 9ba434f90..5fc774c7a 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -94,6 +94,11 @@ // hide cancel upload link on IE9 (not supported) .ie9 #cancel-file-upload { display: none; } +// todo, enable if we ever implement touch grippie... I question the value though (Sam) +.touch #reply-control.open .grippie { + display: none; +} + #reply-control { .toggle-preview, #draft-status, #file-uploading { position: absolute; @@ -112,7 +117,7 @@ } transition: height 0.4s ease; width: 100%; - z-index: 1039; + z-index: 999; height: 0; background-color: dark-light-diff($primary, $secondary, 90%, -60%); bottom: 0; @@ -324,8 +329,8 @@ } .title-input .popup-tip { width: 300px; - left: 0px; - top: -30px; + margin-top: 8px; + left: 150px; } .category-input .popup-tip { width: 240px; @@ -419,6 +424,7 @@ top: 0; position: absolute; border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); background-color: $secondary; z-index: 100; } diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index 2c58ae8fd..17baebbc1 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -185,7 +185,6 @@ body { border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); border-radius: 3px; box-shadow: inset 0 1px 1px rgba(0,0,0, .3); - transition: border linear 0.2s, box-shadow linear 0.2s; } 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"] { @@ -193,7 +192,6 @@ body { border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); border-radius: 3px; box-shadow: inset 0 1px 1px rgba(0,0,0, .3); - transition: border linear 0.2s, box-shadow linear 0.2s; } } textarea:focus { diff --git a/app/assets/stylesheets/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss index f63cadd22..c3929fd25 100644 --- a/app/assets/stylesheets/desktop/topic.scss +++ b/app/assets/stylesheets/desktop/topic.scss @@ -228,40 +228,36 @@ a:hover.reply-new { } @media all -and (max-width : 940px) { +and (max-width : 1000px) { #topic-progress, #topic-progress-expanded { - left: 295px; + right: 0; + left: 0; } -} - - -@media all -and (max-width : 870px) { - - #topic-progress, #topic-progress-expanded { - left: 210px; - } - -} - - -@media all -and (max-width : 724px) { #topic-progress-wrapper { - right: 0; - #topic-progress, #topic-progress-expanded { left: auto; right: 145px; } + right: 160px; } + #topic-progress-wrapper.docked { + right: 152px; + } + + #topic-footer-main-buttons { + max-width: 70%; + } } + @media all and (max-width : 485px) { #topic-progress-wrapper.docked { display: none; - } + } + #topic-footer-main-buttons { + max-width: 100%; + } } diff --git a/app/assets/stylesheets/desktop/user-card.scss b/app/assets/stylesheets/desktop/user-card.scss index 01c26e838..dbe2f757f 100644 --- a/app/assets/stylesheets/desktop/user-card.scss +++ b/app/assets/stylesheets/desktop/user-card.scss @@ -147,7 +147,7 @@ $user_card_background: #222; } a.mention { - background-color: scale-color($user_card_primary, $lightness: 50%); + background-color: scale-color($user_card_primary, $lightness: -60%); } .overflow { max-height: 60px; diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index bce1bfb3a..ea4202fb0 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -104,6 +104,12 @@ } +.viewing-self .user-main .about.collapsed-info { + .secondary, .staff-counters { + display: inherit; + } +} + .user-main { margin-bottom: 50px; @@ -219,6 +225,7 @@ .details { padding: 15px; margin: 0; + color: dark-light-choose($secondary, lighten($primary, 10%)); } } diff --git a/app/assets/stylesheets/embed.css.scss b/app/assets/stylesheets/embed.css.scss index 40eab2875..9eee09134 100644 --- a/app/assets/stylesheets/embed.css.scss +++ b/app/assets/stylesheets/embed.css.scss @@ -14,8 +14,8 @@ article.post { } .quote .title { - border-left: 5px solid darken(dark-light-diff($primary, $secondary, 90%, -60%), 10%); - background-color: dark-light-diff($primary, $secondary, 90%, -60%); + border-left: 5px solid darken(dark-light-diff($primary, $secondary, 97%, -45%), 10%); + background-color: dark-light-diff($primary, $secondary, 97%, -45%); padding: 10px 10px 0 12px; .avatar { margin-right: 7px; } } @@ -27,8 +27,8 @@ article.post { blockquote { padding: 10px 8px 10px 13px; margin: 0 0 10px 0; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); - border-left: 5px solid darken(dark-light-diff($primary, $secondary, 90%, -60%), 10%); + background-color: dark-light-diff($primary, $secondary, 97%, -45%); + border-left: 5px solid darken(dark-light-diff($primary, $secondary, 97%, -45%), 10%); overflow: hidden; p { margin: 0 0 10px 0; @@ -137,9 +137,3 @@ footer { float: right; max-height: 30px; } - -.button { - background-color: #eee; - padding: 5px; - display: inline-block; -} diff --git a/app/assets/stylesheets/mobile.scss b/app/assets/stylesheets/mobile.scss index 91bf389d1..8434f8c73 100644 --- a/app/assets/stylesheets/mobile.scss +++ b/app/assets/stylesheets/mobile.scss @@ -18,6 +18,8 @@ @import "mobile/user"; @import "mobile/history"; @import "mobile/directory"; +@import "mobile/menu-panel"; +@import "mobile/search"; /* These files doesn't actually exist, they are injected by DiscourseSassImporter. */ diff --git a/app/assets/stylesheets/mobile/menu-panel.scss b/app/assets/stylesheets/mobile/menu-panel.scss index e69de29bb..972ea30ab 100644 --- a/app/assets/stylesheets/mobile/menu-panel.scss +++ b/app/assets/stylesheets/mobile/menu-panel.scss @@ -0,0 +1,7 @@ +.menu-panel { + span.badge-category { + max-width: 85px; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/app/assets/stylesheets/mobile/modal.scss b/app/assets/stylesheets/mobile/modal.scss index 3111b6013..7114c9b2e 100644 --- a/app/assets/stylesheets/mobile/modal.scss +++ b/app/assets/stylesheets/mobile/modal.scss @@ -88,9 +88,14 @@ form { margin-top: 20px; input[type=text] { - width: 500px; + box-sizing: border-box; + width: 100%; } } + + .category-combobox { + width: 100%; + } } .flag-modal { diff --git a/app/assets/stylesheets/mobile/search.scss b/app/assets/stylesheets/mobile/search.scss new file mode 100644 index 000000000..0438c41c0 --- /dev/null +++ b/app/assets/stylesheets/mobile/search.scss @@ -0,0 +1,16 @@ +.search button.btn-primary, .search button.btn { + float: none; +} + +.search.row { + margin-top: 5px; +} + +.search.row input.search { + height: 25px; + width: 69%; +} + +.fps-search-context { + margin-bottom: 15px; +} diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index 859998d3e..f3f6c21ee 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -80,7 +80,17 @@ .badge-notification { position: relative; top: -1px; - i {color: $secondary;} + font-size: 1.071em; + padding: 4px 6px 3px 6px; + i {color: $secondary;} + + &.new-topic::before { + margin-right: 0; + } + &.new-topic { + padding-right: 0; + top: -3px; + } } .topic-item-stats { @@ -91,7 +101,7 @@ .category a { max-width: 160px; } - .num .fa { + .num .fa, a, a:visited { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } @@ -110,6 +120,29 @@ // Category list // -------------------------------------------------- +.categories-list .list-container { + margin-left: -10px; // Extend past the .wrap padding to the edge of the window +} + +.category-list-item.category { + // Allow percentage widths on table cells to include their padding + box-sizing: border-box; + *, *:before, *:after { + box-sizing: inherit; + } + + .main-link { + width: 80%; + } + + .posts { + width: 10%; + } + + .age { + width: 10%; + } +} tr.category-topic-link { border-top: darken($secondary, 3%) 1px solid; @@ -338,20 +371,23 @@ ol.category-breadcrumb { padding: 8px 0; } -.period-chooser { - button { - margin-top: 10px; - } -} - button.dismiss-read { margin-right: 10px; } // base defines extra padding for easier click/top of title field // this is a bit too much for mobile -td.main-link { +td .main-link { + width: 80%; + display: inline-block; a.title { padding: 5px 10px 5px 0; } } +.topic-list { + .posts-map { + display: inline; + font-size: 1.071em; + padding-top: 2px; + } +} diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 9c1c6a3dd..842d13e50 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -396,7 +396,6 @@ iframe { } #selected-posts { - float: left; width: 97%; padding-left: 3%; background-color: srgb-scale($tertiary, $secondary, 15%); diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 35f3295d2..767657f1e 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -80,8 +80,10 @@ class Admin::GroupsController < Admin::AdminController users = User.where(username: params[:usernames].split(",")) elsif params[:user_ids].present? users = User.find(params[:user_ids].split(",")) + elsif params[:user_emails].present? + users = User.where(email: params[:user_emails].split(",")) else - raise Discourse::InvalidParameters.new('user_ids or usernames must be present') + raise Discourse::InvalidParameters.new('user_ids or usernames or user_emails must be present') end users.each do |user| diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b1a665be4..d9fe0a497 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -309,7 +309,11 @@ class ApplicationController < ActionController::Base def preload_current_user_data store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, scope: guardian, root: false))) - serializer = ActiveModel::ArraySerializer.new(TopicTrackingState.report(current_user.id), each_serializer: TopicTrackingStateSerializer) + report = TopicTrackingState.report(current_user.id) + if report.length >= SiteSetting.max_tracked_new_unread.to_i + TopicUser.cap_unread_later(current_user.id) + end + serializer = ActiveModel::ArraySerializer.new(report, each_serializer: TopicTrackingStateSerializer) store_preloaded("topicTrackingStates", MultiJson.dump(serializer)) end @@ -413,17 +417,22 @@ class ApplicationController < ActionController::Base raise Discourse::InvalidAccess.new unless current_user && current_user.staff? end + def destination_url + request.original_url unless request.original_url =~ /uploads/ + end + def redirect_to_login_if_required return if current_user || (request.format.json? && api_key_valid?) - # save original URL in a cookie - cookies[:destination_url] = request.original_url unless request.original_url =~ /uploads/ - # redirect user to the SSO page if we need to log in AND SSO is enabled if SiteSetting.login_required? if SiteSetting.enable_sso? + # save original URL in a session so we can redirect after login + session[:destination_url] = destination_url redirect_to path('/session/sso') else + # save original URL in a cookie (javascript redirects after login in this case) + cookies[:destination_url] = destination_url redirect_to :login end end diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 10500fc41..b63ec503d 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -16,6 +16,7 @@ class CategoriesController < ApplicationController options = {} options[:latest_posts] = params[:latest_posts] || SiteSetting.category_featured_topics options[:parent_category_id] = params[:parent_category_id] + options[:is_homepage] = current_homepage == "categories".freeze @list = CategoryList.new(guardian, options) @list.draft_key = Draft::NEW_TOPIC @@ -24,7 +25,7 @@ class CategoriesController < ApplicationController discourse_expires_in 1.minute - unless current_homepage == 'categories' + unless current_homepage == "categories" @title = I18n.t('js.filters.categories.title') end @@ -36,7 +37,7 @@ class CategoriesController < ApplicationController end def move - guardian.ensure_can_create!(Category) + guardian.ensure_can_create_category! params.require("category_id") params.require("position") @@ -49,6 +50,24 @@ class CategoriesController < ApplicationController end end + def reorder + guardian.ensure_can_create_category! + + params.require(:mapping) + change_requests = MultiJson.load(params[:mapping]) + by_category = Hash[change_requests.map { |cat, pos| [Category.find(cat.to_i), pos] }] + + unless guardian.is_admin? + raise Discourse::InvalidAccess unless by_category.keys.all? { |c| guardian.can_see_category? c } + end + + by_category.each do |cat, pos| + cat.position = pos + cat.save if cat.position_changed? + end + render json: success_json + end + def show if Category.topic_create_allowed(guardian).where(id: @category.id).exists? @category.permission = CategoryGroup.permission_types[:full] @@ -139,6 +158,7 @@ class CategoriesController < ApplicationController :position, :email_in, :email_in_allow_strangers, + :suppress_from_homepage, :parent_category_id, :auto_close_hours, :auto_close_based_on_last_post, diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index e5b6f6388..d894ee1f3 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -5,36 +5,34 @@ class ListController < ApplicationController skip_before_filter :check_xhr - @@categories = [ + before_filter :set_category, only: [ # filtered topics lists - Discourse.filters.map { |f| "category_#{f}".to_sym }, - Discourse.filters.map { |f| "category_none_#{f}".to_sym }, - Discourse.filters.map { |f| "parent_category_category_#{f}".to_sym }, - Discourse.filters.map { |f| "parent_category_category_none_#{f}".to_sym }, + Discourse.filters.map { |f| :"category_#{f}" }, + Discourse.filters.map { |f| :"category_none_#{f}" }, + Discourse.filters.map { |f| :"parent_category_category_#{f}" }, + Discourse.filters.map { |f| :"parent_category_category_none_#{f}" }, # top summaries :category_top, :category_none_top, :parent_category_category_top, # top pages (ie. with a period) - TopTopic.periods.map { |p| "category_top_#{p}".to_sym }, - TopTopic.periods.map { |p| "category_none_top_#{p}".to_sym }, - TopTopic.periods.map { |p| "parent_category_category_top_#{p}".to_sym }, + TopTopic.periods.map { |p| :"category_top_#{p}" }, + TopTopic.periods.map { |p| :"category_none_top_#{p}" }, + TopTopic.periods.map { |p| :"parent_category_category_top_#{p}" }, # category feeds :category_feed, ].flatten - before_filter :set_category, only: @@categories - before_filter :ensure_logged_in, except: [ :topics_by, # anonymous filters Discourse.anonymous_filters, - Discourse.anonymous_filters.map { |f| "#{f}_feed".to_sym }, + Discourse.anonymous_filters.map { |f| "#{f}_feed" }, # anonymous categorized filters - Discourse.anonymous_filters.map { |f| "category_#{f}".to_sym }, - Discourse.anonymous_filters.map { |f| "category_none_#{f}".to_sym }, - Discourse.anonymous_filters.map { |f| "parent_category_category_#{f}".to_sym }, - Discourse.anonymous_filters.map { |f| "parent_category_category_none_#{f}".to_sym }, + Discourse.anonymous_filters.map { |f| :"category_#{f}" }, + Discourse.anonymous_filters.map { |f| :"category_none_#{f}" }, + Discourse.anonymous_filters.map { |f| :"parent_category_category_#{f}" }, + Discourse.anonymous_filters.map { |f| :"parent_category_category_none_#{f}" }, # category feeds :category_feed, # top summaries @@ -43,14 +41,14 @@ class ListController < ApplicationController :category_none_top, :parent_category_category_top, # top pages (ie. with a period) - TopTopic.periods.map { |p| "top_#{p}".to_sym }, - TopTopic.periods.map { |p| "category_top_#{p}".to_sym }, - TopTopic.periods.map { |p| "category_none_top_#{p}".to_sym }, - TopTopic.periods.map { |p| "parent_category_category_top_#{p}".to_sym }, + TopTopic.periods.map { |p| :"top_#{p}" }, + TopTopic.periods.map { |p| :"category_top_#{p}" }, + TopTopic.periods.map { |p| :"category_none_top_#{p}" }, + TopTopic.periods.map { |p| :"parent_category_category_top_#{p}" }, ].flatten # Create our filters - Discourse.filters.each_with_index do |filter, idx| + Discourse.filters.each do |filter| define_method(filter) do |options = nil| list_opts = build_topic_list_options list_opts.merge!(options) if options @@ -60,6 +58,10 @@ class ListController < ApplicationController list_opts[:no_definitions] = true end + if filter.to_s == current_homepage + list_opts.merge!(exclude_category_ids: get_excluded_category_ids(list_opts[:category])) + end + list = TopicQuery.new(user, list_opts).public_send("list_#{filter}") list.more_topics_url = construct_url_with(:next, list_opts) list.prev_topics_url = construct_url_with(:prev, list_opts) @@ -83,34 +85,20 @@ class ListController < ApplicationController define_method("category_#{filter}") do canonical_url "#{Discourse.base_url}#{@category.url}" - self.send(filter, { category: @category.id }) + self.send(filter, category: @category.id) end define_method("category_none_#{filter}") do - self.send(filter, { category: @category.id, no_subcategories: true }) + self.send(filter, category: @category.id, no_subcategories: true) end define_method("parent_category_category_#{filter}") do canonical_url "#{Discourse.base_url}#{@category.url}" - self.send(filter, { category: @category.id }) + self.send(filter, category: @category.id) end define_method("parent_category_category_none_#{filter}") do - self.send(filter, { category: @category.id }) - end - end - - Discourse.feed_filters.each do |filter| - define_method("#{filter}_feed") do - discourse_expires_in 1.minute - - @title = "#{SiteSetting.title} - #{I18n.t("rss_description.#{filter}")}" - @link = "#{Discourse.base_url}/#{filter}" - @description = I18n.t("rss_description.#{filter}") - @atom_link = "#{Discourse.base_url}/#{filter}.rss" - @topic_list = TopicQuery.new(nil, order: 'created').public_send("list_#{filter}") - - render 'list', formats: [:rss] + self.send(filter, category: @category.id) end end @@ -127,14 +115,26 @@ class ListController < ApplicationController end end + def latest_feed + discourse_expires_in 1.minute + + @title = "#{SiteSetting.title} - #{I18n.t("rss_description.latest")}" + @link = "#{Discourse.base_url}/latest" + @atom_link = "#{Discourse.base_url}/latest.rss" + @description = I18n.t("rss_description.latest") + @topic_list = TopicQuery.new(nil, order: 'created').list_latest + + render 'list', formats: [:rss] + end + def category_feed guardian.ensure_can_see!(@category) discourse_expires_in 1.minute @title = @category.name @link = "#{Discourse.base_url}#{@category.url}" - @description = "#{I18n.t('topics_in_category', category: @category.name)} #{@category.description}" @atom_link = "#{Discourse.base_url}#{@category.url}.rss" + @description = "#{I18n.t('topics_in_category', category: @category.name)} #{@category.description}" @topic_list = TopicQuery.new.list_new_in_category(@category) render 'list', formats: [:rss] @@ -147,15 +147,15 @@ class ListController < ApplicationController end def category_top - top({ category: @category.id }) + top(category: @category.id) end def category_none_top - top({ category: @category.id, no_subcategories: true }) + top(category: @category.id, no_subcategories: true) end def parent_category_category_top - top({ category: @category.id }) + top(category: @category.id) end TopTopic.periods.each do |period| @@ -163,6 +163,11 @@ class ListController < ApplicationController top_options = build_topic_list_options top_options.merge!(options) if options top_options[:per_page] = SiteSetting.topics_per_period_in_top_page + + if "top".freeze == current_homepage + top_options.merge!(exclude_category_ids: get_excluded_category_ids(top_options[:category])) + end + user = list_target_user list = TopicQuery.new(user, top_options).list_top_for(period) list.for_period = period @@ -177,15 +182,15 @@ class ListController < ApplicationController end define_method("category_top_#{period}") do - self.send("top_#{period}", { category: @category.id }) + self.send("top_#{period}", category: @category.id) end define_method("category_none_top_#{period}") do - self.send("top_#{period}", { category: @category.id, no_subcategories: true }) + self.send("top_#{period}", category: @category.id, no_subcategories: true) end define_method("parent_category_category_top_#{period}") do - self.send("top_#{period}", { category: @category.id }) + self.send("top_#{period}", category: @category.id) end end @@ -204,16 +209,15 @@ class ListController < ApplicationController end end - private def page_params(opts = nil) opts ||= {} - route_params = {format: 'json'} - route_params[:category] = @category.slug_for_url if @category + route_params = { format: 'json' } + route_params[:category] = @category.slug_for_url if @category route_params[:parent_category] = @category.parent_category.slug_for_url if @category && @category.parent_category - route_params[:order] = opts[:order] if opts[:order].present? - route_params[:ascending] = opts[:ascending] if opts[:ascending].present? + route_params[:order] = opts[:order] if opts[:order].present? + route_params[:ascending] = opts[:ascending] if opts[:ascending].present? route_params end @@ -235,11 +239,10 @@ class ListController < ApplicationController end def build_topic_list_options - # exclude_category = 1. from params / 2. parsed from top menu / 3. nil options = { page: params[:page], topic_ids: param_to_integer_list(:topic_ids), - exclude_category: (params[:exclude_category] || select_menu_item.try(:filter)), + exclude_category_ids: params[:exclude_category_ids], category: params[:category], order: params[:order], ascending: params[:ascending], @@ -257,17 +260,6 @@ class ListController < ApplicationController options end - def select_menu_item - menu_item = SiteSetting.top_menu_items.select do |mu| - (mu.has_specific_category? && mu.specific_category == @category.try(:slug)) || - action_name == mu.name || - (action_name.include?("top") && mu.name == "top") - end.first - - menu_item = nil if menu_item.try(:has_specific_category?) && menu_item.specific_category == @category.try(:slug) - menu_item - end - def list_target_user if params[:user_id] && guardian.is_staff? User.find(params[:user_id].to_i) @@ -290,25 +282,16 @@ class ListController < ApplicationController url.sub('.json?','?') end - def generate_top_lists(options) - top = TopLists.new - - options[:per_page] = SiteSetting.topics_per_period_in_top_summary - topic_query = TopicQuery.new(current_user, options) - - periods = [ListController.best_period_for(current_user.try(:previous_visit_at), options[:category])] - - periods.each { |period| top.send("#{period}=", topic_query.list_top_for(period)) } - - top + def get_excluded_category_ids(current_category=nil) + exclude_category_ids = Category.where(suppress_from_homepage: true) + exclude_category_ids = exclude_category_ids.where.not(id: current_category) if current_category + exclude_category_ids.pluck(:id) end def self.best_period_for(previous_visit_at, category_id=nil) best_periods_for(previous_visit_at).each do |period| top_topics = TopTopic.where("#{period}_score > 0") - if category_id - top_topics = top_topics.joins(:topic).where("topics.category_id = ?", category_id) - end + top_topics = top_topics.joins(:topic).where("topics.category_id = ?", category_id) if category_id return period if top_topics.count >= SiteSetting.topics_per_period_in_top_page end # default period is yearly @@ -318,8 +301,8 @@ class ListController < ApplicationController def self.best_periods_for(date) date ||= 1.year.ago periods = [] - periods << :daily if date > 8.days.ago - periods << :weekly if date > 35.days.ago + periods << :daily if date > 8.days.ago + periods << :weekly if date > 35.days.ago periods << :monthly if date > 180.days.ago periods << :yearly periods diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 407d63ce2..9831ee88c 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -7,7 +7,11 @@ class NotificationsController < ApplicationController def index user = current_user if params[:recent].present? - notifications = Notification.recent_report(current_user, 15) + + limit = (params[:limit] || 15).to_i + limit = 50 if limit > 50 + + notifications = Notification.recent_report(current_user, limit) if notifications.present? # ordering can be off due to PMs diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 1e428c6e8..b7c321ed9 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -37,7 +37,8 @@ class PostsController < ApplicationController .where('posts.id <= ?', last_post_id) .where('posts.id > ?', last_post_id - 50) .includes(topic: :category) - .includes(:user) + .includes(user: :primary_group) + .includes(:reply_to_user) .limit(50) # Remove posts the user doesn't have permission to see # This isn't leaking any information we weren't already through the post ID numbers @@ -58,6 +59,7 @@ class PostsController < ApplicationController scope: guardian, root: 'latest_posts', add_raw: true, + add_title: true, all_post_actions: counts) ) end @@ -463,6 +465,10 @@ class PostsController < ApplicationController result[:is_warning] = false end + if current_user.staff? && SiteSetting.enable_whispers? && params[:whisper] == "true" + result[:post_type] = Post.types[:whisper] + end + PostRevisor.tracked_topic_fields.each_key do |f| params.permit(f => []) result[f] = params[f] if params.has_key?(f) diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 4446a0af9..0b857278f 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -9,7 +9,20 @@ class SearchController < ApplicationController end def show - search = Search.new(params[:q], type_filter: 'topic', guardian: guardian, include_blurbs: true, blurb_length: 300) + search_args = { + type_filter: 'topic', + guardian: guardian, + include_blurbs: true, + blurb_length: 300 + } + + context, type = lookup_search_context + if context + search_args[:search_context] = context + search_args[:type_filter] = type if type + end + + search = Search.new(params[:q], search_args) result = search.execute serializer = serialize_data(result, GroupedSearchResultSerializer, result: result) @@ -34,7 +47,29 @@ class SearchController < ApplicationController search_args[:include_blurbs] = params[:include_blurbs] == "true" if params[:include_blurbs].present? search_args[:search_for_id] = true if params[:search_for_id].present? + context,type = lookup_search_context + if context + search_args[:search_context] = context + search_args[:type_filter] = type if type + end + + search = Search.new(params[:term], search_args.symbolize_keys) + result = search.execute + render_serialized(result, GroupedSearchResultSerializer, result: result) + end + + protected + + def lookup_search_context + + return if params[:skip_context] == "true" + search_context = params[:search_context] + unless search_context + if (context = params[:context]) && (id = params[:context_id]) + search_context = {type: context, id: id} + end + end if search_context.present? raise Discourse::InvalidParameters.new(:search_context) unless SearchController.valid_context_types.include?(search_context[:type]) @@ -43,23 +78,21 @@ class SearchController < ApplicationController # A user is found by username context_obj = nil if ['user','private_messages'].include? search_context[:type] - context_obj = User.find_by(username_lower: params[:search_context][:id].downcase) + context_obj = User.find_by(username_lower: search_context[:id].downcase) else klass = search_context[:type].classify.constantize - context_obj = klass.find_by(id: params[:search_context][:id]) + context_obj = klass.find_by(id: search_context[:id]) end + type_filter = nil if search_context[:type] == 'private_messages' - search_args[:type_filter] = 'private_messages' + type_filter = 'private_messages' end guardian.ensure_can_see!(context_obj) - search_args[:search_context] = context_obj - end - search = Search.new(params[:term], search_args.symbolize_keys) - result = search.execute - render_serialized(result, GroupedSearchResultSerializer, result: result) + [context_obj, type_filter] + end end end diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 6b2a8baa1..a2a771a35 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -11,8 +11,16 @@ class SessionController < ApplicationController end def sso - if SiteSetting.enable_sso - redirect_to DiscourseSingleSignOn.generate_url(params[:return_path] || path('/')) + return_path = if params[:return_path] + params[:return_path] + elsif session[:destination_url] + URI::parse(session[:destination_url]).path + else + path('/') + end + + if SiteSetting.enable_sso? + redirect_to DiscourseSingleSignOn.generate_url(return_path) else render nothing: true, status: 404 end diff --git a/app/controllers/user_avatars_controller.rb b/app/controllers/user_avatars_controller.rb index ed8f31e4a..266648107 100644 --- a/app/controllers/user_avatars_controller.rb +++ b/app/controllers/user_avatars_controller.rb @@ -13,7 +13,10 @@ class UserAvatarsController < ApplicationController user.create_user_avatar(user_id: user.id) unless user.user_avatar user.user_avatar.update_gravatar! - render json: { upload_id: user.user_avatar.gravatar_upload_id } + render json: { + gravatar_upload_id: user.user_avatar.gravatar_upload_id, + gravatar_avatar_template: User.avatar_template(user.username, user.user_avatar.gravatar_upload_id) + } else raise Discourse::NotFound end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index bb550483d..bb361de6f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -515,13 +515,13 @@ class UsersController < ApplicationController results = UserSearch.new(term, topic_id: topic_id, topic_allowed_users: topic_allowed_users, searching_user: current_user).search - user_fields = [:username, :upload_avatar_template, :uploaded_avatar_id] + user_fields = [:username, :upload_avatar_template] user_fields << :name if SiteSetting.enable_names? - to_render = { users: results.as_json(only: user_fields, methods: :avatar_template) } + to_render = { users: results.as_json(only: user_fields, methods: [:avatar_template]) } if params[:include_groups] == "true" - to_render[:groups] = Group.search_group(term, current_user).map {|m| {:name=>m.name, :usernames=> m.usernames.split(",")} } + to_render[:groups] = Group.search_group(term, current_user).map { |m| { name: m.name, usernames: m.usernames.split(",") } } end render json: to_render @@ -531,13 +531,15 @@ class UsersController < ApplicationController user = fetch_user_from_params guardian.ensure_can_edit!(user) + type = params[:type] upload_id = params[:upload_id] user.uploaded_avatar_id = upload_id - # ensure we associate the custom avatar properly - if upload_id && user.user_avatar.custom_upload_id != upload_id + if type == "uploaded" || type == "custom" user.user_avatar.custom_upload_id = upload_id + elsif type == "gravatar" + user.user_avatar.gravatar_upload_id = upload_id end user.save! @@ -572,7 +574,7 @@ class UsersController < ApplicationController end def read_faq - if(user = current_user) + if user = current_user user.user_stat.read_faq = 1.second.ago user.user_stat.save end diff --git a/app/jobs/scheduled/dashboard_stats.rb b/app/jobs/scheduled/dashboard_stats.rb index ab716b575..3a04c1070 100644 --- a/app/jobs/scheduled/dashboard_stats.rb +++ b/app/jobs/scheduled/dashboard_stats.rb @@ -5,7 +5,7 @@ module Jobs every 30.minutes def execute(args) - stats = AdminDashboardData.new.as_json + stats = AdminDashboardData.fetch_stats set_cache(AdminDashboardData, stats) stats end diff --git a/app/jobs/scheduled/ensure_db_consistency.rb b/app/jobs/scheduled/ensure_db_consistency.rb index 274c666de..c565023e5 100644 --- a/app/jobs/scheduled/ensure_db_consistency.rb +++ b/app/jobs/scheduled/ensure_db_consistency.rb @@ -13,6 +13,7 @@ module Jobs UserStat.update_view_counts(13.hours.ago) Topic.ensure_consistency! Badge.ensure_consistency! + CategoryUser.ensure_consistency! end end end diff --git a/app/jobs/scheduled/periodical_updates.rb b/app/jobs/scheduled/periodical_updates.rb index 2254235a9..625d67b8d 100644 --- a/app/jobs/scheduled/periodical_updates.rb +++ b/app/jobs/scheduled/periodical_updates.rb @@ -32,6 +32,16 @@ module Jobs user_id = hash[:profile].user_id Discourse.handle_job_exception(hash[:ex], error_context(args, "Rebaking user id #{user_id}", user_id: user_id)) end + + TopicUser.cap_unread_backlog! + + offset = (SiteSetting.max_tracked_new_unread * (2/5.0)).to_i + last_new_topic = Topic.order('created_at desc').offset(offset).select(:created_at).first + if last_new_topic + SiteSetting.min_new_topics_time = last_new_topic.created_at.to_i + end + + nil end end diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 438e63f6f..52e91f644 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -41,11 +41,8 @@ class UserNotifications < ActionMailer::Base build_email( user.email, template: "user_notifications.account_created", email_token: opts[:email_token]) end - # On error, use english def short_date(dt) I18n.l(dt, format: :short) - rescue I18n::MissingTranslationData - I18n.l(dt, format: :short, locale: 'en') end def digest(user, opts={}) diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index abe94003d..425352376 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -32,30 +32,42 @@ class AdminDashboardData MOBILE_REPORTS ||= ['mobile_visits'] + ApplicationRequest.req_types.keys.select {|r| r =~ /mobile/}.map { |r| r + "_reqs" } + def self.add_problem_check(*syms, &blk) + @problem_syms.push(*syms) if syms + @problem_blocks << blk if blk + end + class << self; attr_reader :problem_syms, :problem_blocks; end + def problems - [ rails_env_check, - ruby_version_check, - host_names_check, - gc_checks, - sidekiq_check || queue_size_check, - ram_check, - google_oauth2_config_check, - facebook_config_check, - twitter_config_check, - github_config_check, - s3_config_check, - image_magick_check, - failing_emails_check, - default_logo_check, - contact_email_check, - send_consumer_email_check, - title_check, - site_description_check, - site_contact_username_check, - notification_email_check - ].compact + problems = [] + AdminDashboardData.problem_syms.each do |sym| + problems << send(sym) + end + AdminDashboardData.problem_blocks.each do |blk| + problems << instance_exec(&blk) + end + problems.compact end + # used for testing + def self.reset_problem_checks + @problem_syms = [] + @problem_blocks = [] + + add_problem_check :rails_env_check, :ruby_version_check, :host_names_check, + :gc_checks, :ram_check, :google_oauth2_config_check, + :facebook_config_check, :twitter_config_check, + :github_config_check, :s3_config_check, :image_magick_check, + :failing_emails_check, :default_logo_check, :contact_email_check, + :send_consumer_email_check, :title_check, + :site_description_check, :site_contact_username_check, + :notification_email_check + + add_problem_check do + sidekiq_check || queue_size_check + end + end + reset_problem_checks def self.fetch_stats AdminDashboardData.new.as_json diff --git a/app/models/auto_track_duration_site_setting.rb b/app/models/auto_track_duration_site_setting.rb index 6b10f80a5..b40ff6e0e 100644 --- a/app/models/auto_track_duration_site_setting.rb +++ b/app/models/auto_track_duration_site_setting.rb @@ -3,7 +3,8 @@ require_dependency 'enum_site_setting' class AutoTrackDurationSiteSetting < EnumSiteSetting def self.valid_value?(val) - values.any? { |v| v[:value].to_s == val.to_s } + val.to_i.to_s == val.to_s && + values.any? { |v| v[:value] == val.to_i } end def self.values diff --git a/app/models/category.rb b/app/models/category.rb index 1f6424d71..311f3fad5 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -79,7 +79,7 @@ class Category < ActiveRecord::Base # permission is just used by serialization # we may consider wrapping this in another spot - attr_accessor :displayable_topics, :permission, :subcategory_ids, :notification_level + attr_accessor :displayable_topics, :permission, :subcategory_ids, :notification_level, :has_children def self.last_updated_at order('updated_at desc').limit(1).pluck(:updated_at).first.to_i diff --git a/app/models/category_list.rb b/app/models/category_list.rb index b5cae822b..6f445feaa 100644 --- a/app/models/category_list.rb +++ b/app/models/category_list.rb @@ -66,6 +66,8 @@ class CategoryList @categories = @categories.where('categories.parent_category_id = ?', @options[:parent_category_id].to_i) end + @categories = @categories.where(suppress_from_homepage: false) if @options[:is_homepage] + if SiteSetting.fixed_category_positions @categories = @categories.order('position ASC').order('id ASC') else @@ -76,10 +78,23 @@ class CategoryList end if latest_post_only? - @categories = @categories.includes(:latest_post => {:topic => :last_poster} ) + @categories = @categories.includes(latest_post: { topic: :last_poster }) end @categories = @categories.to_a + + category_user = {} + unless @guardian.anonymous? + category_user = Hash[*CategoryUser.where(user: @guardian.user).pluck(:category_id, :notification_level).flatten] + end + + allowed_topic_create = Set.new(Category.topic_create_allowed(@guardian).pluck(:id)) + @categories.each do |category| + category.notification_level = category_user[category.id] + category.permission = CategoryGroup.permission_types[:full] if allowed_topic_create.include?(category.id) + category.has_children = category.subcategories.present? + end + if @options[:parent_category_id].blank? subcategories = {} to_delete = Set.new diff --git a/app/models/category_user.rb b/app/models/category_user.rb index 1fb537f54..cd7b719af 100644 --- a/app/models/category_user.rb +++ b/app/models/category_user.rb @@ -92,6 +92,10 @@ class CategoryUser < ActiveRecord::Base ) end + def self.ensure_consistency! + exec_sql("DELETE FROM category_users WHERE user_id NOT IN (SELECT id FROM users)") + end + private_class_method :apply_default_to_topic, :remove_default_from_topic end diff --git a/app/models/digest_email_site_setting.rb b/app/models/digest_email_site_setting.rb index 688104950..5276fe3b3 100644 --- a/app/models/digest_email_site_setting.rb +++ b/app/models/digest_email_site_setting.rb @@ -3,16 +3,17 @@ require_dependency 'enum_site_setting' class DigestEmailSiteSetting < EnumSiteSetting def self.valid_value?(val) - val.blank? or values.any? { |v| v[:value] == val.to_s } + val.to_i.to_s == val.to_s && + values.any? { |v| v[:value] == val.to_i } end def self.values @values ||= [ - {name: 'never', value: '' }, - {name: 'daily', value: '1' }, - {name: 'every_three_days', value: '3' }, - {name: 'weekly', value: '7' }, - {name: 'every_two_weeks', value: '14' } + { name: 'never', value: 0 }, + { name: 'daily', value: 1 }, + { name: 'every_three_days', value: 3 }, + { name: 'weekly', value: 7 }, + { name: 'every_two_weeks', value: 14 } ] end diff --git a/app/models/new_topic_duration_site_setting.rb b/app/models/new_topic_duration_site_setting.rb index 92a00ea1c..ccc091a8b 100644 --- a/app/models/new_topic_duration_site_setting.rb +++ b/app/models/new_topic_duration_site_setting.rb @@ -3,7 +3,8 @@ require_dependency 'enum_site_setting' class NewTopicDurationSiteSetting < EnumSiteSetting def self.valid_value?(val) - values.any? { |v| v[:value].to_s == val.to_s } + val.to_i.to_s == val.to_s && + values.any? { |v| v[:value] == val.to_i } end def self.values diff --git a/app/models/notification.rb b/app/models/notification.rb index 842d65726..e142f57b2 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -77,8 +77,12 @@ class Notification < ActiveRecord::Base # Be wary of calling this frequently. O(n) JSON parsing can suck. def data_hash @data_hash ||= begin + return nil if data.blank? - JSON.parse(data).with_indifferent_access + parsed = JSON.parse(data) + return nil if parsed.blank? + + parsed.with_indifferent_access end end diff --git a/app/models/post.rb b/app/models/post.rb index 940cb17cf..3cf1fb367 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -74,7 +74,7 @@ class Post < ActiveRecord::Base end def self.types - @types ||= Enum.new(:regular, :moderator_action, :small_action) + @types ||= Enum.new(:regular, :moderator_action, :small_action, :whisper) end def self.cook_methods @@ -96,15 +96,24 @@ class Post < ActiveRecord::Base end def publish_change_to_clients!(type) - # special failsafe for posts missing topics - # consistency checks should fix, but message + + channel = "/topic/#{topic_id}" + msg = { id: id, + post_number: post_number, + updated_at: Time.now, + type: type } + + # special failsafe for posts missing topics consistency checks should fix, but message # is safe to skip - MessageBus.publish("/topic/#{topic_id}", { - id: id, - post_number: post_number, - updated_at: Time.now, - type: type - }, group_ids: topic.secure_group_ids) if topic + return unless topic + + # Whispers should not be published to everyone + if post_type == Post.types[:whisper] + user_ids = User.where('admin or moderator or id = ?', user_id).pluck(:id) + MessageBus.publish(channel, msg, user_ids: user_ids) + else + MessageBus.publish(channel, msg, group_ids: topic.secure_group_ids) + end end def trash!(trashed_by=nil) @@ -498,6 +507,7 @@ class Post < ActiveRecord::Base args[:image_sizes] = image_sizes if image_sizes.present? args[:invalidate_oneboxes] = true if invalidate_oneboxes.present? Jobs.enqueue(:process_post, args) + DiscourseEvent.trigger(:after_trigger_post_process, self) end def self.public_posts_count_per_day(start_date, end_date, category_id=nil) diff --git a/app/models/report.rb b/app/models/report.rb index 1e924fc0d..204bc3e9b 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -114,7 +114,7 @@ class Report def self.report_time_to_first_response(report) report.data = [] - Topic.time_to_first_response_per_day(report.start_date, report.end_date, report.category_id).each do |r| + Topic.time_to_first_response_per_day(report.start_date, report.end_date, {category_id: report.category_id}).each do |r| report.data << { x: Date.parse(r["date"]), y: r["hours"].to_f.round(2) } end report.total = Topic.time_to_first_response_total(category_id: report.category_id) diff --git a/app/models/site.rb b/app/models/site.rb index 719cf1d71..c6a3313f1 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -30,7 +30,7 @@ class Site end def groups - @groups ||= Group.order(:name).map { |g| {:id => g.id, :name => g.name}} + @groups ||= Group.order(:name).map { |g| { id: g.id, name: g.name } } end def user_fields @@ -41,7 +41,7 @@ class Site @categories ||= begin categories = Category .secured(@guardian) - .includes(:topic_only_relative_url) + .includes(:topic_only_relative_url, :subcategories) .order(:position) unless SiteSetting.allow_uncategorized_topics @@ -62,14 +62,19 @@ class Site categories.each do |category| category.notification_level = category_user[category.id] category.permission = CategoryGroup.permission_types[:full] if allowed_topic_create.include?(category.id) + category.has_children = category.subcategories.present? by_id[category.id] = category end - categories.reject! {|c| c.parent_category_id && !by_id[c.parent_category_id]} + categories.reject! { |c| c.parent_category_id && !by_id[c.parent_category_id] } categories end end + def suppressed_from_homepage_category_ids + categories.select { |c| c.suppress_from_homepage == true }.map(&:id) + end + def archetypes Archetype.list.reject { |t| t.id == Archetype.private_message } end diff --git a/app/models/topic.rb b/app/models/topic.rb index 442fb43c2..ccd21de75 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -218,6 +218,13 @@ class Topic < ActiveRecord::Base end end + def visible_post_types(viewed_by=nil) + types = Post.types + result = [types[:regular], types[:moderator_action], types[:small_action]] + result << types[:whisper] if viewed_by.try(:staff?) + result + end + def self.top_viewed(max = 10) Topic.listable_topics.visible.secured.order('views desc').limit(max) end @@ -904,12 +911,13 @@ class Topic < ActiveRecord::Base builder.where("p.deleted_at IS NULL") builder.where("p.post_number > 1") builder.where("p.user_id != t.user_id") + builder.where("p.user_id in (:user_ids)", {user_ids: opts[:user_ids]}) if opts[:user_ids] builder.where("EXTRACT(EPOCH FROM p.created_at - t.created_at) > 0") builder.exec end - def self.time_to_first_response_per_day(start_date, end_date, category_id=nil) - time_to_first_response(TIME_TO_FIRST_RESPONSE_SQL, start_date: start_date, end_date: end_date, category_id: category_id) + def self.time_to_first_response_per_day(start_date, end_date, opts={}) + time_to_first_response(TIME_TO_FIRST_RESPONSE_SQL, opts.merge({start_date: start_date, end_date: end_date})) end def self.time_to_first_response_total(opts=nil) diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index b6fae1518..0da784219 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -106,11 +106,12 @@ class TopicTrackingState WHEN COALESCE(u.new_topic_duration_minutes, :default_duration) = :always THEN u.created_at WHEN COALESCE(u.new_topic_duration_minutes, :default_duration) = :last_visit THEN COALESCE(u.previous_visit_at,u.created_at) ELSE (:now::timestamp - INTERVAL '1 MINUTE' * COALESCE(u.new_topic_duration_minutes, :default_duration)) - END, us.new_since)", + END, us.new_since, :min_date)", now: DateTime.now, last_visit: User::NewTopicDuration::LAST_VISIT, always: User::NewTopicDuration::ALWAYS, - default_duration: SiteSetting.default_other_new_topic_duration_minutes + default_duration: SiteSetting.default_other_new_topic_duration_minutes, + min_date: Time.at(SiteSetting.min_new_topics_time).to_datetime ).where_values[0] end @@ -125,19 +126,49 @@ class TopicTrackingState # This code needs to be VERY efficient as it is triggered via the message bus and may steal # cycles from usual requests # - - unread = TopicQuery.unread_filter(Topic).where_values.join(" AND ") - new = TopicQuery.new_filter(Topic, "xxx").where_values.join(" AND ").gsub!("'xxx'", treat_as_new_topic_clause) + # + sql = report_raw_sql(topic_id: topic_id) sql = <<SQL WITH x AS ( - SELECT u.id AS user_id, + #{sql} + ) SELECT * FROM x LIMIT #{SiteSetting.max_tracked_new_unread.to_i} +SQL + + SqlBuilder.new(sql) + .map_exec(TopicTrackingState, user_id: user_id, topic_id: topic_id) + + end + + + def self.report_raw_sql(opts=nil) + + unread = + if opts && opts[:skip_unread] + "1=0" + else + TopicQuery.unread_filter(Topic).where_values.join(" AND ") + end + + new = + if opts && opts[:skip_new] + "1=0" + else + TopicQuery.new_filter(Topic, "xxx").where_values.join(" AND ").gsub!("'xxx'", treat_as_new_topic_clause) + end + + select = (opts && opts[:select]) || " + u.id AS user_id, topics.id AS topic_id, topics.created_at, highest_post_number, last_read_post_number, c.id AS category_id, - tu.notification_level + tu.notification_level" + + + sql = <<SQL + SELECT #{select} FROM topics JOIN users u on u.id = :user_id JOIN user_stats AS us ON us.user_id = u.id @@ -162,15 +193,11 @@ class TopicTrackingState SQL - if topic_id + if opts && opts[:topic_id] sql << " AND topics.id = :topic_id" end - sql << " ORDER BY topics.bumped_at DESC ) SELECT * FROM x LIMIT #{SiteSetting.max_tracked_new_unread.to_i}" - - SqlBuilder.new(sql) - .map_exec(TopicTrackingState, user_id: user_id, topic_id: topic_id) - + sql << " ORDER BY topics.bumped_at DESC" end end diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index 01d57c580..d8dc80842 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -284,6 +284,43 @@ SQL builder.exec(action_type_id: PostActionType.types[action_type]) end + # cap number of unread topics at count, bumping up highest_seen / last_read if needed + def self.cap_unread!(user_id, count) + sql = <<SQL + UPDATE topic_users tu + SET last_read_post_number = max_number, + highest_seen_post_number = max_number + FROM ( + SELECT MAX(post_number) max_number, p.topic_id FROM posts p + WHERE deleted_at IS NULL + GROUP BY p.topic_id + ) m + WHERE tu.user_id = :user_id AND + m.topic_id = tu.topic_id AND + tu.topic_id IN ( + #{TopicTrackingState.report_raw_sql(skip_new: true, select: "topics.id")} + offset :count + ) +SQL + + TopicUser.exec_sql(sql, user_id: user_id, count: count) + end + + def self.unread_cap_key + "unread_cap_user".freeze + end + + def self.cap_unread_later(user_id) + $redis.hset TopicUser.unread_cap_key, user_id, "" + end + + def self.cap_unread_backlog! + $redis.hkeys(unread_cap_key).map(&:to_i).each do |user_id| + cap_unread!(user_id, (SiteSetting.max_tracked_new_unread * (2/5.0)).to_i) + $redis.hdel unread_cap_key, user_id + end + end + def self.ensure_consistency!(topic_id=nil) update_post_action_cache(topic_id: topic_id) diff --git a/app/models/topic_view_item.rb b/app/models/topic_view_item.rb index 73bdc636c..958abca82 100644 --- a/app/models/topic_view_item.rb +++ b/app/models/topic_view_item.rb @@ -8,7 +8,8 @@ class TopicViewItem < ActiveRecord::Base def self.add(topic_id, ip, user_id=nil, at=nil, skip_redis=false) # Only store a view once per day per thing per user per ip - redis_key = "view:#{topic_id}:#{Date.today}" + at ||= Date.today + redis_key = "view:#{topic_id}:#{at}" if user_id redis_key << ":user-#{user_id}" else @@ -19,8 +20,6 @@ class TopicViewItem < ActiveRecord::Base skip_redis || $redis.expire(redis_key, SiteSetting.topic_view_duration_hours.hours) TopicViewItem.transaction do - at ||= Date.today - # this is called real frequently, working hard to avoid exceptions sql = "INSERT INTO topic_views (topic_id, ip_address, viewed_at, user_id) SELECT :topic_id, :ip_address, :viewed_at, :user_id diff --git a/app/models/trust_level_setting.rb b/app/models/trust_level_setting.rb index e76f25828..9ea838ad8 100644 --- a/app/models/trust_level_setting.rb +++ b/app/models/trust_level_setting.rb @@ -3,11 +3,12 @@ require_dependency 'enum_site_setting' class TrustLevelSetting < EnumSiteSetting def self.valid_value?(val) - valid_values.any? { |v| v.to_s == val.to_s } + val.to_i.to_s == val.to_s && + valid_values.any? { |v| v == val.to_i } end def self.values - @values ||= valid_values.map {|x| {name: x.to_s, value: x} } + @values ||= valid_values.map { |x| { name: x.to_s, value: x } } end def self.valid_values diff --git a/app/models/upload.rb b/app/models/upload.rb index 59a647f76..bcc468d44 100644 --- a/app/models/upload.rb +++ b/app/models/upload.rb @@ -50,7 +50,7 @@ class Upload < ActiveRecord::Base end # list of image types that will be cropped - CROPPED_IMAGE_TYPES ||= ["avatar", "profile_background", "card_background"] + CROPPED_IMAGE_TYPES ||= %w{avatar profile_background card_background} # options # - content_type @@ -100,6 +100,10 @@ class Upload < ActiveRecord::Base # optimize image ImageOptim.new.optimize_image!(file.path) rescue nil + + # correct size so it displays the optimized image size which is the only + # one that is stored + filesize = File.size(file.path) end # compute the sha of the file diff --git a/app/models/user.rb b/app/models/user.rb index 8aa61600b..3dfb2ed34 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -17,11 +17,12 @@ class User < ActiveRecord::Base has_many :posts has_many :notifications, dependent: :destroy has_many :topic_users, dependent: :destroy + has_many :category_users, dependent: :destroy has_many :topics has_many :user_open_ids, dependent: :destroy has_many :user_actions, dependent: :destroy has_many :post_actions, dependent: :destroy - has_many :user_badges, -> {where('user_badges.badge_id IN (SELECT id FROM badges where enabled)')}, dependent: :destroy + has_many :user_badges, -> { where('user_badges.badge_id IN (SELECT id FROM badges WHERE enabled)') }, dependent: :destroy has_many :badges, through: :user_badges has_many :email_logs, dependent: :delete_all has_many :post_timings @@ -305,10 +306,17 @@ class User < ActiveRecord::Base end def publish_notifications_state + # publish last notification json with the message so we + # can apply an update + notification = notifications.visible.order('notifications.id desc').first + json = NotificationSerializer.new(notification).as_json if notification + MessageBus.publish("/notification/#{id}", {unread_notifications: unread_notifications, unread_private_messages: unread_private_messages, - total_unread_notifications: total_unread_notifications}, + total_unread_notifications: total_unread_notifications, + last_notification: json + }, user_ids: [id] # only publish the notification to this user ) end @@ -446,26 +454,41 @@ class User < ActiveRecord::Base [((result << 5) - result) + char.ord].pack('L').unpack('l').first end - avatar_template = split_avatars[hash.abs % split_avatars.size] + split_avatars[hash.abs % split_avatars.size] end + else + system_avatar_template(username) + end + end + + def self.avatar_template(username, uploaded_avatar_id) + username ||= "" + return default_template(username) if !uploaded_avatar_id + hostname = RailsMultisite::ConnectionManagement.current_hostname + UserAvatar.local_avatar_template(hostname, username.downcase, uploaded_avatar_id) + end + + def self.system_avatar_template(username) + # TODO it may be worth caching this in a distributed cache, should be benched + if SiteSetting.external_system_avatars_enabled + url = SiteSetting.external_system_avatars_url.dup + url.gsub! "{color}", letter_avatar_color(username.downcase) + url.gsub! "{username}", username + url.gsub! "{first_letter}", username[0].downcase + url else "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" end end - def self.avatar_template(username,uploaded_avatar_id) - return default_template(username) if !uploaded_avatar_id + def self.letter_avatar_color(username) username ||= "" - hostname = RailsMultisite::ConnectionManagement.current_hostname - UserAvatar.local_avatar_template(hostname, username.downcase, uploaded_avatar_id) - end - - def self.letter_avatar_template(username) - "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" + color = LetterAvatar::COLORS[Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length] + color.map { |c| c.to_s(16).rjust(2, '0') }.join end def avatar_template - self.class.avatar_template(username,uploaded_avatar_id) + self.class.avatar_template(username, uploaded_avatar_id) end # The following count methods are somewhat slow - definitely don't use them in a loop. @@ -580,14 +603,16 @@ class User < ActiveRecord::Base def treat_as_new_topic_start_date duration = new_topic_duration_minutes || SiteSetting.default_other_new_topic_duration_minutes.to_i - [case duration + times = [case duration when User::NewTopicDuration::ALWAYS created_at when User::NewTopicDuration::LAST_VISIT previous_visit_at || user_stat.new_since else duration.minutes.ago - end, user_stat.new_since].max + end, user_stat.new_since, Time.at(SiteSetting.min_new_topics_time).to_datetime] + + times.max end def readable_name @@ -969,7 +994,7 @@ class User < ActiveRecord::Base def set_default_email_digest_frequency if has_attribute?(:email_digests) - if SiteSetting.default_email_digest_frequency.blank? + if SiteSetting.default_email_digest_frequency.to_i <= 0 self.email_digests = false else self.email_digests = true diff --git a/app/models/user_action.rb b/app/models/user_action.rb index 9b72a755d..30086d006 100644 --- a/app/models/user_action.rb +++ b/app/models/user_action.rb @@ -79,12 +79,21 @@ SQL def self.private_messages_stats(user_id, guardian) return unless guardian.can_see_private_messages?(user_id) # list the stats for: all/mine/unread (topic-based) - private_messages = Topic.where("topics.id IN (SELECT topic_id FROM topic_allowed_users WHERE user_id = #{user_id})") - .joins("LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{user_id})") - .private_messages - all = private_messages.count - mine = private_messages.where(user_id: user_id).count - unread = private_messages.where("tu.last_read_post_number IS NULL OR tu.last_read_post_number < topics.highest_post_number").count + + sql = <<SQL + SELECT COUNT(*) "all", + SUM(CASE WHEN t.user_id = :user_id THEN 1 ELSE 0 END) mine, + SUM(CASE WHEN tu.last_read_post_number IS NULL OR tu.last_read_post_number < t.highest_post_number THEN 1 ELSE 0 END) unread + FROM topics t + LEFT JOIN topic_users tu ON t.id = tu.topic_id AND tu.user_id = :user_id + WHERE t.deleted_at IS NULL AND + t.id IN (SELECT topic_id FROM topic_allowed_users WHERE user_id = :user_id) AND + t.archetype = 'private_message' + +SQL + + all,mine,unread = exec_sql(sql, user_id: user_id).values[0].map(&:to_i) + { all: all, mine: mine, unread: unread } end diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index f61df736b..8d57472c6 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -39,8 +39,7 @@ class UserAvatar < ActiveRecord::Base end def self.local_avatar_url(hostname, username, upload_id, size) - version = self.version(upload_id) - "#{Discourse.base_uri}/user_avatar/#{hostname}/#{username}/#{size}/#{version}.png" + self.local_avatar_template(hostname, username, upload_id).gsub("{size}", size.to_s) end def self.local_avatar_template(hostname, username, upload_id) @@ -49,8 +48,7 @@ class UserAvatar < ActiveRecord::Base end def self.external_avatar_url(user_id, upload_id, size) - version = self.version(upload_id) - "#{Discourse.store.absolute_base_url}/avatars/#{user_id}/#{size}/#{version}.png" + self.external_avatar_template(user_id, upload_id).gsub("{size}", size.to_s) end def self.external_avatar_template(user_id, upload_id) diff --git a/app/models/username_validator.rb b/app/models/username_validator.rb index 3b350d289..ac9adcc95 100644 --- a/app/models/username_validator.rb +++ b/app/models/username_validator.rb @@ -30,6 +30,9 @@ class UsernameValidator username_length_max? username_char_valid? username_first_char_valid? + username_last_char_valid? + username_no_double_special? + username_does_not_end_with_confusing_suffix? errors.empty? end @@ -58,15 +61,36 @@ class UsernameValidator def username_char_valid? return unless errors.empty? - if username =~ /[^A-Za-z0-9_]/ + if username =~ /[^A-Za-z0-9_\.\-]/ self.errors << I18n.t(:'user.username.characters') end end def username_first_char_valid? return unless errors.empty? - if username[0] =~ /[^A-Za-z0-9]/ + if username[0] =~ /[^A-Za-z0-9_]/ self.errors << I18n.t(:'user.username.must_begin_with_alphanumeric') end end + + def username_last_char_valid? + return unless errors.empty? + if username[-1] =~ /[^A-Za-z0-9_]/ + self.errors << I18n.t(:'user.username.must_end_with_alphanumeric') + end + end + + def username_no_double_special? + return unless errors.empty? + if username =~ /[\-_\.]{2,}/ + self.errors << I18n.t(:'user.username.must_not_contain_two_special_chars_in_seq') + end + end + + def username_does_not_end_with_confusing_suffix? + return unless errors.empty? + if username =~ /\.(json|gif|jpeg|png|htm|js|json|xml|woff|tif|html)/i + self.errors << I18n.t(:'user.username.must_not_contain_confusing_suffix') + end + end end diff --git a/app/serializers/admin_post_serializer.rb b/app/serializers/admin_post_serializer.rb index 2dd29df5c..a06328d83 100644 --- a/app/serializers/admin_post_serializer.rb +++ b/app/serializers/admin_post_serializer.rb @@ -3,7 +3,7 @@ class AdminPostSerializer < ApplicationSerializer attributes :id, :created_at, :post_number, - :name, :username, :avatar_template, :uploaded_avatar_id, + :name, :username, :avatar_template, :topic_id, :topic_slug, :topic_title, :category_id, :excerpt, @@ -29,10 +29,6 @@ class AdminPostSerializer < ApplicationSerializer object.user.avatar_template end - def uploaded_avatar_id - object.user.uploaded_avatar_id - end - def topic_slug topic.slug end diff --git a/app/serializers/basic_category_serializer.rb b/app/serializers/basic_category_serializer.rb index 45b824329..f62968a59 100644 --- a/app/serializers/basic_category_serializer.rb +++ b/app/serializers/basic_category_serializer.rb @@ -7,6 +7,7 @@ class BasicCategorySerializer < ApplicationSerializer :slug, :topic_count, :post_count, + :position, :description, :description_text, :topic_url, @@ -17,7 +18,8 @@ class BasicCategorySerializer < ApplicationSerializer :logo_url, :background_url, :can_edit, - :topic_template + :topic_template, + :has_children def include_parent_category_id? parent_category_id diff --git a/app/serializers/basic_post_serializer.rb b/app/serializers/basic_post_serializer.rb index 04b91ecad..8969d19a0 100644 --- a/app/serializers/basic_post_serializer.rb +++ b/app/serializers/basic_post_serializer.rb @@ -4,7 +4,6 @@ class BasicPostSerializer < ApplicationSerializer :name, :username, :avatar_template, - :uploaded_avatar_id, :created_at, :cooked, :cooked_hidden @@ -21,13 +20,10 @@ class BasicPostSerializer < ApplicationSerializer object.user.try(:avatar_template) end - def uploaded_avatar_id - object.user.try(:uploaded_avatar_id) - end - def cooked_hidden object.hidden && !scope.is_staff? end + def include_cooked_hidden? cooked_hidden end diff --git a/app/serializers/basic_user_serializer.rb b/app/serializers/basic_user_serializer.rb index 8911291f3..8880c8dbd 100644 --- a/app/serializers/basic_user_serializer.rb +++ b/app/serializers/basic_user_serializer.rb @@ -1,5 +1,5 @@ class BasicUserSerializer < ApplicationSerializer - attributes :id, :username, :uploaded_avatar_id, :avatar_template + attributes :id, :username, :avatar_template def include_name? SiteSetting.enable_names? @@ -9,7 +9,7 @@ class BasicUserSerializer < ApplicationSerializer if Hash === object User.avatar_template(user[:username], user[:uploaded_avatar_id]) else - object.avatar_template + user.try(:avatar_template) end end diff --git a/app/serializers/category_serializer.rb b/app/serializers/category_serializer.rb index 175765209..6138f1d1b 100644 --- a/app/serializers/category_serializer.rb +++ b/app/serializers/category_serializer.rb @@ -8,8 +8,10 @@ class CategorySerializer < BasicCategorySerializer :position, :email_in, :email_in_allow_strangers, + :suppress_from_homepage, :can_delete, :cannot_delete_reason, + :is_special, :allow_badges, :custom_fields @@ -36,6 +38,15 @@ class CategorySerializer < BasicCategorySerializer true end + def include_is_special? + [SiteSetting.lounge_category_id, SiteSetting.meta_category_id, SiteSetting.staff_category_id, SiteSetting.uncategorized_category_id] + .include? object.id + end + + def is_special + true + end + def include_can_delete? scope && scope.can_delete?(object) end @@ -56,4 +67,8 @@ class CategorySerializer < BasicCategorySerializer scope && scope.can_edit?(object) end + def include_suppress_from_homepage? + scope && scope.can_edit?(object) + end + end diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index cc689e42b..7236c2dc9 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -30,14 +30,19 @@ class CurrentUserSerializer < BasicUserSerializer :dismissed_banner_key, :is_anonymous, :post_queue_new_count, - :show_queued_posts + :show_queued_posts, + :read_faq def include_site_flagged_posts_count? object.staff? end + def read_faq + object.user_stat.read_faq? + end + def topic_count - object.topics.count + object.user_stat.topic_count end def reply_count diff --git a/app/serializers/post_action_user_serializer.rb b/app/serializers/post_action_user_serializer.rb index b69a27348..72dbd8e4d 100644 --- a/app/serializers/post_action_user_serializer.rb +++ b/app/serializers/post_action_user_serializer.rb @@ -9,10 +9,6 @@ class PostActionUserSerializer < BasicUserSerializer object.user.username end - def uploaded_avatar_id - object.user.uploaded_avatar_id - end - def avatar_template object.user.avatar_template end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 4111fbbde..a10f9dbf6 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -5,6 +5,7 @@ class PostSerializer < BasicPostSerializer :topic_view, :parent_post, :add_raw, + :add_title, :single_post_link_counts, :draft_sequence, :post_actions, @@ -28,6 +29,9 @@ class PostSerializer < BasicPostSerializer :yours, :topic_id, :topic_slug, + :topic_title, + :topic_html_title, + :category_id, :display_username, :primary_group_name, :version, @@ -73,6 +77,30 @@ class PostSerializer < BasicPostSerializer object.try(:topic).try(:slug) end + def include_topic_title? + @add_title + end + + def include_topic_html_title? + @add_title + end + + def include_category_id? + @add_title + end + + def topic_title + object.topic.title + end + + def topic_html_title + object.topic.fancy_title + end + + def category_id + object.topic.category_id + end + def moderator? !!(object.try(:user).try(:moderator?)) end @@ -149,8 +177,7 @@ class PostSerializer < BasicPostSerializer def reply_to_user { username: object.reply_to_user.username, - avatar_template: object.reply_to_user.avatar_template, - uploaded_avatar_id: object.reply_to_user.uploaded_avatar_id + avatar_template: object.reply_to_user.avatar_template } end diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index 38c30737b..dec52ed2f 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -11,7 +11,8 @@ class SiteSerializer < ApplicationSerializer :uncategorized_category_id, # this is hidden so putting it here :is_readonly, :disabled_plugins, - :user_field_max_length + :user_field_max_length, + :suppressed_from_homepage_category_ids has_many :categories, serializer: BasicCategorySerializer, embed: :objects has_many :post_action_types, embed: :objects diff --git a/app/serializers/topic_post_count_serializer.rb b/app/serializers/topic_post_count_serializer.rb index 586f9f2d7..c780d1203 100644 --- a/app/serializers/topic_post_count_serializer.rb +++ b/app/serializers/topic_post_count_serializer.rb @@ -14,8 +14,4 @@ class TopicPostCountSerializer < BasicUserSerializer object[:post_count] end - def uploaded_avatar_id - object[:user].uploaded_avatar_id - end - end diff --git a/app/serializers/user_action_serializer.rb b/app/serializers/user_action_serializer.rb index 8b3939963..d9609ff9b 100644 --- a/app/serializers/user_action_serializer.rb +++ b/app/serializers/user_action_serializer.rb @@ -26,10 +26,8 @@ class UserActionSerializer < ApplicationSerializer :action_code, :edit_reason, :category_id, - :uploaded_avatar_id, :closed, - :archived, - :acting_uploaded_avatar_id + :archived def excerpt cooked = object.cooked || PrettyText.cook(object.raw) diff --git a/app/serializers/user_name_serializer.rb b/app/serializers/user_name_serializer.rb index ac7beaa8d..3d7fc0d1f 100644 --- a/app/serializers/user_name_serializer.rb +++ b/app/serializers/user_name_serializer.rb @@ -1,20 +1,3 @@ -class UserNameSerializer < ApplicationSerializer - attributes :id, :username, :name, :title, :uploaded_avatar_id, :avatar_template - - def include_name? - SiteSetting.enable_names? - end - - def avatar_template - if Hash === object - User.avatar_template(user[:username], user[:uploaded_avatar_id]) - else - object.avatar_template - end - end - - def user - object[:user] || object - end - +class UserNameSerializer < BasicUserSerializer + attributes :name, :title end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index a17dd36d2..334b20e2d 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -60,7 +60,6 @@ class UserSerializer < BasicUserSerializer :suspended_till, :uploaded_avatar_id, :badge_count, - :notification_count, :has_title_badges, :edit_history_public, :custom_fields, @@ -93,10 +92,13 @@ class UserSerializer < BasicUserSerializer :tracked_category_ids, :watched_category_ids, :private_messages_stats, - :notification_count, :disable_jump_reply, + :system_avatar_upload_id, + :system_avatar_template, :gravatar_avatar_upload_id, + :gravatar_avatar_template, :custom_avatar_upload_id, + :custom_avatar_template, :has_title_badges, :card_image_badge, :card_image_badge_id, @@ -272,7 +274,7 @@ class UserSerializer < BasicUserSerializer MutedUser.where(user_id: object.id).joins(:muted_user).pluck(:username) end - def include_private_message_stats? + def include_private_messages_stats? can_edit && !(omit_stats == true) end @@ -280,20 +282,34 @@ class UserSerializer < BasicUserSerializer UserAction.private_messages_stats(object.id, scope) end + def system_avatar_upload_id + # should be left blank + end + + def system_avatar_template + User.system_avatar_template(object.username) + end + def gravatar_avatar_upload_id object.user_avatar.try(:gravatar_upload_id) end + def gravatar_avatar_template + return unless gravatar_upload_id = object.user_avatar.try(:gravatar_upload_id) + User.avatar_template(object.username, gravatar_upload_id) + end + def custom_avatar_upload_id object.user_avatar.try(:custom_upload_id) end - def has_title_badges - object.badges.where(allow_title: true).count > 0 + def custom_avatar_template + return unless custom_upload_id = object.user_avatar.try(:custom_upload_id) + User.avatar_template(object.username, custom_upload_id) end - def notification_count - Notification.where(user_id: object.id).count + def has_title_badges + object.badges.where(allow_title: true).count > 0 end def include_edit_history_public? @@ -329,4 +345,5 @@ class UserSerializer < BasicUserSerializer def pending_count 0 end + end diff --git a/app/views/list/list.erb b/app/views/list/list.erb index a6768190d..b9ae8a086 100644 --- a/app/views/list/list.erb +++ b/app/views/list/list.erb @@ -22,7 +22,7 @@ <span itemprop='name'><%= t.title %></span> </a> <%= page_links(t) %> - <% if !@category && t.category %> + <% if (!@category || @category.has_children?) && t.category %> <span>[<a href='<%= t.category.url %>'><%= t.category.name %></a>]</span> <% end %> <span title='<%= t 'posts' %>'>(<a href="<%=t.last_post_url%>"><%= t.posts_count %></a>)</span> diff --git a/config/initializers/i18n.rb b/config/initializers/i18n.rb index 00dab6775..43b729957 100644 --- a/config/initializers/i18n.rb +++ b/config/initializers/i18n.rb @@ -28,8 +28,10 @@ class NoFallbackLocaleList < FallbackLocaleList end end -if Rails.env.production? - I18n.fallbacks = FallbackLocaleList.new -else + +if Rails.env.development? I18n.fallbacks = NoFallbackLocaleList.new +else + I18n.fallbacks = FallbackLocaleList.new + I18n.config.missing_interpolation_argument_handler = proc { throw(:exception) } end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 7a5781f14..bfac8028f 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -6,9 +6,11 @@ end Sidekiq.configure_server do |config| config.redis = Discourse.sidekiq_redis_config - # add our pausable middleware + config.server_middleware do |chain| chain.add Sidekiq::Pausable + # ensure statistic middleware is included in case of a fork + chain.add Sidekiq::Statistic::Middleware end end diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index d939df1ad..11dc28c3e 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -123,7 +123,7 @@ ar: two: "ساعتان" few: "%{count} ساعات" many: "%{count} ساعة" - other: "%{count} ساعة" + other: "%{count} ساعات" x_days: zero: "0 يوم" one: "يوم واحد" @@ -437,6 +437,10 @@ ar: all_subcategories: "جميع" no_subcategory: "لا شيء" category: "تصنيف" + reorder: + title: "إعادة ترتيب الفئات" + save: "حفظ الترتيب" + apply_all: "تطبيق" posts: "مشاركات" topics: "مواضيع" latest: "آخر" @@ -487,6 +491,7 @@ ar: private_messages: "الرسائل" activity_stream: "النشاط" preferences: " التفضيلات" + expand_profile: "توسيع" bookmarks: "المفضلة" bio: "معلومات عنّي" invited_by: "مدعو بواسطة" @@ -663,7 +668,6 @@ ar: title: "دعوة" user: "المستخدمين المدعويين" sent: "تم الإرسال" - none: ".لم تقم بدعوة أي شخص إلى هنا حتى الأن" truncated: "اظهار اوائل {{count}} المدعويين" redeemed: "دعوات مستخدمة" redeemed_tab: "محررة" @@ -1702,6 +1706,8 @@ ar: help: "قمت بتفضيل هذا الموضوع" locked: help: "هذا الموضوع مغلق, لن يتم قبول اي رد " + archived: + help: "هذا الموضوع مؤرشف,لن تستطيع أن تعدل عليه" unpinned: title: "غير مثبت" help: "هذا الموضوع غير مثبت بالنسبة لك, سيتم عرضه بالترتيب العادي" @@ -1711,8 +1717,6 @@ ar: pinned: title: "مثبت" help: "هذا الموضوع مثبت لك, سوف يتم عرضه في اول القسم" - archived: - help: "هذا الموضوع مؤرشف,لن تستطيع أن تعدل عليه" invisible: help: "هذا الموضوع غير مصنف لن يظهر في قائمة التصانيف ولايمكن الدخول عليه الابرابط مباشر." posts: "مشاركات" diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index 68010abc3..3723992ec 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -437,7 +437,6 @@ bs_BA: search: "kucaj da potražiš pozivnice..." title: "Pozivnice" user: "Pozvan Korisnik" - none: "You haven't invited anyone here yet." truncated: "Showing the first {{count}} invites." redeemed: "Redeemed Invites" redeemed_at: "Redeemed" @@ -1083,6 +1082,8 @@ bs_BA: help: "Ovo je zvanično upozorenje." locked: help: "Ova tema je zatvorena; zvanično ne prima nove postove" + archived: + help: "Ova tema je arhivirana; zaleđena je i ne može biti promjenjena" unpinned: title: "Unpinned" help: "This topic is unpinned; it will display in default order" @@ -1092,8 +1093,6 @@ bs_BA: pinned: title: "Zakačena" help: "Ova tema je zakačena; biće na vrhu svoje kategorije" - archived: - help: "Ova tema je arhivirana; zaleđena je i ne može biti promjenjena" invisible: help: "Ovu temu sajt ne lista među najnovijim temama. Neće biti prisutna ni među listama tema unutar kategorija. Jedini način da se dođe do ove teme je direktan link" posts: "Odgovori" diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index a7118147f..95dafdc65 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -524,7 +524,6 @@ cs: search: "pište pro hledání v pozvánkách..." title: "Pozvánky" user: "Pozvaný uživatel" - none: "Zatím jste nikoho nepozvali." truncated: "Showing the first {{count}} invites." redeemed: "Uplatněné pozvánky" redeemed_tab: "Uplatněno" @@ -1424,6 +1423,8 @@ cs: help: "V tématu je vložena záložka" locked: help: "toto téma je uzavřené; další odpovědi nejsou přijímány" + archived: + help: "toto téma je archivováno; je zmraženo a nelze ho již měnit" unpinned: title: "Nepřipnuté" help: "Pro vás toto téma není připnuté; bude se zobrazovat v běžném pořadí" @@ -1433,8 +1434,6 @@ cs: pinned: title: "Připnuto" help: "Pro vás je toto téma připnuté; bude se zobrazovat na vrcholu seznamu ve své kategorii" - archived: - help: "toto téma je archivováno; je zmraženo a nelze ho již měnit" invisible: help: "Toto téma je neviditelné; nebude se zobrazovat v seznamu témat a lze ho navštívit pouze přes přímý odkaz" posts: "Příspěvků" diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 7295b6bc0..13eccd918 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -108,6 +108,16 @@ da: facebook: 'del dette link på Facebook' google+: 'del dette link på Google+' email: 'send dette link i en e-mail' + action_codes: + split_topic: "split dette emne op" + autoclosed: + enabled: 'lukket %{when}' + disabled: 'åbnet %{when}' + closed: + enabled: 'lukket %{when}' + disabled: 'åbnet %{when}' + archived: + enabled: 'arkiveret %{when}' topic_admin_menu: "administrationshandlinger på emne" emails_are_disabled: "Alle udgående emails er blevet deaktiveret globalt af en administrator. Ingen emailnotifikationer af nogen slags vil blive sendt." edit: 'redigér titel og kategori for dette emne' @@ -123,6 +133,7 @@ da: admin_title: "Admin" flags_title: "Flag" show_more: "vis mere" + show_help: "indstillinger" links: "Links" links_lowercase: one: "link" @@ -333,6 +344,8 @@ da: topics_entered: "emner indtastet" post_count: "# indlæg" confirm_delete_other_accounts: "Er du sikker på, at du vil slette disse kontoer?" + user_fields: + none: "(vælg en indstilling)" user: said: "{{username}}:" profile: "Profil" @@ -349,6 +362,14 @@ da: invited_by: "Inviteret af" trust_level: "Tillidsniveau" notifications: "Underretninger" + desktop_notifications: + perm_denied_btn: "Tilladelse nægtet" + perm_denied_expl: "Du har ikke givet tilladelse til meddelelser. Brug din browser til at aktivere meddelelser og klik derefter på knappen, når du er færdig. (Computer: Ikonet længst til venstre i adresselinjen. Mobil: 'Site info'.)" + disable: "Deaktiver meddelelser" + currently_enabled: "(slået til)" + enable: "Aktiver meddelelser" + currently_disabled: "(slået fra)" + each_browser_note: "Bemærk: Du skal ændre indstillingen i alle dine browsere." dismiss_notifications: "Marker alle som læst" dismiss_notifications_tooltip: "Marker alle ulæste meddelelser som læst" disable_jump_reply: "Ikke hop til mit indlæg efter jeg svarer" @@ -361,6 +382,7 @@ da: admin: "{{user}} er admin" moderator_tooltip: "Dette 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" @@ -421,6 +443,7 @@ da: upload_title: "Upload dit profil billede" upload_picture: "Upload et billede" image_is_not_a_square: "Advarsel: vi har klippet i billedet; bredde og højde var ikke ens." + cache_notice: "Du har ændret dit profilbillede, men der kan godt gå lidt tid inden ændringen træder i kraft." change_profile_background: title: "Profil baggrundsbillede" instructions: "Profil baggrunde vil blive centrerede og have en standard bredde på 850 pixels" @@ -487,15 +510,27 @@ da: label: "Betragt emner som nye når" not_viewed: "Jeg har ikke set dem endnu" last_here: "oprettet siden jeg var her sidst" + after_1_day: "oprettet indenfor den seneste dag" + after_2_days: "oprettet i de seneste 2 dage" + after_1_week: "oprettet i seneste uge" + after_2_weeks: "oprettet i de seneste 2 uger" auto_track_topics: "Følg automatisk emner jeg åbner" auto_track_options: never: "aldrig" immediately: "med det samme" + after_30_seconds: "efter 30 sekunder" + after_1_minute: "efter 1 minut" + after_2_minutes: "efter 2 minutter" + after_3_minutes: "efter 3 minutter" + after_4_minutes: "efter 4 minutter" + after_5_minutes: "efter 5 minutter" + after_10_minutes: "efter 10 minutter" invited: search: "tast for at søge invitationer…" title: "Invitationer" user: "Inviteret bruger" - none: "Du har ikke inviteret nogen endnu." + sent: "Sendt" + none: "Der er ingen afventende invitationer." truncated: "Viser de første {{count}} invitationer." redeemed: "Brugte invitationer" redeemed_tab: "Indløst" @@ -513,6 +548,7 @@ da: days_visited: "Besøgsdage" account_age_days: "Kontoens alder i dage" create: "Send en invitation" + generate_link: "Kopier invitations-link" bulk_invite: none: "Du har ikke inviteret nogen her endnu. Du kan sende individuelle invitationer eller invitere en masse mennesker på én gang ved at <a href='https://meta.discourse.org/t/send-bulk-invites/16468'>uploade en samlet liste over invitationer</a>." text: "Masse invitering fra en fil" @@ -778,6 +814,8 @@ da: search: title: "søg efter emner, indlæg, brugere eller kategorier" no_results: "Ingen resultater fundet." + no_more_results: "Ikke flere resultater." + search_help: Hjælp til søgning searching: "Søger…" post_format: "#{{post_number}} af {{username}}" context: @@ -785,6 +823,7 @@ da: category: "Søg i kategorien \"{{category}}\"" topic: "Søg i dette emne" private_messages: "Søg i beskeder" + hamburger_menu: "gå til en anden emneliste eller kategori" go_back: 'gå tilbage' not_logged_in_user: 'bruger side, med oversigt over aktivitet og indstillinger' current_user: 'gå til brugerside' @@ -926,8 +965,10 @@ da: title: "Følger" description: "En optælling af nye svar vil blive vist for denne tråd. Du vil modtage en notifikation, hvis nogen nævner dit @name eller svarer dig." regular: + title: "Normal" description: "Du vil modtage en notifikation, hvis nogen nævner dit @name eller svarer dig." regular_pm: + title: "Normal" description: "Du vil modtage en notifikation, hvis nogen nævner dit @name eller svarer dig." muted_pm: title: "Lydløs" @@ -1362,6 +1403,8 @@ da: help: "Du har bogmærket dette emne" locked: help: "emnet er låst; det modtager ikke flere svar" + archived: + help: "emnet er arkiveret; det er frosset og kan ikke ændres" unpinned: title: "Ikke fastgjort" help: "Dette emne er ikke fastgjort for dig; det vil blive vist i den normale rækkefølge" @@ -1371,8 +1414,6 @@ da: pinned: title: "Fastgjort" help: "Dette emne er fastgjort for dig; det vil blive vist i toppen af dets kategori" - archived: - help: "emnet er arkiveret; det er frosset og kan ikke ændres" invisible: help: "Dette emne er ulistet; det vil ikke blive vist i listen over emner og kan kun tilgås med et direkte link" posts: "Indlæg" @@ -2229,6 +2270,9 @@ da: name: "Navn" image: "Billede" delete_confirm: "Er du sikker på du vil slette emotikonnet: %{name} ?" + embedding: + feed_polling_enabled: "Importer indlæg via RSS/ATOM" + feed_polling_url: "URL på RSS/ATOM feed der skal kravles" permalink: title: "Permalinks" url: "URL" @@ -2270,6 +2314,7 @@ da: title: 'Applikation' create: '<b>c</b> Opret et nyt emne' notifications: '<b>n</b> Åbn notifikationer' + hamburger_menu: '<b>=</b> Åbn i hamburgermenu' user_profile_menu: '<b>p</b> Åben bruger menu' show_incoming_updated_topics: '<b>.</b> Vis opdaterede emner' search: '<b>/</b> Søg' @@ -2403,3 +2448,9 @@ da: reader: name: Læser description: Har læst hver eneste indlæg i et emne med mere end 100 posts + popular_link: + name: Populært link + description: Postede et eksternt link med mindst 50 klik + hot_link: + name: Hot link + description: Postede et eksternt link med mindst 300 klik diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index cb9629d98..cd6ed7972 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -542,7 +542,6 @@ de: title: "Einladungen" user: "Eingeladener Benutzer" sent: "Gesendet" - none: "Du hast bis jetzt noch niemanden hierher eingeladen." truncated: "Zeige die ersten {{count}} Einladungen." redeemed: "Angenommene Einladungen" redeemed_tab: "Angenommen" @@ -1432,6 +1431,8 @@ de: help: "Du hast in diesem Thema ein Lesezeichen gesetzt." locked: help: "Dieses Thema ist geschlossen. Das Antworten ist nicht mehr möglich." + archived: + help: "Dieses Thema ist archiviert; es ist eingefroren und kann nicht mehr geändert werden" unpinned: title: "Losgelöst" help: "Dieses Thema ist für dich losgelöst; es wird in der normalen Reihenfolge angezeigt" @@ -1441,8 +1442,6 @@ de: pinned: title: "Angeheftet" help: "Dieses Thema ist für dich angeheftet; es wird immer am Anfang seiner Kategorie auftauchen" - archived: - help: "Dieses Thema ist archiviert; es ist eingefroren und kann nicht mehr geändert werden" invisible: help: "Dieses Thema ist unsichtbar. Es wird in keiner Themenliste angezeigt und kann nur mit einem direkten Link betrachtet werden." posts: "Beiträge" diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 283b9bdbd..d9ed6cf1d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -120,7 +120,7 @@ en: email: 'send this link in an email' action_codes: - split_topic: "split this topic" + split_topic: "split this topic %{when}" autoclosed: enabled: 'closed %{when}' disabled: 'opened %{when}' @@ -245,6 +245,7 @@ en: upload: "Upload" uploading: "Uploading..." + uploading_filename: "Uploading {{filename}}..." uploaded: "Uploaded!" enable: "Enable" @@ -367,6 +368,14 @@ en: all_subcategories: "all" no_subcategory: "none" category: "Category" + reorder: + title: "Reorder Categories" + title_long: "Reorganize the category list" + fix_order: "Fix Positions" + fix_order_tooltip: "Not all categories have a unique position number, which may cause unexpected results." + save: "Save Order" + apply_all: "Apply" + position: "Position" posts: "Posts" topics: "Topics" latest: "Latest" @@ -412,6 +421,7 @@ en: private_messages: "Messages" activity_stream: "Activity" preferences: "Preferences" + expand_profile: "Expand" bookmarks: "Bookmarks" bio: "About me" invited_by: "Invited By" @@ -819,6 +829,7 @@ en: emoji: "Emoji :smile:" add_warning: "This is an official warning." + add_whisper: "This is a whisper only visible to moderators" posting_not_on_topic: "Which topic do you want to reply to?" saving_draft_tip: "saving..." saved_draft_tip: "saved" @@ -848,7 +859,7 @@ en: title_placeholder: "What is this discussion about in one brief sentence?" edit_reason_placeholder: "why are you editing?" show_edit_reason: "(add edit reason)" - reply_placeholder: "Type here. Use Markdown, BBCode, or HTML to format. Drag or paste an image to upload it." + reply_placeholder: "Type here. Use Markdown, BBCode, or HTML to format. Drag or paste images." view_new_post: "View your new post." saving: "Saving..." saved: "Saved!" @@ -914,6 +925,21 @@ en: linked: "<i title='linked post' class='fa fa-arrow-left'></i><p><span>{{username}}</span> {{description}}</p>" granted_badge: "<i title='badge granted' class='fa fa-certificate'></i><p>Earned '{{description}}'</p>" + alt: + mentioned: "Mentioned by" + quoted: "Quoted by" + replied: "Replied" + posted: "Post by" + edited: "Edit your post by" + liked: "Liked your post" + private_message: "Private message from" + invited_to_private_message: "Invited to a private message from" + invited_to_topic: "Invited to a topic from" + invitee_accepted: "Invite accepted by" + moved_post: "Your post was moved by" + linked: "Link to your post" + granted_badge: "Badge granted" + popup: mentioned: '{{username}} mentioned you in "{{topic}}" - {{site_title}}' quoted: '{{username}} quoted you in "{{topic}}" - {{site_title}}' @@ -932,12 +958,14 @@ en: local_tip: "select images from your device" local_tip_with_attachments: "select images or files from your device ({{authorized_extensions}})" hint: "(you can also drag & drop into the editor to upload them)" - hint_for_supported_browsers: "you can also drag and drop or paste images into the editor to upload them" + hint_for_supported_browsers: "you can also drag and drop or paste images into the editor" uploading: "Uploading" select_file: "Select File" image_link: "link your image will point to" search: + select_all: "Select All" + clear_all: "Clear All" title: "search topics, posts, users, or categories" no_results: "No results found." no_more_results: "No more results found." @@ -952,12 +980,12 @@ en: private_messages: "Search messages" hamburger_menu: "go to another topic list or category" + new_item: "new" go_back: 'go back' not_logged_in_user: 'user page with summary of current activity and preferences' current_user: 'go to your user page' topics: - too_many_tracked: "Warning: you have too many tracked new and unread topics, clear some using \"Dismiss New\" or \"Dismiss Posts\"" bulk: reset_read: "Reset Read" delete: "Delete Topics" @@ -1342,6 +1370,7 @@ en: yes_value: "Yes, abandon" via_email: "this post arrived via email" + whisper: "this post is a private whisper for moderators" wiki: about: "this post is a wiki; basic users can edit it" @@ -1510,6 +1539,7 @@ en: topic_template: "Topic Template" delete: 'Delete Category' create: 'New Category' + create_long: 'Create a new category' save: 'Save Category' slug: 'Category Slug' slug_placeholder: '(Optional) dashed-words for url' @@ -1532,6 +1562,7 @@ en: change_in_category_topic: "Edit Description" already_used: 'This color has been used by another category' security: "Security" + special_warning: "Warning: This category is a pre-seeded category and the security settings cannot be edited. If you do not wish to use this category, delete it instead of repurposing it." images: "Images" auto_close_label: "Auto-close topics after:" auto_close_units: "hours" @@ -1539,6 +1570,7 @@ en: email_in_allow_strangers: "Accept emails from anonymous users with no accounts" email_in_disabled: "Posting new topics via email is disabled in the Site Settings. To enable posting new topics via email, " email_in_disabled_click: 'enable the "email in" setting.' + suppress_from_homepage: "Suppress this category from the homepage." allow_badges_label: "Allow badges to be awarded in this category" edit_permissions: "Edit Permissions" add_permission: "Add Permission" @@ -1923,7 +1955,6 @@ en: enabled: "Enabled?" is_enabled: "Y" not_enabled: "N" - cant_disable: "-" change_settings: "Change Settings" change_settings_short: "Settings" howto: "How do I install plugins?" @@ -2582,6 +2613,7 @@ en: top: '<b>g</b>, <b>t</b> Top' bookmarks: '<b>g</b>, <b>b</b> Bookmarks' profile: '<b>g</b>, <b>p</b> Profile' + messages: '<b>g</b>, <b>m</b> Messages' navigation: title: 'Navigation' jump: '<b>#</b> Go to post #' diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index c4e914521..477b00e88 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -306,6 +306,9 @@ es: mods_and_admins: "Solo moderadores y administradores" members_mods_and_admins: "Solo miembros del grupo, moderadores y administradores" everyone: "Todos" + trust_levels: + title: "Nivel de confianza entregado automáticamente a miembros cuando son añadidos:" + none: "Ninguno" user_action_groups: '1': "'Me gusta' Dados" '2': "'Me gusta' Recibidos" @@ -325,6 +328,10 @@ es: all_subcategories: "todas" no_subcategory: "ninguna" category: "Categoría" + reorder: + title: "Reorganizar Categorías" + save: "Guardar orden" + apply_all: "Aplicar" posts: "Posts" topics: "Temas" latest: "Recientes" @@ -367,6 +374,7 @@ es: private_messages: "Mensajes" activity_stream: "Actividad" preferences: "Preferencias" + expand_profile: "Expandir" bookmarks: "Marcadores" bio: "Acerca de mí" invited_by: "Invitado Por" @@ -543,7 +551,7 @@ es: title: "Invitaciones" user: "Invitar Usuario" sent: "Enviadas" - none: "No has invitado a nadie todavía." + none: "No hay ninguna invitación pendiente que mostrar." truncated: "Mostrando las primeras {{count}} invitaciones." redeemed: "Invitaciones aceptadas" redeemed_tab: "Usado" @@ -563,6 +571,8 @@ es: days_visited: "Días Visitados" account_age_days: "Antigüedad de la cuenta en días" create: "Enviar una Invitación" + generate_link: "Copiar Enlace de Invitación" + generated_link_message: '<p>¡Enlace de Invitación generado con éxito!</p><p><input class="invite-link-input" style="width: 75%;" type="text" value="%{inviteLink}"></p><p>Este enlace de Invitación es sólo válido para la siguiente dirección de email: <b>%{invitedEmail}</b></p>' bulk_invite: none: "No has invitado a nadie todavía. Puedes enviar invitaciones individuales o invitar a un grupo de personas a la vez <a href='https://meta.discourse.org/t/send-bulk-invites/16468'>subiendo un archivo para invitaciones en masa</a>." text: "Archivo de Invitación en Masa" @@ -619,6 +629,7 @@ es: read_only_mode: enabled: "Modo solo-lectura activado. Puedes continuar navegando por el sitio pero las interacciones podrían no funcionar." login_disabled: "Iniciar sesión está desactivado mientras el foro esté en modo solo lectura." + too_few_topics_notice: "¡Vamos a <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>empezar con la comunidad!</a> Hay actualmente <strong>%{currentTopics} / %{requiredTopics}</strong> temas y <strong>%{currentPosts} / %{requiredPosts}</strong> posts. Los nuevos visitantes necesitan algunas conversaciones que leer y a las que responder." learn_more: "saber más..." year: 'año' year_desc: 'temas creados en los últimos 365 días' @@ -804,6 +815,20 @@ es: moved_post: "<i title='moved post' class='fa fa-sign-out'></i><p><span>{{username}}</span> movió {{description}}</p>" linked: "<i title='linked post' class='fa fa-arrow-left'></i><p><span>{{username}}</span> {{description}}</p>" granted_badge: "<i title='badge granted' class='fa fa-certificate'></i><p>Se te ha concedido '{{description}}'</p>" + alt: + mentioned: "Mencionado por" + quoted: "Citado por" + replied: "Respondido" + posted: "Publicado por" + edited: "Editado tu post por" + liked: "Gustado tu post" + private_message: "Mensaje privado de" + invited_to_private_message: "Invitado a un mensaje privado de" + invited_to_topic: "Invitado a un tema de" + invitee_accepted: "Invitación aceptada por" + moved_post: "Tu post fue eliminado por" + linked: "Enlace a tu post" + granted_badge: "Distintivo concedido" popup: mentioned: '{{username}} te mencionó en "{{topic}}" - {{site_title}}' quoted: '{{username}} te citó en "{{topic}}" - {{site_title}}' @@ -837,6 +862,8 @@ es: category: "Buscar en la categoría \"{{category}}\"" topic: "Buscar en este tema" private_messages: "Buscar en mensajes" + hamburger_menu: "ir a otra lista de temas o categoría" + new_item: "¡Nuevo!" go_back: 'volver' not_logged_in_user: 'página con el resumen de actividad y preferencias' current_user: 'ir a tu página de usuario' @@ -1032,6 +1059,7 @@ es: unpin: "Eliminar este tema del top de la categoría {{categoryLink}}." unpin_until: "Quitar este tema del top de la categoría {{categoryLink}} o esperar al <strong>%{until}</strong>." pin_note: "Los usuarios pueden desanclar el tema de forma individual por sí mismos." + pin_validation: "Es obligatorio especificar una fecha para destacar este tema." already_pinned: zero: "No hay temas destacados en {{categoryLink}}." one: "Temas anclados actualmente en {{categoryLink}}: <strong class='badge badge-notification unread'>1</strong>." @@ -1366,11 +1394,13 @@ es: email_in_allow_strangers: "Aceptar emails de usuarios anónimos sin cuenta" email_in_disabled: "La posibilidad de publicar nuevos temas por email está deshabilitada en los ajustes del sitio. Para habilitar la publicación de nuevos temas por email," email_in_disabled_click: 'activa la opción "email in".' + suppress_from_homepage: "Ocultar categoría de la página de inicio." allow_badges_label: "Permitir conceder distintivos en esta categoría" edit_permissions: "Editar permisos" add_permission: "Añadir permisos" this_year: "este año" position: "posición" + reorder: "Reordenar" default_position: "Posición predeterminada" position_disabled: "Las Categorías se mostrarán por orden de actividad. Para controlar el orden en que aparecen en las listas," position_disabled_click: 'activa la opción "fixed category positions".' @@ -1431,6 +1461,10 @@ es: help: "Has guardado en marcadores este tema." locked: help: "este tema está cerrado; ya no aceptan nuevas respuestas" + archived: + help: "este tema está archivado; está congelado y no puede ser cambiado" + locked_and_archived: + help: "Este tema está cerrado y archivado; no acepta nuevas respuestas y no puede ser cambiado de ningún modo." unpinned: title: "Deseleccionado como destacado" help: "Este tema se ha dejado de destacar para ti; en tu listado de temas se mostrará en orden normal" @@ -1440,8 +1474,6 @@ es: pinned: title: "Destacado" help: "Este tema ha sido destacado para ti; se mostrará en la parte superior de su categoría" - archived: - help: "este tema está archivado; está congelado y no puede ser cambiado" invisible: help: "Este tema es invisible; no se mostrará en la lista de temas y solo puede acceder a él a través de su enlace directo." posts: "Posts" @@ -1892,6 +1924,7 @@ es: sent_test: "enviado!" delivery_method: "Método de entrega" preview_digest: "Vista previa de Resumen" + preview_digest_desc: "Previsualiza el contenido del email de resumen enviado a usuarios inactivos." refresh: "Actualizar" format: "Formato" html: "html" @@ -2228,6 +2261,7 @@ es: backups: "Copias de seguridad" login: "Login" plugins: "Plugins" + user_preferences: "Preferencias de los Usuarios" badges: title: Distintivos new_badge: Nuevo distintivo @@ -2314,6 +2348,16 @@ es: feed_settings: "Ajustes de Feed" feed_description: "Discourse podrá importar tu contenido de forma más fácil si proporcionas un feed RSS/ATOM de tu sitio." crawling_settings: "Ajustes de Crawlers" + crawling_description: "Cuando Discourse crea temas para tus posts, si no hay un feed RSS/ATOM presente intentará analizar el contenido de tu HTML. A veces puede ser difícil extraer tu contenido, por eso facilitamos la opción de especificar reglas CSS para hacer la extracción más fácil." + embed_by_username: "Usuario para la creación de temas" + embed_post_limit: "Máximo número de posts a incluir" + embed_username_key_from_feed: "Clave para extraer usuario de discourse del feed" + embed_truncate: "Truncar los posts insertados" + embed_whitelist_selector: "Selector CSS para permitir elementos a embeber" + embed_blacklist_selector: "Selector CSS para restringir elementos a embeber" + feed_polling_enabled: "Importar posts usando RSS/ATOM" + feed_polling_url: "URL del feed RSS/ATOM del que extraer datos" + save: "Guardar ajustes de Insertado" permalink: title: "Enlaces permanentes" url: "URL" @@ -2344,6 +2388,8 @@ es: categories: '<b>g</b>, <b>c</b> Categorías' top: '<b>g</b>, <b>t</b> Arriba' bookmarks: '<b>g</b>, <b>b</b> Marcadores' + profile: '<b>g</b>, <b>p</b> Perfil' + messages: '<b>g</b>, <b>m</b> Mensajes' navigation: title: 'Navegación' jump: '<b>#</b> Ir al post #' @@ -2355,12 +2401,14 @@ es: title: 'Aplicación' create: '<b>c</b> Crear un tema nuevo' notifications: '<b>n</b> Abrir notificaciones' + hamburger_menu: '<b>=</b> Abrir Menú' user_profile_menu: '<b>p</b> Abrir menú de usuario' show_incoming_updated_topics: '<b>.</b> Mostrar temas actualizados' search: '<b>/</b> Buscar' help: '<b>?</b> Abrir la guía de atajos de teclado' dismiss_new_posts: '<b>x</b>, <b>r</b> Descartar Nuevo/Posts' dismiss_topics: '<b>x</b>, <b>t</b> Descartar Temas' + log_out: '<b>shift</b>+<b>z</b> <b>shift</b>+<b>z</b> Cerrar sesión' actions: title: 'Acciones' bookmark_topic: '<b>f</b> Guardar/Quitar el tema de marcadores' @@ -2488,6 +2536,15 @@ es: reader: name: Lector description: Leyó todos los posts en un tema con más de 100 + popular_link: + name: Enlace Popular + description: Publicó un enlace externo con al menos 50 clicks + hot_link: + name: Enlace Candente + description: Publicó un enlace externo con al menos 300 clicks + famous_link: + name: Enlace Famoso + description: Publicó un enlace externo con al menos 1000 clicks google_search: | <h3>Buscar con Google</h3> <p> diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index 29bf483af..55764ac69 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -465,7 +465,6 @@ fa_IR: search: "بنویسید تا فراخوانهها را جستجو کنید..." title: "فراخوانهها" user: "کاربر فراخوانده شده" - none: "هنوز هیچکسی را به اینجا فرانخواندهاید" truncated: "نمایش {{count}} فراخوانهٔ نخست" redeemed: "آزاد سازی دعوتنامه" redeemed_tab: "آزاد شده" @@ -1296,6 +1295,8 @@ fa_IR: help: "شما بر روی این موضوع نشانک گذاشتهاید." locked: help: "این موضوع بسته شده؛ پاسخهای تازه اینجا پذیرفته نمیشوند" + archived: + help: "این موضوع بایگانی شده؛ یخ زده و نمیتواند تغییر کند." unpinned: title: "خارج کردن از سنجاق" help: "این موضوع برای شما شنجاق نشده است، آن طور منظم نمایش داده خواهد شد" @@ -1305,8 +1306,6 @@ fa_IR: pinned: title: "سنجاق شد" help: "این موضوع برای شما سنجاق شده است، آن طور منظم در بالای دسته بندی نمایش داده خواهد شد." - archived: - help: "این موضوع بایگانی شده؛ یخ زده و نمیتواند تغییر کند." invisible: help: "این موضوع از لیست خارج شد: آن درلیست موضوعات نمایش داده نخواهد شد، و فقط از طریق لینک مستقیم در دسترس خواهد بود. " posts: "نوشتهها" diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 6ba830792..068c9c1e5 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -512,7 +512,6 @@ fi: search: "kirjoita etsiäksesi kutsuja..." title: "Kutsut" user: "Kutsuttu käyttäjä" - none: "Et ole vielä kutsunut ketään." truncated: "Näytetään ensimmäiset {{count}} kutsua." redeemed: "Hyväksytyt kutsut" redeemed_tab: "Hyväksytyt" @@ -1387,6 +1386,8 @@ fi: help: "Olet lisännyt ketjun kirjanmerkkeihisi" locked: help: "Tämä ketju on suljettu; siihen ei voi enää vastata." + archived: + help: "Tämä ketju on arkistoitu; se on jäädytetty eikä sitä voi muuttaa" unpinned: title: "Kiinnitys poistettu" help: "Ketjun kiinnitys on poistettu sinulta; se näytetään tavallisessa järjestyksessä." @@ -1396,8 +1397,6 @@ fi: pinned: title: "Kiinnitetty" help: "Tämä ketju on kiinnitetty sinulle; se näytetään alueensa ensimmäisenä" - archived: - help: "Tämä ketju on arkistoitu; se on jäädytetty eikä sitä voi muuttaa" invisible: help: "Tämä ketju on poistettu listauksista; sitä ei näytetä ketjujen listauksissa ja siihen pääsee vain suoralla linkillä" posts: "Viestejä" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index b1c61b5f4..fecec8cd5 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -543,7 +543,7 @@ fr: title: "Invitations" user: "Utilisateurs" sent: "Envoyé" - none: "Vous n'avez encore invité personne." + none: "Il n'y a plus d'invitation en attente à afficher." truncated: "Affichage des {{count}} premières invitations." redeemed: "Invitations acceptées" redeemed_tab: "Utilisés" @@ -563,6 +563,8 @@ fr: days_visited: "Ratio de présence" account_age_days: "Âge du compte en jours" create: "Envoyer une invitation" + generate_link: "Copier le lien d'invitation" + generated_link_message: '<p>Le lien d''invitation a été généré avec succès !</p><p><input class="invite-link-input" style="width: 75%;" type="text" value="%{inviteLink}"></p><p>Le lien d''invitation est valide uniquement pour cette adresse : <b>%{invitedEmail}</b></p>' bulk_invite: none: "Vous n'avez encore invité personne. Vous pouvez envoyé des invitations individuelles, ou en masse en <a href='https://meta.discourse.org/t/send-bulk-invites/16468'>envoyant un fichier d'invitation contenant la liste des courriels</a>." text: "Invitation massive depuis un fichier" @@ -619,6 +621,7 @@ fr: read_only_mode: enabled: "Le mode lecture seule est activé. Vous pouvez continuer à naviguer sur le site, mais ne pouvez pas prendre part aux discussions." login_disabled: "Impossible de se connecté quand le site est en mode lecture seule." + too_few_topics_notice: "Faites <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>partir la discussion !</a> Il y a actuellement <strong>%{currentTopics} / %{requiredTopics}</strong> sujets et <strong>%{currentPosts} / %{requiredPosts}</strong> messages. Les nouveaux utilisateurs ont besoin de discussions qu'ils peuvent lire et sur lesquelles ils peuvent répondre." learn_more: "en savoir plus…" year: 'an' year_desc: 'sujets créés durant les 365 derniers jours' @@ -837,11 +840,11 @@ fr: category: "Rechercher dans la catégorie \"{{category}}\"" topic: "Rechercher dans ce sujet" private_messages: "Rechercher des messages" + hamburger_menu: "se rendre dans une autre liste de sujet ou catégorie" go_back: 'retour' not_logged_in_user: 'page utilisateur avec un résumé de l''activité en cours et les préférences ' current_user: 'voir la page de l''utilisateur' topics: - too_many_tracked: "Avertissement: vous avez trop de sujets nouveaux ou non-lus, il faut en libérer avec \"Ignorer nouveaux\" ou \"Ignorer messages\"" bulk: reset_read: "Réinitialiser la lecture" delete: "Supprimer les sujets" @@ -1037,6 +1040,7 @@ fr: unpin: "Enlever ce sujet du haut de la catégorie {{categoryLink}}." unpin_until: "Enlever ce sujet du haut de la catégorie {{categoryLink}} ou attendre jusqu'à <strong>%{until}</strong>." pin_note: "Les utilisateurs peuvent enlever l'épingle de ce sujet eux-mêmes." + pin_validation: "Une date est requise pour épingler ce sujet." already_pinned: zero: "Aucun sujet actuellement épinglé dans {{categoryLink}}." one: "Sujets actuellement épinglés dans {{categoryLink}}: <strong class='badge badge-notification unread'>1</strong>." @@ -1436,6 +1440,10 @@ fr: help: "Vous avez ajouté ce sujet à vos signets" locked: help: "Ce sujet est fermé; il n'accepte plus de nouvelles réponses" + archived: + help: "Ce sujet est archivé; il est gelé et ne peut être modifié" + locked_and_archived: + help: "Ce sujet est fermé et archivé ; il n'accepte plus de nouvelles réponses et ne peut plus être modifié" unpinned: title: "Désépinglé" help: "Ce sujet est désépinglé pour vous; il sera affiché dans l'ordre par défaut" @@ -1445,8 +1453,6 @@ fr: pinned: title: "Épingler" help: "Ce sujet est épinglé pour vous; il s'affichera en haut de sa catégorie" - archived: - help: "Ce sujet est archivé; il est gelé et ne peut être modifié" invisible: help: "Ce sujet n'apparait plus dans la liste des sujets et sera seulement accessible via un lien direct" posts: "Messages" @@ -2372,6 +2378,7 @@ fr: title: 'Application' create: '<b>c</b> Créer un nouveau sujet' notifications: '<b>n</b> Ouvrir les notifications' + hamburger_menu: '<b>=</b> Ouvrir le menu hamburger' user_profile_menu: '<b>p</b> Ouvrir le menu de votre profil' show_incoming_updated_topics: '<b>.</b> Afficher les sujets mis à jour' search: '<b>/</b> Rechercher' @@ -2505,6 +2512,15 @@ fr: reader: name: Lecteur description: A lu tous les messages d'un sujet contenant plus de 100 messages + popular_link: + name: Lien populaire + description: A posté un lien externe avec au moins 50 clics + hot_link: + name: Lien tendance + description: A posté un lien externe avec au moins 300 clics + famous_link: + name: Lien populaire + description: A posté un lien externe avec au moins 1000 clics google_search: | <h3>Rechercher avec Google</h3> <p> diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index bb87353c1..e6cee16c7 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -543,7 +543,7 @@ he: title: "הזמנות" user: "משתמש/ת שהוזמנו" sent: "נשלח" - none: "עוד לא הזמנת לכאן אף אחד." + none: "אין הזמנות ממתינות להציג" truncated: "מראה את {{count}} ההזמנות הראשונות." redeemed: "הזמנות נוצלו" redeemed_tab: "נענו" @@ -845,7 +845,6 @@ he: not_logged_in_user: 'עמוד משתמש עם סיכום פעילות נוכחית והעדפות' current_user: 'לך לעמוד המשתמש שלך' topics: - too_many_tracked: "אזהרה: יש לך יותר מדי נושאים חדשים שלא נקראו במעקב. נקה חלק בעזרת \"התעלם מחדשים\" או \"התעלם מפרסומים\"" bulk: reset_read: "איפוס נקראו" delete: "מחיקת נושאים" @@ -1437,6 +1436,10 @@ he: help: "יצרת סימניה לנושא זה" locked: help: "הנושא הזה נעול, הוא לא מקבל יותר תגובות חדשות" + archived: + help: "הנושא הזה אוכסן בארכיון; הוא הוקפא ולא ניתן לשנותו" + locked_and_archived: + help: "הנושא הזה סגור ומאורכב. לא ניתן להגיב בו יותר או לשנות אותו. " unpinned: title: "הורד מנעיצה" help: "נושא זה אינו מקובע עבורך; הוא יופיע בסדר הרגיל" @@ -1446,8 +1449,6 @@ he: pinned: title: "נעוץ" help: "נושא זה מקובע עבורך, הוא יופיע בראש הקטגוריה" - archived: - help: "הנושא הזה אוכסן בארכיון; הוא הוקפא ולא ניתן לשנותו" invisible: help: "נושא זה מוסתר; הוא לא יוצג ברשימות הנושאים, וזמין רק באמצעות קישור ישיר." posts: "הודעות" @@ -1543,6 +1544,8 @@ he: title: "תמיד" yearly: title: "שנתי" + quarterly: + title: "רבעוני" monthly: title: "חודשי" weekly: @@ -1551,6 +1554,7 @@ he: title: "יומי" all_time: "כל הזמנים" this_year: "שנה" + this_quarter: "רבע" this_month: "חודש" this_week: "שבוע" today: "היום" @@ -1721,6 +1725,9 @@ he: none_installed: "אין לך הרחבות מותקנות" version: "גרסה" enabled: "מאופשר?" + is_enabled: "Y" + not_enabled: "N" + cant_disable: "-" change_settings: "שינוי הגדרות" change_settings_short: "הגדרות" howto: "איך אני מתקין/מתקינה הרחבות?" @@ -1800,6 +1807,7 @@ he: header: "כותרת" top: "למעלה" footer: "כותרת תחתית" + embedded_css: "Embedded CSS" head_tag: text: "</head>" title: "קוד HTML שיוכנס לפני התגית </head>" @@ -1891,6 +1899,7 @@ he: sent_test: "נשלח!" delivery_method: "שיטת העברה" preview_digest: "תצוגה מקדימה של סיכום" + preview_digest_desc: "תצוגה מקדימה של מייל סיכום שנשלח למשתמשים לא פעילים. " refresh: "רענן" format: "פורמט" html: "html" @@ -1955,6 +1964,7 @@ he: delete_post: "מחיקת פרסום" impersonate: "התחזה" anonymize_user: "הפיכת משתמש/ת לאנונימיים" + roll_up: "roll up IP blocks" screened_emails: title: "הודעות דואר מסוננות" description: "כשמישהו מנסה ליצור חשבון חדש, כתובות הדואר האלקטרוני הבאות ייבדקו וההרשמה תחסם או שיבוצו פעולות אחרות." @@ -2176,6 +2186,7 @@ he: delete: "מחיקה" cancel: "ביטול" delete_confirm: "האם את/ה בטוחים שאתם רוצים למחוק את שדה משתמש/ת הזה?" + options: "אפשרויות" required: title: "נדרש בעת הרשמה?" enabled: "נדרש" @@ -2191,6 +2202,7 @@ he: field_types: text: 'שדה טקסט' confirm: 'אישור' + dropdown: "נגלל" site_text: none: "בחרו את סוג התוכן לתחילת עריכה." title: 'תוכן טקסטואלי' @@ -2224,6 +2236,7 @@ he: backups: "גיבויים" login: "התחברות" plugins: "הרחבות" + user_preferences: "הגדרות משתמש" badges: title: תגים new_badge: תג חדש @@ -2297,6 +2310,11 @@ he: name: "שם" image: "תמונה" delete_confirm: "האם את/ה בטוח/ה שאתם רוצים למחוק את האמוג'י :%{name}:?" + embedding: + get_started: "אם ברצונך לשלב את דיסקורס באתר אחר, התחל בהוספת המערך שלו (host). " + confirm_delete: "האם אתה בטוח שאתה רוצה למחוק את הhost הזה? " + sample: "השתמש בקוד HTML הבא באתר שלך על מנת ליצור נושאי דיסקורס משולבים. החלף <b>REPLACE_ME</b> בURL הקאנוני של העמוד שבו אתה מכניס נושא מכונן. " + title: "שילוב (embedding)" permalink: title: "קישורים קבועים" url: "כתובת" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index e93c9d2dd..2ab2f8668 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -543,7 +543,6 @@ it: title: "Inviti" user: "Utente Invitato" sent: "Spedito" - none: "Non hai ancora invitato nessuno qui." truncated: "Mostro i primi {{count}} inviti." redeemed: "Inviti Accettati" redeemed_tab: "Riscattato" @@ -1425,6 +1424,8 @@ it: help: "Hai aggiunto questo argomento ai segnalibri" locked: help: "Questo argomento è chiuso; non sono ammesse nuove risposte" + archived: + help: "Questo argomento è archiviato; è bloccato e non può essere modificato" unpinned: title: "Spuntato" help: "Questo argomento è per te spuntato; verrà mostrato con l'ordinamento di default" @@ -1434,8 +1435,6 @@ it: pinned: title: "Appuntato" help: "Questo argomento è per te appuntato; verrà mostrato con l'ordinamento di default" - archived: - help: "Questo argomento è archiviato; è bloccato e non può essere modificato" invisible: help: "Questo argomento è invisibile; non verrà mostrato nella liste di argomenti ed è possibile accedervi solo tramite collegamento diretto" posts: "Messaggi" diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index 12f427638..9af856e52 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -482,7 +482,6 @@ ja: search: "招待履歴を検索するためにキーワードを入力してください..." title: "招待" user: "招待したユーザ" - none: "まだ招待を送っていません" truncated: "最初の {{count}} 個の招待履歴を表示しています。" redeemed: "受理された招待" redeemed_tab: "受理" @@ -1319,6 +1318,8 @@ ja: help: "このトピックをブックマークしました" locked: help: "このトピックは終了しています。新たに回答を投稿することはできません。" + archived: + help: "このトピックはアーカイブされています。凍結状態のため一切の変更ができません" unpinned: title: "ピン留め解除しました" help: "このトピックはピン留めされていません。 既定の順番に表示されます。" @@ -1328,8 +1329,6 @@ ja: pinned: title: "ピン留め" help: "このトピックはピン留めされています。常にカテゴリのトップに表示されます" - archived: - help: "このトピックはアーカイブされています。凍結状態のため一切の変更ができません" invisible: help: "このトピックはリストされていません。トピックリストには表示されません。直接リンクでのみアクセス可能です" posts: "投稿" diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index e12299d1e..6ec6c7069 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -513,7 +513,6 @@ ko: title: "초대" user: "사용자 초대" sent: "보냄" - none: "어떤 초대도 발견되지 않았습니다." truncated: "처음 {{count}}개 초대장 보여주기" redeemed: "초대를 받았습니다." redeemed_tab: "Redeemed" @@ -1358,6 +1357,8 @@ ko: help: "북마크한 토픽" locked: help: "이 토픽은 폐쇄되었습니다. 더 이상 새 답글을 받을 수 없습니다." + archived: + help: "이 토픽은 보관중입니다. 고정되어 변경이 불가능합니다." unpinned: title: "핀 제거" help: "이 토픽은 핀 제거 되었습니다. 목록에서 일반적인 순서대로 표시됩니다." @@ -1367,8 +1368,6 @@ ko: pinned: title: "핀 지정됨" help: "이 토픽은 고정되었습니다. 카테고리의 상단에 표시됩니다." - archived: - help: "이 토픽은 보관중입니다. 고정되어 변경이 불가능합니다." invisible: help: "이 토픽은 목록에서 제외됩니다. 토픽 목록에 표시되지 않으며 링크를 통해서만 접근 할 수 있습니다." posts: "게시물" diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index 61b7885ea..c5002097b 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -522,7 +522,6 @@ nb_NO: search: "skriv for å søke etter invitasjoner..." title: "invitasjoner" user: "Invitert bruker" - none: "Du har ikke invitert noen hit ennå." truncated: "Viser de første {{count}} invitasjoner." redeemed: "Løs inn invitasjoner" redeemed_tab: "Brukt" @@ -1393,6 +1392,8 @@ nb_NO: help: "Du lagret dette emnet" locked: help: "dette emnet er låst; det aksepterer ikke lenger nye svar" + archived: + help: "dette emnet er arkivert; det er fryst og kan ikke bli aktivert" unpinned: title: "Løsgjort" help: "Dette emnet er ikke lenger fastsatt, det vil vises i vanlig rekkefølge" @@ -1402,8 +1403,6 @@ nb_NO: pinned: title: "Fastsatt" help: "Dette emnet er fastsatt for deg; det vil vises i toppen av sin kategori" - archived: - help: "dette emnet er arkivert; det er fryst og kan ikke bli aktivert" invisible: help: "Dette emnet er ikke listet; det vil ikke vises i emnelister, og kan kun leses via en direktelenke" posts: "Innlegg" diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 1481dd7fc..684c83590 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -306,6 +306,9 @@ nl: mods_and_admins: "Alleen moderatoren and admins" members_mods_and_admins: "Alleen leden van de groep, moderatoren en admins" everyone: "Iedereen" + trust_levels: + title: "Trustlevel dat automatisch wordt toegekend aan nieuwe gebruikers:" + none: "Geen" user_action_groups: '1': "Likes gegeven" '2': "Likes ontvangen" @@ -367,6 +370,7 @@ nl: private_messages: "Berichten" activity_stream: "Activiteit" preferences: "Voorkeuren" + expand_profile: "Uitklappen" bookmarks: "Bladwijzers" bio: "Over mij" invited_by: "Uitgenodigd door" @@ -543,7 +547,7 @@ nl: title: "Uitnodigingen" user: "Uitgenodigd lid" sent: "Verzonden" - none: "Je hebt nog niemand uitgenodigd." + none: "Er zijn geen uitstaande uitnodigingen om weer te geven." truncated: "De eerste {{count}} uitnodigingen." redeemed: "Verzilverde uitnodigingen" redeemed_tab: "Verzilverd" @@ -807,6 +811,20 @@ nl: moved_post: "<i title='heeft bericht verplaatst' class='fa fa-sign-out'></i><p><span>{{username}}</span> verplaatste {{description}}</p>" linked: "<i title='gelinkt bericht' class='fa fa-arrow-left'></i><p><span>{{username}}</span> {{description}}</p>" granted_badge: "<i title='badge ontvangen' class='fa fa-certificate'></i><p>'{{description}}' ontvangen</p>" + alt: + mentioned: "Genoemd door" + quoted: "Gequoot door" + replied: "Gereageerd" + posted: "Geplaatst door" + edited: "Wijzig je bericht door" + liked: "Vind je bericht leuk" + private_message: "Privébericht van" + invited_to_private_message: "Uitgenodigd voor een privébericht van" + invited_to_topic: "Uitgenodigd voor een topic door" + invitee_accepted: "Uitnodiging geaccepteerd door" + moved_post: "Je bericht is verplaatst door" + linked: "Link naar je bericht" + granted_badge: "Badge toegekend" popup: mentioned: '{{username}} heeft je genoemd in "{{topic}}" - {{site_title}}' quoted: '{{username}} heeft je geciteerd in "{{topic}}" - {{site_title}}' @@ -845,7 +863,7 @@ nl: not_logged_in_user: 'gebruikerspagina met samenvatting van huidige activiteit en voorkeuren' current_user: 'ga naar je gebruikerspagina' topics: - too_many_tracked: "Let op: je hebt te veel nieuwe en ongelezen topics gevolgd, verwijder ze met behulp van \"Markeer nieuwe berichten als gelezen\" of \"Verwijder Berichten\"" + too_many_tracked: "Let op: je hebt te veel topics staan onder nieuw en ongelezen, maak wat ruimte door <a href='/new'>Dismiss New</a> of <a href='/unread'>Dismiss Posts</a> te gebruiken" bulk: reset_read: "markeer als ongelezen" delete: "Verwijder topics" @@ -1372,6 +1390,7 @@ nl: email_in_allow_strangers: "Accepteer mails van anonieme gebruikers zonder account" email_in_disabled: "Het plaatsen van nieuwe discussies via e-mail is uitgeschakeld in de Site Instellingen. Om het plaatsen van nieuwe discussie via e-mail aan te zetten," email_in_disabled_click: 'schakel "e-mail in" instelling in.' + suppress_from_homepage: "Negeer deze categorie op de homepage" allow_badges_label: "Laat badges toekennen voor deze categorie" edit_permissions: "Wijzig permissies" add_permission: "Nieuwe permissie" @@ -1437,6 +1456,10 @@ nl: help: "Je hebt een bladwijzer aan deze topic toegevoegd" locked: help: "Deze topic is gesloten; reageren is niet meer mogelijk" + archived: + help: "Deze topic is gearchiveerd en kan niet meer gewijzigd worden" + locked_and_archived: + help: "Deze topic is gesloten en gearchiveerd; reageren of wijzigen is niet langer mogelijk." unpinned: title: "Niet vastgepind" help: "Dit topic is niet langer vastgepind voor je en zal weer in de normale volgorde getoond worden" @@ -1446,8 +1469,6 @@ nl: pinned: title: "Vastgepind" help: "Dit topic is vastgepind voor je en zal bovenaan de categorie getoond worden" - archived: - help: "Deze topic is gearchiveerd en kan niet meer gewijzigd worden" invisible: help: "Dit topic is niet zichtbaar; het zal niet verschijnen in de topiclijst en kan alleen bekeken worden met een directe link" posts: "Berichten" @@ -2362,6 +2383,8 @@ nl: categories: '<b>g</b>, <b>c</b> Categoriën' top: '<b>g</b>, <b>t</b> Top' bookmarks: '<b>g</b>, <b>b</b> Favorieten' + profile: '<b>g</b>, <b>p</b> Profiel' + messages: '<b>g</b>, <b>m</b> Berichten' navigation: title: 'Navigatie' jump: '<b>#</b> Ga naar bericht #' @@ -2380,6 +2403,7 @@ nl: help: '<b>?</b> Open sneltoetsen help' dismiss_new_posts: '<b>x</b>, <b>r</b> Seponeer Nieuw/Berichten' dismiss_topics: '<b>x</b>, <b>t</b> Seponeer Topics' + log_out: '<b>shift</b>+<b>z</b> <b>shift</b>+<b>z</b> Uitloggen' actions: title: 'Acties' bookmark_topic: '<b>f</b> Toggle bladwijzer van topic' diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 0bcfb2cd1..37d092506 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -334,6 +334,9 @@ pl_PL: mods_and_admins: "Tylko moderatorzy i administratorzy" members_mods_and_admins: "Tylko członkowie grupy, moderatorzy i administratorzy" everyone: "Wszyscy" + trust_levels: + title: "Domyślny poziom zaufania przyznawany nowych użytkownikom:" + none: "Brak" user_action_groups: '1': "Przyznane polubienia" '2': "Otrzymane polubienia" @@ -397,6 +400,7 @@ pl_PL: private_messages: "Wiadomości" activity_stream: "Aktywność" preferences: "Ustawienia" + expand_profile: "Rozwiń" bookmarks: "Zakładki" bio: "O mnie" invited_by: "Zaproszono przez" @@ -573,7 +577,7 @@ pl_PL: title: "Zaproszenia" user: "Zaproszony(-a) użytkownik(-czka)" sent: "Wysłane" - none: "Jeszcze nikt nie został przez ciebie zaproszony." + none: "Nie ma żadnych zaproszeń do wyświetlenia." truncated: "Pokaż pierwsze {{count}} zaproszeń." redeemed: "Cofnięte zaproszenia" redeemed_tab: "Przyjęte" @@ -838,6 +842,20 @@ pl_PL: moved_post: "<i title='przeniesienie wpisu' class='fa fa-sign-out'></i><p><span>{{username}}</span> przenosi {{description}}</p>" linked: "<i title='powiązany wpis' class='fa fa-arrow-left'></i><p><span>{{username}}</span> {{description}}</p>" granted_badge: "<i title='otrzymano odznakę' class='fa fa-certificate'></i><p>Otrzymujesz '{{description}}'</p>" + alt: + mentioned: "Wywołanie przez" + quoted: "Cytowanie przez" + replied: "Odpowiedź" + posted: "Autor wpisu" + edited: "Edycja twojego wpisu" + liked: "Polubienie twojego wpisu" + private_message: "Prywatna wiadomość od" + invited_to_private_message: "Zaproszenie do prywatnej wiadomości od" + invited_to_topic: "Zaproszenie do tematu od" + invitee_accepted: "Zaproszenie zaakceptowane przez" + moved_post: "Twój wpis został przeniesiony przez" + linked: "Linkownie do twojego wpisu" + granted_badge: "Przyznanie odznaki" popup: mentioned: '{{username}} wspomina o tobie w "{{topic}}" - {{site_title}}' quoted: '{{username}} cytuje cie w "{{topic}}" - {{site_title}}' @@ -876,7 +894,7 @@ pl_PL: not_logged_in_user: 'strona użytkownika z podsumowaniem bieżących działań i ustawień' current_user: 'idź do swojej strony użytkowanika' topics: - too_many_tracked: "Uwaga: posiadasz zbyt wiele wpisów w nowych i śledzonych tematach. Aby kontynuować, wyczyść listę przyciskiem na dole strony." + too_many_tracked: "Uwaga: posiadasz zbyt wiele wpisów w nowych i śledzonych tematach. Aby kontynuować, wyczyść <a href='/new'>Nowe</a> lub <a href='/unread'>Śledzone</a>" bulk: reset_read: "Wyzeruj przeczytane" delete: "Usuń tematy" @@ -1440,6 +1458,7 @@ pl_PL: email_in_allow_strangers: "Akceptuj wiadomości email od anonimowych, nieposiadających kont użytkowników " email_in_disabled: "Tworzenie nowych tematów emailem jest wyłączone w ustawieniach serwisu. " email_in_disabled_click: 'Kliknij tu, aby włączyć.' + suppress_from_homepage: "Nie wyświetlaj tej kategorii na stronie głównej." allow_badges_label: "Włącz przyznawanie odznak na podstawie aktywności w tej kategorii" edit_permissions: "Edytuj uprawnienia" add_permission: "Dodaj uprawnienie" @@ -1506,6 +1525,10 @@ pl_PL: help: "Temat został dodany do zakładek." locked: help: "Temat został zamknięty. Dodawanie nowych odpowiedzi nie jest możliwe." + archived: + help: "Ten temat został zarchiwizowany i nie można go zmieniać" + locked_and_archived: + help: "Ten temat jest zamknięty i zarchiwizowany. Dodawanie odpowiedzi i jego edycja nie są możliwe." unpinned: title: "Nieprzypięty" help: "Temat nie jest przypięty w ramach twojego konta. Będzie wyświetlany w normalnej kolejności." @@ -1515,8 +1538,6 @@ pl_PL: pinned: title: "Przypięty" help: "Temat przypięty dla twojego konta. Będzie wyświetlany na początku swojej kategorii." - archived: - help: "Ten temat został zarchiwizowany i nie można go zmieniać" invisible: help: "Temat jest niewidoczny: nie będzie wyświetlany na listach tematów a dostęp do niego można uzyskać jedynie poprzez link bezpośredni" posts: "Wpisy" @@ -2440,6 +2461,8 @@ pl_PL: categories: '<b>g</b>, <b>c</b> Kategorie' top: '<b>g</b>, <b>t</b> Popularne' bookmarks: '<b>g</b>, <b>b</b> Zakładki' + profile: '<b>g</b>, <b>p</b> Profil' + messages: '<b>g</b>, <b>m</b> Wiadomości' navigation: title: 'Nawigacja' jump: '<b>#</b> idź do wpisu #' @@ -2458,6 +2481,7 @@ pl_PL: help: '<b>?</b> skróty klawiszowe' dismiss_new_posts: '<b>x</b>, <b>r</b> wyczyść listę wpisów' dismiss_topics: '<b>x</b>, <b>t</b> wyczyść listę tematów' + log_out: '<b>shift</b>+<b>z</b> <b>shift</b>+<b>z</b> Wylogowanie' actions: title: 'Operacje' bookmark_topic: '<b>f</b> dodaj/usuń zakładkę na temat' diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index 7c54bcd75..33759f113 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -306,6 +306,9 @@ pt: mods_and_admins: "Apenas moderadores e Administradores" members_mods_and_admins: "Apenas membros do grupo, moderadores e administradores" everyone: "Todos" + trust_levels: + title: "Nível de confiança concedido automaticamente a membros quando são adicionados:" + none: "Nenhum" user_action_groups: '1': "Gostos Dados" '2': "Gostos Recebidos" @@ -367,6 +370,7 @@ pt: private_messages: "Mensagens" activity_stream: "Atividade" preferences: "Preferências" + expand_profile: "Expandir" bookmarks: "Marcadores" bio: "Sobre mim" invited_by: "Convidado Por" @@ -543,7 +547,7 @@ pt: title: "Convites" user: "Utilizadores Convidados" sent: "Enviado" - none: "Ainda não convidou ninguém." + none: "Não há convites pendentes para mostrar." truncated: "A mostrar os primeiros {{count}} convites." redeemed: "Convites Resgatados" redeemed_tab: "Resgatado" @@ -807,6 +811,20 @@ pt: moved_post: "<i title='moved post' class='fa fa-sign-out'></i><p><span>{{username}}</span> moveu {{description}}</p>" linked: "<i title='linked post' class='fa fa-arrow-left'></i><p><span>{{username}}</span> {{description}}</p>" granted_badge: "<i title='badge granted' class='fa fa-certificate'></i><p>Ganhou '{{description}}'</p>" + alt: + mentioned: "Mencionado por" + quoted: "Citado por" + replied: "Respondido" + posted: "Publicado por" + edited: "Edição da sua mensagem por" + liked: "Gostou da sua mensagem" + private_message: "Mensagem privada de" + invited_to_private_message: "Convidado para uma mensagem privada de" + invited_to_topic: "Convidado para um tópico de" + invitee_accepted: "Convite aceite por" + moved_post: "A sua mensagem foi movida por" + linked: "Hiperligação para a sua mensagem" + granted_badge: "Distintivo concedido" popup: mentioned: '{{username}} mencionou-o em "{{topic}}" - {{site_title}}' quoted: '{{username}} citou-o em "{{topic}}" - {{site_title}}' @@ -845,7 +863,7 @@ pt: not_logged_in_user: 'página de utilizador com resumo da atividade atual e preferências ' current_user: 'ir para a sua página de utilizador' topics: - too_many_tracked: "Aviso: tem demasiados tópicos novos ou não lidos a serem acompanhados, limpe alguns utilizando \"Destituir Novos\" ou \"Destituir Mensagens\"" + too_many_tracked: "Aviso: tem demasiados tópicos novos e não lidos a serem acompanhados, limpe alguns utilizando <a href='/new'>Destituir Novos</a> ou <a href='/unread'>Destituir Mensagens</a>" bulk: reset_read: "Repor Leitura" delete: "Eliminar Tópicos" @@ -1372,6 +1390,7 @@ pt: email_in_allow_strangers: "Aceitar emails de utilizadores anónimos sem conta" email_in_disabled: "Publicar novos tópicos através do email está desactivado nas Configurações do Sítio. Para permitir a publicação de novos tópicos através do email," email_in_disabled_click: 'ative a definição "email em".' + suppress_from_homepage: "Suprimir esta categoria da página principal." allow_badges_label: "Permitir a atribuição de distintivos nesta categoria" edit_permissions: "Editar Permissões" add_permission: "Adicionar Permissões" @@ -1437,6 +1456,10 @@ pt: help: "Adicionou este tópico aos marcadores" locked: help: "Este tópico está fechado; já não são aceites novas respostas" + archived: + help: "Este tópico está arquivado; está congelado e não pode ser alterado" + locked_and_archived: + help: "Este tópico está fechado e arquivado; já não aceita novas respostas e não pode ser modificado" unpinned: title: "Desafixado" help: "Este tópico foi desafixado por si; será mostrado na ordem habitual" @@ -1446,8 +1469,6 @@ pt: pinned: title: "Fixado" help: "Este tópico foi fixado por si; será mostrado no topo da sua categoria" - archived: - help: "Este tópico está arquivado; está congelado e não pode ser alterado" invisible: help: "Este tópico não está listado; não será apresentado na lista de tópicos e poderá ser acedido apenas através de uma hiperligação direta" posts: "Mensagens" @@ -2362,6 +2383,8 @@ pt: categories: '<b>g</b>, <b>c</b> Categorias' top: '<b>g</b>, <b>t</b> Os Melhores' bookmarks: '<b>g</b>, <b>b</b> Marcadores' + profile: '<b>g</b>, <b>p</b> Perfil' + messages: '<b>g</b>, <b>m</b> Mensagens' navigation: title: 'Navegação' jump: '<b>#</b> Ir para o post #' @@ -2380,6 +2403,7 @@ pt: help: '<b>?</b> Abrir ajuda do teclado' dismiss_new_posts: '<b>x</b>, <b>r</b> Destituir Novos/Mensagens' dismiss_topics: '<b>x</b>, <b>t</b> Destituir Tópicos' + log_out: '<b>shift</b>+<b>z</b> <b>shift</b>+<b>z</b> Terminar Sessão' actions: title: 'Ações' bookmark_topic: '<b>f</b> Alternar marcador de tópico' diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index c4a28d378..187c9b9d3 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -505,7 +505,6 @@ pt_BR: search: "digite para pesquisar convites..." title: "Convites" user: "Usuários convidados" - none: "Você ainda não convidou ninguém." truncated: "Exibindo os primeiros {{count}} convites." redeemed: "Convites usados" redeemed_at: "Usado" @@ -1369,6 +1368,8 @@ pt_BR: help: "Você adicionou este tópico aos favoritos" locked: help: "Este tópico está fechado; não serão aceitas mais respostas" + archived: + help: "Este tópico está arquivado; está congelado e não pode ser alterado" unpinned: title: "Não fixo" help: "Este tópico está desfixado para você; ele será mostrado em ordem normal" @@ -1378,8 +1379,6 @@ pt_BR: pinned: title: "Fixo" help: "Este tópico está fixado para você; ele será mostrado no topo de sua categoria" - archived: - help: "Este tópico está arquivado; está congelado e não pode ser alterado" invisible: help: "Este tópico está invisível; não aparecerá na listagem dos tópicos, e pode apenas ser acessado por link direto" posts: "Mensagens" diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index a352bee21..b9d195509 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -495,7 +495,6 @@ ro: search: "Scrie pentru a căuta invitații..." title: "Invitații" user: "Utilizatori invitați" - none: "Nu ai invitat înca pe nimeni." truncated: "Afișeaza primele {{count}} invitații." redeemed: "Invitații rascumpărate" redeemed_at: "Răscumpărate" @@ -1357,6 +1356,8 @@ ro: help: "Aţi pus un semn de carte pentru această discuţie" locked: help: "Această discuție este închisă; nu mai acceptă răspunsuri noi" + archived: + help: "Această discuție a fost arhivată; Este închetată și nu poate fi editată" unpinned: title: "Desprinde" help: "Această discuţie va fi afişată în ordinea iniţială, nici un mesaj nu este promovat la inceputul listei." @@ -1366,8 +1367,6 @@ ro: pinned: title: "Fixată" help: "Aceast mesaj va fi promovat. Va fi afişat la începutul discuţiei." - archived: - help: "Această discuție a fost arhivată; Este închetată și nu poate fi editată" invisible: help: "Această discuție este invizibilă; nu va fi afișată în listele de discuții și va fi accesată numai prin adresa directă" posts: "Postări" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index cd135e822..51574007e 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -553,7 +553,6 @@ ru: search: "введите текст для поиска приглашений..." title: "Приглашения" user: "Приглашенный пользователь" - none: "Пока вы еще никого не пригласили." truncated: "Отображаются первые {{count}} приглашений." redeemed: "Принятые приглашения" redeemed_tab: "Принято" @@ -1490,6 +1489,8 @@ ru: help: "Вы добавили тему в Избранное " locked: help: "Тема закрыта; в ней больше нельзя отвечать" + archived: + help: "Тема заархивирована и не может быть изменена" unpinned: title: "Откреплена" help: "Эта тема не закреплена; она будет отображаться в обычном порядке" @@ -1499,8 +1500,6 @@ ru: pinned: title: "Закреплена" help: "Тема закреплена; она будет показана вверху соответствующего раздела" - archived: - help: "Тема заархивирована и не может быть изменена" invisible: help: "Тема исключена из всех списков тем и доступна только по прямой ссылке" posts: "Сообщ." @@ -2546,3 +2545,9 @@ ru: reader: name: Читатель description: Прочитал каждое сообщение в теме с более чем 100 сообщениями + popular_link: + name: Популярная ссылка + description: Оставил внешнюю ссылку с более чем 50 кликов + hot_link: + name: Горячая ссылка + description: Оставил внешнюю ссылку с более чем 300 кликов diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index 8c70fd5e0..84e72ad58 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -512,7 +512,6 @@ sq: search: "shkruaj për të kërkuar ftesat..." title: "Ftesa" user: "Anëtarët e Ftuar" - none: "Nuk keni ftuar asnjë deri më tani." truncated: "Shfaq {{count}} ftesat e para." redeemed: "Ridërgo ftesat" redeemed_tab: "Redeemed" @@ -710,7 +709,7 @@ sq: title_placeholder: "What is this discussion about in one brief sentence?" edit_reason_placeholder: "pse jeni duke e redaktuar?" show_edit_reason: "(arsye redaktimit)" - reply_placeholder: "Type here. Use Markdown, BBCode, or HTML to format. Drag or paste an image to upload it." + reply_placeholder: "Type here. Use Markdown, BBCode, or HTML to format. Drag or paste images." view_new_post: "Shikoni postimin tuaj te ri." saving: "Duke ruajtur..." saved: "U Ruajt!" @@ -788,7 +787,7 @@ sq: local_tip: "select images from your device" local_tip_with_attachments: "select images or files from your device ({{authorized_extensions}})" hint: "(you can also drag & drop into the editor to upload them)" - hint_for_supported_browsers: "you can also drag and drop or paste images into the editor to upload them" + hint_for_supported_browsers: "you can also drag and drop or paste images into the editor" uploading: "Duke ngarkaur" select_file: "Select File" image_link: "link your image will point to" @@ -1387,6 +1386,8 @@ sq: help: "You bookmarked this topic" locked: help: "This topic is closed; it no longer accepts new replies" + archived: + help: "This topic is archived; it is frozen and cannot be changed" unpinned: title: "Unpinned" help: "This topic is unpinned for you; it will display in regular order" @@ -1396,8 +1397,6 @@ sq: pinned: title: "Pinned" help: "This topic is pinned for you; it will display at the top of its category" - archived: - help: "This topic is archived; it is frozen and cannot be changed" invisible: help: "This topic is unlisted; it will not be displayed in topic lists, and can only be accessed via a direct link" posts: "Postime" diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index 3a3fdf121..8f4db72c6 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -101,6 +101,7 @@ sv: google+: 'dela denna länk på Google+' email: 'skicka denna länk i ett email' action_codes: + split_topic: "Dela upp denna tråd" autoclosed: enabled: 'stängdes %{when}' disabled: 'öppnades %{when}' @@ -498,7 +499,6 @@ sv: search: "sök efter inbjudningar..." title: "Inbjudningar" user: "Inbjuden Användare" - none: "Du har inte bjudit in någon här ännu." truncated: "Visar de första {{count}} inbjudningarna." redeemed: "Inlösta Inbjudnignar" redeemed_at: "Inlöst" @@ -1356,6 +1356,8 @@ sv: help: "Du bokmärkte nu detta ämnet." locked: help: "Det här ämnet är stängt; det går inte längre att svara på inlägg" + archived: + help: "Det här ämnet är arkiverat; det är fryst och kan inte ändras" unpinned: title: "Avklistrat" help: "Detta ämne är oklistrat för dig. Det visas i vanlig ordning" @@ -1365,8 +1367,6 @@ sv: pinned: title: "Klistrat" help: "Detta ämne är klistrat för dig. Det visas i toppen av dess kategori" - archived: - help: "Det här ämnet är arkiverat; det är fryst och kan inte ändras" invisible: help: "Det här ämnet är olistat; det kommer inte visas i ämneslistorna och kan bara nås via en direktlänk" posts: "Inlägg" diff --git a/config/locales/client.te.yml b/config/locales/client.te.yml index 50dcae640..e9726bc6b 100644 --- a/config/locales/client.te.yml +++ b/config/locales/client.te.yml @@ -418,7 +418,6 @@ te: search: "ఆహ్వానాలను వెతకడానికి రాయండి ... " title: "ఆహ్వానాలు" user: "ఆహ్వానించిన సభ్యుడు" - none: "మీరు ఇక్కడ ఇంకా ఎవరినీ ఆహ్వానించలేదు." truncated: "తొలి {{count}} ఆహ్వానాలను చూపుతున్నాము." redeemed: "మన్నించిన ఆహ్వానాలు" redeemed_at: "మన్నించిన" @@ -1155,6 +1154,8 @@ te: help: "ఈ విషయానికి పేజీక ఉంచారు" locked: help: "ఈ విషయం ముగిసింది. కొత్త జవాబులు అంగీకరించదు. " + archived: + help: "ఈ విషయం కట్టకట్టబడింది. ఇది గడ్డకట్టుకుంది ఇహ మార్చయిత కాదు" unpinned: title: "అగ్గుచ్చిన" help: "ఈ విషయం మీకు అగ్గుచ్చబడింది. ఇది ఇహ క్రమ వరుసలోనే కనిపిస్తుంది" @@ -1164,8 +1165,6 @@ te: pinned: title: "గుచ్చారు" help: "ఈ విషయం మీకు గుచ్చబడింది. దాని వర్గంలో అది అగ్రభాగాన కనిపిస్తుంది." - archived: - help: "ఈ విషయం కట్టకట్టబడింది. ఇది గడ్డకట్టుకుంది ఇహ మార్చయిత కాదు" invisible: help: "ఈ విషయం జాబితాలనుండి తొలగించబడింది. ఇహ కేవలం నేరు లంకె ద్వారా మాత్రమే చూడగలరు." posts: "టపాలు" diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index d359c28e6..4e637adb1 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -89,6 +89,26 @@ tr_TR: facebook: 'bu bağlantıyı Facebook''da paylaşın' google+: 'bu bağlantıyı Google+''da paylaşın' email: 'bu bağlantıyı e-posta ile gönderin' + action_codes: + split_topic: "bu konuyu ayır" + autoclosed: + enabled: '%{when} kapatıldı' + disabled: '%{when} açıldı' + closed: + enabled: '%{when} kapatıldı' + disabled: '%{when} açıldı' + archived: + enabled: '%{when} arşivlendi' + disabled: '%{when} arşivden çıkarıldı' + pinned: + enabled: '%{when} sabitlendi' + disabled: '%{when} sabitlikten çıkarıldı' + pinned_globally: + enabled: '%{when} genel olarak sabitlendi' + disabled: '%{when} genel olarak sabitleme kaldırıldı' + visible: + enabled: '%{when} listelendi' + disabled: '%{when} listelenmedi' topic_admin_menu: "konuyla alakalı yönetici işlemleri" emails_are_disabled: "Tüm giden e-postalar yönetici tarafından evrensel olarak devre dışı bırakıldı. Herhangi bir e-posta bildirimi gönderilmeyecek." edit: 'bu konunun başlığını ve kategorisini düzenleyin' @@ -104,6 +124,7 @@ tr_TR: admin_title: "Yönetici" flags_title: "Bayraklar" show_more: "devamını göster" + show_help: "seçenekler" links: "Bağlantılar" links_lowercase: other: "bağlantılar" @@ -157,16 +178,16 @@ tr_TR: bookmarks: not_logged_in: "üzgünüz, gönderileri işaretleyebilmeniz için oturum açmanız gerekiyor." created: "bu gönderiyi işaretlediniz" - not_bookmarked: "bu gönderiyi okudunuz; işaretlemek için tıklayın" - last_read: "bu okuduğunuz son gönderi; işaretlemek için tıklayın" + not_bookmarked: "bu gönderiyi okudunuz; yer imlerinize eklemek için tıklayın" + last_read: "bu okuduğunuz son gönderi; yer imlerinize eklemek için tıklayın" remove: "İşareti Kaldır" confirm_clear: "Bu konuya ait tüm işaretleri kaldırmak istediğinize emin misiniz?" topic_count_latest: - other: "{{count}} yeni ya da güncellenmiş konular." + other: "{{count}} yeni ya da güncellenmiş konu." topic_count_unread: - other: "{{count}} okunmamış konular." + other: "{{count}} okunmamış konu." topic_count_new: - other: "{{count}} yeni konular." + other: "{{count}} yeni konu." click_to_show: "Görüntülemek için tıklayın." preview: "önizleme" cancel: "İptal" @@ -257,6 +278,9 @@ tr_TR: mods_and_admins: "Sadece Moderatörler ve Yöneticiler" members_mods_and_admins: "Sadece Grup Üyeleri, Moderatörler ve Yöneticiler" everyone: "Herkes" + trust_levels: + title: "Eklendiklerinde üyelere otomatik olarak güven seviyesi verilir:" + none: "Hiç" user_action_groups: '1': "Verilen Beğeniler" '2': "Alınan Beğeniler" @@ -276,6 +300,10 @@ tr_TR: all_subcategories: "hepsi" no_subcategory: "hiçbiri" category: "Kategori" + reorder: + title: "Kategorileri Yeniden Sırala" + save: "Sıralamayı Kaydet" + apply_all: "Uygula" posts: "Gönderiler" topics: "Konular" latest: "En Son" @@ -303,6 +331,8 @@ tr_TR: topics_entered: "açılan konular" post_count: "# gönderi" confirm_delete_other_accounts: "Bu hesapları silmek isteğinize emin misiniz?" + user_fields: + none: "(bir seçenek seçin)" user: said: "{{username}}:" profile: "Profil" @@ -314,11 +344,23 @@ tr_TR: private_messages: "Mesajlar" activity_stream: "Aktivite" preferences: "Seçenekler" + expand_profile: "Genişlet" bookmarks: "İşaretlenenler" bio: "Hakkımda" invited_by: "Tarafından Davet Edildi" trust_level: "Güven Seviyesi" notifications: "Bildirimler" + desktop_notifications: + label: "Masaüstü Bildirimleri" + not_supported: "Bildirimler bu tarayıcıda desteklenmiyor. Üzgünüz." + perm_default: "Bildirimleri Etkinleştirin" + perm_denied_btn: "Erişim İzni Reddedildi" + perm_denied_expl: "Bildirimler için gerekli izne sahip değilsiniz. Bildirimleri etkinleştirmek için tarayıcınızı kullanın, işlem tamamlandığında tuşa basın. (Masaüstü: Adres çubuğunda en soldaki simge. Mobil: 'Site Bilgisi'.)" + disable: "Bildirimleri Devre Dışı Bırakın" + currently_enabled: "(şu anda etkin)" + enable: "Bildirimleri Etkinleştirin" + currently_disabled: "(şu anda devre dışı)" + each_browser_note: "Not: Bu ayarı kullandığınız her tarayıcıda değiştirmelisiniz." dismiss_notifications: "Hepsini okunmuş olarak işaretle" dismiss_notifications_tooltip: "Tüm okunmamış bildirileri okunmuş olarak işaretle" disable_jump_reply: "Cevapladıktan sonra gönderime atlama" @@ -331,6 +373,7 @@ 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_reason: "Neden:" github_profile: "Github" @@ -391,6 +434,7 @@ tr_TR: upload_title: "Resminizi yükleyin" upload_picture: "Resim Yükle" image_is_not_a_square: "Uyarı: resminizi kırptık; genişlik ve yükseklik eşit değildi." + cache_notice: "Profil resminizi başarıyla değiştirdiniz fakat tarayıcı önbelleklemesi nedeniyle görünür olması biraz zaman alabilir." change_profile_background: title: "Profil Arkaplanı" instructions: "Profil arkaplanları ortalanacak ve genişlikleri 850px olacak. " @@ -399,14 +443,14 @@ tr_TR: instructions: "Profil arkaplanları ortalanacak ve genişlikleri 590px olacak. " email: title: "E-posta" - instructions: "Kimseye gösterilmeyecek" + instructions: "Kimseye gösterilmeyecek." ok: "Onay için size e-posta atacağız" invalid: "Lütfen geçerli bir e-posta adresini giriniz" authenticated: "E-posta adresiniz {{provider}} tarafından doğrulanmıştır" frequency: zero: "Eğer yollamak üzere olduğumuz şeyi okumadıysanız size direk e-posta yollayacağız." - one: "Sadece sizi son bir dakika içinde görmediysek size e-posta yollayacağız." - other: "Sadece sizi son {{count}} dakika içerisinde görmediysek size e-posta yollayacağız." + one: "Sadece son bir dakika içinde sizi görmediysek e-posta yollayacağız." + other: "Sadece son {{count}} dakika içinde sizi görmediysek e-posta yollayacağız." name: title: "İsim" instructions: "Tam adınız (zorunlu değil)" @@ -457,21 +501,35 @@ tr_TR: label: "Seçili durumdaki konular yeni sayılsın" not_viewed: "Onları henüz görüntülemedim" last_here: "son ziyaretimden beri oluşturulanlar" + after_1_day: "son 1 gün içinde oluşturuldu" + after_2_days: "son 2 gün içinde oluşturuldu" + after_1_week: "son 1 hafta içinde oluşturuldu" + after_2_weeks: "son 2 hafta içinde oluşturuldu" auto_track_topics: "Girdiğim konuları otomatik olarak takip et" auto_track_options: never: "asla" immediately: "hemen" + after_30_seconds: "30 saniye sonra" + after_1_minute: "1 dakika sonra" + after_2_minutes: "2 dakika sonra" + after_3_minutes: "3 dakika sonra" + after_4_minutes: "4 dakika sonra" + after_5_minutes: "5 dakika sonra" + after_10_minutes: "10 dakika sonra" invited: - search: "davetler arasında aramak için yazın..." + search: "davetiye aramak için yazın..." title: "Davetler" user: "Davet Edilen Kullanıcı" - none: "Henüz buraya kimseyi davet etmediniz." + sent: "Gönderildi" + none: "Bekleyen davet yok." truncated: "İlk {{count}} davetler gösteriliyor." redeemed: "Kabul Edilen Davetler" redeemed_tab: "Kabul Edildi" + redeemed_tab_with_count: "İtfa edilmiş ({{count}})" redeemed_at: "Kabul Edildi" pending: "Bekleyen Davetler" pending_tab: "Bekleyen" + pending_tab_with_count: "Beklemede ({{count}})" topics_entered: "Görüntülenmiş Konular" posts_read_count: "Okunmuş Yazılar" expired: "Bu davetin süresi doldu." @@ -483,6 +541,8 @@ tr_TR: days_visited: "Ziyaret Edilen Günler" account_age_days: "Gün içinde Hesap yaş" create: "Davet Yolla" + generate_link: "Davet bağlantısını kopyala" + generated_link_message: '<p>Davet bağlantısı başarılı bir şekilde oluşturuldu!</p><p><input class="invite-link-input" style="width: 75%;" type="text" value="%{inviteLink}"></p><p>Davet bağlantısı sadece bu e-posta adresi için geçerlidir: <b>%{invitedEmail}</b></p>' bulk_invite: none: "Henüz kimseyi buraya davet etmediniz. Tek tek davetiye gönderebilirsiniz, ya da <a href='https://meta.discourse.org/t/send-bulk-invites/16468'>toplu bir davetiye dosyası yükleyerek</a> birçok kişiyi aynı anda davet edebilirsiniz. " text: "Dosyadan Toplu Davet Gönder" @@ -539,6 +599,7 @@ tr_TR: read_only_mode: enabled: "Salt-okunur modu etkin. Siteyi gezmeye devam edebilirsiniz fakat etkileşimler çalışmayabilir." login_disabled: "Site salt-okunur modda iken oturum açma devre dışı bırakılır ." + too_few_topics_notice: "Hadi <a href='http://blog.discourse.org/2014/08/building-a-discourse-community/'>bu tartışmayı başlatalım!</a> Şu anda <strong>%{currentTopics} / %{requiredTopics}</strong> konu ve <strong>%{currentPosts} / %{requiredPosts}</strong> gönderi var. Yeni ziyaretçiler okumak ve yanıtlamak için birkaç tartışmaya ihtiyaç duyarlar." learn_more: "daha fazlasını öğren..." year: 'yıl' year_desc: 'son 365 günde oluşturulan konular' @@ -719,10 +780,24 @@ tr_TR: private_message: "<i title='özel mesaj' class='fa fa-envelope-o'></i><p><span>{{username}}</span> {{description}}</p>" invited_to_private_message: "<i title='özel mesaj' class='fa fa-envelope-o'></i><p><span>{{username}}</span> {{description}}</p>" invited_to_topic: "<i title='konuya davet edildi' class='fa fa-hand-o-right'></i><p><span>{{username}}</span> {{description}}</p>" - invitee_accepted: "<i title='davetiyeni kabul etti' class='fa fa-user'></i><p><span>{{username}}</span> davetiyeni kabul etti</p>" + invitee_accepted: "<i title='davetiyeni kabul etti' class='fa fa-user'></i><p><span>{{username}}</span> davetini kabul etti!</p>" moved_post: "<i title='gönderiyi taşıdı' class='fa fa-sign-out'></i><p><span>{{username}}</span> taşıdı {{description}}</p>" linked: "<i title='gönderiye bağlantı verdi' class='fa fa-arrow-left'></i><p><span>{{username}}</span> {{description}}</p>" - granted_badge: "<i title='badge granted' class='fa fa-certificate'></i><p>Kazanıldı'{{description}}'</p>" + granted_badge: "<i title='badge granted' class='fa fa-certificate'></i><p><strong>{{description}}</strong> rozeti kazandınız!</p>" + alt: + mentioned: "Bahsedildi, şu kişi tarafından" + quoted: "Alıntılandı, şu kişi tarafından" + replied: "Cevaplandı" + posted: "Gönderildi, şu kişi tarafından" + edited: "Gönderiniz düzenlendi, şu kişi tarafından" + liked: "Gönderiniz beğenildi" + private_message: "Özel mesaj, şu kişiden" + invited_to_private_message: "Bir özel mesaja davet edildiniz, şu kişi tarafından" + invited_to_topic: "Bir konuya davet edildiniz, şu kişi tarafından" + invitee_accepted: "Davet kabul edildi, şu kişi tarafından" + moved_post: "Gönderiniz taşındı, şu kişi tarafından" + linked: "Gönderinize bağlantı" + granted_badge: "Rozet alındı" popup: mentioned: '{{username}}, "{{topic}}" başlıklı konuda sizden bahsetti - {{site_title}}' quoted: '{{username}}, "{{topic}}" başlıklı konuda sizden alıntı yaptı - {{site_title}}' @@ -745,15 +820,19 @@ tr_TR: select_file: "Dosya seçin" image_link: "resminizin yönleneceği bağlantı" search: - title: "konular, gönderiler, kullanıcılar, veya kategoriler arasında ara" + title: "konu, gönderi, kullanıcı veya kategori ara" no_results: "Hiç bir sonuç bulunamadı." + no_more_results: "Başka sonuç yok." + search_help: Arama yardımı searching: "Aranıyor..." post_format: "{{username}} tarafından #{{post_number}}" context: - user: "@{{username}} kullancısına ait gönderiler arasında ara" + user: "@{{username}} kullancısına ait gönderilerde ara" category: "\"{{category}}\" kategorisinde ara" topic: "Bu konuda ara" private_messages: "Mesajlarda ara" + hamburger_menu: "bir diğer konu ya da kategoriye git" + new_item: "Yeni!" go_back: 'geri dön' not_logged_in_user: 'güncel aktivitelerin ve ayarların özetinin bulunduğu kullanıcı sayfası' current_user: 'kendi kullanıcı sayfana git' @@ -777,7 +856,7 @@ tr_TR: other: "<b>{{count}}</b> konu seçtiniz." none: unread: "Okunmamış konunuz yok." - new: "Yeni konunuz yok" + new: "Yeni konunuz yok." read: "Henüz herhangi bir konu okumadınız." posted: "Henüz herhangi bir konuda gönderi oluşturmadınız." latest: "Son bir konu yok. Bu üzücü." @@ -787,7 +866,7 @@ tr_TR: top: "Popüler bir konu yok." search: "Arama sonuçları yok." educate: - new: '<p>Yeni konularınız burada belirir.</p><p>Varsayılan ayarlarda, son 2 gün içerisinde yaratılan konular yeni sayılır ve <span class="badge new-topic badge-notification" style="vertical-align:middle;line-height:inherit;">yeni</span> işaretiyle gösterilir.</p><p>Dilerseniz bu seçeneği <a href="%{userPrefsUrl}">ayarlar</a> sayfanızdan düzenleyebilirsiniz.</p>' + new: '<p>Yeni konularınız burada belirir.</p><p>Varsayılan ayarlarda, son 2 gün içerisinde açılan konular yeni sayılır ve <span class="badge new-topic badge-notification" style="vertical-align:middle;line-height:inherit;">yeni</span> işaretiyle gösterilir.</p><p>Dilerseniz bu seçeneği <a href="%{userPrefsUrl}">ayarlar</a> sayfanızdan düzenleyebilirsiniz.</p>' unread: '<p> Okunmamış konularınız burada belirecek. </p><p> Varsayılan ayarlarda, şu durumlarda konular okunmamış sayılır ve okunmamışların sayısı <span class="badge new-posts badge-notification">1</span> gösterilir: </p><ul><li>Konuyu oluşturduysanız</li><li>Konuyu cevapladıysanız</li><li>Konuyu 4 dakikadan uzun bir süre okuduysanız</li></ul><p> Ya da, konunun altında bulunan bildirim kontrol bölümünden, konuyu Takip Edildi veya Gözlendi diye işaretlediyseniz.</p><p>Bu ayarları <a href="%{userPrefsUrl}">ayarlar</a> sayfasından değiştirebilirsiniz.</p>' bottom: latest: "Daha fazla son konu yok." @@ -801,6 +880,9 @@ tr_TR: bookmarks: "Daha fazla işaretlenmiş konu yok." search: "Daha fazla arama sonucu yok." topic: + unsubscribe: + stop_notifications: "Artık <strong>{{title}}</strong> için daha az bildirim alacaksınız." + change_notification_state: "Geçerli bildirim durumunuz" filter_to: "Bu konuda {{post_count}} gönderi" create: 'Yeni Konu' create_long: 'Yeni bir konu oluştur' @@ -888,8 +970,10 @@ tr_TR: title: "Takip Ediliyor" description: "Okunmamış ve yeni gönderi sayısı başlığın yanında belirecek. Birisi @isim şeklinde sizden bahsederse ya da gönderinize cevap verirse bildirim alacaksınız." regular: + title: "Olağan" description: "Birisi @isim şeklinde sizden bahsederse ya da gönderinize cevap verirse bildirim alacaksınız." regular_pm: + title: "Olağan" description: "Birisi @isim şeklinde sizden bahsederse ya da gönderinize mesajla cevap verirse bildirim alacaksınız." muted_pm: title: "Susturuldu" @@ -932,15 +1016,20 @@ tr_TR: success_message: 'Bu konuyu başarıyla bayrakladınız.' feature_topic: title: "Bu konuyu ön plana çıkar" + pin: "Şu zamana kadar bu konunun {{categoryLink}} kategorisinin başında görünmesini sağla" confirm_pin: "Zaten başa tutturulan {{count}} konunuz var. Çok fazla konuyu başa tutturmak yeni ve anonim kullanıcılara sıkıntı çektirebilir. Bu kategoride bir konuyu başa tutturmak istediğinize emin misiniz?" unpin: "Bu konuyu {{categoryLink}} kategorisinin en üstünden kaldır." + unpin_until: "Bu konuyu {{categoryLink}} kategorisinin başından kaldır ya da şu zamana kadar bekle: <strong>%{until}</strong>." pin_note: "Kullanıcılar kendileri için konunun başa tutturulmasını kaldırabilir." + pin_validation: "Bu konuyu sabitlemek için bir tarih gerekli." already_pinned: zero: " {{categoryLink}} kategorisinde başa tutturulan herhangi bir konu yok." one: "Şu an {{categoryLink}} kategorisinde başa tutturulan konular: <strong class='badge badge-notification unread'>1</strong>." other: "Şu an {{categoryLink}} kategorisinde başa tutturulan konular: <strong class='badge badge-notification unread'>{{count}}</strong>." + pin_globally: "Şu zamana kadar bu konunun bütün konu listelerinin başında yer almasını sağla" confirm_pin_globally: "Zaten her yerde başa tutturulan {{count}} konunuz var. Çok fazla konuyu başa tutturmak yeni ve anonim kullanıcılara sıkıntı çektirebilir. Bir konuyu daha her yerde başa tutturmak istediğinizden emin misiniz?" unpin_globally: "Bu konuyu tüm konu listelerinin en üstünden kaldır." + unpin_globally_until: "Bu konuyu bütün konu listelerinin başından kaldır ya da şu zamana kadar bekle: <strong>%{until}</strong>." global_pin_note: "Kullanıcılar kendileri için konunun başa tutturulmasını kaldırabilir." already_pinned_globally: zero: "Her yerde başa tutturulan herhangi bir konu yok." @@ -1005,6 +1094,12 @@ tr_TR: instructions: other: "Lütfen <b>{{old_user}}</b> kullanıcısına ait {{count}} gönderinin yeni sahibini seçin." instructions_warn: "Bu gönderi ile ilgili geriye dönük biriken bildirimler yeni kullanıcıya aktarılmayacak.<br>Uyarı: Şu an, yeni kullanıcıya hiç bir gönderi-tabanlı ek bilgi aktarılmıyor. Dikkatli olun." + change_timestamp: + title: "Değişiklik Zaman Bilgisi" + action: "değişiklik zaman bilgisi" + invalid_timestamp: "Zaman bilgisi gelecekte olamaz." + error: "Konunun zaman bilgisini değiştirirken bir hata oluştu." + instructions: "Lütfen konunun yeni zaman bilgisini seçiniz. Konudaki gönderiler aynı zaman farkına sahip olmaları için güncellenecekler." multi_select: select: 'seç' selected: '({{count}}) seçildi' @@ -1016,6 +1111,8 @@ tr_TR: description: other: <b>{{count}}</b> gönderi seçtiniz. post: + reply: "<i class='fa fa-mail-forward'></i> {{replyAvatar}} {{usernameLink}}" + reply_topic: "<i class='fa fa-mail-forward'></i> {{link}}" quote_reply: "alıntıyla cevapla" edit: "{{link}} {{replyAvatar}} {{username}} düzenleniyor" edit_reason: "Neden: " @@ -1230,11 +1327,13 @@ tr_TR: email_in_allow_strangers: "Hesabı olmayan, anonim kullanıcılardan e-posta kabul et" email_in_disabled: "E-posta üzerinden yeni konu oluşturma özelliği Site Ayarları'nda devre dışı bırakılmış. E-posta üzerinden yeni konu oluşturma özelliğini etkinleştirmek için," email_in_disabled_click: '"e-postala" ayarını etkinleştir' + suppress_from_homepage: "Bu kategoriyi ana sayfadan gizle" allow_badges_label: "Bu kategoride rozet verilmesine izin ver" edit_permissions: "İzinleri Düzenle" add_permission: "İzin Ekle" this_year: "bu yıl" position: "pozisyon" + reorder: "Yeniden Sırala" default_position: "Varsayılan Pozisyon" position_disabled: "Kategoriler etkinlik sıralarına göre görünecekler. Listelerdeki kategorilerin sıralamalarını kontrol edebilmek için," position_disabled_click: '"sabitlenmiş kategori pozisyonları" ayarını etklinleştirin.' @@ -1294,6 +1393,10 @@ tr_TR: help: "Bu konuyu işaretlediniz" locked: help: "Bu konu kapatıldı; artık yeni cevaplar kabul edilmiyor" + archived: + help: "Bu başlık arşive kaldırıldı; donduruldu ve değiştirilemez" + locked_and_archived: + help: "Bu konu kapatıldı ve arşivlendi; yeni cevaplar kabul edemez ve değiştirilemez." unpinned: title: "Başa tutturma kaldırıldı" help: "Bu konu sizin için başa tutturulmuyor; normal sıralama içerisinde görünecek" @@ -1303,8 +1406,6 @@ tr_TR: pinned: title: "Başa Tutturuldu" help: "Bu konu sizin için başa tutturuldu; kendi kategorisinin en üstünde görünecek" - archived: - help: "Bu başlık arşive kaldırıldı; donduruldu ve değiştirilemez" invisible: help: "Bu konu listelenmemiş; konu listelerinde görünmeyecek, ve sadece doğrudan bağlantı aracılığıyla erişilebilecek" posts: "Gönderi" @@ -1397,6 +1498,8 @@ tr_TR: title: "Tüm Zamanlar" yearly: title: "Yıllık" + quarterly: + title: "Üç aylık" monthly: title: "Aylı" weekly: @@ -1405,6 +1508,7 @@ tr_TR: title: "Günlük" all_time: "Tüm Zamanlar" this_year: "Yıl" + this_quarter: "Çeyrek" this_month: "Ay" this_week: "Hafta" today: "Bugün" @@ -1652,6 +1756,7 @@ tr_TR: header: "Başlık" top: "En Kısım" footer: "Alt Kısım" + embedded_css: "Gömülü CSS" head_tag: text: "</head>" title: "</head> etiketinden önce eklenecek HTML" @@ -1743,6 +1848,7 @@ tr_TR: sent_test: "gönderildi!" delivery_method: "Gönderme Metodu" preview_digest: "Özeti Önizle" + preview_digest_desc: "Durgun kullanıcılara gönderilen özet e-postaların içeriğini önizle." refresh: "Yenile" format: "Format" html: "html" @@ -2022,6 +2128,7 @@ tr_TR: delete: "Sil" cancel: "İptal et" delete_confirm: "Bu kullanıcı alanını silmek istediğinize emin misiniz?" + options: "Seçenekler" required: title: "Kayıt olurken zorunlu mu?" enabled: "gerekli" @@ -2037,6 +2144,7 @@ tr_TR: field_types: text: 'Yazı Alanı' confirm: 'Onay' + dropdown: "Açılır liste" site_text: none: "Düzenlemeye başlamak için içerik tipi seçin." title: 'Yazı İçeriği' @@ -2070,6 +2178,7 @@ tr_TR: backups: "Yedekler" login: "Oturum Açma" plugins: "Eklentiler" + user_preferences: "Kullanıcı Tercihleri" badges: title: Rozetler new_badge: Yeni Rozet @@ -2143,6 +2252,29 @@ tr_TR: name: "İsim" image: "Görsel" delete_confirm: ":%{name}: emojisini silmek istediğinize emin misiniz?" + embedding: + get_started: "Eğer Discourse'u bir başka web sitesine gömmek istiyorsanız, bu sitenin hostunu ekleyerek başlayın." + confirm_delete: "Bu hostu silmek istediğinize emin misiniz?" + sample: "Discourse konuları oluşturmak ve gömmek için aşağıdaki HTML kodunu sitenizde kullanın. <b>REPLACE_ME</b>'yi Discourse'u gömdüğünüz sayfanın tam URL'i ile değiştirin." + title: "Gömme" + host: "İzin Verilen Hostlar" + edit: "düzenle" + category: "Kategoriye Gönder" + add_host: "Host Ekle" + settings: "Ayarları Gömmek" + feed_settings: "Ayarları Besle" + feed_description: "Siteniz için bir RSS/ATOM beslemesi sağlamanız Discourse'un içeriğinizi içe aktarma yeteneğini geliştirebilir." + crawling_settings: "Böcek Ayarları" + crawling_description: "Discourse gönderileriniz için konular oluşturduğu zaman, eğer bir RSS/ATOM beslemesi yoksa içeriğinizi HTML'inizden ayrıştırmaya çalışacaktır. Bazen içeriğinizi çıkartmak çok zor olabilir, bu yüzden ayrıştırmayı kolaylaştırmak için CSS kuralları belirtme yeteneği sağlıyoruz." + embed_by_username: "Konu oluşturmak için kullanıcı adı" + embed_post_limit: "Gömmek için en büyük gönderi sayısı" + embed_username_key_from_feed: "Discourse kullanıcı adını beslemeden çekmek için anahtar" + embed_truncate: "Gömülü gönderileri buda" + embed_whitelist_selector: "Gömülüler içinde izin verilen elementler için CSS seçici" + embed_blacklist_selector: "Gömülülerden kaldırılan elementler için CSS seçici" + feed_polling_enabled: "Konuları RSS/ATOM aracılığıyla içe aktar" + feed_polling_url: "İstila etmek için RSS/ATOM beslemesi URL'i" + save: "Gömme Ayarlarını Kaydet" permalink: title: "Kalıcı Bağlantılar" url: "Bağlantı" @@ -2173,6 +2305,8 @@ tr_TR: categories: '<b>g</b>, <b>c</b> Kategoriler' top: '<b>g</b>, <b>t</b> En Popüler' bookmarks: '<b>g</b>, <b>b</b> İşaretliler' + profile: '<b>g</b>, <b>p</b> Profil' + messages: '<b>g</b>, <b>m</b> İletiler' navigation: title: 'Navigasyon' jump: '<b>#</b> # numaralı gönderiye git' @@ -2184,12 +2318,14 @@ tr_TR: title: 'Uygulama' create: '<b>c</b> Yeni konu aç' notifications: '<b>n</b> Bildirileri aç' + hamburger_menu: '<b>=</b> Hamburger menüsünü aç' user_profile_menu: '<b>p</b> Kullanıcı menüsünü aç' show_incoming_updated_topics: '<b>.</b> Güncellenmiş konuları göster' search: '<b>/</b> Arama' help: '<b>?</b> Klavye yardımını göster' dismiss_new_posts: '<b>x</b>, <b>r</b> Yeni Konuları/Gönderleri Yoksay' dismiss_topics: '<b>x</b>, <b>t</b> Konuları Yoksay' + log_out: '<b>shift</b>+<b>z</b> <b>shift</b>+<b>z</b> Çıkış Yapın' actions: title: 'Seçenekler' bookmark_topic: '<b>f</b> Konu işaretlenmesini aç/kapa' @@ -2314,3 +2450,21 @@ tr_TR: reader: name: Okuyucu description: 100'den fazla gönderiye sahip bir konudaki tüm gönderileri oku + popular_link: + name: Gözde Bağlantı + description: En az 50 kere tıklanmış harici bir bağlantı gönderildi + hot_link: + name: Sıcak Bağlantı + description: En az 300 kere tıklanmış harici bir bağlantı gönderildi + famous_link: + name: Ünlü Bağlantı + description: En az 1000 kere tıklanmış harici bir bağlantı gönderildi + google_search: | + <h3>Google'la Ara</h3> + <p> + <form action='//google.com/search' id='google-search' onsubmit="document.getElementById('google-query').value = 'site:' + window.location.host + ' ' + document.getElementById('user-query').value; return true;"> + <input type="text" id='user-query' value=""> + <input type='hidden' id='google-query' name="q"> + <button class="btn btn-primary">Google</button> + </form> + </p> diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 52807c6bc..545d4d903 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -913,12 +913,12 @@ uk: topic_statuses: locked: help: "цю тему закрито; нові відповіді більше не приймаються" + archived: + help: "цю тему заархівовано; вона заморожена і її не можна змінити" unpinned: title: "Не закріплені" pinned: title: "Закріплені" - archived: - help: "цю тему заархівовано; вона заморожена і її не можна змінити" posts: "Дописи" posts_lowercase: "дописи" posts_long: "тема містить {{number}} дописів" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 3d2ab622b..b351c12c3 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -278,6 +278,9 @@ zh_CN: mods_and_admins: "仅版主与管理员" members_mods_and_admins: "仅组员、版主与管理员" everyone: "任何人" + trust_levels: + title: "当这些用户加入时,信任等级将自动赋予给他们:" + none: "无" user_action_groups: '1': "给赞" '2': "被赞" @@ -337,6 +340,7 @@ zh_CN: private_messages: "消息" activity_stream: "活动" preferences: "设置" + expand_profile: "展开" bookmarks: "书签" bio: "关于我" invited_by: "邀请者为" @@ -513,7 +517,7 @@ zh_CN: title: "邀请" user: "邀请用户" sent: "已发送" - none: "你还没有邀请过任何人。" + none: "没有未接受状态的邀请。" truncated: "只显示前 {{count}} 个邀请。" redeemed: "确认邀请" redeemed_tab: "已确认" @@ -776,6 +780,20 @@ zh_CN: moved_post: "<i title='移动了帖子' class='fa fa-sign-out'></i><p><span>{{username}}</span> 移动了 {{description}}</p>" linked: "<i title='被外链的帖子' class='fa fa-arrow-left'></i><p><span>{{username}}</span> {{description}}</p>" granted_badge: "<i title='徽章授予' class='fa fa-certificate'></i><p>获得“{{description}}”</p>" + alt: + mentioned: "被提及" + quoted: "被引用" + replied: "回复" + posted: "发自" + edited: "编辑你的帖子" + liked: "赞了你的帖子" + private_message: "私信来自" + invited_to_private_message: "私信邀请自" + invited_to_topic: "主题邀请自" + invitee_accepted: "介绍邀请自" + moved_post: "你的帖子被移动自" + linked: "链接至你的帖子" + granted_badge: "勋章授予" popup: mentioned: '{{username}}在“{{topic}}”提到了你 - {{site_title}}' quoted: '{{username}}在“{{topic}}”引用了你的帖子 - {{site_title}}' @@ -814,7 +832,7 @@ zh_CN: not_logged_in_user: '显示当前活动和设置的用户页面' current_user: '去你的用户页面' topics: - too_many_tracked: "警告:你有太多追踪的新主题和未读主题,使用“设为已读”清除一些" + too_many_tracked: "警告:你有太多追踪的新主题和未读主题,使用“<a href='/new'>设为已读</a>”或<a href='/unread'>忽略帖子</a>清除一些" bulk: reset_read: "设为未读" delete: "删除主题" @@ -1305,6 +1323,7 @@ zh_CN: email_in_allow_strangers: "接受无账号的匿名用户的邮件" email_in_disabled: "站点设置中已经禁用通过邮件发表新主题。欲启用通过邮件发表新主题," email_in_disabled_click: '启用“邮件发表”设置。' + suppress_from_homepage: "不在主页中显示这个分类。" allow_badges_label: "允许在这个分类中授予徽章" edit_permissions: "编辑权限" add_permission: "添加权限" @@ -1369,6 +1388,10 @@ zh_CN: help: "你已经收藏了此主题" locked: help: "本主题已关闭;不再接受新的回复" + archived: + help: "本主题已归档;即已经冻结,无法修改" + locked_and_archived: + help: "本主题已经关闭并且存档;不再接受新回复且无法修改" unpinned: title: "解除置顶" help: "主题已经解除置顶;它将以默认顺序显示" @@ -1378,8 +1401,6 @@ zh_CN: pinned: title: "置顶" help: "本主题已置顶;它将始终显示在它所属分类的顶部" - archived: - help: "本主题已归档;即已经冻结,无法修改" invisible: help: "本主题被设置为不显示在主题列表中,并且只能通过直达链接来访问" posts: "帖子" @@ -2279,6 +2300,8 @@ zh_CN: categories: '<b>g</b> 然后 <b>c</b> 分类列表' top: '<b>g</b>, <b>t</b> 顶部' bookmarks: '<b>g</b> 然后 <b>b</b> 书签' + profile: '<b>g</b> 然后 <b>p</b> 个人页面' + messages: '<b>g</b> 然后 <b>m</b> 消息' navigation: title: '导航' jump: '<b>#</b> 跳转到帖子 #' @@ -2297,6 +2320,7 @@ zh_CN: help: '<b>?</b> 打开键盘快捷键帮助' dismiss_new_posts: '<b>x</b>, <b>r</b> 解除新/帖子提示' dismiss_topics: '<b>x</b>, <b>t</b> 解除主题提示' + log_out: '<b>shift</b>+<b>z</b> <b>shift</b>+<b>z</b> 登出' actions: title: '动作' bookmark_topic: '<b>f</b> 切换主题收藏状态' @@ -2423,12 +2447,13 @@ zh_CN: description: 阅读一个超过 100 个帖子的主题中的每一个帖子 popular_link: name: 流行链接 - description: 发布了超过 50 个点击的外部链接 + description: 发布了超过 50 次点击的外部链接 hot_link: name: 热门链接 - description: 发布了超过 300 个点击的外部链接 + description: 发布了超过 300 次点击的外部链接 famous_link: name: 著名链接 + description: 发布了超过 1000 次点击的外部链接 google_search: | <h3>用 Google 搜索</h3> <p> diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 330d74969..6df19a62b 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -66,6 +66,13 @@ zh_TW: other: "%{count} 小時前" x_days: other: "%{count} 天前" + later: + x_days: + other: "%{count} 天後" + x_months: + other: "%{count} 個月後" + x_years: + other: "%{count} 年後" share: topic: '在此話題內分享連結' post: '文章 #%{postNumber}' @@ -74,6 +81,8 @@ zh_TW: facebook: '在 Facebook 分享此連結' google+: '在 Google+ 分享此連結' email: '以電子郵件分享此連結' + action_codes: + split_topic: "拆分此主題" topic_admin_menu: "討論話題管理員操作" emails_are_disabled: "管理員已經停用了所有外寄郵件功能。通知信件都不會寄出。" edit: '編輯此討論話題的標題與分類' @@ -111,6 +120,7 @@ zh_TW: every_two_weeks: "每兩週" every_three_days: "每三天" max_of_count: "(最大 {{count}})" + alternation: "或者" character_count: other: "{{count}} 個字" suggested_topics: @@ -238,6 +248,8 @@ zh_TW: mods_and_admins: "只有板主以及管理員" members_mods_and_admins: "只有群組成員、板主以及管理員" everyone: "所有人" + trust_levels: + none: "無" user_action_groups: '1': "已按讚" '2': "已收到的讚" @@ -295,14 +307,25 @@ zh_TW: private_messages: "訊息" activity_stream: "活動" preferences: "偏好設定" + expand_profile: "展開" bookmarks: "書籤" bio: "關於我" invited_by: "邀請人" trust_level: "信任等級" notifications: "通知" + desktop_notifications: + label: "桌面通知" + not_supported: "非常遺憾,你的瀏覽器不支持桌面通知。" + perm_default: "啟用桌面通知" + perm_denied_btn: "權限被拒絕" + disable: "停用通知" + currently_enabled: "(當前已啟用)" + enable: "啟用通知" + currently_disabled: "(當前已關閉)" dismiss_notifications: "全部標記為已讀" dismiss_notifications_tooltip: "標記所有未讀通知為已讀" disable_jump_reply: "不要在回覆之後直接跳到我的文章" + dynamic_favicon: "在瀏覽器小圖示上顯示新主題/更新的主題數" edit_history_public: "讓其他用戶檢視我的文章修訂紀錄" external_links_in_new_tab: "以新分頁開啟所有外部連結" enable_quoting: "允許引用已標註的文字" @@ -311,6 +334,7 @@ zh_TW: admin: "{{user}} 是管理員" moderator_tooltip: "此用戶為板主" admin_tooltip: "此用戶為管理員" + blocked_tooltip: "此用戶被屏蔽" suspended_notice: "此用戶已被停權至 {{date}}。" suspended_reason: "原因: " github_profile: "Github" @@ -433,14 +457,26 @@ zh_TW: label: "視為新討論話題的條件" not_viewed: "我未看過的討論" last_here: "我上次到訪後的討論" + after_1_day: "昨天發佈的討論" + after_2_days: "過去兩天發佈的討論" + after_1_week: "過去一週發佈的討論" + after_2_weeks: "過去兩週發佈的討論" auto_track_topics: "自動追蹤我參與的討論" auto_track_options: never: "永不" + immediately: "立即" + after_30_seconds: "30 秒後" + after_1_minute: "一分鐘後" + after_2_minutes: "兩分鐘後" + after_3_minutes: "三分鐘後" + after_4_minutes: "四分鐘後" + after_5_minutes: "五分鐘後" + after_10_minutes: "十分鐘後" invited: search: "輸入要搜尋邀請的文字..." title: "邀請" user: "受邀請的用戶" - none: "你尚未邀請任何人。" + sent: "送出" truncated: "顯示前 {{count}} 個邀請。" redeemed: "已接受的邀請" redeemed_at: "接受日期" @@ -456,6 +492,7 @@ zh_TW: days_visited: "到訪天數" account_age_days: "帳號已建立 (天)" create: "送出邀請" + generate_link: "拷貝邀請連結" bulk_invite: none: "你尚未邀請任何人。你可以發送個別邀請,或者透過<a href='https://meta.discourse.org/t/send-bulk-invites/16468'>上傳邀請名單</a>一次邀請一群人。" text: "從檔案大量邀請" @@ -691,6 +728,8 @@ zh_TW: moved_post: "<i title='moved post' class='fa fa-sign-out'></i><p><span>{{username}}</span> 移動了 {{description}}</p>" linked: "<i title='linked post' class='fa fa-arrow-left'></i><p><span>{{username}}</span> {{description}}</p>" granted_badge: "<i title='badge granted' class='fa fa-certificate'></i><p>獲得徽章「{{description}}」</p>" + alt: + linked: "連結到你的討論" upload_selector: title: "加入一張圖片" title_with_attachments: "加入一張圖片或一個檔案" @@ -700,10 +739,12 @@ zh_TW: remote_tip_with_attachments: "圖片或文件連結 ({{authorized_extensions}})" hint: "(你也可以將檔案拖放至編輯器直接上傳)" uploading: "正在上傳" + select_file: "選取檔案" image_link: "連結你的圖片將指向" search: title: "搜尋討論話題、文章、用戶或分類" no_results: "未找到任何結果。" + search_help: 搜尋幫助 searching: "正在搜尋..." post_format: "#{{post_number}} {{username}}" context: @@ -833,6 +874,10 @@ zh_TW: title: "追蹤" tracking: title: "追蹤" + regular: + title: "一般" + regular_pm: + title: "一般" muted_pm: title: "靜音" description: "你將不會再收到關於此訊息的通知。" @@ -922,6 +967,10 @@ zh_TW: instructions: other: "請選擇一位新用戶作為此 {{count}} 篇由 <b>{{old_user}}</b> 撰寫之文章的擁有者。" instructions_warn: "注意,關於此篇文章的舊通知,並不會移轉到新用戶。<br>警告:目前所有與文章相關的資料都不會移轉至新用戶,請謹慎使用。" + change_timestamp: + title: "變更時間戳記" + action: "變更時間戳記" + invalid_timestamp: "時間戳記不能為將來的時刻。" multi_select: select: '選取' selected: '選取了 ({{count}})' @@ -950,6 +999,14 @@ zh_TW: other: "檢視 {{count}} 則隱藏回應" more_links: "{{count}} 更多..." unread: "文章未讀" + has_replies: + other: "{{count}} 個回覆" + has_likes: + other: "{{count}} 個讚" + has_likes_title: + other: "{{count}} 個使用者對此文章讚好" + has_likes_title_you: + zero: "你已按讚" errors: create: "抱歉,建立你的文章時發生錯誤,請再試一次。" edit: "抱歉,編輯你的文章時發生錯誤,請再試一次。" @@ -1098,12 +1155,14 @@ zh_TW: category: can: '可以… ' none: '( 無分類 )' + all: '所有分類' choose: '選擇一個分類…' edit: '編輯' edit_long: "編輯" view: '檢視分類裡的討論話題' general: '一般' settings: '設定' + topic_template: "主題模板" delete: '刪除分類' create: '新分類' save: '儲存分類' @@ -1135,6 +1194,7 @@ zh_TW: email_in_allow_strangers: "接受非用戶的電郵" email_in_disabled: "\"用電子郵件張貼新的討論話題\"功能已被關閉。若要使用此功能," email_in_disabled_click: '請啟用"email in"功能' + suppress_from_homepage: "不在首頁上顯示此分類。" allow_badges_label: "允許授予本分類的徽章" edit_permissions: "編輯權限" add_permission: "新增權限" @@ -1196,6 +1256,8 @@ zh_TW: help: "已將此討論話題加入書籤" locked: help: "此討論話題已關閉,不再接受回覆" + archived: + help: "此討論話題已封存,已被凍結無法再修改" unpinned: title: "取消釘選" help: "此討論話題已取消置頂,將會以預設順序顯示。" @@ -1205,8 +1267,6 @@ zh_TW: pinned: title: "已釘選" help: "此討論話題已置頂,將顯示在它所屬分類話題列表的最上方" - archived: - help: "此討論話題已封存,已被凍結無法再修改" invisible: help: "此討論話題已隱藏,將不會出現在討論話題列表,只能以直接連結開啟。" posts: "文章" @@ -1237,6 +1297,10 @@ zh_TW: with_topics: "%{filter} 討論話題" with_category: "%{filter} %{category} 討論話題" latest: + title: + zero: "最新主題" + one: "最新主題 (1)" + other: "最新主題 ({{count}})" help: "最近的討論話題" hot: title: "熱門" @@ -1289,6 +1353,8 @@ zh_TW: title: "所有時間" yearly: title: "年" + quarterly: + title: "季度" monthly: title: "月" weekly: @@ -1296,6 +1362,10 @@ zh_TW: daily: title: "日" all_time: "所以時間" + this_year: "年" + this_quarter: "季度" + this_month: "月" + this_week: "週" today: "今天" other_periods: "前往頂端" browser_update: '抱歉,<a href="http://www.discourse.org/faq/#browser">您的瀏覽器版本太舊,無法正常訪問該站點。</a>。請<a href="http://browsehappy.com">升級您的瀏覽器</a>。' @@ -1458,6 +1528,8 @@ zh_TW: none_installed: "尚未安裝任何外掛" version: "版本" enabled: "啟用?" + is_enabled: "是" + not_enabled: "否" change_settings: "更改設定" change_settings_short: "設定" howto: "如何安裝外掛?" @@ -1525,6 +1597,8 @@ zh_TW: screened_email: "以 CSV 格式匯出所有已顯示的電子郵件列表" screened_ip: "以 CSV 格式匯出所有已顯示的 IP 列表" screened_url: "以 CSV 格式匯出所有已顯示的 URL 列表" + export_json: + button_text: "匯出" invite: button_text: "送出邀請" button_title: "送出邀請" @@ -1535,6 +1609,7 @@ zh_TW: header: "標頭" top: "精選" footer: "頁尾" + embedded_css: "內嵌 CSS" head_tag: text: "</head>" title: "HTML 將會置於 </head> 之前" @@ -1553,6 +1628,7 @@ zh_TW: new: "新增" new_style: "新增樣式" import: "匯入" + import_title: "選取檔案或貼上文本" delete: "刪除" delete_confirm: "刪除此樣式?" about: "修改網站的 CSS 和 HTML headers。請新增一個自定樣式來開始使用。" @@ -1902,6 +1978,7 @@ zh_TW: delete: "刪除" cancel: "取消" delete_confirm: "你確定要刪除此用戶欄位 ?" + options: "選項" required: title: "在註冊時必填?" enabled: "必填" @@ -1917,6 +1994,7 @@ zh_TW: field_types: text: '文字區域' confirm: '確認' + dropdown: "下拉" site_text: none: "選擇一個內容類別開始編輯" title: '文字內容' @@ -1928,6 +2006,7 @@ zh_TW: no_results: "未找到任何結果。" clear_filter: "清除" add_url: "加入網址" + add_host: "新增主機" categories: all_results: '全部' required: '必要設定' @@ -1949,6 +2028,7 @@ zh_TW: backups: "備份" login: "登入" plugins: "延伸套件" + user_preferences: "偏好設定" badges: title: 徽章 new_badge: 新徽章 @@ -2022,6 +2102,29 @@ zh_TW: name: "名稱" image: "圖片" delete_confirm: "你確定要刪除 :%{name}: emoji ?" + embedding: + confirm_delete: "你確定要刪除此主機?" + title: "嵌入" + host: "允許的主機" + edit: "編輯" + add_host: "新增主機" + settings: "嵌入設定" + crawling_settings: "爬蟲設定" + permalink: + title: "固定連結" + url: "網址" + topic_id: "討論話題 ID" + topic_title: "討論話題" + post_id: "貼文 ID" + post_title: "貼文" + category_id: "分類 ID" + category_title: "分類" + external_url: "外部網址" + delete_confirm: 你確定要刪除此固定連結? + form: + label: "新增:" + add: "新增" + filter: "搜尋 (網址或外部網址)" lightbox: download: "下載" search_help: @@ -2030,12 +2133,14 @@ zh_TW: title: '快捷鍵' jump_to: title: '跳到' + home: '<b>g</b>, <b>h</b> 首頁' latest: '<b>g</b>, <b>l</b> 最新' new: '<b>g</b> , <b>n</b> 新' unread: '<b>g</b> , <b>u</b> 未讀' categories: '<b>g</b>, <b>c</b> 分類' top: '<b>g</b>, <b>t</b> 頂端' bookmarks: '<b>g</b>, <b>b</b> 書籤' + messages: '<b>g</b>, <b>m</b> 私人訊息' navigation: title: '導航' jump: '<b>#</b> 前往文章 #' @@ -2047,12 +2152,14 @@ zh_TW: title: 'Application' create: '<b>c</b> 表示新討論話題' notifications: '<b>n</b> 開啟通知' + hamburger_menu: '<b>=</b> 打開漢堡選單' user_profile_menu: '<b>p</b> 打開使用者選單' show_incoming_updated_topics: '<b>.</b> 顯示有更新的討論話題' search: '<b>/</b> 搜尋' help: '<b>?</b> 打開按鍵說明' dismiss_new_posts: '<b>x</b>, <b>r</b> 解除新文章或回覆的提示' dismiss_topics: '<b>x</b>, <b>t</b> 解除主題的提示' + log_out: '<b>shift</b>+<b>z</b> <b>shift</b>+<b>z</b> 登出' actions: title: '行動' bookmark_topic: '<b>f</b> 加入/移除書籤' @@ -2175,3 +2282,5 @@ zh_TW: reader: name: 讀者 description: 觀看每個超過100篇文章的討論話題 + popular_link: + name: 熱門連結 diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 78a28608e..4889db918 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -20,7 +20,7 @@ ar: is_reserved: "محجوز" purge_reason: "حذف آلي للحسابات المهجوره وغير النشطه " disable_remote_images_download_reason: "لقد تم تعطيل تحميل الصور عن بعد بسبب عدم وجود مساحة كافية" - anonymous: "مجهولون" + anonymous: "مجهول" errors: format: '%{attribute} %{message}' messages: @@ -30,31 +30,31 @@ ar: accepted: يجب أن تُقبل blank: لا يمكن جعله فارغا present: يجب أن يكون فارغ - confirmation: "ليست متوافقه%{attribute}" + confirmation: "ليست مطابقة ل %{attribute}" empty: لا يمكن جعله فارغا equal_to: يجب أن تكون مساوي لـ %{count} - even: يجب أن يكون منتظم + even: يجب أن يكون زوجي exclusion: محجوز greater_than: يجب أن تكون أكبر من %{count} greater_than_or_equal_to: يجب أن تكون أكبر من أو تساوي %{count} inclusion: غير متضمن في القائمة - invalid: فاشل + invalid: غير صالح less_than: يجب أن يكون أقل من %{count} less_than_or_equal_to: يجب أن تكون أقل من أو تساوي %{count} - not_a_number: ليس رقم + not_a_number: ليس عدد not_an_integer: يجب أن يكون عدد صحيح odd: يجب أن يكون مفرد - record_invalid: 'التحقق من الفشل: %{errors}' + record_invalid: 'التأكد من الصلاحية غير ممكن : %{errors}' restrict_dependent_destroy: - one: "لا يمكن حذف السجل " + one: "لا يمكن حذف السجل لإعتماد سجل آخر عليه %{record} " many: "لا يمكن حذف السجل لاعتماده %{record} موجود" too_long: zero: طويل جدا (الحد الأقصى بدون الحروف) one: طويل جدا (الحد الأقصى حرف واحد) two: طويل جدا (الحد الأقصى حرفين) few: طويل جدا (الحد الأقصى حروف قليلة) - many: طويل جدا (الحد الأقصى حروف كثيرة) - other: طويل جدا (الحد الأقصى %{count} حروف) + many: طويل جدا (الحد الأقصى %{count} حرفا) + other: طويل جدا (الحد الأقصى %{count} حرفا) too_short: zero: قصير جدا (الحد الأدنى بدون حروف) one: قصير جدا (الحد الأدنى حرف واحد) @@ -287,8 +287,31 @@ ar: assets_topic_body: "هذا الموضوع مشاهد بشكل دائم للأعضاء فقط . ولحفظ الصورة والملف استخدم موقع التصميم. لا تقوم بحذفها\n\n\nكيف يكون العمل هنا.\n\n1. الرد على الموضوع.\n\n2. رفع كل الصور التي تود أن تستخدمها في الشعار." lounge_welcome: title: "مرحبا بك في الاستراحة." + body: |2 + + تهانينا! :confetti_ball: + + إذا تمكنت من رؤية هذا الموضوع, فأنت رقيت مؤخرا لـ**منتظم** (مستوى الثقة 3). + + You can now … + + * تعديل العنوان لأي موضوع + * تغيير الفئة لأي موضوع + * جميع روابطك المتبوعة بـ ([automatic nofollow](http://en.wikipedia.org/wiki/Nofollow) محذوفة) + * الوصول لفئة استراحة خاصة مرئية فقط لأعضاء مستوى الثقة 3 أو أعلى + * إخفاء الرسائل الغير هامة بتبليغ واحد + + من هنا [القائمة الحالية لمتابع منتظم](/badges/3/regular). طبعا قل مرحبا. + + شكرا لكونك جزءً هاما من هذا المجتمع! + + (لمعلومات أكثر حول مستويات الثقة, [see this topic][trust]. نرجو ملاحظة أن العضو الوحيد الذي يستمر في تلبية إحتياجات أكثر من الوقت سيبقون منتظمين.) + + [trust]: https://meta.discourse.org/t/what-do-user-trust-levels-do/4924 category: topic_prefix: "عن الفئة %{category}." + replace_paragraph: "[استبدل هذا الفقرة الأولى بوصف قصير لفئتك الجديدة. سيظهر هذا التوجيه في منطقة اختيار الفئة, لذا حاول أن تبقيها أقل من 200 حرف. حتى عند تحريرك لهذا النص أو إنشائك لموضوع, هذه الفئة لن تظهر على صفحة الفئات.]" + post_template: "%{replace_paragraph}\n\nاستخدم الفقرات التالي لوصف طويل, إضافة لإنشاء أي فئة قواعد أو توجيهات.\n\nبعض الأشياء تؤخذ بعين الإعتبار في أسفل أي ردود لنقاش:\n\n- لما هذا الفئة؟لماذا يختار الناس هذه الفئة لمواضيعهم؟\n\n- كيف تختلف الفئات الأخرى عن التي لدينا؟\n\n- هل نحتاج هذه الفئة؟\n\n- هل يجب أن ندمج هذه مع فئة أخرى, أو تقسم إلى أكثر من قئة ؟\n" errors: uncategorized_parent: "غير مصنف لا يمكن أن يكون فئة الأم" self_parent: "الفئة الفرعية لا يمكن ان تكون كالفئة الرئيسية." @@ -515,7 +538,7 @@ ar: please_continue: "الاستمرار لـ %{site_name}" error: "حدث خطأ عند تغير بريدك الإلكتروني. ربما يكون هذا البريد الإلكتروني قد أستوخدم من قبل ؟" activation: - action: "تم تفعيل حسابك" + action: "أضغط هنا لتفعيل حسابك" already_done: "رابط تأكيد الحساب لم يعد صالحاً , ربما الحساب نشط ؟" please_continue: "تأكيد حسابك الجديد : جاري توجيهك إلى الصفحة الرئيسية ." continue_button: "الاستمرار لـ %{site_name}" @@ -525,17 +548,21 @@ ar: post_action_types: off_topic: title: 'خارج الموضوع' + description: 'لا صلة للمشاركة بالنقاش الحالي على النحو المحدد بواسطة العنوان والمشاركة الأولى، وربما ينبغي نقلها لمكان أخر.' long_form: 'أبلغ أن هذا خارج الموضوع.' spam: title: 'بريد مزعج' + description: 'هذه المشاركة هي إعلان. ليست مفيدة أو مناسبة للموضوع الحالي، ولكنه نوع ترويجي.' long_form: 'تبليغ على أنه مزعج' email_title: '"%{title}" تم الأبلاغ على أنه بريد مزعج' email_body: "%{link}\n\n%{message}" inappropriate: title: 'غير مناسب' + description: 'هذه المشاركة تشمل محتوى أي شخص عاقل سيعتبره عدواني، أو مسيئ، أو انتهاك لـ<a href="/guidelines">مبادئ مجتمعنا التوجيهية</a>.' long_form: 'ترفع علم هذا عن صورة غير ملائمة' notify_user: title: 'رسالة @{{username}}' + description: 'هذه المشاركة تحتوي بعض الأشياء التي أريد أن أتكلم مع هذا الشخص حولها مباشرة وسرا. لا يمكنك عمل تبليغ.' long_form: 'العضو أرسل' email_title: 'مشاركتك في "%{title}"' email_body: "%{link}\n\n%{message}" @@ -564,13 +591,16 @@ ar: long_form: 'ترفع علم هذا كدعاية' inappropriate: title: 'غير مناسب' + description: 'هذا الموضوع يشمل محتوى أي شخص عاقل سيعتبره عدواني، أو مسيئ، أو انتهاك لـ<a href="/guidelines">مبادئ مجتمعنا التوجيهية</a>.' long_form: 'ترفع علم هذا عن صورة غير ملائمة' notify_moderators: title: "شيء آخر " + description: 'هذا الموضوع يتطلب اهتمام مشرف عام يعتمد على <a href="/guidelines">دليل التوجيهات</a>, <a href="%{tos_url}">TOS</a>, أو لسبب أخر غير مذكور أعلاه.' long_form: 'علم هذا لتنبيه المراقب' email_title: 'الموضوع "%{title}" يتطلب موافقة المشرف' email_body: "%{link}\n\n%{message}" flagging: + you_must_edit: '<p>مشاركتك تم التبليغ عليها بواسطة المجتمع. نرجوا <a href="/my/messages">رؤية رسالتك</a>.</p>' user_must_edit: '<p>هذه المشاركة كانت معلّمة بواسطة المجتمع وهي مخفية مؤقتاً.</p>' archetypes: regular: @@ -736,6 +766,10 @@ ar: sidekiq_warning: "\"Sidekiq\" لا يعمل! \nالعديد من المهام, كإرسال البريدوغيرها, يتم تنفيذها بشكل غير متزامن من قبل \"sidekiq\". الرجاء التحقيق من عمل احدى وضائف الـ\"Sidekiq\". <a href=\"https://github.com/mperham/sidekiq\" target=\"_blank\">Learn about Sidekiq here</a>." queue_size_warning: 'عدد المهام قيد الانتظار هو %{queue_size}, وهو مرتفع. وقد يسبب مشكلة مع احدى/كل مهام "Sidekiq", أو قد تحتاج الى إضافة المزيد من (Sidekiq workers).' memory_warning: 'خادمك يعمل بأقل من 1 جيجا بايت من الذاكرة الإجمالية. يتطلب على الأقل 1 جيجا بايت من الذاكرة.' + google_oauth2_config_warning: 'تم تكوين الخادم للسماح بالتسجيل وتسجيل الدخول مع Google OAuth2 (enable_google_oauth2_logins)، لكن معرف العميل وعميل القيم السرية لم يعين. أذهب إلى <a href="/admin/site_settings">إعدادات الموقع</a>وتحديث الإعدادات. <a href="https://meta.discourse.org/t/configuring-google-login-for-discourse/15858" target="_blank">أنظر لهذا الدليل لمزيد من المعلومات</a>.' + facebook_config_warning: 'تم تكوين الخادم للسماح بالتسجيل وتسجيل الدخول مع Facebook (enable_facebook_logins), لكن معرف العميل وعميل القيم السرية لم يعين. أذهب إلى <a href="/admin/site_settings">إعدادات الموقع</a>وتحديث الإعدادات. <a href="https://meta.discourse.org/t/configuring-facebook-login-for-discourse/13394" target="_blank">أنظر لهذا الدليل لمزيد من المعلومات</a>.' + twitter_config_warning: 'تم تكوين الخادم للسماح بالتسجيل وتسجيل الدخول مع Twitter (enable_twitter_logins), لكن معرف العميل وعميل القيم السرية لم يعين. أذهب إلى <a href="/admin/site_settings">إعدادات الموقع</a> وتحديث الإعدادات. <a href="https://meta.discourse.org/t/configuring-twitter-login-for-discourse/13395" target="_blank">أنظر لهذا الدليل لمزيد من المعلومات</a>.' + github_config_warning: 'تم تكوين الخادم للسماح بالتسجيل وتسجيل الدخول مع GitHub (enable_github_logins), لكن معرف العميل وعميل القيم السرية لم يعين. أذهب إلى <a href="/admin/site_settings">إعدادات الموقع</a> وتحديث الإعدادات. <a href="https://meta.discourse.org/t/configuring-github-login-for-discourse/13745" target="_blank">أنظر لهذا الدليل لمزيد من المعلومات</a>.' contact_email_invalid: "البريد الالكتروني للموقع معطل. حدّثه <a href='/admin/site_settings'>Site Settings</a>." title_nag: "أدخل اسم موقعك. حدث العنوان في الاعدادات <a href='/admin/site_settings'>Site Settings</a>." site_description_missing: "أدخل جملة وصفية واحدة لموقعك والتي ستظهر في نتائج البحث. حدّث وصف_الموقع في اعدادات الموقع <a href='/admin/site_settings'>Site Settings</a>." @@ -755,15 +789,20 @@ ar: title: "مرحباً: عضو مدعو" login_required_welcome_message: title: "مطلوب لتسجيل الدخول : رسالة ترحيب" + description: "رسالة الترحيب التي تظهر للأعضاء المسجلين خروجهم عندما يفعلون إعداد \"تسجيل الدخول مطلوب\"" login_required: title: "مطلوب لتسجيل الدخول : الصفحة الرئيسية" + description: "النص الظاهر للأعضاء الغير مصرح بهم عند الحاجة لتسجيل الدخول للموقع." head: title: "عنوان HTML رئيسي " + description: "HTML الذي سيدرج داخل علامات <head></head>." top: title: "أعلى الصفحة" bottom: title: "أسفل الصفحة" + description: "HTML التي ستضاف قبل علامة </body> ." site_settings: + censored_words: "الكلمات التي ستستبدل تلقائيا مع ■■■■" delete_old_hidden_posts: "سيتم حذف الوظائف المخفية تلقائيًا إذا زادت مدة الإخفاء أكثر من 30 يومًا" default_locale: "اللغة الإفتراضية لهذا الديسكورس نموذج (ISO 639-1 Code)" allow_user_locale: "السماح للمستخدمين باختيار واجهة تفضيل لغة خاصة بهم." @@ -776,8 +815,10 @@ ar: min_private_message_title_length: "الحد الأدنى المسموح به لطول عنوان لرسالة في الأحرف" min_search_term_length: "الحد الأدنى الصالح لطول مصطلح في الأحرف" uncategorized_description: "الوصف للفئة غير المصنفة. اتركه فارغا لعدم الوصف." + allow_duplicate_topic_titles: "اسمح بالمواضيع المماثلة والعناوين المكررة." unique_posts_mins: "كمية الدقائق التي يمكن للعضو قبلها إنشاء مشاركة مع نفس المحتوى مجددا" title: "الاسم لهذا الموقع، كأنه يستخدم علامة العنوان." + site_description: "صف هذا الموقع بجملة واحدة باستخدام علامة الوصف meta." max_image_width: "أقصى عرض للصور المصغرة في مشاركة" max_image_height: "أقصى ارتفاع للصور المصغرة في مشاركة" category_featured_topics: "عدد المواضيع المعروضة لكل فئة من صفحة الفئات /categories .بعد تغير هذه القيمة, قد تستغرق صفحة الفئات 15 دقيقة لتُحَدّث." @@ -821,6 +862,9 @@ ar: new_version_emails: "إرسال بريد إلكتروني إلى عنوان contact_email عندما نسخة جديدة من ديسكورس هو متاح." port: "DEVELOPER فقط! تحذير! استخدام هذا المنفذ HTTP بدلا من الافتراضي من المنفذ 80. المغادرة ابحث عن الافتراضي من 80." force_hostname: "DEVELOPER فقط! تحذير! تحديد اسم المضيف في URL. اتركه فارغا لالافتراضي." + min_username_length: "الحد الأدنى لطول اسم العضو في الأحرف." + max_username_length: "الحد الأعلى لطول اسم العضو في الأحرف." + reserved_usernames: "الأعضاء الغير مسموح لها بالتسجيل." min_password_length: "أقل طول لكلمة المرور" block_common_passwords: "لا تسمح لكلمات المرور المسجلة في قائمة كلمات المرور الشائعةز" sso_url: "نقطة نهاية URL الدخول الموحد" @@ -862,6 +906,7 @@ ar: suggested_topics: "عدد المواضيع المواضيع المقترحة يظهر في أسفل الموضوع." limit_suggested_to_category: "أظهر فقط المواضيع من الفئات الحالية في المواضيع المقترحة." clean_up_uploads: " يتيم أزالة تحديث غير مرجعية لمنع استضافة غير المشروعة. تحذير: قد تحتاج لعمل نسخة احتياطية من الدليل / تحميل قبل تمكين هذا الإعداد." + clean_orphan_uploads_grace_period_hours: "فترة الراحة (بالساعات) قبل الرفع الوحيد تمت إزالته." purge_deleted_uploads_grace_period_days: "يتم مسح فترة سماح (بالأيام) قبل تحميل حذفه." purge_unactivated_users_grace_period_days: "يتم حذف فترة سماح (بالأيام) قبل المستخدم الذي لم تنشيط حساباتهم." enable_s3_uploads: "وضع الإضافات على تخزين الأمازون S3. هام: يتطلب اعتماد S3 صالحة (على حد سواء الوصول معرف مفتاح ومفتاح الوصول السري)." @@ -879,12 +924,19 @@ ar: tl2_requires_read_posts: "كمية المشاركات التي يجب على العضو قرائتها قبل ترقيته لمستوى الثقة 2." tl2_requires_time_spent_mins: "كمية الدقائق التي يجب على العضو قراءة المشاركات فيها قبل ترقيته لمستوى الثقة 2." tl2_requires_days_visited: "كمية الأيام التي يجب على العضو زيارة الموقع فيها قبل ترقيته لمستوى الثقة 2." + tl2_requires_likes_received: "كمية الإعجابات التي يجب على العضو إرسالها قبل ترقيته لمستوى الثقة 2." + tl2_requires_likes_given: "كمية الإعجابات التي يجب على العضو جمعها قبل ترقيته لمستوى الثقة 2." + tl2_requires_topic_reply_count: "كمية مواضيع العضو التي يجب الرد عليها قبل الترقية لمستوى الثقة 2." + min_trust_to_create_topic: "أدنى مستوى ثقة مطلوب لإنشاء موضوع جديد." newuser_max_links: "عدد الروابط التي يمكن للمستخدم الجديد إضافتها للمشاركة." newuser_max_images: "عدد الصور التي يمكن للمستخدم الجديد إضافتها للمشاركة." newuser_max_attachments: "عدد المرفقات التي يمكن للمستخدم الجديد إضافتها للمشاركة." title_max_word_length: "الحد الأقصى المسموح لطول كلمة، بالأحرف، في عنوان الموضوع." category_style: "النمط المرئي لفئة الشارات." auto_respond_to_flag_actions: "تمكين الرد التلقائي عند التخلص من التبليغ." + auto_block_fast_typers_max_trust_level: "الحد الأعلى لمستوى الثقة لأنواع الحظر التلقائي السريع." + reply_by_email_enabled: "تفعيل الرد على الموضوع بواسطة البريد الالكتروني" + short_email_length: "طول أقصر بريد الكتروني بـ Bytes." full_name_required: "الإسم الكامل مطلوب وهو ضروري لإكمال الحساب " enable_names: "عرض الاسم الكامل للعضو , بطاقة العضو , ورسائل البريد الالكتروني , تعطيل عرض الاسم في اي مكان " display_name_on_posts: "عرض الاسم الكامل للعضو على التعليقات بالاضافة الى @username." @@ -894,6 +946,14 @@ ar: delete_drafts_older_than_n_days: حذف المسودات مضى عليها أكثر من (ن) يوما. enable_emoji: "تمكين الرموز التعبيرية " emoji_set: "كيف تريد الرموز التعبيرية الخاصة بك؟" + default_other_external_links_in_new_tab: "أفتح الروابط الخارجية في تبويب جديد إفتراضيا." + default_other_enable_quoting: "فعل إقتباس الردود لتحديد النص إفتراضيا." + default_other_dynamic_favicon: "إعرض عدد المواضيع الجديدة/الحديثة في أيقونة المتصفح إفتراضيا." + default_other_disable_jump_reply: "لا تتجاوز إلى مشاركة الضو بعد ردهم إفتراضيا." + default_other_edit_history_public: "أنشئ مشاركة المراجعات العامة إفتراضيا." + default_categories_watching: "قائمة الفئات التي تشاهد إفتراضيا." + default_categories_tracking: "قائمة الفئات التي تتابع إفتراضيا." + default_categories_muted: "قائمة الفئات التي توضع صامتة إفتراضيا." errors: invalid_email: "بريد الكتروني غير صالح " invalid_username: "لا يوجد مستخدم بهذا الاسم " @@ -937,7 +997,11 @@ ar: most_posts: "معظم المشاركات" most_recent_poster: "معظم المشاركات الاخيرة " frequent_poster: "تكرار المشاركات " + redirected_to_top_reasons: + new_user: "مرحبا بكم في مجتمعنا! هذه هي المواضيع الأكثر شعبية الأخيرة." + not_seen_in_a_month: "مرحبا بعودتك! لم نرك منذ مدة. هذه أكثر المواضيع شعبية عندما كنت بعيدا." change_owner: + post_revision_text: "نقل الملكية من %{old_user} إلى %{new_user}" deleted_user: "عضو محذوف" emoji: errors: @@ -952,8 +1016,10 @@ ar: autoclosed_disabled_lastpost: "تم فتح هذا الموضوع الان ويسمح باضافة ردود جديدة " pinned_enabled: "هذا الموضوع الآن مقيد. سوف تظهر في الجزء العلوي من فئة لها حتى أنها متغيرة من الموظفين لكل فرد، أو بواسطة المستخدمين انفسهم." pinned_disabled: "هذا الموضوع يتم الآن إزالة. لن يظهر في الجزء العلوي من هذه الفئة." + visible_enabled: "هذا الموضوع مدرج الأن. سيظهر في قائمة المواضيع." login: incorrect_username_email_or_password: "اسم المستخدم او كلمة المرور او البريد الالكتروني غير صحيح" + active: "حسابك مفعل وجاهز للاستخدام." not_allowed_from_ip_address: "ﻻ يمكنك تسجيل الدخول كـ %{username} من هذا الـIP" admin_not_allowed_from_ip_address: "لا يمكنك تسجيل الدخول كمدير من خلال هذا العنوان الرقمي - IP." suspended: "ﻻ يمكنك تسجيل الدخول حتى %{date}." @@ -976,7 +1042,9 @@ ar: characters: "يجب ان تحتوي الارقام والاحرف الصغيرة فقط " unique: "يجب أن يكون فريدا" blank: "يجب أن يكون موجود" - must_begin_with_alphanumeric: "يجب ان يبدأ بحرف أو رقم " + must_end_with_alphanumeric: "يجب أن ينتهي بحرف أو رقم." + must_not_contain_two_special_chars_in_seq: "يجب أن لا يحتوي على تسلسل من 2 أو رموز خاصة (.-_)" + must_not_contain_confusing_suffix: "يجب أن لا تحتوي على ملحقة مربكة مثل : json. أو png. الخ." email: not_allowed: "بريد الكتروني غير مسموح . يرجى استخدام بريد الكتروني آخر " blocked: "غير مسموح" @@ -1004,6 +1072,7 @@ ar: disagreed: "شكراً لإعلامنا سننظر في ذلك " deferred: "شكراً لإعلامنا سننظر في ذلك " deferred_and_deleted: "شكرا لاعلامنا تم ازالة هذا التعليق " + temporarily_closed_due_to_flags: "هذا الموضوع مغلق مؤقتا بسبب عدد التبليغات الكبير للمجتمع." system_messages: post_hidden: subject_template: "تم إخفاء المنشور بسبب حظر المجتمع" @@ -1038,8 +1107,8 @@ ar: text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nليس هنالك حساب عضو يمتلك هذا البريد الالكتروني. حاول أن ترسل من بريد الكتروني مختلف، أو أتصل بـ أحد المشرفين.\n\n" email_reject_empty: subject_template: "[%{site_name}] بريد الكتروني -- بدون محتوى" - text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nلم نتمكن من العثور على أي محتوى في البريد الإلكتروني الخاص بك. تأكد من الرد الخاص بك هو في الجزء العلوي من البريد الإلكتروني -- نحن لا يمكننا معالجة الردود في سطر .\n\nإذا كنت الحصول على هذا وانت_ قمت _\ - \ بتضمين المحتوى، حاول مرة أخرى مع محتوى HTML المضمنه في بريدك الالكتروني ( ليس مجرد نص عادي).\n" + text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nلم نتمكن من العثور على أي محتوى في البريد الإلكتروني الخاص بك. تأكد من الرد الخاص بك هو في الجزء العلوي من البريد الإلكتروني -- نحن لا يمكننا معالجة الردود في سطر .\n\nإذا كنت الحصول على هذا وانت_ قمت _ بتضمين المحتوى، حاول مرة أخرى مع محتوى HTML المضمنه في بريدك الالكتروني\ + \ ( ليس مجرد نص عادي).\n" email_reject_parsing: subject_template: "[%{site_name}] بريد الكتروني -- محتواه غير معروف" text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nلم نتمكن من العثور على أي محتوى في البريد الإلكتروني الخاص بك. **تأكد من الرد الخاص بك هو في الجزء العلوي من البريد الإلكتروني -- نحن لا يمكننا معالجة الردود في سطر .\n" diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index 8fcf2759e..b093fa26c 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -247,7 +247,6 @@ bs_BA: please_continue: "Continue to %{site_name}" error: "There was an error changing your email address. Perhaps the address is already in use?" activation: - action: "Aktiviraj svoj nalog" already_done: "Link za konfirmaciju nije validan. Možda ste već aktivirani?" please_continue: "Vaš novi nalog je verifikovan, i možete se ulogovati." continue_button: "Idi na Revolucionar.com" @@ -800,7 +799,6 @@ bs_BA: characters: "must only include numbers, letters and underscores" unique: "must be unique" blank: "must be present" - must_begin_with_alphanumeric: "must begin with a letter or number" email: not_allowed: "is not allowed from that email provider. Please use another email address." blocked: "is not allowed." diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml index efdb2e458..8366c878d 100644 --- a/config/locales/server.cs.yml +++ b/config/locales/server.cs.yml @@ -372,7 +372,6 @@ cs: please_continue: "Pokračovat na %{site_name}" error: "Nastala chyba běhěm změny emailové adresy. Není nová adresa již někým používána?" activation: - action: "Aktivovat účet" already_done: "Bohužel, tento odkaz pro aktivaci účtu již není platný. Není váš účet jíž aktivní?" please_continue: "Váš účet je aktivovaný; budete přesměrování na výchozí stránku." continue_button: "Pokračovat na %{site_name}" @@ -717,7 +716,6 @@ cs: characters: "musí obsahovat pouze písmena a číslice" unique: "musí být unikátní" blank: "nesmí být prázdný" - must_begin_with_alphanumeric: "musí začínat písmenem nebo číslicí" email: not_allowed: "není povolen od tohoto emailového poskytovatele. Prosím použijte jinou emailovou adresu." blocked: "není povolen." diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index dfd87f18a..ca41625e6 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -343,7 +343,6 @@ da: please_continue: "Fortsæt til %{site_name}" error: "Der opstod en fejl under opdateringen af din e-mail-adresse. Måske er adressen allerede i brug?" activation: - action: "Aktiver din konto" already_done: "Beklager, dette bekræftelses-link er ikke længere gyldigt. Måske er din konto allerede aktiv?" please_continue: "Din nye konto er bekræftet; du bliver nu ledt til forsiden." continue_button: "Fortsæt til %{site_name}" @@ -671,7 +670,6 @@ da: characters: "må kun indeholde bogstaver og tal" unique: "skal være unik" blank: "skal udfyldes" - must_begin_with_alphanumeric: "skal starte med et bogstav eller tal" email: not_allowed: "er ikke tilladt fra den e-mail-udbyder. Brug venligst en anden e-mail-adresse." blocked: "er ikke tilladt." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 674f11545..fd3f07f88 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -376,7 +376,6 @@ de: please_continue: "Weiter zu %{site_name}" error: "Es gab einen Fehler beim Ändern deiner Mailadresse. Wird diese Adresse bereits genutzt?" activation: - action: "Aktiviere dein Benutzerkonto" already_done: "Entschuldige, dieser Link zur Aktivierung des Benutzerkontos ist nicht mehr gültig. Ist dein Konto schon aktiviert?" please_continue: "Dein neues Konto ist jetzt bestätigt; du wirst auf die Startseite weitergeleitet." continue_button: "Weiter zu %{site_name}" @@ -1076,7 +1075,6 @@ de: characters: "darf nur aus Zahlen und Buchstaben bestehen" unique: "muss eindeutig sein" blank: "muss angegeben werden" - must_begin_with_alphanumeric: "muss mit einer Zahl oder einem Buchstaben anfangen" email: not_allowed: "ist für diesen Mailprovider nicht erlaubt. Bitte verwende eine andere Mailadresse." blocked: "ist nicht erlaubt." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 0b7c47b3a..4c03b835c 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -94,7 +94,7 @@ en: min_username_length_exists: "You cannot set the minimum username length above the shortest username." min_username_length_range: "You cannot set the minimum above the maximum." max_username_length_exists: "You cannot set the maximum username length below the longest username." - max_username_length_range: "You cannot set the maximum above the minimum." + max_username_length_range: "You cannot set the maximum below the minimum." default_categories_already_selected: "You cannot select a category used in another list." activemodel: @@ -846,7 +846,7 @@ en: 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." - allow_html_tables: "Allow tables to be entered in Markdown using HTML tags, TABLE, THEAD, TD, TR, TH are whiteliseted (requires full rebake on all old posts containing tables)" + allow_html_tables: "Allow tables to be entered in Markdown using HTML tags, TABLE, THEAD, TD, TR, TH are whitelisted (requires full rebake on all old posts containing tables)" post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)." 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!" ga_tracking_code: "Google analytics (ga.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" @@ -880,6 +880,7 @@ en: email_token_grace_period_hours: "Forgot password / activate account tokens are still valid for a grace period of (n) hours after being redeemed." enable_badges: "Enable the badge system" + enable_whispers: "Allow users to whisper to moderators" allow_index_in_robots_txt: "Specify in robots.txt that this site is allowed to be indexed by web search engines." email_domains_blacklist: "A pipe-delimited list of email domains that users are not allowed to register accounts with. Example: mailinator.com|trashmail.net" @@ -979,6 +980,9 @@ en: avatar_sizes: "List of automatically generated avatar sizes." + external_system_avatars_enabled: "Use external system avatars service." + external_system_avatars_url: "URL of the external system avatars service. Allowed substitions are {username} {first_letter} {color} {size}" + enable_flash_video_onebox: "Enable embedding of swf and flv (Adobe Flash) links in oneboxes. WARNING: may introduce security risks." default_invitee_trust_level: "Default trust level (0-4) for invited users." @@ -1067,7 +1071,7 @@ en: white_listed_spam_host_domains: "A list of domains excluded from spam host testing. New users will never be restricted from creating posts with links to these domains." staff_like_weight: "How much extra weighting factor to give staff likes." - topic_view_duration_hours: "Count new a topic view once per IP/User every N hours" + topic_view_duration_hours: "Count a new topic view once per IP/User every N hours" levenshtein_distance_spammer_emails: "When matching spammer emails, number of characters difference that will still allow a fuzzy match." max_new_accounts_per_registration_ip: "If there are already (n) trust level 0 accounts from this IP (and none is a staff member or at TL2 or higher), stop accepting new signups from that IP." @@ -1199,8 +1203,8 @@ en: default_email_mailing_list_mode: "Send an email for every new post by default." default_email_always: "Send an email notification even when the user is active by default." - default_other_new_topic_duration_minutes: "Global default number of minutes a topic is considered new, users can override (-1 for always, -2 for last visit)" - default_other_auto_track_topics_after_msecs: "Global default milliseconds before a topic is automatically tracked, users can override (0 for always, -1 for never)" + default_other_new_topic_duration_minutes: "Global default condition for which a topic is considered new." + default_other_auto_track_topics_after_msecs: "Global default time before a topic is automatically tracked." default_other_external_links_in_new_tab: "Open external links in a new tab by default." default_other_enable_quoting: "Enable quote reply for highlighted text by default." default_other_dynamic_favicon: "Show new/updated topic count on browser icon by default." @@ -1343,7 +1347,10 @@ en: characters: "must only include numbers, letters and underscores" unique: "must be unique" blank: "must be present" - must_begin_with_alphanumeric: "must begin with a letter or number" + must_begin_with_alphanumeric: "must begin with a letter or number or an underscore" + must_end_with_alphanumeric: "must end with a letter or number or an underscore" + must_not_contain_two_special_chars_in_seq: "must not contain a sequence of 2 or more special chars (.-_)" + must_not_contain_confusing_suffix: "must not contain a confusing suffix like .json or .png etc." email: not_allowed: "is not allowed from that email provider. Please use another email address." blocked: "is not allowed." @@ -2426,7 +2433,7 @@ en: ## [Children's Online Privacy Protection Act Compliance](#coppa) - Our site, products and services are all directed to people who are at least 13 years old or older. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA ([Children's Online Privacy Protection Act](http://en.wikipedia.org/wiki/Children)), do not use this site. + Our site, products and services are all directed to people who are at least 13 years old or older. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA ([Children's Online Privacy Protection Act](https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act)), do not use this site. <a name="online"></a> diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index c66bc0b0e..42eceed13 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -65,6 +65,12 @@ es: other: '%{count} errores impidieron guardar este %{model}' embed: load_from_remote: "Hubo un error al cargar ese post." + site_settings: + min_username_length_exists: "No se puede establecer una longitud mínima para nombre de usuario mayor que el nombre de usuario más corto ya existente." + min_username_length_range: "No se puede establecer el mínimo por encima del máximo." + max_username_length_exists: "No se puede establecer una longitud máxima de nombre de usuario menor que el nombre de usuario más largo ya existente." + max_username_length_range: "No se puede establecer el máximo por debajo del mínimo." + default_categories_already_selected: "No se puede seleccionar una categoría ya utilizada en otra lista." bulk_invite: file_should_be_csv: "El archivo a subir debe tener formato csv o txt." backup: @@ -392,7 +398,7 @@ es: please_continue: "Continuar a %{site_name}" error: "Hubo un problema cambiando tu dirección de email. ¿Quizás la dirección ya está en uso?" activation: - action: "Activar tu cuenta" + action: "Haz clic aquí para activar tu cuenta" already_done: "Lo sentimos, este link de confirmación de cuenta ya no es válido. ¿Quizás tu cuenta ya está activa?" please_continue: "Tu nueva cuenta está confirmada; se te redirigirá a la página de inicio." continue_button: "Continuar a %{site_name}" @@ -712,6 +718,7 @@ es: logo_url: "El logo situado en la esquina superior izquierda de tu sitio, debería encajar en las dimensiones de un rectángulo. Si se deja en blanco, se mostrará un texto con el título del sitio." digest_logo_url: "El logo alternativo que irá arriba en los emails de resumen de tu sitio. Debería encajar en las dimensiones de un rectángulo. Si se deja en blanco, se utilizará `logo_url`." logo_small_url: "El logo pequeño situado en la esquina superior izquierda del sitio, debería encajar en las dimensiones de un cuadrado y se muestra cuando se hace scroll hacia abajo. Si se deja en blanco, se mostrará un glifo de una casita." + favicon_url: "Un favicon para tu sitio, mira más info en http://es.wikipedia.org/wiki/Favicon - para que funcione correctamente sobre un CDN debe ser un png" mobile_logo_url: "El logo con posición fija utilizado en la esquina superior izquierda de la versión móvil de tu sitio. Debería encajar en las dimensiones de un cuadrado. Si se deja en blanco, se mostrará el campo introducido en `logo_url`. Ejemplo: http://example.com/uploads/default/logo.png" apple_touch_icon_url: "Icono para los dispositivos táctiles de Apple. Se recomienda un tamaño de 144px por 144px." notification_email: "La dirección de correo electrónico \"remitente\", utilizada al enviar todos los emails esenciales de sistema. El dominio especificado debe tener correctamente configurados los registros SPF, DKIM y PTR inversos para que los emails se reciban correctamente." @@ -742,7 +749,7 @@ es: 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." - allow_html_tables: "Permitir en Markdown la inserción de tablas usando etiquetas HTML como TABLE, THEAD, TD, TR o TH (requiere un rebake completo para todos los posts antiguos que contengan tablas)" + allow_html_tables: "Permitir la inserción de tablas en Markdown usando etiquetas HTML como TABLE, THEAD, TD, TR o TH (requiere un rebake completo para todos los posts antiguos que contengan tablas)" post_undo_action_window_mins: "Número de minutos durante los cuales los usuarios pueden deshacer sus acciones recientes en un post (me gusta, reportes, etc)." 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!" ga_tracking_code: "Código de Google Analytics, ej: UA-12345678-9; visita http://google.com/analytics" @@ -785,6 +792,8 @@ es: invite_passthrough_hours: "Durante cuánto tiempo, en horas, un usuario puede utilizar una clave de invitación redimida para iniciar sesión iniciar sesión" invite_only: "El registro público está deshabilitado, todos los nuevos usuarios deberán ser invitados explícitamente por otros miembros de la comunidad o por el staff." login_required: "Se requiere haber iniciado sesión para leer el contenido de este sitio, deshabilita el acceso anónimo." + min_username_length: "Longitud mínima del nombre de usuario en caracteres." + max_username_length: "Longitud máxima del nombre de usuario en caracteres." reserved_usernames: "Nombres de usuario no permitidos en el registro." min_password_length: "Longitud mínima de contraseña." block_common_passwords: "No permitir contraseñas que están entre las 10.000 más comunes." @@ -997,6 +1006,7 @@ es: enable_cdn_js_debugging: "Permitir /logs mostrar los errores correctamente, añadiendo permisos crossorigin en todas las inclusiones de js" show_create_topics_notice: "Si el sitio tiene menos de 5 temas abiertos al público, mostrar un aviso pidiendo a los administradores crear más temas." delete_drafts_older_than_n_days: Eliminar borradores de más de (n) días de antigüedad. + show_logout_in_header: "Mostrar el botón para cerrar sesión en el desplegable de la cabecera" vacuum_db_days: "Correr VACUUM FULL ANALYZE para reclamar espacio en la base de datos después de las migraciones. (Poner en 0 para inhabilitar)" prevent_anons_from_downloading_files: "Impedir que los usuarios anónimos descarguen archivos. ADVERTENCIA: Esto impedirá que funcione cualquier recurso del sitio publicado como adjunto." slug_generation_method: "Elegir un método de generación de slug. 'encoded' generará cadenas con código porciento. 'none' hara que no se genere slug." @@ -1006,6 +1016,7 @@ es: approve_post_count: "La cantidad de posts de un nuevo usuario que deben ser aprobados" approve_unless_trust_level: "Los posts de usuarios con un nivel de confianza inferior a este deberán ser aprobados" notify_about_queued_posts_after: "Si hay posts esperando a ser revisados por este número de horas, se enviará un email al correo de contacto. Establece este valor a 0 para desactivar estos emails." + default_email_digest_frequency: "Frecuencia por defecto con la que se reciben los emails resumen." errors: invalid_email: "Dirección de correo electrónico inválida. " invalid_username: "No existe ningún usuario con ese nombre de usuario. " @@ -1127,7 +1138,6 @@ es: characters: "solo debe incluir números y letras" unique: "debe ser único" blank: "debe estar presente" - must_begin_with_alphanumeric: "debe comenzar con una letra o número" email: not_allowed: "este proveedor de email no está permitido. Por favor, utiliza otra dirección de email." blocked: "no está permitido." diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index d679cd933..f617f6f9a 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -349,7 +349,6 @@ fa_IR: please_continue: "برو به %{site_name}" error: "در تغییر ایمیلتان خطایی روی داد. شاید آن نشانی از پیش در حال استفاده است؟" activation: - action: "حساب کاربریتان را فعال کنید" already_done: "متاسفیم٬ این پیوند تاییدیه حساب کاربری دیگر معتبر نیست. شاید حساب کاربری شما در حال حاضر فعال است." please_continue: "حساب کاربری جدید شما تایید شد; شما به صفحه اصلی هدایت می شوید. " continue_button: "برو به %{site_name}" @@ -1052,7 +1051,6 @@ fa_IR: characters: "باید شامل٬اعداد٬ حروف و زیر خط باشد" unique: "باید خاص باشد" blank: "باید حاضر باشد" - must_begin_with_alphanumeric: "حتما باید با حرف یا عدد شروع شود" email: not_allowed: "این قابل قبول نیست با این ایمیل ارائه شده. لطفا از یک ایمیل دیگر استفاده کنید. " blocked: "مجاز نیست." diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index c892ed606..40be49411 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -401,7 +401,6 @@ fi: please_continue: "Jatka sivustolle %{site_name}" error: "Sähköpostiosoitteen vaihdossa tapahtui virhe. Ehkäpä tämä sähköpostiosoite on jo käytössä?" activation: - action: "Vahvista käyttäjätilisi" already_done: "Pahoittelut, tämän tilin varmennuslinkki ei ole enää voimassa. Ehkäpä tili on jo varmennettu?" please_continue: "Tilisi on nyt varmennettu; sivu ohjautuu palstan etusivulle." continue_button: "Jatka sivustolle %{site_name}" @@ -751,7 +750,6 @@ fi: notify_mods_when_user_blocked: "Jos käyttäjä estetään automaattisesti, lähetä viesti kaikille valvojille." flag_sockpuppets: "Jos uusi käyttäjä vastaa toisen uuden käyttäjän luomaan ketjun 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." - allow_html_tables: "Salli taulukoiden syöttäminen Markdowniin käyttäen HTML tageja, TABLE, THEAD, TD, TR, TH on whitelistattu (edellyttää kaikkien vanhojen viestien, jotka sisältävät taulukoita, uudelleen rakentamisen)" post_undo_action_window_mins: "Kuinka monta minuuttia käyttäjällä on aikaa perua viestiin kohdistuva toimi (tykkäys, liputus, etc)." 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ä." ga_tracking_code: "Google analytics (ga.js) seurantakoodi, esim.: UA-12345678-9; katso http://google.com/analytics" @@ -1136,7 +1134,6 @@ fi: characters: "täytyy koostua vain numeroista, kirjaimista ja alaviivoista" unique: "täytyy olla uniikki" blank: "pakollinen kenttä" - must_begin_with_alphanumeric: "täytyy alkaa kirjaimella tai numerolla" email: not_allowed: "ei sallita tältä sähköpostin palvelunatarjoajalta. Ole hyvä, ja käytä toista sähköpostiosoitetta." blocked: "ei ole sallittu." diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index 5416f1f20..58e438ddb 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -402,7 +402,6 @@ fr: please_continue: "Continuer vers %{site_name}" error: "Il y a eu une erreur lors de la modification de votre adresse de courriel. Elle est peut-être déjà utilisée ?" activation: - action: "Activer votre compte" already_done: "Désolé, ce lien de confirmation n'est plus valide. Votre compte est peut-être déjà activé ?" please_continue: "Votre nouveau compte est confirmé; vous allez être redirigé vers la page d'accueil." continue_button: "Continuer vers %{site_name}" @@ -753,7 +752,6 @@ fr: 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 I¨P 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." - allow_html_tables: "Autoriser la saisie des tableaux dans le Markdown en utilisant les tags HTML : TABLE, THEAD, TD, TR, TH sont autorisés (nécessite un rebake de tous les anciens messages contenant des tableaux)" 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 !" ga_tracking_code: "Google Analytics (ga.js) code de suivi, par exemple: UA-12345678-9; voir http://google.com/analytics" @@ -1157,7 +1155,6 @@ fr: characters: "doit inclure uniquement des chiffres, lettres et caractères de soulignement" unique: "doit être unique" blank: "doit être présent" - must_begin_with_alphanumeric: "doit commencer par une lettre ou un nombre" email: not_allowed: "n'est pas autorisé pour ce fournisseur de courriels. Merci d'utiliser une autre adresse." blocked: "n'est pas autorisé." diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 4c34760d3..f63e60d74 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -387,7 +387,6 @@ he: please_continue: "המשך ל-%{site_name}" error: "הייתה שגאיה בעדכון כתובת הדואר האלקטרוני. אולי היא כבר בשימוש?" activation: - action: "הפעל/י את חשבונך" already_done: "סליחה, כתובת אישור החשבון הזו אינה זמינה יותר. אולי החשבון שלך כבר פעיל?" please_continue: "חשבונך החדש אושר; הנכם מועברים לעמוד הבית." continue_button: "המשך ל-%{site_name}" @@ -738,7 +737,6 @@ he: notify_mods_when_user_blocked: "If a user is automatically blocked, send a message to all moderators." flag_sockpuppets: "אם משתמש/ת חדשים מגיבים לנושא מכתובת IP זהה לזו של מי שהחל את הנושא, סמנו את הפרסומים של שניהם כספאם פוטנציאלי." traditional_markdown_linebreaks: "שימוש בשבירת שורות מסורתית בסימון, מה שדורש שני רווחים עוקבים למעבר שורה." - allow_html_tables: "אפשר להזין טבלאות ב-Markdown על ידי שימוש HTML tags, TABLE, THEAD, TD, TR, TH אפשריים (דורש הגדרה מחדש על כל הפרסומים הישנים שכוללים טבלאות). " post_undo_action_window_mins: "מספר הדקות בהן מתאפשר למשתמשים לבטל פעולות אחרות בפרסום (לייק, סימון, וכו')." must_approve_users: "על הצוות לאשר את כל המשתמשים החדשים לפני שהם מקבלים גישה לאתר. אזהרה: בחירה זו עבור אתר קיים תשלול גישה ממשתמשים קיימים שאינם מנהלים." ga_tracking_code: "Google analytics (ga.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" @@ -1142,7 +1140,6 @@ he: characters: "חייב לכלול מספרים, אותיות ומקפים תחתונים בלבד." unique: "חייב להיות ייחודי" blank: "חייב להיות מלא" - must_begin_with_alphanumeric: "חייב להתחיל עם אות או מספר" email: not_allowed: "לא מורשה מכתובת הדואר האלקטרוני הזו. בבקשה השתמש בכתובת אחרת." blocked: "לא מורשה." @@ -1370,12 +1367,24 @@ he: לא ידוע לנו על חשבון משתמש/ת עם כתובת כזו. נסו לשלוח מכתובת דוא"ל אחרת, או צרו קשר עם אנשי הצוות שלנו. email_reject_empty: subject_template: "[%{site_name}] בעיית מייל -- ללא תוכן" + text_body_template: "אנחנו מתנצלים, אבל הודעת המייל שלך ל- %{destination} (titled %{former_title}) לא עבדה. \n\nלא מצאנו תוכן במייל שלך. אנא ודא שהתגובה שלך בראש המייל, אנחנו לא יכולים לעבד תגובות בין השורות. \n\nאם כן כללת תוכן, נצא שוב עם תוכן HTML בתוך המייל, לא רק טקסט רגיל. \n" + email_reject_parsing: + subject_template: "[%{site_name}] בעיית מייל -- תוכן לא זוהה." + text_body_template: "אנחנו מתנצלים, אבל הודעת המייל שלך ל- %{destination} (titled %{former_title}) לא עבדה. \n\nלא מצאנו תוכן במייל שלך. אנא ודא שהתגובה שלך בראש המייל, אנחנו לא יכולים לעבד תגובות בין השורות. \n" + email_reject_invalid_access: + subject_template: "[%{site_name}] בעיית מייל -- גישה לא תקינה" + text_body_template: | + אנו מצטערים, אבל הודעת הדוא"ל שלך אל %{destination} (titled %{former_title}) לא עברה. + + אין לחשבון שלך את רמון האמון הנדרשת כדי לפרסם נושאים חדשים בקטגוריה הזו. אם אתם חושבים שזוהי שגיאה, צרו קשר עם אחד מאנשי הצוות. email_reject_post_error: + subject_template: "[%{site_name}] בעיית מייל -- שגיאת פרסום" text_body_template: | אנו מצטערים, אך מסר הדוא"ל שלך ל-%{destination} (שכותרתו %{former_title}) לא עבר. סיבות אפשריות לעניין: מבנה מורכב, המסר ארוך מידי, המסר קצר מידי. אנא נסו שוב, או פרסמו באמצעות האתר עם הבעיה נמשכת. email_reject_post_error_specified: + subject_template: "[%{site_name}] בעיית מייל -- שגיאת פרסום" text_body_template: | אנחנו מצטערים, אבל הודעת הדוא"ל שלך אל %{destination} (titled %{former_title}) לא עברה. @@ -1392,16 +1401,37 @@ he: If you can correct the problem, please try again. email_reject_reply_key: + subject_template: "[%{site_name}] בעיית מייל -- תו תגובה לא מוכר" text_body_template: | אנו מצטערים, אבל הודעת הדוא"ל שלך אל %{destination} (titled %{former_title}) לא עברה. מפתח התגובה לא קיים או לא מוכר, כך שאיננו יודעים לאיזה פרסום המייל הזה אמור להגיב. אנא צרו קשר עם אנשי הצוות. email_reject_destination: + subject_template: "[%{site_name}] בעיית מייל -- כתובת יעד לא מזוהה" text_body_template: | מצטערים, אבל הודעת הדוא"ל שלך ל-%{destination} עם הכותרת %{former_title} לא הצליחה. לא הצלחנו לזהות כתובות יעד. אנא ודא שכתובת האתר נמצאת בשדה "אל:", ולא ב-"עותק" או "עותק מוחבא (CC / BCC), ושאתה שולח לכתובת אימייל שסופקה בידי הצוות. + email_reject_topic_not_found: + subject_template: "[%{site_name}] בעיית מייל -- לא נמצא נושא" + text_body_template: | + אנחנו מצטערים, אך הודעת הדוא"ל ששלחת אל %{destination} (בנושא %{former_title}) לא עברה. + + הנושאחושבים שהגבתם אליו כבר לא קיים, אולי הוא נמחק? אם אתם שמדובר בתקלה, צרו קשר עם איש/אשת צוות. + email_reject_topic_closed: + subject_template: "[%{site_name}] בעיית מייל -- נושא נסגר" + text_body_template: | + אנחנו מצטערים, אך הודעת הדוא"ל שלך אל %{destination} (בנושא %{former_title}) לא נשלחה. + + הנושא שהשבתם אליו נסגר ולכן לא ניתן להשיב לו יותר. אם אתם מאמינים שזוהי תקלה, אנא צרו קשר עם איש/אשת צוות. + email_reject_auto_generated: + subject_template: "[%{site_name}] בעיית מייל -- תגובה נוצרה אוטומטית" + text_body_template: | + אנחנו מצטערים, אך הודעת הדוא"ל ששלחת אל %{destination} (בנושא %{former_title}) לא עברה. + + המייל שלך סומן כ"נוצר אוטומטית", ולכן לא ניתן לקבל אותו. אם אתם מאמינים שמדובר בתקלה, צרו קשר עם איש/אשת צוות. email_error_notification: + subject_template: "[%{site_name}] בעיית מייל -- בעיית אימות POP" text_body_template: | התרחשה תקלת אימות בזמן שבדקנו הודעות מייל בשרת ה-POP. @@ -1455,6 +1485,10 @@ he: download_remote_images_disabled: subject_template: "הורדת תמונות מרחוק מנוטרלת" text_body_template: "האפשרות \"הורדת תמונות מרוחקות\" נוטרלה בגלל שכל שטח האכסון שמוקצה ל\"תמונות שהורדו מרחוק\" נוצל." + unsubscribe_link: | + להסרה מרשימת התפוצה, בקר ב [הגדרות משתמש/ת](%{user_preferences_url}). + + בשביל להפסיק לקבל התראות בנוגע לשיחה הזאת [לחץ כאן](%{unsubscribe_url}). subject_re: "תגובה: " subject_pm: "[PM] " user_notifications: @@ -1465,6 +1499,12 @@ he: reply_by_email: "To respond, reply to this email or visit %{base_url}%{url} in your browser." visit_link_to_respond: "To respond, visit %{base_url}%{url} in your browser." posted_by: "Posted by %{username} on %{post_date}" + user_invited_to_private_message_pm: + subject_template: "[%{site_name}] %{username} הזמין אותך להודעה '%{topic_title}'" + text_body_template: "\n%{username} הזמינ/ה אותך להודעה. \n\n> **%{topic_title}**\n>\n> %{topic_excerpt}\n\nב\n\n> %{site_title} -- %{site_description}\n\nתלחץ על הלינק בשביל לראות את ההודעה %{base_url}%{url}\n" + user_invited_to_topic: + subject_template: "[%{site_name}] %{username} הזמין אותך לנושא '%{topic_title}'" + text_body_template: "\n%{username} הזמינ/ה אותך לשיחה. \n\n> **%{topic_title}**\n>\n> %{topic_excerpt}\n\nב\n\n> %{site_title} -- %{site_description}\n\nתלחץ על הלינק בשביל לראות את ההודעה %{base_url}%{url}\n" user_replied: subject_template: "[%{site_name}] %{topic_title}" text_body_template: | @@ -1558,7 +1598,9 @@ he: %{base_url}/users/authorize-email/%{email_token} signup_after_approval: subject_template: "You've been approved on %{site_name}!" + text_body_template: "ברוכים הבאים ל%{site_name}!\n\nחבר צוות אישר את החשבון שלך ב %{site_name}. \n\nלחץ על הקישור הבא לאשר והפעיל את החשבון החדש שלך:\n%{base_url}/users/activate-account/%{email_token}\n\nאם הלינק לא לחיץ, נסה להעתיק ולהדביק אותו לסרגל הכתובת בראש הדפדפן. \n\n%{new_user_tips}\n\nאנו מאמינים ב [civilized community behavior](%{base_url}/guidelines) בכל זמן.\n\nתהנה מהביקור!\n\n(אם אתה צריך ליצור קשר עם [staff members](%{base_url}/about) כחבר חדש, רק השב להודעה זאת. )\n\n" signup: + subject_template: "[%{site_name}] אשר את חשבונך החדש" text_body_template: | Welcome to %{site_name}! @@ -1586,7 +1628,11 @@ he: unauthorized: "Sorry, the file you are trying to upload is not authorized (authorized extensions: %{authorized_extensions})." pasted_image_filename: "Pasted image" store_failure: "Failed to store upload #%{upload_id} for user #%{user_id}." + file_missing: "סליחה, עליך לספק קובץ להעלות. " + attachments: + too_large: "מצטערים, הקובץ שאתם מנסים להעלות גדול מידי (הגודל המקסימלי המותר הוא %{max_size_kb}KB)." images: + too_large: "סליחה, אך התמונה שאתה מנסה להעלות גדולה מידי. (הגודל המקסימלי הוא %{max_size_kb}KB), אנא שנה את הגודל ונסה שנית." size_not_found: "Sorry, but we couldn't determine the size of the image. Maybe your image is corrupted?" flag_reason: sockpuppet: "A new user created a topic, and another new user at the same IP address replied. See the flag_sockpuppets site setting." @@ -1594,6 +1640,7 @@ he: email_log: no_user: "Can't find user with id %{user_id}" anonymous_user: "המשתמש הוא אנונימי" + suspended_not_pm: "המשתמש מושהה, לא הודעה" seen_recently: "User was seen recently" post_not_found: "Can't find a post with id %{post_id}" notification_already_read: "The notification this email is about has already been read" @@ -1707,6 +1754,35 @@ he: א0 נחליט לשנות את מדיניות הפרטיות שלנו, נפרסם שינויים אלו בעמוד זה. מסמך זה מפורסם תחת רשיון CC-BY-SA. הוא עודכן לאחרונה ב-31 למאי, 2013. + static: + search_help: | + <h3>Tips</h3> + <p> + <ul> + <li>Title matches are prioritized – when in doubt, search for titles</li> + <li>Unique, uncommon words will produce the best results</li> + <li>Try searching within a particular category, topic, or user</li> + </ul> + </p> + <h3>Options</h3> + <p> + <table> + <tr><td><code>order:views</code></td><td><code>order:latest</code></td><td colspan=3></td></tr> + <tr><td><code>status:open</code></td><td><code>status:closed</code></td><td><code>status:archived</code></td><td><code>status:noreplies</code></td><td><code>status:single_user</code></td></tr> + <tr><td><code>category:foo</code></td><td><code>user:foo</code></td><td colspan=3></td></tr> + <tr><td><code>in:likes</code></td><td><code>in:posted</code></td><td><code>in:watching</code></td><td><code>in:tracking</code></td><td><code>in:private</code></td></tr> + <tr><td><code>in:bookmarks</code></td><td><code>in:first</code></td><td colspan=3></td></tr> + <tr><td><code>posts_count:num</code></td><td><code>min_age:days</code></td><td><code>max_age:days</code></td> <td colspan=2></td></tr> + </table> + </p> + <p> + <code>rainbows category:parks status:open order:latest</code> will search for topics containing the word "rainbows" in the category "parks" that are not closed or archived, ordered by date of last post. + </p> + badges: + long_descriptions: + autobiographer: "אות יוענק לך כשתמלא את <a href=\"/my/preferences\">פרופיל המשתמש שלך</a> ותבחר תמונת פרופיל. לשתף את הקהילה לגבי מי אתה ובמה אתה מעוניין עוזר ליצור קהילה יותר טובה ומחוברת. \n" + first_like: | + אות מוענק בפעם הראשונה שאתה עושה "לייק" לפרסום בעזרת כפתור ה :heart: . לעשות לייק לפרסומים היא דרך מעולה לידע את חברך בקהילה שמה שהם פרסמו היה מעניין, שימושי או מגניב. חלוק את האהבה! admin_login: success: "דוא\"ל נשלח" error: "שגיאה!" diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 70a3c1f16..32c036493 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -357,7 +357,6 @@ it: please_continue: "Procedi su %{site_name}." error: "Si è verificato un errore durante la modifica del tuo indirizzo email. Forse l'indirizzo è già in uso?" activation: - action: "Attiva il tuo account" already_done: "Spiacenti, questo collegamento di attivazione non è più valido. Forse il tuo account è già attivo?" please_continue: "Il tuo nuovo account è confermato; verrai ora rediretto alla pagina iniziale." continue_button: "Procedi su %{site_name}." @@ -1005,7 +1004,6 @@ it: characters: "deve includere solo numeri, lettere e trattini bassi" unique: "deve essere univoco" blank: "deve essere presente" - must_begin_with_alphanumeric: "deve iniziare con una lettera o un numero" email: not_allowed: "non è permesso da quel fornitore di email. Per favore usa un altro indirizzo email." blocked: "non è permesso." diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index c792ed84c..af85f5ede 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -327,7 +327,6 @@ ja: please_continue: "%{site_name}へ" error: "メールアドレスの更新に失敗しました。アドレスが既に使用されているかもしれません。" activation: - action: "アカウントを有効にする" already_done: "申し訳ありませんが、このアカウント認証リンクは無効です。既にアカウントがアクティブになっていませんか?" please_continue: "あなたのアカウントは確認されました。ホームにリダイレクトされます" continue_button: "%{site_name} へ" @@ -677,7 +676,6 @@ ja: notify_mods_when_user_blocked: "ユーザが自動的にブロックされた際に、すべてのモデレータにメッセージを送信する。" flag_sockpuppets: "トピックを作成したユーザーと同じIPアドレスで、新規ユーザーがトピックに回答した場合、両者を潜在的なスパムとしてフラグを立てるか" traditional_markdown_linebreaks: "Markdown の従来形式のラインブレーク (行の終わりにダブルスペース) を使う" - allow_html_tables: "Markdown でHTMLタグでのテーブルの入力を許可する、TABLE、THEAD、TD、TR、THタグがホワイトリストに登録されています(テーブルを含むすべての古い投稿の完全な再ベークが必要)" post_undo_action_window_mins: "ポストに対するアクション (「いいね!」、フラグ等) 取り消しを許可する時間 (秒)" must_approve_users: "スタッフはサイトへのアクセスが許可される前のすべての新規ユーザを承認する必要があります。警告:公開中のサイトでこれを有効にすると、既存の非スタッフユーザのアクセスを取り消すことになります!" ga_tracking_code: "Google analytics のトラッキングコード。例: UA-12345678-9; 参考 http://google.com/analytics" @@ -1055,7 +1053,6 @@ ja: characters: "は英数字のみである必要があります" unique: "はユニークである必要があります" blank: "は空であってはなりません" - must_begin_with_alphanumeric: "は英数字で始まる必要があります" email: not_allowed: "はこのメールプロバイダーを許可していません。他のメールアドレスを使用してください。" blocked: "は許可されていません。" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index df459e28a..c467025fb 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -341,7 +341,6 @@ ko: please_continue: "%{site_name}으로 가기" error: "이메일 주소를 변경하는데 문제가 있습니다. 주소가 이미 사용되고 있나요?" activation: - action: "계정 활성화" already_done: "죄송합니다. 이 계정 확인 링크는 더 이상 유효하지 않습니다." please_continue: "계정이 활성화 되었습니다; 홈페이지로 이동합니다." continue_button: "%{site_name}으로 가기" @@ -1034,7 +1033,6 @@ ko: characters: "문자나 숫자만 사용해야 합니다." unique: "이미 사용중입니다." blank: "공백이 없어야 합니다." - must_begin_with_alphanumeric: "첫글자는 문자나 숫자로 시작해야 합니다." email: not_allowed: "이 이메일 제공업체는 허용되지 않습니다. 다른 이메일 제공 업체를 사용하세요." blocked: "는 허용되지 않습니다" diff --git a/config/locales/server.nb_NO.yml b/config/locales/server.nb_NO.yml index 8e231025e..86e266e1e 100644 --- a/config/locales/server.nb_NO.yml +++ b/config/locales/server.nb_NO.yml @@ -379,7 +379,6 @@ nb_NO: please_continue: "Fortsett til %{site_name}" error: "Det oppsto en feil ved endring av din epostadresse. Kanskje addressen allerede er i bruk?" activation: - action: "Aktiver din konto" already_done: "Beklager, denne bekreftelseslenken er ikke lenger gyldig. Kanskje er kontoen din allerede aktivert?" please_continue: "Din nye konto er registrert; du vil bli videresendt til hjemmesiden. " continue_button: "Fortsett til %{site_name}" @@ -743,7 +742,6 @@ nb_NO: long: "kan ikke være mer enn %{max} tegn" unique: "må være unik" blank: "Må være til stede" - must_begin_with_alphanumeric: "må begynne med en bokstav eller et tall" email: blocked: "er ikke tillatt." flags_reminder: diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 1ed065b4c..716af2236 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -400,7 +400,7 @@ nl: please_continue: "Ga verder naar %{site_name}" error: "Er ging iets mis bij het wijzigen van je e-mailadres. Wellicht is deze al in gebruik?" activation: - action: "Activeer je account" + action: "Klik hier om je account te activeren" already_done: "Sorry, de link om je account te activeren is niet langer geldig. Wellicht is je account al geactiveerd?" please_continue: "Je account is bevestigd en je wordt nu doorgestuurd naar de homepage." continue_button: "Ga verder naar %{site_name}" @@ -751,7 +751,6 @@ nl: 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." - allow_html_tables: "Witte lijst toestaan voor tabellen die zijn ingevoerd met Markdown HTML tags, TABLE, THEAD, TD, TR, TH (vereist volledig rebaken van alle oude berichten die tabellen bevatten)" post_undo_action_window_mins: "Het aantal minuten waarin gebruikers hun recente acties op een bericht nog terug kunnen draaien (liken, markeren, etc)." must_approve_users: "Stafleden moeten alle nieuwe gebruikersaccounts goedkeuren voordat ze de site mogen bezoeken. OPGELET: als dit wordt aangezet voor een actieve site wordt alle toegang voor bestaande niet stafleden ingetrokken." ga_tracking_code: "Google analytics (ga.js) trackingcode, bijv. UA-12345678-9; zie: http://google.com/analytics" @@ -995,7 +994,6 @@ nl: characters: "mag alleen nummers, letters en underscores bevatten" unique: "moet uniek zijn" blank: "mag niet leeg zijn" - must_begin_with_alphanumeric: "moet met een letter of nummer beginnen" email: not_allowed: "is niet toegestaan vanaf die e-mailprovider. Gebruik een ander e-mailadres." blocked: "is niet toegestaan." @@ -1195,6 +1193,10 @@ nl: reply_by_email: "Beantwoord deze mail om te reageren op dit forumbericht, of ga naar %{base_url}%{url} in je browser." visit_link_to_respond: "Ga naar %{base_url}%{url} in je browser om te reageren." posted_by: "Geplaatst door %{username} op %{post_date}" + user_invited_to_private_message_pm: + subject_template: "[%{site_name}] %{username} nodigt je uit voor een bericht '%{topic_title}'" + user_invited_to_topic: + subject_template: "[%{site_name}] %{username} nodigt je uit voor een topic '%{topic_title}'" user_replied: subject_template: "[%{site_name}] %{topic_title}" text_body_template: | @@ -1242,10 +1244,11 @@ nl: %{respond_instructions} digest: why: "Een korte samenvatting van %{site_link} sinds we je voor het laatst zagen op %{last_seen_at}." + subject_template: "[%{site_name}] Digest" new_activity: "Nieuwe reacties op je topics en berichten:" top_topics: "Populaire berichten" other_new_topics: "Populaire topics" - unsubscribe: "Deze samenvatting wordt door %{site_link} verstuurd als we je een tijdje niet gezien gebben op onze site. Mocht je dit uit willen zetten of je e-mailvoorkeur willen veranderen, %{unsubscribe_link}." + unsubscribe: "Deze samenvatting wordt door %{site_link} verstuurd als we je een tijdje niet gezien hebben op onze site. Mocht je dit uit willen zetten of je e-mailvoorkeur willen veranderen, %{unsubscribe_link}." click_here: "klik hier" from: "%{site_name} Digest" read_more: "Lees verder" @@ -1369,7 +1372,21 @@ nl: title: "Algemene Voorwaarden" privacy_topic: title: "Privacy Voorwaarden" + badges: + long_descriptions: + basic: | + Deze badge is aan je toegekend met het bereiken van trustlevel 1. Bedankt dat je een tijdje hebt rondgekeken, wat topics hebt gelezen en wat meer te weten bent gekomen over bedoeling van dit forum. De restricties voor nieuwe gebruikers gelden nu niet meer voor jou, en je kunt nu gebruik maken van alle basis communicatie mogelijkheden, zoals persoonlijke berichten, markeren, wijzigen van wiki's en de mogelijkheid om afbeeldingen en meerdere links in één bericht te plaatsen. + member: | + Deze badge is aan je toegekend met het bereiken van trustlevel 2. Bedankt voor je deelname gedurende enkele weken om op het forum actief te zijn. Je kunt nu persoonlijke uitnodigingen versturen vanuit je gebruikerspagina of een bepaalde topic, groepsberichten versturen en je hebt per dag meer likes tot je beschikking. + regular: | + Deze badge is aan je toegekend met het bereiken van trustlevel 3. Bedankt dat je al meerdere maanden deel uit maakt van ons forum, behoort tot de meest actieve lezers en regelmatig bijdraagt aan wat dit forum zo goed maakt. Je kunt nu de naam en categorie van topics aanpassen en gebruik maken van een speciale "lounge", krachtiger spam markeringen en nog veel meer likes per dag. + leader: | + Deze badge is aan je toegekend met het bereiken van trustlevel 4. Je bent door de staf uitgekozen voor een voortrekkersrol, om op dit forum een positief voorbeeld te geven in woord en gedrag. Je kunt nu alle berichten aanpassen, topics modereren door bijvoorbeeld pinnen, sluiten, verbergen, archiveren, splitsen en samenvoegen en je hebt een vrijwel onbeperkt aantal likes per dag. admin_login: success: "E-mail verstuurd" error: "Fout!" + email_input: "Beheerder E-mail" submit_button: "E-mail versturen" + performance_report: + initial_post_raw: Deze topic bevat dagelijkse performance rapporten van je site + initial_topic_title: Website performance rapporten diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index abf40f502..7dab533c0 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -399,7 +399,6 @@ pl_PL: please_continue: "Przejdź do %{site_name}" error: "Podczas próby zmiany Twojego adresu email wystąpił błąd. Być może ten adres jest już używany?" activation: - action: "Aktywuj swoje konto" already_done: "Przepraszamy, ten link aktywujący konto jest już nieważny. Być może Twoje konto jest już aktywne ?" please_continue: "Twoje nowe konto zostało aktywowane, zostaniesz przekierowany na stronę główną." continue_button: "Przejdź do %{site_name}" @@ -887,7 +886,6 @@ pl_PL: characters: "może zawierać tylko litery, cyfry i podkreślenia" unique: "musi być unikalna" blank: "musi zostać podana" - must_begin_with_alphanumeric: "musi zaczynać się od litery lub cyfry" email: not_allowed: "nie jest dopuszczany od tego dostawcy poczty. Użyj innego adresu email." blocked: "is not allowed." diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index 8b4c342ac..d3404bc34 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -410,7 +410,7 @@ pt: please_continue: "Continuar para %{site_name}" error: "Ocorreu um erro ao alterar o seu endereço de email. Talvez o endereço já esteja a ser utilizado?" activation: - action: "Ativar a sua conta" + action: "Clique aqui para ativar a sua conta" already_done: "Pedimos desculpa, esta hiperligação de confirmação já não está válida. Talvez a sua conta já esteja ativa?" please_continue: "A sua nova conta foi confirmada; será redirecionado para a página principal." continue_button: "Continuar para %{site_name}" @@ -761,7 +761,7 @@ pt: 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." - allow_html_tables: "Permitir que sejam inseridas tabelas no Markdown utilizando tags HTML. TABLE, THEAD, TD,TR,TH fazem parte da lista branca (requer que todas as mensagens antigas que contém tabelas sejam refeitas)" + allow_html_tables: "Permitir inserção de tabelas em Markdown utilizando tags HTML, TABLE,THEAD, TD, TR,TH fazem parte da lista branca (requer que todas as mensagens antigas que contém tabelas sejam refeitas)" 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!" ga_tracking_code: "Código de acompanhamento do Google Analytics (ga.js), ex: UA-12345678-9; ver http://google.com/analytics" @@ -1020,6 +1020,7 @@ pt: enable_cdn_js_debugging: "Permitir que /logs exiba erros próprios ao adicionar permissões de origem-cruzada em todos os js incluídos." show_create_topics_notice: "Se o sítio tem menos de 5 tópicos públicos, mostrar um aviso pedindo aos administradores a criação de mais tópicos." delete_drafts_older_than_n_days: Eliminar rascunhos mais antigos que (n) days. + show_logout_in_header: "Mostrar Terminar Sessão no menu suspenso do utilizador no cabeçalho" vacuum_db_days: "Executar VACUUM FULL ANALYZE para reclamar espaço na Base de Dados após a migração (configurar a 0 para desativar)" prevent_anons_from_downloading_files: "Previna que utilizadores anónimos descarreguem anexos. AVISO: isto irá fazer com que quaisquer atributos (que não sejam imagens) publicados como anexos não funcionem." slug_generation_method: "Escolha um método de geração slug. 'encoded' irá gerar sequências de caracteres com código percentual. 'none' irá desativar slug por completo." @@ -1165,7 +1166,10 @@ pt: characters: "pode incluir apenas números, letras e sublinhados" unique: "tem que ser único" blank: "tem que estar preenchido" - must_begin_with_alphanumeric: "tem que começar com uma letra ou com um número" + must_begin_with_alphanumeric: "tem que começar com uma letra ou número ou um sublinhado" + must_end_with_alphanumeric: "tem que terminar com uma letra ou número" + must_not_contain_two_special_chars_in_seq: "não deve conter uma sequência de 2 ou mais caracteres especiais (.-_)" + must_not_contain_confusing_suffix: "não deve conter um sufixo confuso tal como .json ou .png etc." email: not_allowed: "este provedor de emails não é permitido. Por favor utilize outro endereço de email." blocked: "não é permitido." diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 71e821d53..fd902d6b8 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -382,7 +382,6 @@ pt_BR: please_continue: "Continuar no %{site_name}" error: "Houve um erro ao alterar o seu endereço de email. Talvez o endereço já esteja sendo utilizado?" activation: - action: "Ativar sua conta" already_done: "Desculpe, este link de confirmação não está mais válido. Talvez a sua conta já esteja ativa?" please_continue: "Sua conta agora está confirmada; você vai ser redirecionado para a página inicial." continue_button: "Continuar no %{site_name}" @@ -1093,7 +1092,6 @@ pt_BR: characters: "deve incluir apenas números, letras e sublinhados" unique: "tem que ser único" blank: "tem que ser preenchido" - must_begin_with_alphanumeric: "tem de começar com uma letra ou um número" email: not_allowed: "este provedor de emails não é permitido. Por favor utilize outro endereço de email." blocked: "não é permitido." diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 557b272c8..b5699e843 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -369,7 +369,6 @@ ro: please_continue: "Continuă cu %{site_name}" error: "S-a semnalat o eroare la schimbarea adresei de Email. Poate adresa deja e folosită?" activation: - action: "Activați contul dvs" already_done: "Ne pare rău, această adresă pentru confirmarea contului nu mai este valabilă. Poate contul dvs este deja activ?" please_continue: "Noul dvs cont este confirmat, iar acum sunteți autentificat." continue_button: "Continuă cu %{site_name}" @@ -859,7 +858,6 @@ ro: characters: "trebuie să includă doar numere, litere și underscor-uri" unique: "trebuie să fie unice" blank: "trebuie să fie completat" - must_begin_with_alphanumeric: "trebuie să înceapă cu o literă sau un număr" email: not_allowed: "nu este permis din partea acelui furnizor de servicii email. Vă rugăm folosiți altă adresă email." blocked: "nu este permis." diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index f180bf5f2..a85605706 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -439,7 +439,6 @@ ru: please_continue: "Перейти на %{site_name}" error: "При смене электронного адреса произошла ошибка. Возможно, этот адрес уже используется?" activation: - action: "Активируйте вашу учетную запись" already_done: "Извините, ссылка на активацию учетной записи устарела. Возможно, ваша учетная запись уже активирована?" please_continue: "Ваша новая учетная запись успешно активирована, вы будете перенаправлены на главную страницу." continue_button: "Перейти на %{site_name}" @@ -1153,7 +1152,6 @@ ru: characters: "должно состоять только из цифр и латинских букв" unique: "должно быть уникально" blank: "необходимо заполнить" - must_begin_with_alphanumeric: "должно начинаться с буквы или цифры" email: not_allowed: "недопустимый почтовый домен. Пожалуйста, используйте другой адрес." blocked: "не разрешено." diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index f4b7e304c..ecdeeddea 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -401,7 +401,6 @@ sq: please_continue: "Vazhdo tek %{site_name}" error: "There was an error changing your email address. Perhaps the address is already in use?" activation: - action: "Aktivizoni llogarinë tuaj" already_done: "Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?" please_continue: "Your new account is confirmed; you will be redirected to the home page." continue_button: "Vazhdo tek %{site_name}" @@ -751,7 +750,6 @@ sq: 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." - allow_html_tables: "Allow tables to be entered in Markdown using HTML tags, TABLE, THEAD, TD, TR, TH are whiteliseted (requires full rebake on all old posts containing tables)" post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)." 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!" ga_tracking_code: "Google analytics (ga.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" @@ -1137,7 +1135,6 @@ sq: characters: "must only include numbers, letters and underscores" unique: "must be unique" blank: "must be present" - must_begin_with_alphanumeric: "must begin with a letter or number" email: not_allowed: "is not allowed from that email provider. Please use another email address." blocked: "is not allowed." diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 66a524fe2..28d96fba6 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -364,7 +364,6 @@ sv: please_continue: "Fortsätt till %{site_name}" error: "Det uppstod ett fel med ändringen av din e-postadress. Adressen kanske redan används?" activation: - action: "Aktivera ditt konto" already_done: "Tyvärr, denna kontoaktiveringslänk är inte längre giltig. Kanske är ditt konto redan aktiverat?" please_continue: "Ditt nya konto är verifierat; du kommer att skickas till startsidan." continue_button: "Fortsätt till %{site_name}" @@ -813,7 +812,6 @@ sv: characters: "får endast innehålla nummer, bokstäver och understreck" unique: "måste vara unikt" blank: "måste finnas" - must_begin_with_alphanumeric: "måste börja med en bokstav eller siffra" email: not_allowed: "är inte en tillåten e-postleverantör. Vänligen använd en annan e-postadress." blocked: "är inte tillåtet." diff --git a/config/locales/server.te.yml b/config/locales/server.te.yml index 34e33b960..2a40ce421 100644 --- a/config/locales/server.te.yml +++ b/config/locales/server.te.yml @@ -321,7 +321,6 @@ te: please_continue: "%{site_name} కు కొనసాగండి" error: "మీ ఈమెయిల్ చిరునామా మార్చడంలో దోషం. బహుశా ఆ చిరునామా ఈసరికే వాడుకలో ఉందా?" activation: - action: "మీ ఖాతాను చేతనం చేయండి" already_done: "క్షమించాలి. ఖాతా ధృవపరుచు లంకె కాలాతీతమైంది. బహుశా మీ ఖాతా ఇప్పటికే చేతనమై ఉందేమో?" please_continue: "మీ ఖాతా ధృవపర్చబడింది. మీరిప్పుడు తొలిపుటకు మళ్లించబడతారు." continue_button: "%{site_name} కు కొనసాగండి." @@ -632,7 +631,6 @@ te: characters: "కేవలం సంఖ్యలు, అక్షరాలు మరియు అండర్ స్కోరు మాత్రమే ఉండాలి. " unique: "ఏకైకంగా ఉండాలి" blank: "తప్పనిసరిగా ఉండాలి" - must_begin_with_alphanumeric: "తప్పనిసరిగా సంఖ్యతోగాని, అక్షరంతోగాని మొదలవ్వాలి" email: not_allowed: "ఆ ఈమెయిల్ ప్రొవైడరును అనుమంతిచుటలేదు. దయచేసి మరో ఈమెయిల్ చిరునామా రాయండి" blocked: "అనుమతించుటలేదు" diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index fbdf744d9..6c0c4d778 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -7,7 +7,7 @@ tr_TR: dates: - short_date_no_year: "D MMM" + short_date_no_year: "G AAA" short_date: "D MMM, YYYY" long_date: "MMMM D, YYYY h:mma" title: "Discourse" @@ -61,6 +61,12 @@ tr_TR: other: '%{count} hata bu %{model} kaydının alınmasını engelledi' embed: load_from_remote: "Gönderi yüklenirken bir hata oluştu." + site_settings: + min_username_length_exists: "En kısa kullanıcı adının üstünde en küçük kullanıcı adı uzunluğu ayarlayamazsınız." + min_username_length_range: "En büyük değerin üstünde en küçük değer ayarlayamazsınız." + max_username_length_exists: "En uzun kullanıcı adının altında en büyük kullanıcı adı uzunluğu ayarlayamazsınız." + max_username_length_range: "En küçük değerin altında en büyük değer ayarlayamazsınız." + default_categories_already_selected: "Bir başka listede kullanılan bir kategoriyi seçemezsiniz." bulk_invite: file_should_be_csv: "Yüklenen dosya csv veya txt formatında olmalı. " backup: @@ -148,17 +154,25 @@ tr_TR: education: until_posts: other: "%{count} gönderi" - new-topic: "%{site_name} &mdash sitesine hoşgeldiniz; **yeni bir sohbet başlattığınız için teşekkür ederiz! ** \n\n- Başlık konunuz yüksek sesle okuduğunuzda ilgi çekici duyuluyor mu? İyi bir özet mi?\n\n- Bununla kim ilgilenirdi? Neden önemli? Ne gibi cevaplar almayı diliyorsunuz?\n\n- Diğerlerinin de kolayca *bulabilmesi * için konunuzun içerisinde sıkça kullanılan kelimeler bulundurun. Konunuzun benzer konularla gruplanması için bir kategori seçin. \n\nDaha fazlası için, [topluluk yönergelerimize bakın](/guidelines). Bu panel sadece ilk gönderinizde %{education_posts_text} gözükür.\n" + new-topic: "%{site_name} forumuna hoşgeldiniz — **yeni bir sohbet başlattığınız için teşekkür ederiz! **\n\n- Yüksek sesle okuduğunuzda başlık ilgi çekici geliyor mu? İyi bir özet mi?\n\n- Bununla kim ilgilenirdi? Neden önemli? Ne gibi cevaplar almayı diliyorsunuz?\n\n- Diğerlerinin de kolayca *bulabilmesi * için konunuzun içerisine sıkça kullanılan kelimeler dahil edin. Konunuzun benzer konularla gruplanması için bir kategori seçin. \n\nDaha fazlası için, [topluluk yönergelerimize bakın](/guidelines). Bu panel sadece ilk %{education_posts_text} için görünür.\n" new-reply: | - %{site_name} &mdash sitesine hoşgeldiniz; katılımınız için teşekkür ederiz! + %{site_name} forumuna hoşgeldiniz — **katılımınız için teşekkür ederiz!** - - Cevabınız sohbetin gelişmesine yardımcı oluyor mu? + - Cevabınız sohbeti bir şekilde geliştiriyor mu? - Topluluğun diğer üyelerine karşı nazik olun. - - Yapıcı eleştirici her zaman kabul edilir, ama fikirleri eleştirin, insanları değil. + - Yapıcı eleştirici her zaman kabul edilir, ama *fikirleri* eleştirin, insanları değil. - Daha fazlası için, [topluluk yönergelerine bakın](/guidelines). Bu yazı sadece ilk gönderinizde %{education_posts_text} gözükür. + Daha fazlası için, [topluluk yönergelerine bakın](/guidelines). Bu yazı sadece ilk %{education_posts_text} için görünür. + avatar: | + ### Hesabının bir resmi olsun istemez misin? + + Birkaç konu ve cevap gönderdin, fakat profil resmin senin kadar eşsiz değil - sadece bir harf. + + Hiç **[kullanıcı profiline girmeyi](%{profile_path})** ve seni ifade eden bir resim yüklemeyi düşündün mü? + + Herkes eşsiz bir profil resmine sahip olduğunda tartışmaları takip etmek ve tartışmalarda ilgi çekici insanlar bulmak daha kolaydır! sequential_replies: "### Birden fazla gönderiyi aynı anda cevaplayabilirsin \n\nAynı konuya ardı ardına cevaplar yazmak yerine, lütfen önceki gönderilerden alıntı veya @isim referansları içeren tek bir cevap yaz. \n\nHerhangi bir yazıyı seçince çıkan <b>alıntılayarak cevapla</b> butonuna tıklayarak alıntı ekleyebilir, bir önceki cevabınızı düzenleyebilirsiniz. \n\nAz sayıda derinlemesine cevaplardan oluşan konuların okunması, çok fazla kısa ve tekil cevaplardan oluşan konulardan herkes için daha kolay oluyor.\n" dominating_topic: "###Sohbete başkalarının katılmasına izin verin\n\nBu konunun sizin için önemli olduğunu görüyoruz – buradaki cevapların %{percent}% oranından daha fazlasını siz göndermişsiniz. \n\nDiğerlerinin de kendi fikirlerini paylaşmaları için onlara yeteri kadar zaman tanıdığınıza emin misiniz?\n" too_many_replies: "### Bu konu için cevap limitinizi doldurdunuz\n\nÜzgünüz, ancak geçici olarak, yeni kullanıcılar aynı konu içinde en fazla %{newuser_max_replies_per_topic} cevap yazabiliyorlar. \n\nYeni bir cevap yazmak yerine, önceki cevaplarınızı düzenlemeyi, ya da başka konulara göz atmayı düşünün.\n" @@ -319,6 +333,7 @@ tr_TR: almost_x_years: other: "yaklaşık %{count} yıl önce" password_reset: + no_token: "Üzgünüz, bu şifre değiştirme bağlantısı çok eski. Yeni bir bağlantı almak için lütfen 'Giriş Yap' tuşuna basın ve 'Parolamı unuttum'u seçin." choose_new: "Lütfen yeni bir parola seçin" choose: "Lütfen parola seçin" update: 'Parolayı Güncelle' @@ -332,7 +347,7 @@ tr_TR: please_continue: "%{site_name} adresine devam edin" error: "E-posta adresiniz değiştirilirken bir hata oluştu. Bu adres zaten kullanımda olabilir." activation: - action: "Hesabınızı etkinleştirin" + action: "Hesabınızı etkinleştirmek için buraya tıklayın" already_done: "Üzgünüz, hesap doğrulama linki artık geçerli değil. Hesabınız zaten etkin olabilir mi?" please_continue: "Hesabınız doğrulandı; şimdi ana sayfaya yönlendirileceksiniz." continue_button: "%{site_name} adresine devam edin" @@ -558,6 +573,7 @@ tr_TR: host_names_warning: "config/database.yml dosyasınızda, bilgisayar adı olarak varsayılan değer olan \"localhost\" ayarlı. Değeri, sitenizin bilgisayar adını kullanacak biçimde güncelleyiniz." gc_warning: 'Sunucunuz, Ruby''nin varsayılan çöp toplama ayarlarını kullanıyor ki bu size en iyi performansı vermeyecektir. Performansı ayarı için şu konuyu okuyun: <a href="http://meta.discourse.org/t/tuning-ruby-and-rails-for-discourse/4126" target="_blank">Discourse için Ruby on Rails Ayarları</a>.' sidekiq_warning: 'Sidekiq çalışmıyor. E-posta yollamak gibi gibi birçok asenkron görev sidekiq''in işidir. En az bir tane sidekiq süreci çalıştırdığınızdan emin olun. <a href="https://github.com/mperham/sidekiq" target="_blank">Sidekiq ile ilgili bilgi burada</a>.' + queue_size_warning: 'Kuyruğa eklenmiş işlerin sayısı fazla: %{queue_size}. Bu Sidekiq işlem(ler)indeki bir sorunu işaret ediyor olabilir, ya da daha fazla Sidekiq işçisi eklemeniz gerekiyor olabilir.' memory_warning: 'Sunucunuz toplam 1GB''tan az bellek ile çalışıyor. En az 1GB bellek tavsiye edilmektedir.' google_oauth2_config_warning: 'Sunucu Google OAuth2 (enable_google_oauth2_logins) ile üyelik oluşturulması ve giriş yapılmasına elveriyor, fakat the kullanıcı IDsi and gizli kullanıcı değerleri henüz ayarlanmamış. <a href="/admin/site_settings">Site Ayarları</a> sayfasına gidin ve ayarları güncelleyin. <a href="https://meta.discourse.org/t/configuring-google-login-for-discourse/15858" target="_blank">Daha fazla bilgi için bu yönetmeliğe bakın</a>.' facebook_config_warning: 'Sunucu Facebook (enable_facebook_logins) ile üyelik oluşturulması ve giriş yapılmasına izin veriyor, fakat app ID ve gizli app değerleri henüz ayarlanmamış. <a href="/admin/site_settings">Site Ayarları</a> sayfasına gidin ve ayarları güncelleyin. <a href="https://meta.discourse.org/t/configuring-facebook-login-for-discourse/13394" target="_blank">Daha fazla bilgi için bu yönetmeliğe bakın</a>.' @@ -648,6 +664,11 @@ tr_TR: post_excerpt_maxlength: "Gönderi alıntısının / özetinin en fazla uzunluğu." post_onebox_maxlength: "Kutulanmış bir Discourse gönderisinin en fazla karakter uzunluğu" onebox_domains_whitelist: "Kutulamaya izin verilen alan adları listesi; bu alan adları OpenGraph ya da oEmbed desteklemeliler. http://iframely.com/debug adresinden test edebilirsiniz." + logo_url: "Sitenin üst solundaki logo resmi, geniş dikdörtgen şeklinde olmalıdır. Boş bırakılırsa site başlığı gösterilecektir." + digest_logo_url: "Sitenin e-posta özetinin üstünde kullanılan diğer logo resmi. Geniş dikdörtgen şeklinde olmalıdır. Boş bırakılırsa `logo_url` kullanılacaktır." + logo_small_url: "Sitenin üst solundaki küçük logo resmi, kare şeklinde olmalıdır, aşağıya doğru kaydırılırken görünür. Boş bırakılırsa bir ev oyması gösterilecektir." + favicon_url: "Site simgesi, bilgi için http://en.wikipedia.org/wiki/Favicon adresine bakınız, bir CDN ile doğru şekilde çalışmak için png olmalıdır." + mobile_logo_url: "Mobil sitenin üst solunda kullanılan sabit konumlu logo resmi. Kare şeklinde olmalıdır. Boş bırakılırsa, `logo_url` kullanılacaktır. Örneğin: http://example.com/uploads/default/logo.png" apple_touch_icon_url: "Apple dokunmatik cihazları için kullanılan ikon. Önerilen boyut; 144 x 144 pixel." notification_email: "Tüm önemli sistem e-postaları için kullanılacak olan gönderen e-posta adresi. E-postaların başarıyla ulaşması için buraya girilen alan adının SPF, DKIM ve reverse PTR kayıtlarının doğru yapılması lazım." email_custom_headers: "Sınırlandırılmış özel e-posta başlıkları listesi" @@ -677,7 +698,7 @@ tr_TR: notify_mods_when_user_blocked: "Eğer bir kullanıcı otomatik olarak engellendiyse, tüm moderatörlere mesaj 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 spam olarak işaretle. " 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 kullan." - allow_html_tables: "Tabloların HTML etiketleri kullanılarak Markdown'da girilebilmesine izin ver. TABLE, THEAD, TD, TR ve TH beyaz listeye alınmıştır. (Tüm tablo içeren eski gönderilerde full rebake yapılmasını gerektirir)" + allow_html_tables: "Çizelgelerin HTML etiketleri kullanılarak Markdown ile oluşturulmasına izin verin, TABLE, THEAD, TD, TR, TH kabul edilir (çizelge içeren tüm eski gönderilerin yenilenmesini gerektirir)" post_undo_action_window_mins: "Kullanıcıya tanınan, bir gönderide yapılan yeni aksiyonları (beğenme, bayraklama, vs) geri alabilme dakika süresi" 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." ga_tracking_code: "Google analytics (ga.js) takip kodu, ör: UA-12345678-9; bakınız http://google.com/analytics" @@ -720,6 +741,8 @@ tr_TR: invite_passthrough_hours: "Daha önce kabul edilmiş davetiye anahtarının kullanım süresi, saat olarak" invite_only: "Halka açık kayıt sistemi devre dışı bırakıldı, tüm yeni kullanıcıların bir üye ya da görevli tarafından davet edilmesi gerekir. " login_required: "Bu sitede içerik görüntülenebilmesi için kimlik doğrulamayı zorunlu kıl, isimsiz girişe izin verme." + min_username_length: "Karakter olarak en küçük kullanıcı adı uzunluğu." + max_username_length: "Karakter olarak en büyük kullanıcı adı uzunluğu." reserved_usernames: "Üyelik için izin verilmeyen kullanıcı adları." min_password_length: "En az parola uzunluğu." block_common_passwords: "En çok kullanılan 10,000 parola arasında yer alan parolalara izin verme." @@ -749,6 +772,8 @@ tr_TR: github_client_secret: "Github doğrulaması için gereken, https://github.com/settings/applications adresinde kayıtlı client secret" allow_restore: "Geri almaya izin ver. Tüm sitedeki verileri değiştirebilir! Bir yedeklemeyi geri yüklemeyi planlamıyorsanız devre dışı bırakın." maximum_backups: "Diskte tutulacak en fazla yedek sayısı. Eski yedekler otomatik olarak silinir." + automatic_backups_enabled: "Yedek sıklığında tanımlandığı gibi yedekleri otomatik çalıştır" + backup_frequency: "Hangi sıklıkta bir site yedeği oluştururuz, gün olarak." enable_s3_backups: "Tamamlanınca yedeklemeleri S3'e yükle. ÖNEMLİ: Dosyalar ayarında doğru S3 girilmesini gerektirir" s3_backup_bucket: "Yedeklemelerin yüklenmesi için uzak biriktirme yeri. UYARI: Özel bir biriktirme yeri olduğundan emin olun" active_user_rate_limit_secs: "'last_seen_at' alanını ne kadar sıklıkta güncelliyoruz, saniye olarak" @@ -856,6 +881,10 @@ tr_TR: num_flaggers_to_close_topic: "Bir konunun moderatör müdahalesi için otomatik olarak durdurulmadan önce alması gereken en az tekil bayrak sayısı" num_flags_to_close_topic: "Bir konunun moderatör müdahalesi için otomatik olarak durdurulmadan önce alması gereken en az etkin bayrak sayısı " auto_respond_to_flag_actions: "Bir bayrağı kaldırırken otomatik 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" @@ -868,6 +897,7 @@ tr_TR: pop3_polling_host: "POP3 üzerinden e-postaların sorgulanacağı sunucu" pop3_polling_username: "E-postaların sorgulanacağı POP3 hesabının kullanıcı adı." pop3_polling_password: "E-postaların sorgulanacağı POP3 hesabının parolası." + log_mail_processing_failures: "Bütün e-posta işleme hatalarını http://yoursitename.com/logs adresine logla." email_in: "Kullanıcıların e-posta aracılığıyla yeni konu oluşturabilmesine izin ver (pop3 sorgulaması gerektirir). Adresleri her kategorinin \"Ayarlar\" sekmesinden düzenleyin." email_in_min_trust: "Bir kullanıcının e-posta aracılığı ile yeni konu oluşturabilmesi için sahip olması gereken en az güven seviyesi." email_prefix: "E-postaların konu bölümünü belirten [etiket]. Boş bırakılırsa 'title' yazacak." @@ -879,6 +909,8 @@ tr_TR: username_change_period: "Kayıt sonrası, kullanıcı adınının değiştirilebileceği gün sayısı. (Kullanıcı adının değiştirilebilmesini devre dışı bırakmak için 0 girin)" email_editable: "Kullanıcıların kayıt olduktan sonra e-posta adreslerini değiştirmesine izin ver." logout_redirect: "Çıkış yaptıktan sonra tarayıcının yönlendirileceği sayfa, ÖRN: (http://somesite.com/logout)" + allow_uploaded_avatars: "Kullanıcıların özel profil resimleri yüklemelerine izin ver." + allow_animated_avatars: "Kullanıcıların hareketli gif profil resimleri kullanmalarına izin ver. UYARI: Bu ayarı değiştirdikten sonra avatars:refresh rake görevini çalıştırın." allow_animated_thumbnails: "Animasyonlu giflerin, animasyonlu küçük resmini oluşturur." default_avatars: "Yeni kullanıcılar için, onlar değiştirene kadar, varsayılan olarak kullanılacak avatarların URL'leri." automatically_download_gravatars: "Hesap oluşturma veya e-posta değişikliği esnasında kullanıcılar için Gravatarları indir" @@ -886,6 +918,7 @@ tr_TR: digest_min_excerpt_length: "Özet e-postalarında, gönderi alıntılarında olması gereken en az karakter sayısı." suppress_digest_email_after_days: "Siteye (n) günden fazla süredir uğramayan kullanıcılar için özet e-posta gönderimini durdur" disable_digest_emails: "Tüm kullanıcılar için özet e-postalarını devre dışı bırak." + detect_custom_avatars: "Kullanıcıların özel profil resimleri yükleyip yüklemediklerini kontrol et ya da etme." max_daily_gravatar_crawls: "Discourse'un gün içinde özel avatarlar için Gravatar'ı en fazla kaç kere kontrol edeceği." public_user_custom_fields: "Kullanıcıların için, herkes tarafından görüntülenebilir özel alanların beyaz listesi." staff_user_custom_fields: "Kullanıcıların için, sadece görevlilere görüntülenebilir özel alanların beyaz listesi." @@ -924,6 +957,7 @@ tr_TR: enable_cdn_js_debugging: "/logs 'ların asli hataları tüm js içeriklerine crossorigin izinleri ekleyerek göstermesine izin ver." show_create_topics_notice: "Eğer sitede herkese açık konu sayısı 5'den az ise, adminden yeni konular oluşturmasını isteyen bir uyarı mesajı göster. " delete_drafts_older_than_n_days: (n) günden eski taslakları sil. + show_logout_in_header: "Başlık çubuğundaki kullanıcı açılır listesinde oturumu kapatı göster" vacuum_db_days: "Geçiş sonra DB alanı geri kazanmak için TAM VAKUM ANALİZİ'ni çalıştırın (devre dışı bırakmak için 0 girin)" prevent_anons_from_downloading_files: "Anonim kullanıcıların eklenti indirebilmesini önle. DİKKAT: Bu ayar, eklenti olarak gönderilen resim-dışı site içeriklerinin de çalışmasını engelleyebilir." slug_generation_method: "Slug üretim yöntemi seçin. 'kodlanmış' seçeneği yüzde kodlamalı metin oluşturur. 'hiçbiri' seçeneği slug'ı devre dışı bırakır." @@ -933,6 +967,21 @@ tr_TR: approve_post_count: "Yeni bir kullanıcıdan onaylanması gereken gönderi sayısı" approve_unless_trust_level: "Bu güven seviyesi altındaki kullanıcılardan gelen gönderilerin onaylanması gerekir" notify_about_queued_posts_after: "Bu kadar saat geçmesine rağmen hala incelenmemiş konular varsa, iletişim adresine e-posta gönder. Devre dışı bırakmak için 0 girin." + default_email_digest_frequency: "Öntanımlı olarak kullanıcılar hangi sıklıkta özet e-postalar alırlar." + default_email_private_messages: "Öntanımlı olarak birisi bir kullanıcıya mesaj attığında e-posta gönder." + default_email_direct: "Öntanımlı olarak birisi bir kullanıcı hakkında alıntı yapma, cevaplama, bahsetme ya da davet etme eylemlerini gerçekleştirdiğinde e-posta gönder." + default_email_mailing_list_mode: "Öntanımlı olarak her yeni gönderi için bir e-posta gönder." + default_email_always: "Öntanımlı olarak kullanıcı etkin olsa bile e-posta bildirimi gönder." + default_other_new_topic_duration_minutes: "Bir konunun yeni sayıldığı genel öntanımlı dakika sayısı, kullanıcılar geçersiz kılabilir (her zaman için -1, son ziyaret için -2)" + default_other_auto_track_topics_after_msecs: "Bir konu otomatik olarak takip edilmeden önce geçen genel öntanımlı milisaniye sayısı, kullanıcılar geçersiz kılabilir (her zaman için 0, asla için -1)" + default_other_external_links_in_new_tab: "Harici linkleri, öntanımlı olarak, yeni bir sekmede aç." + default_other_enable_quoting: "Vurgulanmış yazılar için alıntı ile cevaplamayı, öntanımlı olarak, etkinleştir." + default_other_dynamic_favicon: "Tarayıcı simgesinde, öntanımlı olarak, yeni/güncellenmiş konu sayısını göster." + default_other_disable_jump_reply: "Kullanıcılar cevapladıktan sonra, öntanımlı olarak, onların gönderilerine atlama." + default_other_edit_history_public: "Gönderi değişikliklerini, öntanımlı olarak, herkese açık yap." + default_categories_watching: "Öntanımlı olarak, izlenen kategorilerin listesi." + default_categories_tracking: "Öntanımlı olarak, takip edilen kategorilerin listesi." + default_categories_muted: "Öntanımlı olarak, sesi kısılan kategorilerin listesi." errors: invalid_email: "Geçersiz e-posta adresi." invalid_username: "Bu kullanıcı adı ile bir kullanıcı bulunmuyor." @@ -979,6 +1028,11 @@ tr_TR: redirected_to_top_reasons: new_user: "Hoşgeldiniz! Bunlar en popüler yeni gönderiler." not_seen_in_a_month: "Hoşgeldiniz! Bir süredir yoktunuz. Bunlar sizin yokluğunuzda en popüler olan konular." + move_posts: + new_topic_moderator_post: + other: "%{count} gönderi yeni bir konu için ayıklandı: %{topic_link}" + existing_topic_moderator_post: + other: "%{count} gönderi var olan bir konu içinde birleştirildi: %{topic_link}" change_owner: post_revision_text: "Sahiplik %{old_user} hesabından %{new_user} hesabına aktarıldı" deleted_user: "silinmiş kullanıcı" @@ -1041,7 +1095,10 @@ tr_TR: characters: "sadece rakam, harf ve altçizgi bulundurabilir " unique: "özgün olmalı" blank: "bulunmalı" - must_begin_with_alphanumeric: "harf ya da sayı ile başlamalı" + must_begin_with_alphanumeric: "bir harf, rakam ya da alt çizgi ile başlamalı" + must_end_with_alphanumeric: "bir harf ya da rakam ile bitmeli" + must_not_contain_two_special_chars_in_seq: "2 ya da daha fazla uzunlukta özel karakter dizisi (.-_) içermemeli" + must_not_contain_confusing_suffix: ".json ya da .png gibi kafa karıştırıcı bir son ek içermemeli" email: not_allowed: "için o e-posta sağlayıcısına izin verilmiyor. Lütfen başka bir email adresi kullanın. " blocked: "için izin yok." @@ -1067,6 +1124,13 @@ tr_TR: Bu davet güvenilir bir kullanıcı tarafından gönderilmiştir. O nedenle giriş yapmanız gerekmeyecek. invite_password_instructions: subject_template: "%{site_name} hesabınız için parola oluşturun" + text_body_template: | + %{site_name}'e olan davetiyeni kabul ettiğin için teşekkür ederiz -- hoşgeldin! + + Şimdi bir şifre seçmek için şu bağlantıya tıklayın: + %{base_url}/users/password-reset/%{email_token} + + (Eğer yukarıdaki bağlantının süresi dolmuşsa e-posta adresinizle giriş yaparken "Parolamı unuttum" bağlantısına tıklayınız.) test_mailer: subject_template: "[%{site_name}] E-posta Ulaştırma Testi" text_body_template: "Bu aşağıdaki adresten gönderilen bir test e-postasıdır.\n\n[**%{base_url}**][0]\n\nE-postaların ulaştırılması karışık bir meseledir. Öncelikle dikkat etmeniz gereken bir kaç önemli nokta:\n\n- Site ayarlarınızda 'bildiri e-postaları' için gönderen adresini doğru ayarladığınıza emin olun. **Yolladığınız e-postalarda \"gönderen\" adresi olarak belirlediğiniz alan adı, e-postalarınızın doğrulanacağı alan adıdır.**\n\n- E-posta başlıklarındaki önemli ipuçlarını yakalayabilmek için e-posta istemcinizde e-postaların kaynak kodunu nasıl görüntüleyebileceğinizi öğrenin. Gmail'da, her e-postanın sağ üstündeki açılır menüden \"show original\" opsiyonuna tıklayabilirsiniz.\n\n- **ÖNEMLİ:** ISP'nizde e-posta yollamak için kullanıdığınız alan adlarıyla IP adreslerinin eşleşmesini sağlayacak bir reverse DNS kaydı var mı? Buradan [reverse PTR kayıtlarınızı test edin][2]. Eğer ISP'niz doğru reverse DNS pointer kaydı girmezse, büyük ihtimal e-postalarınızın hiç biri yerine ulaşmayacaktır.\n\n- Alan adınızın [SPF kaydı][8] doğru mu? Buradan [SPF kaydınızı test edin][1]. SPF için doğru resmi kayıt tipinin TXT olduğunu unutmayın. \n\n- Alan adınızın [DKIM kaydı][3] doğru mu? Bu e-postaların ulaştırılabilirliğini ciddi şekilde artıracaktır. Buradan [DKIM kaydınızı test edin][7].\n\n- Kendi e-posta sunucunuzu kullanıyorsanız, e-posta sunucunuzun IPlerinin [hiç bir e-posta karalistesine][4] alınmadığına emin olun. Sunucunuzun, kesinlikle, HELO mesajında DNS olarak çözümlenen tam tanımlanmış bilgisayar adı da gönderdiğinden emin olun. Göndermemesi, e-postanızın bir çok e-posta servisi tarafından reddedilmesine sebep olacaktır. \n\n(En kolayı, küçük topluluklar için rahat rahat yetecek sayıda bedava email yollama paketleri içeren, [Mandrill][md] veya [Mailgun][mg] veya [Mailjet][mj]'te ücretsiz hesap açmak. Tabi, gene, DNS ayarlarınızda SPF ve DKIM kayıtlarını oluşturmanız gerekecek!) \n\nUmarız bu e-posta ulaştırma testini başarıyla atlatmışsınızdır. \n\nİyi şanslar, \n\n[Discourse](http://www.discourse.org)'tan arkadaşlarınız \n\n[0]: %{base_url} \n[1]: http://www.kitterman.com/spf/validate.html\ @@ -1299,6 +1363,10 @@ tr_TR: download_remote_images_disabled: subject_template: "Uzaktaki resimlerin indirilmesi devre dışı bırakıldı" text_body_template: "`download_remote_images_to_local` ayarı harddisk alanı limiti `download_remote_images_threshold` aşıldığı için devre dışı bırakıldı." + unsubscribe_link: | + Bu e-postaların üyeliklerinden çıkmak için [user preferences](%{user_preferences_url})'i ziyaret edin. + + Bu belirli konuda bildirim almayı durdurmak için [buraya tıklayın](%{unsubscribe_url}). subject_re: "Cvp:" subject_pm: "[ÖM]" user_notifications: @@ -1479,6 +1547,7 @@ tr_TR: unauthorized: "Üzgünüz, yüklemeye çalıştığınız dosya izinli değil (authorized extensions: %{authorized_extensions})." pasted_image_filename: "Yapıştırılan resim" store_failure: "#%{user_id} kullanıcısı için yükleme #%{upload_id} kaydedilemedi." + file_missing: "Affedersiniz, yükleme için bir dosya sağlamalısınız." attachments: too_large: "Üzgünüz, yüklemeye çalıştığınız dosya çok büyük (en fazla %{max_size_kb}KB olabilir)." images: @@ -1752,106 +1821,80 @@ tr_TR: Originally adapted from the [WordPress Terms of Service](http://en.wordpress.com/tos/). privacy_topic: title: "Gizlilik İlkeleri" - body: | - <a name="collect"></a> - - ## [What information do we collect?](#collect) - - We collect information from you when you register on our site and gather data when you participate in the forum by reading, writing, and evaluating the content shared here. - - When registering on our site, you may be asked to enter your name and e-mail address. You may, however, visit our site without registering. Your e-mail address will be verified by an email containing a unique link. If that link is visited, we know that you control the e-mail address. - - When registered and posting, we record the IP address that the post originated from. We also may retain server logs which include the IP address of every request to our server. - - <a name="use"></a> - - ## [What do we use your information for?](#use) - - Any of the information we collect from you may be used in one of the following ways: - - * To personalize your experience — your information helps us to better respond to your individual needs. - * To improve our site — we continually strive to improve our site offerings based on the information and feedback we receive from you. - * To improve customer service — your information helps us to more effectively respond to your customer service requests and support needs. - * To send periodic emails — The email address you provide may be used to send you information, notifications that you request about changes to topics or in response to your user name, respond to inquiries, and/or other requests or questions. - - <a name="protect"></a> - - ## [How do we protect your information?](#protect) - - We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information. - - <a name="data-retention"></a> - - ## [What is your data retention policy?](#data-retention) - - We will make a good faith effort to: - - * Retain server logs containing the IP address of all requests to this server no more than 90 days. - * Retain the IP addresses associated with registered users and their posts no more than 5 years. - - <a name="cookies"></a> - - ## [Do we use cookies?](#cookies) - - Yes. Cookies are small files that a site or its service provider transfers to your computer's hard drive through your Web browser (if you allow). These cookies enable the site to recognize your browser and, if you have a registered account, associate it with your registered account. - - We use cookies to understand and save your preferences for future visits and compile aggregate data about site traffic and site interaction so that we can offer better site experiences and tools in the future. We may contract with third-party service providers to assist us in better understanding our site visitors. These service providers are not permitted to use the information collected on our behalf except to help us conduct and improve our business. - - <a name="disclose"></a> - - ## [Do we disclose any information to outside parties?](#disclose) - - We do not sell, trade, or otherwise transfer to outside parties your personally identifiable information. This does not include trusted third parties who assist us in operating our site, conducting our business, or servicing you, so long as those parties agree to keep this information confidential. We may also release your information when we believe release is appropriate to comply with the law, enforce our site policies, or protect ours or others rights, property, or safety. However, non-personally identifiable visitor information may be provided to other parties for marketing, advertising, or other uses. - - <a name="third-party"></a> - - ## [Third party links](#third-party) - - Occasionally, at our discretion, we may include or offer third party products or services on our site. These third party sites have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these linked sites. Nonetheless, we seek to protect the integrity of our site and welcome any feedback about these sites. - - <a name="coppa"></a> - - ## [Children's Online Privacy Protection Act Compliance](#coppa) - - Our site, products and services are all directed to people who are at least 13 years old or older. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA ([Children's Online Privacy Protection Act](http://en.wikipedia.org/wiki/Children)), do not use this site. - - <a name="online"></a> - - ## [Online Privacy Policy Only](#online) - - This online privacy policy applies only to information collected through our site and not to information collected offline. - - <a name="consent"></a> - - ## [Your Consent](#consent) - - By using our site, you consent to our web site privacy policy. - - <a name="changes"></a> - - ## [Changes to our Privacy Policy](#changes) - - If we decide to change our privacy policy, we will post those changes on this page. - - This document is CC-BY-SA. It was last updated May 31, 2013. + static: + search_help: | + <h3>İpuçları</h3> + <p> + <ul> + <li>Başlık eşlemelerine öncelik verilir – şüpheli durumlarda, başlıklar için aramalara </li> + <li>Eşsiz, alışılmamış kelimeler en iyi sonucu üretecektir</li> + <li>Belirli bir kategori, konu ya da kullanıcı içinde aramayı deneyin</li> + </ul> + </p> + <h3>Seçenekler</h3> + <p> + <table> + <tr><td><code>order:views</code></td><td><code>order:latest</code></td><td colspan=3></td></tr> + <tr><td><code>status:open</code></td><td><code>status:closed</code></td><td><code>status:archived</code></td><td><code>status:noreplies</code></td><td><code>status:single_user</code></td></tr> + <tr><td><code>category:foo</code></td><td><code>user:foo</code></td><td colspan=3></td></tr> + <tr><td><code>in:likes</code></td><td><code>in:posted</code></td><td><code>in:watching</code></td><td><code>in:tracking</code></td><td><code>in:private</code></td></tr> + <tr><td><code>in:bookmarks</code></td><td><code>in:first</code></td><td colspan=3></td></tr> + <tr><td><code>posts_count:num</code></td><td><code>min_age:days</code></td><td><code>max_age:days</code></td> <td colspan=2></td></tr> + </table> + </p> + <p> + <code>gökkuşağı category:parklar status:open order:latest</code> "parklar" kategorisi içinde "gökkuşağı" kelimesini içeren, kapatılmamış ya da arşivlenmemiş ve son gönderinin tarihine göre sıralanmış konuları arayacaktır. + </p> badges: long_descriptions: autobiographer: | Bu rozet <a href="/my/preferences">profilini</a> doldurduğunda ve profil resmini seçtiğinde verilir. Kim olduğun ve nelerle ilgilendiğin hakkında topluluğa daha fazla bilgi vermen daha iyi ve daha yakın bir topluluğun oluşmasına yardım eder. + first_like: | + Bu rozet :heart: tuşunu kullanarak bir gönderiyi ilk kez beğendiğinizde verilir. Gönderileri beğenmek hemcins topluluk üyelerinin ilgi çekici, faydalı, havalı ve eğlenceli nelerin gönderildiğini bilmelerini sağlamak için mükemmel bir yoldur. Sevgiyi paylaş! + first_link: | + Bu rozet bir cevapta başka bir konuya bir bağlantı verdiğinde verilir. Konuları bağlamak, her iki yönde konular arasındaki ilişkiyi göstererek, hemcins okuyucuların ilgi çekici ve ilgili konuşmaları bulmalarına yardım eder. + first_quote: | + Bu rozet bir cevapta ilk kez bir gönderiden alıntı yaptığınızda verilir. Cevabında önceki gönderilerin ilgili bölümlerinden alıntı yapmak tartışmaların odaklanmış halde ve konu içinde tutulmasına yardımcı olur. + first_share: | + Bu rozet paylaş tuşunu kullanarak bir cevaba ya da konuya ilk kez bir bağlantı paylaştığında verilir. Bağlantıları paylaşmak dünyanın geri kalanıyla yapılan ilginç tartışmalarla gösteriş yapmak ve topluluğunu büyütmek için mükemmel bir yoldur. + read_guidelines: | + Bu rozet <a href="/guidelines">topluluk kurallarını okunmasına</a> verilir. Bu basit kuralları benimsemek ve paylaşmak güvenli, eğlenceli ve sürdürülebilir bir topluluğun inşa edilmesine yardım eder. + reader: | + Bu rozet uzun bir konuyu okumaya verilir. Okumak temeldir. Yakından okumak tartışmayı takip etmene ve daha iyi, daha eksiksiz cevaplar yazmana yardım eder. + editor: | + Bu rozet gönderini değiştirmene verilir. Geliştirmek, küçük hataları gidermek ya da unuttuğun bir şeyi eklemek için gönderilerini değiştirmekten hiçbir zaman çekinme. + first_flag: | + Bu rozet bir gönderinin işaretlenmesine verilir. İşaretleme, topluluğunun sağlığı için çok önemlidir. Yönetici incelemesi gereken herhangi bir gönderi görürseniz lütfen işaretlemekten çekinmeyin. Ayrıca hemcins kullanıcılara <b>mesajlar</b> göndermek için işaretleme diyaloğunu da kullanabilirsiniz. + nice_share: | + Bu rozet bir gönderide paylaşılan bir bağlantının 25 yabancı ziyaretçi tarafından ziyaret edilmesine verilir. Tebrikler! İlginç tartışmalara bağlantıları arkadaşlarla paylaşmak topluluğumuzu büyütmek için harika bir yoldur. + welcome: | + Bu rozet bir gönderide ilk beğeninizi aldığınızda verilir. Tebrikler, hemcins topluluk üyelerinin ilginç, havalı ya da kullanışlı bulduğu bir şeyler gönderdin. + anniversary: | + Bu rozet en az bir gönderiye sahip olunan bir yıllık üyeliğe verilir. Buralarda olup topluluğumuza katkıda bulunduğunuz için teşekkür ederiz. good_share: | - Bu rozet 300 yabancı ziyaretçi tarafından ziyaret edilen bir gönderi bağlantısının paylaşılmasına verilir. İyi iş! <i>Bir sürü</> yeni kişiye ilgi çekici bir tartışma gösterdin ve böylece çoğalmamıza yardımcı oldun. + Bu rozet bir gönderide paylaşılan bir bağlantı, 300 yabancı ziyaretçi tarafından ziyaret edildiğinde verilir. İyi iş! <i>Bir sürü</> yeni kişiye ilgi çekici bir tartışma gösterdin ve böylece büyümemize yardımcı oldun. + great_share: | + Bu rozet bir gönderide paylaşılan bir bağlantının 100 yabancı ziyaretçi tarafından ziyaret edilmesine verilir. Vay be! Bu topluluk için ilginç bir tartışmayı yeni kocaman bir izleyici kitlesine terfi ettirdin ve büyümemize önemli bir katkı sağladın. nice_post: | - Bu rozet 10 beğeni alan bir cevabın yazılmasına verilir. İyi iş! + Bu rozet bir cevap 10 beğeni aldığında verilir. İyi iş! nice_topic: | - Bu rozet 10 beğeni alan bir konunun yaratılmasına verilir. İyi iş! + Bu rozet bir konu 10 beğeni aldığında verilir. İyi iş! good_post: | - Bu rozet 25 beğeni alan bir cevabın yazılmasına verilir. İyi iş! + Bu rozet bir cevap 25 beğeni aldığında verilir. İyi iş! good_topic: | - Bu rozet 25 beğeni alan bir konunun yaratılmasına verilir. İyi iş! + Bu rozet bir konu 25 beğeni aldığında verilir. İyi iş! great_post: | - Bu rozet 50 beğeni alan bir gönderinin yaratılmasına verilir. Vay be! + Bu rozet bir gönderi 50 beğeni aldığında verilir. Vay be! great_topic: | - Bu rozet 50 beğeni alan bir cevabın yazılmasına verilir. Vay be! + Bu rozet bir cevap 50 beğeni aldığında verilir. Vay be! + basic: | + Bu rozet güven seviyesi 1'e ulaştığında verilir. Bir süredir buralarda olduğun ve topluluğumuzun ne hakkında olduğu konusunda birkaç konu okuduğun için teşekkür ederiz. Yeni kullanıcı kısıtlamaların kaldırıldı ve kişisel mesajlaşma, işaretleme, wiki düzenleme, resim ve birden çok bağlantı gönderme yeteneği gibi ana topluluk yeteneklerini aldın. + member: | + Bu rozet güven seviyesi 2'ye ulaştığında verilir. Haftalar boyunca buralarda olup katkıda bulunduğun için teşekkür ederiz. Artık kullanıcı sayfandan ya da konularından kişisel davetler gönderebilir, grup mesajları oluşturabilir ve günlük birkaç daha fazla beğeni ekleyebilirsin. + regular: | + Bu rozet güven seviyesi 3'e ulaştığında verilir. Aylardır topluluğumuzun düzenli bir parçası, bu topluluğu harika yapan en etkin okuyucu ve güvenilir katkıcılardan biri olduğun için teşekkür ederiz. Artık konuların kategorilerini ve isimlerini değiştirebilirsin, özel bir lobi alanına erişebilir, daha güçlü spam işaretleri koyabilir ve günlük çok daha fazla beğeni ekleyebilirsin. + leader: | + Bu rozet güven seviyesi 4'e ulaştığında verilir. Eylem ve kelimelerin ile topluluk için olumlu bir izlenim bırakman sebebiyle forum kadrosu tarafından bu topluluğun bir önderi seçildin. Artık tüm gönderileri düzenleyebilir, sabitleme, kapatma, listelememe, arşivleme, ayırma ve birleştirme gibi yönetici eylemlerini yapabilir ve günlük yığınca beğeni ekleyebilirsiniz. admin_login: success: "E-posta Gönderildi" error: "Hata!" diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index 32b8a6a94..046b6fe39 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -144,7 +144,6 @@ uk: confirmed: "Адресу вашої електронної скриньки оновлено." error: "Під час зміни адреси Вашої електронної скриньки трапилася помилка. Можливо, ця адреса вже використовується?" activation: - action: "Активувати ваш обліковий запис" already_done: "Sorry, this account confirmation link is no longer valid. Perhaps your account is already active?" welcome_to: "Ласкаво просимо до сайта %{site_name}!" approval_required: "A moderator must manually approve your new account before you can access this forum. You'll get an email when your account is approved!" @@ -426,7 +425,6 @@ uk: characters: "must only include numbers, letters and underscores" unique: "має бути унікальним" blank: "має бути наявним" - must_begin_with_alphanumeric: "must begin with a letter or number" email: not_allowed: "is not allowed from that email provider. Please use another email address." blocked: "не допускається." diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 5ee7766ef..9778d641c 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -373,7 +373,7 @@ zh_CN: please_continue: "转入到 %{site_name}" error: "在修改你的电子邮箱地址时出现了错误,可能此邮箱已经在论坛中使用了?" activation: - action: "激活你的帐号" + action: "点击这儿激活你的账户" already_done: "抱歉,此帐号激活链接已经失效。可能你的帐号已经被激活了?" please_continue: "你的新帐号已激活;即将转到主页。" continue_button: "转入到 %{site_name}" @@ -724,7 +724,7 @@ zh_CN: notify_mods_when_user_blocked: "如果一个用户被自动封禁了,发送一个消息给所有管理员。" flag_sockpuppets: "如果一个新用户开始了一个主题,并且同时另一个新用户以同一个 IP 在该主题回复,他们所有的帖子都将被自动标记为垃圾。" traditional_markdown_linebreaks: "在 Markdown 中使用传统换行符,即用两个尾随空格来换行" - allow_html_tables: "允许 Markdown 中输入表格的 HTML 表情,TABLE、THEAD、TD、TR、TD 等(需要重制包含表格标签的旧帖子)" + allow_html_tables: "允许在输入 Markdown 时输入表格 HTML 标签,TABLE、THEAD、TD、TR、TH 将被白名单(需要对所有包含表格的老帖子做彻底的 rebake)" post_undo_action_window_mins: "允许用户在帖子上进行撤销操作(赞、标记等)所需等待的间隔分钟数" must_approve_users: "新用户在被允许访问站点前需要由职员批准。警告:在运行的站点中启用将解除所有非职员用户的访问权限!" ga_tracking_code: "Google 分析追踪代码(ga.js),例如:UA-12345678-9。参考 http://google.com/analytics" @@ -983,6 +983,7 @@ zh_CN: enable_cdn_js_debugging: "为包含的 js 启动跨源访问 /logs 权限以显示合适的错误。" show_create_topics_notice: "如果站点只有少于 5 篇的公开帖子时,显示一条请管理员创建帖子的提示。" delete_drafts_older_than_n_days: 删除超过 n 天得草稿。 + show_logout_in_header: "在顶栏的用户下拉菜单中显示登出" vacuum_db_days: "在数据库迁移后使用完整扫描回收数据库空间(设置 0 为禁用)" prevent_anons_from_downloading_files: "禁止匿名用户下载附件。警告:这将禁止他们访问任何发表在帖子中的非图片资源。" slug_generation_method: "选择一个链接生成方式。“encoded”将生成以百分号编码的链接。“none”将禁用自定义链接,只生成默认链接。" @@ -1120,7 +1121,10 @@ zh_CN: characters: "必须只包含字母、数字和下划线" unique: "必须是唯一的" blank: "必须存在" - must_begin_with_alphanumeric: "必须以字母或数字开头" + must_begin_with_alphanumeric: "必须以一个字母或数字或下划线开头" + must_end_with_alphanumeric: "必须以一个字母或数字结尾" + must_not_contain_two_special_chars_in_seq: "必须不包括连续的 2 个或更多的特殊字符(.-_)" + must_not_contain_confusing_suffix: "必须不包含奇怪的后缀,例如 .json 或 .png 等等。" email: not_allowed: "本站不允许使用该邮箱服务商提供的电子邮箱,请使用其它邮箱地址。" blocked: "不被允许。" @@ -1579,7 +1583,7 @@ zh_CN: unsubscribe_link: | 要取消订阅这些邮件,访问你的[用户设置](%{user_preferences_url})。 - 不想在接受这类主题的通知,[点击这儿](%{unsubscribe_url})。 + 不想再接受这类主题的通知,[点击这里](%{unsubscribe_url})。 subject_re: "回复:" subject_pm: "[私信]" user_notifications: @@ -1960,10 +1964,10 @@ zh_CN: 从你那收集的任何数据将以以下方式使用: - * 改进你的个人体验 — 你的信息帮助我们更好地满足你的个人需求。 - * 改进我们的站点 — 我们基于信息和我们从你那收到的反馈不断地试图改进我们的站点。 - * 改善我们的客户服务 — 你的信息帮助我们更有效地回应用户服务请求和支持。 - * 用于发送阶段性的邮件 — 你提供的邮件地址可能用于接受信息、你想看到的通知或与你用户名有关的回复和询问,或是其他的请求和问题。 + * 改进你的个人体验 — 你的信息帮助我们更好地满足你的个人需求。 + * 改进我们的站点 — 我们基于信息和我们从你那收到的反馈不断地试图改进我们的站点。 + * 改善我们的客户服务 — 你的信息帮助我们更有效地回应用户服务请求和支持。 + * 用于发送阶段性的邮件 — 你提供的邮件地址可能用于接受信息、你想看到的通知或与你用户名有关的回复和询问,或是其他的请求和问题。 <a name="protect"></a> @@ -1977,8 +1981,8 @@ zh_CN: 我们将善意地: - * 保存 90 天内的所有向服务器的包含 IP 地址的请求。 - * 保存 5 年内已注册用户和与他们的帖子有关的 IP 地址。 + * 保存 90 天内的所有向服务器的包含 IP 地址的请求。 + * 保存 5 年内已注册用户和与他们的帖子有关的 IP 地址。 <a name="cookies"></a> @@ -2004,8 +2008,7 @@ zh_CN: ## [儿童在线隐私保护法案合规](#coppa) - 我们的站点、产品和服务提供给 13 岁以上的人们。如果服务器位于美国,并且你小于 13 岁,根据 COPPA([儿童在线隐私保护法案合规 - ](http://en.wikipedia.org/wiki/Children)),不要使用这个站点。 + 我们的站点、产品和服务提供给 13 岁以上的人们。如果服务器位于美国,并且你小于 13 岁,根据 COPPA([儿童在线隐私保护法案合规](https://en.wikipedia.org/wiki/Children%27s_Online_Privacy_Protection_Act)),不要使用这个站点。 <a name="online"></a> diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 8d7e32ecf..14f16f475 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -294,7 +294,6 @@ zh_TW: please_continue: "繼續連接至 %{site_name}" error: "修改你的電郵位址時發生錯誤,可能此郵箱已有人使用了。" activation: - action: "啟用您的帳號" already_done: "抱歉,此帳號啟用連結已經失效。可能你的帳號已經啟用了。" please_continue: "你的新帳號已啟用;即將轉到主頁。" continue_button: "繼續連接至 %{site_name}" @@ -853,7 +852,6 @@ zh_TW: characters: "必須只包含字母和數位" unique: "必須是唯一的" blank: "必須存在" - must_begin_with_alphanumeric: "必須以字母或數位開頭" email: not_allowed: "本站不允許使用該郵箱服務商提供的電子郵箱,請使用其它郵箱位址。" blocked: "不被允許。" diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf index cd103e493..6e846f2a9 100644 --- a/config/nginx.sample.conf +++ b/config/nginx.sample.conf @@ -116,7 +116,7 @@ server { } # cache emojis - location ~ /_?emoji/ { + location ~ /_?emoji.*\.(png|gif|jpg|jpeg)$/ { expires 1y; add_header Cache-Control public; } diff --git a/config/routes.rb b/config/routes.rb index 3675a5e76..66125570f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,7 +7,8 @@ require_dependency "permalink_constraint" # This used to be User#username_format, but that causes a preload of the User object # and makes Guard not work properly. -USERNAME_ROUTE_FORMAT = /[A-Za-z0-9\_]+/ unless defined? USERNAME_ROUTE_FORMAT +USERNAME_ROUTE_FORMAT = /[A-Za-z0-9\_.\-]+/ unless defined? USERNAME_ROUTE_FORMAT + BACKUP_ROUTE_FORMAT = /[a-zA-Z0-9\-_]*\d{4}(-\d{2}){2}-\d{6}\.(tar\.gz|t?gz)/i unless defined? BACKUP_ROUTE_FORMAT Discourse::Application.routes.draw do @@ -69,6 +70,7 @@ Discourse::Application.routes.draw do get "groups/:type" => "groups#show", constraints: AdminConstraint.new get "groups/:type/:id" => "groups#show", constraints: AdminConstraint.new + get "users/:id.json" => 'users#show' , id: USERNAME_ROUTE_FORMAT, defaults: {format: 'json'} resources :users, id: USERNAME_ROUTE_FORMAT do collection do get "list/:query" => "users#index" @@ -261,6 +263,7 @@ Discourse::Application.routes.draw do get "users/:username/private-messages/:filter" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/messages" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/messages/:filter" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username.json" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}, defaults: {format: :json} get "users/:username" => "users#show", as: 'user', constraints: {username: USERNAME_ROUTE_FORMAT} put "users/:username" => "users#update", constraints: {username: USERNAME_ROUTE_FORMAT} put "users/:username/emails" => "users#check_emails", constraints: {username: USERNAME_ROUTE_FORMAT} @@ -292,11 +295,12 @@ Discourse::Application.routes.draw do get "users/by-external/:external_id" => "users#show", constraints: {external_id: /[^\/]+/} get "users/:username/flagged-posts" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/deleted-posts" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - get "user-badges/:username" => "user_badges#username" + get "user-badges/:username.json" => "user_badges#username", constraints: {username: USERNAME_ROUTE_FORMAT}, defaults: {format: :json} + get "user-badges/:username" => "user_badges#username", constraints: {username: USERNAME_ROUTE_FORMAT} - post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar" - get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/ } - get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/ } + post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", constraints: {username: USERNAME_ROUTE_FORMAT} + get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: USERNAME_ROUTE_FORMAT} + get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: USERNAME_ROUTE_FORMAT } get "highlight-js/:hostname/:version.js" => "highlight_js#show", format: false, constraints: { hostname: /[\w\.-]+/ } @@ -378,6 +382,7 @@ Discourse::Application.routes.draw do resources :categories, :except => :show post "category/:category_id/move" => "categories#move" + post "categories/reorder" => "categories#reorder" post "category/:category_id/notifications" => "categories#set_notifications" put "category/:category_id/slug" => "categories#update_slug" @@ -404,7 +409,7 @@ Discourse::Application.routes.draw do end Discourse.filters.each do |filter| - get "#{filter}" => "list##{filter}" + get "#{filter}" => "list##{filter}", constraints: { format: /(json|html)/ } get "c/:category/l/#{filter}" => "list#category_#{filter}", as: "category_#{filter}" get "c/:category/none/l/#{filter}" => "list#category_none_#{filter}", as: "category_none_#{filter}" get "c/:parent_category/:category/l/#{filter}" => "list#parent_category_category_#{filter}", as: "parent_category_category_#{filter}" diff --git a/config/site_settings.yml b/config/site_settings.yml index 950555724..cff228d7d 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -182,6 +182,9 @@ basic: enable_badges: client: true default: true + enable_whispers: + client: true + default: false login: invite_only: @@ -377,7 +380,9 @@ posting: min_body_similar_length: client: true default: 15 - enable_private_messages: true + enable_private_messages: + default: true + client: true ninja_edit_window: 300 post_edit_time_limit: 86400 edit_history_visible_to_public: @@ -415,13 +420,10 @@ posting: newuser_max_links: 2 newuser_max_images: client: true - default: 0 + default: 1 newuser_max_attachments: client: true default: 0 - uncategorized_category_id: - default: -1 - hidden: true post_excerpt_maxlength: 300 display_name_on_posts: client: true @@ -573,6 +575,14 @@ files: avatar_sizes: default: '20|25|32|45|60|120' type: list + external_system_avatars_enabled: + default: true + client: true + shadowed_by_global: true + external_system_avatars_url: + default: "https://avatars.discourse.org/letter/{first_letter}/{color}/{size}.png" + client: true + regex: '^https?:\/\/.+[^\/]' trust: default_trust_level: @@ -913,18 +923,25 @@ uncategorized: default: false hidden: true + # Nothing past this threshold is ever considered new + # this is calculated dynamically every 15 minutes + min_new_topics_time: + default: 0 + hidden: true + # Category IDs lounge_category_id: default: -1 hidden: true - meta_category_id: default: -1 hidden: true - staff_category_id: default: -1 hidden: true + uncategorized_category_id: + default: -1 + hidden: true performance_report_topic_id: default: -1 diff --git a/db/migrate/20150828155137_add_suppress_from_homepage_to_category.rb b/db/migrate/20150828155137_add_suppress_from_homepage_to_category.rb new file mode 100644 index 000000000..e706f40f9 --- /dev/null +++ b/db/migrate/20150828155137_add_suppress_from_homepage_to_category.rb @@ -0,0 +1,5 @@ +class AddSuppressFromHomepageToCategory < ActiveRecord::Migration + def change + add_column :categories, :suppress_from_homepage, :boolean, default: false + end +end diff --git a/docs/INSTALL-cloud.md b/docs/INSTALL-cloud.md index 4301163c9..88306633d 100644 --- a/docs/INSTALL-cloud.md +++ b/docs/INSTALL-cloud.md @@ -150,11 +150,13 @@ Do you want... * Users to log in *only* via your pre-existing website's registration system? [Configure Single-Sign-On](https://meta.discourse.org/t/official-single-sign-on-for-discourse/13045). -- Users to log in via Google? (new Oauth2 authentication) [Configure Google logins](https://meta.discourse.org/t/configuring-google-login-for-discourse/15858). +- Users to log in via Google? (new Oauth2 authentication) [Configure Google logins](https://meta.discourse.org/t/configuring-google-oauth2-login-for-discourse/15858). - Users to log in via Facebook? [Configure Facebook logins](https://meta.discourse.org/t/configuring-facebook-login-for-discourse/13394). -- Users to log in via Twitter? [Configure Twitter logins](https://meta.discourse.org/t/configuring-twitter-login-for-discourse/13395/last). +- Users to log in via Twitter? [Configure Twitter logins](https://meta.discourse.org/t/configuring-twitter-login-for-discourse/13395). + +- Users to log in via GitHub? [Configure GitHub logins](https://meta.discourse.org/t/configuring-github-login-for-discourse/13745) - Users to post replies via email? [Configure reply via email](https://meta.discourse.org/t/set-up-reply-via-email-support/14003). @@ -170,7 +172,7 @@ Do you want... - A firewall on your server? [Configure firewall](https://meta.discourse.org/t/configure-a-firewall-for-discourse/20584) -- To embed Discourse [in your WordPress install](https://github.com/discourse/wp-discourse), or [on your static HTML site](http://eviltrout.com/2014/01/22/embedding-discourse.html)? +- To embed Discourse [in your WordPress install](https://github.com/discourse/wp-discourse), or [on your static HTML site](https://meta.discourse.org/t/embedding-discourse-comments-via-javascript/31963)? If anything needs to be improved in this guide, feel free to ask on [meta.discourse.org][meta], or even better, submit a pull request. diff --git a/lib/avatar_lookup.rb b/lib/avatar_lookup.rb index b1fc1c616..4af184052 100644 --- a/lib/avatar_lookup.rb +++ b/lib/avatar_lookup.rb @@ -12,10 +12,7 @@ class AvatarLookup private def self.lookup_columns - @lookup_columns ||= [:id, - :email, - :username, - :uploaded_avatar_id] + @lookup_columns ||= %i{id email username uploaded_avatar_id} end def users diff --git a/lib/discourse.rb b/lib/discourse.rb index b32fcf736..82b1280ee 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -64,18 +64,10 @@ module Discourse @filters ||= [:latest, :unread, :new, :read, :posted, :bookmarks] end - def self.feed_filters - @feed_filters ||= [:latest] - end - def self.anonymous_filters @anonymous_filters ||= [:latest, :top, :categories] end - def self.logged_in_filters - @logged_in_filters ||= Discourse.filters - Discourse.anonymous_filters - end - def self.top_menu_items @top_menu_items ||= Discourse.filters + [:category, :categories, :top] end diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index e3f1a030b..5b03eff43 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -144,10 +144,13 @@ module PostGuardian end def can_see_post?(post) - post.present? && - (is_admin? || - ((is_moderator? || !post.deleted_at.present?) && - can_see_topic?(post.topic))) + return false if post.blank? + return true if is_admin? + return false unless can_see_topic?(post.topic) + return false unless post.user == @user || post.topic.visible_post_types(@user).include?(post.post_type) + return false if !is_moderator? && post.deleted_at.present? + + true end def can_view_edit_history?(post) diff --git a/lib/guardian/topic_guardian.rb b/lib/guardian/topic_guardian.rb index 6543208b5..8c03cc3e3 100644 --- a/lib/guardian/topic_guardian.rb +++ b/lib/guardian/topic_guardian.rb @@ -15,7 +15,7 @@ module TopicGuardian def can_create_topic_on_category?(category) can_create_topic?(nil) && - (!category || Category.topic_create_allowed(self).where(:id => category.id).count == 1) + (!category || Category.topic_create_allowed(self).where(id: category.id).count == 1) end def can_create_post_on_topic?(topic) diff --git a/lib/letter_avatar.rb b/lib/letter_avatar.rb index 4b51056b8..abde76ec0 100644 --- a/lib/letter_avatar.rb +++ b/lib/letter_avatar.rb @@ -45,6 +45,9 @@ class LetterAvatar fullsize = fullsize_path(identity) generate_fullsize(identity) if !cache || !File.exists?(fullsize) + # Optimizing here is dubious, it can save up to 2x for large images (eg 359px) + # BUT... we are talking 2400 bytes down to 1200 bytes, both fit in one packet + # The cost of this is huge, its a 40% perf hit OptimizedImage.resize(fullsize, filename, size, size) filename @@ -81,8 +84,7 @@ class LetterAvatar `convert #{instructions.join(" ")}` - ImageOptim.new.optimize_image!(filename) rescue nil - + ## do not optimize image, it will end up larger than original filename end diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index eef8d5ba2..d51adc4f1 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -78,7 +78,7 @@ class Plugin::Instance # Adds a class method to a class, respecting if plugin is enabled def add_class_method(klass, attr, &block) - klass = klass.to_s.classify.constantize + klass = klass.to_s.classify.constantize rescue klass.to_s.constantize hidden_method_name = :"#{attr}_without_enable_check" klass.send(:define_singleton_method, hidden_method_name, &block) @@ -90,7 +90,7 @@ class Plugin::Instance end def add_model_callback(klass, callback, &block) - klass = klass.to_s.classify.constantize + klass = klass.to_s.classify.constantize rescue klass.to_s.constantize plugin = self # generate a unique method name diff --git a/lib/search.rb b/lib/search.rb index 25840cd67..2dccf4514 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -103,6 +103,7 @@ class Search @limit = Search.per_facet term = process_advanced_search!(term) + if term.present? @term = Search.prepare_data(term.to_s) @original_term = PG::Connection.escape_string(@term) @@ -184,6 +185,15 @@ class Search posts.where("posts.post_number = 1") end + advanced_filter(/with_badge:(.*)/) do |posts,match| + badge_id = Badge.where('name ilike ? OR id = ?', match, match.to_i).pluck(:id).first + if badge_id + posts.where('posts.user_id IN (SELECT ub.user_id FROM user_badges ub WHERE ub.badge_id = ?)', badge_id) + else + posts.where("1 = 0") + end + end + advanced_filter(/in:(likes|bookmarks)/) do |posts, match| if @guardian.user post_action_type = PostActionType.types[:like] if match == "likes" @@ -223,6 +233,15 @@ class Search end end + advanced_filter(/group:(.+)/) do |posts,match| + group_id = Group.where('name ilike ? OR (id = ? AND id > 0)', match, match.to_i).pluck(:id).first + if group_id + posts.where("posts.user_id IN (select gu.user_id from group_users gu where gu.group_id = ?)", group_id) + else + posts.where("1 = 0") + end + end + advanced_filter(/user:(.+)/) do |posts,match| user_id = User.where('username_lower = ? OR id = ?', match.downcase, match.to_i).pluck(:id).first if user_id @@ -247,12 +266,14 @@ class Search def process_advanced_search!(term) - term.to_s.split(/\s+/).map do |word| + term.to_s.scan(/(([^" \t\n\x0B\f\r]+)?(("[^"]+")?))/).to_a.map do |(word,_)| + next if word.blank? found = false Search.advanced_filters.each do |matcher, block| - if word =~ matcher + cleaned = word.gsub(/["']/,"") + if cleaned =~ matcher (@filters ||= []) << [block, $1] found = true end @@ -273,6 +294,9 @@ class Search elsif word == 'order:views' @order = :views nil + elsif word == 'order:likes' + @order = :likes + nil elsif word == 'in:private' @search_pms = true nil @@ -440,6 +464,12 @@ class Search else posts = posts.order("topics.views DESC") end + elsif @order == :likes + if opts[:aggregate_search] + posts = posts.order("MAX(posts.like_count) DESC") + else + posts = posts.order("posts.like_count DESC") + end else posts = posts.order("TS_RANK_CD(TO_TSVECTOR(#{query_locale}, topics.title), #{ts_query}) DESC") diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 4b0671888..90be6860f 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -102,9 +102,7 @@ module SiteSettingExtension if new_choices = opts[:choices] - if String === new_choices - new_choices = eval(new_choices) - end + new_choices = eval(new_choices) if new_choices.is_a?(String) choices.has_key?(name) ? choices[name].concat(new_choices) : @@ -217,26 +215,20 @@ module SiteSettingExtension ensure_listen_for_changes old = current - new_hash = Hash[*(provider.all.map{ |s| - [s.name.intern, convert(s.value,s.data_type)] + new_hash = Hash[*(provider.all.map { |s| + [s.name.intern, convert(s.value, s.data_type, s.name)] }.to_a.flatten)] # add defaults, cause they are cached new_hash = defaults.merge(new_hash) # add shadowed - shadowed_settings.each do |ss| - new_hash[ss] = GlobalSetting.send(ss) - end + shadowed_settings.each { |ss| new_hash[ss] = GlobalSetting.send(ss) } changes, deletions = diff_hash(new_hash, old) - changes.each do |name, val| - current[name] = val - end - deletions.each do |name, val| - current[name] = defaults[name] - end + changes.each { |name, val| current[name] = val } + deletions.each { |name, val| current[name] = defaults[name] } clear_cache! end @@ -286,7 +278,7 @@ module SiteSettingExtension end def add_override!(name, val) - type = get_data_type(name, defaults[name]) + type = get_data_type(name, defaults[name.to_sym]) if type == types[:bool] && val != true && val != false val = (val == "t" || val == "true") ? 't' : 'f' @@ -301,7 +293,7 @@ module SiteSettingExtension end if type == types[:enum] - val = val.to_i if Fixnum === defaults[name.to_sym] + val = val.to_i if defaults[name.to_sym].is_a?(Fixnum) if enum_class(name) raise Discourse::InvalidParameters.new(:value) unless enum_class(name).valid_value?(val) else @@ -321,7 +313,7 @@ module SiteSettingExtension end provider.save(name, val, type) - current[name] = convert(val, type) + current[name] = convert(val, type, name) notify_clients!(name) if client_settings.include? name clear_cache! end @@ -401,8 +393,8 @@ module SiteSettingExtension def get_data_type(name, val) return types[:null] if val.nil? - # Some types are just for validations like email. Only consider - # it valid if includes in `types` + # Some types are just for validations like email. + # Only consider it valid if includes in `types` if static_type = static_types[name.to_sym] return types[static_type] if types.keys.include?(static_type) end @@ -421,7 +413,7 @@ module SiteSettingExtension end end - def convert(value, type) + def convert(value, type, name) case type when types[:float] value.to_f @@ -431,9 +423,10 @@ module SiteSettingExtension value == true || value == "t" || value == "true" when types[:null] nil + when types[:enum] + defaults[name.to_sym].is_a?(Fixnum) ? value.to_i : value else return value if types[type] - # Otherwise it's a type error raise ArgumentError.new :type end diff --git a/lib/site_settings/db_provider.rb b/lib/site_settings/db_provider.rb index 5b4d79e12..7757299fb 100644 --- a/lib/site_settings/db_provider.rb +++ b/lib/site_settings/db_provider.rb @@ -13,29 +13,23 @@ class SiteSettings::DbProvider def all return [] unless table_exists? - # note, not leaking out AR records, cause I want all editing to happen - # via this API - SqlBuilder.new("select name, data_type, value from #{@model.table_name}").map_exec(OpenStruct) + # Not leaking out AR records, cause I want all editing to happen via this API + SqlBuilder.new("SELECT name, data_type, value FROM #{@model.table_name}").map_exec(OpenStruct) end def find(name) return nil unless table_exists? - # note, not leaking out AR records, cause I want all editing to happen - # via this API - SqlBuilder.new("select name, data_type, value from #{@model.table_name} where name = :name") + # Not leaking out AR records, cause I want all editing to happen via this API + SqlBuilder.new("SELECT name, data_type, value FROM #{@model.table_name} WHERE name = :name") .map_exec(OpenStruct, name: name) .first end def save(name, value, data_type) - return unless table_exists? - model = @model.find_by({ - name: name - }) - + model = @model.find_by(name: name) model ||= @model.new model.name = name diff --git a/lib/site_settings/local_process_provider.rb b/lib/site_settings/local_process_provider.rb index 1e3fa609a..a94515054 100644 --- a/lib/site_settings/local_process_provider.rb +++ b/lib/site_settings/local_process_provider.rb @@ -24,7 +24,10 @@ class SiteSettings::LocalProcessProvider end def save(name, value, data_type) - settings[name] = Setting.new(name,value, data_type) + # NOTE: convert to string to simulate the conversion that is happening + # when using DbProvider + value = value.to_s + settings[name] = Setting.new(name, value, data_type) end def destroy(name) diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 9ffa5d8b5..25e2b6fd4 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -1,15 +1,16 @@ # -# Helps us find topics. Returns a TopicList object containing the topics -# found. +# Helps us find topics. +# Returns a TopicList object containing the topics found. # + require_dependency 'topic_list' require_dependency 'suggested_topics_builder' require_dependency 'topic_query_sql' class TopicQuery # Could be rewritten to %i if Ruby 1.9 is no longer supported - VALID_OPTIONS = %w(except_topic_ids - exclude_category + VALID_OPTIONS = %i(except_topic_ids + exclude_category_ids limit page per_page @@ -27,8 +28,7 @@ class TopicQuery search slow_platform filter - q - ).map(&:to_sym) + q) # Maps `order` to a columns in `topics` SORTABLE_MAPPING = { @@ -301,14 +301,17 @@ class TopicQuery if options[:no_subcategories] result = result.where('categories.id = ?', category_id) else - result = result.where('categories.id = ? or (categories.parent_category_id = ? AND categories.topic_id <> topics.id)', category_id, category_id) + result = result.where('categories.id = :category_id OR (categories.parent_category_id = :category_id AND categories.topic_id <> topics.id)', category_id: category_id) end result = result.references(:categories) end result = apply_ordering(result, options) result = result.listable_topics.includes(:category) - result = result.where('categories.name is null or categories.name <> ?', options[:exclude_category]).references(:categories) if options[:exclude_category] + + if options[:exclude_category_ids] && options[:exclude_category_ids].is_a?(Array) && options[:exclude_category_ids].size > 0 + result = result.where("categories.id NOT IN (?)", options[:exclude_category_ids]).references(:categories) + end # Don't include the category topics if excluded if options[:no_definitions] @@ -393,19 +396,20 @@ class TopicQuery def remove_muted_categories(list, user, opts=nil) category_id = get_category_id(opts[:exclude]) if opts + if user - list = list.where("NOT EXISTS( - SELECT 1 FROM category_users cu - WHERE cu.user_id = ? AND - cu.category_id = topics.category_id AND - cu.notification_level = ? AND - cu.category_id <> ? - )", - user.id, - CategoryUser.notification_levels[:muted], - category_id || -1 - ) - .references('cu') + list = list.references("cu") + .where(" + NOT EXISTS ( + SELECT 1 + FROM category_users cu + WHERE cu.user_id = :user_id + AND cu.category_id = topics.category_id + AND cu.notification_level = :muted + AND cu.category_id <> :category_id + )", user_id: user.id, + muted: CategoryUser.notification_levels[:muted], + category_id: category_id || -1) end list diff --git a/lib/topic_view.rb b/lib/topic_view.rb index 54483efd5..52222a6d3 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -191,11 +191,9 @@ class TopicView # Find the sort order for a post in the topic def sort_order_for_post_number(post_number) - Post.where(topic_id: @topic.id, post_number: post_number) - .with_deleted - .select(:sort_order) - .first - .try(:sort_order) + posts = Post.where(topic_id: @topic.id, post_number: post_number).with_deleted + posts = filter_post_types(posts) + posts.select(:sort_order).first.try(:sort_order) end # Filter to all posts near a particular post number @@ -332,11 +330,22 @@ class TopicView private + def filter_post_types(posts) + visible_types = @topic.visible_post_types(@user) + + if @user.present? + posts.where("user_id = ? OR post_type IN (?)", @user.id, visible_types) + else + posts.where(post_type: visible_types) + end + end + def filter_posts_by_ids(post_ids) # TODO: Sort might be off @posts = Post.where(id: post_ids, topic_id: @topic.id) .includes(:user, :reply_to_user) .order('sort_order') + @posts = filter_post_types(@posts) @posts = @posts.with_deleted if @guardian.can_see_deleted_posts? @posts end @@ -355,13 +364,13 @@ class TopicView end def find_topic(topic_id) - finder = Topic.where(id: topic_id).includes(:category) - finder = finder.with_deleted if @guardian.can_see_deleted_topics? + # with_deleted covered in #check_and_raise_exceptions + finder = Topic.with_deleted.where(id: topic_id).includes(:category) finder.first end def unfiltered_posts - result = @topic.posts + result = filter_post_types(@topic.posts) result = result.with_deleted if @guardian.can_see_deleted_posts? result = @topic.posts.where("user_id IS NOT NULL") if @exclude_deleted_users result diff --git a/lib/user_name_suggester.rb b/lib/user_name_suggester.rb index 09e4db698..4819a3f87 100644 --- a/lib/user_name_suggester.rb +++ b/lib/user_name_suggester.rb @@ -38,6 +38,7 @@ module UserNameSuggester name = name.gsub(/^[^[:alnum:]]+|\W+$/, "") .gsub(/\W+/, "_") .gsub(/^\_+/, '') + .gsub(/[\-_\.]{2,}/, "_") name end diff --git a/lib/version.rb b/lib/version.rb index aadc4d223..f9256e88f 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -5,7 +5,7 @@ module Discourse MAJOR = 1 MINOR = 4 TINY = 0 - PRE = 'beta10' + PRE = 'beta11' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/plugins/poll/assets/javascripts/poll_dialect.js b/plugins/poll/assets/javascripts/poll_dialect.js index 70771e415..bc9b6585e 100644 --- a/plugins/poll/assets/javascripts/poll_dialect.js +++ b/plugins/poll/assets/javascripts/poll_dialect.js @@ -36,6 +36,9 @@ } } + // Disable dialect when poll plugin is disabled + if (!Discourse.SiteSettings.poll_enabled) { return ["div"].concat(contents); } + // default poll attributes var attributes = { "class": "poll" }; attributes[DATA_PREFIX + "status"] = "open"; diff --git a/plugins/poll/config/settings.yml b/plugins/poll/config/settings.yml index f75945a5d..2d01ba6b8 100644 --- a/plugins/poll/config/settings.yml +++ b/plugins/poll/config/settings.yml @@ -1,6 +1,7 @@ plugins: poll_enabled: default: true + client: true poll_maximum_options: default: 20 client: true diff --git a/plugins/poll/plugin.rb b/plugins/poll/plugin.rb index 34e71188b..0f061e5b5 100644 --- a/plugins/poll/plugin.rb +++ b/plugins/poll/plugin.rb @@ -17,6 +17,9 @@ PLUGIN_NAME ||= "discourse_poll".freeze POLLS_CUSTOM_FIELD ||= "polls".freeze VOTES_CUSTOM_FIELD ||= "polls-votes".freeze +DATA_PREFIX ||= "data-poll-".freeze +DEFAULT_POLL_NAME ||= "poll".freeze + after_initialize do # remove "Vote Now!" & "Show Results" links in emails @@ -157,6 +160,7 @@ after_initialize do end require_dependency "application_controller" + class DiscoursePoll::PollsController < ::ApplicationController requires_plugin PLUGIN_NAME @@ -217,9 +221,6 @@ after_initialize do end end - DATA_PREFIX ||= "data-poll-".freeze - DEFAULT_POLL_NAME ||= "poll".freeze - validate(:post, :validate_polls) do # only care when raw has changed! return unless self.raw_changed? @@ -285,9 +286,12 @@ after_initialize do # load previous polls previous_polls = post.custom_fields[POLLS_CUSTOM_FIELD] || {} + # extract options + current_options = polls.values.map { |p| p["options"].map { |o| o["id"] } }.flatten.sort + previous_options = previous_polls.values.map { |p| p["options"].map { |o| o["id"] } }.flatten.sort + # are the polls different? - if polls.keys != previous_polls.keys || - polls.values.map { |p| p["options"] } != previous_polls.values.map { |p| p["options"] } + if polls.keys != previous_polls.keys || current_options != previous_options # outside of the 5-minute edit window? if post.created_at < 5.minutes.ago diff --git a/plugins/poll/spec/controllers/posts_controller_spec.rb b/plugins/poll/spec/controllers/posts_controller_spec.rb index a647be132..a47d7def2 100644 --- a/plugins/poll/spec/controllers/posts_controller_spec.rb +++ b/plugins/poll/spec/controllers/posts_controller_spec.rb @@ -10,7 +10,6 @@ describe PostsController do describe "polls" do - it "works" do xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]" } expect(response).to be_success @@ -91,7 +90,7 @@ describe PostsController do describe "within the first 5 minutes" do let(:post_id) do - Timecop.freeze(3.minutes.ago) do + Timecop.freeze(4.minutes.ago) do xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]" } ::JSON.parse(response.body)["id"] end @@ -116,30 +115,54 @@ describe PostsController do describe "after the first 5 minutes" do + let(:poll) { "[poll]\n- A\n- B[/poll]" } + let(:new_option) { "[poll]\n- A\n- C[/poll]" } + let(:updated) { "before\n\n[poll]\n- A\n- B[/poll]\n\nafter" } + let(:post_id) do Timecop.freeze(6.minutes.ago) do - xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]" } + xhr :post, :create, { title: title, raw: poll } ::JSON.parse(response.body)["id"] end end - let(:new_raw) { "[poll]\n- A\n- C[/poll]" } - - it "cannot be changed by OP" do - xhr :put, :update, { id: post_id, post: { raw: new_raw } } + it "OP cannot change the options" do + xhr :put, :update, { id: post_id, post: { raw: new_option } } expect(response).not_to be_success json = ::JSON.parse(response.body) expect(json["errors"][0]).to eq(I18n.t("poll.op_cannot_edit_options_after_5_minutes")) end - it "can be edited by staff" do + it "staff can change the options" do log_in_user(Fabricate(:moderator)) - xhr :put, :update, { id: post_id, post: { raw: new_raw } } + xhr :put, :update, { id: post_id, post: { raw: new_option } } expect(response).to be_success json = ::JSON.parse(response.body) expect(json["post"]["polls"]["poll"]["options"][1]["html"]).to eq("C") end + it "support changes on the post" do + xhr :put, :update, { id: post_id, post: { raw: updated } } + expect(response).to be_success + json = ::JSON.parse(response.body) + expect(json["post"]["cooked"]).to match("before") + end + + describe "with at least one vote" do + + before do + DiscoursePoll::Poll.vote(post_id, "poll", ["5c24fc1df56d764b550ceae1b9319125"], user.id) + end + + it "support changes on the post" do + xhr :put, :update, { id: post_id, post: { raw: updated } } + expect(response).to be_success + json = ::JSON.parse(response.body) + expect(json["post"]["cooked"]).to match("before") + end + + end + end end diff --git a/public/503.tr_TR.html b/public/503.tr_TR.html index f9c2f8028..a8e4090cb 100644 --- a/public/503.tr_TR.html +++ b/public/503.tr_TR.html @@ -1,6 +1,6 @@ <html> <head> -<title>Site Bakıma Alınıyor - Discourse.org</title> +<title>Site Bakım Modunda - Discourse.org</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> </head> <body> diff --git a/spec/components/email/styles_spec.rb b/spec/components/email/styles_spec.rb index a4f8a5dde..3a190e9cc 100644 --- a/spec/components/email/styles_spec.rb +++ b/spec/components/email/styles_spec.rb @@ -66,12 +66,12 @@ describe Email::Styles do end it "attaches a style to a tags" do - frag = html_fragment("<a href='#'>wat</a>") + frag = html_fragment("<a href>wat</a>") expect(frag.at('a')['style']).to be_present end it "attaches a style to a tags" do - frag = html_fragment("<a href='#'>wat</a>") + frag = html_fragment("<a href>wat</a>") expect(frag.at('a')['style']).to be_present end diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index 872766649..66430c8f8 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -437,6 +437,32 @@ describe Guardian do expect(Guardian.new(user).can_see?(post)).to be_falsey expect(Guardian.new(admin).can_see?(post)).to be_truthy end + + it 'respects whispers' do + regular_post = Fabricate.build(:post) + whisper_post = Fabricate.build(:post, post_type: Post.types[:whisper]) + + anon_guardian = Guardian.new + expect(anon_guardian.can_see?(regular_post)).to eq(true) + expect(anon_guardian.can_see?(whisper_post)).to eq(false) + + regular_user = Fabricate.build(:user) + regular_guardian = Guardian.new(regular_user) + expect(regular_guardian.can_see?(regular_post)).to eq(true) + expect(regular_guardian.can_see?(whisper_post)).to eq(false) + + # can see your own whispers + regular_whisper = Fabricate.build(:post, post_type: Post.types[:whisper], user: regular_user) + expect(regular_guardian.can_see?(regular_whisper)).to eq(true) + + mod_guardian = Guardian.new(Fabricate.build(:moderator)) + expect(mod_guardian.can_see?(regular_post)).to eq(true) + expect(mod_guardian.can_see?(whisper_post)).to eq(true) + + admin_guardian = Guardian.new(Fabricate.build(:admin)) + expect(admin_guardian.can_see?(regular_post)).to eq(true) + expect(admin_guardian.can_see?(whisper_post)).to eq(true) + end end describe 'a PostRevision' do diff --git a/spec/components/onebox/engine/discourse_local_onebox_spec.rb b/spec/components/onebox/engine/discourse_local_onebox_spec.rb index 78c7bcb09..4ca2d8eaf 100644 --- a/spec/components/onebox/engine/discourse_local_onebox_spec.rb +++ b/spec/components/onebox/engine/discourse_local_onebox_spec.rb @@ -61,6 +61,7 @@ describe Onebox::Engine::DiscourseLocalOnebox do end it "returns some onebox goodness if post exists and can be seen" do + SiteSetting.external_system_avatars_enabled = false url = "#{topic.url}" Guardian.any_instance.stubs(:can_see?).returns(true) html = Onebox.preview(url).to_s diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 955663e4d..0595a66e0 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -75,6 +75,7 @@ describe PostCreator do DiscourseEvent.expects(:trigger).with(:post_created, anything, anything, user).once DiscourseEvent.expects(:trigger).with(:after_validate_topic, anything, anything).once DiscourseEvent.expects(:trigger).with(:before_create_topic, anything, anything).once + DiscourseEvent.expects(:trigger).with(:after_trigger_post_process, anything).once creator.create end diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index a74bffee1..219ca398a 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -391,6 +391,28 @@ describe Search do expect(Search.execute("user:#{_post.user.username}").posts.length).to eq(1) end + it 'supports group' do + topic = Fabricate(:topic, created_at: 3.months.ago) + post = Fabricate(:post, raw: 'hi this is a test 123 123', topic: topic) + + group = Group.create!(name: "Like_a_Boss") + GroupUser.create!(user_id: post.user_id, group_id: group.id) + + expect(Search.execute('group:like_a_boss').posts.length).to eq(1) + expect(Search.execute('group:"like a brick"').posts.length).to eq(0) + end + + it 'supports with_badge' do + + topic = Fabricate(:topic, created_at: 3.months.ago) + post = Fabricate(:post, raw: 'hi this is a test 123 123', topic: topic) + + badge = Badge.create!(name: "Like a Boss", badge_type_id: 1) + UserBadge.create!(user_id: post.user_id, badge_id: badge.id, granted_at: 1.minute.ago, granted_by_id: -1) + + expect(Search.execute('with_badge:"like a boss"').posts.length).to eq(1) + end + it 'can search numbers correctly, and match exact phrases' do topic = Fabricate(:topic, created_at: 3.months.ago) Fabricate(:post, raw: '3.0 eta is in 2 days horrah', topic: topic) diff --git a/spec/components/site_setting_extension_spec.rb b/spec/components/site_setting_extension_spec.rb index 0f938a6af..82afd880c 100644 --- a/spec/components/site_setting_extension_spec.rb +++ b/spec/components/site_setting_extension_spec.rb @@ -3,24 +3,23 @@ require_dependency 'site_setting_extension' require_dependency 'site_settings/local_process_provider' describe SiteSettingExtension do - let :provider do + let :provider_local do SiteSettings::LocalProcessProvider.new end def new_settings(provider) - c = Class.new do + Class.new do extend SiteSettingExtension self.provider = provider end - c end let :settings do - new_settings(provider) + new_settings(provider_local) end let :settings2 do - new_settings(provider) + new_settings(provider_local) end describe "refresh!" do @@ -232,23 +231,13 @@ describe SiteSettingExtension do def self.values [1,2,3] end - def self.translate_names? - true - end - end - - let :test_enum_class do - TestEnumClass - end - - before do - settings.setting(:test_enum, 1, enum: TestEnumClass) - settings.refresh! end it 'should coerce correctly' do - settings.test_enum = "2" - expect(settings.test_enum).to eq(2) + settings.setting(:test_int_enum, 1, enum: TestIntEnumClass) + settings.test_int_enum = "2" + settings.refresh! + expect(settings.test_int_enum).to eq(2) end end diff --git a/spec/components/topic_view_spec.rb b/spec/components/topic_view_spec.rb index dedc080b9..ad23e0ade 100644 --- a/spec/components/topic_view_spec.rb +++ b/spec/components/topic_view_spec.rb @@ -13,6 +13,7 @@ describe TopicView do expect { TopicView.new(1231232, coding_horror) }.to raise_error(Discourse::NotFound) end + # see also spec/controllers/topics_controller_spec.rb TopicsController::show::permission errors it "raises an error if the user can't see the topic" do Guardian.any_instance.expects(:can_see?).with(topic).returns(false) expect { topic_view }.to raise_error(Discourse::InvalidAccess) @@ -21,7 +22,7 @@ describe TopicView do it "handles deleted topics" do admin = Fabricate(:admin) topic.trash!(admin) - expect { TopicView.new(topic.id, Fabricate(:user)) }.to raise_error(Discourse::NotFound) + expect { TopicView.new(topic.id, Fabricate(:user)) }.to raise_error(Discourse::InvalidAccess) expect { TopicView.new(topic.id, admin) }.not_to raise_error end @@ -251,6 +252,23 @@ describe TopicView do end + context 'whispers' do + it "handles their visibility properly" do + p1 = Fabricate(:post, topic: topic, user: coding_horror) + p2 = Fabricate(:post, topic: topic, user: coding_horror, post_type: Post.types[:whisper]) + p3 = Fabricate(:post, topic: topic, user: coding_horror) + + ch_posts = TopicView.new(topic.id, coding_horror).posts + expect(ch_posts.map(&:id)).to eq([p1.id, p2.id, p3.id]) + + anon_posts = TopicView.new(topic.id).posts + expect(anon_posts.map(&:id)).to eq([p1.id, p3.id]) + + admin_posts = TopicView.new(topic.id, Fabricate(:moderator)).posts + expect(admin_posts.map(&:id)).to eq([p1.id, p2.id, p3.id]) + end + end + context '.posts' do # Create the posts in a different order than the sort_order diff --git a/spec/components/user_name_suggester_spec.rb b/spec/components/user_name_suggester_spec.rb index 8f63ba1b2..ec9eb7a46 100644 --- a/spec/components/user_name_suggester_spec.rb +++ b/spec/components/user_name_suggester_spec.rb @@ -75,6 +75,11 @@ describe UserNameSuggester do expect(UserNameSuggester.suggest("myname.")).to eq('myname') end + it 'handles usernames with a sequence of 2 or more special chars' do + expect(UserNameSuggester.suggest('Darth__Vader')).to eq('Darth_Vader') + expect(UserNameSuggester.suggest('Darth_-_Vader')).to eq('Darth_Vader') + end + it 'should handle typical facebook usernames' do expect(UserNameSuggester.suggest('roger.nelson.3344913')).to eq('roger_nelson_33') end diff --git a/spec/controllers/categories_controller_spec.rb b/spec/controllers/categories_controller_spec.rb index e838b2e97..f03cfd05e 100644 --- a/spec/controllers/categories_controller_spec.rb +++ b/spec/controllers/categories_controller_spec.rb @@ -95,6 +95,42 @@ describe CategoriesController do end + describe "reorder" do + it "reorders the categories" do + admin = log_in(:admin) + + c1 = Fabricate(:category) + c2 = Fabricate(:category) + c3 = Fabricate(:category) + c4 = Fabricate(:category) + if c3.id < c2.id + tmp = c3; c2 = c3; c3 = tmp; + end + c1.position = 8 + c2.position = 6 + c3.position = 7 + c4.position = 5 + + payload = {} + payload[c1.id] = 4 + payload[c2.id] = 6 + payload[c3.id] = 6 + payload[c4.id] = 5 + + xhr :post, :reorder, mapping: MultiJson.dump(payload) + + SiteSetting.fixed_category_positions = true + list = CategoryList.new(Guardian.new(admin)) + expect(list.categories).to eq([ + Category.find(SiteSetting.uncategorized_category_id), + c1, + c4, + c2, + c3 + ]) + end + end + describe "update" do it "requires the user to be logged in" do diff --git a/spec/controllers/list_controller_spec.rb b/spec/controllers/list_controller_spec.rb index 6f55de5b6..41f343ca1 100644 --- a/spec/controllers/list_controller_spec.rb +++ b/spec/controllers/list_controller_spec.rb @@ -32,12 +32,6 @@ describe ListController do end end - Discourse.logged_in_filters.each do |filter| - context "#{filter}" do - it { expect { xhr :get, filter }.to raise_error(Discourse::NotLoggedIn) } - end - end - it 'allows users to filter on a set of topic ids' do p = create_post @@ -51,14 +45,10 @@ describe ListController do describe 'RSS feeds' do - Discourse.feed_filters.each do |filter| - - it 'renders RSS' do - get "#{filter}_feed", format: :rss - expect(response).to be_success - expect(response.content_type).to eq('application/rss+xml') - end - + it 'renders RSS' do + get "latest_feed", format: :rss + expect(response).to be_success + expect(response.content_type).to eq('application/rss+xml') end end diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index fd927ebac..35640b631 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -1,5 +1,23 @@ require 'spec_helper' +def topics_controller_show_gen_perm_tests(expected, ctx) + expected.each do |sym, status| + params = "topic_id: #{sym}.id, slug: #{sym}.slug" + if sym == :nonexist + params = "topic_id: nonexist_topic_id" + end + ctx.instance_eval(" +it 'returns #{status} for #{sym}' do + begin + xhr :get, :show, #{params} + expect(response.status).to eq(#{status}) + rescue Discourse::NotLoggedIn + expect(302).to eq(#{status}) + end +end") + end +end + describe TopicsController do context 'wordpress' do @@ -9,6 +27,8 @@ describe TopicsController do let!(:p2) { Fabricate(:post, topic: topic, user:user )} it "returns the JSON in the format our wordpress plugin needs" do + SiteSetting.external_system_avatars_enabled = false + xhr :get, :wordpress, topic_id: topic.id, best: 3 expect(response).to be_success json = ::JSON.parse(response.body) @@ -554,6 +574,108 @@ describe TopicsController do end end + context 'permission errors' do + let(:allowed_user) { Fabricate(:user) } + let(:allowed_group) { Fabricate(:group) } + let(:secure_category) { + c = Fabricate(:category) + c.permissions = [[allowed_group, :full]] + c.save + allowed_user.groups = [allowed_group] + allowed_user.save + c } + let(:normal_topic) { Fabricate(:topic) } + let(:secure_topic) { Fabricate(:topic, category: secure_category) } + let(:private_topic) { Fabricate(:private_message_topic, user: allowed_user) } + let(:deleted_topic) { Fabricate(:deleted_topic) } + let(:nonexist_topic_id) { Topic.last.id + 10000 } + + context 'anonymous' do + expected = { + :normal_topic => 200, + :secure_topic => 403, + :private_topic => 302, + :deleted_topic => 403, + :nonexist => 404 + } + topics_controller_show_gen_perm_tests(expected, self) + end + + context 'anonymous with login required' do + before do + SiteSetting.login_required = true + end + expected = { + :normal_topic => 302, + :secure_topic => 302, + :private_topic => 302, + :deleted_topic => 302, + :nonexist => 302 + } + topics_controller_show_gen_perm_tests(expected, self) + end + + context 'normal user' do + before do + log_in(:user) + end + + expected = { + :normal_topic => 200, + :secure_topic => 403, + :private_topic => 403, + :deleted_topic => 403, + :nonexist => 404 + } + topics_controller_show_gen_perm_tests(expected, self) + end + + context 'allowed user' do + before do + log_in_user(allowed_user) + end + + expected = { + :normal_topic => 200, + :secure_topic => 200, + :private_topic => 200, + :deleted_topic => 403, + :nonexist => 404 + } + topics_controller_show_gen_perm_tests(expected, self) + end + + context 'moderator' do + before do + log_in(:moderator) + end + + expected = { + :normal_topic => 200, + :secure_topic => 403, + :private_topic => 403, + :deleted_topic => 200, + :nonexist => 404 + } + topics_controller_show_gen_perm_tests(expected, self) + end + + context 'admin' do + before do + log_in(:admin) + end + + expected = { + :normal_topic => 200, + :secure_topic => 200, + :private_topic => 200, + :deleted_topic => 200, + :nonexist => 404 + } + topics_controller_show_gen_perm_tests(expected, self) + end + end + it 'records a view' do expect { xhr :get, :show, topic_id: topic.id, slug: topic.slug }.to change(TopicViewItem, :count).by(1) end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 32544497d..884f3f3ff 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -1301,7 +1301,7 @@ describe UsersController do describe '.pick_avatar' do it 'raises an error when not logged in' do - expect { xhr :put, :pick_avatar, username: 'asdf', avatar_id: 1}.to raise_error(Discourse::NotLoggedIn) + expect { xhr :put, :pick_avatar, username: 'asdf', avatar_id: 1, type: "custom"}.to raise_error(Discourse::NotLoggedIn) end context 'while logged in' do @@ -1310,12 +1310,12 @@ describe UsersController do it 'raises an error when you don\'t have permission to toggle the avatar' do another_user = Fabricate(:user) - xhr :put, :pick_avatar, username: another_user.username, upload_id: 1 + xhr :put, :pick_avatar, username: another_user.username, upload_id: 1, type: "custom" expect(response).to be_forbidden end it 'it successful' do - xhr :put, :pick_avatar, username: user.username, upload_id: 111 + xhr :put, :pick_avatar, username: user.username, upload_id: 111, type: "custom" expect(user.reload.uploaded_avatar_id).to eq(111) expect(user.user_avatar.reload.custom_upload_id).to eq(111) expect(response).to be_success @@ -1326,13 +1326,6 @@ describe UsersController do expect(response).to be_success end - it 'returns success' do - xhr :put, :pick_avatar, username: user.username, upload_id: 111 - expect(user.reload.uploaded_avatar_id).to eq(111) - expect(response).to be_success - json = ::JSON.parse(response.body) - expect(json['success']).to eq("OK") - end end end diff --git a/spec/jobs/enqueue_digest_emails_spec.rb b/spec/jobs/enqueue_digest_emails_spec.rb index 459e08705..d3f6b354d 100644 --- a/spec/jobs/enqueue_digest_emails_spec.rb +++ b/spec/jobs/enqueue_digest_emails_spec.rb @@ -6,7 +6,7 @@ describe Jobs::EnqueueDigestEmails do describe '#target_users' do context 'disabled digests' do - before { SiteSetting.stubs(:default_email_digest_frequency).returns("") } + before { SiteSetting.stubs(:default_email_digest_frequency).returns(0) } let!(:user_no_digests) { Fabricate(:active_user, last_emailed_at: 8.days.ago, last_seen_at: 10.days.ago) } it "doesn't return users with email disabled" do diff --git a/spec/models/admin_dashboard_data_spec.rb b/spec/models/admin_dashboard_data_spec.rb index 30dd8ec2f..36a029b47 100644 --- a/spec/models/admin_dashboard_data_spec.rb +++ b/spec/models/admin_dashboard_data_spec.rb @@ -2,6 +2,36 @@ require 'spec_helper' describe AdminDashboardData do + describe "adding new checks" do + after do + AdminDashboardData.reset_problem_checks + end + + it 'calls the passed block' do + called = false + AdminDashboardData.add_problem_check do + called = true + end + + AdminDashboardData.fetch_problems + expect(called).to eq(true) + end + + it 'calls the passed method' do + $test_AdminDashboardData_global = false + class AdminDashboardData + def my_test_method + $test_AdminDashboardData_global = true + end + end + AdminDashboardData.add_problem_check :my_test_method + + AdminDashboardData.fetch_problems + expect($test_AdminDashboardData_global).to eq(true) + $test_AdminDashboardData_global = nil + end + end + describe "rails_env_check" do subject { described_class.new.rails_env_check } diff --git a/spec/models/category_user_spec.rb b/spec/models/category_user_spec.rb index 349271399..2365f611b 100644 --- a/spec/models/category_user_spec.rb +++ b/spec/models/category_user_spec.rb @@ -80,6 +80,19 @@ describe CategoryUser do expect(TopicUser.get(post.topic, user)).to be_blank end + it "is destroyed when a user is deleted" do + user = Fabricate(:user) + category = Fabricate(:category) + + CategoryUser.create!(user: user, category: category, notification_level: CategoryUser.notification_levels[:watching]) + + expect(CategoryUser.where(user_id: user.id).count).to eq(1) + + user.destroy! + + expect(CategoryUser.where(user_id: user.id).count).to eq(0) + end + end end diff --git a/spec/models/digest_email_site_setting_spec.rb b/spec/models/digest_email_site_setting_spec.rb index cc5f39fa8..dc07f45db 100644 --- a/spec/models/digest_email_site_setting_spec.rb +++ b/spec/models/digest_email_site_setting_spec.rb @@ -11,7 +11,6 @@ describe DigestEmailSiteSetting do end it 'returns false for an invalid value' do - expect(DigestEmailSiteSetting.valid_value?(1.5)).to eq false expect(DigestEmailSiteSetting.valid_value?('7 dogs')).to eq false end end diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index 7471c5cd7..b65ec208c 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -11,6 +11,40 @@ describe Topic do it { is_expected.to rate_limit } + context '#visible_post_types' do + let(:types) { Post.types } + + it "returns the appropriate types for anonymous users" do + topic = Fabricate.build(:topic) + post_types = topic.visible_post_types + + expect(post_types).to include(types[:regular]) + expect(post_types).to include(types[:moderator_action]) + expect(post_types).to include(types[:small_action]) + expect(post_types).to_not include(types[:whisper]) + end + + it "returns the appropriate types for regular users" do + topic = Fabricate.build(:topic) + post_types = topic.visible_post_types(Fabricate.build(:user)) + + expect(post_types).to include(types[:regular]) + expect(post_types).to include(types[:moderator_action]) + expect(post_types).to include(types[:small_action]) + expect(post_types).to_not include(types[:whisper]) + end + + it "returns the appropriate types for staff users" do + topic = Fabricate.build(:topic) + post_types = topic.visible_post_types(Fabricate.build(:moderator)) + + expect(post_types).to include(types[:regular]) + expect(post_types).to include(types[:moderator_action]) + expect(post_types).to include(types[:small_action]) + expect(post_types).to include(types[:whisper]) + end + end + context 'slug' do let(:title) { "hello world topic" } let(:slug) { "hello-world-topic" } diff --git a/spec/models/topic_tracking_state_spec.rb b/spec/models/topic_tracking_state_spec.rb index 2d857e320..de8e3ff6b 100644 --- a/spec/models/topic_tracking_state_spec.rb +++ b/spec/models/topic_tracking_state_spec.rb @@ -39,6 +39,50 @@ describe TopicTrackingState do expect(report.length).to eq(1) end + + it "correctly handles capping" do + $redis.del TopicUser.unread_cap_key + + user = Fabricate(:user) + + post1 = create_post + Fabricate(:post, topic: post1.topic) + + post2 = create_post + Fabricate(:post, topic: post2.topic) + + post3 = create_post + Fabricate(:post, topic: post3.topic) + + tracking = { + notification_level: TopicUser.notification_levels[:tracking], + last_read_post_number: 1, + highest_seen_post_number: 1 + } + + TopicUser.change(user.id, post1.topic_id, tracking) + TopicUser.change(user.id, post2.topic_id, tracking) + TopicUser.change(user.id, post3.topic_id, tracking) + + report = TopicTrackingState.report(user.id) + expect(report.length).to eq(3) + + SiteSetting.max_tracked_new_unread = 5 + # business logic, we allow for 2/5th new .. 2/5th unread ... 1/5th buffer + + TopicUser.cap_unread_backlog! + + report = TopicTrackingState.report(user.id) + expect(report.length).to eq(3) + + TopicUser.cap_unread_later(user.id) + TopicUser.cap_unread_backlog! + + report = TopicTrackingState.report(user.id) + expect(report.length).to eq(2) + + end + it "correctly gets the tracking state" do report = TopicTrackingState.report(user.id) expect(report.length).to eq(0) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e2bd09f77..be2b49e56 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -338,29 +338,57 @@ describe User do end describe 'username format' do - it "should be #{SiteSetting.min_username_length} chars or longer" do - @user = Fabricate.build(:user) - @user.username = 'ss' - expect(@user.save).to eq(false) + def assert_bad(username) + user = Fabricate.build(:user) + user.username = username + expect(user.valid?).to eq(false) end - it "should never end with a ." do - @user = Fabricate.build(:user) - @user.username = 'sam.' - expect(@user.save).to eq(false) + def assert_good(username) + user = Fabricate.build(:user) + user.username = username + expect(user.valid?).to eq(true) end - it "should never contain spaces" do - @user = Fabricate.build(:user) - @user.username = 'sam s' - expect(@user.save).to eq(false) + it "should be SiteSetting.min_username_length chars or longer" do + SiteSetting.min_username_length = 5 + assert_bad("abcd") + assert_good("abcde") end - ['Bad One', 'Giraf%fe', 'Hello!', '@twitter', 'me@example.com', 'no.dots', 'purple.', '.bilbo', '_nope', 'sa$sy'].each do |bad_nickname| - it "should not allow username '#{bad_nickname}'" do - @user = Fabricate.build(:user) - @user.username = bad_nickname - expect(@user.save).to eq(false) + %w{ first.last + first first-last + _name first_last + mc.hammer_nose + UPPERCASE + sgif + }.each do |username| + it "allows #{username}" do + assert_good(username) + end + end + + %w{ + traildot. + has\ space + double__underscore + with%symbol + Exclamation! + @twitter + my@email.com + .tester + sa$sy + sam.json + sam.xml + sam.html + sam.htm + sam.js + sam.woff + sam.Png + sam.gif + }.each do |username| + it "disallows #{username}" do + assert_bad(username) end end end @@ -868,7 +896,11 @@ describe User do let(:user) { build(:user, username: 'Sam') } it "returns a 45-pixel-wide avatar" do + SiteSetting.external_system_avatars_enabled = false expect(user.small_avatar_url).to eq("//test.localhost/letter_avatar/sam/45/#{LetterAvatar.version}.png") + + SiteSetting.external_system_avatars_enabled = true + expect(user.small_avatar_url).to eq("https://avatars.discourse.org/letter/s/5f9b8f/45.png") end end @@ -1042,6 +1074,8 @@ describe User do describe "automatic avatar creation" do it "sets a system avatar for new users" do + SiteSetting.external_system_avatars_enabled = false + u = User.create!(username: "bob", email: "bob@bob.com") u.reload expect(u.uploaded_avatar_id).to eq(nil) diff --git a/spec/phantom_js/smoke_test.js b/spec/phantom_js/smoke_test.js index c41c488ea..969205dc4 100644 --- a/spec/phantom_js/smoke_test.js +++ b/spec/phantom_js/smoke_test.js @@ -17,6 +17,14 @@ page.viewportSize = { height: 768 }; +// In the browser, when the cookies are disabled, it also disables the localStorage +// Here, we're mocking that behavior and making sure the application doesn't blow up +page.onInitialized = function() { + page.evaluate(function() { + localStorage["disableLocalStorage"] = true; + }) +} + // page.onConsoleMessage = function(msg) { // console.log(msg); // } diff --git a/spec/services/user_anonymizer_spec.rb b/spec/services/user_anonymizer_spec.rb index 2ff90ecf9..a18086d89 100644 --- a/spec/services/user_anonymizer_spec.rb +++ b/spec/services/user_anonymizer_spec.rb @@ -4,7 +4,7 @@ describe UserAnonymizer do describe "make_anonymous" do let(:admin) { Fabricate(:admin) } - let(:user) { Fabricate(:user, username: "edward") } + let(:user) { Fabricate(:user, username: "edward", auth_token: "mysecretauthtoken") } subject(:make_anonymous) { described_class.make_anonymous(user, admin) } @@ -45,6 +45,7 @@ describe UserAnonymizer do expect(user.name).not_to be_present expect(user.date_of_birth).to eq(nil) expect(user.title).not_to be_present + expect(user.auth_token).to eq(nil) profile = user.user_profile(true) expect(profile.location).to eq(nil) diff --git a/spec/services/username_checker_service_spec.rb b/spec/services/username_checker_service_spec.rb index 258003b3f..7b9a40941 100644 --- a/spec/services/username_checker_service_spec.rb +++ b/spec/services/username_checker_service_spec.rb @@ -26,7 +26,7 @@ describe UsernameCheckerService do end it 'rejects usernames that do not start with an alphanumeric character' do - result = @service.check_username('_vincent', @nil_email) + result = @service.check_username('.vincent', @nil_email) expect(result).to have_key(:errors) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 63b3b45b0..701c0de01 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -144,7 +144,6 @@ Spork.prefork do FileUtils.cp("#{Rails.root}/spec/fixtures/images/#{filename}", "#{Rails.root}/tmp/spec/#{filename}") File.new("#{Rails.root}/tmp/spec/#{filename}") end - end Spork.each_run do diff --git a/test/javascripts/helpers/component-test.js.es6 b/test/javascripts/helpers/component-test.js.es6 index f2ec007a4..181ae5331 100644 --- a/test/javascripts/helpers/component-test.js.es6 +++ b/test/javascripts/helpers/component-test.js.es6 @@ -13,8 +13,10 @@ export default function(name, opts) { this.container.register('site-settings:main', Discourse.SiteSettings, { instantiate: false }); this.container.register('app-events:main', appEvents, { instantiate: false }); + this.container.register('capabilities:main', Ember.Object); this.container.injection('component', 'siteSettings', 'site-settings:main'); this.container.injection('component', 'appEvents', 'app-events:main'); + this.container.injection('component', 'capabilities', 'capabilities:main'); andThen(() => { this.render(opts.template); diff --git a/test/javascripts/helpers/create-store.js.es6 b/test/javascripts/helpers/create-store.js.es6 index 4be1b8feb..9bfa00be4 100644 --- a/test/javascripts/helpers/create-store.js.es6 +++ b/test/javascripts/helpers/create-store.js.es6 @@ -1,16 +1,29 @@ import Store from "discourse/models/store"; import RestAdapter from 'discourse/adapters/rest'; +import KeyValueStore from 'discourse/lib/key-value-store'; +import TopicTrackingState from 'discourse/models/topic-tracking-state'; import Resolver from 'discourse/ember/resolver'; -let _restAdapter; export default function() { const resolver = Resolver.create(); return Store.create({ container: { lookup(type) { if (type === "adapter:rest") { - _restAdapter = _restAdapter || RestAdapter.create({ container: this }); - return (_restAdapter); + this._restAdapter = this._restAdapter || RestAdapter.create({ container: this }); + return (this._restAdapter); + } + if (type === "key-value-store:main") { + this._kvs = this._kvs || new KeyValueStore(); + return (this._kvs); + } + if (type === "topic-tracking-state:main") { + this._tracker = this._tracker || TopicTrackingState.current(); + return (this._tracker); + } + if (type === "site-settings:main") { + this._settings = this._settings || Discourse.SiteSettings.current(); + return (this._settings); } }, diff --git a/test/javascripts/helpers/qunit-helpers.js.es6 b/test/javascripts/helpers/qunit-helpers.js.es6 index 4b0207e54..44812f7ec 100644 --- a/test/javascripts/helpers/qunit-helpers.js.es6 +++ b/test/javascripts/helpers/qunit-helpers.js.es6 @@ -71,7 +71,7 @@ function acceptance(name, options) { options.teardown.call(this); } Discourse.User.resetCurrent(); - Discourse.Site.resetCurrent(Discourse.Site.create(fixtures['site.json'].site)); + Discourse.Site.resetCurrent(Discourse.Site.create(jQuery.extend(true, {}, fixtures['site.json'].site))); Discourse.Utilities.avatarImg = oldAvatar; Discourse.reset(); diff --git a/test/javascripts/lib/avatar-template-test.js.es6 b/test/javascripts/lib/avatar-template-test.js.es6 deleted file mode 100644 index c2f004c8d..000000000 --- a/test/javascripts/lib/avatar-template-test.js.es6 +++ /dev/null @@ -1,16 +0,0 @@ -import avatarTemplate from 'discourse/lib/avatar-template'; - -module('lib:avatar-template'); - -test("avatarTemplate", function(){ - var oldCDN = Discourse.CDN; - var oldBase = Discourse.BaseUrl; - Discourse.BaseUrl = "frogs.com"; - - equal(avatarTemplate("sam", 1), "/user_avatar/frogs.com/sam/{size}/1.png"); - Discourse.CDN = "http://awesome.cdn.com"; - equal(avatarTemplate("sam", 1), "http://awesome.cdn.com/user_avatar/frogs.com/sam/{size}/1.png"); - Discourse.CDN = oldCDN; - Discourse.BaseUrl = oldBase; -}); - diff --git a/test/javascripts/lib/bbcode-test.js.es6 b/test/javascripts/lib/bbcode-test.js.es6 index 575ad7e75..7aa30ed91 100644 --- a/test/javascripts/lib/bbcode-test.js.es6 +++ b/test/javascripts/lib/bbcode-test.js.es6 @@ -52,7 +52,7 @@ test('spoiler', function() { format("[spoiler]it's a sled[/spoiler]", "<span class=\"spoiler\">it's a sled</span>", "supports spoiler tags on text"); format("[spoiler]<img src='http://eviltrout.com/eviltrout.png' width='50' height='50'>[/spoiler]", "<span class=\"spoiler\"><img src=\"http://eviltrout.com/eviltrout.png\" width=\"50\" height=\"50\"></span>", "supports spoiler tags on images"); - format("[spoiler] This is the **bold** :smiley: [/spoiler]", "<span class=\"spoiler\"> This is the <strong>bold</strong> <img src=\"/images/emoji/emoji_one/smiley.png?v=0\" title=\":smiley:\" class=\"emoji\" alt=\"smiley\"> </span>", "supports spoiler tags on emojis"); + format("[spoiler] This is the **bold** :smiley: [/spoiler]", "<span class=\"spoiler\"> This is the <strong>bold</strong> <img src=\"/images/emoji/emoji_one/smiley.png?v=0\" title=\":smiley:\" class=\"emoji\" alt=\":smiley:\"> </span>", "supports spoiler tags on emojis"); format("[spoiler] Why not both <img src='http://eviltrout.com/eviltrout.png' width='50' height='50'>?[/spoiler]", "<span class=\"spoiler\"> Why not both <img src=\"http://eviltrout.com/eviltrout.png\" width=\"50\" height=\"50\">?</span>", "supports images and text"); format("In a p tag a spoiler [spoiler] <img src='http://eviltrout.com/eviltrout.png' width='50' height='50'>[/spoiler] can work.", "In a p tag a spoiler <span class=\"spoiler\"> <img src=\"http://eviltrout.com/eviltrout.png\" width=\"50\" height=\"50\"></span> can work.", "supports images and text in a p tag"); }); @@ -160,6 +160,13 @@ test("quote formatting", function() { "<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Alice:" + "</div><blockquote><p>[quote=\"Bob, post:2, topic:1\"]</p></blockquote></aside>", "handles mismatched nested quote tags"); + + formatQ("[quote=\"Alice, post:1, topic:1\"]\n```javascript\nvar foo ='foo';\nvar bar = 'bar';\n```\n[/quote]", + "<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Alice:</div><blockquote><p><pre><code class=\"lang-javascript\">var foo ='foo';\nvar bar = 'bar';</code></pre></p></blockquote></aside>", + "quotes can have code blocks without leading newline"); + formatQ("[quote=\"Alice, post:1, topic:1\"]\n\n```javascript\nvar foo ='foo';\nvar bar = 'bar';\n```\n[/quote]", + "<aside class=\"quote\" data-post=\"1\" data-topic=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>Alice:</div><blockquote><p><pre><code class=\"lang-javascript\">var foo ='foo';\nvar bar = 'bar';</code></pre></p></blockquote></aside>", + "quotes can have code blocks with leading newline"); }); test("quotes with trailing formatting", function() { diff --git a/test/javascripts/lib/category-badge-test.js.es6 b/test/javascripts/lib/category-badge-test.js.es6 index 55e2da586..385d478c6 100644 --- a/test/javascripts/lib/category-badge-test.js.es6 +++ b/test/javascripts/lib/category-badge-test.js.es6 @@ -1,3 +1,4 @@ +import createStore from 'helpers/create-store'; import { blank, present } from 'helpers/qunit-helpers'; module("lib:category-link"); @@ -10,33 +11,36 @@ test("categoryBadge without a category", function() { }); test("Regular categoryBadge", function() { - var category = Discourse.Category.create({ - name: 'hello', - id: 123, - description_text: 'cool description', - color: 'ff0', - text_color: 'f00' - }), - tag = parseHTML(categoryBadgeHTML(category))[0]; + const store = createStore(); + const category = store.createRecord('category', { + name: 'hello', + id: 123, + description_text: 'cool description', + color: 'ff0', + text_color: 'f00' + }); + const tag = parseHTML(categoryBadgeHTML(category))[0]; equal(tag.name, 'a', 'it creates a `a` wrapper tag'); equal(tag.attributes['class'].trim(), 'badge-wrapper', 'it has the correct class'); - var label = tag.children[1]; + const label = tag.children[1]; equal(label.attributes.title, 'cool description', 'it has the correct title'); equal(label.children[0].data, 'hello', 'it has the category name'); }); test("undefined color", function() { - var noColor = Discourse.Category.create({ name: 'hello', id: 123 }), - tag = parseHTML(categoryBadgeHTML(noColor))[0]; + const store = createStore(); + const noColor = store.createRecord('category', { name: 'hello', id: 123 }); + const tag = parseHTML(categoryBadgeHTML(noColor))[0]; blank(tag.attributes.style, "it has no color style because there are no colors"); }); test("allowUncategorized", function() { - var uncategorized = Discourse.Category.create({name: 'uncategorized', id: 345}); + const store = createStore(); + const uncategorized = store.createRecord('category', {name: 'uncategorized', id: 345}); sandbox.stub(Discourse.Site, 'currentProp').withArgs('uncategorized_category_id').returns(345); blank(categoryBadgeHTML(uncategorized), "it doesn't return HTML for uncategorized by default"); diff --git a/test/javascripts/lib/url-test.js.es6 b/test/javascripts/lib/url-test.js.es6 index 0598c32e5..39941fd00 100644 --- a/test/javascripts/lib/url-test.js.es6 +++ b/test/javascripts/lib/url-test.js.es6 @@ -16,3 +16,10 @@ test("isInternal with a HTTPS url", function() { sandbox.stub(DiscourseURL, "origin").returns("https://eviltrout.com"); ok(DiscourseURL.isInternal("http://eviltrout.com/monocle"), "HTTPS urls match HTTP urls"); }); + +test("isInternal on subfolder install", function() { + sandbox.stub(DiscourseURL, "origin").returns("http://eviltrout.com/forum"); + not(DiscourseURL.isInternal("http://eviltrout.com"), "the host root is not internal"); + not(DiscourseURL.isInternal("http://eviltrout.com/tophat"), "a url on the same host but on a different folder is not internal"); + ok(DiscourseURL.isInternal("http://eviltrout.com/forum/moustache"), "a url on the same host and on the same folder is internal"); +}); diff --git a/test/javascripts/models/category-test.js.es6 b/test/javascripts/models/category-test.js.es6 index b005ec116..48735f88e 100644 --- a/test/javascripts/models/category-test.js.es6 +++ b/test/javascripts/models/category-test.js.es6 @@ -1,27 +1,30 @@ -module("Discourse.Category"); +import createStore from 'helpers/create-store'; + +module("model:category"); test('slugFor', function(){ + const store = createStore(); - var slugFor = function(cat, val, text) { + const slugFor = function(cat, val, text) { equal(Discourse.Category.slugFor(cat), val, text); }; - slugFor(Discourse.Category.create({slug: 'hello'}), "hello", "It calculates the proper slug for hello"); - slugFor(Discourse.Category.create({id: 123, slug: ''}), "123-category", "It returns id-category for empty strings"); - slugFor(Discourse.Category.create({id: 456}), "456-category", "It returns id-category for undefined slugs"); - slugFor(Discourse.Category.create({slug: '熱帶風暴畫眉'}), "熱帶風暴畫眉", "It can be non english characters"); + slugFor(store.createRecord('category', {slug: 'hello'}), "hello", "It calculates the proper slug for hello"); + slugFor(store.createRecord('category', {id: 123, slug: ''}), "123-category", "It returns id-category for empty strings"); + slugFor(store.createRecord('category', {id: 456}), "456-category", "It returns id-category for undefined slugs"); + slugFor(store.createRecord('category', {slug: '熱帶風暴畫眉'}), "熱帶風暴畫眉", "It can be non english characters"); - var parentCategory = Discourse.Category.create({id: 345, slug: 'darth'}); - slugFor(Discourse.Category.create({slug: 'luke', parentCategory: parentCategory}), + const parentCategory = store.createRecord('category', {id: 345, slug: 'darth'}); + slugFor(store.createRecord('category', {slug: 'luke', parentCategory: parentCategory}), "darth/luke", "it uses the parent slug before the child"); - slugFor(Discourse.Category.create({id: 555, parentCategory: parentCategory}), + slugFor(store.createRecord('category', {id: 555, parentCategory: parentCategory}), "darth/555-category", "it uses the parent slug before the child and then uses id"); parentCategory.set('slug', null); - slugFor(Discourse.Category.create({id: 555, parentCategory: parentCategory}), + slugFor(store.createRecord('category', {id: 555, parentCategory: parentCategory}), "345-category/555-category", "it uses the parent before the child and uses ids for both"); }); @@ -30,12 +33,13 @@ test('slugFor', function(){ test('findBySlug', function() { expect(6); - var darth = Discourse.Category.create({id: 1, slug: 'darth'}), - luke = Discourse.Category.create({id: 2, slug: 'luke', parentCategory: darth}), - hurricane = Discourse.Category.create({id: 3, slug: '熱帶風暴畫眉'}), - newsFeed = Discourse.Category.create({id: 4, slug: '뉴스피드', parentCategory: hurricane}), - time = Discourse.Category.create({id: 5, slug: '时间', parentCategory: darth}), - bah = Discourse.Category.create({id: 6, slug: 'bah', parentCategory: hurricane}), + const store = createStore(); + const darth = store.createRecord('category', {id: 1, slug: 'darth'}), + luke = store.createRecord('category', {id: 2, slug: 'luke', parentCategory: darth}), + hurricane = store.createRecord('category', {id: 3, slug: '熱帶風暴畫眉'}), + newsFeed = store.createRecord('category', {id: 4, slug: '뉴스피드', parentCategory: hurricane}), + time = store.createRecord('category', {id: 5, slug: '时间', parentCategory: darth}), + bah = store.createRecord('category', {id: 6, slug: 'bah', parentCategory: hurricane}), categoryList = [darth, luke, hurricane, newsFeed, time, bah]; sandbox.stub(Discourse.Category, 'list').returns(categoryList); @@ -51,12 +55,13 @@ test('findBySlug', function() { test('findSingleBySlug', function() { expect(6); - var darth = Discourse.Category.create({id: 1, slug: 'darth'}), - luke = Discourse.Category.create({id: 2, slug: 'luke', parentCategory: darth}), - hurricane = Discourse.Category.create({id: 3, slug: '熱帶風暴畫眉'}), - newsFeed = Discourse.Category.create({id: 4, slug: '뉴스피드', parentCategory: hurricane}), - time = Discourse.Category.create({id: 5, slug: '时间', parentCategory: darth}), - bah = Discourse.Category.create({id: 6, slug: 'bah', parentCategory: hurricane}), + const store = createStore(); + const darth = store.createRecord('category', {id: 1, slug: 'darth'}), + luke = store.createRecord('category', {id: 2, slug: 'luke', parentCategory: darth}), + hurricane = store.createRecord('category', {id: 3, slug: '熱帶風暴畫眉'}), + newsFeed = store.createRecord('category', {id: 4, slug: '뉴스피드', parentCategory: hurricane}), + time = store.createRecord('category', {id: 5, slug: '时间', parentCategory: darth}), + bah = store.createRecord('category', {id: 6, slug: 'bah', parentCategory: hurricane}), categoryList = [darth, luke, hurricane, newsFeed, time, bah]; sandbox.stub(Discourse.Category, 'list').returns(categoryList); @@ -70,9 +75,10 @@ test('findSingleBySlug', function() { }); test('findByIds', function() { - var categories = { - 1: Discourse.Category.create({id: 1}), - 2: Discourse.Category.create({id: 2}) + const store = createStore(); + const categories = { + 1: store.createRecord('category', {id: 1}), + 2: store.createRecord('category', {id: 2}) }; sandbox.stub(Discourse.Category, 'idMap').returns(categories); @@ -80,13 +86,14 @@ test('findByIds', function() { }); test('postCountStats', function() { - var category1 = Discourse.Category.create({id: 1, slug: 'unloved', posts_year: 2, posts_month: 0, posts_week: 0, posts_day: 0}), - category2 = Discourse.Category.create({id: 2, slug: 'hasbeen', posts_year: 50, posts_month: 4, posts_week: 0, posts_day: 0}), - category3 = Discourse.Category.create({id: 3, slug: 'solastweek', posts_year: 250, posts_month: 200, posts_week: 50, posts_day: 0}), - category4 = Discourse.Category.create({id: 4, slug: 'hotstuff', posts_year: 500, posts_month: 280, posts_week: 100, posts_day: 22}), - category5 = Discourse.Category.create({id: 6, slug: 'empty', posts_year: 0, posts_month: 0, posts_week: 0, posts_day: 0}); + const store = createStore(); + const category1 = store.createRecord('category', {id: 1, slug: 'unloved', posts_year: 2, posts_month: 0, posts_week: 0, posts_day: 0}), + category2 = store.createRecord('category', {id: 2, slug: 'hasbeen', posts_year: 50, posts_month: 4, posts_week: 0, posts_day: 0}), + category3 = store.createRecord('category', {id: 3, slug: 'solastweek', posts_year: 250, posts_month: 200, posts_week: 50, posts_day: 0}), + category4 = store.createRecord('category', {id: 4, slug: 'hotstuff', posts_year: 500, posts_month: 280, posts_week: 100, posts_day: 22}), + category5 = store.createRecord('category', {id: 6, slug: 'empty', posts_year: 0, posts_month: 0, posts_week: 0, posts_day: 0}); - var result = category1.get('postCountStats'); + let result = category1.get('postCountStats'); equal(result.length, 1, "should only show year"); equal(result[0].value, 2); equal(result[0].unit, 'year'); diff --git a/test/javascripts/models/composer-test.js.es6 b/test/javascripts/models/composer-test.js.es6 index 08559b8bc..a31c569ca 100644 --- a/test/javascripts/models/composer-test.js.es6 +++ b/test/javascripts/models/composer-test.js.es6 @@ -1,6 +1,8 @@ import { blank } from 'helpers/qunit-helpers'; import { currentUser } from 'helpers/qunit-helpers'; import KeyValueStore from 'discourse/lib/key-value-store'; +import Composer from 'discourse/models/composer'; +import createStore from 'helpers/create-store'; module("model:composer"); @@ -9,10 +11,13 @@ const keyValueStore = new KeyValueStore("_test_composer"); function createComposer(opts) { opts = opts || {}; opts.user = opts.user || currentUser(); - opts.site = Discourse.Site.current(); - opts.siteSettings = Discourse.SiteSettings; - opts.keyValueStore = keyValueStore; -; return Discourse.Composer.create(opts); + return createStore().createRecord('composer', opts); +} + +function openComposer(opts) { + const composer = createComposer(opts); + composer.open(opts); + return composer; } test('replyLength', function() { @@ -111,7 +116,7 @@ test("Title length for regular topics", function() { test("Title length for private messages", function() { Discourse.SiteSettings.min_private_message_title_length = 5; Discourse.SiteSettings.max_topic_title_length = 10; - const composer = createComposer({action: Discourse.Composer.PRIVATE_MESSAGE}); + const composer = createComposer({action: Composer.PRIVATE_MESSAGE}); composer.set('title', 'asdf'); ok(!composer.get('titleLengthValid'), "short titles are not valid"); @@ -126,7 +131,7 @@ test("Title length for private messages", function() { test("Title length for private messages", function() { Discourse.SiteSettings.min_private_message_title_length = 5; Discourse.SiteSettings.max_topic_title_length = 10; - const composer = createComposer({action: Discourse.Composer.PRIVATE_MESSAGE}); + const composer = createComposer({action: Composer.PRIVATE_MESSAGE}); composer.set('title', 'asdf'); ok(!composer.get('titleLengthValid'), "short titles are not valid"); @@ -143,7 +148,7 @@ test('editingFirstPost', function() { ok(!composer.get('editingFirstPost'), "it's false by default"); const post = Discourse.Post.create({id: 123, post_number: 2}); - composer.setProperties({post: post, action: Discourse.Composer.EDIT }); + composer.setProperties({post: post, action: Composer.EDIT }); ok(!composer.get('editingFirstPost'), "it's false when not editing the first post"); post.set('post_number', 1); @@ -170,19 +175,19 @@ test('clearState', function() { test('initial category when uncategorized is allowed', function() { Discourse.SiteSettings.allow_uncategorized_topics = true; - const composer = Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); - equal(composer.get('categoryId'),undefined,"Uncategorized by default"); + const composer = openComposer({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); + ok(!composer.get('categoryId'), "Uncategorized by default"); }); test('initial category when uncategorized is not allowed', function() { Discourse.SiteSettings.allow_uncategorized_topics = false; - const composer = Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); - ok(composer.get('categoryId') === undefined, "Uncategorized by default. Must choose a category."); + const composer = openComposer({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); + ok(!composer.get('categoryId'), "Uncategorized by default. Must choose a category."); }); test('showPreview', function() { const newComposer = function() { - return Discourse.Composer.open({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); + return openComposer({action: 'createTopic', draftKey: 'asfd', draftSequence: 1}); }; Discourse.Mobile.mobileView = true; @@ -199,7 +204,7 @@ test('showPreview', function() { test('open with a quote', function() { const quote = '[quote="neil, post:5, topic:413"]\nSimmer down you two.\n[/quote]'; const newComposer = function() { - return Discourse.Composer.open({action: Discourse.Composer.REPLY, draftKey: 'asfd', draftSequence: 1, quote: quote}); + return openComposer({action: Discourse.Composer.REPLY, draftKey: 'asfd', draftSequence: 1, quote: quote}); }; equal(newComposer().get('originalText'), quote, "originalText is the quote" ); @@ -212,7 +217,7 @@ test("Title length for static page topics as admin", function() { const composer = createComposer(); const post = Discourse.Post.create({id: 123, post_number: 2, static_doc: true}); - composer.setProperties({post: post, action: Discourse.Composer.EDIT }); + composer.setProperties({post: post, action: Composer.EDIT }); composer.set('title', 'asdf'); ok(composer.get('titleLengthValid'), "admins can use short titles"); diff --git a/test/javascripts/models/site-test.js.es6 b/test/javascripts/models/site-test.js.es6 index 65804504d..bc1c3c0ac 100644 --- a/test/javascripts/models/site-test.js.es6 +++ b/test/javascripts/models/site-test.js.es6 @@ -1,14 +1,14 @@ +import createStore from 'helpers/create-store'; import { blank, present } from 'helpers/qunit-helpers'; -module("Discourse.Site"); +module("model:site"); test('create', function() { ok(Discourse.Site.create(), 'it can create with no parameters'); }); test('instance', function() { - - var site = Discourse.Site.current(); + const site = Discourse.Site.current(); present(site, "We have a current site singleton"); present(site.get('categories'), "The instance has a list of categories"); @@ -18,24 +18,24 @@ test('instance', function() { }); test('create categories', function() { - - var site = Discourse.Site.create({ + const store = createStore(); + const site = store.createRecord('site', { categories: [{ id: 1234, name: 'Test'}, { id: 3456, name: 'Test Subcategory', parent_category_id: 1234}, - { id: 3456, name: 'Invalid Subcategory', parent_category_id: 6666}] + { id: 3458, name: 'Invalid Subcategory', parent_category_id: 6666}] }); - var categories = site.get('categories'); + const categories = site.get('categories'); site.get('sortedCategories'); present(categories, "The categories are present"); equal(categories.length, 3, "it loaded all three categories"); - var parent = categories.findBy('id', 1234); + const parent = categories.findBy('id', 1234); present(parent, "it loaded the parent category"); blank(parent.get('parentCategory'), 'it has no parent category'); - var subcategory = categories.findBy('id', 3456); + const subcategory = categories.findBy('id', 3456); present(subcategory, "it loaded the subcategory"); equal(subcategory.get('parentCategory'), parent, "it has associated the child with the parent"); diff --git a/test/javascripts/models/topic-tracking-state-test.js.es6 b/test/javascripts/models/topic-tracking-state-test.js.es6 index 305fc1d17..f135584ee 100644 --- a/test/javascripts/models/topic-tracking-state-test.js.es6 +++ b/test/javascripts/models/topic-tracking-state-test.js.es6 @@ -1,21 +1,19 @@ -module("Discourse.TopicTrackingState"); +import TopicTrackingState from 'discourse/models/topic-tracking-state'; -test("sync", function () { +module("model:topic-tracking-state"); - var state = Discourse.TopicTrackingState.create(); - // fake track it +test("sync", function (assert) { + const state = TopicTrackingState.create(); state.states["t111"] = {last_read_post_number: null}; state.updateSeen(111, 7); - var list = {topics: [{ + const list = {topics: [{ highest_post_number: null, id: 111, unread: 10, new_posts: 10 - }]}; + }]}; state.sync(list, "new"); - - equal(list.topics.length, 0, "expect new topic to be removed as it was seen"); - + assert.equal(list.topics.length, 0, "expect new topic to be removed as it was seen"); }); diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 08de7fdce..5b9a489e0 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -105,6 +105,7 @@ QUnit.testStart(function(ctx) { PreloadStore.reset(); window.sandbox = sinon.sandbox.create(); + window.sandbox.stub(ScrollingDOMMethods, "screenNotFull"); window.sandbox.stub(ScrollingDOMMethods, "bindOnScroll"); window.sandbox.stub(ScrollingDOMMethods, "unbindOnScroll"); diff --git a/vendor/assets/javascripts/div_resizer.js b/vendor/assets/javascripts/div_resizer.js index fbcca76be..07a99144f 100644 --- a/vendor/assets/javascripts/div_resizer.js +++ b/vendor/assets/javascripts/div_resizer.js @@ -42,7 +42,12 @@ performDrag = function(e, opts) { thisMousePos = mousePosition(e).y; size = originalDivHeight + (originalPos - thisMousePos); lastMousePos = thisMousePos; - size = Math.min(size, $(window).height()); + + var maxHeight = $(window).height(); + if (opts.maxHeight) { + maxHeight = opts.maxHeight(maxHeight); + } + size = Math.min(size, maxHeight); size = Math.max(min, size); sizePx = size + "px"; if (typeof opts.onDrag === "function") { diff --git a/vendor/assets/javascripts/jquery.ba-replacetext.js b/vendor/assets/javascripts/jquery.ba-replacetext.js deleted file mode 100644 index c6b60c57c..000000000 --- a/vendor/assets/javascripts/jquery.ba-replacetext.js +++ /dev/null @@ -1,129 +0,0 @@ -/*! - * jQuery replaceText - v1.1 - 11/21/2009 - * http://benalman.com/projects/jquery-replacetext-plugin/ - * - * Copyright (c) 2009 "Cowboy" Ben Alman - * Dual licensed under the MIT and GPL licenses. - * http://benalman.com/about/license/ - */ - -// Script: jQuery replaceText: String replace for your jQueries! -// -// *Version: 1.1, Last updated: 11/21/2009* -// -// Project Home - http://benalman.com/projects/jquery-replacetext-plugin/ -// GitHub - http://github.com/cowboy/jquery-replacetext/ -// Source - http://github.com/cowboy/jquery-replacetext/raw/master/jquery.ba-replacetext.js -// (Minified) - http://github.com/cowboy/jquery-replacetext/raw/master/jquery.ba-replacetext.min.js (0.5kb) -// -// About: License -// -// Copyright (c) 2009 "Cowboy" Ben Alman, -// Dual licensed under the MIT and GPL licenses. -// http://benalman.com/about/license/ -// -// About: Examples -// -// This working example, complete with fully commented code, illustrates one way -// in which this plugin can be used. -// -// replaceText - http://benalman.com/code/projects/jquery-replacetext/examples/replacetext/ -// -// About: Support and Testing -// -// Information about what version or versions of jQuery this plugin has been -// tested with, and what browsers it has been tested in. -// -// jQuery Versions - 1.3.2, 1.4.1 -// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome, Opera 9.6-10.1. -// -// About: Release History -// -// 1.1 - (11/21/2009) Simplified the code and API substantially. -// 1.0 - (11/21/2009) Initial release - -(function($){ - '$:nomunge'; // Used by YUI compressor. - - // Method: jQuery.fn.replaceText - // - // Replace text in specified elements. Note that only text content will be - // modified, leaving all tags and attributes untouched. The new text can be - // either text or HTML. - // - // Uses the String prototype replace method, full documentation on that method - // can be found here: - // - // https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/String/Replace - // - // Usage: - // - // > jQuery('selector').replaceText( search, replace [, text_only ] ); - // - // Arguments: - // - // search - (RegExp|String) A RegExp object or substring to be replaced. - // Because the String prototype replace method is used internally, this - // argument should be specified accordingly. - // replace - (String|Function) The String that replaces the substring received - // from the search argument, or a function to be invoked to create the new - // substring. Because the String prototype replace method is used internally, - // this argument should be specified accordingly. - // text_only - (Boolean) If true, any HTML will be rendered as text. Defaults - // to false. - // - // Returns: - // - // (jQuery) The initial jQuery collection of elements. - - $.fn.replaceText = function( search, replace, text_only ) { - return this.each(function(){ - var node = this.firstChild, - val, - new_val, - - // Elements to be removed at the end. - remove = []; - - // Only continue if firstChild exists. - if ( node ) { - - // Loop over all childNodes. - do { - - // Only process text nodes. - if ( node.nodeType === 3 ) { - - // The original node value. - val = node.nodeValue; - - // The new value. - new_val = val.replace( search, replace ); - - // Only replace text if the new value is actually different! - if ( new_val !== val ) { - - if ( !text_only && /</.test( new_val ) ) { - // The new value contains HTML, set it in a slower but far more - // robust way. - $(node).before( new_val ); - - // Don't remove the node yet, or the loop will lose its place. - remove.push( node ); - } else { - // The new value contains no HTML, so it can be set in this - // very fast, simple way. - node.nodeValue = new_val; - } - } - } - - } while ( node = node.nextSibling ); - } - - // Time to remove those elements! - remove.length && $(remove).remove(); - }); - }; - -})(jQuery);