From 6e3efbafac58d491f1a8c935b917271d2bdb4f8d Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Wed, 7 Feb 2024 14:38:06 +0100 Subject: [PATCH] Added handling of moderation events to ban a user. Co-authored-by: Patrick Schmidt (Clon1998) --- src/component/index.ts | 3 +- src/component/no-access/no-access.css | 48 ++++++++++++++++++++ src/component/no-access/no-access.ts | 32 +++++++++++++ src/component/player/player.controller.ts | 42 +++++++++++++++-- src/component/player/player.ts | 2 + src/event/index.ts | 3 +- src/event/lp-participant-joined.event.ts | 4 +- src/event/lp-participant-moderation.event.ts | 9 ++++ src/icons/shield-x.svg | 4 ++ src/locales/de/main.json | 7 ++- src/locales/en/main.json | 7 ++- src/model/moderation.ts | 12 +++++ src/service/event.service.ts | 11 ++++- src/utils/state.ts | 5 +- src/utils/toaster.ts | 22 ++++----- 15 files changed, 189 insertions(+), 22 deletions(-) create mode 100644 src/component/no-access/no-access.css create mode 100644 src/component/no-access/no-access.ts create mode 100644 src/event/lp-participant-moderation.event.ts create mode 100644 src/icons/shield-x.svg create mode 100644 src/model/moderation.ts diff --git a/src/component/index.ts b/src/component/index.ts index 811579ab..9c67ae2f 100644 --- a/src/component/index.ts +++ b/src/component/index.ts @@ -10,6 +10,7 @@ export * from './controls/documents-button'; export * from './controls/document-navigation'; export * from './controls/settings-button'; export * from './loading/player-loading'; +export * from './no-access/no-access'; export * from './offline/player-offline'; export * from './feature-view/feature-view'; export * from './chat-box/chat-box'; @@ -26,4 +27,4 @@ export * from './theme-settings/theme-settings'; export * from './media-settings/camera-settings'; export * from './media-settings/sound-settings'; -export * from './shoelace'; \ No newline at end of file +export * from './shoelace'; diff --git a/src/component/no-access/no-access.css b/src/component/no-access/no-access.css new file mode 100644 index 00000000..e1179864 --- /dev/null +++ b/src/component/no-access/no-access.css @@ -0,0 +1,48 @@ +:host { + display: flex; + flex-direction: column; + align-items: center; + height: inherit; + color: var(--sl-color-neutral-700); +} +:host > div { + display: flex; + flex-direction: column; + flex: 0 0 auto; + width: 50%; + padding-top: 3rem; +} +:host > div > h1 { + padding: 0.5rem 0; + align-self: center; +} +:host > div > p { + align-self: center; +} +:host > div > sl-icon { + fill: var(--sl-color-primary-600); + font-size: 6rem; + align-self: center; + padding-bottom: 0.5rem; +} + +:host > div > sl-button { + margin: var(--sl-spacing-medium); + align-self: center; +} + +:host p { + margin-top: 0; + margin-bottom: 1rem; +} + +@media (max-width: 1000px) { + :host > div { + width: 60%; + } +} +@media (max-width: 800px) { + :host > div { + width: 80%; + } +} diff --git a/src/component/no-access/no-access.ts b/src/component/no-access/no-access.ts new file mode 100644 index 00000000..7f89737c --- /dev/null +++ b/src/component/no-access/no-access.ts @@ -0,0 +1,32 @@ +import {CSSResultGroup, html} from 'lit'; +import {customElement} from 'lit/decorators.js'; +import {I18nLitElement} from '../i18n-mixin'; +import {Component} from '../component'; +import style from './player-no-access.css'; +import {t} from "i18next"; + +@customElement('player-no-access') +export class PlayerNoAccess extends Component { + + static override styles = [ + I18nLitElement.styles, + style, + ]; + + + private home() { + // Use location to navigate to the home page. + location.assign("/") + } + + protected override render() { + return html` +
+ +

${t("course.no_access.title")}

+

${t("course.no_access.description")}

+ ${t("course.overview")} +
+ `; + } +} diff --git a/src/component/player/player.controller.ts b/src/component/player/player.controller.ts index 470aceb0..951edb6c 100644 --- a/src/component/player/player.controller.ts +++ b/src/component/player/player.controller.ts @@ -31,7 +31,7 @@ import { EventEmitter } from '../../utils/event-emitter'; import { RootController } from '../controller/root.controller'; import { Controller } from '../controller/controller'; import { streamStatsStore } from '../../store/stream-stats.store'; -import { LpChatResponseEvent, LpChatStateEvent, LpEventServiceStateEvent, LpMediaStateEvent, LpParticipantPresenceEvent, LpQuizStateEvent, LpRecordingStateEvent, LpStreamStateEvent } from '../../event'; +import { LpChatResponseEvent, LpChatStateEvent, LpEventServiceStateEvent, LpMediaStateEvent, LpParticipantPresenceEvent, LpParticipantModerationEvent, LpQuizStateEvent, LpRecordingStateEvent, LpStreamStateEvent } from '../../event'; import { Toaster } from '../../utils/toaster'; import { t } from 'i18next'; @@ -79,6 +79,7 @@ export class PlayerController extends Controller implements ReactiveController { this.eventEmitter.addEventListener("lp-media-state", this.onMediaState.bind(this)); this.eventEmitter.addEventListener("lp-participant-presence", this.onParticipantPresence.bind(this)); this.eventEmitter.addEventListener("lp-stream-connection-state", this.onStreamConnectionState.bind(this)); + this.eventEmitter.addEventListener("lp-participant-moderation", this.onParticipantModeration.bind(this)); if (this.host.courseId) { this.eventService.connect(); @@ -519,12 +520,43 @@ export class PlayerController extends Controller implements ReactiveController { this.updateConnectionState(); } + private onParticipantModeration(event: LpParticipantModerationEvent) { + const moderation = event.detail; + + if (moderation.userId !== userStore.userId) { + console.log("User moderation event for user, but not me.") + return; + } + + if (moderation.moderationType === "PERMANENT_BAN") { + console.log("User banned.") + this.modalController.closeAllModals(); + + uiStateStore.setStreamState(State.NO_ACCESS); + uiStateStore.setDocumentState(State.NO_ACCESS); + this.streamController.disconnect(); + this.eventService.close(); + this.updateConnectionState(); + + Toaster.showWarning(t("course.moderation.toast.permanent_banned.title"), + t("course.moderation.toast.permanent_banned.description"), Infinity); + } + } + private updateConnectionState() { const state = uiStateStore.state; const streamState = uiStateStore.streamState; const documentState = uiStateStore.documentState; - console.log("** update state:", State[state], ", streamState", State[streamState], ", documentState", State[documentState], ", has features", featureStore.hasFeatures()); + console.log("** update state:", State[state], + ", streamState", State[streamState], + ", documentState", State[documentState], + ", has features", featureStore.hasFeatures()); + + if (streamState == State.NO_ACCESS || documentState == State.NO_ACCESS) { + this.setConnectionState(State.NO_ACCESS); + return; + } if (this.hasStream() && !courseStore.isClassroom) { if (streamState === State.CONNECTED && documentState === State.CONNECTED) { @@ -557,6 +589,10 @@ export class PlayerController extends Controller implements ReactiveController { if (uiStateStore.state === state) { return; } + if (uiStateStore.state == State.NO_ACCESS) { + console.log("no access, skip state change"); + return; + } console.log("new state", State[state]) @@ -577,4 +613,4 @@ export class PlayerController extends Controller implements ReactiveController { break; } } -} \ No newline at end of file +} diff --git a/src/component/player/player.ts b/src/component/player/player.ts index ee6b1c81..81cb61d6 100644 --- a/src/component/player/player.ts +++ b/src/component/player/player.ts @@ -32,6 +32,8 @@ export class LecturePlayer extends Component { return html``; case State.DISCONNECTED: return html``; + case State.NO_ACCESS: + return html``; } } } diff --git a/src/event/index.ts b/src/event/index.ts index becb8c5d..c8652374 100644 --- a/src/event/index.ts +++ b/src/event/index.ts @@ -11,6 +11,7 @@ export * from "./lp-participant-destroyed.event"; export * from "./lp-participant-error.event"; export * from "./lp-participant-joined.event"; export * from "./lp-participant-left.event"; +export * from "./lp-participant-moderation.event"; export * from "./lp-participant-presence.event"; export * from "./lp-participant-state.event"; export * from './lp-quiz-state.event'; @@ -21,4 +22,4 @@ export * from './lp-speech-state.event'; export * from "./lp-stream-capture-stats.event"; export * from "./lp-stream-connection-state.event"; export * from './lp-stream-state.event'; -export * from './lp-void.event'; \ No newline at end of file +export * from './lp-void.event'; diff --git a/src/event/lp-participant-joined.event.ts b/src/event/lp-participant-joined.event.ts index bce511fc..744687e5 100644 --- a/src/event/lp-participant-joined.event.ts +++ b/src/event/lp-participant-joined.event.ts @@ -1,7 +1,9 @@ +import {VideoRoomParticipant} from "janus-gateway"; + export type LpParticipantJoinedEvent = CustomEvent; declare global { interface GlobalEventHandlersEventMap { "lp-participant-joined": LpParticipantJoinedEvent; } -} \ No newline at end of file +} diff --git a/src/event/lp-participant-moderation.event.ts b/src/event/lp-participant-moderation.event.ts new file mode 100644 index 00000000..762c11a0 --- /dev/null +++ b/src/event/lp-participant-moderation.event.ts @@ -0,0 +1,9 @@ +import {CourseParticipantModeration} from "../model/moderation"; + +export type LpParticipantModerationEvent = CustomEvent; + +declare global { + interface GlobalEventHandlersEventMap { + "lp-participant-moderation": LpParticipantModerationEvent; + } +} diff --git a/src/icons/shield-x.svg b/src/icons/shield-x.svg new file mode 100644 index 00000000..248c568c --- /dev/null +++ b/src/icons/shield-x.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/locales/de/main.json b/src/locales/de/main.json index 9d40786d..5269c70c 100644 --- a/src/locales/de/main.json +++ b/src/locales/de/main.json @@ -1,4 +1,5 @@ { + "course.overview": "Kurs Übersicht", "course.loading": "Lade Kurs...", "course.unavailable": "Der Stream hat noch nicht begonnen. Bitte versuchen Sie es kurz vor Beginn der Veranstaltung erneut.", "settings.title": "Einstellungen", @@ -115,6 +116,10 @@ "course.recorded.modal.title": "Dieser Kurs wird aufgezeichnet", "course.recorded.modal.message": "Die Aufzeichnung kann nachträglich veröffentlicht werden. Dies betrifft als digitale Veranstaltung Ihre Beiträge bei aktiven Wortmeldungen.", "course.recorded.modal.accept": "Verstanden", + "course.no_access.title": "Sie haben keinen Zugriff auf diesen Kurs.", + "course.no_access.description": "Der Zugriff auf diesen Kurs ist Ihnen nicht gestattet. Bitte setzen Sie sich mit den Organisatoren:innen in Verbindung.", + "course.moderation.toast.permanent_banned.title": "Sie wurden vom Kurs ausgeschlossen", + "course.moderation.toast.permanent_banned.description": "Bei Fragen wenden Sie sich an die Organisator:innen.", "documents.open.document": "PDF-Dokument öffnen", "documents.open.whiteboard": "Whiteboard öffnen", "entry.modal.title": "Medienwiedergabe Starten", @@ -173,4 +178,4 @@ "vpn.required.description.vpn": "Sichere Verbindung ins TU-Netz und Zugang zu Services außerhalb des TU Datennetzes.", "vpn.required.description.wlan": "Nahezu flächendeckender Internetzugang am Campus und im Kongresszentrum \u201Cdarmstadtium\u201D.", "vpn.required.reconnect": "Bitte die Seite neu laden, nachdem die Verbindung erfolgreich aufgebaut worden ist." -} \ No newline at end of file +} diff --git a/src/locales/en/main.json b/src/locales/en/main.json index 15f83aaf..ce1c5fa1 100644 --- a/src/locales/en/main.json +++ b/src/locales/en/main.json @@ -1,4 +1,5 @@ { + "course.overview": "Course Overview", "course.loading": "Loading Course...", "course.unavailable": "The stream has not started yet. Please try again closer to the event start time.", "settings.title": "Device settings", @@ -115,6 +116,10 @@ "course.recorded.modal.title": "This course is being recorded", "course.recorded.modal.message": "The recording can be published later. As a digital event, this affects your contributions during active requests to speak.", "course.recorded.modal.accept": "Got it", + "course.no_access.title": "You are not allowed to access this course.", + "course.no_access.description": "You do not have access to this course. Please contact the organizers.", + "course.moderation.toast.permanent_banned.title": "You have been permanently banned from the course", + "course.moderation.toast.permanent_banned.description": "If you have any questions, please contact the organizers.", "documents.open.document": "Open PDF Document", "documents.open.whiteboard": "Open Whiteboard", "entry.modal.title": "Start media playback", @@ -168,4 +173,4 @@ "vpn.required.description.vpn": "Secure connection into the TU network and access to services outside of the TU data network.", "vpn.required.description.wlan": "Nearly full-coverage Internet access on campus and in the \u201Cdarmstadtium\u201D congress center.", "vpn.required.reconnect": "Please reload the page after the connection has been successfully established." -} \ No newline at end of file +} diff --git a/src/model/moderation.ts b/src/model/moderation.ts new file mode 100644 index 00000000..7103e89c --- /dev/null +++ b/src/model/moderation.ts @@ -0,0 +1,12 @@ +export type ModerationType = "PERMANENT_BAN"; + +export interface CourseParticipantModeration { + + readonly userId: string; + + readonly firstName: string; + + readonly familyName: string; + + readonly moderationType: ModerationType; +} diff --git a/src/service/event.service.ts b/src/service/event.service.ts index 9352cdce..1f37395c 100644 --- a/src/service/event.service.ts +++ b/src/service/event.service.ts @@ -17,6 +17,8 @@ export class EventService extends EventTarget { private readonly subServices: EventSubService[]; + private client: Client | undefined; + constructor(courseId: number, eventEmitter: EventEmitter) { super(); @@ -85,10 +87,17 @@ export class EventService extends EventTarget { client.deactivate(); }); + this.client = client; + return client; } + close() { + console.log("** EventService closes, disconnecting STOMP"); + this.client?.deactivate() + } + private handleEvent(eventName: string, body: string) { this.eventEmitter.dispatchEvent(Utils.createEvent(eventName, JSON.parse(body))); } -} \ No newline at end of file +} diff --git a/src/utils/state.ts b/src/utils/state.ts index e1489320..9961474c 100644 --- a/src/utils/state.ts +++ b/src/utils/state.ts @@ -4,6 +4,7 @@ export enum State { CONNECTED, CONNECTED_FEATURES, DISCONNECTED, - RECONNECTING + RECONNECTING, + NO_ACCESS, -} \ No newline at end of file +} diff --git a/src/utils/toaster.ts b/src/utils/toaster.ts index b87055b7..a9882017 100644 --- a/src/utils/toaster.ts +++ b/src/utils/toaster.ts @@ -1,23 +1,23 @@ export class Toaster { - public static show(title: string, message?: string): void { - this.showNotification(title, message, "neutral", "info-circle"); + public static show(title: string, message?: string, duration = 3000): void { + this.showNotification(title, message, "neutral", "info-circle", duration); } - public static showInfo(title: string, message?: string): void { - this.showNotification(title, message, "primary", "info-circle"); + public static showInfo(title: string, message?: string, duration = 3000): void { + this.showNotification(title, message, "primary", "info-circle", duration); } - public static showSuccess(title: string, message?: string): void { - this.showNotification(title, message, "success", "check2-circle"); + public static showSuccess(title: string, message?: string, duration = 3000): void { + this.showNotification(title, message, "success", "check2-circle", duration); } - public static showWarning(title: string, message?: string): void { - this.showNotification(title, message, "warning", "exclamation-triangle"); + public static showWarning(title: string, message?: string, duration = 3000): void { + this.showNotification(title, message, "warning", "exclamation-triangle", duration); } - public static showError(title: string, message?: string): void { - this.showNotification(title, message, "danger", "exclamation-octagon"); + public static showError(title: string, message?: string, duration = 3000): void { + this.showNotification(title, message, "danger", "exclamation-octagon", duration); } public static showNotification(title: string, message: string | undefined, variant: string, icon: string, duration = 3000): Promise { @@ -53,4 +53,4 @@ export class Toaster { return div.innerHTML; } -} \ No newline at end of file +}