Skip to content

Commit

Permalink
ft(friend request) users can invite and save friends
Browse files Browse the repository at this point in the history
- setup socket.io for notification
- setup endpoints for accepting, rejecting, and unfriending friends
- create a service that joins users to a personal room when they signin
- create modals for notifying invited users
- create modal that shows friend request and frieds
- implement feature that allow users invite friends on game page
- create friends frontend controller to handle friends operations
- add modal to all pages
- add friends notification link to nav bar

[#158457046]
  • Loading branch information
Ben Onah committed Jul 25, 2018
1 parent f6e623c commit 90d71a0
Show file tree
Hide file tree
Showing 22 changed files with 4,718 additions and 3,846 deletions.
179 changes: 169 additions & 10 deletions app/controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,53 @@ import Services from '../logic/user';

const { handleFetchProfile } = Services;


const avatarsArray = avatarsList();
const User = mongoose.model('User');

/**
* @param {user} user the user whose friend list and
* friend request list will be searched
* @param {*} id the id of the finder
* @description this method verifies the friendship status between a
* finder and the user that is found. Friendship status
* can be friends, not friends, or pending
*/
const checkIfFriends = (user, id) => {
let friends = 'not friends';
friends = user.friend_requests.indexOf(mongoose.Types.ObjectId(id)) > -1
? 'pending' : friends;
friends = user.friends.indexOf(mongoose.Types.ObjectId(id)) > -1
? 'friends' : friends;
return friends;
};

/**
* @param {users} users the list of all returned users
* @description this function takes a list of users from the database,
* removes the sensitive fields and returns the list of users
*/
const cleanUpUsers = (users = [], _id) => users.map(user => ({
_id: user._id,
name: user.name,
email: user.email,
avatar: user.avatar
|| 'https://assets.hotukdeals.com/assets/img/profile-placeholder_f56af.png',
friends: checkIfFriends(user, _id),
}));

/**
* @param {users} users the list of all returned users
* @description this function takes a list of users from the database,
* removes the sensitive fields and returns the list of users
*/
const cleanUpFriends = users => users.map(user => ({
_id: user._id,
name: user.name,
email: user.email,
avatar: user.avatar
|| 'https://assets.hotukdeals.com/assets/img/profile-placeholder_f56af.png',
}));

// disabling no underscore because of the default style of mongoose ids
/* eslint no-underscore-dangle: 0, valid-jsdoc: 0 */

Expand All @@ -38,7 +81,8 @@ const authCallback = (req, res) => {
* @param {res} res handles response status code and messages
* @returns {res} a status code and data
* @description this function is called with an Oauth 2 authentication is complete
* the authentication returns a user which info is sent to the client through the url.
* the authentication returns a user which
* info is sent to the client through the url.
*/
const signin = (req, res) => {
if (!req.user) {
Expand Down Expand Up @@ -82,7 +126,8 @@ const handleLogin = (req, res, next) => {
* @param {object} res - response object provided by express
* @param {function} next - next function for passing the request to next handler
* @description Controller for handling requests to '/api/auth/signup',
* returns the token of the user on signup, users Tokenizer to generate the token as well.
* returns the token of the user on signup,
* users Tokenizer to generate the token as well.
*/
const handleSignUp = (req, res, next) => {
// there has to be the email, username and password
Expand Down Expand Up @@ -126,7 +171,8 @@ const handleSignUp = (req, res, next) => {
* @param {object} req - request object provided by express
* @param {object} res - response object provided by express
* @param {function} next - next function for passing the request to next handler
* @description controller for handling requests to get the user profile, expects that a token
* @description controller for handling requests to get
* the user profile, expects that a token
* has been decoded and payload appended to the request object
*/
const fetchProfile = (req, res, next) => handleFetchProfile(req.user._id)
Expand All @@ -141,7 +187,8 @@ const fetchProfile = (req, res, next) => handleFetchProfile(req.user._id)
/**
* @param {object} req - request object from OAUTH callback
* @param {object} res - request object provided by express
* @description This action generates a token after a successful oauth login and sends the token
* @description This action generates a token after
* a successful oauth login and sends the token
* the client to be used for subsequent requests.
*/

Expand Down Expand Up @@ -235,7 +282,8 @@ const create = (req, res, next) => {
/**
* @param {object} req - request object provided by express
* @param {object} res - response object provided by express
* @description controller handling uploading choosing avatars on POST '/api/users/avatars'
* @description controller handling uploading
* choosing avatars on POST '/api/users/avatars'
*/
const avatars = (req, res) => {
// Update the current user's profile to include the avatar choice they've made
Expand All @@ -256,7 +304,8 @@ const avatars = (req, res) => {
/**
* @param {object} req - request object provided by express
* @param {object} res - response object provided by express
* @description controller handling the new donations request on POST '/api/donations',
* @description controller handling the new donations request
* on POST '/api/donations',
* expects that the request body contains crowdrise data, and the amount.
*/
const addDonation = (req, res) => {
Expand Down Expand Up @@ -354,8 +403,9 @@ const updateUserTour = (req, res, next) => {
* that match the key. It search the name and email only.
*/
const findUsers = (req, res) => {
const { searchKey } = req.params;
const { searchKey, _id } = req.params;
User.find({
_id: { $ne: _id },
$and:
[
{
Expand All @@ -366,7 +416,7 @@ const findUsers = (req, res) => {
]
}
]
}, (err, users) => res.status(200).send({ users }));
}, (err, users) => res.status(200).send({ users: cleanUpUsers(users, _id) }));
};


Expand All @@ -384,6 +434,110 @@ const invite = (req, res) => {
return res.status(400).send({ message: 'An error occurred while sending the invitation' });
};

/**
* @param {object} req - request object provided by express
* @param {object} res - response object provided by express
* @description retrieves all friends and friend request a user has
*/
const getFriends = (req, res, next) => {
const { userId } = req.params;
User.findById(userId).populate('friends')
.populate('friend_requests').exec((error, currentUser) => {
if (error) {
return next(error);
}
return res.status(200).send({
friends: cleanUpFriends(currentUser.friends),
friendRequests: cleanUpFriends(currentUser.friend_requests)
});
});
};

/**
* @param {object} req - request object provided by express
* @param {object} res - response object provided by express
* @description accepts a friend request by
* adding both parties to each other's record
*/
const acceptRequest = (req, res, next) => {
const { userId } = req.params;
const { id } = req.body;
User.update({ _id: userId },
{ $push: { friends: id }, $pull: { friend_requests: id } },
(err) => {
if (err) {
return next(err);
}
User.update({ _id: id },
{ $push: { friends: userId }, $pull: { friend_requests: userId } },
(err) => {
if (err) {
return next(err);
}
res.status(200).send({ message: 'Friend request accepted' });
});
});
};

/**
* @param {object} req - request object provided by express
* @param {object} res - response object provided by express
* @description unfriend user by deleting both parties form each other's record
*/
const unfriendUser = (req, res, next) => {
const { userId } = req.params;
const { id } = req.body;
User.update({ _id: userId },
{ $pull: { friends: id } },
(err) => {
if (err) {
return next(err);
}
User.update({ _id: id },
{ $pull: { friends: userId } },
(err) => {
if (err) {
return next(err);
}
res.status(200).send({ message: 'Friend removed' });
});
});
};

/**
* @param {object} req - request object provided by express
* @param {object} res - response object provided by express
* @description decline friend request by deleting request from receivers record
*/
const declineRequest = (req, res, next) => {
const { userId } = req.params;
const { id } = req.body;
User.update({ _id: userId },
{ $pull: { friend_requests: id } },
(err) => {
if (err) {
return next(err);
}
res.status(200).send({ message: 'Friend request declined' });
});
};

/**
* @param {object} req - request object provided by express
* @param {object} res - response object provided by express
* @description get the number of friend requests a friend has
*/
const getRequestCount = (req, res, next) => {
const { userId } = req.params;
User.findById(userId).exec((error, currentUser) => {
if (error) {
return next(error);
}
return res.status(200).send({
length: currentUser.friend_requests.length,
});
});
};

export default {
authCallback,
Expand All @@ -402,5 +556,10 @@ export default {
handleSignUp,
avatars,
findUsers,
invite
invite,
getFriends,
acceptRequest,
unfriendUser,
declineRequest,
getRequestCount
};
2 changes: 2 additions & 0 deletions app/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const UserSchema = new Schema({
tour: { type: Boolean, default: false },
premium: Number, // null or 0 for non-donors, 1 for everyone else (for now)
donations: [],
friends: [{ type: Schema.Types.ObjectId, ref: 'User' }],
friend_requests: [{ type: Schema.Types.ObjectId, ref: 'User' }],
hashed_password: String,
facebook: {},
twitter: {},
Expand Down
2 changes: 2 additions & 0 deletions app/views/includes/foot.jade
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ script(type='text/javascript', src='/js/services/global.js')
script(type='text/javascript', src='/js/services/socket.js')
script(type='text/javascript', src='/js/services/game.js')
script(type='text/javascript', src='/js/services/dashboard.js')
script(type='text/javascript', src='/js/services/friend.js')

//Application Controllers
script(type='text/javascript', src='/js/controllers/index.js')
Expand All @@ -52,6 +53,7 @@ script(type='text/javascript', src='/js/controllers/auth.js')
script(type='text/javascript', src='/js/controllers/invitePlayers.js')
script(type='text/javascript', src='/js/controllers/tour.js')
script(type='text/javascript', src='/js/controllers/dashboard.js')
script(type='text/javascript', src='/js/controllers/friend.js')
script(type='text/javascript', src='/js/init.js')

// Application components
Expand Down
16 changes: 11 additions & 5 deletions backend-test/integration/findUsers.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,15 @@ const mock = [
}
];

let _id;

describe('User endpoints', () => {
before(() => {
Promise.resolve(User.create(mock));
before((done) => {
Promise.resolve(User.create(mock)).then((users) => {
const { id } = users[0];
_id = id;
done();
});
});

after(() => {
Expand All @@ -46,19 +52,19 @@ describe('User endpoints', () => {

it('GET /api/users/findUsers/:searchKey should return statusCode 200 with 2 users', (done) => {
request(app)
.get('/api/users/findUsers/ben')
.get(`/api/users/findUsers/ben/${_id}`)
.set('Authorization', `Bearer ${token}`)
.end((err, res) => {
if (err) return done(err);
expect(res.statusCode).to.equal(200);
expect(res.body.users.length).to.equal(2);
expect(res.body.users.length).to.equal(1);
done();
});
});

it('GET /api/users/findUsers/:searchKey should return statusCode 200 with no user', (done) => {
request(app)
.get('/api/users/findUsers/nothing')
.get(`/api/users/findUsers/nothing/${_id}`)
.set('Authorization', `Bearer ${token}`)
.end((err, res) => {
if (err) return done(err);
Expand Down
6 changes: 4 additions & 2 deletions backend-test/integration/gameLog.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,10 @@ const mockGames = [
const token = Tokenizer(user);

describe('Game History', () => {
before(() => {
Promise.resolve(Game.create(mockGames));
before((done) => {
Game.create(mockGames).then(() => {
done();
});
});

after(() => {
Expand Down
9 changes: 7 additions & 2 deletions config/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ export default (router, passport, app) => {
api
.post('/auth/login', users.handleLogin)
.post('/auth/signup', users.handleSignUp)
.get('/users/findUsers/:searchKey', ensureUser, users.findUsers)
.get('/users/findUsers/:searchKey/:_id', ensureUser, users.findUsers)
.get('/users/findUsers/', ensureUser, users.findUsers)
.post('/users/invite', ensureUser, users.invite)
.get('/profile', ensureUser, users.fetchProfile)
.get('/signout', users.signout);
.get('/signout', users.signout)
.get('/user/friends/:userId', ensureUser, users.getFriends)
.put('/user/accept/:userId', ensureUser, users.acceptRequest)
.put('/user/decline/:userId', ensureUser, users.declineRequest)
.put('/user/unfriend/:userId', ensureUser, users.unfriendUser)
.get('/user/getRequestCount/:userId', ensureUser, users.getRequestCount);

// Setting up user tour api
api
Expand Down
2 changes: 1 addition & 1 deletion config/socket/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ function Player(socket) {
this.color = null;
}

module.exports = Player;
module.exports = Player;
22 changes: 21 additions & 1 deletion config/socket/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,27 @@ export default (io) => {
}
});

socket.on('create-room', (room) => {
socket.join(room);
});

socket.on('new-friend', (data) => {
User.update({ _id: data.user._id },
{ $push: { friend_requests: data.id } },
(err) => {
if (err) {
return err;
}
io.sockets.in(`room-${data.user._id}`).emit('friendSaved');
io.sockets.in(`room-${data.id}`).emit('saveAsSent');
});
});

socket.on('invite-player', (data) => {
io.sockets.in(`room-${data.user._id}`)
.emit('popup-invitation-modal', data.link);
});

socket.on('new-message', (data) => {
if (!allGames[socket.gameID] || !allPlayers[socket.id]) return;
const targetGame = allGames[socket.gameID];
Expand Down Expand Up @@ -160,7 +181,6 @@ export default (io) => {
}
};


const joinGame = (socket, data) => {
const player = new Player(socket);
data = data || {};
Expand Down
Loading

0 comments on commit 90d71a0

Please sign in to comment.