From c3a5ddac8cf835e59b400121df8fc5d3589b3a51 Mon Sep 17 00:00:00 2001 From: Erlend Sogge Heggen <e.soghe@gmail.com> Date: Thu, 10 Sep 2015 20:43:36 +0200 Subject: [PATCH 01/47] Repurposing CONTRIBUTING.md into a link portal, 2nd attempt - Slight changes to the CLA paragraph, making it slightly easier to digest. - Added brief synopsis of the Discourse Development Contribution Guidelines doc - Replaced Bug Report, Feature Request and Contributing (commits) section with outgoing links The aim of this change is to reduce the maintenance burden, since more detailed information about contribution guidelines is more naturally documented and maintained on Discourse Meta. --- CONTRIBUTING.md | 134 ++++++------------------------------------------ 1 file changed, 16 insertions(+), 118 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f00d5d078..f59bf39dc 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/discourse/discourse](https://github.com/discourse/discourse) project **must read & sign our [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. -## 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!* From ef787b3828f81bbf100ff02eccac549edb9ded83 Mon Sep 17 00:00:00 2001 From: Erlend Sogge Heggen <e.soghe@gmail.com> Date: Thu, 10 Sep 2015 20:46:36 +0200 Subject: [PATCH 02/47] GitHub link was missing the .com The whole point of adding github.com to the link in the first place was to leave no room for misinterpretation. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f59bf39dc..4ad0aec09 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Important note for Developers -Anyone wishing to contribute to the [github/discourse/discourse](https://github.com/discourse/discourse) project **must read & sign our [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 [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. For more information on From 28cd0361d66269e0164886062f27366a54ab9054 Mon Sep 17 00:00:00 2001 From: Erlend Sogge Heggen <e.soghe@gmail.com> Date: Thu, 10 Sep 2015 20:49:03 +0200 Subject: [PATCH 03/47] Proper long form for CLA Seems it's most commonly spelled out as "*Contributor* License Agreement", not *Contribution*. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4ad0aec09..98aa05e4a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Important note for Developers -Anyone wishing to contribute to the [github.com/discourse/discourse](https://github.com/discourse/discourse) project **must read & sign our [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. For more information on From 20c8bb04943204ce79366961759048a5ced964bd Mon Sep 17 00:00:00 2001 From: scossar <scossar3@gmail.com> Date: Thu, 10 Sep 2015 11:46:02 -0700 Subject: [PATCH 04/47] remove hardcoded left: auto --- app/assets/javascripts/discourse/components/menu-panel.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/components/menu-panel.js.es6 b/app/assets/javascripts/discourse/components/menu-panel.js.es6 index 8a5b2ea27..02304afee 100644 --- a/app/assets/javascripts/discourse/components/menu-panel.js.es6 +++ b/app/assets/javascripts/discourse/components/menu-panel.js.es6 @@ -54,7 +54,7 @@ export default Ember.Component.extend({ } $panelBody.height('100%'); - this.$().css({ left: "auto", top: (menuTop) + "px", height }); + this.$().css({ top: menuTop + "px", height }); $('body').removeClass('drop-down-visible'); } From b68be6c5deac5e723b4a60f8f17db04f5d8b1671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Thu, 10 Sep 2015 21:56:51 +0200 Subject: [PATCH 05/47] update onebox --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 44397f9df..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 From d86d4752cbb68acad381f0b30a6702f04d8726db Mon Sep 17 00:00:00 2001 From: Kane York <rikingcoding@gmail.com> Date: Thu, 10 Sep 2015 14:04:21 -0700 Subject: [PATCH 06/47] FIX: Don't allow editing seeded category security settings --- .../components/edit-category-security.js.es6 | 16 ++++++++++++---- .../components/edit-category-security.hbs | 9 ++++++++- app/serializers/category_serializer.rb | 6 ++++++ config/locales/client.en.yml | 1 + config/site_settings.yml | 8 +++----- 5 files changed, 30 insertions(+), 10 deletions(-) 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/templates/components/edit-category-security.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs index 02361e3cf..d5468a496 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,9 @@ <section class='field'> + {{#if category.is_special}} + <p class="warning">{{i18n 'category.special_warning'}}</p> + {{else}} + + {{/if}} <ul class='permission-list'> {{#each category.permissions as |p|}} <li> @@ -16,6 +21,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/serializers/category_serializer.rb b/app/serializers/category_serializer.rb index 1bf08f89f..1f8ee5bf8 100644 --- a/app/serializers/category_serializer.rb +++ b/app/serializers/category_serializer.rb @@ -11,6 +11,7 @@ class CategorySerializer < BasicCategorySerializer :suppress_from_homepage, :can_delete, :cannot_delete_reason, + :is_special, :allow_badges, :custom_fields @@ -37,6 +38,11 @@ class CategorySerializer < BasicCategorySerializer true end + def is_special + [SiteSetting.lounge_category_id, SiteSetting.meta_category_id, SiteSetting.staff_category_id, SiteSetting.uncategorized_category_id] + .include? object.id + end + def include_can_delete? scope && scope.can_delete?(object) end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 7799a8254..bbc1833f0 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1539,6 +1539,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" diff --git a/config/site_settings.yml b/config/site_settings.yml index 754e067b8..c6ec25467 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -418,9 +418,6 @@ posting: newuser_max_attachments: client: true default: 0 - uncategorized_category_id: - default: -1 - hidden: true post_excerpt_maxlength: 300 display_name_on_posts: client: true @@ -922,14 +919,15 @@ uncategorized: 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 From a9d10f454bf32eaf3c427f3bdb0446987d2e5880 Mon Sep 17 00:00:00 2001 From: Kane York <rikingcoding@gmail.com> Date: Thu, 10 Sep 2015 14:12:08 -0700 Subject: [PATCH 07/47] Oop --- .../discourse/templates/components/edit-category-security.hbs | 2 -- 1 file changed, 2 deletions(-) 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 d5468a496..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,8 +1,6 @@ <section class='field'> {{#if category.is_special}} <p class="warning">{{i18n 'category.special_warning'}}</p> - {{else}} - {{/if}} <ul class='permission-list'> {{#each category.permissions as |p|}} From cd774657889f38749a23f72d402835c57af8f60e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Fri, 11 Sep 2015 00:11:48 +0200 Subject: [PATCH 08/47] FEATURE: SVG letter avatars (based on @eviltrout's spike) --- .../discourse/lib/avatar-template.js.es6 | 14 +++------- app/controllers/user_avatars_controller.rb | 26 ++++++++++++++++++- app/models/user.rb | 5 ++-- config/locales/server.en.yml | 2 ++ config/routes.rb | 1 + config/site_settings.yml | 3 +++ lib/email/styles.rb | 2 -- lib/letter_avatar.rb | 26 +++++++++---------- 8 files changed, 51 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 index 542e97959..15948aba2 100644 --- a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 +++ b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 @@ -4,6 +4,7 @@ let _splitAvatars; function defaultAvatar(username) { const defaultAvatars = Discourse.SiteSettings.default_avatars; + if (defaultAvatars && defaultAvatars.length) { _splitAvatars = _splitAvatars || defaultAvatars.split("\n"); @@ -13,20 +14,13 @@ function defaultAvatar(username) { } } - return Discourse.getURLWithCDN("/letter_avatar/" + - username.toLowerCase() + - "/{size}/" + - Discourse.LetterAvatarVersion + ".png"); + const extension = Discourse.SiteSettings.svg_letter_avatars ? "svg" : "png"; + return Discourse.getURLWithCDN(`/letter_avatar/${username.toLowerCase()}/{size}/${Discourse.LetterAvatarVersion}.${extension}`); } export default function(username, uploadedAvatarId) { if (uploadedAvatarId) { - return Discourse.getURLWithCDN("/user_avatar/" + - Discourse.BaseUrl + - "/" + - username.toLowerCase() + - "/{size}/" + - uploadedAvatarId + ".png"); + return Discourse.getURLWithCDN(`/user_avatar/${Discourse.BaseUrl}/${username.toLowerCase()}/{size}/${uploadedAvatarId}.png`); } return defaultAvatar(username); } diff --git a/app/controllers/user_avatars_controller.rb b/app/controllers/user_avatars_controller.rb index ed8f31e4a..f1d6e7d1d 100644 --- a/app/controllers/user_avatars_controller.rb +++ b/app/controllers/user_avatars_controller.rb @@ -3,7 +3,7 @@ require_dependency 'letter_avatar' class UserAvatarsController < ApplicationController DOT = Base64.decode64("R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==") - skip_before_filter :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_letter] + skip_before_filter :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_letter, :show_letter_svg] def refresh_gravatar user = User.find_by(username_lower: params[:username].downcase) @@ -19,6 +19,30 @@ class UserAvatarsController < ApplicationController end end + def show_letter_svg + params.require(:username) + params.require(:version) + params.require(:size) + + no_cookies + + size = params[:size].to_i + username = params[:username] + + identity = LetterAvatar::Identity.from_username(username) + color = identity.color + + svg = <<-SVG + <svg xmlns="http://www.w3.org/2000/svg" width="#{size}" height="#{size}"> + <rect width="100%" height="100%" fill="rgb(#{color[0]},#{color[1]},#{color[2]})" /> + <text font-size="#{size * 0.7}" font-weight="normal" font-family="Helvetica, sans-serif" fill="#FFF" fill-opacity=".8" text-anchor="middle" x="50%" y="75%">#{username[0].capitalize}</text> + </svg> + SVG + + expires_in 1.year, public: true + render inline: svg, content_type: "image/svg+xml" + end + def show_letter params.require(:username) params.require(:version) diff --git a/app/models/user.rb b/app/models/user.rb index 2861fff88..cd2344564 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -457,7 +457,7 @@ class User < ActiveRecord::Base avatar_template = split_avatars[hash.abs % split_avatars.size] end else - "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" + letter_avatar_template(username) end end @@ -469,7 +469,8 @@ class User < ActiveRecord::Base end def self.letter_avatar_template(username) - "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" + extension = SiteSetting.svg_letter_avatars ? "svg" : "png" + "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.#{extension}" end def avatar_template diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index fcd190dc8..921edd59e 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -979,6 +979,8 @@ en: avatar_sizes: "List of automatically generated avatar sizes." + svg_letter_avatars: "Use SVG for letter avatars" + 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." diff --git a/config/routes.rb b/config/routes.rb index 66125570f..f5b5aa1b3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -299,6 +299,7 @@ Discourse::Application.routes.draw do get "user-badges/:username" => "user_badges#username", constraints: {username: USERNAME_ROUTE_FORMAT} post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", constraints: {username: USERNAME_ROUTE_FORMAT} + get "letter_avatar/:username/:size/:version.svg" => "user_avatars#show_letter_svg", format: :svg, constraints: { hostname: /[\w\.-]+/, size: /\d+/, 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 } diff --git a/config/site_settings.yml b/config/site_settings.yml index 754e067b8..0c4950ad8 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -572,6 +572,9 @@ files: avatar_sizes: default: '20|25|32|45|60|120' type: list + svg_letter_avatars: + default: false + client: true trust: default_trust_level: diff --git a/lib/email/styles.rb b/lib/email/styles.rb index 22f9ffe1e..e82106b96 100644 --- a/lib/email/styles.rb +++ b/lib/email/styles.rb @@ -30,7 +30,6 @@ module Email # images @fragment.css('img').each do |img| - next if img['class'] == 'site-logo' if img['class'] == "emoji" || img['src'] =~ /plugins\/emoji/ @@ -58,7 +57,6 @@ module Email # attachments @fragment.css('a.attachment').each do |a| - # ensure all urls are absolute if a['href'] =~ /^\/[^\/]/ a['href'] = "#{Discourse.base_url}#{a['href']}" diff --git a/lib/letter_avatar.rb b/lib/letter_avatar.rb index abde76ec0..e49ab6c41 100644 --- a/lib/letter_avatar.rb +++ b/lib/letter_avatar.rb @@ -7,20 +7,20 @@ class LetterAvatar FULLSIZE = 120 * 3 POINTSIZE = 280 - class << self + class Identity + attr_accessor :color, :letter - class Identity - attr_accessor :color, :letter - - def self.from_username(username) - identity = new - identity.color = LetterAvatar::COLORS[ - Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length - ] - identity.letter = username[0].upcase - identity - end + def self.from_username(username) + identity = new + identity.color = LetterAvatar::COLORS[ + Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length + ] + identity.letter = username[0].upcase + identity end + end + + class << self def version "#{VERSION}_#{image_magick_version}" @@ -32,7 +32,7 @@ class LetterAvatar def generate(username, size, opts = nil) DistributedMutex.synchronize("letter_avatar_#{version}_#{username}") do - identity = Identity.from_username(username) + identity = LetterAvatar::Identity.from_username(username) cache = true cache = false if opts && opts[:cache] == false From e43034f08f2961ceddd3801efaca00c904c43d44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Fri, 11 Sep 2015 00:23:52 +0200 Subject: [PATCH 09/47] Revert "FEATURE: SVG letter avatars (based on @eviltrout's spike)" This reverts commit cd774657889f38749a23f72d402835c57af8f60e. --- .../discourse/lib/avatar-template.js.es6 | 14 +++++++--- app/controllers/user_avatars_controller.rb | 26 +---------------- app/models/user.rb | 5 ++-- config/locales/server.en.yml | 2 -- config/routes.rb | 1 - config/site_settings.yml | 3 -- lib/email/styles.rb | 2 ++ lib/letter_avatar.rb | 28 +++++++++---------- 8 files changed, 29 insertions(+), 52 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 index 15948aba2..542e97959 100644 --- a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 +++ b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 @@ -4,7 +4,6 @@ let _splitAvatars; function defaultAvatar(username) { const defaultAvatars = Discourse.SiteSettings.default_avatars; - if (defaultAvatars && defaultAvatars.length) { _splitAvatars = _splitAvatars || defaultAvatars.split("\n"); @@ -14,13 +13,20 @@ function defaultAvatar(username) { } } - const extension = Discourse.SiteSettings.svg_letter_avatars ? "svg" : "png"; - return Discourse.getURLWithCDN(`/letter_avatar/${username.toLowerCase()}/{size}/${Discourse.LetterAvatarVersion}.${extension}`); + 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 Discourse.getURLWithCDN("/user_avatar/" + + Discourse.BaseUrl + + "/" + + username.toLowerCase() + + "/{size}/" + + uploadedAvatarId + ".png"); } return defaultAvatar(username); } diff --git a/app/controllers/user_avatars_controller.rb b/app/controllers/user_avatars_controller.rb index f1d6e7d1d..ed8f31e4a 100644 --- a/app/controllers/user_avatars_controller.rb +++ b/app/controllers/user_avatars_controller.rb @@ -3,7 +3,7 @@ require_dependency 'letter_avatar' class UserAvatarsController < ApplicationController DOT = Base64.decode64("R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==") - skip_before_filter :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_letter, :show_letter_svg] + skip_before_filter :preload_json, :redirect_to_login_if_required, :check_xhr, :verify_authenticity_token, only: [:show, :show_letter] def refresh_gravatar user = User.find_by(username_lower: params[:username].downcase) @@ -19,30 +19,6 @@ class UserAvatarsController < ApplicationController end end - def show_letter_svg - params.require(:username) - params.require(:version) - params.require(:size) - - no_cookies - - size = params[:size].to_i - username = params[:username] - - identity = LetterAvatar::Identity.from_username(username) - color = identity.color - - svg = <<-SVG - <svg xmlns="http://www.w3.org/2000/svg" width="#{size}" height="#{size}"> - <rect width="100%" height="100%" fill="rgb(#{color[0]},#{color[1]},#{color[2]})" /> - <text font-size="#{size * 0.7}" font-weight="normal" font-family="Helvetica, sans-serif" fill="#FFF" fill-opacity=".8" text-anchor="middle" x="50%" y="75%">#{username[0].capitalize}</text> - </svg> - SVG - - expires_in 1.year, public: true - render inline: svg, content_type: "image/svg+xml" - end - def show_letter params.require(:username) params.require(:version) diff --git a/app/models/user.rb b/app/models/user.rb index cd2344564..2861fff88 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -457,7 +457,7 @@ class User < ActiveRecord::Base avatar_template = split_avatars[hash.abs % split_avatars.size] end else - letter_avatar_template(username) + "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" end end @@ -469,8 +469,7 @@ class User < ActiveRecord::Base end def self.letter_avatar_template(username) - extension = SiteSetting.svg_letter_avatars ? "svg" : "png" - "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.#{extension}" + "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" end def avatar_template diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 921edd59e..fcd190dc8 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -979,8 +979,6 @@ en: avatar_sizes: "List of automatically generated avatar sizes." - svg_letter_avatars: "Use SVG for letter avatars" - 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." diff --git a/config/routes.rb b/config/routes.rb index f5b5aa1b3..66125570f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -299,7 +299,6 @@ Discourse::Application.routes.draw do get "user-badges/:username" => "user_badges#username", constraints: {username: USERNAME_ROUTE_FORMAT} post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", constraints: {username: USERNAME_ROUTE_FORMAT} - get "letter_avatar/:username/:size/:version.svg" => "user_avatars#show_letter_svg", format: :svg, constraints: { hostname: /[\w\.-]+/, size: /\d+/, 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 } diff --git a/config/site_settings.yml b/config/site_settings.yml index 0c4950ad8..754e067b8 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -572,9 +572,6 @@ files: avatar_sizes: default: '20|25|32|45|60|120' type: list - svg_letter_avatars: - default: false - client: true trust: default_trust_level: diff --git a/lib/email/styles.rb b/lib/email/styles.rb index e82106b96..22f9ffe1e 100644 --- a/lib/email/styles.rb +++ b/lib/email/styles.rb @@ -30,6 +30,7 @@ module Email # images @fragment.css('img').each do |img| + next if img['class'] == 'site-logo' if img['class'] == "emoji" || img['src'] =~ /plugins\/emoji/ @@ -57,6 +58,7 @@ module Email # attachments @fragment.css('a.attachment').each do |a| + # ensure all urls are absolute if a['href'] =~ /^\/[^\/]/ a['href'] = "#{Discourse.base_url}#{a['href']}" diff --git a/lib/letter_avatar.rb b/lib/letter_avatar.rb index e49ab6c41..abde76ec0 100644 --- a/lib/letter_avatar.rb +++ b/lib/letter_avatar.rb @@ -7,21 +7,21 @@ class LetterAvatar FULLSIZE = 120 * 3 POINTSIZE = 280 - class Identity - attr_accessor :color, :letter - - def self.from_username(username) - identity = new - identity.color = LetterAvatar::COLORS[ - Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length - ] - identity.letter = username[0].upcase - identity - end - end - class << self + class Identity + attr_accessor :color, :letter + + def self.from_username(username) + identity = new + identity.color = LetterAvatar::COLORS[ + Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length + ] + identity.letter = username[0].upcase + identity + end + end + def version "#{VERSION}_#{image_magick_version}" end @@ -32,7 +32,7 @@ class LetterAvatar def generate(username, size, opts = nil) DistributedMutex.synchronize("letter_avatar_#{version}_#{username}") do - identity = LetterAvatar::Identity.from_username(username) + identity = Identity.from_username(username) cache = true cache = false if opts && opts[:cache] == false From 2742602254c177ca014109c5ab60bafa3ccbfa47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Fri, 11 Sep 2015 02:12:40 +0200 Subject: [PATCH 10/47] FEATURE: support for external letter avatars service --- .../discourse/helpers/application.js.es6 | 8 +- .../discourse/helpers/user-avatar.js.es6 | 11 +- .../discourse/lib/avatar-template.js.es6 | 27 +- .../discourse/models/user-action.js.es6 | 55 +-- .../javascripts/discourse/models/user.js.es6 | 336 +++++++----------- .../discourse/views/composer.js.es6 | 7 +- app/controllers/users_controller.rb | 2 +- app/models/user.rb | 23 +- app/serializers/basic_post_serializer.rb | 6 + app/serializers/basic_user_serializer.rb | 10 +- app/serializers/post_serializer.rb | 3 +- app/serializers/user_action_serializer.rb | 12 +- app/serializers/user_name_serializer.rb | 21 +- app/serializers/user_serializer.rb | 1 + config/locales/server.en.yml | 3 + config/site_settings.yml | 7 + 16 files changed, 250 insertions(+), 282 deletions(-) diff --git a/app/assets/javascripts/discourse/helpers/application.js.es6 b/app/assets/javascripts/discourse/helpers/application.js.es6 index cac2ea1f0..02fa08fa8 100644 --- a/app/assets/javascripts/discourse/helpers/application.js.es6 +++ b/app/assets/javascripts/discourse/helpers/application.js.es6 @@ -9,12 +9,14 @@ Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) { return new safe("<div class='avatar-placeholder'></div>"); } - const username = Em.get(user, 'username'); + const username = Em.get(user, 'username'), + letterAvatarColor = Em.get(user, 'letter_avatar_color'); + 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') || avatarTemplate(username, uploadId, letterAvatarColor); return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar })); -}, 'username', 'uploaded_avatar_id', 'avatar_template'); +}, 'username', 'uploaded_avatar_id', 'letter_avatar_color', 'avatar_template'); /* * Used when we only have a template diff --git a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 index 1ab668ffc..aea2e9baa 100644 --- a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 +++ b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 @@ -5,20 +5,20 @@ function renderAvatar(user, options) { options = options || {}; if (user) { - var username = Em.get(user, 'username'); + let username = Em.get(user, 'username'); if (!username) { if (!options.usernamePath) { return ''; } username = Em.get(user, options.usernamePath); } - var title; + 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 @@ -28,13 +28,14 @@ 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'); + const uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id'), + letterAvatarColor = Em.get(user, 'letter_avatar_color') || Em.get(user, 'user.letter_avatar_color'); return Discourse.Utilities.avatarImg({ size: options.imageSize, extraClasses: Em.get(user, 'extras') || options.extraClasses, title: title || username, - avatarTemplate: avatarTemplate(username, uploadedAvatarId) + avatarTemplate: Em.get("avatar_template") || avatarTemplate(username, uploadedAvatarId, letterAvatarColor) }); } else { return ''; diff --git a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 index 542e97959..731a2047d 100644 --- a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 +++ b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 @@ -2,8 +2,10 @@ import { hashString } from 'discourse/lib/hash'; let _splitAvatars; -function defaultAvatar(username) { - const defaultAvatars = Discourse.SiteSettings.default_avatars; +function defaultAvatar(username, letterAvatarColor) { + const defaultAvatars = Discourse.SiteSettings.default_avatars, + version = Discourse.LetterAvatarVersion; + if (defaultAvatars && defaultAvatars.length) { _splitAvatars = _splitAvatars || defaultAvatars.split("\n"); @@ -13,20 +15,17 @@ function defaultAvatar(username) { } } - return Discourse.getURLWithCDN("/letter_avatar/" + - username.toLowerCase() + - "/{size}/" + - Discourse.LetterAvatarVersion + ".png"); + if (Discourse.SiteSettings.external_letter_avatars_enabled) { + const url = Discourse.SiteSettings.external_letter_avatars_url; + return `${url}/letter/${username[0]}?color=${letterAvatarColor}&size={size}`; + } else { + return Discourse.getURLWithCDN(`/letter_avatar/${username.toLowerCase()}/{size}/${version}.png`); + } } -export default function(username, uploadedAvatarId) { +export default function(username, uploadedAvatarId, letterAvatarColor) { if (uploadedAvatarId) { - return Discourse.getURLWithCDN("/user_avatar/" + - Discourse.BaseUrl + - "/" + - username.toLowerCase() + - "/{size}/" + - uploadedAvatarId + ".png"); + return Discourse.getURLWithCDN(`/user_avatar/${Discourse.BaseUrl}/${username.toLowerCase()}/{size}/${uploadedAvatarId}.png`); } - return defaultAvatar(username); + return defaultAvatar(username, letterAvatarColor); } diff --git a/app/assets/javascripts/discourse/models/user-action.js.es6 b/app/assets/javascripts/discourse/models/user-action.js.es6 index 2d273c43e..05e1e4929 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,23 @@ 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'), + letter_avatar_color: this.get('action_letter_avatar_color'), 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 01aef870a..8fac2812e 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -6,6 +6,7 @@ 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 +19,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 +35,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,38 +58,23 @@ const User = RestModel.extend({ return ('background-image: url(' + Discourse.getURLWithCDN(bgUrl) + ')').htmlSafe(); }, - 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), @@ -100,61 +83,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', @@ -179,10 +137,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; }); @@ -192,26 +150,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') }, @@ -219,73 +170,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) { @@ -294,12 +235,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) { @@ -311,30 +250,26 @@ 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'), + @computed("username", "uploaded_avatar_id", "letter_avatar_color") + avatarTemplate(username, uploadedAvatarId, letterAvatarColor) { + return avatarTemplate(username, uploadedAvatarId, letterAvatarColor); + }, /* Change avatar selection */ - pickAvatar: function(uploadId) { - var self = this; - - return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/avatar/pick", { + pickAvatar(uploadId) { + 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); - }); + }).then(() => this.set('uploaded_avatar_id', uploadId)); }, /** @@ -344,7 +279,7 @@ const User = RestModel.extend({ @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; @@ -357,35 +292,39 @@ const User = RestModel.extend({ @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, groupNames) { return Discourse.ajax('/invites', { type: 'POST', data: {email: email, group_names: groupNames} }); }, - generateInviteLink: function(email, groupNames, topicId) { + generateInviteLink(email, groupNames, topicId) { return Discourse.ajax('/invites/link', { type: 'POST', data: {email: email, group_names: groupNames, topic_id: topicId} }); }, - 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')) { @@ -398,27 +337,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 () {}); + }); } }); @@ -426,14 +364,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); @@ -441,56 +379,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/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index d96c6a55a..6964d102a 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -252,9 +252,12 @@ const ComposerView = Ember.View.extend(Ember.Evented, { const quotedPost = posts.findProperty("post_number", postNumber); if (quotedPost) { const username = quotedPost.get('username'), - uploadId = quotedPost.get('uploaded_avatar_id'); + uploadId = quotedPost.get('uploaded_avatar_id'), + letterAvatarColor = quotedPost.get("letter_avatar_color"); - return Discourse.Utilities.tinyAvatar(avatarTemplate(username, uploadId)); + debugger; + + return Discourse.Utilities.tinyAvatar(avatarTemplate(username, uploadId, letterAvatarColor)); } } } diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index d537c6b7a..cf72afcf6 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -518,7 +518,7 @@ class UsersController < ApplicationController user_fields = [:username, :upload_avatar_template, :uploaded_avatar_id] 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, :letter_avatar_color]) } if params[:include_groups] == "true" to_render[:groups] = Group.search_group(term, current_user).map {|m| {:name=>m.name, :usernames=> m.usernames.split(",")} } diff --git a/app/models/user.rb b/app/models/user.rb index 2861fff88..aa46dd7c7 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -457,11 +457,11 @@ class User < ActiveRecord::Base avatar_template = split_avatars[hash.abs % split_avatars.size] end else - "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" + letter_avatar_template(username) end end - def self.avatar_template(username,uploaded_avatar_id) + def self.avatar_template(username, uploaded_avatar_id) return default_template(username) if !uploaded_avatar_id username ||= "" hostname = RailsMultisite::ConnectionManagement.current_hostname @@ -469,11 +469,26 @@ class User < ActiveRecord::Base end def self.letter_avatar_template(username) - "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" + if SiteSetting.external_letter_avatars_enabled + color = letter_avatar_color(username) + "#{SiteSetting.external_letter_avatars_url}/letter/#{username[0]}?color=#{color}&size={size}" + else + "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" + end + end + + def letter_avatar_color + self.class.letter_avatar_color(username) + end + + def self.letter_avatar_color(username) + username = username || "" + color = LetterAvatar::COLORS[Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length] + color.map { |c| c.to_s(16) }.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. diff --git a/app/serializers/basic_post_serializer.rb b/app/serializers/basic_post_serializer.rb index 04b91ecad..4edb2b5cc 100644 --- a/app/serializers/basic_post_serializer.rb +++ b/app/serializers/basic_post_serializer.rb @@ -5,6 +5,7 @@ class BasicPostSerializer < ApplicationSerializer :username, :avatar_template, :uploaded_avatar_id, + :letter_avatar_color, :created_at, :cooked, :cooked_hidden @@ -25,9 +26,14 @@ class BasicPostSerializer < ApplicationSerializer object.user.try(:uploaded_avatar_id) end + def letter_avatar_color + object.user.try(:letter_avatar_color) + 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..12ed3f363 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, :uploaded_avatar_id, :avatar_template, :letter_avatar_color def include_name? SiteSetting.enable_names? @@ -17,4 +17,12 @@ class BasicUserSerializer < ApplicationSerializer object[:user] || object end + def letter_avatar_color + if Hash === object + User.letter_avatar_color(user[:username]) + else + object.letter_avatar_color + end + end + end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 5ba620ca2..9bc469f04 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -178,7 +178,8 @@ class PostSerializer < BasicPostSerializer { username: object.reply_to_user.username, avatar_template: object.reply_to_user.avatar_template, - uploaded_avatar_id: object.reply_to_user.uploaded_avatar_id + uploaded_avatar_id: object.reply_to_user.uploaded_avatar_id, + letter_avatar_color: object.reply_to_user.letter_avatar_color, } end diff --git a/app/serializers/user_action_serializer.rb b/app/serializers/user_action_serializer.rb index 8b3939963..4bf665004 100644 --- a/app/serializers/user_action_serializer.rb +++ b/app/serializers/user_action_serializer.rb @@ -27,9 +27,11 @@ class UserActionSerializer < ApplicationSerializer :edit_reason, :category_id, :uploaded_avatar_id, + :letter_avatar_color, :closed, :archived, - :acting_uploaded_avatar_id + :acting_uploaded_avatar_id, + :acting_letter_avatar_color def excerpt cooked = object.cooked || PrettyText.cook(object.raw) @@ -84,4 +86,12 @@ class UserActionSerializer < ApplicationSerializer object.topic_archived end + def letter_avatar_color + User.letter_avatar_color(username) + end + + def acting_letter_avatar_color + User.letter_avatar_color(acting_username) + end + end 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 895453990..2d216a515 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -323,4 +323,5 @@ class UserSerializer < BasicUserSerializer def pending_count 0 end + end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index fcd190dc8..bfab232cf 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -979,6 +979,9 @@ en: avatar_sizes: "List of automatically generated avatar sizes." + external_letter_avatars_enabled: "Use external letter avatars service." + external_letter_avatars_url: "URL of the external letter avatars service." + 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." diff --git a/config/site_settings.yml b/config/site_settings.yml index 754e067b8..add19d2db 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -572,6 +572,13 @@ files: avatar_sizes: default: '20|25|32|45|60|120' type: list + external_letter_avatars_enabled: + default: false + client: true + external_letter_avatars_url: + default: "https://avatars.discourse.org" + client: true + regex: '^https?:\/\/.+[^\/]$' trust: default_trust_level: From f6380c66efedeceb646fe0fb2b4507ac4f0d4d0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Fri, 11 Sep 2015 02:15:45 +0200 Subject: [PATCH 11/47] oooops --- app/assets/javascripts/discourse/views/composer.js.es6 | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index 6964d102a..63a8675ec 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -255,8 +255,6 @@ const ComposerView = Ember.View.extend(Ember.Evented, { uploadId = quotedPost.get('uploaded_avatar_id'), letterAvatarColor = quotedPost.get("letter_avatar_color"); - debugger; - return Discourse.Utilities.tinyAvatar(avatarTemplate(username, uploadId, letterAvatarColor)); } } From 90d49d1497ce0b4969d6e624e0e54dfae4fa759e Mon Sep 17 00:00:00 2001 From: Sam <sam.saffron@gmail.com> Date: Fri, 11 Sep 2015 13:18:07 +1000 Subject: [PATCH 12/47] correct paths used for external service --- app/assets/javascripts/discourse/lib/avatar-template.js.es6 | 2 +- app/models/user.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 index 731a2047d..4d2fdb2bf 100644 --- a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 +++ b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 @@ -17,7 +17,7 @@ function defaultAvatar(username, letterAvatarColor) { if (Discourse.SiteSettings.external_letter_avatars_enabled) { const url = Discourse.SiteSettings.external_letter_avatars_url; - return `${url}/letter/${username[0]}?color=${letterAvatarColor}&size={size}`; + return `${url}/letter/${username[0]}/${letterAvatarColor}/{size}.png`; } else { return Discourse.getURLWithCDN(`/letter_avatar/${username.toLowerCase()}/{size}/${version}.png`); } diff --git a/app/models/user.rb b/app/models/user.rb index aa46dd7c7..75d732ea6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -454,7 +454,7 @@ 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 letter_avatar_template(username) @@ -471,7 +471,7 @@ class User < ActiveRecord::Base def self.letter_avatar_template(username) if SiteSetting.external_letter_avatars_enabled color = letter_avatar_color(username) - "#{SiteSetting.external_letter_avatars_url}/letter/#{username[0]}?color=#{color}&size={size}" + "#{SiteSetting.external_letter_avatars_url}/letter/#{username[0]}/#{color}/{size}.png" else "#{Discourse.base_uri}/letter_avatar/#{username.downcase}/{size}/#{LetterAvatar.version}.png" end From 98e8b16c34f1453772eaf3411561ee5c9ad82d40 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan <tgx_world@hotmail.com> Date: Fri, 11 Sep 2015 11:54:08 +0800 Subject: [PATCH 13/47] FIX: Broken BasicUserSerializer. --- app/serializers/basic_user_serializer.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/serializers/basic_user_serializer.rb b/app/serializers/basic_user_serializer.rb index 12ed3f363..2c72eb342 100644 --- a/app/serializers/basic_user_serializer.rb +++ b/app/serializers/basic_user_serializer.rb @@ -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 @@ -21,7 +21,7 @@ class BasicUserSerializer < ApplicationSerializer if Hash === object User.letter_avatar_color(user[:username]) else - object.letter_avatar_color + user.try(:letter_avatar_color) end end From d73d4d476984d6de647423d3869cfdb1d72c427a Mon Sep 17 00:00:00 2001 From: Arpit Jalan <arpit@techapj.com> Date: Fri, 11 Sep 2015 16:53:26 +0530 Subject: [PATCH 14/47] FIX: UserNameSuggester should not suggest usernames with a sequence of 2 or more special chars --- config/locales/server.en.yml | 2 +- lib/user_name_suggester.rb | 1 + spec/components/user_name_suggester_spec.rb | 5 +++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index bfab232cf..735f21496 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1347,7 +1347,7 @@ en: unique: "must be unique" blank: "must be present" 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" + 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: 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/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 From 8ca2ab1b3b25a6bc1ebcdc4e7e02d1333043b766 Mon Sep 17 00:00:00 2001 From: ismail-arilik <arilik.ismail@gmail.com> Date: Fri, 11 Sep 2015 15:02:12 +0300 Subject: [PATCH 15/47] Update some strings to meet referred options lists The options which changed strings are referred, were changed to lists so these strings were supposed to be generalized. --- config/locales/server.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 735f21496..7dd33c874 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1202,8 +1202,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." - default_other_auto_track_topics_after_msecs: "Global default milliseconds before a topic is automatically tracked." + 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." From 6437cd03413a346976efef3e0a11a5eba0e2cf9c Mon Sep 17 00:00:00 2001 From: Sam <sam.saffron@gmail.com> Date: Fri, 11 Sep 2015 18:14:34 +1000 Subject: [PATCH 16/47] FEATURE: add support for generic external avatar services This changes it so we only ship an avatar template down to the client it has no magic, all it knows is how to plug in size --- .../discourse/components/who-liked.js.es6 | 2 +- .../discourse/helpers/application.js.es6 | 11 ++----- .../discourse/helpers/user-avatar.js.es6 | 17 ++++------ .../discourse/lib/avatar-template.js.es6 | 31 ------------------- .../discourse/models/composer.js.es6 | 4 +-- .../discourse/models/user-action.js.es6 | 2 -- .../javascripts/discourse/models/user.js.es6 | 6 ---- .../templates/components/stream-item.hbs | 2 +- .../templates/list/posters-column.raw.hbs | 2 +- .../discourse/views/composer.js.es6 | 7 +---- app/assets/javascripts/main_include.js | 2 -- app/controllers/users_controller.rb | 2 +- app/models/user.rb | 15 ++++++--- app/serializers/basic_post_serializer.rb | 10 ------ app/serializers/basic_user_serializer.rb | 10 +----- app/serializers/post_serializer.rb | 4 +-- app/serializers/user_action_serializer.rb | 14 +-------- config/locales/server.en.yml | 4 +-- config/site_settings.yml | 8 ++--- 19 files changed, 35 insertions(+), 118 deletions(-) delete mode 100644 app/assets/javascripts/discourse/lib/avatar-template.js.es6 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/helpers/application.js.es6 b/app/assets/javascripts/discourse/helpers/application.js.es6 index 02fa08fa8..5c72c6fc8 100644 --- a/app/assets/javascripts/discourse/helpers/application.js.es6 +++ b/app/assets/javascripts/discourse/helpers/application.js.es6 @@ -1,22 +1,17 @@ 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', function(user, size) { if (Em.isEmpty(user)) { return new safe("<div class='avatar-placeholder'></div>"); } - const username = Em.get(user, 'username'), - letterAvatarColor = Em.get(user, 'letter_avatar_color'); - - if (arguments.length < 4) { uploadId = Em.get(user, 'uploaded_avatar_id'); } - const avatar = Em.get(user, 'avatar_template') || avatarTemplate(username, uploadId, letterAvatarColor); + const avatar = Em.get(user, 'avatar_template'); return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar })); -}, 'username', 'uploaded_avatar_id', 'letter_avatar_color', 'avatar_template'); +}, 'username', 'avatar_template'); /* * Used when we only have a template diff --git a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 index aea2e9baa..c5eac31ad 100644 --- a/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 +++ b/app/assets/javascripts/discourse/helpers/user-avatar.js.es6 @@ -1,15 +1,14 @@ import registerUnbound from 'discourse/helpers/register-unbound'; -import avatarTemplate from 'discourse/lib/avatar-template'; function renderAvatar(user, options) { options = options || {}; if (user) { - let username = Em.get(user, 'username'); - if (!username) { - if (!options.usernamePath) { return ''; } - username = Em.get(user, options.usernamePath); - } + + 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) { @@ -27,15 +26,11 @@ function renderAvatar(user, options) { } } - // this is simply done to ensure we cache images correctly - const uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id'), - letterAvatarColor = Em.get(user, 'letter_avatar_color') || Em.get(user, 'user.letter_avatar_color'); - return Discourse.Utilities.avatarImg({ size: options.imageSize, extraClasses: Em.get(user, 'extras') || options.extraClasses, title: title || username, - avatarTemplate: Em.get("avatar_template") || avatarTemplate(username, uploadedAvatarId, letterAvatarColor) + avatarTemplate: avatarTemplate }); } else { return ''; 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 4d2fdb2bf..000000000 --- a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 +++ /dev/null @@ -1,31 +0,0 @@ -import { hashString } from 'discourse/lib/hash'; - -let _splitAvatars; - -function defaultAvatar(username, letterAvatarColor) { - const defaultAvatars = Discourse.SiteSettings.default_avatars, - version = Discourse.LetterAvatarVersion; - - if (defaultAvatars && defaultAvatars.length) { - _splitAvatars = _splitAvatars || defaultAvatars.split("\n"); - - if (_splitAvatars.length) { - const hash = hashString(username); - return _splitAvatars[Math.abs(hash) % _splitAvatars.length]; - } - } - - if (Discourse.SiteSettings.external_letter_avatars_enabled) { - const url = Discourse.SiteSettings.external_letter_avatars_url; - return `${url}/letter/${username[0]}/${letterAvatarColor}/{size}.png`; - } else { - return Discourse.getURLWithCDN(`/letter_avatar/${username.toLowerCase()}/{size}/${version}.png`); - } -} - -export default function(username, uploadedAvatarId, letterAvatarColor) { - if (uploadedAvatarId) { - return Discourse.getURLWithCDN(`/user_avatar/${Discourse.BaseUrl}/${username.toLowerCase()}/{size}/${uploadedAvatarId}.png`); - } - return defaultAvatar(username, letterAvatarColor); -} diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 12cba4936..9cb5db069 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -567,7 +567,7 @@ 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'), actions_summary: [], @@ -587,7 +587,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') } }); } diff --git a/app/assets/javascripts/discourse/models/user-action.js.es6 b/app/assets/javascripts/discourse/models/user-action.js.es6 index 05e1e4929..f03d81908 100644 --- a/app/assets/javascripts/discourse/models/user-action.js.es6 +++ b/app/assets/javascripts/discourse/models/user-action.js.es6 @@ -154,8 +154,6 @@ const UserAction = RestModel.extend({ switchToActing() { this.setProperties({ username: this.get('acting_username'), - uploaded_avatar_id: this.get('acting_uploaded_avatar_id'), - letter_avatar_color: this.get('action_letter_avatar_color'), name: this.get('actingDisplayName') }); } diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 8fac2812e..4fab93f2d 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -1,6 +1,5 @@ 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'; @@ -257,11 +256,6 @@ const User = RestModel.extend({ }); }, - @computed("username", "uploaded_avatar_id", "letter_avatar_color") - avatarTemplate(username, uploadedAvatarId, letterAvatarColor) { - return avatarTemplate(username, uploadedAvatarId, letterAvatarColor); - }, - /* Change avatar selection */ 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/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/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index 63a8675ec..5d62db416 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -1,7 +1,6 @@ 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'; @@ -251,11 +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'), - letterAvatarColor = quotedPost.get("letter_avatar_color"); - - return Discourse.Utilities.tinyAvatar(avatarTemplate(username, uploadId, letterAvatarColor)); + return Discourse.Utilities.tinyAvatar(quotedPost.get('avatar_template')); } } } diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 797cab865..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 @@ -41,7 +40,6 @@ //= 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 diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index cf72afcf6..22334cc9b 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -518,7 +518,7 @@ class UsersController < ApplicationController user_fields = [:username, :upload_avatar_template, :uploaded_avatar_id] user_fields << :name if SiteSetting.enable_names? - to_render = { users: results.as_json(only: user_fields, methods: [:avatar_template, :letter_avatar_color]) } + 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(",")} } diff --git a/app/models/user.rb b/app/models/user.rb index 75d732ea6..4c8e1d9aa 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -457,7 +457,7 @@ class User < ActiveRecord::Base split_avatars[hash.abs % split_avatars.size] end else - letter_avatar_template(username) + system_avatar_template(username) end end @@ -468,10 +468,15 @@ class User < ActiveRecord::Base UserAvatar.local_avatar_template(hostname, username.downcase, uploaded_avatar_id) end - def self.letter_avatar_template(username) - if SiteSetting.external_letter_avatars_enabled + 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 color = letter_avatar_color(username) - "#{SiteSetting.external_letter_avatars_url}/letter/#{username[0]}/#{color}/{size}.png" + url = SiteSetting.external_system_avatars_url.dup + url.gsub! "{color}", color + 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 @@ -484,7 +489,7 @@ class User < ActiveRecord::Base def self.letter_avatar_color(username) username = username || "" color = LetterAvatar::COLORS[Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length] - color.map { |c| c.to_s(16) }.join + color.map { |c| c.to_s(16).rjust(2, '0') }.join end def avatar_template diff --git a/app/serializers/basic_post_serializer.rb b/app/serializers/basic_post_serializer.rb index 4edb2b5cc..8969d19a0 100644 --- a/app/serializers/basic_post_serializer.rb +++ b/app/serializers/basic_post_serializer.rb @@ -4,8 +4,6 @@ class BasicPostSerializer < ApplicationSerializer :name, :username, :avatar_template, - :uploaded_avatar_id, - :letter_avatar_color, :created_at, :cooked, :cooked_hidden @@ -22,14 +20,6 @@ class BasicPostSerializer < ApplicationSerializer object.user.try(:avatar_template) end - def uploaded_avatar_id - object.user.try(:uploaded_avatar_id) - end - - def letter_avatar_color - object.user.try(:letter_avatar_color) - end - def cooked_hidden object.hidden && !scope.is_staff? end diff --git a/app/serializers/basic_user_serializer.rb b/app/serializers/basic_user_serializer.rb index 2c72eb342..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, :letter_avatar_color + attributes :id, :username, :avatar_template def include_name? SiteSetting.enable_names? @@ -17,12 +17,4 @@ class BasicUserSerializer < ApplicationSerializer object[:user] || object end - def letter_avatar_color - if Hash === object - User.letter_avatar_color(user[:username]) - else - user.try(:letter_avatar_color) - end - end - end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 9bc469f04..a10f9dbf6 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -177,9 +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, - letter_avatar_color: object.reply_to_user.letter_avatar_color, + avatar_template: object.reply_to_user.avatar_template } end diff --git a/app/serializers/user_action_serializer.rb b/app/serializers/user_action_serializer.rb index 4bf665004..d9609ff9b 100644 --- a/app/serializers/user_action_serializer.rb +++ b/app/serializers/user_action_serializer.rb @@ -26,12 +26,8 @@ class UserActionSerializer < ApplicationSerializer :action_code, :edit_reason, :category_id, - :uploaded_avatar_id, - :letter_avatar_color, :closed, - :archived, - :acting_uploaded_avatar_id, - :acting_letter_avatar_color + :archived def excerpt cooked = object.cooked || PrettyText.cook(object.raw) @@ -86,12 +82,4 @@ class UserActionSerializer < ApplicationSerializer object.topic_archived end - def letter_avatar_color - User.letter_avatar_color(username) - end - - def acting_letter_avatar_color - User.letter_avatar_color(acting_username) - end - end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 7dd33c874..1309a124e 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -979,8 +979,8 @@ en: avatar_sizes: "List of automatically generated avatar sizes." - external_letter_avatars_enabled: "Use external letter avatars service." - external_letter_avatars_url: "URL of the external letter avatars service." + 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." diff --git a/config/site_settings.yml b/config/site_settings.yml index add19d2db..5c96a0385 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -572,13 +572,13 @@ files: avatar_sizes: default: '20|25|32|45|60|120' type: list - external_letter_avatars_enabled: + external_system_avatars_enabled: default: false client: true - external_letter_avatars_url: - default: "https://avatars.discourse.org" + external_system_avatars_url: + default: "https://avatars.discourse.org/letter/{first_letter}/{color}/{size}.png" client: true - regex: '^https?:\/\/.+[^\/]$' + regex: '^https?:\/\/.+[^\/]' trust: default_trust_level: From 0c58f08207677eae93aad0ead17ef5a91d3dfb63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Fri, 11 Sep 2015 12:56:34 +0200 Subject: [PATCH 17/47] FIX: profile picture selector --- .../components/avatar-uploader.js.es6 | 17 +++++--- .../controllers/avatar-selector.js.es6 | 41 +++++++++++------- .../discourse/helpers/application.js.es6 | 15 +++---- .../javascripts/discourse/models/user.js.es6 | 34 +++++---------- .../discourse/routes/preferences.js.es6 | 43 ++++++++++--------- .../templates/modal/avatar_selector.hbs | 21 ++++----- .../discourse/views/avatar-selector.js.es6 | 12 ++++-- app/controllers/user_avatars_controller.rb | 5 ++- app/models/user.rb | 7 +-- app/serializers/user_serializer.rb | 22 ++++++++++ .../lib/avatar-template-test.js.es6 | 16 ------- 11 files changed, 116 insertions(+), 117 deletions(-) delete mode 100644 test/javascripts/lib/avatar-template-test.js.es6 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/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/helpers/application.js.es6 b/app/assets/javascripts/discourse/helpers/application.js.es6 index 5c72c6fc8..2f4ea292c 100644 --- a/app/assets/javascripts/discourse/helpers/application.js.es6 +++ b/app/assets/javascripts/discourse/helpers/application.js.es6 @@ -3,32 +3,27 @@ import { longDate, autoUpdatingRelativeAge, number } from 'discourse/lib/formatt const safe = Handlebars.SafeString; -Em.Handlebars.helper('bound-avatar', function(user, size) { +Em.Handlebars.helper('bound-avatar', (user, size) => { if (Em.isEmpty(user)) { return new safe("<div class='avatar-placeholder'></div>"); } const avatar = Em.get(user, 'avatar_template'); - return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar })); }, '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/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 4fab93f2d..48b84011b 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -256,47 +256,33 @@ const User = RestModel.extend({ }); }, - /* - Change avatar selection - */ - pickAvatar(uploadId) { + pickAvatar(upload_id, avatar_template) { return Discourse.ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, { type: 'PUT', - data: { upload_id: uploadId } - }).then(() => this.set('uploaded_avatar_id', uploadId)); + data: { upload_id } + }).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(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(email, groupNames) { + createInvite(email, group_names) { return Discourse.ajax('/invites', { type: 'POST', - data: {email: email, group_names: groupNames} + data: { email, group_names } }); }, - generateInviteLink(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 } }); }, diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index d748689f4..e1f9d597d 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -18,50 +18,51 @@ 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"); - // 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, 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/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/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/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/models/user.rb b/app/models/user.rb index 4c8e1d9aa..b00ac21b3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -471,9 +471,8 @@ class User < ActiveRecord::Base 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 - color = letter_avatar_color(username) url = SiteSetting.external_system_avatars_url.dup - url.gsub! "{color}", color + url.gsub! "{color}", letter_avatar_color(username) url.gsub! "{username}", username url.gsub! "{first_letter}", username[0].downcase url @@ -482,10 +481,6 @@ class User < ActiveRecord::Base end end - def letter_avatar_color - self.class.letter_avatar_color(username) - end - def self.letter_avatar_color(username) username = username || "" color = LetterAvatar::COLORS[Digest::MD5.hexdigest(username)[0...15].to_i(16) % LetterAvatar::COLORS.length] diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index 2d216a515..334b20e2d 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -93,8 +93,12 @@ class UserSerializer < BasicUserSerializer :watched_category_ids, :private_messages_stats, :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, @@ -278,14 +282,32 @@ 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 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 has_title_badges object.badges.where(allow_title: true).count > 0 end 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; -}); - From 569f2815d1cff5c9bd749cb4c67cc22424eea0e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Fri, 11 Sep 2015 14:44:14 +0200 Subject: [PATCH 18/47] FIX: ensure we still works with cookies off --- app/assets/javascripts/discourse/lib/key-value-store.js.es6 | 1 + 1 file changed, 1 insertion(+) 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 30f86b16e..243146833 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); }, From 93f9dcfcec9fbbdef247076e3930243843cb0943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Fri, 11 Sep 2015 15:04:29 +0200 Subject: [PATCH 19/47] FIX: don't overwrite custom uploaded avatar when selecting gravatar FIX: remove unecessary serialized fields --- app/assets/javascripts/discourse/models/user.js.es6 | 4 ++-- .../javascripts/discourse/routes/preferences.js.es6 | 7 +++++-- app/controllers/users_controller.rb | 13 ++++++------- app/models/upload.rb | 2 +- app/models/user.rb | 4 ++-- app/models/user_avatar.rb | 6 ++---- app/serializers/admin_post_serializer.rb | 6 +----- app/serializers/post_action_user_serializer.rb | 4 ---- app/serializers/topic_post_count_serializer.rb | 4 ---- lib/avatar_lookup.rb | 5 +---- 10 files changed, 20 insertions(+), 35 deletions(-) diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 48b84011b..bd0e36c5f 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -256,10 +256,10 @@ const User = RestModel.extend({ }); }, - pickAvatar(upload_id, avatar_template) { + pickAvatar(upload_id, type, avatar_template) { return Discourse.ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, { type: 'PUT', - data: { upload_id } + data: { upload_id, type } }).then(() => this.setProperties({ avatar_template, uploaded_avatar_id: upload_id diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index e1f9d597d..8bea9b460 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -49,9 +49,12 @@ export default RestrictedUserRoute.extend({ const user = this.modelFor('user'), controller = this.controllerFor('avatar-selector'), selectedUploadId = controller.get("selectedUploadId"), - selectedAvatarTemplate = controller.get("selectedAvatarTemplate"); + selectedAvatarTemplate = controller.get("selectedAvatarTemplate"), + type = controller.get("selected"); - user.pickAvatar(selectedUploadId, selectedAvatarTemplate) + if (type === "uploaded") { type = "custom" } + + user.pickAvatar(selectedUploadId, type, selectedAvatarTemplate) .then(() => { user.setProperties(controller.getProperties( 'system_avatar_template', diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 22334cc9b..ab1104e8d 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]) } 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 @@ -533,12 +533,11 @@ class UsersController < ApplicationController upload_id = params[:upload_id] - user.uploaded_avatar_id = upload_id + type = params[:type] + type = "custom" if type == "uploaded" - # ensure we associate the custom avatar properly - if upload_id && user.user_avatar.custom_upload_id != upload_id - user.user_avatar.custom_upload_id = upload_id - end + user.uploaded_avatar_id = upload_id + user.user_avatar.send("#{type}_upload_id=", upload_id) user.save! user.user_avatar.save! diff --git a/app/models/upload.rb b/app/models/upload.rb index 39e5fb94e..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 diff --git a/app/models/user.rb b/app/models/user.rb index b00ac21b3..a17e4e9ba 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -462,8 +462,8 @@ class User < ActiveRecord::Base end def self.avatar_template(username, uploaded_avatar_id) - return default_template(username) if !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 @@ -482,7 +482,7 @@ class User < ActiveRecord::Base end def self.letter_avatar_color(username) - username = username || "" + username ||= "" 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 diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index f61df736b..dd17fd6e3 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) 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) end def self.external_avatar_template(user_id, upload_id) 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/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/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/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 From a28df555181b75a1fd2ac8e6f1e8dcf6d974e7b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Fri, 11 Sep 2015 15:06:17 +0200 Subject: [PATCH 20/47] fix the build --- app/assets/javascripts/discourse/routes/preferences.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index 8bea9b460..fd9bba95d 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -49,9 +49,9 @@ export default RestrictedUserRoute.extend({ const user = this.modelFor('user'), controller = this.controllerFor('avatar-selector'), selectedUploadId = controller.get("selectedUploadId"), - selectedAvatarTemplate = controller.get("selectedAvatarTemplate"), - type = controller.get("selected"); + selectedAvatarTemplate = controller.get("selectedAvatarTemplate"); + let type = controller.get("selected"); if (type === "uploaded") { type = "custom" } user.pickAvatar(selectedUploadId, type, selectedAvatarTemplate) From 8128abe6b91b2d8f6496384ac6f32d0d3e1c6701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Fri, 11 Sep 2015 15:10:38 +0200 Subject: [PATCH 21/47] ES6ify user preferences controller --- .../discourse/controllers/preferences.js.es6 | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) 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") }); From 29f25dbf6e9bdfa17e41a3a660cac68e695c1759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Fri, 11 Sep 2015 15:18:17 +0200 Subject: [PATCH 22/47] fix the build --- app/models/user_avatar.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index dd17fd6e3..8d57472c6 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -39,7 +39,7 @@ class UserAvatar < ActiveRecord::Base end def self.local_avatar_url(hostname, username, upload_id, size) - self.local_avatar_template(hostname, username, upload_id).gsub("{size}", size) + self.local_avatar_template(hostname, username, upload_id).gsub("{size}", size.to_s) end def self.local_avatar_template(hostname, username, upload_id) @@ -48,7 +48,7 @@ class UserAvatar < ActiveRecord::Base end def self.external_avatar_url(user_id, upload_id, size) - self.external_avatar_template(user_id, upload_id).gsub("{size}", size) + self.external_avatar_template(user_id, upload_id).gsub("{size}", size.to_s) end def self.external_avatar_template(user_id, upload_id) From 18d7c1c75d7f346b68a63ee1dd8943a674a3a719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Fri, 11 Sep 2015 15:47:48 +0200 Subject: [PATCH 23/47] fix the build - take 2 --- .../javascripts/discourse/routes/preferences.js.es6 | 6 ++---- app/controllers/users_controller.rb | 11 +++++++---- spec/controllers/users_controller_spec.rb | 13 +++---------- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/discourse/routes/preferences.js.es6 b/app/assets/javascripts/discourse/routes/preferences.js.es6 index fd9bba95d..3ae6aed00 100644 --- a/app/assets/javascripts/discourse/routes/preferences.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences.js.es6 @@ -49,10 +49,8 @@ export default RestrictedUserRoute.extend({ const user = this.modelFor('user'), controller = this.controllerFor('avatar-selector'), selectedUploadId = controller.get("selectedUploadId"), - selectedAvatarTemplate = controller.get("selectedAvatarTemplate"); - - let type = controller.get("selected"); - if (type === "uploaded") { type = "custom" } + selectedAvatarTemplate = controller.get("selectedAvatarTemplate"), + type = controller.get("selected"); user.pickAvatar(selectedUploadId, type, selectedAvatarTemplate) .then(() => { diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index ab1104e8d..bb361de6f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -531,13 +531,16 @@ class UsersController < ApplicationController user = fetch_user_from_params guardian.ensure_can_edit!(user) + type = params[:type] upload_id = params[:upload_id] - type = params[:type] - type = "custom" if type == "uploaded" - user.uploaded_avatar_id = upload_id - user.user_avatar.send("#{type}_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! user.user_avatar.save! 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 From 460243d7a319781f1742ccb08c859943098910a5 Mon Sep 17 00:00:00 2001 From: Kane York <rikingcoding@gmail.com> Date: Fri, 11 Sep 2015 08:29:44 -0700 Subject: [PATCH 24/47] FIX: Give 403 for deleted topics, +lots of tests --- lib/topic_view.rb | 4 +- spec/controllers/topics_controller_spec.rb | 120 +++++++++++++++++++++ spec/spec_helper.rb | 1 - 3 files changed, 122 insertions(+), 3 deletions(-) diff --git a/lib/topic_view.rb b/lib/topic_view.rb index 54483efd5..333e155b4 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -355,8 +355,8 @@ 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 diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index fd927ebac..e87a27f48 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 @@ -554,6 +572,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/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 From 4b43edee91e0007c7f8b8d0baadcaea9cf3210e1 Mon Sep 17 00:00:00 2001 From: Neil Lalonde <neillalonde@gmail.com> Date: Thu, 10 Sep 2015 18:17:00 -0400 Subject: [PATCH 25/47] UX: mobile topic list and suggested topics: show new/unread counts OR total post count, not both. --- .../templates/list/post-count-or-badges.raw.hbs | 5 +++++ .../templates/mobile/components/basic-topic-list.hbs | 12 ++++++------ .../templates/mobile/list/topic_list_item.raw.hbs | 7 ++----- .../discourse/views/list/post-count-or-badges.js.es6 | 6 ++++++ app/assets/stylesheets/mobile/topic-list.scss | 2 +- 5 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/discourse/templates/list/post-count-or-badges.raw.hbs create mode 100644 app/assets/javascripts/discourse/views/list/post-count-or-badges.js.es6 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..6ac240e75 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/list/post-count-or-badges.raw.hbs @@ -0,0 +1,5 @@ +{{#if view.showBadges}} + {{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}} +{{else}} + {{raw "list/posts-count-column" topic=topic tagName="div"}} +{{/if}} \ No newline at end of file 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 611d012cc..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"> @@ -25,10 +21,14 @@ </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/list/topic_list_item.raw.hbs b/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs index 01937421a..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 @@ -2,13 +2,10 @@ <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/posts-count-column" topic=content tagName="div"}} + {{raw "list/post-count-or-badges" topic=content postBadgesEnabled=controller.showTopicPostBadges}} </div> <div class="clearfix"></div> @@ -22,8 +19,8 @@ <div class="pull-right"> <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/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..3c968935e --- /dev/null +++ b/app/assets/javascripts/discourse/views/list/post-count-or-badges.js.es6 @@ -0,0 +1,6 @@ +import computed from "ember-addons/ember-computed-decorators"; + +export default Ember.Object.extend({ + postCountsPresent: Ember.computed.or('topic.unread', 'topic.displayNewPosts', 'topic.unseen'), + showBadges: Ember.computed.and('postBadgesEnabled', 'postCountsPresent') +}); diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index 6922da71c..886e35487 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -357,7 +357,7 @@ td .main-link { padding: 5px 10px 5px 0; } } -.topic-list-item { +.topic-list { .posts-map { font-size: 1.071em; padding-top: 2px; From 637b110e76fb72b9983ffc2b34423674d8a0081f Mon Sep 17 00:00:00 2001 From: Neil Lalonde <neillalonde@gmail.com> Date: Fri, 11 Sep 2015 11:38:25 -0400 Subject: [PATCH 26/47] use setter in topic-tracking-state or else get assertion error --- .../javascripts/discourse/models/topic-tracking-state.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ded0d824c..11c8d67fe 100644 --- a/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 @@ -174,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); } } } From 7e50af75473658e83a00697f85e2e855c960646a Mon Sep 17 00:00:00 2001 From: Kane York <rikingcoding@gmail.com> Date: Fri, 11 Sep 2015 09:10:08 -0700 Subject: [PATCH 27/47] Temporarily lock eslint to 1.3.1 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 62961abc4..a3571378a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,7 +41,7 @@ cache: before_install: - gem install bundler - - npm i -g eslint babel-eslint + - npm i -g eslint@1.3.1 babel-eslint - eslint app/assets/javascripts - eslint --ext .es6 app/assets/javascripts - eslint --ext .es6 test/javascripts From b706c59ab9d12a7b776327bebc8ad40a882cc490 Mon Sep 17 00:00:00 2001 From: Kane York <rikingcoding@gmail.com> Date: Fri, 11 Sep 2015 09:14:45 -0700 Subject: [PATCH 28/47] Use the json boolean trick --- app/serializers/category_serializer.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/serializers/category_serializer.rb b/app/serializers/category_serializer.rb index 1f8ee5bf8..6138f1d1b 100644 --- a/app/serializers/category_serializer.rb +++ b/app/serializers/category_serializer.rb @@ -38,11 +38,15 @@ class CategorySerializer < BasicCategorySerializer true end - def is_special + 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 From 3c9a818a2f81d3e1a4076970913119a86e78fa5b Mon Sep 17 00:00:00 2001 From: Neil Lalonde <neillalonde@gmail.com> Date: Fri, 11 Sep 2015 12:22:34 -0400 Subject: [PATCH 29/47] fix broken js tests --- .../discourse/views/list/post-count-or-badges.js.es6 | 2 -- 1 file changed, 2 deletions(-) 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 index 3c968935e..9a8559f1a 100644 --- 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 @@ -1,5 +1,3 @@ -import computed from "ember-addons/ember-computed-decorators"; - export default Ember.Object.extend({ postCountsPresent: Ember.computed.or('topic.unread', 'topic.displayNewPosts', 'topic.unseen'), showBadges: Ember.computed.and('postBadgesEnabled', 'postCountsPresent') From 6119d9fdc0d22a1b077c7be8db30b6d5c12a408f Mon Sep 17 00:00:00 2001 From: Kane York <rikingcoding@gmail.com> Date: Fri, 11 Sep 2015 09:25:03 -0700 Subject: [PATCH 30/47] FIX: Fallbacks for missing interpolation arguments This takes effect when an interpolation is removed from a translation in a Discourse update. The I18n::Backend::Fallbacks loops with a catch(:exception), so calling throw(:exception) will cause it to use the next locale, until it reaches English which is assumed to be correct. Also, enable fallbacks in everything except development (#3724 for more discussion) - we should be able to test this --- config/initializers/i18n.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 From 62cc029886051aeed6774849b13416bd87243773 Mon Sep 17 00:00:00 2001 From: Robin Ward <robin.ward@gmail.com> Date: Fri, 11 Sep 2015 12:44:37 -0400 Subject: [PATCH 31/47] FIX: Support using enter to change the hash, clicks should update hash --- .../discourse/initializers/click-interceptor.js.es6 | 2 ++ app/assets/javascripts/discourse/lib/url.js.es6 | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 b/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 index d74e5264e..763a06b2e 100644 --- a/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 +++ b/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 @@ -1,8 +1,10 @@ import interceptClick from 'discourse/lib/intercept-click'; +import DiscourseURL from 'discourse/lib/url'; export default { name: "click-interceptor", initialize() { $('#main').on('click.discourse', 'a', interceptClick); + $(window).on('hashchange', () => DiscourseURL.routeTo(document.location.hash)); } }; diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index f94c4c2bd..7f2d1bc6c 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; } From 5af0f5f80ea5f03c75a02e18a40d56e4e4bad47c Mon Sep 17 00:00:00 2001 From: Robin Ward <robin.ward@gmail.com> Date: Thu, 10 Sep 2015 16:01:23 -0400 Subject: [PATCH 32/47] FEATURE: Whisper posts --- .../discourse/controllers/composer.js.es6 | 7 +++- .../discourse/models/composer.js.es6 | 6 +++- .../javascripts/discourse/models/post.js.es6 | 3 +- .../discourse/templates/composer.hbs | 10 ++++++ .../javascripts/discourse/templates/post.hbs | 13 ++++--- .../javascripts/discourse/views/post.js.es6 | 12 ++++++- .../stylesheets/common/base/topic-post.scss | 2 +- .../stylesheets/desktop/topic-post.scss | 9 +++++ app/controllers/posts_controller.rb | 4 +++ app/models/post.rb | 27 ++++++++++----- app/models/topic.rb | 7 ++++ config/locales/client.en.yml | 2 ++ config/locales/server.en.yml | 1 + config/site_settings.yml | 3 ++ lib/guardian/post_guardian.rb | 11 +++--- lib/topic_view.rb | 21 ++++++++---- spec/components/guardian_spec.rb | 26 ++++++++++++++ spec/components/topic_view_spec.rb | 17 ++++++++++ spec/models/topic_spec.rb | 34 +++++++++++++++++++ 19 files changed, 186 insertions(+), 29 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 843aae4ee..d6d1224f5 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -3,6 +3,7 @@ import DiscourseURL from 'discourse/lib/url'; import Quote from 'discourse/lib/quote'; import Draft from 'discourse/models/draft'; import Composer from 'discourse/models/composer'; +import computed from 'ember-addons/ember-computed-decorators'; function loadDraft(store, opts) { opts = opts || {}; @@ -64,6 +65,11 @@ export default Ember.Controller.extend({ this.set('similarTopics', []); }.on('init'), + @computed('model.action') + canWhisper(action) { + return this.siteSettings.enable_whispers && action === Composer.REPLY; + }, + showWarning: function() { if (!Discourse.User.currentProp('staff')) { return false; } @@ -132,7 +138,6 @@ export default Ember.Controller.extend({ }, hitEsc() { - const messages = this.get('controllers.composer-messages.model'); if (messages.length) { messages.popObject(); diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 9cb5db069..576bec397 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -24,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', @@ -557,6 +558,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, @@ -569,7 +573,7 @@ const Composer = RestModel.extend({ user_title: user.get('title'), 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'), diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index c332836f6..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,7 +77,6 @@ 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'); diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 711996431..362df1279 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -60,6 +60,16 @@ {{/unless}} </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> diff --git a/app/assets/javascripts/discourse/templates/post.hbs b/app/assets/javascripts/discourse/templates/post.hbs index 29b95d5aa..8beccb2b6 100644 --- a/app/assets/javascripts/discourse/templates/post.hbs +++ b/app/assets/javascripts/discourse/templates/post.hbs @@ -8,7 +8,7 @@ {{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 "user-secret"}}</div> + {{/if}} + {{#if showUserReplyTab}} <a href {{action "toggleReplyHistory" this target="view"}} class='reply-to-tab'> {{#if loadingReplyHistory}} diff --git a/app/assets/javascripts/discourse/views/post.js.es6 b/app/assets/javascripts/discourse/views/post.js.es6 index ef1869e90..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'), diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 3b0d62008..d11b4ac3a 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; diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 41409878f..84730decc 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -582,6 +582,15 @@ a.mention { } } +.whisper { + .topic-body { + .cooked { + font-style: italic; + color: dark-light-diff($primary, $secondary, 55%, -40%); + } + } +} + #share-link { width: 365px; margin-left: -4px; diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index a35e2f423..5ee72c315 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -465,6 +465,10 @@ class PostsController < ApplicationController result[:is_warning] = false end + if 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/models/post.rb b/app/models/post.rb index 3a6a8d515..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) diff --git a/app/models/topic.rb b/app/models/topic.rb index 442fb43c2..703b54726 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 diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index bbc1833f0..881c30a57 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -809,6 +809,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" @@ -1349,6 +1350,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" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 1309a124e..1da6daf5b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -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" diff --git a/config/site_settings.yml b/config/site_settings.yml index 70992a968..fb6adceed 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: 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/topic_view.rb b/lib/topic_view.rb index 333e155b4..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 @@ -361,7 +370,7 @@ class TopicView 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/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/topic_view_spec.rb b/spec/components/topic_view_spec.rb index dedc080b9..fe2658d97 100644 --- a/spec/components/topic_view_spec.rb +++ b/spec/components/topic_view_spec.rb @@ -251,6 +251,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/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" } From b6febb0638b1d8232c0941bd94f07c4f0ef30ae5 Mon Sep 17 00:00:00 2001 From: Kane York <rikingcoding@gmail.com> Date: Fri, 11 Sep 2015 11:37:36 -0700 Subject: [PATCH 33/47] fix the build (460243d7) --- spec/components/topic_view_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/components/topic_view_spec.rb b/spec/components/topic_view_spec.rb index fe2658d97..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 From e5ade5a7611ac0279dbcf781b0c552a73db1e6e6 Mon Sep 17 00:00:00 2001 From: scossar <scossar3@gmail.com> Date: Fri, 11 Sep 2015 11:28:18 -0700 Subject: [PATCH 34/47] set widths on table cells --- app/assets/stylesheets/mobile/topic-list.scss | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index 886e35487..41dd47b8b 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -110,6 +110,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; From 3b46ec7ae3dee62960149c79c498f5987d8dc6bf Mon Sep 17 00:00:00 2001 From: Neil Lalonde <neillalonde@gmail.com> Date: Fri, 11 Sep 2015 16:34:27 -0400 Subject: [PATCH 35/47] visual tweaks for topic lists on mobile --- app/assets/stylesheets/mobile/topic-list.scss | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index 41dd47b8b..1b59b39e7 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 { + content: none; + margin-right: 0; + } + &.new-topic { + padding-right: 0; + } } .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%)); } } From 4252a2ee1e78daff1267e47a4caebe3386d6cd25 Mon Sep 17 00:00:00 2001 From: Jeff Atwood <jatwood@codinghorror.com> Date: Fri, 11 Sep 2015 16:53:20 -0700 Subject: [PATCH 36/47] switch to eye-slash on whisper, similar to unlisted --- app/assets/javascripts/discourse/templates/post.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/post.hbs b/app/assets/javascripts/discourse/templates/post.hbs index 8beccb2b6..ff990eb66 100644 --- a/app/assets/javascripts/discourse/templates/post.hbs +++ b/app/assets/javascripts/discourse/templates/post.hbs @@ -56,7 +56,7 @@ {{/if}} {{#if view.whisper}} - <div class="post-info whisper" title={{i18n 'post.whisper'}}>{{fa-icon "user-secret"}}</div> + <div class="post-info whisper" title={{i18n 'post.whisper'}}>{{fa-icon "eye-slash"}}</div> {{/if}} {{#if showUserReplyTab}} From 1e739e8c96393aedf9cbccf4d1c487b27ce5a139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr> Date: Sat, 12 Sep 2015 20:44:20 +0200 Subject: [PATCH 37/47] FIX: move whisper styling to common --- app/assets/stylesheets/common/base/topic-post.scss | 9 +++++++++ app/assets/stylesheets/desktop/topic-post.scss | 9 --------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index d11b4ac3a..dab4f08f1 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -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/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 84730decc..41409878f 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -582,15 +582,6 @@ a.mention { } } -.whisper { - .topic-body { - .cooked { - font-style: italic; - color: dark-light-diff($primary, $secondary, 55%, -40%); - } - } -} - #share-link { width: 365px; margin-left: -4px; From 1e6bf67b5bc8078ec2db47fe24560198c1b0280f Mon Sep 17 00:00:00 2001 From: Arpit Jalan <arpit@techapj.com> Date: Sat, 12 Sep 2015 23:58:18 +0530 Subject: [PATCH 38/47] FIX: show category links if category has sub-categories in nojs view --- app/views/list/list.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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> From b4974f5876d53969e7b523ac4592cf4fe58fb34c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan <tgx_world@hotmail.com> Date: Sat, 12 Sep 2015 13:38:20 +0800 Subject: [PATCH 39/47] UX: Don't allow search if searchTerm is not valid. --- .../discourse/components/search-menu.js.es6 | 9 ++++----- .../discourse/controllers/full-page-search.js.es6 | 10 ++++++++-- app/assets/javascripts/discourse/lib/search.js.es6 | 10 +++++++++- .../discourse/routes/full-page-search.js.es6 | 4 ++-- .../discourse/templates/full-page-search.hbs | 2 +- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/discourse/components/search-menu.js.es6 b/app/assets/javascripts/discourse/components/search-menu.js.es6 index 8a4f2ddec..153929ecd 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, searchContextDescription} from 'discourse/lib/search'; +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'; @@ -61,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 { @@ -154,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/controllers/full-page-search.js.es6 b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 index fce0fecd8..2e909601f 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -1,4 +1,4 @@ -import { translateResults, searchContextDescription, getSearchKey } from "discourse/lib/search"; +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'; @@ -37,7 +37,12 @@ export default Ember.Controller.extend({ @computed('q') searchActive(q){ - return q && q.length > 0; + return isValidSearchTerm(q); + }, + + @computed('searchTerm') + isNotValidSearchTerm(searchTerm) { + return !isValidSearchTerm(searchTerm); }, @observes('model') @@ -129,6 +134,7 @@ export default Ember.Controller.extend({ }, search() { + if (this.get("isNotValidSearchTerm")) return; this.search(); } } diff --git a/app/assets/javascripts/discourse/lib/search.js.es6 b/app/assets/javascripts/discourse/lib/search.js.es6 index 21fd8afeb..9d7bec2d6 100644 --- a/app/assets/javascripts/discourse/lib/search.js.es6 +++ b/app/assets/javascripts/discourse/lib/search.js.es6 @@ -106,4 +106,12 @@ const getSearchKey = function(args){ ((args.searchContext && args.searchContext.id) || "") }; -export { searchForTerm, searchContextDescription, getSearchKey }; +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/routes/full-page-search.js.es6 b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 index 10072d99f..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,4 +1,4 @@ -import { translateResults, getSearchKey } from "discourse/lib/search"; +import { translateResults, getSearchKey, isValidSearchTerm } from "discourse/lib/search"; export default Discourse.Route.extend({ queryParams: { q: {}, context_id: {}, context: {} }, @@ -23,7 +23,7 @@ export default Discourse.Route.extend({ } return PreloadStore.getAndRemove("search", function() { - if (params.q && params.q.length > 2) { + if (isValidSearchTerm(params.q)) { return Discourse.ajax("/search", { data: args }); } else { return null; diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index db60849ff..43e3e835a 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"}} + {{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"}} From 2ae0ef0ad92a585c2c28b8d79f39b620506a2551 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan <tgx_world@hotmail.com> Date: Sat, 12 Sep 2015 14:06:46 +0800 Subject: [PATCH 40/47] UX: Select search term when focus returns to search box. --- .../javascripts/discourse/components/search-menu.js.es6 | 2 +- .../javascripts/discourse/components/search-text-field.js.es6 | 4 ++++ .../javascripts/discourse/templates/full-page-search.hbs | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/components/search-menu.js.es6 b/app/assets/javascripts/discourse/components/search-menu.js.es6 index 153929ecd..413525280 100644 --- a/app/assets/javascripts/discourse/components/search-menu.js.es6 +++ b/app/assets/javascripts/discourse/components/search-menu.js.es6 @@ -134,7 +134,7 @@ export default Ember.Component.extend({ }, showedSearch() { - $('#search-term').focus(); + $('#search-term').focus().select(); }, showSearchHelp() { 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..bb83304bc 100644 --- a/app/assets/javascripts/discourse/components/search-text-field.js.es6 +++ b/app/assets/javascripts/discourse/components/search-text-field.js.es6 @@ -5,5 +5,9 @@ export default TextField.extend({ @computed('searchService.searchContextEnabled') placeholder: function(searchContextEnabled) { return searchContextEnabled ? "" : I18n.t('search.title'); + }, + + focusIn: function() { + Em.run.later(() => { this.$().select(); }); } }); diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index 43e3e835a..3b9bebb96 100644 --- a/app/assets/javascripts/discourse/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -1,5 +1,5 @@ <div class="search row clearfix"> - {{input type="text" value=searchTerm class="input-xxlarge search no-blur" action="search"}} + {{search-text-field value=searchTerm class="input-xxlarge search no-blur" action="search"}} {{d-button action="search" icon="search" class="btn-primary" disabled=isNotValidSearchTerm}} {{#if canBulkSelect}} {{#if model.posts}} From c1deee772b5eddf262f0be1ea5ffcb5f09b13018 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan <tgx_world@hotmail.com> Date: Sun, 13 Sep 2015 09:41:05 +0800 Subject: [PATCH 41/47] UX: Autofocus when input is empty. --- .../discourse/components/search-text-field.js.es6 | 6 +++++- .../discourse/controllers/full-page-search.js.es6 | 5 +++++ .../javascripts/discourse/templates/full-page-search.hbs | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) 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 bb83304bc..80dcc0ba2 100644 --- a/app/assets/javascripts/discourse/components/search-text-field.js.es6 +++ b/app/assets/javascripts/discourse/components/search-text-field.js.es6 @@ -9,5 +9,9 @@ export default TextField.extend({ focusIn: function() { Em.run.later(() => { this.$().select(); }); - } + }, + + becomeFocused: function() { + if (this.get('hasAutofocus')) this.$().focus(); + }.on('didInsertElement') }); 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 2e909601f..f4fb1e787 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -13,6 +13,11 @@ export default Ember.Controller.extend({ context_id: null, context: null, + @computed('q') + hasAutofocus(q) { + return Em.isEmpty(q); + }, + @computed('skip_context', 'context') searchContextEnabled: { get(skip,context){ diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index 3b9bebb96..8f1874345 100644 --- a/app/assets/javascripts/discourse/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -1,5 +1,5 @@ <div class="search row clearfix"> - {{search-text-field value=searchTerm class="input-xxlarge search no-blur" action="search"}} + {{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}} From 63e96580c4e438bf91a1bdc1a86e47875db95782 Mon Sep 17 00:00:00 2001 From: Jeff Atwood <jatwood@codinghorror.com> Date: Sun, 13 Sep 2015 03:18:44 -0700 Subject: [PATCH 42/47] correct minor default embedding style issues --- app/assets/stylesheets/embed.css.scss | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/embed.css.scss b/app/assets/stylesheets/embed.css.scss index 40eab2875..ef9347c60 100644 --- a/app/assets/stylesheets/embed.css.scss +++ b/app/assets/stylesheets/embed.css.scss @@ -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; -} From 9ba22b5155598c0e3ed97db9cacdd43c94687ef0 Mon Sep 17 00:00:00 2001 From: Jeff Atwood <jatwood@codinghorror.com> Date: Sun, 13 Sep 2015 04:02:58 -0700 Subject: [PATCH 43/47] minor embedding css fix --- app/assets/stylesheets/embed.css.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/embed.css.scss b/app/assets/stylesheets/embed.css.scss index ef9347c60..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; } } From 08dccaa874182e82a0235c10b14d8121c4d5cc34 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan <tgx_world@hotmail.com> Date: Mon, 14 Sep 2015 00:30:39 +0800 Subject: [PATCH 44/47] FIX: Title input validation position. --- app/assets/stylesheets/desktop/compose.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index 9fea1acef..ca821824d 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -324,8 +324,8 @@ } .title-input .popup-tip { width: 300px; - left: 0px; - top: -30px; + margin-top: 8px; + left: 150px; } .category-input .popup-tip { width: 240px; From 6891c7f8aa09309bae0277c0d87530856273bbe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Garc=C3=ADa-Navas=20L=C3=B3pez=20de=20Cu=C3=A9llar?= <davidgarcianavas@gmail.com> Date: Sun, 13 Sep 2015 22:45:26 +0200 Subject: [PATCH 45/47] little typo? --- config/locales/server.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 1da6daf5b..6f765f159 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: From f948ee9e2618793d0ec0dab48562d9593213d4aa Mon Sep 17 00:00:00 2001 From: Sam <sam.saffron@gmail.com> Date: Mon, 14 Sep 2015 09:42:21 +1000 Subject: [PATCH 46/47] FIX: ensure letter avatar service uses same algorithm --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index a17e4e9ba..cd4792ea8 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -472,7 +472,7 @@ class User < ActiveRecord::Base # 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) + url.gsub! "{color}", letter_avatar_color(username.downcase) url.gsub! "{username}", username url.gsub! "{first_letter}", username[0].downcase url From d39faf7ddfa85db9cac8c596d2118f8aa8217c50 Mon Sep 17 00:00:00 2001 From: Sam <sam.saffron@gmail.com> Date: Mon, 14 Sep 2015 09:43:40 +1000 Subject: [PATCH 47/47] FEATURE: allow external avatars to be shadowed by global --- config/site_settings.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/site_settings.yml b/config/site_settings.yml index fb6adceed..1c8e9f112 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -575,6 +575,7 @@ files: external_system_avatars_enabled: default: false client: true + shadowed_by_global: true external_system_avatars_url: default: "https://avatars.discourse.org/letter/{first_letter}/{color}/{size}.png" client: true