diff --git a/.github/workflows/ember.yml b/.github/workflows/ember.yml index 07080e58e4..538b16b316 100644 --- a/.github/workflows/ember.yml +++ b/.github/workflows/ember.yml @@ -9,7 +9,7 @@ on: jobs: build: name: run - if: false # something is broken with this job so skip for now + if: true runs-on: ubuntu-latest container: discourse/discourse_test:release timeout-minutes: 40 diff --git a/Gemfile b/Gemfile index 48f7879ff6..9c2ff7cdbb 100644 --- a/Gemfile +++ b/Gemfile @@ -105,7 +105,10 @@ gem 'omniauth-oauth2', require: false gem 'omniauth-google-oauth2' -gem 'oj' +# Pinning oj until https://github.com/ohler55/oj/issues/699 is resolved. +# Segfaults and stuck processes after upgrading. +gem 'oj', '3.13.2' + gem 'pg' gem 'mini_sql' gem 'pry-rails', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 1aed5f27eb..152aabb99e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -52,7 +52,7 @@ GEM activerecord (>= 3.2, < 7.0) rake (>= 10.4, < 14.0) ast (2.4.2) - aws-eventstream (1.1.1) + aws-eventstream (1.2.0) aws-partitions (1.432.0) aws-sdk-core (3.112.1) aws-eventstream (~> 1, >= 1.0.2) @@ -158,7 +158,7 @@ GEM fast_blank (1.0.1) fast_xs (0.8.0) fastimage (2.2.5) - ffi (1.15.3) + ffi (1.15.4) fspath (3.1.2) gc_tracer (1.5.1) globalid (0.5.2) @@ -550,7 +550,7 @@ DEPENDENCIES multi_json mustache nokogiri - oj + oj (= 3.13.2) omniauth omniauth-facebook omniauth-github @@ -609,4 +609,4 @@ DEPENDENCIES yaml-lint BUNDLED WITH - 2.2.19 + 2.2.26 diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js index c319408bbc..eb2d079853 100644 --- a/app/assets/javascripts/discourse/app/components/composer-editor.js +++ b/app/assets/javascripts/discourse/app/components/composer-editor.js @@ -219,6 +219,8 @@ export default Component.extend(ComposerUpload, { } this._bindUploadTarget(); + this._bindMobileUploadButton(); + this.appEvents.trigger("composer:will-open"); }, @@ -607,6 +609,7 @@ export default Component.extend(ComposerUpload, { @on("willDestroyElement") _composerClosed() { + this._unbindMobileUploadButton(); this.appEvents.trigger("composer:will-close"); next(() => { // need to wait a bit for the "slide down" transition of the composer diff --git a/app/assets/javascripts/discourse/app/lib/plugin-api.js b/app/assets/javascripts/discourse/app/lib/plugin-api.js index 71a6210d91..35877fa5aa 100644 --- a/app/assets/javascripts/discourse/app/lib/plugin-api.js +++ b/app/assets/javascripts/discourse/app/lib/plugin-api.js @@ -86,6 +86,25 @@ import { addSearchSuggestion } from "discourse/widgets/search-menu-results"; // If you add any methods to the API ensure you bump up this number const PLUGIN_API_VERSION = "0.12.2"; +// This helper prevents us from applying the same `modifyClass` over and over in test mode. +function canModify(klass, type, resolverName, changes) { + if (!changes.pluginId) { + // eslint-disable-next-line no-console + console.warn( + "To prevent errors, add a `pluginId` key to your changes when calling `modifyClass`" + ); + return true; + } + + let key = "_" + type + "/" + changes.pluginId + "/" + resolverName; + if (klass.class[key]) { + return false; + } else { + klass.class[key] = 1; + return true; + } +} + class PluginApi { constructor(version, container) { this.version = version; @@ -138,10 +157,14 @@ class PluginApi { /** * Allows you to overwrite or extend methods in a class. * + * You should add a `pluginId` property to identify your plugin + * to help Discourse reload classes properly. + * * For example: * * ``` * api.modifyClass('controller:composer', { + * pluginId: 'my-plugin', * actions: { * newActionHere() { } * } @@ -150,9 +173,15 @@ class PluginApi { **/ modifyClass(resolverName, changes, opts) { const klass = this._resolveClass(resolverName, opts); - if (klass) { + if (!klass) { + return; + } + + if (canModify(klass, "member", resolverName, changes)) { + delete changes.pluginId; klass.class.reopen(changes); } + return klass; } @@ -169,9 +198,15 @@ class PluginApi { **/ modifyClassStatic(resolverName, changes, opts) { const klass = this._resolveClass(resolverName, opts); - if (klass) { + if (!klass) { + return; + } + + if (canModify(klass, "static", resolverName, changes)) { + delete changes.pluginId; klass.class.reopenClass(changes); } + return klass; } diff --git a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js index 885c86b3f9..788a4416ee 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload-uppy.js @@ -46,11 +46,6 @@ export default Mixin.create({ @on("willDestroyElement") _unbindUploadTarget() { - this.mobileUploadButton?.removeEventListener( - "click", - this.mobileUploadButtonEventListener - ); - this.fileInputEl?.removeEventListener( "change", this.fileInputEventListener @@ -86,7 +81,6 @@ export default Mixin.create({ this._unbindUploadTarget(); this._bindFileInputChangeListener(); this._bindPasteListener(); - this._bindMobileUploadButton(); this._uppyInstance = new Uppy({ id: this.uppyId, @@ -153,6 +147,10 @@ export default Mixin.create({ }); this._uppyInstance.on("progress", (progress) => { + if (this.isDestroying || this.isDestroyed) { + return; + } + this.set("uploadProgress", progress); }); diff --git a/app/assets/javascripts/discourse/app/mixins/composer-upload.js b/app/assets/javascripts/discourse/app/mixins/composer-upload.js index c3ada1ccc0..01ab335dda 100644 --- a/app/assets/javascripts/discourse/app/mixins/composer-upload.js +++ b/app/assets/javascripts/discourse/app/mixins/composer-upload.js @@ -327,8 +327,6 @@ export default Mixin.create({ } }); }); - - this._bindMobileUploadButton(); }, _bindMobileUploadButton() { @@ -345,13 +343,15 @@ export default Mixin.create({ } }, - @on("willDestroyElement") - _unbindUploadTarget() { + _unbindMobileUploadButton() { this.mobileUploadButton?.removeEventListener( "click", this.mobileUploadButtonEventListener ); + }, + @on("willDestroyElement") + _unbindUploadTarget() { this._validUploads = 0; const $uploadTarget = $(this.element); try { diff --git a/app/assets/javascripts/discourse/app/mixins/upload.js b/app/assets/javascripts/discourse/app/mixins/upload.js index 780b37536b..ff7a73be57 100644 --- a/app/assets/javascripts/discourse/app/mixins/upload.js +++ b/app/assets/javascripts/discourse/app/mixins/upload.js @@ -103,6 +103,10 @@ export default Mixin.create({ }); $upload.on("fileuploadprogressall", (e, data) => { + if (this.isDestroying || this.isDestroyed) { + return; + } + const progress = parseInt((data.loaded / data.total) * 100, 10); this.set("uploadProgress", progress); }); diff --git a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js index 101fce1ccc..5c9cbb8aeb 100644 --- a/app/assets/javascripts/discourse/app/mixins/uppy-upload.js +++ b/app/assets/javascripts/discourse/app/mixins/uppy-upload.js @@ -128,6 +128,10 @@ export default Mixin.create({ this._uppyInstance.use(UppyChecksum, { capabilities: this.capabilities }); this._uppyInstance.on("progress", (progress) => { + if (this.isDestroying || this.isDestroyed) { + return; + } + this.set("uploadProgress", progress); }); diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-read.js b/app/assets/javascripts/discourse/app/routes/user-activity-read.js index 1cfd2c9856..7622ea03ce 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-read.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-read.js @@ -1,5 +1,6 @@ import UserAction from "discourse/models/user-action"; import UserTopicListRoute from "discourse/routes/user-topic-list"; +import { action } from "@ember/object"; export default UserTopicListRoute.extend({ userActionType: UserAction.TYPES.topics, @@ -9,4 +10,9 @@ export default UserTopicListRoute.extend({ filter: "read", }); }, + + @action + refresh() { + this.refresh(); + }, }); diff --git a/app/assets/javascripts/discourse/app/routes/user-activity-topics.js b/app/assets/javascripts/discourse/app/routes/user-activity-topics.js index 3e736f3514..7a08b365cf 100644 --- a/app/assets/javascripts/discourse/app/routes/user-activity-topics.js +++ b/app/assets/javascripts/discourse/app/routes/user-activity-topics.js @@ -1,5 +1,6 @@ import UserAction from "discourse/models/user-action"; import UserTopicListRoute from "discourse/routes/user-topic-list"; +import { action } from "@ember/object"; export default UserTopicListRoute.extend({ userActionType: UserAction.TYPES.topics, @@ -10,4 +11,9 @@ export default UserTopicListRoute.extend({ "topics/created-by/" + this.modelFor("user").get("username_lower"), }); }, + + @action + refresh() { + this.refresh(); + }, }); diff --git a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-row.js b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-row.js index 7eac79d9ae..cb860e66f8 100644 --- a/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-row.js +++ b/app/assets/javascripts/select-kit/addon/components/select-kit/select-kit-row.js @@ -158,6 +158,10 @@ export default Component.extend(UtilsMixin, { } }, + focusIn(event) { + event.stopImmediatePropagation(); + }, + keyDown(event) { if (this.selectKit.isExpanded) { if (event.key === "Backspace") { diff --git a/app/models/topic_embed.rb b/app/models/topic_embed.rb index 380daafb5b..2b2523aa12 100644 --- a/app/models/topic_embed.rb +++ b/app/models/topic_embed.rb @@ -27,10 +27,10 @@ class TopicEmbed < ActiveRecord::Base end # Import an article from a source (RSS/Atom/Other) - def self.import(user, url, title, contents, category_id: nil) + def self.import(user, url, title, contents, category_id: nil, cook_method: nil) return unless url =~ /^https?\:\/\// - if SiteSetting.embed_truncate + if SiteSetting.embed_truncate && cook_method.nil? contents = first_paragraph_from(contents) end contents ||= '' @@ -47,7 +47,7 @@ class TopicEmbed < ActiveRecord::Base Topic.transaction do eh = EmbeddableHost.record_for_url(url) - cook_method = if SiteSetting.embed_support_markdown + cook_method ||= if SiteSetting.embed_support_markdown Post.cook_methods[:regular] else Post.cook_methods[:raw_html] diff --git a/config/initializers/100-sidekiq.rb b/config/initializers/100-sidekiq.rb index 9311b1633f..d4c1ddfbee 100644 --- a/config/initializers/100-sidekiq.rb +++ b/config/initializers/100-sidekiq.rb @@ -111,5 +111,8 @@ class SidekiqLogsterReporter < Sidekiq::ExceptionHandler::Logger end end -Sidekiq.error_handlers.clear +unless Rails.env.development? + Sidekiq.error_handlers.clear +end + Sidekiq.error_handlers << SidekiqLogsterReporter.new diff --git a/lib/tasks/release_note.rake b/lib/tasks/release_note.rake index 01bc01b35b..0b75aae522 100644 --- a/lib/tasks/release_note.rake +++ b/lib/tasks/release_note.rake @@ -17,7 +17,7 @@ task "release_note:generate", :from, :to, :repo do |t, args| changes = find_changes(repo, args[:from], args[:to]) CHANGE_TYPES.each do |ct| - print_changes(ct[:heading], changes[ct]) + print_changes(ct[:heading], changes[ct], "###") end if changes.values.all?(&:empty?) @@ -54,11 +54,10 @@ task "release_note:plugins:generate", :from, :to, :plugin_glob, :org do |t, args next end - puts "## #{name}\n\n" + puts "### #{name}\n\n" CHANGE_TYPES.each do |ct| - print_changes(ct[:heading], changes[ct]) + print_changes(ct[:heading], changes[ct], "####") end - puts "---", "" end puts "(No changes found in #{no_changes_repos.join(", ")})" @@ -99,10 +98,10 @@ def find_changes(repo, from, to) changes end -def print_changes(heading, changes) +def print_changes(heading, changes, importance) return if changes.length == 0 - puts "### #{heading}", "" + puts "#{importance} #{heading}", "" puts changes.to_a, "" end diff --git a/lib/version.rb b/lib/version.rb index f444aec5b0..67b960944b 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -10,7 +10,7 @@ module Discourse MAJOR = 2 MINOR = 8 TINY = 0 - PRE = 'beta5' + PRE = 'beta6' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end diff --git a/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 b/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 index a086779131..11b2117f8f 100644 --- a/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 +++ b/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 @@ -15,6 +15,7 @@ function initializeDetails(api) { }); api.modifyClass("controller:composer", { + pluginId: "discourse-details", actions: { insertDetails() { this.toolbarEvent.applySurround( diff --git a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 index 68b94e7aeb..30c1b5c510 100644 --- a/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 +++ b/plugins/discourse-local-dates/assets/javascripts/initializers/discourse-local-dates.js.es6 @@ -82,6 +82,7 @@ function initializeDiscourseLocalDates(api) { }); api.modifyClass("component:d-editor", { + pluginId: "discourse-local-dates", actions: { insertDiscourseLocalDate(toolbarEvent) { showModal("discourse-local-dates-create-modal").setProperties({ diff --git a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 index 156c4dd0cd..89d8c460a1 100644 --- a/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 +++ b/plugins/discourse-narrative-bot/assets/javascripts/initializers/new-user-narrative.js.es6 @@ -1,12 +1,15 @@ import { ajax } from "discourse/lib/ajax"; import { withPluginApi } from "discourse/lib/plugin-api"; +const PLUGIN_ID = "new-user-narrative"; + function initialize(api) { const messageBus = api.container.lookup("message-bus:main"); const currentUser = api.getCurrentUser(); const appEvents = api.container.lookup("service:app-events"); api.modifyClass("component:site-header", { + pluginId: PLUGIN_ID, didInsertElement() { this._super(...arguments); this.dispatch("header:search-context-trigger", "header"); @@ -14,6 +17,8 @@ function initialize(api) { }); api.modifyClass("controller:topic", { + pluginId: PLUGIN_ID, + _togglePostBookmark(post) { // if we are talking to discobot then any bookmarks should just // be created without reminder options, to streamline the new user diff --git a/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 b/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 index abeb3056c6..ab5c59cfea 100644 --- a/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 +++ b/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 @@ -4,6 +4,7 @@ import { withPluginApi } from "discourse/lib/plugin-api"; function initializePollUIBuilder(api) { api.modifyClass("controller:composer", { + pluginId: "discourse-poll-ui-builder", @discourseComputed( "siteSettings.poll_enabled", "siteSettings.poll_minimum_trust_level_to_create", diff --git a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 index 6d9505704e..ab0fb09570 100644 --- a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 +++ b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 @@ -4,10 +4,30 @@ import { getRegister } from "discourse-common/lib/get-owner"; import { observes } from "discourse-common/utils/decorators"; import { withPluginApi } from "discourse/lib/plugin-api"; +const PLUGIN_ID = "discourse-poll"; +let _glued = []; +let _interval = null; + +function rerender() { + _glued.forEach((g) => g.queueRerender()); +} + +function cleanUpPolls() { + if (_interval) { + clearInterval(_interval); + _interval = null; + } + + _glued.forEach((g) => g.cleanUp()); + _glued = []; +} + function initializePolls(api) { const register = getRegister(api); + cleanUpPolls(); api.modifyClass("controller:topic", { + pluginId: PLUGIN_ID, subscribe() { this._super(...arguments); this.messageBus.subscribe("/polls/" + this.get("model.id"), (msg) => { @@ -23,14 +43,8 @@ function initializePolls(api) { }, }); - let _glued = []; - let _interval = null; - - function rerender() { - _glued.forEach((g) => g.queueRerender()); - } - api.modifyClass("model:post", { + pluginId: PLUGIN_ID, _polls: null, pollsObject: null, @@ -110,16 +124,6 @@ function initializePolls(api) { }); } - function cleanUpPolls() { - if (_interval) { - clearInterval(_interval); - _interval = null; - } - - _glued.forEach((g) => g.cleanUp()); - _glued = []; - } - api.includePostAttributes("polls", "polls_votes"); api.decorateCooked(attachPolls, { onlyStream: true, id: "discourse-poll" }); api.cleanupStream(cleanUpPolls); diff --git a/spec/models/topic_embed_spec.rb b/spec/models/topic_embed_spec.rb index decb502806..6a4e0025db 100644 --- a/spec/models/topic_embed_spec.rb +++ b/spec/models/topic_embed_spec.rb @@ -99,11 +99,22 @@ describe TopicEmbed do it "creates the topic in the category passed as a parameter" do Jobs.run_immediately! - SiteSetting.embed_unlisted = false imported_post = TopicEmbed.import(user, "http://eviltrout.com/abcd", title, "some random content", category_id: category.id) expect(imported_post.topic.category).not_to eq(embeddable_host.category) expect(imported_post.topic.category).to eq(category) end + + it "respects overriding the cook_method when asked" do + Jobs.run_immediately! + SiteSetting.embed_support_markdown = false + stub_request(:get, "https://www.youtube.com/watch?v=K56soYl0U1w") + .to_return(status: 200, body: "", headers: {}) + stub_request(:get, "https://www.youtube.com/embed/K56soYl0U1w") + .to_return(status: 200, body: "", headers: {}) + + imported_post = TopicEmbed.import(user, "http://eviltrout.com/abcd", title, "https://www.youtube.com/watch?v=K56soYl0U1w", cook_method: Post.cook_methods[:regular]) + expect(imported_post.cooked).to match(/onebox|iframe/) + end end context "post creation supports markdown rendering" do