From 43c4b8d28971919e079218adc8f28bcd9cfa31ac Mon Sep 17 00:00:00 2001 From: Antoine Jaussoin Date: Wed, 25 Jan 2023 20:05:12 +0000 Subject: [PATCH] Allowing to cancel votes (#457) --- backend/package.json | 4 +- backend/scripts/create-empty-migration.sh | 2 +- backend/scripts/create-migration.sh | 2 +- backend/src/common/actions.ts | 2 + backend/src/common/models.ts | 1 + backend/src/common/types.ts | 1 + backend/src/common/ws.ts | 10 +++++ backend/src/db/actions/votes.ts | 20 +++++++++ backend/src/db/entities/SessionOptions.ts | 3 ++ .../1674589758156-AllowCancelVote.ts | 16 +++++++ backend/src/game.ts | 29 ++++++++++++- frontend/src/common/actions.ts | 2 + frontend/src/common/models.ts | 1 + frontend/src/common/types.ts | 1 + frontend/src/common/ws.ts | 12 +++++- frontend/src/translations/locales/ar-SA.json | 6 ++- frontend/src/translations/locales/de-DE.json | 8 +++- frontend/src/translations/locales/en-GB.json | 6 ++- frontend/src/translations/locales/es-ES.json | 6 ++- frontend/src/translations/locales/fr-FR.json | 6 ++- frontend/src/translations/locales/hu-HU.json | 6 ++- frontend/src/translations/locales/it-IT.json | 6 ++- frontend/src/translations/locales/ja-JP.json | 6 ++- frontend/src/translations/locales/nl-NL.json | 6 ++- frontend/src/translations/locales/pl-PL.json | 6 ++- frontend/src/translations/locales/pt-BR.json | 6 ++- frontend/src/translations/locales/pt-PT.json | 6 ++- frontend/src/translations/locales/uk-UA.json | 6 ++- frontend/src/translations/locales/zh-CN.json | 6 ++- frontend/src/translations/locales/zh-TW.json | 6 ++- frontend/src/views/Game.tsx | 2 + frontend/src/views/game/board/Board.tsx | 3 ++ frontend/src/views/game/board/Column.tsx | 4 ++ .../board/__tests__/permissions-logic.test.ts | 42 ++++++++++++++++++ .../src/views/game/board/permissions-logic.ts | 6 +++ frontend/src/views/game/board/post/Post.tsx | 12 ++++++ frontend/src/views/game/useGame.ts | 43 +++++++++++++++++++ frontend/src/views/game/useSession.ts | 25 +++++++++++ .../sections/votes/VotingSection.tsx | 18 ++++++++ 39 files changed, 331 insertions(+), 22 deletions(-) create mode 100644 backend/src/db/migrations/1674589758156-AllowCancelVote.ts diff --git a/backend/package.json b/backend/package.json index 0b341f1c2..283e66856 100644 --- a/backend/package.json +++ b/backend/package.json @@ -9,8 +9,8 @@ "start": "nodemon --exec 'yarn fix & ts-node' --esm --files ./src/index.ts", "create-migration": "scripty", "create-empty-migration": "scripty", - "migrate": "typeorm-ts-node-commonjs -d src/db/index.ts migration:run", - "revert": "typeorm-ts-node-commonjs -d src/db/index.ts migration:revert", + "migrate": "typeorm-ts-node-esm -d src/db/index.ts migration:run", + "revert": "typeorm-ts-node-esm -d src/db/index.ts migration:revert", "lint": "eslint 'src/**/*.ts'", "test": "yarn jest", "ci-test": "CI=true yarn test", diff --git a/backend/scripts/create-empty-migration.sh b/backend/scripts/create-empty-migration.sh index 01353d5bd..e2fceef66 100755 --- a/backend/scripts/create-empty-migration.sh +++ b/backend/scripts/create-empty-migration.sh @@ -1,2 +1,2 @@ #!/usr/bin/env sh -./node_modules/.bin/typeorm-ts-node-commonjs migration:create src/db/migrations/$1 \ No newline at end of file +./node_modules/.bin/typeorm-ts-node-esm migration:create src/db/migrations/$1 \ No newline at end of file diff --git a/backend/scripts/create-migration.sh b/backend/scripts/create-migration.sh index 4bd7ce51b..f3ace4780 100755 --- a/backend/scripts/create-migration.sh +++ b/backend/scripts/create-migration.sh @@ -1,2 +1,2 @@ #!/usr/bin/env sh -./node_modules/.bin/typeorm-ts-node-commonjs -d src/db/index.ts migration:generate src/db/migrations/$1 \ No newline at end of file +./node_modules/.bin/typeorm-ts-node-esm -d src/db/index.ts migration:generate src/db/migrations/$1 \ No newline at end of file diff --git a/backend/src/common/actions.ts b/backend/src/common/actions.ts index 0dbd14585..e3f94a320 100644 --- a/backend/src/common/actions.ts +++ b/backend/src/common/actions.ts @@ -5,6 +5,7 @@ const actions = { EDIT_POST: 'retrospected/posts/edit', MOVE_POST: 'retrospected/posts/move', LIKE_SUCCESS: 'retrospected/posts/like/success', + CANCEL_VOTES_SUCCESS: 'retrospected/posts/cancel-votes/success', ADD_POST_GROUP_SUCCESS: 'retrospected/group/add/success', DELETE_POST_GROUP: 'retrospected/group/delete', EDIT_POST_GROUP: 'retrospected/group/edit', @@ -24,6 +25,7 @@ const actions = { RECEIVE_EDIT_POST: 'retrospected/posts/receive/edit', RECEIVE_MOVE_POST: 'retrospected/posts/receive/move', RECEIVE_LIKE: 'retrospected/posts/receive/like', + RECEIVE_CANCEL_VOTES: 'retrospected/posts/receive/cancel-votes', RECEIVE_POST_GROUP: 'retrospected/group/receive/add', RECEIVE_DELETE_POST_GROUP: 'retrospected/group/receive/delete', RECEIVE_EDIT_POST_GROUP: 'retrospected/group/receive/edit', diff --git a/backend/src/common/models.ts b/backend/src/common/models.ts index b7f0a199c..52f9bb700 100644 --- a/backend/src/common/models.ts +++ b/backend/src/common/models.ts @@ -11,6 +11,7 @@ export const defaultOptions: SessionOptions = { allowGiphy: true, allowGrouping: true, allowReordering: true, + allowCancelVote: false, blurCards: false, newPostsFirst: true, }; diff --git a/backend/src/common/types.ts b/backend/src/common/types.ts index bea1a304a..da3180efb 100644 --- a/backend/src/common/types.ts +++ b/backend/src/common/types.ts @@ -57,6 +57,7 @@ export interface SessionOptions { allowGiphy: boolean; allowGrouping: boolean; allowReordering: boolean; + allowCancelVote: boolean; blurCards: boolean; newPostsFirst: boolean; } diff --git a/backend/src/common/ws.ts b/backend/src/common/ws.ts index 2d6edf9eb..43024079d 100644 --- a/backend/src/common/ws.ts +++ b/backend/src/common/ws.ts @@ -41,10 +41,19 @@ export interface WsReceiveLikeUpdatePayload { vote: VoteExtract; } +export interface WsReceiveCancelVotesPayload { + postId: string; + userId: string; +} + export interface WsDeletePostPayload { postId: string; } +export interface WsCancelVotesPayload { + postId: string; +} + export interface WsDeleteGroupPayload { groupId: string; } @@ -73,6 +82,7 @@ export type WsErrorType = | 'cannot_delete_group' | 'cannot_rename_session' | 'cannot_record_chat_message' + | 'cannot_cancel_votes' | 'unknown_error' | 'action_unauthorised'; diff --git a/backend/src/db/actions/votes.ts b/backend/src/db/actions/votes.ts index e7646d628..ae8f52062 100644 --- a/backend/src/db/actions/votes.ts +++ b/backend/src/db/actions/votes.ts @@ -9,6 +9,26 @@ import { } from '../repositories/index.js'; import { transaction } from './transaction.js'; +export async function cancelVotes( + userId: string, + sessionId: string, + postId: string +): Promise { + return await transaction(async (manager) => { + const sessionRepository = manager.withRepository(SessionRepository); + const voteRepository = manager.withRepository(VoteRepository); + const session = await sessionRepository.findOne({ + where: { id: sessionId }, + }); + if (session && session.options.allowCancelVote) { + await voteRepository.delete({ + user: { id: userId }, + post: { id: postId }, + }); + } + }); +} + export async function registerVote( userId: string, sessionId: string, diff --git a/backend/src/db/entities/SessionOptions.ts b/backend/src/db/entities/SessionOptions.ts index 1fba13962..4daa9585e 100644 --- a/backend/src/db/entities/SessionOptions.ts +++ b/backend/src/db/entities/SessionOptions.ts @@ -27,6 +27,8 @@ export default class SessionOptionsEntity { @Column({ default: true }) public allowReordering: boolean; @Column({ default: false }) + public allowCancelVote: boolean; + @Column({ default: false }) public blurCards: boolean; @Column({ default: true }) public newPostsFirst: boolean; @@ -49,6 +51,7 @@ export default class SessionOptionsEntity { this.allowGiphy = optionsWithDefault.allowGiphy; this.allowGrouping = optionsWithDefault.allowGrouping; this.allowReordering = optionsWithDefault.allowMultipleVotes; + this.allowCancelVote = optionsWithDefault.allowCancelVote; this.blurCards = optionsWithDefault.blurCards; this.newPostsFirst = optionsWithDefault.newPostsFirst; } diff --git a/backend/src/db/migrations/1674589758156-AllowCancelVote.ts b/backend/src/db/migrations/1674589758156-AllowCancelVote.ts new file mode 100644 index 000000000..36286dc94 --- /dev/null +++ b/backend/src/db/migrations/1674589758156-AllowCancelVote.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AllowCancelVote1674589758156 implements MigrationInterface { + name = 'AllowCancelVote1674589758156' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "templates" ADD "options_allow_cancel_vote" boolean NOT NULL DEFAULT false`); + await queryRunner.query(`ALTER TABLE "sessions" ADD "options_allow_cancel_vote" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "sessions" DROP COLUMN "options_allow_cancel_vote"`); + await queryRunner.query(`ALTER TABLE "templates" DROP COLUMN "options_allow_cancel_vote"`); + } + +} diff --git a/backend/src/game.ts b/backend/src/game.ts index ced86b727..9c035d498 100644 --- a/backend/src/game.ts +++ b/backend/src/game.ts @@ -21,6 +21,8 @@ import { WsGroupUpdatePayload, WsUserReadyPayload, Message, + WsCancelVotesPayload, + WsReceiveCancelVotesPayload, } from './common/index.js'; import { RateLimiterMemory } from 'rate-limiter-flexible'; import chalk from 'chalk-template'; @@ -58,7 +60,7 @@ import { updatePostGroup, } from './db/actions/posts.js'; import config from './config.js'; -import { registerVote } from './db/actions/votes.js'; +import { cancelVotes, registerVote } from './db/actions/votes.js'; import { deserialiseIds, UserIds } from './utils.js'; import { QueryFailedError } from 'typeorm'; import { saveChatMessage } from './db/actions/chat.js'; @@ -79,6 +81,8 @@ const { ADD_POST_GROUP_SUCCESS, DELETE_POST, LIKE_SUCCESS, + CANCEL_VOTES_SUCCESS, + RECEIVE_CANCEL_VOTES, EDIT_POST, DELETE_POST_GROUP, EDIT_POST_GROUP, @@ -478,6 +482,28 @@ export default (io: Server) => { } }; + const onCancelVotes = async ( + userIds: UserIds | null, + sessionId: string, + data: WsCancelVotesPayload, + socket: Socket + ) => { + if (checkUser(userIds, socket)) { + await cancelVotes(userIds.userId, sessionId, data.postId); + + sendToAllOrError( + socket, + sessionId, + RECEIVE_CANCEL_VOTES, + 'cannot_cancel_votes', + { + postId: data.postId, + userId: userIds.userId, + } + ); + } + }; + const onEditPost = async ( _userIds: UserIds | null, sessionId: string, @@ -607,6 +633,7 @@ export default (io: Server) => { { type: EDIT_POST, handler: onEditPost }, { type: DELETE_POST, handler: onDeletePost }, { type: LIKE_SUCCESS, handler: onLikePost }, + { type: CANCEL_VOTES_SUCCESS, handler: onCancelVotes }, { type: ADD_POST_GROUP_SUCCESS, handler: onAddPostGroup }, { type: EDIT_POST_GROUP, handler: onEditPostGroup }, diff --git a/frontend/src/common/actions.ts b/frontend/src/common/actions.ts index 0dbd14585..e3f94a320 100644 --- a/frontend/src/common/actions.ts +++ b/frontend/src/common/actions.ts @@ -5,6 +5,7 @@ const actions = { EDIT_POST: 'retrospected/posts/edit', MOVE_POST: 'retrospected/posts/move', LIKE_SUCCESS: 'retrospected/posts/like/success', + CANCEL_VOTES_SUCCESS: 'retrospected/posts/cancel-votes/success', ADD_POST_GROUP_SUCCESS: 'retrospected/group/add/success', DELETE_POST_GROUP: 'retrospected/group/delete', EDIT_POST_GROUP: 'retrospected/group/edit', @@ -24,6 +25,7 @@ const actions = { RECEIVE_EDIT_POST: 'retrospected/posts/receive/edit', RECEIVE_MOVE_POST: 'retrospected/posts/receive/move', RECEIVE_LIKE: 'retrospected/posts/receive/like', + RECEIVE_CANCEL_VOTES: 'retrospected/posts/receive/cancel-votes', RECEIVE_POST_GROUP: 'retrospected/group/receive/add', RECEIVE_DELETE_POST_GROUP: 'retrospected/group/receive/delete', RECEIVE_EDIT_POST_GROUP: 'retrospected/group/receive/edit', diff --git a/frontend/src/common/models.ts b/frontend/src/common/models.ts index 6ebb5d35a..25f87f0c1 100644 --- a/frontend/src/common/models.ts +++ b/frontend/src/common/models.ts @@ -13,6 +13,7 @@ export const defaultOptions: SessionOptions = { allowReordering: true, blurCards: false, newPostsFirst: true, + allowCancelVote: false, }; export const defaultSession: Omit = { diff --git a/frontend/src/common/types.ts b/frontend/src/common/types.ts index bea1a304a..da3180efb 100644 --- a/frontend/src/common/types.ts +++ b/frontend/src/common/types.ts @@ -57,6 +57,7 @@ export interface SessionOptions { allowGiphy: boolean; allowGrouping: boolean; allowReordering: boolean; + allowCancelVote: boolean; blurCards: boolean; newPostsFirst: boolean; } diff --git a/frontend/src/common/ws.ts b/frontend/src/common/ws.ts index 55b2812bf..43024079d 100644 --- a/frontend/src/common/ws.ts +++ b/frontend/src/common/ws.ts @@ -6,7 +6,7 @@ import { User, VoteExtract, VoteType, -} from './types'; +} from './types.js'; export interface WebsocketMessage { payload: T; @@ -41,10 +41,19 @@ export interface WsReceiveLikeUpdatePayload { vote: VoteExtract; } +export interface WsReceiveCancelVotesPayload { + postId: string; + userId: string; +} + export interface WsDeletePostPayload { postId: string; } +export interface WsCancelVotesPayload { + postId: string; +} + export interface WsDeleteGroupPayload { groupId: string; } @@ -73,6 +82,7 @@ export type WsErrorType = | 'cannot_delete_group' | 'cannot_rename_session' | 'cannot_record_chat_message' + | 'cannot_cancel_votes' | 'unknown_error' | 'action_unauthorised'; diff --git a/frontend/src/translations/locales/ar-SA.json b/frontend/src/translations/locales/ar-SA.json index 07f3a1966..8483684b3 100644 --- a/frontend/src/translations/locales/ar-SA.json +++ b/frontend/src/translations/locales/ar-SA.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "لديك {{number}} {{type}}ثانية متبقية.", "voteRemainingOne": "لديك فقط {{type}} واحد متبقي، جعله عديم!", "voteRemainingNone": "ليس لديك أي {{type}} متبقي.", - "toggleGiphyButton": "تبديل الصورة الجافية" + "toggleGiphyButton": "تبديل الصورة الجافية", + "cancelVote": "إلغاء تصويتك على هذا المنشور" }, "Customize": { "title": "تخصيص الجلسة الخاصة بك", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "ما إذا كان يسمح للمستخدم بالتصويت على مشاركته الخاصة", "allowMultipleVotes": "السماح بتعدد الأصوات", "allowMultipleVotesHelp": "ما إذا كان يسمح للمستخدم بالتصويت عدة مرات على نفس المنشور", + "allowCancelVote": "السماح بإلغاء التصويت", + "allowCancelVoteHelp": "ما إذا كان يسمح للمستخدم بإلغاء تصويته على مشاركة محددة", "allowActions": "السماح بالإجراءات", "allowActionsHelp": "ما إذا كان سيتم السماح بحقل \"الإجراء\" (المتابعة) في كل مشاركة", "allowAuthorVisible": "إظهار المؤلف", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "فشل إعادة تسمية الجلسة", "error_cannot_save_columns": "فشل حفظ الأعمدة", "error_cannot_save_options": "فشل حفظ الخيارات", + "error_cannot_cancel_votes": "فشل إلغاء الأصوات", "maxPostsReached": "لقد وصلت إلى الحد الأقصى لعدد المشاركات التي حددها المشرف", "iAmDone": "لقد انتهيت!", "iAmNotDoneYet": "أنا لم أنتهِ بعد...", diff --git a/frontend/src/translations/locales/de-DE.json b/frontend/src/translations/locales/de-DE.json index c6ffdd620..67ee2197a 100644 --- a/frontend/src/translations/locales/de-DE.json +++ b/frontend/src/translations/locales/de-DE.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "Sie können noch {{number}} mal {{type}}.", "voteRemainingOne": "Sie können noch ein mal {{type}}, überlegen Sie es sich gut!", "voteRemainingNone": "Sie können nicht mehr {{type}}.", - "toggleGiphyButton": "Giphy-Bild umschalten" + "toggleGiphyButton": "Giphy-Bild umschalten", + "cancelVote": "Deine Stimme für diesen Beitrag abbrechen" }, "Customize": { "title": "Personalisieren Sie die Sitzung", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "Soll es Nutzern möglich sein für seine eigenen Beiträge zu stimmen?", "allowMultipleVotes": "Mehrfachstimmen", "allowMultipleVotesHelp": "Soll es Nutzern möglich sein mehrfach zu abzustimmen?", + "allowCancelVote": "Abstimmungen abbrechen", + "allowCancelVoteHelp": "Gibt an, ob ein Benutzer seine Stimme für einen bestimmten Beitrag abbrechen kann", "allowActions": "Erlaube Maßnahmen", "allowActionsHelp": "Bestimmt ob Maßnahmen hinzugefügt werden können", "allowAuthorVisible": "Zeige Autor", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "Umbenennen der Sitzung fehlgeschlagen", "error_cannot_save_columns": "Spalten speichern fehlgeschlagen", "error_cannot_save_options": "Speichern der Optionen fehlgeschlagen", + "error_cannot_cancel_votes": "Abbruch der Abstimmung fehlgeschlagen", "maxPostsReached": "Sie haben die vom Moderator festgelegte maximale Anzahl von Beiträgen erreicht.", "iAmDone": "Ich bin fertig!", "iAmNotDoneYet": "Ich bin noch nicht fertig...", @@ -442,4 +446,4 @@ "Chat": { "writeAMessage": "Hier eine Nachricht schreiben..." } -} +} \ No newline at end of file diff --git a/frontend/src/translations/locales/en-GB.json b/frontend/src/translations/locales/en-GB.json index 075a2d08a..ed9329348 100644 --- a/frontend/src/translations/locales/en-GB.json +++ b/frontend/src/translations/locales/en-GB.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "You have {{number}} {{type}}s remaining.", "voteRemainingOne": "You only have one {{type}} remaining, make it count!", "voteRemainingNone": "You don't have any {{type}} remaining.", - "toggleGiphyButton": "Toggle Giphy image" + "toggleGiphyButton": "Toggle Giphy image", + "cancelVote": "Cancel your votes on this post" }, "Customize": { "title": "Customise your Session", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "Whether to allow a user to vote on their own post", "allowMultipleVotes": "Allow Multiple Votes", "allowMultipleVotesHelp": "Whether to allow a user to vote multiple times on the same post", + "allowCancelVote": "Allow Cancel Votes", + "allowCancelVoteHelp": "Whether to allow a user to cancel their vote on a specific post", "allowActions": "Allow Actions", "allowActionsHelp": "Whether to allow the 'Action' (follow-up) field on each post", "allowAuthorVisible": "Show Author", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "Renaming the session failed", "error_cannot_save_columns": "Saving columns failed", "error_cannot_save_options": "Saving options failed", + "error_cannot_cancel_votes": "Cancelling votes failed", "maxPostsReached": "You have reached the maximum number of posts set by the moderator.", "iAmDone": "I'm done!", "iAmNotDoneYet": "I'm not done yet...", diff --git a/frontend/src/translations/locales/es-ES.json b/frontend/src/translations/locales/es-ES.json index d7ee046a8..0aacaa954 100644 --- a/frontend/src/translations/locales/es-ES.json +++ b/frontend/src/translations/locales/es-ES.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "Tienes {{number}} {{type}}s restantes.", "voteRemainingOne": "Solo te queda un {{type}} restante, ¡hazlo contar!", "voteRemainingNone": "No te quedan {{type}}.", - "toggleGiphyButton": "Cambiar imagen de Giphy" + "toggleGiphyButton": "Cambiar imagen de Giphy", + "cancelVote": "Cancela tus votos en esta publicación" }, "Customize": { "title": "Personaliza tu sesión", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "Si permitir que un usuario vote en su propio post", "allowMultipleVotes": "Permitir múltiples votos", "allowMultipleVotesHelp": "Si permitir que un usuario vote varias veces en el mismo post", + "allowCancelVote": "Permitir Cancel Votes", + "allowCancelVoteHelp": "Si permitir que un usuario cancele su voto en un mensaje específico", "allowActions": "Permitir acciones", "allowActionsHelp": "Si permitir o no el campo 'Acción' (seguimiento) en cada publicación", "allowAuthorVisible": "Mostrar autor", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "Fallo al renombrar la sesión", "error_cannot_save_columns": "Error al guardar columnas", "error_cannot_save_options": "Fallo al guardar opciones", + "error_cannot_cancel_votes": "Falló la cancelación de votos", "maxPostsReached": "Has alcanzado el número máximo de mensajes establecidos por el moderador.", "iAmDone": "¡He terminado!", "iAmNotDoneYet": "No he terminado todavía...", diff --git a/frontend/src/translations/locales/fr-FR.json b/frontend/src/translations/locales/fr-FR.json index 3ec8923eb..078a4798a 100644 --- a/frontend/src/translations/locales/fr-FR.json +++ b/frontend/src/translations/locales/fr-FR.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "Il vous reste {{number}} votes {{type}}s.", "voteRemainingOne": "Il ne vous reste plus qu'un vote {{type}}, ne le gâchez pas !", "voteRemainingNone": "Il ne vous reste plus aucun vote {{type}}.", - "toggleGiphyButton": "Montrer/Cacher l'image Giphy" + "toggleGiphyButton": "Montrer/Cacher l'image Giphy", + "cancelVote": "Annuler vos votes sur ce post" }, "Customize": { "title": "Nouvelle session personalisée", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "Autoriser à voter pour ses propres posts", "allowMultipleVotes": "Votes multiples", "allowMultipleVotesHelp": "Autoriser à voter plusieurs fois pour le même post", + "allowCancelVote": "Permettre d'annuler ses votes", + "allowCancelVoteHelp": "Permettre à un utilisateur d'annuler son vote sur un post spécifique", "allowActions": "Activer les Actions", "allowActionsHelp": "Permettre ou non le champ 'Action' (suivi) sur chaque message", "allowAuthorVisible": "Afficher l'auteur", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "La session ne peut être renommée", "error_cannot_save_columns": "Les colonnes n'ont pu être enregistrées", "error_cannot_save_options": "Les options n'ont pu être enregistrées", + "error_cannot_cancel_votes": "L'annulation des votes a échoué", "maxPostsReached": "Vous avez atteint le nombre de posts maximum prévu par le modérateur.", "iAmDone": "J'ai fini !", "iAmNotDoneYet": "Je n'ai pas encore fini...", diff --git a/frontend/src/translations/locales/hu-HU.json b/frontend/src/translations/locales/hu-HU.json index 0f82f950b..dbd34311e 100644 --- a/frontend/src/translations/locales/hu-HU.json +++ b/frontend/src/translations/locales/hu-HU.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "{{number}} {{type}}mp van hátra.", "voteRemainingOne": "Már csak egy {{type}} van hátra, számítson rá!", "voteRemainingNone": "Nem maradt {{type}}.", - "toggleGiphyButton": "Kapcsolja be a Giphy képet" + "toggleGiphyButton": "Kapcsolja be a Giphy képet", + "cancelVote": "" }, "Customize": { "title": "A munkamenet testreszabása", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "Engedélyezi-e a felhasználónak, hogy szavazzon a saját bejegyzésére", "allowMultipleVotes": "Több szavazat engedélyezése", "allowMultipleVotesHelp": "Engedélyezi-e, hogy egy felhasználó többször szavazzon ugyanarra a bejegyzésre", + "allowCancelVote": "", + "allowCancelVoteHelp": "", "allowActions": "Műveletek engedélyezése", "allowActionsHelp": "Engedélyezi-e az „Action” (követés) mezőt az egyes bejegyzéseknél", "allowAuthorVisible": "Szerző megjelenítése", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "A munkamenet átnevezése nem sikerült", "error_cannot_save_columns": "Az oszlopok mentése nem sikerült", "error_cannot_save_options": "A beállítások mentése nem sikerült", + "error_cannot_cancel_votes": "", "maxPostsReached": "Elérted a moderátor által beállított maximális bejegyzések számát.", "iAmDone": "Kész vagyok!", "iAmNotDoneYet": "Még nem végeztem...", diff --git a/frontend/src/translations/locales/it-IT.json b/frontend/src/translations/locales/it-IT.json index 2bd0308ea..203f58170 100644 --- a/frontend/src/translations/locales/it-IT.json +++ b/frontend/src/translations/locales/it-IT.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "Hai {{number}} {{type}}s rimanenti.", "voteRemainingOne": "Hai solo un {{type}} rimanente, farlo contare!", "voteRemainingNone": "Non hai nessun {{type}} rimanente.", - "toggleGiphyButton": "Toggle immagine Giphy" + "toggleGiphyButton": "Toggle immagine Giphy", + "cancelVote": "Annulla i tuoi voti su questo post" }, "Customize": { "title": "Personalizza il tuo gioco!", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "Se consentire ad un utente di votare sul proprio post", "allowMultipleVotes": "Permettere votazioni multiple", "allowMultipleVotesHelp": "Se consentire a un utente di votare più volte sullo stesso post", + "allowCancelVote": "Consenti Annulla Voti", + "allowCancelVoteHelp": "Indica se consentire a un utente di annullare il proprio voto su un post specifico", "allowActions": "Permettere Azioni", "allowActionsHelp": "Se consentire il campo \"Azione\" (follow-up) su ciascun post", "allowAuthorVisible": "Mostra Autore", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "Rinomina della sessione non riuscita", "error_cannot_save_columns": "Salvataggio colonne non riuscito", "error_cannot_save_options": "Opzioni di salvataggio fallite", + "error_cannot_cancel_votes": "Annullamento dei voti fallito", "maxPostsReached": "Hai raggiunto il numero massimo di post impostati dal moderatore.", "iAmDone": "Ho finito!", "iAmNotDoneYet": "Non ho ancora finito...", diff --git a/frontend/src/translations/locales/ja-JP.json b/frontend/src/translations/locales/ja-JP.json index 6b585ec8e..44099d7dc 100644 --- a/frontend/src/translations/locales/ja-JP.json +++ b/frontend/src/translations/locales/ja-JP.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "残り {{number}} {{type}}秒です。", "voteRemainingOne": "残り {{type}} は1つです。カウントしてください!", "voteRemainingNone": "残り {{type}} はありません。", - "toggleGiphyButton": "Giphy画像の切り替え" + "toggleGiphyButton": "Giphy画像の切り替え", + "cancelVote": "この投稿への投票をキャンセル" }, "Customize": { "title": "セッションをカスタマイズ", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "ユーザーが自分の投稿に投票できるようにするかどうか", "allowMultipleVotes": "複数の投票を許可する", "allowMultipleVotesHelp": "ユーザーが同じ投稿に複数回投票できるようにするか", + "allowCancelVote": "投票をキャンセルする", + "allowCancelVoteHelp": "ユーザーに特定の投稿への投票を許可するかどうか", "allowActions": "アクションを許可する", "allowActionsHelp": "各投稿の「アクション」フィールドを許可するかどうか", "allowAuthorVisible": "投稿者を表示", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "セッションの名前を変更できませんでした", "error_cannot_save_columns": "列の保存に失敗しました", "error_cannot_save_options": "オプションの保存に失敗しました", + "error_cannot_cancel_votes": "投票のキャンセルに失敗しました", "maxPostsReached": "モデレーターが設定した投稿の最大数に達しました。", "iAmDone": "終わりました!", "iAmNotDoneYet": "まだ完了していません...", diff --git a/frontend/src/translations/locales/nl-NL.json b/frontend/src/translations/locales/nl-NL.json index d594b6142..c4a71f15d 100644 --- a/frontend/src/translations/locales/nl-NL.json +++ b/frontend/src/translations/locales/nl-NL.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "Je hebt nog {{number}} {{type}}s over.", "voteRemainingOne": "Je hebt nog maar één {{type}} over, maak het mee!", "voteRemainingNone": "Je hebt geen {{type}} over.", - "toggleGiphyButton": "Aan- en uitschakelen Giphy afbeelding" + "toggleGiphyButton": "Aan- en uitschakelen Giphy afbeelding", + "cancelVote": "Uw stemmen op dit bericht annuleren" }, "Customize": { "title": "Sessie aanpassen", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "Of een gebruiker op zijn eigen retropunten mag stemmen", "allowMultipleVotes": "Meerdere stemmen toestaan", "allowMultipleVotesHelp": "Of een gebruiker meerdere stemmen op hetzelfde retropunt kan uitbrengen", + "allowCancelVote": "Annuleren van stemmen toestaan", + "allowCancelVoteHelp": "Wel of niet toestaan dat een gebruiker zijn stem op een bepaald bericht annuleert", "allowActions": "Acties toestaan", "allowActionsHelp": "Toestaan van 'Acties' veld (follow-up) op elk retropunt", "allowAuthorVisible": "Toon auteur", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "Hernoemen van de sessie is mislukt", "error_cannot_save_columns": "Kolommen opslaan mislukt", "error_cannot_save_options": "Opties opslaan mislukt", + "error_cannot_cancel_votes": "Annuleren van stemmen mislukt", "maxPostsReached": "Je hebt het maximum aantal berichten bereikt dat door de moderator is ingesteld.", "iAmDone": "Ik ben klaar!", "iAmNotDoneYet": "Ik ben nog niet klaar...", diff --git a/frontend/src/translations/locales/pl-PL.json b/frontend/src/translations/locales/pl-PL.json index 9530821d2..9f99cb4b7 100644 --- a/frontend/src/translations/locales/pl-PL.json +++ b/frontend/src/translations/locales/pl-PL.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "Pozostało Ci {{number}} {{type}}s.", "voteRemainingOne": "Pozostało Ci tylko jeden {{type}} , zliczaj się!", "voteRemainingNone": "Nie masz jeszcze żadnych {{type}}.", - "toggleGiphyButton": "Przełącz obraz Giphy" + "toggleGiphyButton": "Przełącz obraz Giphy", + "cancelVote": "Anuluj swoje głosy na ten post" }, "Customize": { "title": "Dostosuj swoją sesję", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "Czy zezwolić użytkownikowi na głosowanie na własny post", "allowMultipleVotes": "Zezwalaj na wiele głosów", "allowMultipleVotesHelp": "Czy zezwolić użytkownikowi na wielokrotne głosowanie na ten sam post", + "allowCancelVote": "Zezwalaj na anulowanie głosów", + "allowCancelVoteHelp": "Czy zezwolić użytkownikowi na anulowanie głosowania na konkretny post", "allowActions": "Zezwól na działania", "allowActionsHelp": "Czy zezwolić na pole \"Akcja\" (kontynuacja) dla każdego postu", "allowAuthorVisible": "Pokaż autora", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "Zmiana nazwy sesji nie powiodła się", "error_cannot_save_columns": "Zapisywanie kolumn nie powiodło się", "error_cannot_save_options": "Zapisywanie opcji nie powiodło się", + "error_cannot_cancel_votes": "Anulowanie głosów nie powiodło się", "maxPostsReached": "Osiągnąłeś maksymalną liczbę postów ustawionych przez moderatora.", "iAmDone": "Gotowe!", "iAmNotDoneYet": "Jeszcze nie skończyłem...", diff --git a/frontend/src/translations/locales/pt-BR.json b/frontend/src/translations/locales/pt-BR.json index add836b24..845697955 100644 --- a/frontend/src/translations/locales/pt-BR.json +++ b/frontend/src/translations/locales/pt-BR.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "Você tem {{number}} {{type}}restantes.", "voteRemainingOne": "Você tem apenas um {{type}} restante, faça com que ele conte!", "voteRemainingNone": "Você não tem nenhum {{type}} restante.", - "toggleGiphyButton": "Alternar imagem Giphy" + "toggleGiphyButton": "Alternar imagem Giphy", + "cancelVote": "Cancelar seus votos nesta publicação" }, "Customize": { "title": "Personalize sua sessão", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "Se permite que um usuário vote em sua própria publicação", "allowMultipleVotes": "Permitir Vários Votos", "allowMultipleVotesHelp": "Se permite que um usuário vote várias vezes na mesma publicação", + "allowCancelVote": "Permitir Votos Cancelar", + "allowCancelVoteHelp": "Se permite que um usuário cancele o seu voto em uma publicação específica", "allowActions": "Permitir Ações", "allowActionsHelp": "Se deseja permitir o campo 'Ação' (seguimento) em cada postagem", "allowAuthorVisible": "Mostrar autor", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "Falha ao renomear a sessão", "error_cannot_save_columns": "Falha ao salvar colunas", "error_cannot_save_options": "Opções de salvamento falharam", + "error_cannot_cancel_votes": "Falha ao cancelar votos", "maxPostsReached": "Você atingiu o número máximo de postagens definido pelo moderador.", "iAmDone": "Estou pronto!", "iAmNotDoneYet": "Eu não terminei ainda...", diff --git a/frontend/src/translations/locales/pt-PT.json b/frontend/src/translations/locales/pt-PT.json index ac6e15a13..b09eac4c6 100644 --- a/frontend/src/translations/locales/pt-PT.json +++ b/frontend/src/translations/locales/pt-PT.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "Você tem {{number}} {{type}}restantes.", "voteRemainingOne": "Você tem apenas um {{type}} restante, faça com que ele conte!", "voteRemainingNone": "Você não tem nenhum {{type}} restante.", - "toggleGiphyButton": "Alternar imagem Giphy" + "toggleGiphyButton": "Alternar imagem Giphy", + "cancelVote": "Cancelar seus votos nesta publicação" }, "Customize": { "title": "Personalize sua sessão", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "Se permite que um usuário vote em sua própria publicação", "allowMultipleVotes": "Permitir Vários Votos", "allowMultipleVotesHelp": "Se permite que um usuário vote várias vezes na mesma publicação", + "allowCancelVote": "Permitir Votos Cancelar", + "allowCancelVoteHelp": "Se permite que um usuário cancele o seu voto em uma publicação específica", "allowActions": "Permitir Ações", "allowActionsHelp": "Se deseja permitir o campo 'Ação' (seguimento) em cada postagem", "allowAuthorVisible": "Mostrar autor", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "Falha ao renomear a sessão", "error_cannot_save_columns": "Falha ao salvar colunas", "error_cannot_save_options": "Opções de salvamento falharam", + "error_cannot_cancel_votes": "Falha ao cancelar votos", "maxPostsReached": "Você atingiu o número máximo de postagens definido pelo moderador.", "iAmDone": "Estou pronto!", "iAmNotDoneYet": "Eu não terminei ainda...", diff --git a/frontend/src/translations/locales/uk-UA.json b/frontend/src/translations/locales/uk-UA.json index e64f492f8..8def98d3a 100644 --- a/frontend/src/translations/locales/uk-UA.json +++ b/frontend/src/translations/locales/uk-UA.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "Залишилось {{number}} {{type}}сек.", "voteRemainingOne": "У вас залишилось тільки один {{type}} і ви можете зробити цей рахунок!", "voteRemainingNone": "У вас не залишилося {{type}}.", - "toggleGiphyButton": "Перемкнути зображення Giphy" + "toggleGiphyButton": "Перемкнути зображення Giphy", + "cancelVote": "Скасувати Ваші голосування в цьому повідомленні" }, "Customize": { "title": "Налаштуйте вашу сесію", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "Дозволити користувачу голосувати за власне повідомлення", "allowMultipleVotes": "Дозволити кілька голосів", "allowMultipleVotesHelp": "Чи дозволити користувачу голосувати кілька разів на тому ж повідомленні", + "allowCancelVote": "Дозволити скасування голосування", + "allowCancelVoteHelp": "Чи дозволити користувачу скасувати свій голос у конкретному повідомленні", "allowActions": "Дозволити дії", "allowActionsHelp": "Чи дозволяти поле \"Дія\" (follow-up) в кожному повідомленні", "allowAuthorVisible": "Показати автора", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "Перейменування сесії не вдалося", "error_cannot_save_columns": "Помилка збереження стовпців", "error_cannot_save_options": "Зберегти параметри не вдалося", + "error_cannot_cancel_votes": "Скасування голосування не вдалося", "maxPostsReached": "Ви досягли максимальної кількості повідомлень, встановлених модератором.", "iAmDone": "Я завершив!", "iAmNotDoneYet": "Я ще не завершив...", diff --git a/frontend/src/translations/locales/zh-CN.json b/frontend/src/translations/locales/zh-CN.json index 5b5154268..f901fe164 100644 --- a/frontend/src/translations/locales/zh-CN.json +++ b/frontend/src/translations/locales/zh-CN.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "您还剩下了 {{number}} {{type}}秒。", "voteRemainingOne": "你只剩下一个 {{type}} ,把它算进去!", "voteRemainingNone": "您没有剩余任何 {{type}}。", - "toggleGiphyButton": "切换Giphy图像" + "toggleGiphyButton": "切换Giphy图像", + "cancelVote": "取消您对此帖子的投票" }, "Customize": { "title": "自定义您的会话", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "是否允许用户对自己的帖子投票", "allowMultipleVotes": "允许多个投票", "allowMultipleVotesHelp": "是否允许用户在同一个帖子上多次投票", + "allowCancelVote": "允许取消投票", + "allowCancelVoteHelp": "是否允许用户取消对特定帖子的投票", "allowActions": "允许操作", "allowActionsHelp": "是否允许每个帖子上的“动作”(后续)", "allowAuthorVisible": "显示作者", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "重命名会话失败", "error_cannot_save_columns": "保存列失败", "error_cannot_save_options": "保存选项失败", + "error_cannot_cancel_votes": "取消投票失败", "maxPostsReached": "您已经达到了版主设置的帖子的最大数量。", "iAmDone": "我已完成!", "iAmNotDoneYet": "我还没有完成...", diff --git a/frontend/src/translations/locales/zh-TW.json b/frontend/src/translations/locales/zh-TW.json index 1d7cb2b4c..3e131c06a 100644 --- a/frontend/src/translations/locales/zh-TW.json +++ b/frontend/src/translations/locales/zh-TW.json @@ -55,7 +55,8 @@ "voteRemainingMultiple": "您還剩 {{number}} {{type}}秒。", "voteRemainingOne": "你只剩下一個 {{type}} 了,算了吧!", "voteRemainingNone": "你沒有任何剩餘的 {{type}}。", - "toggleGiphyButton": "切換 Giphy 圖像" + "toggleGiphyButton": "切換 Giphy 圖像", + "cancelVote": "" }, "Customize": { "title": "自定義您的會話", @@ -77,6 +78,8 @@ "allowSelfVotingHelp": "是否允許用戶對自己的帖子進行投票", "allowMultipleVotes": "允許多票", "allowMultipleVotesHelp": "是否允許用戶對同一個帖子進行多次投票", + "allowCancelVote": "", + "allowCancelVoteHelp": "", "allowActions": "允許操作", "allowActionsHelp": "是否允許每個帖子上的“操作”(跟進)字段", "allowAuthorVisible": "顯示作者", @@ -129,6 +132,7 @@ "error_cannot_rename_session": "重命名會話失敗", "error_cannot_save_columns": "保存列失敗", "error_cannot_save_options": "保存選項失敗", + "error_cannot_cancel_votes": "", "maxPostsReached": "您已達到版主設置的最大帖子數。", "iAmDone": "我受夠了!", "iAmNotDoneYet": "我還沒搞定...", diff --git a/frontend/src/views/Game.tsx b/frontend/src/views/Game.tsx index 9a357a82f..13cb237f4 100644 --- a/frontend/src/views/Game.tsx +++ b/frontend/src/views/Game.tsx @@ -65,6 +65,7 @@ function GamePage() { onDeletePostGroup, onEditPostGroup, onLike, + onCancelVotes, onRenameSession, onEditOptions, onEditColumns, @@ -172,6 +173,7 @@ function GamePage() { onAddGroup={onAddGroup} onDeletePost={onDeletePost} onLike={onLike} + onCancelVotes={onCancelVotes} onDeleteGroup={onDeletePostGroup} onEditGroup={onEditPostGroup} onRenameSession={onRenameSession} diff --git a/frontend/src/views/game/board/Board.tsx b/frontend/src/views/game/board/Board.tsx index ccb0f775b..b97063409 100644 --- a/frontend/src/views/game/board/Board.tsx +++ b/frontend/src/views/game/board/Board.tsx @@ -35,6 +35,7 @@ interface GameModeProps { onCombinePost: (post1: Post, post2: Post) => void; onDeletePost: (post: Post) => void; onLike: (post: Post, like: boolean) => void; + onCancelVotes: (post: Post) => void; onEdit: (post: Post) => void; onEditGroup: (group: PostGroup) => void; onDeleteGroup: (group: PostGroup) => void; @@ -76,6 +77,7 @@ function GameMode({ onCombinePost, onDeletePost, onLike, + onCancelVotes, onEdit, onEditGroup, onDeleteGroup, @@ -160,6 +162,7 @@ function GameMode({ onDelete={onDeletePost} onLike={(post) => onLike(post, true)} onDislike={(post) => onLike(post, false)} + onCancelVotes={onCancelVotes} onEdit={onEdit} onEditGroup={onEditGroup} onDeleteGroup={onDeleteGroup} diff --git a/frontend/src/views/game/board/Column.tsx b/frontend/src/views/game/board/Column.tsx index b85dc3264..f65e97f15 100644 --- a/frontend/src/views/game/board/Column.tsx +++ b/frontend/src/views/game/board/Column.tsx @@ -34,6 +34,7 @@ interface ColumnProps { onDeleteGroup: (group: PostGroup) => void; onLike: (post: Post) => void; onDislike: (post: Post) => void; + onCancelVotes: (post: Post) => void; onEdit: (post: Post) => void; onDelete: (post: Post) => void; } @@ -50,6 +51,7 @@ const Column: React.FC = ({ onAddGroup, onLike, onDislike, + onCancelVotes, onEdit, onDelete, onEditGroup, @@ -145,6 +147,7 @@ const Column: React.FC = ({ onLike={() => onLike(post)} onDislike={() => onDislike(post)} onDelete={() => onDelete(post)} + onCancelVotes={() => onCancelVotes(post)} onEdit={(content) => onEdit({ ...post, @@ -188,6 +191,7 @@ const Column: React.FC = ({ color={color} onLike={() => onLike(post)} onDislike={() => onDislike(post)} + onCancelVotes={() => onCancelVotes(post)} onDelete={() => onDelete(post)} onEdit={(content) => onEdit({ diff --git a/frontend/src/views/game/board/__tests__/permissions-logic.test.ts b/frontend/src/views/game/board/__tests__/permissions-logic.test.ts index c9502a676..ec04f8ff5 100644 --- a/frontend/src/views/game/board/__tests__/permissions-logic.test.ts +++ b/frontend/src/views/game/board/__tests__/permissions-logic.test.ts @@ -75,6 +75,8 @@ const session = (options: SessionOptions, ...posts: Post[]): Session => ({ groups: [], encrypted: null, locked: false, + messages: [], + ready: [], }); describe('Session Permission Logic', () => { @@ -492,4 +494,44 @@ describe('Posts Permission Logic', () => { const result = postPermissionLogic(p, s, currentUser); expect(result.isBlurred).toBe(false); }); + + it('When votes can be cancelled', () => { + const p = post(anotherUser, [currentUser]); + const s = session( + { + ...defaultOptions, + allowCancelVote: true, + }, + p + ); + const result = postPermissionLogic(p, s, currentUser); + expect(result.canCancelVote).toBe(true); + }); + + it(`When votes can be cancelled but there aren't any vote`, () => { + const p = post(anotherUser, [currentUser]); + p.votes = []; + const s = session( + { + ...defaultOptions, + allowCancelVote: true, + }, + p + ); + const result = postPermissionLogic(p, s, currentUser); + expect(result.canCancelVote).toBe(false); + }); + + it('When votes cannot be cancelled', () => { + const p = post(anotherUser, [currentUser]); + const s = session( + { + ...defaultOptions, + allowCancelVote: false, + }, + p + ); + const result = postPermissionLogic(p, s, currentUser); + expect(result.canCancelVote).toBe(false); + }); }); diff --git a/frontend/src/views/game/board/permissions-logic.ts b/frontend/src/views/game/board/permissions-logic.ts index ab0ceda2c..39eaa9324 100644 --- a/frontend/src/views/game/board/permissions-logic.ts +++ b/frontend/src/views/game/board/permissions-logic.ts @@ -46,6 +46,7 @@ export interface PostUserPermissions { canUseGiphy: boolean; canReorder: boolean; canCreateGroup: boolean; + canCancelVote: boolean; isBlurred: boolean; } @@ -67,6 +68,7 @@ export function postPermissionLogic( canDisplayUpVote: false, canReorder: false, canCreateGroup: false, + canCancelVote: false, isBlurred: false, }; } @@ -80,6 +82,7 @@ export function postPermissionLogic( allowGiphy, allowGrouping, allowReordering, + allowCancelVote, blurCards, } = session.options; @@ -88,6 +91,7 @@ export function postPermissionLogic( const userId = user ? user.id : -1; const isAuthor = user ? user.id === post.user.id : false; const canPotentiallyVote = isLoggedIn && allowSelfVoting ? true : !isAuthor; + const hasVoted = some(post.votes, (u) => u.userId === userId); const hasVotedOrAuthor = (!allowMultipleVotes && some(post.votes, (u) => u.userId === userId && u.type === 'like')) || @@ -109,6 +113,7 @@ export function postPermissionLogic( const canUseGiphy = isLoggedIn && allowGiphy; const canReorder = isLoggedIn && allowReordering; const canCreateGroup = isLoggedIn && allowGrouping; + const canCancelVote = hasVoted && allowCancelVote; const isBlurred = blurCards && !isAuthor; return { @@ -123,6 +128,7 @@ export function postPermissionLogic( canUseGiphy, canCreateGroup, canReorder, + canCancelVote, isBlurred, }; } diff --git a/frontend/src/views/game/board/post/Post.tsx b/frontend/src/views/game/board/post/Post.tsx index 027524e98..c55026743 100644 --- a/frontend/src/views/game/board/post/Post.tsx +++ b/frontend/src/views/game/board/post/Post.tsx @@ -17,6 +17,7 @@ import { Assignment, AssignmentOutlined, EmojiEmotionsOutlined, + Clear, } from '@mui/icons-material'; import { Draggable, DraggableProvided } from 'react-beautiful-dnd'; import { useTranslation } from 'react-i18next'; @@ -45,6 +46,7 @@ interface PostItemProps { search: string; onLike: () => void; onDislike: () => void; + onCancelVotes: () => void; onEdit: (content: string) => void; onEditAction: (action: string) => void; onEditGiphy: (giphyId: string | null) => void; @@ -72,6 +74,7 @@ const PostItem = ({ search, onLike, onDislike, + onCancelVotes, onEdit, onEditAction, onEditGiphy, @@ -85,6 +88,7 @@ const PostItem = ({ canDownVote, canDisplayUpVote, canDisplayDownVote, + canCancelVote, canShowAuthor, canReorder, canUseGiphy, @@ -308,6 +312,14 @@ const PostItem = ({ ariaLabel="Dislike" /> ) : null} + {canCancelVote ? ( + } + tooltip={t('Post.cancelVote')} + ariaLabel={t('Post.cancelVote')} + onClick={onCancelVotes} + /> + ) : null} )} diff --git a/frontend/src/views/game/useGame.ts b/frontend/src/views/game/useGame.ts index bebfc15ee..ee28107f7 100644 --- a/frontend/src/views/game/useGame.ts +++ b/frontend/src/views/game/useGame.ts @@ -23,6 +23,8 @@ import { Message, WsUserReadyPayload, ChatMessagePayload, + WsCancelVotesPayload, + WsReceiveCancelVotesPayload, } from 'common'; import { v4 } from 'uuid'; import find from 'lodash/find'; @@ -125,12 +127,15 @@ const useGame = (sessionId: string) => { editColumns, lockSession, userReady, + cancelVotes, } = useSession(); const allowMultipleVotes = session ? session.options.allowMultipleVotes : false; + const allowCancelVotes = session ? session.options.allowCancelVote : false; + // Send function, built with current socket, user and sessionId const send = useMemo( () => (socket ? sendFactory(socket, sessionId, setAcks) : null), @@ -322,6 +327,16 @@ const useGame = (sessionId: string) => { } ); + socket.on( + Actions.RECEIVE_CANCEL_VOTES, + ({ postId, userId }: WsReceiveCancelVotesPayload) => { + if (debug) { + console.log('Receive cancel votes: ', postId, userId); + } + cancelVotes(postId, userId); + } + ); + socket.on(Actions.RECEIVE_EDIT_POST, (post: Post | null) => { if (debug) { console.log('Receive edit post: ', post); @@ -421,6 +436,7 @@ const useGame = (sessionId: string) => { enqueueSnackbar, setUnauthorised, userReady, + cancelVotes, userId, ]); @@ -667,6 +683,32 @@ const useGame = (sessionId: string) => { [user, send, updatePost, allowMultipleVotes] ); + const onCancelVotes = useCallback( + (post: Post) => { + if (send) { + if (!user) { + return; + } + const existingVotes = post.votes.filter((v) => v.userId === user.id); + + if (!existingVotes.length || !allowCancelVotes) { + return; + } + + const modifiedPost: Post = { + ...post, + votes: post.votes.filter((v) => v.userId !== user.id), + }; + updatePost(modifiedPost); + send(Actions.CANCEL_VOTES_SUCCESS, { + postId: post.id, + }); + trackAction(Actions.CANCEL_VOTES_SUCCESS); + } + }, + [user, send, updatePost, allowCancelVotes] + ); + const onRenameSession = useCallback( (name: string) => { if (send) { @@ -745,6 +787,7 @@ const useGame = (sessionId: string) => { onDeletePost, onDeletePostGroup, onLike, + onCancelVotes, onRenameSession, onEditOptions, onEditColumns, diff --git a/frontend/src/views/game/useSession.ts b/frontend/src/views/game/useSession.ts index be56e713e..ffaf77dae 100644 --- a/frontend/src/views/game/useSession.ts +++ b/frontend/src/views/game/useSession.ts @@ -29,6 +29,7 @@ interface UseSession { editColumns: (columns: ColumnDefinition[]) => void; lockSession: (locked: boolean) => void; userReady: (userId: string, ready?: boolean) => void; + cancelVotes: (postId: string, userId: string) => void; } export default function useSession(): UseSession { @@ -174,6 +175,29 @@ export default function useSession(): UseSession { [setSession] ); + const cancelVotes = useCallback( + (postId: string, userId: string) => { + setSession((session) => { + if (!session) { + return session; + } + return { + ...session, + posts: session.posts.map((p) => { + if (p.id !== postId) { + return p; + } + return { + ...p, + votes: p.votes.filter((v) => v.userId !== userId), + }; + }), + }; + }); + }, + [setSession] + ); + const deletePost = useCallback( (postId: string) => { setSession((session) => @@ -285,5 +309,6 @@ export default function useSession(): UseSession { editOptions, lockSession, userReady, + cancelVotes, }; } diff --git a/frontend/src/views/session-editor/sections/votes/VotingSection.tsx b/frontend/src/views/session-editor/sections/votes/VotingSection.tsx index 5dcd47bcf..94e92c1c2 100644 --- a/frontend/src/views/session-editor/sections/votes/VotingSection.tsx +++ b/frontend/src/views/session-editor/sections/votes/VotingSection.tsx @@ -32,6 +32,15 @@ function VotingSection({ options, onChange }: VotingSectionProps) { }, [onChange, options] ); + const setAllowCancelVote = useCallback( + (value: boolean) => { + onChange({ + ...options, + allowCancelVote: value, + }); + }, + [onChange, options] + ); const setMaxUpVotes = useCallback( (value: number | null) => { onChange({ @@ -90,6 +99,15 @@ function VotingSection({ options, onChange }: VotingSectionProps) { onChange={setAllowMultipleVotes} /> + + + ); }