diff --git a/app/assets/javascripts/discourse/components/d-modal.js.es6 b/app/assets/javascripts/discourse/components/d-modal.js.es6 index 44792ecfc0..51c5824e51 100644 --- a/app/assets/javascripts/discourse/components/d-modal.js.es6 +++ b/app/assets/javascripts/discourse/components/d-modal.js.es6 @@ -1,9 +1,17 @@ import { on } from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ - classNameBindings: [":modal", ":d-modal", "modalClass", "modalStyle"], + classNameBindings: [ + ":modal", + ":d-modal", + "modalClass", + "modalStyle", + "hasPanels" + ], attributeBindings: ["data-keyboard"], dismissable: true, + title: null, + subtitle: null, init() { this._super(...arguments); diff --git a/app/assets/javascripts/discourse/components/group-selector.js.es6 b/app/assets/javascripts/discourse/components/group-selector.js.es6 index 8746b01fd3..dc3db23567 100644 --- a/app/assets/javascripts/discourse/components/group-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/group-selector.js.es6 @@ -30,6 +30,7 @@ export default Ember.Component.extend({ ? [] : [groupNames], single: this.get("single"), + fullWidthWrap: this.get("fullWidthWrap"), updateData: opts && opts.updateData ? opts.updateData : false, onChangeItems: items => { selectedGroups = items; diff --git a/app/assets/javascripts/discourse/controllers/invite.js.es6 b/app/assets/javascripts/discourse/components/invite-panel.js.es6 similarity index 66% rename from app/assets/javascripts/discourse/controllers/invite.js.es6 rename to app/assets/javascripts/discourse/components/invite-panel.js.es6 index b6144dd7b0..2f0fc23539 100644 --- a/app/assets/javascripts/discourse/controllers/invite.js.es6 +++ b/app/assets/javascripts/discourse/components/invite-panel.js.es6 @@ -1,34 +1,30 @@ -import ModalFunctionality from "discourse/mixins/modal-functionality"; import { emailValid } from "discourse/lib/utilities"; import computed from "ember-addons/ember-computed-decorators"; import Group from "discourse/models/group"; import Invite from "discourse/models/invite"; +import { i18n } from "discourse/lib/computed"; -export default Ember.Controller.extend(ModalFunctionality, { - userInvitedShow: Ember.inject.controller("user-invited-show"), +export default Ember.Component.extend({ + tagName: null, - // If this isn't defined, it will proxy to the user model on the preferences + inviteModel: Ember.computed.alias("panel.model.inviteModel"), + userInvitedShow: Ember.computed.alias("panel.model.userInvitedShow"), + + // If this isn't defined, it will proxy to the user topic on the preferences // page which is wrong. emailOrUsername: null, hasCustomMessage: false, + hasCustomMessage: false, customMessage: null, inviteIcon: "envelope", invitingExistingUserToTopic: false, - @computed("isMessage", "invitingToTopic") - title(isMessage, invitingToTopic) { - if (isMessage) { - return "topic.invite_private.title"; - } else if (invitingToTopic) { - return "topic.invite_reply.title"; - } else { - return "user.invited.create"; - } - }, + isAdmin: Ember.computed.alias("currentUser.admin"), - @computed - isAdmin() { - return this.currentUser.admin; + willDestroyElement() { + this._super(...arguments); + + this.reset(); }, @computed( @@ -36,9 +32,9 @@ export default Ember.Controller.extend(ModalFunctionality, { "emailOrUsername", "invitingToTopic", "isPrivateTopic", - "model.groupNames", - "model.saving", - "model.details.can_invite_to" + "topic.groupNames", + "topic.saving", + "topic.details.can_invite_to" ) disabled( isAdmin, @@ -51,26 +47,39 @@ export default Ember.Controller.extend(ModalFunctionality, { ) { if (saving) return true; if (Ember.isEmpty(emailOrUsername)) return true; + const emailTrimmed = emailOrUsername.trim(); // when inviting to forum, email must be valid - if (!invitingToTopic && !emailValid(emailTrimmed)) return true; - // normal users (not admin) can't invite users to private topic via email - if (!isAdmin && isPrivateTopic && emailValid(emailTrimmed)) return true; - // when inviting to private topic via email, group name must be specified - if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(emailTrimmed)) + if (!invitingToTopic && !emailValid(emailTrimmed)) { return true; + } + + // normal users (not admin) can't invite users to private topic via email + if (!isAdmin && isPrivateTopic && emailValid(emailTrimmed)) { + return true; + } + + // when inviting to private topic via email, group name must be specified + if ( + isPrivateTopic && + Ember.isEmpty(groupNames) && + emailValid(emailTrimmed) + ) { + return true; + } if (can_invite_to) return false; + return false; }, @computed( "isAdmin", "emailOrUsername", - "model.saving", + "inviteModel.saving", "isPrivateTopic", - "model.groupNames", + "inviteModel.groupNames", "hasCustomMessage" ) disabledCopyLink( @@ -84,54 +93,65 @@ export default Ember.Controller.extend(ModalFunctionality, { if (hasCustomMessage) return true; if (saving) return true; if (Ember.isEmpty(emailOrUsername)) return true; + const email = emailOrUsername.trim(); + // email must be valid - if (!emailValid(email)) return true; - // normal users (not admin) can't invite users to private topic via email - if (!isAdmin && isPrivateTopic && emailValid(email)) return true; - // when inviting to private topic via email, group name must be specified - if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(email)) + if (!emailValid(email)) { return true; + } + + // normal users (not admin) can't invite users to private topic via email + if (!isAdmin && isPrivateTopic && emailValid(email)) { + return true; + } + + // when inviting to private topic via email, group name must be specified + if (isPrivateTopic && Ember.isEmpty(groupNames) && emailValid(email)) { + return true; + } + return false; }, - @computed("model.saving") + @computed("inviteModel.saving") buttonTitle(saving) { return saving ? "topic.inviting" : "topic.invite_reply.action"; }, - // We are inviting to a topic if the model isn't the current user. + // We are inviting to a topic if the topic isn't the current user. // The current user would mean we are inviting to the forum in general. - @computed("model") - invitingToTopic(model) { - return model !== this.currentUser; + @computed("inviteModel") + invitingToTopic(inviteModel) { + return inviteModel !== this.currentUser; }, - @computed("model", "model.details.can_invite_via_email") - canInviteViaEmail(model, can_invite_via_email) { - return this.get("model") === this.currentUser ? true : can_invite_via_email; + @computed("inviteModel", "inviteModel.details.can_invite_via_email") + canInviteViaEmail(inviteModel, canInviteViaEmail) { + return this.get("inviteModel") === this.currentUser + ? true + : canInviteViaEmail; }, - @computed("isMessage", "canInviteViaEmail") - showCopyInviteButton(isMessage, canInviteViaEmail) { - return canInviteViaEmail && !isMessage; + @computed("isPM", "canInviteViaEmail") + showCopyInviteButton(isPM, canInviteViaEmail) { + return canInviteViaEmail && !isPM; }, - topicId: Ember.computed.alias("model.id"), + topicId: Ember.computed.alias("inviteModel.id"), - // Is Private Topic? (i.e. visible only to specific group members) + // eg: visible only to specific group members isPrivateTopic: Ember.computed.and( "invitingToTopic", - "model.category.read_restricted" + "inviteModel.category.read_restricted" ), - // Is Private Message? - isMessage: Ember.computed.equal("model.archetype", "private_message"), + isPM: Ember.computed.equal("inviteModel.archetype", "private_message"), - // Allow Existing Members? (username autocomplete) + // scope to allowed usernames allowExistingMembers: Ember.computed.alias("invitingToTopic"), - @computed("isAdmin", "model.group_users") + @computed("isAdmin", "inviteModel.group_users") isGroupOwnerOrAdmin(isAdmin, groupUsers) { return ( isAdmin || (groupUsers && groupUsers.some(groupUser => groupUser.owner)) @@ -143,7 +163,7 @@ export default Ember.Controller.extend(ModalFunctionality, { "isGroupOwnerOrAdmin", "emailOrUsername", "isPrivateTopic", - "isMessage", + "isPM", "invitingToTopic", "canInviteViaEmail" ) @@ -151,14 +171,14 @@ export default Ember.Controller.extend(ModalFunctionality, { isGroupOwnerOrAdmin, emailOrUsername, isPrivateTopic, - isMessage, + isPM, invitingToTopic, canInviteViaEmail ) { return ( isGroupOwnerOrAdmin && canInviteViaEmail && - !isMessage && + !isPM && (emailValid(emailOrUsername) || isPrivateTopic || !invitingToTopic) ); }, @@ -166,13 +186,14 @@ export default Ember.Controller.extend(ModalFunctionality, { @computed("emailOrUsername") showCustomMessage(emailOrUsername) { return ( - this.get("model") === this.currentUser || emailValid(emailOrUsername) + this.get("inviteModel") === this.currentUser || + emailValid(emailOrUsername) ); }, // Instructional text for the modal. @computed( - "isMessage", + "isPM", "invitingToTopic", "emailOrUsername", "isPrivateTopic", @@ -180,7 +201,7 @@ export default Ember.Controller.extend(ModalFunctionality, { "canInviteViaEmail" ) inviteInstructions( - isMessage, + isPM, invitingToTopic, emailOrUsername, isPrivateTopic, @@ -190,7 +211,7 @@ export default Ember.Controller.extend(ModalFunctionality, { if (!canInviteViaEmail) { // can't invite via email, only existing users return I18n.t("topic.invite_reply.sso_enabled"); - } else if (isMessage) { + } else if (isPM) { // inviting to a message return I18n.t("topic.invite_private.email_or_username"); } else if (invitingToTopic) { @@ -222,14 +243,14 @@ export default Ember.Controller.extend(ModalFunctionality, { }, groupFinder(term) { - return Group.findAll({ term: term, ignore_automatic: true }); + return Group.findAll({ term, ignore_automatic: true }); }, - @computed("isMessage", "emailOrUsername", "invitingExistingUserToTopic") - successMessage(isMessage, emailOrUsername, invitingExistingUserToTopic) { + @computed("isPM", "emailOrUsername", "invitingExistingUserToTopic") + successMessage(isPM, emailOrUsername, invitingExistingUserToTopic) { if (this.get("hasGroups")) { return I18n.t("topic.invite_private.success_group"); - } else if (isMessage) { + } else if (isPM) { return I18n.t("topic.invite_private.success"); } else if (invitingExistingUserToTopic) { return I18n.t("topic.invite_reply.success_existing_email", { @@ -242,9 +263,9 @@ export default Ember.Controller.extend(ModalFunctionality, { } }, - @computed("isMessage") - errorMessage(isMessage) { - return isMessage + @computed("isPM") + errorMessage(isPM) { + return isPM ? I18n.t("topic.invite_private.error") : I18n.t("topic.invite_reply.error"); }, @@ -256,18 +277,18 @@ export default Ember.Controller.extend(ModalFunctionality, { : "topic.invite_reply.username_placeholder"; }, - @computed - customMessagePlaceholder() { - return I18n.t("invite.custom_message_placeholder"); - }, + customMessagePlaceholder: i18n("invite.custom_message_placeholder"), // Reset the modal to allow a new user to be invited. reset() { - this.set("emailOrUsername", null); - this.set("hasCustomMessage", false); - this.set("customMessage", null); - this.set("invitingExistingUserToTopic", false); - this.get("model").setProperties({ + this.setProperties({ + emailOrUsername: null, + hasCustomMessage: false, + customMessage: null, + invitingExistingUserToTopic: false + }); + + this.get("inviteModel").setProperties({ groupNames: null, error: false, saving: false, @@ -278,24 +299,23 @@ export default Ember.Controller.extend(ModalFunctionality, { actions: { createInvite() { - const self = this; if (this.get("disabled")) { return; } - const groupNames = this.get("model.groupNames"), - userInvitedController = this.get("userInvitedShow"), - model = this.get("model"); + const groupNames = this.get("inviteModel.groupNames"); + const userInvitedController = this.get("userInvitedShow"); + const model = this.get("inviteModel"); model.setProperties({ saving: true, error: false }); const onerror = e => { if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { - self.set("errorMessage", e.jqXHR.responseJSON.errors[0]); + this.set("errorMessage", e.jqXHR.responseJSON.errors[0]); } else { - self.set( + this.set( "errorMessage", - self.get("isMessage") + this.get("isPM") ? I18n.t("topic.invite_private.error") : I18n.t("topic.invite_reply.error") ); @@ -304,18 +324,18 @@ export default Ember.Controller.extend(ModalFunctionality, { }; if (this.get("hasGroups")) { - return this.get("model") + return this.get("inviteModel") .createGroupInvite(this.get("emailOrUsername").trim()) .then(data => { model.setProperties({ saving: false, finished: true }); - this.get("model.details.allowed_groups").pushObject( + this.get("inviteModel.details.allowed_groups").pushObject( Ember.Object.create(data.group) ); this.appEvents.trigger("post-stream:refresh"); }) .catch(onerror); } else { - return this.get("model") + return this.get("inviteModel") .createInvite( this.get("emailOrUsername").trim(), groupNames, @@ -323,19 +343,18 @@ export default Ember.Controller.extend(ModalFunctionality, { ) .then(result => { model.setProperties({ saving: false, finished: true }); - if (!this.get("invitingToTopic")) { + if (!this.get("invitingToTopic") && userInvitedController) { Invite.findInvitedBy( this.currentUser, userInvitedController.get("filter") - ).then(invite_model => { - userInvitedController.set("model", invite_model); - userInvitedController.set( - "totalInvites", - invite_model.invites.length - ); + ).then(inviteModel => { + userInvitedController.setProperties({ + model: inviteModel, + totalInvites: inviteModel.invites.length + }); }); - } else if (this.get("isMessage") && result && result.user) { - this.get("model.details.allowed_users").pushObject( + } else if (this.get("isPM") && result && result.user) { + this.get("inviteModel.details.allowed_users").pushObject( Ember.Object.create(result.user) ); this.appEvents.trigger("post-stream:refresh"); @@ -353,24 +372,21 @@ export default Ember.Controller.extend(ModalFunctionality, { }, generateInvitelink() { - const self = this; - if (this.get("disabled")) { return; } - const groupNames = this.get("model.groupNames"), - userInvitedController = this.get("userInvitedShow"), - model = this.get("model"); - - var topicId = null; - if (this.get("invitingToTopic")) { - topicId = this.get("model.id"); - } - + const groupNames = this.get("inviteModel.groupNames"); + const userInvitedController = this.get("userInvitedShow"); + const model = this.get("inviteModel"); model.setProperties({ saving: true, error: false }); - return this.get("model") + let topicId; + if (this.get("invitingToTopic")) { + topicId = this.get("inviteModel.id"); + } + + return model .generateInviteLink( this.get("emailOrUsername").trim(), groupNames, @@ -382,24 +398,26 @@ export default Ember.Controller.extend(ModalFunctionality, { finished: true, inviteLink: result }); - Invite.findInvitedBy( - this.currentUser, - userInvitedController.get("filter") - ).then(invite_model => { - userInvitedController.set("model", invite_model); - userInvitedController.set( - "totalInvites", - invite_model.invites.length - ); - }); + + if (userInvitedController) { + Invite.findInvitedBy( + this.currentUser, + userInvitedController.get("filter") + ).then(inviteModel => { + userInvitedController.setProperties({ + model: inviteModel, + totalInvites: inviteModel.invites.length + }); + }); + } }) - .catch(function(e) { + .catch(e => { if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { - self.set("errorMessage", e.jqXHR.responseJSON.errors[0]); + this.set("errorMessage", e.jqXHR.responseJSON.errors[0]); } else { - self.set( + this.set( "errorMessage", - self.get("isMessage") + this.get("isPM") ? I18n.t("topic.invite_private.error") : I18n.t("topic.invite_reply.error") ); @@ -411,7 +429,7 @@ export default Ember.Controller.extend(ModalFunctionality, { showCustomMessageBox() { this.toggleProperty("hasCustomMessage"); if (this.get("hasCustomMessage")) { - if (this.get("model") === this.currentUser) { + if (this.get("inviteModel") === this.currentUser) { this.set( "customMessage", I18n.t("invite.custom_message_template_forum") diff --git a/app/assets/javascripts/discourse/components/modal-panel.js.es6 b/app/assets/javascripts/discourse/components/modal-panel.js.es6 new file mode 100644 index 0000000000..b441457a7d --- /dev/null +++ b/app/assets/javascripts/discourse/components/modal-panel.js.es6 @@ -0,0 +1,11 @@ +import { fmt } from "discourse/lib/computed"; + +export default Ember.Component.extend({ + panel: null, + + panelComponent: fmt("panel.id", "%@-panel"), + + classNameBindings: ["panel.id"], + + classNames: ["modal-panel"] +}); diff --git a/app/assets/javascripts/discourse/components/modal-tab.js.es6 b/app/assets/javascripts/discourse/components/modal-tab.js.es6 new file mode 100644 index 0000000000..275581a293 --- /dev/null +++ b/app/assets/javascripts/discourse/components/modal-tab.js.es6 @@ -0,0 +1,17 @@ +import { propertyEqual } from "discourse/lib/computed"; + +export default Ember.Component.extend({ + tagName: "li", + classNames: ["modal-tab"], + panel: null, + selectedPanel: null, + panelsLength: null, + classNameBindings: ["isActive", "singleTab", "panel.id"], + singleTab: Ember.computed.equal("panelsLength", 1), + title: Ember.computed.alias("panel.title"), + isActive: propertyEqual("panel.id", "selectedPanel.id"), + + click() { + this.onSelectPanel(this.get("panel")); + } +}); diff --git a/app/assets/javascripts/discourse/components/share-button.js.es6 b/app/assets/javascripts/discourse/components/share-button.js.es6 deleted file mode 100644 index 958f821ce6..0000000000 --- a/app/assets/javascripts/discourse/components/share-button.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -import Button from "discourse/components/d-button"; - -export default Button.extend({ - classNames: ["btn-default", "share"], - icon: "link", - title: "topic.share.help", - label: "topic.share.title", - attributeBindings: ["url:data-share-url"], - - click() { - return true; - } -}); diff --git a/app/assets/javascripts/discourse/components/share-panel.js.es6 b/app/assets/javascripts/discourse/components/share-panel.js.es6 new file mode 100644 index 0000000000..2ac3a16cfe --- /dev/null +++ b/app/assets/javascripts/discourse/components/share-panel.js.es6 @@ -0,0 +1,105 @@ +import { escapeExpression } from "discourse/lib/utilities"; +import { longDateNoYear } from "discourse/lib/formatter"; +import { default as computed } from "ember-addons/ember-computed-decorators"; +import Sharing from "discourse/lib/sharing"; + +export default Ember.Component.extend({ + tagName: null, + + date: Ember.computed.alias("panel.model.date"), + type: Ember.computed.alias("panel.model.type"), + postNumber: Ember.computed.alias("panel.model.postNumber"), + postId: Ember.computed.alias("panel.model.postId"), + topic: Ember.computed.alias("panel.model.topic"), + + @computed + sources() { + return Sharing.activeSources(this.siteSettings.share_links); + }, + + @computed("date") + postDate(date) { + return date ? longDateNoYear(new Date(date)) : null; + }, + + @computed("type", "postNumber", "postDate", "topic.title") + shareTitle(type, postNumber, postDate, topicTitle) { + topicTitle = escapeExpression(topicTitle); + + if (type === "topic") { + return I18n.t("share.topic", { topicTitle }); + } + if (postNumber) { + return I18n.t("share.post", { postNumber, postDate }); + } + return I18n.t("share.topic", { topicTitle }); + }, + + @computed("topic.shareUrl") + shareUrl(shareUrl) { + if (Ember.isEmpty(shareUrl)) { + return; + } + + // Relative urls + if (shareUrl.indexOf("/") === 0) { + const location = window.location; + shareUrl = `${location.protocol}//${location.host}${shareUrl}`; + } + + return encodeURI(shareUrl); + }, + + didInsertElement() { + this._super(...arguments); + + const shareUrl = this.get("shareUrl"); + const $linkInput = this.$(".topic-share-url"); + const $linkForTouch = this.$(".topic-share-url-for-touch a"); + + Ember.run.schedule("afterRender", () => { + if (!this.capabilities.touch) { + $linkForTouch.parent().remove(); + + $linkInput + .val(shareUrl) + .select() + .focus(); + } else { + $linkInput.remove(); + + $linkForTouch.attr("href", shareUrl).text(shareUrl); + + const range = window.document.createRange(); + range.selectNode($linkForTouch[0]); + window.getSelection().addRange(range); + } + }); + }, + + actions: { + share(source) { + const url = source.generateUrl( + this.get("shareUrl"), + this.get("topic.title") + ); + const options = { + menubar: "no", + toolbar: "no", + resizable: "yes", + scrollbars: "yes", + width: 600, + height: source.popupHeight || 315 + }; + const stringOptions = Object.keys(options) + .map(k => `${k}=${options[k]}`) + .join(","); + + if (source.shouldOpenInPopup) { + window.open(url, "", stringOptions); + } else { + window.open(url, "_blank"); + } + } + } +}); diff --git a/app/assets/javascripts/discourse/components/share-popup.js.es6 b/app/assets/javascripts/discourse/components/share-popup.js.es6 deleted file mode 100644 index 29f3f482ce..0000000000 --- a/app/assets/javascripts/discourse/components/share-popup.js.es6 +++ /dev/null @@ -1,195 +0,0 @@ -import { wantsNewWindow } from "discourse/lib/intercept-click"; -import { longDateNoYear } from "discourse/lib/formatter"; -import computed from "ember-addons/ember-computed-decorators"; -import Sharing from "discourse/lib/sharing"; - -export default Ember.Component.extend({ - elementId: "share-link", - classNameBindings: ["visible"], - link: null, - visible: null, - - @computed - sources() { - return Sharing.activeSources(this.siteSettings.share_links); - }, - - @computed("type", "postNumber") - shareTitle(type, postNumber) { - if (type === "topic") { - return I18n.t("share.topic"); - } - if (postNumber) { - return I18n.t("share.post", { postNumber }); - } - return I18n.t("share.topic"); - }, - - @computed("date") - displayDate(date) { - return longDateNoYear(new Date(date)); - }, - - _focusUrl() { - const link = this.get("link"); - if (!this.capabilities.touch) { - const $linkInput = $("#share-link input"); - $linkInput.val(link); - - // Wait for the fade-in transition to finish before selecting the link: - window.setTimeout(() => $linkInput.select().focus(), 160); - } else { - const $linkForTouch = $("#share-link .share-for-touch a"); - $linkForTouch.attr("href", link); - $linkForTouch.text(link); - const range = window.document.createRange(); - range.selectNode($linkForTouch[0]); - window.getSelection().addRange(range); - } - }, - - _showUrl($target, url) { - const $currentTargetOffset = $target.offset(); - const $this = this.$(); - - if (Ember.isEmpty(url)) { - return; - } - - // Relative urls - if (url.indexOf("/") === 0) { - url = window.location.protocol + "//" + window.location.host + url; - } - - const shareLinkWidth = $this.width(); - let x = $currentTargetOffset.left - shareLinkWidth / 2; - if (x < 25) { - x = 25; - } - if (x + shareLinkWidth > $(window).width()) { - x -= shareLinkWidth / 2; - } - - const header = $(".d-header"); - let y = $currentTargetOffset.top - ($this.height() + 20); - if (y < header.offset().top + header.height()) { - y = $currentTargetOffset.top + 10; - } - - $this.css({ top: "" + y + "px" }); - - if (!this.site.mobileView) { - $this.css({ left: "" + x + "px" }); - } - this.set("link", encodeURI(url)); - this.set("visible", true); - - Ember.run.scheduleOnce("afterRender", this, this._focusUrl); - }, - - _webShare(url) { - // We can pass title and text too, but most share targets do their own oneboxing - return navigator.share({ url }); - }, - - didInsertElement() { - this._super(...arguments); - - const $html = $("html"); - $html.on("mousedown.outside-share-link", e => { - // Use mousedown instead of click so this event is handled before routing occurs when a - // link is clicked (which is a click event) while the share dialog is showing. - if (this.$().has(e.target).length !== 0) { - return; - } - this.send("close"); - return true; - }); - - $html.on( - "click.discourse-share-link", - "button[data-share-url], .post-info .post-date[data-share-url]", - e => { - // if they want to open in a new tab, let it so - if (wantsNewWindow(e)) { - return true; - } - - e.preventDefault(); - - const $currentTarget = $(e.currentTarget); - const url = $currentTarget.data("share-url"); - const postNumber = $currentTarget.data("post-number"); - const postId = $currentTarget.closest("article").data("post-id"); - const date = $currentTarget.children().data("time"); - - this.setProperties({ postNumber, date, postId }); - - // use native webshare only when the user clicks on the "chain" icon - // navigator.share needs HTTPS, returns undefined on HTTP - if (navigator.share && !$currentTarget.hasClass("post-date")) { - this._webShare(url).catch(() => { - // if navigator fails for unexpected reason fallback to popup - this._showUrl($currentTarget, url); - }); - } else { - this._showUrl($currentTarget, url); - } - - return false; - } - ); - - $html.on("keydown.share-view", e => { - if (e.keyCode === 27) { - this.send("close"); - } - }); - - this.appEvents.on("share:url", (url, $target) => - this._showUrl($target, url) - ); - }, - - willDestroyElement() { - this._super(...arguments); - $("html") - .off("click.discourse-share-link") - .off("mousedown.outside-share-link") - .off("keydown.share-view"); - }, - - actions: { - replyAsNewTopic() { - const postStream = this.get("topic.postStream"); - const postId = - this.get("postId") || postStream.findPostIdForPostNumber(1); - const post = postStream.findLoadedPost(postId); - this.get("replyAsNewTopic")(post); - this.send("close"); - }, - - close() { - this.setProperties({ - link: null, - postNumber: null, - postId: null, - visible: false - }); - }, - - share(source) { - const url = source.generateUrl(this.get("link"), this.get("topic.title")); - if (source.shouldOpenInPopup) { - window.open( - url, - "", - "menubar=no,toolbar=no,resizable=yes,scrollbars=yes,width=600,height=" + - (source.popupHeight || 315) - ); - } else { - window.open(url, "_blank"); - } - } - } -}); diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index e07f06ccf4..a25b3f65e9 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -34,7 +34,8 @@ export default TextField.extend({ single = bool("single"), allowAny = bool("allowAny"), disabled = bool("disabled"), - disallowEmails = bool("disallowEmails"); + disallowEmails = bool("disallowEmails"), + fullWidthWrap = bool("fullWidthWrap"); function excludedUsernames() { // hack works around some issues with allowAny eventing @@ -54,6 +55,7 @@ export default TextField.extend({ single: single, allowAny: allowAny, updateData: opts && opts.updateData ? opts.updateData : false, + fullWidthWrap, dataSource(term) { var results = userSearch({ diff --git a/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js.es6 b/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js.es6 index 9c3ba80f80..812ac3f7d4 100644 --- a/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js.es6 +++ b/app/assets/javascripts/discourse/initializers/topic-footer-buttons.js.es6 @@ -1,3 +1,5 @@ +import showModal from "discourse/lib/show-modal"; +import { share } from "discourse/lib/pwa-utils"; import { registerTopicFooterButton } from "discourse/lib/register-topic-footer-button"; export default { @@ -5,25 +7,100 @@ export default { initialize() { registerTopicFooterButton({ - id: "share", + id: "native-share", icon: "link", priority: 999, label: "topic.share.title", title: "topic.share.help", action() { - this.appEvents.trigger( - "share:url", - this.get("topic.shareUrl"), - $("#topic-footer-buttons") + share({ url: this.get("topic.shareUrl") }).catch(() => + showModal("share-and-invite", { + modalClass: "share-and-invite", + panels: [ + { + id: "share", + title: "topic.share.title", + model: { topic: this.get("topic") } + } + ] + }) ); }, + dropdown: true, + classNames: ["native-share"], + dependentKeys: ["topic.shareUrl", "topic.isPrivateMessage"], + displayed() { + return window.navigator.share; + } + }); + + registerTopicFooterButton({ + id: "share-and-invite", + icon: "link", + priority: 999, + label: "topic.share.title", + title: "topic.share.help", + action() { + const modal = () => { + const panels = [ + { + id: "share", + title: "topic.share.extended_title", + model: { + topic: this.get("topic") + } + } + ]; + + if (this.get("canInviteTo") && !this.get("inviteDisabled")) { + let invitePanelTitle; + + if (this.get("isPM")) { + invitePanelTitle = "topic.invite_private.title"; + } else if (this.get("invitingToTopic")) { + invitePanelTitle = "topic.invite_reply.title"; + } else { + invitePanelTitle = "user.invited.create"; + } + + panels.push({ + id: "invite", + title: invitePanelTitle, + model: { + inviteModel: this.get("topic") + } + }); + } + + showModal("share-and-invite", { + model: this.get("topic"), + modalClass: "share-and-invite", + panels + }); + }; + + if (window.navigator.share) { + window.navigator + .share({ url: this.get("topic.shareUrl") }) + .catch(() => modal()); + } else { + modal(); + } + }, dropdown() { return this.site.mobileView; }, - classNames: ["share"], - dependentKeys: ["topic.shareUrl", "topic.isPrivateMessage"], + classNames: ["share-and-invite"], + dependentKeys: [ + "topic.shareUrl", + "topic.isPrivateMessage", + "canInviteTo", + "inviteDisabled", + "isPM", + "invitingToTopic" + ], displayed() { - return !this.get("topic.isPrivateMessage"); + return !(this.site.mobileView && window.navigator.share); } }); @@ -47,26 +124,6 @@ export default { } }); - registerTopicFooterButton({ - id: "invite", - icon: "users", - priority: 997, - label: "topic.invite_reply.title", - title: "topic.invite_reply.help", - action: "showInvite", - dropdown() { - return this.site.mobileView; - }, - classNames: ["invite-topic"], - dependentKeys: ["canInviteTo", "inviteDisabled"], - displayed() { - return this.get("canInviteTo"); - }, - disabled() { - return this.get("inviteDisabled"); - } - }); - registerTopicFooterButton({ dependentKeys: ["topic.bookmarked", "topic.isPrivateMessage"], id: "bookmark", diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 index 939eef6a90..a75568731f 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -201,7 +201,10 @@ export default function(options) { wrap = this.wrap( "
" ).parent(); - wrap.width(width); + + if (!options.fullWidthWrap) { + wrap.width(width); + } } if (options.single && !options.width) { diff --git a/app/assets/javascripts/discourse/lib/pwa-utils.js.es6 b/app/assets/javascripts/discourse/lib/pwa-utils.js.es6 new file mode 100644 index 0000000000..fc4d896e68 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/pwa-utils.js.es6 @@ -0,0 +1,12 @@ +export function share(data) { + return new Ember.RSVP.Promise((resolve, reject) => { + if (window.location.protocol === "https:" && window.navigator.share) { + window.navigator + .share(data) + .catch(reject) + .then(resolve); + } else { + reject(); + } + }); +} diff --git a/app/assets/javascripts/discourse/lib/show-modal.js.es6 b/app/assets/javascripts/discourse/lib/show-modal.js.es6 index 675a0e37f8..3fa2b31fc1 100644 --- a/app/assets/javascripts/discourse/lib/show-modal.js.es6 +++ b/app/assets/javascripts/discourse/lib/show-modal.js.es6 @@ -64,6 +64,24 @@ export default function(name, opts) { modalController.set("title", I18n.t(opts.title)); } + if (opts.panels) { + modalController.setProperties({ + panels: opts.panels, + selectedPanel: opts.panels[0] + }); + + if (controller.actions.onSelectPanel) { + modalController.set("onSelectPanel", controller.actions.onSelectPanel); + } + + modalController.set( + "modalClass", + `${modalController.get("modalClass")} has-tabs` + ); + } else { + modalController.setProperties({ panels: [], selectedPanel: null }); + } + controller.set("modal", modalController); const model = opts.model; if (model) { diff --git a/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 b/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 index ac024919ee..0caef43943 100644 --- a/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 +++ b/app/assets/javascripts/discourse/mixins/modal-functionality.js.es6 @@ -16,6 +16,11 @@ export default Ember.Mixin.create({ actions: { closeModal() { this.get("modal").send("closeModal"); + this.set("panels", []); + }, + + onSelectPanel(panel) { + this.set("selectedPanel", panel); } } }); diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index c6e4cf4208..78673ca69c 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -92,11 +92,6 @@ const TopicRoute = Discourse.Route.extend({ this.controllerFor("feature_topic").reset(); }, - showInvite() { - showModal("invite", { model: this.modelFor("topic") }); - this.controllerFor("invite").reset(); - }, - showHistory(model, revision) { showModal("history", { model }); const historyController = this.controllerFor("history"); diff --git a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 index 31005cd3d9..d9e2d4ba71 100644 --- a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 @@ -30,8 +30,19 @@ export default Discourse.Route.extend({ actions: { showInvite() { - showModal("invite", { model: this.currentUser }); - this.controllerFor("invite").reset(); + showModal("share-and-invite", { + modalClass: "share-and-invite", + panels: [ + { + id: "invite", + title: "user.invited.create", + model: { + inviteModel: this.currentUser, + userInvitedShow: this.controllerFor("user-invited-show") + } + } + ] + }); } } }); diff --git a/app/assets/javascripts/discourse/templates/components/d-modal.hbs b/app/assets/javascripts/discourse/templates/components/d-modal.hbs index 374868f5e8..4e7cd0dca2 100644 --- a/app/assets/javascripts/discourse/templates/components/d-modal.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-modal.hbs @@ -10,13 +10,25 @@ {{/if}} -{{subtitle}}
- {{/if}} -{{subtitle}}
+ {{/if}} +