From 8c2d7dcaacd0a646c65c86672816790f85e9bdf9 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 31 Mar 2015 00:06:47 +0530 Subject: [PATCH] FEATURE: invite existing user to a topic --- .../controllers/invite-private.js.es6 | 47 ------------- .../discourse/controllers/invite.js.es6 | 66 ++++++++++++++++--- .../javascripts/discourse/routes/topic.js.es6 | 10 --- .../discourse/templates/modal/invite.hbs | 10 ++- .../templates/modal/invite_private.hbs | 23 ------- .../discourse/views/invite-private.js.es6 | 6 -- .../javascripts/discourse/views/invite.js.es6 | 12 ++-- .../views/topic-map-container.js.es6 | 3 +- app/mailers/user_notifications.rb | 8 ++- app/models/notification.rb | 2 +- app/models/topic.rb | 17 ++++- app/models/user_email_observer.rb | 4 ++ config/locales/client.en.yml | 9 +-- config/locales/server.en.yml | 8 +++ spec/mailers/user_notifications_spec.rb | 7 ++ spec/models/user_email_observer_spec.rb | 18 +++++ 16 files changed, 137 insertions(+), 113 deletions(-) delete mode 100644 app/assets/javascripts/discourse/controllers/invite-private.js.es6 delete mode 100644 app/assets/javascripts/discourse/templates/modal/invite_private.hbs delete mode 100644 app/assets/javascripts/discourse/views/invite-private.js.es6 diff --git a/app/assets/javascripts/discourse/controllers/invite-private.js.es6 b/app/assets/javascripts/discourse/controllers/invite-private.js.es6 deleted file mode 100644 index 94e1d6632de..00000000000 --- a/app/assets/javascripts/discourse/controllers/invite-private.js.es6 +++ /dev/null @@ -1,47 +0,0 @@ -import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; - -export default ObjectController.extend(ModalFunctionality, { - modalClass: 'invite', - - isAdmin: function(){ - return Discourse.User.currentProp("admin"); - }.property(), - - onShow: function(){ - this.set('controllers.modal.modalClass', 'invite-modal'); - this.set('emailOrUsername', ''); - }, - - disabled: function() { - if (this.get('saving')) return true; - return this.blank('emailOrUsername'); - }.property('emailOrUsername', 'saving'), - - buttonTitle: function() { - if (this.get('saving')) return I18n.t('topic.inviting'); - return I18n.t('topic.invite_private.action'); - }.property('saving'), - - actions: { - invite: function() { - if (this.get('disabled')) return; - - var self = this; - this.setProperties({saving: true, error: false}); - - // Invite the user to the private message - this.get('model').createInvite(this.get('emailOrUsername')).then(function(result) { - self.setProperties({saving: true, finished: true}); - - if(result && result.user) { - self.get('model.details.allowed_users').pushObject(result.user); - } - }).catch(function() { - self.setProperties({error: true, saving: false}); - }); - return false; - } - } - -}); diff --git a/app/assets/javascripts/discourse/controllers/invite.js.es6 b/app/assets/javascripts/discourse/controllers/invite.js.es6 index 6adc0f02081..ad595bb4c46 100644 --- a/app/assets/javascripts/discourse/controllers/invite.js.es6 +++ b/app/assets/javascripts/discourse/controllers/invite.js.es6 @@ -6,7 +6,7 @@ export default ObjectController.extend(ModalFunctionality, { // If this isn't defined, it will proxy to the user model on the preferences // page which is wrong. - email: null, + emailOrUsername: null, isAdmin: function(){ return Discourse.User.currentProp("admin"); @@ -19,12 +19,12 @@ export default ObjectController.extend(ModalFunctionality, { **/ disabled: function() { if (this.get('saving')) return true; - if (this.blank('email')) return true; - if (!Discourse.Utilities.emailValid(this.get('email'))) return true; + if (this.blank('emailOrUsername')) return true; + if ( !this.get('invitingToTopic') && !Discourse.Utilities.emailValid(this.get('emailOrUsername')) ) return true; if (this.get('model.details.can_invite_to')) return false; if (this.get('isPrivateTopic') && this.blank('groupNames')) return true; return false; - }.property('email', 'isPrivateTopic', 'groupNames', 'saving'), + }.property('emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'groupNames', 'saving'), /** The current text for the invite button @@ -53,18 +53,45 @@ export default ObjectController.extend(ModalFunctionality, { **/ isPrivateTopic: Em.computed.and('invitingToTopic', 'model.category.read_restricted'), + /** + Is Message? + + @property isMessage + **/ + isMessage: Em.computed.equal('model.archetype', 'private_message'), + + /** + Allow Existing Members? (username autocomplete) + + @property allowExistingMembers + **/ + allowExistingMembers: function() { + return this.get('invitingToTopic') && !this.get('isPrivateTopic'); + }.property('invitingToTopic', 'isPrivateTopic'), + + /** + Show Groups? (add invited user to private group) + + @property showGroups + **/ + showGroups: function() { + return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')); + }.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'invitingToTopic'), + /** Instructional text for the modal. @property inviteInstructions **/ inviteInstructions: function() { - if (this.get('invitingToTopic')) { + if (this.get('isMessage')) { + return I18n.t('topic.invite_private.email_or_username'); + } else if (this.get('invitingToTopic')) { return I18n.t('topic.invite_reply.to_topic'); } else { return I18n.t('topic.invite_reply.to_forum'); } - }.property('invitingToTopic'), + }.property('isMessage', 'invitingToTopic'), /** Instructional text for the group selection. @@ -92,8 +119,25 @@ export default ObjectController.extend(ModalFunctionality, { @property successMessage **/ successMessage: function() { - return I18n.t('topic.invite_reply.success', { email: this.get('email') }); - }.property('email'), + if (this.get('isMessage')) { + return I18n.t('topic.invite_private.success'); + } else { + return I18n.t('topic.invite_reply.success', { emailOrUsername: this.get('emailOrUsername') }); + } + }.property('isMessage', 'emailOrUsername'), + + /** + The "error" text for when the invite fails. + + @property errorMessage + **/ + errorMessage: function() { + if (this.get('isMessage')) { + return I18n.t('topic.invite_private.error'); + } else { + return I18n.t('topic.invite_reply.error'); + } + }.property('isMessage'), /** Reset the modal to allow a new user to be invited. @@ -102,7 +146,7 @@ export default ObjectController.extend(ModalFunctionality, { **/ reset: function() { this.setProperties({ - email: null, + emailOrUsername: null, groupNames: null, error: false, saving: false, @@ -126,13 +170,15 @@ export default ObjectController.extend(ModalFunctionality, { var userInvitedController = this.get('controllers.user-invited'); this.setProperties({ saving: true, error: false }); - this.get('model').createInvite(this.get('email'), groupNames).then(function() { + this.get('model').createInvite(this.get('emailOrUsername'), groupNames).then(function(result) { self.setProperties({ saving: false, finished: true }); if (!self.get('invitingToTopic')) { Discourse.Invite.findInvitedBy(Discourse.User.current()).then(function (invite_model) { userInvitedController.set('model', invite_model); userInvitedController.set('totalInvites', invite_model.invites.length); }); + } else if (self.get('isMessage') && result && result.user) { + self.get('model.details.allowed_users').pushObject(result.user); } }).catch(function() { self.setProperties({ saving: false, error: true }); diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index d7948ce3a35..725c895ead2 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -69,16 +69,6 @@ const TopicRoute = Discourse.Route.extend(ShowFooter, { this.controllerFor('invite').reset(); }, - showPrivateInvite() { - showModal('invitePrivate', this.modelFor('topic')); - this.controllerFor('invitePrivate').setProperties({ - email: null, - error: false, - saving: false, - finished: false - }); - }, - showHistory(post) { showModal('history', post); this.controllerFor('history').refresh(post.get("id"), "latest"); diff --git a/app/assets/javascripts/discourse/templates/modal/invite.hbs b/app/assets/javascripts/discourse/templates/modal/invite.hbs index 3b74f7afab0..52a8767298b 100644 --- a/app/assets/javascripts/discourse/templates/modal/invite.hbs +++ b/app/assets/javascripts/discourse/templates/modal/invite.hbs @@ -2,7 +2,7 @@ {{#if error}}
- {{i18n 'topic.invite_reply.error'}} + {{errorMessage}}
{{/if}} @@ -11,9 +11,13 @@ {{else}} - {{text-field value=email placeholderKey="topic.invite_reply.email_placeholder"}} + {{#if allowExistingMembers}} + {{user-selector single="true" allowAny=true usernames=emailOrUsername includeGroups="true" placeholderKey="topic.invite_private.email_or_username_placeholder"}} + {{else}} + {{text-field value=emailOrUsername placeholderKey="topic.invite_reply.email_placeholder"}} + {{/if}} - {{#if isAdmin}} + {{#if showGroups}} {{group-selector groupFinder=groupFinder groupNames=groupNames placeholderKey="topic.invite_private.group_name"}} {{/if}} diff --git a/app/assets/javascripts/discourse/templates/modal/invite_private.hbs b/app/assets/javascripts/discourse/templates/modal/invite_private.hbs deleted file mode 100644 index 0284697cefd..00000000000 --- a/app/assets/javascripts/discourse/templates/modal/invite_private.hbs +++ /dev/null @@ -1,23 +0,0 @@ - - diff --git a/app/assets/javascripts/discourse/views/invite-private.js.es6 b/app/assets/javascripts/discourse/views/invite-private.js.es6 deleted file mode 100644 index 461a6e102de..00000000000 --- a/app/assets/javascripts/discourse/views/invite-private.js.es6 +++ /dev/null @@ -1,6 +0,0 @@ -import ModalBodyView from "discourse/views/modal-body"; - -export default ModalBodyView.extend({ - templateName: 'modal/invite_private', - title: I18n.t('topic.invite_private.title') -}); diff --git a/app/assets/javascripts/discourse/views/invite.js.es6 b/app/assets/javascripts/discourse/views/invite.js.es6 index 03f4a72c4dc..0c4a5b67a65 100644 --- a/app/assets/javascripts/discourse/views/invite.js.es6 +++ b/app/assets/javascripts/discourse/views/invite.js.es6 @@ -4,9 +4,13 @@ export default ModalBodyView.extend({ templateName: 'modal/invite', title: function() { - return this.get('controller.invitingToTopic') ? - I18n.t('topic.invite_reply.title') : - I18n.t('user.invited.create'); - }.property('controller.invitingToTopic') + if (this.get('controller.isMessage')) { + return I18n.t('topic.invite_private.title'); + } else if (this.get('controller.invitingToTopic')) { + return I18n.t('topic.invite_reply.title'); + } else { + return I18n.t('user.invited.create'); + } + }.property('controller.{invitingToTopic,isMessage}') }); diff --git a/app/assets/javascripts/discourse/views/topic-map-container.js.es6 b/app/assets/javascripts/discourse/views/topic-map-container.js.es6 index 5dee27706c3..b12cdd45e32 100644 --- a/app/assets/javascripts/discourse/views/topic-map-container.js.es6 +++ b/app/assets/javascripts/discourse/views/topic-map-container.js.es6 @@ -51,8 +51,7 @@ export default DiscourseContainerView.extend({ // If we have a private message if (this.get('topic.isPrivateMessage')) { - container.attachViewWithArgs({ topic: topic, showPrivateInviteAction: 'showPrivateInvite' }, PrivateMessageMapComponent); + container.attachViewWithArgs({ topic: topic, showPrivateInviteAction: 'showInvite' }, PrivateMessageMapComponent); } } }); - diff --git a/app/mailers/user_notifications.rb b/app/mailers/user_notifications.rb index 5fede4157ef..a977ba653f5 100644 --- a/app/mailers/user_notifications.rb +++ b/app/mailers/user_notifications.rb @@ -124,6 +124,11 @@ class UserNotifications < ActionMailer::Base notification_email(user, opts) end + def user_invited_to_topic(user, opts) + opts[:show_category_in_subject] = true + notification_email(user, opts) + end + def mailing_list_notify(user, post) send_notification_email( title: post.topic.title, @@ -188,11 +193,12 @@ class UserNotifications < ActionMailer::Base use_site_subject = opts[:use_site_subject] add_re_to_subject = opts[:add_re_to_subject] show_category_in_subject = opts[:show_category_in_subject] + original_username = @notification.data_hash[:original_username] || @notification.data_hash[:display_username] send_notification_email( title: title, post: @post, - username: @notification.data_hash[:original_username], + username: original_username, from_alias: user_name, allow_reply_by_email: allow_reply_by_email, use_site_subject: use_site_subject, diff --git a/app/models/notification.rb b/app/models/notification.rb index bd96c36dc98..3c0565907fb 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -31,7 +31,7 @@ class Notification < ActiveRecord::Base @types ||= Enum.new( :mentioned, :replied, :quoted, :edited, :liked, :private_message, :invited_to_private_message, :invitee_accepted, :posted, :moved_post, - :linked, :granted_badge + :linked, :granted_badge, :invited_to_topic ) end diff --git a/app/models/topic.rb b/app/models/topic.rb index eada4a11ff4..d48bf6ccc53 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -537,7 +537,7 @@ class Topic < ActiveRecord::Base # Invite a user to the topic by username or email. Returns success/failure def invite(invited_by, username_or_email, group_ids=nil) if private_message? - # If the user exists, add them to the topic. + # If the user exists, add them to the message. user = User.find_by_username_or_email(username_or_email) if user && topic_allowed_users.create!(user_id: user.id) @@ -555,7 +555,20 @@ class Topic < ActiveRecord::Base # NOTE callers expect an invite object if an invite was sent via email invite_by_email(invited_by, username_or_email, group_ids) else - false + # invite existing member to a topic + user = User.find_by_username_or_email(username_or_email) + if user && topic_allowed_users.create!(user_id: user.id) + + # Notify the user they've been invited + user.notifications.create(notification_type: Notification.types[:invited_to_topic], + topic_id: id, + post_number: 1, + data: { topic_title: title, + display_username: invited_by.username }.to_json) + return true + else + false + end end end diff --git a/app/models/user_email_observer.rb b/app/models/user_email_observer.rb index 034a2e6492a..e2389f5bb4f 100644 --- a/app/models/user_email_observer.rb +++ b/app/models/user_email_observer.rb @@ -32,6 +32,10 @@ class UserEmailObserver < ActiveRecord::Observer enqueue :user_invited_to_private_message end + def invited_to_topic + enqueue :user_invited_to_topic + end + private def enqueue(type) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 532d27e9be2..b4419496095 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -481,7 +481,7 @@ en: weekly: "weekly" every_two_weeks: "every two weeks" - email_direct: "Send me an email when someone quotes me, replies to my post, or mentions my @username" + email_direct: "Send me an email when someone quotes me, replies to my post, or mentions my @username or invites me to a topic" email_private_messages: "Send me an email when someone messages me" email_always: "Do not suppress email notifications when I am active on the site" @@ -786,6 +786,7 @@ en: liked: "

{{username}} {{description}}

" private_message: "

{{username}} {{description}}

" invited_to_private_message: "

{{username}} {{description}}

" + invited_to_topic: "

{{username}} {{description}}

" invitee_accepted: "

{{username}} accepted your invitation

" moved_post: "

{{username}} moved {{description}}

" linked: "

{{username}} {{description}}

" @@ -1067,14 +1068,14 @@ en: invite_reply: title: 'Invite' - action: 'Email Invite' + action: 'Send Invite' help: 'send invitations to friends so they can reply to this topic with a single click' to_topic: "We'll send a brief email allowing your friend to immediately join and reply to this topic by clicking a link, no login required." to_forum: "We'll send a brief email allowing your friend to immediately join by clicking a link, no login required." email_placeholder: 'name@example.com' - success: "We mailed out an invitation to {{email}}. We'll notify you when the invitation is redeemed. Check the invitations tab on your user page to keep track of your invites." - error: "Sorry, we couldn't invite that person. Perhaps they are already a user? (Invites are rate limited)" + success: "We mailed out an invitation to {{emailOrUsername}}. We'll notify you when the invitation is redeemed. Check the invitations tab on your user page to keep track of your invites." + error: "Sorry, we couldn't invite that person. Perhaps they have already been invited? (Invites are rate limited)" login_reply: 'Log In to Reply' diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index cac0c532a07..cdb6d5600f4 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1157,6 +1157,7 @@ en: moved_post: "%{display_username} moved your post to %{link}" private_message: "%{display_username} sent you a message: %{link}" invited_to_private_message: "%{display_username} invited you to a message: %{link}" + invited_to_topic: "%{display_username} invited you to a topic: %{link}" invitee_accepted: "%{display_username} accepted your invitation" linked: "%{display_username} linked you in %{link}" granted_badge: "You earned %{link}" @@ -1772,6 +1773,13 @@ en: Please visit this link to view the topic: %{base_url}%{url} + user_invited_to_topic: + subject_template: "[%{site_name}] %{username} invited you to a topic '%{topic_title}'" + text_body_template: | + %{username} invited you to a topic '%{topic_title}' on %{site_name}: + + Please visit this link to view the topic: %{base_url}%{url} + user_replied: subject_template: "[%{site_name}] %{topic_title}" text_body_template: | diff --git a/spec/mailers/user_notifications_spec.rb b/spec/mailers/user_notifications_spec.rb index 18921cfb97b..100b5cfb172 100644 --- a/spec/mailers/user_notifications_spec.rb +++ b/spec/mailers/user_notifications_spec.rb @@ -326,4 +326,11 @@ describe UserNotifications do end end + describe "user invited to a topic" do + include_examples "notification email building" do + let(:notification_type) { :invited_to_topic } + include_examples "no reply by email" + end + end + end diff --git a/spec/models/user_email_observer_spec.rb b/spec/models/user_email_observer_spec.rb index 331d6eb636c..20c632e1293 100644 --- a/spec/models/user_email_observer_spec.rb +++ b/spec/models/user_email_observer_spec.rb @@ -98,4 +98,22 @@ describe UserEmailObserver do end + context 'user_invited_to_topic' do + + let(:user) { Fabricate(:user) } + let!(:notification) { Fabricate(:notification, user: user, notification_type: 13) } + + it "enqueues a job for the email" do + Jobs.expects(:enqueue_in).with(SiteSetting.email_time_window_mins.minutes, :user_email, type: :user_invited_to_topic, user_id: notification.user_id, notification_id: notification.id) + UserEmailObserver.send(:new).after_commit(notification) + end + + it "doesn't enqueue an email if the user has mention emails disabled" do + user.expects(:email_direct?).returns(false) + Jobs.expects(:enqueue_in).with(SiteSetting.email_time_window_mins.minutes, :user_email, has_entry(type: :user_invited_to_topic)).never + UserEmailObserver.send(:new).after_commit(notification) + end + + end + end