FEATURE: New share topic modal (#12804)

FEATURE: New share topic modal (#12804)

The old share modal used to host both share and invite functionality, under two tabs. The new “Share Topic” modal can be used only for sharing, but has a link to the invite modal.

Among the sharing methods, there is also “Notify” which points out that existing users will simply be notified (this was not clear before). Staff members can notify as many users as they want, but regular users are restricted to one at a time, no more than max_topic_invitations_per_day. The user will not receive another notification if they have been notified of the same topic in past hour.

The “Create Invite” modal also suffered some changes: the two radio boxes for selecting the type (invite or email) have been replaced by a single checkbox (is email?) and then the two labels about emails have been replaced by a single one, some fields were reordered and the advanced options toggle was moved to the bottom right of the modal.

diff --git a/app/assets/javascripts/discourse/app/controllers/create-invite.js b/app/assets/javascripts/discourse/app/controllers/create-invite.js
index 04310fa..d0c5b2b 100644
--- a/app/assets/javascripts/discourse/app/controllers/create-invite.js
+++ b/app/assets/javascripts/discourse/app/controllers/create-invite.js
@@ -30,6 +30,8 @@ export default Controller.extend(
       });
 
       this.setProperties({
+        invite: null,
+        invites: null,
         autogenerated: false,
         showAdvanced: false,
       });
@@ -41,7 +43,7 @@ export default Controller.extend(
       if (this.autogenerated) {
         this.invite
           .destroy()
-          .then(() => this.invites.removeObject(this.invite));
+          .then(() => this.invites && this.invites.removeObject(this.invite));
       }
     },
 
@@ -53,7 +55,7 @@ export default Controller.extend(
     },
 
     setAutogenerated(value) {
-      if ((this.autogenerated || !this.invite.id) && !value) {
+      if (this.invites && (this.autogenerated || !this.invite.id) && !value) {
         this.invites.unshiftObject(this.invite);
       }
 
@@ -169,6 +171,15 @@ export default Controller.extend(
     },
 
     @action
+    toggleLimitToEmail() {
+      const limitToEmail = !this.limitToEmail;
+      this.setProperties({
+        limitToEmail,
+        type: limitToEmail ? "email" : "link",
+      });
+    },
+
+    @action
     saveInvite(sendEmail) {
       this.appEvents.trigger("modal-body:clearFlash");
 
@@ -181,5 +192,10 @@ export default Controller.extend(
         this.set("buffered.email", result[0].email[0]);
       });
     },
+
+    @action
+    toggleAdvanced() {
+      this.toggleProperty("showAdvanced");
+    },
   }
 );
diff --git a/app/assets/javascripts/discourse/app/controllers/share-topic.js b/app/assets/javascripts/discourse/app/controllers/share-topic.js
new file mode 100644
index 0000000..82f3045
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/controllers/share-topic.js
@@ -0,0 +1,116 @@
+import Controller from "@ember/controller";
+import { action } from "@ember/object";
+import { getAbsoluteURL } from "discourse-common/lib/get-url";
+import discourseComputed from "discourse-common/utils/decorators";
+import { ajax } from "discourse/lib/ajax";
+import { extractError } from "discourse/lib/ajax-error";
+import Sharing from "discourse/lib/sharing";
+import showModal from "discourse/lib/show-modal";
+import { bufferedProperty } from "discourse/mixins/buffered-content";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+import I18n from "I18n";
+
+export default Controller.extend(
+  ModalFunctionality,
+  bufferedProperty("invite"),
+  {
+    onShow() {
+      this.set("showNotifyUsers", false);
+    },
+
+    @discourseComputed("topic.shareUrl")
+    topicUrl(url) {
+      return url ? getAbsoluteURL(url) : null;
+    },
+
+    @discourseComputed(
+      "topic.{isPrivateMessage,invisible,category.read_restricted}"
+    )
+    sources(topic) {
+      const privateContext =
+        this.siteSettings.login_required ||
+        (topic && topic.isPrivateMessage) ||
+        (topic && topic.invisible) ||
+        topic.category.read_restricted;
+
+      return Sharing.activeSources(
+        this.siteSettings.share_links,
+        privateContext
+      );
+    },
+
+    @action
+    copied() {
+      return this.appEvents.trigger("modal-body:flash", {
+        text: I18n.t("topic.share.copied"),
+        messageClass: "success",
+      });
+    },
+
+    @action
+    onChangeUsers(usernames) {
+      this.set("users", usernames.uniq());
+    },
+
+    @action
+    share(source) {
+      this.set("showNotifyUsers", false);
+      Sharing.shareSource(source, {
+        title: this.topic.title,
+        url: this.topicUrl,
+      });
+    },
+
+    @action
+    toggleNotifyUsers() {
+      if (this.showNotifyUsers) {
+        this.set("showNotifyUsers", false);
+      } else {
+        this.setProperties({
+          showNotifyUsers: true,
+          users: [],
+        });
+      }
+    },
+
+    @action
+    notifyUsers() {
+      if (this.users.length === 0) {
+        return;
+      }
+
+      ajax(`/t/${this.topic.id}/invite-notify`, {
+        type: "POST",
+        data: { usernames: this.users },
+      })
+        .then(() => {
+          this.setProperties({ showNotifyUsers: false });
+          this.appEvents.trigger("modal-body:flash", {
+            text: I18n.t("topic.share.notify_users.success", {
+              count: this.users.length,
+              username: this.users[0],
+            }),
+            messageClass: "success",
+          });
+        })
+        .catch((error) => {
+          this.appEvents.trigger("modal-body:flash", {
+            text: extractError(error),
+            messageClass: "error",
+          });
+        });
+    },
+
+    @action
+    inviteUsers() {
+      this.set("showNotifyUsers", false);
+      const controller = showModal("create-invite");
+      controller.set("showAdvanced", true);
+      controller.buffered.setProperties({
+        topicId: this.topic.id,
+        topicTitle: this.topic.title,
+      });
+      controller.save({ autogenerated: true });
+    },
+  }
+);
diff --git a/app/assets/javascripts/discourse/app/initializers/topic-footer-buttons.js b/app/assets/javascripts/discourse/app/initializers/topic-footer-buttons.js
index 812e1b8..6cbc34c 100644
--- a/app/assets/javascripts/discourse/app/initializers/topic-footer-buttons.js
+++ b/app/assets/javascripts/discourse/app/initializers/topic-footer-buttons.js
@@ -25,39 +25,10 @@ export default {
       },
       title: "topic.share.help",
       action() {
-        const panels = [
-          {
-            id: "share",
-            title: "topic.share.extended_title",
-            model: {
-              topic: this.topic,
-            },
-          },
-        ];
-
-        if (this.canInviteTo && !this.inviteDisabled) {
-          let invitePanelTitle;
-
-          if (this.isPM) {
-            invitePanelTitle = "topic.invite_private.title";
-          } else if (this.invitingToTopic) {
-            invitePanelTitle = "topic.invite_reply.title";
-          } else {
-            invitePanelTitle = "user.invited.create";
-          }
-
-          panels.push({
-            id: "invite",
-            title: invitePanelTitle,
-            model: {
-              inviteModel: this.topic,
-            },
-          });
-        }
-
-        showModal("share-and-invite", {
-          modalClass: "share-and-invite",
-          panels,
+        const controller = showModal("share-topic");
+        controller.setProperties({
+          allowInvites: this.canInviteTo && !this.inviteDisabled,
+          topic: this.topic,
         });
       },
       dropdown() {
diff --git a/app/assets/javascripts/discourse/app/templates/modal/create-invite.hbs b/app/assets/javascripts/discourse/app/templates/modal/create-invite.hbs
index 0991bc7..1bb823f 100644
--- a/app/assets/javascripts/discourse/app/templates/modal/create-invite.hbs
+++ b/app/assets/javascripts/discourse/app/templates/modal/create-invite.hbs
@@ -16,15 +16,25 @@
     <p>{{expiresAtLabel}}</p>
 
     <div class="input-group invite-type">
-      <div class="radio-group">
-        {{radio-button id="invite-type-link" name="invite-type" value="link" selection=type}}
-        <label for="invite-type-link">{{i18n "user.invited.invite.type_link"}}</label>
-      </div>
+      {{input type="checkbox" id="invite-type" checked=limitToEmail click=(action "toggleLimitToEmail")}}
+      <label for="invite-type">{{i18n "user.invited.invite.restrict_email"}}</label>
 
-      <div class="radio-group">
-        {{radio-button id="invite-type-email" name="invite-type" value="email" selection=type}}
-        <label for="invite-type-email">{{i18n "user.invited.invite.type_email"}}</label>
-      </div>
+      {{#if isEmail}}
+        <div class="invite-input-with-button">
+          {{input

[... diff too long, it was truncated ...]

GitHub sha: cfee2728

This commit appears in #12804 which was approved by eviltrout. It was merged by udan11.