feat(usernames): invite by username

This commit is contained in:
Sabe Jones
2018-11-07 15:00:22 -06:00
parent 4f86abd6b2
commit 1ac4dd8171
10 changed files with 385 additions and 163 deletions

View File

@@ -16,6 +16,7 @@
} }
.modal-dialog { .modal-dialog {
margin: 5.5rem auto 3rem;
width: auto; width: auto;
.title { .title {

View File

@@ -1,152 +1,178 @@
<template lang="pug"> <template lang="pug">
b-modal#invite-modal(:title="$t('inviteFriends')", size='lg') b-modal#invite-modal(:title='$t(`inviteTo${groupType}`)', :hide-footer='true')
.modal-body div
p.alert.alert-info(v-html="$t('inviteAlertInfo')") strong {{ $t('inviteEmailUsername') }}
.form-horizontal .small {{ $t('inviteEmailUsernameInfo') }}
table.table.table-striped .input-group(v-for='(invite, index) in invites')
thead .d-flex.align-items-center.justify-content-center(v-if='index === invites.length - 1 && invite.text.length === 0')
tr .svg-icon.positive-icon(v-html='icons.positiveIcon')
th {{ $t('userId') }} input.form-control(
tbody type='text',
tr(v-for='user in invitees') :placeholder='$t("emailOrUsernameInvite")',
td v-model='invite.text',
input.form-control(type='text', v-model='user.uuid') v-on:change='checkInviteList',
tr :class='{"input-valid": invite.valid, "is-invalid input-invalid": invite.valid === false}',
td )
button.btn.btn-primary.pull-right(@click='addUuid()') .modal-footer.d-flex.justify-content-center
i.glyphicon.glyphicon-plus a.mr-3(@click='close()') {{ $t('cancel') }}
| + button.btn.btn-primary(@click='sendInvites()', :class='{disabled: cannotSubmit}', :disabled='cannotSubmit') {{ $t('sendInvitations') }}
tr
td
.col-6.col-offset-6
button.btn.btn-primary.btn-block(@click='inviteNewUsers("uuid")') {{sendInviteText}}
hr
p.alert.alert-info {{ $t('inviteByEmail') }}
.form-horizontal
table.table.table-striped
thead
tr
th {{ $t('name') }}
th {{ $t('email') }}
tbody
tr(v-for='email in emails')
td
input.form-control(type='text', v-model='email.name')
td
input.form-control(type='email', v-model='email.email')
tr
td(colspan=2)
button.btn.btn-primary.pull-right(@click='addEmail()')
i.glyphicon.glyphicon-plus
| +
tr
td.form-group(colspan=2)
label.col-sm-1.control-label {{ $t('byColon') }}
.col-sm-5
input.form-control(type='text', v-model='inviter')
.col-sm-6
button.btn.btn-primary.btn-block(@click='inviteNewUsers("email")') {{sendInviteText}}
</template> </template>
<style lang="scss">
#invite-modal___BV_modal_outer_ {
.modal-content {
padding: 0rem 0.25rem;
}
}
#invite-modal___BV_modal_header_.modal-header {
border-bottom: 0px;
}
#invite-modal___BV_modal_header_ {
.modal-title {
color: #4F2A93;
font-size: 24px;
}
}
</style>
<style lang="scss" scoped>
@import '~client/assets/scss/colors.scss';
a:not([href]) {
color: $blue-10;
font-size: 16px;
}
.form-control {
border: 0px;
color: $gray-50;
}
.input-group {
border-radius: 2px;
border: solid 1px $gray-400;
margin-top: 0.5rem;
}
::placeholder {
color: $gray-200;
opacity: 1;
}
.input-group:focus-within {
border-color: $purple-500;
}
.modal-footer {
border: 0px;
}
.positive-icon {
color: $green-10;
width: 10px;
margin: auto 0rem auto 1rem;
}
.small {
color: $gray-200;
font-size: 12px;
margin: 0.5rem 0rem 1rem;
}
</style>
<script> <script>
import { mapState } from 'client/libs/store'; import { mapState } from 'client/libs/store';
import clone from 'lodash/clone';
import debounce from 'lodash/debounce';
import filter from 'lodash/filter';
import forEach from 'lodash/forEach';
import isEmail from 'validator/lib/isEmail';
import isUUID from 'validator/lib/isUUID';
import notifications from 'client/mixins/notifications';
import positiveIcon from 'assets/svg/positive.svg';
import filter from 'lodash/filter'; const INVITE_DEFAULTS = {text: '', valid: null};
import map from 'lodash/map';
import notifications from 'client/mixins/notifications';
export default { export default {
mixins: [notifications], computed: {
props: ['group'], ...mapState({user: 'user.data'}),
data () { cannotSubmit () {
return { const filteredInvites = filter(this.invites, (invite) => {
invitees: [], return invite.text.length > 0 && !invite.valid;
emails: [], });
}; if (filteredInvites.length > 0) return true;
}, },
computed: { inviter () {
...mapState({user: 'user.data'}), return this.user.profile.name;
inviter () { },
return this.user.profile.name;
}, },
sendInviteText () { data () {
return 'Send Invites'; return {
// if (!this.group) return 'Send Invites'; invites: [clone(INVITE_DEFAULTS), clone(INVITE_DEFAULTS)],
// return this.group.sendInviteText; icons: Object.freeze({
positiveIcon,
}),
};
}, },
}, methods: {
methods: { checkInviteList: debounce(function checkList () {
addUuid () { this.invites = filter(this.invites, (invite) => {
this.invitees.push({uuid: ''}); return invite.text.length > 0;
});
if (this.invites[this.invites.length - 1].text.length > 0) this.invites.push(clone(INVITE_DEFAULTS));
forEach(this.invites, (value, index) => {
if (value.text.length < 1) return this.invites[index].valid = null;
if (isEmail(value.text)) return this.invites[index].valid = true;
if (isUUID(value.text)) {
this.$store.dispatch('user:userLookup', {uuid: value.text})
.then(res => {
if (res.status === 200) return this.invites[index].valid = true;
return this.invites[index].valid = false;
});
} else {
this.$store.dispatch('user:userLookup', {username: value.text})
.then(res => {
if (res.status === 200) return this.invites[index].valid = true;
return this.invites[index].valid = false;
});
}
});
}, 500),
close () {
this.invites = [clone(INVITE_DEFAULTS), clone(INVITE_DEFAULTS)];
this.$root.$emit('bv::hide::modal', 'invite-modal');
},
async sendInvites () {
let invitationDetails = {
inviter: this.inviter,
emails: [],
uuids: [],
usernames: [],
};
forEach(this.invites, (invite) => {
if (invite.text.length < 1) return;
if (isEmail(invite.text)) {
invitationDetails.emails.push({email: invite.text});
} else if (isUUID(invite.text)) {
invitationDetails.uuids.push(invite.text);
} else {
invitationDetails.usernames.push(invite.text);
}
});
await this.$store.dispatch('guilds:invite', {
invitationDetails,
groupId: this.group._id,
});
const invitesSent = invitationDetails.emails.length + invitationDetails.uuids.length + invitationDetails.usernames.length;
let invitationString = invitesSent > 1 ? 'invitationsSent' : 'invitationSent';
this.text(this.$t(invitationString));
this.close();
},
}, },
addEmail () { mixins: [notifications],
this.emails.push({name: '', email: ''}); props: ['group', 'groupType'],
}, };
inviteNewUsers (inviteMethod) {
if (!this.group._id) {
if (!this.group.name) this.group.name = this.$t('possessiveParty', {name: this.user.profile.name});
// @TODO: Add dispatch
// return Groups.Group.create(this.group)
// .then(function(response) {
// this.group = response.data.data;
// _inviteByMethod(inviteMethod);
// });
}
this.inviteByMethod(inviteMethod);
},
async inviteByMethod (inviteMethod) {
let invitationDetails;
if (inviteMethod === 'email') {
let emails = this.getEmails();
invitationDetails = { inviter: this.inviter, emails };
} else if (inviteMethod === 'uuid') {
let uuids = this.getOnlyUuids();
invitationDetails = { uuids };
} else {
return alert('Invalid invite method.');
}
await this.$store.dispatch('guilds:invite', {
invitationDetails,
groupId: this.group._id,
});
let invitesSent = invitationDetails.emails || invitationDetails.uuids;
let invitationString = invitesSent.length > 1 ? 'invitationsSent' : 'invitationSent';
this.text(this.$t(invitationString));
this.invitees = [];
this.emails = [];
// @TODO: This function didn't make it over this.resetInvitees();
// @TODO: Sync group invites?
// if (this.group.type === 'party') {
// this.$router.push('//party');
// } else {
// this.$router.push(`/groups/guilds/${this.group._id}`);
// }
this.$root.$emit('bv::hide::modal', 'invite-modal');
// @TODO: error?
// _resetInvitees();
},
getOnlyUuids () {
let uuids = map(this.invitees, 'uuid');
let filteredUuids = filter(uuids, (id) => {
return id !== '';
});
return filteredUuids;
},
getEmails () {
let emails = filter(this.emails, (obj) => {
return obj.email !== '';
});
return emails;
},
},
};
</script> </script>

View File

@@ -0,0 +1,152 @@
<template lang="pug">
b-modal#invite-modal(:title="$t('inviteFriends')", size='lg')
.modal-body
p.alert.alert-info(v-html="$t('inviteAlertInfo')")
.form-horizontal
table.table.table-striped
thead
tr
th {{ $t('userId') }}
tbody
tr(v-for='user in invitees')
td
input.form-control(type='text', v-model='user.uuid')
tr
td
button.btn.btn-primary.pull-right(@click='addUuid()')
i.glyphicon.glyphicon-plus
| +
tr
td
.col-6.col-offset-6
button.btn.btn-primary.btn-block(@click='inviteNewUsers("uuid")') {{sendInviteText}}
hr
p.alert.alert-info {{ $t('inviteByEmail') }}
.form-horizontal
table.table.table-striped
thead
tr
th {{ $t('name') }}
th {{ $t('email') }}
tbody
tr(v-for='email in emails')
td
input.form-control(type='text', v-model='email.name')
td
input.form-control(type='email', v-model='email.email')
tr
td(colspan=2)
button.btn.btn-primary.pull-right(@click='addEmail()')
i.glyphicon.glyphicon-plus
| +
tr
td.form-group(colspan=2)
label.col-sm-1.control-label {{ $t('byColon') }}
.col-sm-5
input.form-control(type='text', v-model='inviter')
.col-sm-6
button.btn.btn-primary.btn-block(@click='inviteNewUsers("email")') {{sendInviteText}}
</template>
<script>
import { mapState } from 'client/libs/store';
import filter from 'lodash/filter';
import map from 'lodash/map';
import notifications from 'client/mixins/notifications';
export default {
mixins: [notifications],
props: ['group'],
data () {
return {
invitees: [],
emails: [],
};
},
computed: {
...mapState({user: 'user.data'}),
inviter () {
return this.user.profile.name;
},
sendInviteText () {
return 'Send Invites';
// if (!this.group) return 'Send Invites';
// return this.group.sendInviteText;
},
},
methods: {
addUuid () {
this.invitees.push({uuid: ''});
},
addEmail () {
this.emails.push({name: '', email: ''});
},
inviteNewUsers (inviteMethod) {
if (!this.group._id) {
if (!this.group.name) this.group.name = this.$t('possessiveParty', {name: this.user.profile.name});
// @TODO: Add dispatch
// return Groups.Group.create(this.group)
// .then(function(response) {
// this.group = response.data.data;
// _inviteByMethod(inviteMethod);
// });
}
this.inviteByMethod(inviteMethod);
},
async inviteByMethod (inviteMethod) {
let invitationDetails;
if (inviteMethod === 'email') {
let emails = this.getEmails();
invitationDetails = { inviter: this.inviter, emails };
} else if (inviteMethod === 'uuid') {
let uuids = this.getOnlyUuids();
invitationDetails = { uuids };
} else {
return alert('Invalid invite method.');
}
await this.$store.dispatch('guilds:invite', {
invitationDetails,
groupId: this.group._id,
});
let invitesSent = invitationDetails.emails || invitationDetails.uuids;
let invitationString = invitesSent.length > 1 ? 'invitationsSent' : 'invitationSent';
this.text(this.$t(invitationString));
this.invitees = [];
this.emails = [];
// @TODO: This function didn't make it over this.resetInvitees();
// @TODO: Sync group invites?
// if (this.group.type === 'party') {
// this.$router.push('//party');
// } else {
// this.$router.push(`/groups/guilds/${this.group._id}`);
// }
this.$root.$emit('bv::hide::modal', 'invite-modal');
// @TODO: error?
// _resetInvitees();
},
getOnlyUuids () {
let uuids = map(this.invitees, 'uuid');
let filteredUuids = filter(uuids, (id) => {
return id !== '';
});
return filteredUuids;
},
getEmails () {
let emails = filter(this.emails, (obj) => {
return obj.email !== '';
});
return emails;
},
},
};
</script>

View File

@@ -1,6 +1,6 @@
<template lang="pug"> <template lang="pug">
div div
invite-modal(:group='inviteModalGroup') invite-modal(:group='inviteModalGroup', :groupType='inviteModalGroupType')
create-party-modal create-party-modal
#app-header.row(:class="{'hide-header': $route.name === 'groupPlan'}") #app-header.row(:class="{'hide-header': $route.name === 'groupPlan'}")
members-modal(:hide-badge="true") members-modal(:hide-badge="true")
@@ -115,6 +115,7 @@ export default {
expandedMember: null, expandedMember: null,
currentWidth: 0, currentWidth: 0,
inviteModalGroup: undefined, inviteModalGroup: undefined,
inviteModalGroupType: undefined,
}; };
}, },
computed: { computed: {
@@ -178,6 +179,7 @@ export default {
mounted () { mounted () {
this.$root.$on('inviteModal::inviteToGroup', (group) => { this.$root.$on('inviteModal::inviteToGroup', (group) => {
this.inviteModalGroup = group; this.inviteModalGroup = group;
this.inviteModalGroupType = group.type === 'guild' ? 'Guild' : 'Party';
this.$root.$emit('bv::show::modal', 'invite-modal'); this.$root.$emit('bv::show::modal', 'invite-modal');
}); });
}, },

View File

@@ -24,9 +24,6 @@
padding-left: 2rem; padding-left: 2rem;
padding-right: 2rem; padding-right: 2rem;
} }
.modal-dialog {
transform: translate(0, 50vh) translate(-5%, -48%);
}
} }
</style> </style>

View File

@@ -151,3 +151,14 @@ export async function togglePrivateMessagesOpt (store) {
store.state.user.data.inbox.optOut = !store.state.user.data.inbox.optOut; store.state.user.data.inbox.optOut = !store.state.user.data.inbox.optOut;
return response; return response;
} }
export async function userLookup (store, params) {
let response;
if (params.uuid) {
response = await axios.get(`/api/v4/members/${params.uuid}`);
}
if (params.username) {
response = await axios.get(`/api/v4/members/username/${params.username}`);
}
return response;
}

View File

@@ -185,7 +185,7 @@
"inviteExistUser": "Invite Existing Users", "inviteExistUser": "Invite Existing Users",
"byColon": "By:", "byColon": "By:",
"inviteNewUsers": "Invite New Users", "inviteNewUsers": "Invite New Users",
"sendInvitations": "Send Invitations", "sendInvitations": "Send Invites",
"invitationsSent": "Invitations sent!", "invitationsSent": "Invitations sent!",
"invitationSent": "Invitation sent!", "invitationSent": "Invitation sent!",
"invitedFriend": "Invited a Friend", "invitedFriend": "Invited a Friend",
@@ -367,6 +367,10 @@
"liked": "Liked", "liked": "Liked",
"joinGuild": "Join Guild", "joinGuild": "Join Guild",
"inviteToGuild": "Invite to Guild", "inviteToGuild": "Invite to Guild",
"inviteToParty": "Invite to Party",
"inviteEmailUsername": "Invite via Email or Username",
"inviteEmailUsernameInfo": "Invite users via a valid email or username. If an email isn't registered yet, we'll invite them to join.",
"emailOrUsernameInvite": "Email address or username",
"messageGuildLeader": "Message Guild Leader", "messageGuildLeader": "Message Guild Leader",
"donateGems": "Donate Gems", "donateGems": "Donate Gems",
"updateGuild": "Update Guild", "updateGuild": "Update Guild",

View File

@@ -17,9 +17,9 @@ import {
import { removeFromArray } from '../../libs/collectionManipulators'; import { removeFromArray } from '../../libs/collectionManipulators';
import { sendTxn as sendTxnEmail } from '../../libs/email'; import { sendTxn as sendTxnEmail } from '../../libs/email';
import { import {
_inviteByUUID, inviteByUUID,
_inviteByEmail, inviteByEmail,
_inviteByUserName, inviteByUserName,
} from '../../libs/invites'; } from '../../libs/invites';
import common from '../../../common'; import common from '../../../common';
import payments from '../../libs/payments/payments'; import payments from '../../libs/payments/payments';
@@ -1038,13 +1038,13 @@ api.inviteToGroup = {
const results = []; const results = [];
if (uuids) { if (uuids) {
const uuidInvites = uuids.map((uuid) => _inviteByUUID(uuid, group, user, req, res)); const uuidInvites = uuids.map((uuid) => inviteByUUID(uuid, group, user, req, res));
const uuidResults = await Promise.all(uuidInvites); const uuidResults = await Promise.all(uuidInvites);
results.push(...uuidResults); results.push(...uuidResults);
} }
if (emails) { if (emails) {
const emailInvites = emails.map((invite) => _inviteByEmail(invite, group, user, req, res)); const emailInvites = emails.map((invite) => inviteByEmail(invite, group, user, req, res));
user.invitesSent += emails.length; user.invitesSent += emails.length;
await user.save(); await user.save();
const emailResults = await Promise.all(emailInvites); const emailResults = await Promise.all(emailInvites);
@@ -1052,7 +1052,7 @@ api.inviteToGroup = {
} }
if (usernames) { if (usernames) {
const usernameInvites = usernames.map((username) => _inviteByUserName(username, group, user, req, res)); const usernameInvites = usernames.map((username) => inviteByUserName(username, group, user, req, res));
const usernameResults = await Promise.all(usernameInvites); const usernameResults = await Promise.all(usernameInvites);
results.push(...usernameResults); results.push(...usernameResults);
} }

View File

@@ -119,6 +119,33 @@ api.getMember = {
}, },
}; };
api.getMemberByUsername = {
method: 'GET',
url: '/members/username/:username',
middlewares: [],
async handler (req, res) {
req.checkParams('username', res.t('invalidReqParams')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let username = req.params.username.toLowerCase();
let member = await User
.findOne({'auth.local.lowerCaseUsername': username})
.select(memberFields)
.exec();
if (!member || !member.flags.verifiedUsername) throw new NotFound(res.t('userNotFound'));
// manually call toJSON with minimize: true so empty paths aren't returned
let memberToJSON = member.toJSON({minimize: true});
User.addComputedStatsToJSONObj(memberToJSON.stats, member);
res.respond(200, memberToJSON);
},
};
/** /**
* @api {get} /api/v3/members/:memberId/achievements Get member achievements object * @api {get} /api/v3/members/:memberId/achievements Get member achievements object
* @apiName GetMemberAchievements * @apiName GetMemberAchievements

View File

@@ -100,7 +100,7 @@ async function inviteUserToParty (userToInvite, group, inviter, res) {
userToInvite.invitations.party = partyInvite; userToInvite.invitations.party = partyInvite;
} }
async function addInvitiationToUser (userToInvite, group, inviter, res) { async function addInvitationToUser (userToInvite, group, inviter, res) {
const publicGuild = group.type === 'guild' && group.privacy === 'public'; const publicGuild = group.type === 'guild' && group.privacy === 'public';
if (group.type === 'guild') { if (group.type === 'guild') {
@@ -123,7 +123,7 @@ async function addInvitiationToUser (userToInvite, group, inviter, res) {
} }
} }
async function _inviteByUUID (uuid, group, inviter, req, res) { async function inviteByUUID (uuid, group, inviter, req, res) {
const userToInvite = await User.findById(uuid).exec(); const userToInvite = await User.findById(uuid).exec();
if (!userToInvite) { if (!userToInvite) {
@@ -137,10 +137,10 @@ async function _inviteByUUID (uuid, group, inviter, req, res) {
throw new NotAuthorized(res.t(objections[0], { userId: uuid, username: userToInvite.profile.name})); throw new NotAuthorized(res.t(objections[0], { userId: uuid, username: userToInvite.profile.name}));
} }
return await addInvitiationToUser(userToInvite, group, inviter, res); return await addInvitationToUser(userToInvite, group, inviter, res);
} }
async function _inviteByEmail (invite, group, inviter, req, res) { async function inviteByEmail (invite, group, inviter, req, res) {
let userReturnInfo; let userReturnInfo;
if (!invite.email) throw new BadRequest(res.t('inviteMissingEmail')); if (!invite.email) throw new BadRequest(res.t('inviteMissingEmail'));
@@ -154,7 +154,7 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
.exec(); .exec();
if (userToContact) { if (userToContact) {
userReturnInfo = await _inviteByUUID(userToContact._id, group, inviter, req, res); userReturnInfo = await inviteByUUID(userToContact._id, group, inviter, req, res);
} else { } else {
userReturnInfo = invite.email; userReturnInfo = invite.email;
@@ -188,7 +188,9 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
return userReturnInfo; return userReturnInfo;
} }
async function _inviteByUserName (username, group, inviter, req, res) { async function inviteByUserName (username, group, inviter, req, res) {
if (username.indexOf('@') === 0) username = username.slice(1, username.length);
username = username.toLowerCase();
const userToInvite = await User.findOne({'auth.local.lowerCaseUsername': username}).exec(); const userToInvite = await User.findOne({'auth.local.lowerCaseUsername': username}).exec();
if (!userToInvite) { if (!userToInvite) {
@@ -199,11 +201,11 @@ async function _inviteByUserName (username, group, inviter, req, res) {
throw new BadRequest(res.t('cannotInviteSelfToGroup')); throw new BadRequest(res.t('cannotInviteSelfToGroup'));
} }
return await addInvitiationToUser(userToInvite, group, inviter, res); return await addInvitationToUser(userToInvite, group, inviter, res);
} }
module.exports = { module.exports = {
_inviteByUUID, inviteByUUID,
_inviteByEmail, inviteByEmail,
_inviteByUserName, inviteByUserName,
}; };