mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 06:37:23 +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 () => {
|
||||
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,
|
||||
message: 'Bad request.',
|
||||
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 () => {
|
||||
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,
|
||||
message: 'Bad request.',
|
||||
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 () => {
|
||||
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,
|
||||
message: 'Bad request.',
|
||||
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 () => {
|
||||
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,
|
||||
message: 'Bad request.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
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 () => {
|
||||
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,
|
||||
message: 'Bad request.',
|
||||
name: 'BadRequest',
|
||||
});
|
||||
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 () => {
|
||||
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,
|
||||
message: 'Bad request.',
|
||||
name: 'BadRequest',
|
||||
@@ -639,7 +639,7 @@ describe('Group Model', () => {
|
||||
|
||||
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,
|
||||
message: 'Bad request.',
|
||||
name: 'BadRequest',
|
||||
@@ -657,33 +657,33 @@ describe('Group Model', () => {
|
||||
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;
|
||||
});
|
||||
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,14 @@ describe('GET /challenges/:challengeId', () => {
|
||||
_id: groupLeader._id,
|
||||
id: groupLeader._id,
|
||||
profile: {name: groupLeader.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: groupLeader.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
expect(chal.group).to.eql({
|
||||
_id: group._id,
|
||||
@@ -105,6 +113,14 @@ describe('GET /challenges/:challengeId', () => {
|
||||
_id: challengeLeader._id,
|
||||
id: challengeLeader._id,
|
||||
profile: {name: challengeLeader.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: challengeLeader.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
expect(chal.group).to.eql({
|
||||
_id: group._id,
|
||||
@@ -131,6 +147,14 @@ describe('GET /challenges/:challengeId', () => {
|
||||
_id: challengeLeader._id,
|
||||
id: challengeLeader._id,
|
||||
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,
|
||||
profile: {name: challengeLeader.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: challengeLeader.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
expect(chal.group).to.eql({
|
||||
_id: group._id,
|
||||
@@ -205,6 +237,14 @@ describe('GET /challenges/:challengeId', () => {
|
||||
_id: challengeLeader._id,
|
||||
id: challengeLeader._id,
|
||||
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,
|
||||
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,
|
||||
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']);
|
||||
});
|
||||
|
||||
@@ -88,8 +104,16 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
_id: anotherUser._id,
|
||||
id: anotherUser._id,
|
||||
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']);
|
||||
});
|
||||
|
||||
@@ -107,7 +131,7 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
let res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=not-true`);
|
||||
expect(res.length).to.equal(30);
|
||||
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']);
|
||||
});
|
||||
});
|
||||
@@ -126,7 +150,7 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
let res = await user.get(`/challenges/${challenge._id}/members`);
|
||||
expect(res.length).to.equal(30);
|
||||
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']);
|
||||
});
|
||||
});
|
||||
@@ -145,7 +169,7 @@ describe('GET /challenges/:challengeId/members', () => {
|
||||
let res = await user.get(`/challenges/${challenge._id}/members?includeAllMembers=true`);
|
||||
expect(res.length).to.equal(32);
|
||||
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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('GET /challenges/:challengeId/members/:memberId', () => {
|
||||
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
|
||||
|
||||
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.tasks.length).to.equal(1);
|
||||
});
|
||||
|
||||
@@ -39,6 +39,14 @@ describe('GET challenges/groups/:groupId', () => {
|
||||
_id: publicGuild.leader._id,
|
||||
id: publicGuild.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
@@ -46,6 +54,14 @@ describe('GET challenges/groups/:groupId', () => {
|
||||
_id: publicGuild.leader._id,
|
||||
id: publicGuild.leader._id,
|
||||
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,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
@@ -65,6 +89,14 @@ describe('GET challenges/groups/:groupId', () => {
|
||||
_id: publicGuild.leader._id,
|
||||
id: publicGuild.leader._id,
|
||||
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,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
@@ -132,6 +172,14 @@ describe('GET challenges/groups/:groupId', () => {
|
||||
_id: privateGuild.leader._id,
|
||||
id: privateGuild.leader._id,
|
||||
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,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
@@ -242,6 +298,14 @@ describe('GET challenges/groups/:groupId', () => {
|
||||
_id: party.leader._id,
|
||||
id: party.leader._id,
|
||||
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,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
@@ -261,6 +333,14 @@ describe('GET challenges/groups/:groupId', () => {
|
||||
_id: party.leader._id,
|
||||
id: party.leader._id,
|
||||
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,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
@@ -295,6 +383,14 @@ describe('GET challenges/groups/:groupId', () => {
|
||||
_id: user._id,
|
||||
id: user._id,
|
||||
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,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
let foundChallenge2 = _.find(challenges, { _id: challenge2._id });
|
||||
expect(foundChallenge2).to.exist;
|
||||
@@ -314,6 +418,14 @@ describe('GET challenges/groups/:groupId', () => {
|
||||
_id: user._id,
|
||||
id: user._id,
|
||||
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,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
expect(foundChallenge.group).to.eql({
|
||||
_id: publicGuild._id,
|
||||
@@ -62,6 +70,14 @@ describe('GET challenges/user', () => {
|
||||
_id: publicGuild.leader._id,
|
||||
id: publicGuild.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
expect(foundChallenge1.group).to.eql({
|
||||
_id: publicGuild._id,
|
||||
@@ -79,6 +95,14 @@ describe('GET challenges/user', () => {
|
||||
_id: publicGuild.leader._id,
|
||||
id: publicGuild.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
expect(foundChallenge2.group).to.eql({
|
||||
_id: publicGuild._id,
|
||||
@@ -101,6 +125,14 @@ describe('GET challenges/user', () => {
|
||||
_id: publicGuild.leader._id,
|
||||
id: publicGuild.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
expect(foundChallenge1.group).to.eql({
|
||||
_id: publicGuild._id,
|
||||
@@ -118,6 +150,14 @@ describe('GET challenges/user', () => {
|
||||
_id: publicGuild.leader._id,
|
||||
id: publicGuild.leader._id,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
expect(foundChallenge2.group).to.eql({
|
||||
_id: publicGuild._id,
|
||||
|
||||
@@ -79,6 +79,14 @@ describe('POST /challenges/:challengeId/join', () => {
|
||||
_id: groupLeader._id,
|
||||
id: groupLeader._id,
|
||||
profile: {name: groupLeader.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: groupLeader.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
expect(res.name).to.equal(challenge.name);
|
||||
});
|
||||
|
||||
@@ -79,6 +79,14 @@ describe('PUT /challenges/:challengeId', () => {
|
||||
_id: member._id,
|
||||
id: member._id,
|
||||
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.description).to.equal('New challenge description.');
|
||||
|
||||
@@ -50,6 +50,14 @@ describe('GET /groups/:groupId/invites', () => {
|
||||
_id: invited._id,
|
||||
id: invited._id,
|
||||
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();
|
||||
await user.post(`/groups/${group._id}/invite`, {uuids: [invited._id]});
|
||||
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']);
|
||||
});
|
||||
|
||||
@@ -76,7 +84,7 @@ describe('GET /groups/:groupId/invites', () => {
|
||||
let res = await leader.get(`/groups/${group._id}/invites`);
|
||||
expect(res.length).to.equal(30);
|
||||
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']);
|
||||
});
|
||||
}).timeout(10000);
|
||||
|
||||
@@ -56,13 +56,21 @@ describe('GET /groups/:groupId/members', () => {
|
||||
_id: user._id,
|
||||
id: user._id,
|
||||
profile: {name: user.profile.name},
|
||||
auth: {
|
||||
local: {
|
||||
username: user.auth.local.username,
|
||||
},
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('populates only some fields', async () => {
|
||||
await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||
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']);
|
||||
});
|
||||
|
||||
@@ -74,7 +82,7 @@ describe('GET /groups/:groupId/members', () => {
|
||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||
'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([
|
||||
'size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||
@@ -95,7 +103,7 @@ describe('GET /groups/:groupId/members', () => {
|
||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||
'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([
|
||||
'size', 'hair', 'skin', 'shirt',
|
||||
'chair', 'costume', 'sleep', 'background', 'tasks', 'disableClasses',
|
||||
@@ -120,7 +128,7 @@ describe('GET /groups/:groupId/members', () => {
|
||||
let res = await user.get('/groups/party/members');
|
||||
expect(res.length).to.equal(30);
|
||||
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']);
|
||||
});
|
||||
});
|
||||
@@ -137,7 +145,7 @@ describe('GET /groups/:groupId/members', () => {
|
||||
let res = await user.get('/groups/party/members?includeAllMembers=true');
|
||||
expect(res.length).to.equal(30);
|
||||
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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
it('returns an error when inviter has no chat privileges', async () => {
|
||||
let inviterMuted = await inviter.update({'flags.chatRevoked': true});
|
||||
@@ -93,7 +160,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('inviteMissingUuid'),
|
||||
message: t('inviteMustNotBeEmpty'),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -228,7 +295,7 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('inviteMissingEmail'),
|
||||
message: t('inviteMustNotBeEmpty'),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('GET /members/:memberId', () => {
|
||||
'_id', 'id', 'preferences', 'profile', 'stats', 'achievements', 'party',
|
||||
'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([
|
||||
'size', 'hair', 'skin', 'shirt',
|
||||
'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;
|
||||
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();
|
||||
}
|
||||
|
||||
&:hover:not(.btn-flat) {
|
||||
&:hover:not(.btn-flat):not(.disabled) {
|
||||
@include btn-focus-hover-shadow();
|
||||
border-color: transparent;
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
|
||||
.btn:disabled, .btn.disabled {
|
||||
box-shadow: none;
|
||||
cursor: default;
|
||||
opacity: 0.64;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: 1350;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
margin: 5.5rem auto 3rem;
|
||||
width: auto;
|
||||
|
||||
.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">
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 361 B After Width: | Height: | Size: 346 B |
@@ -82,6 +82,7 @@
|
||||
import hello from 'hellojs';
|
||||
import { setUpAxios } from 'client/libs/auth';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
|
||||
import facebookSquareIcon from 'assets/svg/facebook-square.svg';
|
||||
import googleIcon from 'assets/svg/google.svg';
|
||||
@@ -115,13 +116,13 @@ export default {
|
||||
computed: {
|
||||
emailValid () {
|
||||
if (this.email.length <= 3) return false;
|
||||
return this.validateEmail(this.email);
|
||||
return isEmail(this.email);
|
||||
},
|
||||
emailInvalid () {
|
||||
return !this.emailValid;
|
||||
},
|
||||
usernameValid () {
|
||||
if (this.username.length <= 3) return false;
|
||||
if (this.username.length < 1) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
},
|
||||
usernameInvalid () {
|
||||
@@ -143,7 +144,7 @@ export default {
|
||||
methods: {
|
||||
// eslint-disable-next-line func-names
|
||||
validateUsername: debounce(function (username) {
|
||||
if (username.length <= 3) {
|
||||
if (username.length < 1) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
|
||||
@@ -295,6 +295,7 @@
|
||||
import axios from 'axios';
|
||||
import hello from 'hellojs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
|
||||
import gryphon from 'assets/svg/gryphon.svg';
|
||||
import habiticaIcon from 'assets/svg/habitica-logo.svg';
|
||||
@@ -340,18 +341,18 @@ export default {
|
||||
},
|
||||
emailValid () {
|
||||
if (this.email.length <= 3) return false;
|
||||
return this.validateEmail(this.email);
|
||||
return isEmail(this.email);
|
||||
},
|
||||
emailInvalid () {
|
||||
if (this.email.length <= 3) return false;
|
||||
return !this.emailValid;
|
||||
},
|
||||
usernameValid () {
|
||||
if (this.username.length <= 3) return false;
|
||||
if (this.username.length < 1) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
},
|
||||
usernameInvalid () {
|
||||
if (this.username.length <= 3) return false;
|
||||
if (this.username.length < 1) return false;
|
||||
return !this.usernameValid;
|
||||
},
|
||||
passwordConfirmValid () {
|
||||
|
||||
@@ -1,26 +1,91 @@
|
||||
<template lang="pug">
|
||||
div.autocomplete-selection(v-if='searchResults.length > 0', :style='autocompleteStyle')
|
||||
.autocomplete-results(v-for='result in searchResults', @click='select(result)') {{ result }}
|
||||
.autocomplete-selection(v-if='searchResults.length > 0', :style='autocompleteStyle')
|
||||
.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>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/tiers.scss';
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.autocomplete-results {
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
.autocomplete-selection {
|
||||
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>
|
||||
|
||||
<script>
|
||||
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 {
|
||||
props: ['selections', 'text', 'coords', 'chat', 'textbox'],
|
||||
props: ['selections', 'text', 'caretPosition', 'coords', 'chat', 'textbox'],
|
||||
data () {
|
||||
return {
|
||||
atRegex: /(?!\b)@[\w-]*$/,
|
||||
currentSearch: '',
|
||||
searchActive: false,
|
||||
searchEscaped: false,
|
||||
currentSearchPosition: 0,
|
||||
tmpSelections: [],
|
||||
icons: Object.freeze({
|
||||
tier1,
|
||||
tier2,
|
||||
tier3,
|
||||
tier4,
|
||||
tier5,
|
||||
tier6,
|
||||
tier7,
|
||||
tier8,
|
||||
tier9,
|
||||
tierNPC,
|
||||
}),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -36,32 +101,46 @@ export default {
|
||||
marginTop: '28px',
|
||||
position: 'absolute',
|
||||
minWidth: '100px',
|
||||
minHeight: '100px',
|
||||
zIndex: 100,
|
||||
backgroundColor: 'white',
|
||||
};
|
||||
},
|
||||
searchResults () {
|
||||
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 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 () {
|
||||
this.grabUserNames();
|
||||
},
|
||||
created () {
|
||||
document.addEventListener('keyup', this.handleEsc);
|
||||
},
|
||||
destroyed () {
|
||||
document.removeEventListener('keyup', this.handleEsc);
|
||||
},
|
||||
watch: {
|
||||
text (newText) {
|
||||
if (!newText[newText.length - 1] || newText[newText.length - 1] === ' ') {
|
||||
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.currentSearchPosition = newText.length - 1;
|
||||
},
|
||||
chat () {
|
||||
this.resetDefaults();
|
||||
@@ -72,25 +151,52 @@ export default {
|
||||
resetDefaults () {
|
||||
// Mounted is not called when switching between group pages because they have the
|
||||
// the same parent component. So, reset the data
|
||||
this.currentSearch = '';
|
||||
this.searchActive = false;
|
||||
this.currentSearchPosition = 0;
|
||||
this.searchEscaped = false;
|
||||
this.tmpSelections = [];
|
||||
},
|
||||
grabUserNames () {
|
||||
let usersThatMessage = groupBy(this.chat, 'user');
|
||||
for (let userName in usersThatMessage) {
|
||||
let systemMessage = userName === 'undefined';
|
||||
if (!systemMessage && this.tmpSelections.indexOf(userName) === -1) {
|
||||
this.tmpSelections.push(userName);
|
||||
for (let userKey in usersThatMessage) {
|
||||
let systemMessage = userKey === 'undefined';
|
||||
if (!systemMessage && this.tmpSelections.indexOf(userKey) === -1) {
|
||||
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) {
|
||||
let newText = this.text.slice(0, this.currentSearchPosition + 1) + result;
|
||||
this.searchActive = false;
|
||||
let newText = this.text;
|
||||
const targetName = `${result.username || result.displayName} `;
|
||||
newText = newText.replace(new RegExp(`${this.currentSearch}$`), targetName);
|
||||
this.$emit('select', newText);
|
||||
},
|
||||
handleEsc (e) {
|
||||
if (e.keyCode === 27) {
|
||||
this.searchActive = false;
|
||||
this.searchEscaped = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
mixins: [styleHelper],
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -8,33 +8,46 @@ div
|
||||
:class='userLevelStyle(msg)',
|
||||
@click="showMemberModal(msg.uuid)",
|
||||
v-b-tooltip.hover.top="tierTitle",
|
||||
v-if="msg.user"
|
||||
)
|
||||
| {{msg.user}}
|
||||
.svg-icon(v-html="tierIcon", v-if='showShowTierStyle')
|
||||
p.time(v-b-tooltip="", :title="msg.timestamp | date") {{msg.timestamp | timeAgo}}
|
||||
.text(v-markdown='msg.text')
|
||||
.svg-icon(v-html="tierIcon")
|
||||
p.time
|
||||
span.mr-1(v-if="msg.username") @{{ msg.username }}
|
||||
span.mr-1(v-if="msg.username") •
|
||||
span(v-b-tooltip="", :title="msg.timestamp | date") {{ msg.timestamp | timeAgo }}
|
||||
.text(v-html='atHighlight(parseMarkdown(msg.text))')
|
||||
hr
|
||||
div(v-if='msg.id')
|
||||
.action(@click='like()', v-if='!inbox && msg.likes', :class='{active: msg.likes[user._id]}')
|
||||
.svg-icon(v-html="icons.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)')
|
||||
.d-flex(v-if='msg.id')
|
||||
.action.d-flex.align-items-center(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)')
|
||||
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")
|
||||
| {{$t('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
|
||||
span.action(v-if='msg.uuid === user._id || inbox || user.contributor.admin', @click='remove()')
|
||||
.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')}}
|
||||
span.action.float-right.liked(v-if='likeCount > 0')
|
||||
.svg-icon(v-html="icons.liked")
|
||||
.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') }}
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.at-highlight {
|
||||
background-color: rgba(213, 200, 255, 0.32);
|
||||
color: #6133b4;
|
||||
padding: 0.1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
@import '~client/assets/scss/tiers.scss';
|
||||
|
||||
.mentioned-icon {
|
||||
@@ -54,7 +67,14 @@ div
|
||||
color: red;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.75rem 1.25rem 0.75rem 1.25rem;
|
||||
|
||||
.leader {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -62,6 +82,7 @@ div
|
||||
h3 { // this is the user name
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
@@ -73,13 +94,15 @@ div
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: #878190;
|
||||
width: 150px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
color: #4e4a57;
|
||||
text-align: left !important;
|
||||
min-height: 0rem;
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,25 +110,25 @@ div
|
||||
display: inline-block;
|
||||
color: #878190;
|
||||
margin-right: 1em;
|
||||
}
|
||||
font-size: 12px;
|
||||
|
||||
.action:hover {
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.liked:hover {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.action .svg-icon {
|
||||
.svg-icon {
|
||||
color: #A5A1AC;
|
||||
margin-right: .2em;
|
||||
width: 16px;
|
||||
display: inline-block;
|
||||
color: #A5A1AC;
|
||||
}
|
||||
}
|
||||
|
||||
.action.active, .active .svg-icon {
|
||||
color: #46a7d9
|
||||
.active {
|
||||
color: $purple-300;
|
||||
|
||||
.svg-icon {
|
||||
color: $purple-400;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -114,8 +137,9 @@ import axios from 'axios';
|
||||
import moment from 'moment';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
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 styleHelper from 'client/mixins/styleHelper';
|
||||
|
||||
@@ -161,9 +185,6 @@ export default {
|
||||
}),
|
||||
};
|
||||
},
|
||||
directives: {
|
||||
markdown: markdownDirective,
|
||||
},
|
||||
filters: {
|
||||
timeAgo (value) {
|
||||
return moment(value).fromNow();
|
||||
@@ -177,23 +198,24 @@ export default {
|
||||
...mapState({user: 'user.data'}),
|
||||
isUserMentioned () {
|
||||
const message = this.msg;
|
||||
let user = this.user;
|
||||
const user = this.user;
|
||||
|
||||
if (message.hasOwnProperty('highlight')) return message.highlight;
|
||||
|
||||
message.highlight = false;
|
||||
let messagetext = message.text.toLowerCase();
|
||||
let username = user.profile.name;
|
||||
let mentioned = messagetext.indexOf(username.toLowerCase());
|
||||
let escapedUsername = escapeRegExp(username);
|
||||
let pattern = `@${escapedUsername}([^\w]|$){1}`;
|
||||
|
||||
const messageText = message.text.toLowerCase();
|
||||
const displayName = user.profile.name;
|
||||
const username = user.auth.local && user.auth.local.username;
|
||||
const mentioned = max([messageText.indexOf(username.toLowerCase()), messageText.indexOf(displayName.toLowerCase())]);
|
||||
if (mentioned === -1) return message.highlight;
|
||||
|
||||
let preceedingchar = messagetext.substring(mentioned - 1, mentioned);
|
||||
if (mentioned === 0 || preceedingchar.trim() === '' || preceedingchar === '@') {
|
||||
const escapedDisplayName = escapeRegExp(displayName);
|
||||
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');
|
||||
message.highlight = regex.test(messagetext);
|
||||
message.highlight = regex.test(messageText);
|
||||
}
|
||||
|
||||
return message.highlight;
|
||||
@@ -209,12 +231,6 @@ export default {
|
||||
}
|
||||
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 () {
|
||||
const message = this.msg;
|
||||
const isNPC = Boolean(message.backer && message.backer.npc);
|
||||
@@ -244,6 +260,10 @@ export default {
|
||||
}
|
||||
|
||||
this.$emit('message-liked', message);
|
||||
this.$root.$emit('bv::hide::tooltip');
|
||||
},
|
||||
likeTooltip (likedStatus) {
|
||||
if (!likedStatus) return this.$t('like');
|
||||
},
|
||||
copyAsTodo (message) {
|
||||
this.$root.$emit('habitica::copy-as-todo', message);
|
||||
@@ -273,6 +293,14 @@ export default {
|
||||
showMemberModal (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>
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
v-if='msg.userStyles || (cachedProfileData[msg.uuid] && !cachedProfileData[msg.uuid].rejected)',
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid]",
|
||||
:avatarOnly="true",
|
||||
:overrideTopPadding='"14px"',
|
||||
:hideClassBadge='true',
|
||||
@click.native="showMemberModal(msg.uuid)",
|
||||
)
|
||||
@@ -40,6 +41,7 @@
|
||||
:member="msg.userStyles || cachedProfileData[msg.uuid]",
|
||||
:avatarOnly="true",
|
||||
:hideClassBadge='true',
|
||||
:overrideTopPadding='"14px"',
|
||||
@click.native="showMemberModal(msg.uuid)",
|
||||
)
|
||||
</template>
|
||||
@@ -47,6 +49,10 @@
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
|
||||
.avatar {
|
||||
margin-left: -1.75rem;
|
||||
}
|
||||
|
||||
.hr {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
@@ -70,7 +76,9 @@
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 0px;
|
||||
margin-bottom: .5em;
|
||||
padding: 0rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
h3(v-once) {{$t('welcomeTo')}}
|
||||
.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
|
||||
.user-creation-bg(v-if='!editing')
|
||||
avatar(:member='user', :avatarOnly='!editing', :class='{"edit-avatar": editing}')
|
||||
@@ -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")
|
||||
.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')
|
||||
span.price 15
|
||||
.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')
|
||||
.section.row
|
||||
.col-12.text-center
|
||||
h2 I want to work on:
|
||||
h2 {{ $t('wantToWorkOn') }}
|
||||
.section.row
|
||||
.col-6
|
||||
.task-option
|
||||
@@ -353,24 +353,31 @@ 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')
|
||||
label.custom-control-label(v-once, for="self_care") {{ $t('self_care') }}
|
||||
|
||||
.section.row.justin-message-section(:class='{top: modalPage > 1}', v-if='!editing')
|
||||
.col-12
|
||||
.justin-message.d-flex.flex-column.justify-content-center
|
||||
.section.d-flex.justify-content-center(:class='{top: modalPage > 1}', v-if='!editing')
|
||||
.justin-section.d-flex.align-items-center
|
||||
.featured-label
|
||||
span.rectangle
|
||||
span.text Justin
|
||||
span.rectangle
|
||||
.npc_justin_textbox
|
||||
.justin-message
|
||||
.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')
|
||||
p(v-once) {{$t('justinIntroMessage1')}}
|
||||
p(v-once) {{$t('justinIntroMessage2')}}
|
||||
p(v-once, v-html='$t("justinIntroMessage1")')
|
||||
p(v-once) {{ $t('justinIntroMessageUsername') }}
|
||||
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')
|
||||
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')
|
||||
.row
|
||||
.section.container.footer
|
||||
.row(v-if='!editing && !(modalPage === 1)')
|
||||
.col-3.offset-1.text-center
|
||||
div(v-if='modalPage > 1', @click='prev()')
|
||||
.prev-arrow
|
||||
@@ -390,12 +397,9 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
<style>
|
||||
/* @TODO do not rely on avatar-modal___BV_modal_body_,
|
||||
it already changed once when bootstrap-vue reached version 1 */
|
||||
.page-2 #avatar-modal___BV_modal_body_ {
|
||||
margin-top: 9em;
|
||||
}
|
||||
|
||||
.page-2 .modal-content {
|
||||
margin-top: 7em;
|
||||
.page-2 #avatar-modal___BV_modal_body_ {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
#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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
@@ -444,7 +467,7 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
|
||||
.logo {
|
||||
width: 190px;
|
||||
margin: 0 auto;
|
||||
margin: 0 auto 1.25em;
|
||||
}
|
||||
|
||||
.user-creation-bg {
|
||||
@@ -464,32 +487,22 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
left: 9.2em;
|
||||
}
|
||||
|
||||
.justin-message {
|
||||
background-image: url('~client/assets/svg/for-css/tutorial-border.svg');
|
||||
height: 144px;
|
||||
width: 400px;
|
||||
padding: 2em;
|
||||
margin: 0 auto;
|
||||
.justin-section {
|
||||
position: relative;
|
||||
|
||||
.featured-label {
|
||||
position: absolute;
|
||||
top: -1em;
|
||||
|
||||
.text {
|
||||
min-height: auto;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.npc_justin_textbox {
|
||||
position: absolute;
|
||||
right: 1em;
|
||||
top: -3.6em;
|
||||
width: 48px;
|
||||
height: 52px;
|
||||
background-image: url('~client/assets/images/justin_textbox.png');
|
||||
}
|
||||
.justin-message {
|
||||
border-color: #ffa623;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
outline-color: #b36213;
|
||||
outline-style: solid;
|
||||
outline-width: 2px;
|
||||
position: relative;
|
||||
padding: 2em;
|
||||
margin: 2px;
|
||||
height: 100%;
|
||||
width: 400px;
|
||||
|
||||
p {
|
||||
margin: auto;
|
||||
@@ -500,15 +513,27 @@ b-modal#avatar-modal(title="", :size='editing ? "lg" : "md"', :hide-header='true
|
||||
}
|
||||
}
|
||||
|
||||
.justin-message-section {
|
||||
margin-top: 4em;
|
||||
margin-bottom: 2em;
|
||||
.npc-justin-textbox {
|
||||
position: absolute;
|
||||
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;
|
||||
top: -16em;
|
||||
left: 3.5em;
|
||||
top: -1rem;
|
||||
left: 1.5rem;
|
||||
border-radius: 2px;
|
||||
margin: auto;
|
||||
|
||||
.text {
|
||||
font-size: 12px;
|
||||
min-height: auto;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.circles {
|
||||
@@ -865,6 +890,7 @@ import get from 'lodash/get';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import avatar from './avatar';
|
||||
import usernameForm from './settings/usernameForm';
|
||||
import { getBackgroundShopSets } from '../../common/script/libs/shops';
|
||||
import unlock from '../../common/script/ops/unlock';
|
||||
import buy from '../../common/script/ops/buy/buy';
|
||||
@@ -1022,6 +1048,7 @@ export default {
|
||||
components: {
|
||||
avatar,
|
||||
toggleSwitch,
|
||||
usernameForm,
|
||||
},
|
||||
mounted () {
|
||||
if (this.editing) this.modalPage = 2;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
v-on:select="selectedAutocomplete",
|
||||
:textbox='textbox',
|
||||
:coords='coords',
|
||||
:caretPosition = 'caretPosition',
|
||||
:chat='group.chat')
|
||||
|
||||
.row.chat-actions
|
||||
@@ -56,6 +57,7 @@
|
||||
data () {
|
||||
return {
|
||||
newMessage: '',
|
||||
caretPosition: 0,
|
||||
chat: {
|
||||
submitDisable: false,
|
||||
submitTimeout: null,
|
||||
@@ -75,7 +77,7 @@
|
||||
methods: {
|
||||
// https://medium.com/@_jh3y/how-to-where-s-the-caret-getting-the-xy-position-of-the-caret-a24ba372990a
|
||||
getCoord (e, text) {
|
||||
let carPos = text.selectionEnd;
|
||||
this.caretPosition = text.selectionEnd;
|
||||
let div = document.createElement('div');
|
||||
let span = document.createElement('span');
|
||||
let copyStyle = getComputedStyle(text);
|
||||
@@ -86,8 +88,8 @@
|
||||
|
||||
div.style.position = 'absolute';
|
||||
document.body.appendChild(div);
|
||||
div.textContent = text.value.substr(0, carPos);
|
||||
span.textContent = text.value.substr(carPos) || '.';
|
||||
div.textContent = text.value.substr(0, this.caretPosition);
|
||||
span.textContent = text.value.substr(this.caretPosition) || '.';
|
||||
div.appendChild(span);
|
||||
this.coords = {
|
||||
TOP: span.offsetTop,
|
||||
@@ -194,7 +196,6 @@
|
||||
width: 100%;
|
||||
background-color: $white;
|
||||
border: solid 1px $gray-400;
|
||||
font-size: 16px;
|
||||
font-style: italic;
|
||||
line-height: 1.43;
|
||||
color: $gray-300;
|
||||
|
||||
@@ -1,152 +1,199 @@
|
||||
<template lang="pug">
|
||||
b-modal#invite-modal(:title="$t('inviteFriends')", size='lg')
|
||||
.modal-body
|
||||
p.alert.alert-info(v-html="$t('inviteAlertInfo')")
|
||||
.form-horizontal
|
||||
table.table.table-striped
|
||||
thead
|
||||
tr
|
||||
th {{ $t('userId') }}
|
||||
tbody
|
||||
tr(v-for='user in invitees')
|
||||
td
|
||||
input.form-control(type='text', v-model='user.uuid')
|
||||
tr
|
||||
td
|
||||
button.btn.btn-primary.pull-right(@click='addUuid()')
|
||||
i.glyphicon.glyphicon-plus
|
||||
| +
|
||||
tr
|
||||
td
|
||||
.col-6.col-offset-6
|
||||
button.btn.btn-primary.btn-block(@click='inviteNewUsers("uuid")') {{sendInviteText}}
|
||||
hr
|
||||
p.alert.alert-info {{ $t('inviteByEmail') }}
|
||||
.form-horizontal
|
||||
table.table.table-striped
|
||||
thead
|
||||
tr
|
||||
th {{ $t('name') }}
|
||||
th {{ $t('email') }}
|
||||
tbody
|
||||
tr(v-for='email in emails')
|
||||
td
|
||||
input.form-control(type='text', v-model='email.name')
|
||||
td
|
||||
input.form-control(type='email', v-model='email.email')
|
||||
tr
|
||||
td(colspan=2)
|
||||
button.btn.btn-primary.pull-right(@click='addEmail()')
|
||||
i.glyphicon.glyphicon-plus
|
||||
| +
|
||||
tr
|
||||
td.form-group(colspan=2)
|
||||
label.col-sm-1.control-label {{ $t('byColon') }}
|
||||
.col-sm-5
|
||||
input.form-control(type='text', v-model='inviter')
|
||||
.col-sm-6
|
||||
button.btn.btn-primary.btn-block(@click='inviteNewUsers("email")') {{sendInviteText}}
|
||||
b-modal#invite-modal(:title='$t(`inviteTo${groupType}`)', :hide-footer='true')
|
||||
div
|
||||
strong {{ $t('inviteEmailUsername') }}
|
||||
.small {{ $t('inviteEmailUsernameInfo') }}
|
||||
div(v-for='(invite, index) in invites')
|
||||
.input-group
|
||||
.d-flex.align-items-center.justify-content-center(v-if='index === invites.length - 1 && invite.text.length === 0')
|
||||
.svg-icon.positive-icon(v-html='icons.positiveIcon')
|
||||
input.form-control(
|
||||
type='text',
|
||||
:placeholder='$t("emailOrUsernameInvite")',
|
||||
v-model='invite.text',
|
||||
v-on:keyup='expandInviteList',
|
||||
v-on:change='checkInviteList',
|
||||
:class='{"input-valid": invite.valid, "is-invalid input-invalid": invite.valid === false}',
|
||||
)
|
||||
.input-error.text-center.mt-2(v-if="invite.error") {{ invite.error }}
|
||||
.modal-footer.d-flex.justify-content-center
|
||||
a.mr-3(@click='close()') {{ $t('cancel') }}
|
||||
button.btn.btn-primary(@click='sendInvites()', :class='{disabled: cannotSubmit}', :disabled='cannotSubmit') {{ $t('sendInvitations') }}
|
||||
</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>
|
||||
import { mapState } from 'client/libs/store';
|
||||
|
||||
import clone from 'lodash/clone';
|
||||
import debounce from 'lodash/debounce';
|
||||
import filter from 'lodash/filter';
|
||||
import map from 'lodash/map';
|
||||
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';
|
||||
|
||||
const INVITE_DEFAULTS = {text: '', error: null, valid: null};
|
||||
|
||||
export default {
|
||||
mixins: [notifications],
|
||||
props: ['group'],
|
||||
data () {
|
||||
return {
|
||||
invitees: [],
|
||||
emails: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
cannotSubmit () {
|
||||
const filteredInvites = filter(this.invites, (invite) => {
|
||||
return invite.text.length > 0 && !invite.valid;
|
||||
});
|
||||
if (filteredInvites.length > 0) return true;
|
||||
},
|
||||
inviter () {
|
||||
return this.user.profile.name;
|
||||
},
|
||||
sendInviteText () {
|
||||
return 'Send Invites';
|
||||
// if (!this.group) return 'Send Invites';
|
||||
// return this.group.sendInviteText;
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
invites: [clone(INVITE_DEFAULTS), clone(INVITE_DEFAULTS)],
|
||||
icons: Object.freeze({
|
||||
positiveIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
addUuid () {
|
||||
this.invitees.push({uuid: ''});
|
||||
},
|
||||
addEmail () {
|
||||
this.emails.push({name: '', email: ''});
|
||||
},
|
||||
inviteNewUsers (inviteMethod) {
|
||||
if (!this.group._id) {
|
||||
if (!this.group.name) this.group.name = this.$t('possessiveParty', {name: this.user.profile.name});
|
||||
|
||||
// @TODO: Add dispatch
|
||||
// return Groups.Group.create(this.group)
|
||||
// .then(function(response) {
|
||||
// this.group = response.data.data;
|
||||
// _inviteByMethod(inviteMethod);
|
||||
// });
|
||||
checkInviteList: debounce(function checkList () {
|
||||
this.invites = filter(this.invites, (invite, index) => {
|
||||
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);
|
||||
}
|
||||
|
||||
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 };
|
||||
if (isUUID(value.text)) {
|
||||
this.$store.dispatch('user:userLookup', {uuid: value.text})
|
||||
.then(res => {
|
||||
return this.fillErrors(index, res);
|
||||
});
|
||||
} else {
|
||||
return alert('Invalid invite method.');
|
||||
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,
|
||||
});
|
||||
|
||||
let invitesSent = invitationDetails.emails || invitationDetails.uuids;
|
||||
let invitationString = invitesSent.length > 1 ? 'invitationsSent' : 'invitationSent';
|
||||
const invitesSent = invitationDetails.emails.length + invitationDetails.uuids.length + invitationDetails.usernames.length;
|
||||
let invitationString = invitesSent > 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;
|
||||
this.close();
|
||||
},
|
||||
},
|
||||
mixins: [notifications],
|
||||
props: ['group', 'groupType'],
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -355,7 +355,10 @@ export default {
|
||||
sendMessage (member) {
|
||||
this.$root.$emit('habitica::new-inbox-message', {
|
||||
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 = '') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template lang="pug">
|
||||
div
|
||||
invite-modal(:group='inviteModalGroup')
|
||||
invite-modal(:group='inviteModalGroup', :groupType='inviteModalGroupType')
|
||||
create-party-modal
|
||||
#app-header.row(:class="{'hide-header': $route.name === 'groupPlan'}")
|
||||
members-modal(:hide-badge="true")
|
||||
@@ -115,6 +115,7 @@ export default {
|
||||
expandedMember: null,
|
||||
currentWidth: 0,
|
||||
inviteModalGroup: undefined,
|
||||
inviteModalGroupType: undefined,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -178,6 +179,7 @@ export default {
|
||||
mounted () {
|
||||
this.$root.$on('inviteModal::inviteToGroup', (group) => {
|
||||
this.inviteModalGroup = group;
|
||||
this.inviteModalGroupType = group.type === 'guild' ? 'Guild' : 'Party';
|
||||
this.$root.$emit('bv::show::modal', 'invite-modal');
|
||||
});
|
||||
},
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
| {{member.profile.name}}
|
||||
.is-buffed(v-if="isBuffed", v-b-tooltip.hover.bottom="$t('buffed')")
|
||||
.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')")
|
||||
.svg-icon(v-html="icons.health")
|
||||
.progress
|
||||
|
||||
@@ -25,6 +25,7 @@ div
|
||||
login-incentives(:data='notificationData')
|
||||
quest-completed
|
||||
quest-invitation
|
||||
verify-username
|
||||
</template>
|
||||
|
||||
<style lang='scss'>
|
||||
@@ -118,6 +119,7 @@ import streak from './achievements/streak';
|
||||
import ultimateGear from './achievements/ultimateGear';
|
||||
import wonChallenge from './achievements/wonChallenge';
|
||||
import loginIncentives from './achievements/login-incentives';
|
||||
import verifyUsername from './settings/verifyUsername';
|
||||
|
||||
const NOTIFICATIONS = {
|
||||
CHALLENGE_JOINED_ACHIEVEMENT: {
|
||||
@@ -178,6 +180,7 @@ export default {
|
||||
dropsEnabled,
|
||||
contributor,
|
||||
loginIncentives,
|
||||
verifyUsername,
|
||||
},
|
||||
data () {
|
||||
// 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('tasks:fetchUserTasks'),
|
||||
]).then(() => {
|
||||
this.debounceCheckUserAchievements();
|
||||
|
||||
// @TODO: This is a timeout to ensure dom is loaded
|
||||
window.setTimeout(() => {
|
||||
this.initTour();
|
||||
if (this.user.flags.tour.intro === this.TOUR_END || !this.user.flags.welcomed) return;
|
||||
this.goto('intro', 0);
|
||||
this.runForcedModals();
|
||||
}, 2000);
|
||||
|
||||
this.runYesterDailies();
|
||||
this.debounceCheckUserAchievements();
|
||||
|
||||
// Do not remove the event listener as it's live for the entire app lifetime
|
||||
document.addEventListener('mousemove', this.checkNextCron);
|
||||
@@ -339,6 +338,11 @@ export default {
|
||||
document.removeEventListener('keydown', this.checkNextCron);
|
||||
},
|
||||
methods: {
|
||||
runForcedModals () {
|
||||
if (!this.user.flags.verifiedUsername) return this.$root.$emit('bv::show::modal', 'verify-username');
|
||||
|
||||
return this.runYesterDailies();
|
||||
},
|
||||
showDeathModal () {
|
||||
this.playSound('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
|
||||
if (!this.user.flags.welcomed) {
|
||||
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) {
|
||||
this.showDeathModal();
|
||||
return this.showDeathModal();
|
||||
}
|
||||
|
||||
if (this.questCompleted) {
|
||||
this.$root.$emit('bv::show::modal', 'quest-completed');
|
||||
this.playSound('Achievement_Unlocked');
|
||||
return this.$root.$emit('bv::show::modal', 'quest-completed');
|
||||
}
|
||||
|
||||
if (this.userClassSelect) {
|
||||
this.$root.$emit('bv::show::modal', 'choose-class');
|
||||
this.playSound('Achievement_Unlocked');
|
||||
return this.$root.$emit('bv::show::modal', 'choose-class');
|
||||
}
|
||||
},
|
||||
showLevelUpNotifications (newlevel) {
|
||||
@@ -520,10 +528,6 @@ export default {
|
||||
async handleUserNotifications (after) {
|
||||
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;
|
||||
|
||||
let notificationsToRead = [];
|
||||
|
||||
@@ -130,8 +130,10 @@
|
||||
h5 {{ $t('changeDisplayName') }}
|
||||
.form(name='changeDisplayName', novalidate)
|
||||
.form-group
|
||||
input#changeDisplayname.form-control(type='text', :placeholder="$t('newDisplayName')", v-model='temporaryDisplayName')
|
||||
button.btn.btn-primary(type='submit', @click='changeDisplayName(temporaryDisplayName)') {{ $t('submit') }}
|
||||
input#changeDisplayname.form-control(type='text', :placeholder="$t('newDisplayName')", v-model='temporaryDisplayName', :class='{"is-invalid input-invalid": displayNameInvalid}')
|
||||
.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') }}
|
||||
.form(name='changeUsername', novalidate)
|
||||
@@ -252,6 +254,7 @@ export default {
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
},
|
||||
displayNameIssues: [],
|
||||
usernameIssues: [],
|
||||
};
|
||||
},
|
||||
@@ -312,6 +315,18 @@ export default {
|
||||
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 () {
|
||||
if (this.usernameUpdates.username.length <= 1) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
@@ -332,10 +347,30 @@ export default {
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
temporaryDisplayName: {
|
||||
handler () {
|
||||
this.validateDisplayName(this.temporaryDisplayName);
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// eslint-disable-next-line func-names
|
||||
validateUsername: debounce(function (username) {
|
||||
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;
|
||||
|
||||
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;
|
||||
right: 10px;
|
||||
width: 350px;
|
||||
z-index: 1070; // 1070 is above modal backgrounds
|
||||
z-index: 1400; // 1400 is above modal backgrounds
|
||||
|
||||
&-top-pos {
|
||||
&-normal {
|
||||
|
||||
@@ -560,6 +560,7 @@
|
||||
<script>
|
||||
import hello from 'hellojs';
|
||||
import debounce from 'lodash/debounce';
|
||||
import isEmail from 'validator/lib/isEmail';
|
||||
import googlePlay from 'assets/images/home/google-play-badge.svg';
|
||||
import iosAppStore from 'assets/images/home/ios-app-store.svg';
|
||||
import iphones from 'assets/images/home/iphones.svg';
|
||||
@@ -626,18 +627,18 @@
|
||||
computed: {
|
||||
emailValid () {
|
||||
if (this.email.length <= 3) return false;
|
||||
return this.validateEmail(this.email);
|
||||
return isEmail(this.email);
|
||||
},
|
||||
emailInvalid () {
|
||||
if (this.email.length <= 3) return false;
|
||||
return !this.validateEmail(this.email);
|
||||
return !isEmail(this.email);
|
||||
},
|
||||
usernameValid () {
|
||||
if (this.username.length <= 3) return false;
|
||||
if (this.username.length < 1) return false;
|
||||
return this.usernameIssues.length === 0;
|
||||
},
|
||||
usernameInvalid () {
|
||||
if (this.username.length <= 3) return false;
|
||||
if (this.username.length < 1) return false;
|
||||
return !this.usernameValid;
|
||||
},
|
||||
passwordConfirmValid () {
|
||||
@@ -655,13 +656,9 @@
|
||||
},
|
||||
},
|
||||
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
|
||||
validateUsername: debounce(function (username) {
|
||||
if (username.length <= 3) {
|
||||
if (username.length < 1) {
|
||||
return;
|
||||
}
|
||||
this.$store.dispatch('auth:verifyUsername', {
|
||||
|
||||
@@ -30,8 +30,11 @@
|
||||
.conversation(v-for='conversation in filtersConversations', @click='selectConversation(conversation.key)',
|
||||
:class="{active: selectedConversation.key === conversation.key}")
|
||||
div
|
||||
span(:class="userLevelStyle(conversation)") {{conversation.name}}
|
||||
span.timeago {{conversation.date | timeAgo}}
|
||||
h3(:class="userLevelStyle(conversation)") {{ conversation.name }}
|
||||
.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) : ''}}
|
||||
.col-8.messages.d-flex.flex-column.justify-content-between
|
||||
.empty-messages.text-center(v-if='!selectedConversation.key')
|
||||
@@ -63,6 +66,7 @@
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import '~client/assets/scss/colors.scss';
|
||||
@import '~client/assets/scss/tiers.scss';
|
||||
|
||||
.header-wrap {
|
||||
padding: 0.5em;
|
||||
@@ -73,6 +77,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0rem;
|
||||
|
||||
.svg-icon {
|
||||
width: 10px;
|
||||
display: inline-block;
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.envelope {
|
||||
color: $gray-400 !important;
|
||||
margin: 0;
|
||||
@@ -180,11 +194,6 @@
|
||||
.conversation {
|
||||
padding: 1.5em;
|
||||
background: $white;
|
||||
height: 80px;
|
||||
|
||||
.timeago {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.conversation.active {
|
||||
@@ -194,6 +203,12 @@
|
||||
.conversation:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: $gray-200;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
@@ -207,9 +222,19 @@ import styleHelper from 'client/mixins/styleHelper';
|
||||
import toggleSwitch from 'client/components/ui/toggleSwitch';
|
||||
import axios from 'axios';
|
||||
|
||||
import messageIcon from 'assets/svg/message.svg';
|
||||
import chatMessages from '../chat/chatMessages';
|
||||
import messageIcon from 'assets/svg/message.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 {
|
||||
mixins: [styleHelper],
|
||||
@@ -236,8 +261,11 @@ export default {
|
||||
}
|
||||
|
||||
this.initiatedConversation = {
|
||||
user: data.userName,
|
||||
uuid: data.userIdToMessage,
|
||||
user: data.displayName,
|
||||
username: data.username,
|
||||
backer: data.backer,
|
||||
contributor: data.contributor,
|
||||
};
|
||||
|
||||
this.selectConversation(data.userIdToMessage);
|
||||
@@ -252,6 +280,16 @@ export default {
|
||||
icons: Object.freeze({
|
||||
messageIcon,
|
||||
svgClose,
|
||||
tier1,
|
||||
tier2,
|
||||
tier3,
|
||||
tier4,
|
||||
tier5,
|
||||
tier6,
|
||||
tier7,
|
||||
tier8,
|
||||
tier9,
|
||||
tierNPC,
|
||||
}),
|
||||
displayCreate: true,
|
||||
selectedConversation: {},
|
||||
@@ -276,10 +314,11 @@ export default {
|
||||
// Add placeholder for new conversations
|
||||
if (this.initiatedConversation && this.initiatedConversation.uuid) {
|
||||
inboxGroup[this.initiatedConversation.uuid] = [{
|
||||
uuid: this.initiatedConversation.uuid,
|
||||
user: this.initiatedConversation.user,
|
||||
username: this.initiatedConversation.username,
|
||||
id: '',
|
||||
text: '',
|
||||
user: this.initiatedConversation.user,
|
||||
uuid: this.initiatedConversation.uuid,
|
||||
timestamp: new Date(),
|
||||
}];
|
||||
}
|
||||
@@ -296,8 +335,12 @@ export default {
|
||||
if (newChat.sent) {
|
||||
newChat.toUUID = newChat.uuid;
|
||||
newChat.toUser = newChat.user;
|
||||
newChat.toUserName = newChat.username;
|
||||
newChat.toUserContributor = newChat.contributor;
|
||||
newChat.toUserBacker = newChat.backer;
|
||||
newChat.uuid = this.user._id;
|
||||
newChat.user = this.user.profile.name;
|
||||
newChat.username = this.user.auth.local.username;
|
||||
newChat.contributor = this.user.contributor;
|
||||
newChat.backer = this.user.backer;
|
||||
}
|
||||
@@ -309,11 +352,12 @@ export default {
|
||||
if (!recentMessage.text) newChatModels.splice(newChatModels.length - 1, 1);
|
||||
|
||||
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,
|
||||
messages: newChatModels,
|
||||
lastMessageText: recentMessage.text,
|
||||
name: recentMessage.toUser ? recentMessage.toUser : recentMessage.user, // Handles case where from user sent the only message or the to user sent the only message
|
||||
username: !recentMessage.text ? recentMessage.username : recentMessage.toUserName,
|
||||
date: recentMessage.timestamp,
|
||||
lastMessageText: recentMessage.text,
|
||||
messages: newChatModels,
|
||||
};
|
||||
|
||||
convos.push(convoModel);
|
||||
@@ -382,8 +426,11 @@ export default {
|
||||
const messageIndex = this.messages.findIndex(msg => msg.id === message.id);
|
||||
if (messageIndex !== -1) this.messages.splice(messageIndex, 1);
|
||||
if (this.selectedConversationMessages.length === 0) this.initiatedConversation = {
|
||||
user: this.selectedConversation.name,
|
||||
uuid: this.selectedConversation.key,
|
||||
user: this.selectedConversation.name,
|
||||
username: this.selectedConversation.username,
|
||||
backer: this.selectedConversation.backer,
|
||||
contributor: this.selectedConversation.contributor,
|
||||
};
|
||||
},
|
||||
toggleClick () {
|
||||
@@ -413,6 +460,7 @@ export default {
|
||||
text: this.newMessage,
|
||||
timestamp: new Date(),
|
||||
user: this.selectedConversation.name,
|
||||
username: this.selectedConversation.username,
|
||||
uuid: this.selectedConversation.key,
|
||||
contributor: this.user.contributor,
|
||||
});
|
||||
@@ -444,6 +492,14 @@ export default {
|
||||
close () {
|
||||
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>
|
||||
|
||||
@@ -40,11 +40,12 @@ div
|
||||
#userProfile.standard-page(v-show='selectedPage === "profile"', v-if='user.profile')
|
||||
.row
|
||||
.col-12.col-md-8
|
||||
.header
|
||||
.header.mb-3
|
||||
h1 {{user.profile.name}}
|
||||
h4
|
||||
strong {{ $t('userId') }}:
|
||||
| {{user._id}}
|
||||
div
|
||||
.name(v-if='user.auth && user.auth.local && user.auth.local.username') @{{ user.auth.local.username }}
|
||||
div
|
||||
.name {{ user._id }}
|
||||
.col-12.col-md-4
|
||||
button.btn.btn-secondary(v-if='user._id === userLoggedIn._id', @click='editing = !editing') {{ $t('edit') }}
|
||||
.row(v-if='!editing')
|
||||
@@ -146,7 +147,7 @@ div
|
||||
#profile {
|
||||
.member-details {
|
||||
.character-name, small, .small-text {
|
||||
color: #878190
|
||||
color: #878190;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +194,7 @@ div
|
||||
.gift-icon {
|
||||
width: 14px;
|
||||
margin: auto;
|
||||
color: #686274;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.gift-icon {
|
||||
@@ -202,12 +203,12 @@ div
|
||||
|
||||
.remove-icon {
|
||||
width: 16px;
|
||||
color: #686274;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.positive-icon {
|
||||
width: 14px;
|
||||
color: #686274;
|
||||
color: $gray-100;
|
||||
}
|
||||
|
||||
.photo img {
|
||||
@@ -216,11 +217,12 @@ div
|
||||
|
||||
.header {
|
||||
h1 {
|
||||
color: #4f2a93;
|
||||
color: $purple-200;
|
||||
margin-bottom: 0rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: #686274;
|
||||
color: $gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +243,11 @@ div
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.name {
|
||||
color: $gray-200;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#achievements {
|
||||
.box {
|
||||
margin: 0 auto;
|
||||
@@ -463,7 +470,10 @@ export default {
|
||||
sendMessage () {
|
||||
this.$root.$emit('habitica::new-inbox-message', {
|
||||
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 () {
|
||||
|
||||
@@ -55,6 +55,15 @@ export async function verifyUsername (store, params) {
|
||||
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) {
|
||||
let url = '/api/v4/user/auth/social';
|
||||
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`, {
|
||||
uuids: payload.invitationDetails.uuids,
|
||||
emails: payload.invitationDetails.emails,
|
||||
usernames: payload.invitationDetails.usernames,
|
||||
});
|
||||
|
||||
// @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;
|
||||
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.",
|
||||
"other": "Other",
|
||||
"fullName": "Full Name",
|
||||
"displayName": "Display Name",
|
||||
"displayName": "Display name",
|
||||
"changeDisplayName": "Change Display Name",
|
||||
"newDisplayName": "New Display Name",
|
||||
"displayPhoto": "Photo",
|
||||
|
||||
@@ -271,15 +271,9 @@
|
||||
"emailTaken": "Email address is already used in an account.",
|
||||
"newEmailRequired": "Missing new email address.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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 <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.",
|
||||
"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.",
|
||||
"invalidLoginCredentials": "Incorrect username and/or email and/or password.",
|
||||
"passwordResetPage": "Reset Password",
|
||||
@@ -335,7 +329,7 @@
|
||||
"joinToday": "Join Habitica Today",
|
||||
"featuredIn": "Featured in",
|
||||
"signup": "Sign Up",
|
||||
"getStarted": "Get Started",
|
||||
"getStarted": "Get Started!",
|
||||
"mobileApps": "Mobile Apps",
|
||||
"learnMore": "Learn More"
|
||||
}
|
||||
|
||||
@@ -254,6 +254,7 @@
|
||||
"resetFilters": "Clear all filters",
|
||||
"applyFilters": "Apply Filters",
|
||||
|
||||
"wantToWorkOn": "I want to work on:",
|
||||
"categories": "Categories",
|
||||
"habiticaOfficial": "Habitica Official",
|
||||
"animals": "Animals",
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
"inviteExistUser": "Invite Existing Users",
|
||||
"byColon": "By:",
|
||||
"inviteNewUsers": "Invite New Users",
|
||||
"sendInvitations": "Send Invitations",
|
||||
"sendInvitations": "Send Invites",
|
||||
"invitationsSent": "Invitations sent!",
|
||||
"invitationSent": "Invitation sent!",
|
||||
"invitedFriend": "Invited a Friend",
|
||||
@@ -229,7 +229,7 @@
|
||||
"memberCannotRemoveYourself": "You cannot remove yourself!",
|
||||
"groupMemberNotFound": "User not found among group's members",
|
||||
"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.",
|
||||
"inviteMissingUuid": "Missing user id in invite",
|
||||
"inviteMustNotBeEmpty": "Invite must not be empty.",
|
||||
@@ -241,9 +241,11 @@
|
||||
"userAlreadyPendingInvitation": "UserID: <%= userId %>, User \"<%= username %>\" already pending invitation.",
|
||||
"userAlreadyInAParty": "UserID: <%= userId %>, User \"<%= username %>\" already in a party. ",
|
||||
"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).",
|
||||
"uuidsMustBeAnArray": "User ID 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",
|
||||
"partyExceedsMembersLimit": "Party size is limited to <%= maxMembersParty %> members",
|
||||
"onlyCreatorOrAdminCanDeleteChat": "Not authorized to delete this message!",
|
||||
@@ -366,6 +368,10 @@
|
||||
"liked": "Liked",
|
||||
"joinGuild": "Join 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",
|
||||
"donateGems": "Donate Gems",
|
||||
"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!",
|
||||
|
||||
"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",
|
||||
"welcomeBack": "Welcome back!",
|
||||
"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.",
|
||||
"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!",
|
||||
"prev": "Prev",
|
||||
"next": "Next",
|
||||
|
||||
@@ -201,9 +201,10 @@
|
||||
"usernameIssueInvalidCharacters": "Usernames can only contain letters a to z, numbers 0 to 9, hyphens, or underscores.",
|
||||
"currentUsername": "Current username:",
|
||||
"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",
|
||||
"usernameVerifiedConfirmation": "Your username, <%= username %>, is confirmed!",
|
||||
"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?",
|
||||
"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>.",
|
||||
"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.",
|
||||
"gemsRemaining": "gems remaining",
|
||||
"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,
|
||||
nameFields,
|
||||
} from '../../models/user';
|
||||
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
|
||||
import {
|
||||
NotFound,
|
||||
BadRequest,
|
||||
@@ -17,8 +16,11 @@ import {
|
||||
} from '../../libs/errors';
|
||||
import { removeFromArray } from '../../libs/collectionManipulators';
|
||||
import { sendTxn as sendTxnEmail } from '../../libs/email';
|
||||
import { encrypt } from '../../libs/encryption';
|
||||
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
|
||||
import {
|
||||
inviteByUUID,
|
||||
inviteByEmail,
|
||||
inviteByUserName,
|
||||
} from '../../libs/invites';
|
||||
import common from '../../../common';
|
||||
import payments from '../../libs/payments/payments';
|
||||
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
|
||||
* @apiName InviteToGroup
|
||||
@@ -1147,7 +1007,7 @@ api.inviteToGroup = {
|
||||
url: '/groups/:groupId/invite',
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
const user = res.locals.user;
|
||||
|
||||
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 }));
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
const validationErrors = req.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.purchased && group.purchased.plan.customerId && user._id !== group.leader) throw new NotAuthorized(res.t('onlyGroupLeaderCanInviteToGroupPlan'));
|
||||
|
||||
let uuids = req.body.uuids;
|
||||
let emails = req.body.emails;
|
||||
const {
|
||||
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) {
|
||||
let uuidInvites = uuids.map((uuid) => _inviteByUUID(uuid, group, user, req, res));
|
||||
let uuidResults = await Promise.all(uuidInvites);
|
||||
const uuidInvites = uuids.map((uuid) => inviteByUUID(uuid, group, user, req, res));
|
||||
const uuidResults = await Promise.all(uuidInvites);
|
||||
results.push(...uuidResults);
|
||||
}
|
||||
|
||||
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;
|
||||
await user.save();
|
||||
let emailResults = await Promise.all(emailInvites);
|
||||
const emailResults = await Promise.all(emailInvites);
|
||||
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 = {
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
|
||||
@@ -109,6 +109,36 @@ api.getMember = {
|
||||
|
||||
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
|
||||
let memberToJSON = member.toJSON({minimize: true});
|
||||
User.addComputedStatsToJSONObj(memberToJSON.stats, member);
|
||||
@@ -605,6 +635,7 @@ api.sendPrivateMessage = {
|
||||
const message = req.body.message;
|
||||
const receiver = await User.findById(req.body.toUserId).exec();
|
||||
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);
|
||||
if (objections.length > 0 && !sender.isAdmin()) throw new NotAuthorized(res.t(objections[0]));
|
||||
|
||||
@@ -28,6 +28,7 @@ api.verifyUsername = {
|
||||
|
||||
const issues = verifyUsername(chosenUsername, res);
|
||||
|
||||
if (issues.length < 1) {
|
||||
const existingUser = await User.findOne({
|
||||
'auth.local.lowerCaseUsername': chosenUsername.toLowerCase(),
|
||||
}, {auth: 1}).exec();
|
||||
@@ -35,6 +36,7 @@ api.verifyUsername = {
|
||||
if (existingUser) {
|
||||
if (!user || existingUser._id !== user._id) issues.push(res.t('usernameTaken'));
|
||||
}
|
||||
}
|
||||
|
||||
if (issues.length > 0) {
|
||||
res.respond(200, { isUsable: false, issues });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import * as userLib from '../../libs/user';
|
||||
import { verifyDisplayName } from '../../libs/user/validation';
|
||||
|
||||
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;
|
||||
|
||||
@@ -79,8 +79,8 @@ async function registerLocal (req, res, { isV3 = false }) {
|
||||
notEmpty: true,
|
||||
errorMessage: res.t('missingUsername'),
|
||||
// TODO use the constants in the error message above
|
||||
isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameWrongLength')},
|
||||
matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameBadCharacters')},
|
||||
isLength: {options: {min: USERNAME_LENGTH_MIN, max: USERNAME_LENGTH_MAX}, errorMessage: res.t('usernameIssueLength')},
|
||||
matches: {options: /^[-_a-zA-Z0-9]+$/, errorMessage: res.t('usernameIssueInvalidCharacters')},
|
||||
},
|
||||
email: {
|
||||
notEmpty: true,
|
||||
@@ -138,6 +138,9 @@ async function registerLocal (req, res, { isV3 = false }) {
|
||||
preferences: {
|
||||
language: req.language,
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
};
|
||||
|
||||
if (existingUser) {
|
||||
@@ -159,8 +162,6 @@ async function registerLocal (req, res, { isV3 = false }) {
|
||||
await _handleGroupInvitation(newUser, req.query.groupInvite || req.query.partyInvite);
|
||||
}
|
||||
|
||||
newUser.flags.verifiedUsername = true;
|
||||
|
||||
let savedUser = await newUser.save();
|
||||
|
||||
let userToJSON;
|
||||
|
||||
@@ -61,6 +61,9 @@ async function loginSocial (req, res) {
|
||||
preferences: {
|
||||
language: req.language,
|
||||
},
|
||||
flags: {
|
||||
verifiedUsername: true,
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
let issues = [];
|
||||
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;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 (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;
|
||||
function getInviteError (uuids, emails, usernames) {
|
||||
const uuidsIsArray = Array.isArray(uuids);
|
||||
const emailsIsArray = Array.isArray(emails);
|
||||
const usernamesIsArray = Array.isArray(usernames);
|
||||
const emptyEmails = emailsIsArray && emails.length < 1;
|
||||
const emptyUuids = uuidsIsArray && uuids.length < 1;
|
||||
const emptyUsernames = usernamesIsArray && usernames.length < 1;
|
||||
|
||||
let errorString;
|
||||
|
||||
if (!uuids && !emails) {
|
||||
if (!uuids && !emails && !usernames) {
|
||||
errorString = 'canOnlyInviteEmailUuid';
|
||||
} else if (uuids && !uuidsIsArray) {
|
||||
errorString = 'uuidsMustBeAnArray';
|
||||
} else if (emails && !emailsIsArray) {
|
||||
errorString = 'emailsMustBeAnArray';
|
||||
} else if (!emails && emptyUuids) {
|
||||
errorString = 'inviteMissingUuid';
|
||||
} else if (!uuids && emptyEmails) {
|
||||
errorString = 'inviteMissingEmail';
|
||||
} else if (emptyEmails && emptyUuids) {
|
||||
} else if (usernames && !usernamesIsArray) {
|
||||
errorString = 'usernamesMustBeAnArray';
|
||||
} else if ((!emails || emptyEmails) && (!uuids || emptyUuids) && (!usernames || emptyUsernames)) {
|
||||
errorString = 'inviteMustNotBeEmpty';
|
||||
}
|
||||
|
||||
if (errorString) {
|
||||
throw new BadRequest(res.t(errorString));
|
||||
return errorString;
|
||||
}
|
||||
|
||||
function getInviteCount (uuids, emails) {
|
||||
let totalInvites = 0;
|
||||
|
||||
if (uuids) {
|
||||
@@ -394,6 +386,27 @@ schema.statics.validateInvitations = async function getInvitationError (uuids, e
|
||||
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) {
|
||||
throw new BadRequest(res.t('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT}));
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ const defaultSchema = () => ({
|
||||
text: String,
|
||||
|
||||
// sender properties
|
||||
user: String, // profile name
|
||||
user: String, // profile name (unfortunately)
|
||||
username: String,
|
||||
contributor: {$type: mongoose.Schema.Types.Mixed},
|
||||
backer: {$type: mongoose.Schema.Types.Mixed},
|
||||
uuid: String, // sender uuid
|
||||
@@ -117,6 +118,7 @@ export function messageDefaults (msg, user) {
|
||||
contributor: user.contributor && user.contributor.toObject(),
|
||||
backer: user.backer && user.backer.toObject(),
|
||||
user: user.profile.name,
|
||||
username: user.flags && user.flags.verifiedUsername && user.auth && user.auth.local && user.auth.local.username,
|
||||
});
|
||||
} else {
|
||||
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)
|
||||
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
|
||||
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
|
||||
export let nameFields = 'profile.name';
|
||||
export let nameFields = 'profile.name auth.local.username flags.verifiedUsername';
|
||||
|
||||
export { schema };
|
||||
|
||||
|
||||
@@ -219,6 +219,9 @@ schema.statics.transformJSONUser = function transformJSONUser (jsonUser, addComp
|
||||
// Add id property
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user