Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ TIMEOUT_MULTIPLIER=1

# Set to "true" to enable CI mode (1 worker, retries, blob reporter).
CI="false"

# Public URL of a clientSettingsOverride JSON that sets preventCloseDelaySeconds: 10.
# Required for the countdown / close-prevention behavioural tests.
# Must be reachable by the BBB server (a GitHub Gist raw URL works well).
# Leave empty to skip those tests.
# Example: PREVENT_CLOSE_DELAY_SETTINGS_URL="https://gist.githubusercontent.com/<user>/<id>/raw/<hash>/bbb-pick-random-user-prevent-close-delay.json"
PREVENT_CLOSE_DELAY_SETTINGS_URL="https://bigbluebutton.nyc3.digitaloceanspaces.com/plugins/assets/bbb-plugin-pick-random-user/prevent-close-delay-10s.json"
3 changes: 2 additions & 1 deletion public/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@
"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"
"pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "Präsentator",
"pickRandomUserPlugin.modal.closeDelayMessageMs": "Sie können dieses Fenster in {ms}ms schließen"
}
3 changes: 2 additions & 1 deletion public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@
"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"
"pickRandomUserPlugin.modal.closeDelayMessageSingular": "You can close this modal in {seconds} second",
"pickRandomUserPlugin.modal.closeDelayMessageMs": "You can close this modal in {ms}ms"
}
3 changes: 2 additions & 1 deletion public/locales/fr-FR.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@
"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"
"pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "présentateur",
"pickRandomUserPlugin.modal.closeDelayMessageMs": "Vous pouvez fermer cette fenêtre dans {ms}ms"
}
3 changes: 2 additions & 1 deletion public/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@
"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"
"pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "presentatore",
"pickRandomUserPlugin.modal.closeDelayMessageMs": "Puoi chiudere questa finestra in {ms}ms"
}
3 changes: 2 additions & 1 deletion public/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@
"pickRandomUserPlugin.modal.presenterView.availableSection.emptyState": "選択可能な{0}はいません",
"pickRandomUserPlugin.modal.presenterView.previouslyPickedSection.emptyState": "まだユーザーが選択されていません",
"pickRandomUserPlugin.modal.presenterView.roleLabel.moderator": "モデレーター",
"pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "発表者"
"pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "発表者",
"pickRandomUserPlugin.modal.closeDelayMessageMs": "このモーダルは{ms}ms後に閉じることができます"
}
3 changes: 2 additions & 1 deletion public/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@
"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"
"pickRandomUserPlugin.modal.presenterView.roleLabel.presenter": "apresentador",
"pickRandomUserPlugin.modal.closeDelayMessageMs": "Você pode fechar este modal em {ms}ms"
}
2 changes: 2 additions & 0 deletions src/commons/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export const TIMEOUT_CLOSE_NOTIFICATION = 5000;
export const DEFAULT_PING_SOUND_URL = 'resources/sounds/doorbell.mp3';

export const DEFAULT_PREVENT_CLOSE_DELAY_SECONDS = 1; // seconds

export const MIN_PREVENT_CLOSE_DELAY_FOR_TOAST_SECONDS = 2; // seconds
6 changes: 3 additions & 3 deletions src/commons/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ export const hasCurrentUserSeenPickedUser = (
pickedUserSeenEntries: GraphqlResponseWrapper<
DataChannelEntryResponseType<PickedUserSeenEntryDataChannel>[]>,
currentUserId: string,
pickedUserId: string,
) => pickedUserSeenEntries?.data
pickedUserId?: string,
) => !!(pickedUserSeenEntries?.data
&& pickedUserSeenEntries?.data.length > 0
&& pickedUserSeenEntries.data.some((view) => view.payloadJson
&& view.payloadJson.seenByUserId === currentUserId
&& view.payloadJson.pickedUserId === pickedUserId);
&& view.payloadJson.pickedUserId === pickedUserId));

export const isNumber = (obj: unknown): boolean => obj && typeof obj && !Number.isNaN(obj);
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { PickedUserWithEntryId } from '../../pick-random-user/types';

export interface ActionButtonDropdownManagerProps {
intl: IntlShape
currentPickedUser: PickedUserWithEntryId;
currentPickedUser: PickedUserWithEntryId | null;
currentUser: CurrentUserData;
pluginApi: PluginApi;
setShowModal: React.Dispatch<React.SetStateAction<boolean>>;
currentUserInfo: GraphqlResponseWrapper<CurrentUserData>;
currentUserInfo?: GraphqlResponseWrapper<CurrentUserData>;
}
139 changes: 125 additions & 14 deletions src/components/modal/component.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { useEffect, useRef, useState } from 'react';
import {
useCallback, useEffect, useRef, useState,
} from 'react';
import * as React from 'react';
import { defineMessages } from 'react-intl';
import * as Styled from './styles';
import { PickUserModalProps } from './types';
import { PickedUserViewComponent } from './picked-user-view/component';
import { PresenterViewComponent } from './presenter-view/component';
import { useHandleCurrentUserNotification, usePreventCloseModalCountdown } from './hooks';
import { useGetFilterOptions, useHandleCurrentUserNotification, usePreventCloseModalCountdown } from './hooks';
import { MIN_PREVENT_CLOSE_DELAY_FOR_TOAST_SECONDS } from '../../commons/constants';

const intlMessages = defineMessages({
currentUserPicked: {
Expand All @@ -23,17 +26,50 @@ const intlMessages = defineMessages({
description: 'Aria label for the modal close button',
defaultMessage: 'Close',
},
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',
},
modalCloseDelayMessageMs: {
id: 'pickRandomUserPlugin.modal.closeDelayMessageMs',
description: 'Message showing millisecond countdown before modal can be closed',
defaultMessage: 'You can close this modal in {ms}ms',
Comment thread
GuiLeme marked this conversation as resolved.
},
});

function OverlayWithToast({
overlayProps,
contentEl,
toast,
}: {
overlayProps: React.ComponentPropsWithRef<'div'>;
contentEl: React.ReactElement;
toast: React.ReactNode;
}) {
return (
<div {...overlayProps}>
<Styled.ModalWithToastWrapper>
{contentEl}
{toast}
</Styled.ModalWithToastWrapper>
</div>
);
}

export function PickUserModal(props: PickUserModalProps) {
const {
pickRandomUserSettings,
pluginApi,
intl,
showModal,
handleCloseModal,
users,
currentPickedUser,
handlePickRandomUser,
currentUser,
dataChannelPickedUsers,
deletionFunction,
Expand All @@ -42,6 +78,11 @@ export function PickUserModal(props: PickUserModalProps) {
uuid,
} = props;

const [filterOptions, setFilterOptions] = useGetFilterOptions(
pluginApi,
currentUser?.presenter ?? false,
);

const modalAnchor = useRef(document.getElementById(uuid));

const [showPresenterView, setShowPresenterView] = useState<boolean>(
Expand All @@ -56,9 +97,12 @@ export function PickUserModal(props: PickUserModalProps) {
intl.formatMessage(intlMessages.currentUserPicked),
);

const isPresenter = currentUser?.presenter;
useEffect(() => {
setShowPresenterView(currentUser?.presenter && !currentPickedUser);
}, [currentUser, currentPickedUser]);
setShowPresenterView(isPresenter && !currentPickedUser);
}, [isPresenter, currentPickedUser]);

const { preventCloseDelaySeconds } = pickRandomUserSettings;

const { remainingSeconds, canClose } = usePreventCloseModalCountdown(
currentUser,
Expand All @@ -67,6 +111,77 @@ export function PickUserModal(props: PickUserModalProps) {
pickRandomUserSettings,
);

const toastPhaseRef = useRef<'hidden' | 'visible' | 'exiting'>('hidden');
const [toastRendered, setToastRendered] = useState(false);
const [toastExiting, setToastExiting] = useState(false);

useEffect(() => {
const phase = toastPhaseRef.current;
const show = !showPresenterView && !canClose && remainingSeconds >= 0.3
&& preventCloseDelaySeconds >= MIN_PREVENT_CLOSE_DELAY_FOR_TOAST_SECONDS;
Comment thread
GuiLeme marked this conversation as resolved.
if (show && phase === 'hidden') {
toastPhaseRef.current = 'visible';
setToastRendered(true);
setToastExiting(false);
} else if (!show && phase === 'visible') {
toastPhaseRef.current = 'exiting';
setToastExiting(true);
const t = setTimeout(() => {
toastPhaseRef.current = 'hidden';
setToastRendered(false);
setToastExiting(false);
}, 400);
return () => clearTimeout(t);
} else if (showPresenterView && phase !== 'hidden') {
toastPhaseRef.current = 'hidden';
setToastRendered(false);
setToastExiting(false);
}
return undefined;
}, [showPresenterView, canClose, remainingSeconds]);

const toastMessage = remainingSeconds < 1
? intl.formatMessage(intlMessages.modalCloseDelayMessageMs, {
ms: Math.round(remainingSeconds * 1000),
})
: intl.formatMessage(
Math.ceil(remainingSeconds) === 1
? intlMessages.modalCloseDelayMessageSingular
: intlMessages.modalCloseDelayMessage,
{ seconds: Math.ceil(remainingSeconds) },
);

const toast = toastRendered ? (
<Styled.FloatingToast data-test="countDownMessage" $exiting={toastExiting}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="#4A6CF7"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
{toastMessage}
</Styled.FloatingToast>
) : null;

const renderOverlay = useCallback(
(
overlayProps: React.ComponentPropsWithRef<'div'>,
contentEl: React.ReactElement,
) => (
<OverlayWithToast overlayProps={overlayProps} contentEl={contentEl} toast={toast} />
),
[toast],
);

if (!showModal) return null;

const handleCloseAttempt = () => {
Expand All @@ -75,10 +190,6 @@ export function PickUserModal(props: PickUserModalProps) {
}
};

const progressPercentage = pickRandomUserSettings.preventCloseDelaySeconds > 0
? (remainingSeconds / pickRandomUserSettings.preventCloseDelaySeconds) * 100
: 0;

return (
<Styled.PluginModal
overlayClassName="modalOverlay"
Expand All @@ -89,6 +200,7 @@ export function PickUserModal(props: PickUserModalProps) {
onRequestClose={handleCloseAttempt}
shouldCloseOnOverlayClick={canClose}
shouldCloseOnEsc={canClose}
overlayElement={renderOverlay}
>
<Styled.ModalHeader>
<Styled.ModalTitle>
Expand All @@ -109,11 +221,12 @@ export function PickUserModal(props: PickUserModalProps) {
<PresenterViewComponent
{...{
intl,
filterOptions,
setFilterOptions,
deletionFunction,
handlePickRandomUser,
dataChannelPickedUsers,
pluginApi,
pickedUserWithEntryId: currentPickedUser,
users,
}}
/>
) : (
Expand All @@ -124,11 +237,9 @@ export function PickUserModal(props: PickUserModalProps) {
pickedUserWithEntryId: currentPickedUser,
intl,
currentUser,
showModal,
setShowPresenterView,
remainingSeconds,
canClose,
progressPercentage,
}}
/>
)
Expand Down
Loading
Loading