From 9df5ad06f301c51317db24c4db30f16a83bfebc1 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 12 Mar 2025 16:08:56 -0300 Subject: [PATCH 001/117] [unmount-plugin-on-meeting-end] - Implement function to unmount plugin when meeting ended or user is ejected --- src/components/pick-random-user/component.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index 66401d6..7f41f66 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -25,6 +25,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const [filterOutPickedUsers, setFilterOutPickedUsers] = useState(true); const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); const currentUserInfo = pluginApi.useCurrentUser(); + const shouldUnmountPlugin = pluginApi.useShouldUnmountPlugin(); const { data: currentUser } = currentUserInfo; const allUsersInfo = pluginApi .useCustomSubscription(USERS_MORE_INFORMATION); @@ -124,7 +125,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { useEffect(() => { if (!currentUser?.presenter && dispatchModalInformationFromPresenter) handleCloseModal(); }, [currentUser]); - return ( + return !shouldUnmountPlugin && ( <> Date: Mon, 24 Mar 2025 15:29:31 -0400 Subject: [PATCH 002/117] docs: Update path for bbb-web configs in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 364fee4..003158f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ To use the plugin in BigBlueButton, send this parameter along in create call: pluginManifests=[{"url":"/path/to/manifest.json"}] ``` -Or additionally, you can add this same configuration in the `.properties` file from `bbb-web` in `/usr/share/bbb-web/WEB-INF/classes/bigbluebutton.properties` +Or additionally, you can add this same configuration in `/etc/bigbluebutton/bbb-web.properties`. ## Development mode From cf2995a2ed52035a1b085c710fa8a9b30f0ce30f Mon Sep 17 00:00:00 2001 From: antobinary <6312397+antobinary@users.noreply.github.com> Date: Mon, 24 Mar 2025 19:56:29 +0000 Subject: [PATCH 003/117] chore: bump version to 0.2.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9d558b9..5e7db9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pick-random-user-plugin", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pick-random-user-plugin", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/package.json b/package.json index 2616e63..477f706 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pick-random-user-plugin", - "version": "0.1.0", + "version": "0.2.0", "private": true, "main": "./src/index.tsx", "dependencies": { From 9126da2f555e0da47ed1839a645780448acf2cca Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Mon, 31 Mar 2025 18:11:09 -0300 Subject: [PATCH 004/117] [migrate-to-styled-components] start of styled-component migration --- package-lock.json | 9 +- package.json | 2 +- src/components/modal/component.tsx | 14 ++-- .../modal/picked-user-view/component.tsx | 1 + .../modal/presenter-view/component.tsx | 66 +++++++-------- .../modal/presenter-view/styles.tsx | 81 ++++++++++++++++++ src/components/modal/styles.tsx | 82 +++++++++++++++++++ 7 files changed, 206 insertions(+), 49 deletions(-) create mode 100644 src/components/modal/presenter-view/styles.tsx create mode 100644 src/components/modal/styles.tsx diff --git a/package-lock.json b/package-lock.json index 9d558b9..19964cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.66", + "bigbluebutton-html-plugin-sdk": "0.0.73", "path": "^0.12.7", "react": "^18.2.0", "react-chat-elements": "^12.0.14", @@ -3548,10 +3548,9 @@ "license": "MIT" }, "node_modules/bigbluebutton-html-plugin-sdk": { - "version": "0.0.66", - "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.66.tgz", - "integrity": "sha512-cjwq4N5EKB177bB910Jw6+QP/NN5bdZXxV16rNfpz3B28/NTMQ5of2x62TA7LEdJcM3KZNxFE5Caxhx9cpibmQ==", - "license": "LGPL-3.0", + "version": "0.0.73", + "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.73.tgz", + "integrity": "sha512-upf5Np45+F26qlad1ZUDJFFyAFyPHF3cU81jskEUq2VoEzUCX8zjBVvIsksut79Slz/B622UbX8blGTm+s7/lw==", "dependencies": { "@apollo/client": "^3.8.7", "@browser-bunyan/console-formatted-stream": "^1.8.0", diff --git a/package.json b/package.json index 2616e63..06ef56e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.66", + "bigbluebutton-html-plugin-sdk": "0.0.73", "path": "^0.12.7", "react": "^18.2.0", "react-chat-elements": "^12.0.14", diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 4aece1a..b6a5e7c 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import * as React from 'react'; -import * as ReactModal from 'react-modal'; +import * as Styled from './styles'; import { PickUserModalProps } from './types'; import './style.css'; import { PickedUserViewComponent } from './picked-user-view/component'; @@ -43,9 +43,8 @@ export function PickUserModal(props: PickUserModalProps) { setShowPresenterView(currentUser?.presenter && !pickedUserWithEntryId); }, [currentUser, pickedUserWithEntryId]); return ( - @@ -54,9 +53,8 @@ export function PickUserModal(props: PickUserModalProps) { width: '100%', alignItems: 'flex-end', display: 'flex', flexDirection: 'column', }} > - + { showPresenterView @@ -101,6 +99,6 @@ export function PickUserModal(props: PickUserModalProps) { ) } - + ); } diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index 3cb6db3..47a9b10 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { PickedUserViewComponentProps } from './types'; +import '../style.css'; export function PickedUserViewComponent(props: PickedUserViewComponentProps) { const { diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index d2e5d0b..5c10170 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; import { DataChannelEntryResponseType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; +import * as Styled from './styles'; import { PickedUser } from '../../pick-random-user/types'; import { PresenterViewComponentProps } from './types'; @@ -51,15 +52,11 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { } = props; return ( -
-
-

Options

-

- - - -

-
-
-

Available for selection

-

+ Include already picked users + + + + + Available for selection + {users?.length} {' '} {userRole} : {' '} {makeHorizontalListOfNames(users)} -

-
-
-
-

Previously picked

+ + + + + Previously picked -
-
    + + { makeVerticalListOfNames(dataChannelPickedUsers) } -
-
+ + { users?.length > 0 ? ( - + ) : (

No @@ -153,6 +149,6 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) {

) } -
+ ); } diff --git a/src/components/modal/presenter-view/styles.tsx b/src/components/modal/presenter-view/styles.tsx new file mode 100644 index 0000000..c7b90c9 --- /dev/null +++ b/src/components/modal/presenter-view/styles.tsx @@ -0,0 +1,81 @@ +import styled from 'styled-components'; + +const PresenterViewContentWrapper = styled.div` + width: '100%'; + height: '100%'; + align-items: 'flex-start'; + display: 'flex'; + flex-direction: 'column'; +`; + +const PresenterViewSectionWrapper = styled.div` + align-items: flex-start; + margin-top: 20px; +`; + +const PresenterViewSectionTitle = styled.div` + margin: 5px 0px; + font-weight: 600; + font-size: 20px; +`; + +const PresenterViewSectionTitleWrapper = styled.div` + display: flex; + flex-direction: row; + align-items: center; +`; + +const PresenterViewSectionList = styled.ul` + font-size: 18px; + list-style-type: none; +`; + +const PresenterViewSectionContent = styled.div` + display: flex; + flex-direction: column; + font-size: 18px; + padding-left: 40px; +`; + +// Section Content related: + +const CheckboxLabelWrapper = styled.label` + display: flex; + align-items: center; +`; + +const CheckboxLabel = styled.span` + margin-left: 5px; +`; + +const PickUserButton = styled.button` + border: 3px solid transparent; + overflow: visible; + display: inline-block; + background-color: #0F70D7; + color: #FFF; + border-radius: 2px; + font-weight: 600; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + user-select: none; + padding: 8px 15px; + &:hover { + background-color: #0C57A7; + } +`; + +export { + PresenterViewContentWrapper, + PresenterViewSectionWrapper, + PresenterViewSectionTitle, + PresenterViewSectionTitleWrapper, + PresenterViewSectionList, + PresenterViewSectionContent, + CheckboxLabelWrapper, + CheckboxLabel, + PickUserButton, +}; diff --git a/src/components/modal/styles.tsx b/src/components/modal/styles.tsx new file mode 100644 index 0000000..b9d5cdb --- /dev/null +++ b/src/components/modal/styles.tsx @@ -0,0 +1,82 @@ +import * as ReactModal from 'react-modal'; +import styled from 'styled-components'; + +const PluginModal = styled(ReactModal)` + position: relative; + z-index: 1000 !important; + outline: transparent; + outline-width: 2px; + outline-style: solid; + display: flex; + flex-direction: column; + padding: 2rem; + box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.7); + background-color: #fff !important; + max-width: 60vw; + max-height: 80vh; + border-radius: 0.2rem; + overflow: auto; + overflow-y: hidden; + background-repeat: no-repeat; + background-color: transparent; + background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; + background-attachment: local, local, scroll, scroll; + + &::-webkit-scrollbar { + width: 5px; + height: 5px; + } + &::-webkit-scrollbar-button { + width: 0; + height: 0; + } + &::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.25); + border: none; + border-radius: 50px; + } + &::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.5); + } + &::-webkit-scrollbar-thumb:active { + background: rgba(0, 0, 0, 0.25); + } + &::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.25); + border: none; + border-radius: 50px; + } + &::-webkit-scrollbar-corner { + background: transparent; + } + + @media only screen and (max-width: 40em) { + max-width: 95vw; + } + + @media only screen and (min-width: 40.063em) { + max-width: 80vw; + } +`; + +const CloseButton = styled.button` + font-size: 40px; + background: none; + color: inherit; + font: inherit; + cursor: pointer; + outline: inherit; + border: none; + &:hover { + background-color: #EEE; + } +`; + +const CloseButtonWrapper = styled.div` + width: '100%'; + align-items: 'flex-end'; + display: 'flex'; + flex-direction: 'column'; +`; + +export { PluginModal, CloseButton, CloseButtonWrapper }; From 5903a292e907883933b36a537a06ad29cb6f6192 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Tue, 1 Apr 2025 09:29:36 -0300 Subject: [PATCH 005/117] [migrate-to-styled-components] fix indefinitely growth of picked-user list --- src/components/modal/component.tsx | 8 ++------ src/components/modal/presenter-view/component.tsx | 12 +++++++----- src/components/modal/presenter-view/styles.tsx | 13 ++++++++++++- src/components/modal/styles.tsx | 8 ++++---- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index b6a5e7c..57adfdd 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -48,11 +48,7 @@ export function PickUserModal(props: PickUserModalProps) { isOpen={showModal} onRequestClose={handleCloseModal} > -
+ { @@ -64,7 +60,7 @@ export function PickUserModal(props: PickUserModalProps) { className="icon-bbb-close" /> -
+ { showPresenterView ? ( diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index 5c10170..a0e24ea 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -121,11 +121,13 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { Clear All - - { - makeVerticalListOfNames(dataChannelPickedUsers) - } - + + + { + makeVerticalListOfNames(dataChannelPickedUsers) + } + + { users?.length > 0 ? ( diff --git a/src/components/modal/presenter-view/styles.tsx b/src/components/modal/presenter-view/styles.tsx index c7b90c9..30679ee 100644 --- a/src/components/modal/presenter-view/styles.tsx +++ b/src/components/modal/presenter-view/styles.tsx @@ -9,8 +9,10 @@ const PresenterViewContentWrapper = styled.div` `; const PresenterViewSectionWrapper = styled.div` + display: flex; align-items: flex-start; - margin-top: 20px; + flex-direction: column; + max-height: 30%; `; const PresenterViewSectionTitle = styled.div` @@ -25,8 +27,16 @@ const PresenterViewSectionTitleWrapper = styled.div` align-items: center; `; +const PresenterViewSectionListWrapper = styled.div` + overflow-y: auto; + max-height: 10rem; + margin-bottom: .75rem; + width: 100%; +`; + const PresenterViewSectionList = styled.ul` font-size: 18px; + margin: 0; list-style-type: none; `; @@ -74,6 +84,7 @@ export { PresenterViewSectionTitle, PresenterViewSectionTitleWrapper, PresenterViewSectionList, + PresenterViewSectionListWrapper, PresenterViewSectionContent, CheckboxLabelWrapper, CheckboxLabel, diff --git a/src/components/modal/styles.tsx b/src/components/modal/styles.tsx index b9d5cdb..ab223e3 100644 --- a/src/components/modal/styles.tsx +++ b/src/components/modal/styles.tsx @@ -73,10 +73,10 @@ const CloseButton = styled.button` `; const CloseButtonWrapper = styled.div` - width: '100%'; - align-items: 'flex-end'; - display: 'flex'; - flex-direction: 'column'; + width: 100%; + align-items: flex-end; + display: flex; + flex-direction: column; `; export { PluginModal, CloseButton, CloseButtonWrapper }; From ca220e57508a1e65a00f770d6b2bffd6ee2670f7 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Tue, 1 Apr 2025 09:55:46 -0300 Subject: [PATCH 006/117] [migrate-to-styled-components] finish styled-components migration --- src/components/modal/component.tsx | 1 - .../modal/picked-user-view/component.tsx | 21 +- .../modal/picked-user-view/styles.tsx | 42 ++++ .../modal/picked-user-view/types.ts | 4 + .../modal/presenter-view/component.tsx | 5 +- .../modal/presenter-view/styles.tsx | 15 ++ src/components/modal/style.css | 184 ------------------ 7 files changed, 71 insertions(+), 201 deletions(-) create mode 100644 src/components/modal/picked-user-view/styles.tsx delete mode 100644 src/components/modal/style.css diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 57adfdd..1f225a9 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react'; import * as React from 'react'; import * as Styled from './styles'; import { PickUserModalProps } from './types'; -import './style.css'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index 47a9b10..1fc4d92 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { PickedUserViewComponentProps } from './types'; -import '../style.css'; +import * as Styled from './styles'; export function PickedUserViewComponent(props: PickedUserViewComponentProps) { const { @@ -36,22 +36,17 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { } }; return ( -
-

{title}

+ + {title} { (pickedUserWithEntryId) ? ( <> -
{pickedUserWithEntryId.pickedUser?.name.slice(0, 2)} -
-

{pickedUserWithEntryId.pickedUser?.name}

+ + {pickedUserWithEntryId.pickedUser?.name} ) : null } @@ -60,6 +55,6 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { ) : null } -
+ ); } diff --git a/src/components/modal/picked-user-view/styles.tsx b/src/components/modal/picked-user-view/styles.tsx new file mode 100644 index 0000000..d9e973d --- /dev/null +++ b/src/components/modal/picked-user-view/styles.tsx @@ -0,0 +1,42 @@ +import styled from 'styled-components'; +import { ModalAvatarProps } from './types'; + +const PickedUserViewWrapper = styled.div` + width: 100%; + height: 100%; + align-items: center; + display: flex; + flex-direction: column; +`; + +const PickedUserViewTitle = styled.h1` + font-weight: 600; + font-size: 20px; +`; + +const PickedUserAvatar = styled.div` + background-color: ${({ background }) => background}; + height: 6rem; + width: 6rem; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + color: white; + font-size: 2.75rem; + font-weight: 400; + margin-bottom: .25rem; + text-transform: capitalize; +`; + +const PickedUserName = styled.p` + font-size: 30px; + font-weight: 500; +`; + +export { + PickedUserViewWrapper, + PickedUserViewTitle, + PickedUserAvatar, + PickedUserName, +}; diff --git a/src/components/modal/picked-user-view/types.ts b/src/components/modal/picked-user-view/types.ts index 8827da1..be7ddc1 100644 --- a/src/components/modal/picked-user-view/types.ts +++ b/src/components/modal/picked-user-view/types.ts @@ -10,3 +10,7 @@ export interface PickedUserViewComponentProps { setShowPresenterView: React.Dispatch>; dispatcherPickedUser: PushEntryFunction } + +export interface ModalAvatarProps { + background: string; +} diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index a0e24ea..642ca00 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -111,15 +111,14 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { Previously picked - + diff --git a/src/components/modal/presenter-view/styles.tsx b/src/components/modal/presenter-view/styles.tsx index 30679ee..d6d7ab8 100644 --- a/src/components/modal/presenter-view/styles.tsx +++ b/src/components/modal/presenter-view/styles.tsx @@ -21,6 +21,20 @@ const PresenterViewSectionTitle = styled.div` font-size: 20px; `; +const PresenterViewSectionClearAllButton = styled.button` + padding: 1px 10px; + margin-left: 8px; + font-size: 15px; + background: #efefef; + border: none; + color: inherit; + border-radius: 8px; + cursor: pointer; + &:hover { + background-color: #ddd; + } +`; + const PresenterViewSectionTitleWrapper = styled.div` display: flex; flex-direction: row; @@ -89,4 +103,5 @@ export { CheckboxLabelWrapper, CheckboxLabel, PickUserButton, + PresenterViewSectionClearAllButton, }; diff --git a/src/components/modal/style.css b/src/components/modal/style.css deleted file mode 100644 index c4414cf..0000000 --- a/src/components/modal/style.css +++ /dev/null @@ -1,184 +0,0 @@ -.plugin-modal { - position: relative; - z-index: 900 !important; - - outline: transparent; - outline-width: 2px; - outline-style: solid; - display: flex; - flex-direction: column; - padding: 2rem; - box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.7); - background-color: #FFF !important; - max-width: 60vw; - max-height: 100%; - border-radius: .2rem; - - color: #4E5A66; - - overflow: auto; - - overflow-y: hidden; - - background-repeat: no-repeat; - background-color: transparent; - background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; - background-attachment: local, local, scroll, scroll; - - &::-webkit-scrollbar { - width: 5px; - height: 5px; - } - &::-webkit-scrollbar-button { - width: 0; - height: 0; - } - &::-webkit-scrollbar-thumb { - background: rgba(0,0,0,.25); - border: none; - border-radius: 50px; - } - &::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,.5); } - &::-webkit-scrollbar-thumb:active { background: rgba(0,0,0,.25); } - &::-webkit-scrollbar-track { - background: rgba(0,0,0,.25); - border: none; - border-radius: 50px; - } - &::-webkit-scrollbar-track:hover { background: rgba(0,0,0,.25); } - &::-webkit-scrollbar-track:active { background: rgba(0,0,0,.25); } - &::-webkit-scrollbar-corner { background: 0 0; } - - @media only screen and (max-width: 40em) { - max-width: 95vw; - } - - @media only screen and (min-width: 40.063em) { - max-width: 80vw; - } -} - -.modal-overlay { - z-index: 900; - display: flex; - align-items: center; - justify-content: center; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: rgba(6, 23, 42, 0.75); -} - -.title { - font-weight: 600; - font-size: 20px; -} - -.modal-avatar { -height: 6rem; -width: 6rem; -border-radius: 50%; -display: flex; -justify-content: center; -align-items: center; -color: white; -font-size: 2.75rem; -font-weight: 400; -margin-bottom: .25rem; -text-transform: capitalize; -} - -.user-name { - font-size: 30px; - font-weight: 500; -} - -.moderator-view-label { - margin: 5px 0px; - font-weight: 600; - font-size: 20px; -} - -.moderator-view-value { - display: flex; - flex-direction: column; - font-size: 18px; - padding-left: 40px; -} - -.moderator-view-wrapper { - align-items: flex-start; - margin-top: 20px; -} - -.moderator-view-list { - font-size: 18px; - list-style-type: none; -} - -.clickable-close{ - font-size: 40px; - background: none; - color: inherit; - font: inherit; - cursor: pointer; - outline: inherit; - border: none; -} - -.clickable-close:hover { - background-color: #EEE; -} - -.clickable { - padding: 1px 10px; - margin-left: 8px; - font-size: 15px; - background: #efefef; - border: none; - color: inherit; - border-radius: 8px; - cursor: pointer; -} - -.clickable:hover { - background-color: #ddd; -} - -.moderator-view-wrapper-title { - display: flex; - flex-direction: row; - align-items: center; -} - -.check-box-label { - margin-left: 5px; -} - -.check-box-label-container { - display: flex; - align-items: center; -} - -.button-style { - border: 3px solid transparent; - overflow: visible; - display: inline-block; - background-color: #0F70D7; - color: #FFF; - border-radius: 2px; - font-weight: 600; - line-height: 1; - text-align: center; - white-space: nowrap; - vertical-align: middle; - cursor: pointer; - user-select: none; - padding: 8px 15px; -} - -.button-style:hover { - background-color: #0C57A7; -} From 232dc2708c71ea10a52a1d63c0edbe52ed447524 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Tue, 1 Apr 2025 10:26:16 -0300 Subject: [PATCH 007/117] [migrate-to-styled-components] skip libcheck for tsc --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index 1a65d84..e656983 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "target": "es5", "jsx": "react", "allowJs": true, + "skipLibCheck": true, "moduleResolution": "node" }, "include": ["src/*"], From 271c321cf72b38b8e894f63212be940bd3df9f03 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Tue, 1 Apr 2025 17:20:31 -0300 Subject: [PATCH 008/117] [ping-selected-user] implemented ping for randomly picked user --- src/components/modal/component.tsx | 27 +++++++++++++++++++ .../modal/presenter-view/component.tsx | 3 ++- src/components/modal/types.ts | 3 +++ src/components/pick-random-user/component.tsx | 16 ++++++++++- 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 4aece1a..3bb5ab7 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -1,13 +1,31 @@ import { useEffect, useState } from 'react'; import * as React from 'react'; import * as ReactModal from 'react-modal'; +import { pluginLogger } from 'bigbluebutton-html-plugin-sdk'; import { PickUserModalProps } from './types'; import './style.css'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; +const TIMEOUT_CLOSE_NOTIFICATION = 5000; + +function notifyRandomlyPickedUser(message: string) { + if (!('Notification' in window)) { + pluginLogger.warn('This browser does not support notifications'); + } else if (Notification.permission === 'granted') { + const notification = new Notification(message); + setTimeout(() => { + notification.close(); + }, TIMEOUT_CLOSE_NOTIFICATION); + } else if (Notification.permission !== 'denied') { + pluginLogger.warn('Browser notification permission has been denied'); + } +} + export function PickUserModal(props: PickUserModalProps) { const { + pluginSettings, + isPluginSettingsLoading, showModal, handleCloseModal, users, @@ -41,6 +59,15 @@ export function PickUserModal(props: PickUserModalProps) { ); useEffect(() => { setShowPresenterView(currentUser?.presenter && !pickedUserWithEntryId); + // Play audio when user is selected + const isPingSoundEnabled = !isPluginSettingsLoading && pluginSettings?.pingSoundEnabled; + const pingSoundPath: string = pluginSettings?.pingSoundPath ? String(pluginSettings?.pingSoundPath) : 'resources/sounds/doorbell.mp3'; + if (isPingSoundEnabled && pickedUserWithEntryId + && pickedUserWithEntryId?.pickedUser?.userId === currentUser?.userId) { + const audio = new Audio(pingSoundPath); + audio.play(); + notifyRandomlyPickedUser(title); + } }, [currentUser, pickedUserWithEntryId]); return ( [], ) => list?.filter((u) => !!u.payloadJson).map((u) => { const time = new Date(u.createdAt); + const timeMiliseconds = time.getTime(); return ( -
  • +
  • {u.payloadJson.name} {' '} ( diff --git a/src/components/modal/types.ts b/src/components/modal/types.ts index 0f4748c..a9f3503 100644 --- a/src/components/modal/types.ts +++ b/src/components/modal/types.ts @@ -1,8 +1,11 @@ +import { PluginSettingsData } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-consumption/domain/settings/plugin-settings/types'; import { CurrentUserData, DeleteEntryFunction } from 'bigbluebutton-html-plugin-sdk'; import { DataChannelEntryResponseType, PushEntryFunction, ReplaceEntryFunction } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; import { PickedUser, PickedUserWithEntryId } from '../pick-random-user/types'; export interface PickUserModalProps { + pluginSettings: PluginSettingsData; + isPluginSettingsLoading: boolean; showModal: boolean; updatePickedRandomUser: ReplaceEntryFunction; handleCloseModal: () => void; diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index 66401d6..10d4703 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -16,6 +16,8 @@ import ActionButtonDropdownManager from '../extensible-areas/action-button-dropd function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { BbbPluginSdk.initialize(uuid); + const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); + const [showModal, setShowModal] = useState(false); const [ pickedUserWithEntryId, @@ -23,7 +25,17 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const [userFilterViewer, setUserFilterViewer] = useState(true); const [filterOutPresenter, setFilterOutPresenter] = useState(true); const [filterOutPickedUsers, setFilterOutPickedUsers] = useState(true); - const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); + + const { data: pluginSettings, loading: isPluginSettingsLoading } = pluginApi.usePluginSettings(); + + useEffect(() => { + if (!isPluginSettingsLoading + && pluginSettings + && pluginSettings.pingSoundEnabled) { + Notification.requestPermission(); + } + }, [isPluginSettingsLoading, pluginSettings]); + const currentUserInfo = pluginApi.useCurrentUser(); const { data: currentUser } = currentUserInfo; const allUsersInfo = pluginApi @@ -128,6 +140,8 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { <> Date: Tue, 1 Apr 2025 17:43:38 -0300 Subject: [PATCH 009/117] [ping-selected-user] Added documentation for new feature --- README.md | 41 ++++++++++++++++++++++++++++++ src/components/modal/component.tsx | 4 +-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 364fee4..72dab7e 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,47 @@ The Pick Random User Plugin shows a modal for moderator to pick a user (mainly v ![Gif of plugin demo](./public/assets/plugin.gif) +## Necessary configurations + +By default, no ping is sounded for the randomly picked user. To activate this feature, one must add the following configurations in their `/etc/bigbluebutton/bbb-html5.yml` file. + +So within that file and in `public.plugins` add the following configurations: + +```yaml +- name: PickRandomUserPlugin + settings: + pingSoundEnabled: true + pingSoundUrl: resources/sounds/doorbell.mp3 +``` + +The result yaml will look like: + +```yaml +public: + # ... + plugins: + - name: PickRandomUserPlugin + settings: + pingSoundEnabled: true # Enables the ping sound for this plugin true/false + pingSoundUrl: resources/sounds/doorbell.mp3 # This is the default and is not mandatory +``` + +Some other possible `pingSoundUrl` are (URL can be relative, if the `mp3` sound is being served from within the BBB server as showed right below): +- resources/sounds/alarm.mp3 +- resources/sounds/bbb-handRaise.mp3 +- resources/sounds/LeftCall.mp3 +- resources/sounds/RelaxingMusic.mp3 +- resources/sounds/aristocratDrums.mp3 +- resources/sounds/CalmMusic.mp3 +- resources/sounds/notify.mp3 +- resources/sounds/ScreenshareOff.mp3 +- resources/sounds/audioSample.mp3 +- resources/sounds/doorbell.mp3 +- resources/sounds/Poll.mp3 +- resources/sounds/userJoin.mp3 + +These are the possible `mp3` that already come within a BBB server, if you want a custom sound, just add the URL to the `mp3` file there and it will work out of the box. + ## Building the Plugin To build the plugin for production use, follow these steps: diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 3bb5ab7..82d4490 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -61,10 +61,10 @@ export function PickUserModal(props: PickUserModalProps) { setShowPresenterView(currentUser?.presenter && !pickedUserWithEntryId); // Play audio when user is selected const isPingSoundEnabled = !isPluginSettingsLoading && pluginSettings?.pingSoundEnabled; - const pingSoundPath: string = pluginSettings?.pingSoundPath ? String(pluginSettings?.pingSoundPath) : 'resources/sounds/doorbell.mp3'; + const pingSoundUrl: string = pluginSettings?.pingSoundUrl ? String(pluginSettings?.pingSoundUrl) : 'resources/sounds/doorbell.mp3'; if (isPingSoundEnabled && pickedUserWithEntryId && pickedUserWithEntryId?.pickedUser?.userId === currentUser?.userId) { - const audio = new Audio(pingSoundPath); + const audio = new Audio(pingSoundUrl); audio.play(); notifyRandomlyPickedUser(title); } From 1d468028d6f86d5a2e7e1b741cf7a4f743c5fea3 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 2 Apr 2025 11:18:11 -0300 Subject: [PATCH 010/117] [ping-selected-user] added cluster setups comments and fix --- README.md | 4 +++- src/components/modal/component.tsx | 10 ++++++++-- src/components/modal/types.ts | 12 ++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 72dab7e..814a2a6 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,9 @@ public: pingSoundUrl: resources/sounds/doorbell.mp3 # This is the default and is not mandatory ``` -Some other possible `pingSoundUrl` are (URL can be relative, if the `mp3` sound is being served from within the BBB server as showed right below): +Just a minor comment: This relative URLs can only be configured if the server on which BBB is running is not a cluster setup. If that's your case, you'll need to put the whole URL into the configuration. It's also worth mentioning that the default `pingSoundUrl` will work in cluser setups, so no worries on that. + +Some other possible `pingSoundUrl` are (and notice that this URL can be relative, if the `mp3` sound is being served from within the BBB server - and it's not a cluster setup - as showed right below): - resources/sounds/alarm.mp3 - resources/sounds/bbb-handRaise.mp3 - resources/sounds/LeftCall.mp3 diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 82d4490..fbf3884 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -2,13 +2,15 @@ import { useEffect, useState } from 'react'; import * as React from 'react'; import * as ReactModal from 'react-modal'; import { pluginLogger } from 'bigbluebutton-html-plugin-sdk'; -import { PickUserModalProps } from './types'; +import { PickUserModalProps, WindowClientSettings } from './types'; import './style.css'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; const TIMEOUT_CLOSE_NOTIFICATION = 5000; +declare const window: WindowClientSettings; + function notifyRandomlyPickedUser(message: string) { if (!('Notification' in window)) { pluginLogger.warn('This browser does not support notifications'); @@ -61,9 +63,13 @@ export function PickUserModal(props: PickUserModalProps) { setShowPresenterView(currentUser?.presenter && !pickedUserWithEntryId); // Play audio when user is selected const isPingSoundEnabled = !isPluginSettingsLoading && pluginSettings?.pingSoundEnabled; - const pingSoundUrl: string = pluginSettings?.pingSoundUrl ? String(pluginSettings?.pingSoundUrl) : 'resources/sounds/doorbell.mp3'; if (isPingSoundEnabled && pickedUserWithEntryId && pickedUserWithEntryId?.pickedUser?.userId === currentUser?.userId) { + const { cdn, basename } = window.meetingClientSettings.public.app; + const host = cdn + basename; + const pingSoundUrl: string = pluginSettings?.pingSoundUrl + ? String(pluginSettings?.pingSoundUrl) + : `${host}/resources/sounds/doorbell.mp3`; const audio = new Audio(pingSoundUrl); audio.play(); notifyRandomlyPickedUser(title); diff --git a/src/components/modal/types.ts b/src/components/modal/types.ts index a9f3503..ab0a27a 100644 --- a/src/components/modal/types.ts +++ b/src/components/modal/types.ts @@ -23,3 +23,15 @@ export interface PickUserModalProps { deletionFunction: DeleteEntryFunction; dispatcherPickedUser: PushEntryFunction; } + +export interface WindowClientSettings extends Window { + meetingClientSettings?: { + public: { + app: { + cdn: string; + basename: string; + } + + } + } +} From 9f6bccaae8df1b8e87bd985214b404d33ab81186 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 2 Apr 2025 17:17:34 -0300 Subject: [PATCH 011/117] [issue-34] add avatar picture (when present) to the selected user --- .../modal/picked-user-view/component.tsx | 21 +++++++++++++------ src/components/modal/style.css | 9 ++++++++ src/components/pick-random-user/queries.ts | 1 + src/components/pick-random-user/types.ts | 1 + 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index 3cb6db3..4ffdd80 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -34,6 +34,7 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { dispatcherPickedUser(null); } }; + const avatarUrl = pickedUserWithEntryId.pickedUser.avatar; return (
    -
    - {pickedUserWithEntryId.pickedUser?.name.slice(0, 2)} -
    + {avatarUrl ? ( + {`Avatar + ) : ( +
    + {pickedUserWithEntryId.pickedUser?.name.slice(0, 2)} +
    + )}

    {pickedUserWithEntryId.pickedUser?.name}

    ) : null diff --git a/src/components/modal/style.css b/src/components/modal/style.css index c4414cf..6693980 100644 --- a/src/components/modal/style.css +++ b/src/components/modal/style.css @@ -76,6 +76,15 @@ font-size: 20px; } +.modal-avatar-img { + height: 8rem; + width: 8rem; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; +} + .modal-avatar { height: 6rem; width: 6rem; diff --git a/src/components/pick-random-user/queries.ts b/src/components/pick-random-user/queries.ts index 95821a0..a1c498c 100644 --- a/src/components/pick-random-user/queries.ts +++ b/src/components/pick-random-user/queries.ts @@ -6,6 +6,7 @@ subscription usersMoreInformation { userId role presenter + avatar } } `; diff --git a/src/components/pick-random-user/types.ts b/src/components/pick-random-user/types.ts index 246c90f..f2e8e77 100644 --- a/src/components/pick-random-user/types.ts +++ b/src/components/pick-random-user/types.ts @@ -4,6 +4,7 @@ export interface PickedUser { userId: string; name: string; role: string; + avatar: string; color: string; } From 66359ddd322284af4de83ad23ada96f9ed4c81f1 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 3 Apr 2025 17:16:59 -0300 Subject: [PATCH 012/117] [add-localization] added localization to pick-random-user --- manifest.json | 1 + package-lock.json | 137 +++++++++++++++++- package.json | 3 +- public/index.html | 15 -- public/locales/en.json | 21 +++ public/locales/pt-BR.json | 21 +++ .../action-button-dropdown/component.tsx | 19 ++- .../action-button-dropdown/types.ts | 2 + src/components/modal/component.tsx | 15 +- .../modal/picked-user-view/component.tsx | 26 +++- .../modal/picked-user-view/types.ts | 3 +- .../modal/presenter-view/component.tsx | 103 ++++++++++--- src/components/modal/presenter-view/types.ts | 3 +- src/components/modal/types.ts | 2 + src/components/pick-random-user/component.tsx | 24 +++ tsconfig.json | 2 +- webpack.config.js | 5 + 17 files changed, 340 insertions(+), 62 deletions(-) delete mode 100644 public/index.html create mode 100644 public/locales/en.json create mode 100644 public/locales/pt-BR.json diff --git a/manifest.json b/manifest.json index 7058e2b..0d10b66 100644 --- a/manifest.json +++ b/manifest.json @@ -2,6 +2,7 @@ "requiredSdkVersion": "~0.0.65", "name": "PickRandomUserPlugin", "javascriptEntrypointUrl": "PickRandomUserPlugin.js", + "localesBaseUrl": "locales", "dataChannels": [ { "name": "modalInformationFromPresenter", diff --git a/package-lock.json b/package-lock.json index 9d558b9..ec5cbcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,12 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.66", + "bigbluebutton-html-plugin-sdk": "0.0.73", "path": "^0.12.7", "react": "^18.2.0", "react-chat-elements": "^12.0.14", "react-dom": "^18.2.0", + "react-intl": "^6.6.8", "react-modal": "^3.16.1", "styled-components": "^5.3.3" }, @@ -1912,6 +1913,93 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.4.tgz", + "integrity": "sha512-lFyiQDVvSbQOpU+WFd//ILolGj4UgA/qXrKeZxdV14uKiAUiPAtX6XAn7WBCRi7Mx6I7EybM9E5yYn4BIpZWYg==", + "dependencies": { + "@formatjs/fast-memoize": "2.2.3", + "@formatjs/intl-localematcher": "0.5.8", + "tslib": "2" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.3.tgz", + "integrity": "sha512-3jeJ+HyOfu8osl3GNSL4vVHUuWFXR03Iz9jjgI7RwjG6ysu/Ymdr0JRCPHfF5yGbTE6JCrd63EpvX1/WybYRbA==", + "dependencies": { + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.4.tgz", + "integrity": "sha512-Tbvp5a9IWuxUcpWNIW6GlMQYEc4rwNHR259uUFoKWNN1jM9obf9Ul0e+7r7MvFOBNcN+13K7NuKCKqQiAn1QEg==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "@formatjs/icu-skeleton-parser": "1.8.8", + "tslib": "2" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.8.tgz", + "integrity": "sha512-vHwK3piXwamFcx5YQdCdJxUQ1WdTl6ANclt5xba5zLGDv5Bsur7qz8AD7BevaKxITwpgDeU0u8My3AIibW9ywA==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "tslib": "2" + } + }, + "node_modules/@formatjs/intl": { + "version": "2.10.15", + "resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-2.10.15.tgz", + "integrity": "sha512-i6+xVqT+6KCz7nBfk4ybMXmbKO36tKvbMKtgFz9KV+8idYFyFbfwKooYk8kGjyA5+T5f1kEPQM5IDLXucTAQ9g==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "@formatjs/fast-memoize": "2.2.3", + "@formatjs/icu-messageformat-parser": "2.9.4", + "@formatjs/intl-displaynames": "6.8.5", + "@formatjs/intl-listformat": "7.7.5", + "intl-messageformat": "10.7.7", + "tslib": "2" + }, + "peerDependencies": { + "typescript": "^4.7 || 5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@formatjs/intl-displaynames": { + "version": "6.8.5", + "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-6.8.5.tgz", + "integrity": "sha512-85b+GdAKCsleS6cqVxf/Aw/uBd+20EM0wDpgaxzHo3RIR3bxF4xCJqH/Grbzx8CXurTgDDZHPdPdwJC+May41w==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "@formatjs/intl-localematcher": "0.5.8", + "tslib": "2" + } + }, + "node_modules/@formatjs/intl-listformat": { + "version": "7.7.5", + "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-7.7.5.tgz", + "integrity": "sha512-Wzes10SMNeYgnxYiKsda4rnHP3Q3II4XT2tZyOgnH5fWuHDtIkceuWlRQNsvrI3uiwP4hLqp2XdQTCsfkhXulg==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "@formatjs/intl-localematcher": "0.5.8", + "tslib": "2" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz", + "integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==", + "dependencies": { + "tslib": "2" + } + }, "node_modules/@graphql-typed-document-node/core": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", @@ -2287,7 +2375,6 @@ "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", - "dev": true, "license": "MIT", "dependencies": { "@types/react": "*", @@ -3548,10 +3635,9 @@ "license": "MIT" }, "node_modules/bigbluebutton-html-plugin-sdk": { - "version": "0.0.66", - "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.66.tgz", - "integrity": "sha512-cjwq4N5EKB177bB910Jw6+QP/NN5bdZXxV16rNfpz3B28/NTMQ5of2x62TA7LEdJcM3KZNxFE5Caxhx9cpibmQ==", - "license": "LGPL-3.0", + "version": "0.0.73", + "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.73.tgz", + "integrity": "sha512-upf5Np45+F26qlad1ZUDJFFyAFyPHF3cU81jskEUq2VoEzUCX8zjBVvIsksut79Slz/B622UbX8blGTm+s7/lw==", "dependencies": { "@apollo/client": "^3.8.7", "@browser-bunyan/console-formatted-stream": "^1.8.0", @@ -6563,6 +6649,17 @@ "node": ">=10.13.0" } }, + "node_modules/intl-messageformat": { + "version": "10.7.7", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.7.tgz", + "integrity": "sha512-F134jIoeYMro/3I0h08D0Yt4N9o9pjddU/4IIxMMURqbAtI2wu70X8hvG1V48W49zXHXv3RKSF/po+0fDfsGjA==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "@formatjs/fast-memoize": "2.2.3", + "@formatjs/icu-messageformat-parser": "2.9.4", + "tslib": "2" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -8739,6 +8836,32 @@ "react": "*" } }, + "node_modules/react-intl": { + "version": "6.8.9", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.8.9.tgz", + "integrity": "sha512-TUfj5E7lyUDvz/GtovC9OMh441kBr08rtIbgh3p0R8iF3hVY+V2W9Am7rb8BpJ/29BH1utJOqOOhmvEVh3GfZg==", + "dependencies": { + "@formatjs/ecma402-abstract": "2.2.4", + "@formatjs/icu-messageformat-parser": "2.9.4", + "@formatjs/intl": "2.10.15", + "@formatjs/intl-displaynames": "6.8.5", + "@formatjs/intl-listformat": "7.7.5", + "@types/hoist-non-react-statics": "3", + "@types/react": "16 || 17 || 18", + "hoist-non-react-statics": "3", + "intl-messageformat": "10.7.7", + "tslib": "2" + }, + "peerDependencies": { + "react": "^16.6.0 || 17 || 18", + "typescript": "^4.7 || 5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -10377,7 +10500,7 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 2616e63..fa82e26 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.66", + "bigbluebutton-html-plugin-sdk": "0.0.73", + "react-intl": "^6.6.8", "path": "^0.12.7", "react": "^18.2.0", "react-chat-elements": "^12.0.14", diff --git a/public/index.html b/public/index.html deleted file mode 100644 index bdf3545..0000000 --- a/public/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - Pick random user - - -
    - - diff --git a/public/locales/en.json b/public/locales/en.json new file mode 100644 index 0000000..d253f5c --- /dev/null +++ b/public/locales/en.json @@ -0,0 +1,21 @@ +{ + "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "You have been randomly picked", + "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Randomly picked user", + "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "back", + "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", + "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Skip moderators", + "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Skip presenter", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Include already picked user", + "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Available for selection", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "user", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "users", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "viewer", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "viewers", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Previously picked", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Clear All", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "No {0} available to randomly pick from", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Pick {0}", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Pick again", + "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Pick random user", + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Display last randomly picked user" +} diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json new file mode 100644 index 0000000..19a08a0 --- /dev/null +++ b/public/locales/pt-BR.json @@ -0,0 +1,21 @@ +{ + "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Você foi selecionado aleatoriamente", + "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Usuário selecionado aleatoriamente", + "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "voltar", + "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opções", + "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Ignorar moderadores", + "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Ignorar apresentador", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Incluir usuário já selecionado", + "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponíveis para seleção", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "usuário", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "usuários", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "espectador", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "espectadores", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Selecionados anteriormente", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Limpar tudo", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "Não há disponibilidade de {0} para seleção", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Selecionar {0}", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Selecionar novamente", + "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Selecionar usuário aleatório", + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Exibir último usuário selecionado aleatoriamente" +} diff --git a/src/components/extensible-areas/action-button-dropdown/component.tsx b/src/components/extensible-areas/action-button-dropdown/component.tsx index 36f8488..35dd4bb 100644 --- a/src/components/extensible-areas/action-button-dropdown/component.tsx +++ b/src/components/extensible-areas/action-button-dropdown/component.tsx @@ -1,11 +1,24 @@ import * as React from 'react'; import { useEffect } from 'react'; +import { defineMessages } from 'react-intl'; import { ActionButtonDropdownOption, ActionButtonDropdownSeparator } from 'bigbluebutton-html-plugin-sdk'; import { ActionButtonDropdownManagerProps } from './types'; +const intlMessages = defineMessages({ + pickUserLabel: { + id: 'pickRandomUserPlugin.actionsButtonDropdown.label.pickUser', + description: 'Title to show that current user has been picked', + }, + viewLastPickedUserLabel: { + id: 'pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser', + description: 'Label of the actions button dropdown option to display the last picked user', + }, +}); + function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): React.ReactNode { const { + intl, pickedUserWithEntryId, currentUser, pluginApi, @@ -18,7 +31,7 @@ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): R pluginApi.setActionButtonDropdownItems([ new ActionButtonDropdownSeparator(), new ActionButtonDropdownOption({ - label: 'Pick random user', + label: intl.formatMessage(intlMessages.pickUserLabel), icon: 'user', tooltip: '', allowed: true, @@ -31,7 +44,7 @@ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): R pluginApi.setActionButtonDropdownItems([ new ActionButtonDropdownSeparator(), new ActionButtonDropdownOption({ - label: 'Display last randomly picked user', + label: intl.formatMessage(intlMessages.viewLastPickedUserLabel), icon: 'user', tooltip: '', allowed: true, @@ -43,7 +56,7 @@ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): R } else { pluginApi.setActionButtonDropdownItems([]); } - }, [currentUserInfo, pickedUserWithEntryId]); + }, [currentUserInfo, pickedUserWithEntryId, intl]); return null; } diff --git a/src/components/extensible-areas/action-button-dropdown/types.ts b/src/components/extensible-areas/action-button-dropdown/types.ts index d7f9554..b390e3a 100644 --- a/src/components/extensible-areas/action-button-dropdown/types.ts +++ b/src/components/extensible-areas/action-button-dropdown/types.ts @@ -1,7 +1,9 @@ import { CurrentUserData, GraphqlResponseWrapper, PluginApi } from 'bigbluebutton-html-plugin-sdk'; +import { IntlShape } from 'react-intl'; import { PickedUserWithEntryId } from '../../pick-random-user/types'; export interface ActionButtonDropdownManagerProps { + intl: IntlShape pickedUserWithEntryId: PickedUserWithEntryId; currentUser: CurrentUserData; pluginApi: PluginApi; diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 4aece1a..d23c45d 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -8,6 +8,7 @@ import { PresenterViewComponent } from './presenter-view/component'; export function PickUserModal(props: PickUserModalProps) { const { + intl, showModal, handleCloseModal, users, @@ -26,16 +27,6 @@ export function PickUserModal(props: PickUserModalProps) { dispatcherPickedUser, } = props; - let userRole: string; - if (userFilterViewer) { - userRole = (users?.length !== 1) ? 'viewers' : 'viewer'; - } else { - userRole = (users?.length !== 1) ? 'users' : 'user'; - } - const title = (pickedUserWithEntryId?.pickedUser?.userId === currentUser?.userId) - ? 'You have been randomly picked' - : 'Randomly picked user'; - const [showPresenterView, setShowPresenterView] = useState( currentUser?.presenter && !pickedUserWithEntryId, ); @@ -72,6 +63,7 @@ export function PickUserModal(props: PickUserModalProps) { ? ( @@ -91,7 +82,7 @@ export function PickUserModal(props: PickUserModalProps) { back + ) : null }
    diff --git a/src/components/modal/picked-user-view/types.ts b/src/components/modal/picked-user-view/types.ts index 8827da1..4956c3d 100644 --- a/src/components/modal/picked-user-view/types.ts +++ b/src/components/modal/picked-user-view/types.ts @@ -1,9 +1,10 @@ import { CurrentUserData } from 'bigbluebutton-html-plugin-sdk'; +import { IntlShape } from 'react-intl'; import { PushEntryFunction, ReplaceEntryFunction } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; import { PickedUser, PickedUserWithEntryId } from '../../pick-random-user/types'; export interface PickedUserViewComponentProps { - title: string; + intl: IntlShape; updatePickedRandomUser: ReplaceEntryFunction; pickedUserWithEntryId: PickedUserWithEntryId; currentUser: CurrentUserData; diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index d2e5d0b..1706eab 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -1,10 +1,70 @@ import * as React from 'react'; import { RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; import { DataChannelEntryResponseType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; +import { defineMessages } from 'react-intl'; import { PickedUser } from '../../pick-random-user/types'; import { PresenterViewComponentProps } from './types'; +const intlMessages = defineMessages({ + optionsTitle: { + id: 'pickRandomUserPlugin.modal.presenterView.optionSection.title', + description: 'Title of the options section on modal`s presenter view', + }, + skipModeratorsLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel', + description: 'Label of skip moderator`s option on modal`s presenter view', + }, + skipPresenterLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel', + description: 'Label of skip presenter`s option on modal`s presenter view', + }, + includePickedUsersLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel', + description: 'Label of include picked users option on modal`s presenter view', + }, + availableTitle: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.title', + description: 'Title of the "available users" section on modal`s presenter view', + }, + userLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.userLabel', + description: 'Label to count user in "available users" section on presenter view', + }, + userLabelPlural: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural', + description: 'Label to count users in "available users" section on presenter view', + }, + viewerLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel', + description: 'Label to count viewer in "available users" section on presenter view', + }, + viewerLabelPlural: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural', + description: 'Label to count viewers in "available users" section on presenter view', + }, + previouslyPickedTitle: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title', + description: 'Title of the "previously picked" section on presenter view', + }, + clearButtonLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel', + description: 'Label of button to clear list of already picked users', + }, + noUsersWarning: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning', + description: 'Warning that there is no user to be picked', + }, + pickButtonLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser', + description: 'Label of the button to pick another user', + }, + pickAgainButtonLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain', + description: 'Label of the button to pick another user', + }, +}); + const MAX_NAMES_TO_SHOW = 3; const makeHorizontalListOfNames = (list?: PickedUser[]) => { @@ -36,6 +96,7 @@ const makeVerticalListOfNames = ( export function PresenterViewComponent(props: PresenterViewComponentProps) { const { + intl, filterOutPresenter, setFilterOutPresenter, userFilterViewer, @@ -47,9 +108,19 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { dataChannelPickedUsers, pickedUserWithEntryId, users, - userRole, } = props; + let userRoleLabel: string; + const usersCountVariable = { 0: users?.length }; + if (userFilterViewer) { + userRoleLabel = (users?.length !== 1) + ? intl.formatMessage(intlMessages.viewerLabelPlural, usersCountVariable) + : intl.formatMessage(intlMessages.viewerLabel, usersCountVariable); + } else { + userRoleLabel = (users?.length !== 1) + ? intl.formatMessage(intlMessages.userLabelPlural, usersCountVariable) + : intl.formatMessage(intlMessages.userLabel, usersCountVariable); + } return (
    -

    Options

    +

    {intl.formatMessage(intlMessages.optionsTitle)}

    -

    Available for selection

    +

    {intl.formatMessage(intlMessages.availableTitle)}

    - {users?.length} - {' '} - {userRole} - : - {' '} + {`${users?.length} ${userRoleLabel}: `} {makeHorizontalListOfNames(users)}

    -

    Previously picked

    +

    {intl.formatMessage(intlMessages.previouslyPickedTitle)}

      @@ -140,16 +207,14 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { }} > { - (pickedUserWithEntryId) ? 'Pick again' : `Pick ${userRole}` + (pickedUserWithEntryId) + ? intl.formatMessage(intlMessages.pickAgainButtonLabel) + : intl.formatMessage(intlMessages.pickButtonLabel, { 0: userRoleLabel }) } ) : (

      - No - {' '} - {userRole} - {' '} - available to randomly pick from + {intl.formatMessage(intlMessages.noUsersWarning, { 0: userRoleLabel })}

      ) } diff --git a/src/components/modal/presenter-view/types.ts b/src/components/modal/presenter-view/types.ts index f1f11eb..d00e750 100644 --- a/src/components/modal/presenter-view/types.ts +++ b/src/components/modal/presenter-view/types.ts @@ -1,8 +1,10 @@ import { DataChannelEntryResponseType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; import { DeleteEntryFunction } from 'bigbluebutton-html-plugin-sdk'; +import { IntlShape } from 'react-intl'; import { PickedUser, PickedUserWithEntryId } from '../../pick-random-user/types'; export interface PresenterViewComponentProps { + intl: IntlShape; filterOutPresenter: boolean; setFilterOutPresenter: (filter: boolean) => void; userFilterViewer: boolean; @@ -14,5 +16,4 @@ export interface PresenterViewComponentProps { dataChannelPickedUsers?: DataChannelEntryResponseType[]; pickedUserWithEntryId: PickedUserWithEntryId; users?: PickedUser[]; - userRole: string; } diff --git a/src/components/modal/types.ts b/src/components/modal/types.ts index 0f4748c..ff5e39e 100644 --- a/src/components/modal/types.ts +++ b/src/components/modal/types.ts @@ -1,8 +1,10 @@ import { CurrentUserData, DeleteEntryFunction } from 'bigbluebutton-html-plugin-sdk'; +import { IntlShape } from 'react-intl'; import { DataChannelEntryResponseType, PushEntryFunction, ReplaceEntryFunction } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; import { PickedUser, PickedUserWithEntryId } from '../pick-random-user/types'; export interface PickUserModalProps { + intl: IntlShape showModal: boolean; updatePickedRandomUser: ReplaceEntryFunction; handleCloseModal: () => void; diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index 66401d6..0751d0e 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { useState, useEffect } from 'react'; +import { createIntl, createIntlCache } from 'react-intl'; import { BbbPluginSdk, PluginApi } from 'bigbluebutton-html-plugin-sdk'; import { @@ -14,6 +15,13 @@ import { PickUserModal } from '../modal/component'; import { Role } from './enums'; import ActionButtonDropdownManager from '../extensible-areas/action-button-dropdown/component'; +const LOCALE_REQUEST_OBJECT = (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') + ? { + headers: { + 'ngrok-skip-browser-warning': 'any', + }, + } : null; + function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { BbbPluginSdk.initialize(uuid); const [showModal, setShowModal] = useState(false); @@ -30,6 +38,19 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { .useCustomSubscription(USERS_MORE_INFORMATION); const { data: allUsers } = allUsersInfo; + const { + messages: localeMessages, + currentLocale, + loading: localeMessagesLoading, + } = pluginApi.useLocaleMessages(LOCALE_REQUEST_OBJECT); + + const cache = createIntlCache(); + const intl = (!localeMessagesLoading && localeMessages) ? createIntl({ + locale: currentLocale, + messages: localeMessages, + fallbackOnEmptyString: true, + }, cache) : null; + const { data: pickedUserFromDataChannelResponse, pushEntry: pushPickedUser, @@ -124,10 +145,12 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { useEffect(() => { if (!currentUser?.presenter && dispatchModalInformationFromPresenter) handleCloseModal(); }, [currentUser]); + if (!intl || localeMessagesLoading) return null; return ( <> Date: Tue, 15 Apr 2025 10:47:52 -0300 Subject: [PATCH 013/117] [add-localization] resolve tsc issues --- src/components/modal/component.tsx | 10 +++++++++- src/components/modal/picked-user-view/component.tsx | 3 ++- tsconfig.json | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 7ccb637..0a04c87 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -1,12 +1,20 @@ import { useEffect, useState } from 'react'; import * as React from 'react'; import * as ReactModal from 'react-modal'; +import { defineMessages } from 'react-intl'; import { pluginLogger } from 'bigbluebutton-html-plugin-sdk'; import { PickUserModalProps, WindowClientSettings } from './types'; import './style.css'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; +const intlMessages = defineMessages({ + currentUserPicked: { + id: 'pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked', + description: 'Title to show that current user has been picked', + }, +}); + const TIMEOUT_CLOSE_NOTIFICATION = 5000; declare const window: WindowClientSettings; @@ -63,7 +71,7 @@ export function PickUserModal(props: PickUserModalProps) { : `${host}/resources/sounds/doorbell.mp3`; const audio = new Audio(pingSoundUrl); audio.play(); - notifyRandomlyPickedUser(title); + notifyRandomlyPickedUser(intl.formatMessage(intlMessages.currentUserPicked)); } }, [currentUser, pickedUserWithEntryId]); return ( diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index c198003..2119882 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { useEffect } from 'react'; import { defineMessages } from 'react-intl'; import { PickedUserViewComponentProps } from './types'; @@ -27,7 +28,7 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { dispatcherPickedUser, } = props; - React.useEffect(() => { + useEffect(() => { if (currentUser?.presenter) { updatePickedRandomUser(pickedUserWithEntryId.entryId, { ...pickedUserWithEntryId.pickedUser, diff --git a/tsconfig.json b/tsconfig.json index 7dde00c..4d5cb5d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "outDir": "./dist-tsc/", "noImplicitAny": true, + "skipLibCheck": true, "module": "es6", "target": "es5", "jsx": "react", From 69fada882728866653ec94dbf10bdb31d1fdf79a Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Thu, 17 Apr 2025 13:09:33 -0400 Subject: [PATCH 014/117] chore: set version to 0.0.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3adc2f4..f35258f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pick-random-user-plugin", - "version": "0.2.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pick-random-user-plugin", - "version": "0.2.0", + "version": "0.0.1", "dependencies": { "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/package.json b/package.json index e31b069..9c50497 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pick-random-user-plugin", - "version": "0.2.0", + "version": "0.0.1", "private": true, "main": "./src/index.tsx", "dependencies": { From 1605398b51160036129377aa6ebc05839de7552b Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 23 Apr 2025 09:04:42 -0300 Subject: [PATCH 015/117] [migrate-to-styled-components] fix lint errors --- .../modal/presenter-view/component.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index 5ede3be..08c2ba3 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -126,7 +126,9 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { return ( - {intl.formatMessage(intlMessages.optionsTitle)} + + {intl.formatMessage(intlMessages.optionsTitle)} + - {intl.formatMessage(intlMessages.skipModeratorsLabel)} + + {intl.formatMessage(intlMessages.skipModeratorsLabel)} + - {intl.formatMessage(intlMessages.skipPresenterLabel)} + + {intl.formatMessage(intlMessages.skipPresenterLabel)} + - {intl.formatMessage(intlMessages.includePickedUsersLabel)} + + {intl.formatMessage(intlMessages.includePickedUsersLabel)} + - {intl.formatMessage(intlMessages.availableTitle)} + + {intl.formatMessage(intlMessages.availableTitle)} + {`${users?.length} ${userRoleLabel}: `} {makeHorizontalListOfNames(users)} @@ -178,7 +188,9 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { - {intl.formatMessage(intlMessages.previouslyPickedTitle)} + + {intl.formatMessage(intlMessages.previouslyPickedTitle)} + { From 0a5e054c3192e4fb070afacdef2fd59bd7a17292 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 24 Apr 2025 16:19:59 -0300 Subject: [PATCH 016/117] [issue-34] localize alternative text descriptor for avatar image --- public/locales/en.json | 1 + public/locales/pt-BR.json | 1 + src/components/modal/picked-user-view/component.tsx | 10 +++++++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/public/locales/en.json b/public/locales/en.json index d253f5c..5772509 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -2,6 +2,7 @@ "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "You have been randomly picked", "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Randomly picked user", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "back", + "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Avatar image of user {0}", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Skip moderators", "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Skip presenter", diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json index 19a08a0..9915303 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -2,6 +2,7 @@ "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Você foi selecionado aleatoriamente", "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Usuário selecionado aleatoriamente", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "voltar", + "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Iamgem avatar do usuário {0}", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opções", "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Ignorar moderadores", "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Ignorar apresentador", diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index eef1315..d495a21 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -17,6 +17,10 @@ const intlMessages = defineMessages({ id: 'pickRandomUserPlugin.modal.pickedUserView.backButton.label', description: 'Label of back button in picked-user view on the modal', }, + avatarImageAlternativeText: { + id: 'pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText', + description: 'Alternative text for avatar image', + }, }); export function PickedUserViewComponent(props: PickedUserViewComponentProps) { @@ -57,6 +61,10 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { const title = (pickedUserWithEntryId?.pickedUser?.userId === currentUser?.userId) ? intl.formatMessage(intlMessages.currentUserPicked) : intl.formatMessage(intlMessages.randomUserPicked); + + const avatarAltDescriptor = intl.formatMessage(intlMessages.currentUserPicked, { + 0: pickedUserWithEntryId.pickedUser.name, + }); return ( {title} @@ -65,7 +73,7 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { <> {avatarUrl ? ( ) : ( From 9fc829c082f4a56d2cd5942304c99b7a675439d9 Mon Sep 17 00:00:00 2001 From: antobinary <6312397+antobinary@users.noreply.github.com> Date: Fri, 25 Apr 2025 13:35:35 +0000 Subject: [PATCH 017/117] chore: bump version to 0.0.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f35258f..c63b604 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pick-random-user-plugin", - "version": "0.0.1", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pick-random-user-plugin", - "version": "0.0.1", + "version": "0.0.2", "dependencies": { "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/package.json b/package.json index 9c50497..70f3c13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pick-random-user-plugin", - "version": "0.0.1", + "version": "0.0.2", "private": true, "main": "./src/index.tsx", "dependencies": { From b6de68d8b2f23dd85dd64cbc18b0f367b9b4a195 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 30 Apr 2025 14:55:31 -0300 Subject: [PATCH 018/117] [update-v0.0.79] update plugin-sdk to version 0.0.79 --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index c63b604..87f20f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.73", + "bigbluebutton-html-plugin-sdk": "0.0.79", "path": "^0.12.7", "react": "^18.2.0", "react-chat-elements": "^12.0.14", @@ -3635,9 +3635,9 @@ "license": "MIT" }, "node_modules/bigbluebutton-html-plugin-sdk": { - "version": "0.0.73", - "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.73.tgz", - "integrity": "sha512-upf5Np45+F26qlad1ZUDJFFyAFyPHF3cU81jskEUq2VoEzUCX8zjBVvIsksut79Slz/B622UbX8blGTm+s7/lw==", + "version": "0.0.79", + "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.79.tgz", + "integrity": "sha512-D/YLelzhjWDs8FmUa24TbpZfN+iHuU6T29GYNPFMQ0IGpg0XaRcfQTr9jorWEBwAx1LLwx1SvvF1uXUD+Q3QKQ==", "dependencies": { "@apollo/client": "^3.8.7", "@browser-bunyan/console-formatted-stream": "^1.8.0", diff --git a/package.json b/package.json index 70f3c13..a6afcdd 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.73", + "bigbluebutton-html-plugin-sdk": "0.0.79", "react-intl": "^6.6.8", "path": "^0.12.7", "react": "^18.2.0", From 034acacb47b245502be8a6a0ba0f84f7261f7b10 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 9 May 2025 09:40:35 -0300 Subject: [PATCH 019/117] [fix-modal-behaviour] - Fix modal appearing to viewer after they closed because the presenter re-opened theirs. --- manifest.json | 25 ++++++---- src/components/modal/component.tsx | 8 +++- .../modal/picked-user-view/component.tsx | 46 +++++++++---------- .../modal/picked-user-view/types.ts | 15 ++++-- src/components/modal/types.ts | 10 ++-- src/components/pick-random-user/component.tsx | 24 ++++++++-- src/components/pick-random-user/types.ts | 20 ++------ src/utils/utils.ts | 23 ++++++++++ 8 files changed, 103 insertions(+), 68 deletions(-) create mode 100644 src/utils/utils.ts diff --git a/manifest.json b/manifest.json index 0d10b66..98e4d12 100644 --- a/manifest.json +++ b/manifest.json @@ -4,15 +4,20 @@ "javascriptEntrypointUrl": "PickRandomUserPlugin.js", "localesBaseUrl": "locales", "dataChannels": [ - { - "name": "modalInformationFromPresenter", - "pushPermission": ["presenter"], - "replaceOrDeletePermission": ["creator", "moderator"] - }, - { - "name": "pickRandomUser", - "pushPermission": ["presenter"], - "replaceOrDeletePermission": ["creator", "moderator"] - } + { + "name": "modalInformationFromPresenter", + "pushPermission": ["presenter"], + "replaceOrDeletePermission": ["creator", "moderator"] + }, + { + "name": "pickRandomUser", + "pushPermission": ["presenter"], + "replaceOrDeletePermission": ["creator", "moderator"] + }, + { + "name": "pickedUserSeenEntry", + "pushPermission": ["all"], + "replaceOrDeletePermission": ["moderator"] + } ] } diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index f48fb5b..181c2f7 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -39,7 +39,6 @@ export function PickUserModal(props: PickUserModalProps) { showModal, handleCloseModal, users, - updatePickedRandomUser, pickedUserWithEntryId, handlePickRandomUser, currentUser, @@ -52,11 +51,14 @@ export function PickUserModal(props: PickUserModalProps) { dataChannelPickedUsers, deletionFunction, dispatcherPickedUser, + pickedUserSeenEntries, + pushPickedUserSeen, } = props; const [showPresenterView, setShowPresenterView] = useState( currentUser?.presenter && !pickedUserWithEntryId, ); + useEffect(() => { setShowPresenterView(currentUser?.presenter && !pickedUserWithEntryId); // Play audio when user is selected @@ -115,10 +117,12 @@ export function PickUserModal(props: PickUserModalProps) { ) : ( { - if (currentUser?.presenter) { - updatePickedRandomUser(pickedUserWithEntryId.entryId, { - ...pickedUserWithEntryId.pickedUser, - isPresenterViewing: true, - }); - } - return () => { - if (currentUser?.presenter) { - updatePickedRandomUser(pickedUserWithEntryId.entryId, { - ...pickedUserWithEntryId.pickedUser, - isPresenterViewing: false, - }); - } - }; - }, []); - const handleBackToPresenterView = () => { if (currentUser?.presenter) { setShowPresenterView(true); - dispatcherPickedUser(null); } }; - const avatarUrl = pickedUserWithEntryId.pickedUser.avatar; + const avatarUrl = pickedUserWithEntryId?.pickedUser?.avatar; + useEffect(() => { + const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUser?.userId, + pickedUserWithEntryId?.pickedUser?.userId, + ); + if (pickedUserWithEntryId && !hasCurrentUserSeen) { + pushPickedUserSeen({ + pickedUserId: pickedUserWithEntryId?.pickedUser.userId, + seenByUserId: currentUser.userId, + }); + } + }, []); const title = (pickedUserWithEntryId?.pickedUser?.userId === currentUser?.userId) ? intl.formatMessage(intlMessages.currentUserPicked) : intl.formatMessage(intlMessages.randomUserPicked); const avatarAltDescriptor = intl.formatMessage(intlMessages.currentUserPicked, { - 0: pickedUserWithEntryId.pickedUser.name, + 0: pickedUserWithEntryId?.pickedUser?.name, }); return ( @@ -78,12 +74,12 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { /> ) : ( - {pickedUserWithEntryId.pickedUser?.name.slice(0, 2)} + {pickedUserWithEntryId?.pickedUser?.name.slice(0, 2)} )} - {pickedUserWithEntryId.pickedUser?.name} + {pickedUserWithEntryId?.pickedUser?.name} ) : null } diff --git a/src/components/modal/picked-user-view/types.ts b/src/components/modal/picked-user-view/types.ts index bf6c83e..3252535 100644 --- a/src/components/modal/picked-user-view/types.ts +++ b/src/components/modal/picked-user-view/types.ts @@ -1,15 +1,20 @@ -import { CurrentUserData } from 'bigbluebutton-html-plugin-sdk'; +import { + CurrentUserData, + DataChannelEntryResponseType, + GraphqlResponseWrapper, + PushEntryFunction, +} from 'bigbluebutton-html-plugin-sdk'; import { IntlShape } from 'react-intl'; -import { PushEntryFunction, ReplaceEntryFunction } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; -import { PickedUser, PickedUserWithEntryId } from '../../pick-random-user/types'; +import { PickedUserWithEntryId, PickedUserSeenEntryDataChannel } from '../../pick-random-user/types'; export interface PickedUserViewComponentProps { intl: IntlShape; - updatePickedRandomUser: ReplaceEntryFunction; pickedUserWithEntryId: PickedUserWithEntryId; currentUser: CurrentUserData; + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>; + pushPickedUserSeen: PushEntryFunction; setShowPresenterView: React.Dispatch>; - dispatcherPickedUser: PushEntryFunction } export interface ModalAvatarProps { diff --git a/src/components/modal/types.ts b/src/components/modal/types.ts index 1c3e603..a76b49d 100644 --- a/src/components/modal/types.ts +++ b/src/components/modal/types.ts @@ -1,15 +1,14 @@ import { PluginSettingsData } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-consumption/domain/settings/plugin-settings/types'; -import { CurrentUserData, DeleteEntryFunction } from 'bigbluebutton-html-plugin-sdk'; +import { CurrentUserData, DeleteEntryFunction, GraphqlResponseWrapper } from 'bigbluebutton-html-plugin-sdk'; import { IntlShape } from 'react-intl'; -import { DataChannelEntryResponseType, PushEntryFunction, ReplaceEntryFunction } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; -import { PickedUser, PickedUserWithEntryId } from '../pick-random-user/types'; +import { DataChannelEntryResponseType, PushEntryFunction } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; +import { PickedUser, PickedUserWithEntryId, PickedUserSeenEntryDataChannel } from '../pick-random-user/types'; export interface PickUserModalProps { pluginSettings: PluginSettingsData; isPluginSettingsLoading: boolean; intl: IntlShape showModal: boolean; - updatePickedRandomUser: ReplaceEntryFunction; handleCloseModal: () => void; users?: PickedUser[]; pickedUserWithEntryId: PickedUserWithEntryId; @@ -24,6 +23,9 @@ export interface PickUserModalProps { dataChannelPickedUsers?: DataChannelEntryResponseType[]; deletionFunction: DeleteEntryFunction; dispatcherPickedUser: PushEntryFunction; + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>; + pushPickedUserSeen: PushEntryFunction; } export interface WindowClientSettings extends Window { diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index edae94a..2a0114e 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -2,10 +2,12 @@ import * as React from 'react'; import { useState, useEffect } from 'react'; import { createIntl, createIntlCache } from 'react-intl'; -import { BbbPluginSdk, PluginApi } from 'bigbluebutton-html-plugin-sdk'; +import { BbbPluginSdk, PluginApi, RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; +import hasCurrentUserSeenPickedUser from '../../utils/utils'; import { ModalInformationFromPresenter, PickRandomUserPluginProps, + PickedUserSeenEntryDataChannel, PickedUser, PickedUserWithEntryId, UsersMoreInformationGraphqlResponse, @@ -66,7 +68,6 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const { data: pickedUserFromDataChannelResponse, pushEntry: pushPickedUser, - replaceEntry: updatePickedRandomUser, deleteEntry: deletePickedUser, } = pluginApi.useDataChannel('pickRandomUser'); const { @@ -74,6 +75,12 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { pushEntry: dispatchModalInformationFromPresenter, } = pluginApi.useDataChannel('modalInformationFromPresenter'); + const { + data: pickedUserSeenEntries, + pushEntry: pushPickedUserSeen, + deleteEntry: deletePickedUserSeenEntries, + } = pluginApi.useDataChannel('pickedUserSeenEntry'); + const pickedUserFromDataChannel = { data: pickedUserFromDataChannelResponse?.data, loading: false, @@ -110,6 +117,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const handlePickRandomUser = () => { if (usersToBePicked && usersToBePicked.user.length > 0 && currentUser?.presenter) { + deletePickedUserSeenEntries([RESET_DATA_CHANNEL]); const randomIndex = Math.floor(Math.random() * usersToBePicked.user.length); const randomlyPickedUser = usersToBePicked.user[randomIndex]; pushPickedUser(randomlyPickedUser); @@ -140,7 +148,12 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { entryId: pickedUserToUpdate.entryId, }); } - if (pickedUserToUpdate?.payloadJson && pickedUserToUpdate?.payloadJson.isPresenterViewing) { + const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUser?.userId, + pickedUserToUpdate?.payloadJson.userId, + ); + if (!hasCurrentUserSeen && !pickedUserSeenEntries?.loading) { setShowModal(true); } } else if (pickedUserFromDataChannel.data @@ -148,7 +161,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { setPickedUserWithEntryId(null); if (currentUser && !currentUser.presenter) setShowModal(false); } - }, [pickedUserFromDataChannelResponse]); + }, [pickedUserFromDataChannelResponse, pickedUserSeenEntries]); useEffect(() => { if (!pickedUserWithEntryId && !currentUser?.presenter) setShowModal(false); @@ -178,9 +191,10 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { filterOutPickedUsers, setFilterOutPickedUsers, dataChannelPickedUsers: pickedUserFromDataChannel.data, - updatePickedRandomUser, dispatcherPickedUser: pushPickedUser, deletionFunction: deletePickedUser, + pickedUserSeenEntries, + pushPickedUserSeen, }} /> { - createdAt: string; - dataChannel: string; - fromUserId: string; - messageId: string; - payloadJson: T; - pluginName: string; - toRoles: string[]; -} - -export interface DataChannelPickedUserResponse { - pluginDataChannelMessage: DataChannelArrayMessages[]; -} - -export interface DataChannelLastResetTimeResponse { - pluginDataChannelMessage: DataChannelArrayMessages[]; +export interface PickedUserSeenEntryDataChannel { + pickedUserId: string; + seenByUserId: string; } diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..04bbf7e --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,23 @@ +import { DataChannelEntryResponseType, GraphqlResponseWrapper } from 'bigbluebutton-html-plugin-sdk'; +import { PickedUserSeenEntryDataChannel } from '../components/pick-random-user/types'; + +/** + * It process data-channel information on the picked-users already seen + * and returns a boolean indicating if the current user has seen the picked-user + * @param pickedUserSeenEntries DataChannelEntry result + * @param currentUserId userId from current user on which the plugin is running + * @param pickedUserId userId from the picked user + * @returns boolean indicating if the current user has seen the picked-user + */ +const hasCurrentUserSeenPickedUser = ( + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>, + currentUserId: string, + pickedUserId: string, +) => pickedUserSeenEntries?.data + && pickedUserSeenEntries?.data.length > 0 + && pickedUserSeenEntries.data.some((view) => view.payloadJson + && view.payloadJson.seenByUserId === currentUserId + && view.payloadJson.pickedUserId === pickedUserId); + +export default hasCurrentUserSeenPickedUser; From 2f56ca370783da5e4a6f90203a09b824049da44d Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Mon, 12 May 2025 11:12:56 -0300 Subject: [PATCH 020/117] [add-locales-to-build] --- webpack.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/webpack.config.js b/webpack.config.js index 6e74155..9b64635 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -63,6 +63,7 @@ module.exports = { new CopyWebpackPlugin({ patterns: [ { from: 'manifest.json', to: './' }, // Copy manifest.json to static/ in the output folder + { from: 'public/locales/', to: './locales/' }, // Copy manifest.json to static/ in the output folder ], }), ], From 556529382d08f6eb106efebbe0359539138d8ffc Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Mon, 12 May 2025 11:11:36 -0400 Subject: [PATCH 021/117] Update webpack.config.js --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 9b64635..85d0b95 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -63,7 +63,7 @@ module.exports = { new CopyWebpackPlugin({ patterns: [ { from: 'manifest.json', to: './' }, // Copy manifest.json to static/ in the output folder - { from: 'public/locales/', to: './locales/' }, // Copy manifest.json to static/ in the output folder + { from: 'public/locales/', to: './locales/' }, // Copy all locales to the output folder ], }), ], From ba54720d381b1bee98792b9872a771193e202afc Mon Sep 17 00:00:00 2001 From: antobinary <6312397+antobinary@users.noreply.github.com> Date: Mon, 12 May 2025 15:12:48 +0000 Subject: [PATCH 022/117] chore: bump version to 0.0.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 87f20f2..80bfc16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pick-random-user-plugin", - "version": "0.0.2", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pick-random-user-plugin", - "version": "0.0.2", + "version": "0.0.3", "dependencies": { "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/package.json b/package.json index a6afcdd..db9fc25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pick-random-user-plugin", - "version": "0.0.2", + "version": "0.0.3", "private": true, "main": "./src/index.tsx", "dependencies": { From a2ab3919ffb421d72f381599f7e762eef21ec91c Mon Sep 17 00:00:00 2001 From: antobinary <6312397+antobinary@users.noreply.github.com> Date: Wed, 14 May 2025 14:55:12 +0000 Subject: [PATCH 023/117] chore: bump version to 0.0.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 80bfc16..94f0227 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pick-random-user-plugin", - "version": "0.0.3", + "version": "0.0.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pick-random-user-plugin", - "version": "0.0.3", + "version": "0.0.4", "dependencies": { "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/package.json b/package.json index db9fc25..5a38469 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pick-random-user-plugin", - "version": "0.0.3", + "version": "0.0.4", "private": true, "main": "./src/index.tsx", "dependencies": { From 18ab6e2494cf75de931e41dfd741f0759883f9d9 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 14 May 2025 16:08:20 -0300 Subject: [PATCH 024/117] [add-hardcoded-fallback] - add hardcode fallback instead of the key if the language fails --- .../action-button-dropdown/component.tsx | 2 ++ src/components/modal/component.tsx | 1 + .../modal/picked-user-view/component.tsx | 4 ++++ src/components/modal/presenter-view/component.tsx | 14 ++++++++++++++ 4 files changed, 21 insertions(+) diff --git a/src/components/extensible-areas/action-button-dropdown/component.tsx b/src/components/extensible-areas/action-button-dropdown/component.tsx index 35dd4bb..8e4fb4b 100644 --- a/src/components/extensible-areas/action-button-dropdown/component.tsx +++ b/src/components/extensible-areas/action-button-dropdown/component.tsx @@ -9,10 +9,12 @@ const intlMessages = defineMessages({ pickUserLabel: { id: 'pickRandomUserPlugin.actionsButtonDropdown.label.pickUser', description: 'Title to show that current user has been picked', + defaultMessage: 'Pick random user', }, viewLastPickedUserLabel: { id: 'pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser', description: 'Label of the actions button dropdown option to display the last picked user', + defaultMessage: 'Display last randomly picked user', }, }); diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 181c2f7..069cec6 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -11,6 +11,7 @@ const intlMessages = defineMessages({ currentUserPicked: { id: 'pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked', description: 'Title to show that current user has been picked', + defaultMessage: 'You have been randomly picked', }, }); diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index 1f643ff..7199a99 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -9,18 +9,22 @@ const intlMessages = defineMessages({ currentUserPicked: { id: 'pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked', description: 'Title to show that current user has been picked', + defaultMessage: 'You have been randomly picked', }, randomUserPicked: { id: 'pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked', description: 'Title to show that random user has been picked', + defaultMessage: 'Randomly picked user', }, backButtonLabel: { id: 'pickRandomUserPlugin.modal.pickedUserView.backButton.label', description: 'Label of back button in picked-user view on the modal', + defaultMessage: 'back', }, avatarImageAlternativeText: { id: 'pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText', description: 'Alternative text for avatar image', + defaultMessage: 'Avatar image of user {0}', }, }); diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index 08c2ba3..3744190 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -11,58 +11,72 @@ const intlMessages = defineMessages({ optionsTitle: { id: 'pickRandomUserPlugin.modal.presenterView.optionSection.title', description: 'Title of the options section on modal`s presenter view', + defaultMessage: 'Options', }, skipModeratorsLabel: { id: 'pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel', description: 'Label of skip moderator`s option on modal`s presenter view', + defaultMessage: 'Skip moderators', }, skipPresenterLabel: { id: 'pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel', description: 'Label of skip presenter`s option on modal`s presenter view', + defaultMessage: 'Skip presenter', }, includePickedUsersLabel: { id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel', description: 'Label of include picked users option on modal`s presenter view', + defaultMessage: 'Include already picked user', }, availableTitle: { id: 'pickRandomUserPlugin.modal.presenterView.availableSection.title', description: 'Title of the "available users" section on modal`s presenter view', + defaultMessage: 'Available for selection', }, userLabel: { id: 'pickRandomUserPlugin.modal.presenterView.availableSection.userLabel', description: 'Label to count user in "available users" section on presenter view', + defaultMessage: 'user', }, userLabelPlural: { id: 'pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural', description: 'Label to count users in "available users" section on presenter view', + defaultMessage: 'users', }, viewerLabel: { id: 'pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel', description: 'Label to count viewer in "available users" section on presenter view', + defaultMessage: 'viewer', }, viewerLabelPlural: { id: 'pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural', description: 'Label to count viewers in "available users" section on presenter view', + defaultMessage: 'viewers', }, previouslyPickedTitle: { id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title', description: 'Title of the "previously picked" section on presenter view', + defaultMessage: 'Previously picked', }, clearButtonLabel: { id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel', description: 'Label of button to clear list of already picked users', + defaultMessage: 'Clear All', }, noUsersWarning: { id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning', description: 'Warning that there is no user to be picked', + defaultMessage: 'No {0} available to randomly pick from', }, pickButtonLabel: { id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser', description: 'Label of the button to pick another user', + defaultMessage: 'Pick {0}', }, pickAgainButtonLabel: { id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain', description: 'Label of the button to pick another user', + defaultMessage: 'Pick again', }, }); From c251d0bfe48686559a46469d81f6f0b7a944920d Mon Sep 17 00:00:00 2001 From: Jan Kessler Date: Thu, 15 May 2025 08:03:32 +0200 Subject: [PATCH 025/117] add german translations --- public/locales/de.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 public/locales/de.json diff --git a/public/locales/de.json b/public/locales/de.json new file mode 100644 index 0000000..455f371 --- /dev/null +++ b/public/locales/de.json @@ -0,0 +1,22 @@ +{ + "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Sie wurden zufällig ausgewählt", + "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Zufällig gewählter Teilnehmer", + "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "zurück", + "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Avatar von Teilnehmer {0}", + "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Optionen", + "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Überspringe Moderatoren", + "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Überspringe Präsentator", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Wähle bereits gewählte Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Zur Auswahl stehen", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Zuvor gewählt", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Alle löschen", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "Keine {0} zum Auswählen verfügbar", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Wähle {0}", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Wähle erneut", + "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Wähle zufälligen Teilnehmer", + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Zeige zuletzt gewählten Teilnehmer" +} From 3a9d6cd59f121ef42b9596670880d32eea4e7916 Mon Sep 17 00:00:00 2001 From: albanobattistella <34811668+albanobattistella@users.noreply.github.com> Date: Thu, 22 May 2025 13:18:43 +0200 Subject: [PATCH 026/117] Create it.json --- public/locales/it.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 public/locales/it.json diff --git a/public/locales/it.json b/public/locales/it.json new file mode 100644 index 0000000..0b82bf4 --- /dev/null +++ b/public/locales/it.json @@ -0,0 +1,21 @@ +{ + "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Sei stato scelto a caso", + "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Utente scelto a caso", + "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "indietro", + "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opzioni", + "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Salta i moderatori", + "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Salta il presentatore", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Includi l'utente già selezionato", + "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponibile per la selezione", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "utente", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "utenti", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "partecipante", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "partecipanti", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Precedentemente selezionato", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Cancella tutto", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "Nessun {0} disponibile da cui scegliere casualmente", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Scegli {0}", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Scegli di nuovo", + "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Scegli un utente casuale", + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Visualizza l'ultimo utente scelto casualmente" +} From e331a8b8f3c75512c2b77068b5ac9aa835ccc139 Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Sat, 24 May 2025 09:59:53 +0900 Subject: [PATCH 027/117] Add files via upload --- public/locales/ja.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 public/locales/ja.json diff --git a/public/locales/ja.json b/public/locales/ja.json new file mode 100644 index 0000000..08d132c --- /dev/null +++ b/public/locales/ja.json @@ -0,0 +1,22 @@ +{ + "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "あなたが指名されました", + "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "指名された人", + "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "戻る", + "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "ユーザー {0} のアバター画像", + "pickRandomUserPlugin.modal.presenterView.optionSection.title": "設定", + "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "司会者を含めない", + "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "発表者を含めない", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "既に指名した人を含める", + "pickRandomUserPlugin.modal.presenterView.availableSection.title": "指名可能な人", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "ユーザー", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "ユーザー", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "視聴者", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "視聴者", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "既に指名された人", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "全消去", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "{0}の中で指名できる人はいません", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "{0}から指名する", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "もう一度指名する", + "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "ランダムに指名する", + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "最後に指名されたユーザーを表示" +} From 9c04fe71fc77133a9c69ba72f6d72dc5c7aeabee Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Mon, 26 May 2025 14:53:48 -0400 Subject: [PATCH 028/117] docs: Update list of dist contents --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 10a980e..ef0a32f 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ npm ci npm run build-bundle ``` -The above command will generate the `dist` folder, containing the bundled JavaScript file named `PickRandomUserPlugin.js`. This file can be hosted on any HTTPS server along with its `manifest.json`. +The above command will generate the `dist` folder, containing the bundled JavaScript file named `PickRandomUserPlugin.js`, a directory of locale files and a license files. These files can be hosted on any HTTPS server along with the `manifest.json` which is also part of the `dist/` directory. If you install the Plugin separated to the manifest, remember to change the `javascriptEntrypointUrl` in the `manifest.json` to the correct endpoint. From b374609848c421be3af0a53ee14eee905ce1808d Mon Sep 17 00:00:00 2001 From: Thierry Kauffmann Date: Thu, 15 May 2025 17:40:53 +0200 Subject: [PATCH 029/117] Create fr-FR.json French localization --- public/locales/fr-FR.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 public/locales/fr-FR.json diff --git a/public/locales/fr-FR.json b/public/locales/fr-FR.json new file mode 100644 index 0000000..02b93f1 --- /dev/null +++ b/public/locales/fr-FR.json @@ -0,0 +1,21 @@ +{ + "pickRandomUserPlugin.modal.pickedUserView.title.currentUserPicked": "Vous avez été sélectionné au hasard", + "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Participant sélectionné au hasard", + "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "retour", + "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", + "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Ignorer les modérateurs", + "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Ignorer le présentateur", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Inclure un participant déjà sélectionné", + "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponible pour la sélection", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "participant", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "participants", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "spectateur", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "spectateurs", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "Sélectionné précédemment", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "Vider la sélection", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "Aucun {0} disponible pour être sélectionné au hasard", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Sélectionner {0}", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Sélectionner à nouveau", + "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Sélectionner un participant au hasard", + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Afficher le dernier participant sélectionné au hasard" +} From 27da28b0b3c6b1169089d65bd6f3c5242b2f29a8 Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:43:55 +0200 Subject: [PATCH 030/117] Fix button styles --- .../modal/picked-user-view/component.tsx | 4 ++-- .../modal/picked-user-view/styles.tsx | 22 +++++++++++++++++++ .../modal/presenter-view/styles.tsx | 7 +++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index 7199a99..ce49867 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -89,9 +89,9 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { } { (currentUser?.presenter) ? ( - + ) : null } diff --git a/src/components/modal/picked-user-view/styles.tsx b/src/components/modal/picked-user-view/styles.tsx index ba67f15..e50c940 100644 --- a/src/components/modal/picked-user-view/styles.tsx +++ b/src/components/modal/picked-user-view/styles.tsx @@ -43,10 +43,32 @@ const PickedUserName = styled.p` font-weight: 500; `; +const BackButton = styled.button` + border: 3px solid transparent; + overflow: visible; + display: inline-block; + background-color: var(--btn-primary-bg, var(--color-primary, #0F70D7)); + color: var(--btn-primary-color, var(--color-white, #FFF)); + border-radius: 2px; + font-weight: 600; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + user-select: none; + padding: 8px 15px; + &:hover { + color: var(--btn-primary-color, var(--color-white, #FFF)); + background-color: var(--btn-primary-hover-bg, #0C57A7) !important; + } +`; + export { PickedUserViewWrapper, PickedUserViewTitle, PickedUserAvatarInitials, PickedUserAvatarImage, PickedUserName, + BackButton }; diff --git a/src/components/modal/presenter-view/styles.tsx b/src/components/modal/presenter-view/styles.tsx index d6d7ab8..fb2b8d7 100644 --- a/src/components/modal/presenter-view/styles.tsx +++ b/src/components/modal/presenter-view/styles.tsx @@ -76,8 +76,8 @@ const PickUserButton = styled.button` border: 3px solid transparent; overflow: visible; display: inline-block; - background-color: #0F70D7; - color: #FFF; + background-color: var(--btn-primary-bg, var(--color-primary, #0F70D7)); + color: var(--btn-primary-color, var(--color-white, #FFF)); border-radius: 2px; font-weight: 600; line-height: 1; @@ -88,7 +88,8 @@ const PickUserButton = styled.button` user-select: none; padding: 8px 15px; &:hover { - background-color: #0C57A7; + color: var(--btn-primary-color, var(--color-white, #FFF)); + background-color: var(--btn-primary-hover-bg, #0C57A7) !important; } `; From d81331099aa2427457f08351518bc4a78d42c79b Mon Sep 17 00:00:00 2001 From: hiroshisuga <45039819+hiroshisuga@users.noreply.github.com> Date: Fri, 18 Jul 2025 22:20:42 +0900 Subject: [PATCH 031/117] Update ja.json --- public/locales/ja.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/locales/ja.json b/public/locales/ja.json index 08d132c..2590bed 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -8,10 +8,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "発表者を含めない", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "既に指名した人を含める", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "指名可能な人", - "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "ユーザー", - "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "ユーザー", - "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "視聴者", - "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "視聴者", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "人のユーザー", + "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "人のユーザー", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabel": "人の視聴者", + "pickRandomUserPlugin.modal.presenterView.availableSection.viewerLabelPlural": "人の視聴者", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.title": "既に指名された人", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.clearButtonLabel": "全消去", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.noUsersWarning": "{0}の中で指名できる人はいません", From 71dc786796f608060cc497d04b84e812111e2f04 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Wed, 23 Jul 2025 15:54:11 -0400 Subject: [PATCH 032/117] Added Plugin Versioning info --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index ef0a32f..a1acea5 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,19 @@ The Pick Random User Plugin shows a modal for moderator to pick a user (mainly v ![Gif of plugin demo](./public/assets/plugin.gif) +## Plugin Versioning + +Please be aware that we have a separate branch of this plugin for each version of the SDK. This ensures that everything merged into a branch is compatible with the corresponding version of the BigBlueButton core. As of now, here’s the correspondence between the branches, SDK versions, and BigBlueButton core versions: + +| Repository Branch | Plugin-SDK Version | BigBlueButton Core Version | +|------------------|--------------------|----------------------------| +| v0.0.x | v0.0.x | v3.0.x | +| v0.1.x | v0.1.x | v3.1.x | + +For more information about the plugin API features, see the documentation (`readme` files) within the specific branch you are interested in. We separate the branches because, going forward, `v0.1.x` is becoming more and more different from `v0.0.x`. + +If you have any suggestions, requirements, or questions, don’t hesitate to contact us. + ## Necessary configurations By default, no ping is sounded for the randomly picked user. To activate this feature, one must add the following configurations in their `/etc/bigbluebutton/bbb-html5.yml` file. From 5277ad7e3f3af893d57107cea979e96a7f3e72fb Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Mon, 28 Jul 2025 10:54:07 -0300 Subject: [PATCH 033/117] [fix-no-notification] fix no notification when changing presenter --- src/components/modal/component.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 069cec6..2bc1c44 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -60,12 +60,13 @@ export function PickUserModal(props: PickUserModalProps) { currentUser?.presenter && !pickedUserWithEntryId, ); + const [userId, setUserId] = useState(currentUser?.userId || ''); + useEffect(() => { - setShowPresenterView(currentUser?.presenter && !pickedUserWithEntryId); // Play audio when user is selected const isPingSoundEnabled = !isPluginSettingsLoading && pluginSettings?.pingSoundEnabled; if (isPingSoundEnabled && pickedUserWithEntryId - && pickedUserWithEntryId?.pickedUser?.userId === currentUser?.userId) { + && pickedUserWithEntryId?.pickedUser?.userId === userId) { const { cdn, basename } = window.meetingClientSettings.public.app; const host = cdn + basename; const pingSoundUrl: string = pluginSettings?.pingSoundUrl @@ -75,6 +76,13 @@ export function PickUserModal(props: PickUserModalProps) { audio.play(); notifyRandomlyPickedUser(intl.formatMessage(intlMessages.currentUserPicked)); } + }, [userId, pickedUserWithEntryId]); + + useEffect(() => { + setShowPresenterView(currentUser?.presenter && !pickedUserWithEntryId); + if (userId === '') { + setUserId(currentUser.userId); + } }, [currentUser, pickedUserWithEntryId]); return ( Date: Mon, 28 Jul 2025 11:23:25 -0300 Subject: [PATCH 034/117] [fix-no-notification] - fix lint error --- src/components/modal/picked-user-view/styles.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/modal/picked-user-view/styles.tsx b/src/components/modal/picked-user-view/styles.tsx index e50c940..3590791 100644 --- a/src/components/modal/picked-user-view/styles.tsx +++ b/src/components/modal/picked-user-view/styles.tsx @@ -70,5 +70,5 @@ export { PickedUserAvatarInitials, PickedUserAvatarImage, PickedUserName, - BackButton + BackButton, }; From 907b25643705f8097203fd448ffa911ff7722399 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Mon, 28 Jul 2025 15:56:36 -0300 Subject: [PATCH 035/117] [fix-no-notification] fix notification should not appear once the user has already seen the picked user --- src/components/modal/component.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 2bc1c44..8d93a49 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -6,6 +6,7 @@ import * as Styled from './styles'; import { PickUserModalProps, WindowClientSettings } from './types'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; +import hasCurrentUserSeenPickedUser from '../../utils/utils'; const intlMessages = defineMessages({ currentUserPicked: { @@ -64,9 +65,17 @@ export function PickUserModal(props: PickUserModalProps) { useEffect(() => { // Play audio when user is selected + + const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + userId, + pickedUserWithEntryId?.pickedUser?.userId, + ); const isPingSoundEnabled = !isPluginSettingsLoading && pluginSettings?.pingSoundEnabled; if (isPingSoundEnabled && pickedUserWithEntryId - && pickedUserWithEntryId?.pickedUser?.userId === userId) { + && pickedUserWithEntryId?.pickedUser?.userId === userId + // Current user must not have seen this entry and data should be done loading + && !hasCurrentUserSeen && !pickedUserSeenEntries?.loading) { const { cdn, basename } = window.meetingClientSettings.public.app; const host = cdn + basename; const pingSoundUrl: string = pluginSettings?.pingSoundUrl @@ -76,7 +85,7 @@ export function PickUserModal(props: PickUserModalProps) { audio.play(); notifyRandomlyPickedUser(intl.formatMessage(intlMessages.currentUserPicked)); } - }, [userId, pickedUserWithEntryId]); + }, [userId, pickedUserWithEntryId, pickedUserSeenEntries]); useEffect(() => { setShowPresenterView(currentUser?.presenter && !pickedUserWithEntryId); From c5976390f7f8cebe417fce79b6cc52c48e1e839b Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Mon, 28 Jul 2025 18:21:53 -0300 Subject: [PATCH 036/117] [feat-implement-time-window] implemented time window for new users that join to not see the modal of previously selected user. --- README.md | 29 +++++++++++++++---- src/components/pick-random-user/component.tsx | 23 +++++++++++++-- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a1acea5..d7b70ea 100644 --- a/README.md +++ b/README.md @@ -19,17 +19,36 @@ For more information about the plugin API features, see the documentation (`read If you have any suggestions, requirements, or questions, don’t hesitate to contact us. -## Necessary configurations +## Configurations -By default, no ping is sounded for the randomly picked user. To activate this feature, one must add the following configurations in their `/etc/bigbluebutton/bbb-html5.yml` file. +Down below, we list all the possible configurations this plugin supports, and then a brief explanation: + +```yaml +- name: PickRandomUserPlugin + settings: + pingSoundEnabled: false + pingSoundUrl: resources/sounds/doorbell.mp3 + pickedUserTimeWindow: 30 seconds +``` + +| Name | Description | Default | +|------------------------|--------------------------------------|-----------------------------| +| `pingSoundEnabled` | Whether the ping sound is enabled | `false` | +| `pingSoundUrl` | URL of the ping sound file | `resources/sounds/doorbell.mp3` | +| `pickedUserTimeWindow` | Time window to consider a user as recently picked (users that join after that time will not see the last modal) | `30` | + + +### Notification/Ping sound + +By default, no ping sound is played for the randomly picked user. To activate this feature, one must add the following configurations in their `/etc/bigbluebutton/bbb-html5.yml` file. So within that file and in `public.plugins` add the following configurations: ```yaml - name: PickRandomUserPlugin - settings: - pingSoundEnabled: true - pingSoundUrl: resources/sounds/doorbell.mp3 + settings: + pingSoundEnabled: true + pingSoundUrl: resources/sounds/doorbell.mp3 ``` The result yaml will look like: diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index bbde2e2..3dcc40e 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -24,6 +24,8 @@ const LOCALE_REQUEST_OBJECT = (!process.env.NODE_ENV || process.env.NODE_ENV === }, } : null; +const PICKED_USER_TIME_WINDOW = 30; // seconds + function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { BbbPluginSdk.initialize(uuid); const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); @@ -35,6 +37,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const [userFilterViewer, setUserFilterViewer] = useState(true); const [filterOutPresenter, setFilterOutPresenter] = useState(true); const [filterOutPickedUsers, setFilterOutPickedUsers] = useState(true); + const [pickedUserTimeWindow, setPickedUserTimeWindow] = useState(PICKED_USER_TIME_WINDOW); const { data: pluginSettings, loading: isPluginSettingsLoading } = pluginApi.usePluginSettings(); @@ -44,6 +47,14 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { && pluginSettings.pingSoundEnabled) { Notification.requestPermission(); } + if (!isPluginSettingsLoading + && pluginSettings + && pluginSettings.pickedUserTimeWindow + && typeof pluginSettings.pickedUserTimeWindow === 'number' + && !Number.isNaN(pluginSettings.pickedUserTimeWindow) + ) { + setPickedUserTimeWindow(pluginSettings.pickedUserTimeWindow); + } }, [isPluginSettingsLoading, pluginSettings]); const currentUserInfo = pluginApi.useCurrentUser(); @@ -154,7 +165,15 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { currentUser?.userId, pickedUserToUpdate?.payloadJson.userId, ); - if (!hasCurrentUserSeen && !pickedUserSeenEntries?.loading) { + const isPreviousPickInTimeWindow = ( + (new Date().getTime() - new Date(pickedUserToUpdate.createdAt).getTime()) / 1000 + <= pickedUserTimeWindow + ); + if ( + !hasCurrentUserSeen + && !pickedUserSeenEntries?.loading + && isPreviousPickInTimeWindow + ) { setShowModal(true); } } else if (pickedUserFromDataChannel.data @@ -162,7 +181,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { setPickedUserWithEntryId(null); if (currentUser && !currentUser.presenter) setShowModal(false); } - }, [pickedUserFromDataChannelResponse, pickedUserSeenEntries]); + }, [pickedUserFromDataChannelResponse, pickedUserSeenEntries, pickedUserTimeWindow]); useEffect(() => { if (!pickedUserWithEntryId && !currentUser?.presenter) setShowModal(false); From 1809e753d1ce1a26248b5a964ed2112665f5306e Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Tue, 29 Jul 2025 17:41:34 -0300 Subject: [PATCH 037/117] [feat-implement-time-window] - changed default from 30 seconds to 10 seconds --- README.md | 2 +- src/components/pick-random-user/component.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d7b70ea..93476d8 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Down below, we list all the possible configurations this plugin supports, and th settings: pingSoundEnabled: false pingSoundUrl: resources/sounds/doorbell.mp3 - pickedUserTimeWindow: 30 seconds + pickedUserTimeWindow: 10 # seconds ``` | Name | Description | Default | diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index 3dcc40e..ea641a6 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -24,7 +24,7 @@ const LOCALE_REQUEST_OBJECT = (!process.env.NODE_ENV || process.env.NODE_ENV === }, } : null; -const PICKED_USER_TIME_WINDOW = 30; // seconds +const PICKED_USER_TIME_WINDOW = 10; // seconds function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { BbbPluginSdk.initialize(uuid); From 5b822d1a2e3aad9b8f37f464b697f94116885688 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Mon, 4 Aug 2025 13:35:38 -0300 Subject: [PATCH 038/117] [update-sdk] update SDK and include index.json for the new useLocaleMessages --- package-lock.json | 255 ++++++++++++++++---------------------- package.json | 2 +- public/locales/index.json | 9 ++ 3 files changed, 114 insertions(+), 152 deletions(-) create mode 100644 public/locales/index.json diff --git a/package-lock.json b/package-lock.json index 94f0227..c01a2d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.79", + "bigbluebutton-html-plugin-sdk": "0.0.89", "path": "^0.12.7", "react": "^18.2.0", "react-chat-elements": "^12.0.14", @@ -114,14 +114,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz", - "integrity": "sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -412,19 +411,17 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "license": "MIT", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "engines": { "node": ">=6.9.0" } @@ -454,25 +451,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", - "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", - "license": "MIT", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.26.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz", - "integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==", - "license": "MIT", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1671,26 +1666,21 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.2.tgz", + "integrity": "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", - "license": "MIT", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1715,13 +1705,12 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", - "license": "MIT", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1851,11 +1840,10 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2026,11 +2014,10 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3635,9 +3622,9 @@ "license": "MIT" }, "node_modules/bigbluebutton-html-plugin-sdk": { - "version": "0.0.79", - "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.79.tgz", - "integrity": "sha512-D/YLelzhjWDs8FmUa24TbpZfN+iHuU6T29GYNPFMQ0IGpg0XaRcfQTr9jorWEBwAx1LLwx1SvvF1uXUD+Q3QKQ==", + "version": "0.0.89", + "resolved": "https://registry.npmjs.org/bigbluebutton-html-plugin-sdk/-/bigbluebutton-html-plugin-sdk-0.0.89.tgz", + "integrity": "sha512-KD09jHDaANHkDOFn3dNZtVrgP0OY5RK03m3Nw+LOyuXfg2KTjR9DWgmxU55k6JD5F8bYJ6uw0Zr6MB7jIWusDw==", "dependencies": { "@apollo/client": "^3.8.7", "@browser-bunyan/console-formatted-stream": "^1.8.0", @@ -3686,16 +3673,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -3725,11 +3702,10 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -3798,11 +3774,10 @@ "license": "MIT" }, "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -4046,18 +4021,17 @@ } }, "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, - "license": "MIT", "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", + "bytes": "3.1.2", + "compressible": "~2.0.18", "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", "vary": "~1.1.2" }, "engines": { @@ -4081,12 +4055,14 @@ "dev": true, "license": "MIT" }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, - "license": "MIT" + "engines": { + "node": ">= 0.6" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -4269,11 +4245,10 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -5113,11 +5088,10 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5200,11 +5174,10 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5270,11 +5243,10 @@ } }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5383,11 +5355,10 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5567,11 +5538,10 @@ "license": "BSD-3-Clause" }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, - "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -5592,7 +5562,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -5607,6 +5577,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -6069,11 +6043,10 @@ "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6410,11 +6383,10 @@ } }, "node_modules/http-proxy-middleware": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", - "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "dev": true, - "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -7824,9 +7796,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, "funding": [ { @@ -7834,7 +7806,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -8057,11 +8028,10 @@ } }, "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -8314,11 +8284,10 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", - "dev": true, - "license": "MIT" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true }, "node_modules/path-type": { "version": "4.0.0", @@ -8775,16 +8744,6 @@ "node": ">= 0.8" } }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -8989,12 +8948,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "license": "MIT" - }, "node_modules/regenerator-transform": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", diff --git a/package.json b/package.json index 5a38469..d0cb46e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@types/react": "^18.2.13", "@types/react-dom": "^18.2.6", "babel-plugin-syntax-dynamic-import": "^6.18.0", - "bigbluebutton-html-plugin-sdk": "0.0.79", + "bigbluebutton-html-plugin-sdk": "0.0.89", "react-intl": "^6.6.8", "path": "^0.12.7", "react": "^18.2.0", diff --git a/public/locales/index.json b/public/locales/index.json new file mode 100644 index 0000000..6c72eae --- /dev/null +++ b/public/locales/index.json @@ -0,0 +1,9 @@ +[ + "de.json", + "en.json", + "fr-FR.json", + "index.json", + "it.json", + "ja.json", + "pt-BR.json" +] From 533d679c6774687b81ecfb24c596081637c341ae Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 14 Aug 2025 11:10:34 -0300 Subject: [PATCH 039/117] [general-refactor] refactor settings fetching and use behavior --- src/commons/constants.ts | 3 + src/commons/types.ts | 5 ++ src/{utils => commons}/utils.ts | 4 +- src/components/modal/component.tsx | 15 ++-- .../modal/picked-user-view/component.tsx | 2 +- src/components/modal/types.ts | 5 +- src/components/pick-random-user/component.tsx | 32 +++------ src/components/pick-random-user/hooks.ts | 70 +++++++++++++++++++ 8 files changed, 97 insertions(+), 39 deletions(-) create mode 100644 src/commons/constants.ts create mode 100644 src/commons/types.ts rename src/{utils => commons}/utils.ts (88%) create mode 100644 src/components/pick-random-user/hooks.ts diff --git a/src/commons/constants.ts b/src/commons/constants.ts new file mode 100644 index 0000000..96072dd --- /dev/null +++ b/src/commons/constants.ts @@ -0,0 +1,3 @@ +const PICKED_USER_TIME_WINDOW = 10; // seconds + +export default PICKED_USER_TIME_WINDOW; diff --git a/src/commons/types.ts b/src/commons/types.ts new file mode 100644 index 0000000..fc54250 --- /dev/null +++ b/src/commons/types.ts @@ -0,0 +1,5 @@ +export interface PickRandomUserSettings { + pingSoundEnabled: boolean; + pingSoundUrl: string; + pickedUserTimeWindow: number; +} diff --git a/src/utils/utils.ts b/src/commons/utils.ts similarity index 88% rename from src/utils/utils.ts rename to src/commons/utils.ts index 04bbf7e..f4ed9cf 100644 --- a/src/utils/utils.ts +++ b/src/commons/utils.ts @@ -9,7 +9,7 @@ import { PickedUserSeenEntryDataChannel } from '../components/pick-random-user/t * @param pickedUserId userId from the picked user * @returns boolean indicating if the current user has seen the picked-user */ -const hasCurrentUserSeenPickedUser = ( +export const hasCurrentUserSeenPickedUser = ( pickedUserSeenEntries: GraphqlResponseWrapper< DataChannelEntryResponseType[]>, currentUserId: string, @@ -20,4 +20,4 @@ const hasCurrentUserSeenPickedUser = ( && view.payloadJson.seenByUserId === currentUserId && view.payloadJson.pickedUserId === pickedUserId); -export default hasCurrentUserSeenPickedUser; +export const isNumber = (obj: unknown): boolean => obj && typeof obj && !Number.isNaN(obj); diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 8d93a49..899a61a 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -6,7 +6,7 @@ import * as Styled from './styles'; import { PickUserModalProps, WindowClientSettings } from './types'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; -import hasCurrentUserSeenPickedUser from '../../utils/utils'; +import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; const intlMessages = defineMessages({ currentUserPicked: { @@ -35,8 +35,7 @@ function notifyRandomlyPickedUser(message: string) { export function PickUserModal(props: PickUserModalProps) { const { - pluginSettings, - isPluginSettingsLoading, + pickRandomUserSettings, intl, showModal, handleCloseModal, @@ -57,6 +56,7 @@ export function PickUserModal(props: PickUserModalProps) { pushPickedUserSeen, } = props; + const { pingSoundEnabled, pingSoundUrl } = pickRandomUserSettings; const [showPresenterView, setShowPresenterView] = useState( currentUser?.presenter && !pickedUserWithEntryId, ); @@ -65,22 +65,15 @@ export function PickUserModal(props: PickUserModalProps) { useEffect(() => { // Play audio when user is selected - const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( pickedUserSeenEntries, userId, pickedUserWithEntryId?.pickedUser?.userId, ); - const isPingSoundEnabled = !isPluginSettingsLoading && pluginSettings?.pingSoundEnabled; - if (isPingSoundEnabled && pickedUserWithEntryId + if (pingSoundEnabled && pickedUserWithEntryId && pickedUserWithEntryId?.pickedUser?.userId === userId // Current user must not have seen this entry and data should be done loading && !hasCurrentUserSeen && !pickedUserSeenEntries?.loading) { - const { cdn, basename } = window.meetingClientSettings.public.app; - const host = cdn + basename; - const pingSoundUrl: string = pluginSettings?.pingSoundUrl - ? String(pluginSettings?.pingSoundUrl) - : `${host}/resources/sounds/doorbell.mp3`; const audio = new Audio(pingSoundUrl); audio.play(); notifyRandomlyPickedUser(intl.formatMessage(intlMessages.currentUserPicked)); diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index ce49867..87b8b2b 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react'; import { defineMessages } from 'react-intl'; import { PickedUserViewComponentProps } from './types'; import * as Styled from './styles'; -import hasCurrentUserSeenPickedUser from '../../../utils/utils'; +import { hasCurrentUserSeenPickedUser } from '../../../commons/utils'; const intlMessages = defineMessages({ currentUserPicked: { diff --git a/src/components/modal/types.ts b/src/components/modal/types.ts index a76b49d..6c1136c 100644 --- a/src/components/modal/types.ts +++ b/src/components/modal/types.ts @@ -1,12 +1,11 @@ -import { PluginSettingsData } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-consumption/domain/settings/plugin-settings/types'; import { CurrentUserData, DeleteEntryFunction, GraphqlResponseWrapper } from 'bigbluebutton-html-plugin-sdk'; import { IntlShape } from 'react-intl'; import { DataChannelEntryResponseType, PushEntryFunction } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; import { PickedUser, PickedUserWithEntryId, PickedUserSeenEntryDataChannel } from '../pick-random-user/types'; +import { PickRandomUserSettings } from '../../commons/types'; export interface PickUserModalProps { - pluginSettings: PluginSettingsData; - isPluginSettingsLoading: boolean; + pickRandomUserSettings: PickRandomUserSettings intl: IntlShape showModal: boolean; handleCloseModal: () => void; diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index ea641a6..60c120b 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -3,7 +3,11 @@ import { useState, useEffect } from 'react'; import { createIntl, createIntlCache } from 'react-intl'; import { BbbPluginSdk, PluginApi, RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; -import hasCurrentUserSeenPickedUser from '../../utils/utils'; +import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; +import { + useGetAllSettings, + useRequestPermissionForNotification, +} from './hooks'; import { ModalInformationFromPresenter, PickRandomUserPluginProps, @@ -24,8 +28,6 @@ const LOCALE_REQUEST_OBJECT = (!process.env.NODE_ENV || process.env.NODE_ENV === }, } : null; -const PICKED_USER_TIME_WINDOW = 10; // seconds - function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { BbbPluginSdk.initialize(uuid); const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); @@ -37,25 +39,12 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const [userFilterViewer, setUserFilterViewer] = useState(true); const [filterOutPresenter, setFilterOutPresenter] = useState(true); const [filterOutPickedUsers, setFilterOutPickedUsers] = useState(true); - const [pickedUserTimeWindow, setPickedUserTimeWindow] = useState(PICKED_USER_TIME_WINDOW); - const { data: pluginSettings, loading: isPluginSettingsLoading } = pluginApi.usePluginSettings(); + const settingsResponseData = pluginApi.usePluginSettings(); + const pickRandomUserSettings = useGetAllSettings(settingsResponseData); + const { pickedUserTimeWindow, pingSoundEnabled } = pickRandomUserSettings; - useEffect(() => { - if (!isPluginSettingsLoading - && pluginSettings - && pluginSettings.pingSoundEnabled) { - Notification.requestPermission(); - } - if (!isPluginSettingsLoading - && pluginSettings - && pluginSettings.pickedUserTimeWindow - && typeof pluginSettings.pickedUserTimeWindow === 'number' - && !Number.isNaN(pluginSettings.pickedUserTimeWindow) - ) { - setPickedUserTimeWindow(pluginSettings.pickedUserTimeWindow); - } - }, [isPluginSettingsLoading, pluginSettings]); + useRequestPermissionForNotification(pingSoundEnabled); const currentUserInfo = pluginApi.useCurrentUser(); const shouldUnmountPlugin = pluginApi.useShouldUnmountPlugin(); @@ -195,8 +184,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { <> void, + settingsData: GraphqlResponseWrapper, +) => { + const { data: pluginSettings, loading: isPluginSettingsLoading } = settingsData; + useEffect(() => { + if (!isPluginSettingsLoading && pluginSettings) { + callback(pluginSettings); + } + }, [isPluginSettingsLoading, pluginSettings]); +}; + +export const useRequestPermissionForNotification = ( + pingSoundEnabled: boolean, +) => { + useEffect(() => { + if (pingSoundEnabled) { + Notification.requestPermission(); + } + }, [pingSoundEnabled]); +}; + +export const getPingSoundEnabled = ( + settings: PluginSettingsData, +): boolean => !!settings.pingSoundEnabled; + +const getPickedUserTimeWindowFromSettings = (settings: PluginSettingsData) => { + const settingTimeWindow = settings.pickedUserTimeWindow as unknown; + if (isNumber(settingTimeWindow)) { + const timeWindow: number = settingTimeWindow as number; + return timeWindow; + } return PICKED_USER_TIME_WINDOW; +}; + +const getPingSoundUrl = (settings: PluginSettingsData): string => { + const { cdn, basename } = window.meetingClientSettings.public.app; + const host = cdn + basename; + const pingSoundUrl: string = settings.pingSoundUrl + ? String(settings.pingSoundUrl) + : `${host}/resources/sounds/doorbell.mp3`; + return pingSoundUrl; +}; + +export const useGetAllSettings = ( + settingsData: GraphqlResponseWrapper, +): PickRandomUserSettings => { + const [pingSoundEnabled, setPingSoundEnabled] = useState(false); + const [pingSoundUrl, setPingSoundUrl] = useState(''); + const [pickedUserTimeWindow, setPickedUserTimeWindow] = useState(PICKED_USER_TIME_WINDOW); + useSettingsLoaded((settings) => { + setPickedUserTimeWindow(getPickedUserTimeWindowFromSettings(settings)); + setPingSoundEnabled(getPingSoundEnabled(settings)); + setPingSoundUrl(getPingSoundUrl(settings)); + }, settingsData); + return { + pingSoundEnabled, + pingSoundUrl, + pickedUserTimeWindow, + }; +}; From 08361bb1d2822e634a9b68cac11e7c80e13956ad Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 Aug 2025 09:33:56 -0300 Subject: [PATCH 040/117] [general-refactor] Filter Options have been moved to be controlled by a hook only --- manifest.json | 2 +- src/commons/constants.ts | 4 +- src/components/modal/component.tsx | 15 --- .../modal/presenter-view/component.tsx | 42 +++++--- src/components/modal/presenter-view/types.ts | 6 -- src/components/modal/types.ts | 7 -- src/components/pick-random-user/component.tsx | 95 +++++++++---------- src/components/pick-random-user/context.ts | 16 ++++ src/components/pick-random-user/hooks.ts | 94 +++++++++++++++++- src/components/pick-random-user/types.ts | 12 +-- 10 files changed, 188 insertions(+), 105 deletions(-) create mode 100644 src/components/pick-random-user/context.ts diff --git a/manifest.json b/manifest.json index 98e4d12..85b7167 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,7 @@ "localesBaseUrl": "locales", "dataChannels": [ { - "name": "modalInformationFromPresenter", + "name": "filterOptions", "pushPermission": ["presenter"], "replaceOrDeletePermission": ["creator", "moderator"] }, diff --git a/src/commons/constants.ts b/src/commons/constants.ts index 96072dd..dfa0a11 100644 --- a/src/commons/constants.ts +++ b/src/commons/constants.ts @@ -1,3 +1 @@ -const PICKED_USER_TIME_WINDOW = 10; // seconds - -export default PICKED_USER_TIME_WINDOW; +export const PICKED_USER_TIME_WINDOW = 10; // seconds diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 899a61a..e22787a 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -43,15 +43,8 @@ export function PickUserModal(props: PickUserModalProps) { pickedUserWithEntryId, handlePickRandomUser, currentUser, - filterOutPresenter, - setFilterOutPresenter, - userFilterViewer, - setUserFilterViewer, - filterOutPickedUsers, - setFilterOutPickedUsers, dataChannelPickedUsers, deletionFunction, - dispatcherPickedUser, pickedUserSeenEntries, pushPickedUserSeen, } = props; @@ -111,18 +104,11 @@ export function PickUserModal(props: PickUserModalProps) { ) : ( @@ -135,7 +121,6 @@ export function PickUserModal(props: PickUserModalProps) { currentUser, showModal, setShowPresenterView, - dispatcherPickedUser, }} /> ) diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index 3744190..ff19cc7 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -2,10 +2,12 @@ import * as React from 'react'; import { RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; import { DataChannelEntryResponseType } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-channel/types'; import { defineMessages } from 'react-intl'; +import { useContext } from 'react'; import * as Styled from './styles'; import { PickedUser } from '../../pick-random-user/types'; import { PresenterViewComponentProps } from './types'; +import { FilterOptionsContext } from '../../pick-random-user/context'; const intlMessages = defineMessages({ optionsTitle: { @@ -113,12 +115,6 @@ const makeVerticalListOfNames = ( export function PresenterViewComponent(props: PresenterViewComponentProps) { const { intl, - filterOutPresenter, - setFilterOutPresenter, - userFilterViewer, - setUserFilterViewer, - filterOutPickedUsers, - setFilterOutPickedUsers, deletionFunction, handlePickRandomUser, dataChannelPickedUsers, @@ -126,9 +122,20 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { users, } = props; + const { + filterOptions, + setFilterOptions, + } = useContext(FilterOptionsContext); + + const { + skipModerators, + skipPresenter, + includePickedUsers, + } = filterOptions; + let userRoleLabel: string; const usersCountVariable = { 0: users?.length }; - if (userFilterViewer) { + if (skipModerators) { userRoleLabel = (users?.length !== 1) ? intl.formatMessage(intlMessages.viewerLabelPlural, usersCountVariable) : intl.formatMessage(intlMessages.viewerLabel, usersCountVariable); @@ -148,9 +155,12 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { { - setUserFilterViewer(!userFilterViewer); + setFilterOptions((filterOptionsPrevious) => ({ + ...filterOptionsPrevious, + skipModerators: !filterOptionsPrevious.skipModerators, + })); }} name="options" value="skipModerators" @@ -163,9 +173,12 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { { - setFilterOutPresenter(!filterOutPresenter); + setFilterOptions((filterOptionsPrevious) => ({ + ...filterOptionsPrevious, + skipPresenter: !filterOptionsPrevious.skipPresenter, + })); }} name="options" value="skipPresenter" @@ -178,9 +191,12 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { { - setFilterOutPickedUsers(!filterOutPickedUsers); + setFilterOptions((filterOptionsPrevious) => ({ + ...filterOptionsPrevious, + includePickedUsers: !filterOptionsPrevious.includePickedUsers, + })); }} name="options" value="includePickedUsers" diff --git a/src/components/modal/presenter-view/types.ts b/src/components/modal/presenter-view/types.ts index d00e750..26cabcc 100644 --- a/src/components/modal/presenter-view/types.ts +++ b/src/components/modal/presenter-view/types.ts @@ -5,12 +5,6 @@ import { PickedUser, PickedUserWithEntryId } from '../../pick-random-user/types' export interface PresenterViewComponentProps { intl: IntlShape; - filterOutPresenter: boolean; - setFilterOutPresenter: (filter: boolean) => void; - userFilterViewer: boolean; - setUserFilterViewer: (filter: boolean) => void; - filterOutPickedUsers: boolean; - setFilterOutPickedUsers: (filter: boolean) => void; deletionFunction: DeleteEntryFunction; handlePickRandomUser: () => void; dataChannelPickedUsers?: DataChannelEntryResponseType[]; diff --git a/src/components/modal/types.ts b/src/components/modal/types.ts index 6c1136c..b02fea4 100644 --- a/src/components/modal/types.ts +++ b/src/components/modal/types.ts @@ -13,15 +13,8 @@ export interface PickUserModalProps { pickedUserWithEntryId: PickedUserWithEntryId; handlePickRandomUser: () => void; currentUser: CurrentUserData; - filterOutPresenter: boolean, - setFilterOutPresenter: (filter: boolean) => void, - userFilterViewer: boolean; - setUserFilterViewer: (filter: boolean) => void; - filterOutPickedUsers: boolean, - setFilterOutPickedUsers: (filter: boolean) => void, dataChannelPickedUsers?: DataChannelEntryResponseType[]; deletionFunction: DeleteEntryFunction; - dispatcherPickedUser: PushEntryFunction; pickedUserSeenEntries: GraphqlResponseWrapper< DataChannelEntryResponseType[]>; pushPickedUserSeen: PushEntryFunction; diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index 60c120b..60ad8bf 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -1,21 +1,22 @@ import * as React from 'react'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { createIntl, createIntlCache } from 'react-intl'; import { BbbPluginSdk, PluginApi, RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; import { useGetAllSettings, + useGetFilterOptions, useRequestPermissionForNotification, } from './hooks'; import { - ModalInformationFromPresenter, PickRandomUserPluginProps, PickedUserSeenEntryDataChannel, PickedUser, PickedUserWithEntryId, UsersMoreInformationGraphqlResponse, } from './types'; +import { FilterOptionsContext } from './context'; import { USERS_MORE_INFORMATION } from './queries'; import { PickUserModal } from '../modal/component'; import { Role } from './enums'; @@ -36,9 +37,6 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const [ pickedUserWithEntryId, setPickedUserWithEntryId] = useState(); - const [userFilterViewer, setUserFilterViewer] = useState(true); - const [filterOutPresenter, setFilterOutPresenter] = useState(true); - const [filterOutPickedUsers, setFilterOutPickedUsers] = useState(true); const settingsResponseData = pluginApi.usePluginSettings(); const pickRandomUserSettings = useGetAllSettings(settingsResponseData); @@ -71,10 +69,13 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { pushEntry: pushPickedUser, deleteEntry: deletePickedUser, } = pluginApi.useDataChannel('pickRandomUser'); + + const [filterOptions, setFilterOptions] = useGetFilterOptions(pluginApi, currentUser?.presenter); const { - data: modalInformationFromPresenter, - pushEntry: dispatchModalInformationFromPresenter, - } = pluginApi.useDataChannel('modalInformationFromPresenter'); + skipModerators, + skipPresenter, + includePickedUsers, + } = filterOptions; const { data: pickedUserSeenEntries, @@ -87,23 +88,11 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { loading: false, }; - useEffect(() => { - const modalInformationList = modalInformationFromPresenter - .data; - const modalInformation = modalInformationList - ? modalInformationList[modalInformationList.length - 1]?.payloadJson : null; - if (modalInformation) { - setFilterOutPresenter(modalInformation.skipPresenter); - setUserFilterViewer(modalInformation.skipModerators); - setFilterOutPickedUsers(!modalInformation.includePickedUsers); - } - }, [modalInformationFromPresenter]); - const usersToBePicked: UsersMoreInformationGraphqlResponse = { user: allUsers?.user.filter((user) => { let roleFilter = true; - if (userFilterViewer) roleFilter = user.role === Role.VIEWER; - if (filterOutPickedUsers && pickedUserFromDataChannel.data) { + if (skipModerators) roleFilter = user.role === Role.VIEWER; + if (!includePickedUsers && pickedUserFromDataChannel.data) { return roleFilter && pickedUserFromDataChannel .data.findIndex( (u) => u?.payloadJson?.userId === user?.userId, @@ -111,7 +100,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { } return roleFilter; }).filter((user) => { - if (filterOutPresenter) return !user.presenter; + if (skipPresenter) return !user.presenter; return true; }), }; @@ -128,11 +117,6 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const handleCloseModal = (): void => { if (currentUser?.presenter) { - dispatchModalInformationFromPresenter({ - skipModerators: userFilterViewer, - skipPresenter: filterOutPresenter, - includePickedUsers: !filterOutPickedUsers, - }); pushPickedUser(null); } setShowModal(false); @@ -176,35 +160,42 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { if (!pickedUserWithEntryId && !currentUser?.presenter) setShowModal(false); }, [pickedUserWithEntryId]); + const value = useMemo( + () => ({ + filterOptions, + setFilterOptions, + }), + [filterOptions, setFilterOptions], + ); + useEffect(() => { - if (!currentUser?.presenter && dispatchModalInformationFromPresenter) handleCloseModal(); + if (!currentUser?.presenter) handleCloseModal(); }, [currentUser]); + if (!intl || localeMessagesLoading) return null; + return !shouldUnmountPlugin && ( <> - + + + > +} + +export const FilterOptionsContext = createContext({ + filterOptions: { + skipModerators: true, + skipPresenter: true, + includePickedUsers: true, + }, + setFilterOptions: () => {}, +}); diff --git a/src/components/pick-random-user/hooks.ts b/src/components/pick-random-user/hooks.ts index 5eb1586..f4ec451 100644 --- a/src/components/pick-random-user/hooks.ts +++ b/src/components/pick-random-user/hooks.ts @@ -1,13 +1,22 @@ -import { GraphqlResponseWrapper } from 'bigbluebutton-html-plugin-sdk'; +import { + DataChannelEntryResponseType, + DataChannelTypes, + GraphqlResponseWrapper, + PluginApi, + PushEntryFunction, +} from 'bigbluebutton-html-plugin-sdk'; import { PluginSettingsData } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-consumption/domain/settings/plugin-settings/types'; import { useEffect, useState } from 'react'; -import PICKED_USER_TIME_WINDOW from '../../commons/constants'; +import { PICKED_USER_TIME_WINDOW } from '../../commons/constants'; import { isNumber } from '../../commons/utils'; import { PickRandomUserSettings } from '../../commons/types'; import { WindowClientSettings } from '../modal/types'; +import { FilterOptionsType } from './types'; declare const window: WindowClientSettings; +// Settings hooks and utilities + const useSettingsLoaded = ( callback: (settings: PluginSettingsData) => void, settingsData: GraphqlResponseWrapper, @@ -68,3 +77,84 @@ export const useGetAllSettings = ( pickedUserTimeWindow, }; }; + +// Filter Options hooks and utilities + +const useUpdateFilterOptionsOnDataChannel = ( + pushFilterOptionsToDataChannel: PushEntryFunction, + filterOptions: FilterOptionsType, + isPresenter: boolean, + dataChannelLoading: boolean, +) => { + useEffect(() => { + if (isPresenter && !dataChannelLoading) { + pushFilterOptionsToDataChannel(filterOptions); + } + }, [isPresenter, filterOptions, dataChannelLoading]); +}; + +const hasFilterOptionsChanged = ( + currentFilterOptions: FilterOptionsType, + filterOptionsFromDataChannel?: FilterOptionsType, +) => filterOptionsFromDataChannel?.includePickedUsers !== currentFilterOptions.includePickedUsers + || filterOptionsFromDataChannel?.skipModerators !== currentFilterOptions.skipModerators + || filterOptionsFromDataChannel?.skipPresenter !== currentFilterOptions.skipPresenter; + +const useObserveFilterOptionsFromDataChannel = ( + currentFilterOptions: FilterOptionsType, + filterOptionsFromDataChannel: FilterOptionsType | undefined, + setFilterOptions: React.Dispatch>, +) => { + useEffect(() => { + if ( + filterOptionsFromDataChannel + && hasFilterOptionsChanged(currentFilterOptions, filterOptionsFromDataChannel)) { + setFilterOptions({ + includePickedUsers: filterOptionsFromDataChannel.includePickedUsers, + skipModerators: filterOptionsFromDataChannel.skipModerators, + skipPresenter: filterOptionsFromDataChannel.skipPresenter, + }); + } + }, [filterOptionsFromDataChannel]); +}; + +const getLatestFilterOptionsFromDataChannel = ( + filterOptionsFromDataChannelResponse: GraphqlResponseWrapper< + DataChannelEntryResponseType[] + >, +) => { + const persistedFilterOptionsList = filterOptionsFromDataChannelResponse.data; + const currentFilterOptionsFromDataChannel = persistedFilterOptionsList + ? persistedFilterOptionsList[0]?.payloadJson : null; + return currentFilterOptionsFromDataChannel; +}; + +export const useGetFilterOptions = ( + pluginApi: PluginApi, + currentUserPresenter: boolean, +): [FilterOptionsType, React.Dispatch>] => { + const [filterOptions, setFilterOptions] = useState({ + skipModerators: true, + skipPresenter: true, + includePickedUsers: true, + }); + const { + data: filterOptionsFromDataChannel, + pushEntry: pushFilterOptionsToDataChannel, + } = pluginApi.useDataChannel('filterOptions', DataChannelTypes.LATEST_ITEM); + const latestFilterOptionFromDataChannel = getLatestFilterOptionsFromDataChannel( + filterOptionsFromDataChannel, + ); + useObserveFilterOptionsFromDataChannel( + filterOptions, + latestFilterOptionFromDataChannel, + setFilterOptions, + ); + useUpdateFilterOptionsOnDataChannel( + pushFilterOptionsToDataChannel, + filterOptions, + currentUserPresenter, + filterOptionsFromDataChannel.loading, + ); + return [filterOptions, setFilterOptions]; +}; diff --git a/src/components/pick-random-user/types.ts b/src/components/pick-random-user/types.ts index f72deb1..587e1db 100644 --- a/src/components/pick-random-user/types.ts +++ b/src/components/pick-random-user/types.ts @@ -21,13 +21,13 @@ export interface UsersMoreInformationGraphqlResponse { user: PickedUser[]; } -export interface ModalInformationFromPresenter { - skipModerators: boolean; - skipPresenter: boolean; - includePickedUsers: boolean; -} - export interface PickedUserSeenEntryDataChannel { pickedUserId: string; seenByUserId: string; } + +export interface FilterOptionsType { + skipModerators: boolean; + skipPresenter: boolean; + includePickedUsers: boolean; +} From ee3a66acbe24741b378d84828c68f0570b9c7e95 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 Aug 2025 13:50:39 -0300 Subject: [PATCH 041/117] [general-refactor] simplify with some more hooks and utilities new flows. --- src/commons/constants.ts | 2 + src/commons/hooks.ts | 29 ++++ .../action-button-dropdown/component.tsx | 6 +- .../action-button-dropdown/types.ts | 2 +- src/components/modal/component.tsx | 41 ++---- src/components/modal/types.ts | 2 +- src/components/modal/utils.ts | 19 +++ src/components/pick-random-user/component.tsx | 131 +++++------------- src/components/pick-random-user/hooks.ts | 87 +++++++++++- src/components/pick-random-user/utils.ts | 28 ++++ 10 files changed, 215 insertions(+), 132 deletions(-) create mode 100644 src/commons/hooks.ts create mode 100644 src/components/modal/utils.ts create mode 100644 src/components/pick-random-user/utils.ts diff --git a/src/commons/constants.ts b/src/commons/constants.ts index dfa0a11..3bf1ac6 100644 --- a/src/commons/constants.ts +++ b/src/commons/constants.ts @@ -1 +1,3 @@ export const PICKED_USER_TIME_WINDOW = 10; // seconds + +export const TIMEOUT_CLOSE_NOTIFICATION = 5000; diff --git a/src/commons/hooks.ts b/src/commons/hooks.ts new file mode 100644 index 0000000..627850c --- /dev/null +++ b/src/commons/hooks.ts @@ -0,0 +1,29 @@ +import { PluginApi } from 'bigbluebutton-html-plugin-sdk'; +import { createIntl, createIntlCache } from 'react-intl'; + +const LOCALE_REQUEST_OBJECT = (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') + ? { + headers: { + 'ngrok-skip-browser-warning': 'any', + }, + } : undefined; + +export const useGetInternationalization = (pluginApi: PluginApi) => { + const { + messages: localeMessages, + currentLocale, + loading: localeMessagesLoading, + } = pluginApi.useLocaleMessages!(LOCALE_REQUEST_OBJECT); + + const cache = createIntlCache(); + const intl = (!localeMessagesLoading && localeMessages) ? createIntl({ + locale: currentLocale, + messages: localeMessages, + fallbackOnEmptyString: true, + }, cache) : null; + + return { + intl, + localeMessagesLoading, + }; +}; diff --git a/src/components/extensible-areas/action-button-dropdown/component.tsx b/src/components/extensible-areas/action-button-dropdown/component.tsx index 8e4fb4b..6263e0a 100644 --- a/src/components/extensible-areas/action-button-dropdown/component.tsx +++ b/src/components/extensible-areas/action-button-dropdown/component.tsx @@ -21,7 +21,7 @@ const intlMessages = defineMessages({ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): React.ReactNode { const { intl, - pickedUserWithEntryId, + currentPickedUser, currentUser, pluginApi, setShowModal, @@ -42,7 +42,7 @@ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): R }, }), ]); - } else if (!currentUser?.presenter && pickedUserWithEntryId) { + } else if (!currentUser?.presenter && currentPickedUser) { pluginApi.setActionButtonDropdownItems([ new ActionButtonDropdownSeparator(), new ActionButtonDropdownOption({ @@ -58,7 +58,7 @@ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): R } else { pluginApi.setActionButtonDropdownItems([]); } - }, [currentUserInfo, pickedUserWithEntryId, intl]); + }, [currentUserInfo, currentPickedUser, intl]); return null; } diff --git a/src/components/extensible-areas/action-button-dropdown/types.ts b/src/components/extensible-areas/action-button-dropdown/types.ts index b390e3a..3b4e437 100644 --- a/src/components/extensible-areas/action-button-dropdown/types.ts +++ b/src/components/extensible-areas/action-button-dropdown/types.ts @@ -4,7 +4,7 @@ import { PickedUserWithEntryId } from '../../pick-random-user/types'; export interface ActionButtonDropdownManagerProps { intl: IntlShape - pickedUserWithEntryId: PickedUserWithEntryId; + currentPickedUser: PickedUserWithEntryId; currentUser: CurrentUserData; pluginApi: PluginApi; setShowModal: React.Dispatch>; diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index e22787a..55a1ccb 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -1,12 +1,12 @@ import { useEffect, useState } from 'react'; import * as React from 'react'; import { defineMessages } from 'react-intl'; -import { pluginLogger } from 'bigbluebutton-html-plugin-sdk'; import * as Styled from './styles'; -import { PickUserModalProps, WindowClientSettings } from './types'; +import { PickUserModalProps } from './types'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; +import { notifyRandomlyPickedUser } from './utils'; const intlMessages = defineMessages({ currentUserPicked: { @@ -16,23 +16,6 @@ const intlMessages = defineMessages({ }, }); -const TIMEOUT_CLOSE_NOTIFICATION = 5000; - -declare const window: WindowClientSettings; - -function notifyRandomlyPickedUser(message: string) { - if (!('Notification' in window)) { - pluginLogger.warn('This browser does not support notifications'); - } else if (Notification.permission === 'granted') { - const notification = new Notification(message); - setTimeout(() => { - notification.close(); - }, TIMEOUT_CLOSE_NOTIFICATION); - } else if (Notification.permission !== 'denied') { - pluginLogger.warn('Browser notification permission has been denied'); - } -} - export function PickUserModal(props: PickUserModalProps) { const { pickRandomUserSettings, @@ -40,7 +23,7 @@ export function PickUserModal(props: PickUserModalProps) { showModal, handleCloseModal, users, - pickedUserWithEntryId, + currentPickedUser, handlePickRandomUser, currentUser, dataChannelPickedUsers, @@ -51,7 +34,7 @@ export function PickUserModal(props: PickUserModalProps) { const { pingSoundEnabled, pingSoundUrl } = pickRandomUserSettings; const [showPresenterView, setShowPresenterView] = useState( - currentUser?.presenter && !pickedUserWithEntryId, + currentUser?.presenter && !currentPickedUser, ); const [userId, setUserId] = useState(currentUser?.userId || ''); @@ -61,24 +44,24 @@ export function PickUserModal(props: PickUserModalProps) { const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( pickedUserSeenEntries, userId, - pickedUserWithEntryId?.pickedUser?.userId, + currentPickedUser?.pickedUser?.userId, ); - if (pingSoundEnabled && pickedUserWithEntryId - && pickedUserWithEntryId?.pickedUser?.userId === userId + if (pingSoundEnabled && currentPickedUser + && currentPickedUser?.pickedUser?.userId === userId // Current user must not have seen this entry and data should be done loading && !hasCurrentUserSeen && !pickedUserSeenEntries?.loading) { const audio = new Audio(pingSoundUrl); audio.play(); notifyRandomlyPickedUser(intl.formatMessage(intlMessages.currentUserPicked)); } - }, [userId, pickedUserWithEntryId, pickedUserSeenEntries]); + }, [userId, currentPickedUser, pickedUserSeenEntries]); useEffect(() => { - setShowPresenterView(currentUser?.presenter && !pickedUserWithEntryId); + setShowPresenterView(currentUser?.presenter && !currentPickedUser); if (userId === '') { setUserId(currentUser.userId); } - }, [currentUser, pickedUserWithEntryId]); + }, [currentUser, currentPickedUser]); return ( @@ -116,7 +99,7 @@ export function PickUserModal(props: PickUserModalProps) { {...{ pickedUserSeenEntries, pushPickedUserSeen, - pickedUserWithEntryId, + pickedUserWithEntryId: currentPickedUser, intl, currentUser, showModal, diff --git a/src/components/modal/types.ts b/src/components/modal/types.ts index b02fea4..298cae3 100644 --- a/src/components/modal/types.ts +++ b/src/components/modal/types.ts @@ -10,7 +10,7 @@ export interface PickUserModalProps { showModal: boolean; handleCloseModal: () => void; users?: PickedUser[]; - pickedUserWithEntryId: PickedUserWithEntryId; + currentPickedUser: PickedUserWithEntryId; handlePickRandomUser: () => void; currentUser: CurrentUserData; dataChannelPickedUsers?: DataChannelEntryResponseType[]; diff --git a/src/components/modal/utils.ts b/src/components/modal/utils.ts new file mode 100644 index 0000000..4cfb41e --- /dev/null +++ b/src/components/modal/utils.ts @@ -0,0 +1,19 @@ +import { pluginLogger } from 'bigbluebutton-html-plugin-sdk'; + +import { TIMEOUT_CLOSE_NOTIFICATION } from '../../commons/constants'; +import { WindowClientSettings } from './types'; + +declare const window: WindowClientSettings; + +export function notifyRandomlyPickedUser(message: string) { + if (!('Notification' in window)) { + pluginLogger.warn('This browser does not support notifications'); + } else if (Notification.permission === 'granted') { + const notification = new Notification(message); + setTimeout(() => { + notification.close(); + }, TIMEOUT_CLOSE_NOTIFICATION); + } else if (Notification.permission !== 'denied') { + pluginLogger.warn('Browser notification permission has been denied'); + } +} diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index 60ad8bf..4a76360 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { useState, useEffect, useMemo } from 'react'; -import { createIntl, createIntlCache } from 'react-intl'; import { BbbPluginSdk, PluginApi, RESET_DATA_CHANNEL } from 'bigbluebutton-html-plugin-sdk'; -import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; import { + useControlModalState, useGetAllSettings, + useGetCurrentPickedUser, useGetFilterOptions, useRequestPermissionForNotification, } from './hooks'; @@ -13,32 +13,23 @@ import { PickRandomUserPluginProps, PickedUserSeenEntryDataChannel, PickedUser, - PickedUserWithEntryId, UsersMoreInformationGraphqlResponse, } from './types'; import { FilterOptionsContext } from './context'; import { USERS_MORE_INFORMATION } from './queries'; import { PickUserModal } from '../modal/component'; -import { Role } from './enums'; import ActionButtonDropdownManager from '../extensible-areas/action-button-dropdown/component'; - -const LOCALE_REQUEST_OBJECT = (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') - ? { - headers: { - 'ngrok-skip-browser-warning': 'any', - }, - } : null; +import { filterPossibleUsersToBePicked } from './utils'; +import { useGetInternationalization } from '../../commons/hooks'; function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { BbbPluginSdk.initialize(uuid); const pluginApi: PluginApi = BbbPluginSdk.getPluginApi(uuid); const [showModal, setShowModal] = useState(false); - const [ - pickedUserWithEntryId, - setPickedUserWithEntryId] = useState(); const settingsResponseData = pluginApi.usePluginSettings(); + const pickRandomUserSettings = useGetAllSettings(settingsResponseData); const { pickedUserTimeWindow, pingSoundEnabled } = pickRandomUserSettings; @@ -52,30 +43,20 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const { data: allUsers } = allUsersInfo; const { - messages: localeMessages, - currentLocale, - loading: localeMessagesLoading, - } = pluginApi.useLocaleMessages(LOCALE_REQUEST_OBJECT); - - const cache = createIntlCache(); - const intl = (!localeMessagesLoading && localeMessages) ? createIntl({ - locale: currentLocale, - messages: localeMessages, - fallbackOnEmptyString: true, - }, cache) : null; + intl, + localeMessagesLoading, + } = useGetInternationalization(pluginApi); const { data: pickedUserFromDataChannelResponse, pushEntry: pushPickedUser, deleteEntry: deletePickedUser, } = pluginApi.useDataChannel('pickRandomUser'); + const pickedUserFromDataChannel = pickedUserFromDataChannelResponse?.data; const [filterOptions, setFilterOptions] = useGetFilterOptions(pluginApi, currentUser?.presenter); - const { - skipModerators, - skipPresenter, - includePickedUsers, - } = filterOptions; + + const currentPickedUser = useGetCurrentPickedUser(pickedUserFromDataChannel); const { data: pickedUserSeenEntries, @@ -83,36 +64,23 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { deleteEntry: deletePickedUserSeenEntries, } = pluginApi.useDataChannel('pickedUserSeenEntry'); - const pickedUserFromDataChannel = { - data: pickedUserFromDataChannelResponse?.data, - loading: false, - }; - - const usersToBePicked: UsersMoreInformationGraphqlResponse = { - user: allUsers?.user.filter((user) => { - let roleFilter = true; - if (skipModerators) roleFilter = user.role === Role.VIEWER; - if (!includePickedUsers && pickedUserFromDataChannel.data) { - return roleFilter && pickedUserFromDataChannel - .data.findIndex( - (u) => u?.payloadJson?.userId === user?.userId, - ) === -1; - } - return roleFilter; - }).filter((user) => { - if (skipPresenter) return !user.presenter; - return true; - }), - }; + const possibleUsersToBePicked = filterPossibleUsersToBePicked( + allUsers, + pickedUserFromDataChannel, + filterOptions, + ); const handlePickRandomUser = () => { - if (usersToBePicked && usersToBePicked.user.length > 0 && currentUser?.presenter) { + if ( + possibleUsersToBePicked + && possibleUsersToBePicked.user.length > 0 + && currentUser?.presenter + ) { deletePickedUserSeenEntries([RESET_DATA_CHANNEL]); - const randomIndex = Math.floor(Math.random() * usersToBePicked.user.length); - const randomlyPickedUser = usersToBePicked.user[randomIndex]; + const randomIndex = Math.floor(Math.random() * possibleUsersToBePicked.user.length); + const randomlyPickedUser = possibleUsersToBePicked.user[randomIndex]; pushPickedUser(randomlyPickedUser); } - setShowModal(true); }; const handleCloseModal = (): void => { @@ -122,43 +90,14 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { setShowModal(false); }; - useEffect(() => { - if (pickedUserFromDataChannel.data - && pickedUserFromDataChannel.data?.length > 0) { - const pickedUserToUpdate = pickedUserFromDataChannel - .data[0]; - if (pickedUserToUpdate?.entryId !== pickedUserWithEntryId?.entryId) { - setPickedUserWithEntryId({ - pickedUser: pickedUserToUpdate?.payloadJson, - entryId: pickedUserToUpdate.entryId, - }); - } - const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( - pickedUserSeenEntries, - currentUser?.userId, - pickedUserToUpdate?.payloadJson.userId, - ); - const isPreviousPickInTimeWindow = ( - (new Date().getTime() - new Date(pickedUserToUpdate.createdAt).getTime()) / 1000 - <= pickedUserTimeWindow - ); - if ( - !hasCurrentUserSeen - && !pickedUserSeenEntries?.loading - && isPreviousPickInTimeWindow - ) { - setShowModal(true); - } - } else if (pickedUserFromDataChannel.data - && pickedUserFromDataChannel.data?.length === 0) { - setPickedUserWithEntryId(null); - if (currentUser && !currentUser.presenter) setShowModal(false); - } - }, [pickedUserFromDataChannelResponse, pickedUserSeenEntries, pickedUserTimeWindow]); - - useEffect(() => { - if (!pickedUserWithEntryId && !currentUser?.presenter) setShowModal(false); - }, [pickedUserWithEntryId]); + useControlModalState( + pickedUserFromDataChannel, + pickedUserSeenEntries, + currentUser, + pickedUserTimeWindow, + currentPickedUser, + setShowModal, + ); const value = useMemo( () => ({ @@ -185,11 +124,11 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { intl, showModal, handleCloseModal, - users: usersToBePicked?.user, - pickedUserWithEntryId, + users: possibleUsersToBePicked?.user, + currentPickedUser, handlePickRandomUser, currentUser, - dataChannelPickedUsers: pickedUserFromDataChannel.data, + dataChannelPickedUsers: pickedUserFromDataChannel, deletionFunction: deletePickedUser, pickedUserSeenEntries, pushPickedUserSeen, @@ -199,7 +138,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { [], +): PickedUserWithEntryId | undefined => { + const [ + pickedUserWithEntryId, + setPickedUserWithEntryId] = useState(); + + useEffect(() => { + if (pickedUserFromDataChannel + && pickedUserFromDataChannel?.length > 0) { + const pickedUserToUpdate = pickedUserFromDataChannel[0]; + if (pickedUserToUpdate?.entryId !== pickedUserWithEntryId?.entryId) { + setPickedUserWithEntryId({ + pickedUser: pickedUserToUpdate?.payloadJson, + entryId: pickedUserToUpdate.entryId, + }); + } + } else if (pickedUserFromDataChannel + && pickedUserFromDataChannel?.length === 0) { + setPickedUserWithEntryId(null); + } + }, [pickedUserFromDataChannel]); + return pickedUserWithEntryId; +}; + +export const useControlModalState = ( + pickedUserFromDataChannel: DataChannelEntryResponseType[], + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>, + currentUser: CurrentUserData, + pickedUserTimeWindow: number, + currentPickedUser: PickedUserWithEntryId, + setShowModal: React.Dispatch>, +) => { + const hasValidPickInTimeWindow = (pickedUser?: DataChannelEntryResponseType) => { + if (!pickedUser) return false; + const secondsSincePick = (Date.now() - new Date(pickedUser.createdAt).getTime()) / 1000; + return secondsSincePick <= pickedUserTimeWindow; + }; + + const hasUserSeenPickedUser = (pickedUser?: DataChannelEntryResponseType) => { + if (!pickedUser) return false; + return hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUser?.userId, + pickedUser.payloadJson.userId, + ); + }; + + const shouldShowModal = (pickedUser?: DataChannelEntryResponseType) => { + if (!pickedUser) return false; + return ( + !hasUserSeenPickedUser(pickedUser) + && !pickedUserSeenEntries?.loading + && hasValidPickInTimeWindow(pickedUser) + ); + }; + + useEffect(() => { + const firstPick = pickedUserFromDataChannel?.[0]; + + if (firstPick && shouldShowModal(firstPick)) { + setShowModal(true); + return; + } + + const shouldCloseModal = (!firstPick && !currentUser?.presenter) + || (!currentPickedUser && !currentUser?.presenter); + + if (shouldCloseModal) { + setShowModal(false); + } + }, [currentPickedUser, pickedUserFromDataChannel, pickedUserSeenEntries, pickedUserTimeWindow]); +}; diff --git a/src/components/pick-random-user/utils.ts b/src/components/pick-random-user/utils.ts new file mode 100644 index 0000000..20861c2 --- /dev/null +++ b/src/components/pick-random-user/utils.ts @@ -0,0 +1,28 @@ +import { DataChannelEntryResponseType } from 'bigbluebutton-html-plugin-sdk'; +import { + FilterOptionsType, + PickedUser, + UsersMoreInformationGraphqlResponse, +} from './types'; +import { Role } from './enums'; + +export const filterPossibleUsersToBePicked = ( + allUsers: UsersMoreInformationGraphqlResponse, + pickedUserFromDataChannel: DataChannelEntryResponseType[], + filterOptions: FilterOptionsType, +) => ({ + user: allUsers?.user.filter((user) => { + if (filterOptions.skipModerators) return user.role === Role.VIEWER; + return true; + }).filter((user) => { + if (filterOptions.skipPresenter) return !user.presenter; + return true; + }).filter((user) => { + if (!filterOptions.includePickedUsers && pickedUserFromDataChannel) { + return pickedUserFromDataChannel.findIndex( + (u) => u?.payloadJson?.userId === user?.userId, + ) === -1; + } + return true; + }), +}); From 1a32f7e8c8071db9effe19727c2681f00a562b0d Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 Aug 2025 13:55:14 -0300 Subject: [PATCH 042/117] [general-refactor] return back the original initial state of filterOptions --- src/components/pick-random-user/context.ts | 2 +- src/components/pick-random-user/hooks.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/pick-random-user/context.ts b/src/components/pick-random-user/context.ts index 3bec055..08a2604 100644 --- a/src/components/pick-random-user/context.ts +++ b/src/components/pick-random-user/context.ts @@ -10,7 +10,7 @@ export const FilterOptionsContext = createContext({ filterOptions: { skipModerators: true, skipPresenter: true, - includePickedUsers: true, + includePickedUsers: false, }, setFilterOptions: () => {}, }); diff --git a/src/components/pick-random-user/hooks.ts b/src/components/pick-random-user/hooks.ts index 6c7cbda..79e0d1a 100644 --- a/src/components/pick-random-user/hooks.ts +++ b/src/components/pick-random-user/hooks.ts @@ -142,7 +142,7 @@ export const useGetFilterOptions = ( const [filterOptions, setFilterOptions] = useState({ skipModerators: true, skipPresenter: true, - includePickedUsers: true, + includePickedUsers: false, }); const { data: filterOptionsFromDataChannel, From 8c176e81ab175d96a9fdab19595a6a24354914f8 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 Aug 2025 16:22:15 -0300 Subject: [PATCH 043/117] [general-refactor] rename skip presenter/moderator to include presenter/moderator --- public/locales/de.json | 2 - public/locales/en.json | 4 +- public/locales/fr-FR.json | 2 - public/locales/it.json | 2 - public/locales/ja.json | 2 - public/locales/pt-BR.json | 2 - .../modal/presenter-view/component.tsx | 42 +++++++++---------- src/components/pick-random-user/context.ts | 4 +- src/components/pick-random-user/hooks.ts | 12 +++--- src/components/pick-random-user/types.ts | 4 +- src/components/pick-random-user/utils.ts | 4 +- 11 files changed, 35 insertions(+), 45 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index 455f371..d4aca6b 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -4,8 +4,6 @@ "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "zurück", "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Avatar von Teilnehmer {0}", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Optionen", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Überspringe Moderatoren", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Überspringe Präsentator", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Wähle bereits gewählte Teilnehmer", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Zur Auswahl stehen", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "Teilnehmer", diff --git a/public/locales/en.json b/public/locales/en.json index 5772509..e15908c 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -4,8 +4,8 @@ "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "back", "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Avatar image of user {0}", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Skip moderators", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Skip presenter", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Include moderators", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Include presenter", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Include already picked user", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Available for selection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "user", diff --git a/public/locales/fr-FR.json b/public/locales/fr-FR.json index 02b93f1..b817097 100644 --- a/public/locales/fr-FR.json +++ b/public/locales/fr-FR.json @@ -3,8 +3,6 @@ "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Participant sélectionné au hasard", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "retour", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Ignorer les modérateurs", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Ignorer le présentateur", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Inclure un participant déjà sélectionné", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponible pour la sélection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "participant", diff --git a/public/locales/it.json b/public/locales/it.json index 0b82bf4..01084d5 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -3,8 +3,6 @@ "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Utente scelto a caso", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "indietro", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opzioni", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Salta i moderatori", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Salta il presentatore", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Includi l'utente già selezionato", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponibile per la selezione", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "utente", diff --git a/public/locales/ja.json b/public/locales/ja.json index 2590bed..2454325 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -4,8 +4,6 @@ "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "戻る", "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "ユーザー {0} のアバター画像", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "設定", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "司会者を含めない", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "発表者を含めない", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "既に指名した人を含める", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "指名可能な人", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "人のユーザー", diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json index 9915303..85bfd03 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -4,8 +4,6 @@ "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "voltar", "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Iamgem avatar do usuário {0}", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opções", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel": "Ignorar moderadores", - "pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel": "Ignorar apresentador", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Incluir usuário já selecionado", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponíveis para seleção", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "usuário", diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index ff19cc7..18b9adf 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -15,13 +15,13 @@ const intlMessages = defineMessages({ description: 'Title of the options section on modal`s presenter view', defaultMessage: 'Options', }, - skipModeratorsLabel: { - id: 'pickRandomUserPlugin.modal.presenterView.optionSection.skipModeratorsLabel', - description: 'Label of skip moderator`s option on modal`s presenter view', - defaultMessage: 'Skip moderators', + includeModeratorsLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel', + description: 'Label to include moderator`s option on modal`s presenter view', + defaultMessage: 'Include moderators', }, - skipPresenterLabel: { - id: 'pickRandomUserPlugin.modal.presenterView.optionSection.skipPresenterLabel', + includePresenterLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel', description: 'Label of skip presenter`s option on modal`s presenter view', defaultMessage: 'Skip presenter', }, @@ -128,14 +128,14 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { } = useContext(FilterOptionsContext); const { - skipModerators, - skipPresenter, + includeModerators, + includePresenter, includePickedUsers, } = filterOptions; let userRoleLabel: string; const usersCountVariable = { 0: users?.length }; - if (skipModerators) { + if (!includeModerators) { userRoleLabel = (users?.length !== 1) ? intl.formatMessage(intlMessages.viewerLabelPlural, usersCountVariable) : intl.formatMessage(intlMessages.viewerLabel, usersCountVariable); @@ -151,40 +151,40 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { {intl.formatMessage(intlMessages.optionsTitle)} - + { setFilterOptions((filterOptionsPrevious) => ({ ...filterOptionsPrevious, - skipModerators: !filterOptionsPrevious.skipModerators, + includeModerators: !filterOptionsPrevious.includeModerators, })); }} name="options" - value="skipModerators" + value="includeModerators" /> - {intl.formatMessage(intlMessages.skipModeratorsLabel)} + {intl.formatMessage(intlMessages.includeModeratorsLabel)} - + { setFilterOptions((filterOptionsPrevious) => ({ ...filterOptionsPrevious, - skipPresenter: !filterOptionsPrevious.skipPresenter, + includePresenter: !filterOptionsPrevious.includePresenter, })); }} name="options" - value="skipPresenter" + value="includePresenter" /> - {intl.formatMessage(intlMessages.skipPresenterLabel)} + {intl.formatMessage(intlMessages.includePresenterLabel)} diff --git a/src/components/pick-random-user/context.ts b/src/components/pick-random-user/context.ts index 08a2604..018c1b6 100644 --- a/src/components/pick-random-user/context.ts +++ b/src/components/pick-random-user/context.ts @@ -8,8 +8,8 @@ interface FilterOptionsContextType { export const FilterOptionsContext = createContext({ filterOptions: { - skipModerators: true, - skipPresenter: true, + includeModerators: false, + includePresenter: false, includePickedUsers: false, }, setFilterOptions: () => {}, diff --git a/src/components/pick-random-user/hooks.ts b/src/components/pick-random-user/hooks.ts index 79e0d1a..3b9f88a 100644 --- a/src/components/pick-random-user/hooks.ts +++ b/src/components/pick-random-user/hooks.ts @@ -103,8 +103,8 @@ const hasFilterOptionsChanged = ( currentFilterOptions: FilterOptionsType, filterOptionsFromDataChannel?: FilterOptionsType, ) => filterOptionsFromDataChannel?.includePickedUsers !== currentFilterOptions.includePickedUsers - || filterOptionsFromDataChannel?.skipModerators !== currentFilterOptions.skipModerators - || filterOptionsFromDataChannel?.skipPresenter !== currentFilterOptions.skipPresenter; + || filterOptionsFromDataChannel?.includeModerators !== currentFilterOptions.includeModerators + || filterOptionsFromDataChannel?.includePresenter !== currentFilterOptions.includePresenter; const useObserveFilterOptionsFromDataChannel = ( currentFilterOptions: FilterOptionsType, @@ -117,8 +117,8 @@ const useObserveFilterOptionsFromDataChannel = ( && hasFilterOptionsChanged(currentFilterOptions, filterOptionsFromDataChannel)) { setFilterOptions({ includePickedUsers: filterOptionsFromDataChannel.includePickedUsers, - skipModerators: filterOptionsFromDataChannel.skipModerators, - skipPresenter: filterOptionsFromDataChannel.skipPresenter, + includeModerators: filterOptionsFromDataChannel.includeModerators, + includePresenter: filterOptionsFromDataChannel.includePresenter, }); } }, [filterOptionsFromDataChannel]); @@ -140,8 +140,8 @@ export const useGetFilterOptions = ( currentUserPresenter: boolean, ): [FilterOptionsType, React.Dispatch>] => { const [filterOptions, setFilterOptions] = useState({ - skipModerators: true, - skipPresenter: true, + includeModerators: false, + includePresenter: false, includePickedUsers: false, }); const { diff --git a/src/components/pick-random-user/types.ts b/src/components/pick-random-user/types.ts index 587e1db..bc5ee58 100644 --- a/src/components/pick-random-user/types.ts +++ b/src/components/pick-random-user/types.ts @@ -27,7 +27,7 @@ export interface PickedUserSeenEntryDataChannel { } export interface FilterOptionsType { - skipModerators: boolean; - skipPresenter: boolean; + includeModerators: boolean; + includePresenter: boolean; includePickedUsers: boolean; } diff --git a/src/components/pick-random-user/utils.ts b/src/components/pick-random-user/utils.ts index 20861c2..b1053af 100644 --- a/src/components/pick-random-user/utils.ts +++ b/src/components/pick-random-user/utils.ts @@ -12,10 +12,10 @@ export const filterPossibleUsersToBePicked = ( filterOptions: FilterOptionsType, ) => ({ user: allUsers?.user.filter((user) => { - if (filterOptions.skipModerators) return user.role === Role.VIEWER; + if (!filterOptions.includeModerators) return user.role === Role.VIEWER; return true; }).filter((user) => { - if (filterOptions.skipPresenter) return !user.presenter; + if (!filterOptions.includePresenter) return !user.presenter; return true; }).filter((user) => { if (!filterOptions.includePickedUsers && pickedUserFromDataChannel) { From 21803c5cc686eaeb733123127180c06e9e621ec2 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 Aug 2025 17:24:12 -0300 Subject: [PATCH 044/117] [browser-notification-enabled] separate ping sound from browser notification settings --- README.md | 29 +++++++++++----- src/commons/types.ts | 1 + src/components/modal/component.tsx | 15 ++++---- src/components/modal/utils.ts | 5 +++ src/components/pick-random-user/component.tsx | 4 +-- src/components/pick-random-user/hooks.ts | 34 +++++++++++++++---- 6 files changed, 66 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 93476d8..1166d7f 100644 --- a/README.md +++ b/README.md @@ -26,29 +26,43 @@ Down below, we list all the possible configurations this plugin supports, and th ```yaml - name: PickRandomUserPlugin settings: - pingSoundEnabled: false + pingSoundEnabled: true pingSoundUrl: resources/sounds/doorbell.mp3 + browserNotificationEnabled: true pickedUserTimeWindow: 10 # seconds ``` | Name | Description | Default | |------------------------|--------------------------------------|-----------------------------| -| `pingSoundEnabled` | Whether the ping sound is enabled | `false` | +| `pingSoundEnabled` | Flag that decides whether the ping sound is enabled for picked user | `true` | | `pingSoundUrl` | URL of the ping sound file | `resources/sounds/doorbell.mp3` | +| `browserNotificationEnabled` | Flag that decides whether to send browser notification when user is picked | `false` | | `pickedUserTimeWindow` | Time window to consider a user as recently picked (users that join after that time will not see the last modal) | `30` | -### Notification/Ping sound +### Notification -By default, no ping sound is played for the randomly picked user. To activate this feature, one must add the following configurations in their `/etc/bigbluebutton/bbb-html5.yml` file. +By default, browser notification when user is randomly picked is not enabled. To enable it, add the following settings in the `/etc/bigbluebutton/bbb-html5.yml` file: + +```yaml +public: + # ... + plugins: + - name: PickRandomUserPlugin + settings: + browserNotificationEnabled: false +``` + +### Ping sound + +By default, ping sound is played for the randomly picked user. To remove this feature, one must add the following configurations in their `/etc/bigbluebutton/bbb-html5.yml` file. So within that file and in `public.plugins` add the following configurations: ```yaml - name: PickRandomUserPlugin settings: - pingSoundEnabled: true - pingSoundUrl: resources/sounds/doorbell.mp3 + pingSoundEnabled: false ``` The result yaml will look like: @@ -59,8 +73,7 @@ public: plugins: - name: PickRandomUserPlugin settings: - pingSoundEnabled: true # Enables the ping sound for this plugin true/false - pingSoundUrl: resources/sounds/doorbell.mp3 # This is the default and is not mandatory + pingSoundEnabled: false ``` Just a minor comment: This relative URLs can only be configured if the server on which BBB is running is not a cluster setup. If that's your case, you'll need to put the whole URL into the configuration. It's also worth mentioning that the default `pingSoundUrl` will work in cluser setups, so no worries on that. diff --git a/src/commons/types.ts b/src/commons/types.ts index fc54250..f309889 100644 --- a/src/commons/types.ts +++ b/src/commons/types.ts @@ -1,5 +1,6 @@ export interface PickRandomUserSettings { pingSoundEnabled: boolean; pingSoundUrl: string; + browserNotificationEnabled: boolean; pickedUserTimeWindow: number; } diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 55a1ccb..f730adb 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -6,7 +6,7 @@ import { PickUserModalProps } from './types'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; -import { notifyRandomlyPickedUser } from './utils'; +import { notifyRandomlyPickedUser, pingSoundForRandomlyPickedUser } from './utils'; const intlMessages = defineMessages({ currentUserPicked: { @@ -32,7 +32,7 @@ export function PickUserModal(props: PickUserModalProps) { pushPickedUserSeen, } = props; - const { pingSoundEnabled, pingSoundUrl } = pickRandomUserSettings; + const { pingSoundEnabled, pingSoundUrl, browserNotificationEnabled } = pickRandomUserSettings; const [showPresenterView, setShowPresenterView] = useState( currentUser?.presenter && !currentPickedUser, ); @@ -46,13 +46,16 @@ export function PickUserModal(props: PickUserModalProps) { userId, currentPickedUser?.pickedUser?.userId, ); - if (pingSoundEnabled && currentPickedUser + if (currentPickedUser && currentPickedUser?.pickedUser?.userId === userId // Current user must not have seen this entry and data should be done loading && !hasCurrentUserSeen && !pickedUserSeenEntries?.loading) { - const audio = new Audio(pingSoundUrl); - audio.play(); - notifyRandomlyPickedUser(intl.formatMessage(intlMessages.currentUserPicked)); + if (pingSoundEnabled) pingSoundForRandomlyPickedUser(pingSoundUrl); + if (browserNotificationEnabled) { + notifyRandomlyPickedUser( + intl.formatMessage(intlMessages.currentUserPicked), + ); + } } }, [userId, currentPickedUser, pickedUserSeenEntries]); diff --git a/src/components/modal/utils.ts b/src/components/modal/utils.ts index 4cfb41e..7c7e93a 100644 --- a/src/components/modal/utils.ts +++ b/src/components/modal/utils.ts @@ -17,3 +17,8 @@ export function notifyRandomlyPickedUser(message: string) { pluginLogger.warn('Browser notification permission has been denied'); } } + +export function pingSoundForRandomlyPickedUser(pingSoundUrl: string) { + const audio = new Audio(pingSoundUrl); + audio.play(); +} diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index 4a76360..34985ac 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -31,9 +31,9 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { const settingsResponseData = pluginApi.usePluginSettings(); const pickRandomUserSettings = useGetAllSettings(settingsResponseData); - const { pickedUserTimeWindow, pingSoundEnabled } = pickRandomUserSettings; + const { pickedUserTimeWindow, browserNotificationEnabled } = pickRandomUserSettings; - useRequestPermissionForNotification(pingSoundEnabled); + useRequestPermissionForNotification(browserNotificationEnabled); const currentUserInfo = pluginApi.useCurrentUser(); const shouldUnmountPlugin = pluginApi.useShouldUnmountPlugin(); diff --git a/src/components/pick-random-user/hooks.ts b/src/components/pick-random-user/hooks.ts index 3b9f88a..d85d9bd 100644 --- a/src/components/pick-random-user/hooks.ts +++ b/src/components/pick-random-user/hooks.ts @@ -36,18 +36,33 @@ const useSettingsLoaded = ( }; export const useRequestPermissionForNotification = ( - pingSoundEnabled: boolean, + browserNotificationEnabled: boolean, ) => { useEffect(() => { - if (pingSoundEnabled) { + if (browserNotificationEnabled) { Notification.requestPermission(); } - }, [pingSoundEnabled]); + }, [browserNotificationEnabled]); }; export const getPingSoundEnabled = ( settings: PluginSettingsData, -): boolean => !!settings.pingSoundEnabled; + previousState: boolean, +): boolean => { + if (settings.pingSoundEnabled === undefined || settings.pingSoundEnabled === null) { + return previousState; + } return !!settings.pingSoundEnabled; +}; + +export const getBrowserNotificationEnabled = ( + settings: PluginSettingsData, + previousState: boolean, +): boolean => { + if (settings.browserNotificationEnabled === undefined + || settings.browserNotificationEnabled === null) { + return previousState; + } return !!settings.browserNotificationEnabled; +}; const getPickedUserTimeWindowFromSettings = (settings: PluginSettingsData) => { const settingTimeWindow = settings.pickedUserTimeWindow as unknown; @@ -69,17 +84,24 @@ const getPingSoundUrl = (settings: PluginSettingsData): string => { export const useGetAllSettings = ( settingsData: GraphqlResponseWrapper, ): PickRandomUserSettings => { - const [pingSoundEnabled, setPingSoundEnabled] = useState(false); + const [pingSoundEnabled, setPingSoundEnabled] = useState(true); + const [browserNotificationEnabled, setBrowserNotificationEnabled] = useState(false); const [pingSoundUrl, setPingSoundUrl] = useState(''); const [pickedUserTimeWindow, setPickedUserTimeWindow] = useState(PICKED_USER_TIME_WINDOW); useSettingsLoaded((settings) => { + setBrowserNotificationEnabled( + (previousState) => getBrowserNotificationEnabled(settings, previousState), + ); + setPingSoundEnabled( + (previousState) => getPingSoundEnabled(settings, previousState), + ); setPickedUserTimeWindow(getPickedUserTimeWindowFromSettings(settings)); - setPingSoundEnabled(getPingSoundEnabled(settings)); setPingSoundUrl(getPingSoundUrl(settings)); }, settingsData); return { pingSoundEnabled, pingSoundUrl, + browserNotificationEnabled, pickedUserTimeWindow, }; }; From 39f85d890099d9484689fb77da23958e3ffa3d63 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 21 Aug 2025 17:42:27 -0300 Subject: [PATCH 045/117] [browser-notification-enabled] Fixed multiple ping sounds and fix no audio playing --- src/commons/constants.ts | 2 + src/components/modal/component.tsx | 35 +++------- src/components/modal/hooks.ts | 70 +++++++++++++++++++ .../modal/picked-user-view/component.tsx | 2 +- src/components/pick-random-user/hooks.ts | 6 +- 5 files changed, 84 insertions(+), 31 deletions(-) create mode 100644 src/components/modal/hooks.ts diff --git a/src/commons/constants.ts b/src/commons/constants.ts index 3bf1ac6..56b86e5 100644 --- a/src/commons/constants.ts +++ b/src/commons/constants.ts @@ -1,3 +1,5 @@ export const PICKED_USER_TIME_WINDOW = 10; // seconds export const TIMEOUT_CLOSE_NOTIFICATION = 5000; + +export const DEFAULT_PING_SOUND_URL = 'resources/sounds/doorbell.mp3'; diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index f730adb..fa04e45 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -5,8 +5,7 @@ import * as Styled from './styles'; import { PickUserModalProps } from './types'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; -import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; -import { notifyRandomlyPickedUser, pingSoundForRandomlyPickedUser } from './utils'; +import { useHandleCurrentUserNotification } from './hooks'; const intlMessages = defineMessages({ currentUserPicked: { @@ -32,38 +31,20 @@ export function PickUserModal(props: PickUserModalProps) { pushPickedUserSeen, } = props; - const { pingSoundEnabled, pingSoundUrl, browserNotificationEnabled } = pickRandomUserSettings; const [showPresenterView, setShowPresenterView] = useState( currentUser?.presenter && !currentPickedUser, ); - const [userId, setUserId] = useState(currentUser?.userId || ''); - - useEffect(() => { - // Play audio when user is selected - const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( - pickedUserSeenEntries, - userId, - currentPickedUser?.pickedUser?.userId, - ); - if (currentPickedUser - && currentPickedUser?.pickedUser?.userId === userId - // Current user must not have seen this entry and data should be done loading - && !hasCurrentUserSeen && !pickedUserSeenEntries?.loading) { - if (pingSoundEnabled) pingSoundForRandomlyPickedUser(pingSoundUrl); - if (browserNotificationEnabled) { - notifyRandomlyPickedUser( - intl.formatMessage(intlMessages.currentUserPicked), - ); - } - } - }, [userId, currentPickedUser, pickedUserSeenEntries]); + useHandleCurrentUserNotification( + currentUser, + pickedUserSeenEntries, + currentPickedUser, + pickRandomUserSettings, + intl.formatMessage(intlMessages.currentUserPicked), + ); useEffect(() => { setShowPresenterView(currentUser?.presenter && !currentPickedUser); - if (userId === '') { - setUserId(currentUser.userId); - } }, [currentUser, currentPickedUser]); return ( { + const [currentUserId, setCurrentUserId] = useState(currentUser?.userId || ''); + + useEffect(() => { + if (currentUserId === '') { + setCurrentUserId(currentUser.userId); + } + }, [currentUser]); + + return currentUserId; +}; + +export const useHandleCurrentUserNotification = ( + currentUser: CurrentUserData, + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>, + currentPickedUser: PickedUserWithEntryId, + pickRandomUserSettings: PickRandomUserSettings, + notificationMessage: string, +) => { + const currentUserId = useCurrentUserId(currentUser); + + const { pingSoundEnabled, pingSoundUrl, browserNotificationEnabled } = pickRandomUserSettings; + + const [currentUserNotified, setCurrentUserNotified] = useState(false); + + const currentPickedUserId = currentPickedUser?.pickedUser?.userId; + + // Control internal state of user-notification to not rely only on data-channel information + useEffect(() => { + const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUserId, + currentPickedUserId, + ); + if (hasCurrentUserSeen) { + setCurrentUserNotified(false); + } + }, [pickedUserSeenEntries]); + + // Notify or ping audio (or both) when user is selected + useEffect(() => { + const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUserId, + currentPickedUserId, + ); + if (currentPickedUser + && currentPickedUserId === currentUserId + // Current user must not have seen this entry and data should be done loading + && !hasCurrentUserSeen && !pickedUserSeenEntries?.loading + && !currentUserNotified + ) { + if (pingSoundEnabled) pingSoundForRandomlyPickedUser(pingSoundUrl); + if (browserNotificationEnabled) { + notifyRandomlyPickedUser( + notificationMessage, + ); + } + setCurrentUserNotified(true); + } + }, [currentUserId, currentPickedUser, pickedUserSeenEntries, pickRandomUserSettings]); +}; diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index 87b8b2b..cba664a 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -57,7 +57,7 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { seenByUserId: currentUser.userId, }); } - }, []); + }, [pickedUserWithEntryId]); const title = (pickedUserWithEntryId?.pickedUser?.userId === currentUser?.userId) ? intl.formatMessage(intlMessages.currentUserPicked) : intl.formatMessage(intlMessages.randomUserPicked); diff --git a/src/components/pick-random-user/hooks.ts b/src/components/pick-random-user/hooks.ts index d85d9bd..f0c9636 100644 --- a/src/components/pick-random-user/hooks.ts +++ b/src/components/pick-random-user/hooks.ts @@ -8,7 +8,7 @@ import { } from 'bigbluebutton-html-plugin-sdk'; import { PluginSettingsData } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-consumption/domain/settings/plugin-settings/types'; import { useEffect, useState } from 'react'; -import { PICKED_USER_TIME_WINDOW } from '../../commons/constants'; +import { DEFAULT_PING_SOUND_URL, PICKED_USER_TIME_WINDOW } from '../../commons/constants'; import { hasCurrentUserSeenPickedUser, isNumber } from '../../commons/utils'; import { PickRandomUserSettings } from '../../commons/types'; import { WindowClientSettings } from '../modal/types'; @@ -77,7 +77,7 @@ const getPingSoundUrl = (settings: PluginSettingsData): string => { const host = cdn + basename; const pingSoundUrl: string = settings.pingSoundUrl ? String(settings.pingSoundUrl) - : `${host}/resources/sounds/doorbell.mp3`; + : `${host}/${DEFAULT_PING_SOUND_URL}`; return pingSoundUrl; }; @@ -86,7 +86,7 @@ export const useGetAllSettings = ( ): PickRandomUserSettings => { const [pingSoundEnabled, setPingSoundEnabled] = useState(true); const [browserNotificationEnabled, setBrowserNotificationEnabled] = useState(false); - const [pingSoundUrl, setPingSoundUrl] = useState(''); + const [pingSoundUrl, setPingSoundUrl] = useState(DEFAULT_PING_SOUND_URL); const [pickedUserTimeWindow, setPickedUserTimeWindow] = useState(PICKED_USER_TIME_WINDOW); useSettingsLoaded((settings) => { setBrowserNotificationEnabled( From 16de812a8488a5b3e0f4d54c9f4cedb3ee5984dd Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Mon, 25 Aug 2025 09:29:24 -0400 Subject: [PATCH 046/117] fix: rename main branch to v0.0.x in build workflow --- .github/workflows/build-plugin-deb-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-plugin-deb-package.yml b/.github/workflows/build-plugin-deb-package.yml index 43bd609..942f4e1 100644 --- a/.github/workflows/build-plugin-deb-package.yml +++ b/.github/workflows/build-plugin-deb-package.yml @@ -2,11 +2,11 @@ name: "📦 Build .deb package for plugin" on: push: - branches: [ "main"] + branches: [ "v0.0.x"] paths-ignore: - debian/changelog pull_request: - branches: [ "main" ] + branches: [ "v0.0.x" ] paths-ignore: - debian/changelog From 77aa69f1bbd28f93d5ff13df0e3f179f4c02299d Mon Sep 17 00:00:00 2001 From: antobinary <6312397+antobinary@users.noreply.github.com> Date: Thu, 28 Aug 2025 17:59:14 +0000 Subject: [PATCH 047/117] chore: bump version to 0.0.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c01a2d1..a036850 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pick-random-user-plugin", - "version": "0.0.4", + "version": "0.0.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pick-random-user-plugin", - "version": "0.0.4", + "version": "0.0.5", "dependencies": { "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/package.json b/package.json index d0cb46e..5713c04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pick-random-user-plugin", - "version": "0.0.4", + "version": "0.0.5", "private": true, "main": "./src/index.tsx", "dependencies": { From 4c3b69007b5d1eb1c902d179100c7f6416e8e3be Mon Sep 17 00:00:00 2001 From: antobinary <6312397+antobinary@users.noreply.github.com> Date: Thu, 28 Aug 2025 18:02:21 +0000 Subject: [PATCH 048/117] chore: bump version to 0.0.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a036850..2dfc9b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pick-random-user-plugin", - "version": "0.0.5", + "version": "0.0.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pick-random-user-plugin", - "version": "0.0.5", + "version": "0.0.6", "dependencies": { "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/package.json b/package.json index 5713c04..f9c7698 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pick-random-user-plugin", - "version": "0.0.5", + "version": "0.0.6", "private": true, "main": "./src/index.tsx", "dependencies": { From 68e998f093fd7eda914dfce32abd5d9f3850e872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Victor?= Date: Thu, 4 Sep 2025 10:24:53 -0300 Subject: [PATCH 049/117] fix: Render modal in core container element with low priority --- src/components/modal/component.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index fa04e45..dfa7831 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -46,9 +46,14 @@ export function PickUserModal(props: PickUserModalProps) { useEffect(() => { setShowPresenterView(currentUser?.presenter && !currentPickedUser); }, [currentUser, currentPickedUser]); + + if (!showModal) return null; + return ( document.querySelector('#modals-container')} isOpen={showModal} onRequestClose={handleCloseModal} > From 71298405510a4caf0d4badf8e29046b2aaf1e0db Mon Sep 17 00:00:00 2001 From: antobinary <6312397+antobinary@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:51:17 +0000 Subject: [PATCH 050/117] chore: bump version to 0.0.7 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2dfc9b0..442f393 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pick-random-user-plugin", - "version": "0.0.6", + "version": "0.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pick-random-user-plugin", - "version": "0.0.6", + "version": "0.0.7", "dependencies": { "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/package.json b/package.json index f9c7698..ec35118 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pick-random-user-plugin", - "version": "0.0.6", + "version": "0.0.7", "private": true, "main": "./src/index.tsx", "dependencies": { From fd8e117d199b228c644732e8b32c886955d07b6a Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 10 Sep 2025 17:30:33 -0300 Subject: [PATCH 051/117] [fix-notification-when-not-picked] fix issue where current user hears ping when they are not selected (context: they were selected previously). --- src/commons/hooks.ts | 9 ++++ src/components/modal/hooks.ts | 97 ++++++++++++++++++++++------------- 2 files changed, 71 insertions(+), 35 deletions(-) diff --git a/src/commons/hooks.ts b/src/commons/hooks.ts index 627850c..09e8b35 100644 --- a/src/commons/hooks.ts +++ b/src/commons/hooks.ts @@ -1,4 +1,5 @@ import { PluginApi } from 'bigbluebutton-html-plugin-sdk'; +import { useEffect, useRef } from 'react'; import { createIntl, createIntlCache } from 'react-intl'; const LOCALE_REQUEST_OBJECT = (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') @@ -27,3 +28,11 @@ export const useGetInternationalization = (pluginApi: PluginApi) => { localeMessagesLoading, }; }; + +export const usePreviousValue = (value: T) => { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; diff --git a/src/components/modal/hooks.ts b/src/components/modal/hooks.ts index 1797e70..bd1a75d 100644 --- a/src/components/modal/hooks.ts +++ b/src/components/modal/hooks.ts @@ -1,9 +1,10 @@ import { useEffect, useState } from 'react'; import { CurrentUserData, DataChannelEntryResponseType, GraphqlResponseWrapper } from 'bigbluebutton-html-plugin-sdk'; -import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; import { PickedUserSeenEntryDataChannel, PickedUserWithEntryId } from '../pick-random-user/types'; import { PickRandomUserSettings } from '../../commons/types'; import { notifyRandomlyPickedUser, pingSoundForRandomlyPickedUser } from './utils'; +import { usePreviousValue } from '../../commons/hooks'; +import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; const useCurrentUserId = (currentUser: CurrentUserData) => { const [currentUserId, setCurrentUserId] = useState(currentUser?.userId || ''); @@ -17,54 +18,80 @@ const useCurrentUserId = (currentUser: CurrentUserData) => { return currentUserId; }; -export const useHandleCurrentUserNotification = ( +export const useRunWhenNeedNotification = ( + notifyAndPingCallback: () => void, currentUser: CurrentUserData, pickedUserSeenEntries: GraphqlResponseWrapper< DataChannelEntryResponseType[]>, currentPickedUser: PickedUserWithEntryId, - pickRandomUserSettings: PickRandomUserSettings, - notificationMessage: string, ) => { const currentUserId = useCurrentUserId(currentUser); - const { pingSoundEnabled, pingSoundUrl, browserNotificationEnabled } = pickRandomUserSettings; + const [notificationRequestQueue, setNotificationRequestQueue] = useState>(new Set()); - const [currentUserNotified, setCurrentUserNotified] = useState(false); + const previousPickedUserEntryId = usePreviousValue(currentPickedUser?.entryId); const currentPickedUserId = currentPickedUser?.pickedUser?.userId; - // Control internal state of user-notification to not rely only on data-channel information useEffect(() => { - const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( - pickedUserSeenEntries, - currentUserId, - currentPickedUserId, - ); - if (hasCurrentUserSeen) { - setCurrentUserNotified(false); - } - }, [pickedUserSeenEntries]); - - // Notify or ping audio (or both) when user is selected - useEffect(() => { - const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( - pickedUserSeenEntries, - currentUserId, - currentPickedUserId, - ); if (currentPickedUser && currentPickedUserId === currentUserId - // Current user must not have seen this entry and data should be done loading - && !hasCurrentUserSeen && !pickedUserSeenEntries?.loading - && !currentUserNotified + && previousPickedUserEntryId !== currentPickedUser.entryId ) { - if (pingSoundEnabled) pingSoundForRandomlyPickedUser(pingSoundUrl); - if (browserNotificationEnabled) { - notifyRandomlyPickedUser( - notificationMessage, - ); - } - setCurrentUserNotified(true); + setNotificationRequestQueue((prevQueue) => { + const newQueue = new Set(prevQueue); + newQueue.add(currentPickedUser.entryId); + return newQueue; + }); } - }, [currentUserId, currentPickedUser, pickedUserSeenEntries, pickRandomUserSettings]); + }, [currentUserId, currentPickedUser]); + + useEffect(() => { + Array.from(notificationRequestQueue).filter( + (notificationRequest) => notificationRequest === currentPickedUser?.entryId, + ).filter(() => { + const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUserId, + currentPickedUser?.pickedUser.userId, + ); + return !hasCurrentUserSeen; + }).forEach((notificationRequest) => { + notifyAndPingCallback(); + setNotificationRequestQueue((previousNotificationRequests) => { + const newQueue = new Set(previousNotificationRequests); + newQueue.delete(notificationRequest); + return newQueue; + }); + }); + }, [ + notificationRequestQueue, + ]); +}; + +export const useHandleCurrentUserNotification = ( + currentUser: CurrentUserData, + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>, + currentPickedUser: PickedUserWithEntryId, + pickRandomUserSettings: PickRandomUserSettings, + notificationMessage: string, +) => { + const { pingSoundEnabled, pingSoundUrl, browserNotificationEnabled } = pickRandomUserSettings; + + function notifyAndPing() { + if (pingSoundEnabled) pingSoundForRandomlyPickedUser(pingSoundUrl); + if (browserNotificationEnabled) { + notifyRandomlyPickedUser( + notificationMessage, + ); + } + } + + useRunWhenNeedNotification( + notifyAndPing, + currentUser, + pickedUserSeenEntries, + currentPickedUser, + ); }; From d82764f1e71987c9cd8f4ded279cdc0b25cb61b9 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 11 Sep 2025 10:55:06 -0300 Subject: [PATCH 052/117] [fix-notification-when-not-picked] refactor flow to a more readable and less complex --- src/components/modal/hooks.ts | 39 +++++++++++++---------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/src/components/modal/hooks.ts b/src/components/modal/hooks.ts index bd1a75d..1b840d8 100644 --- a/src/components/modal/hooks.ts +++ b/src/components/modal/hooks.ts @@ -18,7 +18,7 @@ const useCurrentUserId = (currentUser: CurrentUserData) => { return currentUserId; }; -export const useRunWhenNeedNotification = ( +export const useObserveForNotification = ( notifyAndPingCallback: () => void, currentUser: CurrentUserData, pickedUserSeenEntries: GraphqlResponseWrapper< @@ -27,7 +27,7 @@ export const useRunWhenNeedNotification = ( ) => { const currentUserId = useCurrentUserId(currentUser); - const [notificationRequestQueue, setNotificationRequestQueue] = useState>(new Set()); + const [shouldNotify, setShouldNotify] = useState(false); const previousPickedUserEntryId = usePreviousValue(currentPickedUser?.entryId); @@ -38,34 +38,23 @@ export const useRunWhenNeedNotification = ( && currentPickedUserId === currentUserId && previousPickedUserEntryId !== currentPickedUser.entryId ) { - setNotificationRequestQueue((prevQueue) => { - const newQueue = new Set(prevQueue); - newQueue.add(currentPickedUser.entryId); - return newQueue; - }); + setShouldNotify(true); } }, [currentUserId, currentPickedUser]); useEffect(() => { - Array.from(notificationRequestQueue).filter( - (notificationRequest) => notificationRequest === currentPickedUser?.entryId, - ).filter(() => { - const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( - pickedUserSeenEntries, - currentUserId, - currentPickedUser?.pickedUser.userId, - ); - return !hasCurrentUserSeen; - }).forEach((notificationRequest) => { + const hasCurrentUserSeen = hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUserId, + currentPickedUser?.pickedUser.userId, + ); + const notifyUser = !pickedUserSeenEntries?.loading && !hasCurrentUserSeen && shouldNotify; + if (notifyUser) { notifyAndPingCallback(); - setNotificationRequestQueue((previousNotificationRequests) => { - const newQueue = new Set(previousNotificationRequests); - newQueue.delete(notificationRequest); - return newQueue; - }); - }); + } + setShouldNotify(false); }, [ - notificationRequestQueue, + shouldNotify, ]); }; @@ -88,7 +77,7 @@ export const useHandleCurrentUserNotification = ( } } - useRunWhenNeedNotification( + useObserveForNotification( notifyAndPing, currentUser, pickedUserSeenEntries, From d1ff4220e494633430977deb08f52e44465a871a Mon Sep 17 00:00:00 2001 From: antobinary <6312397+antobinary@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:47:33 +0000 Subject: [PATCH 053/117] chore: bump version to 0.0.8 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 442f393..806f3c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pick-random-user-plugin", - "version": "0.0.7", + "version": "0.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pick-random-user-plugin", - "version": "0.0.7", + "version": "0.0.8", "dependencies": { "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/package.json b/package.json index ec35118..4ce1f46 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pick-random-user-plugin", - "version": "0.0.7", + "version": "0.0.8", "private": true, "main": "./src/index.tsx", "dependencies": { From 530c2b7c2d978edc5747be75a782fe368bccf834 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 29 Oct 2025 10:48:08 -0300 Subject: [PATCH 054/117] [fix-error-logs] fixed some error logs in the console. --- src/components/modal/component.tsx | 6 +++++- src/components/modal/types.ts | 1 + src/components/pick-random-user/component.tsx | 4 +--- tsconfig.json | 1 - webpack.config.js | 5 +++++ 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index dfa7831..9cfcf54 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import * as React from 'react'; import { defineMessages } from 'react-intl'; import * as Styled from './styles'; @@ -29,8 +29,11 @@ export function PickUserModal(props: PickUserModalProps) { deletionFunction, pickedUserSeenEntries, pushPickedUserSeen, + uuid, } = props; + const modalAnchor = useRef(document.getElementById(uuid)); + const [showPresenterView, setShowPresenterView] = useState( currentUser?.presenter && !currentPickedUser, ); @@ -53,6 +56,7 @@ export function PickUserModal(props: PickUserModalProps) { document.querySelector('#modals-container')} isOpen={showModal} onRequestClose={handleCloseModal} diff --git a/src/components/modal/types.ts b/src/components/modal/types.ts index 298cae3..d63e820 100644 --- a/src/components/modal/types.ts +++ b/src/components/modal/types.ts @@ -6,6 +6,7 @@ import { PickRandomUserSettings } from '../../commons/types'; export interface PickUserModalProps { pickRandomUserSettings: PickRandomUserSettings + uuid: string, intl: IntlShape showModal: boolean; handleCloseModal: () => void; diff --git a/src/components/pick-random-user/component.tsx b/src/components/pick-random-user/component.tsx index 34985ac..74b6f94 100644 --- a/src/components/pick-random-user/component.tsx +++ b/src/components/pick-random-user/component.tsx @@ -84,9 +84,6 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { }; const handleCloseModal = (): void => { - if (currentUser?.presenter) { - pushPickedUser(null); - } setShowModal(false); }; @@ -120,6 +117,7 @@ function PickRandomUserPlugin({ pluginUuid: uuid }: PickRandomUserPluginProps) { > { if (!devServer) { From 8f0944ec3ae6278d0ba9cd95e4f04f921262af3c Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 5 Nov 2025 10:48:44 -0300 Subject: [PATCH 055/117] [add-manifest-version] added version directive to manifest --- manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/manifest.json b/manifest.json index 85b7167..49ff3d5 100644 --- a/manifest.json +++ b/manifest.json @@ -1,6 +1,7 @@ { "requiredSdkVersion": "~0.0.65", "name": "PickRandomUserPlugin", + "version": "0.0.8", "javascriptEntrypointUrl": "PickRandomUserPlugin.js", "localesBaseUrl": "locales", "dataChannels": [ From 8ed12431f9ccf3f0a2741bcc53b0c70f79c4defd Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Mon, 17 Nov 2025 11:21:22 -0300 Subject: [PATCH 056/117] [countdown-close-modal] added of modal lock countdown --- README.md | 27 ++++++- public/locales/en.json | 4 +- src/commons/constants.ts | 2 + src/commons/types.ts | 1 + src/components/modal/component.tsx | 35 +++++++-- src/components/modal/hooks.ts | 73 ++++++++++++++++++- .../modal/picked-user-view/component.tsx | 34 ++++++++- .../modal/picked-user-view/styles.tsx | 33 +++++++++ .../modal/picked-user-view/types.ts | 3 + src/components/modal/styles.tsx | 19 ++++- src/components/pick-random-user/hooks.ts | 23 +++++- 11 files changed, 241 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1166d7f..e811bba 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Down below, we list all the possible configurations this plugin supports, and th pingSoundUrl: resources/sounds/doorbell.mp3 browserNotificationEnabled: true pickedUserTimeWindow: 10 # seconds + preventCloseDelaySeconds: 3 # seconds ``` | Name | Description | Default | @@ -37,9 +38,33 @@ Down below, we list all the possible configurations this plugin supports, and th | `pingSoundEnabled` | Flag that decides whether the ping sound is enabled for picked user | `true` | | `pingSoundUrl` | URL of the ping sound file | `resources/sounds/doorbell.mp3` | | `browserNotificationEnabled` | Flag that decides whether to send browser notification when user is picked | `false` | -| `pickedUserTimeWindow` | Time window to consider a user as recently picked (users that join after that time will not see the last modal) | `30` | +| `pickedUserTimeWindow` | Time window to consider a user as recently picked (users that join after that time will not see the last modal) | `10` | +| `preventCloseDelaySeconds` | Delay in seconds before the modal can be closed to prevent accidental closures | `3` | +### Prevent Close Delay + +By default, when the modal appears showing the picked user (the view that displays the selected user's name and avatar), it cannot be closed for 3 seconds. This prevents accidental closures from misclicks. During this time: +- The close button (X) is disabled and grayed out +- Clicking outside the modal does nothing +- Pressing the Escape key does nothing +- Visual countdown indicator: + - **For viewers**: A text message below the user's name: "You can close this modal in X seconds" + - **For presenters**: A blue progress bar (similar to YouTube's) that shrinks as time passes + +**Note:** This delay only applies to the picked user view. The presenter view (used for selecting users) can be closed immediately without any delay. + +After the countdown finishes, all close methods become available. To change this delay, add the following settings in the `/etc/bigbluebutton/bbb-html5.yml` file: + +```yaml +public: + # ... + plugins: + - name: PickRandomUserPlugin + settings: + preventCloseDelaySeconds: 5 # Set to 0 to disable the delay +``` + ### Notification By default, browser notification when user is randomly picked is not enabled. To enable it, add the following settings in the `/etc/bigbluebutton/bbb-html5.yml` file: diff --git a/public/locales/en.json b/public/locales/en.json index e15908c..383e974 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -18,5 +18,7 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Pick {0}", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Pick again", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Pick random user", - "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Display last randomly picked user" + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Display last randomly picked user", + "pickRandomUserPlugin.modal.closeDelayMessage": "You can close this modal in {seconds} seconds", + "pickRandomUserPlugin.modal.closeDelayMessageSingular": "You can close this modal in {seconds} second" } diff --git a/src/commons/constants.ts b/src/commons/constants.ts index 56b86e5..b3a47b0 100644 --- a/src/commons/constants.ts +++ b/src/commons/constants.ts @@ -3,3 +3,5 @@ export const PICKED_USER_TIME_WINDOW = 10; // seconds export const TIMEOUT_CLOSE_NOTIFICATION = 5000; export const DEFAULT_PING_SOUND_URL = 'resources/sounds/doorbell.mp3'; + +export const DEFAULT_PREVENT_CLOSE_DELAY_SECONDS = 3; // seconds diff --git a/src/commons/types.ts b/src/commons/types.ts index f309889..921a070 100644 --- a/src/commons/types.ts +++ b/src/commons/types.ts @@ -3,4 +3,5 @@ export interface PickRandomUserSettings { pingSoundUrl: string; browserNotificationEnabled: boolean; pickedUserTimeWindow: number; + preventCloseDelaySeconds: number; } diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index dfa7831..9e1a775 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -5,7 +5,7 @@ import * as Styled from './styles'; import { PickUserModalProps } from './types'; import { PickedUserViewComponent } from './picked-user-view/component'; import { PresenterViewComponent } from './presenter-view/component'; -import { useHandleCurrentUserNotification } from './hooks'; +import { useHandleCurrentUserNotification, usePreventCloseModalCountdown } from './hooks'; const intlMessages = defineMessages({ currentUserPicked: { @@ -47,23 +47,45 @@ export function PickUserModal(props: PickUserModalProps) { setShowPresenterView(currentUser?.presenter && !currentPickedUser); }, [currentUser, currentPickedUser]); + const {remainingSeconds, canClose} = usePreventCloseModalCountdown( + currentUser, + pickedUserSeenEntries, + currentPickedUser, + pickRandomUserSettings, + ); + if (!showModal) return null; + const handleCloseAttempt = () => { + if (canClose) { + handleCloseModal(); + } + }; + + const progressPercentage = pickRandomUserSettings.preventCloseDelaySeconds > 0 + ? (remainingSeconds / pickRandomUserSettings.preventCloseDelaySeconds) * 100 + : 0; + return ( document.querySelector('#modals-container')} isOpen={showModal} - onRequestClose={handleCloseModal} + onRequestClose={handleCloseAttempt} + shouldCloseOnOverlayClick={canClose} + shouldCloseOnEsc={canClose} > { - handleCloseModal(); - }} + onClick={handleCloseAttempt} aria-label="Close button" + disabled={!canClose} + style={{ + opacity: canClose ? 1 : 0.5, + cursor: canClose ? 'pointer' : 'not-allowed', + }} > ) diff --git a/src/components/modal/hooks.ts b/src/components/modal/hooks.ts index 1b840d8..443b2df 100644 --- a/src/components/modal/hooks.ts +++ b/src/components/modal/hooks.ts @@ -1,4 +1,6 @@ -import { useEffect, useState } from 'react'; +import { + useEffect, useMemo, useRef, useState, +} from 'react'; import { CurrentUserData, DataChannelEntryResponseType, GraphqlResponseWrapper } from 'bigbluebutton-html-plugin-sdk'; import { PickedUserSeenEntryDataChannel, PickedUserWithEntryId } from '../pick-random-user/types'; import { PickRandomUserSettings } from '../../commons/types'; @@ -6,6 +8,9 @@ import { notifyRandomlyPickedUser, pingSoundForRandomlyPickedUser } from './util import { usePreviousValue } from '../../commons/hooks'; import { hasCurrentUserSeenPickedUser } from '../../commons/utils'; +const UPDATE_COUNTDOWN_RATE = 100; // milliseconds +const COUNTDOWN_RATE_IN_SECONDS = UPDATE_COUNTDOWN_RATE / 1000; + const useCurrentUserId = (currentUser: CurrentUserData) => { const [currentUserId, setCurrentUserId] = useState(currentUser?.userId || ''); @@ -84,3 +89,69 @@ export const useHandleCurrentUserNotification = ( currentPickedUser, ); }; + +export const usePreventCloseModalCountdown = ( + currentUser: CurrentUserData, + pickedUserSeenEntries: GraphqlResponseWrapper< + DataChannelEntryResponseType[]>, + currentPickedUser: PickedUserWithEntryId, + pickRandomUserSettings: PickRandomUserSettings, +) => { + const { preventCloseDelaySeconds } = pickRandomUserSettings; + + const [remainingSeconds, setRemainingSeconds] = useState(preventCloseDelaySeconds); + const [canClose, setCanClose] = useState(true); + const intervalInstanceRef = useRef(null); + + const currentUserSeenPickedUser: boolean = useMemo( + () => hasCurrentUserSeenPickedUser( + pickedUserSeenEntries, + currentUser.userId, + currentPickedUser?.pickedUser.userId, + ), + [pickedUserSeenEntries, currentPickedUser, preventCloseDelaySeconds], + ); + + const stopCountdown = (interval: NodeJS.Timeout) => { + intervalInstanceRef.current = null; + clearInterval(interval); + setCanClose(true); + }; + + const handleCountdownTick = (interval: NodeJS.Timeout) => { + setRemainingSeconds((prev) => { + const newValue = prev - COUNTDOWN_RATE_IN_SECONDS; + if (newValue >= 0) { + return newValue; + } + stopCountdown(interval); + return 0; + }); + }; + + const startCountdown = () => { + setCanClose(false); + const interval = setInterval(() => { + handleCountdownTick(interval); + }, UPDATE_COUNTDOWN_RATE); + intervalInstanceRef.current = interval; + }; + + const resetCountdown = () => { + setRemainingSeconds(preventCloseDelaySeconds); + if (intervalInstanceRef.current) { + clearInterval(intervalInstanceRef.current); + } + if (preventCloseDelaySeconds > 0) { + startCountdown(); + } + }; + + useEffect(() => { + if (!currentUserSeenPickedUser && currentPickedUser) { + resetCountdown(); + } else if (!currentPickedUser) stopCountdown(intervalInstanceRef.current); + }, [currentUserSeenPickedUser, currentPickedUser]); + + return { remainingSeconds, canClose }; +}; diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index cba664a..2825d2d 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { defineMessages } from 'react-intl'; import { PickedUserViewComponentProps } from './types'; import * as Styled from './styles'; @@ -26,8 +26,19 @@ const intlMessages = defineMessages({ description: 'Alternative text for avatar image', defaultMessage: 'Avatar image of user {0}', }, + modalCloseDelayMessage: { + id: 'pickRandomUserPlugin.modal.closeDelayMessage', + description: 'Message showing countdown before modal can be closed', + defaultMessage: 'You can close this modal in {seconds} seconds', + }, + modalCloseDelayMessageSingular: { + id: 'pickRandomUserPlugin.modal.closeDelayMessageSingular', + description: 'Message showing countdown before modal can be closed (singular)', + defaultMessage: 'You can close this modal in {seconds} second', + }, }); + export function PickedUserViewComponent(props: PickedUserViewComponentProps) { const { intl, @@ -36,8 +47,13 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { setShowPresenterView, pickedUserSeenEntries, pushPickedUserSeen, + remainingSeconds, + canClose, + progressPercentage, } = props; + + const handleBackToPresenterView = () => { if (currentUser?.presenter) { setShowPresenterView(true); @@ -65,6 +81,7 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { const avatarAltDescriptor = intl.formatMessage(intlMessages.currentUserPicked, { 0: pickedUserWithEntryId?.pickedUser?.name, }); + return ( {title} @@ -87,6 +104,16 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { ) : null } + {!canClose && remainingSeconds > 0 && !currentUser?.presenter && ( + + {intl.formatMessage( + remainingSeconds === 1 + ? intlMessages.modalCloseDelayMessageSingular + : intlMessages.modalCloseDelayMessage, + { seconds: Math.ceil(remainingSeconds) }, + )} + + )} { (currentUser?.presenter) ? ( @@ -94,6 +121,11 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { ) : null } + {!canClose && remainingSeconds > 0 && currentUser?.presenter && ( + + + + )} ); } diff --git a/src/components/modal/picked-user-view/styles.tsx b/src/components/modal/picked-user-view/styles.tsx index 3590791..8ab2364 100644 --- a/src/components/modal/picked-user-view/styles.tsx +++ b/src/components/modal/picked-user-view/styles.tsx @@ -64,6 +64,36 @@ const BackButton = styled.button` } `; +const CountdownMessage = styled.div` + width: 100%; + text-align: center; + padding: 0.75rem 1rem; + margin-top: 1rem; + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + color: #6c757d; + font-size: 0.9rem; + font-weight: 500; +`; + +const CountdownBarContainer = styled.div` + width: 100%; + height: 4px; + background-color: #e9ecef; + border-radius: 2px; + overflow: hidden; + margin-top: 1rem; +`; + +const CountdownBar = styled.div<{ progress: number }>` + height: 100%; + background: linear-gradient(90deg, #0F70D7 0%, #0C57A7 100%); + width: ${({ progress }) => progress}%; + transition: width linear 0.1s; + border-radius: 2px; +`; + export { PickedUserViewWrapper, PickedUserViewTitle, @@ -71,4 +101,7 @@ export { PickedUserAvatarImage, PickedUserName, BackButton, + CountdownMessage, + CountdownBarContainer, + CountdownBar, }; diff --git a/src/components/modal/picked-user-view/types.ts b/src/components/modal/picked-user-view/types.ts index 3252535..b195c17 100644 --- a/src/components/modal/picked-user-view/types.ts +++ b/src/components/modal/picked-user-view/types.ts @@ -15,6 +15,9 @@ export interface PickedUserViewComponentProps { DataChannelEntryResponseType[]>; pushPickedUserSeen: PushEntryFunction; setShowPresenterView: React.Dispatch>; + remainingSeconds: number; + canClose: boolean; + progressPercentage: number; } export interface ModalAvatarProps { diff --git a/src/components/modal/styles.tsx b/src/components/modal/styles.tsx index ab223e3..cfb9af3 100644 --- a/src/components/modal/styles.tsx +++ b/src/components/modal/styles.tsx @@ -76,7 +76,22 @@ const CloseButtonWrapper = styled.div` width: 100%; align-items: flex-end; display: flex; - flex-direction: column; + flex-direction: column; +`; + +const CountdownMessage = styled.div` + width: 100%; + text-align: center; + padding: 0.75rem 1rem; + margin-bottom: 1rem; + background-color: #f8f9fa; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + color: #6c757d; + font-size: 0.9rem; + font-weight: 500; `; -export { PluginModal, CloseButton, CloseButtonWrapper }; +export { + PluginModal, CloseButton, CloseButtonWrapper, CountdownMessage, +}; diff --git a/src/components/pick-random-user/hooks.ts b/src/components/pick-random-user/hooks.ts index f0c9636..f9796e9 100644 --- a/src/components/pick-random-user/hooks.ts +++ b/src/components/pick-random-user/hooks.ts @@ -8,7 +8,11 @@ import { } from 'bigbluebutton-html-plugin-sdk'; import { PluginSettingsData } from 'bigbluebutton-html-plugin-sdk/dist/cjs/data-consumption/domain/settings/plugin-settings/types'; import { useEffect, useState } from 'react'; -import { DEFAULT_PING_SOUND_URL, PICKED_USER_TIME_WINDOW } from '../../commons/constants'; +import { + DEFAULT_PING_SOUND_URL, + DEFAULT_PREVENT_CLOSE_DELAY_SECONDS, + PICKED_USER_TIME_WINDOW, +} from '../../commons/constants'; import { hasCurrentUserSeenPickedUser, isNumber } from '../../commons/utils'; import { PickRandomUserSettings } from '../../commons/types'; import { WindowClientSettings } from '../modal/types'; @@ -81,13 +85,26 @@ const getPingSoundUrl = (settings: PluginSettingsData): string => { return pingSoundUrl; }; +const getPreventCloseDelayFromSettings = (settings: PluginSettingsData) => { + const settingPreventCloseDelay = settings.preventCloseDelaySeconds as unknown; + if (isNumber(settingPreventCloseDelay)) { + const delay: number = settingPreventCloseDelay as number; + return delay; + } return DEFAULT_PREVENT_CLOSE_DELAY_SECONDS; +}; + export const useGetAllSettings = ( settingsData: GraphqlResponseWrapper, ): PickRandomUserSettings => { const [pingSoundEnabled, setPingSoundEnabled] = useState(true); const [browserNotificationEnabled, setBrowserNotificationEnabled] = useState(false); const [pingSoundUrl, setPingSoundUrl] = useState(DEFAULT_PING_SOUND_URL); - const [pickedUserTimeWindow, setPickedUserTimeWindow] = useState(PICKED_USER_TIME_WINDOW); + const [pickedUserTimeWindow, setPickedUserTimeWindow] = useState( + PICKED_USER_TIME_WINDOW, + ); + const [preventCloseDelaySeconds, setPreventCloseDelaySeconds] = useState( + DEFAULT_PREVENT_CLOSE_DELAY_SECONDS, + ); useSettingsLoaded((settings) => { setBrowserNotificationEnabled( (previousState) => getBrowserNotificationEnabled(settings, previousState), @@ -97,12 +114,14 @@ export const useGetAllSettings = ( ); setPickedUserTimeWindow(getPickedUserTimeWindowFromSettings(settings)); setPingSoundUrl(getPingSoundUrl(settings)); + setPreventCloseDelaySeconds(getPreventCloseDelayFromSettings(settings)); }, settingsData); return { pingSoundEnabled, pingSoundUrl, browserNotificationEnabled, pickedUserTimeWindow, + preventCloseDelaySeconds, }; }; From fd02c392d69771747e81ce4a1a13c10bb88fe0d2 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Mon, 17 Nov 2025 18:05:52 -0300 Subject: [PATCH 057/117] [countdown-close-modal] fix lint errors --- src/components/modal/component.tsx | 2 +- src/components/modal/picked-user-view/component.tsx | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 9e1a775..73a6766 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -47,7 +47,7 @@ export function PickUserModal(props: PickUserModalProps) { setShowPresenterView(currentUser?.presenter && !currentPickedUser); }, [currentUser, currentPickedUser]); - const {remainingSeconds, canClose} = usePreventCloseModalCountdown( + const { remainingSeconds, canClose } = usePreventCloseModalCountdown( currentUser, pickedUserSeenEntries, currentPickedUser, diff --git a/src/components/modal/picked-user-view/component.tsx b/src/components/modal/picked-user-view/component.tsx index 2825d2d..400ea5d 100644 --- a/src/components/modal/picked-user-view/component.tsx +++ b/src/components/modal/picked-user-view/component.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { useEffect, useState } from 'react'; +import { useEffect } from 'react'; import { defineMessages } from 'react-intl'; import { PickedUserViewComponentProps } from './types'; import * as Styled from './styles'; @@ -38,7 +38,6 @@ const intlMessages = defineMessages({ }, }); - export function PickedUserViewComponent(props: PickedUserViewComponentProps) { const { intl, @@ -52,8 +51,6 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { progressPercentage, } = props; - - const handleBackToPresenterView = () => { if (currentUser?.presenter) { setShowPresenterView(true); From 01a4e9b21de12cf1647d02e657ab9973308982da Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 19 Nov 2025 17:15:06 -0300 Subject: [PATCH 058/117] [add-manifest-version] added version control in github workflow --- .github/workflows/publish-tag.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-tag.yml b/.github/workflows/publish-tag.yml index 3f58930..09ffeff 100644 --- a/.github/workflows/publish-tag.yml +++ b/.github/workflows/publish-tag.yml @@ -34,11 +34,29 @@ jobs: git config user.name "${{ github.actor }}" git config user.email "${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com" - - - name: Bump version and push tag + - name: Bump version, update manifest, commit and tag run: | + set -euo pipefail + npm install - npm version ${{ github.event.inputs.release_type }} -m "chore: bump version to %s" + + # 1) Bump package.json + package-lock.json but DO NOT commit/tag yet + npm version ${{ github.event.inputs.release_type }} --no-git-tag-version + + # 2) Read the new version from package.json + NEW_VERSION=$(node -p "require('./package.json').version") + echo "New version is: $NEW_VERSION" + + # 3) Update manifest.json's "version" field to match + jq --arg v "$NEW_VERSION" '.version = $v' manifest.json > manifest.json.tmp + mv manifest.json.tmp manifest.json + + # 4) Commit everything and create an annotated tag + git add package.json package-lock.json manifest.json + git commit -m "chore: bump version to $NEW_VERSION" + git tag -a "v$NEW_VERSION" -m "v$NEW_VERSION" + + # 5) Push commit and tag git push --follow-tags env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 23a44d4f653d53c52f1089f72e965bcd36ad87fc Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 9 Jan 2026 08:31:16 -0300 Subject: [PATCH 059/117] [i-79] filtered out users with bot flag --- src/components/pick-random-user/queries.ts | 4 +++- src/components/pick-random-user/types.ts | 1 + src/components/pick-random-user/utils.ts | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/pick-random-user/queries.ts b/src/components/pick-random-user/queries.ts index a1c498c..45d35a9 100644 --- a/src/components/pick-random-user/queries.ts +++ b/src/components/pick-random-user/queries.ts @@ -1,12 +1,14 @@ +// TODO: Replace this query with the useUsersBasicInfo once it's merged in export const USERS_MORE_INFORMATION = ` subscription usersMoreInformation { - user { + user(where: { bot: { _eq: false } }) { color name userId role presenter avatar + bot } } `; diff --git a/src/components/pick-random-user/types.ts b/src/components/pick-random-user/types.ts index bc5ee58..f314959 100644 --- a/src/components/pick-random-user/types.ts +++ b/src/components/pick-random-user/types.ts @@ -5,6 +5,7 @@ export interface PickedUser { role: string; avatar: string; color: string; + bot: boolean; } export interface PickedUserWithEntryId { diff --git a/src/components/pick-random-user/utils.ts b/src/components/pick-random-user/utils.ts index b1053af..3ea3912 100644 --- a/src/components/pick-random-user/utils.ts +++ b/src/components/pick-random-user/utils.ts @@ -11,7 +11,7 @@ export const filterPossibleUsersToBePicked = ( pickedUserFromDataChannel: DataChannelEntryResponseType[], filterOptions: FilterOptionsType, ) => ({ - user: allUsers?.user.filter((user) => { + user: allUsers?.user.filter((user) => !user.bot).filter((user) => { if (!filterOptions.includeModerators) return user.role === Role.VIEWER; return true; }).filter((user) => { From e85062361aafa6340eb2f0ba82fc150dceed70f8 Mon Sep 17 00:00:00 2001 From: Guenther Schwienbacher Date: Mon, 16 Feb 2026 17:24:50 +0100 Subject: [PATCH 060/117] Add translation for moderators and presenter includes. Fixes #70 --- public/locales/de.json | 2 ++ public/locales/fr-FR.json | 2 ++ public/locales/it.json | 2 ++ public/locales/ja.json | 2 ++ public/locales/pt-BR.json | 2 ++ 5 files changed, 10 insertions(+) diff --git a/public/locales/de.json b/public/locales/de.json index d4aca6b..f9e7c1d 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -4,6 +4,8 @@ "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "zurück", "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Avatar von Teilnehmer {0}", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Optionen", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Moderatoren einbeziehen", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Präsentator einbeziehen", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Wähle bereits gewählte Teilnehmer", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Zur Auswahl stehen", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "Teilnehmer", diff --git a/public/locales/fr-FR.json b/public/locales/fr-FR.json index b817097..716933a 100644 --- a/public/locales/fr-FR.json +++ b/public/locales/fr-FR.json @@ -3,6 +3,8 @@ "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Participant sélectionné au hasard", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "retour", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Inclure les modérateurs", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Inclure le présentateur", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Inclure un participant déjà sélectionné", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponible pour la sélection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "participant", diff --git a/public/locales/it.json b/public/locales/it.json index 01084d5..4a5591c 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -3,6 +3,8 @@ "pickRandomUserPlugin.modal.pickedUserView.title.randomUserPicked": "Utente scelto a caso", "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "indietro", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opzioni", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Includi moderatori", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Includi il presentatore", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Includi l'utente già selezionato", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponibile per la selezione", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "utente", diff --git a/public/locales/ja.json b/public/locales/ja.json index 2454325..d21e691 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -4,6 +4,8 @@ "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "戻る", "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "ユーザー {0} のアバター画像", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "設定", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "モデレーターを含める", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "発表者を含める", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "既に指名した人を含める", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "指名可能な人", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "人のユーザー", diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json index 85bfd03..4cceabe 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -4,6 +4,8 @@ "pickRandomUserPlugin.modal.pickedUserView.backButton.label": "voltar", "pickRandomUserPlugin.modal.pickedUserView.avatarImage.alternativeText": "Iamgem avatar do usuário {0}", "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opções", + "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Incluir moderadores", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Incluir apresentador", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Incluir usuário já selecionado", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponíveis para seleção", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "usuário", From e64f99008e95a0fe1dc0249acb3bc7a76c951d81 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Wed, 4 Mar 2026 15:03:11 -0300 Subject: [PATCH 061/117] [close-button-enabled] enable back close button even when modal is in cant close state --- src/components/modal/component.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 96448f6..1b9b1e7 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -59,8 +59,8 @@ export function PickUserModal(props: PickUserModalProps) { if (!showModal) return null; - const handleCloseAttempt = () => { - if (canClose) { + const handleCloseAttempt = (isFromCloseButton: boolean = false) => { + if (canClose || isFromCloseButton) { handleCloseModal(); } }; @@ -83,13 +83,8 @@ export function PickUserModal(props: PickUserModalProps) { handleCloseAttempt(true)} aria-label="Close button" - disabled={!canClose} - style={{ - opacity: canClose ? 1 : 0.5, - cursor: canClose ? 'pointer' : 'not-allowed', - }} > Date: Wed, 4 Mar 2026 15:09:33 -0300 Subject: [PATCH 062/117] implement a better solution for that --- src/components/modal/component.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 1b9b1e7..b34be28 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -59,8 +59,8 @@ export function PickUserModal(props: PickUserModalProps) { if (!showModal) return null; - const handleCloseAttempt = (isFromCloseButton: boolean = false) => { - if (canClose || isFromCloseButton) { + const handleCloseAttempt = () => { + if (canClose) { handleCloseModal(); } }; @@ -83,7 +83,7 @@ export function PickUserModal(props: PickUserModalProps) { handleCloseAttempt(true)} + onClick={handleCloseModal} aria-label="Close button" > Date: Mon, 9 Mar 2026 14:41:34 -0300 Subject: [PATCH 063/117] [lock-countdown-change] Change lock countdown span to 1 second --- src/commons/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commons/constants.ts b/src/commons/constants.ts index b3a47b0..070b395 100644 --- a/src/commons/constants.ts +++ b/src/commons/constants.ts @@ -4,4 +4,4 @@ export const TIMEOUT_CLOSE_NOTIFICATION = 5000; export const DEFAULT_PING_SOUND_URL = 'resources/sounds/doorbell.mp3'; -export const DEFAULT_PREVENT_CLOSE_DELAY_SECONDS = 3; // seconds +export const DEFAULT_PREVENT_CLOSE_DELAY_SECONDS = 1; // seconds From 63c9c668d2a1c99f1feb3caeaec9f469487bbe98 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 10 Apr 2026 15:21:24 -0300 Subject: [PATCH 064/117] [automated-test-implementation] Implemented boilerplate testing --- .env.template | 23 + .gitignore | 4 + package-lock.json | 1284 ++++++++++------- package.json | 11 +- playwright.config.ts | 53 + scripts/copy-plugin-to-container.sh | 55 + .../action-button-dropdown/component.tsx | 2 + src/components/modal/component.tsx | 1 + .../modal/picked-user-view/component.tsx | 15 +- .../modal/presenter-view/component.tsx | 8 +- tests/README.md | 108 ++ tests/behavioral/fixture.ts | 92 ++ tests/behavioral/multi-user.spec.ts | 330 +++++ tests/behavioral/single-user.spec.ts | 200 +++ tests/core/constants.ts | 28 + tests/core/coreElements.ts | 22 + tests/core/fixtures/README.md | 47 + tests/core/fixtures/sampleBeforeAll.ts | 51 + tests/core/fixtures/sampleFixture.ts | 39 + tests/core/helpers.ts | 195 +++ tests/core/parameters.ts | 6 + tests/core/sample.ts | 28 + .../core/scripts/copy-sample-to-container.sh | 57 + tests/core/sessionPage.ts | 135 ++ tests/elements.ts | 38 + tests/structural/test.spec.ts | 108 ++ 26 files changed, 2444 insertions(+), 496 deletions(-) create mode 100644 .env.template create mode 100644 playwright.config.ts create mode 100755 scripts/copy-plugin-to-container.sh create mode 100644 tests/README.md create mode 100644 tests/behavioral/fixture.ts create mode 100644 tests/behavioral/multi-user.spec.ts create mode 100644 tests/behavioral/single-user.spec.ts create mode 100644 tests/core/constants.ts create mode 100644 tests/core/coreElements.ts create mode 100644 tests/core/fixtures/README.md create mode 100644 tests/core/fixtures/sampleBeforeAll.ts create mode 100644 tests/core/fixtures/sampleFixture.ts create mode 100644 tests/core/helpers.ts create mode 100644 tests/core/parameters.ts create mode 100644 tests/core/sample.ts create mode 100755 tests/core/scripts/copy-sample-to-container.sh create mode 100644 tests/core/sessionPage.ts create mode 100644 tests/elements.ts create mode 100644 tests/structural/test.spec.ts diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..268df93 --- /dev/null +++ b/.env.template @@ -0,0 +1,23 @@ +# ── Required ──────────────────────────────────────────────────────────────── +# Full URL of the BigBlueButton server API (must end with /bigbluebutton/) +BBB_URL="https://your.bbb.server/bigbluebutton/" + +# Shared secret of the BigBlueButton server +BBB_SECRET="yoursupersecretkey" + +# ── Optional ──────────────────────────────────────────────────────────────── +# Direct URL to the plugin manifest (skips the server-side availability check). +# Use this when the plugin is not deployed to the default BBB plugins path. +# Example: PICK_RANDOM_USER_PLUGIN_URL="https://your.bbb.server/plugins/pick-random-user-plugin/dist/manifest.json" +PICK_RANDOM_USER_PLUGIN_URL="" + +# Name of the local Docker container running BigBlueButton (only needed when +# deploying the built plugin into a local container with the copy script). +LOCAL_CONTAINER_NAME="bbb-local-container" + +# Multiply all Playwright timeouts by this factor. +# Defaults to 2 in CI and 1 locally when not set. +TIMEOUT_MULTIPLIER=1 + +# Set to "true" to enable CI mode (1 worker, retries, blob reporter). +CI="false" diff --git a/.gitignore b/.gitignore index ea80a77..0e33e3a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ debian/**/copyright debian/**/changelog.gz debian/**/md5sums debian/**/var/www/* +memory +playwright-report +test-results +.env diff --git a/package-lock.json b/package-lock.json index 806f3c2..06a1581 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,16 +27,20 @@ "@babel/core": "^7.21.8", "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", + "@playwright/test": "^1.51.1", "@types/node": "^20.4.4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@types/react-modal": "^3.16.0", + "@types/sha.js": "^2.4.4", "@types/styled-components": "^5.1.26", "@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/parser": "^6.2.0", + "axios": "^1.8.4", "babel-loader": "^9.1.2", "copy-webpack-plugin": "^12.0.2", "css-loader": "^6.7.4", + "dotenv": "^16.4.7", "eslint": "^8.45.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-base": "^15.0.0", @@ -48,13 +52,15 @@ "lint-staged": "11.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "sha.js": "^2.4.11", "style-loader": "^3.3.3", "ts-loader": "^9.4.3", "typescript": "^5.1.6", "watch": "^1.0.2", "webpack": "^5.95.0", "webpack-cli": "^5.1.1", - "webpack-dev-server": "^4.15.1" + "webpack-dev-server": "^4.15.1", + "xml2js": "^0.6.2" } }, "node_modules/@ampproject/remapping": { @@ -1840,9 +1846,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -1866,11 +1872,10 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2014,9 +2019,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -2024,11 +2029,10 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2091,11 +2095,10 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", - "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "dev": true, - "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25" @@ -2162,6 +2165,21 @@ "node": ">= 8" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -2312,12 +2330,31 @@ "@types/node": "*" } }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, - "license": "MIT" + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true }, "node_modules/@types/express": { "version": "4.17.21", @@ -2529,6 +2566,15 @@ "@types/send": "*" } }, + "node_modules/@types/sha.js": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/sha.js/-/sha.js-2.4.4.tgz", + "integrity": "sha512-Qukd+D6S2Hm0wLVt2Vh+/eWBIoUt+wF8jWjBsG4F8EFQRwKtYvtXCPcNl2OEUQ1R+eTr3xuSaBYUyM3WD1x/Qw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/sockjs": { "version": "0.3.36", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", @@ -2806,163 +2852,148 @@ "license": "ISC" }, "node_modules/@webassemblyjs/ast": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", - "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", - "dev": true, - "license": "MIT" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", - "dev": true, - "license": "MIT" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", - "dev": true, - "license": "MIT" + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", - "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.6", - "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", - "dev": true, - "license": "MIT" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", - "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/wasm-gen": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", - "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", "dev": true, - "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", - "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, - "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.6", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", - "dev": true, - "license": "MIT" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", - "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/helper-wasm-section": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-opt": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1", - "@webassemblyjs/wast-printer": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", - "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", - "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-buffer": "1.12.1", - "@webassemblyjs/wasm-gen": "1.12.1", - "@webassemblyjs/wasm-parser": "1.12.1" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", - "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", - "@webassemblyjs/helper-api-error": "1.11.6", - "@webassemblyjs/helper-wasm-bytecode": "1.11.6", - "@webassemblyjs/ieee754": "1.11.6", - "@webassemblyjs/leb128": "1.11.6", - "@webassemblyjs/utf8": "1.11.6" + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", - "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", "dev": true, - "license": "MIT", "dependencies": { - "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, @@ -3065,15 +3096,13 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" + "dev": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" + "dev": true }, "node_modules/accepts": { "version": "1.3.8", @@ -3090,11 +3119,10 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, - "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -3102,14 +3130,16 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, - "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, "peerDependencies": { - "acorn": "^8" + "acorn": "^8.14.0" } }, "node_modules/acorn-jsx": { @@ -3137,11 +3167,10 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3172,11 +3201,10 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3195,16 +3223,6 @@ "dev": true, "license": "MIT" }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -3490,6 +3508,12 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3515,6 +3539,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", + "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3614,6 +3649,17 @@ "dev": true, "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz", + "integrity": "sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -3649,24 +3695,23 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", "dev": true, - "license": "MIT", "dependencies": { - "bytes": "3.1.2", + "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", "type-is": "~1.6.18", - "unpipe": "1.0.0" + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8", @@ -3683,6 +3728,26 @@ "ms": "2.0.0" } }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -3690,6 +3755,15 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/bonjour-service": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", @@ -3702,9 +3776,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "dependencies": { "balanced-match": "^1.0.0" @@ -3735,9 +3809,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "funding": [ { "type": "opencollective", @@ -3752,12 +3826,12 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -3770,8 +3844,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/bytes": { "version": "3.1.2", @@ -3783,16 +3856,41 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", - "license": "MIT", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dependencies": { - "es-define-property": "^1.0.0", "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -3821,9 +3919,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001674", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001674.tgz", - "integrity": "sha512-jOsKlZVRnzfhLojb+Ykb+gyUSp9Xb57So+fAiFlLzzTKpqg8xxSav0e40c8/4F/v9N8QSvrRRaLeVzQbLqomYw==", + "version": "1.0.30001787", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz", + "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==", "funding": [ { "type": "opencollective", @@ -3837,8 +3935,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/chalk": { "version": "4.1.2", @@ -3990,6 +4087,18 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", @@ -4520,6 +4629,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4603,6 +4721,31 @@ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "license": "MIT" }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -4611,10 +4754,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.49", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.49.tgz", - "integrity": "sha512-ZXfs1Of8fDb6z7WEYZjXpgIRF6MEu8JdeGA0A40aZq6OQbS+eJpnnV49epZRna2DU/YsEjSQuGtQPPtvt6J65A==", - "license": "ISC" + "version": "1.5.334", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz", + "integrity": "sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -4634,14 +4776,13 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", - "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -4746,13 +4887,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -4813,18 +4950,15 @@ } }, "node_modules/es-module-lexer": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", - "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", - "dev": true, - "license": "MIT" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, - "license": "MIT", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dependencies": { "es-errors": "^1.3.0" }, @@ -4833,15 +4967,15 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, - "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -4879,7 +5013,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", "engines": { "node": ">=6" } @@ -5088,9 +5221,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -5121,11 +5254,10 @@ } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5174,9 +5306,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -5184,11 +5316,10 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5243,9 +5374,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -5266,11 +5397,10 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5355,9 +5485,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -5381,11 +5511,10 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5538,39 +5667,39 @@ "license": "BSD-3-Clause" }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", "dev": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", - "on-finished": "2.4.1", + "on-finished": "~2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", + "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", - "qs": "6.13.0", + "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", + "send": "~0.19.0", + "serve-static": "~1.16.2", "setprototypeof": "1.2.0", - "statuses": "2.0.1", + "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" @@ -5813,16 +5942,15 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true, - "license": "ISC" + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "dev": true, "funding": [ { @@ -5830,7 +5958,6 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -5841,12 +5968,33 @@ } }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", - "license": "MIT", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, "node_modules/forwarded": { @@ -5944,16 +6092,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", - "license": "MIT", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -5969,6 +6121,18 @@ "dev": true, "license": "ISC" }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -6039,13 +6203,12 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" + "dev": true }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "dependencies": { "balanced-match": "^1.0.0", @@ -6053,11 +6216,10 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -6113,12 +6275,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6204,6 +6365,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6213,10 +6375,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "license": "MIT", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -6421,7 +6582,6 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, - "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -7068,13 +7228,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, - "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -7182,7 +7341,6 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -7197,7 +7355,6 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -7215,11 +7372,10 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -7455,13 +7611,16 @@ "license": "MIT" }, "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/loaders.css": { @@ -7487,10 +7646,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -7615,6 +7773,14 @@ "lz-string": "bin/bin.js" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -7838,20 +8004,18 @@ "license": "MIT" }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", - "license": "MIT" + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -7886,10 +8050,9 @@ } }, "node_modules/object-inspect": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", - "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", - "license": "MIT", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "engines": { "node": ">= 0.4" }, @@ -8284,9 +8447,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "dev": true }, "node_modules/path-type": { @@ -8306,10 +8469,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "engines": { "node": ">=8.6" }, @@ -8421,6 +8583,50 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/please-upgrade-node": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz", @@ -8661,6 +8867,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8672,13 +8887,12 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dev": true, - "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -8729,21 +8943,49 @@ } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, - "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.8" } }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -9264,8 +9506,16 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, - "license": "MIT" + "engines": { + "node": ">=11.0.0" + } }, "node_modules/scheduler": { "version": "0.23.2", @@ -9277,11 +9527,10 @@ } }, "node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, - "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -9289,7 +9538,7 @@ "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 10.13.0" }, "funding": { "type": "opencollective", @@ -9297,11 +9546,10 @@ } }, "node_modules/schema-utils/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -9573,6 +9821,26 @@ "dev": true, "license": "ISC" }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -9635,15 +9903,65 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", - "license": "MIT", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -9721,7 +10039,6 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, - "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -9732,7 +10049,6 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -10102,24 +10418,26 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/terser": { - "version": "5.36.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", - "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -10131,17 +10449,15 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.10", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", - "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", "dev": true, - "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.20", + "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", - "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.1", - "terser": "^5.26.0" + "schema-utils": "^4.3.0", + "terser": "^5.31.1" }, "engines": { "node": ">= 10.13.0" @@ -10165,31 +10481,11 @@ } } }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/text-table": { "version": "0.2.0", @@ -10218,6 +10514,20 @@ "integrity": "sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==", "license": "MIT" }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -10373,15 +10683,14 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, - "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -10554,9 +10863,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -10571,10 +10880,9 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -10672,11 +10980,10 @@ } }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", "dev": true, - "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -10696,35 +11003,36 @@ } }, "node_modules/webpack": { - "version": "5.95.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz", - "integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.5", - "@webassemblyjs/ast": "^1.12.1", - "@webassemblyjs/wasm-edit": "^1.12.1", - "@webassemblyjs/wasm-parser": "^1.12.1", - "acorn": "^8.7.1", - "acorn-import-attributes": "^1.9.5", - "browserslist": "^4.21.10", + "version": "5.106.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.0.tgz", + "integrity": "sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.1", - "es-module-lexer": "^1.2.1", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", + "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", - "watchpack": "^2.4.1", - "webpack-sources": "^3.2.3" + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" }, "bin": { "webpack": "bin/webpack.js" @@ -10919,11 +11227,10 @@ } }, "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -10952,25 +11259,6 @@ "node": ">=4.0" } }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -11074,15 +11362,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", - "license": "MIT", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -11156,6 +11445,28 @@ } } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -11163,11 +11474,10 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", "dev": true, - "license": "ISC", "engines": { "node": ">= 6" } diff --git a/package.json b/package.json index 4ce1f46..5cd2285 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,10 @@ "build:watch": "rm -rf dist && tsc -w --module CommonJS", "lint": "eslint ./src/*", "lint:fix": "npm run lint -- --fix", - "lint:watch": "watch 'yarn lint'" + "lint:watch": "watch 'yarn lint'", + "publish-plugin:dev": "bash scripts/copy-plugin-to-container.sh", + "test": "npx playwright test", + "test-chromium-ci": "export CI='true' && npx playwright test --project=chromium" }, "browserslist": { "production": [ @@ -40,6 +43,12 @@ ] }, "devDependencies": { + "@playwright/test": "^1.51.1", + "@types/sha.js": "^2.4.4", + "sha.js": "^2.4.11", + "dotenv": "^16.4.7", + "axios": "^1.8.4", + "xml2js": "^0.6.2", "@babel/core": "^7.21.8", "@babel/preset-env": "^7.21.5", "@babel/preset-react": "^7.18.6", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..49bf569 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,53 @@ +import { defineConfig, devices } from '@playwright/test'; +import { CI, ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from './tests/core/constants'; +import { server } from './tests/core/parameters'; + +export default defineConfig({ + testDir: process.cwd(), + workers: CI ? 1 : undefined, + retries: CI ? 1 : 0, + fullyParallel: true, + forbidOnly: CI, + reporter: CI + ? [['list'], ['blob']] + : [['list'], ['html', { open: 'never' }]], + use: { + baseURL: server, + headless: true, + trace: 'on', + screenshot: 'on', + video: CI ? 'retain-on-failure' : 'on', + actionTimeout: ELEMENT_WAIT_LONGER_TIME, + viewport: { width: 1280, height: 720 }, + launchOptions: { + slowMo: 0, + }, + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + launchOptions: { + args: [ + '--no-sandbox', + '--ignore-certificate-errors', + '--use-fake-ui-for-media-stream', + '--use-fake-device-for-media-stream', + '--allow-file-access-from-files', + ], + }, + }, + }, + ], + expect: { + toHaveScreenshot: { + maxDiffPixelRatio: 0.05, + }, + toMatchSnapshot: { + maxDiffPixelRatio: 0.05, + }, + timeout: ELEMENT_WAIT_TIME, + }, +}); diff --git a/scripts/copy-plugin-to-container.sh b/scripts/copy-plugin-to-container.sh new file mode 100755 index 0000000..93462ea --- /dev/null +++ b/scripts/copy-plugin-to-container.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# Copies the built plugin (dist/) into the running BBB Docker container. +# Usage: ./scripts/copy-plugin-to-container.sh [containerName] +# +# containerName defaults to $LOCAL_CONTAINER_NAME from .env when omitted. + +set -e + +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") +PLUGIN_ROOT="$SCRIPT_DIR/.." + +# Load .env from the plugin root +ENV_FILE="$PLUGIN_ROOT/.env" +if [ -f "$ENV_FILE" ]; then + set -o allexport + source "$ENV_FILE" + set +o allexport +fi + +PLUGIN_NAME="pick-random-user-plugin" +CONTAINER_NAME=${1:-$LOCAL_CONTAINER_NAME} + +if [ -z "$CONTAINER_NAME" ]; then + echo "Container name is not provided. Pass it as an argument or set LOCAL_CONTAINER_NAME in .env." + exit 1 +fi + +PLUGINS_FOLDER_CONTAINER_PATH="/var/www/bigbluebutton-default/assets/plugins" +PLUGIN_CONTAINER_PATH="$PLUGINS_FOLDER_CONTAINER_PATH/$PLUGIN_NAME" + +# Verify the container is running +if ! docker ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then + echo "Container '$CONTAINER_NAME' is not running. Exiting." + exit 1 +fi + +# Verify the dist/ folder exists locally +if [ ! -d "$PLUGIN_ROOT/dist" ]; then + echo "dist/ folder not found. Run 'npm run build-bundle' first." + exit 1 +fi + +# Remove stale files in the container and recreate the target directory +if docker exec "$CONTAINER_NAME" [ -d "$PLUGIN_CONTAINER_PATH" ]; then + docker exec "$CONTAINER_NAME" rm -rf "$PLUGIN_CONTAINER_PATH" +fi + +echo "Creating container path: $PLUGIN_CONTAINER_PATH ..." +docker exec "$CONTAINER_NAME" mkdir -p "$PLUGIN_CONTAINER_PATH" + +echo "Copying dist/ to $CONTAINER_NAME:$PLUGIN_CONTAINER_PATH/dist ..." +docker cp "$PLUGIN_ROOT/dist/." "$CONTAINER_NAME:$PLUGIN_CONTAINER_PATH/dist" + +echo "Done. Plugin deployed to $CONTAINER_NAME at $PLUGIN_CONTAINER_PATH/dist" diff --git a/src/components/extensible-areas/action-button-dropdown/component.tsx b/src/components/extensible-areas/action-button-dropdown/component.tsx index 6263e0a..0627a12 100644 --- a/src/components/extensible-areas/action-button-dropdown/component.tsx +++ b/src/components/extensible-areas/action-button-dropdown/component.tsx @@ -35,6 +35,7 @@ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): R new ActionButtonDropdownOption({ label: intl.formatMessage(intlMessages.pickUserLabel), icon: 'user', + dataTest: 'actionDropdownButtonPlugin', tooltip: '', allowed: true, onClick: () => { @@ -49,6 +50,7 @@ function ActionButtonDropdownManager(props: ActionButtonDropdownManagerProps): R label: intl.formatMessage(intlMessages.viewLastPickedUserLabel), icon: 'user', tooltip: '', + dataTest: 'displayLastRandomlyPickedUser', allowed: true, onClick: () => { setShowModal(true); diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index b34be28..9d91194 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -85,6 +85,7 @@ export function PickUserModal(props: PickUserModalProps) { type="button" onClick={handleCloseModal} aria-label="Close button" + data-test="pickRandomUserModalCloseButton" > - {title} + {title} { (pickedUserWithEntryId) ? ( <> @@ -97,12 +97,14 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { {pickedUserWithEntryId?.pickedUser?.name.slice(0, 2)} )} - {pickedUserWithEntryId?.pickedUser?.name} + {pickedUserWithEntryId?.pickedUser?.name} ) : null } {!canClose && remainingSeconds > 0 && !currentUser?.presenter && ( - + {intl.formatMessage( remainingSeconds === 1 ? intlMessages.modalCloseDelayMessageSingular @@ -113,14 +115,17 @@ export function PickedUserViewComponent(props: PickedUserViewComponentProps) { )} { (currentUser?.presenter) ? ( - + {intl.formatMessage(intlMessages.backButtonLabel)} ) : null } {!canClose && remainingSeconds > 0 && currentUser?.presenter && ( - + )} diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index 18b9adf..b771eab 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -211,7 +211,7 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { {intl.formatMessage(intlMessages.availableTitle)} - + {`${users?.length} ${userRoleLabel}: `} {makeHorizontalListOfNames(users)} @@ -223,6 +223,7 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { { deletionFunction([RESET_DATA_CHANNEL]); }} @@ -231,7 +232,7 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { - + { makeVerticalListOfNames(dataChannelPickedUsers) } @@ -242,6 +243,7 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { users?.length > 0 ? ( { handlePickRandomUser(); }} @@ -253,7 +255,7 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { } ) : ( -

      +

      {intl.formatMessage(intlMessages.noUsersWarning, { 0: userRoleLabel })}

      ) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..193358e --- /dev/null +++ b/tests/README.md @@ -0,0 +1,108 @@ +# Pick Random User Plugin – Automated Tests + +End-to-end tests for the **Pick Random User Plugin** written with [Playwright](https://playwright.dev/). +They reuse the shared test infrastructure from the +[BigBlueButton HTML Plugin SDK](https://github.com/bigbluebutton/bigbluebutton-html-plugin-sdk). + +--- + +## Test scenarios + +### Structural (`tests/structural/test.spec.ts`) + +Verify that the plugin renders the correct elements with the correct labels and +initial state. + +### Behavioural – single user (`tests/behavioral/single-user.spec.ts`) + +Verify the pick-and-track workflow using only the moderator/presenter. + +### Behavioural – multi-user (`tests/behavioral/multi-user.spec.ts`) + +Verify real-time data-channel synchronization between the presenter and an +attendee in the same meeting. + +--- + +## How to run the tests + +### 1 – Install dependencies + +From the **plugin root** (`plugin-pick-random-user/`): + +```bash +npm install +npx playwright install --with-deps chromium +``` + +The SDK test utilities are resolved via relative paths to the sibling +`bigbluebutton-html-plugin-sdk/` directory: + +```bash +cd ../bigbluebutton-html-plugin-sdk && npm install && cd - +``` + +### 2 – Configure environment variables + +```bash +cp .env.template .env +# edit .env and set BBB_URL and BBB_SECRET +``` + +| Variable | Required | Description | +|----------|----------|-------------| +| `BBB_URL` | **yes** | Full API URL, e.g. `https://bbb.example.com/bigbluebutton/` | +| `BBB_SECRET` | **yes** | Shared secret of the BBB server | +| `PICK_RANDOM_USER_PLUGIN_URL` | no | Direct URL to `manifest.json`; auto-detected from server otherwise | +| `LOCAL_CONTAINER_NAME` | no | Docker container name for the local deployment script | +| `TIMEOUT_MULTIPLIER` | no | Multiply all timeouts (default 1 locally, 2 in CI) | +| `CI` | no | `"true"` enables CI reporter and single-worker mode | + +### 3 – Build and deploy the plugin + +To build the plugin use the command: + +```bash +npm run build-bundle +``` + +Then you need to deploy the plugin serve it so that the target BBB server can access it. You can deploy it to a S3-like environment or to your own server that you are targeting the test to. For the second option, we have a command to do that already: + +```bash +npm run publish-plugin:dev +``` + +--- + +## Running the tests + +```bash +# All suites +npm test + +# Only structural tests +npm test -- tests/structural + +# Only behavioural tests +npm test -- tests/behavioral + +# Only multi-user behavioural tests +npm test -- tests/behavioral/multi-user + +# A single test by name +npm test -- -g "should show the same picked user name" + +# View the HTML report after a run +npx playwright show-report +``` + +--- + +## Test output + +| Artifact | Location | +|----------|----------| +| HTML report | `playwright-report/index.html` | +| Traces | Attached to every test in the HTML report | +| Screenshots | Captured for every test | +| Video | Every test locally; failure-only in CI | diff --git a/tests/behavioral/fixture.ts b/tests/behavioral/fixture.ts new file mode 100644 index 0000000..c5a85b9 --- /dev/null +++ b/tests/behavioral/fixture.ts @@ -0,0 +1,92 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { test as base } from '@playwright/test'; +import { Sample } from '../core/sample'; +import { SessionPage } from '../core/sessionPage'; +import { + encodeCustomParams, + getJoinURL, + generateSettingsData, +} from '../core/helpers'; +import { ELEMENT_WAIT_EXTRA_LONG_TIME } from '../core/constants'; + +export interface MultiUserTestFixtures { + multiUserTest: { + modPage: SessionPage; + attendeePage: SessionPage; + }; +} + +export type MultiUserTestConfig = { + envVarName: string; + getPluginUrl?: () => string | undefined; +}; + +/** + * Creates a Playwright test instance whose `multiUserTest` fixture spins up + * two independent browser contexts: one moderator (also presenter) and one + * attendee, both joined into the same BBB meeting with the plugin active. + */ +export function createMultiUserTest(config: MultiUserTestConfig) { + let pluginUrl: string | undefined = process.env[config.envVarName]; + + const test = base.extend({ + multiUserTest: async ({ browser }, use) => { + const customUrl = config.getPluginUrl?.() || pluginUrl; + if (!customUrl) { + throw new Error( + `Plugin URL is not set. Set ${config.envVarName} or ensure beforeAll ran successfully.`, + ); + } + + const createParameter = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: customUrl }])}`, + ); + + // ── Moderator / presenter ──────────────────────────────────────────── + const modContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const modRawPage = await modContext.newPage(); + const sample = new Sample({ browser, context: modContext }); + await sample.initModPage(modRawPage, { createParameter }); + const { modPage } = sample; + + // ── Attendee (viewer) ──────────────────────────────────────────────── + // Joins the same meeting already created by the moderator. + const attendeeContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const attendeeRawPage = await attendeeContext.newPage(); + const attendeePage = new SessionPage({ browser, page: attendeeRawPage }); + + const joinUrl = getJoinURL({ + meetingID: modPage.meetingId, + isModerator: false, + skipSessionDetailsModal: true, + }); + await attendeeRawPage.goto(joinUrl); + await attendeeRawPage.waitForSelector('div#layout', { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME }); + attendeePage.settings = await generateSettingsData(attendeeRawPage); + if (attendeePage.settings?.autoJoinAudioModal) { + await attendeePage.closeAudioModal(); + } + await attendeeRawPage.addStyleTag({ + content: "body { font-family: 'Liberation Sans', Arial, sans-serif; }", + }); + attendeePage.meetingId = modPage.meetingId; + + await use({ modPage, attendeePage }); + + await modContext.close(); + await attendeeContext.close(); + }, + }); + + return { + test, + setPluginUrl: (url: string) => { pluginUrl = url; }, + getPluginUrl: () => pluginUrl, + }; +} diff --git a/tests/behavioral/multi-user.spec.ts b/tests/behavioral/multi-user.spec.ts new file mode 100644 index 0000000..7c1b6c6 --- /dev/null +++ b/tests/behavioral/multi-user.spec.ts @@ -0,0 +1,330 @@ +/** + * Behavioural tests – multi-user (moderator/presenter + attendee/viewer). + * + * These tests verify data-channel synchronisation and cross-user UX: + * - Both participants see the SAME picked user name. + * - The attendee's modal opens automatically when they are picked. + * - Title text differs correctly per role (presenter vs. picked viewer). + * - Countdown message is shown only to the viewer, not the presenter. + * - The "Display last randomly picked user" option appears in the attendee's + * actions dropdown after a pick. + * + * Setup: two independent browser contexts joined into the same BBB meeting. + * With default filters (includeModerators=false, includePresenter=false) the + * attendee is the only eligible user, so every pick targets them deterministically. + */ +import { wait } from '@testing-library/user-event/dist/utils'; +import { checkPluginAvailability } from '../core/fixtures/sampleBeforeAll'; +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; +import { createMultiUserTest } from './fixture'; +import { elements as e } from '../elements'; +import { SessionPage as Page } from '../core/sessionPage'; + +const PLUGIN_NAME = 'pick-random-user-plugin'; +const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; + +const { test, setPluginUrl, getPluginUrl } = createMultiUserTest({ + envVarName: ENV_VAR_NAME, + getPluginUrl: () => process.env[ENV_VAR_NAME], +}); + +/** Open the "Pick random user" modal from the actions dropdown. */ +async function openModal(modPage: Page): Promise { + await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); + await modPage.page.click(e.actions); + await modPage.hasElement(e.pickRandomUserActionButton, 'action button should appear'); + await modPage.page.click(e.pickRandomUserActionButton); +} + +/** + * Wait for the attendee's page to finish loading the whiteboard. + * This ensures the attendee is fully in the meeting before the presenter picks. + */ +async function waitForAttendeeMeeting(attendeePage: Page): Promise { + await attendeePage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); +} + +/** Click the back button on the presenter's picked-user view to return to the presenter view. */ +async function goBackToPresenterView(modPage: Page): Promise { + await modPage.hasElement(e.pickRandomUserBackButton, 'back button should be visible'); + await modPage.page.click(e.pickRandomUserBackButton); + await modPage.hasElement( + e.includeModeratorsCheckbox, + 'presenter view should be restored after clicking back', + ELEMENT_WAIT_TIME, + ); +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +test.describe.parallel('Pick Random User Plugin - Behavioural (multi-user)', () => { + test.beforeAll(checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })); + + test('should show the same picked user name on both the presenter page and the attendee page', async ({ multiUserTest }): Promise => { + const { modPage, attendeePage } = multiUserTest; + + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + + // With default filters the attendee is the only eligible viewer. + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should be visible: there is 1 eligible viewer (the attendee)', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Pick the user. + await modPage.page.click(e.pickRandomUserPickButton); + + // Presenter page: transition to picked-user view. + await modPage.hasElement( + e.pickRandomUserPickedUserName, + 'presenter page should show the picked user name', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Attendee page: modal opens automatically. + await attendeePage.hasElement( + e.pickRandomUserPickedUserName, + 'attendee page should show the same picked user name (data channel sync)', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Both pages must display the exact same name text. + const presenterName = await modPage.getLocator(e.pickRandomUserPickedUserName).textContent(); + const attendeeName = await attendeePage + .getLocator(e.pickRandomUserPickedUserName).textContent(); + + test.expect( + presenterName, + 'picked user name on presenter page should not be empty', + ).toBeTruthy(); + test.expect( + attendeeName, + 'picked user name shown to attendee must match the name shown to the presenter', + ).toBe(presenterName); + + wait(1000); + }); + + test('should open the attendee modal automatically without any action on the attendee side', async ({ multiUserTest }): Promise => { + const { modPage, attendeePage } = multiUserTest; + + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + + // Pick – the attendee has not interacted with their page at all. + await modPage.page.click(e.pickRandomUserPickButton); + + // The attendee's modal should open driven purely by the data-channel push. + await attendeePage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'attendee modal should open automatically via data channel after the presenter picks', + ELEMENT_WAIT_LONGER_TIME, + ); + }); + + test('should show "You have been randomly picked" to the picked attendee and "Randomly picked user" to the presenter', async ({ multiUserTest }): Promise => { + const { modPage, attendeePage } = multiUserTest; + + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + await modPage.page.click(e.pickRandomUserPickButton); + + // Attendee is the picked user → sees "You have been randomly picked". + await attendeePage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'attendee modal should open', + ELEMENT_WAIT_LONGER_TIME, + ); + await attendeePage.hasText( + e.pickRandomUserPickedUserViewTitle, + 'You have been randomly picked', + 'picked attendee should see the "You have been randomly picked" title', + ); + + // Presenter sees "Randomly picked user" (different user was picked). + await modPage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'presenter modal should transition to picked-user view', + ELEMENT_WAIT_LONGER_TIME, + ); + await modPage.hasText( + e.pickRandomUserPickedUserViewTitle, + 'Randomly picked user', + 'presenter should see the "Randomly picked user" title (not their own name)', + ); + }); + + test('should show a countdown message only to the attendee and a countdown bar only to the presenter', async ({ multiUserTest }): Promise => { + const { modPage, attendeePage } = multiUserTest; + + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + await modPage.page.click(e.pickRandomUserPickButton); + + await modPage.hasElement(e.pickRandomUserPickedUserViewTitle, 'presenter view should open', ELEMENT_WAIT_LONGER_TIME); + await attendeePage.hasElement(e.pickRandomUserPickedUserViewTitle, 'attendee view should open', ELEMENT_WAIT_LONGER_TIME); + + const attendeeCountdown = attendeePage.getLocator(e.pickRandomUserCountDownMessage); + await test.expect( + attendeeCountdown, + 'attendee should see the countdown message during the prevent-close delay', + ).toBeVisible({ timeout: ELEMENT_WAIT_TIME }); + + const presenterCountdown = modPage.getLocator(e.pickRandomUserCountDownMessage); + await test.expect( + presenterCountdown, + 'presenter should NOT see the viewer-side countdown message', + ).toBeHidden({ timeout: ELEMENT_WAIT_TIME }); + + const presenterProgressBar = modPage.getLocator(e.pickRandomUserCountDownProgressBar); + await test.expect( + presenterProgressBar, + 'presenter should see the progress countdown bar', + ).toBeVisible({ timeout: ELEMENT_WAIT_TIME }); + }); + + test('should not close the picked attendee modal when clicking the overlay during the countdown lock period', async ({ multiUserTest }): Promise => { + const { modPage, attendeePage } = multiUserTest; + + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + + // Pick the attendee – the countdown lock starts on the attendee side immediately. + await modPage.page.click(e.pickRandomUserPickButton); + + // Wait for the attendee's modal to open via data-channel sync. + await attendeePage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'attendee modal should open after being picked', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Click the modal overlay at the top-left corner (well outside the centred modal + // content) while the countdown is still active (shouldCloseOnOverlayClick === false). + await attendeePage.page.locator(e.pickRandomUserModalOverlay).click({ + position: { x: 5, y: 5 }, + force: true, + }); + + // The modal must still be open – the overlay click should have been swallowed. + await test.expect( + attendeePage.getLocator(e.pickRandomUserPickedUserViewTitle), + 'modal should remain open after clicking the overlay during the countdown lock period', + ).toBeVisible({ timeout: ELEMENT_WAIT_TIME }); + }); + + test('should keep the previously-picked viewer in the available pool and re-pick the same user when "include already picked users" is enabled and the presenter navigates back', async ({ multiUserTest }): Promise => { + const { modPage, attendeePage } = multiUserTest; + + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + + // Enable "Include already picked users" so the viewer stays eligible after being picked. + await modPage.page.click(e.includePickedUsersCheckbox); + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should be visible once the attendee is an eligible viewer', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Pick the attendee (the only eligible viewer with default filters). + await modPage.page.click(e.pickRandomUserPickButton); + await modPage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'presenter should transition to the picked-user view', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Capture the name of the first picked user for later comparison. + const firstPickedName = await modPage.getLocator(e.pickRandomUserPickedUserName).textContent(); + test.expect(firstPickedName, 'first picked user name should not be empty').toBeTruthy(); + + // Return to the presenter view via the back button. + await goBackToPresenterView(modPage); + + // ── Assertion 1: attendee appears in the "Previously picked" list ──────── + const pickedList = modPage.getLocator(`${e.pickRandomUserPreviouslyPickedList} li`); + await pickedList.first().waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + const pickedListCount = await pickedList.count(); + test.expect( + pickedListCount, + 'previously-picked list must contain at least one entry – the attendee was picked', + ).toBeGreaterThanOrEqual(1); + + // ── Assertion 2: picked user is still in the available-for-selection pool ─ + // With includePickedUsers=true the attendee is not removed from the eligible pool. + await modPage.hasText( + e.pickRandomUserAvailableContent, + '1 viewer', + 'available section should still list the attendee as eligible (includePickedUsers is ON)', + ); + await modPage.hasText( + e.pickRandomUserPickButton, + 'Pick again', + 'pick button should read "Pick again" because a user has already been picked this session', + ); + + // ── Assertion 3: re-picking selects the same (and only) eligible user ───── + await modPage.page.click(e.pickRandomUserPickButton); + await modPage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'presenter should transition to picked-user view after re-picking', + ELEMENT_WAIT_LONGER_TIME, + ); + const secondPickedName = await modPage.getLocator(e.pickRandomUserPickedUserName).textContent(); + test.expect( + secondPickedName, + 'the same user must be picked again - they are the only eligible viewer in the pool', + ).toBe(firstPickedName); + }); + + test('should inject "Display last randomly picked user" into the attendee actions dropdown after a pick', async ({ multiUserTest }): Promise => { + const { modPage, attendeePage } = multiUserTest; + + await waitForAttendeeMeeting(attendeePage); + await openModal(modPage); + await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); + await modPage.page.click(e.pickRandomUserPickButton); + + // Wait for the data channel to sync on the attendee side. + await attendeePage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'attendee modal should open (confirms data channel received the pick)', + ELEMENT_WAIT_LONGER_TIME, + ); + + // Close the attendee modal so the actions dropdown is accessible. + await attendeePage.page.click(e.pickRandomUserModalCloseButton); + await attendeePage.wasRemoved( + e.pickRandomUserPickedUserViewTitle, + 'attendee modal should close after clicking the close button', + ELEMENT_WAIT_TIME, + ); + + // Open the attendee's actions dropdown. + await attendeePage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); + await attendeePage.page.click(e.actions); + + // The plugin injects "Display last randomly picked user" for attendees + await attendeePage.hasElement( + e.displayLastRandomlyPickedUser, + 'attendee actions dropdown should contain the plugin option', + ); + await attendeePage.hasText( + e.displayLastRandomlyPickedUser, + 'Display last randomly picked user', + 'plugin option should read "Display last randomly picked user" for the attendee', + ); + }); +}); diff --git a/tests/behavioral/single-user.spec.ts b/tests/behavioral/single-user.spec.ts new file mode 100644 index 0000000..40e7591 --- /dev/null +++ b/tests/behavioral/single-user.spec.ts @@ -0,0 +1,200 @@ +/** + * Behavioural tests – single user (moderator / presenter only). + * + * These tests exercise the pick-and-track workflow: picking a user, verifying + * the "Previously picked" list, and exercising the Clear All and filter + * interactions that affect the eligible-user count. + * + * Setup: one BBB meeting, one participant (moderator who is also presenter). + * The "Include moderators" and "Include presenter" filters are always enabled + * so the presenter can pick themselves. + */ +// eslint-disable-next-line import/no-extraneous-dependencies +import { createSampleTest } from '../core/fixtures/sampleFixture'; +import { checkPluginAvailability } from '../core/fixtures/sampleBeforeAll'; +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; +import { elements as e } from '../elements'; +import { SessionPage as ModPage } from '../core/sessionPage'; + +const PLUGIN_NAME = 'pick-random-user-plugin'; +const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; + +const { test, setPluginUrl, getPluginUrl } = createSampleTest({ + envVarName: ENV_VAR_NAME, + getPluginUrl: () => process.env[ENV_VAR_NAME], +}); + +async function openModal(modPage: ModPage): Promise { + await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); + await modPage.page.click(e.actions); + await modPage.hasElement(e.pickRandomUserActionButton, 'action button should appear'); + await modPage.page.click(e.pickRandomUserActionButton); +} + +/** Enable both inclusion filters so the single moderator/presenter is eligible. */ +async function enableInclusionFilters(modPage: ModPage): Promise { + await modPage.page.click(e.includeModeratorsCheckbox); + await modPage.page.click(e.includePresenterCheckbox); + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should appear after enabling both inclusion filters', + ELEMENT_WAIT_LONGER_TIME, + ); +} + +/** Enable all three filters (include picked users too, so "Pick again" is reachable). */ +async function enableAllFilters(modPage: ModPage): Promise { + await modPage.page.click(e.includeModeratorsCheckbox); + await modPage.page.click(e.includePresenterCheckbox); + await modPage.page.click(e.includePickedUsersCheckbox); + await modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should appear after enabling all filters', + ELEMENT_WAIT_LONGER_TIME, + ); +} + +/** Pick a user and wait for the picked-user view to appear. */ +async function pickUser(modPage: ModPage): Promise { + await modPage.page.click(e.pickRandomUserPickButton); + await modPage.hasElement( + e.pickRandomUserPickedUserViewTitle, + 'picked-user view should appear after clicking pick', + ELEMENT_WAIT_LONGER_TIME, + ); +} + +/** Click the back button to return from picked-user view to presenter view. */ +async function goBackToPresenterView(modPage: ModPage): Promise { + await modPage.hasElement(e.pickRandomUserBackButton, 'back button should be visible'); + await modPage.page.click(e.pickRandomUserBackButton); + await modPage.hasElement( + e.includeModeratorsCheckbox, + 'presenter view should be restored after clicking back', + ELEMENT_WAIT_TIME, + ); +} + +// ── Tests ───────────────────────────────────────────────────────────────────── +test.describe.parallel('Pick Random User Plugin – Behavioural (single user)', () => { + test.beforeAll(checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })); + + test('should show "Pick again" button (not "Pick user") after navigating back from picked-user view', async ({ sampleTest }): Promise => { + // With "Include already picked users" ON the presenter stays in the pool + // after being picked, so the pick button remains visible on return. + await openModal(sampleTest.modPage); + await enableAllFilters(sampleTest.modPage); + await pickUser(sampleTest.modPage); + await goBackToPresenterView(sampleTest.modPage); + + await sampleTest.modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should still be visible (includePickedUsers is enabled)', + ); + await sampleTest.modPage.hasText( + e.pickRandomUserPickButton, + 'Pick again', + 'button label should read "Pick again" after a user has already been picked', + ); + }); + + test('should list the picked user in the "Previously picked" section after picking', async ({ sampleTest }): Promise => { + await openModal(sampleTest.modPage); + await enableAllFilters(sampleTest.modPage); + await pickUser(sampleTest.modPage); + await goBackToPresenterView(sampleTest.modPage); + + // The "Previously picked"
        should contain at least one
      • entry. + const pickedList = sampleTest.modPage.getLocator( + `${e.pickRandomUserPreviouslyPickedList} li`, + ); + await pickedList.first().waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + const count = await pickedList.count(); + test.expect(count, 'previously-picked list should have at least one entry after picking').toBeGreaterThanOrEqual(1); + }); + + test('should empty the "Previously picked" list when "Clear All" is clicked', async ({ sampleTest }): Promise => { + await openModal(sampleTest.modPage); + await enableAllFilters(sampleTest.modPage); + await pickUser(sampleTest.modPage); + await goBackToPresenterView(sampleTest.modPage); + + // Confirm there is at least one entry before clearing. + const pickedList = sampleTest.modPage.getLocator( + `${e.pickRandomUserPreviouslyPickedList} li`, + ); + await pickedList.first().waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + + // Click Clear All. + await sampleTest.modPage.page.click(e.pickRandomUserClearAllButton); + + // The list should now be empty. + await sampleTest.modPage.wasRemoved( + `${e.pickRandomUserPreviouslyPickedList} li`, + '"Previously picked" list should be empty after clicking "Clear All"', + ELEMENT_WAIT_LONGER_TIME, + ); + }); + + test('should drop available count to 0 and show "no users" warning after picking with "Include already picked users" unchecked', async ({ sampleTest }): Promise => { + // With includePickedUsers=false (default), a picked user is immediately + // removed from the eligible pool after being selected. + await openModal(sampleTest.modPage); + await enableInclusionFilters(sampleTest.modPage); // does NOT enable includePickedUsers + + // Verify 1 user is available before picking. + await sampleTest.modPage.hasText( + e.pickRandomUserAvailableContent, + '1 user', + '"1 user" should be displayed before picking', + ); + + await pickUser(sampleTest.modPage); + await goBackToPresenterView(sampleTest.modPage); + + // After picking, the presenter is now in the picked list and excluded. + await sampleTest.modPage.hasElement( + e.pickRandomUserNoUsersWarning, + 'no-users warning should appear: the only user was already picked', + ); + await sampleTest.modPage.wasRemoved( + e.pickRandomUserPickButton, + 'pick button should be hidden when there are 0 eligible users', + ELEMENT_WAIT_TIME, + ); + }); + + test('should restore the pick button after "Clear All" resets the picked-user history', async ({ sampleTest }): Promise => { + // Scenario: pick once (user excluded) → warning shown → Clear All → user eligible again. + await openModal(sampleTest.modPage); + await enableInclusionFilters(sampleTest.modPage); // includePickedUsers stays OFF + await pickUser(sampleTest.modPage); + await goBackToPresenterView(sampleTest.modPage); + + // No-users warning is shown (picked user excluded). + await sampleTest.modPage.hasElement( + e.pickRandomUserNoUsersWarning, + 'no-users warning should be visible before Clear All', + ); + + // Clear the pick history – the user is no longer in the "picked" list. + await sampleTest.modPage.page.click(e.pickRandomUserClearAllButton); + + // The user is now eligible again, so the pick button should reappear. + await sampleTest.modPage.hasElement( + e.pickRandomUserPickButton, + 'pick button should reappear after clearing the pick history', + ELEMENT_WAIT_LONGER_TIME, + ); + await sampleTest.modPage.wasRemoved( + e.pickRandomUserNoUsersWarning, + 'no-users warning should disappear once the pick button is available again', + ELEMENT_WAIT_TIME, + ); + }); +}); diff --git a/tests/core/constants.ts b/tests/core/constants.ts new file mode 100644 index 0000000..301b95c --- /dev/null +++ b/tests/core/constants.ts @@ -0,0 +1,28 @@ +import dotenv from 'dotenv'; +import path from 'path'; + +dotenv.config({ path: path.resolve(__dirname, '../../.env') }); + +export const CI = process.env.CI === 'true'; +const TIMEOUT_MULTIPLIER = Number(process.env.TIMEOUT_MULTIPLIER); +const MULTIPLIER = CI ? TIMEOUT_MULTIPLIER || 2 : TIMEOUT_MULTIPLIER || 1; + +// GLOBAL TESTS VARS +export const ELEMENT_WAIT_TIME: number = 5000 * MULTIPLIER; +export const ELEMENT_WAIT_LONGER_TIME: number = 10000 * MULTIPLIER; +export const ELEMENT_WAIT_EXTRA_LONG_TIME: number = 15000 * MULTIPLIER; +export const LOOP_INTERVAL: number = 1200; + +// STRESS TESTS VARS +export const JOIN_AS_MODERATOR_TEST_ROUNDS: number = 15; +export const MAX_JOIN_AS_MODERATOR_FAIL_RATE: number = 0.05; +export const BREAKOUT_ROOM_INVITATION_TEST_ROUNDS: number = 20; +export const JOIN_TWO_USERS_ROUNDS: number = 20; +export const JOIN_TWO_USERS_KEEPING_CONNECTED_ROUNDS: number = 20; +export const JOIN_TWO_USERS_EXCEEDING_MAX_PARTICIPANTS: number = 20; +export const MAX_PARTICIPANTS_TO_JOIN: number = 4; + +// MEDIA CONNECTION TIMEOUTS +export const VIDEO_LOADING_WAIT_TIME: number = 15000; +export const UPLOAD_PDF_WAIT_TIME: number = 25000 * MULTIPLIER; +export const CUSTOM_MEETING_ID: string = 'custom-meeting'; diff --git a/tests/core/coreElements.ts b/tests/core/coreElements.ts new file mode 100644 index 0000000..c2892f0 --- /dev/null +++ b/tests/core/coreElements.ts @@ -0,0 +1,22 @@ +export const coreElements = { + audioModal: 'div[data-test="audioModal"]', + closeModal: 'button[data-test="closeModal"]', + errorMessageLabel: 'span[id="error-message"]', + whiteboard: 'div[data-testid="canvas"]', + actions: 'button[data-test="actionsButton"]', + // audio + joinAudioButton: 'button[data-test="joinAudio"]', + microphoneBtn: 'button[data-test="microphoneBtn"]', + joinEchoTestButton: 'button[data-test="joinEchoTestButton"]', + audioDropdownMenu: 'button[data-test="audioDropdownMenu"]', + establishingAudioLabel: 'span[data-test="establishingAudioLabel"]', + // video + joinVideoButton: 'button[data-test="joinVideo"]', + startSharingWebcam: 'button[data-test="startSharingWebcam"]', + leaveVideo: 'button[data-test="leaveVideo"]', + videoDropdownMenu: 'button[data-test="videoDropdownMenu"]', + // presentation + presentationToolbarWrapper: 'div[id="presentationToolbarWrapper"]', + skipSlide: 'select[data-test="skipSlide"]', + nextSlide: 'button[data-test="nextSlide"]', +}; diff --git a/tests/core/fixtures/README.md b/tests/core/fixtures/README.md new file mode 100644 index 0000000..759b7c0 --- /dev/null +++ b/tests/core/fixtures/README.md @@ -0,0 +1,47 @@ +# Sample Test Utilities + +This directory contains shared utilities for sample plugin tests. + +## Usage + +### 1. Using the shared fixture (`sampleFixture.ts`) + +The `createSampleTest` function creates a Playwright test instance with a `sampleTest` fixture that automatically sets up the plugin for testing. + +```typescript +import { expect } from '@playwright/test'; +import { createSampleTest } from '../../../tests/core/sampleFixture'; +import { checkPluginAvailability } from '../../../tests/core/sampleBeforeAll'; + +const { test, setPluginUrl, getPluginUrl } = createSampleTest({ + envVarName: 'YOUR_PLUGIN_URL_ENV_VAR', // e.g., 'ACTIONS_BAR_URL' + getPluginUrl: () => process.env.YOUR_PLUGIN_URL_ENV_VAR, +}); +``` + +### 2. Using the shared beforeAll hook (`sampleBeforeAll.ts`) + +The `checkPluginAvailability` function creates a `beforeAll` hook that automatically checks the plugin availability, fetches the plugin manifest and sets up the plugin URL. + +```typescript +test.describe.parallel('Your Plugin Tests', () => { + test.beforeAll(checkPluginAvailability({ + pluginName: 'sample-your-plugin-name', // Must match the folder name in /plugins/ + envVarName: 'YOUR_PLUGIN_URL_ENV_VAR', + setPluginUrl, + getPluginUrl, + })); + + test('your test', async ({ sampleTest }) => { + // Use sampleTest fixture here + await sampleTest.modPage.hasElement('your-element'); + }); +}); +``` + +## Environment Variable Handling + +The utilities support two ways to provide plugin URLs: + +1. **Custom URL via environment variable**: Set `YOUR_PLUGIN_URL_ENV_VAR` to a custom plugin URL +2. **Auto-generated URL**: If no custom URL is provided, the `beforeAll` hook will construct the URL using the server configuration and plugin name diff --git a/tests/core/fixtures/sampleBeforeAll.ts b/tests/core/fixtures/sampleBeforeAll.ts new file mode 100644 index 0000000..7bb929f --- /dev/null +++ b/tests/core/fixtures/sampleBeforeAll.ts @@ -0,0 +1,51 @@ +/* eslint-disable no-console */ +/* eslint-disable import/no-extraneous-dependencies */ +import { TestInfo, APIRequestContext } from '@playwright/test'; +import { secret, server } from '../parameters'; + +export type SampleBeforeAllConfig = { + pluginName: string; + envVarName: string; + setPluginUrl: (url: string) => void; + getPluginUrl?: () => string | undefined; +}; + +export function checkPluginAvailability(config: SampleBeforeAllConfig) { + return async ({ request }: { request: APIRequestContext }, testInfo: TestInfo): Promise => { + // Check if custom URL is already provided via environment variable + const customUrl = config.getPluginUrl?.(); + if (customUrl) { + config.setPluginUrl(customUrl); + return; + } + + const BBB_URL_PATTERN = /^https:\/\/[^/]+\/bigbluebutton\/$/; + if (!secret) throw new Error('BBB_SECRET environment variable is not set'); + if (!server) throw new Error('BBB_URL environment variable is not set'); + if (!BBB_URL_PATTERN.test(server)) throw new Error('BBB_URL must follow the pattern "https://DOMAIN_NAME/bigbluebutton/"'); + + const serverDomain = new URL(server).origin; + const manifestUrlPath = `/plugins/${config.pluginName}/dist/manifest.json`; + const pluginUrl = `${serverDomain}${manifestUrlPath}`; + + const response = await request.get(pluginUrl); + if (!response.ok()) { + const msg = `Failed to fetch plugin manifest for ${pluginUrl} plugin. returned status ${response.status()}`; + console.error(msg); + testInfo.skip( + true, + msg, + ); + return; + } + + try { + await response.json(); + config.setPluginUrl(pluginUrl); + } catch (error) { + const msg = `Invalid JSON response from plugin manifest for ${testInfo.title} plugin`; + console.error(msg, error); + testInfo.skip(true, msg); + } + }; +} diff --git a/tests/core/fixtures/sampleFixture.ts b/tests/core/fixtures/sampleFixture.ts new file mode 100644 index 0000000..9536279 --- /dev/null +++ b/tests/core/fixtures/sampleFixture.ts @@ -0,0 +1,39 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { test as base } from '@playwright/test'; +import { Sample } from '../sample'; +import { encodeCustomParams } from '../helpers'; + +export interface SampleTestFixtures { + sampleTest: Sample; +} + +export type SampleTestConfig = { + envVarName: string; + getPluginUrl?: () => string | undefined; +}; + +export function createSampleTest(config: SampleTestConfig) { + let pluginUrl: string | undefined = process.env[config.envVarName]; + + const test = base.extend({ + sampleTest: async ({ browser, context, page }, use) => { + const customUrl = config.getPluginUrl?.() || pluginUrl; + if (!customUrl) { + throw new Error(`Plugin URL is not set. Either set ${config.envVarName} environment variable or ensure beforeAll has run successfully.`); + } + + const createParameter = encodeCustomParams(`pluginManifests=${JSON.stringify([{ url: customUrl }])}`); + const sampleTest = new Sample({ browser, context }); + await sampleTest.initModPage(page, { createParameter }); + await use(sampleTest); + }, + }); + + return { + test, + setPluginUrl: (url: string) => { + pluginUrl = url; + }, + getPluginUrl: () => pluginUrl, + }; +} diff --git a/tests/core/helpers.ts b/tests/core/helpers.ts new file mode 100644 index 0000000..71ec5aa --- /dev/null +++ b/tests/core/helpers.ts @@ -0,0 +1,195 @@ +import { expect, Page } from '@playwright/test'; +import sha, { Algorithm } from 'sha.js'; +import xml2js from 'xml2js'; +import axios from 'axios'; +import { + attendeePW, fullName, moderatorPW, secret, server, +} from './parameters'; +import { InitParameters } from './sessionPage'; + +declare global { + interface Window { + meetingClientSettings: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public: any; + }; + } +} + +interface getJoinURLParams { + meetingID: string; + isModerator: boolean; + joinParameter?: string; + skipSessionDetailsModal?: boolean; +} + +export interface SessionSettings { + reactionsButton: boolean; + sharedNotesEnabled: boolean; + directLeaveButton: boolean; + // Audio + autoJoinAudioModal: boolean; + listenOnlyMode: boolean; + forceListenOnly: boolean; + skipEchoTest: boolean; + skipEchoTestOnJoin: boolean; + skipEchoTestIfPreviousDevice: boolean; + speechRecognitionEnabled: boolean; + // Chat + chatEnabled: boolean; + publicChatOptionsEnabled: boolean; + maxMessageLength: number; + emojiPickerEnabled: boolean; + autoConvertEmojiEnabled: boolean; + // Polling + pollEnabled: boolean; + pollChatMessage: boolean; + // Presentation + originalPresentationDownloadable: boolean; + presentationWithAnnotationsDownloadable: boolean; + externalVideoPlayer: boolean; + hidePresentationOnJoin: boolean; + // Screensharing + screensharingEnabled: boolean; + // Timeouts + listenOnlyCallTimeout: number; + videoPreviewTimeout: number; + // Webcam + webcamSharingEnabled: boolean; + skipVideoPreview: boolean; + skipVideoPreviewOnFirstJoin: boolean; + skipVideoPreviewIfPreviousDevice: boolean; + // Emoji + emojiRain: boolean; +} + +function getChecksum(text: string) { + let algorithm: Algorithm = (process.env.CHECKSUM || '').toLowerCase() as Algorithm; + if (!['sha1', 'sha256', 'sha512'].includes(algorithm)) { + switch (secret?.length) { + case 128: + algorithm = 'sha512'; + break; + case 64: + algorithm = 'sha256'; + break; + case 40: + default: + algorithm = 'sha1'; + } + } + return sha(algorithm).update(text).digest('hex'); +} + +function getRandomInt(min: number, max: number) { + const adjustedMin = Math.ceil(min); + const adjustedMax = Math.floor(max); + return Math.floor(Math.random() * (adjustedMax - adjustedMin)) + adjustedMin; +} + +function createMeetingUrl(params: InitParameters, createParameter: string | undefined) { + const meetingID = `random-${getRandomInt(1000000, 10000000).toString()}`; + const mp = params.moderatorPW; + const ap = params.attendeePW; + const baseQuery = `name=${meetingID}&meetingID=${meetingID}&attendeePW=${ap}&moderatorPW=${mp}` + + `&allowStartStopRecording=true&autoStartRecording=false&welcome=${params.welcome}`; + const query = createParameter !== undefined ? `${baseQuery}&${createParameter}` : baseQuery; + const apiCall = `create${query}${params.secret}`; + const checksum = getChecksum(apiCall); + const url = `${params.server}/api/create?${query}&checksum=${checksum}`; + return url; +} + +function createMeetingPromise(params: InitParameters, createParameter: string | undefined) { + const url = createMeetingUrl(params, createParameter); + return axios.get(url, { adapter: 'http' }); +} + +export async function createMeeting( + params: InitParameters, + createParameter: string | undefined, +): Promise { + const promise = createMeetingPromise(params, createParameter); + const response = await promise; + expect(response.status).toEqual(200); + const xmlResponse = await xml2js.parseStringPromise(response.data); + return xmlResponse.response.meetingID[0]; +} + +export function getJoinURL({ + meetingID, isModerator, joinParameter, skipSessionDetailsModal, +}: getJoinURLParams) { + const pw = isModerator ? moderatorPW : attendeePW; + const shouldSkipSessionDetailsModal = skipSessionDetailsModal ? '&userdata-bbb_show_session_details_on_join=false' : ''; // default value in settings.yml is true + const baseQuery = `fullName=${fullName}&meetingID=${meetingID}&password=${pw}${shouldSkipSessionDetailsModal}`; + const query = joinParameter ? `${baseQuery}&${joinParameter}` : baseQuery; + const apiCall = `join${query}${secret}`; + const checksum = getChecksum(apiCall); + return `${server}/api/join?${query}&checksum=${checksum}`; +} + +export function encodeCustomParams(param: string): string { + try { + const splitted = param.split('='); + if (splitted.length > 2) { + const aux = splitted.shift(); + splitted[1] = splitted.join('='); + splitted[0] = aux || ''; + } + splitted[1] = encodeURIComponent(splitted[1]).replace(/%20/g, '+'); + return splitted.join('='); + } catch (err) { + throw new Error(`Error encoding custom params: ${err}`); + } +} + +export async function generateSettingsData(page: Page): Promise { + try { + const settingsData = await page.evaluate(() => window.meetingClientSettings.public); + + const settings = { + reactionsButton: settingsData.app?.reactionsButton?.enabled, + sharedNotesEnabled: settingsData.notes?.enabled, + directLeaveButton: settingsData.app?.defaultSettings?.application?.directLeaveButton, + // Audio + autoJoinAudioModal: settingsData.app?.autoJoin, + listenOnlyMode: settingsData.app?.listenOnlyMode, + forceListenOnly: settingsData.app?.forceListenOnly, + skipEchoTest: settingsData.app?.skipCheck, + skipEchoTestOnJoin: settingsData.app?.skipCheckOnJoin, + skipEchoTestIfPreviousDevice: settingsData.app?.skipEchoTestIfPreviousDevice, + speechRecognitionEnabled: settingsData.app?.audioCaptions?.enabled, + // Chat + chatEnabled: settingsData.chat?.enabled, + publicChatOptionsEnabled: settingsData.chat?.enableSaveAndCopyPublicChat, + maxMessageLength: settingsData.chat?.max_message_length, + emojiPickerEnabled: settingsData.chat?.emojiPicker?.enable, + autoConvertEmojiEnabled: settingsData.chat?.autoConvertEmoji, + // Polling + pollEnabled: settingsData.poll?.enabled, + pollChatMessage: settingsData.poll?.chatMessage, + // Presentation + originalPresentationDownloadable: settingsData.presentation?.allowDownloadOriginal, + presentationWithAnnotationsDownloadable: + settingsData.presentation?.allowDownloadWithAnnotations, + externalVideoPlayer: settingsData.externalVideoPlayer?.enabled, + hidePresentationOnJoin: settingsData.layout?.hidePresentationOnJoin, + // Screensharing + screensharingEnabled: settingsData.kurento?.enableScreensharing, + // Timeouts + listenOnlyCallTimeout: parseInt(settingsData.media?.listenOnlyCallTimeout, 10), + videoPreviewTimeout: parseInt(settingsData.kurento?.gUMTimeout, 10), + // Webcam + webcamSharingEnabled: settingsData.kurento?.enableVideo, + skipVideoPreview: settingsData.kurento?.skipVideoPreview, + skipVideoPreviewOnFirstJoin: settingsData.kurento?.skipVideoPreviewOnFirstJoin, + skipVideoPreviewIfPreviousDevice: settingsData.kurento?.skipVideoPreviewIfPreviousDevice, + // Emoji + emojiRain: settingsData.app?.emojiRain?.enabled, + }; + + return settings; + } catch (err) { + throw new Error(`Error generating settings data: ${err}`); + } +} diff --git a/tests/core/parameters.ts b/tests/core/parameters.ts new file mode 100644 index 0000000..7029805 --- /dev/null +++ b/tests/core/parameters.ts @@ -0,0 +1,6 @@ +export const server: string | undefined = process.env.BBB_URL; +export const secret: string | undefined = process.env.BBB_SECRET; +export const welcome: string = '%3Cbr%3EWelcome+to+%3Cb%3E%25%25CONFNAME%25%25%3C%2Fb%3E%21'; +export const fullName: string = 'User1'; +export const moderatorPW: string = 'mp'; +export const attendeePW: string = 'ap'; diff --git a/tests/core/sample.ts b/tests/core/sample.ts new file mode 100644 index 0000000..afd0a42 --- /dev/null +++ b/tests/core/sample.ts @@ -0,0 +1,28 @@ +import { Page, Browser, BrowserContext } from '@playwright/test'; +import { InitOptions, SessionPage } from './sessionPage'; + +interface SampleProps { + browser: Browser; + context: BrowserContext; +} + +export class Sample { + readonly browser: Browser; + readonly context: BrowserContext; + modPage!: SessionPage; + + constructor({ browser, context }: SampleProps) { + this.browser = browser; + this.context = context; + } + + async initModPage(page: Page, { + fullName = 'Moderator', + shouldCloseAudioModal = true, + ...restOptions + }: InitOptions = {}) { + const options = { fullName, ...restOptions }; + this.modPage = new SessionPage({ browser: this.browser, page }); + await this.modPage.init({ isModerator: true, shouldCloseAudioModal, initOptions: options }); + } +} diff --git a/tests/core/scripts/copy-sample-to-container.sh b/tests/core/scripts/copy-sample-to-container.sh new file mode 100755 index 0000000..42689a0 --- /dev/null +++ b/tests/core/scripts/copy-sample-to-container.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Check if plugin name is provided +if [ -z "$1" ]; then + echo "Usage: $0 [containerName]" + exit 1 +fi + +# Set the "-e" flag to make the script exit immediately if any command fails. +set -e + +# Determine the directory of the script +SCRIPT_DIR=$(dirname "$(readlink -f "$0")") + +# Load .env file +ENV_FILE="$SCRIPT_DIR/../../../.env" # Adjusted path to .env +if [ -f "$ENV_FILE" ]; then + set -o allexport + source "$ENV_FILE" + set +o allexport +fi + +PLUGIN_NAME=$1 +CONTAINER_NAME=${2:-$LOCAL_CONTAINER_NAME} + +# check if container name is provided +if [ -z "$CONTAINER_NAME" ]; then + echo "Container name is not provided. Exiting." + exit 1 +fi + +PLUGINS_FOLDER_CONTAINER_PATH="/var/www/bigbluebutton-default/assets/plugins" +PLUGIN_CONTAINER_PATH="$PLUGINS_FOLDER_CONTAINER_PATH/$PLUGIN_NAME" + +# Check if container is running +if ! docker ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then + echo "Container $CONTAINER_NAME is not running. Exiting." + exit 1 +fi + +# Check if the plugins folder exists in the container +if docker exec $CONTAINER_NAME [ -d "$PLUGIN_CONTAINER_PATH" ]; then + # Remove the existing files in the container path + docker exec $CONTAINER_NAME rm -rf "$PLUGIN_CONTAINER_PATH" +fi + +echo "Creating container path: $PLUGIN_CONTAINER_PATH..." +docker exec $CONTAINER_NAME mkdir -p "$PLUGIN_CONTAINER_PATH" + +echo "Copying dist contents to container path: $PLUGIN_CONTAINER_PATH..." +docker cp dist/. $CONTAINER_NAME:$PLUGIN_CONTAINER_PATH/dist + +# Check if the copy was successful +if [ $? -ne 0 ]; then + echo "Failed to copy dist to container." + exit 1 +fi diff --git a/tests/core/sessionPage.ts b/tests/core/sessionPage.ts new file mode 100644 index 0000000..555e127 --- /dev/null +++ b/tests/core/sessionPage.ts @@ -0,0 +1,135 @@ +import { + expect, Page, Browser, Locator, +} from '@playwright/test'; +import { ELEMENT_WAIT_EXTRA_LONG_TIME, ELEMENT_WAIT_TIME } from './constants'; +import * as parameters from './parameters'; +import { + createMeeting, generateSettingsData, getJoinURL, SessionSettings, +} from './helpers'; +import { coreElements as e } from './coreElements'; + +interface PageProps { + browser: Browser; + page: Page; +} + +export interface InitOptions { + fullName?: string; + createParameter?: string; + joinParameter?: string; + shouldCloseAudioModal?: boolean; + skipSessionDetailsModal?: boolean; + shouldCheckAllInitialSteps?: boolean; +} + +interface InitFunctionParameters { + isModerator: boolean; + shouldCloseAudioModal: boolean; + initOptions: InitOptions; +} + +export interface InitParameters { + server: string | undefined; + secret: string | undefined; + welcome: string; + fullName: string; + moderatorPW: string; + attendeePW: string; +} + +export class SessionPage { + readonly page: Page; + + readonly browser: Browser; + + settings: SessionSettings | undefined; + + initParameters: InitParameters; + + username: string; + + meetingId: string; + + constructor({ browser, page }: PageProps) { + this.browser = browser; + this.page = page; + this.username = ''; + this.meetingId = ''; + this.initParameters = { ...parameters }; + } + + async init({ isModerator, shouldCloseAudioModal, initOptions = {} }: InitFunctionParameters) { + const { + fullName, + createParameter, + joinParameter, + skipSessionDetailsModal = true, + shouldCheckAllInitialSteps = true, + } = initOptions; + + if (!isModerator) this.initParameters.moderatorPW = ''; + this.username = fullName || this.initParameters.fullName; + + this.meetingId = await createMeeting(this.initParameters, createParameter); + const joinUrl = getJoinURL({ + meetingID: this.meetingId, isModerator, joinParameter, skipSessionDetailsModal, + }); + const response = await this.page.goto(joinUrl); + await expect(response?.ok()).toBeTruthy(); + const hasErrorLabel = await this.checkElement(e.errorMessageLabel); + await expect(hasErrorLabel, 'should pass the authentication and the layout element should be displayed').toBeFalsy(); + if (shouldCheckAllInitialSteps) { + await this.page.waitForSelector('div#layout', { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME }); + this.settings = await generateSettingsData(this.page); + const autoJoinAudioModal = this.settings?.autoJoinAudioModal; + if (shouldCloseAudioModal && autoJoinAudioModal) await this.closeAudioModal(); + } + // overwrite for font used in CI + await this.page.addStyleTag({ + content: ` + body { + font-family: 'Liberation Sans', Arial, sans-serif; + }`, + }); + } + + async hasElement(selector: string, description: string, timeout = ELEMENT_WAIT_TIME) { + const locator = this.getLocator(selector); + await expect(locator, description).toBeVisible({ timeout }); + } + + async wasRemoved(selector: string, description: string, timeout = ELEMENT_WAIT_TIME) { + const locator = this.getLocator(selector); + await expect(locator, description).toBeHidden({ timeout }); + } + + async checkElement(selector: string, index = 0): Promise { + // eslint-disable-next-line @typescript-eslint/no-shadow + return this.page.evaluate(([selector, index]) => { + if (typeof selector !== 'string') throw new Error('Selector must be a string'); + const element = document.querySelectorAll(selector); + if (element.length > 0) { + return element[index as number] !== undefined; + } + return false; + }, [selector, index]); + } + + getLocator(selector: string): Locator { + return this.page.locator(selector); + } + + async waitForPluginLogger() { + return this.page.waitForEvent('console', (msg) => msg.text().includes('PluginLogger')); + } + + async hasText(selector: string, text: string, description: string, timeout = ELEMENT_WAIT_TIME) { + const locator = this.getLocator(selector).first(); + await expect(locator, description).toContainText(text, { timeout }); + } + + async closeAudioModal() { + await this.hasElement(e.audioModal, 'should display the audio modal', ELEMENT_WAIT_EXTRA_LONG_TIME); + await this.page.click(e.closeModal); + } +} diff --git a/tests/elements.ts b/tests/elements.ts new file mode 100644 index 0000000..1943b1f --- /dev/null +++ b/tests/elements.ts @@ -0,0 +1,38 @@ +import { coreElements } from './core/coreElements'; + +export const elements = { + ...coreElements, + + // Action button dropdown items injected by the plugin + pickRandomUserActionButton: 'li[data-test="actionDropdownButtonPlugin"]', + displayLastRandomlyPickedUser: 'li[data-test="displayLastRandomlyPickedUser"]', + + // Modal close button + pickRandomUserModalCloseButton: '[data-test="pickRandomUserModalCloseButton"]', + + // Presenter view – filter checkboxes (identified by their HTML id attributes) + includeModeratorsCheckbox: '#includeModerators', + includePresenterCheckbox: '#includePresenter', + includePickedUsersCheckbox: '#includePickedUsers', + + // Presenter view – available users section + pickRandomUserAvailableContent: '[data-test="pickRandomUserAvailableContent"]', + + // Presenter view – pick button and "no users" warning + pickRandomUserPickButton: '[data-test="pickRandomUserPickButton"]', + pickRandomUserNoUsersWarning: '[data-test="pickRandomUserNoUsersWarning"]', + + // Presenter view – previously picked section + pickRandomUserClearAllButton: '[data-test="pickRandomUserClearAllButton"]', + pickRandomUserPreviouslyPickedList: '[data-test="pickRandomUserPreviouslyPickedList"]', + + // Picked-user view + pickRandomUserPickedUserViewTitle: '[data-test="pickRandomUserPickedUserViewTitle"]', + pickRandomUserPickedUserName: '[data-test="pickRandomUserPickedUserName"]', + pickRandomUserBackButton: '[data-test="pickRandomUserBackButton"]', + pickRandomUserCountDownMessage: 'div[data-test="countDownMessage"]', + pickRandomUserCountDownProgressBar: 'div[data-test="countDownProgressBar"]', + + // Modal overlay (ReactModal renders this as a full-screen backdrop) + pickRandomUserModalOverlay: '.modalOverlay', +}; diff --git a/tests/structural/test.spec.ts b/tests/structural/test.spec.ts new file mode 100644 index 0000000..7ae5e03 --- /dev/null +++ b/tests/structural/test.spec.ts @@ -0,0 +1,108 @@ +import { createSampleTest } from '../core/fixtures/sampleFixture'; +import { checkPluginAvailability } from '../core/fixtures/sampleBeforeAll'; +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; +import { elements as e } from '../elements'; +import { SessionPage as ModPage } from '../core/sessionPage'; + +const PLUGIN_NAME = 'pick-random-user-plugin'; +const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; + +const { test, setPluginUrl, getPluginUrl } = createSampleTest({ + envVarName: ENV_VAR_NAME, + getPluginUrl: () => process.env[ENV_VAR_NAME], +}); + +/** Helper: open the plugin modal from the actions dropdown. */ +async function openPickRandomUserModal( + modPage: ModPage, +) { + await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); + await modPage.page.click(e.actions); + await modPage.hasElement(e.pickRandomUserActionButton, 'action button should be visible'); + await modPage.page.click(e.pickRandomUserActionButton); +} + +test.describe.parallel('Pick Random User Plugin – Structural', () => { + test.beforeAll(checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })); + + test('should show "Pick random user" label in the actions dropdown for a presenter', async ({ sampleTest }): Promise => { + await sampleTest.modPage.page.waitForSelector( + e.whiteboard, + { timeout: ELEMENT_WAIT_LONGER_TIME }, + ); + await sampleTest.modPage.page.click(e.actions); + await sampleTest.modPage.hasElement( + e.pickRandomUserActionButton, + 'should display the plugin action-button item', + ); + await sampleTest.modPage.hasText( + e.pickRandomUserActionButton, + 'Pick random user', + 'should display the correct label "Pick random user"', + ); + }); + + test('should open the presenter modal when clicking the action-button option', async ({ sampleTest }): Promise => { + await openPickRandomUserModal(sampleTest.modPage); + await sampleTest.modPage.hasElement( + e.includeModeratorsCheckbox, + 'should show the modal presenter view with the "Include moderators" checkbox', + ); + await sampleTest.modPage.hasElement( + e.pickRandomUserAvailableContent, + 'should display the "Available for selection" section', + ); + }); + + test('should display all three filter checkboxes in the presenter view', async ({ sampleTest }): Promise => { + await openPickRandomUserModal(sampleTest.modPage); + await sampleTest.modPage.hasElement( + e.includeModeratorsCheckbox, + 'should display the "Include moderators" checkbox', + ); + await sampleTest.modPage.hasElement( + e.includePresenterCheckbox, + 'should display the "Include presenter" checkbox', + ); + await sampleTest.modPage.hasElement( + e.includePickedUsersCheckbox, + 'should display the "Include already picked user" checkbox', + ); + }); + + test('should have all three filter checkboxes unchecked by default', async ({ sampleTest }): Promise => { + await openPickRandomUserModal(sampleTest.modPage); + await test.expect( + sampleTest.modPage.getLocator(e.includeModeratorsCheckbox), + '"Include moderators" should be unchecked by default', + ).not.toBeChecked(); + await test.expect( + sampleTest.modPage.getLocator(e.includePresenterCheckbox), + '"Include presenter" should be unchecked by default', + ).not.toBeChecked(); + await test.expect( + sampleTest.modPage.getLocator(e.includePickedUsersCheckbox), + '"Include already picked user" should be unchecked by default', + ).not.toBeChecked(); + }); + + test('should show "no users" warning and hide the pick button with default filters (only presenter in meeting)', async ({ sampleTest }): Promise => { + // Default: includeModerators=false, includePresenter=false → + // the single moderator/presenter user is excluded by both rules → 0 eligible. + await openPickRandomUserModal(sampleTest.modPage); + await sampleTest.modPage.hasElement( + e.pickRandomUserNoUsersWarning, + 'should show the "No {0} available" warning when 0 users are eligible', + ); + await sampleTest.modPage.wasRemoved( + e.pickRandomUserPickButton, + 'should NOT render the pick button when there are no eligible users', + ELEMENT_WAIT_TIME, + ); + }); +}); From 74c5a149d3d305694bf164947a329430f86147a8 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Mon, 13 Apr 2026 11:17:57 -0300 Subject: [PATCH 065/117] [automated-test-implementation] enhance performance by creating sequential tests reusing same session across similar tests --- package-lock.json | 10 + package.json | 1 + tests/behavioral/{fixture.ts => fixtures.ts} | 4 +- tests/behavioral/helpers.ts | 63 ++++++ tests/behavioral/multi-user.spec.ts | 190 ++++++++++-------- tests/behavioral/single-user.spec.ts | 167 +++++++-------- tests/core/constants.ts | 14 -- tests/core/coreElements.ts | 15 -- ...{sampleBeforeAll.ts => pluginBeforeAll.ts} | 0 tests/core/fixtures/sampleFixture.ts | 39 ---- tests/core/{sample.ts => plugin.ts} | 8 +- .../core/scripts/copy-sample-to-container.sh | 57 ------ tests/structural/test.spec.ts | 153 +++++++++----- 13 files changed, 371 insertions(+), 350 deletions(-) rename tests/behavioral/{fixture.ts => fixtures.ts} (97%) create mode 100644 tests/behavioral/helpers.ts rename tests/core/fixtures/{sampleBeforeAll.ts => pluginBeforeAll.ts} (100%) delete mode 100644 tests/core/fixtures/sampleFixture.ts rename tests/core/{sample.ts => plugin.ts} (87%) delete mode 100755 tests/core/scripts/copy-sample-to-container.sh diff --git a/package-lock.json b/package-lock.json index 06a1581..d4ec91d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@types/react-modal": "^3.16.0", "@types/sha.js": "^2.4.4", "@types/styled-components": "^5.1.26", + "@types/xml2js": "^0.4.14", "@typescript-eslint/eslint-plugin": "^6.2.0", "@typescript-eslint/parser": "^6.2.0", "axios": "^1.8.4", @@ -2607,6 +2608,15 @@ "@types/node": "*" } }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", diff --git a/package.json b/package.json index 5cd2285..19a0a1f 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ ] }, "devDependencies": { + "@types/xml2js": "^0.4.14", "@playwright/test": "^1.51.1", "@types/sha.js": "^2.4.4", "sha.js": "^2.4.11", diff --git a/tests/behavioral/fixture.ts b/tests/behavioral/fixtures.ts similarity index 97% rename from tests/behavioral/fixture.ts rename to tests/behavioral/fixtures.ts index c5a85b9..d646a30 100644 --- a/tests/behavioral/fixture.ts +++ b/tests/behavioral/fixtures.ts @@ -1,6 +1,6 @@ /* eslint-disable import/no-extraneous-dependencies */ import { test as base } from '@playwright/test'; -import { Sample } from '../core/sample'; +import { Plugin } from '../core/plugin'; import { SessionPage } from '../core/sessionPage'; import { encodeCustomParams, @@ -48,7 +48,7 @@ export function createMultiUserTest(config: MultiUserTestConfig) { viewport: { width: 1280, height: 720 }, }); const modRawPage = await modContext.newPage(); - const sample = new Sample({ browser, context: modContext }); + const sample = new Plugin({ browser, context: modContext }); await sample.initModPage(modRawPage, { createParameter }); const { modPage } = sample; diff --git a/tests/behavioral/helpers.ts b/tests/behavioral/helpers.ts new file mode 100644 index 0000000..657df3b --- /dev/null +++ b/tests/behavioral/helpers.ts @@ -0,0 +1,63 @@ +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; +import { elements as e } from '../elements'; +import { SessionPage as Page } from '../core/sessionPage'; + +export async function openModal(modPage: Page): Promise { + await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); + await modPage.page.click(e.actions); + await modPage.hasElement(e.pickRandomUserActionButton, 'action button should appear'); + await modPage.page.click(e.pickRandomUserActionButton); +} + +export async function goBackToPresenterView(modPage: Page): Promise { + await modPage.hasElement(e.pickRandomUserBackButton, 'back button should be visible'); + await modPage.page.click(e.pickRandomUserBackButton); + await modPage.hasElement( + e.includeModeratorsCheckbox, + 'presenter view should be restored after clicking back', + ELEMENT_WAIT_TIME, + ); +} + +export async function moderatorCleanupAfterTest(modPage: Page): Promise { + if (await modPage.page.locator(e.pickRandomUserPickedUserViewTitle).isVisible()) { + await modPage.page.click(e.pickRandomUserBackButton); + await modPage.page.locator(e.pickRandomUserModalCloseButton).waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + } + + const modCloseBtn = modPage.page.locator(e.pickRandomUserModalCloseButton); + if (await modCloseBtn.isVisible()) { + // Uncheck any filter checkboxes that were left enabled. + const includePickedUsers = modPage.page.locator(e.includePickedUsersCheckbox); + if (await includePickedUsers.isChecked()) await includePickedUsers.click(); + const includeModerators = modPage.page.locator(e.includeModeratorsCheckbox); + if (await includeModerators.isChecked()) await includeModerators.click(); + const includePresenter = modPage.page.locator(e.includePresenterCheckbox); + if (await includePresenter.isChecked()) await includePresenter.click(); + + // Clear the previously-picked history. + const clearBtn = modPage.page.locator(e.pickRandomUserClearAllButton); + if (await clearBtn.isVisible()) { + await modPage.page.click(e.pickRandomUserClearAllButton); + } + + await modCloseBtn.click(); + return; + } + + // Dismiss the mod's actions dropdown if it was left open. + if (await modPage.page.locator(e.pickRandomUserActionButton).isVisible()) { + await modPage.page.keyboard.press('Escape'); + } +} + +export async function attendeeCleanupAfterTest(attendeePage: Page): Promise { + const attendeeCloseBtn = attendeePage.page.locator(e.pickRandomUserModalCloseButton); + if (await attendeeCloseBtn.isVisible()) { + await attendeeCloseBtn.click(); + } + // Dismiss the attendee's actions dropdown if it was left open. + if (await attendeePage.page.locator(e.pickRandomUserActionButton).isVisible()) { + await attendeePage.page.keyboard.press('Escape'); + } +} diff --git a/tests/behavioral/multi-user.spec.ts b/tests/behavioral/multi-user.spec.ts index 7c1b6c6..8923896 100644 --- a/tests/behavioral/multi-user.spec.ts +++ b/tests/behavioral/multi-user.spec.ts @@ -1,40 +1,30 @@ /** * Behavioural tests – multi-user (moderator/presenter + attendee/viewer). - * - * These tests verify data-channel synchronisation and cross-user UX: - * - Both participants see the SAME picked user name. - * - The attendee's modal opens automatically when they are picked. - * - Title text differs correctly per role (presenter vs. picked viewer). - * - Countdown message is shown only to the viewer, not the presenter. - * - The "Display last randomly picked user" option appears in the attendee's - * actions dropdown after a pick. - * - * Setup: two independent browser contexts joined into the same BBB meeting. - * With default filters (includeModerators=false, includePresenter=false) the - * attendee is the only eligible user, so every pick targets them deterministically. */ -import { wait } from '@testing-library/user-event/dist/utils'; -import { checkPluginAvailability } from '../core/fixtures/sampleBeforeAll'; -import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; -import { createMultiUserTest } from './fixture'; +import { test, BrowserContext } from '@playwright/test'; +import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; +import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME, ELEMENT_WAIT_EXTRA_LONG_TIME } from '../core/constants'; import { elements as e } from '../elements'; import { SessionPage as Page } from '../core/sessionPage'; +import { Plugin } from '../core/plugin'; +import { + encodeCustomParams, + getJoinURL, + generateSettingsData, +} from '../core/helpers'; +import { + attendeeCleanupAfterTest, + goBackToPresenterView, + moderatorCleanupAfterTest, + openModal, +} from './helpers'; const PLUGIN_NAME = 'pick-random-user-plugin'; const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; -const { test, setPluginUrl, getPluginUrl } = createMultiUserTest({ - envVarName: ENV_VAR_NAME, - getPluginUrl: () => process.env[ENV_VAR_NAME], -}); - -/** Open the "Pick random user" modal from the actions dropdown. */ -async function openModal(modPage: Page): Promise { - await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); - await modPage.page.click(e.actions); - await modPage.hasElement(e.pickRandomUserActionButton, 'action button should appear'); - await modPage.page.click(e.pickRandomUserActionButton); -} +let pluginUrl: string | undefined = process.env[ENV_VAR_NAME]; +const setPluginUrl = (url: string) => { pluginUrl = url; }; +const getPluginUrl = () => pluginUrl; /** * Wait for the attendee's page to finish loading the whiteboard. @@ -44,30 +34,83 @@ async function waitForAttendeeMeeting(attendeePage: Page): Promise { await attendeePage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); } -/** Click the back button on the presenter's picked-user view to return to the presenter view. */ -async function goBackToPresenterView(modPage: Page): Promise { - await modPage.hasElement(e.pickRandomUserBackButton, 'back button should be visible'); - await modPage.page.click(e.pickRandomUserBackButton); - await modPage.hasElement( - e.includeModeratorsCheckbox, - 'presenter view should be restored after clicking back', - ELEMENT_WAIT_TIME, - ); +/** + * Reset state after each test for both pages: + */ +async function cleanupAfterTest(modPage: Page, attendeePage: Page): Promise { + await attendeeCleanupAfterTest(attendeePage); + await moderatorCleanupAfterTest(modPage); } // ── Tests ───────────────────────────────────────────────────────────────────── +test.describe('Pick Random User Plugin - Behavioural (multi-user)', () => { + test.describe.configure({ mode: 'serial' }); + + let modPage: Page; + let attendeePage: Page; + let modContext: BrowserContext; + let attendeeContext: BrowserContext; + + test.beforeAll(async ({ browser, request }, testInfo) => { + await checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })({ request }, testInfo); + + const resolvedUrl = getPluginUrl(); + if (!resolvedUrl) return; + + const createParameter = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: resolvedUrl }])}`, + ); + + // ── Moderator / presenter ────────────────────────────────────────────── + modContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const modRawPage = await modContext.newPage(); + const sample = new Plugin({ browser, context: modContext }); + await sample.initModPage(modRawPage, { createParameter }); + modPage = sample.modPage; + + // ── Attendee (viewer) ────────────────────────────────────────────────── + attendeeContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const attendeeRawPage = await attendeeContext.newPage(); + attendeePage = new Page({ browser, page: attendeeRawPage }); + + const joinUrl = getJoinURL({ + meetingID: modPage.meetingId, + isModerator: false, + skipSessionDetailsModal: true, + }); + await attendeeRawPage.goto(joinUrl); + await attendeeRawPage.waitForSelector('div#layout', { timeout: ELEMENT_WAIT_EXTRA_LONG_TIME }); + attendeePage.settings = await generateSettingsData(attendeeRawPage); + if (attendeePage.settings?.autoJoinAudioModal) { + await attendeePage.closeAudioModal(); + } + await attendeeRawPage.addStyleTag({ + content: "body { font-family: 'Liberation Sans', Arial, sans-serif; }", + }); + attendeePage.meetingId = modPage.meetingId; + }); -test.describe.parallel('Pick Random User Plugin - Behavioural (multi-user)', () => { - test.beforeAll(checkPluginAvailability({ - pluginName: PLUGIN_NAME, - envVarName: ENV_VAR_NAME, - setPluginUrl, - getPluginUrl, - })); + test.afterAll(async () => { + await modContext?.close(); + await attendeeContext?.close(); + }); - test('should show the same picked user name on both the presenter page and the attendee page', async ({ multiUserTest }): Promise => { - const { modPage, attendeePage } = multiUserTest; + test.afterEach(async () => { + if (modPage && attendeePage) await cleanupAfterTest(modPage, attendeePage); + }); + test('should show the same picked user name on both the presenter page and the attendee page', async (): Promise => { await waitForAttendeeMeeting(attendeePage); await openModal(modPage); @@ -96,25 +139,20 @@ test.describe.parallel('Pick Random User Plugin - Behavioural (multi-user)', () ); // Both pages must display the exact same name text. - const presenterName = await modPage.getLocator(e.pickRandomUserPickedUserName).textContent(); - const attendeeName = await attendeePage + const pickedNamePresenter = await modPage.getLocator( + e.pickRandomUserPickedUserName, + ).textContent(); + const pickedNameAttendee = await attendeePage .getLocator(e.pickRandomUserPickedUserName).textContent(); + test.expect(pickedNamePresenter, 'picked user name on presenter page should not be empty').toBeTruthy(); test.expect( - presenterName, - 'picked user name on presenter page should not be empty', - ).toBeTruthy(); - test.expect( - attendeeName, + pickedNameAttendee, 'picked user name shown to attendee must match the name shown to the presenter', - ).toBe(presenterName); - - wait(1000); + ).toBe(pickedNamePresenter); }); - test('should open the attendee modal automatically without any action on the attendee side', async ({ multiUserTest }): Promise => { - const { modPage, attendeePage } = multiUserTest; - + test('should open the attendee modal automatically without any action on the attendee side', async (): Promise => { await waitForAttendeeMeeting(attendeePage); await openModal(modPage); await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); @@ -130,20 +168,14 @@ test.describe.parallel('Pick Random User Plugin - Behavioural (multi-user)', () ); }); - test('should show "You have been randomly picked" to the picked attendee and "Randomly picked user" to the presenter', async ({ multiUserTest }): Promise => { - const { modPage, attendeePage } = multiUserTest; - + test('should show "You have been randomly picked" to the picked attendee and "Randomly picked user" to the presenter', async (): Promise => { await waitForAttendeeMeeting(attendeePage); await openModal(modPage); await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); await modPage.page.click(e.pickRandomUserPickButton); // Attendee is the picked user → sees "You have been randomly picked". - await attendeePage.hasElement( - e.pickRandomUserPickedUserViewTitle, - 'attendee modal should open', - ELEMENT_WAIT_LONGER_TIME, - ); + await attendeePage.hasElement(e.pickRandomUserPickedUserViewTitle, 'attendee modal should open', ELEMENT_WAIT_LONGER_TIME); await attendeePage.hasText( e.pickRandomUserPickedUserViewTitle, 'You have been randomly picked', @@ -151,11 +183,7 @@ test.describe.parallel('Pick Random User Plugin - Behavioural (multi-user)', () ); // Presenter sees "Randomly picked user" (different user was picked). - await modPage.hasElement( - e.pickRandomUserPickedUserViewTitle, - 'presenter modal should transition to picked-user view', - ELEMENT_WAIT_LONGER_TIME, - ); + await modPage.hasElement(e.pickRandomUserPickedUserViewTitle, 'presenter modal should transition to picked-user view', ELEMENT_WAIT_LONGER_TIME); await modPage.hasText( e.pickRandomUserPickedUserViewTitle, 'Randomly picked user', @@ -163,9 +191,7 @@ test.describe.parallel('Pick Random User Plugin - Behavioural (multi-user)', () ); }); - test('should show a countdown message only to the attendee and a countdown bar only to the presenter', async ({ multiUserTest }): Promise => { - const { modPage, attendeePage } = multiUserTest; - + test('should show a countdown message only to the attendee and a countdown bar only to the presenter', async (): Promise => { await waitForAttendeeMeeting(attendeePage); await openModal(modPage); await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); @@ -193,9 +219,7 @@ test.describe.parallel('Pick Random User Plugin - Behavioural (multi-user)', () ).toBeVisible({ timeout: ELEMENT_WAIT_TIME }); }); - test('should not close the picked attendee modal when clicking the overlay during the countdown lock period', async ({ multiUserTest }): Promise => { - const { modPage, attendeePage } = multiUserTest; - + test('should not close the picked attendee modal when clicking the overlay during the countdown lock period', async (): Promise => { await waitForAttendeeMeeting(attendeePage); await openModal(modPage); await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); @@ -224,9 +248,7 @@ test.describe.parallel('Pick Random User Plugin - Behavioural (multi-user)', () ).toBeVisible({ timeout: ELEMENT_WAIT_TIME }); }); - test('should keep the previously-picked viewer in the available pool and re-pick the same user when "include already picked users" is enabled and the presenter navigates back', async ({ multiUserTest }): Promise => { - const { modPage, attendeePage } = multiUserTest; - + test('should keep the previously-picked viewer in the available pool and re-pick the same user when "include already picked users" is enabled and the presenter navigates back', async (): Promise => { await waitForAttendeeMeeting(attendeePage); await openModal(modPage); @@ -289,9 +311,7 @@ test.describe.parallel('Pick Random User Plugin - Behavioural (multi-user)', () ).toBe(firstPickedName); }); - test('should inject "Display last randomly picked user" into the attendee actions dropdown after a pick', async ({ multiUserTest }): Promise => { - const { modPage, attendeePage } = multiUserTest; - + test('should inject "Display last randomly picked user" into the attendee actions dropdown after a pick', async (): Promise => { await waitForAttendeeMeeting(attendeePage); await openModal(modPage); await modPage.hasElement(e.pickRandomUserPickButton, 'pick button should be visible', ELEMENT_WAIT_LONGER_TIME); @@ -316,7 +336,7 @@ test.describe.parallel('Pick Random User Plugin - Behavioural (multi-user)', () await attendeePage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); await attendeePage.page.click(e.actions); - // The plugin injects "Display last randomly picked user" for attendees + // The plugin injects "Display last randomly picked user" for attendees. await attendeePage.hasElement( e.displayLastRandomlyPickedUser, 'attendee actions dropdown should contain the plugin option', diff --git a/tests/behavioral/single-user.spec.ts b/tests/behavioral/single-user.spec.ts index 40e7591..b2ff80c 100644 --- a/tests/behavioral/single-user.spec.ts +++ b/tests/behavioral/single-user.spec.ts @@ -1,35 +1,21 @@ /** * Behavioural tests – single user (moderator / presenter only). - * - * These tests exercise the pick-and-track workflow: picking a user, verifying - * the "Previously picked" list, and exercising the Clear All and filter - * interactions that affect the eligible-user count. - * - * Setup: one BBB meeting, one participant (moderator who is also presenter). - * The "Include moderators" and "Include presenter" filters are always enabled - * so the presenter can pick themselves. */ -// eslint-disable-next-line import/no-extraneous-dependencies -import { createSampleTest } from '../core/fixtures/sampleFixture'; -import { checkPluginAvailability } from '../core/fixtures/sampleBeforeAll'; +import { test, BrowserContext } from '@playwright/test'; +import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; import { elements as e } from '../elements'; import { SessionPage as ModPage } from '../core/sessionPage'; +import { Plugin } from '../core/plugin'; +import { encodeCustomParams } from '../core/helpers'; +import { goBackToPresenterView, openModal, moderatorCleanupAfterTest } from './helpers'; const PLUGIN_NAME = 'pick-random-user-plugin'; const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; -const { test, setPluginUrl, getPluginUrl } = createSampleTest({ - envVarName: ENV_VAR_NAME, - getPluginUrl: () => process.env[ENV_VAR_NAME], -}); - -async function openModal(modPage: ModPage): Promise { - await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); - await modPage.page.click(e.actions); - await modPage.hasElement(e.pickRandomUserActionButton, 'action button should appear'); - await modPage.page.click(e.pickRandomUserActionButton); -} +let pluginUrl: string | undefined = process.env[ENV_VAR_NAME]; +const setPluginUrl = (url: string) => { pluginUrl = url; }; +const getPluginUrl = () => pluginUrl; /** Enable both inclusion filters so the single moderator/presenter is eligible. */ async function enableInclusionFilters(modPage: ModPage): Promise { @@ -64,134 +50,153 @@ async function pickUser(modPage: ModPage): Promise { ); } -/** Click the back button to return from picked-user view to presenter view. */ -async function goBackToPresenterView(modPage: ModPage): Promise { - await modPage.hasElement(e.pickRandomUserBackButton, 'back button should be visible'); - await modPage.page.click(e.pickRandomUserBackButton); - await modPage.hasElement( - e.includeModeratorsCheckbox, - 'presenter view should be restored after clicking back', - ELEMENT_WAIT_TIME, - ); +async function cleanupAfterTest(modPage: ModPage) { + await moderatorCleanupAfterTest(modPage); } // ── Tests ───────────────────────────────────────────────────────────────────── -test.describe.parallel('Pick Random User Plugin – Behavioural (single user)', () => { - test.beforeAll(checkPluginAvailability({ - pluginName: PLUGIN_NAME, - envVarName: ENV_VAR_NAME, - setPluginUrl, - getPluginUrl, - })); - - test('should show "Pick again" button (not "Pick user") after navigating back from picked-user view', async ({ sampleTest }): Promise => { +test.describe('Pick Random User Plugin - Behavioural (single user)', () => { + test.describe.configure({ mode: 'serial' }); + + let modPage: ModPage; + let sharedContext: BrowserContext; + + test.beforeAll(async ({ browser, request }, testInfo) => { + await checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })({ request }, testInfo); + + const resolvedUrl = getPluginUrl(); + if (!resolvedUrl) return; + + const createParameter = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: resolvedUrl }])}`, + ); + sharedContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const page = await sharedContext.newPage(); + const plugin = new Plugin({ browser, context: sharedContext }); + await plugin.initModPage(page, { createParameter }); + modPage = plugin.modPage; + }); + + test.afterAll(async () => { + await sharedContext?.close(); + }); + + test.afterEach(async () => { + if (modPage) await cleanupAfterTest(modPage); + }); + + test('should show "Pick again" button (not "Pick user") after navigating back from picked-user view', async (): Promise => { // With "Include already picked users" ON the presenter stays in the pool // after being picked, so the pick button remains visible on return. - await openModal(sampleTest.modPage); - await enableAllFilters(sampleTest.modPage); - await pickUser(sampleTest.modPage); - await goBackToPresenterView(sampleTest.modPage); + await openModal(modPage); + await enableAllFilters(modPage); + await pickUser(modPage); + await goBackToPresenterView(modPage); - await sampleTest.modPage.hasElement( + await modPage.hasElement( e.pickRandomUserPickButton, 'pick button should still be visible (includePickedUsers is enabled)', ); - await sampleTest.modPage.hasText( + await modPage.hasText( e.pickRandomUserPickButton, 'Pick again', 'button label should read "Pick again" after a user has already been picked', ); }); - test('should list the picked user in the "Previously picked" section after picking', async ({ sampleTest }): Promise => { - await openModal(sampleTest.modPage); - await enableAllFilters(sampleTest.modPage); - await pickUser(sampleTest.modPage); - await goBackToPresenterView(sampleTest.modPage); + test('should list the picked user in the "Previously picked" section after picking', async (): Promise => { + await openModal(modPage); + await enableAllFilters(modPage); + await pickUser(modPage); + await goBackToPresenterView(modPage); // The "Previously picked"
          should contain at least one
        • entry. - const pickedList = sampleTest.modPage.getLocator( - `${e.pickRandomUserPreviouslyPickedList} li`, - ); + const pickedList = modPage.getLocator(`${e.pickRandomUserPreviouslyPickedList} li`); await pickedList.first().waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); const count = await pickedList.count(); test.expect(count, 'previously-picked list should have at least one entry after picking').toBeGreaterThanOrEqual(1); }); - test('should empty the "Previously picked" list when "Clear All" is clicked', async ({ sampleTest }): Promise => { - await openModal(sampleTest.modPage); - await enableAllFilters(sampleTest.modPage); - await pickUser(sampleTest.modPage); - await goBackToPresenterView(sampleTest.modPage); + test('should empty the "Previously picked" list when "Clear All" is clicked', async (): Promise => { + await openModal(modPage); + await enableAllFilters(modPage); + await pickUser(modPage); + await goBackToPresenterView(modPage); // Confirm there is at least one entry before clearing. - const pickedList = sampleTest.modPage.getLocator( - `${e.pickRandomUserPreviouslyPickedList} li`, - ); + const pickedList = modPage.getLocator(`${e.pickRandomUserPreviouslyPickedList} li`); await pickedList.first().waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); // Click Clear All. - await sampleTest.modPage.page.click(e.pickRandomUserClearAllButton); + await modPage.page.click(e.pickRandomUserClearAllButton); // The list should now be empty. - await sampleTest.modPage.wasRemoved( + await modPage.wasRemoved( `${e.pickRandomUserPreviouslyPickedList} li`, '"Previously picked" list should be empty after clicking "Clear All"', ELEMENT_WAIT_LONGER_TIME, ); }); - test('should drop available count to 0 and show "no users" warning after picking with "Include already picked users" unchecked', async ({ sampleTest }): Promise => { + test('should drop available count to 0 and show "no users" warning after picking with "Include already picked users" unchecked', async (): Promise => { // With includePickedUsers=false (default), a picked user is immediately // removed from the eligible pool after being selected. - await openModal(sampleTest.modPage); - await enableInclusionFilters(sampleTest.modPage); // does NOT enable includePickedUsers + await openModal(modPage); + await enableInclusionFilters(modPage); // does NOT enable includePickedUsers // Verify 1 user is available before picking. - await sampleTest.modPage.hasText( + await modPage.hasText( e.pickRandomUserAvailableContent, '1 user', '"1 user" should be displayed before picking', ); - await pickUser(sampleTest.modPage); - await goBackToPresenterView(sampleTest.modPage); + await pickUser(modPage); + await goBackToPresenterView(modPage); // After picking, the presenter is now in the picked list and excluded. - await sampleTest.modPage.hasElement( + await modPage.hasElement( e.pickRandomUserNoUsersWarning, 'no-users warning should appear: the only user was already picked', ); - await sampleTest.modPage.wasRemoved( + await modPage.wasRemoved( e.pickRandomUserPickButton, 'pick button should be hidden when there are 0 eligible users', ELEMENT_WAIT_TIME, ); }); - test('should restore the pick button after "Clear All" resets the picked-user history', async ({ sampleTest }): Promise => { + test('should restore the pick button after "Clear All" resets the picked-user history', async (): Promise => { // Scenario: pick once (user excluded) → warning shown → Clear All → user eligible again. - await openModal(sampleTest.modPage); - await enableInclusionFilters(sampleTest.modPage); // includePickedUsers stays OFF - await pickUser(sampleTest.modPage); - await goBackToPresenterView(sampleTest.modPage); + await openModal(modPage); + await enableInclusionFilters(modPage); // includePickedUsers stays OFF + await pickUser(modPage); + await goBackToPresenterView(modPage); // No-users warning is shown (picked user excluded). - await sampleTest.modPage.hasElement( + await modPage.hasElement( e.pickRandomUserNoUsersWarning, 'no-users warning should be visible before Clear All', ); // Clear the pick history – the user is no longer in the "picked" list. - await sampleTest.modPage.page.click(e.pickRandomUserClearAllButton); + await modPage.page.click(e.pickRandomUserClearAllButton); // The user is now eligible again, so the pick button should reappear. - await sampleTest.modPage.hasElement( + await modPage.hasElement( e.pickRandomUserPickButton, 'pick button should reappear after clearing the pick history', ELEMENT_WAIT_LONGER_TIME, ); - await sampleTest.modPage.wasRemoved( + await modPage.wasRemoved( e.pickRandomUserNoUsersWarning, 'no-users warning should disappear once the pick button is available again', ELEMENT_WAIT_TIME, diff --git a/tests/core/constants.ts b/tests/core/constants.ts index 301b95c..5df6056 100644 --- a/tests/core/constants.ts +++ b/tests/core/constants.ts @@ -12,17 +12,3 @@ export const ELEMENT_WAIT_TIME: number = 5000 * MULTIPLIER; export const ELEMENT_WAIT_LONGER_TIME: number = 10000 * MULTIPLIER; export const ELEMENT_WAIT_EXTRA_LONG_TIME: number = 15000 * MULTIPLIER; export const LOOP_INTERVAL: number = 1200; - -// STRESS TESTS VARS -export const JOIN_AS_MODERATOR_TEST_ROUNDS: number = 15; -export const MAX_JOIN_AS_MODERATOR_FAIL_RATE: number = 0.05; -export const BREAKOUT_ROOM_INVITATION_TEST_ROUNDS: number = 20; -export const JOIN_TWO_USERS_ROUNDS: number = 20; -export const JOIN_TWO_USERS_KEEPING_CONNECTED_ROUNDS: number = 20; -export const JOIN_TWO_USERS_EXCEEDING_MAX_PARTICIPANTS: number = 20; -export const MAX_PARTICIPANTS_TO_JOIN: number = 4; - -// MEDIA CONNECTION TIMEOUTS -export const VIDEO_LOADING_WAIT_TIME: number = 15000; -export const UPLOAD_PDF_WAIT_TIME: number = 25000 * MULTIPLIER; -export const CUSTOM_MEETING_ID: string = 'custom-meeting'; diff --git a/tests/core/coreElements.ts b/tests/core/coreElements.ts index c2892f0..6d07619 100644 --- a/tests/core/coreElements.ts +++ b/tests/core/coreElements.ts @@ -4,19 +4,4 @@ export const coreElements = { errorMessageLabel: 'span[id="error-message"]', whiteboard: 'div[data-testid="canvas"]', actions: 'button[data-test="actionsButton"]', - // audio - joinAudioButton: 'button[data-test="joinAudio"]', - microphoneBtn: 'button[data-test="microphoneBtn"]', - joinEchoTestButton: 'button[data-test="joinEchoTestButton"]', - audioDropdownMenu: 'button[data-test="audioDropdownMenu"]', - establishingAudioLabel: 'span[data-test="establishingAudioLabel"]', - // video - joinVideoButton: 'button[data-test="joinVideo"]', - startSharingWebcam: 'button[data-test="startSharingWebcam"]', - leaveVideo: 'button[data-test="leaveVideo"]', - videoDropdownMenu: 'button[data-test="videoDropdownMenu"]', - // presentation - presentationToolbarWrapper: 'div[id="presentationToolbarWrapper"]', - skipSlide: 'select[data-test="skipSlide"]', - nextSlide: 'button[data-test="nextSlide"]', }; diff --git a/tests/core/fixtures/sampleBeforeAll.ts b/tests/core/fixtures/pluginBeforeAll.ts similarity index 100% rename from tests/core/fixtures/sampleBeforeAll.ts rename to tests/core/fixtures/pluginBeforeAll.ts diff --git a/tests/core/fixtures/sampleFixture.ts b/tests/core/fixtures/sampleFixture.ts deleted file mode 100644 index 9536279..0000000 --- a/tests/core/fixtures/sampleFixture.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-disable import/no-extraneous-dependencies */ -import { test as base } from '@playwright/test'; -import { Sample } from '../sample'; -import { encodeCustomParams } from '../helpers'; - -export interface SampleTestFixtures { - sampleTest: Sample; -} - -export type SampleTestConfig = { - envVarName: string; - getPluginUrl?: () => string | undefined; -}; - -export function createSampleTest(config: SampleTestConfig) { - let pluginUrl: string | undefined = process.env[config.envVarName]; - - const test = base.extend({ - sampleTest: async ({ browser, context, page }, use) => { - const customUrl = config.getPluginUrl?.() || pluginUrl; - if (!customUrl) { - throw new Error(`Plugin URL is not set. Either set ${config.envVarName} environment variable or ensure beforeAll has run successfully.`); - } - - const createParameter = encodeCustomParams(`pluginManifests=${JSON.stringify([{ url: customUrl }])}`); - const sampleTest = new Sample({ browser, context }); - await sampleTest.initModPage(page, { createParameter }); - await use(sampleTest); - }, - }); - - return { - test, - setPluginUrl: (url: string) => { - pluginUrl = url; - }, - getPluginUrl: () => pluginUrl, - }; -} diff --git a/tests/core/sample.ts b/tests/core/plugin.ts similarity index 87% rename from tests/core/sample.ts rename to tests/core/plugin.ts index afd0a42..6685b06 100644 --- a/tests/core/sample.ts +++ b/tests/core/plugin.ts @@ -1,17 +1,19 @@ import { Page, Browser, BrowserContext } from '@playwright/test'; import { InitOptions, SessionPage } from './sessionPage'; -interface SampleProps { +interface PluginProps { browser: Browser; context: BrowserContext; } -export class Sample { +export class Plugin { readonly browser: Browser; + readonly context: BrowserContext; + modPage!: SessionPage; - constructor({ browser, context }: SampleProps) { + constructor({ browser, context }: PluginProps) { this.browser = browser; this.context = context; } diff --git a/tests/core/scripts/copy-sample-to-container.sh b/tests/core/scripts/copy-sample-to-container.sh deleted file mode 100755 index 42689a0..0000000 --- a/tests/core/scripts/copy-sample-to-container.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -# Check if plugin name is provided -if [ -z "$1" ]; then - echo "Usage: $0 [containerName]" - exit 1 -fi - -# Set the "-e" flag to make the script exit immediately if any command fails. -set -e - -# Determine the directory of the script -SCRIPT_DIR=$(dirname "$(readlink -f "$0")") - -# Load .env file -ENV_FILE="$SCRIPT_DIR/../../../.env" # Adjusted path to .env -if [ -f "$ENV_FILE" ]; then - set -o allexport - source "$ENV_FILE" - set +o allexport -fi - -PLUGIN_NAME=$1 -CONTAINER_NAME=${2:-$LOCAL_CONTAINER_NAME} - -# check if container name is provided -if [ -z "$CONTAINER_NAME" ]; then - echo "Container name is not provided. Exiting." - exit 1 -fi - -PLUGINS_FOLDER_CONTAINER_PATH="/var/www/bigbluebutton-default/assets/plugins" -PLUGIN_CONTAINER_PATH="$PLUGINS_FOLDER_CONTAINER_PATH/$PLUGIN_NAME" - -# Check if container is running -if ! docker ps -q --filter "name=$CONTAINER_NAME" | grep -q .; then - echo "Container $CONTAINER_NAME is not running. Exiting." - exit 1 -fi - -# Check if the plugins folder exists in the container -if docker exec $CONTAINER_NAME [ -d "$PLUGIN_CONTAINER_PATH" ]; then - # Remove the existing files in the container path - docker exec $CONTAINER_NAME rm -rf "$PLUGIN_CONTAINER_PATH" -fi - -echo "Creating container path: $PLUGIN_CONTAINER_PATH..." -docker exec $CONTAINER_NAME mkdir -p "$PLUGIN_CONTAINER_PATH" - -echo "Copying dist contents to container path: $PLUGIN_CONTAINER_PATH..." -docker cp dist/. $CONTAINER_NAME:$PLUGIN_CONTAINER_PATH/dist - -# Check if the copy was successful -if [ $? -ne 0 ]; then - echo "Failed to copy dist to container." - exit 1 -fi diff --git a/tests/structural/test.spec.ts b/tests/structural/test.spec.ts index 7ae5e03..e8c55b4 100644 --- a/tests/structural/test.spec.ts +++ b/tests/structural/test.spec.ts @@ -1,105 +1,150 @@ -import { createSampleTest } from '../core/fixtures/sampleFixture'; -import { checkPluginAvailability } from '../core/fixtures/sampleBeforeAll'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { test, BrowserContext } from '@playwright/test'; +import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; import { elements as e } from '../elements'; import { SessionPage as ModPage } from '../core/sessionPage'; +import { Plugin } from '../core/plugin'; +import { encodeCustomParams } from '../core/helpers'; const PLUGIN_NAME = 'pick-random-user-plugin'; const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; -const { test, setPluginUrl, getPluginUrl } = createSampleTest({ - envVarName: ENV_VAR_NAME, - getPluginUrl: () => process.env[ENV_VAR_NAME], -}); +let pluginUrl: string | undefined = process.env[ENV_VAR_NAME]; +const setPluginUrl = (url: string) => { pluginUrl = url; }; +const getPluginUrl = () => pluginUrl; /** Helper: open the plugin modal from the actions dropdown. */ -async function openPickRandomUserModal( - modPage: ModPage, -) { +async function openPickRandomUserModal(modPage: ModPage) { await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); await modPage.page.click(e.actions); await modPage.hasElement(e.pickRandomUserActionButton, 'action button should be visible'); await modPage.page.click(e.pickRandomUserActionButton); } -test.describe.parallel('Pick Random User Plugin – Structural', () => { - test.beforeAll(checkPluginAvailability({ - pluginName: PLUGIN_NAME, - envVarName: ENV_VAR_NAME, - setPluginUrl, - getPluginUrl, - })); - - test('should show "Pick random user" label in the actions dropdown for a presenter', async ({ sampleTest }): Promise => { - await sampleTest.modPage.page.waitForSelector( - e.whiteboard, - { timeout: ELEMENT_WAIT_LONGER_TIME }, - ); - await sampleTest.modPage.page.click(e.actions); - await sampleTest.modPage.hasElement( - e.pickRandomUserActionButton, - 'should display the plugin action-button item', +/** + * Reset state after each test: close the presenter modal if open, + * or dismiss the actions dropdown if it was left open without opening the modal. + */ +async function cleanupAfterTest(modPage: ModPage): Promise { + // If on the picked-user view, go back to the presenter view first. + if (await modPage.page.locator(e.pickRandomUserPickedUserViewTitle).isVisible()) { + await modPage.page.click(e.pickRandomUserBackButton); + await modPage.page.locator(e.pickRandomUserModalCloseButton).waitFor({ state: 'visible', timeout: ELEMENT_WAIT_TIME }); + } + + // If the modal is open, clear the picked-user history and close it. + const closeBtn = modPage.page.locator(e.pickRandomUserModalCloseButton); + if (await closeBtn.isVisible()) { + const clearBtn = modPage.page.locator(e.pickRandomUserClearAllButton); + if (await clearBtn.isVisible()) { + await modPage.page.click(e.pickRandomUserClearAllButton); + } + await closeBtn.click(); + return; + } + + // If only the actions dropdown is open (modal was never opened), dismiss it. + if (await modPage.page.locator(e.pickRandomUserActionButton).isVisible()) { + await modPage.page.keyboard.press('Escape'); + } +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +test.describe('Pick Random User Plugin - Structural', () => { + test.describe.configure({ mode: 'serial' }); + + let modPage: ModPage; + let sharedContext: BrowserContext; + + test.beforeAll(async ({ browser, request }, testInfo) => { + await checkPluginAvailability({ + pluginName: PLUGIN_NAME, + envVarName: ENV_VAR_NAME, + setPluginUrl, + getPluginUrl, + })({ request }, testInfo); + + const resolvedUrl = getPluginUrl(); + if (!resolvedUrl) return; + + const createParameter = encodeCustomParams( + `pluginManifests=${JSON.stringify([{ url: resolvedUrl }])}`, ); - await sampleTest.modPage.hasText( + sharedContext = await browser.newContext({ + permissions: ['clipboard-read', 'clipboard-write', 'camera', 'microphone'], + viewport: { width: 1280, height: 720 }, + }); + const page = await sharedContext.newPage(); + const plugin = new Plugin({ browser, context: sharedContext }); + await plugin.initModPage(page, { createParameter }); + modPage = plugin.modPage; + }); + + test.afterAll(async () => { + await sharedContext?.close(); + }); + + test.afterEach(async () => { + if (modPage) await cleanupAfterTest(modPage); + }); + + test('should show "Pick random user" label in the actions dropdown for a presenter', async (): Promise => { + await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); + await modPage.page.click(e.actions); + await modPage.hasElement(e.pickRandomUserActionButton, 'should display the plugin action-button item'); + await modPage.hasText( e.pickRandomUserActionButton, 'Pick random user', 'should display the correct label "Pick random user"', ); }); - test('should open the presenter modal when clicking the action-button option', async ({ sampleTest }): Promise => { - await openPickRandomUserModal(sampleTest.modPage); - await sampleTest.modPage.hasElement( + test('should open the presenter modal when clicking the action-button option', async (): Promise => { + await openPickRandomUserModal(modPage); + await modPage.hasElement( e.includeModeratorsCheckbox, 'should show the modal presenter view with the "Include moderators" checkbox', ); - await sampleTest.modPage.hasElement( + await modPage.hasElement( e.pickRandomUserAvailableContent, 'should display the "Available for selection" section', ); }); - test('should display all three filter checkboxes in the presenter view', async ({ sampleTest }): Promise => { - await openPickRandomUserModal(sampleTest.modPage); - await sampleTest.modPage.hasElement( - e.includeModeratorsCheckbox, - 'should display the "Include moderators" checkbox', - ); - await sampleTest.modPage.hasElement( - e.includePresenterCheckbox, - 'should display the "Include presenter" checkbox', - ); - await sampleTest.modPage.hasElement( - e.includePickedUsersCheckbox, - 'should display the "Include already picked user" checkbox', - ); + test('should display all three filter checkboxes in the presenter view', async (): Promise => { + await openPickRandomUserModal(modPage); + await modPage.hasElement(e.includeModeratorsCheckbox, 'should display the "Include moderators" checkbox'); + await modPage.hasElement(e.includePresenterCheckbox, 'should display the "Include presenter" checkbox'); + await modPage.hasElement(e.includePickedUsersCheckbox, 'should display the "Include already picked user" checkbox'); }); - test('should have all three filter checkboxes unchecked by default', async ({ sampleTest }): Promise => { - await openPickRandomUserModal(sampleTest.modPage); + test('should have all three filter checkboxes unchecked by default', async (): Promise => { + await openPickRandomUserModal(modPage); await test.expect( - sampleTest.modPage.getLocator(e.includeModeratorsCheckbox), + modPage.getLocator(e.includeModeratorsCheckbox), '"Include moderators" should be unchecked by default', ).not.toBeChecked(); await test.expect( - sampleTest.modPage.getLocator(e.includePresenterCheckbox), + modPage.getLocator(e.includePresenterCheckbox), '"Include presenter" should be unchecked by default', ).not.toBeChecked(); await test.expect( - sampleTest.modPage.getLocator(e.includePickedUsersCheckbox), + modPage.getLocator(e.includePickedUsersCheckbox), '"Include already picked user" should be unchecked by default', ).not.toBeChecked(); }); - test('should show "no users" warning and hide the pick button with default filters (only presenter in meeting)', async ({ sampleTest }): Promise => { + test('should show "no users" warning and hide the pick button with default filters (only presenter in meeting)', async (): Promise => { // Default: includeModerators=false, includePresenter=false → // the single moderator/presenter user is excluded by both rules → 0 eligible. - await openPickRandomUserModal(sampleTest.modPage); - await sampleTest.modPage.hasElement( + await openPickRandomUserModal(modPage); + await modPage.hasElement( e.pickRandomUserNoUsersWarning, 'should show the "No {0} available" warning when 0 users are eligible', ); - await sampleTest.modPage.wasRemoved( + await modPage.wasRemoved( e.pickRandomUserPickButton, 'should NOT render the pick button when there are no eligible users', ELEMENT_WAIT_TIME, From c80d0fa518c92fd40b85e0ecee0d615dadaac316 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Apr 2026 07:16:17 -0400 Subject: [PATCH 066/117] chore: rename repository references from plugin-X to bbb-plugin-X (#90) - Update package.json and package-lock.json name to bbb-plugin-pick-random-user - Update webpack.config.js filename/library to BbbPluginPickRandomUser - Update manifest.json name and entrypoint to BbbPluginPickRandomUser - Update README.md references (plugin name, build path, output filename) - Update debian/control homepage URL to new repo name Agent-Logs-Url: https://github.com/bigbluebutton/bbb-plugin-pick-random-user/sessions/d7f61ccb-6f39-4d13-a509-ce6829758a36 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: antobinary <6312397+antobinary@users.noreply.github.com> --- README.md | 14 +++++++------- debian/control | 2 +- manifest.json | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- webpack.config.js | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e811bba..f60f2b1 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ If you have any suggestions, requirements, or questions, don’t hesitate to con Down below, we list all the possible configurations this plugin supports, and then a brief explanation: ```yaml -- name: PickRandomUserPlugin +- name: BbbPluginPickRandomUser settings: pingSoundEnabled: true pingSoundUrl: resources/sounds/doorbell.mp3 @@ -60,7 +60,7 @@ After the countdown finishes, all close methods become available. To change this public: # ... plugins: - - name: PickRandomUserPlugin + - name: BbbPluginPickRandomUser settings: preventCloseDelaySeconds: 5 # Set to 0 to disable the delay ``` @@ -73,7 +73,7 @@ By default, browser notification when user is randomly picked is not enabled. To public: # ... plugins: - - name: PickRandomUserPlugin + - name: BbbPluginPickRandomUser settings: browserNotificationEnabled: false ``` @@ -85,7 +85,7 @@ By default, ping sound is played for the randomly picked user. To remove this fe So within that file and in `public.plugins` add the following configurations: ```yaml -- name: PickRandomUserPlugin +- name: BbbPluginPickRandomUser settings: pingSoundEnabled: false ``` @@ -96,7 +96,7 @@ The result yaml will look like: public: # ... plugins: - - name: PickRandomUserPlugin + - name: BbbPluginPickRandomUser settings: pingSoundEnabled: false ``` @@ -124,12 +124,12 @@ These are the possible `mp3` that already come within a BBB server, if you want To build the plugin for production use, follow these steps: ```bash -cd $HOME/src/plugin-pick-random-user-plugin +cd $HOME/src/bbb-plugin-pick-random-user npm ci npm run build-bundle ``` -The above command will generate the `dist` folder, containing the bundled JavaScript file named `PickRandomUserPlugin.js`, a directory of locale files and a license files. These files can be hosted on any HTTPS server along with the `manifest.json` which is also part of the `dist/` directory. +The above command will generate the `dist` folder, containing the bundled JavaScript file named `BbbPluginPickRandomUser.js`, a directory of locale files and a license files. These files can be hosted on any HTTPS server along with the `manifest.json` which is also part of the `dist/` directory. If you install the Plugin separated to the manifest, remember to change the `javascriptEntrypointUrl` in the `manifest.json` to the correct endpoint. diff --git a/debian/control b/debian/control index 9b18e8b..0405041 100644 --- a/debian/control +++ b/debian/control @@ -4,7 +4,7 @@ Priority: extra Maintainer: Anton Georgiev Build-Depends: debhelper (>= 13), nodejs (>= 18) Standards-Version: 4.1.4 -Homepage: https://github.com/bigbluebutton/plugin-pick-random-user +Homepage: https://github.com/bigbluebutton/bbb-plugin-pick-random-user Package: bbb-plugin-pick-random-user Architecture: all diff --git a/manifest.json b/manifest.json index 49ff3d5..2713334 100644 --- a/manifest.json +++ b/manifest.json @@ -1,8 +1,8 @@ { "requiredSdkVersion": "~0.0.65", - "name": "PickRandomUserPlugin", + "name": "BbbPluginPickRandomUser", "version": "0.0.8", - "javascriptEntrypointUrl": "PickRandomUserPlugin.js", + "javascriptEntrypointUrl": "BbbPluginPickRandomUser.js", "localesBaseUrl": "locales", "dataChannels": [ { diff --git a/package-lock.json b/package-lock.json index 806f3c2..975fe40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "pick-random-user-plugin", + "name": "bbb-plugin-pick-random-user", "version": "0.0.8", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "pick-random-user-plugin", + "name": "bbb-plugin-pick-random-user", "version": "0.0.8", "dependencies": { "@testing-library/react": "^13.4.0", diff --git a/package.json b/package.json index 4ce1f46..7b5ad6f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "pick-random-user-plugin", + "name": "bbb-plugin-pick-random-user", "version": "0.0.8", "private": true, "main": "./src/index.tsx", diff --git a/webpack.config.js b/webpack.config.js index c6ec8a5..0282138 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -5,8 +5,8 @@ const path = require('path'); module.exports = { entry: './src/index.tsx', output: { - filename: 'PickRandomUserPlugin.js', - library: 'PickRandomUserPlugin', + filename: 'BbbPluginPickRandomUser.js', + library: 'BbbPluginPickRandomUser', libraryTarget: 'umd', publicPath: '/', globalObject: 'this', From 0dc06b1d77e979010cf33f2ce3b17436da33a06e Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 May 2026 11:04:30 -0300 Subject: [PATCH 067/117] [ref-ui] refactor the ui and design to feel more up-to-date - Updated tests along --- package.json | 1 + playwright.config.ts | 2 +- public/locales/de.json | 8 +- public/locales/en.json | 6 + public/locales/fr-FR.json | 8 +- public/locales/it.json | 8 +- public/locales/ja.json | 8 +- public/locales/pt-BR.json | 8 +- src/components/modal/component.tsx | 24 +- .../modal/picked-user-view/styles.tsx | 18 +- .../modal/presenter-view/component.tsx | 415 +++++++++++------- .../modal/presenter-view/styles.tsx | 339 +++++++++++--- src/components/modal/styles.tsx | 68 ++- tests/README.md | 10 +- tests/behavioral/helpers.ts | 20 + tests/behavioral/multi-user.spec.ts | 40 +- tests/behavioral/single-user.spec.ts | 61 +-- tests/structural/test.spec.ts | 37 +- 18 files changed, 742 insertions(+), 339 deletions(-) diff --git a/package.json b/package.json index 863aa3e..de88cd6 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "lint:watch": "watch 'yarn lint'", "publish-plugin:dev": "bash scripts/copy-plugin-to-container.sh", "test": "npx playwright test", + "test:isolated": "TEST_MEETINGS=isolated npx playwright test", "test-chromium-ci": "export CI='true' && npx playwright test --project=chromium" }, "browserslist": { diff --git a/playwright.config.ts b/playwright.config.ts index 49bf569..dd276dd 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ fullyParallel: true, forbidOnly: CI, reporter: CI - ? [['list'], ['blob']] + ? [['blob'], ['./core/setup/customReporter.ts']] : [['list'], ['html', { open: 'never' }]], use: { baseURL: server, diff --git a/public/locales/de.json b/public/locales/de.json index f9e7c1d..e163cf4 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -18,5 +18,11 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Wähle {0}", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Wähle erneut", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Wähle zufälligen Teilnehmer", - "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Zeige zuletzt gewählten Teilnehmer" + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Zeige zuletzt gewählten Teilnehmer", + "pickRandomUserPlugin.modal.title": "Zufälligen Teilnehmer wählen", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Schließen", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "Keine {0} zur Auswahl verfügbar", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "Noch kein Teilnehmer ausgewählt", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "Moderator", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "Präsentator" } diff --git a/public/locales/en.json b/public/locales/en.json index 383e974..3aac237 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -19,6 +19,12 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Pick again", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Pick random user", "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Display last randomly picked user", + "pickRandomUserPlugin.modal.title": "Pick random user", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Close", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "No {0} available for selection", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "No user selected yet", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "moderator", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "presenter", "pickRandomUserPlugin.modal.closeDelayMessage": "You can close this modal in {seconds} seconds", "pickRandomUserPlugin.modal.closeDelayMessageSingular": "You can close this modal in {seconds} second" } diff --git a/public/locales/fr-FR.json b/public/locales/fr-FR.json index 716933a..04ad3cd 100644 --- a/public/locales/fr-FR.json +++ b/public/locales/fr-FR.json @@ -17,5 +17,11 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Sélectionner {0}", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Sélectionner à nouveau", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Sélectionner un participant au hasard", - "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Afficher le dernier participant sélectionné au hasard" + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Afficher le dernier participant sélectionné au hasard", + "pickRandomUserPlugin.modal.title": "Sélectionner un participant au hasard", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Fermer", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "Aucun {0} disponible pour la sélection", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "Aucun participant sélectionné pour l'instant", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "modérateur", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "présentateur" } diff --git a/public/locales/it.json b/public/locales/it.json index 4a5591c..7f312ab 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -17,5 +17,11 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Scegli {0}", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Scegli di nuovo", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Scegli un utente casuale", - "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Visualizza l'ultimo utente scelto casualmente" + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Visualizza l'ultimo utente scelto casualmente", + "pickRandomUserPlugin.modal.title": "Scegli un utente casuale", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Chiudi", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "Nessun {0} disponibile per la selezione", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "Nessun utente selezionato ancora", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "moderatore", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "presentatore" } diff --git a/public/locales/ja.json b/public/locales/ja.json index d21e691..fcc10c0 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -18,5 +18,11 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "{0}から指名する", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "もう一度指名する", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "ランダムに指名する", - "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "最後に指名されたユーザーを表示" + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "最後に指名されたユーザーを表示", + "pickRandomUserPlugin.modal.title": "ランダムにユーザーを選択", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "閉じる", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "選択可能な{0}はいません", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "まだユーザーが選択されていません", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "モデレーター", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "発表者" } diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json index 4cceabe..c2c239c 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -18,5 +18,11 @@ "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickUser": "Selecionar {0}", "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.pickButtonLabel.pickAgain": "Selecionar novamente", "pickRandomUserPlugin.actionsButtonDropdown.label.pickUser": "Selecionar usuário aleatório", - "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Exibir último usuário selecionado aleatoriamente" + "pickRandomUserPlugin.actionsButtonDropdown.label.viewLastPickedUser": "Exibir último usuário selecionado aleatoriamente", + "pickRandomUserPlugin.modal.title": "Selecionar usuário aleatório", + "pickRandomUserPlugin.modal.closeButton.ariaLabel": "Fechar", + "pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "Nenhum {0} disponível para seleção", + "pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "Nenhum usuário selecionado ainda", + "pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "moderador", + "pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "apresentador" } diff --git a/src/components/modal/component.tsx b/src/components/modal/component.tsx index 9d91194..6de75a8 100644 --- a/src/components/modal/component.tsx +++ b/src/components/modal/component.tsx @@ -13,6 +13,16 @@ const intlMessages = defineMessages({ description: 'Title to show that current user has been picked', defaultMessage: 'You have been randomly picked', }, + modalTitle: { + id: 'pickRandomUserPlugin.modal.title', + description: 'Title of the pick random user modal', + defaultMessage: 'Pick random user', + }, + closeButtonAriaLabel: { + id: 'pickRandomUserPlugin.modal.closeButton.ariaLabel', + description: 'Aria label for the modal close button', + defaultMessage: 'Close', + }, }); export function PickUserModal(props: PickUserModalProps) { @@ -80,18 +90,19 @@ export function PickUserModal(props: PickUserModalProps) { shouldCloseOnOverlayClick={canClose} shouldCloseOnEsc={canClose} > - + + + {intl.formatMessage(intlMessages.modalTitle)} + - + - + { showPresenterView ? ( @@ -121,7 +132,6 @@ export function PickUserModal(props: PickUserModalProps) { }} /> ) - } ); diff --git a/src/components/modal/picked-user-view/styles.tsx b/src/components/modal/picked-user-view/styles.tsx index 8ab2364..6e74782 100644 --- a/src/components/modal/picked-user-view/styles.tsx +++ b/src/components/modal/picked-user-view/styles.tsx @@ -11,7 +11,7 @@ const PickedUserViewWrapper = styled.div` const PickedUserViewTitle = styled.h1` font-weight: 600; - font-size: 20px; + font-size: 1.25rem; `; const PickedUserAvatarInitials = styled.div` @@ -39,17 +39,18 @@ const PickedUserAvatarImage = styled.img` `; const PickedUserName = styled.p` - font-size: 30px; + font-size: 1.875rem; font-weight: 500; `; const BackButton = styled.button` - border: 3px solid transparent; + border: 0.1875rem solid transparent; + margin-bottom: 1rem; overflow: visible; display: inline-block; background-color: var(--btn-primary-bg, var(--color-primary, #0F70D7)); color: var(--btn-primary-color, var(--color-white, #FFF)); - border-radius: 2px; + border-radius: 0.125rem; font-weight: 600; line-height: 1; text-align: center; @@ -57,7 +58,7 @@ const BackButton = styled.button` vertical-align: middle; cursor: pointer; user-select: none; - padding: 8px 15px; + padding: 0.5rem 0.9375rem; &:hover { color: var(--btn-primary-color, var(--color-white, #FFF)); background-color: var(--btn-primary-hover-bg, #0C57A7) !important; @@ -79,11 +80,10 @@ const CountdownMessage = styled.div` const CountdownBarContainer = styled.div` width: 100%; - height: 4px; + height: 0.25rem; background-color: #e9ecef; - border-radius: 2px; + border-radius: 0.125rem; overflow: hidden; - margin-top: 1rem; `; const CountdownBar = styled.div<{ progress: number }>` @@ -91,7 +91,7 @@ const CountdownBar = styled.div<{ progress: number }>` background: linear-gradient(90deg, #0F70D7 0%, #0C57A7 100%); width: ${({ progress }) => progress}%; transition: width linear 0.1s; - border-radius: 2px; + border-radius: 0.125rem; `; export { diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index b771eab..f9c5760 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -23,7 +23,7 @@ const intlMessages = defineMessages({ includePresenterLabel: { id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel', description: 'Label of skip presenter`s option on modal`s presenter view', - defaultMessage: 'Skip presenter', + defaultMessage: 'Include presenter', }, includePickedUsersLabel: { id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel', @@ -80,37 +80,82 @@ const intlMessages = defineMessages({ description: 'Label of the button to pick another user', defaultMessage: 'Pick again', }, + emptyState: { + id: 'pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState', + description: 'Empty state text shown when no user has been picked yet', + defaultMessage: 'No user selected yet', + }, + availableEmptyState: { + id: 'pickRandomUserPlugin.modal.presenterView.availableSection.emptyState', + description: 'Empty state text shown when no user is available for selection', + defaultMessage: 'No {0} available for selection', + }, + moderatorRoleLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.roleLabel.moderator', + description: 'Role badge label for moderators', + defaultMessage: 'moderator', + }, + presenterRoleLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.roleLabel.presenter', + description: 'Role badge label for presenters', + defaultMessage: 'presenter', + }, }); -const MAX_NAMES_TO_SHOW = 3; +const FALLBACK_AVATAR_COLORS = ['#4E7FF8', '#2BA084', '#E07A3A', '#7B61D9', '#D4733B']; -const makeHorizontalListOfNames = (list?: PickedUser[]) => { - const length = list?.length; - const formattedList = list?.filter((_, index) => { - if (length > MAX_NAMES_TO_SHOW) return index < MAX_NAMES_TO_SHOW; - return true; - }).reduce((accumulator, currentValue) => ((accumulator !== '') ? `${accumulator}, ${currentValue.name}` : currentValue.name), ''); - if (length > MAX_NAMES_TO_SHOW) return `${formattedList}...`; - return formattedList; -}; +function getAvatarColorFallback(name: string): string { + const hash = name.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); + return FALLBACK_AVATAR_COLORS[hash % FALLBACK_AVATAR_COLORS.length]; +} + +function getInitials(name: string): string { + return name.slice(0, 2); +} -const makeVerticalListOfNames = ( - list?: DataChannelEntryResponseType[], -) => list?.filter((u) => !!u.payloadJson).map((u) => { - const time = new Date(u.createdAt); - const timeMiliseconds = time.getTime(); +function ShieldIcon() { return ( -
        • - {u.payloadJson.name} - {' '} - ( - {time.getHours().toString().padStart(2, '0')} - : - {time.getMinutes().toString().padStart(2, '0')} - ) -
        • + + + ); -}); +} + +function PresentationIcon() { + return ( + + + + ); +} + +function UserCheckIcon() { + return ( + + + + ); +} + +function makePickedUserRows(list?: DataChannelEntryResponseType[]) { + return list?.filter((u) => !!u.payloadJson).map((u) => { + const time = new Date(u.createdAt); + const { avatar, color, name } = u.payloadJson; + const initials = getInitials(name); + return ( + + {avatar ? ( + + ) : ( + + {initials} + + )} + {name} + + ); + }); +} export function PresenterViewComponent(props: PresenterViewComponentProps) { const { @@ -122,144 +167,202 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { users, } = props; - const { - filterOptions, - setFilterOptions, - } = useContext(FilterOptionsContext); + const { filterOptions, setFilterOptions } = useContext(FilterOptionsContext); + const { includeModerators, includePresenter, includePickedUsers } = filterOptions; - const { - includeModerators, - includePresenter, - includePickedUsers, - } = filterOptions; + const usersCount = users?.length ?? 0; + const userRoleLabel = (() => { + if (!includeModerators) { + return usersCount !== 1 + ? intl.formatMessage(intlMessages.viewerLabelPlural, { 0: usersCount }) + : intl.formatMessage(intlMessages.viewerLabel, { 0: usersCount }); + } + return usersCount !== 1 + ? intl.formatMessage(intlMessages.userLabelPlural, { 0: usersCount }) + : intl.formatMessage(intlMessages.userLabel, { 0: usersCount }); + })(); + + const hasPickedUsers = dataChannelPickedUsers?.some((u) => !!u.payloadJson); - let userRoleLabel: string; - const usersCountVariable = { 0: users?.length }; - if (!includeModerators) { - userRoleLabel = (users?.length !== 1) - ? intl.formatMessage(intlMessages.viewerLabelPlural, usersCountVariable) - : intl.formatMessage(intlMessages.viewerLabel, usersCountVariable); - } else { - userRoleLabel = (users?.length !== 1) - ? intl.formatMessage(intlMessages.userLabelPlural, usersCountVariable) - : intl.formatMessage(intlMessages.userLabel, usersCountVariable); - } return ( - - - - {intl.formatMessage(intlMessages.optionsTitle)} - - - - { - setFilterOptions((filterOptionsPrevious) => ({ - ...filterOptionsPrevious, - includeModerators: !filterOptionsPrevious.includeModerators, - })); - }} - name="options" - value="includeModerators" - /> - - {intl.formatMessage(intlMessages.includeModeratorsLabel)} - - - - { - setFilterOptions((filterOptionsPrevious) => ({ - ...filterOptionsPrevious, - includePresenter: !filterOptionsPrevious.includePresenter, - })); - }} - name="options" - value="includePresenter" - /> - - {intl.formatMessage(intlMessages.includePresenterLabel)} - - - - { - setFilterOptions((filterOptionsPrevious) => ({ - ...filterOptionsPrevious, - includePickedUsers: !filterOptionsPrevious.includePickedUsers, - })); - }} - name="options" - value="includePickedUsers" - /> - - {intl.formatMessage(intlMessages.includePickedUsersLabel)} - - - - - - - {intl.formatMessage(intlMessages.availableTitle)} - - - {`${users?.length} ${userRoleLabel}: `} - {makeHorizontalListOfNames(users)} - - - - - - {intl.formatMessage(intlMessages.previouslyPickedTitle)} - - { - deletionFunction([RESET_DATA_CHANNEL]); - }} - > - {intl.formatMessage(intlMessages.clearButtonLabel)} - - - - - { - makeVerticalListOfNames(dataChannelPickedUsers) - } - - - - { - users?.length > 0 ? ( - + + + {/* OPTIONS SECTION */} + + + {intl.formatMessage(intlMessages.optionsTitle)} + + + + + + + + + {intl.formatMessage(intlMessages.includeModeratorsLabel)} + + + { + setFilterOptions((prev) => ({ + ...prev, + includeModerators: !prev.includeModerators, + })); + }} + /> + + + + + + + + + {intl.formatMessage(intlMessages.includePresenterLabel)} + + + { + setFilterOptions((prev) => ({ + ...prev, + includePresenter: !prev.includePresenter, + })); + }} + /> + + + + + + + + + {intl.formatMessage(intlMessages.includePickedUsersLabel)} + + + { + setFilterOptions((prev) => ({ + ...prev, + includePickedUsers: !prev.includePickedUsers, + })); + }} + /> + + + + + {/* AVAILABLE USERS SECTION */} + + + + {intl.formatMessage(intlMessages.availableTitle)} + + + {usersCount} + {' '} + {userRoleLabel} + + + {usersCount === 0 ? ( + + + {intl.formatMessage(intlMessages.availableEmptyState, { 0: userRoleLabel })} + + + ) : ( + + {users?.map((user) => { + const initials = getInitials(user.name); + let roleBadgeLabel: string | null = null; + if (user.role === 'MODERATOR') { + roleBadgeLabel = intl.formatMessage(intlMessages.moderatorRoleLabel); + } else if (user.presenter) { + roleBadgeLabel = intl.formatMessage(intlMessages.presenterRoleLabel); + } + return ( + + {user.avatar ? ( + + ) : ( + + {initials} + + )} + {user.name} + {roleBadgeLabel && ( + {roleBadgeLabel} + )} + + ); + })} + + )} + + + {/* PREVIOUSLY PICKED SECTION */} + + + + {intl.formatMessage(intlMessages.previouslyPickedTitle)} + + deletionFunction([RESET_DATA_CHANNEL])} + > + {intl.formatMessage(intlMessages.clearButtonLabel)} + + + {hasPickedUsers ? ( + + + {makePickedUserRows(dataChannelPickedUsers)} + + + ) : ( + <> + + + {intl.formatMessage(intlMessages.emptyState)} + + + {/* Empty list kept in DOM so [data-test] li selectors resolve correctly */} + + + )} + + + + + {/* FOOTER */} + + {usersCount > 0 ? ( + { - handlePickRandomUser(); - }} + onClick={handlePickRandomUser} > - { - (pickedUserWithEntryId) + {pickedUserWithEntryId ? intl.formatMessage(intlMessages.pickAgainButtonLabel) - : intl.formatMessage(intlMessages.pickButtonLabel, { 0: userRoleLabel }) - } - + : intl.formatMessage(intlMessages.pickButtonLabel, { 0: userRoleLabel })} + ) : ( -

          + {intl.formatMessage(intlMessages.noUsersWarning, { 0: userRoleLabel })} -

          - ) - } -
          + + )} + + ); } diff --git a/src/components/modal/presenter-view/styles.tsx b/src/components/modal/presenter-view/styles.tsx index fb2b8d7..bc440d9 100644 --- a/src/components/modal/presenter-view/styles.tsx +++ b/src/components/modal/presenter-view/styles.tsx @@ -1,108 +1,313 @@ import styled from 'styled-components'; -const PresenterViewContentWrapper = styled.div` - width: '100%'; - height: '100%'; - align-items: 'flex-start'; - display: 'flex'; - flex-direction: 'column'; +// ── Section labels ──────────────────────────────────────────────────────────── + +const SectionLabel = styled.span` + font-size: 0.6875rem; + font-weight: 600; + color: #8B9AAF; + text-transform: uppercase; + letter-spacing: 0.6px; +`; + +// ── Options section (toggle rows) ───────────────────────────────────────────── + +const OptionsContainer = styled.div` + margin-top: 0.5rem; `; -const PresenterViewSectionWrapper = styled.div` +const ToggleRow = styled.label` display: flex; - align-items: flex-start; - flex-direction: column; - max-height: 30%; + align-items: center; + justify-content: space-between; + padding: 0.4375rem 0; + cursor: pointer; + border-bottom: 1px solid #F2F4F7; + + &:last-child { + border-bottom: none; + } `; -const PresenterViewSectionTitle = styled.div` - margin: 5px 0px; - font-weight: 600; - font-size: 20px; +const ToggleRowLeft = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; `; -const PresenterViewSectionClearAllButton = styled.button` - padding: 1px 10px; - margin-left: 8px; - font-size: 15px; - background: #efefef; - border: none; - color: inherit; - border-radius: 8px; +const IconCircle = styled.span` + width: 1.5rem; + height: 1.5rem; + border-radius: 50%; + background: #EEF2F8; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + color: #4E7FF8; +`; + +const ToggleLabelText = styled.span` + font-size: 1rem; + color: #1C2B3A; +`; + +// Checkbox styled as a toggle switch. +// Keeps the real in the DOM so Playwright selectors +// (#includeModerators etc.) and isChecked() / toBeVisible() all keep working. +const ToggleCheckbox = styled.input` + appearance: none; + -webkit-appearance: none; + width: 2.25rem; + height: 1.25rem; + border-radius: 0.625rem; + background: #D1D9E3; cursor: pointer; - &:hover { - background-color: #ddd; + outline: none; + border: none; + transition: background 0.2s ease; + position: relative; + flex-shrink: 0; + margin: 0; + + &::before { + content: ''; + position: absolute; + width: 1rem; + height: 1rem; + background: #fff; + border-radius: 50%; + top: 0.125rem; + left: 0.125rem; + transition: left 0.2s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + } + + &:checked { + background: #4E7FF8; + } + + &:checked::before { + left: 1.125rem; } `; -const PresenterViewSectionTitleWrapper = styled.div` +// ── Section header row (label + count/action) ───────────────────────────────── + +const SectionHeaderRow = styled.div` display: flex; - flex-direction: row; align-items: center; + justify-content: space-between; + margin-bottom: 0.5rem; `; -const PresenterViewSectionListWrapper = styled.div` - overflow-y: auto; +// ── Available users section ─────────────────────────────────────────────────── + +const CountBadge = styled.span` + font-size: 0.75rem; + color: #4E7FF8; + font-weight: 600; +`; + +const UserListContainer = styled.div` + background: #F7F9FB; + border-radius: 0.375rem; + padding: 0.625rem 0.75rem; + display: flex; + flex-direction: column; + gap: 0.375rem; max-height: 10rem; - margin-bottom: .75rem; - width: 100%; + overflow-y: auto; `; -const PresenterViewSectionList = styled.ul` - font-size: 18px; - margin: 0; - list-style-type: none; +const UserRow = styled.div` + display: flex; + align-items: center; + gap: 0.5rem; +`; + +const UserAvatar = styled.div<{ $color: string }>` + width: 1.625rem; + height: 1.625rem; + border-radius: 50%; + background: ${({ $color }) => $color}; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.625rem; + font-weight: 600; + color: #fff; + flex-shrink: 0; +`; + +const UserAvatarImage = styled.img` + width: 1.625rem; + height: 1.625rem; + border-radius: 50%; + object-fit: cover; + flex-shrink: 0; +`; + +const UserNameText = styled.span` + font-size: 0.8125rem; + color: #1C2B3A; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const RoleBadge = styled.span` + font-size: 0.625rem; + color: #8B9AAF; + background: #E8EDF2; + padding: 0.0625rem 0.375rem; + border-radius: 0.1875rem; + margin-left: auto; + flex-shrink: 0; + white-space: nowrap; +`; + +// ── Previously picked section ───────────────────────────────────────────────── + +const ClearAllButton = styled.button` + font-size: 0.6875rem; + color: #8B9AAF; + background: none; + border: none; + text-decoration: underline; + text-underline-offset: 2px; + cursor: pointer; + padding: 0; + + &:hover { + color: #6b7d92; + } +`; + +const EmptyStateContainer = styled.div` + background: #F7F9FB; + border-radius: 0.375rem; + padding: 0.875rem; + text-align: center; `; -const PresenterViewSectionContent = styled.div` +const EmptyStateText = styled.span` + font-size: 0.8125rem; + color: #A7B3C3; +`; + +const PickedUserListContainer = styled.div` + background: #F7F9FB; + border-radius: 0.375rem; + padding: 0.625rem 0.75rem; display: flex; flex-direction: column; - font-size: 18px; - padding-left: 40px; + gap: 0.375rem; + max-height: 7.5rem; + overflow-y: auto; `; -// Section Content related: +//
            that test selectors target with [data-test="pickRandomUserPreviouslyPickedList"] +const PickedList = styled.ul` + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.375rem; +`; -const CheckboxLabelWrapper = styled.label` +const PickedUserRow = styled.li` display: flex; align-items: center; + gap: 0.5rem; + list-style: none; `; -const CheckboxLabel = styled.span` - margin-left: 5px; +// ── Footer / action button ──────────────────────────────────────────────────── + +const FooterContainer = styled.div` + padding: 0 1.25rem 1rem; `; -const PickUserButton = styled.button` - border: 3px solid transparent; - overflow: visible; - display: inline-block; - background-color: var(--btn-primary-bg, var(--color-primary, #0F70D7)); - color: var(--btn-primary-color, var(--color-white, #FFF)); - border-radius: 2px; +const PickButton = styled.button` + width: 100%; + padding: 0.625rem 0; + background: #4E7FF8; + color: #fff; + border: none; + border-radius: 0.375rem; + font-size: 0.875rem; font-weight: 600; - line-height: 1; - text-align: center; - white-space: nowrap; - vertical-align: middle; + font-family: inherit; cursor: pointer; - user-select: none; - padding: 8px 15px; + display: flex; + align-items: center; + justify-content: center; + gap: 0.375rem; + &:hover { - color: var(--btn-primary-color, var(--color-white, #FFF)); - background-color: var(--btn-primary-hover-bg, #0C57A7) !important; + background: #3D6DE0; } `; +const NoUsersWarning = styled.p` + font-size: 0.8125rem; + color: #8B9AAF; + text-align: center; + margin: 0; + padding: 0.5rem 0; +`; + +// ── Outer wrappers ──────────────────────────────────────────────────────────── + +const PresenterViewWrapper = styled.div` + font-family: 'Source Sans Pro', Arial, sans-serif; +`; + +const ContentPadding = styled.div` + padding: 1rem 1.25rem 0.75rem; + display: flex; + flex-direction: column; + gap: 1rem; +`; + +const OptionsSection = styled.div``; + +const AvailableSection = styled.div``; + +const PreviouslyPickedSection = styled.div``; + export { - PresenterViewContentWrapper, - PresenterViewSectionWrapper, - PresenterViewSectionTitle, - PresenterViewSectionTitleWrapper, - PresenterViewSectionList, - PresenterViewSectionListWrapper, - PresenterViewSectionContent, - CheckboxLabelWrapper, - CheckboxLabel, - PickUserButton, - PresenterViewSectionClearAllButton, + SectionLabel, + OptionsContainer, + ToggleRow, + ToggleRowLeft, + IconCircle, + ToggleLabelText, + ToggleCheckbox, + SectionHeaderRow, + CountBadge, + UserListContainer, + UserRow, + UserAvatar, + UserAvatarImage, + UserNameText, + RoleBadge, + ClearAllButton, + EmptyStateContainer, + EmptyStateText, + PickedUserListContainer, + PickedList, + PickedUserRow, + FooterContainer, + PickButton, + NoUsersWarning, + PresenterViewWrapper, + ContentPadding, + OptionsSection, + AvailableSection, + PreviouslyPickedSection, }; diff --git a/src/components/modal/styles.tsx b/src/components/modal/styles.tsx index cfb9af3..352a1be 100644 --- a/src/components/modal/styles.tsx +++ b/src/components/modal/styles.tsx @@ -9,18 +9,13 @@ const PluginModal = styled(ReactModal)` outline-style: solid; display: flex; flex-direction: column; - padding: 2rem; - box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.7); background-color: #fff !important; - max-width: 60vw; - max-height: 80vh; - border-radius: 0.2rem; - overflow: auto; - overflow-y: hidden; - background-repeat: no-repeat; - background-color: transparent; - background-size: 100% 40px, 100% 40px, 100% 14px, 100% 14px; - background-attachment: local, local, scroll, scroll; + width: 25rem; + max-width: 95vw; + border-radius: 0.5rem; + box-shadow: 0 0.5rem 2rem rgba(0, 0, 0, 0.4); + overflow: hidden; + font-family: 'Source Sans Pro', Arial, sans-serif; &::-webkit-scrollbar { width: 5px; @@ -38,47 +33,50 @@ const PluginModal = styled(ReactModal)` &::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.5); } - &::-webkit-scrollbar-thumb:active { - background: rgba(0, 0, 0, 0.25); - } &::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.25); + background: rgba(0, 0, 0, 0.1); border: none; border-radius: 50px; } &::-webkit-scrollbar-corner { background: transparent; } +`; - @media only screen and (max-width: 40em) { - max-width: 95vw; - } +const ModalHeader = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.25rem 0.875rem; + border-bottom: 1px solid #E8EDF2; + flex-shrink: 0; +`; - @media only screen and (min-width: 40.063em) { - max-width: 80vw; - } +const ModalTitle = styled.span` + font-weight: 600; + font-size: 1.15rem; + color: #1C2B3A; `; const CloseButton = styled.button` - font-size: 40px; + font-size: 1rem; background: none; - color: inherit; - font: inherit; - cursor: pointer; - outline: inherit; + color: #8B9AAF; border: none; + cursor: pointer; + padding: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.25rem; + line-height: 1; + &:hover { - background-color: #EEE; + background-color: #EEF2F8; + color: #1C2B3A; } `; -const CloseButtonWrapper = styled.div` - width: 100%; - align-items: flex-end; - display: flex; - flex-direction: column; -`; - const CountdownMessage = styled.div` width: 100%; text-align: center; @@ -93,5 +91,5 @@ const CountdownMessage = styled.div` `; export { - PluginModal, CloseButton, CloseButtonWrapper, CountdownMessage, + PluginModal, ModalHeader, ModalTitle, CloseButton, CountdownMessage, }; diff --git a/tests/README.md b/tests/README.md index 193358e..8b112ab 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,8 +1,7 @@ # Pick Random User Plugin – Automated Tests End-to-end tests for the **Pick Random User Plugin** written with [Playwright](https://playwright.dev/). -They reuse the shared test infrastructure from the -[BigBlueButton HTML Plugin SDK](https://github.com/bigbluebutton/bigbluebutton-html-plugin-sdk). +The test infrastructure lives entirely inside `tests/core/`. --- @@ -35,13 +34,6 @@ npm install npx playwright install --with-deps chromium ``` -The SDK test utilities are resolved via relative paths to the sibling -`bigbluebutton-html-plugin-sdk/` directory: - -```bash -cd ../bigbluebutton-html-plugin-sdk && npm install && cd - -``` - ### 2 – Configure environment variables ```bash diff --git a/tests/behavioral/helpers.ts b/tests/behavioral/helpers.ts index 657df3b..7220d64 100644 --- a/tests/behavioral/helpers.ts +++ b/tests/behavioral/helpers.ts @@ -1,7 +1,27 @@ +import { test } from '@playwright/test'; import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; import { elements as e } from '../elements'; import { SessionPage as Page } from '../core/sessionPage'; +/** Click a toggle/checkbox and verify it reaches :checked state, retrying once if needed. */ +export async function clickToggleOnWithRetry( + page: Page, + selector: string, + description: string, +): Promise { + await page.page.click(selector); + try { + await page.hasElement(`${selector}:checked`, `${description} toggle should be on`, ELEMENT_WAIT_LONGER_TIME); + } catch { + test.info().annotations.push({ + type: 'toggle-retry', + description: `"${description}" toggle didn't register on first click and was retried`, + }); + await page.page.click(selector); + await page.hasElement(`${selector}:checked`, `${description} toggle should be on (retry)`, ELEMENT_WAIT_LONGER_TIME); + } +} + export async function openModal(modPage: Page): Promise { await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); await modPage.page.click(e.actions); diff --git a/tests/behavioral/multi-user.spec.ts b/tests/behavioral/multi-user.spec.ts index 8923896..d7762bf 100644 --- a/tests/behavioral/multi-user.spec.ts +++ b/tests/behavioral/multi-user.spec.ts @@ -1,7 +1,9 @@ /** * Behavioural tests – multi-user (moderator/presenter + attendee/viewer). */ -import { test, BrowserContext } from '@playwright/test'; +import { + test, BrowserContext, Browser, APIRequestContext, TestInfo, +} from '@playwright/test'; import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME, ELEMENT_WAIT_EXTRA_LONG_TIME } from '../core/constants'; import { elements as e } from '../elements'; @@ -42,16 +44,18 @@ async function cleanupAfterTest(modPage: Page, attendeePage: Page): Promise { - test.describe.configure({ mode: 'serial' }); + test.describe.configure({ mode: ISOLATED ? 'default' : 'serial' }); let modPage: Page; let attendeePage: Page; let modContext: BrowserContext; let attendeeContext: BrowserContext; - test.beforeAll(async ({ browser, request }, testInfo) => { + async function setupMeeting(browser: Browser, request: APIRequestContext, testInfo: TestInfo) { await checkPluginAvailability({ pluginName: PLUGIN_NAME, envVarName: ENV_VAR_NAME, @@ -99,16 +103,28 @@ test.describe('Pick Random User Plugin - Behavioural (multi-user)', () => { content: "body { font-family: 'Liberation Sans', Arial, sans-serif; }", }); attendeePage.meetingId = modPage.meetingId; - }); + } - test.afterAll(async () => { - await modContext?.close(); - await attendeeContext?.close(); - }); - - test.afterEach(async () => { - if (modPage && attendeePage) await cleanupAfterTest(modPage, attendeePage); - }); + if (ISOLATED) { + test.beforeEach(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterEach(async () => { + await modContext?.close(); + await attendeeContext?.close(); + }); + } else { + test.beforeAll(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterAll(async () => { + await modContext?.close(); + await attendeeContext?.close(); + }); + test.afterEach(async () => { + if (modPage && attendeePage) await cleanupAfterTest(modPage, attendeePage); + }); + } test('should show the same picked user name on both the presenter page and the attendee page', async (): Promise => { await waitForAttendeeMeeting(attendeePage); diff --git a/tests/behavioral/single-user.spec.ts b/tests/behavioral/single-user.spec.ts index b2ff80c..6e09f72 100644 --- a/tests/behavioral/single-user.spec.ts +++ b/tests/behavioral/single-user.spec.ts @@ -1,14 +1,18 @@ /** * Behavioural tests – single user (moderator / presenter only). */ -import { test, BrowserContext } from '@playwright/test'; +import { + test, BrowserContext, Browser, APIRequestContext, TestInfo, +} from '@playwright/test'; import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; import { elements as e } from '../elements'; import { SessionPage as ModPage } from '../core/sessionPage'; import { Plugin } from '../core/plugin'; import { encodeCustomParams } from '../core/helpers'; -import { goBackToPresenterView, openModal, moderatorCleanupAfterTest } from './helpers'; +import { + goBackToPresenterView, openModal, moderatorCleanupAfterTest, clickToggleOnWithRetry, +} from './helpers'; const PLUGIN_NAME = 'pick-random-user-plugin'; const ENV_VAR_NAME = 'PICK_RANDOM_USER_PLUGIN_URL'; @@ -19,25 +23,15 @@ const getPluginUrl = () => pluginUrl; /** Enable both inclusion filters so the single moderator/presenter is eligible. */ async function enableInclusionFilters(modPage: ModPage): Promise { - await modPage.page.click(e.includeModeratorsCheckbox); - await modPage.page.click(e.includePresenterCheckbox); - await modPage.hasElement( - e.pickRandomUserPickButton, - 'pick button should appear after enabling both inclusion filters', - ELEMENT_WAIT_LONGER_TIME, - ); + await clickToggleOnWithRetry(modPage, e.includeModeratorsCheckbox, 'includeModerators'); + await clickToggleOnWithRetry(modPage, e.includePresenterCheckbox, 'includePresenter'); } /** Enable all three filters (include picked users too, so "Pick again" is reachable). */ async function enableAllFilters(modPage: ModPage): Promise { - await modPage.page.click(e.includeModeratorsCheckbox); - await modPage.page.click(e.includePresenterCheckbox); - await modPage.page.click(e.includePickedUsersCheckbox); - await modPage.hasElement( - e.pickRandomUserPickButton, - 'pick button should appear after enabling all filters', - ELEMENT_WAIT_LONGER_TIME, - ); + await clickToggleOnWithRetry(modPage, e.includeModeratorsCheckbox, 'includeModerators'); + await clickToggleOnWithRetry(modPage, e.includePresenterCheckbox, 'includePresenter'); + await clickToggleOnWithRetry(modPage, e.includePickedUsersCheckbox, 'includePickedUsers'); } /** Pick a user and wait for the picked-user view to appear. */ @@ -54,14 +48,16 @@ async function cleanupAfterTest(modPage: ModPage) { await moderatorCleanupAfterTest(modPage); } +const ISOLATED = process.env.TEST_MEETINGS === 'isolated'; + // ── Tests ───────────────────────────────────────────────────────────────────── test.describe('Pick Random User Plugin - Behavioural (single user)', () => { - test.describe.configure({ mode: 'serial' }); + test.describe.configure({ mode: ISOLATED ? 'default' : 'serial' }); let modPage: ModPage; let sharedContext: BrowserContext; - test.beforeAll(async ({ browser, request }, testInfo) => { + async function setupMeeting(browser: Browser, request: APIRequestContext, testInfo: TestInfo) { await checkPluginAvailability({ pluginName: PLUGIN_NAME, envVarName: ENV_VAR_NAME, @@ -83,15 +79,26 @@ test.describe('Pick Random User Plugin - Behavioural (single user)', () => { const plugin = new Plugin({ browser, context: sharedContext }); await plugin.initModPage(page, { createParameter }); modPage = plugin.modPage; - }); + } - test.afterAll(async () => { - await sharedContext?.close(); - }); - - test.afterEach(async () => { - if (modPage) await cleanupAfterTest(modPage); - }); + if (ISOLATED) { + test.beforeEach(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterEach(async () => { + await sharedContext?.close(); + }); + } else { + test.beforeAll(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterAll(async () => { + await sharedContext?.close(); + }); + test.afterEach(async () => { + if (modPage) await cleanupAfterTest(modPage); + }); + } test('should show "Pick again" button (not "Pick user") after navigating back from picked-user view', async (): Promise => { // With "Include already picked users" ON the presenter stays in the pool diff --git a/tests/structural/test.spec.ts b/tests/structural/test.spec.ts index e8c55b4..f6a71a7 100644 --- a/tests/structural/test.spec.ts +++ b/tests/structural/test.spec.ts @@ -1,5 +1,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies -import { test, BrowserContext } from '@playwright/test'; +import { + test, BrowserContext, Browser, APIRequestContext, TestInfo, +} from '@playwright/test'; import { checkPluginAvailability } from '../core/fixtures/pluginBeforeAll'; import { ELEMENT_WAIT_LONGER_TIME, ELEMENT_WAIT_TIME } from '../core/constants'; import { elements as e } from '../elements'; @@ -50,15 +52,17 @@ async function cleanupAfterTest(modPage: ModPage): Promise { } } +const ISOLATED = process.env.TEST_MEETINGS === 'isolated'; + // ── Tests ───────────────────────────────────────────────────────────────────── test.describe('Pick Random User Plugin - Structural', () => { - test.describe.configure({ mode: 'serial' }); + test.describe.configure({ mode: ISOLATED ? 'default' : 'serial' }); let modPage: ModPage; let sharedContext: BrowserContext; - test.beforeAll(async ({ browser, request }, testInfo) => { + async function setupMeeting(browser: Browser, request: APIRequestContext, testInfo: TestInfo) { await checkPluginAvailability({ pluginName: PLUGIN_NAME, envVarName: ENV_VAR_NAME, @@ -80,15 +84,26 @@ test.describe('Pick Random User Plugin - Structural', () => { const plugin = new Plugin({ browser, context: sharedContext }); await plugin.initModPage(page, { createParameter }); modPage = plugin.modPage; - }); - - test.afterAll(async () => { - await sharedContext?.close(); - }); + } - test.afterEach(async () => { - if (modPage) await cleanupAfterTest(modPage); - }); + if (ISOLATED) { + test.beforeEach(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterEach(async () => { + await sharedContext?.close(); + }); + } else { + test.beforeAll(async ({ browser, request }, testInfo) => { + await setupMeeting(browser, request, testInfo); + }); + test.afterAll(async () => { + await sharedContext?.close(); + }); + test.afterEach(async () => { + if (modPage) await cleanupAfterTest(modPage); + }); + } test('should show "Pick random user" label in the actions dropdown for a presenter', async (): Promise => { await modPage.page.waitForSelector(e.whiteboard, { timeout: ELEMENT_WAIT_LONGER_TIME }); From ab5ff32ba5e9674e139c6d3dac6acbf833308395 Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Fri, 15 May 2026 11:32:45 -0300 Subject: [PATCH 068/117] [ref-ui] added test:isolated documentation on the tests/readme file --- tests/README.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index 8b112ab..11a64be 100644 --- a/tests/README.md +++ b/tests/README.md @@ -49,6 +49,7 @@ cp .env.template .env | `LOCAL_CONTAINER_NAME` | no | Docker container name for the local deployment script | | `TIMEOUT_MULTIPLIER` | no | Multiply all timeouts (default 1 locally, 2 in CI) | | `CI` | no | `"true"` enables CI reporter and single-worker mode | +| `TEST_MEETINGS` | no | Set to `"isolated"` to give each test its own meeting (see [Meeting isolation](#meeting-isolation)) | ### 3 – Build and deploy the plugin @@ -66,12 +67,28 @@ npm run publish-plugin:dev --- +## Meeting isolation + +By default every test suite (`test.describe` block) shares **one BBB meeting** for all its tests — the meeting is created once in `beforeAll` and torn down in `afterAll`. This is fast (~3 meetings total) but tests within a suite depend on the cleanup performed between them. + +Setting `TEST_MEETINGS=isolated` switches every suite to **one meeting per test**: the meeting is created in `beforeEach` and destroyed in `afterEach`. Tests become fully independent and can run in parallel, at the cost of more meeting setups (~15 meetings total). + +| Mode | Meetings created | Tests run | When to use | +|------|-----------------|-----------|-------------| +| default (`npm test`) | ~3 (one per suite) | serially within each suite | Normal development | +| isolated (`npm run test:isolated`) | ~1 per test | can run in parallel | Debugging flaky state, CI full isolation | + +--- + ## Running the tests ```bash -# All suites +# All suites – shared meetings (default) npm test +# All suites – one meeting per test +npm run test:isolated + # Only structural tests npm test -- tests/structural From 4fcc1ad834b8a6c26dbdc633a3243f2cd36e1f4b Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 21 May 2026 09:23:56 -0300 Subject: [PATCH 069/117] [ref-ui] fix labels based on review --- public/locales/en.json | 2 +- public/locales/fr-FR.json | 2 +- public/locales/it.json | 2 +- public/locales/pt-BR.json | 2 +- src/components/modal/presenter-view/component.tsx | 8 ++++++-- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/public/locales/en.json b/public/locales/en.json index 3aac237..967d1be 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -6,7 +6,7 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Include moderators", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Include presenter", - "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Include already picked user", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Include already picked users", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Available for selection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "user", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "users", diff --git a/public/locales/fr-FR.json b/public/locales/fr-FR.json index 04ad3cd..ffc9734 100644 --- a/public/locales/fr-FR.json +++ b/public/locales/fr-FR.json @@ -5,7 +5,7 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Options", "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Inclure les modérateurs", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Inclure le présentateur", - "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Inclure un participant déjà sélectionné", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Inclure des participants déjà sélectionnés", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponible pour la sélection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "participant", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "participants", diff --git a/public/locales/it.json b/public/locales/it.json index 7f312ab..13fe116 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -5,7 +5,7 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opzioni", "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Includi moderatori", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Includi il presentatore", - "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Includi l'utente già selezionato", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Includi gli utenti già selezionati", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponibile per la selezione", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "utente", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "utenti", diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json index c2c239c..b5e79fb 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -6,7 +6,7 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.title": "Opções", "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Incluir moderadores", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Incluir apresentador", - "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Incluir usuário já selecionado", + "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Incluir usuários já selecionados", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponíveis para seleção", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "usuário", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "usuários", diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index f9c5760..a323e33 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -28,7 +28,7 @@ const intlMessages = defineMessages({ includePickedUsersLabel: { id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel', description: 'Label of include picked users option on modal`s presenter view', - defaultMessage: 'Include already picked user', + defaultMessage: 'Include already picked users', }, availableTitle: { id: 'pickRandomUserPlugin.modal.presenterView.availableSection.title', @@ -182,6 +182,10 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { : intl.formatMessage(intlMessages.userLabel, { 0: usersCount }); })(); + const userRoleLabelSingular = (!includeModerators) + ? intl.formatMessage(intlMessages.viewerLabel, { 0: usersCount }) + : intl.formatMessage(intlMessages.userLabel, { 0: usersCount }); + const hasPickedUsers = dataChannelPickedUsers?.some((u) => !!u.payloadJson); return ( @@ -355,7 +359,7 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { > {pickedUserWithEntryId ? intl.formatMessage(intlMessages.pickAgainButtonLabel) - : intl.formatMessage(intlMessages.pickButtonLabel, { 0: userRoleLabel })} + : intl.formatMessage(intlMessages.pickButtonLabel, { 0: userRoleLabelSingular })} ) : ( From 6f3c373649a697b66d90689fbdd4f0c8654a624e Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 21 May 2026 09:56:00 -0300 Subject: [PATCH 070/117] [ref-ui] change options to improve user experience. --- public/locales/de.json | 4 + public/locales/en.json | 4 + public/locales/fr-FR.json | 4 + public/locales/it.json | 4 + public/locales/ja.json | 4 + public/locales/pt-BR.json | 4 + .../modal/presenter-view/component.tsx | 193 ++++++++---------- .../modal/presenter-view/styles.tsx | 113 ++++------ 8 files changed, 152 insertions(+), 178 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index e163cf4..486a3e2 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -7,6 +7,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Moderatoren einbeziehen", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Präsentator einbeziehen", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Wähle bereits gewählte Teilnehmer", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Auch einbeziehen:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Moderatoren", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Präsentator", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Bereits gewählt", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Zur Auswahl stehen", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "Teilnehmer", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "Teilnehmer", diff --git a/public/locales/en.json b/public/locales/en.json index 967d1be..544d2ab 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -7,6 +7,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Include moderators", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Include presenter", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Include already picked users", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Also include:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Moderators", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Presenter", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Already picked", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Available for selection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "user", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "users", diff --git a/public/locales/fr-FR.json b/public/locales/fr-FR.json index ffc9734..0580acb 100644 --- a/public/locales/fr-FR.json +++ b/public/locales/fr-FR.json @@ -6,6 +6,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Inclure les modérateurs", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Inclure le présentateur", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Inclure des participants déjà sélectionnés", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Inclure aussi :", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Modérateurs", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Présentateur", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Déjà sélectionnés", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponible pour la sélection", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "participant", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "participants", diff --git a/public/locales/it.json b/public/locales/it.json index 13fe116..4867a5a 100644 --- a/public/locales/it.json +++ b/public/locales/it.json @@ -6,6 +6,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Includi moderatori", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Includi il presentatore", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Includi gli utenti già selezionati", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Includi anche:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Moderatori", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Presentatore", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Già selezionati", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponibile per la selezione", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "utente", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "utenti", diff --git a/public/locales/ja.json b/public/locales/ja.json index fcc10c0..e6d65b6 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -7,6 +7,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "モデレーターを含める", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "発表者を含める", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "既に指名した人を含める", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "含める:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "モデレーター", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "発表者", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "指名済みの人", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "指名可能な人", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "人のユーザー", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "人のユーザー", diff --git a/public/locales/pt-BR.json b/public/locales/pt-BR.json index b5e79fb..4f79f17 100644 --- a/public/locales/pt-BR.json +++ b/public/locales/pt-BR.json @@ -7,6 +7,10 @@ "pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel": "Incluir moderadores", "pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel": "Incluir apresentador", "pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel": "Incluir usuários já selecionados", + "pickRandomUserPlugin.modal.presenterView.filterChips.label": "Incluir também:", + "pickRandomUserPlugin.modal.presenterView.filterChips.moderators": "Moderadores", + "pickRandomUserPlugin.modal.presenterView.filterChips.presenter": "Apresentador", + "pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers": "Já selecionados", "pickRandomUserPlugin.modal.presenterView.availableSection.title": "Disponíveis para seleção", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabel": "usuário", "pickRandomUserPlugin.modal.presenterView.availableSection.userLabelPlural": "usuários", diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index a323e33..c058261 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -10,25 +10,25 @@ import { PresenterViewComponentProps } from './types'; import { FilterOptionsContext } from '../../pick-random-user/context'; const intlMessages = defineMessages({ - optionsTitle: { - id: 'pickRandomUserPlugin.modal.presenterView.optionSection.title', - description: 'Title of the options section on modal`s presenter view', - defaultMessage: 'Options', + filterChipsLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.filterChips.label', + description: 'Label preceding the filter chip group', + defaultMessage: 'Also include:', }, - includeModeratorsLabel: { - id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includeModeratorsLabel', - description: 'Label to include moderator`s option on modal`s presenter view', - defaultMessage: 'Include moderators', + moderatorsChipLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.filterChips.moderators', + description: 'Chip label to include moderators', + defaultMessage: 'Moderators', }, - includePresenterLabel: { - id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePresenterLabel', - description: 'Label of skip presenter`s option on modal`s presenter view', - defaultMessage: 'Include presenter', + presenterChipLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.filterChips.presenter', + description: 'Chip label to include presenter', + defaultMessage: 'Presenter', }, - includePickedUsersLabel: { - id: 'pickRandomUserPlugin.modal.presenterView.optionSection.includePickedUsersLabel', - description: 'Label of include picked users option on modal`s presenter view', - defaultMessage: 'Include already picked users', + pickedUsersChipLabel: { + id: 'pickRandomUserPlugin.modal.presenterView.filterChips.pickedUsers', + description: 'Chip label to include already picked users', + defaultMessage: 'Already picked', }, availableTitle: { id: 'pickRandomUserPlugin.modal.presenterView.availableSection.title', @@ -113,27 +113,35 @@ function getInitials(name: string): string { return name.slice(0, 2); } -function ShieldIcon() { +function CheckboxSquare({ active }: { active: boolean }) { + const style: React.CSSProperties = active ? { + width: '0.875rem', + height: '0.875rem', + borderRadius: '3px', + background: '#4E7FF8', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + } : { + width: '0.875rem', + height: '0.875rem', + borderRadius: '3px', + background: '#fff', + border: '1.5px solid #C0C8D4', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + }; return ( - - - - ); -} - -function PresentationIcon() { - return ( - - - - ); -} - -function UserCheckIcon() { - return ( - - - + + {active && ( + + + + )} + ); } @@ -192,78 +200,53 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { - {/* OPTIONS SECTION */} + {/* FILTER CHIPS */} - - {intl.formatMessage(intlMessages.optionsTitle)} - - - - - - - - - {intl.formatMessage(intlMessages.includeModeratorsLabel)} - - - { - setFilterOptions((prev) => ({ - ...prev, - includeModerators: !prev.includeModerators, - })); - }} - /> - + + + {intl.formatMessage(intlMessages.filterChipsLabel)} + + + + setFilterOptions((prev) => ({ + ...prev, includeModerators: !prev.includeModerators, + }))} + /> + + {intl.formatMessage(intlMessages.moderatorsChipLabel)} + - - - - - - - {intl.formatMessage(intlMessages.includePresenterLabel)} - - - { - setFilterOptions((prev) => ({ - ...prev, - includePresenter: !prev.includePresenter, - })); - }} - /> - + + setFilterOptions((prev) => ({ + ...prev, includePresenter: !prev.includePresenter, + }))} + /> + + {intl.formatMessage(intlMessages.presenterChipLabel)} + - - - - - - - {intl.formatMessage(intlMessages.includePickedUsersLabel)} - - - { - setFilterOptions((prev) => ({ - ...prev, - includePickedUsers: !prev.includePickedUsers, - })); - }} - /> - - + + setFilterOptions((prev) => ({ + ...prev, includePickedUsers: !prev.includePickedUsers, + }))} + /> + + {intl.formatMessage(intlMessages.pickedUsersChipLabel)} + + + {/* AVAILABLE USERS SECTION */} diff --git a/src/components/modal/presenter-view/styles.tsx b/src/components/modal/presenter-view/styles.tsx index bc440d9..89e8dcf 100644 --- a/src/components/modal/presenter-view/styles.tsx +++ b/src/components/modal/presenter-view/styles.tsx @@ -10,86 +10,54 @@ const SectionLabel = styled.span` letter-spacing: 0.6px; `; -// ── Options section (toggle rows) ───────────────────────────────────────────── +// ── Filter chips section ────────────────────────────────────────────────────── -const OptionsContainer = styled.div` - margin-top: 0.5rem; -`; - -const ToggleRow = styled.label` +const FilterRow = styled.div` display: flex; - align-items: center; - justify-content: space-between; - padding: 0.4375rem 0; - cursor: pointer; - border-bottom: 1px solid #F2F4F7; - - &:last-child { - border-bottom: none; - } + flex-direction: column; + gap: 0.375rem; `; -const ToggleRowLeft = styled.div` - display: flex; - align-items: center; - gap: 0.5rem; +const FilterLabel = styled.span` + font-size: 0.8125rem; + font-weight: 600; + color: #8B9AAF; + white-space: nowrap; `; -const IconCircle = styled.span` - width: 1.5rem; - height: 1.5rem; - border-radius: 50%; - background: #EEF2F8; +const ChipGroup = styled.div` display: flex; align-items: center; - justify-content: center; - flex-shrink: 0; - color: #4E7FF8; + gap: 0.375rem; + flex-wrap: wrap; `; -const ToggleLabelText = styled.span` - font-size: 1rem; - color: #1C2B3A; +const ChipInput = styled.input` + display: none; `; -// Checkbox styled as a toggle switch. -// Keeps the real in the DOM so Playwright selectors -// (#includeModerators etc.) and isChecked() / toBeVisible() all keep working. -const ToggleCheckbox = styled.input` - appearance: none; - -webkit-appearance: none; - width: 2.25rem; - height: 1.25rem; - border-radius: 0.625rem; - background: #D1D9E3; +const FilterChip = styled.label<{ $active: boolean }>` + display: inline-flex; + align-items: center; + gap: 0.3125rem; + padding: 0.25rem 0.625rem; + border-radius: 999px; + font-size: 0.75rem; cursor: pointer; - outline: none; - border: none; - transition: background 0.2s ease; - position: relative; - flex-shrink: 0; - margin: 0; - - &::before { - content: ''; - position: absolute; - width: 1rem; - height: 1rem; - background: #fff; - border-radius: 50%; - top: 0.125rem; - left: 0.125rem; - transition: left 0.2s ease; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); - } - - &:checked { - background: #4E7FF8; - } - - &:checked::before { - left: 1.125rem; - } + user-select: none; + transition: all 0.15s; + + ${({ $active }) => ($active ? ` + background: #EBF1FF; + border: 1.5px solid #4E7FF8; + color: #4E7FF8; + font-weight: 600; + ` : ` + background: #F0F2F6; + border: 1.5px solid #D1D9E3; + color: #8B9AAF; + font-weight: 500; + `)} `; // ── Section header row (label + count/action) ───────────────────────────────── @@ -282,12 +250,11 @@ const PreviouslyPickedSection = styled.div``; export { SectionLabel, - OptionsContainer, - ToggleRow, - ToggleRowLeft, - IconCircle, - ToggleLabelText, - ToggleCheckbox, + FilterRow, + FilterLabel, + ChipGroup, + ChipInput, + FilterChip, SectionHeaderRow, CountBadge, UserListContainer, From bde47062e43647874c02d5f98e438f83b227522a Mon Sep 17 00:00:00 2001 From: Guilherme Leme Date: Thu, 21 May 2026 10:14:25 -0300 Subject: [PATCH 071/117] [ref-ui] Fixed tests due to change in the UI --- .../modal/presenter-view/component.tsx | 18 ++++++++++-- tests/behavioral/helpers.ts | 28 +++++++++++-------- tests/behavioral/multi-user.spec.ts | 2 +- tests/behavioral/single-user.spec.ts | 10 +++---- tests/elements.ts | 7 ++++- tests/structural/test.spec.ts | 10 +++---- 6 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/components/modal/presenter-view/component.tsx b/src/components/modal/presenter-view/component.tsx index c058261..2060101 100644 --- a/src/components/modal/presenter-view/component.tsx +++ b/src/components/modal/presenter-view/component.tsx @@ -207,7 +207,11 @@ export function PresenterViewComponent(props: PresenterViewComponentProps) { {intl.formatMessage(intlMessages.filterChipsLabel)} - + - + - +