mirror of
https://github.com/discourse/discourse.git
synced 2025-02-20 14:32:01 +08:00
Merge pull request #3313 from techAPJ/patch-1
FEATURE: invite existing user to a topic
This commit is contained in:
commit
0e1c4a0a44
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -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 });
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{{#if error}}
|
||||
<div class="alert alert-error">
|
||||
<button class="close" data-dismiss="alert">×</button>
|
||||
{{i18n 'topic.invite_reply.error'}}
|
||||
{{errorMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
@ -11,9 +11,13 @@
|
|||
{{else}}
|
||||
|
||||
<label>{{inviteInstructions}}</label>
|
||||
{{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}}
|
||||
<label>{{{groupInstructions}}}</label>
|
||||
{{group-selector groupFinder=groupFinder groupNames=groupNames placeholderKey="topic.invite_private.group_name"}}
|
||||
{{/if}}
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
<div class="modal-body">
|
||||
{{#if error}}
|
||||
<div class="alert alert-error">
|
||||
<button class="close" data-dismiss="alert">×</button>
|
||||
{{i18n 'topic.invite_private.error'}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if finished}}
|
||||
{{i18n 'topic.invite_private.success'}}
|
||||
{{else}}
|
||||
<label>{{i18n 'topic.invite_private.email_or_username'}}</label>
|
||||
{{user-selector single="true" allowAny=true usernames=emailOrUsername includeGroups="true" placeholderKey="topic.invite_private.email_or_username_placeholder"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
{{#if finished}}
|
||||
<button class='btn btn-primary' {{action "closeModal"}}>{{i18n 'close'}}</button>
|
||||
{{else}}
|
||||
<button class='btn btn-primary' {{bind-attr disabled="disabled"}} {{action "invite"}}>{{buttonTitle}}</button>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
|
@ -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')
|
||||
});
|
|
@ -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}')
|
||||
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -484,7 +484,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"
|
||||
|
||||
|
@ -789,6 +789,7 @@ en:
|
|||
liked: "<i title='liked' class='fa fa-heart'></i><p><span>{{username}}</span> {{description}}</p>"
|
||||
private_message: "<i title='private message' class='fa fa-envelope-o'></i><p><span>{{username}}</span> {{description}}</p>"
|
||||
invited_to_private_message: "<i title='private message' class='fa fa-envelope-o'></i><p><span>{{username}}</span> {{description}}</p>"
|
||||
invited_to_topic: "<i title='invited to topic' class='fa fa-envelope-o'></i><p><span>{{username}}</span> {{description}}</p>"
|
||||
invitee_accepted: "<i title='accepted your invitation' class='fa fa-user'></i><p><span>{{username}}</span> accepted your invitation</p>"
|
||||
moved_post: "<i title='moved post' class='fa fa-sign-out'></i><p><span>{{username}}</span> moved {{description}}</p>"
|
||||
linked: "<i title='linked post' class='fa fa-arrow-left'></i><p><span>{{username}}</span> {{description}}</p>"
|
||||
|
@ -1072,14 +1073,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 <b>{{email}}</b>. 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 <b>{{emailOrUsername}}</b>. 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'
|
||||
|
||||
|
|
|
@ -1158,6 +1158,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}"
|
||||
|
@ -1773,6 +1774,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: |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user