diff --git a/app/assets/javascripts/discourse/app/components/category-list-item.js b/app/assets/javascripts/discourse/app/components/category-list-item.js
index eaef68762f2..2aef3d62ce4 100644
--- a/app/assets/javascripts/discourse/app/components/category-list-item.js
+++ b/app/assets/javascripts/discourse/app/components/category-list-item.js
@@ -1,4 +1,5 @@
 import Component from "@ember/component";
+import { tagName } from "@ember-decorators/component";
 import discourseComputed from "discourse-common/utils/decorators";
 
 const LIST_TYPE = {
@@ -6,10 +7,10 @@ const LIST_TYPE = {
   MUTED: "muted",
 };
 
-export default Component.extend({
-  tagName: "",
-  category: null,
-  listType: LIST_TYPE.NORMAL,
+@tagName("")
+export default class CategoryListItem extends Component {
+  category = null;
+  listType = LIST_TYPE.NORMAL;
 
   @discourseComputed("category.isHidden", "category.hasMuted", "listType")
   isHidden(isHiddenCategory, hasMuted, listType) {
@@ -17,7 +18,7 @@ export default Component.extend({
       (isHiddenCategory && listType === LIST_TYPE.NORMAL) ||
       (!hasMuted && listType === LIST_TYPE.MUTED)
     );
-  },
+  }
 
   @discourseComputed("category.isMuted", "listType")
   isMuted(isMutedCategory, listType) {
@@ -25,20 +26,20 @@ export default Component.extend({
       (isMutedCategory && listType === LIST_TYPE.NORMAL) ||
       (!isMutedCategory && listType === LIST_TYPE.MUTED)
     );
-  },
+  }
 
   @discourseComputed("topicTrackingState.messageCount")
   unreadTopicsCount() {
     return this.category.unreadTopicsCount;
-  },
+  }
 
   @discourseComputed("topicTrackingState.messageCount")
   newTopicsCount() {
     return this.category.newTopicsCount;
-  },
+  }
 
   @discourseComputed("category.path")
   slugPath(categoryPath) {
     return categoryPath.substring("/c/".length);
-  },
-});
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/category-name-fields.js b/app/assets/javascripts/discourse/app/components/category-name-fields.js
index 87d5ddb040f..edc8b634fd0 100644
--- a/app/assets/javascripts/discourse/app/components/category-name-fields.js
+++ b/app/assets/javascripts/discourse/app/components/category-name-fields.js
@@ -1,3 +1,3 @@
 import Component from "@ember/component";
 
-export default Component.extend({});
+export default class CategoryNameFields extends Component {}
diff --git a/app/assets/javascripts/discourse/app/components/category-permission-row.js b/app/assets/javascripts/discourse/app/components/category-permission-row.js
index b7a80a193e3..aa2d0a6b271 100644
--- a/app/assets/javascripts/discourse/app/components/category-permission-row.js
+++ b/app/assets/javascripts/discourse/app/components/category-permission-row.js
@@ -1,43 +1,45 @@
 import Component from "@ember/component";
 import { action } from "@ember/object";
 import { alias, equal } from "@ember/object/computed";
+import { classNames } from "@ember-decorators/component";
+import { observes } from "@ember-decorators/object";
 import PermissionType from "discourse/models/permission-type";
-import discourseComputed, { observes } from "discourse-common/utils/decorators";
+import discourseComputed from "discourse-common/utils/decorators";
 import I18n from "discourse-i18n";
 
 const EVERYONE = "everyone";
 
-export default Component.extend({
-  classNames: ["permission-row", "row-body"],
-  canCreate: equal("type", PermissionType.FULL),
-  everyonePermissionType: alias("everyonePermission.permission_type"),
+@classNames("permission-row", "row-body")
+export default class CategoryPermissionRow extends Component {
+  @equal("type", PermissionType.FULL) canCreate;
+  @alias("everyonePermission.permission_type") everyonePermissionType;
 
   @discourseComputed("type")
   canReply(value) {
     return (
       value === PermissionType.CREATE_POST || value === PermissionType.FULL
     );
-  },
+  }
 
   @discourseComputed("type")
   canReplyIcon() {
     return this.canReply ? "check-square" : "far-square";
-  },
+  }
 
   @discourseComputed("type")
   canCreateIcon() {
     return this.canCreate ? "check-square" : "far-square";
-  },
+  }
 
   @discourseComputed("type")
   replyGranted() {
     return this.type <= PermissionType.CREATE_POST ? "reply-granted" : "";
-  },
+  }
 
   @discourseComputed("type")
   createGranted() {
     return this.type === PermissionType.FULL ? "create-granted" : "";
-  },
+  }
 
   @observes("everyonePermissionType")
   inheritFromEveryone() {
@@ -49,7 +51,7 @@ export default Component.extend({
     if (this.everyonePermissionType < this.type) {
       this.updatePermission(this.everyonePermissionType);
     }
-  },
+  }
 
   @discourseComputed("everyonePermissionType", "type")
   replyDisabled(everyonePermissionType) {
@@ -61,14 +63,14 @@ export default Component.extend({
       return true;
     }
     return false;
-  },
+  }
 
   @discourseComputed("replyDisabled")
   replyTooltip(replyDisabled) {
     return replyDisabled
       ? I18n.t("category.permissions.inherited")
       : I18n.t("category.permissions.toggle_reply");
-  },
+  }
 
   @discourseComputed("everyonePermissionType", "type")
   createDisabled(everyonePermissionType) {
@@ -80,47 +82,47 @@ export default Component.extend({
       return true;
     }
     return false;
-  },
+  }
 
   @discourseComputed("createDisabled")
   createTooltip(createDisabled) {
     return createDisabled
       ? I18n.t("category.permissions.inherited")
       : I18n.t("category.permissions.toggle_full");
-  },
+  }
 
   updatePermission(type) {
     this.category.updatePermission(this.group_name, type);
-  },
+  }
 
   @action
   removeRow(event) {
     event?.preventDefault();
     this.category.removePermission(this.group_name);
-  },
+  }
 
-  actions: {
-    setPermissionReply() {
-      if (this.type <= PermissionType.CREATE_POST) {
-        this.updatePermission(PermissionType.READONLY);
-      } else {
-        this.updatePermission(PermissionType.CREATE_POST);
-      }
-    },
+  @action
+  setPermissionReply() {
+    if (this.type <= PermissionType.CREATE_POST) {
+      this.updatePermission(PermissionType.READONLY);
+    } else {
+      this.updatePermission(PermissionType.CREATE_POST);
+    }
+  }
 
-    setPermissionFull() {
-      if (
-        this.group_name !== EVERYONE &&
-        this.everyonePermissionType === PermissionType.FULL
-      ) {
-        return;
-      }
+  @action
+  setPermissionFull() {
+    if (
+      this.group_name !== EVERYONE &&
+      this.everyonePermissionType === PermissionType.FULL
+    ) {
+      return;
+    }
 
-      if (this.type === PermissionType.FULL) {
-        this.updatePermission(PermissionType.CREATE_POST);
-      } else {
-        this.updatePermission(PermissionType.FULL);
-      }
-    },
-  },
-});
+    if (this.type === PermissionType.FULL) {
+      this.updatePermission(PermissionType.CREATE_POST);
+    } else {
+      this.updatePermission(PermissionType.FULL);
+    }
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/category-read-only-banner.js b/app/assets/javascripts/discourse/app/components/category-read-only-banner.js
index d8371808f27..495409e4780 100644
--- a/app/assets/javascripts/discourse/app/components/category-read-only-banner.js
+++ b/app/assets/javascripts/discourse/app/components/category-read-only-banner.js
@@ -2,10 +2,11 @@ import Component from "@ember/component";
 import { and } from "@ember/object/computed";
 import discourseComputed from "discourse-common/utils/decorators";
 
-export default Component.extend({
+export default class CategoryReadOnlyBanner extends Component {
+  @and("category.read_only_banner", "readOnly", "user") shouldShow;
+
   @discourseComputed
   user() {
     return this.currentUser;
-  },
-  shouldShow: and("category.read_only_banner", "readOnly", "user"),
-});
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/category-title-before.js b/app/assets/javascripts/discourse/app/components/category-title-before.js
index 0e6d50b17d4..60ba901dff1 100644
--- a/app/assets/javascripts/discourse/app/components/category-title-before.js
+++ b/app/assets/javascripts/discourse/app/components/category-title-before.js
@@ -1,4 +1,5 @@
 import Component from "@ember/component";
-export default Component.extend({
-  tagName: "",
-});
+import { tagName } from "@ember-decorators/component";
+
+@tagName("")
+export default class CategoryTitleBefore extends Component {}
diff --git a/app/assets/javascripts/discourse/app/components/category-title-link.js b/app/assets/javascripts/discourse/app/components/category-title-link.js
index 1ac2a2fbab3..a5120584b6e 100644
--- a/app/assets/javascripts/discourse/app/components/category-title-link.js
+++ b/app/assets/javascripts/discourse/app/components/category-title-link.js
@@ -1,6 +1,8 @@
 import Component from "@ember/component";
-export default Component.extend({
-  tagName: "h3",
-  // icon name defined here so it can be easily overridden in theme components
-  lockIcon: "lock",
-});
+import { tagName } from "@ember-decorators/component";
+
+@tagName("h3")
+export default class CategoryTitleLink extends Component {}
+
+// icon name defined on prototype so it can be easily overridden in theme components
+CategoryTitleLink.prototype.lockIcon = "lock";
diff --git a/app/assets/javascripts/discourse/app/components/category-unread.js b/app/assets/javascripts/discourse/app/components/category-unread.js
index 4719d63b642..33d4e31ad81 100644
--- a/app/assets/javascripts/discourse/app/components/category-unread.js
+++ b/app/assets/javascripts/discourse/app/components/category-unread.js
@@ -1,5 +1,6 @@
 import Component from "@ember/component";
-export default Component.extend({
-  tagName: "span",
-  classNames: ["category__badges"],
-});
+import { classNames, tagName } from "@ember-decorators/component";
+
+@tagName("span")
+@classNames("category__badges")
+export default class CategoryUnread extends Component {}
diff --git a/app/assets/javascripts/discourse/app/components/choose-message.js b/app/assets/javascripts/discourse/app/components/choose-message.js
index c91a2dd8f4b..d8f00087a4e 100644
--- a/app/assets/javascripts/discourse/app/components/choose-message.js
+++ b/app/assets/javascripts/discourse/app/components/choose-message.js
@@ -2,14 +2,15 @@ import Component from "@ember/component";
 import { action, get } from "@ember/object";
 import { next } from "@ember/runloop";
 import { isEmpty } from "@ember/utils";
+import { observes } from "@ember-decorators/object";
 import $ from "jquery";
 import { searchForTerm } from "discourse/lib/search";
-import { debounce, observes } from "discourse-common/utils/decorators";
+import { debounce } from "discourse-common/utils/decorators";
 
-export default Component.extend({
-  loading: null,
-  noResults: null,
-  messages: null,
+export default class ChooseMessage extends Component {
+  loading = null;
+  noResults = null;
+  messages = null;
 
   @observes("messageTitle")
   messageTitleChanged() {
@@ -19,7 +20,7 @@ export default Component.extend({
       selectedTopicId: null,
     });
     this.search(this.messageTitle);
-  },
+  }
 
   @observes("messages")
   messagesChanged() {
@@ -28,7 +29,7 @@ export default Component.extend({
       this.set("noResults", messages.length === 0);
     }
     this.set("loading", false);
-  },
+  }
 
   @debounce(300)
   search(title) {
@@ -53,7 +54,7 @@ export default Component.extend({
         this.setProperties({ messages: null, loading: false });
       }
     });
-  },
+  }
 
   @action
   chooseMessage(message, event) {
@@ -61,5 +62,5 @@ export default Component.extend({
     const messageId = get(message, "id");
     this.set("selectedTopicId", messageId);
     next(() => $(`#choose-message-${messageId}`).prop("checked", "true"));
-  },
-});
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/color-picker-choice.js b/app/assets/javascripts/discourse/app/components/color-picker-choice.js
index 36a0bfd441a..e3fba98d10e 100644
--- a/app/assets/javascripts/discourse/app/components/color-picker-choice.js
+++ b/app/assets/javascripts/discourse/app/components/color-picker-choice.js
@@ -1,30 +1,34 @@
 import Component from "@ember/component";
 import { htmlSafe } from "@ember/template";
+import {
+  attributeBindings,
+  classNameBindings,
+  tagName,
+} from "@ember-decorators/component";
 import discourseComputed from "discourse-common/utils/decorators";
 import I18n from "discourse-i18n";
 
-export default Component.extend({
-  tagName: "button",
-  attributeBindings: ["style", "title"],
-  classNameBindings: [":colorpicker", "isUsed:used-color:unused-color"],
-
+@tagName("button")
+@attributeBindings("style", "title")
+@classNameBindings(":colorpicker", "isUsed:used-color:unused-color")
+export default class ColorPickerChoice extends Component {
   @discourseComputed("color", "usedColors")
   isUsed(color, usedColors) {
     return (usedColors || []).includes(color.toUpperCase());
-  },
+  }
 
   @discourseComputed("isUsed")
   title(isUsed) {
     return isUsed ? I18n.t("category.already_used") : null;
-  },
+  }
 
   @discourseComputed("color")
   style(color) {
     return htmlSafe(`background-color: #${color};`);
-  },
+  }
 
   click(e) {
     e.preventDefault();
     this.selectColor(this.color);
-  },
-});
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/color-picker.js b/app/assets/javascripts/discourse/app/components/color-picker.js
index 22ff988c837..a61baf746ee 100644
--- a/app/assets/javascripts/discourse/app/components/color-picker.js
+++ b/app/assets/javascripts/discourse/app/components/color-picker.js
@@ -1,10 +1,11 @@
 import Component from "@ember/component";
-export default Component.extend({
-  classNames: "colors-container",
+import { action } from "@ember/object";
+import { classNames } from "@ember-decorators/component";
 
-  actions: {
-    selectColor(color) {
-      this.set("value", color);
-    },
-  },
-});
+@classNames("colors-container")
+export default class ColorPicker extends Component {
+  @action
+  selectColor(color) {
+    this.set("value", color);
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/composer-action-title.js b/app/assets/javascripts/discourse/app/components/composer-action-title.js
index 84590d2ecac..a72c4607fa9 100644
--- a/app/assets/javascripts/discourse/app/components/composer-action-title.js
+++ b/app/assets/javascripts/discourse/app/components/composer-action-title.js
@@ -1,6 +1,7 @@
 import Component from "@ember/component";
 import { alias } from "@ember/object/computed";
 import { htmlSafe } from "@ember/template";
+import { classNames } from "@ember-decorators/component";
 import {
   CREATE_SHARED_DRAFT,
   CREATE_TOPIC,
@@ -21,10 +22,10 @@ const TITLES = {
   [EDIT_SHARED_DRAFT]: "composer.edit_shared_draft",
 };
 
-export default Component.extend({
-  classNames: ["composer-action-title"],
-  options: alias("model.replyOptions"),
-  action: alias("model.action"),
+@classNames("composer-action-title")
+export default class ComposerActionTitle extends Component {
+  @alias("model.replyOptions") options;
+  @alias("model.action") action;
 
   // Note we update when some other attributes like tag/category change to allow
   // text customizations to use those.
@@ -57,7 +58,7 @@ export default Component.extend({
         );
       }
     }
-  },
+  }
 
   _formatEditUserPost(userAvatar, userLink, postLink, originalUser) {
     let editTitle = `
@@ -75,7 +76,7 @@ export default Component.extend({
     }
 
     return htmlSafe(editTitle);
-  },
+  }
 
   _formatReplyToTopic(link) {
     return htmlSafe(
@@ -83,12 +84,12 @@ export default Component.extend({
         "model.topic.id"
       )}">${link.anchor}</a>`
     );
-  },
+  }
 
   _formatReplyToUserPost(avatar, link) {
     const htmlLink = `<a class="user-link" href="${link.href}">${escape(
       link.anchor
     )}</a>`;
     return htmlSafe(`${avatar}${htmlLink}`);
-  },
-});
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/composer-body.js b/app/assets/javascripts/discourse/app/components/composer-body.js
index 120c5803a72..af2bca42844 100644
--- a/app/assets/javascripts/discourse/app/components/composer-body.js
+++ b/app/assets/javascripts/discourse/app/components/composer-body.js
@@ -1,15 +1,14 @@
 import Component from "@ember/component";
 import { cancel, schedule, throttle } from "@ember/runloop";
+import { classNameBindings } from "@ember-decorators/component";
+import { observes } from "@ember-decorators/object";
 import { headerOffset } from "discourse/lib/offset-calculator";
 import positioningWorkaround from "discourse/lib/safari-hacks";
 import { isiPad } from "discourse/lib/utilities";
 import Composer from "discourse/models/composer";
 import discourseDebounce from "discourse-common/lib/debounce";
 import discourseLater from "discourse-common/lib/later";
-import discourseComputed, {
-  bind,
-  observes,
-} from "discourse-common/utils/decorators";
+import discourseComputed, { bind } from "discourse-common/utils/decorators";
 
 const START_DRAG_EVENTS = ["touchstart", "mousedown"];
 const DRAG_EVENTS = ["touchmove", "mousemove"];
@@ -21,37 +20,36 @@ function mouseYPos(e) {
   return e.clientY || (e.touches && e.touches[0] && e.touches[0].clientY);
 }
 
-export default Component.extend({
-  elementId: "reply-control",
-
-  classNameBindings: [
-    "composer.creatingPrivateMessage:private-message",
-    "composeState",
-    "composer.loading",
-    "prefixedComposerAction",
-    "composer.canEditTitle:edit-title",
-    "composer.createdPost:created-post",
-    "composer.creatingTopic:topic",
-    "composer.whisper:composing-whisper",
-    "composer.sharedDraft:composing-shared-draft",
-    "showPreview:show-preview:hide-preview",
-    "currentUserPrimaryGroupClass",
-  ],
+@classNameBindings(
+  "composer.creatingPrivateMessage:private-message",
+  "composeState",
+  "composer.loading",
+  "prefixedComposerAction",
+  "composer.canEditTitle:edit-title",
+  "composer.createdPost:created-post",
+  "composer.creatingTopic:topic",
+  "composer.whisper:composing-whisper",
+  "composer.sharedDraft:composing-shared-draft",
+  "showPreview:show-preview:hide-preview",
+  "currentUserPrimaryGroupClass"
+)
+export default class ComposerBody extends Component {
+  elementId = "reply-control";
 
   @discourseComputed("composer.action")
   prefixedComposerAction(action) {
     return action ? `composer-action-${action}` : "";
-  },
+  }
 
   @discourseComputed("currentUser.primary_group_name")
   currentUserPrimaryGroupClass(primaryGroupName) {
     return primaryGroupName && `group-${primaryGroupName}`;
-  },
+  }
 
   @discourseComputed("composer.composeState")
   composeState(composeState) {
     return composeState || Composer.CLOSED;
-  },
+  }
 
   keyUp() {
     this.typed();
@@ -68,14 +66,14 @@ export default Component.extend({
       }
       this.appEvents.trigger("composer:find-similar");
     }, 1000);
-  },
+  }
 
   @observes("composeState")
   disableFullscreen() {
     if (this.composeState !== Composer.OPEN && positioningWorkaround.blur) {
       positioningWorkaround.blur();
     }
-  },
+  }
 
   setupComposerResizeEvents() {
     this.origComposerSize = 0;
@@ -88,7 +86,7 @@ export default Component.extend({
           passive: false,
         });
     });
-  },
+  }
 
   @bind
   performDragHandler() {
@@ -112,14 +110,14 @@ export default Component.extend({
     );
 
     this._triggerComposerResized();
-  },
+  }
 
   @observes("composeState", "composer.{action,canEditTopicFeaturedLink}")
   _triggerComposerResized() {
     schedule("afterRender", () => {
       discourseDebounce(this, this.composerResized, 300);
     });
-  },
+  }
 
   composerResized() {
     if (!this.element || this.isDestroying || this.isDestroyed) {
@@ -127,7 +125,7 @@ export default Component.extend({
     }
 
     this.appEvents.trigger("composer:resized");
-  },
+  }
 
   @bind
   startDragHandler(event) {
@@ -145,7 +143,7 @@ export default Component.extend({
     });
 
     this.appEvents.trigger("composer:resize-started");
-  },
+  }
 
   @bind
   endDragHandler() {
@@ -161,16 +159,16 @@ export default Component.extend({
 
     this.element.classList.remove("clear-transitions");
     this.element.focus();
-  },
+  }
 
   @bind
   throttledPerformDrag(event) {
     event.preventDefault();
     throttle(this, this.performDragHandler, event, THROTTLE_RATE);
-  },
+  }
 
   didInsertElement() {
-    this._super(...arguments);
+    super.didInsertElement(...arguments);
 
     this.setupComposerResizeEvents();
 
@@ -188,10 +186,10 @@ export default Component.extend({
     });
 
     positioningWorkaround(this.element);
-  },
+  }
 
   willDestroyElement() {
-    this._super(...arguments);
+    super.willDestroyElement(...arguments);
 
     START_DRAG_EVENTS.forEach((startDragEvent) => {
       this.element
@@ -200,11 +198,11 @@ export default Component.extend({
     });
 
     cancel(this._lastKeyTimeout);
-  },
+  }
 
   click() {
     this.openIfDraft();
-  },
+  }
 
   keyDown(e) {
     if (e.key === "Escape") {
@@ -220,5 +218,5 @@ export default Component.extend({
       e.preventDefault();
       this.save(undefined, e);
     }
-  },
-});
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.hbs b/app/assets/javascripts/discourse/app/components/composer-editor.hbs
index 33672a3afa9..60ceaebc44b 100644
--- a/app/assets/javascripts/discourse/app/components/composer-editor.hbs
+++ b/app/assets/javascripts/discourse/app/components/composer-editor.hbs
@@ -4,9 +4,9 @@
   @previewUpdated={{action "previewUpdated"}}
   @markdownOptions={{this.markdownOptions}}
   @extraButtons={{action "extraButtons"}}
-  @importQuote={{action "importQuote"}}
+  @importQuote={{this.importQuote}}
   @showUploadModal={{this.showUploadModal}}
-  @togglePreview={{action "togglePreview"}}
+  @togglePreview={{this.togglePreview}}
   @processPreview={{this.processPreview}}
   @validation={{this.validation}}
   @loading={{this.composer.loading}}
diff --git a/app/assets/javascripts/discourse/app/components/composer-editor.js b/app/assets/javascripts/discourse/app/components/composer-editor.js
index 65a3819f24d..c30bfc29239 100644
--- a/app/assets/javascripts/discourse/app/components/composer-editor.js
+++ b/app/assets/javascripts/discourse/app/components/composer-editor.js
@@ -1,8 +1,10 @@
 import Component from "@ember/component";
-import EmberObject, { computed } from "@ember/object";
+import EmberObject, { action, computed } from "@ember/object";
 import { alias } from "@ember/object/computed";
 import { getOwner } from "@ember/owner";
 import { next, schedule, throttle } from "@ember/runloop";
+import { classNameBindings } from "@ember-decorators/component";
+import { observes } from "@ember-decorators/object";
 import { BasePlugin } from "@uppy/core";
 import $ from "jquery";
 import { resolveAllShortUrls } from "pretty-text/upload-short-url";
@@ -39,7 +41,6 @@ import { findRawTemplate } from "discourse-common/lib/raw-templates";
 import discourseComputed, {
   bind,
   debounce,
-  observes,
   on,
 } from "discourse-common/utils/decorators";
 import I18n from "discourse-i18n";
@@ -109,32 +110,32 @@ export function addApiImageWrapperButtonClickEvent(fn) {
 const DEBOUNCE_FETCH_MS = 450;
 const DEBOUNCE_JIT_MS = 2000;
 
-export default Component.extend(ComposerUploadUppy, {
-  classNameBindings: ["showToolbar:toolbar-visible", ":wmd-controls"],
+@classNameBindings("showToolbar:toolbar-visible", ":wmd-controls")
+export default class ComposerEditor extends Component.extend(
+  ComposerUploadUppy
+) {
+  editorClass = ".d-editor";
+  fileUploadElementId = "file-uploader";
+  mobileFileUploaderId = "mobile-file-upload";
+  composerEventPrefix = "composer";
+  uploadType = "composer";
+  uppyId = "composer-editor-uppy";
+  composerModelContentKey = "reply";
+  editorInputClass = ".d-editor-input";
+  shouldBuildScrollMap = true;
+  scrollMap = null;
+  processPreview = true;
+  uploadMarkdownResolvers = uploadMarkdownResolvers;
+  uploadPreProcessors = uploadPreProcessors;
+  uploadHandlers = uploadHandlers;
 
-  editorClass: ".d-editor",
-  fileUploadElementId: "file-uploader",
-  mobileFileUploaderId: "mobile-file-upload",
-
-  composerEventPrefix: "composer",
-  uploadType: "composer",
-  uppyId: "composer-editor-uppy",
-  composerModel: alias("composer"),
-  composerModelContentKey: "reply",
-  editorInputClass: ".d-editor-input",
-  shouldBuildScrollMap: true,
-  scrollMap: null,
-  processPreview: true,
-
-  uploadMarkdownResolvers,
-  uploadPreProcessors,
-  uploadHandlers,
+  @alias("composer") composerModel;
 
   init() {
-    this._super(...arguments);
+    super.init(...arguments);
     this.warnedCannotSeeMentions = [];
     this.warnedGroupMentions = [];
-  },
+  }
 
   @discourseComputed("composer.requiredCategoryMissing")
   replyPlaceholder(requiredCategoryMissing) {
@@ -149,19 +150,19 @@ export default Component.extend(ComposerUploadUppy, {
         : "reply_placeholder_no_images";
       return `composer.${key}`;
     }
-  },
+  }
 
   @discourseComputed
   showLink() {
     return this.currentUser && this.currentUser.link_posting_access !== "none";
-  },
+  }
 
   @observes("focusTarget")
   setFocus() {
     if (this.focusTarget === "editor") {
       putCursorAtEnd(this.element.querySelector("textarea"));
     }
-  },
+  }
 
   @discourseComputed
   markdownOptions() {
@@ -204,7 +205,7 @@ export default Component.extend(ComposerUploadUppy, {
         this.site.hashtag_configurations["topic-composer"],
       hashtagIcons: this.site.hashtag_icons,
     };
-  },
+  }
 
   @bind
   _afterMentionComplete(value) {
@@ -216,7 +217,7 @@ export default Component.extend(ComposerUploadUppy, {
       input?.blur();
       input?.focus();
     });
-  },
+  }
 
   @on("didInsertElement")
   _composerEditorInit() {
@@ -266,7 +267,7 @@ export default Component.extend(ComposerUploadUppy, {
     }
 
     this.appEvents.trigger(`${this.composerEventPrefix}:will-open`);
-  },
+  }
 
   @discourseComputed(
     "composer.reply",
@@ -313,7 +314,7 @@ export default Component.extend(ComposerUploadUppy, {
         lastShownAt: lastValidatedAt,
       });
     }
-  },
+  }
 
   @computed("composer.{creatingTopic,editingFirstPost,creatingSharedDraft}")
   get _isNewTopic() {
@@ -322,11 +323,11 @@ export default Component.extend(ComposerUploadUppy, {
       this.composer.editingFirstPost ||
       this.composer.creatingSharedDraft
     );
-  },
+  }
 
   _resetShouldBuildScrollMap() {
     this.set("shouldBuildScrollMap", true);
-  },
+  }
 
   @bind
   _handleInputInteraction(event) {
@@ -338,7 +339,7 @@ export default Component.extend(ComposerUploadUppy, {
 
     preview.removeEventListener("scroll", this._handleInputOrPreviewScroll);
     event.target.addEventListener("scroll", this._handleInputOrPreviewScroll);
-  },
+  }
 
   @bind
   _handleInputOrPreviewScroll(event) {
@@ -347,7 +348,7 @@ export default Component.extend(ComposerUploadUppy, {
       $(event.target),
       $(this.element.querySelector(".d-editor-preview-wrapper"))
     );
-  },
+  }
 
   @bind
   _handlePreviewInteraction(event) {
@@ -356,7 +357,7 @@ export default Component.extend(ComposerUploadUppy, {
       ?.removeEventListener("scroll", this._handleInputOrPreviewScroll);
 
     event.target?.addEventListener("scroll", this._handleInputOrPreviewScroll);
-  },
+  }
 
   _syncScroll($callback, $input, $preview) {
     if (!this.scrollMap || this.shouldBuildScrollMap) {
@@ -365,7 +366,7 @@ export default Component.extend(ComposerUploadUppy, {
     }
 
     throttle(this, $callback, $input, $preview, this.scrollMap, 20);
-  },
+  }
 
   // Adapted from https://github.com/markdown-it/markdown-it.github.io
   _buildScrollMap($input, $preview) {
@@ -452,7 +453,7 @@ export default Component.extend(ComposerUploadUppy, {
     }
 
     return scrollMap;
-  },
+  }
 
   @bind
   _throttledSyncEditorAndPreviewScroll(event) {
@@ -465,7 +466,7 @@ export default Component.extend(ComposerUploadUppy, {
       $preview,
       20
     );
-  },
+  }
 
   _syncEditorAndPreviewScroll($input, $preview) {
     if (!$input) {
@@ -490,7 +491,7 @@ export default Component.extend(ComposerUploadUppy, {
     const factor = previewHeight / inputHeight;
     const desired = scrollPosition * factor;
     $preview.scrollTop(desired + 50);
-  },
+  }
 
   _renderMentions(preview, unseen) {
     unseen ||= linkSeenMentions(preview, this.siteSettings);
@@ -500,7 +501,7 @@ export default Component.extend(ComposerUploadUppy, {
       this._warnMentionedGroups(preview);
       this._warnCannotSeeMention(preview);
     }
-  },
+  }
 
   @debounce(DEBOUNCE_FETCH_MS)
   _renderUnseenMentions(preview, unseen) {
@@ -514,7 +515,7 @@ export default Component.extend(ComposerUploadUppy, {
       this._warnCannotSeeMention(preview);
       this._warnHereMention(response.here_count);
     });
-  },
+  }
 
   _renderHashtags(preview, unseen) {
     const context = this.site.hashtag_configurations["topic-composer"];
@@ -522,14 +523,14 @@ export default Component.extend(ComposerUploadUppy, {
     if (unseen.length > 0) {
       this._renderUnseenHashtags(preview, unseen, context);
     }
-  },
+  }
 
   @debounce(DEBOUNCE_FETCH_MS)
   _renderUnseenHashtags(preview, unseen, context) {
     fetchUnseenHashtagsInContext(context, unseen).then(() =>
       linkSeenHashtagsInContext(context, preview)
     );
-  },
+  }
 
   @debounce(DEBOUNCE_FETCH_MS)
   _refreshOneboxes(preview) {
@@ -549,15 +550,15 @@ export default Component.extend(ComposerUploadUppy, {
     if (refresh && loaded > 0) {
       post.set("refreshedPost", true);
     }
-  },
+  }
 
   _expandShortUrls(preview) {
     resolveAllShortUrls(ajax, this.siteSettings, preview);
-  },
+  }
 
   _decorateCookedElement(preview) {
     this.appEvents.trigger("decorate-non-stream-cooked-element", preview);
-  },
+  }
 
   @debounce(DEBOUNCE_JIT_MS)
   _warnMentionedGroups(preview) {
@@ -581,7 +582,7 @@ export default Component.extend(ComposerUploadUppy, {
           });
         });
     });
-  },
+  }
 
   // add a delay to allow for typing, so you don't open the warning right away
   // previously we would warn after @bob even if you were about to mention @bob2
@@ -620,7 +621,7 @@ export default Component.extend(ComposerUploadUppy, {
           isGroup: true,
         });
       });
-  },
+  }
 
   _warnHereMention(hereCount) {
     if (!hereCount || hereCount === 0) {
@@ -628,7 +629,7 @@ export default Component.extend(ComposerUploadUppy, {
     }
 
     this.hereMention(hereCount);
-  },
+  }
 
   @bind
   _handleImageScaleButtonClick(event) {
@@ -665,7 +666,7 @@ export default Component.extend(ComposerUploadUppy, {
 
     event.preventDefault();
     return;
-  },
+  }
 
   resetImageControls(buttonWrapper) {
     const imageResize = buttonWrapper.querySelector(".scale-btn-container");
@@ -684,7 +685,7 @@ export default Component.extend(ComposerUploadUppy, {
     readonlyContainer.removeAttribute("hidden");
     buttonWrapper.removeAttribute("editing");
     editContainer.setAttribute("hidden", "true");
-  },
+  }
 
   commitAltText(buttonWrapper) {
     const index = parseInt(buttonWrapper.getAttribute("data-image-index"), 10);
@@ -704,7 +705,7 @@ export default Component.extend(ComposerUploadUppy, {
     );
 
     this.resetImageControls(buttonWrapper);
-  },
+  }
 
   @bind
   _handleAltTextInputKeypress(event) {
@@ -720,7 +721,7 @@ export default Component.extend(ComposerUploadUppy, {
       const buttonWrapper = event.target.closest(".button-wrapper");
       this.commitAltText(buttonWrapper);
     }
-  },
+  }
 
   @bind
   _handleAltTextEditButtonClick(event) {
@@ -750,7 +751,7 @@ export default Component.extend(ComposerUploadUppy, {
     editContainer.removeAttribute("hidden");
     editContainerInput.focus();
     event.preventDefault();
-  },
+  }
 
   @bind
   _handleAltTextOkButtonClick(event) {
@@ -760,7 +761,7 @@ export default Component.extend(ComposerUploadUppy, {
 
     const buttonWrapper = event.target.closest(".button-wrapper");
     this.commitAltText(buttonWrapper);
-  },
+  }
 
   @bind
   _handleAltTextCancelButtonClick(event) {
@@ -770,7 +771,7 @@ export default Component.extend(ComposerUploadUppy, {
 
     const buttonWrapper = event.target.closest(".button-wrapper");
     this.resetImageControls(buttonWrapper);
-  },
+  }
 
   @bind
   _handleImageDeleteButtonClick(event) {
@@ -789,7 +790,7 @@ export default Component.extend(ComposerUploadUppy, {
       "",
       { regex: IMAGE_MARKDOWN_REGEX, index }
     );
-  },
+  }
 
   @bind
   _handleImageGridButtonClick(event) {
@@ -818,7 +819,7 @@ export default Component.extend(ComposerUploadUppy, {
       "grid_surround",
       { useBlockMode: true }
     );
-  },
+  }
 
   _registerImageAltTextButtonClick(preview) {
     preview.addEventListener("click", this._handleAltTextCancelButtonClick);
@@ -832,7 +833,7 @@ export default Component.extend(ComposerUploadUppy, {
     apiImageWrapperBtnEvents.forEach((fn) =>
       preview.addEventListener("click", fn)
     );
-  },
+  }
 
   @on("willDestroyElement")
   _composerClosed() {
@@ -870,17 +871,18 @@ export default Component.extend(ComposerUploadUppy, {
     apiImageWrapperBtnEvents.forEach((fn) =>
       preview?.removeEventListener("click", fn)
     );
-  },
+  }
 
+  @action
   onExpandPopupMenuOptions(toolbarEvent) {
     const selected = toolbarEvent.selected;
     toolbarEvent.selectText(selected.start, selected.end - selected.start);
     this.storeToolbarState(toolbarEvent);
-  },
+  }
 
   showPreview() {
     this.send("togglePreview");
-  },
+  }
 
   _isInQuote(element) {
     let parent = element.parentElement;
@@ -893,18 +895,18 @@ export default Component.extend(ComposerUploadUppy, {
     }
 
     return false;
-  },
+  }
 
   _isPreviewRoot(element) {
     return (
       element.tagName === "DIV" &&
       element.classList.contains("d-editor-preview")
     );
-  },
+  }
 
   _isQuote(element) {
     return element.tagName === "ASIDE" && element.classList.contains("quote");
-  },
+  }
 
   _cursorIsOnEmptyLine() {
     const textArea = this.element.querySelector(".d-editor-input");
@@ -916,7 +918,7 @@ export default Component.extend(ComposerUploadUppy, {
     } else {
       return false;
     }
-  },
+  }
 
   _findMatchingUploadHandler(fileName) {
     return this.uploadHandlers.find((handler) => {
@@ -924,62 +926,50 @@ export default Component.extend(ComposerUploadUppy, {
       const regex = new RegExp(`\\.(${ext})$`, "i");
       return regex.test(fileName);
     });
-  },
+  }
 
-  actions: {
-    importQuote(toolbarEvent) {
-      this.importQuote(toolbarEvent);
-    },
+  @action
+  extraButtons(toolbar) {
+    toolbar.addButton({
+      id: "quote",
+      group: "fontStyles",
+      icon: "far-comment",
+      sendAction: this.importQuote,
+      title: "composer.quote_post_title",
+      unshift: true,
+    });
 
-    onExpandPopupMenuOptions(toolbarEvent) {
-      this.onExpandPopupMenuOptions(toolbarEvent);
-    },
-
-    togglePreview() {
-      this.togglePreview();
-    },
-
-    extraButtons(toolbar) {
+    if (this.allowUpload && this.uploadIcon && this.site.desktopView) {
       toolbar.addButton({
-        id: "quote",
-        group: "fontStyles",
-        icon: "far-comment",
-        sendAction: this.importQuote,
-        title: "composer.quote_post_title",
-        unshift: true,
+        id: "upload",
+        group: "insertions",
+        icon: this.uploadIcon,
+        title: "upload",
+        sendAction: this.showUploadModal,
       });
+    }
 
-      if (this.allowUpload && this.uploadIcon && this.site.desktopView) {
-        toolbar.addButton({
-          id: "upload",
-          group: "insertions",
-          icon: this.uploadIcon,
-          title: "upload",
-          sendAction: this.showUploadModal,
-        });
-      }
+    toolbar.addButton({
+      id: "options",
+      group: "extras",
+      icon: "cog",
+      title: "composer.options",
+      sendAction: this.onExpandPopupMenuOptions.bind(this),
+      popupMenu: true,
+    });
+  }
 
-      toolbar.addButton({
-        id: "options",
-        group: "extras",
-        icon: "cog",
-        title: "composer.options",
-        sendAction: this.onExpandPopupMenuOptions.bind(this),
-        popupMenu: true,
-      });
-    },
+  @action
+  previewUpdated(preview, unseenMentions, unseenHashtags) {
+    this._renderMentions(preview, unseenMentions);
+    this._renderHashtags(preview, unseenHashtags);
+    this._refreshOneboxes(preview);
+    this._expandShortUrls(preview);
 
-    previewUpdated(preview, unseenMentions, unseenHashtags) {
-      this._renderMentions(preview, unseenMentions);
-      this._renderHashtags(preview, unseenHashtags);
-      this._refreshOneboxes(preview);
-      this._expandShortUrls(preview);
+    if (!this.siteSettings.enable_diffhtml_preview) {
+      this._decorateCookedElement(preview);
+    }
 
-      if (!this.siteSettings.enable_diffhtml_preview) {
-        this._decorateCookedElement(preview);
-      }
-
-      this.afterRefresh(preview);
-    },
-  },
-});
+    this.afterRefresh(preview);
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/composer-message.js b/app/assets/javascripts/discourse/app/components/composer-message.js
index cf72b63ad0c..db44b73f556 100644
--- a/app/assets/javascripts/discourse/app/components/composer-message.js
+++ b/app/assets/javascripts/discourse/app/components/composer-message.js
@@ -1,23 +1,12 @@
 import Component from "@ember/component";
 import { getOwner } from "@ember/owner";
-import deprecated from "discourse-common/lib/deprecated";
+import { classNameBindings } from "@ember-decorators/component";
 import discourseComputed from "discourse-common/utils/decorators";
 
-export default Component.extend({
-  classNameBindings: [":composer-popup", "message.extraClass"],
-
+@classNameBindings(":composer-popup", "message.extraClass")
+export default class ComposerMessage extends Component {
   @discourseComputed("message.templateName")
   layout(templateName) {
     return getOwner(this).lookup(`template:composer/${templateName}`);
-  },
-
-  actions: {
-    closeMessage() {
-      deprecated(
-        'You should use `action=(closeMessage message)` instead of `action=(action "closeMessage")`',
-        { id: "discourse.composer-message.closeMessage" }
-      );
-      this.closeMessage(this.message);
-    },
-  },
-});
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/composer-title.js b/app/assets/javascripts/discourse/app/components/composer-title.js
index 5e88d20a5cb..b874679c0d3 100644
--- a/app/assets/javascripts/discourse/app/components/composer-title.js
+++ b/app/assets/javascripts/discourse/app/components/composer-title.js
@@ -2,23 +2,26 @@ import Component from "@ember/component";
 import EmberObject from "@ember/object";
 import { alias, or } from "@ember/object/computed";
 import { next, schedule } from "@ember/runloop";
+import { classNames } from "@ember-decorators/component";
+import { observes } from "@ember-decorators/object";
 import { load } from "pretty-text/oneboxer";
 import { lookupCache } from "pretty-text/oneboxer-cache";
 import { ajax } from "discourse/lib/ajax";
 import putCursorAtEnd from "discourse/lib/put-cursor-at-end";
 import { isTesting } from "discourse-common/config/environment";
 import discourseDebounce from "discourse-common/lib/debounce";
-import discourseComputed, { observes } from "discourse-common/utils/decorators";
+import discourseComputed from "discourse-common/utils/decorators";
 import I18n from "discourse-i18n";
 
-export default Component.extend({
-  classNames: ["title-input"],
-  watchForLink: alias("composer.canEditTopicFeaturedLink"),
-  disabled: or("composer.loading", "composer.disableTitleInput"),
-  isTitleFocused: false,
+@classNames("title-input")
+export default class ComposerTitle extends Component {
+  @alias("composer.canEditTopicFeaturedLink") watchForLink;
+  @or("composer.loading", "composer.disableTitleInput") disabled;
+
+  isTitleFocused = false;
 
   didInsertElement() {
-    this._super(...arguments);
+    super.didInsertElement(...arguments);
     const titleInput = this.element.querySelector("input");
 
     this._focusHandler = () => this.set("isTitleFocused", true);
@@ -34,17 +37,17 @@ export default Component.extend({
     if (this.get("composer.titleLength") > 0) {
       discourseDebounce(this, this._titleChanged, 10);
     }
-  },
+  }
 
   willDestroyElement() {
-    this._super(...arguments);
+    super.willDestroyElement(...arguments);
     const titleInput = this.element.querySelector("input");
 
     if (titleInput) {
       titleInput.removeEventListener("focus", this._focusHandler);
       titleInput.removeEventListener("blur", this._blurHandler);
     }
-  },
+  }
 
   @discourseComputed(
     "composer.titleLength",
@@ -83,14 +86,14 @@ export default Component.extend({
         lastShownAt: lastValidatedAt,
       });
     }
-  },
+  }
 
   @discourseComputed("watchForLink")
   titleMaxLength(watchForLink) {
     // maxLength gets in the way of pasting long links, so don't use it if featured links are allowed.
     // Validation will display a message if titles are too long.
     return watchForLink ? null : this.siteSettings.max_topic_title_length;
-  },
+  }
 
   @observes("composer.titleLength", "watchForLink")
   _titleChanged() {
@@ -110,14 +113,14 @@ export default Component.extend({
     } else {
       discourseDebounce(this, this._checkForUrl, 500);
     }
-  },
+  }
 
   @observes("composer.replyLength")
   _clearFeaturedLink() {
     if (this.watchForLink && this.bodyIsDefault()) {
       this.set("composer.featuredLink", null);
     }
-  },
+  }
 
   _checkForUrl() {
     if (!this.element || this.isDestroying || this.isDestroyed) {
@@ -169,7 +172,7 @@ export default Component.extend({
         });
       }
     }
-  },
+  }
 
   _updatePost(html) {
     if (html) {
@@ -207,13 +210,13 @@ export default Component.extend({
         }
       }
     }
-  },
+  }
 
   changeTitle(val) {
     if (val && val.length > 0) {
       this.set("composer.title", val.trim());
     }
-  },
+  }
 
   @discourseComputed("composer.title", "composer.titleLength")
   isAbsoluteUrl(title, titleLength) {
@@ -222,7 +225,7 @@ export default Component.extend({
       /^(https?:)?\/\/[\w\.\-]+/i.test(title) &&
       !/\s/.test(title)
     );
-  },
+  }
 
   bodyIsDefault() {
     const reply = this.get("composer.reply") || "";
@@ -230,5 +233,5 @@ export default Component.extend({
       reply.length === 0 ||
       reply === (this.get("composer.category.topic_template") || "")
     );
-  },
-});
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/composer-toggles.js b/app/assets/javascripts/discourse/app/components/composer-toggles.js
index 50fd6547aa7..8462bb8036a 100644
--- a/app/assets/javascripts/discourse/app/components/composer-toggles.js
+++ b/app/assets/javascripts/discourse/app/components/composer-toggles.js
@@ -1,20 +1,20 @@
 import Component from "@ember/component";
+import { tagName } from "@ember-decorators/component";
 import discourseComputed from "discourse-common/utils/decorators";
 
-export default Component.extend({
-  tagName: "",
-
+@tagName("")
+export default class ComposerToggles extends Component {
   @discourseComputed("composeState")
   toggleTitle(composeState) {
     return composeState === "draft" || composeState === "saving"
       ? "composer.abandon"
       : "composer.collapse";
-  },
+  }
 
   @discourseComputed("showToolbar")
   toggleToolbarTitle(showToolbar) {
     return showToolbar ? "composer.hide_toolbar" : "composer.show_toolbar";
-  },
+  }
 
   @discourseComputed("composeState")
   fullscreenTitle(composeState) {
@@ -23,14 +23,14 @@ export default Component.extend({
       : composeState === "fullscreen"
       ? "composer.exit_fullscreen"
       : "composer.enter_fullscreen";
-  },
+  }
 
   @discourseComputed("composeState")
   toggleIcon(composeState) {
     return composeState === "draft" || composeState === "saving"
       ? "times"
       : "chevron-down";
-  },
+  }
 
   @discourseComputed("composeState")
   fullscreenIcon(composeState) {
@@ -39,7 +39,7 @@ export default Component.extend({
       : composeState === "fullscreen"
       ? "discourse-compress"
       : "discourse-expand";
-  },
+  }
 
   @discourseComputed("disableTextarea")
   showFullScreenButton(disableTextarea) {
@@ -47,5 +47,5 @@ export default Component.extend({
       return false;
     }
     return !disableTextarea;
-  },
-});
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/composer-user-selector.js b/app/assets/javascripts/discourse/app/components/composer-user-selector.js
index 8bf2d053912..69921636042 100644
--- a/app/assets/javascripts/discourse/app/components/composer-user-selector.js
+++ b/app/assets/javascripts/discourse/app/components/composer-user-selector.js
@@ -1,19 +1,17 @@
 import Component from "@ember/component";
+import { action } from "@ember/object";
 import discourseComputed from "discourse-common/utils/decorators";
 
-export default Component.extend({
-  init() {
-    this._super(...arguments);
-    this.set("_groups", []);
-  },
+export default class ComposerUserSelector extends Component {
+  _groups = [];
 
   didInsertElement() {
-    this._super(...arguments);
+    super.didInsertElement(...arguments);
 
     if (this.focusTarget === "usernames") {
       this.element.querySelector(".select-kit .select-kit-header").focus();
     }
-  },
+  }
 
   @discourseComputed("recipients")
   splitRecipients(recipients) {
@@ -21,7 +19,7 @@ export default Component.extend({
       return recipients;
     }
     return recipients ? recipients.split(",").filter(Boolean) : [];
-  },
+  }
 
   _updateGroups(selected, newGroups) {
     const groups = [];
@@ -39,13 +37,12 @@ export default Component.extend({
       _groups: groups,
       hasGroups: groups.length > 0,
     });
-  },
+  }
 
-  actions: {
-    updateRecipients(selected, content) {
-      const newGroups = content.filterBy("isGroup").mapBy("id");
-      this._updateGroups(selected, newGroups);
-      this.set("recipients", selected.join(","));
-    },
-  },
-});
+  @action
+  updateRecipients(selected, content) {
+    const newGroups = content.filterBy("isGroup").mapBy("id");
+    this._updateGroups(selected, newGroups);
+    this.set("recipients", selected.join(","));
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/conditional-loading-section.js b/app/assets/javascripts/discourse/app/components/conditional-loading-section.js
index 903bb472881..5b2e1e80e3c 100644
--- a/app/assets/javascripts/discourse/app/components/conditional-loading-section.js
+++ b/app/assets/javascripts/discourse/app/components/conditional-loading-section.js
@@ -1,11 +1,10 @@
 import Component from "@ember/component";
+import { classNameBindings, classNames } from "@ember-decorators/component";
 import I18n from "discourse-i18n";
-export default Component.extend({
-  classNames: ["conditional-loading-section"],
 
-  classNameBindings: ["isLoading"],
-
-  isLoading: false,
-
-  title: I18n.t("conditional_loading_section.loading"),
-});
+@classNames("conditional-loading-section")
+@classNameBindings("isLoading")
+export default class ConditionalLoadingSection extends Component {
+  isLoading = false;
+  title = I18n.t("conditional_loading_section.loading");
+}
diff --git a/app/assets/javascripts/discourse/app/components/connector-container.js b/app/assets/javascripts/discourse/app/components/connector-container.js
index 6c296745780..ecfa43b9e3f 100644
--- a/app/assets/javascripts/discourse/app/components/connector-container.js
+++ b/app/assets/javascripts/discourse/app/components/connector-container.js
@@ -1,2 +1,3 @@
 import Component from "@ember/component";
-export default Component.extend();
+
+export default class ConnectorContainer extends Component {}
diff --git a/app/assets/javascripts/discourse/app/components/copy-button.js b/app/assets/javascripts/discourse/app/components/copy-button.js
index f447629f498..10f6bdc08df 100644
--- a/app/assets/javascripts/discourse/app/components/copy-button.js
+++ b/app/assets/javascripts/discourse/app/components/copy-button.js
@@ -1,12 +1,13 @@
 import Component from "@ember/component";
 import { action } from "@ember/object";
+import { tagName } from "@ember-decorators/component";
 import discourseDebounce from "discourse-common/lib/debounce";
 import { bind } from "discourse-common/utils/decorators";
 
-export default Component.extend({
-  tagName: "",
-  copyIcon: "copy",
-  copyClass: "btn-primary",
+@tagName("")
+export default class CopyButton extends Component {
+  copyIcon = "copy";
+  copyClass = "btn-primary";
 
   @bind
   _restoreButton() {
@@ -16,7 +17,7 @@ export default Component.extend({
 
     this.set("copyIcon", "copy");
     this.set("copyClass", "btn-primary");
-  },
+  }
 
   @action
   copy() {
@@ -36,5 +37,5 @@ export default Component.extend({
 
       discourseDebounce(this._restoreButton, 3000);
     } catch (err) {}
-  },
-});
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/create-invite-uploader.js b/app/assets/javascripts/discourse/app/components/create-invite-uploader.js
index dea920d2c92..ed4491da6ed 100644
--- a/app/assets/javascripts/discourse/app/components/create-invite-uploader.js
+++ b/app/assets/javascripts/discourse/app/components/create-invite-uploader.js
@@ -1,38 +1,41 @@
 import Component from "@ember/component";
 import { action } from "@ember/object";
+import { tagName } from "@ember-decorators/component";
 import UppyUploadMixin from "discourse/mixins/uppy-upload";
 import discourseComputed from "discourse-common/utils/decorators";
 
-export default Component.extend(UppyUploadMixin, {
-  id: "create-invite-uploader",
-  tagName: "div",
-  type: "csv",
-  autoStartUploads: false,
-  uploadUrl: "/invites/upload_csv",
-  preventDirectS3Uploads: true,
-  fileInputSelector: "#csv-file",
+@tagName("div")
+export default class CreateInviteUploader extends Component.extend(
+  UppyUploadMixin
+) {
+  id = "create-invite-uploader";
+  type = "csv";
+  autoStartUploads = false;
+  uploadUrl = "/invites/upload_csv";
+  preventDirectS3Uploads = true;
+  fileInputSelector = "#csv-file";
 
   validateUploadedFilesOptions() {
     return { bypassNewUserRestriction: true, csvOnly: true };
-  },
+  }
 
   @discourseComputed("filesAwaitingUpload", "uploading")
   submitDisabled(filesAwaitingUpload, uploading) {
     return !filesAwaitingUpload || uploading;
-  },
+  }
 
   uploadDone() {
     this.set("uploaded", true);
-  },
+  }
 
   @action
   startUpload() {
     this._startUpload();
-  },
+  }
 
   @action
   setElement(element) {
     this.set("fileInputEl", element);
     this._initialize();
-  },
-});
+  }
+}
diff --git a/app/assets/javascripts/discourse/app/components/create-topic-button.js b/app/assets/javascripts/discourse/app/components/create-topic-button.js
index e424b5ad7a2..11257c0059e 100644
--- a/app/assets/javascripts/discourse/app/components/create-topic-button.js
+++ b/app/assets/javascripts/discourse/app/components/create-topic-button.js
@@ -1,6 +1,8 @@
 import Component from "@ember/component";
-export default Component.extend({
-  tagName: "",
-  label: "topic.create",
-  btnClass: "btn-default",
-});
+import { tagName } from "@ember-decorators/component";
+
+@tagName("")
+export default class CreateTopicButton extends Component {
+  label = "topic.create";
+  btnClass = "btn-default";
+}
diff --git a/app/assets/javascripts/discourse/app/components/custom-html.js b/app/assets/javascripts/discourse/app/components/custom-html.js
index 63c0b9873a4..076ac6799b1 100644
--- a/app/assets/javascripts/discourse/app/components/custom-html.js
+++ b/app/assets/javascripts/discourse/app/components/custom-html.js
@@ -4,11 +4,11 @@ import { hbs } from "ember-cli-htmlbars";
 import { getCustomHTML } from "discourse/helpers/custom-html";
 import deprecated from "discourse-common/lib/deprecated";
 
-export default Component.extend({
-  triggerAppEvent: null,
+export default class CustomHtml extends Component {
+  triggerAppEvent = null;
 
   init() {
-    this._super(...arguments);
+    super.init(...arguments);
     const name = this.name;
     const html = getCustomHTML(name);
 
@@ -25,19 +25,19 @@ export default Component.extend({
         this.set("layout", template);
       }
     }
-  },
+  }
 
   didInsertElement() {
-    this._super(...arguments);
+    super.didInsertElement(...arguments);
     if (this.triggerAppEvent === "true") {
       this.appEvents.trigger(`inserted-custom-html:${this.name}`);
     }
-  },
+  }
 
   willDestroyElement() {
-    this._super(...arguments);
+    super.willDestroyElement(...arguments);
     if (this.triggerAppEvent === "true") {
       this.appEvents.trigger(`destroyed-custom-html:${this.name}`);
     }
-  },
-});
+  }
+}