Commit ed5ae79a authored by Christopher Mark Fullarton's avatar Christopher Mark Fullarton
Browse files

Merge branch 'assignment-change-lobby' into 'staging'

Change the layout of the quiz lobby

See merge request !143
parents 88979b7a 8a99bba9
......@@ -4,56 +4,78 @@
<span class="ml-2">{{attendeeService.getActiveMembers().length}}</span>
</h3>
<div class="ml-auto" *ngIf="(quizService.isOwner && quizService.quiz?.sessionConfig?.music.enabled.lobby) || (quizService.playAudio && !quizService.isOwner && quizService.quiz?.sessionConfig?.music.shared.lobby)">
<div class="ml-auto"
*ngIf="(quizService.isOwner && quizService.quiz?.sessionConfig?.music.enabled.lobby) || (quizService.playAudio && !quizService.isOwner && quizService.quiz?.sessionConfig?.music.shared.lobby)">
<app-audio-player [config]="musicConfig"></app-audio-player>
</div>
<div *ngIf="attendeeService.ownAttendee as myself"
class="my-1 d-flex flex-wrap flex-sm-nowrap col-auto">
<p class="text-center mr-sm-2 d-flex justify-content-center flex-grow-1 align-self-center text-nowrap">{{'component.lobby.own_nickname' | translate}}</p>
<div [style.background-color]="'#' + sanitizeHTML(myself.colorCode)"
[style.color]="'#' + sanitizeHTML(ColorTransform.transformForegroundColor(ColorTransform.hexToRgb(myself.colorCode)))"
class="p-2 rounded d-flex align-items-center justify-content-center own-nick w-100">
<div *ngIf="myself.groupName" class="mr-2 flex-grow-1 flex-shrink-0">
<div *ngIf="isHtmlNickname(myself.groupName)"
[innerHTML]="parseNickname(myself.groupName)"></div>
<div *ngIf="!isHtmlNickname(myself.groupName)">[{{myself.groupName}}]</div>
</div>
<p class="flex-grow-1 break-word" [class.text-center]="!myself.groupName">
<span *ngIf="isHtmlNickname(myself.name)"
[innerHTML]="parseNickname(myself.name)"></span>
<span *ngIf="!isHtmlNickname(myself.name)">{{myself.name}}</span>
</p>
</div>
</div>
</div>
<div class="row">
<ng-container *ngIf="!attendeeService.attendees.length">
<div class="col-12">
<h3 class="text-center center-top d-flex align-self-center justify-content-center">{{'component.lobby.waiting_for_players' | translate}}</h3>
<ng-container *ngIf="this.teams?.length">
<div *ngFor="let team of teams"
[style.--bg-color]="sanitizeHTML(team.color)"
[style.color]="'#' + sanitizeHTML(ColorTransform.transformForegroundColor(ColorTransform.hexToRgb(team.color)))"
class="my-1 p-2 rounded d-flex align-items-center flex-wrap w-100 position-relative team-container">
<div *ngIf="isHtmlNickname(team.name)"
[innerHTML]="parseNickname(team.name)"
class="col-12 team-name-header p-2"></div>
<div *ngIf="!isHtmlNickname(team.name)"
class="col-12 team-name-header p-2 h3">
<p>{{team.name}}</p>
</div>
<ng-container *ngIf="!getMembersForTeam(team.name)?.length">
<div class="col-12">
<p class="text-center d-flex align-self-center h4 m-2">{{'component.lobby.waiting_for_players' | translate}}</p>
</div>
</ng-container>
<ng-container *ngIf="getMembersForTeam(team.name)?.length">
<p class="p-2 col-sm-6 col-md-4 col-lg-3 text-center animated-attendee"
*ngFor="let elem of getMembersForTeam(team.name)"
(click)="openKickMemberModal(removeMemberModal, elem.attendee.name)"
[class.remove]="elem.isRemoved"
[class.new]="elem.isNew"
[class.own]="attendeeService.ownAttendee && attendeeService.ownAttendee.name === elem.attendee.name"
[class.cursor-pointer]="quizService.isOwner">
<span *ngIf="attendeeService.ownAttendee && attendeeService.ownAttendee.name === elem.attendee.name">
{{'component.lobby.you' | translate}}
</span>
<span *ngIf="isHtmlNickname(elem.attendee.name)"
[innerHTML]="parseNickname(elem.attendee.name)"></span>
<span *ngIf="!isHtmlNickname(elem.attendee.name)">{{elem.attendee.name}}</span>
</p>
</ng-container>
</div>
</ng-container>
<div *ngFor="let elem of attendeeService.attendees"
class="my-1 col-sm-6 col-md-4 col-lg-3 d-flex">
<div (click)="openKickMemberModal(removeMemberModal, elem.name)"
[class.cursor-pointer]="quizService.isOwner"
[style.background-color]="sanitizeHTML(getColorForNick(elem))"
[style.color]="'#' + sanitizeHTML(ColorTransform.transformForegroundColor(ColorTransform.hexToRgb(getColorForNick(elem))))"
class="p-2 rounded d-flex justify-content-center align-items-center w-100">
<div *ngIf="elem.groupName" class="mr-2 flex-grow-1 flex-shrink-0">
<div *ngIf="isHtmlNickname(elem.groupName)"
[innerHTML]="parseNickname(elem.groupName)"></div>
<div *ngIf="!isHtmlNickname(elem.groupName)">[{{elem.groupName}}]</div>
<ng-container *ngIf="!this.teams?.length">
<ng-container *ngIf="!getMembersForTeam()?.length">
<div class="col-12">
<h3 class="text-center center-top d-flex align-self-center">{{'component.lobby.waiting_for_players' | translate}}</h3>
</div>
<p class="flex-grow-1 break-word" [class.text-center]="!elem.groupName">
<span *ngIf="isHtmlNickname(elem.name)"
[innerHTML]="parseNickname(elem.name)"></span>
<span *ngIf="!isHtmlNickname(elem.name)">{{elem.name}}</span>
</p>
</div>
</div>
</ng-container>
<ng-container *ngIf="getMembersForTeam()?.length">
<div *ngFor="let elem of getMembersForTeam()"
[class.remove]="elem.isRemoved"
[class.new]="elem.isNew"
[class.own]="attendeeService.ownAttendee && attendeeService.ownAttendee.name === elem.attendee.name"
class="my-1 col-sm-6 col-md-4 col-lg-3 d-flex animated-attendee">
<div (click)="openKickMemberModal(removeMemberModal, elem.attendee.name)"
[class.cursor-pointer]="quizService.isOwner"
[style.background-color]="sanitizeHTML('#' + elem.attendee.colorCode)"
[style.color]="'#' + sanitizeHTML(ColorTransform.transformForegroundColor(ColorTransform.hexToRgb('#' + elem.attendee.colorCode)))"
class="p-2 rounded d-flex justify-content-center align-items-center w-100">
<p class="flex-grow-1 break-word text-center">
<span *ngIf="attendeeService.ownAttendee && attendeeService.ownAttendee.name === elem.attendee.name">
{{'component.lobby.you' | translate}}
</span>
<span *ngIf="isHtmlNickname(elem.attendee.name)"
[innerHTML]="parseNickname(elem.attendee.name)"></span>
<span *ngIf="!isHtmlNickname(elem.attendee.name)">{{elem.attendee.name}}</span>
</p>
</div>
</div>
</ng-container>
</ng-container>
</div>
<ng-template #removeMemberModal
......
.attendee-text {
/* Font size of "fas fa-user" => fa-2x */
line-height: 2em;
$line-height: (1.75rem * 1.2);
$half-line-height: ($line-height / 2);
$padding: 0.5rem;
$double-padding: ($padding * 2);
$tab-border-radius: 15px;
$outer-border-radius: 4.5px;
$arc-width: $tab-border-radius;
$arc-height: $tab-border-radius;
$arc-width-minus-1: calc(#{$arc-width} - 1px);
$arc-width-minus-1-negated: calc(1px - #{$arc-width});
.team-container {
background: var(--bg-color);
margin-top: calc(#{$line-height} + #{$double-padding + $padding}) !important;
}
:host .team-wrapper {
:host .team-name-header {
display: inline-block;
position: absolute;
border-top-left-radius: $tab-border-radius;
border-top-right-radius: $tab-border-radius;
background: var(--bg-color);
width: unset;
max-width: calc(100% - #{2 * $outer-border-radius} - #{2 * $arc-width});
left: calc(#{$arc-width} + #{$outer-border-radius});
top: calc(#{-$line-height} - #{$double-padding});
::ng-deep p {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
::ng-deep img[alt*="emoji_"] {
height: 64px;
width: 64px;
width: $line-height;
height: $line-height;
}
&::before {
content: "";
position: absolute;
left: $arc-width-minus-1-negated;
bottom: 0;
width: $arc-width;
height: $arc-height;
background: radial-gradient(ellipse at 0 0, transparent $arc-width-minus-1, var(--bg-color) $arc-width);
}
&::after {
content: "";
position: absolute;
right: $arc-width-minus-1-negated;
bottom: 0;
width: $arc-width;
height: $arc-height;
background: radial-gradient(ellipse at 100% 0, transparent $arc-width-minus-1, var(--bg-color) $arc-width);
}
}
.animated-attendee {
&.new {
animation: smallBounceIn 2s ease-in-out;
}
&.own {
animation: bounceIn 2s ease-in-out;
}
&.remove {
animation: bounceOut 1s ease-out;
}
}
@keyframes bounceOut {
30% {
transform: scale(1.1);
}
100% {
transform: scale(0);
}
}
@keyframes bounceIn {
0% {
transform: scale(1.4);
}
20% {
transform: scale(0.6);
}
40% {
transform: scale(1.2);
}
60% {
transform: scale(0.8);
}
80% {
transform: scale(1.1);
}
}
@keyframes smallBounceIn {
0% {
transform: scale(1.3);
}
33% {
transform: scale(0.7);
}
66% {
transform: scale(1.1);
}
}
......@@ -33,7 +33,14 @@ import { TrackingService } from '../../../service/tracking/tracking.service';
import { UserService } from '../../../service/user/user.service';
import { EditModeConfirmComponent } from './modals/edit-mode-confirm/edit-mode-confirm.component';
import { QrCodeContentComponent } from './modals/qr-code-content/qr-code-content.component';
import {SettingsService} from '../../../service/settings/settings.service';
import { SettingsService } from '../../../service/settings/settings.service';
import { IMemberGroupBase } from '../../../lib/interfaces/users/IMemberGroupBase';
interface IAttendeeInformation {
attendee: MemberEntity;
isRemoved?: boolean;
isNew?: boolean;
}
@Component({
selector: 'app-quiz-lobby',
......@@ -49,34 +56,36 @@ export class QuizLobbyComponent implements OnInit, OnDestroy, IHasTriggeredNavig
private _kickMemberModalRef: NgbActiveModal;
private readonly _destroy = new Subject();
private readonly _messageSubscriptions: Array<string> = [];
private readonly _teamMembers = new Map<string, IAttendeeInformation[]>();
public hasTriggeredNavigation: boolean;
public musicConfig: IAudioPlayerConfig;
public readonly ColorTransform = ColorTransform;
public teams: IMemberGroupBase[] = null;
get nickToRemove(): string {
return this._nickToRemove;
}
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
public quizService: QuizService,
public attendeeService: AttendeeService,
private footerBarService: FooterBarService,
private headerLabelService: HeaderLabelService,
private themesService: ThemesService,
private router: Router,
private connectionService: ConnectionService,
private sanitizer: DomSanitizer,
private modalService: NgbModal,
private trackingService: TrackingService,
private memberApiService: MemberApiService,
private quizApiService: QuizApiService,
private ngbModal: NgbModal,
private sharedService: SharedService,
private userService: UserService,
private messageQueue: SimpleMQ,
private customMarkdownService: CustomMarkdownService,
private settingsService: SettingsService
@Inject(PLATFORM_ID) private platformId: Object,
public quizService: QuizService,
public attendeeService: AttendeeService,
private footerBarService: FooterBarService,
private headerLabelService: HeaderLabelService,
private themesService: ThemesService,
private router: Router,
private connectionService: ConnectionService,
private sanitizer: DomSanitizer,
private modalService: NgbModal,
private trackingService: TrackingService,
private memberApiService: MemberApiService,
private quizApiService: QuizApiService,
private ngbModal: NgbModal,
private sharedService: SharedService,
private userService: UserService,
private messageQueue: SimpleMQ,
private customMarkdownService: CustomMarkdownService,
private settingsService: SettingsService
) {
if (isPlatformBrowser(this.platformId)) {
sessionStorage.removeItem(StorageKey.CurrentQuestionIndex);
......@@ -85,8 +94,12 @@ export class QuizLobbyComponent implements OnInit, OnDestroy, IHasTriggeredNavig
}
public ngOnInit(): void {
this.attendeeService.attendeeAmount.pipe(takeUntil(this._destroy)).subscribe(() => this.updateAttendees());
this.quizService.quizUpdateEmitter.pipe(takeUntil(this._destroy)).subscribe(quiz => {
console.log('QuizLobbyComponent: quizUpdateEmitter fired', quiz);
this.teams = quiz?.sessionConfig?.nicks?.memberGroups;
this._teamMembers.clear();
this.attendeeService.reloadData();
if (!quiz) {
return;
......@@ -107,8 +120,8 @@ export class QuizLobbyComponent implements OnInit, OnDestroy, IHasTriggeredNavig
autostart: true,
hideControls: true,
original_volume: String(this.quizService.quiz.sessionConfig.music.volumeConfig.useGlobalVolume ?
this.quizService.quiz.sessionConfig.music.volumeConfig.global :
this.quizService.quiz.sessionConfig.music.volumeConfig.lobby),
this.quizService.quiz.sessionConfig.music.volumeConfig.global :
this.quizService.quiz.sessionConfig.music.volumeConfig.lobby),
src: this.quizService.quiz.sessionConfig.music.titleConfig.lobby,
target: AudioPlayerConfigTarget.lobby
};
......@@ -202,13 +215,8 @@ export class QuizLobbyComponent implements OnInit, OnDestroy, IHasTriggeredNavig
return String(value);
}
public getColorForNick(elem: MemberEntity): string {
const groups = this.quizService.quiz?.sessionConfig.nicks.memberGroups;
if (elem.groupName && groups.length) {
return groups.find(value => value.name === elem.groupName)?.color ?? '#' + elem.colorCode;
}
return '#' + elem.colorCode;
public getMembersForTeam(teamName: string = 'default'): IAttendeeInformation[] {
return this._teamMembers.get(teamName);
}
private handleNewQuiz(): void {
......@@ -283,9 +291,8 @@ export class QuizLobbyComponent implements OnInit, OnDestroy, IHasTriggeredNavig
}
self.isLoading = true;
this.quizApiService.nextStep(this.quizService.quiz.name).subscribe((data: IMessage) => {
this.quizService.readingConfirmationRequested = this.settingsService.frontEnv?.readingConfirmationEnabled ? data.step
=== MessageProtocol.ReadingConfirmationRequested
: false;
this.quizService.readingConfirmationRequested = this.settingsService.frontEnv?.readingConfirmationEnabled ?
data.step === MessageProtocol.ReadingConfirmationRequested : false;
this.hasTriggeredNavigation = true;
this.router.navigate(['/quiz', 'flow', 'results']);
self.isLoading = false;
......@@ -303,18 +310,19 @@ export class QuizLobbyComponent implements OnInit, OnDestroy, IHasTriggeredNavig
}
const promise = this.attendeeService.attendees.length ? //
this.ngbModal.open(EditModeConfirmComponent).result : //
new Promise<void>(resolve => resolve());
this.ngbModal.open(EditModeConfirmComponent).result : //
new Promise<void>(resolve => resolve());
promise.then(() => {
this.hasTriggeredNavigation = true;
this._destroy.next();
this.quizService.loadDataToEdit(sessionStorage.getItem(StorageKey.CurrentQuizName), false)
.then(() => this.router.navigate(['/quiz', 'manager', 'overview']))
.then(() => this.quizApiService.deleteActiveQuiz(this.quizService.quiz).subscribe())
.then(() => {
this.attendeeService.cleanUp().subscribe();
});
}).catch(() => {});
.then(() => this.router.navigate(['/quiz', 'manager', 'overview']))
.then(() => this.quizApiService.deleteActiveQuiz(this.quizService.quiz).subscribe())
.then(() => {
this.attendeeService.cleanUp().subscribe();
});
}).catch(() => {
});
};
}
......@@ -350,6 +358,49 @@ export class QuizLobbyComponent implements OnInit, OnDestroy, IHasTriggeredNavig
}
private updateAttendees(): void {
const hasTeams = !!this.teams?.length;
const own = this.attendeeService.ownAttendee;
if (own && hasTeams) {
this.teams.sort((a, b) => {
return (b.name === own.groupName ? 1 : 0) - (a.name === own.groupName ? 1 : 0);
});
}
if (!hasTeams) {
this.updateTeam('default', this.attendeeService.attendees);
return;
}
for (const team of this.teams) {
this.updateTeam(team.name, this.attendeeService.attendees.filter(attendee => attendee.groupName === team.name));
}
}
private updateTeam(name: string, currentAttendees: MemberEntity[]): void {
let memberInfos = this._teamMembers.get(name);
let isNew = true;
if (!memberInfos) {
memberInfos = [];
isNew = false;
this._teamMembers.set(name, memberInfos);
}
const newRemovedMembers = memberInfos.filter(info => !info.isRemoved && !currentAttendees.some(attendee => attendee.name === info.attendee.name));
newRemovedMembers.forEach(v => {
v.isRemoved = true;
v.isNew = false;
});
currentAttendees.filter(attendee => !memberInfos.some(info => !info.isRemoved && info.attendee.name === attendee.name))
.forEach(newMember => memberInfos.push({ isRemoved: false, attendee: newMember, isNew }));
setTimeout(() => newRemovedMembers.forEach(member => {
const index = memberInfos.indexOf(member);
if (index < 0) {
console.error('Can\'t find attendee on remove.');
return;
}
memberInfos.splice(index, 1);
}), 1000);
}
private canStartQuiz(): boolean {
if (!this.quizService.quiz.sessionConfig.nicks.memberGroups.length) {
return this.attendeeService.attendees.length > 0;
......
......@@ -395,7 +395,7 @@
"lobby": {
"start_quiz": "Los geht's!",
"kick_member_confirmation": "Soll die Person »{NAME}« aus dem Quiz entfernt werden?",
"own_nickname": "Du bist:",
"you": "[Du]",
"waiting_for_players": "Warten auf die anderen…"
},
"nickname_categories": {
......
......@@ -395,7 +395,7 @@
"lobby": {
"start_quiz": "Let's go!",
"kick_member_confirmation": "Shall the player »{NAME}« be removed from the session?",
"own_nickname": "Your nickname:",
"you": "[You]",
"waiting_for_players": "Waiting for the players…"
},
"nickname_categories": {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment