mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Merge branch 'sabrecat/usernames-master' into develop
This commit is contained in:
@@ -569,7 +569,7 @@ describe('Group Model', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if no uuids or emails are passed in', async () => {
|
it('throws an error if no uuids or emails are passed in', async () => {
|
||||||
await expect(Group.validateInvitations(null, null, res)).to.eventually.be.rejected.and.eql({
|
await expect(Group.validateInvitations({}, res)).to.eventually.be.rejected.and.eql({
|
||||||
httpCode: 400,
|
httpCode: 400,
|
||||||
message: 'Bad request.',
|
message: 'Bad request.',
|
||||||
name: 'BadRequest',
|
name: 'BadRequest',
|
||||||
@@ -579,7 +579,7 @@ describe('Group Model', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if only uuids are passed in, but they are not an array', async () => {
|
it('throws an error if only uuids are passed in, but they are not an array', async () => {
|
||||||
await expect(Group.validateInvitations({ uuid: 'user-id'}, null, res)).to.eventually.be.rejected.and.eql({
|
await expect(Group.validateInvitations({ uuids: 'user-id'}, res)).to.eventually.be.rejected.and.eql({
|
||||||
httpCode: 400,
|
httpCode: 400,
|
||||||
message: 'Bad request.',
|
message: 'Bad request.',
|
||||||
name: 'BadRequest',
|
name: 'BadRequest',
|
||||||
@@ -589,7 +589,7 @@ describe('Group Model', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if only emails are passed in, but they are not an array', async () => {
|
it('throws an error if only emails are passed in, but they are not an array', async () => {
|
||||||
await expect(Group.validateInvitations(null, { emails: 'user@example.com'}, res)).to.eventually.be.rejected.and.eql({
|
await expect(Group.validateInvitations({emails: 'user@example.com'}, res)).to.eventually.be.rejected.and.eql({
|
||||||
httpCode: 400,
|
httpCode: 400,
|
||||||
message: 'Bad request.',
|
message: 'Bad request.',
|
||||||
name: 'BadRequest',
|
name: 'BadRequest',
|
||||||
@@ -599,27 +599,27 @@ describe('Group Model', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if emails are not passed in, and uuid array is empty', async () => {
|
it('throws an error if emails are not passed in, and uuid array is empty', async () => {
|
||||||
await expect(Group.validateInvitations([], null, res)).to.eventually.be.rejected.and.eql({
|
await expect(Group.validateInvitations({uuids: []}, res)).to.eventually.be.rejected.and.eql({
|
||||||
httpCode: 400,
|
httpCode: 400,
|
||||||
message: 'Bad request.',
|
message: 'Bad request.',
|
||||||
name: 'BadRequest',
|
name: 'BadRequest',
|
||||||
});
|
});
|
||||||
expect(res.t).to.be.calledOnce;
|
expect(res.t).to.be.calledOnce;
|
||||||
expect(res.t).to.be.calledWith('inviteMissingUuid');
|
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if uuids are not passed in, and email array is empty', async () => {
|
it('throws an error if uuids are not passed in, and email array is empty', async () => {
|
||||||
await expect(Group.validateInvitations(null, [], res)).to.eventually.be.rejected.and.eql({
|
await expect(Group.validateInvitations({emails: []}, res)).to.eventually.be.rejected.and.eql({
|
||||||
httpCode: 400,
|
httpCode: 400,
|
||||||
message: 'Bad request.',
|
message: 'Bad request.',
|
||||||
name: 'BadRequest',
|
name: 'BadRequest',
|
||||||
});
|
});
|
||||||
expect(res.t).to.be.calledOnce;
|
expect(res.t).to.be.calledOnce;
|
||||||
expect(res.t).to.be.calledWith('inviteMissingEmail');
|
expect(res.t).to.be.calledWith('inviteMustNotBeEmpty');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('throws an error if uuids and emails are passed in as empty arrays', async () => {
|
it('throws an error if uuids and emails are passed in as empty arrays', async () => {
|
||||||
await expect(Group.validateInvitations([], [], res)).to.eventually.be.rejected.and.eql({
|
await expect(Group.validateInvitations({emails: [], uuids: []}, res)).to.eventually.be.rejected.and.eql({
|
||||||
httpCode: 400,
|
httpCode: 400,
|
||||||
message: 'Bad request.',
|
message: 'Bad request.',
|
||||||
name: 'BadRequest',
|
name: 'BadRequest',
|
||||||
@@ -639,7 +639,7 @@ describe('Group Model', () => {
|
|||||||
|
|
||||||
uuids.push('one-more-uuid'); // to put it over the limit
|
uuids.push('one-more-uuid'); // to put it over the limit
|
||||||
|
|
||||||
await expect(Group.validateInvitations(uuids, emails, res)).to.eventually.be.rejected.and.eql({
|
await expect(Group.validateInvitations({uuids, emails}, res)).to.eventually.be.rejected.and.eql({
|
||||||
httpCode: 400,
|
httpCode: 400,
|
||||||
message: 'Bad request.',
|
message: 'Bad request.',
|
||||||
name: 'BadRequest',
|
name: 'BadRequest',
|
||||||
@@ -657,33 +657,33 @@ describe('Group Model', () => {
|
|||||||
emails.push(`user-${i}@example.com`);
|
emails.push(`user-${i}@example.com`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Group.validateInvitations(uuids, emails, res);
|
await Group.validateInvitations({uuids, emails}, res);
|
||||||
expect(res.t).to.not.be.called;
|
expect(res.t).to.not.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('does not throw an error if only user ids are passed in', async () => {
|
it('does not throw an error if only user ids are passed in', async () => {
|
||||||
await Group.validateInvitations(['user-id', 'user-id2'], null, res);
|
await Group.validateInvitations({uuids: ['user-id', 'user-id2']}, res);
|
||||||
expect(res.t).to.not.be.called;
|
expect(res.t).to.not.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw an error if only emails are passed in', async () => {
|
it('does not throw an error if only emails are passed in', async () => {
|
||||||
await Group.validateInvitations(null, ['user1@example.com', 'user2@example.com'], res);
|
await Group.validateInvitations({emails: ['user1@example.com', 'user2@example.com']}, res);
|
||||||
expect(res.t).to.not.be.called;
|
expect(res.t).to.not.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw an error if both uuids and emails are passed in', async () => {
|
it('does not throw an error if both uuids and emails are passed in', async () => {
|
||||||
await Group.validateInvitations(['user-id', 'user-id2'], ['user1@example.com', 'user2@example.com'], res);
|
await Group.validateInvitations({uuids: ['user-id', 'user-id2'], emails: ['user1@example.com', 'user2@example.com']}, res);
|
||||||
expect(res.t).to.not.be.called;
|
expect(res.t).to.not.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw an error if uuids are passed in and emails are an empty array', async () => {
|
it('does not throw an error if uuids are passed in and emails are an empty array', async () => {
|
||||||
await Group.validateInvitations(['user-id', 'user-id2'], [], res);
|
await Group.validateInvitations({uuids: ['user-id', 'user-id2'], emails: []}, res);
|
||||||
expect(res.t).to.not.be.called;
|
expect(res.t).to.not.be.called;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not throw an error if emails are passed in and uuids are an empty array', async () => {
|
it('does not throw an error if emails are passed in and uuids are an empty array', async () => {
|
||||||
await Group.validateInvitations([], ['user1@example.com', 'user2@example.com'], res);
|
await Group.validateInvitations({uuids: [], emails: ['user1@example.com', 'user2@example.com']}, res);
|
||||||
expect(res.t).to.not.be.called;
|
expect(res.t).to.not.be.called;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,6 +47,14 @@ describe('GET /challenges/:challengeId', () => {
|
|||||||
_id: groupLeader._id,
|
_id: groupLeader._id,
|
||||||
id: groupLeader._id,
|
id: groupLeader._id,
|
||||||
profile: {name: groupLeader.profile.name},
|
profile: {name: groupLeader.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: groupLeader.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(chal.group).to.eql({
|
expect(chal.group).to.eql({
|
||||||
_id: group._id,
|
_id: group._id,
|
||||||
@@ -105,6 +113,14 @@ describe('GET /challenges/:challengeId', () => {
|
|||||||
_id: challengeLeader._id,
|
_id: challengeLeader._id,
|
||||||
id: challengeLeader._id,
|
id: challengeLeader._id,
|
||||||
profile: {name: challengeLeader.profile.name},
|
profile: {name: challengeLeader.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: challengeLeader.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(chal.group).to.eql({
|
expect(chal.group).to.eql({
|
||||||
_id: group._id,
|
_id: group._id,
|
||||||
@@ -131,6 +147,14 @@ describe('GET /challenges/:challengeId', () => {
|
|||||||
_id: challengeLeader._id,
|
_id: challengeLeader._id,
|
||||||
id: challengeLeader._id,
|
id: challengeLeader._id,
|
||||||
profile: {name: challengeLeader.profile.name},
|
profile: {name: challengeLeader.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: challengeLeader.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -179,6 +203,14 @@ describe('GET /challenges/:challengeId', () => {
|
|||||||
_id: challengeLeader._id,
|
_id: challengeLeader._id,
|
||||||
id: challengeLeader._id,
|
id: challengeLeader._id,
|
||||||
profile: {name: challengeLeader.profile.name},
|
profile: {name: challengeLeader.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: challengeLeader.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(chal.group).to.eql({
|
expect(chal.group).to.eql({
|
||||||
_id: group._id,
|
_id: group._id,
|
||||||
@@ -205,6 +237,14 @@ describe('GET /challenges/:challengeId', () => {
|
|||||||
_id: challengeLeader._id,
|
_id: challengeLeader._id,
|
||||||
id: challengeLeader._id,
|
id: challengeLeader._id,
|
||||||
profile: {name: challengeLeader.profile.name},
|
profile: {name: challengeLeader.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: challengeLeader.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -60,6 +60,14 @@ describe('GET /challenges/:challengeId/members', () => {
|
|||||||
_id: groupLeader._id,
|
_id: groupLeader._id,
|
||||||
id: groupLeader._id,
|
id: groupLeader._id,
|
||||||
profile: {name: groupLeader.profile.name},
|
profile: {name: groupLeader.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: groupLeader.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,8 +81,16 @@ describe('GET /challenges/:challengeId/members', () => {
|
|||||||
_id: leader._id,
|
_id: leader._id,
|
||||||
id: leader._id,
|
id: leader._id,
|
||||||
profile: {name: leader.profile.name},
|
profile: {name: leader.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: leader.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
|
expect(res[0]).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||||
expect(res[0].profile).to.have.all.keys(['name']);
|
expect(res[0].profile).to.have.all.keys(['name']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -88,8 +104,16 @@ describe('GET /challenges/:challengeId/members', () => {
|
|||||||
_id: anotherUser._id,
|
_id: anotherUser._id,
|
||||||
id: anotherUser._id,
|
id: anotherUser._id,
|
||||||
profile: {name: anotherUser.profile.name},
|
profile: {name: anotherUser.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: anotherUser.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
|
expect(res[0]).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||||
expect(res[0].profile).to.have.all.keys(['name']);
|
expect(res[0].profile).to.have.all.keys(['name']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,7 +131,7 @@ describe('GET /challenges/:challengeId/members', () => {
|
|||||||
let res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=not-true`);
|
let res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=not-true`);
|
||||||
expect(res.length).to.equal(30);
|
expect(res.length).to.equal(30);
|
||||||
res.forEach(member => {
|
res.forEach(member => {
|
||||||
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
|
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||||
expect(member.profile).to.have.all.keys(['name']);
|
expect(member.profile).to.have.all.keys(['name']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -126,7 +150,7 @@ describe('GET /challenges/:challengeId/members', () => {
|
|||||||
let res = await user.get(`/challenges/${challenge._id}/members`);
|
let res = await user.get(`/challenges/${challenge._id}/members`);
|
||||||
expect(res.length).to.equal(30);
|
expect(res.length).to.equal(30);
|
||||||
res.forEach(member => {
|
res.forEach(member => {
|
||||||
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
|
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||||
expect(member.profile).to.have.all.keys(['name']);
|
expect(member.profile).to.have.all.keys(['name']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -145,7 +169,7 @@ describe('GET /challenges/:challengeId/members', () => {
|
|||||||
let res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=true`);
|
let res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=true`);
|
||||||
expect(res.length).to.equal(32);
|
expect(res.length).to.equal(32);
|
||||||
res.forEach(member => {
|
res.forEach(member => {
|
||||||
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
|
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||||
expect(member.profile).to.have.all.keys(['name']);
|
expect(member.profile).to.have.all.keys(['name']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
|
|||||||
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
|
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
|
||||||
|
|
||||||
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${groupLeader._id}`);
|
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${groupLeader._id}`);
|
||||||
expect(memberProgress).to.have.all.keys(['_id', 'id', 'profile', 'tasks']);
|
expect(memberProgress).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile', 'tasks']);
|
||||||
expect(memberProgress.profile).to.have.all.keys(['name']);
|
expect(memberProgress.profile).to.have.all.keys(['name']);
|
||||||
expect(memberProgress.tasks.length).to.equal(1);
|
expect(memberProgress.tasks.length).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,6 +39,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: publicGuild.leader._id,
|
_id: publicGuild.leader._id,
|
||||||
id: publicGuild.leader._id,
|
id: publicGuild.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||||
expect(foundChallenge2).to.exist;
|
expect(foundChallenge2).to.exist;
|
||||||
@@ -46,6 +54,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: publicGuild.leader._id,
|
_id: publicGuild.leader._id,
|
||||||
id: publicGuild.leader._id,
|
id: publicGuild.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,6 +74,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: publicGuild.leader._id,
|
_id: publicGuild.leader._id,
|
||||||
id: publicGuild.leader._id,
|
id: publicGuild.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||||
expect(foundChallenge2).to.exist;
|
expect(foundChallenge2).to.exist;
|
||||||
@@ -65,6 +89,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: publicGuild.leader._id,
|
_id: publicGuild.leader._id,
|
||||||
id: publicGuild.leader._id,
|
id: publicGuild.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,6 +157,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: privateGuild.leader._id,
|
_id: privateGuild.leader._id,
|
||||||
id: privateGuild.leader._id,
|
id: privateGuild.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||||
expect(foundChallenge2).to.exist;
|
expect(foundChallenge2).to.exist;
|
||||||
@@ -132,6 +172,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: privateGuild.leader._id,
|
_id: privateGuild.leader._id,
|
||||||
id: privateGuild.leader._id,
|
id: privateGuild.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -235,6 +283,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: party.leader._id,
|
_id: party.leader._id,
|
||||||
id: party.leader._id,
|
id: party.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||||
expect(foundChallenge2).to.exist;
|
expect(foundChallenge2).to.exist;
|
||||||
@@ -242,6 +298,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: party.leader._id,
|
_id: party.leader._id,
|
||||||
id: party.leader._id,
|
id: party.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -254,6 +318,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: party.leader._id,
|
_id: party.leader._id,
|
||||||
id: party.leader._id,
|
id: party.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||||
expect(foundChallenge2).to.exist;
|
expect(foundChallenge2).to.exist;
|
||||||
@@ -261,6 +333,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: party.leader._id,
|
_id: party.leader._id,
|
||||||
id: party.leader._id,
|
id: party.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -288,6 +368,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: user._id,
|
_id: user._id,
|
||||||
id: user._id,
|
id: user._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||||
expect(foundChallenge2).to.exist;
|
expect(foundChallenge2).to.exist;
|
||||||
@@ -295,6 +383,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: user._id,
|
_id: user._id,
|
||||||
id: user._id,
|
id: user._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -307,6 +403,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: user._id,
|
_id: user._id,
|
||||||
id: user._id,
|
id: user._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||||
expect(foundChallenge2).to.exist;
|
expect(foundChallenge2).to.exist;
|
||||||
@@ -314,6 +418,14 @@ describe('GET challenges/groups/:groupId', () => {
|
|||||||
_id: user._id,
|
_id: user._id,
|
||||||
id: user._id,
|
id: user._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -40,6 +40,14 @@ describe('GET challenges/user', () => {
|
|||||||
_id: publicGuild.leader._id,
|
_id: publicGuild.leader._id,
|
||||||
id: publicGuild.leader._id,
|
id: publicGuild.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(foundChallenge.group).to.eql({
|
expect(foundChallenge.group).to.eql({
|
||||||
_id: publicGuild._id,
|
_id: publicGuild._id,
|
||||||
@@ -62,6 +70,14 @@ describe('GET challenges/user', () => {
|
|||||||
_id: publicGuild.leader._id,
|
_id: publicGuild.leader._id,
|
||||||
id: publicGuild.leader._id,
|
id: publicGuild.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(foundChallenge1.group).to.eql({
|
expect(foundChallenge1.group).to.eql({
|
||||||
_id: publicGuild._id,
|
_id: publicGuild._id,
|
||||||
@@ -79,6 +95,14 @@ describe('GET challenges/user', () => {
|
|||||||
_id: publicGuild.leader._id,
|
_id: publicGuild.leader._id,
|
||||||
id: publicGuild.leader._id,
|
id: publicGuild.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(foundChallenge2.group).to.eql({
|
expect(foundChallenge2.group).to.eql({
|
||||||
_id: publicGuild._id,
|
_id: publicGuild._id,
|
||||||
@@ -101,6 +125,14 @@ describe('GET challenges/user', () => {
|
|||||||
_id: publicGuild.leader._id,
|
_id: publicGuild.leader._id,
|
||||||
id: publicGuild.leader._id,
|
id: publicGuild.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(foundChallenge1.group).to.eql({
|
expect(foundChallenge1.group).to.eql({
|
||||||
_id: publicGuild._id,
|
_id: publicGuild._id,
|
||||||
@@ -118,6 +150,14 @@ describe('GET challenges/user', () => {
|
|||||||
_id: publicGuild.leader._id,
|
_id: publicGuild.leader._id,
|
||||||
id: publicGuild.leader._id,
|
id: publicGuild.leader._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(foundChallenge2.group).to.eql({
|
expect(foundChallenge2.group).to.eql({
|
||||||
_id: publicGuild._id,
|
_id: publicGuild._id,
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ describe('POST /challenges/:challengeId/join', () => {
|
|||||||
_id: groupLeader._id,
|
_id: groupLeader._id,
|
||||||
id: groupLeader._id,
|
id: groupLeader._id,
|
||||||
profile: {name: groupLeader.profile.name},
|
profile: {name: groupLeader.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: groupLeader.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(res.name).to.equal(challenge.name);
|
expect(res.name).to.equal(challenge.name);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -79,6 +79,14 @@ describe('PUT /challenges/:challengeId', () => {
|
|||||||
_id: member._id,
|
_id: member._id,
|
||||||
id: member._id,
|
id: member._id,
|
||||||
profile: {name: member.profile.name},
|
profile: {name: member.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: member.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(res.name).to.equal('New Challenge Name');
|
expect(res.name).to.equal('New Challenge Name');
|
||||||
expect(res.description).to.equal('New challenge description.');
|
expect(res.description).to.equal('New challenge description.');
|
||||||
|
|||||||
@@ -50,6 +50,14 @@ describe('GET /groups/:groupId/invites', () => {
|
|||||||
_id: invited._id,
|
_id: invited._id,
|
||||||
id: invited._id,
|
id: invited._id,
|
||||||
profile: {name: invited.profile.name},
|
profile: {name: invited.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: invited.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,7 +66,7 @@ describe('GET /groups/:groupId/invites', () => {
|
|||||||
let invited = await generateUser();
|
let invited = await generateUser();
|
||||||
await user.post(`/groups/${group._id}/invite`, {uuids: [invited._id]});
|
await user.post(`/groups/${group._id}/invite`, {uuids: [invited._id]});
|
||||||
let res = await user.get('/groups/party/invites');
|
let res = await user.get('/groups/party/invites');
|
||||||
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
|
expect(res[0]).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||||
expect(res[0].profile).to.have.all.keys(['name']);
|
expect(res[0].profile).to.have.all.keys(['name']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,7 +84,7 @@ describe('GET /groups/:groupId/invites', () => {
|
|||||||
let res = await leader.get(`/groups/${group._id}/invites`);
|
let res = await leader.get(`/groups/${group._id}/invites`);
|
||||||
expect(res.length).to.equal(30);
|
expect(res.length).to.equal(30);
|
||||||
res.forEach(member => {
|
res.forEach(member => {
|
||||||
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
|
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||||
expect(member.profile).to.have.all.keys(['name']);
|
expect(member.profile).to.have.all.keys(['name']);
|
||||||
});
|
});
|
||||||
}).timeout(10000);
|
}).timeout(10000);
|
||||||
|
|||||||
@@ -56,13 +56,21 @@ describe('GET /groups/:groupId/members', () => {
|
|||||||
_id: user._id,
|
_id: user._id,
|
||||||
id: user._id,
|
id: user._id,
|
||||||
profile: {name: user.profile.name},
|
profile: {name: user.profile.name},
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: user.auth.local.username,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('populates only some fields', async () => {
|
it('populates only some fields', async () => {
|
||||||
await generateGroup(user, {type: 'party', name: generateUUID()});
|
await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||||
let res = await user.get('/groups/party/members');
|
let res = await user.get('/groups/party/members');
|
||||||
expect(res[0]).to.have.all.keys(['_id', 'id', 'profile']);
|
expect(res[0]).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||||
expect(res[0].profile).to.have.all.keys(['name']);
|
expect(res[0].profile).to.have.all.keys(['name']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -74,7 +82,7 @@ describe('GET /groups/:groupId/members', () => {
|
|||||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||||
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
|
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
|
||||||
]);
|
]);
|
||||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
expect(Object.keys(memberRes.auth)).to.eql(['local', 'timestamps']);
|
||||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||||
'size', 'hair', 'skin', 'shirt',
|
'size', 'hair', 'skin', 'shirt',
|
||||||
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||||
@@ -95,7 +103,7 @@ describe('GET /groups/:groupId/members', () => {
|
|||||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||||
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
|
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
|
||||||
]);
|
]);
|
||||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
expect(Object.keys(memberRes.auth)).to.eql(['local', 'timestamps']);
|
||||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||||
'size', 'hair', 'skin', 'shirt',
|
'size', 'hair', 'skin', 'shirt',
|
||||||
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||||
@@ -120,7 +128,7 @@ describe('GET /groups/:groupId/members', () => {
|
|||||||
let res = await user.get('/groups/party/members');
|
let res = await user.get('/groups/party/members');
|
||||||
expect(res.length).to.equal(30);
|
expect(res.length).to.equal(30);
|
||||||
res.forEach(member => {
|
res.forEach(member => {
|
||||||
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
|
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||||
expect(member.profile).to.have.all.keys(['name']);
|
expect(member.profile).to.have.all.keys(['name']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -137,7 +145,7 @@ describe('GET /groups/:groupId/members', () => {
|
|||||||
let res = await user.get('/groups/party/members?includeAllMembers=true');
|
let res = await user.get('/groups/party/members?includeAllMembers=true');
|
||||||
expect(res.length).to.equal(30);
|
expect(res.length).to.equal(30);
|
||||||
res.forEach(member => {
|
res.forEach(member => {
|
||||||
expect(member).to.have.all.keys(['_id', 'id', 'profile']);
|
expect(member).to.have.all.keys(['_id', 'auth', 'flags', 'id', 'profile']);
|
||||||
expect(member.profile).to.have.all.keys(['name']);
|
expect(member.profile).to.have.all.keys(['name']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,73 @@ describe('Post /groups/:groupId/invite', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('username invites', () => {
|
||||||
|
it('returns an error when invited user is not found', async () => {
|
||||||
|
const fakeID = 'fakeuserid';
|
||||||
|
|
||||||
|
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||||
|
usernames: [fakeID],
|
||||||
|
}))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('userWithUsernameNotFound', {username: fakeID}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error when inviting yourself to a group', async () => {
|
||||||
|
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||||
|
usernames: [inviter.auth.local.lowerCaseUsername],
|
||||||
|
}))
|
||||||
|
.to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('cannotInviteSelfToGroup'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invites a user to a group by username', async () => {
|
||||||
|
const userToInvite = await generateUser();
|
||||||
|
|
||||||
|
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||||
|
usernames: [userToInvite.auth.local.lowerCaseUsername],
|
||||||
|
})).to.eventually.deep.equal([{
|
||||||
|
id: group._id,
|
||||||
|
name: groupName,
|
||||||
|
inviter: inviter._id,
|
||||||
|
publicGuild: false,
|
||||||
|
}]);
|
||||||
|
|
||||||
|
await expect(userToInvite.get('/user'))
|
||||||
|
.to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('invites multiple users to a group by uuid', async () => {
|
||||||
|
const userToInvite = await generateUser();
|
||||||
|
const userToInvite2 = await generateUser();
|
||||||
|
|
||||||
|
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||||
|
usernames: [userToInvite.auth.local.lowerCaseUsername, userToInvite2.auth.local.lowerCaseUsername],
|
||||||
|
})).to.eventually.deep.equal([
|
||||||
|
{
|
||||||
|
id: group._id,
|
||||||
|
name: groupName,
|
||||||
|
inviter: inviter._id,
|
||||||
|
publicGuild: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: group._id,
|
||||||
|
name: groupName,
|
||||||
|
inviter: inviter._id,
|
||||||
|
publicGuild: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(userToInvite.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
|
||||||
|
await expect(userToInvite2.get('/user')).to.eventually.have.nested.property('invitations.guilds[0].id', group._id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('user id invites', () => {
|
describe('user id invites', () => {
|
||||||
it('returns an error when inviter has no chat privileges', async () => {
|
it('returns an error when inviter has no chat privileges', async () => {
|
||||||
let inviterMuted = await inviter.update({'flags.chatRevoked': true});
|
let inviterMuted = await inviter.update({'flags.chatRevoked': true});
|
||||||
@@ -93,7 +160,7 @@ describe('Post /groups/:groupId/invite', () => {
|
|||||||
.to.eventually.be.rejected.and.eql({
|
.to.eventually.be.rejected.and.eql({
|
||||||
code: 400,
|
code: 400,
|
||||||
error: 'BadRequest',
|
error: 'BadRequest',
|
||||||
message: t('inviteMissingUuid'),
|
message: t('inviteMustNotBeEmpty'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -228,7 +295,7 @@ describe('Post /groups/:groupId/invite', () => {
|
|||||||
.to.eventually.be.rejected.and.eql({
|
.to.eventually.be.rejected.and.eql({
|
||||||
code: 400,
|
code: 400,
|
||||||
error: 'BadRequest',
|
error: 'BadRequest',
|
||||||
message: t('inviteMissingEmail'),
|
message: t('inviteMustNotBeEmpty'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ describe('GET /members/:memberId', () => {
|
|||||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||||
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
|
'backer', 'contributor', 'auth', 'items', 'inbox', 'loginIncentives', 'flags',
|
||||||
]);
|
]);
|
||||||
expect(Object.keys(memberRes.auth)).to.eql(['timestamps']);
|
expect(Object.keys(memberRes.auth)).to.eql(['local', 'timestamps']);
|
||||||
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
expect(Object.keys(memberRes.preferences).sort()).to.eql([
|
||||||
'size', 'hair', 'skin', 'shirt',
|
'size', 'hair', 'skin', 'shirt',
|
||||||
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||||
|
|||||||
57
test/api/v4/user/auth/POST-user_verify_display_name.test.js
Normal file
57
test/api/v4/user/auth/POST-user_verify_display_name.test.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../../helpers/api-integration/v4';
|
||||||
|
|
||||||
|
const ENDPOINT = '/user/auth/verify-display-name';
|
||||||
|
|
||||||
|
describe('POST /user/auth/verify-display-name', async () => {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('successfully verifies display name including funky characters', async () => {
|
||||||
|
let newDisplayName = 'Sabé 🤬';
|
||||||
|
let response = await user.post(ENDPOINT, {
|
||||||
|
displayName: newDisplayName,
|
||||||
|
});
|
||||||
|
expect(response).to.eql({ isUsable: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
context('errors', async () => {
|
||||||
|
it('errors if display name is not provided', async () => {
|
||||||
|
await expect(user.post(ENDPOINT, {
|
||||||
|
})).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if display name is a slur', async () => {
|
||||||
|
await expect(user.post(ENDPOINT, {
|
||||||
|
displayName: 'TESTPLACEHOLDERSLURWORDHERE',
|
||||||
|
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueSlur')] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if display name contains a slur', async () => {
|
||||||
|
await expect(user.post(ENDPOINT, {
|
||||||
|
displayName: 'TESTPLACEHOLDERSLURWORDHERE_otherword',
|
||||||
|
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] });
|
||||||
|
await expect(user.post(ENDPOINT, {
|
||||||
|
displayName: 'something_TESTPLACEHOLDERSLURWORDHERE',
|
||||||
|
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] });
|
||||||
|
await expect(user.post(ENDPOINT, {
|
||||||
|
displayName: 'somethingTESTPLACEHOLDERSLURWORDHEREotherword',
|
||||||
|
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength'), t('displaynameIssueSlur')] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if display name has incorrect length', async () => {
|
||||||
|
await expect(user.post(ENDPOINT, {
|
||||||
|
displayName: 'this is a very long display name over 30 characters',
|
||||||
|
})).to.eventually.eql({ isUsable: false, issues: [t('displaynameIssueLength')] });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -70,3 +70,9 @@
|
|||||||
width: 366px;
|
width: 366px;
|
||||||
height: 285px;
|
height: 285px;
|
||||||
}
|
}
|
||||||
|
.scene_veteran_pets {
|
||||||
|
background-image: url('~assets/images/sprites/spritesmith-largeSprites-0.png');
|
||||||
|
background-position: -582px -727px;
|
||||||
|
width: 242px;
|
||||||
|
height: 62px;
|
||||||
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 193 KiB |
@@ -20,7 +20,7 @@
|
|||||||
@include btn-focus-hover-shadow();
|
@include btn-focus-hover-shadow();
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover:not(.btn-flat) {
|
&:hover:not(.btn-flat):not(.disabled) {
|
||||||
@include btn-focus-hover-shadow();
|
@include btn-focus-hover-shadow();
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
@@ -37,6 +37,7 @@
|
|||||||
|
|
||||||
.btn:disabled, .btn.disabled {
|
.btn:disabled, .btn.disabled {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
cursor: default;
|
||||||
opacity: 0.64;
|
opacity: 0.64;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
z-index: 1350;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-dialog {
|
.modal-dialog {
|
||||||
|
margin: 5.5rem auto 3rem;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|||||||
34
website/client/assets/svg/hello-habitican.svg
Normal file
34
website/client/assets/svg/hello-habitican.svg
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="104" viewBox="0 0 256 104">
|
||||||
|
<defs>
|
||||||
|
<rect id="b" width="96" height="56" rx="6"/>
|
||||||
|
<filter id="a" width="112.5%" height="128.6%" x="-6.2%" y="-10.7%" filterUnits="objectBoundingBox">
|
||||||
|
<feMorphology in="SourceAlpha" operator="dilate" radius="4" result="shadowSpreadOuter1"/>
|
||||||
|
<feOffset dy="4" in="shadowSpreadOuter1" result="shadowOffsetOuter1"/>
|
||||||
|
<feComposite in="shadowOffsetOuter1" in2="SourceAlpha" operator="out" result="shadowOffsetOuter1"/>
|
||||||
|
<feColorMatrix in="shadowOffsetOuter1" values="0 0 0 0 0.407843137 0 0 0 0 0.384313725 0 0 0 0 0.454901961 0 0 0 0.24 0"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<g opacity=".64">
|
||||||
|
<path fill="#3FDAA2" d="M194.359 74.179l3.074-1.341-2.917-1.655-1.341-3.075-1.655 2.918-3.075 1.34 2.918 1.656 1.34 3.074z" opacity=".96"/>
|
||||||
|
<path fill="#FF6165" d="M127.439 4.105l-.023 2.982 2.399-1.771 2.981.023-1.77-2.4.022-2.98-2.399 1.77-2.98-.022z" opacity=".84"/>
|
||||||
|
<path fill="#3FDAA2" d="M70.501 38.126l2.574.428-1.202-2.315.428-2.574-2.316 1.202-2.573-.427 1.202 2.315-.428 2.573z" opacity=".83"/>
|
||||||
|
<path fill="#50B5E9" d="M240.929 73.34l.173 6.333 4.962-3.939 6.334-.173-3.939-4.962-.173-6.334-4.962 3.939-6.334.173z" opacity=".73"/>
|
||||||
|
<path fill="#FFBE5D" d="M198.881 41.724l-3.984 5.397 6.708-.05 5.397 3.983-.05-6.708 3.983-5.397-6.708.051-5.397-3.984z" opacity=".82"/>
|
||||||
|
<path fill="#50B5E9" d="M81.165 96.829l-.589-4.433-3.193 3.13-4.433.59 3.13 3.193.59 4.433 3.193-3.13 4.433-.59z" opacity=".99"/>
|
||||||
|
<path fill="#50B5E9" d="M119.702 40.186l-3.901 5.91 7.068-.425 5.91 3.902-.425-7.069 3.901-5.909-7.068.425-5.909-3.902z"/>
|
||||||
|
<path fill="#FF6165" d="M162.14 91.367l3.404 2.9.278-4.463 2.9-3.404-4.463-.278-3.404-2.9-.278 4.463-2.9 3.404z" opacity=".84"/>
|
||||||
|
<path fill="#FF944C" d="M6.708 37.066l.62 3.675 2.568-2.7 3.675-.62-2.7-2.568-.62-3.675-2.568 2.7-3.675.62zM253.486 43.18l-.037-3.727-2.96 2.265-3.726.037 2.265 2.959.037 3.727 2.96-2.266 3.726-.037z" opacity=".93"/>
|
||||||
|
<path fill="#9A62FF" d="M51.481 70.952l5.13-.957-3.843-3.53-.957-5.128-3.53 3.842-5.128.957 3.843 3.53.956 5.128z" opacity=".99"/>
|
||||||
|
<path fill="#FFBE5D" d="M78.061 8.656l-.952 3.987 3.761-1.63 3.987.952-1.63-3.761.953-3.988-3.762 1.63-3.987-.952z"/>
|
||||||
|
<path fill="#3FDAA2" d="M5.863 70.74l4.692 1.207-1.849-4.478 1.208-4.692-4.478 1.849-4.692-1.208 1.849 4.478-1.208 4.692z" opacity=".96"/>
|
||||||
|
<path fill="#9A62FF" d="M182.63 14.447l5.026-1.4-4.135-3.18-1.4-5.027-3.181 4.136-5.026 1.4 4.135 3.18 1.4 5.027z" opacity=".99"/>
|
||||||
|
</g>
|
||||||
|
<g transform="translate(79 24)">
|
||||||
|
<use fill="#000" filter="url(#a)" xlink:href="#b"/>
|
||||||
|
<rect width="100" height="60" x="-2" y="-2" fill="#6133B4" stroke="#4F2A93" stroke-width="4" rx="6"/>
|
||||||
|
</g>
|
||||||
|
<path fill="#FFF" d="M99.91 39v-9.8h3.024v3.528h2.366V29.2h3.024V39H105.3v-3.584h-2.366V39H99.91zm12.138 0v-9.8h7.182v2.576h-4.186v1.12h3.878v2.352h-3.878v1.176h4.256V39h-7.252zm10.794 0v-9.8h3.024v7.14h3.738V39h-6.762zm10.178 0v-9.8h3.024v7.14h3.738V39h-6.762zm14.14.21c-2.688 0-4.634-2.03-4.634-4.97v-.252c0-2.94 1.96-4.998 4.648-4.998 2.702 0 4.648 2.03 4.648 4.97v.252c0 2.94-1.974 4.998-4.662 4.998zm.014-2.702c.98 0 1.582-.84 1.582-2.296v-.21c0-1.456-.616-2.31-1.596-2.31-.966 0-1.582.84-1.582 2.296v.21c0 1.456.616 2.31 1.596 2.31zM79 44h96v28H79z"/>
|
||||||
|
<path fill="#4E4A57" d="M102.99 64.82c.04-1.6.04-3.34-.12-4.96-.8-.18-1.76-.26-2.88-.1v5.18c0 .18-.38.28-.5.28-.24 0-.5-.14-.54-.4l-.24-14.22c0-.42.06-.76.52-.76.1 0 .72.1.72.28l.02 8.54c.74.1 2.34.06 2.98.14v-.84c0-2.74-.16-5.1-.16-7.48 0-.24.04-.64.34-.64.3 0 .74.14.78.52l.26 14.36c0 .34-.3.62-.68.62h-.1c-.14-.02-.36-.4-.4-.52zm7.64-3.18h-2.96l-.12.12-.34 2.26c-.04.3-.02 1.16-.6 1.16-.26 0-.44-.52-.44-.72 2.08-14.5 2.1-14.54 2.1-14.54.1-.38.24-.94.74-.94.22 0 .5.1.62.3l.16.42c1.16 4.58 1.7 9.28 2.24 14l.12 1.3c0 .02-.02.02-.02.04-.14.38-.22.54-.6.54h-.06c-.12 0-.32 0-.34-.14l-.5-3.8zm-1.5-10.52l-1.16 9.28 2.5.06-1.34-9.34zm4.3-.68c1.3-1.48 4.24-1.06 4.24 1.4 0 1.04-.4 1.98-1.08 2.74 2.28 1.42 3.18 3.12 3.18 5.8 0 2.86-1.58 5.3-4.72 5.3-.14 0-.54-.02-.7-.3-.22-3.08-.22-6.14-.52-9.76l-.4-4.76v-.42zm1.98 14.02c.12.02.24.02.36.02 2.12 0 2.82-2.18 2.82-4.1 0-2.22-1.02-4.92-3.64-4.92 0 3.02.22 6 .46 9zm-.82-13.58l.3 3.3c1.02 0 1.7-1.16 1.7-2.26 0-1.14-.94-1.54-2-1.04zm6.54 14.74l.54-14.84c.14-.4.24-.42.66-.42.58 0 .58.26.58.84 0 .06-.02.32-.02.36l-.6 13.68c-.04.88-1.16.96-1.16.38zm5.08-1.86l-.4-12.66c-1.24.08-1.8.06-1.84-.28.04-.68.04-.92 2.12-.8h1.68c.44.02 1.18.08 1.18.66 0 .18-.22.38-.4.38h-1.68l.38 12.7c0 .18-.4.28-.52.28s-.52-.1-.52-.28zm4.12 1.86l.54-14.84c.14-.4.24-.42.66-.42.58 0 .58.26.58.84 0 .06-.02.32-.02.36l-.6 13.68c-.04.88-1.16.96-1.16.38zm7.66-15.56c.42 0 .94.28.94.74 0 .7-1.16.56-1.44.56h-.12c-1.94 0-2.8 4.02-2.8 6.28 0 2.56.16 4.38 1.92 5.84.86.72 2.18-.02 2.48.52.4.7-.34.94-.98.94-4.54-.1-5.24-6.12-4.32-10.02.48-2.08 1.72-4.86 4.32-4.86zm7.4 11.58h-2.96l-.12.12-.34 2.26c-.04.3-.02 1.16-.6 1.16-.26 0-.44-.52-.44-.72 2.08-14.5 2.1-14.54 2.1-14.54.1-.38.24-.94.74-.94.22 0 .5.1.62.3l.16.42c1.16 4.58 1.7 9.28 2.24 14l.12 1.3c0 .02-.02.02-.02.04-.14.38-.22.54-.6.54h-.06c-.12 0-.32 0-.34-.14l-.5-3.8zm-1.5-10.52l-1.16 9.28 2.5.06-1.34-9.34zm5.02 14.24l.24-14.08c-.04-.8 1.06-1.28 1.48-.12l4.08 11.06c.02-.88.18-5.8.3-7.76.06-1.2-.02-2.42.24-3.54 0-.22.22-.22.52-.22.48.02.52.04.5.48-.06 2.08-.36 4.16-.44 6.24-.04 1.16-.24 6.7-.24 7.86-.14.42-.76.18-.92-.16-.2-.42-.36-.82-4.6-12.16l-.14 12.28c0 .6-1.02.54-1.02.12z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.6 KiB |
@@ -1,3 +1,3 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
<path fill="#A5A1AC" fill-rule="evenodd" d="M11 10c0 1.654-1.346 3-3 3s-3-1.346-3-3h6zm2-3.5a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 13 6.5zm-7 0a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 6 6.5zM14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
|
<path fill-rule="evenodd" d="M11 10c0 1.654-1.346 3-3 3s-3-1.346-3-3h6zm2-3.5a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 13 6.5zm-7 0a1.5 1.5 0 1 1-3.001-.001A1.5 1.5 0 0 1 6 6.5zM14 0H2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 361 B After Width: | Height: | Size: 346 B |
@@ -82,6 +82,7 @@
|
|||||||
import hello from 'hellojs';
|
import hello from 'hellojs';
|
||||||
import { setUpAxios } from 'client/libs/auth';
|
import { setUpAxios } from 'client/libs/auth';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
import isEmail from 'validator/lib/isEmail';
|
||||||
|
|
||||||
import facebookSquareIcon from 'assets/svg/facebook-square.svg';
|
import facebookSquareIcon from 'assets/svg/facebook-square.svg';
|
||||||
import googleIcon from 'assets/svg/google.svg';
|
import googleIcon from 'assets/svg/google.svg';
|
||||||
@@ -115,13 +116,13 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
emailValid () {
|
emailValid () {
|
||||||
if (this.email.length <= 3) return false;
|
if (this.email.length <= 3) return false;
|
||||||
return this.validateEmail(this.email);
|
return isEmail(this.email);
|
||||||
},
|
},
|
||||||
emailInvalid () {
|
emailInvalid () {
|
||||||
return !this.emailValid;
|
return !this.emailValid;
|
||||||
},
|
},
|
||||||
usernameValid () {
|
usernameValid () {
|
||||||
if (this.username.length <= 3) return false;
|
if (this.username.length < 1) return false;
|
||||||
return this.usernameIssues.length === 0;
|
return this.usernameIssues.length === 0;
|
||||||
},
|
},
|
||||||
usernameInvalid () {
|
usernameInvalid () {
|
||||||
@@ -143,7 +144,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
// eslint-disable-next-line func-names
|
// eslint-disable-next-line func-names
|
||||||
validateUsername: debounce(function (username) {
|
validateUsername: debounce(function (username) {
|
||||||
if (username.length <= 3) {
|
if (username.length < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$store.dispatch('auth:verifyUsername', {
|
this.$store.dispatch('auth:verifyUsername', {
|
||||||
|
|||||||
@@ -295,6 +295,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import hello from 'hellojs';
|
import hello from 'hellojs';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
import isEmail from 'validator/lib/isEmail';
|
||||||
|
|
||||||
import gryphon from 'assets/svg/gryphon.svg';
|
import gryphon from 'assets/svg/gryphon.svg';
|
||||||
import habiticaIcon from 'assets/svg/habitica-logo.svg';
|
import habiticaIcon from 'assets/svg/habitica-logo.svg';
|
||||||
@@ -340,18 +341,18 @@ export default {
|
|||||||
},
|
},
|
||||||
emailValid () {
|
emailValid () {
|
||||||
if (this.email.length <= 3) return false;
|
if (this.email.length <= 3) return false;
|
||||||
return this.validateEmail(this.email);
|
return isEmail(this.email);
|
||||||
},
|
},
|
||||||
emailInvalid () {
|
emailInvalid () {
|
||||||
if (this.email.length <= 3) return false;
|
if (this.email.length <= 3) return false;
|
||||||
return !this.emailValid;
|
return !this.emailValid;
|
||||||
},
|
},
|
||||||
usernameValid () {
|
usernameValid () {
|
||||||
if (this.username.length <= 3) return false;
|
if (this.username.length < 1) return false;
|
||||||
return this.usernameIssues.length === 0;
|
return this.usernameIssues.length === 0;
|
||||||
},
|
},
|
||||||
usernameInvalid () {
|
usernameInvalid () {
|
||||||
if (this.username.length <= 3) return false;
|
if (this.username.length < 1) return false;
|
||||||
return !this.usernameValid;
|
return !this.usernameValid;
|
||||||
},
|
},
|
||||||
passwordConfirmValid () {
|
passwordConfirmValid () {
|
||||||
|
|||||||
@@ -1,26 +1,91 @@
|
|||||||
<template lang="pug">
|
<template lang="pug">
|
||||||
div.autocomplete-selection(v-if='searchResults.length > 0', :style='autocompleteStyle')
|
.autocomplete-selection(v-if='searchResults.length > 0', :style='autocompleteStyle')
|
||||||
.autocomplete-results(v-for='result in searchResults', @click='select(result)') {{ result }}
|
.autocomplete-results.d-flex.align-items-center(
|
||||||
|
v-for='result in searchResults',
|
||||||
|
@click='select(result)',
|
||||||
|
@mouseenter='result.hover = true',
|
||||||
|
@mouseleave='result.hover = false',
|
||||||
|
:class='{"hover-background": result.hover}',
|
||||||
|
)
|
||||||
|
span
|
||||||
|
h3.profile-name(:class='userLevelStyle(result.msg)') {{ result.displayName }}
|
||||||
|
.svg-icon(v-html="tierIcon(result.msg)", v-if='showTierStyle(result.msg)')
|
||||||
|
span.username.ml-2(v-if='result.username', :class='{"hover-foreground": result.hover}') @{{ result.username }}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="scss" scoped>
|
||||||
|
@import '~client/assets/scss/tiers.scss';
|
||||||
|
@import '~client/assets/scss/colors.scss';
|
||||||
|
|
||||||
.autocomplete-results {
|
.autocomplete-results {
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete-selection {
|
||||||
box-shadow: 1px 1px 1px #efefef;
|
box-shadow: 1px 1px 1px #efefef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hover-background {
|
||||||
|
background-color: rgba(213, 200, 255, 0.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-foreground {
|
||||||
|
color: $purple-300 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-name {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-icon {
|
||||||
|
width: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
color: $gray-200;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import groupBy from 'lodash/groupBy';
|
import groupBy from 'lodash/groupBy';
|
||||||
|
import styleHelper from 'client/mixins/styleHelper';
|
||||||
|
import tier1 from 'assets/svg/tier-1.svg';
|
||||||
|
import tier2 from 'assets/svg/tier-2.svg';
|
||||||
|
import tier3 from 'assets/svg/tier-3.svg';
|
||||||
|
import tier4 from 'assets/svg/tier-4.svg';
|
||||||
|
import tier5 from 'assets/svg/tier-5.svg';
|
||||||
|
import tier6 from 'assets/svg/tier-6.svg';
|
||||||
|
import tier7 from 'assets/svg/tier-7.svg';
|
||||||
|
import tier8 from 'assets/svg/tier-mod.svg';
|
||||||
|
import tier9 from 'assets/svg/tier-staff.svg';
|
||||||
|
import tierNPC from 'assets/svg/tier-npc.svg';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ['selections', 'text', 'coords', 'chat', 'textbox'],
|
props: ['selections', 'text', 'caretPosition', 'coords', 'chat', 'textbox'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
atRegex: /(?!\b)@[\w-]*$/,
|
||||||
currentSearch: '',
|
currentSearch: '',
|
||||||
searchActive: false,
|
searchActive: false,
|
||||||
|
searchEscaped: false,
|
||||||
currentSearchPosition: 0,
|
currentSearchPosition: 0,
|
||||||
tmpSelections: [],
|
tmpSelections: [],
|
||||||
|
icons: Object.freeze({
|
||||||
|
tier1,
|
||||||
|
tier2,
|
||||||
|
tier3,
|
||||||
|
tier4,
|
||||||
|
tier5,
|
||||||
|
tier6,
|
||||||
|
tier7,
|
||||||
|
tier8,
|
||||||
|
tier9,
|
||||||
|
tierNPC,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -36,32 +101,46 @@ export default {
|
|||||||
marginTop: '28px',
|
marginTop: '28px',
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
minWidth: '100px',
|
minWidth: '100px',
|
||||||
minHeight: '100px',
|
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
searchResults () {
|
searchResults () {
|
||||||
if (!this.searchActive) return [];
|
if (!this.searchActive) return [];
|
||||||
let currentSearch = this.text.substring(this.currentSearchPosition + 1, this.text.length);
|
if (!this.atRegex.exec(this.text)) return [];
|
||||||
|
this.currentSearch = this.atRegex.exec(this.text)[0];
|
||||||
|
this.currentSearch = this.currentSearch.substring(1, this.currentSearch.length);
|
||||||
|
|
||||||
return this.tmpSelections.filter((option) => {
|
return this.tmpSelections.filter((option) => {
|
||||||
return option.toLowerCase().indexOf(currentSearch.toLowerCase()) !== -1;
|
return option.displayName.toLowerCase().indexOf(this.currentSearch.toLowerCase()) !== -1 || option.username && option.username.toLowerCase().indexOf(this.currentSearch.toLowerCase()) !== -1;
|
||||||
});
|
}).slice(0, 4);
|
||||||
},
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.grabUserNames();
|
this.grabUserNames();
|
||||||
},
|
},
|
||||||
|
created () {
|
||||||
|
document.addEventListener('keyup', this.handleEsc);
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
document.removeEventListener('keyup', this.handleEsc);
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
text (newText) {
|
text (newText) {
|
||||||
if (!newText[newText.length - 1] || newText[newText.length - 1] === ' ') {
|
if (!newText[newText.length - 1] || newText[newText.length - 1] === ' ') {
|
||||||
this.searchActive = false;
|
this.searchActive = false;
|
||||||
|
this.searchEscaped = false;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (newText[newText.length - 1] === '@') {
|
||||||
|
this.searchEscaped = false;
|
||||||
|
}
|
||||||
|
if (this.searchEscaped) return;
|
||||||
|
|
||||||
|
if (!this.atRegex.test(newText)) return;
|
||||||
|
|
||||||
if (newText[newText.length - 1] !== '@') return;
|
|
||||||
this.searchActive = true;
|
this.searchActive = true;
|
||||||
this.currentSearchPosition = newText.length - 1;
|
|
||||||
},
|
},
|
||||||
chat () {
|
chat () {
|
||||||
this.resetDefaults();
|
this.resetDefaults();
|
||||||
@@ -72,25 +151,52 @@ export default {
|
|||||||
resetDefaults () {
|
resetDefaults () {
|
||||||
// Mounted is not called when switching between group pages because they have the
|
// Mounted is not called when switching between group pages because they have the
|
||||||
// the same parent component. So, reset the data
|
// the same parent component. So, reset the data
|
||||||
this.currentSearch = '';
|
|
||||||
this.searchActive = false;
|
this.searchActive = false;
|
||||||
this.currentSearchPosition = 0;
|
this.searchEscaped = false;
|
||||||
this.tmpSelections = [];
|
this.tmpSelections = [];
|
||||||
},
|
},
|
||||||
grabUserNames () {
|
grabUserNames () {
|
||||||
let usersThatMessage = groupBy(this.chat, 'user');
|
let usersThatMessage = groupBy(this.chat, 'user');
|
||||||
for (let userName in usersThatMessage) {
|
for (let userKey in usersThatMessage) {
|
||||||
let systemMessage = userName === 'undefined';
|
let systemMessage = userKey === 'undefined';
|
||||||
if (!systemMessage && this.tmpSelections.indexOf(userName) === -1) {
|
if (!systemMessage && this.tmpSelections.indexOf(userKey) === -1) {
|
||||||
this.tmpSelections.push(userName);
|
this.tmpSelections.push({
|
||||||
|
displayName: userKey,
|
||||||
|
username: usersThatMessage[userKey][0].username,
|
||||||
|
msg: {
|
||||||
|
backer: usersThatMessage[userKey][0].backer,
|
||||||
|
contributor: usersThatMessage[userKey][0].contributor,
|
||||||
|
},
|
||||||
|
hover: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
showTierStyle (message) {
|
||||||
|
const isContributor = Boolean(message.contributor && message.contributor.level);
|
||||||
|
const isNPC = Boolean(message.backer && message.backer.npc);
|
||||||
|
return isContributor || isNPC;
|
||||||
|
},
|
||||||
|
tierIcon (message) {
|
||||||
|
const isNPC = Boolean(message.backer && message.backer.npc);
|
||||||
|
if (isNPC) {
|
||||||
|
return this.icons.tierNPC;
|
||||||
|
}
|
||||||
|
return this.icons[`tier${message.contributor.level}`];
|
||||||
|
},
|
||||||
select (result) {
|
select (result) {
|
||||||
let newText = this.text.slice(0, this.currentSearchPosition + 1) + result;
|
let newText = this.text;
|
||||||
this.searchActive = false;
|
const targetName = `${result.username || result.displayName} `;
|
||||||
|
newText = newText.replace(new RegExp(`${this.currentSearch}$`), targetName);
|
||||||
this.$emit('select', newText);
|
this.$emit('select', newText);
|
||||||
},
|
},
|
||||||
|
handleEsc (e) {
|
||||||
|
if (e.keyCode === 27) {
|
||||||
|
this.searchActive = false;
|
||||||
|
this.searchEscaped = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
mixins: [styleHelper],
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,37 +4,50 @@ div
|
|||||||
.message-hidden(v-if='msg.flagCount === 1 && user.contributor.admin') Message flagged once, not hidden
|
.message-hidden(v-if='msg.flagCount === 1 && user.contributor.admin') Message flagged once, not hidden
|
||||||
.message-hidden(v-if='msg.flagCount > 1 && user.contributor.admin') Message hidden
|
.message-hidden(v-if='msg.flagCount > 1 && user.contributor.admin') Message hidden
|
||||||
.card-body
|
.card-body
|
||||||
h3.leader(
|
h3.leader(
|
||||||
:class='userLevelStyle(msg)',
|
:class='userLevelStyle(msg)',
|
||||||
@click="showMemberModal(msg.uuid)",
|
@click="showMemberModal(msg.uuid)",
|
||||||
v-b-tooltip.hover.top="tierTitle",
|
v-b-tooltip.hover.top="tierTitle",
|
||||||
)
|
v-if="msg.user"
|
||||||
| {{msg.user}}
|
)
|
||||||
.svg-icon(v-html="tierIcon", v-if='showShowTierStyle')
|
| {{msg.user}}
|
||||||
p.time(v-b-tooltip="", :title="msg.timestamp | date") {{msg.timestamp | timeAgo}}
|
.svg-icon(v-html="tierIcon")
|
||||||
.text(v-markdown='msg.text')
|
p.time
|
||||||
hr
|
span.mr-1(v-if="msg.username") @{{ msg.username }}
|
||||||
div(v-if='msg.id')
|
span.mr-1(v-if="msg.username") •
|
||||||
.action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}')
|
span(v-b-tooltip="", :title="msg.timestamp | date") {{ msg.timestamp | timeAgo }}
|
||||||
.svg-icon(v-html="icons.like")
|
.text(v-html='atHighlight(parseMarkdown(msg.text))')
|
||||||
|
hr
|
||||||
|
.d-flex(v-if='msg.id')
|
||||||
|
.action.d-flex.align-items-center(v-if='!inbox', @click='copyAsTodo(msg)')
|
||||||
|
.svg-icon(v-html="icons.copy")
|
||||||
|
div {{$t('copyAsTodo')}}
|
||||||
|
.action.d-flex.align-items-center(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
|
||||||
|
.svg-icon(v-html="icons.report")
|
||||||
|
div {{$t('report')}}
|
||||||
|
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
|
||||||
|
.action.d-flex.align-items-center(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
|
||||||
|
.svg-icon(v-html="icons.delete")
|
||||||
|
| {{$t('delete')}}
|
||||||
|
.ml-auto.d-flex(v-b-tooltip="{title: likeTooltip(msg.likes[user._id])}", v-if='!inbox')
|
||||||
|
.action.d-flex.align-items-center.mr-0(@click='like()', v-if='likeCount > 0', :class='{active: msg.likes[user._id]}')
|
||||||
|
.svg-icon(v-html="icons.liked", :title='$t("liked")')
|
||||||
|
| +{{ likeCount }}
|
||||||
|
.action.d-flex.align-items-center.mr-0(@click='like()', v-if='likeCount === 0', :class='{active: msg.likes[user._id]}')
|
||||||
|
.svg-icon(v-html="icons.like", :title='$t("like")')
|
||||||
span(v-if='!msg.likes[user._id]') {{ $t('like') }}
|
span(v-if='!msg.likes[user._id]') {{ $t('like') }}
|
||||||
span(v-if='msg.likes[user._id]') {{ $t('liked') }}
|
|
||||||
span.action(v-if='!inbox', @click='copyAsTodo(msg)')
|
|
||||||
.svg-icon(v-html="icons.copy")
|
|
||||||
| {{$t('copyAsTodo')}}
|
|
||||||
span.action(v-if='!inbox && user.flags.communityGuidelinesAccepted && msg.uuid !== "system"', @click='report(msg)')
|
|
||||||
.svg-icon(v-html="icons.report")
|
|
||||||
| {{$t('report')}}
|
|
||||||
// @TODO make flagging/reporting work in the inbox. NOTE: it must work even if the communityGuidelines are not accepted and it MUST work for messages that you have SENT as well as received. -- Alys
|
|
||||||
span.action(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
|
|
||||||
.svg-icon(v-html="icons.delete")
|
|
||||||
| {{$t('delete')}}
|
|
||||||
span.action.float-right.liked(v-if='likeCount > 0')
|
|
||||||
.svg-icon(v-html="icons.liked")
|
|
||||||
| + {{ likeCount }}
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.at-highlight {
|
||||||
|
background-color: rgba(213, 200, 255, 0.32);
|
||||||
|
color: #6133b4;
|
||||||
|
padding: 0.1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@import '~client/assets/scss/colors.scss';
|
||||||
@import '~client/assets/scss/tiers.scss';
|
@import '~client/assets/scss/tiers.scss';
|
||||||
|
|
||||||
.mentioned-icon {
|
.mentioned-icon {
|
||||||
@@ -54,7 +67,14 @@ div
|
|||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.card-body {
|
.card-body {
|
||||||
|
padding: 0.75rem 1.25rem 0.75rem 1.25rem;
|
||||||
|
|
||||||
.leader {
|
.leader {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
@@ -62,6 +82,7 @@ div
|
|||||||
h3 { // this is the user name
|
h3 { // this is the user name
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
.svg-icon {
|
.svg-icon {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
@@ -73,13 +94,15 @@ div
|
|||||||
.time {
|
.time {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #878190;
|
color: #878190;
|
||||||
width: 150px;
|
margin-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #4e4a57;
|
color: #4e4a57;
|
||||||
text-align: left !important;
|
text-align: left !important;
|
||||||
|
min-height: 0rem;
|
||||||
|
margin-bottom: -0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,25 +110,25 @@ div
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
color: #878190;
|
color: #878190;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.svg-icon {
|
||||||
|
color: #A5A1AC;
|
||||||
|
margin-right: .2em;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action:hover {
|
.active {
|
||||||
cursor: pointer;
|
color: $purple-300;
|
||||||
}
|
|
||||||
|
|
||||||
.liked:hover {
|
.svg-icon {
|
||||||
cursor: default;
|
color: $purple-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action .svg-icon {
|
|
||||||
margin-right: .2em;
|
|
||||||
width: 16px;
|
|
||||||
display: inline-block;
|
|
||||||
color: #A5A1AC;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action.active, .active .svg-icon {
|
|
||||||
color: #46a7d9
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -114,8 +137,9 @@ import axios from 'axios';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import escapeRegExp from 'lodash/escapeRegExp';
|
import escapeRegExp from 'lodash/escapeRegExp';
|
||||||
|
import max from 'lodash/max';
|
||||||
|
|
||||||
import markdownDirective from 'client/directives/markdown';
|
import habiticaMarkdown from 'habitica-markdown';
|
||||||
import { mapState } from 'client/libs/store';
|
import { mapState } from 'client/libs/store';
|
||||||
import styleHelper from 'client/mixins/styleHelper';
|
import styleHelper from 'client/mixins/styleHelper';
|
||||||
|
|
||||||
@@ -161,9 +185,6 @@ export default {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
directives: {
|
|
||||||
markdown: markdownDirective,
|
|
||||||
},
|
|
||||||
filters: {
|
filters: {
|
||||||
timeAgo (value) {
|
timeAgo (value) {
|
||||||
return moment(value).fromNow();
|
return moment(value).fromNow();
|
||||||
@@ -177,23 +198,24 @@ export default {
|
|||||||
...mapState({user: 'user.data'}),
|
...mapState({user: 'user.data'}),
|
||||||
isUserMentioned () {
|
isUserMentioned () {
|
||||||
const message = this.msg;
|
const message = this.msg;
|
||||||
let user = this.user;
|
const user = this.user;
|
||||||
|
|
||||||
if (message.hasOwnProperty('highlight')) return message.highlight;
|
if (message.hasOwnProperty('highlight')) return message.highlight;
|
||||||
|
|
||||||
message.highlight = false;
|
message.highlight = false;
|
||||||
let messagetext = message.text.toLowerCase();
|
const messageText = message.text.toLowerCase();
|
||||||
let username = user.profile.name;
|
const displayName = user.profile.name;
|
||||||
let mentioned = messagetext.indexOf(username.toLowerCase());
|
const username = user.auth.local && user.auth.local.username;
|
||||||
let escapedUsername = escapeRegExp(username);
|
const mentioned = max([messageText.indexOf(username.toLowerCase()), messageText.indexOf(displayName.toLowerCase())]);
|
||||||
let pattern = `@${escapedUsername}([^\w]|$){1}`;
|
|
||||||
|
|
||||||
if (mentioned === -1) return message.highlight;
|
if (mentioned === -1) return message.highlight;
|
||||||
|
|
||||||
let preceedingchar = messagetext.substring(mentioned - 1, mentioned);
|
const escapedDisplayName = escapeRegExp(displayName);
|
||||||
if (mentioned === 0 || preceedingchar.trim() === '' || preceedingchar === '@') {
|
const escapedUsername = escapeRegExp(username);
|
||||||
|
const pattern = `@(${escapedUsername}|${escapedDisplayName})([^\w]|$)`;
|
||||||
|
const precedingChar = messageText.substring(mentioned - 1, mentioned);
|
||||||
|
if (mentioned === 0 || precedingChar.trim() === '' || precedingChar === '@') {
|
||||||
let regex = new RegExp(pattern, 'i');
|
let regex = new RegExp(pattern, 'i');
|
||||||
message.highlight = regex.test(messagetext);
|
message.highlight = regex.test(messageText);
|
||||||
}
|
}
|
||||||
|
|
||||||
return message.highlight;
|
return message.highlight;
|
||||||
@@ -209,12 +231,6 @@ export default {
|
|||||||
}
|
}
|
||||||
return likeCount;
|
return likeCount;
|
||||||
},
|
},
|
||||||
showShowTierStyle () {
|
|
||||||
const message = this.msg;
|
|
||||||
const isContributor = Boolean(message.contributor && message.contributor.level);
|
|
||||||
const isNPC = Boolean(message.backer && message.backer.npc);
|
|
||||||
return isContributor || isNPC;
|
|
||||||
},
|
|
||||||
tierIcon () {
|
tierIcon () {
|
||||||
const message = this.msg;
|
const message = this.msg;
|
||||||
const isNPC = Boolean(message.backer && message.backer.npc);
|
const isNPC = Boolean(message.backer && message.backer.npc);
|
||||||
@@ -244,6 +260,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.$emit('message-liked', message);
|
this.$emit('message-liked', message);
|
||||||
|
this.$root.$emit('bv::hide::tooltip');
|
||||||
|
},
|
||||||
|
likeTooltip (likedStatus) {
|
||||||
|
if (!likedStatus) return this.$t('like');
|
||||||
},
|
},
|
||||||
copyAsTodo (message) {
|
copyAsTodo (message) {
|
||||||
this.$root.$emit('habitica::copy-as-todo', message);
|
this.$root.$emit('habitica::copy-as-todo', message);
|
||||||
@@ -273,6 +293,14 @@ export default {
|
|||||||
showMemberModal (memberId) {
|
showMemberModal (memberId) {
|
||||||
this.$emit('show-member-modal', memberId);
|
this.$emit('show-member-modal', memberId);
|
||||||
},
|
},
|
||||||
|
atHighlight (text) {
|
||||||
|
return text.replace(new RegExp(/(?!\b)@[\w-]+/g), match => {
|
||||||
|
return `<span class="at-highlight">${match}</span>`;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
parseMarkdown (text) {
|
||||||
|
return habiticaMarkdown.render(text);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
v-if='msg.userStyles || (cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected)',
|
v-if='msg.userStyles || (cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected)',
|
||||||
:member="msg.userStyles || cachedProfileData[msg.uuid]",
|
:member="msg.userStyles || cachedProfileData[msg.uuid]",
|
||||||
:avatarOnly="true",
|
:avatarOnly="true",
|
||||||
|
:overrideTopPadding='"14px"',
|
||||||
:hideClassBadge='true',
|
:hideClassBadge='true',
|
||||||
@click.native="showMemberModal(msg.uuid)",
|
@click.native="showMemberModal(msg.uuid)",
|
||||||
)
|
)
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
:member="msg.userStyles || cachedProfileData[msg.uuid]",
|
:member="msg.userStyles || cachedProfileData[msg.uuid]",
|
||||||
:avatarOnly="true",
|
:avatarOnly="true",
|
||||||
:hideClassBadge='true',
|
:hideClassBadge='true',
|
||||||
|
:overrideTopPadding='"14px"',
|
||||||
@click.native="showMemberModal(msg.uuid)",
|
@click.native="showMemberModal(msg.uuid)",
|
||||||
)
|
)
|
||||||
</template>
|
</template>
|
||||||
@@ -47,6 +49,10 @@
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~client/assets/scss/colors.scss';
|
@import '~client/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
margin-left: -1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.hr {
|
.hr {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@@ -70,7 +76,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
border: 0px;
|
||||||
margin-bottom: .5em;
|
margin-bottom: .5em;
|
||||||
|
padding: 0rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
|||||||
h3(v-once) {{$t('welcomeTo')}}
|
h3(v-once) {{$t('welcomeTo')}}
|
||||||
.svg-icon.logo(v-html='icons.logoPurple')
|
.svg-icon.logo(v-html='icons.logoPurple')
|
||||||
|
|
||||||
.avatar-section.row(:class='{"page-2": modalPage === 2}')
|
.avatar-section.row(v-if='modalPage > 1', :class='{"page-2": modalPage === 2}')
|
||||||
.col-6.offset-3
|
.col-6.offset-3
|
||||||
.user-creation-bg(v-if='!editing')
|
.user-creation-bg(v-if='!editing')
|
||||||
avatar(:member='user', :avatarOnly='!editing', :class='{"edit-avatar": editing}')
|
avatar(:member='user', :avatarOnly='!editing', :class='{"edit-avatar": editing}')
|
||||||
@@ -187,18 +187,18 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
|||||||
#extra.section.container.customize-section(v-if='activeTopPage === "extra"')
|
#extra.section.container.customize-section(v-if='activeTopPage === "extra"')
|
||||||
.row.sub-menu
|
.row.sub-menu
|
||||||
.col-3.offset-1.text-center.sub-menu-item(@click='changeSubPage("glasses")', :class='{active: activeSubPage === "glasses"}')
|
.col-3.offset-1.text-center.sub-menu-item(@click='changeSubPage("glasses")', :class='{active: activeSubPage === "glasses"}')
|
||||||
strong(v-once) {{$t('glasses')}}
|
strong(v-once) {{ $t('glasses') }}
|
||||||
.col-4.text-center.sub-menu-item(@click='changeSubPage("wheelchair")', :class='{active: activeSubPage === "wheelchair"}')
|
.col-4.text-center.sub-menu-item(@click='changeSubPage("wheelchair")', :class='{active: activeSubPage === "wheelchair"}')
|
||||||
strong(v-once) {{$t('wheelchair')}}
|
strong(v-once) {{ $t('wheelchair') }}
|
||||||
.col-3.text-center.sub-menu-item(@click='changeSubPage("flower")', :class='{active: activeSubPage === "flower"}')
|
.col-3.text-center.sub-menu-item(@click='changeSubPage("flower")', :class='{active: activeSubPage === "flower"}')
|
||||||
strong(v-once) {{$t('accent')}}
|
strong(v-once) {{ $t('accent') }}
|
||||||
.row.sub-menu(v-if='editing')
|
.row.sub-menu(v-if='editing')
|
||||||
.col-4.text-center.sub-menu-item(@click='changeSubPage("ears")' :class='{active: activeSubPage === "ears"}')
|
.col-4.text-center.sub-menu-item(@click='changeSubPage("ears")' :class='{active: activeSubPage === "ears"}')
|
||||||
strong(v-once) {{$t('animalEars')}}
|
strong(v-once) {{ $t('animalEars') }}
|
||||||
.col-4.text-center.sub-menu-item(@click='changeSubPage("tails")' :class='{active: activeSubPage === "tails"}')
|
.col-4.text-center.sub-menu-item(@click='changeSubPage("tails")' :class='{active: activeSubPage === "tails"}')
|
||||||
strong(v-once) {{$t('animalTails')}}
|
strong(v-once) {{ $t('animalTails') }}
|
||||||
.col-4.text-center.sub-menu-item(@click='changeSubPage("headband")' :class='{active: activeSubPage === "headband"}')
|
.col-4.text-center.sub-menu-item(@click='changeSubPage("headband")' :class='{active: activeSubPage === "headband"}')
|
||||||
strong(v-once) {{$t('headband')}}
|
strong(v-once) {{ $t('headband') }}
|
||||||
#glasses.row(v-if='activeSubPage === "glasses"')
|
#glasses.row(v-if='activeSubPage === "glasses"')
|
||||||
.col-12.customize-options
|
.col-12.customize-options
|
||||||
.option(v-for='option in eyewear', :class='{active: option.active}')
|
.option(v-for='option in eyewear', :class='{active: option.active}')
|
||||||
@@ -305,7 +305,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
|||||||
)
|
)
|
||||||
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
|
span.svg-icon.inline.icon-12.color(v-html="icons.pin")
|
||||||
.purchase-background.set(v-if='!ownsSet("background", set.items) && set.identifier !== "incentiveBackgrounds"' @click='unlock(setKeys("background", set.items))')
|
.purchase-background.set(v-if='!ownsSet("background", set.items) && set.identifier !== "incentiveBackgrounds"' @click='unlock(setKeys("background", set.items))')
|
||||||
span.label Purchase Set
|
span.label {{ $t('purchaseAll') }}
|
||||||
.svg-icon.gem(v-html='icons.gem')
|
.svg-icon.gem(v-html='icons.gem')
|
||||||
span.price 15
|
span.price 15
|
||||||
.row.customize-menu(v-if='filterBackgrounds')
|
.row.customize-menu(v-if='filterBackgrounds')
|
||||||
@@ -320,7 +320,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
|||||||
.container.interests-section(v-if='modalPage === 3 && !editing')
|
.container.interests-section(v-if='modalPage === 3 && !editing')
|
||||||
.section.row
|
.section.row
|
||||||
.col-12.text-center
|
.col-12.text-center
|
||||||
h2 I want to work on:
|
h2 {{ $t('wantToWorkOn') }}
|
||||||
.section.row
|
.section.row
|
||||||
.col-6
|
.col-6
|
||||||
.task-option
|
.task-option
|
||||||
@@ -353,28 +353,35 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
|||||||
input.custom-control-input#self_care(type="checkbox", value='self_care', v-model='taskCategories')
|
input.custom-control-input#self_care(type="checkbox", value='self_care', v-model='taskCategories')
|
||||||
label.custom-control-label(v-once, for="self_care") {{ $t('self_care') }}
|
label.custom-control-label(v-once, for="self_care") {{ $t('self_care') }}
|
||||||
|
|
||||||
.section.row.justin-message-section(:class='{top: modalPage > 1}', v-if='!editing')
|
.section.d-flex.justify-content-center(:class='{top: modalPage > 1}', v-if='!editing')
|
||||||
.col-12
|
.justin-section.d-flex.align-items-center
|
||||||
.justin-message.d-flex.flex-column.justify-content-center
|
.featured-label
|
||||||
.featured-label
|
span.rectangle
|
||||||
span.rectangle
|
span.text Justin
|
||||||
span.text Justin
|
span.rectangle
|
||||||
span.rectangle
|
.justin-message
|
||||||
.npc_justin_textbox
|
.corner-decoration(:style="{top: '-2px', right: '-2px'}")
|
||||||
|
.corner-decoration(:style="{top: '-2px', left: '-2px'}")
|
||||||
|
.corner-decoration(:style="{bottom: '-2px', right: '-2px'}")
|
||||||
|
.corner-decoration(:style="{bottom: '-2px', left: '-2px'}")
|
||||||
div(v-if='modalPage === 1')
|
div(v-if='modalPage === 1')
|
||||||
p(v-once) {{$t('justinIntroMessage1')}}
|
p(v-once, v-html='$t("justinIntroMessage1")')
|
||||||
p(v-once) {{$t('justinIntroMessage2')}}
|
p(v-once) {{ $t('justinIntroMessageUsername') }}
|
||||||
div(v-if='modalPage === 2')
|
div(v-if='modalPage === 2')
|
||||||
p So how would you like to look? Don’t worry, you can change this later.
|
p {{ $t('justinIntroMessageAppearance') }}
|
||||||
div(v-if='modalPage === 3')
|
div(v-if='modalPage === 3')
|
||||||
p(v-once) {{$t('justinIntroMessage3')}}
|
p(v-once) {{ $t('justinIntroMessage3') }}
|
||||||
|
.npc-justin-textbox
|
||||||
|
.section.mr-5.ml-5(v-if='modalPage === 1')
|
||||||
|
username-form(@usernameConfirmed='modalPage += 1', :avatarIntro='true')
|
||||||
|
.small.text-center(v-html="$t('usernameTOSRequirements')")
|
||||||
|
|
||||||
.section.container.footer(v-if='!editing')
|
.section.container.footer
|
||||||
.row
|
.row(v-if='!editing && !(modalPage === 1)')
|
||||||
.col-3.offset-1.text-center
|
.col-3.offset-1.text-center
|
||||||
div(v-if='modalPage > 1', @click='prev()')
|
div(v-if='modalPage > 1', @click='prev()')
|
||||||
.prev-arrow
|
.prev-arrow
|
||||||
.prev(v-once) {{$t('prev')}}
|
.prev(v-once) {{ $t('prev') }}
|
||||||
.col-4.text-center.circles
|
.col-4.text-center.circles
|
||||||
.circle(:class="{active: modalPage === 1}")
|
.circle(:class="{active: modalPage === 1}")
|
||||||
.circle(:class="{active: modalPage === 2}")
|
.circle(:class="{active: modalPage === 2}")
|
||||||
@@ -390,12 +397,9 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
|||||||
<style>
|
<style>
|
||||||
/* @TODO do not rely on avatar-modal___BV_modal_body_,
|
/* @TODO do not rely on avatar-modal___BV_modal_body_,
|
||||||
it already changed once when bootstrap-vue reached version 1 */
|
it already changed once when bootstrap-vue reached version 1 */
|
||||||
.page-2 #avatar-modal___BV_modal_body_ {
|
|
||||||
margin-top: 9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-2 .modal-content {
|
.page-2 #avatar-modal___BV_modal_body_ {
|
||||||
margin-top: 7em;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
#avatar-modal___BV_modal_body_, #avatar-modal___BV_modal_body_ {
|
#avatar-modal___BV_modal_body_, #avatar-modal___BV_modal_body_ {
|
||||||
@@ -421,6 +425,25 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.corner-decoration {
|
||||||
|
position: absolute;
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
background-color: #ffbe5d;
|
||||||
|
border: inherit;
|
||||||
|
outline: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
color: $gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: $gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
.purchase-all {
|
.purchase-all {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
@@ -444,7 +467,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
|||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
width: 190px;
|
width: 190px;
|
||||||
margin: 0 auto;
|
margin: 0 auto 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-creation-bg {
|
.user-creation-bg {
|
||||||
@@ -464,32 +487,22 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
|||||||
left: 9.2em;
|
left: 9.2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.justin-message {
|
.justin-section {
|
||||||
background-image: url('~client/assets/svg/for-css/tutorial-border.svg');
|
|
||||||
height: 144px;
|
|
||||||
width: 400px;
|
|
||||||
padding: 2em;
|
|
||||||
margin: 0 auto;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.featured-label {
|
.justin-message {
|
||||||
position: absolute;
|
border-color: #ffa623;
|
||||||
top: -1em;
|
border-style: solid;
|
||||||
|
border-width: 2px;
|
||||||
.text {
|
outline-color: #b36213;
|
||||||
min-height: auto;
|
outline-style: solid;
|
||||||
color: $white;
|
outline-width: 2px;
|
||||||
}
|
position: relative;
|
||||||
}
|
padding: 2em;
|
||||||
|
margin: 2px;
|
||||||
.npc_justin_textbox {
|
height: 100%;
|
||||||
position: absolute;
|
width: 400px;
|
||||||
right: 1em;
|
|
||||||
top: -3.6em;
|
|
||||||
width: 48px;
|
|
||||||
height: 52px;
|
|
||||||
background-image: url('~client/assets/images/justin_textbox.png');
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@@ -500,15 +513,27 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.justin-message-section {
|
.npc-justin-textbox {
|
||||||
margin-top: 4em;
|
position: absolute;
|
||||||
margin-bottom: 2em;
|
right: 1rem;
|
||||||
|
top: -3.1rem;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
background-image: url('~client/assets/images/justin_textbox.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
.justin-message-section.top {
|
.featured-label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -16em;
|
top: -1rem;
|
||||||
left: 3.5em;
|
left: 1.5rem;
|
||||||
|
border-radius: 2px;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 12px;
|
||||||
|
min-height: auto;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.circles {
|
.circles {
|
||||||
@@ -865,6 +890,7 @@ import get from 'lodash/get';
|
|||||||
import groupBy from 'lodash/groupBy';
|
import groupBy from 'lodash/groupBy';
|
||||||
import { mapState } from 'client/libs/store';
|
import { mapState } from 'client/libs/store';
|
||||||
import avatar from './avatar';
|
import avatar from './avatar';
|
||||||
|
import usernameForm from './settings/usernameForm';
|
||||||
import { getBackgroundShopSets } from '../../common/script/libs/shops';
|
import { getBackgroundShopSets } from '../../common/script/libs/shops';
|
||||||
import unlock from '../../common/script/ops/unlock';
|
import unlock from '../../common/script/ops/unlock';
|
||||||
import buy from '../../common/script/ops/buy/buy';
|
import buy from '../../common/script/ops/buy/buy';
|
||||||
@@ -1022,6 +1048,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
avatar,
|
avatar,
|
||||||
toggleSwitch,
|
toggleSwitch,
|
||||||
|
usernameForm,
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
if (this.editing) this.modalPage = 2;
|
if (this.editing) this.modalPage = 2;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
v-on:select="selectedAutocomplete",
|
v-on:select="selectedAutocomplete",
|
||||||
:textbox='textbox',
|
:textbox='textbox',
|
||||||
:coords='coords',
|
:coords='coords',
|
||||||
|
:caretPosition = 'caretPosition',
|
||||||
:chat='group.chat')
|
:chat='group.chat')
|
||||||
|
|
||||||
.row.chat-actions
|
.row.chat-actions
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
newMessage: '',
|
newMessage: '',
|
||||||
|
caretPosition: 0,
|
||||||
chat: {
|
chat: {
|
||||||
submitDisable: false,
|
submitDisable: false,
|
||||||
submitTimeout: null,
|
submitTimeout: null,
|
||||||
@@ -75,7 +77,7 @@
|
|||||||
methods: {
|
methods: {
|
||||||
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
|
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
|
||||||
getCoord (e, text) {
|
getCoord (e, text) {
|
||||||
let carPos = text.selectionEnd;
|
this.caretPosition = text.selectionEnd;
|
||||||
let div = document.createElement('div');
|
let div = document.createElement('div');
|
||||||
let span = document.createElement('span');
|
let span = document.createElement('span');
|
||||||
let copyStyle = getComputedStyle(text);
|
let copyStyle = getComputedStyle(text);
|
||||||
@@ -86,8 +88,8 @@
|
|||||||
|
|
||||||
div.style.position = 'absolute';
|
div.style.position = 'absolute';
|
||||||
document.body.appendChild(div);
|
document.body.appendChild(div);
|
||||||
div.textContent = text.value.substr(0, carPos);
|
div.textContent = text.value.substr(0, this.caretPosition);
|
||||||
span.textContent = text.value.substr(carPos) || '.';
|
span.textContent = text.value.substr(this.caretPosition) || '.';
|
||||||
div.appendChild(span);
|
div.appendChild(span);
|
||||||
this.coords = {
|
this.coords = {
|
||||||
TOP: span.offsetTop,
|
TOP: span.offsetTop,
|
||||||
@@ -194,7 +196,6 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
border: solid 1px $gray-400;
|
border: solid 1px $gray-400;
|
||||||
font-size: 16px;
|
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
line-height: 1.43;
|
line-height: 1.43;
|
||||||
color: $gray-300;
|
color: $gray-300;
|
||||||
|
|||||||
@@ -1,152 +1,199 @@
|
|||||||
<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
|
div(v-for='(invite, index) in invites')
|
||||||
thead
|
.input-group
|
||||||
tr
|
.d-flex.align-items-center.justify-content-center(v-if='index === invites.length - 1 && invite.text.length === 0')
|
||||||
th {{ $t('userId') }}
|
.svg-icon.positive-icon(v-html='icons.positiveIcon')
|
||||||
tbody
|
input.form-control(
|
||||||
tr(v-for='user in invitees')
|
type='text',
|
||||||
td
|
:placeholder='$t("emailOrUsernameInvite")',
|
||||||
input.form-control(type='text', v-model='user.uuid')
|
v-model='invite.text',
|
||||||
tr
|
v-on:keyup='expandInviteList',
|
||||||
td
|
v-on:change='checkInviteList',
|
||||||
button.btn.btn-primary.pull-right(@click='addUuid()')
|
:class='{"input-valid": invite.valid, "is-invalid input-invalid": invite.valid === false}',
|
||||||
i.glyphicon.glyphicon-plus
|
)
|
||||||
| +
|
.input-error.text-center.mt-2(v-if="invite.error") {{ invite.error }}
|
||||||
tr
|
.modal-footer.d-flex.justify-content-center
|
||||||
td
|
a.mr-3(@click='close()') {{ $t('cancel') }}
|
||||||
.col-6.col-offset-6
|
button.btn.btn-primary(@click='sendInvites()', :class='{disabled: cannotSubmit}', :disabled='cannotSubmit') {{ $t('sendInvitations') }}
|
||||||
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-error {
|
||||||
|
color: $red-50;
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: '', error: null, 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, index) => {
|
||||||
this.invitees.push({uuid: ''});
|
return invite.text.length > 0 || index === this.invites.length - 1;
|
||||||
|
});
|
||||||
|
while (this.invites.length < 2) this.invites.push(clone(INVITE_DEFAULTS));
|
||||||
|
forEach(this.invites, (value, index) => {
|
||||||
|
if (value.text.length < 1 || isEmail(value.text)) {
|
||||||
|
return this.fillErrors(index);
|
||||||
|
}
|
||||||
|
if (isUUID(value.text)) {
|
||||||
|
this.$store.dispatch('user:userLookup', {uuid: value.text})
|
||||||
|
.then(res => {
|
||||||
|
return this.fillErrors(index, res);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let searchUsername = value.text;
|
||||||
|
if (searchUsername[0] === '@') searchUsername = searchUsername.slice(1, searchUsername.length);
|
||||||
|
this.$store.dispatch('user:userLookup', {username: searchUsername})
|
||||||
|
.then(res => {
|
||||||
|
return this.fillErrors(index, res);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 250),
|
||||||
|
expandInviteList () {
|
||||||
|
if (this.invites[this.invites.length - 1].text.length > 0) this.invites.push(clone(INVITE_DEFAULTS));
|
||||||
|
},
|
||||||
|
fillErrors (index, res) {
|
||||||
|
if (!res || res.status === 200) {
|
||||||
|
this.invites[index].error = null;
|
||||||
|
if (this.invites[index].text.length < 1) return this.invites[index].valid = null;
|
||||||
|
return this.invites[index].valid = true;
|
||||||
|
}
|
||||||
|
this.invites[index].error = res.response.data.message;
|
||||||
|
return this.invites[index].valid = false;
|
||||||
|
},
|
||||||
|
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>
|
||||||
|
|||||||
@@ -355,7 +355,10 @@ export default {
|
|||||||
sendMessage (member) {
|
sendMessage (member) {
|
||||||
this.$root.$emit('habitica::new-inbox-message', {
|
this.$root.$emit('habitica::new-inbox-message', {
|
||||||
userIdToMessage: member._id,
|
userIdToMessage: member._id,
|
||||||
userName: member.profile.name,
|
displayName: member.profile.name,
|
||||||
|
username: member.auth.local.username,
|
||||||
|
backer: member.backer,
|
||||||
|
contributor: member.contributor,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async searchMembers (searchTerm = '') {
|
async searchMembers (searchTerm = '') {
|
||||||
|
|||||||
@@ -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');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,7 +19,10 @@
|
|||||||
| {{member.profile.name}}
|
| {{member.profile.name}}
|
||||||
.is-buffed(v-if="isBuffed", v-b-tooltip.hover.bottom="$t('buffed')")
|
.is-buffed(v-if="isBuffed", v-b-tooltip.hover.bottom="$t('buffed')")
|
||||||
.svg-icon(v-html="icons.buff")
|
.svg-icon(v-html="icons.buff")
|
||||||
span.small-text.character-level {{ characterLevel }}
|
.small-text.character-level
|
||||||
|
span.mr-1(v-if="member.auth && member.auth.local && member.auth.local.username") @{{ member.auth.local.username }}
|
||||||
|
span.mr-1(v-if="member.auth && member.auth.local && member.auth.local.username") •
|
||||||
|
span {{ characterLevel }}
|
||||||
.progress-container(v-b-tooltip.hover.bottom="$t('health')")
|
.progress-container(v-b-tooltip.hover.bottom="$t('health')")
|
||||||
.svg-icon(v-html="icons.health")
|
.svg-icon(v-html="icons.health")
|
||||||
.progress
|
.progress
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ div
|
|||||||
login-incentives(:data='notificationData')
|
login-incentives(:data='notificationData')
|
||||||
quest-completed
|
quest-completed
|
||||||
quest-invitation
|
quest-invitation
|
||||||
|
verify-username
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang='scss'>
|
<style lang='scss'>
|
||||||
@@ -118,6 +119,7 @@ import streak from './achievements/streak';
|
|||||||
import ultimateGear from './achievements/ultimateGear';
|
import ultimateGear from './achievements/ultimateGear';
|
||||||
import wonChallenge from './achievements/wonChallenge';
|
import wonChallenge from './achievements/wonChallenge';
|
||||||
import loginIncentives from './achievements/login-incentives';
|
import loginIncentives from './achievements/login-incentives';
|
||||||
|
import verifyUsername from './settings/verifyUsername';
|
||||||
|
|
||||||
const NOTIFICATIONS = {
|
const NOTIFICATIONS = {
|
||||||
CHALLENGE_JOINED_ACHIEVEMENT: {
|
CHALLENGE_JOINED_ACHIEVEMENT: {
|
||||||
@@ -178,6 +180,7 @@ export default {
|
|||||||
dropsEnabled,
|
dropsEnabled,
|
||||||
contributor,
|
contributor,
|
||||||
loginIncentives,
|
loginIncentives,
|
||||||
|
verifyUsername,
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
// Levels that already display modals and should not trigger generic Level Up
|
// Levels that already display modals and should not trigger generic Level Up
|
||||||
@@ -314,16 +317,12 @@ export default {
|
|||||||
this.$store.dispatch('user:fetch'),
|
this.$store.dispatch('user:fetch'),
|
||||||
this.$store.dispatch('tasks:fetchUserTasks'),
|
this.$store.dispatch('tasks:fetchUserTasks'),
|
||||||
]).then(() => {
|
]).then(() => {
|
||||||
this.debounceCheckUserAchievements();
|
|
||||||
|
|
||||||
// @TODO: This is a timeout to ensure dom is loaded
|
// @TODO: This is a timeout to ensure dom is loaded
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
this.initTour();
|
this.runForcedModals();
|
||||||
if (this.user.flags.tour.intro === this.TOUR_END || !this.user.flags.welcomed) return;
|
|
||||||
this.goto('intro', 0);
|
|
||||||
}, 2000);
|
}, 2000);
|
||||||
|
|
||||||
this.runYesterDailies();
|
this.debounceCheckUserAchievements();
|
||||||
|
|
||||||
// Do not remove the event listener as it's live for the entire app lifetime
|
// Do not remove the event listener as it's live for the entire app lifetime
|
||||||
document.addEventListener('mousemove', this.checkNextCron);
|
document.addEventListener('mousemove', this.checkNextCron);
|
||||||
@@ -339,6 +338,11 @@ export default {
|
|||||||
document.removeEventListener('keydown', this.checkNextCron);
|
document.removeEventListener('keydown', this.checkNextCron);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
runForcedModals () {
|
||||||
|
if (!this.user.flags.verifiedUsername) return this.$root.$emit('bv::show::modal', 'verify-username');
|
||||||
|
|
||||||
|
return this.runYesterDailies();
|
||||||
|
},
|
||||||
showDeathModal () {
|
showDeathModal () {
|
||||||
this.playSound('Death');
|
this.playSound('Death');
|
||||||
this.$root.$emit('bv::show::modal', 'death');
|
this.$root.$emit('bv::show::modal', 'death');
|
||||||
@@ -413,21 +417,25 @@ export default {
|
|||||||
// List of prompts for user on changes. Sounds like we may need a refactor here, but it is clean for now
|
// List of prompts for user on changes. Sounds like we may need a refactor here, but it is clean for now
|
||||||
if (!this.user.flags.welcomed) {
|
if (!this.user.flags.welcomed) {
|
||||||
this.$store.state.avatarEditorOptions.editingUser = false;
|
this.$store.state.avatarEditorOptions.editingUser = false;
|
||||||
this.$root.$emit('bv::show::modal', 'avatar-modal');
|
return this.$root.$emit('bv::show::modal', 'avatar-modal');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.user.flags.newStuff) {
|
||||||
|
return this.$root.$emit('bv::show::modal', 'new-stuff');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.user.stats.hp <= 0) {
|
if (this.user.stats.hp <= 0) {
|
||||||
this.showDeathModal();
|
return this.showDeathModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.questCompleted) {
|
if (this.questCompleted) {
|
||||||
this.$root.$emit('bv::show::modal', 'quest-completed');
|
|
||||||
this.playSound('Achievement_Unlocked');
|
this.playSound('Achievement_Unlocked');
|
||||||
|
return this.$root.$emit('bv::show::modal', 'quest-completed');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.userClassSelect) {
|
if (this.userClassSelect) {
|
||||||
this.$root.$emit('bv::show::modal', 'choose-class');
|
|
||||||
this.playSound('Achievement_Unlocked');
|
this.playSound('Achievement_Unlocked');
|
||||||
|
return this.$root.$emit('bv::show::modal', 'choose-class');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showLevelUpNotifications (newlevel) {
|
showLevelUpNotifications (newlevel) {
|
||||||
@@ -520,10 +528,6 @@ export default {
|
|||||||
async handleUserNotifications (after) {
|
async handleUserNotifications (after) {
|
||||||
if (this.$store.state.isRunningYesterdailies) return;
|
if (this.$store.state.isRunningYesterdailies) return;
|
||||||
|
|
||||||
if (this.user.flags.newStuff) {
|
|
||||||
this.$root.$emit('bv::show::modal', 'new-stuff');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!after || after.length === 0 || !Array.isArray(after)) return;
|
if (!after || after.length === 0 || !Array.isArray(after)) return;
|
||||||
|
|
||||||
let notificationsToRead = [];
|
let notificationsToRead = [];
|
||||||
|
|||||||
@@ -130,8 +130,10 @@
|
|||||||
h5 {{ $t('changeDisplayName') }}
|
h5 {{ $t('changeDisplayName') }}
|
||||||
.form(name='changeDisplayName', novalidate)
|
.form(name='changeDisplayName', novalidate)
|
||||||
.form-group
|
.form-group
|
||||||
input#changeDisplayname.form-control(type='text', :placeholder="$t('newDisplayName')", v-model='temporaryDisplayName')
|
input#changeDisplayname.form-control(type='text', :placeholder="$t('newDisplayName')", v-model='temporaryDisplayName', :class='{"is-invalid input-invalid": displayNameInvalid}')
|
||||||
button.btn.btn-primary(type='submit', @click='changeDisplayName(temporaryDisplayName)') {{ $t('submit') }}
|
.mb-3(v-if="displayNameIssues.length > 0")
|
||||||
|
.input-error(v-for="issue in displayNameIssues") {{ issue }}
|
||||||
|
button.btn.btn-primary(type='submit', @click='changeDisplayName(temporaryDisplayName)', :disabled='displayNameCannotSubmit') {{ $t('submit') }}
|
||||||
|
|
||||||
h5 {{ $t('changeUsername') }}
|
h5 {{ $t('changeUsername') }}
|
||||||
.form(name='changeUsername', novalidate)
|
.form(name='changeUsername', novalidate)
|
||||||
@@ -252,6 +254,7 @@ export default {
|
|||||||
password: '',
|
password: '',
|
||||||
confirmPassword: '',
|
confirmPassword: '',
|
||||||
},
|
},
|
||||||
|
displayNameIssues: [],
|
||||||
usernameIssues: [],
|
usernameIssues: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -312,6 +315,18 @@ export default {
|
|||||||
verifiedUsername () {
|
verifiedUsername () {
|
||||||
return this.user.flags.verifiedUsername;
|
return this.user.flags.verifiedUsername;
|
||||||
},
|
},
|
||||||
|
displayNameInvalid () {
|
||||||
|
if (this.temporaryDisplayName.length <= 1) return false;
|
||||||
|
return !this.displayNameValid;
|
||||||
|
},
|
||||||
|
displayNameValid () {
|
||||||
|
if (this.temporaryDisplayName.length <= 1) return false;
|
||||||
|
return this.displayNameIssues.length === 0;
|
||||||
|
},
|
||||||
|
displayNameCannotSubmit () {
|
||||||
|
if (this.temporaryDisplayName.length <= 1) return true;
|
||||||
|
return !this.displayNameValid;
|
||||||
|
},
|
||||||
usernameValid () {
|
usernameValid () {
|
||||||
if (this.usernameUpdates.username.length <= 1) return false;
|
if (this.usernameUpdates.username.length <= 1) return false;
|
||||||
return this.usernameIssues.length === 0;
|
return this.usernameIssues.length === 0;
|
||||||
@@ -332,10 +347,30 @@ export default {
|
|||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
},
|
},
|
||||||
|
temporaryDisplayName: {
|
||||||
|
handler () {
|
||||||
|
this.validateDisplayName(this.temporaryDisplayName);
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
// eslint-disable-next-line func-names
|
validateDisplayName: debounce(function checkName (displayName) {
|
||||||
validateUsername: debounce(function (username) {
|
if (displayName.length <= 1 || displayName === this.user.profile.name) {
|
||||||
|
this.displayNameIssues = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$store.dispatch('auth:verifyDisplayName', {
|
||||||
|
displayName,
|
||||||
|
}).then(res => {
|
||||||
|
if (res.issues !== undefined) {
|
||||||
|
this.displayNameIssues = res.issues;
|
||||||
|
} else {
|
||||||
|
this.displayNameIssues = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 500),
|
||||||
|
validateUsername: debounce(function checkName (username) {
|
||||||
if (username.length <= 1 || username === this.user.auth.local.username) {
|
if (username.length <= 1 || username === this.user.auth.local.username) {
|
||||||
this.usernameIssues = [];
|
this.usernameIssues = [];
|
||||||
return;
|
return;
|
||||||
|
|||||||
221
website/client/components/settings/usernameForm.vue
Normal file
221
website/client/components/settings/usernameForm.vue
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
div
|
||||||
|
.form-group
|
||||||
|
.d-flex.align-items-center
|
||||||
|
label.mr-3(for='displayName') {{ $t('displayName') }}
|
||||||
|
.flex-grow-1
|
||||||
|
input#displayName.form-control(
|
||||||
|
type='text',
|
||||||
|
:placeholder="$t('newDisplayName')",
|
||||||
|
v-model='temporaryDisplayName',
|
||||||
|
@blur='restoreEmptyDisplayName()',
|
||||||
|
:class='{"is-invalid input-invalid": displayNameInvalid, "input-valid": displayNameValid, "text-darker": temporaryDisplayName.length > 0}')
|
||||||
|
.mb-3(v-if="displayNameIssues.length > 0")
|
||||||
|
.input-error.text-center(v-for="issue in displayNameIssues") {{ issue }}
|
||||||
|
.form-group
|
||||||
|
.d-flex.align-items-center
|
||||||
|
label.mr-3(for='username') {{ $t('username') }}
|
||||||
|
.flex-grow-1
|
||||||
|
.input-group-prepend.input-group-text @
|
||||||
|
input#username.form-control(
|
||||||
|
type='text',
|
||||||
|
:placeholder="$t('newUsername')",
|
||||||
|
v-model='temporaryUsername',
|
||||||
|
@blur='restoreEmptyUsername()',
|
||||||
|
:class='{"is-invalid input-invalid": usernameInvalid, "input-valid": usernameValid, "text-darker": temporaryUsername.length > 0}')
|
||||||
|
.mb-3(v-if="usernameIssues.length > 0")
|
||||||
|
.input-error.text-center(v-for="issue in usernameIssues") {{ issue }}
|
||||||
|
.small.text-center.mb-3(v-if='!avatarIntro') {{ $t('usernameLimitations') }}
|
||||||
|
.row.justify-content-center
|
||||||
|
button.btn.btn-primary(type='submit', @click='submitNames()', :class='{disabled: usernameCannotSubmit}', :disabled='usernameCannotSubmit') {{ $t(avatarIntro ? 'getStarted' : 'saveAndConfirm') }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~client/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: 0.25rem auto 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-3 {
|
||||||
|
padding-right: 0rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
height: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
background-color: $gray-700;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: solid 1px $gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-error {
|
||||||
|
color: $red-50;
|
||||||
|
font-size: 90%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-prepend {
|
||||||
|
margin-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group-text {
|
||||||
|
background-color: $white;
|
||||||
|
border: 0px;
|
||||||
|
border-radius: 0px;
|
||||||
|
color: $gray-300;
|
||||||
|
padding: 0rem 0.1rem 0rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
color: $gray-100;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
min-width: 90px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
color: $gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-darker {
|
||||||
|
color: $gray-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
#username {
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import axios from 'axios';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
import { mapState } from 'client/libs/store';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
user: 'user.data',
|
||||||
|
}),
|
||||||
|
displayNameInvalid () {
|
||||||
|
if (this.temporaryDisplayName.length < 1) return false;
|
||||||
|
return !this.displayNameValid;
|
||||||
|
},
|
||||||
|
displayNameValid () {
|
||||||
|
if (this.temporaryDisplayName.length < 1) return false;
|
||||||
|
return this.displayNameIssues.length === 0;
|
||||||
|
},
|
||||||
|
usernameCannotSubmit () {
|
||||||
|
if (this.temporaryUsername.length < 1) return true;
|
||||||
|
return !this.usernameValid || !this.displayNameValid;
|
||||||
|
},
|
||||||
|
usernameInvalid () {
|
||||||
|
if (this.temporaryUsername.length < 1) return false;
|
||||||
|
return !this.usernameValid;
|
||||||
|
},
|
||||||
|
usernameValid () {
|
||||||
|
if (this.temporaryUsername.length < 1) return false;
|
||||||
|
return this.usernameIssues.length === 0;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
displayNameIssues: [],
|
||||||
|
temporaryDisplayName: '',
|
||||||
|
temporaryUsername: '',
|
||||||
|
usernameIssues: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async close () {
|
||||||
|
this.$root.$emit('habitica::resync-requested');
|
||||||
|
await this.$store.dispatch('user:fetch', {forceLoad: true});
|
||||||
|
this.$root.$emit('habitica::resync-completed');
|
||||||
|
if (this.avatarIntro) {
|
||||||
|
this.$emit('usernameConfirmed');
|
||||||
|
} else {
|
||||||
|
this.$root.$emit('bv::hide::modal', 'verify-username');
|
||||||
|
this.$router.go(0);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restoreEmptyDisplayName () {
|
||||||
|
if (this.temporaryDisplayName.length < 1) {
|
||||||
|
this.temporaryDisplayName = this.user.profile.name;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restoreEmptyUsername () {
|
||||||
|
if (this.temporaryUsername.length < 1) {
|
||||||
|
this.temporaryUsername = this.user.auth.local.username;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submitNames () {
|
||||||
|
if (this.temporaryDisplayName !== this.user.profile.name) {
|
||||||
|
await axios.put('/api/v4/user/', {'profile.name': this.temporaryDisplayName});
|
||||||
|
}
|
||||||
|
await axios.put('/api/v4/user/auth/update-username', {username: this.temporaryUsername});
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
validateDisplayName: debounce(function checkName (displayName) {
|
||||||
|
if (displayName.length <= 1 || displayName === this.user.profile.name) {
|
||||||
|
this.displayNameIssues = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$store.dispatch('auth:verifyDisplayName', {
|
||||||
|
displayName,
|
||||||
|
}).then(res => {
|
||||||
|
if (res.issues !== undefined) {
|
||||||
|
this.displayNameIssues = res.issues;
|
||||||
|
} else {
|
||||||
|
this.displayNameIssues = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 500),
|
||||||
|
validateUsername: debounce(function checkName (username) {
|
||||||
|
if (username.length <= 1 || username === this.user.auth.local.username) {
|
||||||
|
this.usernameIssues = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$store.dispatch('auth:verifyUsername', {
|
||||||
|
username,
|
||||||
|
}).then(res => {
|
||||||
|
if (res.issues !== undefined) {
|
||||||
|
this.usernameIssues = res.issues;
|
||||||
|
} else {
|
||||||
|
this.usernameIssues = [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 500),
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.temporaryDisplayName = this.user.profile.name;
|
||||||
|
this.temporaryUsername = this.user.auth.local.username;
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
avatarIntro: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
temporaryDisplayName: {
|
||||||
|
handler () {
|
||||||
|
this.validateDisplayName(this.temporaryDisplayName);
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
temporaryUsername: {
|
||||||
|
handler () {
|
||||||
|
this.validateUsername(this.temporaryUsername);
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
84
website/client/components/settings/verifyUsername.vue
Normal file
84
website/client/components/settings/verifyUsername.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<template lang="pug">
|
||||||
|
b-modal#verify-username(
|
||||||
|
size="m",
|
||||||
|
:no-close-on-backdrop="true",
|
||||||
|
:no-close-on-esc="true",
|
||||||
|
:hide-header="true",
|
||||||
|
:hide-footer="true",
|
||||||
|
@hide="$emit('hide')",
|
||||||
|
).d-flex
|
||||||
|
div.nametag-header(v-html='icons.helloNametag')
|
||||||
|
h2.text-center {{ $t('usernameTime') }}
|
||||||
|
p.text-center(v-html="$t('usernameInfo')")
|
||||||
|
username-form
|
||||||
|
.scene_veteran_pets.center-block
|
||||||
|
.small.text-center.mb-3 {{ $t('verifyUsernameVeteranPet') }}
|
||||||
|
.small.text-center.tos-footer(v-html="$t('usernameTOSRequirements')")
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#verify-username___BV_modal_outer_ {
|
||||||
|
.modal-content {
|
||||||
|
height: 100%;
|
||||||
|
width: 566px;
|
||||||
|
padding-left: 2rem;
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~client/assets/scss/colors.scss';
|
||||||
|
|
||||||
|
.center-block {
|
||||||
|
margin: 0 auto 1em auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
color: $purple-200;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nametag-header {
|
||||||
|
background-color: $gray-700;
|
||||||
|
border-radius: 0.3rem 0.3rem 0rem 0rem;
|
||||||
|
margin-left: -3rem;
|
||||||
|
margin-right: -3rem;
|
||||||
|
padding: 1rem 9rem 1rem 9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: $gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
color: $gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tos-footer {
|
||||||
|
background-color: $gray-700;
|
||||||
|
border-radius: 0rem 0rem 0.3rem 0.3rem;
|
||||||
|
margin-left: -3rem;
|
||||||
|
margin-right: -3rem;
|
||||||
|
margin-top: -0.1rem;
|
||||||
|
padding: 1rem 4rem 1rem 4rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import helloNametag from 'assets/svg/hello-habitican.svg';
|
||||||
|
import usernameForm from './usernameForm';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
usernameForm,
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
icons: Object.freeze({
|
||||||
|
helloNametag,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
width: 350px;
|
width: 350px;
|
||||||
z-index: 1070; // 1070 is above modal backgrounds
|
z-index: 1400; // 1400 is above modal backgrounds
|
||||||
|
|
||||||
&-top-pos {
|
&-top-pos {
|
||||||
&-normal {
|
&-normal {
|
||||||
|
|||||||
@@ -560,6 +560,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import hello from 'hellojs';
|
import hello from 'hellojs';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
import isEmail from 'validator/lib/isEmail';
|
||||||
import googlePlay from 'assets/images/home/google-play-badge.svg';
|
import googlePlay from 'assets/images/home/google-play-badge.svg';
|
||||||
import iosAppStore from 'assets/images/home/ios-app-store.svg';
|
import iosAppStore from 'assets/images/home/ios-app-store.svg';
|
||||||
import iphones from 'assets/images/home/iphones.svg';
|
import iphones from 'assets/images/home/iphones.svg';
|
||||||
@@ -626,18 +627,18 @@
|
|||||||
computed: {
|
computed: {
|
||||||
emailValid () {
|
emailValid () {
|
||||||
if (this.email.length <= 3) return false;
|
if (this.email.length <= 3) return false;
|
||||||
return this.validateEmail(this.email);
|
return isEmail(this.email);
|
||||||
},
|
},
|
||||||
emailInvalid () {
|
emailInvalid () {
|
||||||
if (this.email.length <= 3) return false;
|
if (this.email.length <= 3) return false;
|
||||||
return !this.validateEmail(this.email);
|
return !isEmail(this.email);
|
||||||
},
|
},
|
||||||
usernameValid () {
|
usernameValid () {
|
||||||
if (this.username.length <= 3) return false;
|
if (this.username.length < 1) return false;
|
||||||
return this.usernameIssues.length === 0;
|
return this.usernameIssues.length === 0;
|
||||||
},
|
},
|
||||||
usernameInvalid () {
|
usernameInvalid () {
|
||||||
if (this.username.length <= 3) return false;
|
if (this.username.length < 1) return false;
|
||||||
return !this.usernameValid;
|
return !this.usernameValid;
|
||||||
},
|
},
|
||||||
passwordConfirmValid () {
|
passwordConfirmValid () {
|
||||||
@@ -655,13 +656,9 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
validateEmail (email) {
|
|
||||||
let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
||||||
return re.test(email);
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line func-names
|
// eslint-disable-next-line func-names
|
||||||
validateUsername: debounce(function (username) {
|
validateUsername: debounce(function (username) {
|
||||||
if (username.length <= 3) {
|
if (username.length < 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.$store.dispatch('auth:verifyUsername', {
|
this.$store.dispatch('auth:verifyUsername', {
|
||||||
|
|||||||
@@ -30,8 +30,11 @@
|
|||||||
.conversation(v-for='conversation in filtersConversations', @click='selectConversation(conversation.key)',
|
.conversation(v-for='conversation in filtersConversations', @click='selectConversation(conversation.key)',
|
||||||
:class="{active: selectedConversation.key === conversation.key}")
|
:class="{active: selectedConversation.key === conversation.key}")
|
||||||
div
|
div
|
||||||
span(:class="userLevelStyle(conversation)") {{conversation.name}}
|
h3(:class="userLevelStyle(conversation)") {{ conversation.name }}
|
||||||
span.timeago {{conversation.date | timeAgo}}
|
.svg-icon(v-html="tierIcon(conversation)")
|
||||||
|
.time
|
||||||
|
span.mr-1(v-if='conversation.username') @{{ conversation.username }} •
|
||||||
|
span {{ conversation.date | timeAgo }}
|
||||||
div {{conversation.lastMessageText ? conversation.lastMessageText.substring(0, 30) : ''}}
|
div {{conversation.lastMessageText ? conversation.lastMessageText.substring(0, 30) : ''}}
|
||||||
.col-8.messages.d-flex.flex-column.justify-content-between
|
.col-8.messages.d-flex.flex-column.justify-content-between
|
||||||
.empty-messages.text-center(v-if='!selectedConversation.key')
|
.empty-messages.text-center(v-if='!selectedConversation.key')
|
||||||
@@ -63,6 +66,7 @@
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import '~client/assets/scss/colors.scss';
|
@import '~client/assets/scss/colors.scss';
|
||||||
|
@import '~client/assets/scss/tiers.scss';
|
||||||
|
|
||||||
.header-wrap {
|
.header-wrap {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
@@ -73,6 +77,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0rem;
|
||||||
|
|
||||||
|
.svg-icon {
|
||||||
|
width: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.envelope {
|
.envelope {
|
||||||
color: $gray-400 !important;
|
color: $gray-400 !important;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -180,11 +194,6 @@
|
|||||||
.conversation {
|
.conversation {
|
||||||
padding: 1.5em;
|
padding: 1.5em;
|
||||||
background: $white;
|
background: $white;
|
||||||
height: 80px;
|
|
||||||
|
|
||||||
.timeago {
|
|
||||||
margin-left: 1em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation.active {
|
.conversation.active {
|
||||||
@@ -194,6 +203,12 @@
|
|||||||
.conversation:hover {
|
.conversation:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
font-size: 12px;
|
||||||
|
color: $gray-200;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -207,9 +222,19 @@ import styleHelper from 'client/mixins/styleHelper';
|
|||||||
import toggleSwitch from 'client/components/ui/toggleSwitch';
|
import toggleSwitch from 'client/components/ui/toggleSwitch';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
import messageIcon from 'assets/svg/message.svg';
|
|
||||||
import chatMessages from '../chat/chatMessages';
|
import chatMessages from '../chat/chatMessages';
|
||||||
|
import messageIcon from 'assets/svg/message.svg';
|
||||||
import svgClose from 'assets/svg/close.svg';
|
import svgClose from 'assets/svg/close.svg';
|
||||||
|
import tier1 from 'assets/svg/tier-1.svg';
|
||||||
|
import tier2 from 'assets/svg/tier-2.svg';
|
||||||
|
import tier3 from 'assets/svg/tier-3.svg';
|
||||||
|
import tier4 from 'assets/svg/tier-4.svg';
|
||||||
|
import tier5 from 'assets/svg/tier-5.svg';
|
||||||
|
import tier6 from 'assets/svg/tier-6.svg';
|
||||||
|
import tier7 from 'assets/svg/tier-7.svg';
|
||||||
|
import tier8 from 'assets/svg/tier-mod.svg';
|
||||||
|
import tier9 from 'assets/svg/tier-staff.svg';
|
||||||
|
import tierNPC from 'assets/svg/tier-npc.svg';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [styleHelper],
|
mixins: [styleHelper],
|
||||||
@@ -236,8 +261,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.initiatedConversation = {
|
this.initiatedConversation = {
|
||||||
user: data.userName,
|
|
||||||
uuid: data.userIdToMessage,
|
uuid: data.userIdToMessage,
|
||||||
|
user: data.displayName,
|
||||||
|
username: data.username,
|
||||||
|
backer: data.backer,
|
||||||
|
contributor: data.contributor,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.selectConversation(data.userIdToMessage);
|
this.selectConversation(data.userIdToMessage);
|
||||||
@@ -252,6 +280,16 @@ export default {
|
|||||||
icons: Object.freeze({
|
icons: Object.freeze({
|
||||||
messageIcon,
|
messageIcon,
|
||||||
svgClose,
|
svgClose,
|
||||||
|
tier1,
|
||||||
|
tier2,
|
||||||
|
tier3,
|
||||||
|
tier4,
|
||||||
|
tier5,
|
||||||
|
tier6,
|
||||||
|
tier7,
|
||||||
|
tier8,
|
||||||
|
tier9,
|
||||||
|
tierNPC,
|
||||||
}),
|
}),
|
||||||
displayCreate: true,
|
displayCreate: true,
|
||||||
selectedConversation: {},
|
selectedConversation: {},
|
||||||
@@ -276,10 +314,11 @@ export default {
|
|||||||
// Add placeholder for new conversations
|
// Add placeholder for new conversations
|
||||||
if (this.initiatedConversation && this.initiatedConversation.uuid) {
|
if (this.initiatedConversation && this.initiatedConversation.uuid) {
|
||||||
inboxGroup[this.initiatedConversation.uuid] = [{
|
inboxGroup[this.initiatedConversation.uuid] = [{
|
||||||
|
uuid: this.initiatedConversation.uuid,
|
||||||
|
user: this.initiatedConversation.user,
|
||||||
|
username: this.initiatedConversation.username,
|
||||||
id: '',
|
id: '',
|
||||||
text: '',
|
text: '',
|
||||||
user: this.initiatedConversation.user,
|
|
||||||
uuid: this.initiatedConversation.uuid,
|
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
@@ -296,8 +335,12 @@ export default {
|
|||||||
if (newChat.sent) {
|
if (newChat.sent) {
|
||||||
newChat.toUUID = newChat.uuid;
|
newChat.toUUID = newChat.uuid;
|
||||||
newChat.toUser = newChat.user;
|
newChat.toUser = newChat.user;
|
||||||
|
newChat.toUserName = newChat.username;
|
||||||
|
newChat.toUserContributor = newChat.contributor;
|
||||||
|
newChat.toUserBacker = newChat.backer;
|
||||||
newChat.uuid = this.user._id;
|
newChat.uuid = this.user._id;
|
||||||
newChat.user = this.user.profile.name;
|
newChat.user = this.user.profile.name;
|
||||||
|
newChat.username = this.user.auth.local.username;
|
||||||
newChat.contributor = this.user.contributor;
|
newChat.contributor = this.user.contributor;
|
||||||
newChat.backer = this.user.backer;
|
newChat.backer = this.user.backer;
|
||||||
}
|
}
|
||||||
@@ -309,11 +352,12 @@ export default {
|
|||||||
if (!recentMessage.text) newChatModels.splice(newChatModels.length - 1, 1);
|
if (!recentMessage.text) newChatModels.splice(newChatModels.length - 1, 1);
|
||||||
|
|
||||||
const convoModel = {
|
const convoModel = {
|
||||||
name: recentMessage.toUser ? recentMessage.toUser : recentMessage.user, // Handles case where from user sent the only message or the to user sent the only message
|
|
||||||
key: recentMessage.toUUID ? recentMessage.toUUID : recentMessage.uuid,
|
key: recentMessage.toUUID ? recentMessage.toUUID : recentMessage.uuid,
|
||||||
messages: newChatModels,
|
name: recentMessage.toUser ? recentMessage.toUser : recentMessage.user, // Handles case where from user sent the only message or the to user sent the only message
|
||||||
lastMessageText: recentMessage.text,
|
username: !recentMessage.text ? recentMessage.username : recentMessage.toUserName,
|
||||||
date: recentMessage.timestamp,
|
date: recentMessage.timestamp,
|
||||||
|
lastMessageText: recentMessage.text,
|
||||||
|
messages: newChatModels,
|
||||||
};
|
};
|
||||||
|
|
||||||
convos.push(convoModel);
|
convos.push(convoModel);
|
||||||
@@ -382,8 +426,11 @@ export default {
|
|||||||
const messageIndex = this.messages.findIndex(msg => msg.id === message.id);
|
const messageIndex = this.messages.findIndex(msg => msg.id === message.id);
|
||||||
if (messageIndex !== -1) this.messages.splice(messageIndex, 1);
|
if (messageIndex !== -1) this.messages.splice(messageIndex, 1);
|
||||||
if (this.selectedConversationMessages.length === 0) this.initiatedConversation = {
|
if (this.selectedConversationMessages.length === 0) this.initiatedConversation = {
|
||||||
user: this.selectedConversation.name,
|
|
||||||
uuid: this.selectedConversation.key,
|
uuid: this.selectedConversation.key,
|
||||||
|
user: this.selectedConversation.name,
|
||||||
|
username: this.selectedConversation.username,
|
||||||
|
backer: this.selectedConversation.backer,
|
||||||
|
contributor: this.selectedConversation.contributor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
toggleClick () {
|
toggleClick () {
|
||||||
@@ -413,6 +460,7 @@ export default {
|
|||||||
text: this.newMessage,
|
text: this.newMessage,
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
user: this.selectedConversation.name,
|
user: this.selectedConversation.name,
|
||||||
|
username: this.selectedConversation.username,
|
||||||
uuid: this.selectedConversation.key,
|
uuid: this.selectedConversation.key,
|
||||||
contributor: this.user.contributor,
|
contributor: this.user.contributor,
|
||||||
});
|
});
|
||||||
@@ -444,6 +492,14 @@ export default {
|
|||||||
close () {
|
close () {
|
||||||
this.$root.$emit('bv::hide::modal', 'inbox-modal');
|
this.$root.$emit('bv::hide::modal', 'inbox-modal');
|
||||||
},
|
},
|
||||||
|
tierIcon (message) {
|
||||||
|
const isNPC = Boolean(message.backer && message.backer.npc);
|
||||||
|
if (isNPC) {
|
||||||
|
return this.icons.tierNPC;
|
||||||
|
}
|
||||||
|
if (!message.contributor) return;
|
||||||
|
return this.icons[`tier${message.contributor.level}`];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -40,11 +40,12 @@ div
|
|||||||
#userProfile.standard-page(v-show='selectedPage === "profile"', v-if='user.profile')
|
#userProfile.standard-page(v-show='selectedPage === "profile"', v-if='user.profile')
|
||||||
.row
|
.row
|
||||||
.col-12.col-md-8
|
.col-12.col-md-8
|
||||||
.header
|
.header.mb-3
|
||||||
h1 {{user.profile.name}}
|
h1 {{user.profile.name}}
|
||||||
h4
|
div
|
||||||
strong {{ $t('userId') }}:
|
.name(v-if='user.auth && user.auth.local && user.auth.local.username') @{{ user.auth.local.username }}
|
||||||
| {{user._id}}
|
div
|
||||||
|
.name {{ user._id }}
|
||||||
.col-12.col-md-4
|
.col-12.col-md-4
|
||||||
button.btn.btn-secondary(v-if='user._id === userLoggedIn._id', @click='editing = !editing') {{ $t('edit') }}
|
button.btn.btn-secondary(v-if='user._id === userLoggedIn._id', @click='editing = !editing') {{ $t('edit') }}
|
||||||
.row(v-if='!editing')
|
.row(v-if='!editing')
|
||||||
@@ -146,7 +147,7 @@ div
|
|||||||
#profile {
|
#profile {
|
||||||
.member-details {
|
.member-details {
|
||||||
.character-name, small, .small-text {
|
.character-name, small, .small-text {
|
||||||
color: #878190
|
color: #878190;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,7 +194,7 @@ div
|
|||||||
.gift-icon {
|
.gift-icon {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
color: #686274;
|
color: $gray-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gift-icon {
|
.gift-icon {
|
||||||
@@ -201,13 +202,13 @@ div
|
|||||||
}
|
}
|
||||||
|
|
||||||
.remove-icon {
|
.remove-icon {
|
||||||
width:16px;
|
width: 16px;
|
||||||
color: #686274;
|
color: $gray-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.positive-icon {
|
.positive-icon {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
color: #686274;
|
color: $gray-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo img {
|
.photo img {
|
||||||
@@ -216,11 +217,12 @@ div
|
|||||||
|
|
||||||
.header {
|
.header {
|
||||||
h1 {
|
h1 {
|
||||||
color: #4f2a93;
|
color: $purple-200;
|
||||||
|
margin-bottom: 0rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
color: #686274;
|
color: $gray-100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +243,11 @@ div
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
color: $gray-200;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
#achievements {
|
#achievements {
|
||||||
.box {
|
.box {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -463,7 +470,10 @@ export default {
|
|||||||
sendMessage () {
|
sendMessage () {
|
||||||
this.$root.$emit('habitica::new-inbox-message', {
|
this.$root.$emit('habitica::new-inbox-message', {
|
||||||
userIdToMessage: this.user._id,
|
userIdToMessage: this.user._id,
|
||||||
userName: this.user.profile.name,
|
displayName: this.user.profile.name,
|
||||||
|
username: this.user.auth.local.username,
|
||||||
|
backer: this.user.backer,
|
||||||
|
contributor: this.user.contributor,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getProgressDisplay () {
|
getProgressDisplay () {
|
||||||
|
|||||||
@@ -55,6 +55,15 @@ export async function verifyUsername (store, params) {
|
|||||||
return result.data.data;
|
return result.data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function verifyDisplayName (store, params) {
|
||||||
|
let url = '/api/v4/user/auth/verify-display-name';
|
||||||
|
let result = await axios.post(url, {
|
||||||
|
displayName: params.displayName,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function socialAuth (store, params) {
|
export async function socialAuth (store, params) {
|
||||||
let url = '/api/v4/user/auth/social';
|
let url = '/api/v4/user/auth/social';
|
||||||
let result = await axios.post(url, {
|
let result = await axios.post(url, {
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ export async function invite (store, payload) {
|
|||||||
let response = await axios.post(`/api/v4/groups/${payload.groupId}/invite`, {
|
let response = await axios.post(`/api/v4/groups/${payload.groupId}/invite`, {
|
||||||
uuids: payload.invitationDetails.uuids,
|
uuids: payload.invitationDetails.uuids,
|
||||||
emails: payload.invitationDetails.emails,
|
emails: payload.invitationDetails.emails,
|
||||||
|
usernames: payload.invitationDetails.usernames,
|
||||||
});
|
});
|
||||||
|
|
||||||
// @TODO: find guild and add invites
|
// @TODO: find guild and add invites
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"noPhoto": "This Habitican hasn't added a photo.",
|
"noPhoto": "This Habitican hasn't added a photo.",
|
||||||
"other": "Other",
|
"other": "Other",
|
||||||
"fullName": "Full Name",
|
"fullName": "Full Name",
|
||||||
"displayName": "Display Name",
|
"displayName": "Display name",
|
||||||
"changeDisplayName": "Change Display Name",
|
"changeDisplayName": "Change Display Name",
|
||||||
"newDisplayName": "New Display Name",
|
"newDisplayName": "New Display Name",
|
||||||
"displayPhoto": "Photo",
|
"displayPhoto": "Photo",
|
||||||
|
|||||||
@@ -271,15 +271,9 @@
|
|||||||
"emailTaken": "Email address is already used in an account.",
|
"emailTaken": "Email address is already used in an account.",
|
||||||
"newEmailRequired": "Missing new email address.",
|
"newEmailRequired": "Missing new email address.",
|
||||||
"usernameTime": "It's time to set your username!",
|
"usernameTime": "It's time to set your username!",
|
||||||
"usernameInfo": "Your display name hasn't changed, but your old login name will now become your public username. This username will be used for invitations, @mentions in chat, and messaging.<br><br>If you'd like to learn more about this change, visit the wiki's <a href='http://habitica.wikia.com/wiki/Player_Names' target='_blank'>Player Names</a> page.",
|
"usernameInfo": "Login names are now unique usernames that will be visible beside your display name and used for invitations, chat @mentions, and messaging.<br><br>If you'd like to learn more about this change, <a href='http://habitica.wikia.com/wiki/Player_Names' target='_blank'>visit our wiki</a>.",
|
||||||
"usernameTOSRequirements": "Usernames must conform to our Terms of Service and Community Guidelines. If you didn’t previously set a login name, your username was auto-generated.",
|
"usernameTOSRequirements": "Usernames must conform to our <a href='/static/terms' target='_blank'>Terms of Service</a> and <a href='/static/community-guidelines' target='_blank'>Community Guidelines</a>. If you didn’t previously set a login name, your username was auto-generated.",
|
||||||
"usernameTaken": "Username already taken.",
|
"usernameTaken": "Username already taken.",
|
||||||
"usernameWrongLength": "Username must be between 1 and 20 characters long.",
|
|
||||||
"displayNameWrongLength": "Display names must be between 1 and 30 characters long.",
|
|
||||||
"usernameBadCharacters": "Usernames can only contain letters a to z, numbers 0 to 9, hyphens, or underscores.",
|
|
||||||
"nameBadWords": "Names cannot include any inappropriate words.",
|
|
||||||
"confirmUsername": "Confirm Username",
|
|
||||||
"usernameConfirmed": "Username Confirmed",
|
|
||||||
"passwordConfirmationMatch": "Password confirmation doesn't match password.",
|
"passwordConfirmationMatch": "Password confirmation doesn't match password.",
|
||||||
"invalidLoginCredentials": "Incorrect username and/or email and/or password.",
|
"invalidLoginCredentials": "Incorrect username and/or email and/or password.",
|
||||||
"passwordResetPage": "Reset Password",
|
"passwordResetPage": "Reset Password",
|
||||||
@@ -335,7 +329,7 @@
|
|||||||
"joinToday": "Join Habitica Today",
|
"joinToday": "Join Habitica Today",
|
||||||
"featuredIn": "Featured in",
|
"featuredIn": "Featured in",
|
||||||
"signup": "Sign Up",
|
"signup": "Sign Up",
|
||||||
"getStarted": "Get Started",
|
"getStarted": "Get Started!",
|
||||||
"mobileApps": "Mobile Apps",
|
"mobileApps": "Mobile Apps",
|
||||||
"learnMore": "Learn More"
|
"learnMore": "Learn More"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,6 +254,7 @@
|
|||||||
"resetFilters": "Clear all filters",
|
"resetFilters": "Clear all filters",
|
||||||
"applyFilters": "Apply Filters",
|
"applyFilters": "Apply Filters",
|
||||||
|
|
||||||
|
"wantToWorkOn": "I want to work on:",
|
||||||
"categories": "Categories",
|
"categories": "Categories",
|
||||||
"habiticaOfficial": "Habitica Official",
|
"habiticaOfficial": "Habitica Official",
|
||||||
"animals": "Animals",
|
"animals": "Animals",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -229,7 +229,7 @@
|
|||||||
"memberCannotRemoveYourself": "You cannot remove yourself!",
|
"memberCannotRemoveYourself": "You cannot remove yourself!",
|
||||||
"groupMemberNotFound": "User not found among group's members",
|
"groupMemberNotFound": "User not found among group's members",
|
||||||
"mustBeGroupMember": "Must be member of the group.",
|
"mustBeGroupMember": "Must be member of the group.",
|
||||||
"canOnlyInviteEmailUuid": "Can only invite using uuids or emails.",
|
"canOnlyInviteEmailUuid": "Can only invite using user IDs, emails, or usernames.",
|
||||||
"inviteMissingEmail": "Missing email address in invite.",
|
"inviteMissingEmail": "Missing email address in invite.",
|
||||||
"inviteMissingUuid": "Missing user id in invite",
|
"inviteMissingUuid": "Missing user id in invite",
|
||||||
"inviteMustNotBeEmpty": "Invite must not be empty.",
|
"inviteMustNotBeEmpty": "Invite must not be empty.",
|
||||||
@@ -241,9 +241,11 @@
|
|||||||
"userAlreadyPendingInvitation": "UserID: <%= userId %>, User \"<%= username %>\" already pending invitation.",
|
"userAlreadyPendingInvitation": "UserID: <%= userId %>, User \"<%= username %>\" already pending invitation.",
|
||||||
"userAlreadyInAParty": "UserID: <%= userId %>, User \"<%= username %>\" already in a party. ",
|
"userAlreadyInAParty": "UserID: <%= userId %>, User \"<%= username %>\" already in a party. ",
|
||||||
"userWithIDNotFound": "User with id \"<%= userId %>\" not found.",
|
"userWithIDNotFound": "User with id \"<%= userId %>\" not found.",
|
||||||
|
"userWithUsernameNotFound": "User with username \"<%= username %>\" not found.",
|
||||||
"userHasNoLocalRegistration": "User does not have a local registration (username, email, password).",
|
"userHasNoLocalRegistration": "User does not have a local registration (username, email, password).",
|
||||||
"uuidsMustBeAnArray": "User ID invites must be an array.",
|
"uuidsMustBeAnArray": "User ID invites must be an array.",
|
||||||
"emailsMustBeAnArray": "Email address invites must be an array.",
|
"emailsMustBeAnArray": "Email address invites must be an array.",
|
||||||
|
"usernamesMustBeAnArray": "Username invites must be an array.",
|
||||||
"canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
|
"canOnlyInviteMaxInvites": "You can only invite \"<%= maxInvites %>\" at a time",
|
||||||
"partyExceedsMembersLimit": "Party size is limited to <%= maxMembersParty %> members",
|
"partyExceedsMembersLimit": "Party size is limited to <%= maxMembersParty %> members",
|
||||||
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
|
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
|
||||||
@@ -366,6 +368,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",
|
||||||
|
|||||||
@@ -70,5 +70,7 @@
|
|||||||
|
|
||||||
"beginningOfConversation": "This is the beginning of your conversation with <%= userName %>. Remember to be kind, respectful, and follow the Community Guidelines!",
|
"beginningOfConversation": "This is the beginning of your conversation with <%= userName %>. Remember to be kind, respectful, and follow the Community Guidelines!",
|
||||||
|
|
||||||
"messageDeletedUser": "Sorry, this user has deleted their account."
|
"messageDeletedUser": "Sorry, this user has deleted their account.",
|
||||||
|
|
||||||
|
"messageMissingDisplayName": "Missing display name."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,11 @@
|
|||||||
"welcomeTo": "Welcome to",
|
"welcomeTo": "Welcome to",
|
||||||
"welcomeBack": "Welcome back!",
|
"welcomeBack": "Welcome back!",
|
||||||
"justin": "Justin",
|
"justin": "Justin",
|
||||||
"justinIntroMessage1": "Hello there! You must be new here. My name is Justin, your guide to Habitica.",
|
"justinIntroMessage1": "Hello there! You must be new here. My name is <strong>Justin</strong>, and I'll be your guide in Habitica.",
|
||||||
"justinIntroMessage2": "To start, you'll need to create an avatar.",
|
"justinIntroMessage2": "To start, you'll need to create an avatar.",
|
||||||
"justinIntroMessage3": "Great! Now, what are you interested in working on throughout this journey?",
|
"justinIntroMessage3": "Great! Now, what are you interested in working on throughout this journey?",
|
||||||
|
"justinIntroMessageUsername": "Before we begin, let’s figure out what to call you. Below you’ll find a display name and username I’ve generated for you. After you’ve picked a display name and username, we’ll get started by creating an avatar!",
|
||||||
|
"justinIntroMessageAppearance": "So how would you like to look? Don’t worry, you can change this later.",
|
||||||
"introTour": "Here we are! I've filled out some Tasks for you based on your interests, so you can get started right away. Click a Task to edit or add new Tasks to fit your routine!",
|
"introTour": "Here we are! I've filled out some Tasks for you based on your interests, so you can get started right away. Click a Task to edit or add new Tasks to fit your routine!",
|
||||||
"prev": "Prev",
|
"prev": "Prev",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
|
|||||||
@@ -201,9 +201,10 @@
|
|||||||
"usernameIssueInvalidCharacters": "Usernames can only contain letters a to z, numbers 0 to 9, hyphens, or underscores.",
|
"usernameIssueInvalidCharacters": "Usernames can only contain letters a to z, numbers 0 to 9, hyphens, or underscores.",
|
||||||
"currentUsername": "Current username:",
|
"currentUsername": "Current username:",
|
||||||
"displaynameIssueLength": "Display Names must be between 1 and 30 characters.",
|
"displaynameIssueLength": "Display Names must be between 1 and 30 characters.",
|
||||||
"displaynameIssueSlur": "Display Names may not contain inappropriate language",
|
"displaynameIssueSlur": "Display Names may not contain inappropriate language.",
|
||||||
"goToSettings": "Go to Settings",
|
"goToSettings": "Go to Settings",
|
||||||
"usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!",
|
"usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!",
|
||||||
"usernameNotVerified": "Please confirm your username.",
|
"usernameNotVerified": "Please confirm your username.",
|
||||||
"changeUsernameDisclaimer": "We will be transitioning login names to unique, public usernames soon. This username will be used for invitations, @mentions in chat, and messaging."
|
"changeUsernameDisclaimer": "We will be transitioning login names to unique, public usernames soon. This username will be used for invitations, @mentions in chat, and messaging.",
|
||||||
|
"verifyUsernameVeteranPet": "One of these Veteran Pets will be waiting for you after you've finished confirming!"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,7 @@
|
|||||||
"haveCouponCode": "Do you have a coupon code?",
|
"haveCouponCode": "Do you have a coupon code?",
|
||||||
"subscriptionAlreadySubscribedLeadIn": "Thanks for subscribing!",
|
"subscriptionAlreadySubscribedLeadIn": "Thanks for subscribing!",
|
||||||
"subscriptionAlreadySubscribed1": "To see your subscription details and cancel, renew, or change your subscription, please go to <a href='/user/settings/subscription'>User icon > Settings > Subscription</a>.",
|
"subscriptionAlreadySubscribed1": "To see your subscription details and cancel, renew, or change your subscription, please go to <a href='/user/settings/subscription'>User icon > Settings > Subscription</a>.",
|
||||||
"purchaseAll": "Purchase All",
|
"purchaseAll": "Purchase Set",
|
||||||
"gemsPurchaseNote": "Subscribers can buy gems for gold in the Market! For easy access, you can also pin the gem to your Rewards column.",
|
"gemsPurchaseNote": "Subscribers can buy gems for gold in the Market! For easy access, you can also pin the gem to your Rewards column.",
|
||||||
"gemsRemaining": "gems remaining",
|
"gemsRemaining": "gems remaining",
|
||||||
"notEnoughGemsToBuy": "You are unable to buy that amount of gems"
|
"notEnoughGemsToBuy": "You are unable to buy that amount of gems"
|
||||||
|
|||||||
BIN
website/raw_sprites/spritesmith_large/scene_veteran_pets.png
Normal file
BIN
website/raw_sprites/spritesmith_large/scene_veteran_pets.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
@@ -9,7 +9,6 @@ import {
|
|||||||
model as User,
|
model as User,
|
||||||
nameFields,
|
nameFields,
|
||||||
} from '../../models/user';
|
} from '../../models/user';
|
||||||
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
|
||||||
import {
|
import {
|
||||||
NotFound,
|
NotFound,
|
||||||
BadRequest,
|
BadRequest,
|
||||||
@@ -17,8 +16,11 @@ import {
|
|||||||
} from '../../libs/errors';
|
} from '../../libs/errors';
|
||||||
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 { encrypt } from '../../libs/encryption';
|
import {
|
||||||
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
|
inviteByUUID,
|
||||||
|
inviteByEmail,
|
||||||
|
inviteByUserName,
|
||||||
|
} from '../../libs/invites';
|
||||||
import common from '../../../common';
|
import common from '../../../common';
|
||||||
import payments from '../../libs/payments/payments';
|
import payments from '../../libs/payments/payments';
|
||||||
import stripePayments from '../../libs/payments/stripe';
|
import stripePayments from '../../libs/payments/stripe';
|
||||||
@@ -919,148 +921,6 @@ api.removeGroupMember = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async function _inviteByUUID (uuid, group, inviter, req, res) {
|
|
||||||
let userToInvite = await User.findById(uuid).exec();
|
|
||||||
const publicGuild = group.type === 'guild' && group.privacy === 'public';
|
|
||||||
|
|
||||||
if (!userToInvite) {
|
|
||||||
throw new NotFound(res.t('userWithIDNotFound', {userId: uuid}));
|
|
||||||
} else if (inviter._id === userToInvite._id) {
|
|
||||||
throw new BadRequest(res.t('cannotInviteSelfToGroup'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const objections = inviter.getObjectionsToInteraction('group-invitation', userToInvite);
|
|
||||||
if (objections.length > 0) {
|
|
||||||
throw new NotAuthorized(res.t(objections[0], { userId: uuid, username: userToInvite.profile.name}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group.type === 'guild') {
|
|
||||||
if (_.includes(userToInvite.guilds, group._id)) {
|
|
||||||
throw new NotAuthorized(res.t('userAlreadyInGroup', { userId: uuid, username: userToInvite.profile.name}));
|
|
||||||
}
|
|
||||||
if (_.find(userToInvite.invitations.guilds, {id: group._id})) {
|
|
||||||
throw new NotAuthorized(res.t('userAlreadyInvitedToGroup', { userId: uuid, username: userToInvite.profile.name}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let guildInvite = {
|
|
||||||
id: group._id,
|
|
||||||
name: group.name,
|
|
||||||
inviter: inviter._id,
|
|
||||||
publicGuild,
|
|
||||||
};
|
|
||||||
if (group.isSubscribed() && !group.hasNotCancelled()) guildInvite.cancelledPlan = true;
|
|
||||||
userToInvite.invitations.guilds.push(guildInvite);
|
|
||||||
} else if (group.type === 'party') {
|
|
||||||
// Do not add to invitations.parties array if the user is already invited to that party
|
|
||||||
if (_.find(userToInvite.invitations.parties, {id: group._id})) {
|
|
||||||
throw new NotAuthorized(res.t('userAlreadyPendingInvitation', { userId: uuid, username: userToInvite.profile.name}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userToInvite.party._id) {
|
|
||||||
let userParty = await Group.getGroup({user: userToInvite, groupId: 'party', fields: 'memberCount'});
|
|
||||||
|
|
||||||
// Allow user to be invited to a new party when they're partying solo
|
|
||||||
if (userParty && userParty.memberCount !== 1) throw new NotAuthorized(res.t('userAlreadyInAParty', { userId: uuid, username: userToInvite.profile.name}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let partyInvite = {id: group._id, name: group.name, inviter: inviter._id};
|
|
||||||
if (group.isSubscribed() && !group.hasNotCancelled()) partyInvite.cancelledPlan = true;
|
|
||||||
|
|
||||||
userToInvite.invitations.parties.push(partyInvite);
|
|
||||||
userToInvite.invitations.party = partyInvite;
|
|
||||||
}
|
|
||||||
|
|
||||||
let groupLabel = group.type === 'guild' ? 'Guild' : 'Party';
|
|
||||||
let groupTemplate = group.type === 'guild' ? 'guild' : 'party';
|
|
||||||
if (userToInvite.preferences.emailNotifications[`invited${groupLabel}`] !== false) {
|
|
||||||
let emailVars = [
|
|
||||||
{name: 'INVITER', content: inviter.profile.name},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (group.type === 'guild') {
|
|
||||||
emailVars.push(
|
|
||||||
{name: 'GUILD_NAME', content: group.name},
|
|
||||||
{name: 'GUILD_URL', content: '/groups/discovery'}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
emailVars.push(
|
|
||||||
{name: 'PARTY_NAME', content: group.name},
|
|
||||||
{name: 'PARTY_URL', content: '/party'}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
sendTxnEmail(userToInvite, `invited-${groupTemplate}`, emailVars);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userToInvite.preferences.pushNotifications[`invited${groupLabel}`] !== false) {
|
|
||||||
let identifier = group.type === 'guild' ? 'invitedGuild' : 'invitedParty';
|
|
||||||
sendPushNotification(
|
|
||||||
userToInvite,
|
|
||||||
{
|
|
||||||
title: group.name,
|
|
||||||
message: res.t(identifier),
|
|
||||||
identifier,
|
|
||||||
payload: {groupID: group._id, publicGuild},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let userInvited = await userToInvite.save();
|
|
||||||
if (group.type === 'guild') {
|
|
||||||
return userInvited.invitations.guilds[userToInvite.invitations.guilds.length - 1];
|
|
||||||
} else if (group.type === 'party') {
|
|
||||||
return userInvited.invitations.parties[userToInvite.invitations.parties.length - 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _inviteByEmail (invite, group, inviter, req, res) {
|
|
||||||
let userReturnInfo;
|
|
||||||
|
|
||||||
if (!invite.email) throw new BadRequest(res.t('inviteMissingEmail'));
|
|
||||||
|
|
||||||
let userToContact = await User.findOne({$or: [
|
|
||||||
{'auth.local.email': invite.email},
|
|
||||||
{'auth.facebook.emails.value': invite.email},
|
|
||||||
{'auth.google.emails.value': invite.email},
|
|
||||||
]})
|
|
||||||
.select({_id: true, 'preferences.emailNotifications': true})
|
|
||||||
.exec();
|
|
||||||
|
|
||||||
if (userToContact) {
|
|
||||||
userReturnInfo = await _inviteByUUID(userToContact._id, group, inviter, req, res);
|
|
||||||
} else {
|
|
||||||
userReturnInfo = invite.email;
|
|
||||||
|
|
||||||
let cancelledPlan = false;
|
|
||||||
if (group.isSubscribed() && !group.hasNotCancelled()) cancelledPlan = true;
|
|
||||||
|
|
||||||
const groupQueryString = JSON.stringify({
|
|
||||||
id: group._id,
|
|
||||||
inviter: inviter._id,
|
|
||||||
publicGuild: group.type === 'guild' && group.privacy === 'public',
|
|
||||||
sentAt: Date.now(), // so we can let it expire
|
|
||||||
cancelledPlan,
|
|
||||||
});
|
|
||||||
let link = `/static/front?groupInvite=${encrypt(groupQueryString)}`;
|
|
||||||
|
|
||||||
let variables = [
|
|
||||||
{name: 'LINK', content: link},
|
|
||||||
{name: 'INVITER', content: req.body.inviter || inviter.profile.name},
|
|
||||||
];
|
|
||||||
|
|
||||||
if (group.type === 'guild') {
|
|
||||||
variables.push({name: 'GUILD_NAME', content: group.name});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for the email address not to be unsubscribed
|
|
||||||
let userIsUnsubscribed = await EmailUnsubscription.findOne({email: invite.email}).exec();
|
|
||||||
let groupLabel = group.type === 'guild' ? '-guild' : '';
|
|
||||||
if (!userIsUnsubscribed) sendTxnEmail(invite, `invite-friend${groupLabel}`, variables);
|
|
||||||
}
|
|
||||||
|
|
||||||
return userReturnInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {post} /api/v3/groups/:groupId/invite Invite users to a group
|
* @api {post} /api/v3/groups/:groupId/invite Invite users to a group
|
||||||
* @apiName InviteToGroup
|
* @apiName InviteToGroup
|
||||||
@@ -1147,7 +1007,7 @@ api.inviteToGroup = {
|
|||||||
url: '/groups/:groupId/invite',
|
url: '/groups/:groupId/invite',
|
||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
const user = res.locals.user;
|
||||||
|
|
||||||
if (user.flags.chatRevoked) throw new NotAuthorized(res.t('cannotInviteWhenMuted'));
|
if (user.flags.chatRevoked) throw new NotAuthorized(res.t('cannotInviteWhenMuted'));
|
||||||
|
|
||||||
@@ -1155,35 +1015,48 @@ api.inviteToGroup = {
|
|||||||
|
|
||||||
if (user.invitesSent >= MAX_EMAIL_INVITES_BY_USER) throw new NotAuthorized(res.t('inviteLimitReached', { techAssistanceEmail: TECH_ASSISTANCE_EMAIL }));
|
if (user.invitesSent >= MAX_EMAIL_INVITES_BY_USER) throw new NotAuthorized(res.t('inviteLimitReached', { techAssistanceEmail: TECH_ASSISTANCE_EMAIL }));
|
||||||
|
|
||||||
let validationErrors = req.validationErrors();
|
const validationErrors = req.validationErrors();
|
||||||
if (validationErrors) throw validationErrors;
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: '-chat'});
|
const group = await Group.getGroup({user, groupId: req.params.groupId, fields: '-chat'});
|
||||||
if (!group) throw new NotFound(res.t('groupNotFound'));
|
if (!group) throw new NotFound(res.t('groupNotFound'));
|
||||||
|
|
||||||
if (group.purchased && group.purchased.plan.customerId && user._id !== group.leader) throw new NotAuthorized(res.t('onlyGroupLeaderCanInviteToGroupPlan'));
|
if (group.purchased && group.purchased.plan.customerId && user._id !== group.leader) throw new NotAuthorized(res.t('onlyGroupLeaderCanInviteToGroupPlan'));
|
||||||
|
|
||||||
let uuids = req.body.uuids;
|
const {
|
||||||
let emails = req.body.emails;
|
uuids,
|
||||||
|
emails,
|
||||||
|
usernames,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
await Group.validateInvitations(uuids, emails, res, group);
|
await Group.validateInvitations({
|
||||||
|
uuids,
|
||||||
|
emails,
|
||||||
|
usernames,
|
||||||
|
}, res, group);
|
||||||
|
|
||||||
let results = [];
|
const results = [];
|
||||||
|
|
||||||
if (uuids) {
|
if (uuids) {
|
||||||
let uuidInvites = uuids.map((uuid) => _inviteByUUID(uuid, group, user, req, res));
|
const uuidInvites = uuids.map((uuid) => inviteByUUID(uuid, group, user, req, res));
|
||||||
let uuidResults = await Promise.all(uuidInvites);
|
const uuidResults = await Promise.all(uuidInvites);
|
||||||
results.push(...uuidResults);
|
results.push(...uuidResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emails) {
|
if (emails) {
|
||||||
let 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();
|
||||||
let emailResults = await Promise.all(emailInvites);
|
const emailResults = await Promise.all(emailInvites);
|
||||||
results.push(...emailResults);
|
results.push(...emailResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (usernames) {
|
||||||
|
const usernameInvites = usernames.map((username) => inviteByUserName(username, group, user, req, res));
|
||||||
|
const usernameResults = await Promise.all(usernameInvites);
|
||||||
|
results.push(...usernameResults);
|
||||||
|
}
|
||||||
|
|
||||||
let analyticsObject = {
|
let analyticsObject = {
|
||||||
uuid: user._id,
|
uuid: user._id,
|
||||||
hitType: 'event',
|
hitType: 'event',
|
||||||
|
|||||||
@@ -109,6 +109,36 @@ api.getMember = {
|
|||||||
|
|
||||||
if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId}));
|
if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId}));
|
||||||
|
|
||||||
|
if (!member.flags.verifiedUsername) member.auth.local.username = null;
|
||||||
|
|
||||||
|
// 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.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();
|
||||||
|
if (username[0] === '@') username = username.slice(1, username.length);
|
||||||
|
|
||||||
|
let member = await User
|
||||||
|
.findOne({'auth.local.lowerCaseUsername': username, 'flags.verifiedUsername': true})
|
||||||
|
.select(memberFields)
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
if (!member) throw new NotFound(res.t('userNotFound'));
|
||||||
|
|
||||||
// manually call toJSON with minimize: true so empty paths aren't returned
|
// manually call toJSON with minimize: true so empty paths aren't returned
|
||||||
let memberToJSON = member.toJSON({minimize: true});
|
let memberToJSON = member.toJSON({minimize: true});
|
||||||
User.addComputedStatsToJSONObj(memberToJSON.stats, member);
|
User.addComputedStatsToJSONObj(memberToJSON.stats, member);
|
||||||
@@ -605,6 +635,7 @@ api.sendPrivateMessage = {
|
|||||||
const message = req.body.message;
|
const message = req.body.message;
|
||||||
const receiver = await User.findById(req.body.toUserId).exec();
|
const receiver = await User.findById(req.body.toUserId).exec();
|
||||||
if (!receiver) throw new NotFound(res.t('userNotFound'));
|
if (!receiver) throw new NotFound(res.t('userNotFound'));
|
||||||
|
if (!receiver.flags.verifiedUsername) delete receiver.auth.local.username;
|
||||||
|
|
||||||
const objections = sender.getObjectionsToInteraction('send-private-message', receiver);
|
const objections = sender.getObjectionsToInteraction('send-private-message', receiver);
|
||||||
if (objections.length > 0 && !sender.isAdmin()) throw new NotAuthorized(res.t(objections[0]));
|
if (objections.length > 0 && !sender.isAdmin()) throw new NotAuthorized(res.t(objections[0]));
|
||||||
|
|||||||
@@ -28,12 +28,14 @@ api.verifyUsername = {
|
|||||||
|
|
||||||
const issues = verifyUsername(chosenUsername, res);
|
const issues = verifyUsername(chosenUsername, res);
|
||||||
|
|
||||||
const existingUser = await User.findOne({
|
if (issues.length < 1) {
|
||||||
'auth.local.lowerCaseUsername': chosenUsername.toLowerCase(),
|
const existingUser = await User.findOne({
|
||||||
}, {auth: 1}).exec();
|
'auth.local.lowerCaseUsername': chosenUsername.toLowerCase(),
|
||||||
|
}, {auth: 1}).exec();
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
if (!user || existingUser._id !== user._id) issues.push(res.t('usernameTaken'));
|
if (!user || existingUser._id !== user._id) issues.push(res.t('usernameTaken'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (issues.length > 0) {
|
if (issues.length > 0) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { authWithHeaders } from '../../middlewares/auth';
|
import { authWithHeaders } from '../../middlewares/auth';
|
||||||
import * as userLib from '../../libs/user';
|
import * as userLib from '../../libs/user';
|
||||||
|
import { verifyDisplayName } from '../../libs/user/validation';
|
||||||
|
|
||||||
const api = {};
|
const api = {};
|
||||||
|
|
||||||
@@ -206,4 +207,32 @@ api.userReset = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
api.verifyDisplayName = {
|
||||||
|
method: 'POST',
|
||||||
|
url: '/user/auth/verify-display-name',
|
||||||
|
middlewares: [authWithHeaders({
|
||||||
|
optional: true,
|
||||||
|
})],
|
||||||
|
async handler (req, res) {
|
||||||
|
req.checkBody({
|
||||||
|
displayName: {
|
||||||
|
notEmpty: {errorMessage: res.t('messageMissingDisplayName')},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
|
const chosenDisplayName = req.body.displayName;
|
||||||
|
|
||||||
|
const issues = verifyDisplayName(chosenDisplayName, res);
|
||||||
|
|
||||||
|
if (issues.length > 0) {
|
||||||
|
res.respond(200, { isUsable: false, issues });
|
||||||
|
} else {
|
||||||
|
res.respond(200, { isUsable: true });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = api;
|
module.exports = api;
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ async function registerLocal (req, res, { isV3 = false }) {
|
|||||||
notEmpty: true,
|
notEmpty: true,
|
||||||
errorMessage: res.t('missingUsername'),
|
errorMessage: res.t('missingUsername'),
|
||||||
// TODO use the constants in the error message above
|
// TODO use the constants in the error message above
|
||||||
isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameWrongLength')},
|
isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameIssueLength')},
|
||||||
matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameBadCharacters')},
|
matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameIssueInvalidCharacters')},
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
notEmpty: true,
|
notEmpty: true,
|
||||||
@@ -138,6 +138,9 @@ async function registerLocal (req, res, { isV3 = false }) {
|
|||||||
preferences: {
|
preferences: {
|
||||||
language: req.language,
|
language: req.language,
|
||||||
},
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
@@ -159,8 +162,6 @@ async function registerLocal (req, res, { isV3 = false }) {
|
|||||||
await _handleGroupInvitation(newUser, req.query.groupInvite || req.query.partyInvite);
|
await _handleGroupInvitation(newUser, req.query.groupInvite || req.query.partyInvite);
|
||||||
}
|
}
|
||||||
|
|
||||||
newUser.flags.verifiedUsername = true;
|
|
||||||
|
|
||||||
let savedUser = await newUser.save();
|
let savedUser = await newUser.save();
|
||||||
|
|
||||||
let userToJSON;
|
let userToJSON;
|
||||||
|
|||||||
@@ -61,6 +61,9 @@ async function loginSocial (req, res) {
|
|||||||
preferences: {
|
preferences: {
|
||||||
language: req.language,
|
language: req.language,
|
||||||
},
|
},
|
||||||
|
flags: {
|
||||||
|
verifiedUsername: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
|
|||||||
211
website/server/libs/invites/index.js
Normal file
211
website/server/libs/invites/index.js
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import { encrypt } from '../encryption';
|
||||||
|
import { sendNotification as sendPushNotification } from '../pushNotifications';
|
||||||
|
import {
|
||||||
|
NotFound,
|
||||||
|
BadRequest,
|
||||||
|
NotAuthorized,
|
||||||
|
} from '../errors';
|
||||||
|
import { sendTxn as sendTxnEmail } from '../email';
|
||||||
|
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
||||||
|
import {
|
||||||
|
model as User,
|
||||||
|
} from '../../models/user';
|
||||||
|
import {
|
||||||
|
model as Group,
|
||||||
|
} from '../../models/group';
|
||||||
|
|
||||||
|
function sendInvitePushNotification (userToInvite, groupLabel, group, publicGuild, res) {
|
||||||
|
if (userToInvite.preferences.pushNotifications[`invited${groupLabel}`] === false) return;
|
||||||
|
|
||||||
|
const identifier = group.type === 'guild' ? 'invitedGuild' : 'invitedParty';
|
||||||
|
|
||||||
|
sendPushNotification(
|
||||||
|
userToInvite,
|
||||||
|
{
|
||||||
|
title: group.name,
|
||||||
|
message: res.t(identifier),
|
||||||
|
identifier,
|
||||||
|
payload: {groupID: group._id, publicGuild},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendInviteEmail (userToInvite, groupLabel, group, inviter) {
|
||||||
|
if (userToInvite.preferences.emailNotifications[`invited${groupLabel}`] === false) return;
|
||||||
|
const groupTemplate = group.type === 'guild' ? 'guild' : 'party';
|
||||||
|
|
||||||
|
const emailVars = [
|
||||||
|
{name: 'INVITER', content: inviter.profile.name},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (group.type === 'guild') {
|
||||||
|
emailVars.push(
|
||||||
|
{name: 'GUILD_NAME', content: group.name},
|
||||||
|
{name: 'GUILD_URL', content: '/groups/discovery'}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
emailVars.push(
|
||||||
|
{name: 'PARTY_NAME', content: group.name},
|
||||||
|
{name: 'PARTY_URL', content: '/party'}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendTxnEmail(userToInvite, `invited-${groupTemplate}`, emailVars);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inviteUserToGuild (userToInvite, group, inviter, publicGuild, res) {
|
||||||
|
const uuid = userToInvite._id;
|
||||||
|
|
||||||
|
if (_.includes(userToInvite.guilds, group._id)) {
|
||||||
|
throw new NotAuthorized(res.t('userAlreadyInGroup', { userId: uuid, username: userToInvite.profile.name}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_.find(userToInvite.invitations.guilds, {id: group._id})) {
|
||||||
|
throw new NotAuthorized(res.t('userAlreadyInvitedToGroup', { userId: uuid, username: userToInvite.profile.name}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const guildInvite = {
|
||||||
|
id: group._id,
|
||||||
|
name: group.name,
|
||||||
|
inviter: inviter._id,
|
||||||
|
publicGuild,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (group.isSubscribed() && !group.hasNotCancelled()) guildInvite.cancelledPlan = true;
|
||||||
|
|
||||||
|
userToInvite.invitations.guilds.push(guildInvite);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inviteUserToParty (userToInvite, group, inviter, res) {
|
||||||
|
const uuid = userToInvite._id;
|
||||||
|
|
||||||
|
// Do not add to invitations.parties array if the user is already invited to that party
|
||||||
|
if (_.find(userToInvite.invitations.parties, {id: group._id})) {
|
||||||
|
throw new NotAuthorized(res.t('userAlreadyPendingInvitation', { userId: uuid, username: userToInvite.profile.name}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userToInvite.party._id) {
|
||||||
|
let userParty = await Group.getGroup({user: userToInvite, groupId: 'party', fields: 'memberCount'});
|
||||||
|
|
||||||
|
// Allow user to be invited to a new party when they're partying solo
|
||||||
|
if (userParty && userParty.memberCount !== 1) throw new NotAuthorized(res.t('userAlreadyInAParty', { userId: uuid, username: userToInvite.profile.name}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let partyInvite = {id: group._id, name: group.name, inviter: inviter._id};
|
||||||
|
if (group.isSubscribed() && !group.hasNotCancelled()) partyInvite.cancelledPlan = true;
|
||||||
|
|
||||||
|
userToInvite.invitations.parties.push(partyInvite);
|
||||||
|
userToInvite.invitations.party = partyInvite;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addInvitationToUser (userToInvite, group, inviter, res) {
|
||||||
|
const publicGuild = group.type === 'guild' && group.privacy === 'public';
|
||||||
|
|
||||||
|
if (group.type === 'guild') {
|
||||||
|
inviteUserToGuild(userToInvite, group, inviter, publicGuild, res);
|
||||||
|
} else if (group.type === 'party') {
|
||||||
|
await inviteUserToParty(userToInvite, group, inviter, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupLabel = group.type === 'guild' ? 'Guild' : 'Party';
|
||||||
|
sendInviteEmail(userToInvite, groupLabel, group, inviter);
|
||||||
|
sendInvitePushNotification(userToInvite, groupLabel, group, publicGuild, res);
|
||||||
|
|
||||||
|
const userInvited = await userToInvite.save();
|
||||||
|
if (group.type === 'guild') {
|
||||||
|
return userInvited.invitations.guilds[userToInvite.invitations.guilds.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.type === 'party') {
|
||||||
|
return userInvited.invitations.parties[userToInvite.invitations.parties.length - 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inviteByUUID (uuid, group, inviter, req, res) {
|
||||||
|
const userToInvite = await User.findById(uuid).exec();
|
||||||
|
|
||||||
|
if (!userToInvite) {
|
||||||
|
throw new NotFound(res.t('userWithIDNotFound', {userId: uuid}));
|
||||||
|
} else if (inviter._id === userToInvite._id) {
|
||||||
|
throw new BadRequest(res.t('cannotInviteSelfToGroup'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const objections = inviter.getObjectionsToInteraction('group-invitation', userToInvite);
|
||||||
|
if (objections.length > 0) {
|
||||||
|
throw new NotAuthorized(res.t(objections[0], { userId: uuid, username: userToInvite.profile.name}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await addInvitationToUser(userToInvite, group, inviter, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function inviteByEmail (invite, group, inviter, req, res) {
|
||||||
|
let userReturnInfo;
|
||||||
|
|
||||||
|
if (!invite.email) throw new BadRequest(res.t('inviteMissingEmail'));
|
||||||
|
|
||||||
|
let userToContact = await User.findOne({$or: [
|
||||||
|
{'auth.local.email': invite.email},
|
||||||
|
{'auth.facebook.emails.value': invite.email},
|
||||||
|
{'auth.google.emails.value': invite.email},
|
||||||
|
]})
|
||||||
|
.select({_id: true, 'preferences.emailNotifications': true})
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
if (userToContact) {
|
||||||
|
userReturnInfo = await inviteByUUID(userToContact._id, group, inviter, req, res);
|
||||||
|
} else {
|
||||||
|
userReturnInfo = invite.email;
|
||||||
|
|
||||||
|
let cancelledPlan = false;
|
||||||
|
if (group.isSubscribed() && !group.hasNotCancelled()) cancelledPlan = true;
|
||||||
|
|
||||||
|
const groupQueryString = JSON.stringify({
|
||||||
|
id: group._id,
|
||||||
|
inviter: inviter._id,
|
||||||
|
publicGuild: group.type === 'guild' && group.privacy === 'public',
|
||||||
|
sentAt: Date.now(), // so we can let it expire
|
||||||
|
cancelledPlan,
|
||||||
|
});
|
||||||
|
let link = `/static/front?groupInvite=${encrypt(groupQueryString)}`;
|
||||||
|
|
||||||
|
let variables = [
|
||||||
|
{name: 'LINK', content: link},
|
||||||
|
{name: 'INVITER', content: req.body.inviter || inviter.profile.name},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (group.type === 'guild') {
|
||||||
|
variables.push({name: 'GUILD_NAME', content: group.name});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for the email address not to be unsubscribed
|
||||||
|
let userIsUnsubscribed = await EmailUnsubscription.findOne({email: invite.email}).exec();
|
||||||
|
let groupLabel = group.type === 'guild' ? '-guild' : '';
|
||||||
|
if (!userIsUnsubscribed) sendTxnEmail(invite, `invite-friend${groupLabel}`, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
return userReturnInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (!userToInvite) {
|
||||||
|
throw new NotFound(res.t('userWithUsernameNotFound', { username }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inviter._id === userToInvite._id) {
|
||||||
|
throw new BadRequest(res.t('cannotInviteSelfToGroup'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return await addInvitationToUser(userToInvite, group, inviter, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
inviteByUUID,
|
||||||
|
inviteByEmail,
|
||||||
|
inviteByUserName,
|
||||||
|
};
|
||||||
@@ -26,6 +26,14 @@ function usernameContainsInvalidCharacters (username) {
|
|||||||
return match !== null && match[0] !== null;
|
return match !== null && match[0] !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function verifyDisplayName (displayName, res) {
|
||||||
|
let issues = [];
|
||||||
|
if (displayName.length < 1 || displayName.length > 30) issues.push(res.t('displaynameIssueLength'));
|
||||||
|
if (nameContainsSlur(displayName)) issues.push(res.t('displaynameIssueSlur'));
|
||||||
|
|
||||||
|
return issues;
|
||||||
|
}
|
||||||
|
|
||||||
export function verifyUsername (username, res) {
|
export function verifyUsername (username, res) {
|
||||||
let issues = [];
|
let issues = [];
|
||||||
if (username.length < 1 || username.length > 20) issues.push(res.t('usernameIssueLength'));
|
if (username.length < 1 || username.length > 20) issues.push(res.t('usernameIssueLength'));
|
||||||
|
|||||||
@@ -350,40 +350,32 @@ schema.statics.toJSONCleanChat = async function groupToJSONCleanChat (group, use
|
|||||||
return toJSON;
|
return toJSON;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
function getInviteError (uuids, emails, usernames) {
|
||||||
* Checks invitation uuids and emails for possible errors.
|
const uuidsIsArray = Array.isArray(uuids);
|
||||||
*
|
const emailsIsArray = Array.isArray(emails);
|
||||||
* @param uuids An array of user ids
|
const usernamesIsArray = Array.isArray(usernames);
|
||||||
* @param emails An array of emails
|
const emptyEmails = emailsIsArray && emails.length < 1;
|
||||||
* @param res Express res object for use with translations
|
const emptyUuids = uuidsIsArray && uuids.length < 1;
|
||||||
* @throws BadRequest An error describing the issue with the invitations
|
const emptyUsernames = usernamesIsArray && usernames.length < 1;
|
||||||
*/
|
|
||||||
schema.statics.validateInvitations = async function getInvitationError (uuids, emails, res, group = null) {
|
|
||||||
let uuidsIsArray = Array.isArray(uuids);
|
|
||||||
let emailsIsArray = Array.isArray(emails);
|
|
||||||
let emptyEmails = emailsIsArray && emails.length < 1;
|
|
||||||
let emptyUuids = uuidsIsArray && uuids.length < 1;
|
|
||||||
|
|
||||||
let errorString;
|
let errorString;
|
||||||
|
|
||||||
if (!uuids && !emails) {
|
if (!uuids && !emails && !usernames) {
|
||||||
errorString = 'canOnlyInviteEmailUuid';
|
errorString = 'canOnlyInviteEmailUuid';
|
||||||
} else if (uuids && !uuidsIsArray) {
|
} else if (uuids && !uuidsIsArray) {
|
||||||
errorString = 'uuidsMustBeAnArray';
|
errorString = 'uuidsMustBeAnArray';
|
||||||
} else if (emails && !emailsIsArray) {
|
} else if (emails && !emailsIsArray) {
|
||||||
errorString = 'emailsMustBeAnArray';
|
errorString = 'emailsMustBeAnArray';
|
||||||
} else if (!emails && emptyUuids) {
|
} else if (usernames && !usernamesIsArray) {
|
||||||
errorString = 'inviteMissingUuid';
|
errorString = 'usernamesMustBeAnArray';
|
||||||
} else if (!uuids && emptyEmails) {
|
} else if ((!emails || emptyEmails) && (!uuids || emptyUuids) && (!usernames || emptyUsernames)) {
|
||||||
errorString = 'inviteMissingEmail';
|
|
||||||
} else if (emptyEmails && emptyUuids) {
|
|
||||||
errorString = 'inviteMustNotBeEmpty';
|
errorString = 'inviteMustNotBeEmpty';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorString) {
|
return errorString;
|
||||||
throw new BadRequest(res.t(errorString));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
function getInviteCount (uuids, emails) {
|
||||||
let totalInvites = 0;
|
let totalInvites = 0;
|
||||||
|
|
||||||
if (uuids) {
|
if (uuids) {
|
||||||
@@ -394,6 +386,27 @@ schema.statics.validateInvitations = async function getInvitationError (uuids, e
|
|||||||
totalInvites += emails.length;
|
totalInvites += emails.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return totalInvites;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks invitation uuids and emails for possible errors.
|
||||||
|
*
|
||||||
|
* @param uuids An array of user ids
|
||||||
|
* @param emails An array of emails
|
||||||
|
* @param res Express res object for use with translations
|
||||||
|
* @throws BadRequest An error describing the issue with the invitations
|
||||||
|
*/
|
||||||
|
schema.statics.validateInvitations = async function getInvitationError (invites, res, group = null) {
|
||||||
|
const {
|
||||||
|
uuids,
|
||||||
|
emails,
|
||||||
|
usernames,
|
||||||
|
} = invites;
|
||||||
|
const errorString = getInviteError(uuids, emails, usernames);
|
||||||
|
if (errorString) throw new BadRequest(res.t(errorString));
|
||||||
|
|
||||||
|
const totalInvites = getInviteCount(uuids, emails);
|
||||||
if (totalInvites > INVITES_LIMIT) {
|
if (totalInvites > INVITES_LIMIT) {
|
||||||
throw new BadRequest(res.t('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT}));
|
throw new BadRequest(res.t('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ const defaultSchema = () => ({
|
|||||||
text: String,
|
text: String,
|
||||||
|
|
||||||
// sender properties
|
// sender properties
|
||||||
user: String, // profile name
|
user: String, // profile name (unfortunately)
|
||||||
|
username: String,
|
||||||
contributor: {$type: mongoose.Schema.Types.Mixed},
|
contributor: {$type: mongoose.Schema.Types.Mixed},
|
||||||
backer: {$type: mongoose.Schema.Types.Mixed},
|
backer: {$type: mongoose.Schema.Types.Mixed},
|
||||||
uuid: String, // sender uuid
|
uuid: String, // sender uuid
|
||||||
@@ -117,6 +118,7 @@ export function messageDefaults (msg, user) {
|
|||||||
contributor: user.contributor && user.contributor.toObject(),
|
contributor: user.contributor && user.contributor.toObject(),
|
||||||
backer: user.backer && user.backer.toObject(),
|
backer: user.backer && user.backer.toObject(),
|
||||||
user: user.profile.name,
|
user: user.profile.name,
|
||||||
|
username: user.flags && user.flags.verifiedUsername && user.auth && user.auth.local && user.auth.local.username,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
message.uuid = 'system';
|
message.uuid = 'system';
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ require('./methods');
|
|||||||
// A list of publicly accessible fields (not everything from preferences because there are also a lot of settings tha should remain private)
|
// A list of publicly accessible fields (not everything from preferences because there are also a lot of settings tha should remain private)
|
||||||
export let publicFields = `preferences.size preferences.hair preferences.skin preferences.shirt
|
export let publicFields = `preferences.size preferences.hair preferences.skin preferences.shirt
|
||||||
preferences.chair preferences.costume preferences.sleep preferences.background preferences.tasks preferences.disableClasses profile stats
|
preferences.chair preferences.costume preferences.sleep preferences.background preferences.tasks preferences.disableClasses profile stats
|
||||||
achievements party backer contributor auth.timestamps items inbox.optOut loginIncentives flags.classSelected`;
|
achievements party backer contributor auth.timestamps items inbox.optOut loginIncentives flags.classSelected
|
||||||
|
flags.verifiedUsername auth.local.username`;
|
||||||
|
|
||||||
// The minimum amount of data needed when populating multiple users
|
// The minimum amount of data needed when populating multiple users
|
||||||
export let nameFields = 'profile.name';
|
export let nameFields = 'profile.name auth.local.username flags.verifiedUsername';
|
||||||
|
|
||||||
export { schema };
|
export { schema };
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,9 @@ schema.statics.transformJSONUser = function transformJSONUser (jsonUser, addComp
|
|||||||
// Add id property
|
// Add id property
|
||||||
jsonUser.id = jsonUser._id;
|
jsonUser.id = jsonUser._id;
|
||||||
|
|
||||||
|
// Remove username if not verified
|
||||||
|
if (!jsonUser.flags.verifiedUsername) jsonUser.auth.local.username = null;
|
||||||
|
|
||||||
if (addComputedStats) this.addComputedStatsToJSONObj(jsonUser.stats, jsonUser);
|
if (addComputedStats) this.addComputedStatsToJSONObj(jsonUser.stats, jsonUser);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user