Skip to content

Commit

Permalink
refactor: votes with debounce
Browse files Browse the repository at this point in the history
  • Loading branch information
nunocaseiro committed Jan 13, 2023
1 parent 1fd7923 commit c7d8f92
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 70 deletions.
1 change: 1 addition & 0 deletions backend/src/modules/socket/gateway/socket.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default class SocketGateway

sendUpdateVotes(excludedClient: string, voteDto: VoteDto) {
// voteDto.userId = 'aaaaaaaaaaaaaaaaaaaaaaaa';
voteDto.fromRequest = false;
this.server.except(excludedClient).emit(`${voteDto.boardId}vote`, voteDto);
}

Expand Down
42 changes: 12 additions & 30 deletions backend/src/modules/votes/controller/votes.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,26 +72,14 @@ export default class VotesController {
const { boardId, cardId, itemId } = params;
const { count, socketId } = data;

try {
if (count < 0) {
await this.deleteVoteApp.deleteVoteFromCard(
boardId,
cardId,
request.user._id,
itemId,
count
);
} else {
await this.createVoteApp.addVoteToCard(boardId, cardId, request.user._id, itemId, count);
}

this.socketService.sendUpdateVotes(socketId, data);
} catch (e) {
this.socketService.sendUpdatedBoard(boardId, socketId);

throw e;
if (count < 0) {
await this.deleteVoteApp.deleteVoteFromCard(boardId, cardId, request.user._id, itemId, count);
} else {
await this.createVoteApp.addVoteToCard(boardId, cardId, request.user._id, itemId, count);
}

this.socketService.sendUpdateVotes(socketId, data);

return HttpStatus.OK;
}

Expand Down Expand Up @@ -128,20 +116,14 @@ export default class VotesController {
const { boardId, cardId } = params;
const { count, socketId } = data;

try {
if (count < 0) {
await this.deleteVoteApp.deleteVoteFromCardGroup(boardId, cardId, request.user._id, count);
} else {
await this.createVoteApp.addVoteToCardGroup(boardId, cardId, request.user._id, count);
}

this.socketService.sendUpdateVotes(socketId, data);
} catch (e) {
this.socketService.sendUpdatedBoard(boardId, socketId);

throw e;
if (count < 0) {
await this.deleteVoteApp.deleteVoteFromCardGroup(boardId, cardId, request.user._id, count);
} else {
await this.createVoteApp.addVoteToCardGroup(boardId, cardId, request.user._id, count);
}

this.socketService.sendUpdateVotes(socketId, data);

return HttpStatus.OK;
}
}
2 changes: 2 additions & 0 deletions backend/src/modules/votes/dto/vote.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ export default class VoteDto {
@IsMongoId()
@IsString()
userId: string;

fromRequest: boolean;
}
5 changes: 3 additions & 2 deletions backend/src/modules/votes/services/create.vote.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export default class CreateVoteServiceImpl implements CreateVoteServiceInterface
if (!canUserVote) throw new BadRequestException(INSERT_VOTE_FAILED);

try {
await this.incrementVoteUser(boardId, userId, count);
await this.incrementVoteUser(boardId, userId, count, userSession);
const board = await this.boardModel
.updateOne(
{
Expand All @@ -97,7 +97,8 @@ export default class CreateVoteServiceImpl implements CreateVoteServiceInterface
}
},
{
arrayFilters: [{ 'c._id': cardId }, { 'i._id': cardItemId }]
arrayFilters: [{ 'c._id': cardId }, { 'i._id': cardItemId }],
session
}
)
.lean()
Expand Down
41 changes: 30 additions & 11 deletions backend/src/modules/votes/services/delete.vote.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export default class DeleteVoteServiceImpl implements DeleteVoteServiceInterface
const canUserVote = await this.canUserVote(boardId, userId, count, session, userSession);

if (!canUserVote) throw new BadRequestException(DELETE_VOTE_FAILED);
const currentCount = Math.abs(count);
let currentCount = Math.abs(count);
let card = await this.getCardService.getCardFromBoard(boardId, cardId);

if (!card) throw new BadRequestException(DELETE_VOTE_FAILED);
Expand All @@ -132,13 +132,13 @@ export default class DeleteVoteServiceImpl implements DeleteVoteServiceInterface

if (!isEmpty(userVotes)) {
mappedVotes = mappedVotes.filter((vote) => vote.toString() !== userId.toString());

userVotes.splice(0, Math.abs(currentCount));
const votesToReduce = userVotes.length / currentCount >= 1 ? currentCount : userVotes.length;
userVotes.splice(0, Math.abs(votesToReduce));

mappedVotes = mappedVotes.concat(userVotes);

try {
await this.decrementVoteUser(boardId, userId, -currentCount, userSession);
await this.decrementVoteUser(boardId, userId, -votesToReduce, userSession);
const board = await this.setCardVotes(boardId, mappedVotes, cardId, session);

if (board.modifiedCount !== 1) throw new BadRequestException(DELETE_VOTE_FAILED);
Expand All @@ -153,19 +153,38 @@ export default class DeleteVoteServiceImpl implements DeleteVoteServiceInterface
await userSession.endSession();
}

return;
currentCount -= Math.abs(votesToReduce);

if (currentCount === 0) return;
}

if (!isEmpty(currentCount)) {
card = await this.getCardService.getCardFromBoard(boardId, cardId);
while (currentCount > 0) {
card = await this.getCardService.getCardFromBoard(boardId, cardId);

const item = card.items.find(({ votes: itemVotes }) =>
arrayIdToString(itemVotes as unknown as string[]).includes(userId.toString())
);

const item = card.items.find(({ votes: itemVotes }) =>
arrayIdToString(itemVotes as unknown as string[]).includes(userId.toString())
);
if (!item) return null;

if (!item) return null;
const votesOfUser = (item.votes as unknown as string[]).filter(
(vote) => vote.toString() === userId.toString()
);

await this.deleteVoteFromCard(boardId, cardId, userId, item._id.toString(), -currentCount);
const itemVotesToReduce =
votesOfUser.length / currentCount >= 1 ? currentCount : votesOfUser.length;

await this.deleteVoteFromCard(
boardId,
cardId,
userId,
item._id.toString(),
-itemVotesToReduce
);

currentCount -= itemVotesToReduce;
}
}
}

Expand Down
50 changes: 37 additions & 13 deletions frontend/src/components/Board/Card/CardFooter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React, { useEffect, useMemo, useState } from 'react';

import { styled } from '@/styles/stitches/stitches.config';

Expand Down Expand Up @@ -89,11 +89,14 @@ const CardFooter = ({
const {
handleVote: { mutate, status },
toastInfoMessage,
updateVote,
} = useVotes();

const user = boardUser;
const userVotes = user?.votesCount ?? 0;

const [count, setCount] = useState(0);

const calculateVotes = () => {
const cardTyped = card as CardType;
if (Object.hasOwnProperty.call(card, 'items')) {
Expand All @@ -114,38 +117,59 @@ const CardFooter = ({

const handleDeleteVote = () => {
if ((hideCards && createdBy?._id !== userId) || status === 'loading') return;
mutate({
if (maxVotes) {
toastInfoMessage(`You have ${maxVotes! - (userVotes - 1)} votes left.`);
}
updateVote({
boardId,
cardId: card._id,
socketId,
cardItemId,
isCardGroup: cardItemId === undefined,
count: -1,
count: count - 1,
userId,
fromRequest: true,
});

if (maxVotes) {
toastInfoMessage(`You have ${maxVotes! - (userVotes - 1)} votes left.`);
}
setCount((prev) => prev - 1);
};

const handleAddVote = () => {
if (status === 'loading') return;
mutate({
if (maxVotes) {
toastInfoMessage(`You have ${maxVotes - (userVotes + 1)} votes left.`);
}
updateVote({
boardId,
cardId: card._id,
socketId,
cardItemId,
isCardGroup: cardItemId === undefined,
count: 1,
count: count + 1,
userId,
fromRequest: true,
});

if (maxVotes) {
toastInfoMessage(`You have ${maxVotes - (userVotes + 1)} votes left.`);
}
setCount((prev) => prev + 1);
};

useEffect(() => {
const timer = setTimeout(() => {
if (count === 0) return;
mutate({
boardId,
cardId: card._id,
socketId,
cardItemId,
isCardGroup: cardItemId === undefined,
count,
userId,
fromRequest: true,
});
setCount(0);
}, 300);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [count]);

return (
<Flex align="center" gap="6" justify={!anonymous || createdByTeam ? 'between' : 'end'}>
{!anonymous && !createdByTeam && (
Expand Down
17 changes: 15 additions & 2 deletions frontend/src/helper/board/transformBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,13 @@ export const handleMergeCard = (board: BoardType, changes: MergeCardsDto) => {

if (cardGroup && selectedCard && sourceColumn && currentCardPosition !== undefined) {
try {
cardGroup.comments = cardGroup.comments.concat(selectedCard.comments);
cardGroup.votes = cardGroup.votes.concat(selectedCard.votes);
sourceColumn.cards = removeElementAtIndex(sourceColumn.cards, currentCardPosition);
cardGroup.items = addElementAtIndex(cardGroup.items, cardGroup.items.length, {
...selectedCard,
selectedCard.items.forEach((_, idx) => {
cardGroup.items = addElementAtIndex(cardGroup.items, cardGroup.items.length, {
...selectedCard.items[idx],
});
});
} catch (e) {
return boardData;
Expand All @@ -120,9 +124,18 @@ export const handleUnMergeCard = (board: BoardType, changes: RemoveFromCardGroup
cardGroup.items = cardGroup.items.filter((item) => item._id !== selectedCard._id);
if (cardGroup.items.length === 1) {
cardGroup.text = cardGroup.items[0].text;
cardGroup.items[0].comments = cardGroup.items[0].comments.concat(cardGroup.comments);
cardGroup.items[0].votes = cardGroup.items[0].votes.concat(cardGroup.votes);
cardGroup.anonymous = cardGroup.items[0].anonymous;
cardGroup.createdBy = cardGroup.items[0].createdBy;
cardGroup.createdByTeam = cardGroup.items[0].createdByTeam;
cardGroup.comments = [];
cardGroup.votes = [];
}
column.cards = addElementAtIndex(column.cards, newPosition, {
...selectedCard,
comments: [],
votes: [],
items: [selectedCard],
});
} catch (e) {
Expand Down
48 changes: 36 additions & 12 deletions frontend/src/hooks/useVotes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,18 @@ const useVotes = () => {
return newBoardData;
};

const updateBoardUser = (boardData: BoardType, action: Action, currentUser: string) => {
const updateBoardUser = (
boardData: BoardType,
action: Action,
currentUser: string,
count: number,
) => {
boardData.users = boardData.users.map((boardUser) => {
if (boardUser.user._id !== currentUser) return boardUser;

return {
...boardUser,
votesCount: action === Action.Add ? boardUser.votesCount + 1 : boardUser.votesCount - 1,
votesCount: boardUser.votesCount + count,
};
});
};
Expand Down Expand Up @@ -186,11 +191,26 @@ const useVotes = () => {
isCardGroup: boolean,
action: Action,
userIdOfVote: string,
count: number,
fromRequest: boolean,
) => {
if (isCardGroup)
return updateCardsVotesOptimistic(prevBoardData, indexes, action, userIdOfVote);
let board = prevBoardData;
let countAbs = Math.abs(count);
while (countAbs !== 0) {
if (isCardGroup) {
board = updateCardsVotesOptimistic(board, indexes, action, userIdOfVote);
} else {
board = updateCardItemVoteOptimistic(board, indexes, action, userIdOfVote);
}

if (fromRequest) {
countAbs = 0;
} else {
countAbs -= 1;
}
}

return updateCardItemVoteOptimistic(prevBoardData, indexes, action, userIdOfVote);
return board;
};

const updateBoardDataOptimistic = (
Expand Down Expand Up @@ -224,8 +244,17 @@ const useVotes = () => {
isCardGroup,
action,
voteData.userId,
count,
voteData.fromRequest,
);

const currentCount = count > 0 ? 1 : -1;
updateBoardUser(
newBoard,
count > 0 ? Action.Add : Action.Remove,
voteData.userId,
voteData.fromRequest ? currentCount : count,
);
updateBoardUser(newBoard, count > 0 ? Action.Add : Action.Remove, voteData.userId);

return newBoard;
}
Expand Down Expand Up @@ -280,12 +309,7 @@ const useVotes = () => {
};

const handleVote = useMutation(handleVotes, {
onMutate: async (variables) => {
const { newBoardData, prevBoardData } = await updateVote(variables);

return { previousBoard: prevBoardData, data: newBoardData };
},
onError: (_, variables, context) => {
onError: (_, variables) => {
queryClient.invalidateQueries(['board', { id: variables.boardId }]);
toastErrorMessage(`Error ${variables.count > 0 ? 'adding' : 'removing'} the vote`);
},
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/types/vote/vote.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ export default interface VoteDto {
count: number;

userId: string;

fromRequest: boolean;
}

0 comments on commit c7d8f92

Please sign in to comment.