From 7056db26e6862c08d765b1208c8a85e8866c7624 Mon Sep 17 00:00:00 2001 From: Dan Singerman Date: Tue, 2 Jun 2015 12:29:27 +0100 Subject: [PATCH 001/224] Respect cookie[:destination_url] in Single Sign On When the login_required setting is true, the destination URL is dropped. This change means it will be respected at login time --- app/controllers/session_controller.rb | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 6b2a8baa1fe..6f5a4b98efe 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -11,8 +11,16 @@ class SessionController < ApplicationController end def sso + if params[:return_path] + return_path = params[:return_path] + elsif cookies[:destination_url] + return_path = URI::parse(cookies[:destination_url]).path + else + return_path = path('/') + end + if SiteSetting.enable_sso - redirect_to DiscourseSingleSignOn.generate_url(params[:return_path] || path('/')) + redirect_to DiscourseSingleSignOn.generate_url(return_path) else render nothing: true, status: 404 end From 8055d065f2a07e83c333df931bc8134873f4d334 Mon Sep 17 00:00:00 2001 From: Dan Singerman Date: Tue, 11 Aug 2015 16:27:56 +0100 Subject: [PATCH 002/224] Refactor ApplicationController#redirect_to_login_if_required to use session for SSO --- app/controllers/application_controller.rb | 11 ++++++++--- app/controllers/session_controller.rb | 12 ++++++------ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b1a665be4b1..60b6ba9938b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -413,17 +413,22 @@ class ApplicationController < ActionController::Base raise Discourse::InvalidAccess.new unless current_user && current_user.staff? end + def destination_url + request.original_url unless request.original_url =~ /uploads/ + end + def redirect_to_login_if_required return if current_user || (request.format.json? && api_key_valid?) - # save original URL in a cookie - cookies[:destination_url] = request.original_url unless request.original_url =~ /uploads/ - # redirect user to the SSO page if we need to log in AND SSO is enabled if SiteSetting.login_required? if SiteSetting.enable_sso? + # save original URL in a session so we can redirect after login + session[:destination_url] = destination_url redirect_to path('/session/sso') else + # save original URL in a cookie (javascript redirects after login in this case) + cookies[:destination_url] = destination_url redirect_to :login end end diff --git a/app/controllers/session_controller.rb b/app/controllers/session_controller.rb index 6f5a4b98efe..a2a771a354a 100644 --- a/app/controllers/session_controller.rb +++ b/app/controllers/session_controller.rb @@ -11,15 +11,15 @@ class SessionController < ApplicationController end def sso - if params[:return_path] - return_path = params[:return_path] - elsif cookies[:destination_url] - return_path = URI::parse(cookies[:destination_url]).path + return_path = if params[:return_path] + params[:return_path] + elsif session[:destination_url] + URI::parse(session[:destination_url]).path else - return_path = path('/') + path('/') end - if SiteSetting.enable_sso + if SiteSetting.enable_sso? redirect_to DiscourseSingleSignOn.generate_url(return_path) else render nothing: true, status: 404 From 3cbfc45bf67aca32775da2ac065ab44bbddc2316 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 25 Aug 2015 17:07:40 -0700 Subject: [PATCH 003/224] FEATURE: Allow plugins to add admin dashboard warnings --- app/models/admin_dashboard_data.rb | 51 ++++++++++++++---------- spec/models/admin_dashboard_data_spec.rb | 26 ++++++++++++ 2 files changed, 56 insertions(+), 21 deletions(-) diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index abe94003dd6..001d23f3cc1 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -32,28 +32,37 @@ class AdminDashboardData MOBILE_REPORTS ||= ['mobile_visits'] + ApplicationRequest.req_types.keys.select {|r| r =~ /mobile/}.map { |r| r + "_reqs" } + def self.add_problem_check(*syms, &blk) + @problem_syms ||= [] + @problem_blocks ||= [] + + @problem_syms.push(*syms) if syms + @problem_blocks << blk if blk + end + class << self; attr_reader :problem_syms, :problem_blocks; end + def problems - [ rails_env_check, - ruby_version_check, - host_names_check, - gc_checks, - sidekiq_check || queue_size_check, - ram_check, - google_oauth2_config_check, - facebook_config_check, - twitter_config_check, - github_config_check, - s3_config_check, - image_magick_check, - failing_emails_check, - default_logo_check, - contact_email_check, - send_consumer_email_check, - title_check, - site_description_check, - site_contact_username_check, - notification_email_check - ].compact + problems = [] + AdminDashboardData.problem_syms.each do |sym| + problems << send(sym) + end + AdminDashboardData.problem_blocks.each do |blk| + problems << instance_exec(&blk) + end + problems.compact + end + + add_problem_check :rails_env_check, :ruby_version_check, :host_names_check, + :gc_checks, :ram_check, :google_oauth2_config_check, + :facebook_config_check, :twitter_config_check, + :github_config_check, :s3_config_check, :image_magick_check, + :failing_emails_check, :default_logo_check, :contact_email_check, + :send_consumer_email_check, :title_check, + :site_description_check, :site_contact_username_check, + :notification_email_check + + add_problem_check do + sidekiq_check || queue_size_check end diff --git a/spec/models/admin_dashboard_data_spec.rb b/spec/models/admin_dashboard_data_spec.rb index 30dd8ec2f05..80286f86046 100644 --- a/spec/models/admin_dashboard_data_spec.rb +++ b/spec/models/admin_dashboard_data_spec.rb @@ -2,6 +2,32 @@ require 'spec_helper' describe AdminDashboardData do + describe "adding new checks" do + it 'calls the passed block' do + called = false + AdminDashboardData.add_problem_check do + called = true + end + + AdminDashboardData.fetch_problems + expect(called).to eq(true) + end + + it 'calls the passed method' do + $test_AdminDashboardData_global = false + class AdminDashboardData + def my_test_method + $test_AdminDashboardData_global = true + end + end + AdminDashboardData.add_problem_check :my_test_method + + AdminDashboardData.fetch_problems + expect($test_AdminDashboardData_global).to eq(true) + $test_AdminDashboardData_global = nil + end + end + describe "rails_env_check" do subject { described_class.new.rails_env_check } From 983d3606df2879b6cd704bdc654eb784803ff791 Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Thu, 27 Aug 2015 18:37:52 -0300 Subject: [PATCH 004/224] Fix same domains links on subfolder installs isInternal link handling logic needs to test if the link is inside the same subfolder --- app/assets/javascripts/discourse/lib/url.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index f94c4c2bd54..b9a56ba4af1 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -271,7 +271,7 @@ const DiscourseURL = Ember.Object.createWithMixins({ // This has been extracted so it can be tested. origin: function() { - return window.location.origin; + return Discourse.BaseUrl + Discourse.BaseUri; }, /** From 5baa6ea66c76bc502fdb9a9a30f5f165a45c01a8 Mon Sep 17 00:00:00 2001 From: scossar Date: Sun, 30 Aug 2015 10:02:12 -0700 Subject: [PATCH 005/224] use transition and transform mixins --- app/assets/stylesheets/common/admin/admin_base.scss | 8 ++++---- app/assets/stylesheets/common/foundation/mixins.scss | 8 ++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index d7686805af2..63e8a7c8428 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -278,15 +278,15 @@ td.flaggers td { .admin-detail.mobile-open { @media (max-width: $mobile-breakpoint) { - transition: transform 0.3s ease; - transform: (translateX(50%)); + @include transition(transform 0.3s ease); + @include transform(translateX(50%)); } } .admin-detail.mobile-closed { @media (max-width: $mobile-breakpoint) { - transition: transform 0.3s ease; - transform: (translateX(0)); + @include transition(transform 0.3s ease); + @include transform(translateX(0)); } } diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss index 55b15a9b537..399c5258917 100644 --- a/app/assets/stylesheets/common/foundation/mixins.scss +++ b/app/assets/stylesheets/common/foundation/mixins.scss @@ -113,3 +113,11 @@ -webkit-transform: $transforms; transform: $transforms; } + +@mixin transition($transitions) { + -o-transition: $transitions; + -moz-transition: $transitions; + -webkit-transition: $transitions; + transition: $transitions; +} + From adccdf4b89d40037ef298534676bb91df8541c3d Mon Sep 17 00:00:00 2001 From: scossar Date: Sun, 30 Aug 2015 10:30:56 -0700 Subject: [PATCH 006/224] restore mobile breakpoint --- app/assets/stylesheets/common/admin/admin_base.scss | 6 +++--- app/assets/stylesheets/common/foundation/mixins.scss | 8 -------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 63e8a7c8428..c3e203ac130 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -3,7 +3,7 @@ @import "common/foundation/mixins"; @import "common/foundation/helpers"; -$mobile-breakpoint: 700px; +$mobile-breakpoint: 600px; // Change the box model for .admin-content @media (max-width: $mobile-breakpoint) { @@ -278,14 +278,14 @@ td.flaggers td { .admin-detail.mobile-open { @media (max-width: $mobile-breakpoint) { - @include transition(transform 0.3s ease); + transition: transform 0.3s ease; @include transform(translateX(50%)); } } .admin-detail.mobile-closed { @media (max-width: $mobile-breakpoint) { - @include transition(transform 0.3s ease); + transition: transform 0.3s ease; @include transform(translateX(0)); } } diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss index 399c5258917..55b15a9b537 100644 --- a/app/assets/stylesheets/common/foundation/mixins.scss +++ b/app/assets/stylesheets/common/foundation/mixins.scss @@ -113,11 +113,3 @@ -webkit-transform: $transforms; transform: $transforms; } - -@mixin transition($transitions) { - -o-transition: $transitions; - -moz-transition: $transitions; - -webkit-transition: $transitions; - transition: $transitions; -} - From 640c0f81f60ce964f5abab27f0b317cda3e9556c Mon Sep 17 00:00:00 2001 From: scossar Date: Sun, 30 Aug 2015 10:36:54 -0700 Subject: [PATCH 007/224] hide search checkbox at 550px --- app/assets/stylesheets/common/admin/admin_base.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index c3e203ac130..91afaab8a56 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -198,7 +198,7 @@ td.flaggers td { } // Hide the search checkbox for very small screens // Todo: find somewhere to display it - probably requires switching its order in the html - @media (max-width: 450px) { + @media (max-width: 550px) { display: none; } } From 1df860008112421f4b306d767fb03d82a3d08ddf Mon Sep 17 00:00:00 2001 From: scossar Date: Sun, 30 Aug 2015 12:09:28 -0700 Subject: [PATCH 008/224] set mobile breakpoint at 700px --- app/assets/stylesheets/common/admin/admin_base.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 91afaab8a56..6007fd8305f 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -3,7 +3,7 @@ @import "common/foundation/mixins"; @import "common/foundation/helpers"; -$mobile-breakpoint: 600px; +$mobile-breakpoint: 700px; // Change the box model for .admin-content @media (max-width: $mobile-breakpoint) { From 9909c715dca1b07e89a8c9f4bd511dd334017467 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 1 Sep 2015 13:23:03 +0800 Subject: [PATCH 009/224] FIX: Emoji can't be copy and pasted. --- app/assets/javascripts/discourse/lib/emoji/emoji.js.erb | 3 ++- test/javascripts/lib/bbcode-test.js.es6 | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/emoji/emoji.js.erb b/app/assets/javascripts/discourse/lib/emoji/emoji.js.erb index e662df804e6..8a8fceb2528 100644 --- a/app/assets/javascripts/discourse/lib/emoji/emoji.js.erb +++ b/app/assets/javascripts/discourse/lib/emoji/emoji.js.erb @@ -77,7 +77,8 @@ function imageFor(code) { code = code.toLowerCase(); var url = urlFor(code); if (url) { - return ['img', { href: url, title: ':' + code + ':', 'class': 'emoji', alt: code }]; + var code = ':' + code + ':'; + return ['img', { href: url, title: code, 'class': 'emoji', alt: code }]; } } diff --git a/test/javascripts/lib/bbcode-test.js.es6 b/test/javascripts/lib/bbcode-test.js.es6 index 575ad7e7558..385569c9c05 100644 --- a/test/javascripts/lib/bbcode-test.js.es6 +++ b/test/javascripts/lib/bbcode-test.js.es6 @@ -52,7 +52,7 @@ test('spoiler', function() { format("[spoiler]it's a sled[/spoiler]", "it's a sled", "supports spoiler tags on text"); format("[spoiler][/spoiler]", "", "supports spoiler tags on images"); - format("[spoiler] This is the **bold** :smiley: [/spoiler]", " This is the bold \"smiley\" ", "supports spoiler tags on emojis"); + format("[spoiler] This is the **bold** :smiley: [/spoiler]", " This is the bold \":smiley:\" ", "supports spoiler tags on emojis"); format("[spoiler] Why not both ?[/spoiler]", " Why not both ?", "supports images and text"); format("In a p tag a spoiler [spoiler] [/spoiler] can work.", "In a p tag a spoiler can work.", "supports images and text in a p tag"); }); From bef3084516ebac401a903e30f3b06964d39c4e43 Mon Sep 17 00:00:00 2001 From: awesomerobot Date: Tue, 1 Sep 2015 10:38:37 -0400 Subject: [PATCH 010/224] flexbox for the post header --- .../stylesheets/common/base/header.scss | 14 ++++- .../stylesheets/common/base/menu-panel.scss | 3 -- app/assets/stylesheets/common/base/topic.scss | 7 +-- .../common/components/badges.css.scss | 4 +- .../stylesheets/common/foundation/mixins.scss | 54 +++++++++++++++++++ 5 files changed, 73 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index bc20cefeaa4..6de1b2fcc3e 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -6,6 +6,14 @@ background-color: $header_background; box-shadow: 0 2px 4px -1px rgba(0,0,0, .25); + .ember-view { + min-width: 0; //flexbox fix + } + + .title { + @include flex(0,0,auto); + } + .docked & { position: fixed; backface-visibility: hidden; /** do magic for scrolling performance **/ @@ -13,6 +21,8 @@ .contents { margin: 8px 0; + @include flexbox(); + @include align-items(center); } .title { @@ -34,8 +44,10 @@ } .panel { - float: right; position: relative; + margin-left: auto; + min-width: 125px; + @include order(3) } .login-button, button.sign-up-button { diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index bf718f19ad4..deb49386d5c 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -268,6 +268,3 @@ div.menu-links-header { margin-right: 0.2em; } } - - - diff --git a/app/assets/stylesheets/common/base/topic.scss b/app/assets/stylesheets/common/base/topic.scss index 015b4b68714..873ab685bc6 100644 --- a/app/assets/stylesheets/common/base/topic.scss +++ b/app/assets/stylesheets/common/base/topic.scss @@ -27,11 +27,12 @@ } .extra-info-wrapper { + @include order(2); + line-height: 1.5; .badge-wrapper { float: left; - &.bullet { - margin-top: 5px; - } + margin-left: 2px; + line-height: 1.2; } } diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index 47fb1a1cb6e..34b97c1784e 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -52,8 +52,8 @@ &.bullet { //bullet category style - display: inline-flex; - align-items: baseline; + @include inline-flex; + @include align-items(baseline); margin-right: 10px; span.badge-category { diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss index 55b15a9b537..e9cb375a50c 100644 --- a/app/assets/stylesheets/common/foundation/mixins.scss +++ b/app/assets/stylesheets/common/foundation/mixins.scss @@ -113,3 +113,57 @@ -webkit-transform: $transforms; transform: $transforms; } + +// --------------------------------------------------- + +//Flexbox + +@mixin flexbox() { + display: -webkit-box; + display: -moz-box; + display: -ms-flexbox; + display: -webkit-flex; + display: flex; +} + +@mixin inline-flex() { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -moz-inline-box; + display: -ms-inline-flexbox; + display: inline-flex; +} + + +@mixin align-items($alignment) { + -webkit-box-align: $alignment; + -webkit-align-items: $alignment; + -ms-flex-align: $alignment; + -ms-align-items: $alignment; + align-items:$alignment; +} + +@mixin order($val) { + -webkit-box-ordinal-group: $val; + -moz-box-ordinal-group: $val; + -ms-flex-order: $val; + -webkit-order: $val; + order: $val; +} + +@mixin flex($fg: 1, $fs: null, $fb: null) { + + $fg-boxflex: $fg; + + // Box-Flex only supports a flex-grow value + @if type-of($fg) == 'list' { + $fg-boxflex: nth($fg, 1); + } + + -webkit-box-flex: $fg-boxflex; + -webkit-flex: $fg $fs $fb; + -moz-box-flex: $fg-boxflex; + -moz-flex: $fg $fs $fb; + -ms-flex: $fg $fs $fb; + flex: $fg $fs $fb; +} From cb9c603b6b7e318d2d84ccc80ef60c2b88649e02 Mon Sep 17 00:00:00 2001 From: kerryliu Date: Mon, 31 Aug 2015 10:01:36 -0700 Subject: [PATCH 011/224] workaround for code block being parsed before quote block. --- app/assets/javascripts/discourse/dialects/code_dialect.js | 1 + app/assets/javascripts/discourse/dialects/dialect.js | 6 ++++++ test/javascripts/lib/bbcode-test.js.es6 | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/app/assets/javascripts/discourse/dialects/code_dialect.js b/app/assets/javascripts/discourse/dialects/code_dialect.js index e7322a4abeb..2594cd8316d 100644 --- a/app/assets/javascripts/discourse/dialects/code_dialect.js +++ b/app/assets/javascripts/discourse/dialects/code_dialect.js @@ -33,6 +33,7 @@ function codeFlattenBlocks(blocks) { Discourse.Dialect.replaceBlock({ start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm, stop: /^```$/gm, + withoutLeading: /\[quote/gm, //if leading text contains a quote this should not match emitter: function(blockContents, matches) { var klass = Discourse.SiteSettings.default_code_lang; diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js index e80e5e96710..1614c1c0f19 100644 --- a/app/assets/javascripts/discourse/dialects/dialect.js +++ b/app/assets/javascripts/discourse/dialects/dialect.js @@ -501,6 +501,12 @@ Discourse.Dialect = { var pos = args.start.lastIndex - match[0].length, leading = block.slice(0, pos), trailing = match[2] ? match[2].replace(/^\n*/, "") : ""; + + if(args.withoutLeading && args.withoutLeading.test(leading)) { + //The other leading block should be processed first! eg a code block wrapped around a code block. + return; + } + // just give up if there's no stop tag in this or any next block args.stop.lastIndex = block.length - trailing.length; if (!args.stop.exec(block) && lastChance()) { return; } diff --git a/test/javascripts/lib/bbcode-test.js.es6 b/test/javascripts/lib/bbcode-test.js.es6 index 575ad7e7558..1c1b4436d87 100644 --- a/test/javascripts/lib/bbcode-test.js.es6 +++ b/test/javascripts/lib/bbcode-test.js.es6 @@ -160,6 +160,13 @@ test("quote formatting", function() { "", "handles mismatched nested quote tags"); + + formatQ("[quote=\"Alice, post:1, topic:1\"]\n```javascript\nvar foo ='foo';\nvar bar = 'bar';\n```\n[/quote]", + "", + "quotes can have code blocks without leading newline"); + formatQ("[quote=\"Alice, post:1, topic:1\"]\n\n```javascript\nvar foo ='foo';\nvar bar = 'bar';\n```\n[/quote]", + "", + "quotes can have code blocks with leading newline"); }); test("quotes with trailing formatting", function() { From c2eceaa50de002f69d6bba513f5fd7e0b6db3460 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 1 Sep 2015 14:26:23 +0800 Subject: [PATCH 012/224] FIX: Allow user to quote Emojis. --- app/assets/javascripts/discourse/lib/utilities.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js index ac55bdaf498..05ed05cafc4 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js +++ b/app/assets/javascripts/discourse/lib/utilities.js @@ -97,7 +97,10 @@ Discourse.Utilities = { // Strip out any .click elements from the HTML before converting it to text var div = document.createElement('div'); div.innerHTML = html; - $('.clicks', $(div)).remove(); + var $div = $(div); + // Find all emojis and replace with its title attribute. + $div.find('img.emoji').replaceWith(function() { return this.title }); + $('.clicks', $div).remove(); var text = div.textContent || div.innerText || ""; return String(text).trim(); From 65790452d833fc38460b2a5222c59896f9872af4 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 1 Sep 2015 13:32:35 -0700 Subject: [PATCH 013/224] Clean up the test checks when done --- app/models/admin_dashboard_data.rb | 31 +++++++++++++----------- spec/models/admin_dashboard_data_spec.rb | 4 +++ 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/app/models/admin_dashboard_data.rb b/app/models/admin_dashboard_data.rb index 001d23f3cc1..42535237636 100644 --- a/app/models/admin_dashboard_data.rb +++ b/app/models/admin_dashboard_data.rb @@ -33,9 +33,6 @@ class AdminDashboardData MOBILE_REPORTS ||= ['mobile_visits'] + ApplicationRequest.req_types.keys.select {|r| r =~ /mobile/}.map { |r| r + "_reqs" } def self.add_problem_check(*syms, &blk) - @problem_syms ||= [] - @problem_blocks ||= [] - @problem_syms.push(*syms) if syms @problem_blocks << blk if blk end @@ -52,19 +49,25 @@ class AdminDashboardData problems.compact end - add_problem_check :rails_env_check, :ruby_version_check, :host_names_check, - :gc_checks, :ram_check, :google_oauth2_config_check, - :facebook_config_check, :twitter_config_check, - :github_config_check, :s3_config_check, :image_magick_check, - :failing_emails_check, :default_logo_check, :contact_email_check, - :send_consumer_email_check, :title_check, - :site_description_check, :site_contact_username_check, - :notification_email_check + # used for testing + def self.reset_problem_checks + @problem_syms = [] + @problem_blocks = [] - add_problem_check do - sidekiq_check || queue_size_check + add_problem_check :rails_env_check, :ruby_version_check, :host_names_check, + :gc_checks, :ram_check, :google_oauth2_config_check, + :facebook_config_check, :twitter_config_check, + :github_config_check, :s3_config_check, :image_magick_check, + :failing_emails_check, :default_logo_check, :contact_email_check, + :send_consumer_email_check, :title_check, + :site_description_check, :site_contact_username_check, + :notification_email_check + + add_problem_check do + sidekiq_check || queue_size_check + end end - + reset_problem_checks def self.fetch_stats AdminDashboardData.new.as_json diff --git a/spec/models/admin_dashboard_data_spec.rb b/spec/models/admin_dashboard_data_spec.rb index 80286f86046..36a029b4778 100644 --- a/spec/models/admin_dashboard_data_spec.rb +++ b/spec/models/admin_dashboard_data_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe AdminDashboardData do describe "adding new checks" do + after do + AdminDashboardData.reset_problem_checks + end + it 'calls the passed block' do called = false AdminDashboardData.add_problem_check do From f5cbaf56092f9a46217ec8d122d5276fe4f55992 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 1 Sep 2015 17:33:37 -0400 Subject: [PATCH 014/224] Have menu panels show up on top of the composer, stop composer at header --- .../discourse/views/composer.js.es6 | 38 ++++++++++--------- .../stylesheets/common/base/menu-panel.scss | 4 ++ app/assets/stylesheets/desktop/compose.scss | 2 +- vendor/assets/javascripts/div_resizer.js | 7 +++- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index 22ab46f0fc6..a8b49480180 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -60,23 +60,22 @@ const ComposerView = Ember.View.extend(Ember.Evented, { }, resize: function() { - const self = this; - Ember.run.scheduleOnce('afterRender', function() { - const h = $('#reply-control').height() || 0; - self.movePanels.apply(self, [h + "px"]); + Ember.run.scheduleOnce('afterRender', () => { + let h = $('#reply-control').height() || 0; + this.movePanels(h + "px"); // Figure out the size of the fields - const $fields = self.$('.composer-fields'); + const $fields = this.$('.composer-fields'); let pos = $fields.position(); if (pos) { - self.$('.wmd-controls').css('top', $fields.height() + pos.top + 5); + this.$('.wmd-controls').css('top', $fields.height() + pos.top + 5); } // get the submit panel height - pos = self.$('.submit-panel').position(); + pos = this.$('.submit-panel').position(); if (pos) { - self.$('.wmd-controls').css('bottom', h - pos.top + 7); + this.$('.wmd-controls').css('bottom', h - pos.top + 7); } }); @@ -117,20 +116,25 @@ const ComposerView = Ember.View.extend(Ember.Evented, { }, _enableResizing: function() { - const $replyControl = $('#reply-control'), - self = this; + const $replyControl = $('#reply-control'); - const resizer = function() { - Ember.run(function() { - self.resize(); - }); + const runResize = () => { + Ember.run(() => this.resize()); }; $replyControl.DivResizer({ - resize: resizer, - onDrag(sizePx) { self.movePanels.apply(self, [sizePx]); } + maxHeight(winHeight) { + const $header = $('header.d-header'); + const headerOffset = $header.offset(); + const headerOffsetTop = (headerOffset) ? headerOffset.top : 0; + const headerHeight = parseInt($header.height() + headerOffsetTop - $(window).scrollTop() + 5); + return winHeight - headerHeight; + }, + resize: runResize, + onDrag: (sizePx) => this.movePanels(sizePx) }); - afterTransition($replyControl, resizer); + + afterTransition($replyControl, runResize); this.set('controller.view', this); positioningWorkaround(this.$()); diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index bf718f19ad4..84ea5e6475f 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -24,6 +24,10 @@ padding: 0.5em; width: 300px; + hr { + margin: 3px 0; + } + .panel-header { position: absolute; right: 20px; diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index 9ba434f9040..714910492f7 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -112,7 +112,7 @@ } transition: height 0.4s ease; width: 100%; - z-index: 1039; + z-index: 999; height: 0; background-color: dark-light-diff($primary, $secondary, 90%, -60%); bottom: 0; diff --git a/vendor/assets/javascripts/div_resizer.js b/vendor/assets/javascripts/div_resizer.js index fbcca76beb8..07a99144f87 100644 --- a/vendor/assets/javascripts/div_resizer.js +++ b/vendor/assets/javascripts/div_resizer.js @@ -42,7 +42,12 @@ performDrag = function(e, opts) { thisMousePos = mousePosition(e).y; size = originalDivHeight + (originalPos - thisMousePos); lastMousePos = thisMousePos; - size = Math.min(size, $(window).height()); + + var maxHeight = $(window).height(); + if (opts.maxHeight) { + maxHeight = opts.maxHeight(maxHeight); + } + size = Math.min(size, maxHeight); size = Math.max(min, size); sizePx = size + "px"; if (typeof opts.onDrag === "function") { From c84a2632e38a9f4f0685d8f8f8332bcfdf67b204 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 1 Sep 2015 17:36:34 -0400 Subject: [PATCH 015/224] FIX: Number alignment on categories in hamburger --- app/assets/stylesheets/common/base/menu-panel.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 84ea5e6475f..7ec8405613d 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -66,8 +66,6 @@ .badge-notification { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); background-color: transparent; - vertical-align: top; - padding: 5px 5px 2px 5px; display: inline; } } From 32e5016dbb8c14fd0c8c1f8993e3be9fa8304802 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 1 Sep 2015 17:45:09 -0700 Subject: [PATCH 016/224] FEATURE: Include topic title, category in posts.json --- app/controllers/posts_controller.rb | 1 + app/serializers/post_serializer.rb | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 1e428c6e8c1..9dd5c203726 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -58,6 +58,7 @@ class PostsController < ApplicationController scope: guardian, root: 'latest_posts', add_raw: true, + add_title: true, all_post_actions: counts) ) end diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 4111fbbde2d..5ba620ca27a 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -5,6 +5,7 @@ class PostSerializer < BasicPostSerializer :topic_view, :parent_post, :add_raw, + :add_title, :single_post_link_counts, :draft_sequence, :post_actions, @@ -28,6 +29,9 @@ class PostSerializer < BasicPostSerializer :yours, :topic_id, :topic_slug, + :topic_title, + :topic_html_title, + :category_id, :display_username, :primary_group_name, :version, @@ -73,6 +77,30 @@ class PostSerializer < BasicPostSerializer object.try(:topic).try(:slug) end + def include_topic_title? + @add_title + end + + def include_topic_html_title? + @add_title + end + + def include_category_id? + @add_title + end + + def topic_title + object.topic.title + end + + def topic_html_title + object.topic.fancy_title + end + + def category_id + object.topic.category_id + end + def moderator? !!(object.try(:user).try(:moderator?)) end From 262f561a877e0296d7ca7f6ec9f27edd0d30ca8e Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 2 Sep 2015 12:13:44 +1000 Subject: [PATCH 017/224] FEATURE: relax username rules to allow - and . and leading _ This relaxes our very strict username rules to allow for some long asked for requests - leading _ is now allowed - . is allowed except for trailing char and confusing extensions like .gif .json - dash (-) is now permitted --- .../discourse/components/user-selector.js.es6 | 2 +- .../discourse/dialects/mention_dialect.js | 2 +- .../discourse/lib/user-search.js.es6 | 2 +- app/models/username_validator.rb | 28 ++++++++- config/locales/server.en.yml | 5 +- config/routes.rb | 2 +- spec/models/user_spec.rb | 62 ++++++++++++++----- .../services/username_checker_service_spec.rb | 2 +- 8 files changed, 80 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index d3bed034e29..5b0be280c10 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -25,7 +25,7 @@ export default TextField.extend({ dataSource: function(term) { return userSearch({ - term: term.replace(/[^a-zA-Z0-9_]/, ''), + term: term.replace(/[^a-zA-Z0-9_\-\.]/, ''), topicId: self.get('topicId'), exclude: excludedUsernames(), includeGroups, diff --git a/app/assets/javascripts/discourse/dialects/mention_dialect.js b/app/assets/javascripts/discourse/dialects/mention_dialect.js index 4832d436b85..4f1b8b8da17 100644 --- a/app/assets/javascripts/discourse/dialects/mention_dialect.js +++ b/app/assets/javascripts/discourse/dialects/mention_dialect.js @@ -7,7 +7,7 @@ Discourse.Dialect.inlineRegexp({ start: '@', // NOTE: we really should be using SiteSettings here, but it loads later in process // also, if we do, we must ensure serverside version works as well - matcher: /^(@[A-Za-z0-9][A-Za-z0-9_]{0,40})/, + matcher: /^(@[A-Za-z0-9][A-Za-z0-9_\.\-]{0,40}[A-Za-z0-9])/, wordBoundary: true, emitter: function(matches) { diff --git a/app/assets/javascripts/discourse/lib/user-search.js.es6 b/app/assets/javascripts/discourse/lib/user-search.js.es6 index 790a00ff091..3600814bb8e 100644 --- a/app/assets/javascripts/discourse/lib/user-search.js.es6 +++ b/app/assets/javascripts/discourse/lib/user-search.js.es6 @@ -89,7 +89,7 @@ export default function userSearch(options) { return new Ember.RSVP.Promise(function(resolve) { // TODO site setting for allowed regex in username - if (term.match(/[^a-zA-Z0-9_\.]/)) { + if (term.match(/[^a-zA-Z0-9_\.\-]/)) { resolve([]); return; } diff --git a/app/models/username_validator.rb b/app/models/username_validator.rb index 3b350d289ea..b93ef78a03e 100644 --- a/app/models/username_validator.rb +++ b/app/models/username_validator.rb @@ -30,6 +30,9 @@ class UsernameValidator username_length_max? username_char_valid? username_first_char_valid? + username_last_char_valid? + username_no_double_special? + username_does_not_end_with_confusing_suffix? errors.empty? end @@ -58,15 +61,36 @@ class UsernameValidator def username_char_valid? return unless errors.empty? - if username =~ /[^A-Za-z0-9_]/ + if username =~ /[^A-Za-z0-9_\.\-]/ self.errors << I18n.t(:'user.username.characters') end end def username_first_char_valid? return unless errors.empty? - if username[0] =~ /[^A-Za-z0-9]/ + if username[0] =~ /[^A-Za-z0-9_]/ self.errors << I18n.t(:'user.username.must_begin_with_alphanumeric') end end + + def username_last_char_valid? + return unless errors.empty? + if username[-1] =~ /[^A-Za-z0-9]/ + self.errors << I18n.t(:'user.username.must_end_with_alphanumeric') + end + end + + def username_no_double_special? + return unless errors.empty? + if username =~ /[\-_\.]{2,}/ + self.errors << I18n.t(:'user.username.must_not_contain_two_special_chars_in_seq') + end + end + + def username_does_not_end_with_confusing_suffix? + return unless errors.empty? + if username =~ /\.(json|gif|jpeg|png|htm|js|json|xml|woff|tif|html)/i + self.errors << I18n.t(:'user.username.must_not_contain_confusing_suffix') + end + end end diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 0b7c47b3aff..77a8754d889 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1343,7 +1343,10 @@ en: characters: "must only include numbers, letters and underscores" unique: "must be unique" blank: "must be present" - must_begin_with_alphanumeric: "must begin with a letter or number" + must_begin_with_alphanumeric: "must begin with a letter or number or an underscore" + must_end_with_alphanumeric: "must end with a letter or number" + must_not_contain_two_special_chars_in_seq: "must not contain a sequence of 2 or more special chars (.-_)" + must_not_contain_confusing_suffix: "must not contain a confusing suffix like .json or .png etc." email: not_allowed: "is not allowed from that email provider. Please use another email address." blocked: "is not allowed." diff --git a/config/routes.rb b/config/routes.rb index 3675a5e769a..c3164a2ff81 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,7 +7,7 @@ require_dependency "permalink_constraint" # This used to be User#username_format, but that causes a preload of the User object # and makes Guard not work properly. -USERNAME_ROUTE_FORMAT = /[A-Za-z0-9\_]+/ unless defined? USERNAME_ROUTE_FORMAT +USERNAME_ROUTE_FORMAT = /[A-Za-z0-9\_.\-]+/ unless defined? USERNAME_ROUTE_FORMAT BACKUP_ROUTE_FORMAT = /[a-zA-Z0-9\-_]*\d{4}(-\d{2}){2}-\d{6}\.(tar\.gz|t?gz)/i unless defined? BACKUP_ROUTE_FORMAT Discourse::Application.routes.draw do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e2bd09f772d..5eaa7f64196 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -338,29 +338,57 @@ describe User do end describe 'username format' do - it "should be #{SiteSetting.min_username_length} chars or longer" do - @user = Fabricate.build(:user) - @user.username = 'ss' - expect(@user.save).to eq(false) + def assert_bad(username) + user = Fabricate.build(:user) + user.username = username + expect(user.valid?).to eq(false) end - it "should never end with a ." do - @user = Fabricate.build(:user) - @user.username = 'sam.' - expect(@user.save).to eq(false) + def assert_good(username) + user = Fabricate.build(:user) + user.username = username + expect(user.valid?).to eq(true) end - it "should never contain spaces" do - @user = Fabricate.build(:user) - @user.username = 'sam s' - expect(@user.save).to eq(false) + it "should be SiteSetting.min_username_length chars or longer" do + SiteSetting.min_username_length = 5 + assert_bad("abcd") + assert_good("abcde") end - ['Bad One', 'Giraf%fe', 'Hello!', '@twitter', 'me@example.com', 'no.dots', 'purple.', '.bilbo', '_nope', 'sa$sy'].each do |bad_nickname| - it "should not allow username '#{bad_nickname}'" do - @user = Fabricate.build(:user) - @user.username = bad_nickname - expect(@user.save).to eq(false) + %w{ first.last + first first-last + _name first_last + mc.hammer_nose + UPPERCASE + sgif + }.each do |username| + it "allows #{username}" do + assert_good(username) + end + end + + %w{ + traildot. + has\ space + double__underscore + with%symbol + Exclamation! + @twitter + my@email.com + .tester + sa$sy + sam.json + sam.xml + sam.html + sam.htm + sam.js + sam.woff + sam.Png + sam.gif + }.each do |username| + it "disallows #{username}" do + assert_bad(username) end end end diff --git a/spec/services/username_checker_service_spec.rb b/spec/services/username_checker_service_spec.rb index 258003b3fed..7b9a4094101 100644 --- a/spec/services/username_checker_service_spec.rb +++ b/spec/services/username_checker_service_spec.rb @@ -26,7 +26,7 @@ describe UsernameCheckerService do end it 'rejects usernames that do not start with an alphanumeric character' do - result = @service.check_username('_vincent', @nil_email) + result = @service.check_username('.vincent', @nil_email) expect(result).to have_key(:errors) end end From c2e964455857f4ee92fdaafc70f9879b5b2bf912 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 2 Sep 2015 12:20:35 +1000 Subject: [PATCH 018/224] Revert "flexbox for the post header" This reverts commit bef3084516ebac401a903e30f3b06964d39c4e43. --- .../stylesheets/common/base/header.scss | 14 +---- .../stylesheets/common/base/menu-panel.scss | 3 ++ app/assets/stylesheets/common/base/topic.scss | 7 ++- .../common/components/badges.css.scss | 4 +- .../stylesheets/common/foundation/mixins.scss | 54 ------------------- 5 files changed, 9 insertions(+), 73 deletions(-) diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index 6de1b2fcc3e..bc20cefeaa4 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -6,14 +6,6 @@ background-color: $header_background; box-shadow: 0 2px 4px -1px rgba(0,0,0, .25); - .ember-view { - min-width: 0; //flexbox fix - } - - .title { - @include flex(0,0,auto); - } - .docked & { position: fixed; backface-visibility: hidden; /** do magic for scrolling performance **/ @@ -21,8 +13,6 @@ .contents { margin: 8px 0; - @include flexbox(); - @include align-items(center); } .title { @@ -44,10 +34,8 @@ } .panel { + float: right; position: relative; - margin-left: auto; - min-width: 125px; - @include order(3) } .login-button, button.sign-up-button { diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 7561b34f80d..7ec8405613d 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -270,3 +270,6 @@ div.menu-links-header { margin-right: 0.2em; } } + + + diff --git a/app/assets/stylesheets/common/base/topic.scss b/app/assets/stylesheets/common/base/topic.scss index 873ab685bc6..015b4b68714 100644 --- a/app/assets/stylesheets/common/base/topic.scss +++ b/app/assets/stylesheets/common/base/topic.scss @@ -27,12 +27,11 @@ } .extra-info-wrapper { - @include order(2); - line-height: 1.5; .badge-wrapper { float: left; - margin-left: 2px; - line-height: 1.2; + &.bullet { + margin-top: 5px; + } } } diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index 34b97c1784e..47fb1a1cb6e 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -52,8 +52,8 @@ &.bullet { //bullet category style - @include inline-flex; - @include align-items(baseline); + display: inline-flex; + align-items: baseline; margin-right: 10px; span.badge-category { diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss index e9cb375a50c..55b15a9b537 100644 --- a/app/assets/stylesheets/common/foundation/mixins.scss +++ b/app/assets/stylesheets/common/foundation/mixins.scss @@ -113,57 +113,3 @@ -webkit-transform: $transforms; transform: $transforms; } - -// --------------------------------------------------- - -//Flexbox - -@mixin flexbox() { - display: -webkit-box; - display: -moz-box; - display: -ms-flexbox; - display: -webkit-flex; - display: flex; -} - -@mixin inline-flex() { - display: -webkit-inline-box; - display: -webkit-inline-flex; - display: -moz-inline-box; - display: -ms-inline-flexbox; - display: inline-flex; -} - - -@mixin align-items($alignment) { - -webkit-box-align: $alignment; - -webkit-align-items: $alignment; - -ms-flex-align: $alignment; - -ms-align-items: $alignment; - align-items:$alignment; -} - -@mixin order($val) { - -webkit-box-ordinal-group: $val; - -moz-box-ordinal-group: $val; - -ms-flex-order: $val; - -webkit-order: $val; - order: $val; -} - -@mixin flex($fg: 1, $fs: null, $fb: null) { - - $fg-boxflex: $fg; - - // Box-Flex only supports a flex-grow value - @if type-of($fg) == 'list' { - $fg-boxflex: nth($fg, 1); - } - - -webkit-box-flex: $fg-boxflex; - -webkit-flex: $fg $fs $fb; - -moz-box-flex: $fg-boxflex; - -moz-flex: $fg $fs $fb; - -ms-flex: $fg $fs $fb; - flex: $fg $fs $fb; -} From d8490fb65f2bbf04e592cb07c89a53f22034b658 Mon Sep 17 00:00:00 2001 From: James Kiesel Date: Tue, 1 Sep 2015 20:25:22 -0700 Subject: [PATCH 019/224] Move plugin menu outlet to live with other menus --- app/assets/javascripts/discourse/templates/header.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/header.hbs b/app/assets/javascripts/discourse/templates/header.hbs index d09c4209ff1..ae0ec11d40b 100644 --- a/app/assets/javascripts/discourse/templates/header.hbs +++ b/app/assets/javascripts/discourse/templates/header.hbs @@ -1,3 +1,4 @@ +{{plugin-outlet "header-before-dropdowns"}} {{user-menu visible=userMenuVisible logoutAction="logout"}} {{hamburger-menu visible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}} {{search-menu visible=searchVisible}} @@ -51,7 +52,6 @@ {{/if}} {{/header-dropdown}} {{/if}} - {{plugin-outlet "header-before-dropdowns"}} From 6e04e5bd2c30a326fc2e397927b71c3504a7382e Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 2 Sep 2015 14:57:26 +1000 Subject: [PATCH 020/224] correct routing to allow for wider regex matching username --- config/routes.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index c3164a2ff81..f10ea5c2e95 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -8,6 +8,7 @@ require_dependency "permalink_constraint" # This used to be User#username_format, but that causes a preload of the User object # and makes Guard not work properly. USERNAME_ROUTE_FORMAT = /[A-Za-z0-9\_.\-]+/ unless defined? USERNAME_ROUTE_FORMAT + BACKUP_ROUTE_FORMAT = /[a-zA-Z0-9\-_]*\d{4}(-\d{2}){2}-\d{6}\.(tar\.gz|t?gz)/i unless defined? BACKUP_ROUTE_FORMAT Discourse::Application.routes.draw do @@ -69,6 +70,7 @@ Discourse::Application.routes.draw do get "groups/:type" => "groups#show", constraints: AdminConstraint.new get "groups/:type/:id" => "groups#show", constraints: AdminConstraint.new + get "users/:id.json" => 'users#show' , id: USERNAME_ROUTE_FORMAT, defaults: {format: 'json'} resources :users, id: USERNAME_ROUTE_FORMAT do collection do get "list/:query" => "users#index" @@ -261,6 +263,7 @@ Discourse::Application.routes.draw do get "users/:username/private-messages/:filter" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/messages" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/messages/:filter" => "user_actions#private_messages", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username.json" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT}, defaults: {format: :json} get "users/:username" => "users#show", as: 'user', constraints: {username: USERNAME_ROUTE_FORMAT} put "users/:username" => "users#update", constraints: {username: USERNAME_ROUTE_FORMAT} put "users/:username/emails" => "users#check_emails", constraints: {username: USERNAME_ROUTE_FORMAT} @@ -292,11 +295,12 @@ Discourse::Application.routes.draw do get "users/by-external/:external_id" => "users#show", constraints: {external_id: /[^\/]+/} get "users/:username/flagged-posts" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/deleted-posts" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} - get "user-badges/:username" => "user_badges#username" + get "user-badges/:username.json" => "user_badges#username", constraints: {username: USERNAME_ROUTE_FORMAT}, defaults: {format: :json} + get "user-badges/:username" => "user_badges#username", constraints: {username: USERNAME_ROUTE_FORMAT} - post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar" - get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/ } - get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/ } + post "user_avatar/:username/refresh_gravatar" => "user_avatars#refresh_gravatar", constraints: {username: USERNAME_ROUTE_FORMAT} + get "letter_avatar/:username/:size/:version.png" => "user_avatars#show_letter", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: USERNAME_ROUTE_FORMAT} + get "user_avatar/:hostname/:username/:size/:version.png" => "user_avatars#show", format: false, constraints: { hostname: /[\w\.-]+/, size: /\d+/, username: USERNAME_ROUTE_FORMAT } get "highlight-js/:hostname/:version.js" => "highlight_js#show", format: false, constraints: { hostname: /[\w\.-]+/ } From a0dd0bf1af3d4048e5c2c1864e2273bd20721aa6 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 2 Sep 2015 16:50:40 +1000 Subject: [PATCH 021/224] when looking at your own profile it should be collapsed. --- app/assets/javascripts/discourse/controllers/user.js.es6 | 9 ++++++++- app/assets/javascripts/discourse/templates/user/user.hbs | 7 ++++++- app/assets/stylesheets/desktop/user.scss | 6 ++++++ config/locales/client.en.yml | 1 + 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index 6df5a32d6d1..757779abec8 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -1,5 +1,6 @@ import { exportUserArchive } from 'discourse/lib/export-csv'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; +import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Controller.extend(CanCheckEmails, { indexStream: false, @@ -11,7 +12,10 @@ export default Ember.Controller.extend(CanCheckEmails, { return this.get('content.username') === Discourse.User.currentProp('username'); }.property('content.username'), - collapsedInfo: Em.computed.not('indexStream'), + @computed('indexStream', 'viewingSelf', 'forceExpand') + collapsedInfo(indexStream, viewingSelf, forceExpand){ + return (!indexStream || viewingSelf) && !forceExpand; + }, linkWebsite: Em.computed.not('model.isBasic'), @@ -59,6 +63,9 @@ export default Ember.Controller.extend(CanCheckEmails, { privateMessagesUnreadActive: Em.computed.equal('pmView', 'unread'), actions: { + expandProfile: function() { + this.set('forceExpand', true); + }, adminDelete: function() { Discourse.AdminUser.find(this.get('model.username').toLowerCase()).then(function(user){ user.destroy({deletePosts: true}); diff --git a/app/assets/javascripts/discourse/templates/user/user.hbs b/app/assets/javascripts/discourse/templates/user/user.hbs index 586ef838b35..475708f4ff7 100644 --- a/app/assets/javascripts/discourse/templates/user/user.hbs +++ b/app/assets/javascripts/discourse/templates/user/user.hbs @@ -1,4 +1,4 @@ -
+
@@ -52,6 +52,11 @@ {{#if canInviteToForum}}
  • {{#link-to 'userInvited' class="btn right"}}{{fa-icon "user-plus"}}{{i18n 'user.invited.title'}}{{/link-to}}
  • {{/if}} + {{#if collapsedInfo}} + {{#if viewingSelf}} +
  • {{fa-icon "angle-double-down"}}{{i18n 'user.expand_profile'}}
  • + {{/if}} + {{/if}}
    diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index bce1bfb3a1d..9ce019329d6 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -104,6 +104,12 @@ } +.viewing-self .user-main .about.collapsed-info { + .secondary, .staff-counters { + display: inherit; + } +} + .user-main { margin-bottom: 50px; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 7fe190f1573..0b6db04ac03 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -412,6 +412,7 @@ en: private_messages: "Messages" activity_stream: "Activity" preferences: "Preferences" + expand_profile: "Expand" bookmarks: "Bookmarks" bio: "About me" invited_by: "Invited By" From 0b20ded4fb3b9e44793a3fcf701856d4bdf9c6e3 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 2 Sep 2015 17:18:24 +0800 Subject: [PATCH 022/224] FIX: Timegap only shows up for sequential posts. --- .../discourse/components/time-gap.js.es6 | 2 ++ .../discourse/models/post-stream.js.es6 | 17 ++++++----------- .../javascripts/discourse/templates/post.hbs | 2 +- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/discourse/components/time-gap.js.es6 b/app/assets/javascripts/discourse/components/time-gap.js.es6 index 3cd887ec43a..9e410c4b73e 100644 --- a/app/assets/javascripts/discourse/components/time-gap.js.es6 +++ b/app/assets/javascripts/discourse/components/time-gap.js.es6 @@ -2,6 +2,8 @@ import SmallActionComponent from 'discourse/components/small-action'; export default SmallActionComponent.extend({ classNames: ['time-gap'], + classNameBindings: ['hideTimeGap::hidden'], + hideTimeGap: Em.computed.alias('postStream.hasNoFilters'), icon: 'clock-o', description: function() { diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index 9f625f3477c..b8a71fd6514 100644 --- a/app/assets/javascripts/discourse/models/post-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/post-stream.js.es6 @@ -5,18 +5,13 @@ function calcDayDiff(p1, p2) { if (!p1) { return; } const date = p1.get('created_at'); - if (date) { - if (p2) { - const numDiff = p1.get('post_number') - p2.get('post_number'); - if (numDiff === 1) { - const lastDate = p2.get('created_at'); - if (lastDate) { - const delta = new Date(date).getTime() - new Date(lastDate).getTime(); - const days = Math.round(delta / (1000 * 60 * 60 * 24)); + if (date && p2) { + const lastDate = p2.get('created_at'); + if (lastDate) { + const delta = new Date(date).getTime() - new Date(lastDate).getTime(); + const days = Math.round(delta / (1000 * 60 * 60 * 24)); - p1.set('daysSincePrevious', days); - } - } + p1.set('daysSincePrevious', days); } } } diff --git a/app/assets/javascripts/discourse/templates/post.hbs b/app/assets/javascripts/discourse/templates/post.hbs index 67903530253..0ea628c93ad 100644 --- a/app/assets/javascripts/discourse/templates/post.hbs +++ b/app/assets/javascripts/discourse/templates/post.hbs @@ -1,7 +1,7 @@ {{post-gap post=this postStream=controller.model.postStream before="true"}} {{#if hasTimeGap}} - {{time-gap daysAgo=daysSincePrevious}} + {{time-gap daysAgo=daysSincePrevious postStream=controller.model.postStream}} {{/if}}
    From 1fbc142b0c421ea68836eff9b46d257010ad9626 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 2 Sep 2015 12:29:29 -0400 Subject: [PATCH 023/224] UX: add screen reader support to notifications. (title attribute is ignored by screen readers) --- .../discourse/components/notification-item.js.es6 | 14 ++++++++------ config/locales/client.en.yml | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/components/notification-item.js.es6 b/app/assets/javascripts/discourse/components/notification-item.js.es6 index f0add4c2f35..2aa93e141f4 100644 --- a/app/assets/javascripts/discourse/components/notification-item.js.es6 +++ b/app/assets/javascripts/discourse/components/notification-item.js.es6 @@ -4,17 +4,19 @@ export default Ember.Component.extend({ tagName: 'li', classNameBindings: ['notification.read', 'notification.is_warning'], - scope: function() { + name: function() { var notificationType = this.get("notification.notification_type"); var lookup = this.site.get("notificationLookup"); - var name = lookup[notificationType]; + return lookup[notificationType]; + }.property("notification.notification_type"), - if (name === "custom") { + scope: function() { + if (this.get("name") === "custom") { return this.get("notification.data.message"); } else { - return "notifications." + name; + return "notifications." + this.get("name"); } - }.property("notification.notification_type"), + }.property("name"), url: function() { const it = this.get('notification'); @@ -57,7 +59,7 @@ export default Ember.Component.extend({ const url = this.get('url'); if (url) { - buffer.push('' + text + ''); + buffer.push('' + text + ''); } else { buffer.push(text); } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0b6db04ac03..2d75eef63a5 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -899,6 +899,21 @@ en: linked: "

    {{username}} {{description}}

    " granted_badge: "

    Earned '{{description}}'

    " + alt: + mentioned: "Mentioned by" + quoted: "Quoted by" + replied: "Replied" + posted: "Post by" + edited: "Edit your post by" + liked: "Liked your post" + private_message: "Private message from" + invited_to_private_message: "Invited to a private message from" + invited_to_topic: "Invited to a topic from" + invitee_accepted: "Invite accepted by" + moved_post: "Your post was moved by" + linked: "Link to your post" + granted_badge: "Badge granted" + popup: mentioned: '{{username}} mentioned you in "{{topic}}" - {{site_title}}' quoted: '{{username}} quoted you in "{{topic}}" - {{site_title}}' From a76d1079b25c48f6d2048274cf1648bad6d7ea2c Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Sep 2015 13:38:59 -0400 Subject: [PATCH 024/224] Support jumping to messages --- app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 | 3 ++- .../discourse/templates/modal/keyboard-shortcuts-help.hbs | 1 + config/locales/client.en.yml | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index be0f13800db..29dbc01d669 100644 --- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -8,7 +8,8 @@ const PATH_BINDINGS = { 'g c': '/categories', 'g t': '/top', 'g b': '/bookmarks', - 'g p': '/my/activity' + 'g p': '/my/activity', + 'g m': '/my/messages' }, SELECTED_POST_BINDINGS = { diff --git a/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs b/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs index 7581daf46fd..4b1053397a2 100644 --- a/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs +++ b/app/assets/javascripts/discourse/templates/modal/keyboard-shortcuts-help.hbs @@ -11,6 +11,7 @@
  • {{{i18n 'keyboard_shortcuts_help.jump_to.top'}}}
  • {{{i18n 'keyboard_shortcuts_help.jump_to.bookmarks'}}}
  • {{{i18n 'keyboard_shortcuts_help.jump_to.profile'}}}
  • +
  • {{{i18n 'keyboard_shortcuts_help.jump_to.messages'}}}
  • {{i18n 'keyboard_shortcuts_help.navigation.title'}}

      diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 2d75eef63a5..76feed42e2d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2582,6 +2582,7 @@ en: top: 'g, t Top' bookmarks: 'g, b Bookmarks' profile: 'g, p Profile' + messages: 'g, m Messages' navigation: title: 'Navigation' jump: '# Go to post #' From a501947d67de04bc8000da9ce4e2b109a1603af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 2 Sep 2015 20:25:18 +0200 Subject: [PATCH 025/224] FEATURE: suppress categories from the homepage --- .../javascripts/discourse/models/category.js | 3 +- .../discourse/routes/build-topic-route.js.es6 | 13 +- .../components/edit-category-settings.hbs | 7 + app/controllers/categories_controller.rb | 4 +- app/controllers/list_controller.rb | 141 ++++++++---------- app/models/category_list.rb | 2 + app/serializers/category_serializer.rb | 5 + config/locales/client.en.yml | 1 + ..._add_suppress_from_homepage_to_category.rb | 5 + lib/discourse.rb | 8 - lib/topic_query.rb | 44 +++--- spec/controllers/list_controller_spec.rb | 18 +-- 12 files changed, 116 insertions(+), 135 deletions(-) create mode 100644 db/migrate/20150828155137_add_suppress_from_homepage_to_category.rb diff --git a/app/assets/javascripts/discourse/models/category.js b/app/assets/javascripts/discourse/models/category.js index 7c3cefc1e55..33b57894a7f 100644 --- a/app/assets/javascripts/discourse/models/category.js +++ b/app/assets/javascripts/discourse/models/category.js @@ -77,7 +77,8 @@ Discourse.Category = Discourse.Model.extend({ background_url: this.get('background_url'), allow_badges: this.get('allow_badges'), custom_fields: this.get('custom_fields'), - topic_template: this.get('topic_template') + topic_template: this.get('topic_template'), + suppress_from_homepage: this.get('suppress_from_homepage'), }, type: this.get('id') ? 'PUT' : 'POST' }); diff --git a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 index 185c2fd529a..0011b030554 100644 --- a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 @@ -40,7 +40,6 @@ function findTopicList(store, filter, filterParams, extras) { session.setProperties({topicList: null, topicListScrollPosition: null}); } - // Clean up any string parameters that might slip through filterParams = filterParams || {}; Ember.keys(filterParams).forEach(function(k) { @@ -50,17 +49,7 @@ function findTopicList(store, filter, filterParams, extras) { } }); - const findParams = {}; - Discourse.SiteSettings.top_menu.split('|').forEach(function (i) { - if (i.indexOf(filter) === 0) { - const exclude = i.split("-"); - if (exclude && exclude.length === 2) { - findParams.exclude_category = exclude[1]; - } - } - }); - return resolve(store.findFiltered('topicList', { filter, params:_.extend(findParams, filterParams || {})})); - + return resolve(store.findFiltered('topicList', { filter, params: filterParams || {} })); }).then(function(list) { list.set('listParams', filterParams); if (tracking) { diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs index 9dab35610f3..dd943f94944 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs @@ -44,4 +44,11 @@ {{/if}}
    +
    + +
    + {{plugin-outlet "category-custom-settings"}} diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 10500fc4115..9d3ac191e9c 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -16,6 +16,7 @@ class CategoriesController < ApplicationController options = {} options[:latest_posts] = params[:latest_posts] || SiteSetting.category_featured_topics options[:parent_category_id] = params[:parent_category_id] + options[:is_homepage] = current_homepage == "categories".freeze @list = CategoryList.new(guardian, options) @list.draft_key = Draft::NEW_TOPIC @@ -24,7 +25,7 @@ class CategoriesController < ApplicationController discourse_expires_in 1.minute - unless current_homepage == 'categories' + unless current_homepage == "categories" @title = I18n.t('js.filters.categories.title') end @@ -139,6 +140,7 @@ class CategoriesController < ApplicationController :position, :email_in, :email_in_allow_strangers, + :suppress_from_homepage, :parent_category_id, :auto_close_hours, :auto_close_based_on_last_post, diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index e5b6f638879..d894ee1f37c 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -5,36 +5,34 @@ class ListController < ApplicationController skip_before_filter :check_xhr - @@categories = [ + before_filter :set_category, only: [ # filtered topics lists - Discourse.filters.map { |f| "category_#{f}".to_sym }, - Discourse.filters.map { |f| "category_none_#{f}".to_sym }, - Discourse.filters.map { |f| "parent_category_category_#{f}".to_sym }, - Discourse.filters.map { |f| "parent_category_category_none_#{f}".to_sym }, + Discourse.filters.map { |f| :"category_#{f}" }, + Discourse.filters.map { |f| :"category_none_#{f}" }, + Discourse.filters.map { |f| :"parent_category_category_#{f}" }, + Discourse.filters.map { |f| :"parent_category_category_none_#{f}" }, # top summaries :category_top, :category_none_top, :parent_category_category_top, # top pages (ie. with a period) - TopTopic.periods.map { |p| "category_top_#{p}".to_sym }, - TopTopic.periods.map { |p| "category_none_top_#{p}".to_sym }, - TopTopic.periods.map { |p| "parent_category_category_top_#{p}".to_sym }, + TopTopic.periods.map { |p| :"category_top_#{p}" }, + TopTopic.periods.map { |p| :"category_none_top_#{p}" }, + TopTopic.periods.map { |p| :"parent_category_category_top_#{p}" }, # category feeds :category_feed, ].flatten - before_filter :set_category, only: @@categories - before_filter :ensure_logged_in, except: [ :topics_by, # anonymous filters Discourse.anonymous_filters, - Discourse.anonymous_filters.map { |f| "#{f}_feed".to_sym }, + Discourse.anonymous_filters.map { |f| "#{f}_feed" }, # anonymous categorized filters - Discourse.anonymous_filters.map { |f| "category_#{f}".to_sym }, - Discourse.anonymous_filters.map { |f| "category_none_#{f}".to_sym }, - Discourse.anonymous_filters.map { |f| "parent_category_category_#{f}".to_sym }, - Discourse.anonymous_filters.map { |f| "parent_category_category_none_#{f}".to_sym }, + Discourse.anonymous_filters.map { |f| :"category_#{f}" }, + Discourse.anonymous_filters.map { |f| :"category_none_#{f}" }, + Discourse.anonymous_filters.map { |f| :"parent_category_category_#{f}" }, + Discourse.anonymous_filters.map { |f| :"parent_category_category_none_#{f}" }, # category feeds :category_feed, # top summaries @@ -43,14 +41,14 @@ class ListController < ApplicationController :category_none_top, :parent_category_category_top, # top pages (ie. with a period) - TopTopic.periods.map { |p| "top_#{p}".to_sym }, - TopTopic.periods.map { |p| "category_top_#{p}".to_sym }, - TopTopic.periods.map { |p| "category_none_top_#{p}".to_sym }, - TopTopic.periods.map { |p| "parent_category_category_top_#{p}".to_sym }, + TopTopic.periods.map { |p| :"top_#{p}" }, + TopTopic.periods.map { |p| :"category_top_#{p}" }, + TopTopic.periods.map { |p| :"category_none_top_#{p}" }, + TopTopic.periods.map { |p| :"parent_category_category_top_#{p}" }, ].flatten # Create our filters - Discourse.filters.each_with_index do |filter, idx| + Discourse.filters.each do |filter| define_method(filter) do |options = nil| list_opts = build_topic_list_options list_opts.merge!(options) if options @@ -60,6 +58,10 @@ class ListController < ApplicationController list_opts[:no_definitions] = true end + if filter.to_s == current_homepage + list_opts.merge!(exclude_category_ids: get_excluded_category_ids(list_opts[:category])) + end + list = TopicQuery.new(user, list_opts).public_send("list_#{filter}") list.more_topics_url = construct_url_with(:next, list_opts) list.prev_topics_url = construct_url_with(:prev, list_opts) @@ -83,34 +85,20 @@ class ListController < ApplicationController define_method("category_#{filter}") do canonical_url "#{Discourse.base_url}#{@category.url}" - self.send(filter, { category: @category.id }) + self.send(filter, category: @category.id) end define_method("category_none_#{filter}") do - self.send(filter, { category: @category.id, no_subcategories: true }) + self.send(filter, category: @category.id, no_subcategories: true) end define_method("parent_category_category_#{filter}") do canonical_url "#{Discourse.base_url}#{@category.url}" - self.send(filter, { category: @category.id }) + self.send(filter, category: @category.id) end define_method("parent_category_category_none_#{filter}") do - self.send(filter, { category: @category.id }) - end - end - - Discourse.feed_filters.each do |filter| - define_method("#{filter}_feed") do - discourse_expires_in 1.minute - - @title = "#{SiteSetting.title} - #{I18n.t("rss_description.#{filter}")}" - @link = "#{Discourse.base_url}/#{filter}" - @description = I18n.t("rss_description.#{filter}") - @atom_link = "#{Discourse.base_url}/#{filter}.rss" - @topic_list = TopicQuery.new(nil, order: 'created').public_send("list_#{filter}") - - render 'list', formats: [:rss] + self.send(filter, category: @category.id) end end @@ -127,14 +115,26 @@ class ListController < ApplicationController end end + def latest_feed + discourse_expires_in 1.minute + + @title = "#{SiteSetting.title} - #{I18n.t("rss_description.latest")}" + @link = "#{Discourse.base_url}/latest" + @atom_link = "#{Discourse.base_url}/latest.rss" + @description = I18n.t("rss_description.latest") + @topic_list = TopicQuery.new(nil, order: 'created').list_latest + + render 'list', formats: [:rss] + end + def category_feed guardian.ensure_can_see!(@category) discourse_expires_in 1.minute @title = @category.name @link = "#{Discourse.base_url}#{@category.url}" - @description = "#{I18n.t('topics_in_category', category: @category.name)} #{@category.description}" @atom_link = "#{Discourse.base_url}#{@category.url}.rss" + @description = "#{I18n.t('topics_in_category', category: @category.name)} #{@category.description}" @topic_list = TopicQuery.new.list_new_in_category(@category) render 'list', formats: [:rss] @@ -147,15 +147,15 @@ class ListController < ApplicationController end def category_top - top({ category: @category.id }) + top(category: @category.id) end def category_none_top - top({ category: @category.id, no_subcategories: true }) + top(category: @category.id, no_subcategories: true) end def parent_category_category_top - top({ category: @category.id }) + top(category: @category.id) end TopTopic.periods.each do |period| @@ -163,6 +163,11 @@ class ListController < ApplicationController top_options = build_topic_list_options top_options.merge!(options) if options top_options[:per_page] = SiteSetting.topics_per_period_in_top_page + + if "top".freeze == current_homepage + top_options.merge!(exclude_category_ids: get_excluded_category_ids(top_options[:category])) + end + user = list_target_user list = TopicQuery.new(user, top_options).list_top_for(period) list.for_period = period @@ -177,15 +182,15 @@ class ListController < ApplicationController end define_method("category_top_#{period}") do - self.send("top_#{period}", { category: @category.id }) + self.send("top_#{period}", category: @category.id) end define_method("category_none_top_#{period}") do - self.send("top_#{period}", { category: @category.id, no_subcategories: true }) + self.send("top_#{period}", category: @category.id, no_subcategories: true) end define_method("parent_category_category_top_#{period}") do - self.send("top_#{period}", { category: @category.id }) + self.send("top_#{period}", category: @category.id) end end @@ -204,16 +209,15 @@ class ListController < ApplicationController end end - private def page_params(opts = nil) opts ||= {} - route_params = {format: 'json'} - route_params[:category] = @category.slug_for_url if @category + route_params = { format: 'json' } + route_params[:category] = @category.slug_for_url if @category route_params[:parent_category] = @category.parent_category.slug_for_url if @category && @category.parent_category - route_params[:order] = opts[:order] if opts[:order].present? - route_params[:ascending] = opts[:ascending] if opts[:ascending].present? + route_params[:order] = opts[:order] if opts[:order].present? + route_params[:ascending] = opts[:ascending] if opts[:ascending].present? route_params end @@ -235,11 +239,10 @@ class ListController < ApplicationController end def build_topic_list_options - # exclude_category = 1. from params / 2. parsed from top menu / 3. nil options = { page: params[:page], topic_ids: param_to_integer_list(:topic_ids), - exclude_category: (params[:exclude_category] || select_menu_item.try(:filter)), + exclude_category_ids: params[:exclude_category_ids], category: params[:category], order: params[:order], ascending: params[:ascending], @@ -257,17 +260,6 @@ class ListController < ApplicationController options end - def select_menu_item - menu_item = SiteSetting.top_menu_items.select do |mu| - (mu.has_specific_category? && mu.specific_category == @category.try(:slug)) || - action_name == mu.name || - (action_name.include?("top") && mu.name == "top") - end.first - - menu_item = nil if menu_item.try(:has_specific_category?) && menu_item.specific_category == @category.try(:slug) - menu_item - end - def list_target_user if params[:user_id] && guardian.is_staff? User.find(params[:user_id].to_i) @@ -290,25 +282,16 @@ class ListController < ApplicationController url.sub('.json?','?') end - def generate_top_lists(options) - top = TopLists.new - - options[:per_page] = SiteSetting.topics_per_period_in_top_summary - topic_query = TopicQuery.new(current_user, options) - - periods = [ListController.best_period_for(current_user.try(:previous_visit_at), options[:category])] - - periods.each { |period| top.send("#{period}=", topic_query.list_top_for(period)) } - - top + def get_excluded_category_ids(current_category=nil) + exclude_category_ids = Category.where(suppress_from_homepage: true) + exclude_category_ids = exclude_category_ids.where.not(id: current_category) if current_category + exclude_category_ids.pluck(:id) end def self.best_period_for(previous_visit_at, category_id=nil) best_periods_for(previous_visit_at).each do |period| top_topics = TopTopic.where("#{period}_score > 0") - if category_id - top_topics = top_topics.joins(:topic).where("topics.category_id = ?", category_id) - end + top_topics = top_topics.joins(:topic).where("topics.category_id = ?", category_id) if category_id return period if top_topics.count >= SiteSetting.topics_per_period_in_top_page end # default period is yearly @@ -318,8 +301,8 @@ class ListController < ApplicationController def self.best_periods_for(date) date ||= 1.year.ago periods = [] - periods << :daily if date > 8.days.ago - periods << :weekly if date > 35.days.ago + periods << :daily if date > 8.days.ago + periods << :weekly if date > 35.days.ago periods << :monthly if date > 180.days.ago periods << :yearly periods diff --git a/app/models/category_list.rb b/app/models/category_list.rb index b5cae822b87..2e32369fda1 100644 --- a/app/models/category_list.rb +++ b/app/models/category_list.rb @@ -66,6 +66,8 @@ class CategoryList @categories = @categories.where('categories.parent_category_id = ?', @options[:parent_category_id].to_i) end + @categories = @categories.where(suppress_from_homepage: false) if @options[:is_homepage] + if SiteSetting.fixed_category_positions @categories = @categories.order('position ASC').order('id ASC') else diff --git a/app/serializers/category_serializer.rb b/app/serializers/category_serializer.rb index 1757652094b..1bf08f89fcb 100644 --- a/app/serializers/category_serializer.rb +++ b/app/serializers/category_serializer.rb @@ -8,6 +8,7 @@ class CategorySerializer < BasicCategorySerializer :position, :email_in, :email_in_allow_strangers, + :suppress_from_homepage, :can_delete, :cannot_delete_reason, :allow_badges, @@ -56,4 +57,8 @@ class CategorySerializer < BasicCategorySerializer scope && scope.can_edit?(object) end + def include_suppress_from_homepage? + scope && scope.can_edit?(object) + end + end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 76feed42e2d..7f4e13300e9 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1539,6 +1539,7 @@ en: email_in_allow_strangers: "Accept emails from anonymous users with no accounts" email_in_disabled: "Posting new topics via email is disabled in the Site Settings. To enable posting new topics via email, " email_in_disabled_click: 'enable the "email in" setting.' + suppress_from_homepage: "Suppress this category from the homepage." allow_badges_label: "Allow badges to be awarded in this category" edit_permissions: "Edit Permissions" add_permission: "Add Permission" diff --git a/db/migrate/20150828155137_add_suppress_from_homepage_to_category.rb b/db/migrate/20150828155137_add_suppress_from_homepage_to_category.rb new file mode 100644 index 00000000000..e706f40f97c --- /dev/null +++ b/db/migrate/20150828155137_add_suppress_from_homepage_to_category.rb @@ -0,0 +1,5 @@ +class AddSuppressFromHomepageToCategory < ActiveRecord::Migration + def change + add_column :categories, :suppress_from_homepage, :boolean, default: false + end +end diff --git a/lib/discourse.rb b/lib/discourse.rb index b32fcf736aa..82b1280eed2 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -64,18 +64,10 @@ module Discourse @filters ||= [:latest, :unread, :new, :read, :posted, :bookmarks] end - def self.feed_filters - @feed_filters ||= [:latest] - end - def self.anonymous_filters @anonymous_filters ||= [:latest, :top, :categories] end - def self.logged_in_filters - @logged_in_filters ||= Discourse.filters - Discourse.anonymous_filters - end - def self.top_menu_items @top_menu_items ||= Discourse.filters + [:category, :categories, :top] end diff --git a/lib/topic_query.rb b/lib/topic_query.rb index 9ffa5d8b598..25e2b6fd488 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -1,15 +1,16 @@ # -# Helps us find topics. Returns a TopicList object containing the topics -# found. +# Helps us find topics. +# Returns a TopicList object containing the topics found. # + require_dependency 'topic_list' require_dependency 'suggested_topics_builder' require_dependency 'topic_query_sql' class TopicQuery # Could be rewritten to %i if Ruby 1.9 is no longer supported - VALID_OPTIONS = %w(except_topic_ids - exclude_category + VALID_OPTIONS = %i(except_topic_ids + exclude_category_ids limit page per_page @@ -27,8 +28,7 @@ class TopicQuery search slow_platform filter - q - ).map(&:to_sym) + q) # Maps `order` to a columns in `topics` SORTABLE_MAPPING = { @@ -301,14 +301,17 @@ class TopicQuery if options[:no_subcategories] result = result.where('categories.id = ?', category_id) else - result = result.where('categories.id = ? or (categories.parent_category_id = ? AND categories.topic_id <> topics.id)', category_id, category_id) + result = result.where('categories.id = :category_id OR (categories.parent_category_id = :category_id AND categories.topic_id <> topics.id)', category_id: category_id) end result = result.references(:categories) end result = apply_ordering(result, options) result = result.listable_topics.includes(:category) - result = result.where('categories.name is null or categories.name <> ?', options[:exclude_category]).references(:categories) if options[:exclude_category] + + if options[:exclude_category_ids] && options[:exclude_category_ids].is_a?(Array) && options[:exclude_category_ids].size > 0 + result = result.where("categories.id NOT IN (?)", options[:exclude_category_ids]).references(:categories) + end # Don't include the category topics if excluded if options[:no_definitions] @@ -393,19 +396,20 @@ class TopicQuery def remove_muted_categories(list, user, opts=nil) category_id = get_category_id(opts[:exclude]) if opts + if user - list = list.where("NOT EXISTS( - SELECT 1 FROM category_users cu - WHERE cu.user_id = ? AND - cu.category_id = topics.category_id AND - cu.notification_level = ? AND - cu.category_id <> ? - )", - user.id, - CategoryUser.notification_levels[:muted], - category_id || -1 - ) - .references('cu') + list = list.references("cu") + .where(" + NOT EXISTS ( + SELECT 1 + FROM category_users cu + WHERE cu.user_id = :user_id + AND cu.category_id = topics.category_id + AND cu.notification_level = :muted + AND cu.category_id <> :category_id + )", user_id: user.id, + muted: CategoryUser.notification_levels[:muted], + category_id: category_id || -1) end list diff --git a/spec/controllers/list_controller_spec.rb b/spec/controllers/list_controller_spec.rb index 6f55de5b607..41f343ca10b 100644 --- a/spec/controllers/list_controller_spec.rb +++ b/spec/controllers/list_controller_spec.rb @@ -32,12 +32,6 @@ describe ListController do end end - Discourse.logged_in_filters.each do |filter| - context "#{filter}" do - it { expect { xhr :get, filter }.to raise_error(Discourse::NotLoggedIn) } - end - end - it 'allows users to filter on a set of topic ids' do p = create_post @@ -51,14 +45,10 @@ describe ListController do describe 'RSS feeds' do - Discourse.feed_filters.each do |filter| - - it 'renders RSS' do - get "#{filter}_feed", format: :rss - expect(response).to be_success - expect(response.content_type).to eq('application/rss+xml') - end - + it 'renders RSS' do + get "latest_feed", format: :rss + expect(response).to be_success + expect(response.content_type).to eq('application/rss+xml') end end From b3a930f2edfb79efbaddc9c35cc30952900beb09 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Sep 2015 14:12:40 -0400 Subject: [PATCH 026/224] DRY up header height calculation --- .../discourse/components/menu-panel.js.es6 | 7 +-- .../discourse/views/composer.js.es6 | 7 +-- .../javascripts/discourse/views/header.js.es6 | 55 ++++++++++--------- app/assets/javascripts/main_include.js | 1 + 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/app/assets/javascripts/discourse/components/menu-panel.js.es6 b/app/assets/javascripts/discourse/components/menu-panel.js.es6 index 6d1edefb26a..a6a0b725a5e 100644 --- a/app/assets/javascripts/discourse/components/menu-panel.js.es6 +++ b/app/assets/javascripts/discourse/components/menu-panel.js.es6 @@ -1,4 +1,5 @@ import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators'; +import { headerHeight } from 'discourse/views/header'; const PANEL_BODY_MARGIN = 30; const mutationSupport = !!window['MutationObserver']; @@ -46,11 +47,7 @@ export default Ember.Component.extend({ $('body').addClass('drop-down-visible'); } else { $panelBody.height('auto'); - const $header = $('header.d-header'); - const headerOffset = $header.offset(); - const headerOffsetTop = (headerOffset) ? headerOffset.top : 0; - const headerHeight = parseInt($header.height() + headerOffsetTop - $window.scrollTop() + 3); - this.$().css({ left: "auto", top: headerHeight + "px" }); + this.$().css({ left: "auto", top: headerHeight() + "px" }); $('body').removeClass('drop-down-visible'); } diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index a8b49480180..b17c9c73bad 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -5,6 +5,7 @@ import avatarTemplate from 'discourse/lib/avatar-template'; import positioningWorkaround from 'discourse/lib/safari-hacks'; import debounce from 'discourse/lib/debounce'; import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions'; +import { headerHeight } from 'discourse/views/header'; const ComposerView = Ember.View.extend(Ember.Evented, { _lastKeyTimeout: null, @@ -124,11 +125,7 @@ const ComposerView = Ember.View.extend(Ember.Evented, { $replyControl.DivResizer({ maxHeight(winHeight) { - const $header = $('header.d-header'); - const headerOffset = $header.offset(); - const headerOffsetTop = (headerOffset) ? headerOffset.top : 0; - const headerHeight = parseInt($header.height() + headerOffsetTop - $(window).scrollTop() + 5); - return winHeight - headerHeight; + return winHeight - headerHeight(); }, resize: runResize, onDrag: (sizePx) => this.movePanels(sizePx) diff --git a/app/assets/javascripts/discourse/views/header.js.es6 b/app/assets/javascripts/discourse/views/header.js.es6 index d7088739c32..17062acde27 100644 --- a/app/assets/javascripts/discourse/views/header.js.es6 +++ b/app/assets/javascripts/discourse/views/header.js.es6 @@ -1,52 +1,55 @@ +import { on } from 'ember-addons/ember-computed-decorators'; + export default Ember.View.extend({ tagName: 'header', classNames: ['d-header', 'clearfix'], classNameBindings: ['editingTopic'], templateName: 'header', - examineDockHeader: function() { - var headerView = this; - + examineDockHeader() { // Check the dock after the current run loop. While rendering, // it's much slower to calculate `outlet.offset()` - Em.run.next(function () { - if (!headerView.docAt) { - var outlet = $('#main-outlet'); + Ember.run.next(() => { + if (!this.docAt) { + const outlet = $('#main-outlet'); if (!(outlet && outlet.length === 1)) return; - headerView.docAt = outlet.offset().top; + this.docAt = outlet.offset().top; } - var offset = window.pageYOffset || $('html').scrollTop(); - if (offset >= headerView.docAt) { - if (!headerView.dockedHeader) { + const offset = window.pageYOffset || $('html').scrollTop(); + if (offset >= this.docAt) { + if (!this.dockedHeader) { $('body').addClass('docked'); - headerView.dockedHeader = true; + this.dockedHeader = true; } } else { - if (headerView.dockedHeader) { + if (this.dockedHeader) { $('body').removeClass('docked'); - headerView.dockedHeader = false; + this.dockedHeader = false; } } }); }, - _tearDown: function() { + @on('willDestroyElement') + _tearDown() { $(window).unbind('scroll.discourse-dock'); $(document).unbind('touchmove.discourse-dock'); this.$('a.unread-private-messages, a.unread-notifications, a[data-notifications]').off('click.notifications'); $('body').off('keydown.header'); - }.on('willDestroyElement'), + }, - _setup: function() { - const self = this; - - $(window).bind('scroll.discourse-dock', function() { - self.examineDockHeader(); - }); - $(document).bind('touchmove.discourse-dock', function() { - self.examineDockHeader(); - }); - self.examineDockHeader(); - }.on('didInsertElement') + @on('didInsertElement') + _setup() { + $(window).bind('scroll.discourse-dock', () => this.examineDockHeader()); + $(document).bind('touchmove.discourse-dock', () => this.examineDockHeader()); + this.examineDockHeader(); + } }); + +export function headerHeight() { + const $header = $('header.d-header'); + const headerOffset = $header.offset(); + const headerOffsetTop = (headerOffset) ? headerOffset.top : 0; + return parseInt($header.height() + headerOffsetTop - $(window).scrollTop() + 5); +} diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 25238bf9235..1181452b4f5 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -73,6 +73,7 @@ //= require ./discourse/components/notifications-button //= require ./discourse/components/topic-notifications-button //= require ./discourse/lib/link-mentions +//= require ./discourse/views/header //= require ./discourse/views/composer //= require ./discourse/lib/show-modal //= require ./discourse/lib/screen-track From e624b7198dbff56cae33ada62bf5b51cff8138fa Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Sep 2015 14:29:53 -0400 Subject: [PATCH 027/224] Try to estimate the amount of notifications to return based on height --- .../javascripts/discourse/components/user-menu.js.es6 | 6 +++++- app/controllers/notifications_controller.rb | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/components/user-menu.js.es6 b/app/assets/javascripts/discourse/components/user-menu.js.es6 index a6a7e46be88..b60a0eb8282 100644 --- a/app/assets/javascripts/discourse/components/user-menu.js.es6 +++ b/app/assets/javascripts/discourse/components/user-menu.js.es6 @@ -1,5 +1,6 @@ import { url } from 'discourse/lib/computed'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import { headerHeight } from 'discourse/views/header'; export default Ember.Component.extend({ classNames: ['user-menu'], @@ -43,10 +44,13 @@ export default Ember.Component.extend({ refreshNotifications() { if (this.get('loadingNotifications')) { return; } + // estimate (poorly) the amount of notifications to return + const limit = Math.round(($(window).height() - headerHeight()) / 50); + // TODO: It's a bit odd to use the store in a component, but this one really // wants to reach out and grab notifications const store = this.container.lookup('store:main'); - const stale = store.findStale('notification', {recent: true}); + const stale = store.findStale('notification', {recent: true, limit }); if (stale.hasResults) { this.set('notifications', stale.results); diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 407d63ce2d2..53e26afda2d 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -7,7 +7,11 @@ class NotificationsController < ApplicationController def index user = current_user if params[:recent].present? - notifications = Notification.recent_report(current_user, 15) + + limit = params[:limit].to_i || 15 + limit = 50 if limit > 50 + + notifications = Notification.recent_report(current_user, limit) if notifications.present? # ordering can be off due to PMs From 5984b62347da402a48d202289195e9c3d5b386cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 2 Sep 2015 20:43:15 +0200 Subject: [PATCH 028/224] FIX: ensure we remove 'category_users' records when a user is deleted --- app/models/user.rb | 3 ++- spec/models/category_user_spec.rb | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 8aa61600b4b..a329f88dc05 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -17,11 +17,12 @@ class User < ActiveRecord::Base has_many :posts has_many :notifications, dependent: :destroy has_many :topic_users, dependent: :destroy + has_many :category_users, dependent: :destroy has_many :topics has_many :user_open_ids, dependent: :destroy has_many :user_actions, dependent: :destroy has_many :post_actions, dependent: :destroy - has_many :user_badges, -> {where('user_badges.badge_id IN (SELECT id FROM badges where enabled)')}, dependent: :destroy + has_many :user_badges, -> { where('user_badges.badge_id IN (SELECT id FROM badges WHERE enabled)') }, dependent: :destroy has_many :badges, through: :user_badges has_many :email_logs, dependent: :delete_all has_many :post_timings diff --git a/spec/models/category_user_spec.rb b/spec/models/category_user_spec.rb index 34927139996..2365f611b5c 100644 --- a/spec/models/category_user_spec.rb +++ b/spec/models/category_user_spec.rb @@ -80,6 +80,19 @@ describe CategoryUser do expect(TopicUser.get(post.topic, user)).to be_blank end + it "is destroyed when a user is deleted" do + user = Fabricate(:user) + category = Fabricate(:category) + + CategoryUser.create!(user: user, category: category, notification_level: CategoryUser.notification_levels[:watching]) + + expect(CategoryUser.where(user_id: user.id).count).to eq(1) + + user.destroy! + + expect(CategoryUser.where(user_id: user.id).count).to eq(0) + end + end end From f11bdd13fcdf0952bed3d912afe48266636ce295 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Sep 2015 15:12:35 -0400 Subject: [PATCH 029/224] FIX: Menu panels scrolled weird in iOS --- .../javascripts/discourse/components/menu-panel.js.es6 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/components/menu-panel.js.es6 b/app/assets/javascripts/discourse/components/menu-panel.js.es6 index a6a0b725a5e..18a36917c00 100644 --- a/app/assets/javascripts/discourse/components/menu-panel.js.es6 +++ b/app/assets/javascripts/discourse/components/menu-panel.js.es6 @@ -79,7 +79,11 @@ export default Ember.Component.extend({ }); this.performLayout(); this._watchSizeChanges(); - $(window).on('scroll.discourse-menu-panel', () => this.performLayout()); + + // iOS does not handle scroll events well + if (!this.capabilities.touch) { + $(window).on('scroll.discourse-menu-panel', () => this.performLayout()); + } } else { Ember.run.scheduleOnce('afterRender', () => this.sendAction('onHidden')); $('html').off('click.close-menu-panel'); @@ -175,7 +179,7 @@ export default Ember.Component.extend({ $('body').off('keydown.discourse-menu-panel'); $('html').off('click.close-menu-panel'); $(window).off('resize.discourse-menu-panel'); - $(window).off('scroll.discourse-menu-panel'); + $(window).off('scroll.discourse-menu-panel'); }, hide() { From d1717cdb12dc58fd06959c72ffb5a82b7f666801 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Sep 2015 15:33:44 -0400 Subject: [PATCH 030/224] FIX: Safer JS code --- .../javascripts/discourse/components/menu-panel.js.es6 | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/components/menu-panel.js.es6 b/app/assets/javascripts/discourse/components/menu-panel.js.es6 index 18a36917c00..e92e855798a 100644 --- a/app/assets/javascripts/discourse/components/menu-panel.js.es6 +++ b/app/assets/javascripts/discourse/components/menu-panel.js.es6 @@ -125,9 +125,13 @@ export default Ember.Component.extend({ clearInterval(this._resizeInterval); this._resizeInterval = setInterval(() => { Ember.run(() => { - const contentHeight = parseInt(this.$('.panel-body-contents').height()); - if (contentHeight !== this._lastHeight) { this.performLayout(); } - this._lastHeight = contentHeight; + const $panelBodyContents = this.$('.panel-body-contents'); + + if ($panelBodyContents.length) { + const contentHeight = parseInt($panelBodyContents.height()); + if (contentHeight !== this._lastHeight) { this.performLayout(); } + this._lastHeight = contentHeight; + } }); }, 500); } From 4a6f617f4da1e7301ef9980f465c4aaed11711ea Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Sep 2015 15:42:20 -0400 Subject: [PATCH 031/224] UX: Long category names pushed badges to a new line in the hamburger --- app/assets/stylesheets/common/base/menu-panel.scss | 8 ++++++++ .../stylesheets/common/components/badges.css.scss | 11 ----------- app/assets/stylesheets/mobile.scss | 1 + app/assets/stylesheets/mobile/menu-panel.scss | 7 +++++++ 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 7ec8405613d..4151fdab160 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -67,6 +67,7 @@ color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); background-color: transparent; display: inline; + padding: 0; } } @@ -76,6 +77,13 @@ font-weight: normal; font-size: 11px; } + + span.badge-category { + max-width: 90px; + overflow: hidden; + text-overflow: ellipsis; + } + } .search-menu { diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index 47fb1a1cb6e..ed6dec22ebf 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -126,17 +126,6 @@ } } } - - span.badge-category { - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - - .menu-panel & { - max-width: 90px; - } - } - } // Category badge dropdown diff --git a/app/assets/stylesheets/mobile.scss b/app/assets/stylesheets/mobile.scss index 91bf389d175..984da0c6683 100644 --- a/app/assets/stylesheets/mobile.scss +++ b/app/assets/stylesheets/mobile.scss @@ -18,6 +18,7 @@ @import "mobile/user"; @import "mobile/history"; @import "mobile/directory"; +@import "mobile/menu-panel"; /* These files doesn't actually exist, they are injected by DiscourseSassImporter. */ diff --git a/app/assets/stylesheets/mobile/menu-panel.scss b/app/assets/stylesheets/mobile/menu-panel.scss index e69de29bb2d..972ea30ab2a 100644 --- a/app/assets/stylesheets/mobile/menu-panel.scss +++ b/app/assets/stylesheets/mobile/menu-panel.scss @@ -0,0 +1,7 @@ +.menu-panel { + span.badge-category { + max-width: 85px; + overflow: hidden; + text-overflow: ellipsis; + } +} From 85154422f129113352f4cbb1099a670ae85d2038 Mon Sep 17 00:00:00 2001 From: Tobias Eigen Date: Wed, 2 Sep 2015 12:46:47 -0700 Subject: [PATCH 032/224] Fix typo about table settings in server.en.yml Intrepid n00b pull request at encouragement by @zogstrip. Fixing a small typo, as discussed on meta: https://meta.discourse.org/t/typo-in-description-of-allow-html-tables-admin-setting/32835?u=tobiaseigen --- 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 77a8754d889..39ca89934e5 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -846,7 +846,7 @@ en: flag_sockpuppets: "If a new user replies to a topic from the same IP address as the new user who started the topic, flag both of their posts as potential spam." traditional_markdown_linebreaks: "Use traditional linebreaks in Markdown, which require two trailing spaces for a linebreak." - allow_html_tables: "Allow tables to be entered in Markdown using HTML tags, TABLE, THEAD, TD, TR, TH are whiteliseted (requires full rebake on all old posts containing tables)" + allow_html_tables: "Allow tables to be entered in Markdown using HTML tags, TABLE, THEAD, TD, TR, TH are whitelisted (requires full rebake on all old posts containing tables)" post_undo_action_window_mins: "Number of minutes users are allowed to undo recent actions on a post (like, flag, etc)." must_approve_users: "Staff must approve all new user accounts before they are allowed to access the site. WARNING: enabling this for a live site will revoke access for existing non-staff users!" ga_tracking_code: "Google analytics (ga.js) tracking code code, eg: UA-12345678-9; see http://google.com/analytics" From 73dba5af38d852dbe3e48615f010f12ec837c056 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Sep 2015 15:48:41 -0400 Subject: [PATCH 033/224] FIX: Notifications when no limit is provided --- app/controllers/notifications_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb index 53e26afda2d..9831ee88c33 100644 --- a/app/controllers/notifications_controller.rb +++ b/app/controllers/notifications_controller.rb @@ -8,7 +8,7 @@ class NotificationsController < ApplicationController user = current_user if params[:recent].present? - limit = params[:limit].to_i || 15 + limit = (params[:limit] || 15).to_i limit = 50 if limit > 50 notifications = Notification.recent_report(current_user, limit) From 2b9b29c8c8cdb001f0eb31ebf2371b22284da490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 2 Sep 2015 22:02:31 +0200 Subject: [PATCH 034/224] FIX: ensure CategoryUser consistency --- app/jobs/scheduled/ensure_db_consistency.rb | 1 + app/models/category_user.rb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/jobs/scheduled/ensure_db_consistency.rb b/app/jobs/scheduled/ensure_db_consistency.rb index 274c666deaa..c565023e5b5 100644 --- a/app/jobs/scheduled/ensure_db_consistency.rb +++ b/app/jobs/scheduled/ensure_db_consistency.rb @@ -13,6 +13,7 @@ module Jobs UserStat.update_view_counts(13.hours.ago) Topic.ensure_consistency! Badge.ensure_consistency! + CategoryUser.ensure_consistency! end end end diff --git a/app/models/category_user.rb b/app/models/category_user.rb index 1fb537f546e..cd7b719afda 100644 --- a/app/models/category_user.rb +++ b/app/models/category_user.rb @@ -92,6 +92,10 @@ class CategoryUser < ActiveRecord::Base ) end + def self.ensure_consistency! + exec_sql("DELETE FROM category_users WHERE user_id NOT IN (SELECT id FROM users)") + end + private_class_method :apply_default_to_topic, :remove_default_from_topic end From 286738c71268e1a34e49af71d1a336e2c981c800 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Sep 2015 16:17:46 -0400 Subject: [PATCH 035/224] FIX: Include dummy capabilities object in component tests --- test/javascripts/helpers/component-test.js.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/javascripts/helpers/component-test.js.es6 b/test/javascripts/helpers/component-test.js.es6 index f2ec007a4df..181ae533171 100644 --- a/test/javascripts/helpers/component-test.js.es6 +++ b/test/javascripts/helpers/component-test.js.es6 @@ -13,8 +13,10 @@ export default function(name, opts) { this.container.register('site-settings:main', Discourse.SiteSettings, { instantiate: false }); this.container.register('app-events:main', appEvents, { instantiate: false }); + this.container.register('capabilities:main', Ember.Object); this.container.injection('component', 'siteSettings', 'site-settings:main'); this.container.injection('component', 'appEvents', 'app-events:main'); + this.container.injection('component', 'capabilities', 'capabilities:main'); andThen(() => { this.render(opts.template); From 0cd393f310b4090a95de642fe2dcf9a95e8931c1 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Sep 2015 16:46:25 -0400 Subject: [PATCH 036/224] Experiment with variable heights for slide-in menus --- .../discourse/components/menu-panel.js.es6 | 21 +++++++++++++------ .../stylesheets/common/base/menu-panel.scss | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/discourse/components/menu-panel.js.es6 b/app/assets/javascripts/discourse/components/menu-panel.js.es6 index e92e855798a..8b5767137a2 100644 --- a/app/assets/javascripts/discourse/components/menu-panel.js.es6 +++ b/app/assets/javascripts/discourse/components/menu-panel.js.es6 @@ -22,32 +22,41 @@ export default Ember.Component.extend({ const viewMode = this.get('viewMode'); const $panelBody = this.$('.panel-body'); + let contentHeight = parseInt(this.$('.panel-body-contents').height()); if (viewMode === 'drop-down') { const $buttonPanel = $('header ul.icons'); if ($buttonPanel.length === 0) { return; } const buttonPanelPos = $buttonPanel.offset(); - const posTop = parseInt(buttonPanelPos.top + $buttonPanel.height() - $('header.d-header').offset().top); const posLeft = parseInt(buttonPanelPos.left + $buttonPanel.width() - width); - this.$().css({ left: posLeft + "px", top: posTop + "px" }); + this.$().css({ left: posLeft + "px", top: posTop + "px", height: 'auto' }); // adjust panel height - let contentHeight = parseInt(this.$('.panel-body-contents').height()); const fullHeight = parseInt($window.height()); - const offsetTop = this.$().offset().top; const scrollTop = $window.scrollTop(); + if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) { contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN; } $panelBody.height(contentHeight); $('body').addClass('drop-down-visible'); } else { - $panelBody.height('auto'); - this.$().css({ left: "auto", top: headerHeight() + "px" }); + + const menuTop = headerHeight(); + + let height; + if ((menuTop + contentHeight) < ($(window).height() - 20)) { + height = contentHeight + "px"; + } else { + height = $(window).height() - menuTop; + } + + $panelBody.height('100%'); + this.$().css({ left: "auto", top: (menuTop - 2) + "px", height }); $('body').removeClass('drop-down-visible'); } diff --git a/app/assets/stylesheets/common/base/menu-panel.scss b/app/assets/stylesheets/common/base/menu-panel.scss index 4151fdab160..8a599647f18 100644 --- a/app/assets/stylesheets/common/base/menu-panel.scss +++ b/app/assets/stylesheets/common/base/menu-panel.scss @@ -2,7 +2,7 @@ position: fixed; right: 0; top: 0; - height: 100%; + .panel-body { position: absolute; top: 3px; From d34f42d2f70bd9c880e97cf63e7a38646567a59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 2 Sep 2015 23:46:04 +0200 Subject: [PATCH 037/224] FIX: hide category column in topic list only when the current category has no children --- app/assets/javascripts/discourse/models/topic-list.js.es6 | 5 ++--- app/models/category.rb | 2 +- app/models/site.rb | 7 ++++--- app/serializers/basic_category_serializer.rb | 3 ++- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/models/topic-list.js.es6 b/app/assets/javascripts/discourse/models/topic-list.js.es6 index 782cb9fd066..cf65eb212ac 100644 --- a/app/assets/javascripts/discourse/models/topic-list.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-list.js.es6 @@ -163,10 +163,9 @@ TopicList.reopenClass({ return this.find(filter); }, - // Sets `hideCategory` if all topics in the last have a particular category + // hide the category when it has no children hideUniformCategory(list, category) { - const hideCategory = !list.get('topics').any(function (t) { return t.get('category') !== category; }); - list.set('hideCategory', hideCategory); + list.set('hideCategory', !category.get("has_children")); } }); diff --git a/app/models/category.rb b/app/models/category.rb index 1f6424d71ab..311f3fad506 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -79,7 +79,7 @@ class Category < ActiveRecord::Base # permission is just used by serialization # we may consider wrapping this in another spot - attr_accessor :displayable_topics, :permission, :subcategory_ids, :notification_level + attr_accessor :displayable_topics, :permission, :subcategory_ids, :notification_level, :has_children def self.last_updated_at order('updated_at desc').limit(1).pluck(:updated_at).first.to_i diff --git a/app/models/site.rb b/app/models/site.rb index 719cf1d7123..c2fcfeb4a45 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -30,7 +30,7 @@ class Site end def groups - @groups ||= Group.order(:name).map { |g| {:id => g.id, :name => g.name}} + @groups ||= Group.order(:name).map { |g| { id: g.id, name: g.name } } end def user_fields @@ -41,7 +41,7 @@ class Site @categories ||= begin categories = Category .secured(@guardian) - .includes(:topic_only_relative_url) + .includes(:topic_only_relative_url, :subcategories) .order(:position) unless SiteSetting.allow_uncategorized_topics @@ -62,10 +62,11 @@ class Site categories.each do |category| category.notification_level = category_user[category.id] category.permission = CategoryGroup.permission_types[:full] if allowed_topic_create.include?(category.id) + category.has_children = category.subcategories.present? by_id[category.id] = category end - categories.reject! {|c| c.parent_category_id && !by_id[c.parent_category_id]} + categories.reject! { |c| c.parent_category_id && !by_id[c.parent_category_id] } categories end end diff --git a/app/serializers/basic_category_serializer.rb b/app/serializers/basic_category_serializer.rb index 45b8243297f..96b0268cfae 100644 --- a/app/serializers/basic_category_serializer.rb +++ b/app/serializers/basic_category_serializer.rb @@ -17,7 +17,8 @@ class BasicCategorySerializer < ApplicationSerializer :logo_url, :background_url, :can_edit, - :topic_template + :topic_template, + :has_children def include_parent_category_id? parent_category_id From be6e6dc129ee7d47bf372b6fd5034d1fd29b7a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 3 Sep 2015 00:22:25 +0200 Subject: [PATCH 038/224] UX: tidy up the category settings tab --- .../components/auto-close-form.js.es6 | 28 ++++--- .../components/edit-category-settings.hbs | 83 ++++++++++--------- .../stylesheets/common/base/compose.scss | 3 + 3 files changed, 64 insertions(+), 50 deletions(-) diff --git a/app/assets/javascripts/discourse/components/auto-close-form.js.es6 b/app/assets/javascripts/discourse/components/auto-close-form.js.es6 index fdad14df537..51669101067 100644 --- a/app/assets/javascripts/discourse/components/auto-close-form.js.es6 +++ b/app/assets/javascripts/discourse/components/auto-close-form.js.es6 @@ -1,25 +1,29 @@ +import computed from "ember-addons/ember-computed-decorators"; +import { observes } from "ember-addons/ember-computed-decorators"; + export default Ember.Component.extend({ autoCloseValid: false, limited: false, - autoCloseUnits: function() { - var key = this.get("limited") ? "composer.auto_close.limited.units" - : "composer.auto_close.all.units"; + @computed("limited") + autoCloseUnits(limited) { + const key = limited ? "composer.auto_close.limited.units" : "composer.auto_close.all.units"; return I18n.t(key); - }.property("limited"), + }, - autoCloseExamples: function() { - var key = this.get("limited") ? "composer.auto_close.limited.examples" - : "composer.auto_close.all.examples"; + @computed("limited") + autoCloseExamples(limited) { + const key = limited ? "composer.auto_close.limited.examples" : "composer.auto_close.all.examples"; return I18n.t(key); - }.property("limited"), + }, - _updateAutoCloseValid: function() { - var isValid = this._isAutoCloseValid(this.get("autoCloseTime"), this.get("limited")); + @observes("autoCloseTime", "limited") + _updateAutoCloseValid(autoCloseTime, limited) { + var isValid = this._isAutoCloseValid(autoCloseTime, limited); this.set("autoCloseValid", isValid); - }.observes("autoCloseTime", "limited"), + }, - _isAutoCloseValid: function(autoCloseTime, limited) { + _isAutoCloseValid(autoCloseTime, limited) { var t = (autoCloseTime || "").toString().trim(); if (t.length === 0) { // "empty" is always valid diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs index dd943f94944..722a9f5e1f8 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs @@ -1,54 +1,61 @@
    {{auto-close-form autoCloseTime=category.auto_close_hours autoCloseBasedOnLastPost=category.auto_close_based_on_last_post + autoCloseExamples="" limited="true" }}
    -
    -
    - {{input type="checkbox" checked=category.allow_badges}} - {{i18n 'category.allow_badges_label'}} -
    -
    -
    - -
    - -
    - -
    - {{#if showPositionInput}} - - {{text-field value=category.position class="position-input"}} - {{else}} - {{i18n 'category.position_disabled'}} - {{i18n 'category.position_disabled_click'}} - {{/if}} +
    -
    +{{#if emailInEnabled}} +
    + +
    +
    + +
    +{{/if}} + +{{#if showPositionInput}} +
    + +
    +{{/if}} + +{{#unless emailInEnabled}} +
    + {{i18n 'category.email_in_disabled'}} + {{i18n 'category.email_in_disabled_click'}} +
    +{{/unless}} + +{{#unless showPositionInput}} +
    + {{i18n 'category.position_disabled'}} + {{i18n 'category.position_disabled_click'}} +
    +{{/unless}} + {{plugin-outlet "category-custom-settings"}} diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 4ebe91a7eee..844a9e32d37 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -130,6 +130,9 @@ div.ac-wrap { input[type=text] { width: 50px; } + label { + font-size: .929em; + } } } From b97764554b195d2416f50c9de359c65d78f3d6d7 Mon Sep 17 00:00:00 2001 From: Anton Davydov Date: Thu, 3 Sep 2015 01:24:02 +0300 Subject: [PATCH 039/224] Add plugin for displaying sidekiq statistic in web ui --- Gemfile | 1 + Gemfile.lock | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index 98cdfdd7900..e8b2403e5ac 100644 --- a/Gemfile +++ b/Gemfile @@ -90,6 +90,7 @@ gem 'rinku' gem 'sanitize' gem 'sass' gem 'sidekiq' +gem 'sidekiq-statistic' # for sidekiq web gem 'sinatra', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 607e318db69..338070cfa7b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -333,6 +333,8 @@ GEM json (~> 1.0) redis (~> 3.2, >= 3.2.1) redis-namespace (~> 1.5, >= 1.5.2) + sidekiq-statistic (1.1.0) + sidekiq (~> 3.3, >= 3.3.4) simple-rss (1.3.1) simplecov (0.9.1) docile (~> 1.1.0) @@ -474,6 +476,7 @@ DEPENDENCIES seed-fu (~> 2.3.3) shoulda sidekiq + sidekiq-statistic simple-rss simplecov sinatra From 25fb684565247acb5a694865d68d67ba0622fa93 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 3 Sep 2015 12:00:19 +1000 Subject: [PATCH 040/224] ensure statistic collection is on --- config/initializers/sidekiq.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 7a5781f1462..bfac8028f19 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -6,9 +6,11 @@ end Sidekiq.configure_server do |config| config.redis = Discourse.sidekiq_redis_config - # add our pausable middleware + config.server_middleware do |chain| chain.add Sidekiq::Pausable + # ensure statistic middleware is included in case of a fork + chain.add Sidekiq::Statistic::Middleware end end From 73e6eebde88a99775eb9c6742072ce97194f0550 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 3 Sep 2015 13:46:55 +0530 Subject: [PATCH 041/224] UX: fix group header font color --- app/assets/stylesheets/desktop/user.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 9ce019329d6..ea4202fb0cd 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -225,6 +225,7 @@ .details { padding: 15px; margin: 0; + color: dark-light-choose($secondary, lighten($primary, 10%)); } } From a77d5d0cefd11c1a6bd4bc7cb09de21755d1c365 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 3 Sep 2015 00:09:44 +0800 Subject: [PATCH 042/224] UX: Make autocomplete usable on mobile. --- app/assets/javascripts/discourse/lib/autocomplete.js.es6 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 index 7750475c3ba..6b7ec5e1407 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -220,6 +220,13 @@ export default function(options) { vOffset = div.height(); } + if (Discourse.Mobile.mobileView && !isInput) { + div.css('width', 'auto'); + + if ((me.height() / 2) >= pos.top) { vOffset = -23; } + if ((me.width() / 2) <= pos.left) { hOffset = -div.width(); } + } + var mePos = me.position(); var borderTop = parseInt(me.css('border-top-width'), 10) || 0; div.css({ From 6a25a62e631ce39fd02bb0fd08001157e7589b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 3 Sep 2015 11:56:33 +0200 Subject: [PATCH 043/224] FIX: make sure we have a category --- app/assets/javascripts/discourse/models/topic-list.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/models/topic-list.js.es6 b/app/assets/javascripts/discourse/models/topic-list.js.es6 index cf65eb212ac..742abdac406 100644 --- a/app/assets/javascripts/discourse/models/topic-list.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-list.js.es6 @@ -165,7 +165,7 @@ TopicList.reopenClass({ // hide the category when it has no children hideUniformCategory(list, category) { - list.set('hideCategory', !category.get("has_children")); + list.set('hideCategory', category && !category.get("has_children")); } }); From e53d9f0e8bae9a25424f50c4169dba5d74f0dde5 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 3 Sep 2015 11:10:04 -0400 Subject: [PATCH 044/224] FIX: Don't use observers to update data Message bus events were triggering users who didn't have access to update posts to update them. Instead, perform the update in the action itself. --- .../discourse/controllers/topic.js.es6 | 14 ++++-------- .../javascripts/discourse/models/post.js.es6 | 22 +++++-------------- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 2a6d0ffaf9b..80ac937398b 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -428,20 +428,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { }, toggleWiki(post) { - // the request to the server is made in an observer in the post class - post.toggleProperty('wiki'); + post.updatePostField('wiki', !post.get('wiki')); }, togglePostType(post) { - // the request to the server is made in an observer in the post class - const regular = this.site.get('post_types.regular'), - moderator = this.site.get('post_types.moderator_action'); + const regular = this.site.get('post_types.regular'); + const moderator = this.site.get('post_types.moderator_action'); - if (post.get("post_type") === moderator) { - post.set("post_type", regular); - } else { - post.set("post_type", moderator); - } + post.updatePostField('post_type', post.get('post_type') === moderator ? regular : moderator); }, rebakePost(post) { diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index 3719dc3d0db..c332836f67f 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -83,23 +83,13 @@ const Post = RestModel.extend({ return this.get("user_id") === Discourse.User.currentProp("id") || Discourse.User.currentProp('staff'); }.property("user_id"), - wikiChanged: function() { - const data = { wiki: this.get("wiki") }; - this._updatePost("wiki", data); - }.observes('wiki'), + updatePostField(field, value) { + const data = {}; + data[field] = value; - postTypeChanged: function () { - const data = { post_type: this.get("post_type") }; - this._updatePost("post_type", data); - }.observes("post_type"), - - _updatePost(field, data) { - const self = this; - Discourse.ajax("/posts/" + this.get("id") + "/" + field, { - type: "PUT", - data: data - }).then(function () { - self.incrementProperty("version"); + Discourse.ajax(`/posts/${this.get('id')}/${field}`, { type: 'PUT', data }).then(() => { + this.set(field, value); + this.incrementProperty("version"); }).catch(popupAjaxError); }, From 12e0225c510e2bae2c0e8c23a16c12993ebf74b7 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 3 Sep 2015 11:47:18 -0400 Subject: [PATCH 045/224] FIX: Better `metaKey` support for menu panels --- .../discourse/components/d-link.js.es6 | 13 +++---- .../discourse/components/menu-panel.js.es6 | 5 +-- .../initializers/click-interceptor.js.es6 | 35 ++----------------- .../discourse/lib/intercept-click.js.es6 | 32 +++++++++++++++++ 4 files changed, 43 insertions(+), 42 deletions(-) create mode 100644 app/assets/javascripts/discourse/lib/intercept-click.js.es6 diff --git a/app/assets/javascripts/discourse/components/d-link.js.es6 b/app/assets/javascripts/discourse/components/d-link.js.es6 index a0babf3040d..51387a53252 100644 --- a/app/assets/javascripts/discourse/components/d-link.js.es6 +++ b/app/assets/javascripts/discourse/components/d-link.js.es6 @@ -1,9 +1,10 @@ import computed from 'ember-addons/ember-computed-decorators'; import { iconHTML } from 'discourse/helpers/fa-icon'; -import DiscourseURL from 'discourse/lib/url'; +import interceptClick from 'discourse/lib/intercept-click'; export default Ember.Component.extend({ tagName: 'a', + classNames: ['d-link'], attributeBindings: ['translatedTitle:title', 'translatedTitle:aria-title', 'href'], @computed('path') @@ -27,18 +28,14 @@ export default Ember.Component.extend({ if (text) return I18n.t(text); }, - click() { + click(e) { const action = this.get('action'); if (action) { this.sendAction('action'); return false; } - const href = this.get('href'); - if (href) { - DiscourseURL.routeTo(href); - return false; - } - return false; + + return interceptClick(e); }, render(buffer) { diff --git a/app/assets/javascripts/discourse/components/menu-panel.js.es6 b/app/assets/javascripts/discourse/components/menu-panel.js.es6 index 8b5767137a2..13330f21a41 100644 --- a/app/assets/javascripts/discourse/components/menu-panel.js.es6 +++ b/app/assets/javascripts/discourse/components/menu-panel.js.es6 @@ -156,7 +156,8 @@ export default Ember.Component.extend({ @on('didInsertElement') _bindEvents() { - this.$().on('click.discourse-menu-panel', 'a', (e) => { + this.$().on('click.discourse-menu-panel', 'a', e => { + if (e.metaKey) { return; } if ($(e.target).data('ember-action')) { return; } this.hide(); }); @@ -164,7 +165,7 @@ export default Ember.Component.extend({ this.appEvents.on('dropdowns:closeAll', this, this.hide); this.appEvents.on('dom:clean', this, this.hide); - $('body').on('keydown.discourse-menu-panel', (e) => { + $('body').on('keydown.discourse-menu-panel', e => { if (e.which === 27) { this.hide(); } diff --git a/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 b/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 index 5c04f67d018..d74e5264e32 100644 --- a/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 +++ b/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 @@ -1,37 +1,8 @@ -import DiscourseURL from 'discourse/lib/url'; +import interceptClick from 'discourse/lib/intercept-click'; -/** - Discourse does some server side rendering of HTML, such as the `cooked` contents of - posts. The downside of this in an Ember app is the links will not go through the router. - This jQuery code intercepts clicks on those links and routes them properly. -**/ export default { name: "click-interceptor", - initialize: function() { - $('#main').on('click.discourse', 'a', function(e) { - if (e.isDefaultPrevented() || e.shiftKey || e.metaKey || e.ctrlKey) { return; } - - var $currentTarget = $(e.currentTarget), - href = $currentTarget.attr('href'); - - if (!href || - href === '#' || - $currentTarget.attr('target') || - $currentTarget.data('ember-action') || - $currentTarget.data('auto-route') || - $currentTarget.data('share-url') || - $currentTarget.data('user-card') || - $currentTarget.hasClass('mention') || - $currentTarget.hasClass('ember-view') || - $currentTarget.hasClass('lightbox') || - href.indexOf("mailto:") === 0 || - (href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i")))) { - return; - } - - e.preventDefault(); - DiscourseURL.routeTo(href); - return false; - }); + initialize() { + $('#main').on('click.discourse', 'a', interceptClick); } }; diff --git a/app/assets/javascripts/discourse/lib/intercept-click.js.es6 b/app/assets/javascripts/discourse/lib/intercept-click.js.es6 new file mode 100644 index 00000000000..6ce7a300f37 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/intercept-click.js.es6 @@ -0,0 +1,32 @@ +import DiscourseURL from 'discourse/lib/url'; + +/** + Discourse does some server side rendering of HTML, such as the `cooked` contents of + posts. The downside of this in an Ember app is the links will not go through the router. + This jQuery code intercepts clicks on those links and routes them properly. +**/ +export default function interceptClick(e) { + if (e.isDefaultPrevented() || e.shiftKey || e.metaKey || e.ctrlKey) { return; } + + const $currentTarget = $(e.currentTarget), + href = $currentTarget.attr('href'); + + if (!href || + href === '#' || + $currentTarget.attr('target') || + $currentTarget.data('ember-action') || + $currentTarget.data('auto-route') || + $currentTarget.data('share-url') || + $currentTarget.data('user-card') || + $currentTarget.hasClass('mention') || + (!$currentTarget.hasClass('d-link') && $currentTarget.hasClass('ember-view')) || + $currentTarget.hasClass('lightbox') || + href.indexOf("mailto:") === 0 || + (href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^http:\\/\\/" + window.location.hostname, "i")))) { + return; + } + + e.preventDefault(); + DiscourseURL.routeTo(href); + return false; +} From 7516643f1193680feefbf619aa621357e33c4cd6 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 3 Sep 2015 11:53:42 -0400 Subject: [PATCH 046/224] Middle clicking the avatar should go to profile --- .../discourse/components/header-dropdown.js.es6 | 7 +++++++ app/assets/javascripts/discourse/models/user.js.es6 | 6 ------ .../discourse/templates/components/header-dropdown.hbs | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/discourse/components/header-dropdown.js.es6 b/app/assets/javascripts/discourse/components/header-dropdown.js.es6 index 4990aa107a1..07123063382 100644 --- a/app/assets/javascripts/discourse/components/header-dropdown.js.es6 +++ b/app/assets/javascripts/discourse/components/header-dropdown.js.es6 @@ -1,7 +1,14 @@ +import computed from 'ember-addons/ember-computed-decorators'; + export default Ember.Component.extend({ tagName: 'li', classNameBindings: [':header-dropdown-toggle', 'active'], + @computed('showUser') + href(showUser) { + return showUser ? this.currentUser.get('path') : ''; + }, + active: Ember.computed.alias('toggleVisible'), actions: { diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 3f5941bc7dc..01aef870afa 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -60,12 +60,6 @@ const User = RestModel.extend({ return ('background-image: url(' + Discourse.getURLWithCDN(bgUrl) + ')').htmlSafe(); }, - /** - Path to this user. - - @property path - @type {String} - **/ path: function(){ return Discourse.getURL('/users/' + this.get('username_lower')); // no need to observe, requires a hard refresh to update diff --git a/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs b/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs index 732e31e06ae..6cb4af742b4 100644 --- a/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs +++ b/app/assets/javascripts/discourse/templates/components/header-dropdown.hbs @@ -1,4 +1,4 @@ - + {{#if showUser}} {{bound-avatar currentUser "medium"}} {{else}} From 80041b874c3300ec8b49eec8d8ce51d344892460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 3 Sep 2015 19:18:46 +0200 Subject: [PATCH 047/224] FIX: don't show new topic notifications in homepag for suppressed categories --- .../models/topic-tracking-state.js.es6 | 109 +++++++++--------- app/models/site.rb | 4 + app/serializers/site_serializer.rb | 3 +- 3 files changed, 61 insertions(+), 55 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 cf5f8903751..87c256e202e 100644 --- a/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 @@ -1,4 +1,6 @@ import NotificationLevels from 'discourse/lib/notification-levels'; +import computed from "ember-addons/ember-computed-decorators"; +import { on } from "ember-addons/ember-computed-decorators"; function isNew(topic) { return topic.last_read_post_number === null && @@ -15,24 +17,25 @@ function isUnread(topic) { const TopicTrackingState = Discourse.Model.extend({ messageCount: 0, - _setup: function() { + @on("init") + _setup() { this.unreadSequence = []; this.newSequence = []; this.states = {}; - }.on('init'), + }, establishChannels() { const tracker = this; - const process = function(data){ + const process = data => { if (data.message_type === "delete") { tracker.removeTopic(data.topic_id); tracker.incrementMessageCount(); } if (data.message_type === "new_topic" || data.message_type === "latest") { - const ignored_categories = Discourse.User.currentProp("muted_category_ids"); - if(_.include(ignored_categories, data.payload.category_id)){ + const muted_category_ids = Discourse.User.currentProp("muted_category_ids"); + if (_.include(muted_category_ids, data.payload.category_id)) { return; } } @@ -45,7 +48,7 @@ const TopicTrackingState = Discourse.Model.extend({ tracker.notify(data); const old = tracker.states["t" + data.topic_id]; - if(!_.isEqual(old, data.payload)){ + if (!_.isEqual(old, data.payload)) { tracker.states["t" + data.topic_id] = data.payload; tracker.incrementMessageCount(); } @@ -60,20 +63,27 @@ const TopicTrackingState = Discourse.Model.extend({ }, updateSeen(topicId, highestSeen) { - if(!topicId || !highestSeen) { return; } + if (!topicId || !highestSeen) { return; } const state = this.states["t" + topicId]; - if(state && (!state.last_read_post_number || state.last_read_post_number < highestSeen)) { + if (state && (!state.last_read_post_number || state.last_read_post_number < highestSeen)) { state.last_read_post_number = highestSeen; this.incrementMessageCount(); } }, - notify(data){ + notify(data) { if (!this.newIncoming) { return; } const filter = this.get("filter"); - if ((filter === "all" || filter === "latest" || filter === "new") && data.message_type === "new_topic" ) { + if (filter === Discourse.Utilities.defaultHomepage()) { + const suppressed_from_homepage_category_ids = Discourse.Site.currentProp("suppressed_from_homepage_category_ids"); + if (_.include(suppressed_from_homepage_category_ids, data.payload.category_id)) { + return; + } + } + + if ((filter === "all" || filter === "latest" || filter === "new") && data.message_type === "new_topic") { this.addIncoming(data.topic_id); } @@ -84,7 +94,7 @@ const TopicTrackingState = Discourse.Model.extend({ } } - if(filter === "latest" && data.message_type === "latest") { + if (filter === "latest" && data.message_type === "latest") { this.addIncoming(data.topic_id); } @@ -92,12 +102,12 @@ const TopicTrackingState = Discourse.Model.extend({ }, addIncoming(topicId) { - if(this.newIncoming.indexOf(topicId) === -1){ + if (this.newIncoming.indexOf(topicId) === -1) { this.newIncoming.push(topicId); } }, - resetTracking(){ + resetTracking() { this.newIncoming = []; this.set("incomingCount", 0); }, @@ -109,10 +119,10 @@ const TopicTrackingState = Discourse.Model.extend({ this.set("incomingCount", 0); }, - hasIncoming: function(){ - const count = this.get('incomingCount'); - return count && count > 0; - }.property('incomingCount'), + @computed("incomingCount") + hasIncoming(incomingCount) { + return incomingCount && incomingCount > 0; + }, removeTopic(topic_id) { delete this.states["t" + topic_id]; @@ -124,7 +134,7 @@ const TopicTrackingState = Discourse.Model.extend({ if (Em.isEmpty(topics)) { return; } const states = this.states; - topics.forEach(function(t) { + topics.forEach(t => { const state = states['t' + t.get('id')]; if (state) { @@ -135,9 +145,7 @@ const TopicTrackingState = Discourse.Model.extend({ unread = postsCount - state.last_read_post_number; if (newPosts < 0) { newPosts = 0; } - if (!state.last_read_post_number) { - unread = 0; - } + if (!state.last_read_post_number) { unread = 0; } if (unread < 0) { unread = 0; } t.setProperties({ @@ -154,7 +162,7 @@ const TopicTrackingState = Discourse.Model.extend({ sync(list, filter) { const tracker = this, - states = tracker.states; + states = tracker.states; if (!list || !list.topics) { return; } @@ -198,14 +206,12 @@ const TopicTrackingState = Discourse.Model.extend({ }); // Correct missing states, safeguard in case message bus is corrupt - if((filter === "new" || filter === "unread") && !list.more_topics_url){ + if ((filter === "new" || filter === "unread") && !list.more_topics_url) { const ids = {}; - list.topics.forEach(function(r){ - ids["t" + r.id] = true; - }); + list.topics.forEach(r => ids["t" + r.id] = true); - _.each(tracker.states, function(v, k){ + _.each(tracker.states, (v, k) => { // we are good if we are on the list if (ids[k]) { return; } @@ -229,12 +235,12 @@ const TopicTrackingState = Discourse.Model.extend({ this.set("messageCount", this.get("messageCount") + 1); }, - countNew(category_id){ + countNew(category_id) { return _.chain(this.states) - .where(isNew) - .where(function(topic){ return topic.category_id === category_id || !category_id;}) - .value() - .length; + .where(isNew) + .where(topic => topic.category_id === category_id || !category_id) + .value() + .length; }, tooManyTracked() { @@ -242,20 +248,19 @@ const TopicTrackingState = Discourse.Model.extend({ }, resetNew() { - const self = this; - Object.keys(this.states).forEach(function (id) { - if (self.states[id].last_read_post_number === null) { - delete self.states[id]; + Object.keys(this.states).forEach(id => { + if (this.states[id].last_read_post_number === null) { + delete this.states[id]; } }); }, - countUnread(category_id){ + countUnread(category_id) { return _.chain(this.states) - .where(isUnread) - .where(function(topic){ return topic.category_id === category_id || !category_id;}) - .value() - .length; + .where(isUnread) + .where(topic => topic.category_id === category_id || !category_id) + .value() + .length; }, countCategory(category_id) { @@ -269,42 +274,37 @@ const TopicTrackingState = Discourse.Model.extend({ return sum; }, - lookupCount(name, category){ - + lookupCount(name, category) { if (name === "latest") { return this.lookupCount("new", category) + this.lookupCount("unread", category); } let categoryName = category ? Em.get(category, "name") : null; - if(name === "new") { + if (name === "new") { return this.countNew(categoryName); - } else if(name === "unread") { + } else if (name === "unread") { return this.countUnread(categoryName); } else { categoryName = name.split("/")[1]; - if(categoryName) { + if (categoryName) { return this.countCategory(categoryName); } } }, loadStates(data) { - // not exposed const states = this.states; - - if(data) { - _.each(data,function(topic){ - states["t" + topic.topic_id] = topic; - }); + if (data) { + _.each(data,topic => states["t" + topic.topic_id] = topic); } } }); TopicTrackingState.reopenClass({ - createFromStates(data) { + createFromStates(data) { // TODO: This should be a model that does injection automatically const container = Discourse.__container__, messageBus = container.lookup('message-bus:main'), @@ -316,7 +316,8 @@ TopicTrackingState.reopenClass({ instance.establishChannels(); return instance; }, - current(){ + + current() { if (!this.tracker) { const data = PreloadStore.get('topicTrackingStates'); this.tracker = this.createFromStates(data); diff --git a/app/models/site.rb b/app/models/site.rb index c2fcfeb4a45..c6a3313f172 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -71,6 +71,10 @@ class Site end end + def suppressed_from_homepage_category_ids + categories.select { |c| c.suppress_from_homepage == true }.map(&:id) + end + def archetypes Archetype.list.reject { |t| t.id == Archetype.private_message } end diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index 38c30737b2b..dec52ed2f2d 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -11,7 +11,8 @@ class SiteSerializer < ApplicationSerializer :uncategorized_category_id, # this is hidden so putting it here :is_readonly, :disabled_plugins, - :user_field_max_length + :user_field_max_length, + :suppressed_from_homepage_category_ids has_many :categories, serializer: BasicCategorySerializer, embed: :objects has_many :post_action_types, embed: :objects From 0e1d6272b9882f67502c8d28dda4d080dc274e36 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 3 Sep 2015 13:37:40 -0400 Subject: [PATCH 048/224] FIX: `highest_post_number` was not being updated from gaps --- .../discourse/components/post-gap.js.es6 | 20 +++++----- .../controllers/topic-entrance.js.es6 | 8 ++-- .../discourse/models/post-stream.js.es6 | 39 ++++++++----------- .../templates/components/topic-map.hbs | 2 +- 4 files changed, 30 insertions(+), 39 deletions(-) diff --git a/app/assets/javascripts/discourse/components/post-gap.js.es6 b/app/assets/javascripts/discourse/components/post-gap.js.es6 index 3c3209a89e0..159d047f076 100644 --- a/app/assets/javascripts/discourse/components/post-gap.js.es6 +++ b/app/assets/javascripts/discourse/components/post-gap.js.es6 @@ -3,8 +3,8 @@ export default Ember.Component.extend({ initGaps: function(){ this.set('loading', false); - var before = this.get('before') === 'true', - gaps = before ? this.get('postStream.gaps.before') : this.get('postStream.gaps.after'); + const before = this.get('before') === 'true'; + const gaps = before ? this.get('postStream.gaps.before') : this.get('postStream.gaps.after'); if (gaps) { this.set('gap', gaps[this.get('post.id')]); @@ -16,29 +16,27 @@ export default Ember.Component.extend({ this.rerender(); }.observes('post.hasGap'), - render: function(buffer) { + render(buffer) { if (this.get('loading')) { buffer.push(I18n.t('loading')); } else { - var gapLength = this.get('gap.length'); + const gapLength = this.get('gap.length'); if (gapLength) { buffer.push(I18n.t('post.gap', {count: gapLength})); } } }, - click: function() { + click() { if (this.get('loading') || (!this.get('gap'))) { return false; } this.set('loading', true); this.rerender(); - var self = this, - postStream = this.get('postStream'), - filler = this.get('before') === 'true' ? postStream.fillGapBefore : postStream.fillGapAfter; + const postStream = this.get('postStream'); + const filler = this.get('before') === 'true' ? postStream.fillGapBefore : postStream.fillGapAfter; - filler.call(postStream, this.get('post'), this.get('gap')).then(function() { - // hide this control after the promise is resolved - self.set('gap', null); + filler.call(postStream, this.get('post'), this.get('gap')).then(() => { + this.set('gap', null); }); return false; diff --git a/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 b/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 index 4f453b98f14..4a7fdf61c61 100644 --- a/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 @@ -1,7 +1,7 @@ import DiscourseURL from 'discourse/lib/url'; function entranceDate(dt, showTime) { - var today = new Date(); + const today = new Date(); if (dt.toDateString() === today.toDateString()) { return moment(dt).format(I18n.t("dates.time")); @@ -44,7 +44,7 @@ export default Ember.Controller.extend({ }.property('bumpedDate'), actions: { - show: function(data) { + show(data) { // Show the chooser but only if the model changes if (this.get('model') !== data.topic) { this.set('model', data.topic); @@ -52,11 +52,11 @@ export default Ember.Controller.extend({ } }, - enterTop: function() { + enterTop() { DiscourseURL.routeTo(this.get('model.url')); }, - enterBottom: function() { + enterBottom() { DiscourseURL.routeTo(this.get('model.lastPostUrl')); } } diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index 9f625f3477c..b2f903526e7 100644 --- a/app/assets/javascripts/discourse/models/post-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/post-stream.js.es6 @@ -281,14 +281,13 @@ const PostStream = RestModel.extend({ // Fill in a gap of posts after a particular post fillGapAfter(post, gap) { const postId = post.get('id'), - stream = this.get('stream'), - idx = stream.indexOf(postId), - self = this; + stream = this.get('stream'), + idx = stream.indexOf(postId); if (idx !== -1) { stream.pushObjects(gap); - return this.appendMore().then(function() { - self.get('stream').enumerableContentDidChange(); + return this.appendMore().then(() => { + this.get('stream').enumerableContentDidChange(); }); } return Ember.RSVP.resolve(); @@ -296,24 +295,18 @@ const PostStream = RestModel.extend({ // Appends the next window of posts to the stream. Call it when scrolling downwards. appendMore() { - const self = this; - // Make sure we can append more posts - if (!self.get('canAppendMore')) { return Ember.RSVP.resolve(); } + if (!this.get('canAppendMore')) { return Ember.RSVP.resolve(); } - const postIds = self.get('nextWindow'); + const postIds = this.get('nextWindow'); if (Ember.isEmpty(postIds)) { return Ember.RSVP.resolve(); } - self.set('loadingBelow', true); + this.set('loadingBelow', true); - const stopLoading = function() { - self.set('loadingBelow', false); - }; + const stopLoading = () => this.set('loadingBelow', false); - return self.findPostsByIds(postIds).then(function(posts) { - posts.forEach(function(p) { - self.appendPost(p); - }); + return this.findPostsByIds(postIds).then((posts) => { + posts.forEach(p => this.appendPost(p)); stopLoading(); }, stopLoading); }, @@ -685,6 +678,12 @@ const PostStream = RestModel.extend({ const postIdentityMap = this.get('postIdentityMap'), existing = postIdentityMap.get(post.get('id')); + // Update the `highest_post_number` if this post is higher. + const postNumber = post.get('post_number'); + if (postNumber && postNumber > (this.get('topic.highest_post_number') || 0)) { + this.set('topic.highest_post_number', postNumber); + } + if (existing) { // If the post is in the identity map, update it and return the old reference. existing.updateFromPost(post); @@ -693,12 +692,6 @@ const PostStream = RestModel.extend({ post.set('topic', this.get('topic')); postIdentityMap.set(post.get('id'), post); - - // Update the `highest_post_number` if this post is higher. - const postNumber = post.get('post_number'); - if (postNumber && postNumber > (this.get('topic.highest_post_number') || 0)) { - this.set('topic.highest_post_number', postNumber); - } } return post; }, diff --git a/app/assets/javascripts/discourse/templates/components/topic-map.hbs b/app/assets/javascripts/discourse/templates/components/topic-map.hbs index b851f03bcdc..915c3f0e711 100644 --- a/app/assets/javascripts/discourse/templates/components/topic-map.hbs +++ b/app/assets/javascripts/discourse/templates/components/topic-map.hbs @@ -14,7 +14,7 @@
  • - +

    {{i18n 'last_reply_lowercase'}}

    {{avatar details.last_poster imageSize="tiny"}} {{format-date topic.last_posted_at}} From ecf21cabe15992e78958ec677fd43615ae6bd46a Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 3 Sep 2015 15:05:47 -0400 Subject: [PATCH 049/224] Move Keyboard item to bottom. Add `d-link` to hamburger --- .../discourse/components/d-link.js.es6 | 8 ++- .../templates/components/hamburger-menu.hbs | 64 +++++++++---------- .../templates/components/user-menu.hbs | 2 +- 3 files changed, 38 insertions(+), 36 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-link.js.es6 b/app/assets/javascripts/discourse/components/d-link.js.es6 index 51387a53252..bde5d654a5a 100644 --- a/app/assets/javascripts/discourse/components/d-link.js.es6 +++ b/app/assets/javascripts/discourse/components/d-link.js.es6 @@ -15,7 +15,13 @@ export default Ember.Component.extend({ if (route) { const router = this.container.lookup('router:main'); if (router && router.router) { - return router.router.generate(route, this.get('model')); + const params = [route]; + const model = this.get('model'); + if (model) { + params.push(model); + } + + return router.router.generate.apply(router.router, params); } } diff --git a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs b/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs index 098d310157c..72bf62a4cfe 100644 --- a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs +++ b/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs @@ -1,73 +1,69 @@ {{#menu-panel visible=visible}}
    +
    + {{#if categories}} {{/if}} + +
    + + {{/menu-panel}} diff --git a/app/assets/javascripts/discourse/templates/components/user-menu.hbs b/app/assets/javascripts/discourse/templates/components/user-menu.hbs index 2133666413d..78593d30dea 100644 --- a/app/assets/javascripts/discourse/templates/components/user-menu.hbs +++ b/app/assets/javascripts/discourse/templates/components/user-menu.hbs @@ -4,7 +4,7 @@
  • {{d-link route='user' model=currentUser class="user-activity-link" icon="user" label="user.profile"}}
  • {{#if showDisableAnon}} -
  • {{d-link action="toggleAnon" label="switch_from_anon"}}
  • +
  • {{d-link action="toggleAnon" label="switch_from_anon"}}
  • {{/if}}
  • {{d-link path=bookmarksPath title="user.bookmarks" icon="bookmark"}} From 0818a502f35e6eb1163f94fbbe9bf0b85b8fcc5b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 3 Sep 2015 15:39:45 -0400 Subject: [PATCH 050/224] Group admin stuff in Hamburger --- .../templates/components/hamburger-menu.hbs | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs b/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs index 72bf62a4cfe..c5ab7182b59 100644 --- a/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs +++ b/app/assets/javascripts/discourse/templates/components/hamburger-menu.hbs @@ -1,8 +1,7 @@ {{#menu-panel visible=visible}} -
  • +{{#if context}} +
    + +
    +{{/if}} + {{#conditional-loading-spinner condition=loading}} {{#unless model.posts}}

    - {{i18n "search.no_results"}} {{i18n "search.search_help"}} + {{#if searchActive}} + {{i18n "search.no_results"}} + {{/if}} + {{i18n "search.search_help"}}

    {{/unless}} diff --git a/app/assets/javascripts/discourse/templates/header.hbs b/app/assets/javascripts/discourse/templates/header.hbs index 2c11cae568e..04c87250f98 100644 --- a/app/assets/javascripts/discourse/templates/header.hbs +++ b/app/assets/javascripts/discourse/templates/header.hbs @@ -23,6 +23,7 @@ {{#header-dropdown iconId="search-button" icon="search" toggleVisible=searchVisible + mobileAction="fullPageSearch" loginAction="showLogin" title="search.title"}} {{/header-dropdown}} diff --git a/app/assets/javascripts/discourse/views/choose-topic.js.es6 b/app/assets/javascripts/discourse/views/choose-topic.js.es6 index da4cbac2f48..cb2a346614b 100644 --- a/app/assets/javascripts/discourse/views/choose-topic.js.es6 +++ b/app/assets/javascripts/discourse/views/choose-topic.js.es6 @@ -1,5 +1,5 @@ import debounce from 'discourse/lib/debounce'; -import searchForTerm from 'discourse/lib/search-for-term'; +import { searchForTerm } from 'discourse/lib/search'; export default Ember.View.extend({ templateName: 'choose_topic', diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 5ea3455611d..797cab86581 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -35,7 +35,7 @@ //= require_tree ./discourse/mixins //= require ./discourse/lib/ajax-error //= require ./discourse/lib/markdown -//= require ./discourse/lib/search-for-term +//= require ./discourse/lib/search //= require ./discourse/lib/user-search //= require ./discourse/lib/export-csv //= require ./discourse/lib/autocomplete diff --git a/app/assets/javascripts/preload_store.js b/app/assets/javascripts/preload_store.js index b4b0e8b0de8..4911d4e54ae 100644 --- a/app/assets/javascripts/preload_store.js +++ b/app/assets/javascripts/preload_store.js @@ -42,7 +42,7 @@ window.PreloadStore = { var result = finder(); // If the finder returns a promise, we support that too - if (result.then) { + if (result && result.then) { result.then(function(result) { return resolve(result); }, function(result) { diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index ec75223b5dd..83e82ecf23a 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -18,10 +18,20 @@ top: -3px; margin-right: 4px; } + a.search-link:visited .topic-title { + color: scale-color($tertiary, $lightness: 15%); + } .search-link { .topic-statuses, .topic-title { font-size: 1.25em; } + + .topic-statuses { + float: none; + display: inline-block; + color: $primary; + font-size: 1.0em; + } } .blurb { font-size: 1.0em; @@ -50,3 +60,7 @@ .search-footer { margin-bottom: 30px; } + +.panel-body-contents .search-context label { + float: left; +} diff --git a/app/assets/stylesheets/mobile.scss b/app/assets/stylesheets/mobile.scss index 984da0c6683..8434f8c731c 100644 --- a/app/assets/stylesheets/mobile.scss +++ b/app/assets/stylesheets/mobile.scss @@ -19,6 +19,7 @@ @import "mobile/history"; @import "mobile/directory"; @import "mobile/menu-panel"; +@import "mobile/search"; /* These files doesn't actually exist, they are injected by DiscourseSassImporter. */ diff --git a/app/assets/stylesheets/mobile/search.scss b/app/assets/stylesheets/mobile/search.scss new file mode 100644 index 00000000000..0438c41c059 --- /dev/null +++ b/app/assets/stylesheets/mobile/search.scss @@ -0,0 +1,16 @@ +.search button.btn-primary, .search button.btn { + float: none; +} + +.search.row { + margin-top: 5px; +} + +.search.row input.search { + height: 25px; + width: 69%; +} + +.fps-search-context { + margin-bottom: 15px; +} diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 4446a0af99e..0b857278f03 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -9,7 +9,20 @@ class SearchController < ApplicationController end def show - search = Search.new(params[:q], type_filter: 'topic', guardian: guardian, include_blurbs: true, blurb_length: 300) + search_args = { + type_filter: 'topic', + guardian: guardian, + include_blurbs: true, + blurb_length: 300 + } + + context, type = lookup_search_context + if context + search_args[:search_context] = context + search_args[:type_filter] = type if type + end + + search = Search.new(params[:q], search_args) result = search.execute serializer = serialize_data(result, GroupedSearchResultSerializer, result: result) @@ -34,7 +47,29 @@ class SearchController < ApplicationController search_args[:include_blurbs] = params[:include_blurbs] == "true" if params[:include_blurbs].present? search_args[:search_for_id] = true if params[:search_for_id].present? + context,type = lookup_search_context + if context + search_args[:search_context] = context + search_args[:type_filter] = type if type + end + + search = Search.new(params[:term], search_args.symbolize_keys) + result = search.execute + render_serialized(result, GroupedSearchResultSerializer, result: result) + end + + protected + + def lookup_search_context + + return if params[:skip_context] == "true" + search_context = params[:search_context] + unless search_context + if (context = params[:context]) && (id = params[:context_id]) + search_context = {type: context, id: id} + end + end if search_context.present? raise Discourse::InvalidParameters.new(:search_context) unless SearchController.valid_context_types.include?(search_context[:type]) @@ -43,23 +78,21 @@ class SearchController < ApplicationController # A user is found by username context_obj = nil if ['user','private_messages'].include? search_context[:type] - context_obj = User.find_by(username_lower: params[:search_context][:id].downcase) + context_obj = User.find_by(username_lower: search_context[:id].downcase) else klass = search_context[:type].classify.constantize - context_obj = klass.find_by(id: params[:search_context][:id]) + context_obj = klass.find_by(id: search_context[:id]) end + type_filter = nil if search_context[:type] == 'private_messages' - search_args[:type_filter] = 'private_messages' + type_filter = 'private_messages' end guardian.ensure_can_see!(context_obj) - search_args[:search_context] = context_obj - end - search = Search.new(params[:term], search_args.symbolize_keys) - result = search.execute - render_serialized(result, GroupedSearchResultSerializer, result: result) + [context_obj, type_filter] + end end end From 0c5189fa2aed03fd6375d310acba78052bf8ff7a Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 8 Sep 2015 15:25:00 +1000 Subject: [PATCH 086/224] SECURITY: fix possible XSS expanding quotes --- .../javascripts/discourse/views/post.js.es6 | 9 +- app/assets/javascripts/vendor.js | 1 - .../javascripts/jquery.ba-replacetext.js | 129 ------------------ 3 files changed, 4 insertions(+), 135 deletions(-) delete mode 100644 vendor/assets/javascripts/jquery.ba-replacetext.js diff --git a/app/assets/javascripts/discourse/views/post.js.es6 b/app/assets/javascripts/discourse/views/post.js.es6 index bc555c67832..ef1869e90a5 100644 --- a/app/assets/javascripts/discourse/views/post.js.es6 +++ b/app/assets/javascripts/discourse/views/post.js.es6 @@ -145,11 +145,10 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, { topicId = parseInt(topicId, 10); Discourse.ajax("/posts/by_number/" + topicId + "/" + postId).then(function (result) { - // slightly double escape the cooked html to prevent jQuery from unescaping it - const escaped = result.cooked.replace(/&[^gla]/, "&"); - const parsed = $(escaped); - parsed.replaceText(originalText, "" + originalText + ""); - $blockQuote.showHtml(parsed, 'fast', finished); + const div = $("
    "); + div.html(result.cooked); + div.highlight(originalText, {caseSensitive: true, element: 'span', className: 'highlighted'}); + $blockQuote.showHtml(div, 'fast', finished); }); } else { // Hide expanded quote diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js index 5c0788f8c7a..1a52785b5f4 100644 --- a/app/assets/javascripts/vendor.js +++ b/app/assets/javascripts/vendor.js @@ -22,7 +22,6 @@ //= require div_resizer //= require caret_position //= require favcount.js -//= require jquery.ba-replacetext.js //= require jquery.ba-resize.min.js //= require jquery.color.js //= require jquery.cookie.js diff --git a/vendor/assets/javascripts/jquery.ba-replacetext.js b/vendor/assets/javascripts/jquery.ba-replacetext.js deleted file mode 100644 index c6b60c57c10..00000000000 --- a/vendor/assets/javascripts/jquery.ba-replacetext.js +++ /dev/null @@ -1,129 +0,0 @@ -/*! - * jQuery replaceText - v1.1 - 11/21/2009 - * http://benalman.com/projects/jquery-replacetext-plugin/ - * - * Copyright (c) 2009 "Cowboy" Ben Alman - * Dual licensed under the MIT and GPL licenses. - * http://benalman.com/about/license/ - */ - -// Script: jQuery replaceText: String replace for your jQueries! -// -// *Version: 1.1, Last updated: 11/21/2009* -// -// Project Home - http://benalman.com/projects/jquery-replacetext-plugin/ -// GitHub - http://github.com/cowboy/jquery-replacetext/ -// Source - http://github.com/cowboy/jquery-replacetext/raw/master/jquery.ba-replacetext.js -// (Minified) - http://github.com/cowboy/jquery-replacetext/raw/master/jquery.ba-replacetext.min.js (0.5kb) -// -// About: License -// -// Copyright (c) 2009 "Cowboy" Ben Alman, -// Dual licensed under the MIT and GPL licenses. -// http://benalman.com/about/license/ -// -// About: Examples -// -// This working example, complete with fully commented code, illustrates one way -// in which this plugin can be used. -// -// replaceText - http://benalman.com/code/projects/jquery-replacetext/examples/replacetext/ -// -// About: Support and Testing -// -// Information about what version or versions of jQuery this plugin has been -// tested with, and what browsers it has been tested in. -// -// jQuery Versions - 1.3.2, 1.4.1 -// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome, Opera 9.6-10.1. -// -// About: Release History -// -// 1.1 - (11/21/2009) Simplified the code and API substantially. -// 1.0 - (11/21/2009) Initial release - -(function($){ - '$:nomunge'; // Used by YUI compressor. - - // Method: jQuery.fn.replaceText - // - // Replace text in specified elements. Note that only text content will be - // modified, leaving all tags and attributes untouched. The new text can be - // either text or HTML. - // - // Uses the String prototype replace method, full documentation on that method - // can be found here: - // - // https://developer.mozilla.org/En/Core_JavaScript_1.5_Reference/Objects/String/Replace - // - // Usage: - // - // > jQuery('selector').replaceText( search, replace [, text_only ] ); - // - // Arguments: - // - // search - (RegExp|String) A RegExp object or substring to be replaced. - // Because the String prototype replace method is used internally, this - // argument should be specified accordingly. - // replace - (String|Function) The String that replaces the substring received - // from the search argument, or a function to be invoked to create the new - // substring. Because the String prototype replace method is used internally, - // this argument should be specified accordingly. - // text_only - (Boolean) If true, any HTML will be rendered as text. Defaults - // to false. - // - // Returns: - // - // (jQuery) The initial jQuery collection of elements. - - $.fn.replaceText = function( search, replace, text_only ) { - return this.each(function(){ - var node = this.firstChild, - val, - new_val, - - // Elements to be removed at the end. - remove = []; - - // Only continue if firstChild exists. - if ( node ) { - - // Loop over all childNodes. - do { - - // Only process text nodes. - if ( node.nodeType === 3 ) { - - // The original node value. - val = node.nodeValue; - - // The new value. - new_val = val.replace( search, replace ); - - // Only replace text if the new value is actually different! - if ( new_val !== val ) { - - if ( !text_only && / Date: Tue, 8 Sep 2015 16:11:21 +1000 Subject: [PATCH 087/224] FEATURE: select all / deselect all on search page --- .../controllers/full-page-search.js.es6 | 16 +++++++++++++++- .../discourse/templates/full-page-search.hbs | 9 +++++++++ app/assets/stylesheets/common/base/search.scss | 14 ++++++++++++++ config/locales/client.en.yml | 2 ++ 4 files changed, 40 insertions(+), 1 deletion(-) 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 8efc5c6adfe..3641329f833 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -79,12 +79,26 @@ export default Ember.Controller.extend({ Discourse.ajax("/search", { data: args }).then(results => { this.set("model", translateResults(results) || {}); - // this.set("model.q", this.get("q")); }); }, actions: { + selectAll() { + this.get('selected').addObjects(this.get('model.posts').map(r => r.topic)); + // Doing this the proper way is a HUGE pain, + // we can hack this to work by observing each on the array + // in the component, however, when we select ANYTHING, we would force + // 50 traversals of the list + // This hack is cheap and easy + $('.fps-result input[type=checkbox]').prop('checked', true); + }, + + clearAll() { + this.get('selected').clear() + $('.fps-result input[type=checkbox]').prop('checked', false); + }, + toggleBulkSelect() { this.toggleProperty('bulkSelectEnabled'); this.get('selected').clear(); diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs index f0278b64072..db60849ff9a 100644 --- a/app/assets/javascripts/discourse/templates/full-page-search.hbs +++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs @@ -9,6 +9,15 @@ {{/if}}
    +{{#if model.posts}} + {{#if bulkSelectEnabled}} + + {{/if}} +{{/if}} + {{#if context}}
    diff --git a/app/assets/javascripts/admin/templates/logs/site_customization_change_modal.hbs b/app/assets/javascripts/admin/templates/logs/site_customization_change_modal.hbs index 2c995713b7d..556f6310dbc 100644 --- a/app/assets/javascripts/admin/templates/logs/site_customization_change_modal.hbs +++ b/app/assets/javascripts/admin/templates/logs/site_customization_change_modal.hbs @@ -1,10 +1,10 @@
    {{/if}} diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs index 349cabd1a92..dc7db78da8d 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs @@ -30,7 +30,7 @@ {{#unless t.canClearPin}}{{i18n 'read_more'}}{{/unless}} {{/if}} {{#if t.canClearPin}} - {{i18n 'topic.clear_pin.title'}} + {{i18n 'topic.clear_pin.title'}} {{/if}}
    {{/if}} @@ -60,7 +60,7 @@
    {{number c.topics_week}}
    {{i18n 'week'}}
    {{#if controller.canEdit}} - {{i18n 'category.edit'}} + {{i18n 'category.edit'}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs index 7a02b2a7781..22cbceea9cd 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs @@ -38,7 +38,7 @@

    {{footerMessage}} - {{#if model.can_create_topic}}{{i18n 'topic.suggest_create_topic'}}{{/if}} + {{#if model.can_create_topic}}{{i18n 'topic.suggest_create_topic'}}{{/if}}

    {{else}} {{#if top}} diff --git a/app/assets/javascripts/discourse/templates/modal/not-activated.hbs b/app/assets/javascripts/discourse/templates/modal/not-activated.hbs index 4266126c8d3..f8df2639e9d 100644 --- a/app/assets/javascripts/discourse/templates/modal/not-activated.hbs +++ b/app/assets/javascripts/discourse/templates/modal/not-activated.hbs @@ -3,7 +3,7 @@ {{{i18n 'login.sent_activation_email_again' currentEmail=currentEmail}}} {{else}} {{{i18n 'login.not_activated' sentTo=sentTo}}} - {{i18n 'login.resend_activation_email'}} + {{i18n 'login.resend_activation_email'}} {{/if}} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 36c1f56dd3e..6c30944c10c 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -958,7 +958,7 @@ en: private_messages: "Search messages" hamburger_menu: "go to another topic list or category" - new_item: "New!" + new_item: "new" go_back: 'go back' not_logged_in_user: 'user page with summary of current activity and preferences' current_user: 'go to your user page' From 2922cc3036a4a28f55d50a2777f35fcf66944596 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 9 Sep 2015 16:05:02 -0400 Subject: [PATCH 114/224] UX: mobile topic list: move post count to right side of topic title --- .../templates/mobile/list/topic_list_item.raw.hbs | 7 +++++-- app/assets/stylesheets/mobile/topic-list.scss | 10 +++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) 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 972acfa16a8..01937421ae6 100644 --- a/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs @@ -1,5 +1,5 @@ -