From 3bdade897047ff1ece548083b2c62e685fcf2107 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Thu, 31 Aug 2017 15:55:56 -0400 Subject: [PATCH 001/159] correct fragile spec --- spec/components/file_store/local_store_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/components/file_store/local_store_spec.rb b/spec/components/file_store/local_store_spec.rb index 2ccebf0986..b9696ed091 100644 --- a/spec/components/file_store/local_store_spec.rb +++ b/spec/components/file_store/local_store_spec.rb @@ -36,10 +36,9 @@ describe FileStore::LocalStore do end it "moves the file to the tombstone" do - FileUtils.expects(:mkdir_p) - FileUtils.expects(:move) - File.expects(:exists?).returns(true) + filename = File.basename(store.path_for(upload)) store.remove_upload(upload) + expect(File.exist?(store.tombstone_dir + "/" + filename)) end end From 7f8a90ef631e018bf9ad15c8032669af5f7b861b Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Thu, 31 Aug 2017 17:00:37 -0400 Subject: [PATCH 002/159] remove non english comment --- lib/discourse.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/discourse.rb b/lib/discourse.rb index 639e8a2d2f..e9c06f020b 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -340,7 +340,7 @@ module Discourse unless version_value begin version_value = `#{git_cmd}`.strip - rescue # sollte noch ausspezifiziert werden… + rescue version_value = default_value end end From dffb1fc4ee8a4d58f48145decb1590a304e8cf7d Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 29 Jun 2017 16:22:19 -0400 Subject: [PATCH 003/159] FEATURE: Use Glimmer compiler for widget templates Widgets can now specify a template which is precompiled using Glimmer's AST and then converted into our virtual dom code. Example: ```javascript createWidget('post-link-arrow', { template: hbs` {{#if attrs.above}} {{else}} {{/if}} `, click() { DiscourseURL.routeTo(this.attrs.shareUrl); } }); ``` --- .../discourse/widgets/embedded-post.js.es6 | 24 +-- .../discourse/widgets/hbs-compiler.js.es6 | 3 + .../discourse/widgets/menu-panel.js.es6 | 12 +- .../discourse/widgets/post-placeholder.js.es6 | 25 +-- .../widgets/private-message-map.js.es6 | 11 +- .../discourse/widgets/widget.js.es6 | 4 + .../tilt/es6_module_transpiler_template.rb | 28 ++- lib/javascripts/widget-hbs-compiler.js.es6 | 196 ++++++++++++++++++ script/compile_hbs.rb | 25 +++ test/javascripts/widgets/widget-test.js.es6 | 152 ++++++++++---- 10 files changed, 403 insertions(+), 77 deletions(-) create mode 100644 app/assets/javascripts/discourse/widgets/hbs-compiler.js.es6 create mode 100644 lib/javascripts/widget-hbs-compiler.js.es6 create mode 100644 script/compile_hbs.rb diff --git a/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 b/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 index 11036d8541..f3476faec4 100644 --- a/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/embedded-post.js.es6 @@ -2,21 +2,21 @@ import PostCooked from 'discourse/widgets/post-cooked'; import DecoratorHelper from 'discourse/widgets/decorator-helper'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; -import { iconNode } from 'discourse-common/lib/icon-library'; import DiscourseURL from 'discourse/lib/url'; +import hbs from 'discourse/widgets/hbs-compiler'; createWidget('post-link-arrow', { - html(attrs) { - if (attrs.above) { - return h('a.post-info.arrow', { - attributes: { title: I18n.t('topic.jump_reply_up') } - }, iconNode('arrow-up')); - } else { - return h('a.post-info.arrow', { - attributes: { title: I18n.t('topic.jump_reply_down') } - }, iconNode('arrow-down')); - } - }, + template: hbs` + {{#if attrs.above}} + + {{else}} + + {{/if}} + `, click() { DiscourseURL.routeTo(this.attrs.shareUrl); diff --git a/app/assets/javascripts/discourse/widgets/hbs-compiler.js.es6 b/app/assets/javascripts/discourse/widgets/hbs-compiler.js.es6 new file mode 100644 index 0000000000..ef58ffc796 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/hbs-compiler.js.es6 @@ -0,0 +1,3 @@ +export default function hbs() { + console.log('Templates should be precompiled server side'); +} diff --git a/app/assets/javascripts/discourse/widgets/menu-panel.js.es6 b/app/assets/javascripts/discourse/widgets/menu-panel.js.es6 index 607da731a2..058b64607b 100644 --- a/app/assets/javascripts/discourse/widgets/menu-panel.js.es6 +++ b/app/assets/javascripts/discourse/widgets/menu-panel.js.es6 @@ -1,3 +1,4 @@ +import hbs from 'discourse/widgets/hbs-compiler'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; @@ -23,14 +24,17 @@ createWidget('menu-links', { createWidget('menu-panel', { tagName: 'div.menu-panel', + template: hbs` +
+
+ {{yield}} +
+
+ `, buildAttributes(attrs) { if (attrs.maxWidth) { return { 'data-max-width': attrs.maxWidth }; } }, - - html(attrs) { - return h('div.panel-body', h('div.panel-body-contents.clearfix', attrs.contents())); - } }); diff --git a/app/assets/javascripts/discourse/widgets/post-placeholder.js.es6 b/app/assets/javascripts/discourse/widgets/post-placeholder.js.es6 index f12fb26f34..1579413613 100644 --- a/app/assets/javascripts/discourse/widgets/post-placeholder.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-placeholder.js.es6 @@ -1,17 +1,18 @@ import { createWidget } from 'discourse/widgets/widget'; -import { h } from 'virtual-dom'; +import hbs from 'discourse/widgets/hbs-compiler'; export default createWidget('post-placeholder', { tagName: 'article.placeholder', - - html() { - return h('div.row', [ - h('div.topic-avatar', h('div.placeholder-avatar')), - h('div.topic-body', [ - h('div.placeholder-text'), - h('div.placeholder-text'), - h('div.placeholder-text') - ]) - ]); - } + template: hbs` +
+
+
+
+
+
+
+
+
+
+ ` }); diff --git a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 index 60441867e1..2aa4ba4ef0 100644 --- a/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/private-message-map.js.es6 @@ -2,13 +2,11 @@ import { iconNode } from 'discourse-common/lib/icon-library'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; import { avatarFor } from 'discourse/widgets/post'; +import hbs from 'discourse/widgets/hbs-compiler'; createWidget('pm-remove-group-link', { tagName: 'a.remove-invited', - - html() { - return iconNode('times'); - }, + template: hbs`{{fa-icon "times"}}`, click() { bootbox.confirm(I18n.t("private_message_info.remove_allowed_group", {name: this.attrs.name}), (confirmed) => { @@ -35,10 +33,7 @@ createWidget('pm-map-user-group', { createWidget('pm-remove-link', { tagName: 'a.remove-invited', - - html() { - return iconNode('times'); - }, + template: hbs`{{fa-icon "times"}}`, click() { bootbox.confirm(I18n.t("private_message_info.remove_allowed_user", {name: this.attrs.username}), (confirmed) => { diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 index 2d0ea8d587..8b2c34e8ad 100644 --- a/app/assets/javascripts/discourse/widgets/widget.js.es6 +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -112,6 +112,10 @@ export function createWidget(name, opts) { opts.html = opts.html || emptyContent; opts.draw = drawWidget; + if (opts.template) { + opts.html = opts.template; + } + Object.keys(opts).forEach(k => result.prototype[k] = opts[k]); return result; } diff --git a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb index 545f752280..704a95f1f9 100644 --- a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb +++ b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb @@ -27,6 +27,7 @@ module Tilt # timeout any eval that takes longer than 15 seconds ctx = MiniRacer::Context.new(timeout: 15000) ctx.eval("var self = this; #{File.read("#{Rails.root}/vendor/assets/javascripts/babel.js")}") + ctx.eval(File.read(Ember::Source.bundled_path_for('ember-template-compiler.js'))) ctx.eval("module = {}; exports = {};"); ctx.attach("rails.logger.info", proc { |err| Rails.logger.info(err.to_s) }) ctx.attach("rails.logger.error", proc { |err| Rails.logger.error(err.to_s) }) @@ -36,7 +37,13 @@ module Tilt log: function(msg){ rails.logger.info(console.prefix + msg); }, error: function(msg){ rails.logger.error(console.prefix + msg); } } + JS + source = File.read("#{Rails.root}/lib/javascripts/widget-hbs-compiler.js.es6") + js_source = ::JSON.generate(source, quirks_mode: true) + js = ctx.eval("Babel.transform(#{js_source}, { ast: false, plugins: ['check-es2015-constants', 'transform-es2015-arrow-functions', 'transform-es2015-block-scoped-functions', 'transform-es2015-block-scoping', 'transform-es2015-classes', 'transform-es2015-computed-properties', 'transform-es2015-destructuring', 'transform-es2015-duplicate-keys', 'transform-es2015-for-of', 'transform-es2015-function-name', 'transform-es2015-literals', 'transform-es2015-object-super', 'transform-es2015-parameters', 'transform-es2015-shorthand-properties', 'transform-es2015-spread', 'transform-es2015-sticky-regex', 'transform-es2015-template-literals', 'transform-es2015-typeof-symbol', 'transform-es2015-unicode-regex'] }).code") + ctx.eval(js) + ctx end @@ -105,7 +112,11 @@ JS klass = self.class klass.protect do klass.v8.eval("console.prefix = 'BABEL: babel-eval: ';") - transpiled = babel_source(source, module_name: module_name(root_path, logical_path)) + transpiled = babel_source( + source, + module_name: module_name(root_path, logical_path), + filename: logical_path + ) @output = klass.v8.eval(transpiled) end end @@ -116,7 +127,14 @@ JS klass = self.class klass.protect do klass.v8.eval("console.prefix = 'BABEL: #{scope.logical_path}: ';") - @output = klass.v8.eval(babel_source(data, module_name: module_name(scope.root_path, scope.logical_path))) + + source = babel_source( + data, + module_name: module_name(scope.root_path, scope.logical_path), + filename: scope.logical_path + ) + + @output = klass.v8.eval(source) end # For backwards compatibility with plugins, for now export the Global format too. @@ -156,15 +174,15 @@ JS end def babel_source(source, opts = nil) - opts ||= {} js_source = ::JSON.generate(source, quirks_mode: true) if opts[:module_name] - "Babel.transform(#{js_source}, { moduleId: '#{opts[:module_name]}', ast: false, presets: ['es2015'], plugins: [['transform-es2015-modules-amd', {noInterop: true}], 'transform-decorators-legacy'] }).code" + filename = opts[:filename] || 'unknown' + "Babel.transform(#{js_source}, { moduleId: '#{opts[:module_name]}', filename: '#{filename}', ast: false, presets: ['es2015'], plugins: [['transform-es2015-modules-amd', {noInterop: true}], 'transform-decorators-legacy', exports.WidgetHbsCompiler] }).code" else - "Babel.transform(#{js_source}, { ast: false, plugins: ['check-es2015-constants', 'transform-es2015-arrow-functions', 'transform-es2015-block-scoped-functions', 'transform-es2015-block-scoping', 'transform-es2015-classes', 'transform-es2015-computed-properties', 'transform-es2015-destructuring', 'transform-es2015-duplicate-keys', 'transform-es2015-for-of', 'transform-es2015-function-name', 'transform-es2015-literals', 'transform-es2015-object-super', 'transform-es2015-parameters', 'transform-es2015-shorthand-properties', 'transform-es2015-spread', 'transform-es2015-sticky-regex', 'transform-es2015-template-literals', 'transform-es2015-typeof-symbol', 'transform-es2015-unicode-regex', 'transform-regenerator', 'transform-decorators-legacy'] }).code" + "Babel.transform(#{js_source}, { ast: false, plugins: ['check-es2015-constants', 'transform-es2015-arrow-functions', 'transform-es2015-block-scoped-functions', 'transform-es2015-block-scoping', 'transform-es2015-classes', 'transform-es2015-computed-properties', 'transform-es2015-destructuring', 'transform-es2015-duplicate-keys', 'transform-es2015-for-of', 'transform-es2015-function-name', 'transform-es2015-literals', 'transform-es2015-object-super', 'transform-es2015-parameters', 'transform-es2015-shorthand-properties', 'transform-es2015-spread', 'transform-es2015-sticky-regex', 'transform-es2015-template-literals', 'transform-es2015-typeof-symbol', 'transform-es2015-unicode-regex', 'transform-regenerator', 'transform-decorators-legacy', exports.WidgetHbsCompiler] }).code" end end diff --git a/lib/javascripts/widget-hbs-compiler.js.es6 b/lib/javascripts/widget-hbs-compiler.js.es6 new file mode 100644 index 0000000000..9b28d186a4 --- /dev/null +++ b/lib/javascripts/widget-hbs-compiler.js.es6 @@ -0,0 +1,196 @@ +function resolve(path) { + return (path.indexOf('settings') === 0) ? `this.${path}` : path; +} + +function mustacheValue(node) { + let path = node.path.original; + + switch(path) { + case 'attach': + const widgetName = node.hash.pairs.find(p => p.key === "widget").value.value; + return `this.attach("${widgetName}", attrs, state)`; + break; + case 'yield': + return `this.attrs.contents()`; + break; + case 'i18n': + let value; + if (node.params[0].type === "StringLiteral") { + value = `"${node.params[0].value}"`; + } else if (node.params[0].type === "PathExpression") { + value = node.params[0].original; + } + + if (value) { + return `I18n.t(${value})`; + } + + break; + case 'fa-icon': + let icon = node.params[0].value; + return `virtualDom.h('i.fa.fa-${icon}')`; + break; + default: + return `${resolve(path)}`; + break; + } +} + +class Compiler { + constructor(ast) { + this.idx = 0; + this.ast = ast; + } + + newAcc() { + return `_a${this.idx++}`; + } + + processNode(parentAcc, node) { + let instructions = []; + let innerAcc; + + switch(node.type) { + case "Program": + node.body.forEach(bodyNode => { + instructions = instructions.concat(this.processNode(parentAcc, bodyNode)); + }); + break; + case "ElementNode": + innerAcc = this.newAcc(); + instructions.push(`var ${innerAcc} = [];`); + node.children.forEach(child => { + instructions = instructions.concat(this.processNode(innerAcc, child)); + }); + + if (node.attributes.length) { + + let attributes = []; + node.attributes.forEach(a => { + const name = a.name === 'class' ? 'className' : a.name; + if (a.value.type === "MustacheStatement") { + attributes.push(`"${name}":${mustacheValue(a.value)}`); + } else { + attributes.push(`"${name}":"${a.value.chars}"`); + } + }); + + const attrString = `{${attributes.join(', ')}}`; + instructions.push(`${parentAcc}.push(virtualDom.h('${node.tag}', ${attrString}, ${innerAcc}));`); + } else { + instructions.push(`${parentAcc}.push(virtualDom.h('${node.tag}', ${innerAcc}));`); + } + + break; + + case "TextNode": + return `${parentAcc}.push(${JSON.stringify(node.chars)});`; + + case "MustacheStatement": + const value = mustacheValue(node); + if (value) { + instructions.push(`${parentAcc}.push(${value})`); + } + break; + case "BlockStatement": + switch(node.path.original) { + case 'if': + instructions.push(`if (${node.params[0].original}) {`); + node.program.body.forEach(child => { + instructions = instructions.concat(this.processNode(parentAcc, child)); + }); + + if (node.inverse) { + instructions.push(`} else {`); + node.inverse.body.forEach(child => { + instructions = instructions.concat(this.processNode(parentAcc, child)); + }); + } + instructions.push(`}`); + break; + case 'each': + const collection = node.params[0].original; + instructions.push(`if (${collection} && ${collection}.length) {`); + instructions.push(` ${collection}.forEach(${node.program.blockParams[0]} => {`); + node.program.body.forEach(child => { + instructions = instructions.concat(this.processNode(parentAcc, child)); + }); + instructions.push(` });`); + instructions.push('}'); + + break; + } + break; + default: + break; + } + + return instructions.join("\n"); + } + + compile() { + return this.processNode('_r', this.ast); + } + +} + +function compile(template) { + const preprocessor = Ember.__loader.require('@glimmer/syntax'); + const compiled = preprocessor.preprocess(template); + const compiler = new Compiler(compiled); + + return `function(attrs, state) { var _r = [];\n${compiler.compile()}\nreturn _r; }`; +} + +exports.compile = compile; + +function error(path, state, msg) { + const filename = state.file.opts.filename; + return path.replaceWithSourceString(`function() { console.error("${filename}: ${msg}"); }`); +} + +exports.WidgetHbsCompiler = function(babel) { + let t = babel.types; + return { + visitor: { + ImportDeclaration(path, state) { + let node = path.node; + if (t.isLiteral(node.source, { value: "discourse/widgets/hbs-compiler" })) { + let first = node.specifiers && node.specifiers[0]; + if (!t.isImportDefaultSpecifier(first)) { + let input = state.file.code; + let usedImportStatement = input.slice(node.start, node.end); + let msg = `Only \`import hbs from 'discourse/widgets/hbs-compiler'\` is supported. You used: \`${usedImportStatement}\``; + throw path.buildCodeFrameError(msg); + } + + state.importId = state.importId || path.scope.generateUidIdentifierBasedOnNode(path.node.id); + path.scope.rename(first.local.name, state.importId.name); + path.remove(); + } + }, + + TaggedTemplateExpression(path, state) { + if (!state.importId) { return; } + + let tagPath = path.get('tag'); + if (tagPath.node.name !== state.importId.name) { + return; + } + + if (path.node.quasi.expressions.length) { + return error(path, state, "placeholders inside a tagged template string are not supported"); + } + + let template = path.node.quasi.quasis.map(quasi => quasi.value.cooked).join(''); + + try { + path.replaceWithSourceString(compile(template)); + } catch(e) { + return error(path, state, e.toString()); + } + + } + } + }; +}; diff --git a/script/compile_hbs.rb b/script/compile_hbs.rb new file mode 100644 index 0000000000..484a7e897d --- /dev/null +++ b/script/compile_hbs.rb @@ -0,0 +1,25 @@ +ctx = MiniRacer::Context.new(timeout: 15000) +ctx.eval("var self = this; #{File.read("#{Rails.root}/vendor/assets/javascripts/babel.js")}") +ctx.eval(File.read(Ember::Source.bundled_path_for('ember-template-compiler.js'))) +ctx.eval("module = {}; exports = {};"); +ctx.attach("rails.logger.info", proc{|err| puts(">> #{err.to_s}")}) +ctx.attach("rails.logger.error", proc{|err| puts(">> #{err.to_s}")}) +ctx.eval <Hello {{attrs.name}}` + }); + + this.set('args', { name: 'Robin' }); + }, + + test(assert) { + assert.equal(this.$('div.test').text(), "Hello Robin"); + } +}); + +widgetTest("hbs template - with tagName", { + template: `{{mount-widget widget="hbs-test" args=args}}`, + + beforeEach() { + createWidget('hbs-test', { + tagName: 'div.test', + template: hbs`Hello {{attrs.name}}` + }); + + this.set('args', { name: 'Robin' }); + }, + + test(assert) { + assert.equal(this.$('div.test').text(), "Hello Robin"); + } +}); + widgetTest('buildClasses', { template: `{{mount-widget widget="classname-test" args=args}}`, @@ -90,15 +121,12 @@ widgetTest('widget state', { createWidget('state-test', { tagName: 'button.test', buildKey: () => `button-test`, + template: hbs`{{state.clicks}} clicks`, defaultState() { return { clicks: 0 }; }, - html(attrs, state) { - return `${state.clicks} clicks`; - }, - click() { this.state.clicks++; } @@ -123,10 +151,13 @@ widgetTest('widget update with promise', { createWidget('promise-test', { tagName: 'button.test', buildKey: () => 'promise-test', - - html(attrs, state) { - return state.name || "No name"; - }, + template: hbs` + {{#if state.name}} + {{state.name}} + {{else}} + No name + {{/if}} + `, click() { return new Ember.RSVP.Promise(resolve => { @@ -140,11 +171,11 @@ widgetTest('widget update with promise', { }, test(assert) { - assert.equal(this.$('button.test').text(), "No name"); + assert.equal(this.$('button.test').text().trim(), "No name"); click(this.$('button')); andThen(() => { - assert.equal(this.$('button.test').text(), "Robin"); + assert.equal(this.$('button.test').text().trim(), "Robin"); }); } }); @@ -157,9 +188,7 @@ widgetTest('widget attaching', { createWidget('attach-test', { tagName: 'div.container', - html() { - return this.attach('test-embedded'); - }, + template: hbs`{{attach widget="test-embedded"}}` }); }, @@ -169,15 +198,78 @@ widgetTest('widget attaching', { } }); +widgetTest("handlebars fa-icon", { + template: `{{mount-widget widget="hbs-icon-test" args=args}}`, + + beforeEach() { + createWidget('hbs-icon-test', { + template: hbs`{{fa-icon "arrow-down"}}` + }); + }, + + test(assert) { + assert.equal(this.$('i.fa.fa-arrow-down').length, 1); + } +}); + +widgetTest("handlebars i18n", { + template: `{{mount-widget widget="hbs-i18n-test" args=args}}`, + + beforeEach() { + createWidget('hbs-i18n-test', { + template: hbs` + {{i18n "hbs_test0"}} + {{i18n attrs.key}} + test + ` + }); + I18n.extras = [ { + "hbs_test0": "evil", + "hbs_test1": "trout" + } ]; + this.set('args', { key: 'hbs_test1' }); + }, + + test(assert) { + // comin up + assert.equal(this.$('span.string').text(), 'evil'); + assert.equal(this.$('span.var').text(), 'trout'); + assert.equal(this.$('a').prop('title'), 'evil'); + } +}); + +widgetTest('handlebars #each', { + template: `{{mount-widget widget="hbs-each-test" args=args}}`, + + beforeEach() { + createWidget('hbs-each-test', { + tagName: 'ul', + template: hbs` + {{#each attrs.items as |item|}} +
  • {{item}}
  • + {{/each}} + ` + }); + + this.set('args', { + items: ['one', 'two', 'three'] + }); + }, + + test(assert) { + assert.equal(this.$('ul li').length, 3); + assert.equal(this.$('ul li:eq(0)').text(), "one"); + } + +}); + widgetTest('widget decorating', { template: `{{mount-widget widget="decorate-test"}}`, beforeEach() { createWidget('decorate-test', { tagName: 'div.decorate', - html() { - return "main content"; - }, + template: hbs`main content` }); withPluginApi('0.1', api => { @@ -204,14 +296,8 @@ widgetTest('widget settings', { beforeEach() { createWidget('settings-test', { tagName: 'div.settings', - - settings: { - age: 36 - }, - - html() { - return `age is ${this.settings.age}`; - }, + template: hbs`age is {{settings.age}}`, + settings: { age: 36 } }); }, @@ -226,14 +312,8 @@ widgetTest('override settings', { beforeEach() { createWidget('ov-settings-test', { tagName: 'div.settings', - - settings: { - age: 36 - }, - - html() { - return `age is ${this.settings.age}`; - }, + template: hbs`age is {{settings.age}}`, + settings: { age: 36 }, }); withPluginApi('0.1', api => { From 611d62e4a514bf0f818beb671a121ade810b9672 Mon Sep 17 00:00:00 2001 From: minusfive Date: Fri, 1 Sep 2017 07:29:50 -0400 Subject: [PATCH 004/159] Cleanup, deduplicate, debug user css, separated user-info component css --- .../javascripts/discourse/templates/user.hbs | 1 - .../stylesheets/common/base/user-badges.scss | 13 - app/assets/stylesheets/common/base/user.scss | 411 +++++++++++++---- .../common/components/user-info.scss | 68 +++ .../desktop/components/user-info.scss | 14 + app/assets/stylesheets/desktop/user.scss | 387 ++++------------ .../mobile/components/user-info.scss | 14 + app/assets/stylesheets/mobile/user.scss | 427 ++++-------------- 8 files changed, 583 insertions(+), 752 deletions(-) create mode 100644 app/assets/stylesheets/common/components/user-info.scss create mode 100644 app/assets/stylesheets/desktop/components/user-info.scss create mode 100644 app/assets/stylesheets/mobile/components/user-info.scss diff --git a/app/assets/javascripts/discourse/templates/user.hbs b/app/assets/javascripts/discourse/templates/user.hbs index 5634f64102..c2897679c6 100644 --- a/app/assets/javascripts/discourse/templates/user.hbs +++ b/app/assets/javascripts/discourse/templates/user.hbs @@ -116,7 +116,6 @@
    - {{#unless collapsedInfo}}
    diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss index 5d155ddb00..cdcfc202ad 100644 --- a/app/assets/stylesheets/common/base/user-badges.scss +++ b/app/assets/stylesheets/common/base/user-badges.scss @@ -58,19 +58,6 @@ } } -.user-info.medium.badge-info { - min-height: 80px; - - .granted-on { - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - } - - .post-link { - display: block; - margin-top: 0.2em; - } -} - .show-badge .badge-user-info { .earned { font-size: 1.3em; diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index ad5bb38f46..1487250545 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -1,8 +1,265 @@ // Common styles for "/user" section +.user-right { + .list-actions { + margin-bottom: 10px; + + .btn { + margin-right: 10px; + } + } +} + .user-main { .d-icon-heart { color: $love !important; } + + .about { + overflow: hidden; + width: 100%; + + .secondary { + font-size: 0.929em; + + .btn { + padding: 3px 12px; + } + + dl { + margin: 0; + } + + dd { + padding: 0; + overflow: hidden; + text-overflow: ellipsis; + color: $primary; + + &.groups { + span:after { + content: ',' + } + span:last-of-type:after { + content:'' + } + } + } + + dt { + color: $secondary-medium; + margin: 0; + display: inline-block; + } + } + + .details { + background: rgba($secondary, .85); + + blockquote { + background-color: $secondary-low; + border-left-color: $secondary-low; + } + + h1 { + font-size: 2.143em; + font-weight: normal; + i {font-size: .8em;} + } + + h2 { + font-size: 1.214em; + font-weight: normal; + margin-top: 10px; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + h3 { + font-weight: normal; + font-size: 1em; + margin: 5px 0; + i:not(:first-of-type) { + margin-left: 10px; + } + } + + .groups { + margin-left: 10px; + display: inline; + } + + img.avatar { + float: left; + } + + .suspended { + color: $danger; + } + + .primary { + width: 100%; + position: relative; + float: left; + + h1 { + font-weight: bold; + } + + .bio { + max-height: 300px; + overflow: auto; + + a[href] { + text-decoration: underline; + } + + img { + max-width: 100%; + } + } + } + } + + .controls { + ul { + list-style-type: none; + } + + a { + padding: 5px 10px; + margin-bottom: 10px; + } + } + + &.collapsed-info { + .controls { + margin-top: 0; + } + + .profile-image { + height: 0; + } + + .details { + margin-top: 0; + background: rgba($secondary, .85); + + .bio { + display: none; + } + + .primary { + text-align: left; + margin-top: 0; + width: 100%; + + .avatar { + float: left; + margin-right: 10px; + width: 45px; + height: 45px; + } + + h1 { + font-size: 1.429em; + } + + h2 { + font-size: 1.071em; + margin-top: 4px; + } + + h3 { + display: none; + } + } + } + } + } + + .staff-counters { + text-align: left; + background: $primary; + + > div { + margin: 0 10px 0 0; + display: inline-block; + padding: 5px 0; + &:first-of-type { + padding-left: 10px; + } + span { + padding: 1px 5px; + border-radius: 10px; + } + } + + a { + color: $secondary; + } + + .active { + font-weight: bold; + } + } + + .pill { + border-radius: 15px; + display: inline-block; + height: 30px; + width: 30px; + text-align: center; + vertical-align: middle; + line-height: 30px; + } + + .helpful-flags { + background-color: green; + } + + .flagged-posts { + background-color: #E49735; + } + + .warnings-received { + background-color: #EC441B; + } + + .deleted-posts { + background-color: #EC441B; + } + + .suspensions { + background-color: #c22020; + } + + .user-field { + clear: both; + margin-bottom: 10px; + + &.text { + padding-top: 18px; + } + + .controls { + label { + width: auto; + text-align: left; + font-weight: normal; + float: auto; + } + + .instructions { + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + margin-top: 5px; + margin-bottom: 10px; + font-size: 80%; + line-height: 1.4em; + } + } + } } .user-field { @@ -17,24 +274,26 @@ .public-user-fields { margin-top: 8px; margin-bottom: 8px; + .user-field-name { font-weight: bold; } -} -.collapsed-info .public-user-fields { - display: none; + .collapsed-info & { + display: none; + } } .user-navigation { - .map { height: 50px; } + .avatar { float: left; width: 45px; } + nav.buttons { width: 180px; padding: 0; @@ -44,6 +303,7 @@ box-sizing: border-box; } } + h2 { a { font-size: 1em; @@ -51,7 +311,6 @@ cursor: pointer; } } - } .user-table { @@ -78,69 +337,6 @@ margin-bottom: 15px; } -.user-info { - display: inline-block; - clear: both; - margin-bottom: 1em; - - .user-image { - float: left; - padding-right: 4px; - } - - .user-detail { - float: left; - width: 70%; - padding-left: 5px; - font-size: 13px; - - .name-line { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .username a { - font-weight: bold; - color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); - } - - .name { - margin-left: 5px; - color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); - } - - .title { - margin-top: 3px; - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - } - } -} - -.user-info.small { - width: 333px; -} - -.user-info.medium { - width: 480px; - min-height: 60px; - - .user-image { - width: 55px; - } - .user-detail { - width: 380px; - } - - .username, .name { - display: block; - } - - .name { - margin-left: 0; - } -} - .user-nav { margin: 5px 0px; padding-top: 10px; @@ -150,15 +346,6 @@ } } -.user-right { - .list-actions { - margin-bottom: 10px; - .btn { - margin-right: 10px; - } - } -} - .top-section { @include clearfix(); ul { @@ -229,6 +416,11 @@ .topic-info { color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 40%)); } + + @media all and (max-width : 600px) { + float: none; + width: 100%; + } } .replies-section, @@ -252,19 +444,62 @@ } } -@media all -and (max-width : 600px) { - .top-sub-section { - float: none; - width: 100%; +.groups { + .group-link { + color: $tertiary; } } -.user-preferences .tags .select2-container-multi { - border: 1px solid $primary-low; - width: 540px; - border-radius: 0; - .select2-choices { - border: none; +.user-preferences { + textarea { + height: 100px; + } + + .static { + color: $primary; + display: inline-block; + } + + .instructions { + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + margin-bottom: 10px; + font-size: 80%; + line-height: 1.4em; + + a[href] { + color: $tertiary; + } + } + + .avatar { + margin-left: 3px; + } + + .warning { + background-color: scale-color($danger, $lightness: 30%); + padding: 5px 8px; + color: $secondary; + width: 520px; + } + + .category-notifications .category-controls, + .tag-notifications .tag-controls { + margin-top: 24px; + } + + .tags .select2-container-multi { + border: 1px solid $primary-low; + width: 540px; + border-radius: 0; + .select2-choices { + border: none; + } + } +} + +.paginated-topics-list { + .user-content { + width: 100%; + margin-top: 0; } } diff --git a/app/assets/stylesheets/common/components/user-info.scss b/app/assets/stylesheets/common/components/user-info.scss new file mode 100644 index 0000000000..8c274f7b5f --- /dev/null +++ b/app/assets/stylesheets/common/components/user-info.scss @@ -0,0 +1,68 @@ +// Common styles for "user-info" component +.user-info { + display: inline-block; + clear: both; + margin-bottom: 1em; + + .user-image { + float: left; + padding-right: 4px; + } + + .user-detail { + float: left; + width: 70%; + padding-left: 5px; + font-size: 13px; + + .name-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .username a { + font-weight: bold; + color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); + } + + .name { + margin-left: 5px; + color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); + } + + .title { + margin-top: 3px; + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + } + } + + &.small { + width: 333px; + } + + &.medium { + min-height: 60px; + + .username, .name { + display: block; + } + + .name { + margin-left: 0; + } + + &.badge-info { + min-height: 80px; + + .granted-on { + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + } + + .post-link { + display: block; + margin-top: 0.2em; + } + } + } +} diff --git a/app/assets/stylesheets/desktop/components/user-info.scss b/app/assets/stylesheets/desktop/components/user-info.scss new file mode 100644 index 0000000000..f1b7260d19 --- /dev/null +++ b/app/assets/stylesheets/desktop/components/user-info.scss @@ -0,0 +1,14 @@ +.user-info { + &.medium { + width: 480px; + + .user-image { + width: 55px; + } + + .user-detail { + width: 380px; + } + + } +} diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 6a54197484..f6f39a8d30 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -1,7 +1,17 @@ // Desktop styles for "/user" section -.groups { - .group-link { - color: $tertiary; +.user-right { + display: table-cell; + + &, + > .user-stream { + > .alert:first-child { + margin-top: 10px; + } + } + + .group-notification-menu { + float: right; + margin-bottom: 5px; } } @@ -18,81 +28,6 @@ } } -.user-preferences { - display: table-cell; - vertical-align: top; - padding-top: 10px; - padding-left: 30px; - - h3 { - color: $primary; - margin: 20px 0 10px 0; - } - - input.category-selector, input.user-selector, input.tag-chooser { - width: 530px; - } - - textarea { - width: 530px; - height: 100px; - } - - input[type=text] { - @include small-width { - width: 450px; - } - } - - .static { - color: $primary; - display: inline-block; - } - .instructions { - display: inline-block; - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - margin-top: 0; - margin-bottom: 10px; - font-size: 80%; - line-height: 1.4em; - } - .form-horizontal .instructions { - margin-left: 160px; - } - .avatar { - margin-left: 3px; - } - .instructions a[href] { - color: $tertiary; - } - .warning { - background-color: scale-color($danger, $lightness: 30%); - padding: 5px 8px; - color: $secondary; - width: 520px; - } - - .pref-mailing-list-mode .controls { - select { - width: 400px; - } - } - - .notifications, .category-notifications, .tag-notifications, .user-custom-preferences-outlet { - .controls select { - width: 280px; - } - } - - .category-notifications .category-controls, .tag-notifications .tag-controls { - margin-top: 24px; - } -} - -.user-main .user-preferences .user-field.text { - padding-top: 0; -} - .form-horizontal .control-group.category { margin-top: 18px; } @@ -101,6 +36,7 @@ width: 100%; display: table; table-layout: fixed; + .wrapper { display: table-row; } @@ -138,12 +74,6 @@ } } -.viewing-self .user-main .about.collapsed-info { - .secondary, .staff-counters { - display: inherit; - } -} - .user-content { padding: 10px 8px; background-color: $secondary; @@ -206,8 +136,6 @@ .about { background-position: center center; background-size: cover; - width: 100%; - overflow: hidden; &.group { .details { @@ -221,108 +149,33 @@ background: scale-color($secondary, $lightness: -5%); border-top: 1px solid $primary-low; border-bottom: 1px solid $primary-low; - font-size: 0.929em; - - .btn { - padding: 3px 12px; - } - - dl dd { - display: inline; - margin: 0 10px 0 0; - padding: 0; - } - - dl dt { - display: inline-block; - margin: 0 5px 0 0; - padding: 0; - } dl { - margin: 0; padding: 8px 10px; } dd { - overflow: hidden; - text-overflow: ellipsis; - color: $primary; - } - - dd.groups { - span:after { - content: ',' - } - span:last-of-type:after { - content:'' - } + display: inline; + margin: 0 10px 0 0; } dt { - color: $secondary-medium; - margin: 0; + margin: 0 5px 0 0; + padding: 0; } } .details { padding: 0 0 4px 0; - background: rgba($secondary, .85); margin-top: -200px; transition: margin .15s linear; - blockquote { - background-color: $secondary-low; - border-left-color: $secondary-low; - } - - h1 { - font-size: 2.143em; - font-weight: normal; - i {font-size: .8em;} - } - - h2 { - font-size: 1.214em; - font-weight: normal; - margin-top: 10px; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - h3 { - font-weight: normal; - font-size: 1em; - margin: 5px 0; - i:not(:first-of-type) { - margin-left: 10px; - } - } - - .groups { - margin-left: 10px; - display: inline; - } - img.avatar { margin: 0 20px 10px 0; - float: left; transition: all .1s linear; } - .suspended { - color: $danger; - } - .primary { - width: 100%; - position: relative; - float: left; - - h1 {font-weight: bold;} - .primary-textual { padding: 3px; a[href] { @@ -331,21 +184,11 @@ } .bio { - max-height: 300px; - overflow: auto; max-width: 750px; - a[href] { - text-decoration: underline; - } - a.mention { text-decoration: none; } - - img { - max-width: 100%; - } } } } @@ -361,13 +204,9 @@ float: right; text-align: right; width: 180px; - ul { - list-style-type: none; - } + a { - padding: 5px 10px; width: 140px; - margin-bottom: 10px; } .right { @@ -378,176 +217,120 @@ &.collapsed-info { .controls { - margin-top: 0; width: auto; + ul { - li {display: inline;} + + li { + display: inline; + } + a { padding: 5px 10px; margin-bottom: 10px; width: auto; - } + } } } - .staff-counters { - display: none; - } - - .secondary { - display: none; - } - - .profile-image { - height: 0; - } - .details { padding: 0 0 2px 0; - margin-top: 0; - background: rgba($secondary, .85); border-bottom: 1px solid $primary-low; - .bio { - display: none; - } - - .primary { - text-align: left; - margin-top: 0; - width: 100%; - - .avatar { - float: left; - margin-right: 10px; - width: 45px; - height: 45px; - } - - h1 { - font-size: 1.429em; - } - - h2 { - font-size: 1.071em; - margin-top: 4px; - } - - h3 { - display: none; - } - } } &.has-background { - .details { padding: 12px 15px 2px 15px;} + .details { + padding: 12px 15px 2px 15px; + } } } } .staff-counters { - text-align: left; - background: $primary; color: $secondary; margin-bottom: 20px; - a { - color: $secondary; - } - > div { - margin: 0 10px 0 0; - display: inline-block; - padding: 5px 0; - &:first-of-type { - padding-left: 10px; - } - span { - padding: 1px 5px; - border-radius: 10px; - } - } - .active { - font-weight: bold; - } - } - - .pill { - border-radius: 15px; - display: inline-block; - height: 30px; - width: 30px; - text-align: center; - vertical-align: middle; - line-height: 30px; - } - .helpful-flags { - background-color: green; - } - .flagged-posts { - background-color: #E49735; - } - .warnings-received { - background-color: #EC441B; - } - .deleted-posts { - background-color: #EC441B; - } - .suspensions { - background-color: #c22020; - } - - .user-field.text { - padding-top: 18px; } .user-field { input[type=text] { width: 530px; } + .controls { - label { - width: auto; - text-align: left; - font-weight: normal; - float: auto; - } .instructions { display: block; - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - margin-top: 5px; - margin-bottom: 10px; - font-size: 80%; - line-height: 1.4em; } } - clear: both; - margin-bottom: 10px; } + .group-notification-menu .dropdown-menu { top: 30px; bottom: auto; left: auto; right: 10px; } -} -.paginated-topics-list { - .user-content { - width: 100%; - margin-top: 0; + .viewing-self & .about.collapsed-info { + .secondary, .staff-counters { + display: inherit; + } } } -.user-right { +.user-preferences { display: table-cell; + vertical-align: top; + padding-top: 10px; + padding-left: 30px; - &, - > .user-stream { - > .alert:first-child { - margin-top: 10px; + h3 { + color: $primary; + margin: 20px 0 10px 0; + } + + textarea { + width: 530px; + } + + input { + &.category-selector, + &.user-selector, + &.tag-chooser { + width: 530px; + } + + &[type=text] { + @include small-width { + width: 450px; + } } } - .group-notification-menu { - float: right; - margin-bottom: 5px; + .instructions { + display: inline-block; + margin-top: 0; + } + + .form-horizontal .instructions { + margin-left: 160px; + } + + .pref-mailing-list-mode .controls { + select { + width: 400px; + } + } + + .notifications, + .category-notifications, + .tag-notifications, + .user-custom-preferences-outlet { + .controls select { + width: 280px; + } + } + + .user-main & .user-field.text { + padding-top: 0; } } diff --git a/app/assets/stylesheets/mobile/components/user-info.scss b/app/assets/stylesheets/mobile/components/user-info.scss new file mode 100644 index 0000000000..8cf359ea18 --- /dev/null +++ b/app/assets/stylesheets/mobile/components/user-info.scss @@ -0,0 +1,14 @@ +// Mobile styles for "user-info" component +.user-info { + &.medium { + width: 300px; + + .user-image { + width: auto; + } + + .user-detail { + width: 240px; + } + } +} diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index d69b789a37..e6a84e5721 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -8,140 +8,6 @@ } } -.user-preferences { - .control-group { - padding: 8px 36px 8px 8px; - } - textarea { - width: 530px; - height: 100px; - } - .static { - color: $primary; - margin-top: 5px; - margin-left: 5px; - display: inline-block; - } - .instructions { - color: $primary; - margin-top: 5px; - } - .category-controls { - padding-top: 8px; - } - .avatar { - margin-left: 3px; - } - .instructions a[href] { - color: $primary; - } - .warning { - @include border-radius-all(6px); - background-color: $danger; - padding: 5px 8px; - color: $primary; - width: 520px; - } - - .controls-dropdown { - margin-top: 10px; - margin-bottom: 15px; - padding-left: 5px; - select { - width: 280px; - } - } - - .save-button { - width: 100%; - overflow: auto; - max-width: 200px; - button { - display: block; - } - } - - .delete-account { - overflow: hidden; - } - - .checkbox-label { - overflow: auto; - display: block; - width: 100%; - padding: 5px 8px; - } - - textarea {width: 100%;} - - .desktop-notifications button { - float: none; - } - .apps .controls button { - float: right; - } - .category-notifications .category-controls, .tag-notifications .tag-controls { - margin-top: 24px; - } -} - -.profile-image { - height: 25px; - width: 100%; -} - - -.groups { - .group-link { - color: $tertiary; - } -} - -.hasBackground .details {margin-top: 200px; -} - -.user-preferences { - input.category-selector { - } - - textarea { - height: 100px; - } - - input[type=text] { - @include small-width { - } - } - - .static { - color: $primary; - display: inline-block; - } - .instructions { - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - margin-top: 5px; - margin-bottom: 10px; - font-size: 80%; - line-height: 1.4em; - } - .avatar { - margin-left: 3px; - } - .instructions a[href] { - color: $tertiary; - } - .warning { - background-color: scale-color($danger, $lightness: 30%); - padding: 5px 8px; - color: $secondary; - } - -} - -.form-horizontal .control-group.category { - margin-top: 18px; -} - .user-main { table.group-members { @@ -205,98 +71,31 @@ .about { background: dark-light-diff($primary, $secondary, 0%, -75%) center; - width: 100%; margin-bottom: 10px; - overflow: hidden; color: $secondary; .secondary { background: $primary-low; - font-size: 0.929em; - - .btn { padding: 3px 12px; } - - dl dd { - margin: 0 15px 0 5px; - padding: 0; - } - - dl dt { - display: inline-block; - } dl { padding: 10px 15px; - margin: 0; } dd { - overflow: hidden; - text-overflow: ellipsis; - color: $primary; - } - - dt { - color: $secondary-medium; - margin: 0; + margin: 0 15px 0 5px; } } .details { - padding: 15px 10px 4px 10px; - background: rgba($secondary, .85); - - blockquote { - background-color: $secondary-low; - border-left-color: $secondary-low; - } + padding: 15px 0 4px 0; h1 { - font-size: 2.143em; - font-weight: normal; margin: 10px 0 0 0; } - h2 { - font-size: 1.214em; - font-weight: normal; - margin-top: 10px; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - h3 { - font-weight: normal; - font-size: 1em; - margin: 5px 0; - i:not(:first-of-type) { - margin-left: 10px; - } - } - - .groups { - margin-left: 10px; - display: inline; - } - - img.avatar { - float: left; - } - - .suspended { - color: $danger; - } - .primary { - width: 100%; - position: relative; - float: left; color: $primary; - h1 {font-weight: bold;} - .primary-textual { float: left; padding-left: 15px; @@ -307,17 +106,7 @@ .bio { color: $primary; - max-height: 300px; - overflow: auto; max-width: 700px; - - a[href] { - text-decoration: underline; - } - - img { - max-width: 100%; - } } } } @@ -325,119 +114,26 @@ .controls { float: left; padding-left: 15px; + ul { - list-style-type: none; margin: 0; } + a { - padding: 5px 10px; width: 120px; - margin-bottom: 10px; } } - } - .about.collapsed-info { - .controls { - margin-top: 0; - - } - - .staff-counters { - display: none; - } - .secondary { display: none; } - - .profile-image { - height: 0; - } - - .details { - padding: 12px 15px 2px 15px; - margin-top: 0; - background: rgba($secondary, .85); - .bio { display: none; } - - .primary { - text-align: left; - margin-top: 0; - width: 100%; - - .avatar { - float: left; - margin-right: 10px; - width: 45px; - height: 45px; - } - - h1 { - font-size: 1.429em; - } - - h2 { - font-size: 1.071em; - margin-top: 4px; - } - - h3 { - display: none; - } + &.collapsed-info { + .details { + padding: 12px 15px 2px 15px; } } } .staff-counters { - text-align: left; - background: $primary; padding: 7px 0; display: inline; - > div { - margin: 0 10px 0 0; - display: inline-block; - padding: 5px 0; - &:first-of-type { - padding-left: 10px; - } - span { - padding: 1px 5px; - border-radius: 10px; - } - - } - a { - color: $secondary; - } - .active { - font-weight: bold; - } - } - .pill { - border-radius: 15px; - display: inline-block; - height: 30px; - width: 30px; - text-align: center; - vertical-align: middle; - line-height: 30px; - } - .helpful-flags { - background-color: green; - } - .flagged-posts { - background-color: #E49735; - } - .warnings-received { - background-color: #EC441B; - } - .deleted-posts { - background-color: #EC441B; - } - .suspensions { - background-color: #c22020; - } - - .user-field.text { - padding-top: 18px; } .user-field { @@ -446,61 +142,96 @@ text-align: left; font-weight: bold; } - .controls { - label { - width: auto; - text-align: left; - font-weight: normal; - float: auto; - } - .instructions { - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - margin-top: 5px; - margin-bottom: 10px; - font-size: 80%; - line-height: 1.4em; - } - } - clear: both; - margin-bottom: 10px; } } +.profile-image { + height: 25px; + width: 100%; +} + +.has-background .details { + margin-top: 200px; +} + +.form-horizontal .control-group.category { + margin-top: 18px; +} + .paginated-topics-list { margin-top: 20px; - - .user-content { - width: 100%; - margin-top: 0; - } } // mobile fixups for badges - .badge-card.medium { width: 300px; } -.show-badge-details { - margin-bottom: 1em; -} - .user-badges { margin-bottom: 2em; } -.show-badge-details .badge-grant-info { - display: none; +.show-badge-details { + margin-bottom: 1em; + + .badge-grant-info { + display: none; + } } -.user-info.medium { - width: 300px; -} +.user-preferences { + .control-group { + padding: 8px 36px 8px 8px; + } -.user-info.medium .user-detail { - width: 240px; -} + .static { + margin-top: 5px; + margin-left: 5px; + } -.user-info.medium .user-image { - width: auto; + .instructions { + margin-top: 5px; + } + + .category-controls { + padding-top: 8px; + } + + .controls-dropdown { + margin-top: 10px; + margin-bottom: 15px; + padding-left: 5px; + + select { + width: 280px; + } + } + + .save-button { + width: 100%; + overflow: auto; + max-width: 200px; + + button { + display: block; + } + } + + .delete-account { + overflow: hidden; + } + + .checkbox-label { + overflow: auto; + display: block; + width: 100%; + padding: 5px 8px; + } + + .desktop-notifications button { + float: none; + } + .apps .controls button { + float: right; + } } From b3b6eeff977405a88f43f6a01e91994546692b0c Mon Sep 17 00:00:00 2001 From: minusfive Date: Fri, 1 Sep 2017 08:32:27 -0400 Subject: [PATCH 005/159] Ensure css applies to .user-main > .about icons regardless of tag --- app/assets/stylesheets/common/base/user.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index 1487250545..134771b80c 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -80,7 +80,8 @@ font-weight: normal; font-size: 1em; margin: 5px 0; - i:not(:first-of-type) { + + .d-icon:not(:first-of-type) { margin-left: 10px; } } From e283e6aea002fb533640e68f8ee7447cf0a7c90d Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 1 Sep 2017 10:15:34 -0400 Subject: [PATCH 006/159] FEATURE: allowed_iframes site setting for allowing iframes This allows you to whitelist custom iframes if needed in posts --- .../pretty-text/pretty-text.js.es6 | 1 + .../javascripts/pretty-text/sanitizer.js.es6 | 18 ++++++----------- .../pretty-text/white-lister.js.es6 | 5 +++++ config/locales/server.en.yml | 1 + config/site_settings.yml | 4 ++++ spec/components/pretty_text_spec.rb | 20 +++++++++++++++++++ 6 files changed, 37 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/pretty-text/pretty-text.js.es6 b/app/assets/javascripts/pretty-text/pretty-text.js.es6 index 094ad9a5e8..eb493b3640 100644 --- a/app/assets/javascripts/pretty-text/pretty-text.js.es6 +++ b/app/assets/javascripts/pretty-text/pretty-text.js.es6 @@ -62,6 +62,7 @@ export function buildOptions(state) { lookupImageUrls, censoredWords, allowedHrefSchemes: siteSettings.allowed_href_schemes ? siteSettings.allowed_href_schemes.split('|') : null, + allowedIframes: (siteSettings.allowed_iframes || '').split('|'), markdownIt: true, previewing }; diff --git a/app/assets/javascripts/pretty-text/sanitizer.js.es6 b/app/assets/javascripts/pretty-text/sanitizer.js.es6 index d865c9be5c..f62df9d4bb 100644 --- a/app/assets/javascripts/pretty-text/sanitizer.js.es6 +++ b/app/assets/javascripts/pretty-text/sanitizer.js.es6 @@ -1,7 +1,5 @@ import xss from 'pretty-text/xss'; -const _validIframes = []; - function attr(name, value) { if (value) { return `${name}="${xss.escapeAttrValue(value)}"`; @@ -69,7 +67,8 @@ export function sanitize(text, whiteLister) { text = text.replace(/<([^A-Za-z\/\!]|$)/g, "<$1"); const whiteList = whiteLister.getWhiteList(), - allowedHrefSchemes = whiteLister.getAllowedHrefSchemes(); + allowedHrefSchemes = whiteLister.getAllowedHrefSchemes(), + allowedIframes = whiteLister.getAllowedIframes(); let extraHrefMatchers = null; if (allowedHrefSchemes && allowedHrefSchemes.length > 0) { @@ -85,11 +84,13 @@ export function sanitize(text, whiteLister) { const forTag = whiteList.attrList[tag]; if (forTag) { const forAttr = forTag[name]; - if ((forAttr && (forAttr.indexOf('*') !== -1 || forAttr.indexOf(value) !== -1)) || + if ( + (forAttr && (forAttr.indexOf('*') !== -1 || forAttr.indexOf(value) !== -1)) || (name.indexOf('data-') === 0 && forTag['data-*']) || ((tag === 'a' && name === 'href') && hrefAllowed(value, extraHrefMatchers)) || (tag === 'img' && name === 'src' && (/^data:image.*$/i.test(value) || hrefAllowed(value, extraHrefMatchers))) || - (tag === 'iframe' && name === 'src' && _validIframes.some(i => i.test(value)))) { + (tag === 'iframe' && name === 'src' && allowedIframes.some(i => { return value.toLowerCase().indexOf((i || '').toLowerCase()) === 0;})) + ) { return attr(name, value); } @@ -114,10 +115,3 @@ export function sanitize(text, whiteLister) { .replace(/'/g, "'") .replace(/ \/>/g, '>'); }; - -export function whiteListIframe(regexp) { - _validIframes.push(regexp); -} - -whiteListIframe(/^(https?:)?\/\/www\.google\.com\/maps\/embed\?.+/i); -whiteListIframe(/^(https?:)?\/\/www\.openstreetmap\.org\/export\/embed.html\?.+/i); diff --git a/app/assets/javascripts/pretty-text/white-lister.js.es6 b/app/assets/javascripts/pretty-text/white-lister.js.es6 index 30f8f23c96..cc022a827f 100644 --- a/app/assets/javascripts/pretty-text/white-lister.js.es6 +++ b/app/assets/javascripts/pretty-text/white-lister.js.es6 @@ -9,6 +9,7 @@ export default class WhiteLister { this._enabled = { "default": true }; this._allowedHrefSchemes = (options && options.allowedHrefSchemes) || []; + this._allowedIframes = (options && options.allowedIframes) || []; this._rawFeatures = [["default", DEFAULT_LIST]]; this._cache = null; @@ -102,6 +103,10 @@ export default class WhiteLister { getAllowedHrefSchemes() { return this._allowedHrefSchemes; } + + getAllowedIframes() { + return this._allowedIframes; + } } // Only add to `default` when you always want your whitelist to occur. In other words, diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 351832b275..6e9a1a6a55 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1058,6 +1058,7 @@ en: use_admin_ip_whitelist: "Admins can only log in if they are at an IP address defined in the Screened IPs list (Admin > Logs > Screened Ips)." blacklist_ip_blocks: "A list of private IP blocks that should never be crawled by Discourse" whitelist_internal_hosts: "A list of internal hosts that discourse can safely crawl for oneboxing and other purposes" + allowed_iframes: "A list of iframe src domain prefixes that discourse can safely allow in posts" top_menu: "Determine which items appear in the homepage navigation, and in what order. Example latest|new|unread|categories|top|read|posted|bookmarks" post_menu: "Determine which items appear on the post menu, and in what order. Example like|edit|flag|delete|share|bookmark|reply" post_menu_hidden_items: "The menu items to hide by default in the post menu unless an expansion ellipsis is clicked on." diff --git a/config/site_settings.yml b/config/site_settings.yml index 29e574dc73..2cd16d920f 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -940,6 +940,10 @@ security: whitelist_internal_hosts: default: '' type: list + allowed_iframes: + default: 'https://www.google.com/maps/embed?|https://www.openstreetmap.org/export/embed.html?' + type: list + client: true onebox: enable_flash_video_onebox: false diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 4015ad3301..0cffc4862b 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -1104,4 +1104,24 @@ HTML end + it "can properly whitelist iframes" do + SiteSetting.allowed_iframes = "https://bob.com/a|http://silly.com?EMBED=" + raw = <<~IFRAMES + + + + IFRAMES + + # we require explicit HTTPS here + html = <<~IFRAMES + + + IFRAMES + + cooked = PrettyText.cook(raw).strip + + expect(cooked).to eq(html.strip) + + end + end From 0fb783174991fff7723c46b74ed9520081aede2c Mon Sep 17 00:00:00 2001 From: Vinoth Kannan Date: Fri, 1 Sep 2017 19:56:13 +0530 Subject: [PATCH 007/159] FEATURE: Add placeholders to broken and large image files (#5113) --- .../stylesheets/common/base/topic-post.scss | 6 ++ app/jobs/regular/pull_hotlinked_images.rb | 25 ++++++ spec/jobs/pull_hotlinked_images_spec.rb | 77 ++++++++++++++++--- 3 files changed, 96 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 3f34809d20..1485257753 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -420,3 +420,9 @@ a.mention, a.mention-group { padding-bottom: 15px; } } + +.broken-image, .large-image { + border: 1px solid $primary-low; + font-size: 32px; + padding: 16px; +} diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index c38d9a55e2..ccd9b20543 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -25,6 +25,7 @@ module Jobs raw = post.raw.dup start_raw = raw.dup downloaded_urls = {} + broken_images, large_images = [], [] extract_images_from(post.cooked).each do |image| src = original_src = image['src'] @@ -56,9 +57,11 @@ module Jobs end else log(:info, "Failed to pull hotlinked image for post: #{post_id}: #{src} - Image is bigger than #{@max_size}") + large_images << original_src end else log(:info, "There was an error while downloading '#{src}' locally for post: #{post_id}") + broken_images << original_src end end # have we successfully downloaded that file? @@ -98,6 +101,28 @@ module Jobs post.revise(Discourse.system_user, changes, options) elsif downloaded_urls.present? post.trigger_post_process(true) + elsif broken_images.present? || large_images.present? + start_html = post.cooked + doc = Nokogiri::HTML::fragment(start_html) + images = doc.css("img[src]") - doc.css("img.avatar") + images.each do |tag| + src = tag['src'] + if broken_images.include?(src) + tag.name = 'span' + tag.set_attribute('class', 'broken-image fa fa-chain-broken') + tag.remove_attribute('src') + elsif large_images.include?(src) + tag.name = 'a' + tag.set_attribute('href', src) + tag.set_attribute('target', '_blank') + tag.remove_attribute('src') + tag.inner_html = '' + end + end + if start_html == post.cooked && doc.to_html != post.cooked + post.update_column(:cooked, doc.to_html) + post.publish_change_to_clients! :revised + end end end diff --git a/spec/jobs/pull_hotlinked_images_spec.rb b/spec/jobs/pull_hotlinked_images_spec.rb index 19e0049d05..566b37f354 100644 --- a/spec/jobs/pull_hotlinked_images_spec.rb +++ b/spec/jobs/pull_hotlinked_images_spec.rb @@ -3,19 +3,29 @@ require 'jobs/regular/pull_hotlinked_images' describe Jobs::PullHotlinkedImages do - describe '#execute' do - let(:image_url) { "http://wiki.mozilla.org/images/2/2e/Longcat1.png" } - let(:png) { Base64.decode64("R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==") } + let(:image_url) { "http://wiki.mozilla.org/images/2/2e/Longcat1.png" } + let(:broken_image_url) { "http://wiki.mozilla.org/images/2/2e/Longcat2.png" } + let(:large_image_url) { "http://wiki.mozilla.org/images/2/2e/Longcat3.png" } + let(:png) { Base64.decode64("R0lGODlhAQABALMAAAAAAIAAAACAAICAAAAAgIAAgACAgMDAwICAgP8AAAD/AP//AAAA//8A/wD//wBiZCH5BAEAAA8ALAAAAAABAAEAAAQC8EUAOw==") } + let(:large_png) { Base64.decode64("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAK10lEQVR42r3aeVRTVx4H8Oc2atWO7Sw9OnM6HWvrOON0aFlcAZ3RopZWOyqgoCACKqPWBUVQi4gIqAVllciiKPu+JOyGnQQSNgkIIQgoKljAYVARCZnf4yXhkeXlJmDP+f4hOUF+n3fvffe++y5W0i4qJqWoDU8hKQUPxWFKcq9VnHxJ8gTi5EqS0yJOtiRZfHEyJWE0i0MnJaMJTzopaQ/wpJKS0ogneTQYABANTDlDvpxBCsiu72eUP0zPq8Fzr45e8TircRDFQAAy5ABpcgDCgJV2iCbRQM+rinU/E26ie9NgfrDO1GBtTBy96SH/WhBhaxwfGEjndmfKGeiaGsYAJXIANQyCkfR05u3dhuOKVhLamnmRzocyKp9mNo9QG9IRDDiAiMaG3Nqfo45aoJROzk3DDxNCbjGahBM0yAKoDfIDOpNZE/bNYrVKJyfylB2D91pdA3lAjwE0MDAyS+BCalw9kdu2xvT6AY0NWBkJoNaAzsrj4CN1YtUTidi/hdH4BvGmJGPAAYgGMuMery/U6ONJqZ5I1PlTjNExre7kgJU/EqEbJC0gjDpiiv9hnSkJ2z+t9dzxwNcSUudlUuuxnXP+W/bZTWWO64uO6hccWQ0pPm4IP1a6GFe5bYXvNF7f0xxg3XrzgCDYjn1m4+218/D/SndaYnSqBpMDDlDXkHYnMlh7Srj+HLanxfOsyyOVN0ScYI0zkOeVZvYZGEI2/DFDMkWgTw7jAGWUA5owMOt7QtcvDF09qybA/mGC6zA7aCLVExkq9U3895/wm9LpgyonBxmDGKDQoHBySPQ8B5e/zM2kJdalN/fqxKsn8oLhFr5mdvDyX6UVNqqcpMmDAWNJACjtUMDrDVn7m6SdS/kxPwrizg+zAycLAKm5tA0a4a7DPpSFhmIAxWAgDKm0IJrutBr/g3D5n9E9J7F6oiNFGf2WtnI2vboH3YADEA0AuG2ml2i2BC4/AAYKr00uAHL/ihk0QnxQMPqKFWM/FiEamFWPYMHD8tgF1UMmZfjKZLDIJ1z/vQibzTKrbop2wAGIhoxbt8IN5zZHnoHqO5LdJr16IkXHDG4afJDJG0B8chADUAxxTnbp1trE5Z/0ASDN09hTcJdLy+EoawQZgyyAwhCxcznr0k4C0JNz5R0BYFqM3PBhQugtxKdQrEICUGFoE4ZtWPAg4jQBeJHv/Y4AkBKHdTHuZ8lP0hSDAQdQGwhAUUNv4s6/EvcfSD/T590B2u8cj3SwltkNUGaQBSgbDAXc9pxTW4jqIf8ruAa37efJLg/DfuBd21ftYU7OA387+QXSk2gHWMmRw/M2F9D2d8WffsW8Sv5+X/mtyBN7s+V2NBQasMpOEYqhuLG3MimMqL4h/GTu4fW01b/z05qrMKEGC96W+8sA8g/qKX281JuWafX350lniG++rIpOTcknb8lQGHAAoqG+pgqqr7hqE2K4kCg0bO3CJDMthvVKInTrlUmm/4j+9vO7mxYNlfrJAJiHVsYaL0g1XZy194scmy+JMCyXxWz+CAD4anTFjLrLpiMVQW+4t1G2lQiDGIBiuF/NLbmwM1B3PpQe892SFtqh4fIAhZ14mBUo34WE7ECFC29hRdDz5LO5dtrwdAGM0pP/HKoMzWsZRtwakwVQGPJjo/2/ej9Q74N8xy19o+tQYcWNzjT3mJNmR/W/uPi9fobr3ifpl6hXeG9Zge1JF5LPWvz4zYoTa7VSzu0mniggMEigNcBQ7GjE5A9Kt/eoOxLGkQBUGkoyGeEbPqnys2+OPlcbdir80PdOX+usmDFdG8OIwCc3bI0vm657WeSrsPouhuelbQZh/9nqY7FB+lsGc2ad27w86oTJo5SLrwu9s/dpVXuYFPEHELcocQC1QXpjhS4EpcMwiPhh2/U9XzfedYYFhe7UKdJSqkNOIt4oMy/uIwP68n6C3/WzMmIFHIUeJawMLm7ul9lmVdYOYgCKob6aK72NEo8yQ+UBtl99BkXoTMFcv1sF3UNaIpd24vCqvykDvCr2PbJ6GQFwNtKFrjhuCHFCCvmvcuW2ihUaMO4TWYCyAU0GSJcSsCblRTjDSJAZoFnuNiafLqReMrQlukKTylQvBZC3iikMOIDCQGaQAT9nq1gLqQRQBABFLa9U7tcTBjEApR3IALh1/DIAlQZZAIWBDOjO9HrXAMT3JliVBKCyHciALsYvAUAx4IAqOYDCmxKPBFD5QDNBQHHLS2XvfmQMYgCKgQx4muGhFmCw1B8dIOTQyvj9FO+vyDclrPqpLECZgVczBoAlA3URMCubLv6D9I657ZOP0lws1QJQv4OTGnAAogEdAF+A+TXHw3b0R5qoszLLyx4+gc8RAeUt/SrfIxIGMYDCoBDwONVdaQ9mB+3XWeK87kvJ1EYTDfYLn9XDgsdO+3NYKSACUN6FQsYAKg2IgIqgY6tnzmi6bP8y2X2EmGUbkkWCPJitV82cURfuqPq5nhPM4vchvpDGauQAygxkAMW+ULCdsfWSj/tCTr8IdeqPdBnK94FnFCEr8DXd68CyRXeObkfpRWx+D+JLdRxANlC0QwMaINHZfP37c4oczQkDnjDnvlCnMuc9RvPnxp/ehQKokAAoOlIeGUDdDvKAtsQLyv72mzJ/P6uN+rNnHtf5S7GjRVeQQ6nTbge9pdB/vEzWDso9aqoEUBuw2mciZY0gY0AEEBHEuZzZqAdFG743c/n0aQ7rtBruOKO/y+HwnyMebsABiIbG2jFAa7wryh4bPDaUXD+swWuoKv5TxMMNYgCFgQSoIgHOv7uNLbgLcfldiAc0xgAqDbVtLwTJXgQAeojmLzLKAzjBxyl257vqcgsfChUeDJA3YHUkgEpDQz2vJU7cCDJTEnQSWOHBDK0wMACgL0U7mLptXWO/fGmCk7myGW2gOra09Q36aSUcoIahc4Rfmi59JBi3H5j3k5fJOs8dhgoTYL0Jqi/1PfyMTrUKHOKGcwS9Kg9okA1iALqh+tGggBFIGJRtn2gWWEHwmlsRD5lIDdj9LpG8gXpyuN/yRJBwEQCwRYWytkEcuB28iuK2EXVPXOEAqaEW2dBUzZI+HE/wTT2RnjpGSZtQg1NjYoDa7dA50sKMIgywyTPB6l9VRbPaXmt28m0MQNEOCgdDbXu/IM17tCO5TaQjveWG1Qi6NT75htWTAOoaeA/4gnhXlF0Wiq7f3NSk1okrGQMO0NzQOdLMziU60usSPw2q7+SVlnWMlE3g1BjG6xZNxFDe1s2OO0Z0JHhxBuMBJlroUSgju682ldUxTH24QaVhDFAvB1Bp4HS+PRO/5ZDP7xtjnaXLJGKlBMtVeGqDuRk2If97z/tl0XVYZg+T3nF0F3tcjN1W2vFWrdNK8gYcgGiQvykFFl7a7oFBvG5o5UfvVRQrRuQu+mjgH5lRu7JjLPISLAtTrJ1pf94dj4U0+mhw4opsEAPU6kiEIZ1XYnZlFgFQKzu8MYtYzKYUs63E7Lnz0ls5iKeVFBrGAGq1A6uj1zZw0XZPzPwuZhqE7biiqm4vzNQP/7JVFmZbgdlxxnKienFBe4/G7YA1kADI7TDilmQJZVlE41cRirBlYdZMzIqB7UnGdseRkohZZmDW+ZhNmfibEHvuzAOcaWTD5XpLuBepdfKtiAxQ1xDPTdnhOdXUH7Nlj7uWKDnAme7bvPlI1a/Hfz4ljp+BfnqPPKD/DzQWIVWNoUiJAAAAAElFTkSuQmCC") } + before do + stub_request(:get, image_url).to_return(body: png, headers: { "Content-Type" => "image/png" }) + stub_request(:head, image_url) + stub_request(:head, broken_image_url).to_return(status: 404) + stub_request(:get, large_image_url).to_return(body: large_png, headers: { "Content-Type" => "image/png" }) + stub_request(:head, large_image_url) + SiteSetting.download_remote_images_to_local = true + SiteSetting.max_image_size_kb = 2 + end + + describe '#execute' do before do - stub_request(:get, image_url).to_return(body: png, headers: { "Content-Type" => "image/png" }) - stub_request(:head, image_url) - SiteSetting.download_remote_images_to_local = true FastImage.expects(:size).returns([100, 100]).at_least_once end it 'replaces images' do - post = Fabricate(:post, raw: "") + post = Fabricate(:post, raw: "") Jobs::PullHotlinkedImages.new.execute(post_id: post.id) post.reload @@ -24,7 +34,8 @@ describe Jobs::PullHotlinkedImages do end it 'replaces images without protocol' do - post = Fabricate(:post, raw: "") + url = image_url.sub(/^https?\:/, '') + post = Fabricate(:post, raw: "") Jobs::PullHotlinkedImages.new.execute(post_id: post.id) post.reload @@ -33,10 +44,10 @@ describe Jobs::PullHotlinkedImages do end it 'replaces images without extension' do - extensionless_url = "http://wiki.mozilla.org/images/2/2e/Longcat1" - stub_request(:get, extensionless_url).to_return(body: png, headers: { "Content-Type" => "image/png" }) - stub_request(:head, extensionless_url) - post = Fabricate(:post, raw: "") + url = image_url.sub(/\.[a-zA-Z0-9]+$/, '') + stub_request(:get, url).to_return(body: png, headers: { "Content-Type" => "image/png" }) + stub_request(:head, url) + post = Fabricate(:post, raw: "") Jobs::PullHotlinkedImages.new.execute(post_id: post.id) post.reload @@ -80,6 +91,48 @@ describe Jobs::PullHotlinkedImages do expect(post.cooked).to match(/ +#{url} + + + ") + + Jobs::ProcessPost.new.execute(post_id: post.id) + Jobs::PullHotlinkedImages.new.execute(post_id: post.id) + Jobs::ProcessPost.new.execute(post_id: post.id) + Jobs::PullHotlinkedImages.new.execute(post_id: post.id) + post.reload + + expect(post.cooked).to match(/

    <\/span><\/a>/) + end + end + end + + describe 'replace' do + it 'broken image with placeholder' do + post = Fabricate(:post, raw: "") + + Jobs::ProcessPost.new.execute(post_id: post.id) + Jobs::PullHotlinkedImages.new.execute(post_id: post.id) + post.reload + + expect(post.cooked).to match(/") + + Jobs::ProcessPost.new.execute(post_id: post.id) + Jobs::PullHotlinkedImages.new.execute(post_id: post.id) + post.reload + + expect(post.cooked).to match(/<\/span><\/a>/) end end From cb56dcdf2ea2ee93267cbec49c97643530296138 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 1 Sep 2017 11:20:14 -0400 Subject: [PATCH 008/159] FIX: Use proper `iconNode` when compiling virtual dom templates --- .../discourse-common/lib/icon-library.js.es6 | 2 ++ app/assets/javascripts/discourse.js.es6 | 1 + lib/javascripts/widget-hbs-compiler.js.es6 | 22 ++++++++++++++----- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index 60fe773995..f1c3c09ffd 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -23,6 +23,8 @@ export function iconNode(id, params) { return renderIcon('node', id, params); } +Discourse.__widget_helpers.iconNode = iconNode; + export function registerIconRenderer(renderer) { _renderers.unshift(renderer); } diff --git a/app/assets/javascripts/discourse.js.es6 b/app/assets/javascripts/discourse.js.es6 index 1c572af7ba..cee3fcbd57 100644 --- a/app/assets/javascripts/discourse.js.es6 +++ b/app/assets/javascripts/discourse.js.es6 @@ -7,6 +7,7 @@ const Discourse = Ember.Application.extend({ rootElement: '#main', _docTitle: document.title, RAW_TEMPLATES: {}, + __widget_helpers: {}, getURL(url) { if (!url) return url; diff --git a/lib/javascripts/widget-hbs-compiler.js.es6 b/lib/javascripts/widget-hbs-compiler.js.es6 index 9b28d186a4..24f02eecce 100644 --- a/lib/javascripts/widget-hbs-compiler.js.es6 +++ b/lib/javascripts/widget-hbs-compiler.js.es6 @@ -2,7 +2,7 @@ function resolve(path) { return (path.indexOf('settings') === 0) ? `this.${path}` : path; } -function mustacheValue(node) { +function mustacheValue(node, state) { let path = node.path.original; switch(path) { @@ -27,8 +27,9 @@ function mustacheValue(node) { break; case 'fa-icon': + state.helpersUsed.iconNode = true; let icon = node.params[0].value; - return `virtualDom.h('i.fa.fa-${icon}')`; + return `__iN("${icon}")`; break; default: return `${resolve(path)}`; @@ -40,6 +41,10 @@ class Compiler { constructor(ast) { this.idx = 0; this.ast = ast; + + this.state = { + helpersUsed: {} + }; } newAcc() { @@ -69,7 +74,7 @@ class Compiler { node.attributes.forEach(a => { const name = a.name === 'class' ? 'className' : a.name; if (a.value.type === "MustacheStatement") { - attributes.push(`"${name}":${mustacheValue(a.value)}`); + attributes.push(`"${name}":${mustacheValue(a.value, this.state)}`); } else { attributes.push(`"${name}":"${a.value.chars}"`); } @@ -87,7 +92,7 @@ class Compiler { return `${parentAcc}.push(${JSON.stringify(node.chars)});`; case "MustacheStatement": - const value = mustacheValue(node); + const value = mustacheValue(node, this.state); if (value) { instructions.push(`${parentAcc}.push(${value})`); } @@ -139,7 +144,14 @@ function compile(template) { const compiled = preprocessor.preprocess(template); const compiler = new Compiler(compiled); - return `function(attrs, state) { var _r = [];\n${compiler.compile()}\nreturn _r; }`; + let code = compiler.compile(); + + let imports = ''; + if (compiler.state.helpersUsed.iconNode) { + imports = "var __iN = Discourse.__widget_helpers.iconNode; "; + } + + return `function(attrs, state) { ${imports}var _r = [];\n${code}\nreturn _r; }`; } exports.compile = compile; From 46ebd0ee40c23363122a0fc71abd3ca6548e6efa Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 1 Sep 2017 12:08:39 -0400 Subject: [PATCH 009/159] correct spec and allow for zero allowed iframes --- app/assets/javascripts/pretty-text/pretty-text.js.es6 | 2 +- test/javascripts/lib/sanitizer-test.js.es6 | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/pretty-text/pretty-text.js.es6 b/app/assets/javascripts/pretty-text/pretty-text.js.es6 index eb493b3640..9c7a814940 100644 --- a/app/assets/javascripts/pretty-text/pretty-text.js.es6 +++ b/app/assets/javascripts/pretty-text/pretty-text.js.es6 @@ -62,7 +62,7 @@ export function buildOptions(state) { lookupImageUrls, censoredWords, allowedHrefSchemes: siteSettings.allowed_href_schemes ? siteSettings.allowed_href_schemes.split('|') : null, - allowedIframes: (siteSettings.allowed_iframes || '').split('|'), + allowedIframes: siteSettings.allowed_iframes ? siteSettings.allowed_iframes.split('|') : [], markdownIt: true, previewing }; diff --git a/test/javascripts/lib/sanitizer-test.js.es6 b/test/javascripts/lib/sanitizer-test.js.es6 index cc5dcb205c..8650bc90ba 100644 --- a/test/javascripts/lib/sanitizer-test.js.es6 +++ b/test/javascripts/lib/sanitizer-test.js.es6 @@ -4,7 +4,9 @@ import { hrefAllowed } from 'pretty-text/sanitizer'; QUnit.module("lib:sanitizer"); QUnit.test("sanitize", assert => { - const pt = new PrettyText(buildOptions({ siteSettings: {} })); + const pt = new PrettyText(buildOptions({ siteSettings: { + "allowed_iframes": 'https://www.google.com/maps/embed?|https://www.openstreetmap.org/export/embed.html?' + } })); const cooked = (input, expected, text) => assert.equal(pt.cook(input), expected.replace(/\/>/g, ">"), text); assert.equal(pt.sanitize("bug"), "bug"); @@ -28,8 +30,8 @@ QUnit.test("sanitize", assert => { "", "it allows iframe to google maps"); - cooked("", - "", + cooked("", + "", "it allows iframe to OpenStreetMap"); assert.equal(pt.sanitize(""), "hullo"); From c6ff387ce9d7660c804e9ba55096167dc427a751 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 1 Sep 2017 12:14:16 -0400 Subject: [PATCH 010/159] Use more semantic names for various tracking icons This way they can be replaced by plugins without conflicting with other icons. For example `circle` is used in some places that doesn't represent `tracking`. --- .../discourse-common/lib/icon-library.js.es6 | 12 ++++++++++++ .../discourse/controllers/topic-bulk-actions.js.es6 | 2 +- .../discourse/lib/notification-levels.js.es6 | 10 +++++----- .../discourse/templates/preferences/categories.hbs | 8 ++++---- .../templates/preferences/notifications.hbs | 2 +- .../discourse/templates/preferences/tags.hbs | 8 ++++---- 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index f1c3c09ffd..03c2240d5c 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -1,6 +1,14 @@ import { h } from 'virtual-dom'; let _renderers = []; +const REPLACEMENTS = { + 'd-tracking': 'circle', + 'd-muted': 'times-circle', + 'd-regular': 'circle-o', + 'd-watching': 'exclamation-circle', + 'd-watching-first': 'dot-circle-o' +}; + export function renderIcon(renderType, id, params) { for (let i=0; i<_renderers.length; i++) { let renderer = _renderers[i]; @@ -44,6 +52,8 @@ registerIconRenderer({ name: 'font-awesome', string(id, params) { + id = REPLACEMENTS[id] || id; + let tagName = params.tagName || 'i'; let html = `<${tagName} class='${faClasses(id, params)}'`; if (params.title) { html += ` title='${I18n.t(params.title)}'`; } @@ -56,6 +66,8 @@ registerIconRenderer({ }, node(id, params) { + id = REPLACEMENTS[id] || id; + let tagName = params.tagName || 'i'; const properties = { diff --git a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 index ef7d23eba7..4b2a19d457 100644 --- a/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-bulk-actions.js.es6 @@ -25,7 +25,7 @@ function addBulkButton(action, key, opts) { addBulkButton('showChangeCategory', 'change_category', {icon: 'pencil'}); addBulkButton('closeTopics', 'close_topics', {icon: 'lock'}); addBulkButton('archiveTopics', 'archive_topics', {icon: 'folder'}); -addBulkButton('showNotificationLevel', 'notification_level', {icon: 'circle-o'}); +addBulkButton('showNotificationLevel', 'notification_level', {icon: 'd-regular'}); addBulkButton('resetRead', 'reset_read', {icon: 'backward'}); addBulkButton('unlistTopics', 'unlist_topics', { icon: 'eye-slash', diff --git a/app/assets/javascripts/discourse/lib/notification-levels.js.es6 b/app/assets/javascripts/discourse/lib/notification-levels.js.es6 index 6d744790e2..adeae1bcaa 100644 --- a/app/assets/javascripts/discourse/lib/notification-levels.js.es6 +++ b/app/assets/javascripts/discourse/lib/notification-levels.js.es6 @@ -9,15 +9,15 @@ export const NotificationLevels = { WATCHING_FIRST_POST, WATCHING, TRACKING, REG export function buttonDetails(level) { switch(level) { case WATCHING_FIRST_POST: - return { id: WATCHING_FIRST_POST, key: 'watching_first_post', icon: 'dot-circle-o' }; + return { id: WATCHING_FIRST_POST, key: 'watching_first_post', icon: 'd-watching-first' }; case WATCHING: - return { id: WATCHING, key: 'watching', icon: 'exclamation-circle' }; + return { id: WATCHING, key: 'watching', icon: 'd-watching' }; case TRACKING: - return { id: TRACKING, key: 'tracking', icon: 'circle' }; + return { id: TRACKING, key: 'tracking', icon: 'd-tracking' }; case MUTED: - return { id: MUTED, key: 'muted', icon: 'times-circle' }; + return { id: MUTED, key: 'muted', icon: 'd-muted' }; default: - return { id: REGULAR, key: 'regular', icon: 'circle-o' }; + return { id: REGULAR, key: 'regular', icon: 'd-regular' }; } } diff --git a/app/assets/javascripts/discourse/templates/preferences/categories.hbs b/app/assets/javascripts/discourse/templates/preferences/categories.hbs index 856feedcf7..27a07fef11 100644 --- a/app/assets/javascripts/discourse/templates/preferences/categories.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/categories.hbs @@ -3,7 +3,7 @@

    {{i18n 'user.watched_categories_instructions'}}
    @@ -12,7 +12,7 @@
    - + {{category-selector categories=model.trackedCategories blacklist=selectedCategories}}
    {{i18n 'user.tracked_categories_instructions'}}
    @@ -21,13 +21,13 @@
    - + {{category-selector categories=model.watchedFirstPostCategories}}
    {{i18n 'user.watched_first_post_categories_instructions'}}
    - + {{category-selector categories=model.mutedCategories blacklist=selectedCategories}}
    {{i18n 'user.muted_categories_instructions'}}
    diff --git a/app/assets/javascripts/discourse/templates/preferences/notifications.hbs b/app/assets/javascripts/discourse/templates/preferences/notifications.hbs index c9cf37ac4d..0ddeefe998 100644 --- a/app/assets/javascripts/discourse/templates/preferences/notifications.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/notifications.hbs @@ -29,7 +29,7 @@
    - + {{user-selector excludeCurrentUser=true usernames=model.muted_usernames class="user-selector"}}
    {{i18n 'user.muted_users_instructions'}}
    diff --git a/app/assets/javascripts/discourse/templates/preferences/tags.hbs b/app/assets/javascripts/discourse/templates/preferences/tags.hbs index 390429833f..1c24147a2f 100644 --- a/app/assets/javascripts/discourse/templates/preferences/tags.hbs +++ b/app/assets/javascripts/discourse/templates/preferences/tags.hbs @@ -4,25 +4,25 @@
    - + {{tag-chooser tags=model.watched_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
    {{i18n 'user.watched_tags_instructions'}}
    - + {{tag-chooser tags=model.tracked_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
    {{i18n 'user.tracked_tags_instructions'}}
    - + {{tag-chooser tags=model.watching_first_post_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
    {{i18n 'user.watched_first_post_tags_instructions'}}
    - + {{tag-chooser tags=model.muted_tags blacklist=selectedTags allowCreate=false placeholder="" everyTag="true" unlimitedTagCount="true"}}
    {{i18n 'user.muted_tags_instructions'}}
    From dfe347fb1de256d57b7ae4ec6983fdf6ba0c252e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 1 Sep 2017 12:26:42 -0400 Subject: [PATCH 011/159] FIX: Wizard tests don't need `Discourse` defined --- .../javascripts/discourse-common/lib/icon-library.js.es6 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index 03c2240d5c..bc8abf3c35 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -31,7 +31,10 @@ export function iconNode(id, params) { return renderIcon('node', id, params); } -Discourse.__widget_helpers.iconNode = iconNode; +// TODO: Improve how helpers are registered for vdom compliation +if (typeof Discourse !== "undefined") { + Discourse.__widget_helpers.iconNode = iconNode; +} export function registerIconRenderer(renderer) { _renderers.unshift(renderer); From a329f37b44656840a47bb3c6aad2bfbf860c3fb4 Mon Sep 17 00:00:00 2001 From: minusfive Date: Fri, 1 Sep 2017 12:25:30 -0400 Subject: [PATCH 012/159] Match capitalization of Groups to other stats on user > about box --- config/locales/client.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0635913191..7a1a75c10d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -443,8 +443,8 @@ en: title: "Groups" empty: "There are no visible groups." title: - one: "group" - other: "groups" + one: "Group" + other: "Groups" activity: "Activity" members: "Members" topics: "Topics" From 936582b8d1b22e4c98765ceb51f80cd210c3b2c5 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 1 Sep 2017 13:09:43 -0400 Subject: [PATCH 013/159] Correct flaky spec Can fail if the machine running tests has less that 10% free space --- spec/jobs/pull_hotlinked_images_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/jobs/pull_hotlinked_images_spec.rb b/spec/jobs/pull_hotlinked_images_spec.rb index 566b37f354..2b37f3a2b0 100644 --- a/spec/jobs/pull_hotlinked_images_spec.rb +++ b/spec/jobs/pull_hotlinked_images_spec.rb @@ -82,6 +82,7 @@ describe Jobs::PullHotlinkedImages do end it 'replaces image src' do + SiteSetting.download_remote_images_threshold = 0 post = Fabricate(:post, raw: "#{url}") Jobs::ProcessPost.new.execute(post_id: post.id) From 4cef3cd762f5965aaf4d2e7328c54ed68c99aa28 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Fri, 1 Sep 2017 13:12:47 -0400 Subject: [PATCH 014/159] move fix to top of test --- spec/jobs/pull_hotlinked_images_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/jobs/pull_hotlinked_images_spec.rb b/spec/jobs/pull_hotlinked_images_spec.rb index 2b37f3a2b0..b9c6bb0ba5 100644 --- a/spec/jobs/pull_hotlinked_images_spec.rb +++ b/spec/jobs/pull_hotlinked_images_spec.rb @@ -17,6 +17,7 @@ describe Jobs::PullHotlinkedImages do stub_request(:head, large_image_url) SiteSetting.download_remote_images_to_local = true SiteSetting.max_image_size_kb = 2 + SiteSetting.download_remote_images_threshold = 0 end describe '#execute' do @@ -82,12 +83,12 @@ describe Jobs::PullHotlinkedImages do end it 'replaces image src' do - SiteSetting.download_remote_images_threshold = 0 post = Fabricate(:post, raw: "#{url}") Jobs::ProcessPost.new.execute(post_id: post.id) Jobs::PullHotlinkedImages.new.execute(post_id: post.id) Jobs::ProcessPost.new.execute(post_id: post.id) + post.reload expect(post.cooked).to match(/ Date: Fri, 1 Sep 2017 13:34:26 -0400 Subject: [PATCH 015/159] FIX: Linting errors in Ruby --- script/compile_hbs.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/script/compile_hbs.rb b/script/compile_hbs.rb index 484a7e897d..785aae6b38 100644 --- a/script/compile_hbs.rb +++ b/script/compile_hbs.rb @@ -2,8 +2,8 @@ ctx = MiniRacer::Context.new(timeout: 15000) ctx.eval("var self = this; #{File.read("#{Rails.root}/vendor/assets/javascripts/babel.js")}") ctx.eval(File.read(Ember::Source.bundled_path_for('ember-template-compiler.js'))) ctx.eval("module = {}; exports = {};"); -ctx.attach("rails.logger.info", proc{|err| puts(">> #{err.to_s}")}) -ctx.attach("rails.logger.error", proc{|err| puts(">> #{err.to_s}")}) +ctx.attach("rails.logger.info", proc { |err| puts(">> #{err.to_s}") }) +ctx.attach("rails.logger.error", proc { |err| puts(">> #{err.to_s}") }) ctx.eval < Date: Sat, 2 Sep 2017 02:30:47 -0700 Subject: [PATCH 016/159] jhead is now in brew (#5128) --- docs/DEVELOPMENT-OSX-NATIVE.md | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/docs/DEVELOPMENT-OSX-NATIVE.md b/docs/DEVELOPMENT-OSX-NATIVE.md index c3ff8b9677..abb88cb2aa 100644 --- a/docs/DEVELOPMENT-OSX-NATIVE.md +++ b/docs/DEVELOPMENT-OSX-NATIVE.md @@ -229,25 +229,16 @@ config.action_mailer.smtp_settings = { address: "localhost", port: 1025 } Set up [MailCatcher](https://github.com/sj26/mailcatcher) so the app can intercept outbound email and you can verify what is being sent. -## Additional Setup Tasks +## Additional Image Tooling In addition to ImageMagick we also need to install some other image related software: ```sh -brew install gifsicle jpegoptim optipng +brew install gifsicle jpegoptim optipng jhead npm install -g svgo ``` -Install jhead - -```sh -curl "http://www.sentex.net/~mwandel/jhead/jhead-2.97.tar.gz" | tar xzf - -cd jhead-2.97 -make -make install -``` - ## Setting up your Discourse ### Check out the repository From ebbdd4fe0f4409fdbee5cc3f19f941addeceebcf Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Sat, 2 Sep 2017 22:22:29 +0530 Subject: [PATCH 017/159] FIX: error when rebaking posts --- .../pretty-text/engines/discourse-markdown/bbcode-block.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 index c2706b7e66..8276d2318a 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 @@ -176,8 +176,9 @@ function findInlineCloseTag(state, openTag, start, max) { closeTag = parseBBCodeTag(state.src, j, max); if (!closeTag || closeTag.tag !== openTag.tag || !closeTag.closing) { closeTag = null; + } else { + closeTag.start = j; } - closeTag.start = j; break; } } From 1043a2e99f28065a83a65b0227d717bac50b4bfd Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 4 Sep 2017 10:47:25 +0800 Subject: [PATCH 018/159] Run specs for `discourse-chat-integration` as well. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 791ff459c5..4438315558 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,6 +43,7 @@ before_install: - git clone --depth=1 https://github.com/discourse/discourse-cakeday.git plugins/discourse-cakeday - git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies - git clone --depth=1 https://github.com/discourse/discourse-slack-official.git plugins/discourse-slack-official + - git clone --depth=1 https://github.com/discourse/discourse-chat-integration.git plugins/discourse-chat-integration install: - bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails seed-fu; fi" From 5c1143cd55a95172357466fcebaeb6dfc7912636 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 4 Sep 2017 16:36:02 +0800 Subject: [PATCH 019/159] Add missing test case for `PostController#timings`. --- app/controllers/topics_controller.rb | 3 ++- spec/requests/topics_controller_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 996dab4d26..15bcbb2336 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -588,9 +588,10 @@ class TopicsController < ApplicationController current_user, params[:topic_id].to_i, params[:topic_time].to_i, - (params[:timings] || []).map { |post_number, t| [post_number.to_i, t.to_i] }, + (params[:timings] || {}).map { |post_number, t| [post_number.to_i, t.to_i] }, mobile: view_context.mobile_view? ) + render nothing: true end diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb index 6da482ec63..d2e1990e3c 100644 --- a/spec/requests/topics_controller_spec.rb +++ b/spec/requests/topics_controller_spec.rb @@ -4,6 +4,27 @@ RSpec.describe TopicsController do let(:topic) { Fabricate(:topic) } let(:user) { Fabricate(:user) } + describe '#timings' do + let(:post_1) { Fabricate(:post, topic: topic) } + + it 'should record the timing' do + sign_in(user) + + post "/topics/timings.json", + topic_id: topic.id, + topic_time: 5, + timings: { post_1.post_number => 2 } + + expect(response).to be_success + + post_timing = PostTiming.first + + expect(post_timing.topic).to eq(topic) + expect(post_timing.user).to eq(user) + expect(post_timing.msecs).to eq(2) + end + end + describe '#timer' do context 'when a user is not logged in' do it 'should return the right response' do From 90c14106fad14dbae7bb4e88cbcf836ec9afe62f Mon Sep 17 00:00:00 2001 From: Quangbuu Le Date: Mon, 4 Sep 2017 16:04:54 +0700 Subject: [PATCH 020/159] Enhance BulkImport pre_cook (#5015) * Enhance BulkImport pre_cook * BulkImport: Trim
    at begining and ending [quote][quote/] --- script/bulk_import/base.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/script/bulk_import/base.rb b/script/bulk_import/base.rb index 6ef8e3b011..66f61df4e0 100644 --- a/script/bulk_import/base.rb +++ b/script/bulk_import/base.rb @@ -18,7 +18,8 @@ class BulkImport::Base @raw_connection = PG.connect(dbname: db[:database], host: db[:host_names]&.first, port: db[:port]) @markdown = Redcarpet::Markdown.new( - Redcarpet::Render::HTML, + Redcarpet::Render::HTML.new(hard_wrap: true), + no_intra_emphasis: true, fenced_code_blocks: true, autolink: true ) @@ -579,8 +580,11 @@ class BulkImport::Base cooked = @markdown.render(cooked).scrub.strip cooked.gsub!(/\[QUOTE="?([^,"]+)(?:, post:(\d+), topic:(\d+))?"?\](.+?)\[\/QUOTE\]/im) do - username, post_id, topic_id = $1, $2, $3 - quote = @markdown.render($4.presence || "").scrub.strip + username, post_id, topic_id, quote = $1, $2, $3, $4 + + quote = quote.scrub.strip + quote.gsub!(/^(
    \n?)+/, "") + quote.gsub!(/(
    \n?)+$/, "") if post_id.present? && topic_id.present? <<-HTML From 3b7128102c12d72d0303c83e4f113aa06c23655c Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 4 Sep 2017 16:20:41 +0530 Subject: [PATCH 021/159] FIX: disable follow in topic summary links https://meta.discourse.org/t/links-in-popular-links-appear-without-nofollow/68974 --- app/assets/javascripts/discourse/widgets/topic-map.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 index 0564351b56..6cfd0f55f0 100644 --- a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 @@ -136,7 +136,8 @@ createWidget('topic-map-link', { target: "_blank", 'data-user-id': attrs.user_id, 'data-ignore-post-id': 'true', - title: attrs.url }; + title: attrs.url, + rel: 'nofollow noopener' }; }, html(attrs) { From caedefd67569144841b0bea76c62436c13efa590 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 4 Sep 2017 13:27:58 +0200 Subject: [PATCH 022/159] FIX: correctly resets user_themes template This commit adds tests for this behaviour and also adds support for reseting cache when updating a theme name and destroying a theme. --- app/models/theme.rb | 5 +++-- spec/models/theme_spec.rb | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/app/models/theme.rb b/app/models/theme.rb index f38668e1fa..2906262eaa 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -28,7 +28,7 @@ class Theme < ActiveRecord::Base changed_fields.each(&:save!) changed_fields.clear - Theme.expire_site_cache! if user_selectable_changed? + Theme.expire_site_cache! if user_selectable_changed? || name_changed? @dependant_themes = nil @included_themes = nil @@ -46,7 +46,6 @@ class Theme < ActiveRecord::Base end if self.id - ColorScheme .where(theme_id: self.id) .where("id NOT IN (SELECT color_scheme_id FROM themes where color_scheme_id IS NOT NULL)") @@ -56,6 +55,8 @@ class Theme < ActiveRecord::Base .where(theme_id: self.id) .update_all(theme_id: nil) end + + Theme.expire_site_cache! end after_commit ->(theme) do diff --git a/spec/models/theme_spec.rb b/spec/models/theme_spec.rb index 43e1a91fda..5ff7368462 100644 --- a/spec/models/theme_spec.rb +++ b/spec/models/theme_spec.rb @@ -10,6 +10,10 @@ describe Theme do Fabricate(:user) end + let(:guardian) do + Guardian.new(user) + end + let :customization_params do { name: 'my name', user_id: user.id, header: "my awesome header" } end @@ -214,4 +218,32 @@ HTML expect(Theme.user_theme_keys).to eq(Set.new([])) end + it 'correctly caches user_themes template' do + Theme.destroy_all + + json = Site.json_for(guardian) + user_themes = JSON.parse(json)["user_themes"] + expect(user_themes).to eq([]) + + theme = Theme.create!(name: "bob", user_id: -1, user_selectable: true) + theme.save! + + json = Site.json_for(guardian) + user_themes = JSON.parse(json)["user_themes"].map { |t| t["name"] } + expect(user_themes).to eq(["bob"]) + + theme.name = "sam" + theme.save! + + json = Site.json_for(guardian) + user_themes = JSON.parse(json)["user_themes"].map { |t| t["name"] } + expect(user_themes).to eq(["sam"]) + + Theme.destroy_all + + json = Site.json_for(guardian) + user_themes = JSON.parse(json)["user_themes"] + expect(user_themes).to eq([]) + end + end From afe6f46b03bc4c955a85408acce96a1e2f35df5b Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Mon, 4 Sep 2017 13:40:11 +0200 Subject: [PATCH 023/159] merges after_save --- app/models/theme.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/models/theme.rb b/app/models/theme.rb index 2906262eaa..b9bf76c53d 100644 --- a/app/models/theme.rb +++ b/app/models/theme.rb @@ -32,9 +32,7 @@ class Theme < ActiveRecord::Base @dependant_themes = nil @included_themes = nil - end - after_save do remove_from_cache! notify_scheme_change if color_scheme_id_changed? end From bb098af38ee0900b838d5527fe29db6f8a42b4c3 Mon Sep 17 00:00:00 2001 From: tophee Date: Mon, 4 Sep 2017 15:32:04 +0200 Subject: [PATCH 024/159] Update "email in" help text https://meta.discourse.org/t/straightforward-direct-delivery-incoming-mail/49487/98?u=tophee --- 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 131a69c221..cb59c843c0 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1372,7 +1372,7 @@ en: pop3_polling_username: "The username for the POP3 account to poll for email." pop3_polling_password: "The password for the POP3 account to poll for email." log_mail_processing_failures: "Log all email processing failures to http://yoursitename.com/logs" - email_in: "Allow users to post new topics via email (requires pop3 polling). Configure the addresses in the \"Settings\" tab of each category." + email_in: "Allow users to post new topics via email (requires manual or pop3 polling). Configure the addresses in the \"Settings\" tab of each category." email_in_min_trust: "The minimum trust level a user needs to have to be allowed to post new topics via email." email_prefix: "The [label] used in the subject of emails. It will default to 'title' if not set." email_site_title: "The title of the site used as the sender of emails from the site. Default to 'title' if not set. If your 'title' contains characters that are not allowed in email sender strings, use this setting." From be1cce503c3576426ac9759d88fdcab4c0b87be2 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 4 Sep 2017 11:48:36 -0400 Subject: [PATCH 025/159] FIX: Don't bind events in `defaultState` --- .../discourse/components/site-header.js.es6 | 2 ++ .../discourse/widgets/search-menu-controls.js.es6 | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/components/site-header.js.es6 b/app/assets/javascripts/discourse/components/site-header.js.es6 index 9285f5355b..e071b9eb74 100644 --- a/app/assets/javascripts/discourse/components/site-header.js.es6 +++ b/app/assets/javascripts/discourse/components/site-header.js.es6 @@ -62,6 +62,8 @@ const SiteHeaderComponent = MountWidget.extend(Docking, { this.dispatch('notifications:changed', 'user-notifications'); this.dispatch('header:keyboard-trigger', 'header'); + this.dispatch('header:keyboard-trigger', 'header'); + this.dispatch('search-autocomplete:after-complete', 'search-term'); this.appEvents.on('dom:clean', () => { // For performance, only trigger a re-render if any menu panels are visible diff --git a/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 index 689e749ee8..c91a9ccb07 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 @@ -5,16 +5,16 @@ import { createWidget } from 'discourse/widgets/widget'; createWidget('search-term', { tagName: 'input', buildId: () => 'search-term', - buildKey: (attrs) => `search-term-${attrs.id}`, + buildKey: () => `search-term`, defaultState() { - this.appEvents.on("search-autocomplete:after-complete", () => { - this.state.afterAutocomplete = true; - }); - return { afterAutocomplete: false }; }, + searchAutocompleteAfterComplete() { + this.state.afterAutocomplete = true; + }, + buildAttributes(attrs) { return { type: 'text', value: attrs.value || '', From 153eca23e3bb90f5ae544fc6d056bd16c65f0176 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 4 Sep 2017 12:14:34 -0400 Subject: [PATCH 026/159] Switch Development Database via ENV var This is useful if you use multiple development databases locally and don't want to constantly `db:drop db:create` into `discourse_development`. Simply add `DISCOURSE_DEV_DB=whatever_db` as an ENV variable and Discourse will use it in development mode. --- config/database.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/database.yml b/config/database.yml index ff68ec1a85..c157512262 100644 --- a/config/database.yml +++ b/config/database.yml @@ -1,7 +1,7 @@ development: prepared_statements: false adapter: postgresql - database: discourse_development + database: <%= ENV['DISCOURSE_DEV_DB'] || 'discourse_development' %> min_messages: warning pool: 5 timeout: 5000 From db929e58fc02923ddc2d09add5103aaba73c027f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 4 Sep 2017 12:55:23 -0400 Subject: [PATCH 027/159] FIX: Don't allow staff to approve users with unverified emails --- app/assets/javascripts/admin/templates/user-index.hbs | 4 ++-- lib/admin_user_index_query.rb | 2 +- lib/guardian.rb | 2 +- spec/components/admin_user_index_query_spec.rb | 8 +++++--- spec/components/guardian_spec.rb | 5 +++++ 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index 7a016f150f..da712cd51e 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -466,7 +466,7 @@ {{/if}}
    -
    +
    {{#unless model.anonymizeForbidden}} {{d-button label="admin.user.anonymize" @@ -487,7 +487,7 @@ {{#if model.deleteExplanation}}
    -
    +
    {{d-icon "exclamation-triangle"}} {{model.deleteExplanation}}
    diff --git a/lib/admin_user_index_query.rb b/lib/admin_user_index_query.rb index f333f5619b..eb36e4a25d 100644 --- a/lib/admin_user_index_query.rb +++ b/lib/admin_user_index_query.rb @@ -95,7 +95,7 @@ class AdminUserIndexQuery when 'moderators' then @query.where(moderator: true) when 'blocked' then @query.blocked when 'suspended' then @query.suspended - when 'pending' then @query.not_suspended.where(approved: false) + when 'pending' then @query.not_suspended.where(approved: false, active: true) when 'suspect' then suspect_users end end diff --git a/lib/guardian.rb b/lib/guardian.rb index a09d11ae1d..773dfcb0da 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -176,7 +176,7 @@ class Guardian # Can we approve it? def can_approve?(target) - is_staff? && target && not(target.approved?) + is_staff? && target && target.active? && not(target.approved?) end def can_activate?(target) diff --git a/spec/components/admin_user_index_query_spec.rb b/spec/components/admin_user_index_query_spec.rb index 913e997d0c..f885350202 100644 --- a/spec/components/admin_user_index_query_spec.rb +++ b/spec/components/admin_user_index_query_spec.rb @@ -100,18 +100,20 @@ describe AdminUserIndexQuery do describe "with a pending user" do - let!(:user) { Fabricate(:user, approved: false) } + let!(:user) { Fabricate(:user, active: true, approved: false) } + let!(:inactive_user) { Fabricate(:user, approved: false, active: false) } it "finds the unapproved user" do query = ::AdminUserIndexQuery.new(query: 'pending') - expect(query.find_users.count).to eq(1) + expect(query.find_users).to include(user) + expect(query.find_users).not_to include(inactive_user) end context 'and a suspended pending user' do let!(:suspended_user) { Fabricate(:user, approved: false, suspended_at: 1.hour.ago, suspended_till: 20.years.from_now) } it "doesn't return the suspended user" do query = ::AdminUserIndexQuery.new(query: 'pending') - expect(query.find_users.count).to eq(1) + expect(query.find_users).not_to include(suspended_user) end end diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb index dcc5f22937..87a3413c4f 100644 --- a/spec/components/guardian_spec.rb +++ b/spec/components/guardian_spec.rb @@ -1653,6 +1653,11 @@ describe Guardian do expect(Guardian.new(admin).can_approve?(user)).to be_falsey end + it "returns false when the user is not active" do + user.active = false + expect(Guardian.new(admin).can_approve?(user)).to be_falsey + end + it "allows an admin to approve a user" do expect(Guardian.new(admin).can_approve?(user)).to be_truthy end From fd91e53e684c7daa8cbd6092111825ea90a5a7a8 Mon Sep 17 00:00:00 2001 From: minusfive Date: Fri, 1 Sep 2017 18:59:50 -0400 Subject: [PATCH 028/159] Cleaned, de-duplicated/DRYed and organized discourse.css (common, mobile and desktop) --- .../stylesheets/common/base/discourse.scss | 459 ++++++---- app/assets/stylesheets/desktop/discourse.scss | 826 ++++++++++-------- app/assets/stylesheets/mobile/discourse.scss | 260 +++--- 3 files changed, 819 insertions(+), 726 deletions(-) diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 02709d7da8..2cca6788a5 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -1,45 +1,39 @@ -img.avatar { - border-radius: 50%; -} +// Common +// global styles that apply to the Discourse application specifically +// BEWARE: changing these styles implies they take effect anywhere they are seen +// throughout the Discourse application -.container { - @extend .clearfix; -} - -.wrap { - @extend .clearfix; - margin-right: auto; - margin-left: auto; - padding: 0 8px; - .contents { - position: relative; +// Animation Keyframes +@keyframes ping { + from { + transform: scale(0.25); + opacity: 1; + } + to { + transform: scale(2); + opacity: 0; } } -.full-width { - margin-left: 12px; +@keyframes rotate-forever { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } } -big { - font-size: 28px; -} - -small { - font-size: 9px; -} - -//setting a static limit on big and small prevents nesting abuse - - -blockquote { - @include post-aside; - clear: both; -} - -a.no-href { - cursor: pointer; +@keyframes background-fade-highlight { + 0% { + background-color: $tertiary-low; + } + 100% { + background-color: transparent; + } } +// Base Elements html { height: 100%; } @@ -52,56 +46,170 @@ body { @include clearfix; } -h1, h2, h3, h4, h5, h6 { +big { + font-size: 28px; +} + +small { + font-size: 9px; +} + +//setting a static limit on big and small prevents nesting abuse +blockquote { + @include post-aside; + clear: both; +} + +h1, +h2, +h3, +h4, +h5, +h6 { margin-top: 0; margin-bottom: .5rem; } -button.ok { - background: $success; - color: $secondary; - @include hover { - background: lighten($success, 10%); +button { + &.ok { + background: $success; color: $secondary; + + @include hover { + background: lighten($success, 10%); + color: $secondary; + } + } + + &.cancel { + background: $danger; + color: $secondary; + + @include hover { + background: lighten($danger, 10%); + color: $secondary; + } } } -button.cancel { - background: $danger; - color: $secondary; - @include hover { - background: lighten($danger, 10%); - color: $secondary; +ul.breadcrumb { + margin: 0 10px 0 10px; +} + +a.no-href { + cursor: pointer; +} + +img.avatar { + border-radius: 50%; +} + +// don't wrap relative dates; we want Jul 26, '15, not: Jul +// 26, +// '15 +span.relative-date { + white-space:nowrap; +} + +label { + display: block; + margin-bottom: 5px; +} + +input { + &[type="radio"], + &[type="checkbox"] { + margin: 3px 0; + line-height: normal; + cursor: pointer; } + &[type="submit"], + &[type="reset"], + &[type="button"], + &[type="radio"], + &[type="checkbox"] { + width: auto; + } + + &.invalid { + background-color: dark-light-choose(scale-color($danger, $lightness: 80%), scale-color($danger, $lightness: -60%)); + } + + .radio &[type="radio"], + .checkbox &[type="checkbox"] { + float: left; + margin-left: -18px; + } +} + +// Common Classes +.radio, +.checkbox { + min-height: 18px; + padding-left: 18px; + + .controls > &:first-child { + padding-top: 5px; + } + + &.inline { + display: inline-block; + padding-top: 5px; + margin-bottom: 0; + vertical-align: middle; + } +} + +.radio.inline .radio.inline, +.checkbox.inline .checkbox.inline { + margin-left: 10px; +} + +.container { + @extend .clearfix; +} + +.wrap { + @extend .clearfix; + margin-right: auto; + margin-left: auto; + padding: 0 8px; + + .contents { + position: relative; + } +} + +.boxed { + &.white { + background-color: $secondary; + } +} + +.full-width { + margin-left: 12px; } // the default for table cells in topic list // is scale-color($primary, $lightness: 50%) // numbers get dimmer as they get colder -.coldmap-high { - color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)) !important; -} -.coldmap-med { - color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)) !important; -} -.coldmap-low { - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)) !important; +.coldmap { + &-high { + color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)) !important; + } + + &-med { + color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)) !important; + } + + &-low { + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)) !important; + } } -#loading-message { - position: absolute; - font-size: 2.143em; - text-align: center; - top: 120px; - left: 500px; - color: $primary; -} - .top-space { +.top-space { margin-top: 10px; } -ul.breadcrumb { - margin: 0 10px 0 10px; -} .message { @include border-radius-all(8px); @@ -113,18 +221,6 @@ ul.breadcrumb { } } -#footer { - .container { - height: 50px; - .contents { - padding-top: 10px; - a[href] { - color: $secondary; - } - } - } -} - .clear-transitions { transition:none !important; } @@ -139,10 +235,6 @@ ul.breadcrumb { } } -input[type].invalid { - background-color: dark-light-choose(scale-color($danger, $lightness: 80%), scale-color($danger, $lightness: -60%)); -} - .d-editor-input { resize: none; } @@ -157,43 +249,6 @@ input[type].invalid { top: 60px !important; } -label { - display: block; - margin-bottom: 5px; -} -input { - &[type="radio"], &[type="checkbox"] { - margin: 3px 0; - line-height: normal; - cursor: pointer; - } - &[type="submit"], &[type="reset"], &[type="button"], &[type="radio"], &[type="checkbox"] { - width: auto; - } -} -.radio, .checkbox { - min-height: 18px; - padding-left: 18px; -} -.radio input[type="radio"], .checkbox input[type="checkbox"] { - float: left; - margin-left: -18px; -} -.controls > { - .radio:first-child, .checkbox:first-child { - padding-top: 5px; - } -} -.radio.inline, .checkbox.inline { - display: inline-block; - padding-top: 5px; - margin-bottom: 0; - vertical-align: middle; -} -.radio.inline .radio.inline, .checkbox.inline .checkbox.inline { - margin-left: 10px; -} - .flex-center-align { display: flex; align-items: center; @@ -202,7 +257,10 @@ input { .unread-private-messages { color: $secondary; background: $success; - &.badge-notification[href] {color: $secondary;} + + &.badge-notification[href] { + color: $secondary; + } } .ring-backdrop-spotlight { @@ -256,50 +314,12 @@ input { -webkit-animation-name: ping; } -@-webkit-keyframes ping { - from { - $scale: 0.25; - transform: scale($scale); - -ms-transform: scale($scale); - -webkit-transform: scale($scale); - -o-transform: scale($scale); - -moz-transform: scale($scale); - opacity: 1; - } - to { - $scale: 2; - transform: scale($scale); - -ms-transform: scale($scale); - -webkit-transform: scale($scale); - -o-transform: scale($scale); - -moz-transform: scale($scale); - opacity: 0; - } -} - .fade { opacity: 0; transition: opacity 0.15s linear; -} -.fade.in { - opacity: 1; -} - -@-webkit-keyframes rotate-forever { - 0% { - -webkit-transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(360deg); - } -} -@keyframes rotate-forever { - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); + &.in { + opacity: 1; } } @@ -328,7 +348,6 @@ input { } .content-list { - h3 { color: $primary-medium; font-size: 1.071em; @@ -340,50 +359,106 @@ input { list-style: none; margin: 0; - li:first-of-type { - border-top: 1px solid $primary-low; - } li { border-bottom: 1px solid $primary-low; - } - li a { - display: block; - padding: 10px; - color: $primary; - - &:hover { - background-color: $primary-low; - color: $primary; + &:first-of-type { + border-top: 1px solid $primary-low; } - &.active { - font-weight: bold; + a { + display: block; + padding: 10px; color: $primary; + + &:hover { + background-color: $primary-low; + color: $primary; + } + + &.active { + font-weight: bold; + color: $primary; + } } } } } -// don't wrap relative dates, we want -// -// Jul 26, '15 -// -// not -// -// Jul -// 26, -// '15 -// -span.relative-date { - white-space:nowrap; +.form-vertical { + input, + textarea, + select, + .input-prepend, + .input-append { + display: inline-block; + margin-bottom: 0; + } + + .control-group { + @include clearfix; + } + + .control-label { + font-weight: bold; + font-size: 1.2em; + line-height: 2; + } + + .controls { + margin-left: 0; + } } -@keyframes background-fade-highlight { - 0% { - background-color: $tertiary-low; +// Special elements +// Special elements +#main { + img.avatar { + &.header { + width: 45px; + height: 45px; + } + + &.medium { + width: 32px; + height: 32px; + } + + &.small { + width: 25px; + height: 25px; + } + + &.tiny { + width: 20px; + height: 20px; + } } - 100% { - background-color: transparent; + + .user-list { + .user { + padding-bottom: 5px; + } + } +} + +#loading-message { + position: absolute; + font-size: 2.143em; + text-align: center; + top: 120px; + left: 500px; + color: $primary; +} + +#footer { + .container { + height: 50px; + .contents { + padding-top: 10px; + a[href] { + color: $secondary; + } + } } } diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index aa120ec8a9..268ee3beae 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -1,96 +1,43 @@ +// Desktop // global styles that apply to the Discourse application specifically // BEWARE: changing these styles implies they take effect anywhere they are seen // throughout the Discourse application -@media all -and (max-width : 570px) { - body { - min-width: 0; - } - .wrap, - .full-width { - min-width: 0; - } +// Base Elements +body.widget-dragging { + cursor: ns-resize; } header { margin-bottom: 15px; } -body.widget-dragging { - cursor: ns-resize; +// Common classes +.boxed { + height: 100%; } -body { +.grippie { + cursor: row-resize; + padding: 4px 0px; - .boxed { - height: 100%; - &.white { - background-color: $secondary; - } - } - #main { - a.star { - color: $secondary-medium; - &:before { - font-family: "FontAwesome"; - content: "\f005"; - } - @include hover { - opacity: 0.6; - } - - &:active { - opacity: 1; - } - } - img.avatar { - &.header { - width: 45px; - height: 45px; - } - &.medium { - width: 32px; - height: 32px; - } - &.small { - width: 25px; - height: 25px; - } - &.tiny { - width: 20px; - height: 20px; - } - } - .user-list { - .user { - padding-bottom: 5px; - } - } - } - - $grippie-border-color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 50%)); - - .grippie { - cursor: row-resize; - padding: 4px 0px; - } - - .grippie:before { + &:before { content: ''; display: block; width: 27px; margin: auto; - border-top: 3px double $grippie-border-color; + border-top: 3px double dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 50%)); } } .topic-statuses { float: left; padding: 0; + .topic-status { padding: 0 2px 0 0; margin: 0; + i { font-size: 1.071em; } @@ -101,406 +48,515 @@ body { } } -body { - .form-vertical input, .form-vertical textarea, .form-vertical select, .form-vertical .input-prepend, .form-vertical .input-append { - display: inline-block; - margin-bottom: 0; - } - .form-vertical { - .control-group { - margin-bottom: 24px; - &:before { - display: table; - content: ""; - } - &:after { - display: table; - content: ""; - clear: both; - } - } - .control-label { - font-weight: bold; - font-size: 1.2em; - line-height: 2; - } - .controls { - margin-left: 0; - } +.form-vertical { + .control-group { + margin-bottom: 24px; } } + +/***********************/ /* bootstrap carryover */ +/***********************/ +code, +pre { + font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace; +} +/* page not found styles */ +h1.page-not-found { + line-height: 30px; +} -body { +.page-not-found { + margin: 20px 0 40px 0; - input, textarea, select { - color: $primary; - } - - code, pre { - font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace; - } - - /* page not found styles */ - h1.page-not-found { - line-height: 30px; - } - - .page-not-found { - margin: 20px 0 40px 0; - } - - .page-not-found-search { + &-search { margin-top: 20px; } - .popular-topics-title, .recent-topics-title { - margin-bottom: 10px; - } - - .not-found-topic { - > a { margin-right: 10px; line-height: 2;} - } - - .page-not-found-topics .span8 { + &-topics .span8 { line-height: 1.5em; margin-right: 20px; } +} - // this removes the unwanted top margin on a paragraph under a heading - h1+p, h2+p, h3+p, h4+p, h5+p, h6+p { +.popular-topics-title, +.recent-topics-title { + margin-bottom: 10px; +} + +.not-found-topic { + > a { + margin-right: 10px; + line-height: 2; + } +} + +// this removes the unwanted top margin on a paragraph under a heading +h1, +h2, +h3, +h4, +h5, +h6 { + + p { margin-top:0px; } +} +form { + margin: 0 0 18px; +} - form { - margin: 0 0 18px; +label, +input, +button, +select, +textarea { + font-size: 0.929em; + font-weight: normal; + line-height: 18px; +} + +input, +button, +select, +textarea { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +input, +select, +textarea { + color: $primary; + + &[class*="span"] { + float: none; + margin-left: 0; } - label, input, button, select, textarea { - font-size: 0.929em; - font-weight: normal; - line-height: 18px; + + &[disabled], + &[readonly] { + cursor: not-allowed; + background-color: $primary-low; + border-color: $primary-low; } - input, button, select, textarea { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + + &:focus:required:invalid { + color: $danger; + border-color: $danger; } - select, textarea { + + &:focus:required:invalid:focus { + border-color: $danger; + box-shadow: 0 0 6px $danger; + } +} + +select, +textarea { + display: inline-block; + padding: 4px; + margin-bottom: 9px; + font-size: 0.929em; + line-height: 18px; + color: $primary; +} + +input { + width: 210px; + + &[type="text"], + &[type="password"], + &[type="datetime"], + &[type="datetime-local"], + &[type="date"], + &[type="month"], + &[type="time"], + &[type="week"], + &[type="number"], + &[type="email"], + &[type="url"], + &[type="search"], + &[type="tel"], + &[type="color"] { display: inline-block; + height: 18px; padding: 4px; margin-bottom: 9px; font-size: 0.929em; line-height: 18px; color: $primary; - } - input { - &[type="text"], &[type="password"], &[type="datetime"], &[type="datetime-local"], &[type="date"], &[type="month"], &[type="time"], &[type="week"], &[type="number"], &[type="email"], &[type="url"], &[type="search"], &[type="tel"], &[type="color"] { - display: inline-block; - height: 18px; - padding: 4px; - margin-bottom: 9px; - font-size: 0.929em; - line-height: 18px; - color: $primary; - } - } - input { - width: 210px; - } - textarea { - width: 210px; - height: auto; - background-color:$secondary; - border: 1px solid $primary-low; + background-color: $secondary; + border: 1px solid $primary-low; border-radius: 3px; box-shadow: inset 0 1px 1px rgba(0,0,0, .3); - } - input { - &[type="text"], &[type="password"], &[type="datetime"], &[type="datetime-local"], &[type="date"], &[type="month"], &[type="time"], &[type="week"], &[type="number"], &[type="email"], &[type="url"], &[type="search"], &[type="tel"], &[type="color"] { - background-color: $secondary; - border: 1px solid $primary-low; - border-radius: 3px; - box-shadow: inset 0 1px 1px rgba(0,0,0, .3); - } - } - textarea:focus { - border-color: $tertiary; - outline: 0; - box-shadow: inset 0 1px 1px rgba(0,0,0, .3), 0 0 8px $tertiary; - } - input { - &[type="text"]:focus, &[type="password"]:focus, &[type="datetime"]:focus, &[type="datetime-local"]:focus, &[type="date"]:focus, &[type="month"]:focus, &[type="time"]:focus, &[type="week"]:focus, &[type="number"]:focus, &[type="email"]:focus, &[type="url"]:focus, &[type="search"]:focus, &[type="tel"]:focus, &[type="color"]:focus { + + &:focus { border-color: $tertiary; outline: 0; box-shadow: inset 0 1px 1px rgba(0,0,0, .3), 0 0 8px $tertiary; } } +} - select, input[type="file"] { - line-height: 28px; - } +textarea { + width: 210px; + height: auto; + background-color:$secondary; + border: 1px solid $primary-low; + border-radius: 3px; + box-shadow: inset 0 1px 1px rgba(0,0,0, .3); - select { - width: 220px; - border: 1px solid $primary-low; - &[multiple], &[size] { - height: auto; - } + &:focus { + border-color: $tertiary; + outline: 0; + box-shadow: inset 0 1px 1px rgba(0,0,0, .3), 0 0 8px $tertiary; } +} - .input-xxlarge { - width: 530px; +select, +input[type="file"] { + line-height: 28px; +} + +select { + width: 220px; + border: 1px solid $primary-low; + + &[multiple], + &[size] { + height: auto; } - input[class*="span"], select[class*="span"], textarea[class*="span"] { - float: none; - margin-left: 0; - } - .input-append { - input[class*="span"] { - display: inline-block; - } - } - .input-prepend { - input[class*="span"] { - display: inline-block; - } - } - input, textarea { - margin-left: 0; - } - input[disabled], select[disabled], textarea[disabled], input[readonly], select[readonly], textarea[readonly] { - cursor: not-allowed; - background-color: $primary-low; - border-color: $primary-low; - } - input { - &[type="radio"][disabled], &[type="checkbox"][disabled], &[type="radio"][readonly], &[type="checkbox"][readonly] { +} + +input, +textarea { + margin-left: 0; +} + +input { + &[type="radio"], + &[type="checkbox"] { + &[disabled], + &[readonly] { background-color: transparent; } } - .controls-dropdown { - margin-bottom: 10px; +} + +.input { + &-xxlarge { + width: 530px; } - .control-group { - margin-bottom: 9px; - &.warning { - > label { - color: $danger; + + &-prepend, + &-append { + margin-bottom: 5px; + + input[class*="span"] { + display: inline-block; + } + + input, + select { + position: relative; + margin-bottom: 0; + vertical-align: middle; + border-radius: 0 3px 3px 0; + + &:focus { + z-index: 2; } - .checkbox, .radio, input, select, textarea { - color: $danger; - border-color: $danger; + } + + .add-on { + display: inline-block; + width: auto; + height: 18px; + min-width: 16px; + padding: 4px 5px; + font-weight: normal; + line-height: 18px; + text-align: center; + vertical-align: middle; + background-color: $secondary; + border: 1px solid $primary-low; + } + + .add-on, + .btn { + margin-left: -1px; + border-radius: 0; + } + + .active { + background-color: scale-color($danger, $lightness: 30%); + border-color: $danger; + } + } + + &-prepend { + .add-on, + .btn { + margin-right: -1px; + + &:first-child { + border-radius: 3px 0 0 3px; } - .checkbox:focus, .radio:focus, input:focus, select:focus, textarea:focus { + } + } + + &-append { + input, + select { + border-radius: 3px 0 0 3px; + } + + .add-on, + .btn { + &:last-child { + border-radius: 0 3px 3px 0; + } + } + } +} + +.input-prepend.input-append { + input, + select { + border-radius: 0; + } + + .add-on, + .btn { + &:first-child { + margin-right: -1px; + border-radius: 3px 0 0 3px; + } + } + + .add-on, + .btn { + &:last-child { + margin-left: -1px; + border-radius: 0 3px 3px 0; + } + } +} + +.controls-dropdown { + margin-bottom: 10px; +} + +.control-group { + margin-bottom: 9px; + + &.warning, + &.error { + > label { + color: $danger; + } + + .checkbox, + .radio, + input, + select, + textarea { + color: $danger; + border-color: $danger; + + &:focus { border-color: scale-color($danger, $lightness: -30%); box-shadow: 0 0 6px $danger; } - .input-prepend .add-on, .input-append .add-on { + } + + } + + &.warning { + .input-prepend, + .input-append { + .add-on { color: $danger; background-color: $danger; border-color: $danger; } } - &.error { - > label { - color: $danger; - } - .checkbox, .radio, input, select, textarea { - color: $danger; - border-color: $danger; - } - .checkbox:focus, .radio:focus, input:focus, select:focus, textarea:focus { - border-color: scale-color($danger, $lightness: -30%); - box-shadow: 0 0 6px $danger; - } - .input-prepend .add-on, .input-append .add-on { + } + + &.error { + .input-prepend, + .input-append { + .add-on { color: $danger; background-color: scale-color($danger, $lightness: 30%); border-color: scale-color($danger, $lightness: -20%); } } - &.success { - > label { - color: $success; - } - .checkbox, .radio, input, select, textarea { - color: $success; - border-color: $success; - } - .checkbox:focus, .radio:focus, input:focus, select:focus, textarea:focus { + } + + &.success { + > label { + color: $success; + } + + .checkbox, + .radio, + input, + select, + textarea { + color: $success; + border-color: $success; + + &:focus { border-color: $success; box-shadow: 0 0 6px $success; } - .input-prepend .add-on, .input-append .add-on { + } + + .input-prepend, + .input-append { + .add-on { color: $success; background-color: scale-color($success, $lightness: 90%); border-color: $success; } } } - input:focus:required:invalid, textarea:focus:required:invalid, select:focus:required:invalid { - color: $danger; - border-color: $danger; - } - input:focus:required:invalid:focus, textarea:focus:required:invalid:focus, select:focus:required:invalid:focus { - border-color: $danger; - box-shadow: 0 0 6px $danger; - } +} - .input-prepend, .input-append { - margin-bottom: 5px; - } - .input-prepend input, .input-append input, .input-prepend select, .input-append select { - position: relative; - margin-bottom: 0; - vertical-align: middle; - border-radius: 0 3px 3px 0; - } - .input-prepend input:focus, .input-append input:focus, .input-prepend select:focus, .input-append select:focus { - z-index: 2; - } - .input-prepend .add-on, .input-append .add-on { - display: inline-block; - width: auto; - height: 18px; - min-width: 16px; - padding: 4px 5px; - font-weight: normal; - line-height: 18px; - text-align: center; - vertical-align: middle; - background-color: $secondary; - border: 1px solid $primary-low; - } - .input-prepend .add-on, .input-append .add-on, .input-prepend .btn, .input-append .btn { - margin-left: -1px; - border-radius: 0; - } - .input-prepend .active, .input-append .active { - background-color: scale-color($danger, $lightness: 30%); - border-color: $danger; - } - .input-prepend { - .add-on, .btn { - margin-right: -1px; - } - .add-on:first-child, .btn:first-child { - border-radius: 3px 0 0 3px; - } - } - .input-append { - input, select { - border-radius: 3px 0 0 3px; - } - .add-on:last-child, .btn:last-child { - border-radius: 0 3px 3px 0; - } - } - .input-prepend.input-append { - input, select { - border-radius: 0; - } - .add-on:first-child, .btn:first-child { - margin-right: -1px; - border-radius: 3px 0 0 3px; - } - .add-on:last-child, .btn:last-child { - margin-left: -1px; - border-radius: 0 3px 3px 0; - } - } - - .form-horizontal input, .form-horizontal textarea, .form-horizontal select, .form-horizontal .input-prepend, .form-horizontal .input-append { - display: inline-block; - margin-bottom: 0; - } - .form-horizontal .hide { +.form-horizontal { + .hide { display: none; } - .form-horizontal { - .control-group { - margin-bottom: 18px; - &:before { - display: table; - content: ""; - } - &:after { - display: table; - content: ""; - clear: both; - } - } - .control-indent { - margin-left: 20px; - margin-bottom: 10px; - } - .control-label { - float: left; - width: 140px; - text-align: right; - font-weight: bold; - } - .controls { - margin-left: 160px; - } + input, + textarea, + select, + .input-prepend, + .input-append { + display: inline-block; + margin-bottom: 0; } - .bootbox.modal { - .modal-footer { - a.btn-primary { - color: $secondary; - } + .control-group { + @include clearfix; + margin-bottom: 18px; + } + + .control-indent { + margin-left: 20px; + margin-bottom: 10px; + } + + .control-label { + float: left; + width: 140px; + text-align: right; + font-weight: bold; + } + + .controls { + margin-left: 160px; + } +} + +.bootbox.modal { + .modal-footer { + a.btn-primary { + color: $secondary; } } } /* bootstrap columns */ - -.row:before, -.row:after { - display: table; - content: ""; -} -.row:after { - clear: both; +.row { + @include clearfix; } -.span24 { - width: 1236px; - float: left; +.span { + &4 { + width: 196px; + margin-right: 12px; + float: left; + } + + &6 { + width: 27.027%; + float: left; + } + + &8 { + width: 404px; + float: left; + } + + &10 { + width: 508px; + float: left; + } + + &13 { + width: 59.8198%; + float: left; + } + + &15 { + /* intentionally no width set here, do not add one */ + margin-left: 12px; + float: left; + } + + &24 { + width: 1236px; + float: left; + color: amarillo; + } } -.span15 { - /* intentionally no width set here, do not add one */ - margin-left: 12px; - float: left; + +.offset { + &2 { + margin-left: 116px; + } + + &1 { + margin-left: 64px; + } } -.span13 { - width: 59.8198%; - float: left; + +// Special elements +#main { + a.star { + color: $secondary-medium; + + &:before { + font-family: "FontAwesome"; + content: "\f005"; + } + + @include hover { + opacity: 0.6; + } + + &:active { + opacity: 1; + } + } } -.span10 { - width: 508px; - float: left; -} -.span8 { - width: 404px; - float: left; -} -.span6 { - width: 27.027%; - float: left; -} -.span4 { - width: 196px; - margin-right: 12px; - float: left; -} -.offset2 { - margin-left: 116px; -} -.offset1 { - margin-left: 64px; + +// Media Queries +@media all +and (max-width : 570px) { + + body { + min-width: 0; + } + + .wrap, + .full-width { + min-width: 0; + } } diff --git a/app/assets/stylesheets/mobile/discourse.scss b/app/assets/stylesheets/mobile/discourse.scss index b4337cfe67..fce24121a2 100644 --- a/app/assets/stylesheets/mobile/discourse.scss +++ b/app/assets/stylesheets/mobile/discourse.scss @@ -1,98 +1,75 @@ +// Mobile // global styles that apply to the Discourse application specifically // BEWARE: changing these styles implies they take effect anywhere they are seen // throughout the Discourse application +// Base Elements body { background-color: $secondary; - textarea { - background-color:$secondary; - } } -// This sets the space between the application content and the edge of the -// screen. This value is required in 'mobile/header.scss' to set the position -// of the drop-down menu. -$mobile-wrapper-padding: 10px; -.wrap { - padding: 0 $mobile-wrapper-padding; -} - -body { - - .boxed { - .contents { - padding: 10px 0 0 0; - } - &.white { - background-color: $secondary; - } - } - #main { - position: relative; - img.avatar { - &.header { - width: 45px; - height: 45px; - } - &.medium { - width: 32px; - height: 32px; - } - &.small { - width: 25px; - height: 25px; - } - &.tiny { - width: 20px; - height: 20px; - } - } - .user-list { - .user { - padding-bottom: 5px; - } - } - } - .control-group { - margin-bottom: 9px; - } +textarea { + background-color:$secondary; } blockquote { /* 13px left is intentional here to properly align with post quotes */ padding: 10px 8px 10px 13px; + p { margin: 0 0 10px 0; + + &:last-of-type { + margin-bottom:0; + } } - p:last-of-type { - margin-bottom:0; + +} + +h1.page-not-found { + line-height: 24px; + margin: 5px 0 -5px 0; +} + +h2 { + &.popular-topics-title { + margin-bottom: 6px; + font-size: 1.2em; } + + &.recent-topics-title { + margin-bottom: 6px; + font-size: 1.2em; + } +} + +// Common classes +.wrap { + padding: 0 10px; +} + +.boxed { + .contents { + padding: 10px 0 0 0; + } +} + +.control-group { + margin-bottom: 9px; } .topic-statuses { display: inline-block; + .topic-status { i { color: $secondary-medium; } } -} -.topic-statuses:empty { - display: none; -} -// Styles used before the user is logged into discourse. For example, activating their -// account or changing their email. - -#simple-container { - width: 90%; -} - -// somehow the image logo assumption inherits margins from earlier in the CSS stack -// we must remove margins for text site titles -h2#site-text-logo -{ - margin: 0 0 0 10px; + &:empty { + display: none; + } } // categories should not be bold on mobile; they fight with the topic title too much @@ -101,22 +78,6 @@ h2#site-text-logo } .mobile-view .mobile-nav { - &.messages-nav, &.notifications-nav, &.activity-nav, &.preferences-nav { - position: absolute; - right: 0px; - top: -55px; - } -} - - -.mobile-view .mobile-nav { - a .fa { - margin-right: 8px; - color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - } - a { - color: $primary; - } margin: 0; padding: 0; background: $primary-low; @@ -125,6 +86,24 @@ h2#site-text-logo position: relative; width: 45%; + &.messages-nav, + &.notifications-nav, + &.activity-nav, + &.preferences-nav { + position: absolute; + right: 0px; + top: -55px; + } + + a { + color: $primary; + + .fa { + margin-right: 8px; + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + } + } + > li > a { padding: 8px 10px; height: 100%; @@ -132,6 +111,7 @@ h2#site-text-logo box-sizing: border-box; display: block; } + .d-icon-caret-down { position: absolute; right: 0px; @@ -139,81 +119,63 @@ h2#site-text-logo .drop { display: none; - } - .drop.expanded { - left: 0; - display: block; - position: absolute; - z-index: 10000000; - background-color: $secondary; - width: 100%; - list-style: none; - margin: 0; - padding: 5px; - border: 1px solid #eaeaea; - box-sizing: border-box; - li { - margin: 5px 0; - padding: 0; - a { - height: 100%; - display: block; - padding: 5px 8px; + &.expanded { + left: 0; + display: block; + position: absolute; + z-index: 10000000; + background-color: $secondary; + width: 100%; + list-style: none; + margin: 0; + padding: 5px; + border: 1px solid #eaeaea; + box-sizing: border-box; + + li { + margin: 5px 0; + padding: 0; + + a { + height: 100%; + display: block; + padding: 5px 8px; + } } } } } /* page not found styles */ -h1.page-not-found { - line-height: 24px; - margin: 5px 0 -5px 0; +.page-not-found { + &-topics a.badge-wrapper { + margin-left: 8px; + } + + &-search h2 { + font-size: 1.2em; + } } -.page-not-found-topics a.badge-wrapper { - margin-left: 8px; -} - -h2.popular-topics-title { - margin-bottom: 6px; - font-size: 1.2em; -} - -h2.recent-topics-title { - margin-bottom: 6px; - font-size: 1.2em; -} - -.page-not-found-search h2 { - font-size: 1.2em; -} - - - .form-vertical { - input, textarea, select, .input-prepend, .input-append { - display: inline-block; - margin-bottom: 0; - } - .control-group { margin-bottom: 12px; - &:before { - display: table; - content: ""; - } - &:after { - display: table; - content: ""; - clear: both; - } - } - .control-label { - font-weight: bold; - font-size: 1.2em; - line-height: 2; - } - .controls { - margin-left: 0; } } + +// Special elements +#main { + position: relative; +} + +// Styles used before the user is logged into discourse. For example, activating +// their account or changing their email. +#simple-container { + width: 90%; +} + +// somehow the image logo assumption inherits margins from earlier in the CSS +// stack we must remove margins for text site titles +h2#site-text-logo { + margin: 0 0 0 10px; +} From 51ffbf1c1fa209a5bd5a1cf6dbc5a85f33bdd22e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 4 Sep 2017 15:39:44 -0400 Subject: [PATCH 029/159] FIX: Remove duplicate event typo --- app/assets/javascripts/discourse/components/site-header.js.es6 | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/discourse/components/site-header.js.es6 b/app/assets/javascripts/discourse/components/site-header.js.es6 index e071b9eb74..56731c96df 100644 --- a/app/assets/javascripts/discourse/components/site-header.js.es6 +++ b/app/assets/javascripts/discourse/components/site-header.js.es6 @@ -62,7 +62,6 @@ const SiteHeaderComponent = MountWidget.extend(Docking, { this.dispatch('notifications:changed', 'user-notifications'); this.dispatch('header:keyboard-trigger', 'header'); - this.dispatch('header:keyboard-trigger', 'header'); this.dispatch('search-autocomplete:after-complete', 'search-term'); this.appEvents.on('dom:clean', () => { From 7786c6c6f29222e7441e953b4bf8c334ae9ed89b Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 5 Sep 2017 09:14:35 +0800 Subject: [PATCH 030/159] Remove duplicated scope. --- app/models/user.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index e86f5966a3..74792fa87b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -139,8 +139,6 @@ class User < ActiveRecord::Base ucf.value::int > 0 )', 'master_id') } - scope :staff, -> { where("admin OR moderator") } - # TODO-PERF: There is no indexes on any of these # and NotifyMailingListSubscribers does a select-all-and-loop # may want to create an index on (active, blocked, suspended_till)? From 672b7cb9a535e5cdb57db92985c4c2a7d0135aed Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 5 Sep 2017 09:39:56 +0800 Subject: [PATCH 031/159] Require missing dependency. --- app/models/user.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/user.rb b/app/models/user.rb index 74792fa87b..4e164817fb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -10,6 +10,7 @@ require_dependency 'pretty_text' require_dependency 'url_helper' require_dependency 'letter_avatar' require_dependency 'promotion' +require_dependency 'password_validator' class User < ActiveRecord::Base include Searchable From 935afe63f75be03f7fc379d7c68377a17b7a3fdd Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 5 Sep 2017 11:23:03 +0800 Subject: [PATCH 032/159] Fix profile db generator not seeding. --- script/profile_db_generator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/profile_db_generator.rb b/script/profile_db_generator.rb index fc9c30b448..c5ac79e05d 100644 --- a/script/profile_db_generator.rb +++ b/script/profile_db_generator.rb @@ -62,8 +62,8 @@ unless Rails.env == "profile" exit end -# by default, Discourse has a "system" account -if User.count > 1 +# by default, Discourse has a "system" and `discobot` account +if User.count > 2 puts "Only run this script against an empty DB" exit end From d61109388cb12da7593c4c7ae99a9de35e683f4c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 6 Sep 2017 11:18:46 +0800 Subject: [PATCH 033/159] Activate mini-profiler when in profiling env. --- app/controllers/application_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 22c0631362..4db548de6a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -520,7 +520,7 @@ class ApplicationController < ActionController::Base end def mini_profiler_enabled? - defined?(Rack::MiniProfiler) && (guardian.is_developer? || Rails.env.development?) + defined?(Rack::MiniProfiler) && (guardian.is_developer? || Rails.env.development? || Rails.env.profile?) end def authorize_mini_profiler From 8463b676df75ab3210d6e51dbfa2d3d4eeeb85b1 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 6 Sep 2017 11:26:03 +0800 Subject: [PATCH 034/159] Revert "Activate mini-profiler when in profiling env." This reverts commit d61109388cb12da7593c4c7ae99a9de35e683f4c. --- app/controllers/application_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4db548de6a..22c0631362 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -520,7 +520,7 @@ class ApplicationController < ActionController::Base end def mini_profiler_enabled? - defined?(Rack::MiniProfiler) && (guardian.is_developer? || Rails.env.development? || Rails.env.profile?) + defined?(Rack::MiniProfiler) && (guardian.is_developer? || Rails.env.development?) end def authorize_mini_profiler From 7d29ccf2071dbb39c596aade5250e9cb3f6dd44d Mon Sep 17 00:00:00 2001 From: Guo Yunhe Date: Wed, 6 Sep 2017 09:13:57 +0300 Subject: [PATCH 035/159] bbcode find close tag loop end condition Only break loop when close tag has been found. Otherwise, keep searching until the end of string. --- .../pretty-text/engines/discourse-markdown/bbcode-block.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 index 8276d2318a..ddb92eefdb 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode-block.js.es6 @@ -178,8 +178,8 @@ function findInlineCloseTag(state, openTag, start, max) { closeTag = null; } else { closeTag.start = j; + break; } - break; } } } From a14ab488294215063a08448c29a819edf1b633d8 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 6 Sep 2017 09:06:47 +0100 Subject: [PATCH 036/159] Do not load javascripts for disabled plugins (#5103) * Do not load javascript for disabled plugins * Appease rubocop --- app/assets/javascripts/discourse.js.es6 | 19 ++++++++++++++ config/initializers/014-wrap_plugin_js.rb | 5 ++++ lib/discourse_wrap_plugin_js.rb | 31 +++++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 config/initializers/014-wrap_plugin_js.rb create mode 100644 lib/discourse_wrap_plugin_js.rb diff --git a/app/assets/javascripts/discourse.js.es6 b/app/assets/javascripts/discourse.js.es6 index cee3fcbd57..25c05f1c30 100644 --- a/app/assets/javascripts/discourse.js.es6 +++ b/app/assets/javascripts/discourse.js.es6 @@ -1,7 +1,9 @@ import { buildResolver } from 'discourse-common/resolver'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; +import PreloadStore from 'preload-store'; const _pluginCallbacks = []; +const _pluginDefinitions = {}; const Discourse = Ember.Application.extend({ rootElement: '#main', @@ -101,6 +103,16 @@ const Discourse = Ember.Application.extend({ $('noscript').remove(); + // Load plugin definions. + const disabledPlugins = PreloadStore.get('site').disabled_plugins; + Object.keys(_pluginDefinitions).forEach((key) => { + if(!(disabledPlugins.includes(key))){ // Not disabled, so load it + _pluginDefinitions[key].forEach((func) => { + func(); + }); + } + }); + Object.keys(requirejs._eak_seen).forEach(function(key) { if (/\/pre\-initializers\//.test(key)) { const module = requirejs(key, null, null, true); @@ -154,6 +166,13 @@ const Discourse = Ember.Application.extend({ _pluginCallbacks.push({ version, code }); }, + _registerPluginScriptDefinition(pluginName, definition) { + if(!(pluginName in _pluginDefinitions)){ + _pluginDefinitions[pluginName] = []; + } + _pluginDefinitions[pluginName].push(definition); + }, + assetVersion: Ember.computed({ get() { return this.get("currentAssetVersion"); diff --git a/config/initializers/014-wrap_plugin_js.rb b/config/initializers/014-wrap_plugin_js.rb new file mode 100644 index 0000000000..bbf13ab356 --- /dev/null +++ b/config/initializers/014-wrap_plugin_js.rb @@ -0,0 +1,5 @@ +require 'discourse_wrap_plugin_js' + +Rails.application.config.assets.configure do |env| + env.register_preprocessor('application/javascript', DiscourseWrapPluginJS) +end diff --git a/lib/discourse_wrap_plugin_js.rb b/lib/discourse_wrap_plugin_js.rb new file mode 100644 index 0000000000..67cc850bd0 --- /dev/null +++ b/lib/discourse_wrap_plugin_js.rb @@ -0,0 +1,31 @@ +class DiscourseWrapPluginJS + def initialize(options = {}, &block) + end + + def self.instance + @instance ||= new + end + + def self.call(input) + instance.call(input) + end + + # Add stuff around javascript + def call(input) + path = input[:environment].context_class.new(input).pathname.to_s + data = input[:data] + + # Only apply to plugin paths + return data unless (path =~ /\/plugins\//) + + # Find the folder name of the plugin + folder_name = path[/\/plugins\/(\S+?)\//, 1] + + # Lookup plugin name + plugin = Discourse.plugins.find { |p| p.path =~ /\/plugins\/#{folder_name}\// } + plugin_name = plugin.name + + "Discourse._registerPluginScriptDefinition('#{plugin_name}', function(){#{data}}); \n" + end + +end From b840170f8da3c2fb841d55e1192a3522cbe5ae87 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Wed, 6 Sep 2017 11:18:58 +0100 Subject: [PATCH 037/159] Add disabled_plugins to preloadstore for login_required anonymous users (#5134) --- app/models/site.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/site.rb b/app/models/site.rb index 9d90b2c1bc..cfeae213a1 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -87,7 +87,8 @@ class Site filters: Discourse.filters.map(&:to_s), user_fields: UserField.all.map do |userfield| UserFieldSerializer.new(userfield, root: false, scope: guardian) - end + end, + disabled_plugins: Discourse.disabled_plugin_names }.to_json end From ccf5005febbbc54a4bbdfe49def377277d31aa49 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 6 Sep 2017 16:58:00 +0200 Subject: [PATCH 038/159] FEATURE: uses select-box for topic-notifications-button component - Introduces ComponentConnector to use a component inside a widget - Use css to set size of components instead of properties - Smarted positionning - Style tweaks --- .../components/dropdown-select-box.js.es6 | 10 + .../dropdown-header.js.es6 | 7 + .../discourse/components/select-box.js.es6 | 85 +++--- .../select-box/select-box-row.js.es6 | 2 +- .../topic-footer-mobile-dropdown.js.es6 | 2 +- .../topic-notifications-button.js.es6 | 22 +- .../components/topic-notifications.js.es6 | 81 ++++++ .../dropdown-select-box/dropdown-header.hbs | 6 + .../templates/components/select-box.hbs | 2 - .../select-box/select-box-collection.hbs | 2 - .../components/topic-notifications-button.hbs | 7 + .../discourse/templates/topic/unsubscribe.hbs | 5 +- .../widgets/component_connector.js.es6 | 35 +++ .../widgets/topic-notifications-button.js.es6 | 101 ------- .../discourse/widgets/topic-timeline.js.es6 | 10 +- .../components/category-select-box.scss | 2 +- .../components/dropdown-select-box.scss | 39 +++ .../common/components/select-box.scss | 256 +++++++++--------- .../components/topic-notifications.scss | 89 ++++++ .../stylesheets/common/topic-timeline.scss | 4 + .../topic-notifications-button-test.js.es6 | 6 +- .../components/select-box-test.js.es6 | 2 +- 22 files changed, 472 insertions(+), 303 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/dropdown-select-box.js.es6 create mode 100644 app/assets/javascripts/discourse/components/dropdown-select-box/dropdown-header.js.es6 create mode 100644 app/assets/javascripts/discourse/components/topic-notifications.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/dropdown-select-box/dropdown-header.hbs create mode 100644 app/assets/javascripts/discourse/templates/components/topic-notifications-button.hbs create mode 100644 app/assets/javascripts/discourse/widgets/component_connector.js.es6 delete mode 100644 app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 create mode 100644 app/assets/stylesheets/common/components/dropdown-select-box.scss create mode 100644 app/assets/stylesheets/common/components/topic-notifications.scss diff --git a/app/assets/javascripts/discourse/components/dropdown-select-box.js.es6 b/app/assets/javascripts/discourse/components/dropdown-select-box.js.es6 new file mode 100644 index 0000000000..1135bddd01 --- /dev/null +++ b/app/assets/javascripts/discourse/components/dropdown-select-box.js.es6 @@ -0,0 +1,10 @@ +import SelectBoxComponent from "discourse/components/select-box"; + +export default SelectBoxComponent.extend({ + classNames: ["dropdown-select-box"], + wrapper: false, + verticalOffset: 3, + collectionHeight: "auto", + + selectBoxHeaderComponent: "dropdown-select-box/dropdown-header" +}); diff --git a/app/assets/javascripts/discourse/components/dropdown-select-box/dropdown-header.js.es6 b/app/assets/javascripts/discourse/components/dropdown-select-box/dropdown-header.js.es6 new file mode 100644 index 0000000000..b5ec32600f --- /dev/null +++ b/app/assets/javascripts/discourse/components/dropdown-select-box/dropdown-header.js.es6 @@ -0,0 +1,7 @@ +import SelectBoxHeaderComponent from "discourse/components/select-box/select-box-header"; + +export default SelectBoxHeaderComponent.extend({ + layoutName: "components/dropdown-select-box/dropdown-header", + + classNames: ["dropdown-header"], +}); diff --git a/app/assets/javascripts/discourse/components/select-box.js.es6 b/app/assets/javascripts/discourse/components/select-box.js.es6 index 8a48b9e4d3..0fa6c33c10 100644 --- a/app/assets/javascripts/discourse/components/select-box.js.es6 +++ b/app/assets/javascripts/discourse/components/select-box.js.es6 @@ -4,7 +4,6 @@ import { iconHTML } from "discourse-common/lib/icon-library"; export default Ember.Component.extend({ layoutName: "components/select-box", - classNames: "select-box", classNameBindings: ["expanded:is-expanded"], @@ -26,7 +25,6 @@ export default Ember.Component.extend({ value: null, selectedContent: null, noContentLabel: I18n.t("select_box.no_content"), - lastHovered: null, clearSelectionLabel: null, idKey: "id", @@ -44,9 +42,10 @@ export default Ember.Component.extend({ selectBoxCollectionComponent: "select-box/select-box-collection", minWidth: 220, - maxCollectionHeight: 200, + collectionHeight: 200, verticalOffset: 0, horizontalOffset: 0, + fullWidthOnMobile: false, castInteger: false, @@ -67,16 +66,8 @@ export default Ember.Component.extend({ shouldHighlightRow: function() { return (rowComponent) => { - if (Ember.isNone(this.get("value")) && Ember.isNone(this.get("lastHovered"))) { - return false; - } - const id = this._castInteger(rowComponent.get(`content.${this.get("idKey")}`)); - if (Ember.isNone(this.get("lastHovered"))) { - return id === this.get("value"); - } else { - return id === this.get("lastHovered"); - } + return id === this.get("value"); }; }.property(), @@ -96,26 +87,46 @@ export default Ember.Component.extend({ }.property(), applyDirection() { - const offsetTop = this.$()[0].getBoundingClientRect().top; - const windowHeight = $(window).height(); + this.$().removeClass("is-above is-below is-left-aligned is-right-aligned"); + let options = { left: "auto", bottom: "auto", left: "auto", top: "auto" }; const headerHeight = this.$(".select-box-header").outerHeight(false); const filterHeight = this.$(".select-box-filter").outerHeight(false); + const collectionHeight = this.$(".select-box-collection").outerHeight(false); + const windowWidth = $(window).width(); + const windowHeight = $(window).height(); + const boundingRect = this.$()[0].getBoundingClientRect(); + const offsetTop = boundingRect.top; - if (windowHeight - (offsetTop + this.get("maxCollectionHeight") + filterHeight + headerHeight) < 0) { - this.$().addClass("is-reversed"); - this.$(".select-box-body").css({ - left: this.get("horizontalOffset"), - top: "auto", - bottom: headerHeight + this.get("verticalOffset") - }); + if (this.get("fullWidthOnMobile") && this.site.isMobileDevice) { + const margin = 10; + const relativeLeft = this.$().offset().left - $(window).scrollLeft(); + options.left = margin - relativeLeft; + options.width = windowWidth - margin * 2; } else { - this.$().removeClass("is-reversed"); - this.$(".select-box-body").css({ - left: this.get("horizontalOffset"), - top: headerHeight + this.get("verticalOffset"), - bottom: "auto" - }); + const offsetLeft = boundingRect.left; + const bodyWidth = this.$(".select-box-body").outerWidth(false); + const hasRightSpace = (windowWidth - (this.get("horizontalOffset") + offsetLeft + filterHeight + bodyWidth) > 0); + + if (hasRightSpace) { + this.$().addClass("is-left-aligned"); + options.left = this.get("horizontalOffset"); + } else { + this.$().addClass("is-right-aligned"); + options.right = this.get("horizontalOffset"); + } } + + const componentHeight = this.get("verticalOffset") + collectionHeight + filterHeight + headerHeight; + const hasBelowSpace = windowHeight - offsetTop - componentHeight > 0; + if (hasBelowSpace) { + this.$().addClass("is-below"); + options.top = headerHeight + this.get("verticalOffset"); + } else { + this.$().addClass("is-above"); + options.bottom = headerHeight + this.get("verticalOffset"); + } + + this.$(".select-box-body").css(options); }, init() { @@ -161,22 +172,22 @@ export default Ember.Component.extend({ const computedWidth = this.$().outerWidth(false); const computedHeight = this.$().outerHeight(false); - this.$(".select-box-header").css("height", computedHeight); this.$(".select-box-filter").css("height", computedHeight); + this.$(".select-box-header").css("height", computedHeight); if (this.get("expanded")) { if (this.get("scrollableParent").length === 1) { this._applyFixedPosition(computedWidth, computedHeight); } - this.$(".select-box-body").css("width", computedWidth); - this.$(".select-box-collection").css("max-height", this.get("maxCollectionHeight")); + this.$(".select-box-collection").css("max-height", this.get("collectionHeight")); - this.applyDirection(); - - if (this.get("wrapper")) { - this._positionSelectBoxWrapper(); - } + Ember.run.schedule("afterRender", () => { + this.applyDirection(); + if (this.get("wrapper")) { + this._positionSelectBoxWrapper(); + } + }); } else { if (this.get("wrapper")) { this.$(".select-box-wrapper").hide(); @@ -311,10 +322,6 @@ export default Ember.Component.extend({ onClearSelection() { this.setProperties({ value: null, expanded: false }); - }, - - onHoverRow(content) { - this.set("lastHovered", this._castInteger(content[this.get("idKey")])); } }, diff --git a/app/assets/javascripts/discourse/components/select-box/select-box-row.js.es6 b/app/assets/javascripts/discourse/components/select-box/select-box-row.js.es6 index 4c47769e19..a976769ac0 100644 --- a/app/assets/javascripts/discourse/components/select-box/select-box-row.js.es6 +++ b/app/assets/javascripts/discourse/components/select-box/select-box-row.js.es6 @@ -21,7 +21,7 @@ export default Ember.Component.extend({ return templateForRow(this); }, - @computed("shouldHighlightRow", "lastHovered", "value") + @computed("shouldHighlightRow", "value") isHighlighted(shouldHighlightRow) { return shouldHighlightRow(this); }, diff --git a/app/assets/javascripts/discourse/components/topic-footer-mobile-dropdown.js.es6 b/app/assets/javascripts/discourse/components/topic-footer-mobile-dropdown.js.es6 index af7ac64574..2395e43c78 100644 --- a/app/assets/javascripts/discourse/components/topic-footer-mobile-dropdown.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-footer-mobile-dropdown.js.es6 @@ -8,7 +8,7 @@ export default SelectBoxComponent.extend({ dynamicHeaderText: false, - maxCollectionHeight: 300, + collectionHeight: 300, init() { this._super(); diff --git a/app/assets/javascripts/discourse/components/topic-notifications-button.js.es6 b/app/assets/javascripts/discourse/components/topic-notifications-button.js.es6 index b2f5ff0511..3622f7631e 100644 --- a/app/assets/javascripts/discourse/components/topic-notifications-button.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-notifications-button.js.es6 @@ -1,21 +1,9 @@ -import MountWidget from 'discourse/components/mount-widget'; -import { observes } from 'ember-addons/ember-computed-decorators'; +export default Ember.Component.extend({ + layoutName: "components/topic-notifications-button", -export default MountWidget.extend({ - classNames: ['topic-notifications-container'], - widget: 'topic-notifications-button', + classNames: ['topic-notifications-button'], - buildArgs() { - return { topic: this.get('topic'), appendReason: true, showFullTitle: true }; - }, + appendReason: true, - @observes('topic.details.notification_level') - _queueRerender() { - this.queueRerender(); - }, - - didInsertElement() { - this._super(); - this.dispatch('topic-notifications-button:changed', 'topic-notifications-button'); - } + showFullTitle: true }); diff --git a/app/assets/javascripts/discourse/components/topic-notifications.js.es6 b/app/assets/javascripts/discourse/components/topic-notifications.js.es6 new file mode 100644 index 0000000000..3d19b3e7c7 --- /dev/null +++ b/app/assets/javascripts/discourse/components/topic-notifications.js.es6 @@ -0,0 +1,81 @@ +import DropdownSelectBoxComponent from "discourse/components/dropdown-select-box"; +import { observes, on } from "ember-addons/ember-computed-decorators"; +import computed from "ember-addons/ember-computed-decorators"; +import { topicLevels, buttonDetails } from 'discourse/lib/notification-levels'; +import { iconHTML } from 'discourse-common/lib/icon-library'; + +export default DropdownSelectBoxComponent.extend({ + classNames: ["topic-notifications"], + + content: topicLevels, + + i18nPrefix: 'category.notifications', + i18nPostfix: '', + + textKey: "key", + showFullTitle: true, + fullWidthOnMobile: true, + + minWidth: "auto", + + @on("init") + _setInitialNotificationLevel() { + this.set("value", this.get("topic.details.notification_level")); + }, + + @on("didInsertElement") + _bindGlobalLevelChanged() { + this.appEvents.on("topic-notifications-button:changed", (msg) => { + if (msg.type === "notification") { + this.set("value", msg.id); + } + }); + }, + + @on("willDestroyElement") + _unbindGlobalLevelChanged() { + this.appEvents.off("topic-notifications-button:changed"); + }, + + @observes("value") + _notificationLevelChanged() { + this.get("topic.details").updateNotifications(this.get("value")); + this.appEvents.trigger('topic-notifications-button:changed', {type: 'notification', id: this.get("value")}); + }, + + @computed("topic.details.notification_level") + icon(notificationLevel) { + const details = buttonDetails(notificationLevel); + return iconHTML(details.icon, {class: details.key}).htmlSafe(); + }, + + @computed("topic.details.notification_level", "showFullTitle") + generatedHeadertext(notificationLevel, showFullTitle) { + if (showFullTitle) { + const details = buttonDetails(notificationLevel); + return I18n.t(`topic.notifications.${details.key}.title`); + } else { + return null; + } + }, + + templateForRow: function() { + return (rowComponent) => { + const content = rowComponent.get("content"); + const start = `${this.get('i18nPrefix')}.${content.key}${this.get('i18nPostfix')}`; + const title = I18n.t(`${start}.title`); + const description = I18n.t(`${start}.description`); + + return ` +
    + + ${iconHTML(content.icon, { class: content.key })} +
    +
    + ${title} + ${description} +
    + `; + }; + }.property(), +}); diff --git a/app/assets/javascripts/discourse/templates/components/dropdown-select-box/dropdown-header.hbs b/app/assets/javascripts/discourse/templates/components/dropdown-select-box/dropdown-header.hbs new file mode 100644 index 0000000000..f62d5bd35d --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/dropdown-select-box/dropdown-header.hbs @@ -0,0 +1,6 @@ + diff --git a/app/assets/javascripts/discourse/templates/components/select-box.hbs b/app/assets/javascripts/discourse/templates/components/select-box.hbs index 338c850b88..fd8e9bc171 100644 --- a/app/assets/javascripts/discourse/templates/components/select-box.hbs +++ b/app/assets/javascripts/discourse/templates/components/select-box.hbs @@ -36,9 +36,7 @@ templateForRow=templateForRow shouldHighlightRow=shouldHighlightRow titleForRow=titleForRow - lastHovered=lastHovered onSelectRow=(action "onSelectRow") - onHoverRow=(action "onHoverRow") onClearSelection=(action "onClearSelection") noContentLabel=noContentLabel value=value diff --git a/app/assets/javascripts/discourse/templates/components/select-box/select-box-collection.hbs b/app/assets/javascripts/discourse/templates/components/select-box/select-box-collection.hbs index 1e0aae5268..9cc47c1586 100644 --- a/app/assets/javascripts/discourse/templates/components/select-box/select-box-collection.hbs +++ b/app/assets/javascripts/discourse/templates/components/select-box/select-box-collection.hbs @@ -11,9 +11,7 @@ templateForRow=templateForRow titleForRow=titleForRow shouldHighlightRow=shouldHighlightRow - lastHovered=lastHovered onSelect=onSelectRow - onHover=onHoverRow value=value }} {{else}} diff --git a/app/assets/javascripts/discourse/templates/components/topic-notifications-button.hbs b/app/assets/javascripts/discourse/templates/components/topic-notifications-button.hbs new file mode 100644 index 0000000000..6ceb4d9332 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/topic-notifications-button.hbs @@ -0,0 +1,7 @@ +{{topic-notifications topic=topic showFullTitle=showFullTitle}} + +{{#if appendReason}} +

    + {{{topic.details.notificationReasonText}}} +

    +{{/if}} diff --git a/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs b/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs index f330d1046b..54947fbdaa 100644 --- a/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs +++ b/app/assets/javascripts/discourse/templates/topic/unsubscribe.hbs @@ -3,8 +3,11 @@

    {{{stopNotificiationsText}}}

    +

    - {{i18n "topic.unsubscribe.change_notification_state"}} {{topic-notifications-button topic=model}} + {{i18n "topic.unsubscribe.change_notification_state"}}

    + + {{topic-notifications-button topic=model}}
    diff --git a/app/assets/javascripts/discourse/widgets/component_connector.js.es6 b/app/assets/javascripts/discourse/widgets/component_connector.js.es6 new file mode 100644 index 0000000000..7484fad179 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/component_connector.js.es6 @@ -0,0 +1,35 @@ +export default class ComponentConnector { + constructor(widget, componentName, opts) { + this.widget = widget; + this.opts = opts; + this.componentName = componentName; + } + + init() { + const $elem = $('
    '); + const elem = $elem[0]; + const { opts, widget, componentName } = this; + + Ember.run.next(() => { + const mounted = widget._findView(); + + const view = widget + .register + .lookupFactory(`component:${componentName}`) + .create(opts); + + if (Ember.setOwner) { + Ember.setOwner(view, Ember.getOwner(mounted)); + } + + mounted._connected.push(view); + view.renderer.appendTo(view, $elem[0]); + }); + + return elem; + } + + update() { } +} + +ComponentConnector.prototype.type = 'Widget'; diff --git a/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 b/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 deleted file mode 100644 index 9293c36cc6..0000000000 --- a/app/assets/javascripts/discourse/widgets/topic-notifications-button.js.es6 +++ /dev/null @@ -1,101 +0,0 @@ -import { createWidget } from 'discourse/widgets/widget'; -import { topicLevels, buttonDetails } from 'discourse/lib/notification-levels'; -import { h } from 'virtual-dom'; -import RawHTML from 'discourse/widgets/raw-html'; -import { iconNode } from 'discourse-common/lib/icon-library'; - -createWidget('notification-option', { - buildKey: attrs => `topic-notifications-button-${attrs.id}`, - tagName: 'li', - - html(attrs) { - return h('a', [ - iconNode(attrs.icon, { class: `icon ${attrs.key}`, tagName: 'span' }), - h('div', [ - h('span.title', I18n.t(`topic.notifications.${attrs.key}.title`)), - h('span.desc', I18n.t(`topic.notifications.${attrs.key}.description`)), - ]) - ]); - }, - - click() { - this.sendWidgetAction('notificationLevelChanged', this.attrs.id); - } -}); - -export default createWidget('topic-notifications-button', { - tagName: 'span.btn-group.notification-options', - buildKey: () => `topic-notifications-button`, - - defaultState() { - return { expanded: false }; - }, - - buildClasses(attrs, state) { - if (state.expanded) { return "open"; } - }, - - buildAttributes() { - return { title: I18n.t('topic.notifications.title') }; - }, - - buttonFor(level) { - const details = buttonDetails(level); - - const button = { - className: `toggle-notification-options`, - label: null, - icon: details.icon, - action: 'toggleDropdown', - iconClass: details.key - }; - - if (this.attrs.showFullTitle) { - button.label = `topic.notifications.${details.key}.title`; - } else { - button.className = 'btn toggle-notifications-options notifications-dropdown'; - } - - return this.attach('button', button); - }, - - html(attrs, state) { - const details = attrs.topic.get('details'); - const result = [ this.buttonFor(details.get('notification_level')) ]; - - if (state.expanded) { - result.push(h('ul.dropdown-menu', topicLevels.map(l => this.attach('notification-option', l)))); - } - - if (attrs.appendReason) { - result.push(new RawHTML({ html: `

    ${details.get('notificationReasonText')}

    ` })); - } - - return result; - }, - - toggleDropdown() { - this.state.expanded = !this.state.expanded; - }, - - clickOutside() { - if (this.state.expanded) { - this.sendWidgetAction('toggleDropdown'); - } - }, - - notificationLevelChanged(id) { - this.state.expanded = false; - return this.attrs.topic.get('details').updateNotifications(id); - }, - - topicNotificationsButtonChanged(msg) { - switch(msg.type) { - case 'notification': - if (this.attrs.topic.get('details.notification_level') !== msg.id) { - this.notificationLevelChanged(msg.id); - } - break; - } - } -}); diff --git a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 index 06ba942197..2fb698a38c 100644 --- a/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-timeline.js.es6 @@ -1,4 +1,5 @@ import { createWidget } from 'discourse/widgets/widget'; +import ComponentConnector from 'discourse/widgets/component_connector'; import { h } from 'virtual-dom'; import { relativeAge } from 'discourse/lib/formatter'; import { iconNode } from 'discourse-common/lib/icon-library'; @@ -313,7 +314,14 @@ createWidget('timeline-footer-controls', { } if (currentUser) { - controls.push(this.attach('topic-notifications-button', { topic })); + controls.push(new ComponentConnector(this, + 'topic-notifications-button', + { + topic, + appendReason: false, + showFullTitle: false + } + )); } return controls; diff --git a/app/assets/stylesheets/common/components/category-select-box.scss b/app/assets/stylesheets/common/components/category-select-box.scss index 48c414d775..a68634949d 100644 --- a/app/assets/stylesheets/common/components/category-select-box.scss +++ b/app/assets/stylesheets/common/components/category-select-box.scss @@ -1,4 +1,4 @@ -.select-box.category-select-box { +.category-select-box { .select-box-row { display: -webkit-box; display: -ms-flexbox; diff --git a/app/assets/stylesheets/common/components/dropdown-select-box.scss b/app/assets/stylesheets/common/components/dropdown-select-box.scss new file mode 100644 index 0000000000..053d14f1e9 --- /dev/null +++ b/app/assets/stylesheets/common/components/dropdown-select-box.scss @@ -0,0 +1,39 @@ +.dropdown-select-box.dropdown-select-box { + &.is-expanded { + z-index: 9999; + } + + .select-box-body { + border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + } + + .select-box-row { + margin: 0; + padding: 5px 10px; + + &.is-highlighted { + background: none; + } + + &:hover { + background: $highlight-medium; + } + } + + .dropdown-header { + padding: 0; + border: 0; + outline: 0; + justify-content: flex-start; + width: min-content; + background: none; + + .btn { + align-items: center; + justify-content: space-between; + flex-direction: row; + display: inline-flex; + height: 100%; + } + } +} diff --git a/app/assets/stylesheets/common/components/select-box.scss b/app/assets/stylesheets/common/components/select-box.scss index d56b7d381d..d043e1623a 100644 --- a/app/assets/stylesheets/common/components/select-box.scss +++ b/app/assets/stylesheets/common/components/select-box.scss @@ -39,14 +39,14 @@ } .collection, { - border-radius: 0 0 3px 3px; + border-radius: inherit; } .select-box-header { border-radius: 3px 3px 0 0; } - &.is-reversed { + &.is-above { .select-box-header { border-radius: 0 0 3px 3px; } @@ -61,7 +61,7 @@ } } - &.is-reversed { + &.is-above { .select-box-body { bottom: 0; top: auto; @@ -90,6 +90,15 @@ -ms-flex-align: center; align-items: center; justify-content: space-between; + -webkit-box-orient: horizontal; + -webkit-box-direction: normal; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + padding-left: 10px; + padding-right: 10px; &.is-focused { border: 1px solid $tertiary; @@ -97,10 +106,31 @@ -webkit-box-shadow: $tertiary 0px 0px 6px 0px; box-shadow: $tertiary 0px 0px 6px 0px; } + + .current-selection { + text-align: left; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + color: inherit; + } + + .icon { + margin-right: 5px; + } + + .caret-icon { + margin-left: 5px; + pointer-events: none; + } } .select-box-body { display: none; + width: 100%; background: $secondary; -webkit-box-sizing: border-box; box-sizing: border-box; @@ -115,10 +145,31 @@ display: -webkit-box; display: -ms-flexbox; display: flex; - align-items: center; -webkit-box-pack: start; -ms-flex-pack: start; justify-content: flex-start; + + .text { + margin: 0; + } + + .d-icon { + margin-right: 5px; + } + + &.is-highlighted { + background: $highlight-medium; + } + + &:hover { + background: $highlight-medium; + } + + &.is-selected { + a { + background: $highlight-medium; + } + } } .select-box-collection { @@ -127,9 +178,9 @@ display: -webkit-box; display: -ms-flexbox; display: flex; - -webkit-box-flex: 1; - -ms-flex: 1; - flex: 1; + -webkit-box-flex: 0; + -ms-flex: 0 1 auto; + flex: 0 1 auto; -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-direction: column; @@ -137,15 +188,68 @@ background: $secondary; overflow-x: hidden; overflow-y: auto; - border-radius: 0 0 3px 3px; + border-radius: inherit; margin: 0; padding: 0; -webkit-overflow-scrolling: touch; + + .collection { + padding: 0; + margin: 0; + + &:hover .select-box-row.is-highlighted { + background: none; + } + + &:hover .select-box-row.is-highlighted:hover { + background: $highlight-medium; + } + } + + &::-webkit-scrollbar { + -webkit-appearance: none; + width: 10px; + } + + &::-webkit-scrollbar-thumb { + cursor: pointer; + border-radius: 5px; + background: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + } + + &::-webkit-scrollbar-track { + background: transparent; + border-radius: 0; + } } .select-box-filter { border-bottom: 1px solid $primary-low; background: $secondary; + display: -webkit-box; + display: -ms-flexbox; + display: flex; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 0 10px; + + .filter-query, .filter-query:focus, .filter-query:active { + background: none; + margin: 0; + -webkit-box-flex: 1; + -ms-flex: 1; + flex: 1; + outline: none; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + width: 100%; + padding: 5px 0; + } } .select-box-wrapper { @@ -159,132 +263,18 @@ pointer-events: none; border: 1px solid transparent; } -} -.select-box .select-box-header { - height: inherit; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-orient: horizontal; - -webkit-box-direction: normal; - -ms-flex-direction: row; - flex-direction: row; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - padding-left: 10px; - padding-right: 10px; - - .current-selection { - text-align: left; - -webkit-box-flex: 1; - -ms-flex: 1; - flex: 1; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - color: inherit; - } - - .icon { - margin-right: 5px; - } - - .caret-icon { - margin-left: 5px; - pointer-events: none; - } -} - -.select-box .select-box-collection { - -webkit-box-flex: 0; - -ms-flex: 0 1 auto; - flex: 0 1 auto; - - .collection { - padding: 0; - margin: 0; - } - - &::-webkit-scrollbar { - -webkit-appearance: none; - width: 10px; - } - - &::-webkit-scrollbar-thumb { - cursor: pointer; - border-radius: 5px; - background: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); - } - - &::-webkit-scrollbar-track { - background: transparent; - border-radius: 0; - } -} - -.select-box .select-box-row { - .text { - margin: 0; - } - - .d-icon { - margin-right: 5px; - } - - &.is-highlighted { - background: $highlight-medium; - } - - &.is-selected { - a { - background: $highlight-medium; - } - } -} - -.select-box .select-box-filter { - background: $secondary; - display: -webkit-box; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -ms-flex-align: center; - align-items: center; - -webkit-box-pack: justify; - -ms-flex-pack: justify; - justify-content: space-between; - padding: 0 10px; - - .filter-query, .filter-query:focus, .filter-query:active { - background: none; - margin: 0; - -webkit-box-flex: 1; - -ms-flex: 1; - flex: 1; - outline: none; + .select-box-offscreen, .select-box .select-box-offscreen:focus { + clip: rect(0 0 0 0); + width: 1px; + height: 1px; border: 0; - -webkit-box-shadow: none; - box-shadow: none; - width: 100%; - padding: 5px 0; + margin: 0; + padding: 0; + overflow: hidden; + position: absolute; + outline: 0; + left: 0px; + top: 0px; } } - -.select-box .select-box-offscreen, .select-box .select-box-offscreen:focus { - clip: rect(0 0 0 0); - width: 1px; - height: 1px; - border: 0; - margin: 0; - padding: 0; - overflow: hidden; - position: absolute; - outline: 0; - left: 0px; - top: 0px; -} diff --git a/app/assets/stylesheets/common/components/topic-notifications.scss b/app/assets/stylesheets/common/components/topic-notifications.scss new file mode 100644 index 0000000000..8d3f5b7255 --- /dev/null +++ b/app/assets/stylesheets/common/components/topic-notifications.scss @@ -0,0 +1,89 @@ +#topic-footer-buttons .topic-notifications .btn { + margin: 0; +} + +#topic-footer-buttons p.reason { + line-height: 16px; + margin: 0 0 0 5px; +} + +.topic-notifications-button { + display: inline-flex; + justify-content: flex-start; + align-items: center; + margin: 5px 0; + + .topic-notifications, .reason { + display: inline-flex; + } +} + +.topic-notifications.topic-notifications { + display: inline-flex; + height: 30px; + + &.is-expanded .collection, &.is-expanded .select-box-collection, &.is-expanded .select-box-body { + border-radius: 0; + } + + .select-box-collection { + padding: 0; + } + + .select-box-body { + background-clip: padding-box; + border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + width: 550px; + } + + .select-box-row { + &.is-highlighted .icons .selection-indicator { + visibility: visible; + } + + .icons { + display: flex; + align-items: flex-start; + justify-content: space-between; + align-self: flex-start; + margin-right: 10px; + + .selection-indicator { + width: 6px; + height: 6px; + background: $tertiary; + visibility: hidden; + border-radius: 12px; + align-self: center; + margin-right: 5px; + } + + .d-icon { + font-size: 1.286em; + align-self: flex-start; + margin-right: 0; + opacity: 1; + } + } + + .texts { + line-height: 18px; + flex: 1; + + .title { + font-weight: bold; + display: block; + font-size: 1em; + color: $primary; + } + + .desc { + font-size: 0.857em; + font-weight: normal; + color: #919191; + white-space: normal; + } + } + } +} diff --git a/app/assets/stylesheets/common/topic-timeline.scss b/app/assets/stylesheets/common/topic-timeline.scss index afba6f72cd..532822ef4e 100644 --- a/app/assets/stylesheets/common/topic-timeline.scss +++ b/app/assets/stylesheets/common/topic-timeline.scss @@ -212,6 +212,10 @@ margin-right: 0.5em; } + button:last-child { + margin-right: 0; + } + ul.dropdown-menu { right: 0.5em; top: auto; diff --git a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 index bb1c314a1c..e00f7d5062 100644 --- a/test/javascripts/acceptance/topic-notifications-button-test.js.es6 +++ b/test/javascripts/acceptance/topic-notifications-button-test.js.es6 @@ -19,7 +19,7 @@ acceptance("Topic Notifications button", { QUnit.test("Updating topic notification level", assert => { visit("/t/internationalization-localization/280"); - const notificationOptions = "#topic-footer-buttons .notification-options"; + const notificationOptions = "#topic-footer-buttons .topic-notifications"; andThen(() => { assert.ok( @@ -29,7 +29,7 @@ QUnit.test("Updating topic notification level", assert => { }); click(`${notificationOptions} .tracking`); - click(`${notificationOptions} .dropdown-menu .watching`); + click(`${notificationOptions} .select-box-collection .select-box-row[title=tracking]`); andThen(() => { assert.ok( @@ -44,4 +44,4 @@ QUnit.test("Updating topic notification level", assert => { // 'it should display the right notification level in topic timeline' // ); }); -}); \ No newline at end of file +}); diff --git a/test/javascripts/components/select-box-test.js.es6 b/test/javascripts/components/select-box-test.js.es6 index 94e07f1713..9028cb82e7 100644 --- a/test/javascripts/components/select-box-test.js.es6 +++ b/test/javascripts/components/select-box-test.js.es6 @@ -222,7 +222,7 @@ componentTest('persists filter state when expandind/collapsing', { }); componentTest('supports options to limit size', { - template: '{{select-box maxCollectionHeight=20 content=content}}', + template: '{{select-box collectionHeight=20 content=content}}', beforeEach() { this.set("content", [{ id: 1, text: "robin" }]); From 777f024b8c79a025d816ebbffc0b0a9c937edb21 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 6 Sep 2017 10:47:48 -0400 Subject: [PATCH 039/159] Clean up weird indentation --- .../stylesheets/common/base/header.scss | 256 +++++++++--------- 1 file changed, 128 insertions(+), 128 deletions(-) diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index 2245734e84..640cdebbf9 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -1,149 +1,149 @@ .d-header { - width: 100%; - position: absolute; - top: 0; - z-index: 1001; - background-color: $header_background; - box-shadow: 0 2px 4px -1px rgba(0,0,0, .25); + width: 100%; + position: absolute; + top: 0; + z-index: 1001; + background-color: $header_background; + box-shadow: 0 2px 4px -1px rgba(0,0,0, .25); - .docked & { - position: fixed; - backface-visibility: hidden; /** do magic for scrolling performance **/ + .docked & { + position: fixed; + backface-visibility: hidden; /** do magic for scrolling performance **/ + } + + .contents { + margin: 8px 0; + } + + .title { + float: left; + a, a:visited { + color: $header_primary; } + } - .contents { - margin: 8px 0; - } + #site-logo { + max-height: 40px; + } - .title { + .d-icon-home { + font-size: 1.643em; + } + + .panel { + float: right; + position: relative; + } + + .login-button, button.sign-up-button { + float: left; + margin-top: 7px; + padding: 6px 10px; + .fa { margin-right: 3px; } + } + + button.login-button { + margin-left: 7px; + } + + .icons { + float: right; + text-align: center; + margin: 0 0 0 5px; + list-style: none; + + > li { float: left; - a, a:visited { - color: $header_primary; - } } + .icon { + display: block; + padding: 3px; + color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); + text-decoration: none; + cursor: pointer; + border-top: 1px solid transparent; + border-left: 1px solid transparent; + border-right: 1px solid transparent; + transition: all linear .15s; - #site-logo { - max-height: 40px; - } - .d-icon-home { - font-size: 1.643em; - } - - .panel { - float: right; - position: relative; - } - - .login-button, button.sign-up-button { - float: left; - margin-top: 7px; - padding: 6px 10px; - .fa { margin-right: 3px; } - } - - button.login-button { - margin-left: 7px; - } - - .icons { - float: right; - text-align: center; - margin: 0 0 0 5px; - list-style: none; - - > li { - float: left; - } - .icon { - display: block; - padding: 3px; - color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); - text-decoration: none; - cursor: pointer; + &:hover { + color: $primary; + background-color: $primary-low; border-top: 1px solid transparent; border-left: 1px solid transparent; border-right: 1px solid transparent; - transition: all linear .15s; - - - &:hover { - color: $primary; - background-color: $primary-low; - border-top: 1px solid transparent; - border-left: 1px solid transparent; - border-right: 1px solid transparent; - } - &:active { - color: $primary; - background-color: $primary-low; - } } - .drop-down-visible & { - .active .icon { - position: relative; - color: #7b7b7b; - background-color: $secondary; - cursor: default; - border-top: 1px solid $primary-low; - border-left: 1px solid $primary-low; - border-right: 1px solid $primary-low; - - .badge-notification { - top: -10px; - } - - .flagged-posts { - right: 24px; - } - - &:after { - display: block; - position: absolute; - top: 100%; - left: 0; - z-index: 1101; - width: 100%; - height: 0; - content: ""; - border-top: 1px solid $secondary; - } - &:hover { - border-bottom: none; - } - } + &:active { + color: $primary; + background-color: $primary-low; } - - .d-icon { - width: 32px; - height: 32px; - font-size: 1.714em; - line-height: 32px; - display: inline-block; - } - .notifications { + } + .drop-down-visible & { + .active .icon { position: relative; - } - .badge-notification, .ring { - position: absolute; - top: -9px; - z-index: 1; - margin-left: 0; - } - .unread-notifications { - right: 0; - background-color: scale-color($tertiary, $lightness: 50%); - } - .unread-private-messages, .ring { - right: 25px; - } - .flagged-posts { - right: 65px; + color: #7b7b7b; + background-color: $secondary; + cursor: default; + border-top: 1px solid $primary-low; + border-left: 1px solid $primary-low; + border-right: 1px solid $primary-low; + + .badge-notification { + top: -10px; + } + + .flagged-posts { + right: 24px; + } + + &:after { + display: block; + position: absolute; + top: 100%; + left: 0; + z-index: 1101; + width: 100%; + height: 0; + content: ""; + border-top: 1px solid $secondary; + } + &:hover { + border-bottom: none; + } } } - .flagged-posts, .queued-posts { - background: $danger; + + .d-icon { + width: 32px; + height: 32px; + font-size: 1.714em; + line-height: 32px; + display: inline-block; } + .notifications { + position: relative; + } + .badge-notification, .ring { + position: absolute; + top: -9px; + z-index: 1; + margin-left: 0; + } + .unread-notifications { + right: 0; + background-color: scale-color($tertiary, $lightness: 50%); + } + .unread-private-messages, .ring { + right: 25px; + } + .flagged-posts { + right: 65px; + } + } + .flagged-posts, .queued-posts { + background: $danger; + } } From 825452df7622b0f931dc7f8f7f77db809769902f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 6 Sep 2017 11:29:43 -0400 Subject: [PATCH 040/159] Refactor header CSS for composability --- .../discourse/widgets/header.js.es6 | 32 +-- .../stylesheets/common/base/header.scss | 203 +++++++++--------- app/assets/stylesheets/mobile/header.scss | 22 +- 3 files changed, 135 insertions(+), 122 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/header.js.es6 b/app/assets/javascripts/discourse/widgets/header.js.es6 index 10f0f97c24..c131128f89 100644 --- a/app/assets/javascripts/discourse/widgets/header.js.es6 +++ b/app/assets/javascripts/discourse/widgets/header.js.es6 @@ -28,23 +28,23 @@ createWidget('header-notifications', { }, html(attrs) { - const { currentUser } = this; + const { user } = attrs; const contents = [ avatarImg(this.settings.avatarSize, { - template: currentUser.get('avatar_template'), - username: currentUser.get('username') + template: user.get('avatar_template'), + username: user.get('username') }) ]; - const unreadNotifications = currentUser.get('unread_notifications'); + const unreadNotifications = user.get('unread_notifications'); if (!!unreadNotifications) { contents.push(this.attach('link', { action: attrs.action, className: 'badge-notification unread-notifications', rawLabel: unreadNotifications })); } - const unreadPMs = currentUser.get('unread_private_messages'); + const unreadPMs = user.get('unread_private_messages'); if (!!unreadPMs) { - if (!currentUser.get('read_first_notification')) { + if (!user.get('read_first_notification')) { contents.push(h('span.ring')); if (!attrs.active && attrs.ringBackdrop) { contents.push(h('span.ring-backdrop-spotlight')); @@ -72,9 +72,7 @@ createWidget('user-dropdown', jQuery.extend({ }, html(attrs) { - const { currentUser } = this; - - return h('a.icon', { attributes: { href: currentUser.get('path'), 'data-auto-route': true } }, + return h('a.icon', { attributes: { href: attrs.user.get('path'), 'data-auto-route': true } }, this.attach('header-notifications', attrs)); } }, dropdown)); @@ -106,7 +104,7 @@ createWidget('header-dropdown', jQuery.extend({ }, dropdown)); createWidget('header-icons', { - tagName: 'ul.icons.clearfix', + tagName: 'ul.icons.d-header-icons.clearfix', buildAttributes() { return { role: 'navigation' }; @@ -139,10 +137,13 @@ createWidget('header-icons', { }); const icons = [search, hamburger]; - if (this.currentUser) { - icons.push(this.attach('user-dropdown', { active: attrs.userVisible, - action: 'toggleUserMenu', - ringBackdrop: attrs.ringBackdrop })); + if (attrs.user) { + icons.push(this.attach('user-dropdown', { + active: attrs.userVisible, + action: 'toggleUserMenu', + ringBackdrop: attrs.ringBackdrop, + user: attrs.user + })); } return icons; @@ -204,7 +205,8 @@ export default createWidget('header', { userVisible: state.userVisible, searchVisible: state.searchVisible, ringBackdrop: state.ringBackdrop, - flagCount: attrs.flagCount })]; + flagCount: attrs.flagCount, + user: this.currentUser })]; if (state.searchVisible) { const contextType = this.searchContextType(); diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index 640cdebbf9..9dfdf12d92 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -46,106 +46,117 @@ margin-left: 7px; } - .icons { + .d-header-icons { float: right; - text-align: center; - margin: 0 0 0 5px; - list-style: none; - - > li { - float: left; - } - .icon { - display: block; - padding: 3px; - color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); - text-decoration: none; - cursor: pointer; - border-top: 1px solid transparent; - border-left: 1px solid transparent; - border-right: 1px solid transparent; - transition: all linear .15s; - - - &:hover { - color: $primary; - background-color: $primary-low; - border-top: 1px solid transparent; - border-left: 1px solid transparent; - border-right: 1px solid transparent; - } - &:active { - color: $primary; - background-color: $primary-low; - } - } - .drop-down-visible & { - .active .icon { - position: relative; - color: #7b7b7b; - background-color: $secondary; - cursor: default; - border-top: 1px solid $primary-low; - border-left: 1px solid $primary-low; - border-right: 1px solid $primary-low; - - .badge-notification { - top: -10px; - } - - .flagged-posts { - right: 24px; - } - - &:after { - display: block; - position: absolute; - top: 100%; - left: 0; - z-index: 1101; - width: 100%; - height: 0; - content: ""; - border-top: 1px solid $secondary; - } - &:hover { - border-bottom: none; - } - } - } - - .d-icon { - width: 32px; - height: 32px; - font-size: 1.714em; - line-height: 32px; - display: inline-block; - } - .notifications { - position: relative; - } - .badge-notification, .ring { - position: absolute; - top: -9px; - z-index: 1; - margin-left: 0; - } - .unread-notifications { - right: 0; - background-color: scale-color($tertiary, $lightness: 50%); - } - .unread-private-messages, .ring { - right: 25px; - } - .flagged-posts { - right: 65px; - } - } - .flagged-posts, .queued-posts { - background: $danger; } } +.d-header-icons { + text-align: center; + margin: 0 0 0 5px; + list-style: none; + + .flagged-posts, .queued-posts { + background: $danger; + } + + > li { + float: left; + } + .icon { + display: block; + padding: 3px; + color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); + text-decoration: none; + cursor: pointer; + border-top: 1px solid transparent; + border-left: 1px solid transparent; + border-right: 1px solid transparent; + transition: all linear .15s; + + + &:hover { + color: $primary; + background-color: $primary-low; + border-top: 1px solid transparent; + border-left: 1px solid transparent; + border-right: 1px solid transparent; + } + &:active { + color: $primary; + background-color: $primary-low; + } + } + .drop-down-visible & { + .active .icon { + position: relative; + color: #7b7b7b; + background-color: $secondary; + cursor: default; + border-top: 1px solid $primary-low; + border-left: 1px solid $primary-low; + border-right: 1px solid $primary-low; + + .badge-notification { + top: -10px; + } + + .flagged-posts { + right: 24px; + } + + &:after { + display: block; + position: absolute; + top: 100%; + left: 0; + z-index: 1101; + width: 100%; + height: 0; + content: ""; + border-top: 1px solid $secondary; + } + &:hover { + border-bottom: none; + } + } + } + + .d-icon { + width: 32px; + height: 32px; + font-size: 1.714em; + line-height: 32px; + display: inline-block; + } + .notifications { + position: relative; + } + .ring { + position: absolute; + top: -9px; + z-index: 1; + margin-left: 0; + } + .header-dropdown-toggle { + position: relative; + } + .badge-notification { + position: absolute; + z-index: 1; + left: 0; + top: -9px; + } + .unread-notifications { + left: auto; + right: 0; + background-color: scale-color($tertiary, $lightness: 50%); + } + .unread-private-messages, .ring { + left: auto; + right: 25px; + } +} .highlight-strong { background-color: $highlight-medium; diff --git a/app/assets/stylesheets/mobile/header.scss b/app/assets/stylesheets/mobile/header.scss index ab112d8bda..528f26b721 100644 --- a/app/assets/stylesheets/mobile/header.scss +++ b/app/assets/stylesheets/mobile/header.scss @@ -28,22 +28,22 @@ } } - .icons { - .badge-notification { - top: -5px; - color: $secondary; - } - - .active .icon { - &:after { margin-top: -1px; } - } - } - button.sign-up-button { display:none; } } +.d-header-icons { + .badge-notification { + top: -5px; + color: $secondary; + } + + .active .icon { + &:after { margin-top: -1px; } + } +} + #main-outlet { padding-top: 60px; } From 0fca5ed533a123d75c45c84375199ac2079c9ca3 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Wed, 6 Sep 2017 19:59:23 +0200 Subject: [PATCH 041/159] FIX: stricter check on presence of notification_level_change When `notification_level_change` was `0` it was evaluating to false --- app/assets/javascripts/discourse/controllers/topic.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 79d10418bb..bb8500f08d 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -827,7 +827,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { this.messageBus.subscribe(`/topic/${this.get('model.id')}`, data => { const topic = this.get('model'); - if (data.notification_level_change) { + if (Ember.isPresent(data.notification_level_change)) { topic.set('details.notification_level', data.notification_level_change); topic.set('details.notifications_reason_id', data.notifications_reason_id); return; From 8a935a4b5fbb836474d3344c33f6e169ee62acdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 6 Sep 2017 22:35:08 +0200 Subject: [PATCH 042/159] FEATURE: new badges when visiting the forum for 10, 100 and 365 consecutive days --- app/models/badge.rb | 4 ++++ config/locales/server.en.yml | 12 ++++++++++++ db/fixtures/006_badges.rb | 19 +++++++++++++++++++ lib/badge_queries.rb | 12 ++++++++++++ 4 files changed, 47 insertions(+) diff --git a/app/models/badge.rb b/app/models/badge.rb index 46502613f5..637cb24010 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -56,6 +56,10 @@ class Badge < ActiveRecord::Base GivesBack = 32 Empathetic = 39 + Enthusiast = 45 + Aficionado = 46 + Devotee = 47 + NewUserOfTheMonth = 44 # other consts diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 233414a027..dfeb7af7b0 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -3392,6 +3392,18 @@ en: description: Outstanding contributions in their first month long_description: | This badge is granted to congratulate two new users each month for their excellent overall contributions, as measured by how often their posts were liked, and by whom. + enthusiast: + name: Enthusiast + description: Has visited the forum for 10 consecutive days + long_description: This badge is granted the first time you visit the forum for 10 consecutive days. + aficionado: + name: Aficionado + description: Has visited the forum for 100 consecutive days + long_description: This badge is granted the first time you visit the forum for 100 consecutive days. + devotee: + name: Devotee + description: Has visited the forum for 365 consecutive days + long_description: This badge is granted the first time you visit the forum for 365 consecutive days. badge_title_metadata: "%{display_name} badge on %{site_title}" admin_login: diff --git a/db/fixtures/006_badges.rb b/db/fixtures/006_badges.rb index 22cf2c7714..18022ab451 100644 --- a/db/fixtures/006_badges.rb +++ b/db/fixtures/006_badges.rb @@ -415,6 +415,25 @@ Badge.seed do |b| b.system = true end +[ + [Badge::Enthusiast, "Enthusiast", BadgeType::Bronze, 10], + [Badge::Aficionado, "Aficionado", BadgeType::Silver, 100], + [Badge::Devotee, "Devotee", BadgeType::Gold, 365], +].each do |id, name, level, days| + Badge.seed do |b| + b.id = id + b.name = name + b.default_icon = "fa-eye" + b.badge_type_id = level + b.query = BadgeQueries.consecutive_visits(days) + b.badge_grouping_id = BadgeGrouping::Community + b.default_badge_grouping_id = BadgeGrouping::Community + b.trigger = Badge::Trigger::None + b.auto_revoke = false + b.system = true + end +end + Badge.where("NOT system AND id < 100").each do |badge| new_id = [Badge.maximum(:id) + 1, 100].max old_id = badge.id diff --git a/lib/badge_queries.rb b/lib/badge_queries.rb index cdb5ac8d6a..e249f228bc 100644 --- a/lib/badge_queries.rb +++ b/lib/badge_queries.rb @@ -232,4 +232,16 @@ SQL SQL end + def self.consecutive_visits(days) + <<~SQL + SELECT user_id, "start" + interval '1' day * COUNT(*) AS "granted_at" + FROM ( + SELECT user_id, visited_at - (DENSE_RANK() OVER (PARTITION BY user_id ORDER BY visited_at))::int "start" + FROM user_visits + ) s + GROUP BY user_id, "start" + HAVING COUNT(*) >= #{days} + SQL + end + end From a1957b772399e5747c70f5e42cd608951d721b3b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 6 Sep 2017 17:33:35 -0400 Subject: [PATCH 043/159] FIX: Stop moving notifications on large screens --- app/assets/stylesheets/common/base/header.scss | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index 9dfdf12d92..1100ccbb6e 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -64,6 +64,7 @@ float: left; } .icon { + position: relative; display: block; padding: 3px; color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); @@ -97,10 +98,6 @@ border-left: 1px solid $primary-low; border-right: 1px solid $primary-low; - .badge-notification { - top: -10px; - } - .flagged-posts { right: 24px; } @@ -146,6 +143,7 @@ z-index: 1; left: 0; top: -9px; + min-width: 6px; } .unread-notifications { left: auto; From 27e4baf3571c1589bba01d3f5f62dc447de6ba6a Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 6 Sep 2017 15:01:04 -0700 Subject: [PATCH 044/159] minor copyedits on visit days badges --- config/locales/server.en.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index dfeb7af7b0..27fd973d2d 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -3394,16 +3394,13 @@ en: This badge is granted to congratulate two new users each month for their excellent overall contributions, as measured by how often their posts were liked, and by whom. enthusiast: name: Enthusiast - description: Has visited the forum for 10 consecutive days - long_description: This badge is granted the first time you visit the forum for 10 consecutive days. + long_description: This badge is granted for visiting 10 consecutive days. Thanks for sticking with us for over a week! aficionado: name: Aficionado - description: Has visited the forum for 100 consecutive days - long_description: This badge is granted the first time you visit the forum for 100 consecutive days. + long_description: This badge is granted for visiting 100 consecutive days. That's more than three months! devotee: name: Devotee - description: Has visited the forum for 365 consecutive days - long_description: This badge is granted the first time you visit the forum for 365 consecutive days. + long_description: This badge is granted for visiting 365 consecutive days. Wow, an entire year! badge_title_metadata: "%{display_name} badge on %{site_title}" admin_login: From 4142bed1af8cf6d5ca4accd2205d3f7ff5277398 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 7 Sep 2017 06:00:47 +0800 Subject: [PATCH 045/159] Fix incorrect topic id in profiiing script. --- script/bench.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bench.rb b/script/bench.rb index 84893f5ebd..f390ab69d2 100644 --- a/script/bench.rb +++ b/script/bench.rb @@ -193,7 +193,7 @@ begin tests = [ ["categories", "/categories"], ["home", "/"], - ["topic", "/t/oh-how-i-wish-i-could-shut-up-like-a-tunnel-for-so/69"] + ["topic", "/t/oh-how-i-wish-i-could-shut-up-like-a-tunnel-for-so/179"] # ["user", "/u/admin1/activity"], ] From db920673dc49616cb92cdb77d5073e1ae50382d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 7 Sep 2017 01:08:28 +0200 Subject: [PATCH 046/159] FIX: consecutive_visits query wasn't return only the first result per user --- lib/badge_queries.rb | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/badge_queries.rb b/lib/badge_queries.rb index e249f228bc..515e44cfa8 100644 --- a/lib/badge_queries.rb +++ b/lib/badge_queries.rb @@ -234,13 +234,18 @@ SQL def self.consecutive_visits(days) <<~SQL - SELECT user_id, "start" + interval '1' day * COUNT(*) AS "granted_at" - FROM ( - SELECT user_id, visited_at - (DENSE_RANK() OVER (PARTITION BY user_id ORDER BY visited_at))::int "start" - FROM user_visits - ) s - GROUP BY user_id, "start" + WITH consecutive_visits AS ( + SELECT user_id, visited_at - (DENSE_RANK() OVER (PARTITION BY user_id ORDER BY visited_at))::int "start" + FROM user_visits + ), visits AS ( + SELECT user_id, "start", DENSE_RANK() OVER (PARTITION BY user_id ORDER BY "start") "rank" + FROM consecutive_visits + GROUP BY user_id, "start" HAVING COUNT(*) >= #{days} + ) + SELECT user_id, "start" + interval '#{days} days' "granted_at" + FROM visits + WHERE "rank" = 1 SQL end From 5aba30ede681bd9b41aab963a487e026780bf190 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 6 Sep 2017 18:46:37 -0700 Subject: [PATCH 047/159] description wasn't checked in. ???? --- config/locales/server.en.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 27fd973d2d..2035588b35 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -3394,12 +3394,15 @@ en: This badge is granted to congratulate two new users each month for their excellent overall contributions, as measured by how often their posts were liked, and by whom. enthusiast: name: Enthusiast + description: Visited 10 days long_description: This badge is granted for visiting 10 consecutive days. Thanks for sticking with us for over a week! aficionado: name: Aficionado + description: Visited 100 days long_description: This badge is granted for visiting 100 consecutive days. That's more than three months! devotee: name: Devotee + description: Visited 365 days long_description: This badge is granted for visiting 365 consecutive days. Wow, an entire year! badge_title_metadata: "%{display_name} badge on %{site_title}" From 4d840d10db71a44a658bc81afaf4cdbbc9656cef Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 7 Sep 2017 13:29:30 +0800 Subject: [PATCH 048/159] PERF: Reduce number of Redis hits per requests. --- app/controllers/application_controller.rb | 11 ++++++++--- lib/discourse.rb | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 22c0631362..c1b66293e3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -36,6 +36,7 @@ class ApplicationController < ActionController::Base end end + before_action :check_readonly_mode before_filter :handle_theme before_filter :set_current_user_for_logs before_filter :clear_notifications @@ -61,7 +62,7 @@ class ApplicationController < ActionController::Base end def add_readonly_header - response.headers['Discourse-Readonly'] = 'true' if Discourse.readonly_mode? + response.headers['Discourse-Readonly'] = 'true' if @readonly_mode end def perform_refresh_session @@ -182,7 +183,7 @@ class ApplicationController < ActionController::Base end def clear_notifications - if current_user && !Discourse.readonly_mode? + if current_user && !@readonly_mode cookie_notifications = cookies['cn'.freeze] notifications = request.headers['Discourse-Clear-Notifications'.freeze] @@ -400,6 +401,10 @@ class ApplicationController < ActionController::Base private + def check_readonly_mode + @readonly_mode = Discourse.readonly_mode? + end + def locale_from_header begin # Rails I18n uses underscores between the locale and the region; the request @@ -574,7 +579,7 @@ class ApplicationController < ActionController::Base def block_if_readonly_mode return if request.fullpath.start_with?(path "/admin/backups") - raise Discourse::ReadOnly.new if !(request.get? || request.head?) && Discourse.readonly_mode? + raise Discourse::ReadOnly.new if !(request.get? || request.head?) && @readonly_mode end def build_not_found_page(status = 404, layout = false) diff --git a/lib/discourse.rb b/lib/discourse.rb index e9c06f020b..a12dacdcf9 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -276,7 +276,7 @@ module Discourse end def self.readonly_mode? - recently_readonly? || READONLY_KEYS.any? { |key| !!$redis.get(key) } + recently_readonly? || $redis.mget(*READONLY_KEYS).compact.present? end def self.last_read_only From c9912fcc370063d48f980111aae26747f82388c2 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 7 Sep 2017 08:40:18 +0100 Subject: [PATCH 049/159] Add discourse-presence as a core plugin (#5137) * Add discourse-presence as a core plugin * Default enabled --- .gitignore | 1 + plugins/discourse-presence/README.md | 14 ++ .../composer-presence-display.js.es6 | 21 +++ .../composer-controller-presence.js.es6 | 128 +++++++++++++++ .../components/composer-presence-display.hbs | 18 +++ .../connectors/composer-fields/presence.hbs | 3 + .../assets/stylesheets/presence.scss | 45 ++++++ .../config/locales/client.en.yml | 9 ++ .../config/locales/server.en.yml | 3 + .../discourse-presence/config/settings.yml | 4 + plugins/discourse-presence/plugin.rb | 148 ++++++++++++++++++ .../spec/presence_controller_spec.rb | 80 ++++++++++ .../spec/presence_manager_spec.rb | 64 ++++++++ 13 files changed, 538 insertions(+) create mode 100644 plugins/discourse-presence/README.md create mode 100644 plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6 create mode 100644 plugins/discourse-presence/assets/javascripts/discourse/initializers/composer-controller-presence.js.es6 create mode 100644 plugins/discourse-presence/assets/javascripts/discourse/templates/components/composer-presence-display.hbs create mode 100644 plugins/discourse-presence/assets/javascripts/discourse/templates/connectors/composer-fields/presence.hbs create mode 100644 plugins/discourse-presence/assets/stylesheets/presence.scss create mode 100644 plugins/discourse-presence/config/locales/client.en.yml create mode 100644 plugins/discourse-presence/config/locales/server.en.yml create mode 100644 plugins/discourse-presence/config/settings.yml create mode 100644 plugins/discourse-presence/plugin.rb create mode 100644 plugins/discourse-presence/spec/presence_controller_spec.rb create mode 100644 plugins/discourse-presence/spec/presence_manager_spec.rb diff --git a/.gitignore b/.gitignore index 67dcad40e5..8a1f8652b9 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ bootsnap-compile-cache/ !/plugins/discourse-details/ !/plugins/discourse-nginx-performance-report !/plugins/discourse-narrative-bot +!/plugins/discourse-presence /plugins/*/auto_generated/ /spec/fixtures/plugins/my_plugin/auto_generated diff --git a/plugins/discourse-presence/README.md b/plugins/discourse-presence/README.md new file mode 100644 index 0000000000..4e41c6c62e --- /dev/null +++ b/plugins/discourse-presence/README.md @@ -0,0 +1,14 @@ +# Discourse Presence plugin +This plugin shows which users are currently writing a reply at the same time as you. + +## Installation + +Follow the directions at [Install a Plugin](https://meta.discourse.org/t/install-a-plugin/19157) using https://github.com/discourse/discourse-presence.git as the repository URL. + +## Authors + +André Pereira, David Taylor + +## License + +GNU GPL v2 diff --git a/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6 b/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6 new file mode 100644 index 0000000000..44184dd2c8 --- /dev/null +++ b/plugins/discourse-presence/assets/javascripts/discourse/components/composer-presence-display.js.es6 @@ -0,0 +1,21 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + composer: Ember.inject.controller(), + + @computed('composer.presenceUsers', 'currentUser.id') + users(presenceUsers, currentUser_id){ + return presenceUsers.filter(user => user.id !== currentUser_id); + }, + + @computed('composer.presenceState.action') + isReply(action){ + return action === 'reply'; + }, + + @computed('users.length') + shouldDisplay(length){ + return length > 0; + } + +}); \ No newline at end of file diff --git a/plugins/discourse-presence/assets/javascripts/discourse/initializers/composer-controller-presence.js.es6 b/plugins/discourse-presence/assets/javascripts/discourse/initializers/composer-controller-presence.js.es6 new file mode 100644 index 0000000000..0fcdbbb55b --- /dev/null +++ b/plugins/discourse-presence/assets/javascripts/discourse/initializers/composer-controller-presence.js.es6 @@ -0,0 +1,128 @@ +import { ajax } from 'discourse/lib/ajax'; +import { observes} from 'ember-addons/ember-computed-decorators'; +import { withPluginApi } from 'discourse/lib/plugin-api'; +import pageVisible from 'discourse/lib/page-visible'; + +function initialize(api) { + api.modifyClass('controller:composer', { + + oldPresenceState: { compose_state: 'closed' }, + presenceState: { compose_state: 'closed' }, + keepAliveTimer: null, + messageBusChannel: null, + + @observes('model.composeState', 'model.action', 'model.post', 'model.topic') + openStatusChanged(){ + Ember.run.once(this, 'updateStateObject'); + }, + + updateStateObject(){ + const composeState = this.get('model.composeState'); + + const stateObject = { + compose_state: composeState ? composeState : 'closed' + }; + + if(stateObject.compose_state === 'open'){ + stateObject.action = this.get('model.action'); + + // Add some context if we're editing or replying + switch(stateObject.action){ + case 'edit': + stateObject.post_id = this.get('model.post.id'); + break; + case 'reply': + stateObject.topic_id = this.get('model.topic.id'); + break; + default: + break; // createTopic or privateMessage + } + } + + this.set('oldPresenceState', this.get('presenceState')); + this.set('presenceState', stateObject); + }, + + shouldSharePresence(){ + const isOpen = this.get('presenceState.compose_state') !== 'open'; + const isEditing = ['edit','reply'].includes(this.get('presenceState.action')); + return isOpen && isEditing; + }, + + @observes('presenceState') + presenceStateChanged(){ + if(this.get('messageBusChannel')){ + this.messageBus.unsubscribe(this.get('messageBusChannel')); + this.set('messageBusChannel', null); + } + + this.set('presenceUsers', []); + + ajax('/presence/publish/', { + type: 'POST', + data: { + response_needed: true, + previous: this.get('oldPresenceState'), + current: this.get('presenceState') + } + }).then((data) => { + const messageBusChannel = data['messagebus_channel']; + if(messageBusChannel){ + const users = data['users']; + const messageBusId = data['messagebus_id']; + this.set('presenceUsers', users); + this.set('messageBusChannel', messageBusChannel); + this.messageBus.subscribe(messageBusChannel, message => { + this.set('presenceUsers', message['users']); + }, messageBusId); + } + }).catch((error) => { + // This isn't a critical failure, so don't disturb the user + console.error("Error publishing composer status", error); + }); + + + Ember.run.cancel(this.get('keepAliveTimer')); + if(this.shouldSharePresence()){ + // Send presence data every 10 seconds + this.set('keepAliveTimer', Ember.run.later(this, 'keepPresenceAlive', 10000)); + } + }, + + + + keepPresenceAlive(){ + // If the composer isn't open, or we're not editing, + // don't update anything, and don't schedule this task again + if(!this.shouldSharePresence()){ + return; + } + + // Only send the keepalive message if the browser has focus + if(pageVisible()){ + ajax('/presence/publish/', { + type: 'POST', + data: { current: this.get('presenceState') } + }).catch((error) => { + // This isn't a critical failure, so don't disturb the user + console.error("Error publishing composer status", error); + }); + } + + // Schedule again in another 30 seconds + Ember.run.cancel(this.get('keepAliveTimer')); + this.set('keepAliveTimer', Ember.run.later(this, 'keepPresenceAlive', 10000)); + } + + }); +} + +export default { + name: "composer-controller-presence", + after: "message-bus", + + initialize(container) { + const siteSettings = container.lookup('site-settings:main'); + if (siteSettings.presence_enabled) withPluginApi('0.8.9', initialize); + } +}; diff --git a/plugins/discourse-presence/assets/javascripts/discourse/templates/components/composer-presence-display.hbs b/plugins/discourse-presence/assets/javascripts/discourse/templates/components/composer-presence-display.hbs new file mode 100644 index 0000000000..1b105bcd81 --- /dev/null +++ b/plugins/discourse-presence/assets/javascripts/discourse/templates/components/composer-presence-display.hbs @@ -0,0 +1,18 @@ +{{#if shouldDisplay}} +
    + {{#each users as |user|}} + {{avatar user avatarTemplatePath="avatar_template" usernamePath="username" imageSize="small"}} + {{/each}} + + + + {{#if isReply ~}} + {{i18n 'presence.is_replying' count=users.length}} + {{~else~}} + {{i18n 'presence.is_editing' count=users.length}} + {{~/if}}{{!-- (using comment to stop whitespace) + --}}{{!-- + --}}... + +
    +{{/if}} \ No newline at end of file diff --git a/plugins/discourse-presence/assets/javascripts/discourse/templates/connectors/composer-fields/presence.hbs b/plugins/discourse-presence/assets/javascripts/discourse/templates/connectors/composer-fields/presence.hbs new file mode 100644 index 0000000000..0f3c2e1b95 --- /dev/null +++ b/plugins/discourse-presence/assets/javascripts/discourse/templates/connectors/composer-fields/presence.hbs @@ -0,0 +1,3 @@ +{{#if siteSettings.presence_enabled}} + {{composer-presence-display}} +{{/if}} diff --git a/plugins/discourse-presence/assets/stylesheets/presence.scss b/plugins/discourse-presence/assets/stylesheets/presence.scss new file mode 100644 index 0000000000..7b2c04c466 --- /dev/null +++ b/plugins/discourse-presence/assets/stylesheets/presence.scss @@ -0,0 +1,45 @@ +.presence-users{ + + background-color: $primary-low; + + color: $primary-medium; + padding: 0px 5px; + position: absolute; + top: 8px; + right: 30px; + + + .wave { + + .dot { + display: inline-block; + animation: wave 1.8s linear infinite; + + &:nth-child(2) { + animation-delay: -1.6s; + } + + &:nth-child(3) { + animation-delay: -1.4s; + } + } + } + + @keyframes wave { + 0%, 60%, 100% { + transform: initial; + } + + 30% { + transform: translateY(-0.2em); + } + } +} + +.mobile-view .presence-users{ + top: 5px; + right: 60px; + .description{ + display:none; + } +} \ No newline at end of file diff --git a/plugins/discourse-presence/config/locales/client.en.yml b/plugins/discourse-presence/config/locales/client.en.yml new file mode 100644 index 0000000000..9de83306d7 --- /dev/null +++ b/plugins/discourse-presence/config/locales/client.en.yml @@ -0,0 +1,9 @@ +en: + js: + presence: + is_replying: + one: "is also replying" + other: "are also replying" + is_editing: + one: "is also editing" + other: "are also editing" \ No newline at end of file diff --git a/plugins/discourse-presence/config/locales/server.en.yml b/plugins/discourse-presence/config/locales/server.en.yml new file mode 100644 index 0000000000..72768d3f25 --- /dev/null +++ b/plugins/discourse-presence/config/locales/server.en.yml @@ -0,0 +1,3 @@ +en: + site_settings: + presence_enabled: 'Show users that are currently replying to the current topic, or editing the current post?' diff --git a/plugins/discourse-presence/config/settings.yml b/plugins/discourse-presence/config/settings.yml new file mode 100644 index 0000000000..0bc0899aa8 --- /dev/null +++ b/plugins/discourse-presence/config/settings.yml @@ -0,0 +1,4 @@ +plugins: + presence_enabled: + default: true + client: true \ No newline at end of file diff --git a/plugins/discourse-presence/plugin.rb b/plugins/discourse-presence/plugin.rb new file mode 100644 index 0000000000..9ee7a49847 --- /dev/null +++ b/plugins/discourse-presence/plugin.rb @@ -0,0 +1,148 @@ +# name: discourse-presence +# about: Show which users are writing a reply to a topic +# version: 1.0 +# authors: André Pereira, David Taylor +# url: https://github.com/discourse/discourse-presence.git + +enabled_site_setting :presence_enabled + +register_asset 'stylesheets/presence.scss' + +PLUGIN_NAME ||= "discourse-presence".freeze + +after_initialize do + + module ::Presence + class Engine < ::Rails::Engine + engine_name PLUGIN_NAME + isolate_namespace Presence + end + end + + module ::Presence::PresenceManager + def self.get_redis_key(type, id) + "presence:#{type}:#{id}" + end + + def self.get_messagebus_channel(type, id) + "/presence/#{type}/#{id}" + end + + def self.add(type, id, user_id) + redis_key = get_redis_key(type, id) + response = $redis.hset(redis_key, user_id, Time.zone.now) + + response # Will be true if a new key + end + + def self.remove(type, id, user_id) + redis_key = get_redis_key(type, id) + response = $redis.hdel(redis_key, user_id) + + response > 0 # Return true if key was actually deleted + end + + def self.get_users(type, id) + redis_key = get_redis_key(type, id) + user_ids = $redis.hkeys(redis_key).map(&:to_i) + + User.where(id: user_ids) + end + + def self.publish(type, id) + users = get_users(type, id) + serialized_users = users.map { |u| BasicUserSerializer.new(u, root: false) } + message = { + users: serialized_users + } + MessageBus.publish(get_messagebus_channel(type, id), message.as_json) + + users + end + + def self.cleanup(type, id) + hash = $redis.hgetall(get_redis_key(type, id)) + original_hash_size = hash.length + + any_changes = false + + # Delete entries older than 20 seconds + hash.each do |user_id, time| + if Time.zone.now - Time.parse(time) >= 20 + any_changes ||= remove(type, id, user_id) + end + end + + any_changes + end + + end + + require_dependency "application_controller" + + class Presence::PresencesController < ::ApplicationController + before_filter :ensure_logged_in + + def publish + data = params.permit(:response_needed, + current: [:compose_state, :action, :topic_id, :post_id], + previous: [:compose_state, :action, :topic_id, :post_id] + ) + + if data[:previous] && + data[:previous][:compose_state] == 'open' && + data[:previous][:action].in?(['edit', 'reply']) + + type = data[:previous][:post_id] ? 'post' : 'topic' + id = data[:previous][:post_id] ? data[:previous][:post_id] : data[:previous][:topic_id] + + any_changes = false + any_changes ||= Presence::PresenceManager.remove(type, id, current_user.id) + any_changes ||= Presence::PresenceManager.cleanup(type, id) + + users = Presence::PresenceManager.publish(type, id) if any_changes + end + + if data[:current] && + data[:current][:compose_state] == 'open' && + data[:current][:action].in?(['edit', 'reply']) + + type = data[:current][:post_id] ? 'post' : 'topic' + id = data[:current][:post_id] ? data[:current][:post_id] : data[:current][:topic_id] + + any_changes = false + any_changes ||= Presence::PresenceManager.add(type, id, current_user.id) + any_changes ||= Presence::PresenceManager.cleanup(type, id) + + users = Presence::PresenceManager.publish(type, id) if any_changes + + if data[:response_needed] + users ||= Presence::PresenceManager.get_users(type, id) + + serialized_users = users.map { |u| BasicUserSerializer.new(u, root: false) } + + messagebus_channel = Presence::PresenceManager.get_messagebus_channel(type, id) + + render json: { + messagebus_channel: messagebus_channel, + messagebus_id: MessageBus.last_id(messagebus_channel), + users: serialized_users + } + return + end + end + + render json: {} + end + + end + + Presence::Engine.routes.draw do + post '/publish' => 'presences#publish' + end + + Discourse::Application.routes.append do + mount ::Presence::Engine, at: '/presence' + end + +end diff --git a/plugins/discourse-presence/spec/presence_controller_spec.rb b/plugins/discourse-presence/spec/presence_controller_spec.rb new file mode 100644 index 0000000000..c15263714e --- /dev/null +++ b/plugins/discourse-presence/spec/presence_controller_spec.rb @@ -0,0 +1,80 @@ +require 'rails_helper' + +describe ::Presence::PresencesController, type: :request do + + before do + SiteSetting.presence_enabled = true + end + + let(:user1) { Fabricate(:user) } + let(:user2) { Fabricate(:user) } + let(:user3) { Fabricate(:user) } + + after(:each) do + $redis.del('presence:post:22') + $redis.del('presence:post:11') + end + + context 'when not logged in' do + it 'should raise the right error' do + expect { post '/presence/publish.json' }.to raise_error(Discourse::NotLoggedIn) + end + end + + context 'when logged in' do + before do + sign_in(user1) + end + + it "doesn't produce an error" do + expect { post '/presence/publish.json' }.not_to raise_error + end + + it "returns a response when requested" do + messages = MessageBus.track_publish do + post '/presence/publish.json', current: { compose_state: 'open', action: 'edit', post_id: 22 }, response_needed: true + end + + expect(messages.count).to eq (1) + + data = JSON.parse(response.body) + + expect(data['messagebus_channel']).to eq('/presence/post/22') + expect(data['messagebus_id']).to eq(MessageBus.last_id('/presence/post/22')) + expect(data['users'][0]["id"]).to eq(user1.id) + end + + it "doesn't return a response when not requested" do + messages = MessageBus.track_publish do + post '/presence/publish.json', current: { compose_state: 'open', action: 'edit', post_id: 22 } + end + + expect(messages.count).to eq (1) + + data = JSON.parse(response.body) + expect(data).to eq({}) + end + + it "doesn't send duplicate messagebus messages" do + messages = MessageBus.track_publish do + post '/presence/publish.json', current: { compose_state: 'open', action: 'edit', post_id: 22 } + end + expect(messages.count).to eq (1) + + messages = MessageBus.track_publish do + post '/presence/publish.json', current: { compose_state: 'open', action: 'edit', post_id: 22 } + end + expect(messages.count).to eq (0) + end + + it "clears 'previous' state when supplied" do + messages = MessageBus.track_publish do + post '/presence/publish.json', current: { compose_state: 'open', action: 'edit', post_id: 22 } + post '/presence/publish.json', current: { compose_state: 'open', action: 'edit', post_id: 11 }, previous: { compose_state: 'open', action: 'edit', post_id: 22 } + end + expect(messages.count).to eq (3) + end + + end + +end diff --git a/plugins/discourse-presence/spec/presence_manager_spec.rb b/plugins/discourse-presence/spec/presence_manager_spec.rb new file mode 100644 index 0000000000..0c98197a47 --- /dev/null +++ b/plugins/discourse-presence/spec/presence_manager_spec.rb @@ -0,0 +1,64 @@ +require 'rails_helper' + +describe ::Presence::PresenceManager do + + let(:user1) { Fabricate(:user) } + let(:user2) { Fabricate(:user) } + let(:user3) { Fabricate(:user) } + let(:manager) { ::Presence::PresenceManager } + + after(:each) do + $redis.del('presence:post:22') + $redis.del('presence:post:11') + end + + it 'adds, removes and lists users correctly' do + expect(manager.get_users('post', 22).count).to eq(0) + + expect(manager.add('post', 22, user1.id)).to be true + expect(manager.add('post', 22, user2.id)).to be true + expect(manager.add('post', 11, user3.id)).to be true + + expect(manager.get_users('post', 22).count).to eq(2) + expect(manager.get_users('post', 11).count).to eq(1) + + expect(manager.get_users('post', 22)).to contain_exactly(user1, user2) + expect(manager.get_users('post', 11)).to contain_exactly(user3) + + expect(manager.remove('post', 22, user1.id)).to be true + expect(manager.get_users('post', 22).count).to eq(1) + expect(manager.get_users('post', 22)).to contain_exactly(user2) + end + + it 'publishes correctly' do + expect(manager.get_users('post', 22).count).to eq(0) + + manager.add('post', 22, user1.id) + manager.add('post', 22, user2.id) + + messages = MessageBus.track_publish do + manager.publish('post', 22) + end + + expect(messages.count).to eq (1) + message = messages.first + + expect(message.channel).to eq('/presence/post/22') + + expect(message.data["users"].map { |u| u[:id] }).to contain_exactly(user1.id, user2.id) + end + + it 'cleans up correctly' do + freeze_time Time.zone.now do + expect(manager.add('post', 22, user1.id)).to be true + expect(manager.cleanup('post', 22)).to be false # Nothing to cleanup + expect(manager.get_users('post', 22).count).to eq(1) + end + + # Anything older than 20 seconds should be cleaned up + freeze_time 30.seconds.from_now do + expect(manager.cleanup('post', 22)).to be true + expect(manager.get_users('post', 22).count).to eq(0) + end + end +end From dd27c0c80ed3fb4d66cac2fa45e5c059859a14ab Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 7 Sep 2017 11:06:04 +0200 Subject: [PATCH 050/159] FIX: supports emojis in pinned topic excerpt --- app/assets/javascripts/discourse/models/topic.js.es6 | 4 ++++ .../discourse/templates/list/topic-excerpt.raw.hbs | 2 +- test/javascripts/models/topic-test.js.es6 | 8 ++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index f8e75e517f..c3a8c61995 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -407,6 +407,10 @@ const Topic = RestModel.extend({ }); }, + @computed('excerpt') + escapedExcerpt(excerpt) { + return emojiUnescape(excerpt); + }, hasExcerpt: Em.computed.notEmpty('excerpt'), diff --git a/app/assets/javascripts/discourse/templates/list/topic-excerpt.raw.hbs b/app/assets/javascripts/discourse/templates/list/topic-excerpt.raw.hbs index 76be2c412f..0a42a6a0c5 100644 --- a/app/assets/javascripts/discourse/templates/list/topic-excerpt.raw.hbs +++ b/app/assets/javascripts/discourse/templates/list/topic-excerpt.raw.hbs @@ -1,6 +1,6 @@ {{#if topic.hasExcerpt}}
    - {{{topic.excerpt}}} + {{{topic.escapedExcerpt}}} {{#if topic.excerptTruncated}} {{i18n 'read_more'}} {{/if}} diff --git a/test/javascripts/models/topic-test.js.es6 b/test/javascripts/models/topic-test.js.es6 index 8738d2b91b..fd8c79c7d3 100644 --- a/test/javascripts/models/topic-test.js.es6 +++ b/test/javascripts/models/topic-test.js.es6 @@ -75,3 +75,11 @@ QUnit.test('fancyTitle', assert => { `smile with all slight_smile the emojis pearpeach`, "supports emojis"); }); + +QUnit.test('excerpt', assert => { + var topic = Topic.create({ excerpt: "This is a test topic :smile:", pinned: true }); + + assert.equal(topic.get('escapedExcerpt'), + `This is a test topic smile`, + "supports emojis"); +}); From 3e123b1a399a05d1f501ae0caf1e558fdbcc1420 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 7 Sep 2017 17:22:39 +0800 Subject: [PATCH 051/159] PERF: Use `pluck` instead of enmurating through all the records. --- lib/topic_view.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/topic_view.rb b/lib/topic_view.rb index 9a268f9668..0e072f9d77 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -68,11 +68,11 @@ class TopicView if @posts if (added_fields = User.whitelisted_user_custom_fields(@guardian)).present? - @user_custom_fields = User.custom_fields_for_ids(@posts.map(&:user_id), added_fields) + @user_custom_fields = User.custom_fields_for_ids(@posts.pluck(:user_id), added_fields) end if (whitelisted_fields = TopicView.whitelisted_post_custom_fields(@user)).present? - @post_custom_fields = Post.custom_fields_for_ids(@posts.map(&:id), whitelisted_fields) + @post_custom_fields = Post.custom_fields_for_ids(@posts.pluck(:id), whitelisted_fields) end end From e0d5d9670ab2d0fb923fef54e3bdcbbcfc524fb1 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 7 Sep 2017 18:41:44 +0800 Subject: [PATCH 052/159] Fix the build. --- lib/filter_best_posts.rb | 2 +- lib/topic_view.rb | 12 ++---------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/filter_best_posts.rb b/lib/filter_best_posts.rb index e01e79e0e0..960ee136f7 100644 --- a/lib/filter_best_posts.rb +++ b/lib/filter_best_posts.rb @@ -59,7 +59,7 @@ class FilterBestPosts end def sort_posts - @posts.to_a.sort! { |a, b| a.post_number <=> b.post_number } + @posts = Post.from(@posts, :posts).order(post_number: :asc) end end diff --git a/lib/topic_view.rb b/lib/topic_view.rb index 0e072f9d77..06e5d0921d 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -66,7 +66,7 @@ class TopicView filter_posts(options) - if @posts + if @posts.present? if (added_fields = User.whitelisted_user_custom_fields(@guardian)).present? @user_custom_fields = User.custom_fields_for_ids(@posts.pluck(:user_id), added_fields) end @@ -331,14 +331,6 @@ class TopicView @filtered_posts.by_newest.with_user.first(25) end - def current_post_ids - @current_post_ids ||= if @posts.is_a?(Array) - @posts.map { |p| p.id } - else - @posts.pluck(:post_number) - end - end - # Returns an array of [id, post_number, days_ago] tuples. # `days_ago` is there for the timeline calculations. def filtered_post_stream @@ -361,7 +353,7 @@ class TopicView post_numbers = PostTiming .where(topic_id: @topic.id, user_id: @user.id) - .where(post_number: current_post_ids) + .where(post_number: @posts.pluck(:post_number)) .pluck(:post_number) post_numbers.each { |pn| result << pn } From a0daa7cad0daa7561e632bf3ae12c1244e080e5a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 7 Sep 2017 18:59:02 +0800 Subject: [PATCH 053/159] Oops fix build again. --- spec/components/topic_view_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/components/topic_view_spec.rb b/spec/components/topic_view_spec.rb index 6e5f8149c9..22a501e99b 100644 --- a/spec/components/topic_view_spec.rb +++ b/spec/components/topic_view_spec.rb @@ -64,7 +64,7 @@ describe TopicView do best = TopicView.new(topic.id, nil, best: 99) expect(best.posts.count).to eq(2) expect(best.filtered_post_ids.size).to eq(3) - expect(best.current_post_ids).to match_array([p2.id, p3.id]) + expect(best.posts.pluck(:id)).to match_array([p2.id, p3.id]) # should get no results for trust level too low best = TopicView.new(topic.id, nil, best: 99, min_trust_level: coding_horror.trust_level + 1) From 7d350d0d75f024e6817d15f1dfafcf391a061331 Mon Sep 17 00:00:00 2001 From: David Taylor Date: Thu, 7 Sep 2017 14:15:29 +0100 Subject: [PATCH 054/159] Revert plugin js changes (#5139) * Revert "Add disabled_plugins to preloadstore for login_required anonymous users (#5134)" This reverts commit b840170f8da3c2fb841d55e1192a3522cbe5ae87. * Revert "Do not load javascripts for disabled plugins (#5103)" This reverts commit a14ab488294215063a08448c29a819edf1b633d8. --- app/assets/javascripts/discourse.js.es6 | 19 -------------- app/models/site.rb | 3 +-- config/initializers/014-wrap_plugin_js.rb | 5 ---- lib/discourse_wrap_plugin_js.rb | 31 ----------------------- 4 files changed, 1 insertion(+), 57 deletions(-) delete mode 100644 config/initializers/014-wrap_plugin_js.rb delete mode 100644 lib/discourse_wrap_plugin_js.rb diff --git a/app/assets/javascripts/discourse.js.es6 b/app/assets/javascripts/discourse.js.es6 index 25c05f1c30..cee3fcbd57 100644 --- a/app/assets/javascripts/discourse.js.es6 +++ b/app/assets/javascripts/discourse.js.es6 @@ -1,9 +1,7 @@ import { buildResolver } from 'discourse-common/resolver'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; -import PreloadStore from 'preload-store'; const _pluginCallbacks = []; -const _pluginDefinitions = {}; const Discourse = Ember.Application.extend({ rootElement: '#main', @@ -103,16 +101,6 @@ const Discourse = Ember.Application.extend({ $('noscript').remove(); - // Load plugin definions. - const disabledPlugins = PreloadStore.get('site').disabled_plugins; - Object.keys(_pluginDefinitions).forEach((key) => { - if(!(disabledPlugins.includes(key))){ // Not disabled, so load it - _pluginDefinitions[key].forEach((func) => { - func(); - }); - } - }); - Object.keys(requirejs._eak_seen).forEach(function(key) { if (/\/pre\-initializers\//.test(key)) { const module = requirejs(key, null, null, true); @@ -166,13 +154,6 @@ const Discourse = Ember.Application.extend({ _pluginCallbacks.push({ version, code }); }, - _registerPluginScriptDefinition(pluginName, definition) { - if(!(pluginName in _pluginDefinitions)){ - _pluginDefinitions[pluginName] = []; - } - _pluginDefinitions[pluginName].push(definition); - }, - assetVersion: Ember.computed({ get() { return this.get("currentAssetVersion"); diff --git a/app/models/site.rb b/app/models/site.rb index cfeae213a1..9d90b2c1bc 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -87,8 +87,7 @@ class Site filters: Discourse.filters.map(&:to_s), user_fields: UserField.all.map do |userfield| UserFieldSerializer.new(userfield, root: false, scope: guardian) - end, - disabled_plugins: Discourse.disabled_plugin_names + end }.to_json end diff --git a/config/initializers/014-wrap_plugin_js.rb b/config/initializers/014-wrap_plugin_js.rb deleted file mode 100644 index bbf13ab356..0000000000 --- a/config/initializers/014-wrap_plugin_js.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'discourse_wrap_plugin_js' - -Rails.application.config.assets.configure do |env| - env.register_preprocessor('application/javascript', DiscourseWrapPluginJS) -end diff --git a/lib/discourse_wrap_plugin_js.rb b/lib/discourse_wrap_plugin_js.rb deleted file mode 100644 index 67cc850bd0..0000000000 --- a/lib/discourse_wrap_plugin_js.rb +++ /dev/null @@ -1,31 +0,0 @@ -class DiscourseWrapPluginJS - def initialize(options = {}, &block) - end - - def self.instance - @instance ||= new - end - - def self.call(input) - instance.call(input) - end - - # Add stuff around javascript - def call(input) - path = input[:environment].context_class.new(input).pathname.to_s - data = input[:data] - - # Only apply to plugin paths - return data unless (path =~ /\/plugins\//) - - # Find the folder name of the plugin - folder_name = path[/\/plugins\/(\S+?)\//, 1] - - # Lookup plugin name - plugin = Discourse.plugins.find { |p| p.path =~ /\/plugins\/#{folder_name}\// } - plugin_name = plugin.name - - "Discourse._registerPluginScriptDefinition('#{plugin_name}', function(){#{data}}); \n" - end - -end From 58321d0783b29d5fbac92eb4404d4d63616c407a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 7 Sep 2017 21:35:16 +0800 Subject: [PATCH 055/159] PERF: Remove `Object#present?` check introduced in https://github.com/discourse/discourse/commit/e0d5d9670ab2d0fb923fef54e3bdcbbcfc524fb1. --- lib/filter_best_posts.rb | 2 +- lib/topic_view.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/filter_best_posts.rb b/lib/filter_best_posts.rb index 960ee136f7..b517149708 100644 --- a/lib/filter_best_posts.rb +++ b/lib/filter_best_posts.rb @@ -15,7 +15,7 @@ class FilterBestPosts def filter @posts = if @min_replies && @topic.posts_count < @min_replies + 1 - [] + Post.none else filter_posts_liked_by_moderators setup_posts diff --git a/lib/topic_view.rb b/lib/topic_view.rb index 06e5d0921d..0e73dd8af3 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -66,7 +66,7 @@ class TopicView filter_posts(options) - if @posts.present? + if @posts if (added_fields = User.whitelisted_user_custom_fields(@guardian)).present? @user_custom_fields = User.custom_fields_for_ids(@posts.pluck(:user_id), added_fields) end @@ -388,7 +388,7 @@ class TopicView max = [max, post_count].min - return @posts = [] if min > max + return @posts = Post.none if min > max min = [[min, max].min, 0].max From 0ce909833922d533cd1db0bd0c273036cb94d31b Mon Sep 17 00:00:00 2001 From: Leo McArdle Date: Thu, 7 Sep 2017 15:17:27 +0100 Subject: [PATCH 056/159] add admin-user-details plugin outlet (#5141) --- app/assets/javascripts/admin/templates/user-index.hbs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index da712cd51e..e00fbfedf5 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -174,6 +174,8 @@ {{/if}} +{{plugin-outlet name="admin-user-details" args=(hash model=model)}} +

    {{i18n 'admin.user.permissions'}}

    From d04aa5c7af9ec6f4c703fba41afbd1607efca4c3 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 7 Sep 2017 16:55:36 +0200 Subject: [PATCH 057/159] FIX: component height computation was wrong --- app/assets/javascripts/discourse/components/select-box.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/components/select-box.js.es6 b/app/assets/javascripts/discourse/components/select-box.js.es6 index 0fa6c33c10..eb4f9485dd 100644 --- a/app/assets/javascripts/discourse/components/select-box.js.es6 +++ b/app/assets/javascripts/discourse/components/select-box.js.es6 @@ -91,7 +91,7 @@ export default Ember.Component.extend({ let options = { left: "auto", bottom: "auto", left: "auto", top: "auto" }; const headerHeight = this.$(".select-box-header").outerHeight(false); const filterHeight = this.$(".select-box-filter").outerHeight(false); - const collectionHeight = this.$(".select-box-collection").outerHeight(false); + const bodyHeight = this.$(".select-box-body").outerHeight(false); const windowWidth = $(window).width(); const windowHeight = $(window).height(); const boundingRect = this.$()[0].getBoundingClientRect(); @@ -116,7 +116,7 @@ export default Ember.Component.extend({ } } - const componentHeight = this.get("verticalOffset") + collectionHeight + filterHeight + headerHeight; + const componentHeight = this.get("verticalOffset") + bodyHeight + headerHeight; const hasBelowSpace = windowHeight - offsetTop - componentHeight > 0; if (hasBelowSpace) { this.$().addClass("is-below"); From ca58a8228c0e402a485342128c552e6d29da8e21 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 7 Sep 2017 16:56:00 +0200 Subject: [PATCH 058/159] minor css fixes on select-box --- .../discourse/components/select-box.js.es6 | 1 - .../common/components/category-select-box.scss | 3 ++- .../stylesheets/common/components/select-box.scss | 7 +------ .../common/components/topic-notifications.scss | 6 ++++++ app/assets/stylesheets/common/topic-timeline.scss | 12 ------------ 5 files changed, 9 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/discourse/components/select-box.js.es6 b/app/assets/javascripts/discourse/components/select-box.js.es6 index eb4f9485dd..9a49815154 100644 --- a/app/assets/javascripts/discourse/components/select-box.js.es6 +++ b/app/assets/javascripts/discourse/components/select-box.js.es6 @@ -173,7 +173,6 @@ export default Ember.Component.extend({ const computedHeight = this.$().outerHeight(false); this.$(".select-box-filter").css("height", computedHeight); - this.$(".select-box-header").css("height", computedHeight); if (this.get("expanded")) { if (this.get("scrollableParent").length === 1) { diff --git a/app/assets/stylesheets/common/components/category-select-box.scss b/app/assets/stylesheets/common/components/category-select-box.scss index a68634949d..4f0d681405 100644 --- a/app/assets/stylesheets/common/components/category-select-box.scss +++ b/app/assets/stylesheets/common/components/category-select-box.scss @@ -23,6 +23,7 @@ } .category-status { + color: $primary; -webkit-box-flex: 0; -ms-flex: 1 1 auto; flex: 1 1 auto; @@ -32,7 +33,7 @@ -webkit-box-flex: 0; -ms-flex: 1 1 auto; flex: 1 1 auto; - color: $primary; + color: #919191; font-size: 0.857em; line-height: 16px; } diff --git a/app/assets/stylesheets/common/components/select-box.scss b/app/assets/stylesheets/common/components/select-box.scss index d043e1623a..ddf4286f6c 100644 --- a/app/assets/stylesheets/common/components/select-box.scss +++ b/app/assets/stylesheets/common/components/select-box.scss @@ -99,6 +99,7 @@ justify-content: space-between; padding-left: 10px; padding-right: 10px; + height: inherit; &.is-focused { border: 1px solid $tertiary; @@ -164,12 +165,6 @@ &:hover { background: $highlight-medium; } - - &.is-selected { - a { - background: $highlight-medium; - } - } } .select-box-collection { diff --git a/app/assets/stylesheets/common/components/topic-notifications.scss b/app/assets/stylesheets/common/components/topic-notifications.scss index 8d3f5b7255..95d8f2bb58 100644 --- a/app/assets/stylesheets/common/components/topic-notifications.scss +++ b/app/assets/stylesheets/common/components/topic-notifications.scss @@ -70,8 +70,13 @@ .texts { line-height: 18px; flex: 1; + align-items: flex-start; + display: flex; + flex-wrap: wrap; + flex-direction: column; .title { + flex: 1; font-weight: bold; display: block; font-size: 1em; @@ -79,6 +84,7 @@ } .desc { + flex: 1; font-size: 0.857em; font-weight: normal; color: #919191; diff --git a/app/assets/stylesheets/common/topic-timeline.scss b/app/assets/stylesheets/common/topic-timeline.scss index 532822ef4e..e98732bd54 100644 --- a/app/assets/stylesheets/common/topic-timeline.scss +++ b/app/assets/stylesheets/common/topic-timeline.scss @@ -177,18 +177,6 @@ } - &.timeline-fullscreen .topic-timeline .timeline-footer-controls ul.dropdown-menu { - width: auto; - min-width: 250px; - right: auto; - .desc { - display: none; - } - .title { - padding-left: 0; - } - } - .topic-timeline { margin-left: 3em; width: 150px; From 633d2fde4594d5b3f73375f4f713f622147bd724 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 7 Sep 2017 16:56:16 +0200 Subject: [PATCH 059/159] removes select-box minWidth property Using css offers more flexibility --- app/assets/javascripts/discourse/components/select-box.js.es6 | 3 --- .../discourse/components/topic-notifications.js.es6 | 2 -- app/assets/stylesheets/common/components/select-box.scss | 1 + 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/components/select-box.js.es6 b/app/assets/javascripts/discourse/components/select-box.js.es6 index 9a49815154..93a8223512 100644 --- a/app/assets/javascripts/discourse/components/select-box.js.es6 +++ b/app/assets/javascripts/discourse/components/select-box.js.es6 @@ -41,7 +41,6 @@ export default Ember.Component.extend({ selectBoxHeaderComponent: "select-box/select-box-header", selectBoxCollectionComponent: "select-box/select-box-collection", - minWidth: 220, collectionHeight: 200, verticalOffset: 0, horizontalOffset: 0, @@ -167,8 +166,6 @@ export default Ember.Component.extend({ this._removeFixedPosition(); } - this.$().css("min-width", this.get("minWidth")); - const computedWidth = this.$().outerWidth(false); const computedHeight = this.$().outerHeight(false); diff --git a/app/assets/javascripts/discourse/components/topic-notifications.js.es6 b/app/assets/javascripts/discourse/components/topic-notifications.js.es6 index 3d19b3e7c7..70e2a48133 100644 --- a/app/assets/javascripts/discourse/components/topic-notifications.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-notifications.js.es6 @@ -16,8 +16,6 @@ export default DropdownSelectBoxComponent.extend({ showFullTitle: true, fullWidthOnMobile: true, - minWidth: "auto", - @on("init") _setInitialNotificationLevel() { this.set("value", this.get("topic.details.notification_level")); diff --git a/app/assets/stylesheets/common/components/select-box.scss b/app/assets/stylesheets/common/components/select-box.scss index ddf4286f6c..745d7b2fee 100644 --- a/app/assets/stylesheets/common/components/select-box.scss +++ b/app/assets/stylesheets/common/components/select-box.scss @@ -9,6 +9,7 @@ flex-direction: column; position: relative; height: 34px; + min-width: 220px; &.small { height: 28px; From 0374dc1a0e30e892c8bc28b872818c696c078833 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 7 Sep 2017 16:56:38 +0200 Subject: [PATCH 060/159] FIX: keys were incorrectly set to category --- .../javascripts/discourse/components/topic-notifications.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/components/topic-notifications.js.es6 b/app/assets/javascripts/discourse/components/topic-notifications.js.es6 index 70e2a48133..5187ebb0b6 100644 --- a/app/assets/javascripts/discourse/components/topic-notifications.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-notifications.js.es6 @@ -9,7 +9,7 @@ export default DropdownSelectBoxComponent.extend({ content: topicLevels, - i18nPrefix: 'category.notifications', + i18nPrefix: 'topic.notifications', i18nPostfix: '', textKey: "key", From 70d0e175a260d797732bbd0c82251c0868495247 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 7 Sep 2017 17:03:48 +0200 Subject: [PATCH 061/159] FIX: default select-box-row should be aligned vertically --- app/assets/stylesheets/common/components/select-box.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/common/components/select-box.scss b/app/assets/stylesheets/common/components/select-box.scss index 745d7b2fee..b60bc206b5 100644 --- a/app/assets/stylesheets/common/components/select-box.scss +++ b/app/assets/stylesheets/common/components/select-box.scss @@ -147,6 +147,7 @@ display: -webkit-box; display: -ms-flexbox; display: flex; + align-items: center; -webkit-box-pack: start; -ms-flex-pack: start; justify-content: flex-start; From 8e88bf019f0d54a0fdf9624d858a6b93a09cf585 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 7 Sep 2017 11:09:49 -0400 Subject: [PATCH 062/159] Add `span` to various labels so they can be targetted --- .../templates/components/category-drop.hbs | 2 +- .../discourse/widgets/header.js.es6 | 18 ++++++++++++------ .../javascripts/discourse/widgets/link.js.es6 | 8 +++++++- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/components/category-drop.hbs b/app/assets/javascripts/discourse/templates/components/category-drop.hbs index d660be87db..ba5062a429 100644 --- a/app/assets/javascripts/discourse/templates/components/category-drop.hbs +++ b/app/assets/javascripts/discourse/templates/components/category-drop.hbs @@ -4,7 +4,7 @@ {{#if category.read_restricted}} {{d-icon "lock"}} {{/if}} - {{category.name}} + {{category.name}} {{else}} {{#if noSubcategories}} diff --git a/app/assets/javascripts/discourse/widgets/header.js.es6 b/app/assets/javascripts/discourse/widgets/header.js.es6 index c131128f89..1c7c1e6ce6 100644 --- a/app/assets/javascripts/discourse/widgets/header.js.es6 +++ b/app/assets/javascripts/discourse/widgets/header.js.es6 @@ -37,9 +37,12 @@ createWidget('header-notifications', { const unreadNotifications = user.get('unread_notifications'); if (!!unreadNotifications) { - contents.push(this.attach('link', { action: attrs.action, - className: 'badge-notification unread-notifications', - rawLabel: unreadNotifications })); + contents.push(this.attach('link', { + action: attrs.action, + className: 'badge-notification unread-notifications', + rawLabel: unreadNotifications, + omitSpan: true + })); } const unreadPMs = user.get('unread_private_messages'); @@ -55,9 +58,12 @@ createWidget('header-notifications', { } }; - contents.push(this.attach('link', { action: attrs.action, - className: 'badge-notification unread-private-messages', - rawLabel: unreadPMs })); + contents.push(this.attach('link', { + action: attrs.action, + className: 'badge-notification unread-private-messages', + rawLabel: unreadPMs, + omitSpan: true + })); } return contents; diff --git a/app/assets/javascripts/discourse/widgets/link.js.es6 b/app/assets/javascripts/discourse/widgets/link.js.es6 index c6426a8623..5774ccc763 100644 --- a/app/assets/javascripts/discourse/widgets/link.js.es6 +++ b/app/assets/javascripts/discourse/widgets/link.js.es6 @@ -54,7 +54,13 @@ export default createWidget('link', { } if (!attrs.hideLabel) { - result.push(this.label(attrs)); + let label = this.label(attrs); + + if (attrs.omitSpan) { + result.push(label); + } else { + result.push(h('span.d-label', label)); + } } const currentUser = this.currentUser; From 0ba789de176e20a99a178d53fbc3cdb160044cf3 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 7 Sep 2017 11:18:59 -0400 Subject: [PATCH 063/159] Allow for customization of header dropdown icons --- .../javascripts/discourse-common/lib/icon-library.js.es6 | 4 +++- .../javascripts/discourse/components/category-drop.js.es6 | 2 +- app/assets/javascripts/discourse/components/tag-drop.js.es6 | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 index bc8abf3c35..fcf3ce404e 100644 --- a/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/icon-library.js.es6 @@ -6,7 +6,9 @@ const REPLACEMENTS = { 'd-muted': 'times-circle', 'd-regular': 'circle-o', 'd-watching': 'exclamation-circle', - 'd-watching-first': 'dot-circle-o' + 'd-watching-first': 'dot-circle-o', + 'd-drop-expanded': 'caret-down', + 'd-drop-collapsed': 'caret-right', }; export function renderIcon(renderType, id, params) { diff --git a/app/assets/javascripts/discourse/components/category-drop.js.es6 b/app/assets/javascripts/discourse/components/category-drop.js.es6 index 7a79c6b915..379e326ac8 100644 --- a/app/assets/javascripts/discourse/components/category-drop.js.es6 +++ b/app/assets/javascripts/discourse/components/category-drop.js.es6 @@ -12,7 +12,7 @@ export default Ember.Component.extend({ @computed('expanded') expandIcon(expanded) { - return expanded ? 'caret-down' : 'caret-right'; + return expanded ? 'd-drop-expanded' : 'd-drop-collapsed'; }, allCategoriesUrl: function() { diff --git a/app/assets/javascripts/discourse/components/tag-drop.js.es6 b/app/assets/javascripts/discourse/components/tag-drop.js.es6 index 698106c061..201d9a134d 100644 --- a/app/assets/javascripts/discourse/components/tag-drop.js.es6 +++ b/app/assets/javascripts/discourse/components/tag-drop.js.es6 @@ -22,7 +22,7 @@ export default Ember.Component.extend({ @computed('expanded') expandedIcon(expanded) { - return expanded ? 'caret-down' : 'caret-right'; + return expanded ? 'd-drop-expanded' : 'd-drop-collapsed'; }, @computed('tagId') From afc075d93b75d7b50b89f37ee4bcf6ebdaf2544b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 7 Sep 2017 11:41:19 -0400 Subject: [PATCH 064/159] UX: Convert bookmark icon from CSS to a proper icon using our helper --- .../discourse/widgets/post-menu.js.es6 | 18 ++++++++---------- app/assets/stylesheets/desktop/topic-post.scss | 18 +++--------------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index f5fefe0d91..deda3b167a 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -173,20 +173,18 @@ registerButton('reply', attrs => { registerButton('bookmark', attrs => { if (!attrs.canBookmark) { return; } - let iconClass = 'read-icon'; - let buttonClass = 'bookmark'; - let tooltip = 'bookmarks.not_bookmarked'; + let className = 'bookmark'; if (attrs.bookmarked) { - iconClass += ' bookmarked'; - buttonClass += ' bookmarked'; - tooltip = 'bookmarks.created'; + className += ' bookmarked'; } - return { action: 'toggleBookmark', - title: tooltip, - className: buttonClass, - contents: h('div', { className: iconClass }) }; + return { + action: 'toggleBookmark', + title: attrs.bookmarked ? "bookmarks.created" : "bookmarks.not_bookmarked", + className, + icon: 'bookmark' + }; }); registerButton('admin', attrs => { diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 5a05da205b..b0526c878a 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -161,22 +161,10 @@ nav.post-controls { box-shadow: none; } - &.bookmark {padding: 8px 11px; } - - .read-icon { - &:before { - font-family: "FontAwesome"; - content: "\f02e"; - } - &.unseen { - &:before { - content: "\f097"; - } - } + &.bookmark { + padding: 8px 11px; &.bookmarked { - &:before { - color: $tertiary; - } + color: $tertiary; } } } From 657440b8beffddace50a94fb3213e51663ec9d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 7 Sep 2017 18:41:56 +0200 Subject: [PATCH 065/159] FIX: consecutive_visits query wasn't properly setting 'granted_at' (3rd time's a charm) --- lib/badge_queries.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/badge_queries.rb b/lib/badge_queries.rb index 515e44cfa8..dd70192c08 100644 --- a/lib/badge_queries.rb +++ b/lib/badge_queries.rb @@ -235,15 +235,20 @@ SQL def self.consecutive_visits(days) <<~SQL WITH consecutive_visits AS ( - SELECT user_id, visited_at - (DENSE_RANK() OVER (PARTITION BY user_id ORDER BY visited_at))::int "start" + SELECT user_id + , visited_at + , visited_at - (DENSE_RANK() OVER (PARTITION BY user_id ORDER BY visited_at))::int s FROM user_visits ), visits AS ( - SELECT user_id, "start", DENSE_RANK() OVER (PARTITION BY user_id ORDER BY "start") "rank" + SELECT user_id + , MIN(visited_at) "start" + , DENSE_RANK() OVER (PARTITION BY user_id ORDER BY s) "rank" FROM consecutive_visits - GROUP BY user_id, "start" + GROUP BY user_id, s HAVING COUNT(*) >= #{days} ) - SELECT user_id, "start" + interval '#{days} days' "granted_at" + SELECT user_id + , "start" + interval '#{days} days' "granted_at" FROM visits WHERE "rank" = 1 SQL From 5054065e81ec40474d30105451e882a3edc9fbc8 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 7 Sep 2017 19:18:27 +0200 Subject: [PATCH 066/159] FIX: broken spec --- .../discourse/components/topic-notifications.js.es6 | 12 +++++++++--- .../discourse/components/topic-timeline.js.es6 | 1 - 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/components/topic-notifications.js.es6 b/app/assets/javascripts/discourse/components/topic-notifications.js.es6 index 5187ebb0b6..a1bdc7cbc8 100644 --- a/app/assets/javascripts/discourse/components/topic-notifications.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-notifications.js.es6 @@ -25,7 +25,9 @@ export default DropdownSelectBoxComponent.extend({ _bindGlobalLevelChanged() { this.appEvents.on("topic-notifications-button:changed", (msg) => { if (msg.type === "notification") { - this.set("value", msg.id); + if (this.get("topic.details.notification_level") !== msg.id) { + this.get("topic.details").updateNotifications(msg.id); + } } }); }, @@ -37,7 +39,6 @@ export default DropdownSelectBoxComponent.extend({ @observes("value") _notificationLevelChanged() { - this.get("topic.details").updateNotifications(this.get("value")); this.appEvents.trigger('topic-notifications-button:changed', {type: 'notification', id: this.get("value")}); }, @@ -47,6 +48,11 @@ export default DropdownSelectBoxComponent.extend({ return iconHTML(details.icon, {class: details.key}).htmlSafe(); }, + @observes("topic.details.notification_level") + _content() { + this.set("value", this.get("topic.details.notification_level")); + }, + @computed("topic.details.notification_level", "showFullTitle") generatedHeadertext(notificationLevel, showFullTitle) { if (showFullTitle) { @@ -75,5 +81,5 @@ export default DropdownSelectBoxComponent.extend({
    `; }; - }.property(), + }.property() }); diff --git a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 b/app/assets/javascripts/discourse/components/topic-timeline.js.es6 index cc72ba7f2c..cfa9340e94 100644 --- a/app/assets/javascripts/discourse/components/topic-timeline.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-timeline.js.es6 @@ -84,6 +84,5 @@ export default MountWidget.extend(Docking, { } this.dispatch('topic:current-post-scrolled', 'timeline-scrollarea'); - this.dispatch('topic-notifications-button:changed', 'topic-notifications-button'); } }); From 3c0a9d4d195f9cf438a13e2431b6db1a15dfbc27 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 7 Sep 2017 19:55:56 +0200 Subject: [PATCH 067/159] FIX: topic-notifications taking too much space in timeline --- app/assets/stylesheets/common/topic-timeline.scss | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/app/assets/stylesheets/common/topic-timeline.scss b/app/assets/stylesheets/common/topic-timeline.scss index e98732bd54..36605f5f4b 100644 --- a/app/assets/stylesheets/common/topic-timeline.scss +++ b/app/assets/stylesheets/common/topic-timeline.scss @@ -121,12 +121,6 @@ bottom: 10px; left: 10px; - .dropdown-menu { - position: absolute; - left: 0; - bottom: 30px; - } - button, .btn-group { float: none; display: inline-block; @@ -204,11 +198,8 @@ margin-right: 0; } - ul.dropdown-menu { - right: 0.5em; - top: auto; - bottom: 25px; - left: auto; + .topic-notifications { + min-width: auto; } } From d2d1c29f48a82319673efdde12d3afa1296ed2e6 Mon Sep 17 00:00:00 2001 From: Joffrey JAFFEUX Date: Thu, 7 Sep 2017 20:13:32 +0200 Subject: [PATCH 068/159] FIX: topic-notifications was taking too much space on mobile --- .../stylesheets/common/components/topic-notifications.scss | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/components/topic-notifications.scss b/app/assets/stylesheets/common/components/topic-notifications.scss index 95d8f2bb58..489620e5a1 100644 --- a/app/assets/stylesheets/common/components/topic-notifications.scss +++ b/app/assets/stylesheets/common/components/topic-notifications.scss @@ -1,5 +1,8 @@ -#topic-footer-buttons .topic-notifications .btn { - margin: 0; +#topic-footer-buttons .topic-notifications { + min-width: auto; + .btn { + margin: 0; + } } #topic-footer-buttons p.reason { From e18360056396c452f8395be26a3a6632df61741b Mon Sep 17 00:00:00 2001 From: Leo McArdle Date: Wed, 30 Aug 2017 16:58:40 +0100 Subject: [PATCH 069/159] FIX: redirect loop for new users visiting /new-topic using full screen login --- app/assets/javascripts/discourse.js.es6 | 1 + app/assets/javascripts/discourse/routes/new-message.js.es6 | 6 +++++- app/assets/javascripts/discourse/routes/new-topic.js.es6 | 7 ++++++- app/views/common/_discourse_javascript.html.erb | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse.js.es6 b/app/assets/javascripts/discourse.js.es6 index cee3fcbd57..f4bb1fdb02 100644 --- a/app/assets/javascripts/discourse.js.es6 +++ b/app/assets/javascripts/discourse.js.es6 @@ -8,6 +8,7 @@ const Discourse = Ember.Application.extend({ _docTitle: document.title, RAW_TEMPLATES: {}, __widget_helpers: {}, + showingSignup: false, getURL(url) { if (!url) return url; diff --git a/app/assets/javascripts/discourse/routes/new-message.js.es6 b/app/assets/javascripts/discourse/routes/new-message.js.es6 index 06d929c552..91269c4a5b 100644 --- a/app/assets/javascripts/discourse/routes/new-message.js.es6 +++ b/app/assets/javascripts/discourse/routes/new-message.js.es6 @@ -34,7 +34,11 @@ export default Discourse.Route.extend({ }); } else { $.cookie('destination_url', window.location.href); - this.replaceWith('login'); + if (Discourse.showingSignup) { + Discourse.showingSignup = false; + } else { + self.replaceWith('login'); + } } } diff --git a/app/assets/javascripts/discourse/routes/new-topic.js.es6 b/app/assets/javascripts/discourse/routes/new-topic.js.es6 index 88dcd61368..0459e75d4c 100644 --- a/app/assets/javascripts/discourse/routes/new-topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/new-topic.js.es6 @@ -14,7 +14,12 @@ export default Discourse.Route.extend({ } else { // User is not logged in $.cookie('destination_url', window.location.href); - self.replaceWith('login'); + if (Discourse.showingSignup) { + // We're showing the sign up modal + Discourse.showingSignup = false; + } else { + self.replaceWith('login'); + } } } }); diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index f9b0146793..72b3da6068 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -21,6 +21,7 @@