diff --git a/src/components/RejectionPage/RejectionPage.scss b/src/components/RejectionPage/RejectionPage.scss index 5061820ea9..fa689bf462 100644 --- a/src/components/RejectionPage/RejectionPage.scss +++ b/src/components/RejectionPage/RejectionPage.scss @@ -82,6 +82,30 @@ box-shadow: 0 5px 4px color.change(styles.$pink--500, $alpha: 0.25); cursor: pointer; + + &:disabled { + opacity: 0.6; + cursor: progress; + } +} + +.rejection-page__return-button--retry { + background: styles.$blue--500; + box-shadow: 0 5px 4px color.change(styles.$blue--500, $alpha: 0.25); +} + +.rejection-page__hint { + margin-top: styles.$spacing--xs; + font-style: italic; + font-size: styles.$text--sm; + opacity: 0.75; +} + +.rejection-page__button-group { + display: flex; + flex-direction: column; + gap: styles.$spacing--xs; + align-items: center; } // default look is for very small screens and works its way up to match larger screen sizes (could theoretically do it the other way around) @@ -134,6 +158,12 @@ align-items: flex-start; // prevent button from taking the whole space } + .rejection-page__button-group { + flex-direction: row; + align-items: flex-start; + gap: styles.$spacing--base; + } + .rejection-page__logo-stan { width: clamp(200px, 50vw, 1000px); } diff --git a/src/components/RejectionPage/RejectionPage.tsx b/src/components/RejectionPage/RejectionPage.tsx index bc766cc12a..4bb9aa2e11 100644 --- a/src/components/RejectionPage/RejectionPage.tsx +++ b/src/components/RejectionPage/RejectionPage.tsx @@ -1,5 +1,10 @@ import {useTranslation} from "react-i18next"; +import {useEffect, useState, useRef, useCallback} from "react"; +import {useParams} from "react-router"; import {ScrumlrLogo} from "components/ScrumlrLogo"; +import {API} from "api"; +import {useAppDispatch} from "store"; +import {permittedBoardAccess} from "store/features/board"; import StanLight from "assets/stan/Stan_Toilette_Light.svg?react"; import StanDark from "assets/stan/Stan_Toilette_Dark.svg?react"; import BackgroundFreeFormLight from "assets/pages/404/404_Background_light.svg?react"; @@ -11,8 +16,55 @@ type RejectionPageProps = { status: "rejected" | "too_many_join_requests" | "banned"; }; +// retry interval for banned users in milliseconds +const BANNED_RETRY_INTERVAL_MS = 30_000; + export const RejectionPage = ({status}: RejectionPageProps) => { const {t} = useTranslation(); + const dispatch = useAppDispatch(); + const {boardId} = useParams<"boardId">(); + const [isRetrying, setIsRetrying] = useState(false); + const intervalRef = useRef | null>(null); + + const isBanned = status === "banned"; + + // probes the join endpoint without putting the redux store into "pending" + // so the rejection page (and its retry button) stays mounted when the attempt fails. + const probeRejoin = useCallback(async () => { + if (!boardId) return; + try { + const result = await API.joinBoard(boardId); + if (result.status === "ACCEPTED") { + dispatch(permittedBoardAccess(boardId)); + } + } catch { + // network/temporary error: stay on the page and let the next tick try again. + } + }, [boardId, dispatch]); + + const tryRejoin = async () => { + setIsRetrying(true); + try { + await probeRejoin(); + } finally { + // give the user some visual feedback before re-enabling the button + setTimeout(() => setIsRetrying(false), 1000); + } + }; + + // automatically poll for unban while banned + useEffect(() => { + if (!isBanned || !boardId) return undefined; + + intervalRef.current = setInterval(probeRejoin, BANNED_RETRY_INTERVAL_MS); + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [isBanned, boardId, probeRejoin]); + return (
@@ -28,15 +80,23 @@ export const RejectionPage = ({status}: RejectionPageProps) => {
{t("RejectionPage.title")}
-
{status === "banned" ? t("RejectionPage.banned") : t("RejectionPage.description")}
- +
{isBanned ? t("RejectionPage.banned") : t("RejectionPage.description")}
+ {isBanned &&
{t("RejectionPage.bannedRetryHint")}
} +
+ {isBanned && ( + + )} + +
diff --git a/src/components/SettingsDialog/Participants/Participants.tsx b/src/components/SettingsDialog/Participants/Participants.tsx index d32f67c449..8e20466d51 100644 --- a/src/components/SettingsDialog/Participants/Participants.tsx +++ b/src/components/SettingsDialog/Participants/Participants.tsx @@ -114,7 +114,7 @@ export const Participants = () => { .every((s) => participant.user.name.toLowerCase().includes(s)) ) .filter((participant) => participant.role === permissionFilter || permissionFilter === "ALL") - .filter((participant) => participant.connected === onlineFilter) + .filter((participant) => participant.banned || participant.connected === onlineFilter) .map((participant) => (
  • diff --git a/src/i18n/de/translation.json b/src/i18n/de/translation.json index 97ceeb9e15..0c35739f42 100644 --- a/src/i18n/de/translation.json +++ b/src/i18n/de/translation.json @@ -808,6 +808,9 @@ "title": "Ups!", "description": "Es sieht so aus, als ob die Türen zu dieser Sitzung vorerst geschlossen bleiben. Wir freuen uns über dein Interesse, aber leider wurde dein Antrag auf Teilnahme abgelehnt.", "banned": "Es tut uns Leid, aber es sieht so aus, als ob du von dieser Sitzung ausgeschlossen wurdest.", + "bannedRetryHint": "Falls du wieder zugelassen wurdest, kannst du erneut beitreten. Wir prüfen das auch automatisch alle 30 Sekunden.", + "retry": "Erneut beitreten", + "retrying": "Wird versucht...", "button": "Zurück zur Startseite" }, "Votes": { diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json index a461eeb0d1..d205f69b85 100644 --- a/src/i18n/en/translation.json +++ b/src/i18n/en/translation.json @@ -807,6 +807,9 @@ "title": "Oops!", "description": "It looks like the doors to this session remain closed for now. We appreciate your interest, but unfortunately, your request to join has been declined.", "banned": "We're sorry, but it looks like you have been excluded from this session.", + "bannedRetryHint": "If you have been re-admitted, you can try to rejoin. We will also check automatically every 30 seconds.", + "retry": "Try to rejoin", + "retrying": "Retrying...", "button": "Back to Homepage" }, "Votes": { diff --git a/src/i18n/fr/translation.json b/src/i18n/fr/translation.json index 33471ad595..243bd36913 100644 --- a/src/i18n/fr/translation.json +++ b/src/i18n/fr/translation.json @@ -807,6 +807,9 @@ "title": "Oups !", "description": "Il semble que les portes de cette session restent fermées pour le moment. Nous apprécions votre intérêt, mais malheureusement, votre demande de participation a été refusée.", "banned": "Nous sommes désolés, mais il semble que vous ayez été exclu de cette session.", + "bannedRetryHint": "Si vous avez été ré-admis, vous pouvez essayer de rejoindre la session. Nous vérifions également automatiquement toutes les 30 secondes.", + "retry": "Réessayer de rejoindre", + "retrying": "Tentative en cours...", "button": "Retour à la page d'accueil" }, "Votes": {