diff --git a/conf/evolutions/default/133.sql b/conf/evolutions/default/133.sql index afef3c930..f214fd8c0 100644 --- a/conf/evolutions/default/133.sql +++ b/conf/evolutions/default/133.sql @@ -1,5 +1,4 @@ # --- !Ups -<<<<<<< HEAD INSERT INTO permission (id, object_version, type, description) VALUES (2, 1, 2, 'can create BYOD exams') # --- !Downs diff --git a/ui/src/app/enrolment/search/exam-search.component.ts b/ui/src/app/enrolment/search/exam-search.component.ts index f68b8d9af..d0ca7cef8 100644 --- a/ui/src/app/enrolment/search/exam-search.component.ts +++ b/ui/src/app/enrolment/search/exam-search.component.ts @@ -112,7 +112,7 @@ import { ExamSearchService } from './exam-search.service'; }) export class ExamSearchComponent implements OnInit, OnDestroy { exams: EnrolmentInfo[] = []; - filterChanged: Subject = new Subject(); + filterChanged = new Subject(); ngUnsubscribe = new Subject(); filter = { text: '' }; permissionCheck = { active: false }; diff --git a/ui/src/app/enrolment/waiting-room/waiting-room.component.html b/ui/src/app/enrolment/waiting-room/waiting-room.component.html index 8140d8c91..aba0e9c4a 100644 --- a/ui/src/app/enrolment/waiting-room/waiting-room.component.html +++ b/ui/src/app/enrolment/waiting-room/waiting-room.component.html @@ -9,12 +9,12 @@

@if (delayCounter$) {
- + {{ delayCounter$ | async }} {{ 'i18n_seconds_until_examination_starts' | translate }}
} @else if (isUpcoming()) {
- + {{ 'i18n_redirect_to_exam_notice' | translate }}
} diff --git a/ui/src/app/enrolment/waiting-room/waiting-room.component.ts b/ui/src/app/enrolment/waiting-room/waiting-room.component.ts index 3d8ae71d3..d9cae3cc0 100644 --- a/ui/src/app/enrolment/waiting-room/waiting-room.component.ts +++ b/ui/src/app/enrolment/waiting-room/waiting-room.component.ts @@ -28,7 +28,7 @@ import { MathJaxDirective } from '../../shared/math/math-jax.directive'; import { CourseCodeComponent } from '../../shared/miscellaneous/course-code.component'; import { TeacherListComponent } from '../../shared/user/teacher-list.component'; import type { ExamEnrolment } from '../enrolment.model'; -import { Observable, interval, map, startWith, take } from 'rxjs'; +import { Observable, interval, map, startWith } from 'rxjs'; type WaitingReservation = Reservation & { occasion: { startAt: string; endAt: string } }; type WaitingEnrolment = Omit & { @@ -99,14 +99,13 @@ export class WaitingRoomComponent implements OnInit, OnDestroy { private startScheduled = () => { window.clearTimeout(this.startTimerId); - const offset = Math.round( + const offset = Math.ceil( DateTime.fromJSDate(this.getStart()).plus({ seconds: this.enrolment.delay }).toSeconds() - DateTime.now().toSeconds(), ); - this.delayTimerId = window.setTimeout(this.Session.checkSession, offset); + this.delayTimerId = window.setTimeout(this.Session.checkSession, offset * 1000); this.delayCounter$ = interval(1000).pipe( startWith(0), - take(offset), map((n) => offset - n), ); }; diff --git a/ui/src/app/examination/clock/examination-clock.component.ts b/ui/src/app/examination/clock/examination-clock.component.ts index a2865d6e8..2eca73a13 100644 --- a/ui/src/app/examination/clock/examination-clock.component.ts +++ b/ui/src/app/examination/clock/examination-clock.component.ts @@ -12,10 +12,12 @@ * on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and limitations under the Licence. */ -import { NgClass } from '@angular/common'; +import { AsyncPipe, NgClass } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, signal } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; +import { Duration } from 'luxon'; +import { Observable, Subject, interval, map, startWith, switchMap, take } from 'rxjs'; @Component({ selector: 'xm-examination-clock', @@ -23,34 +25,32 @@ import { TranslateModule } from '@ngx-translate/core';
- @if (showRemainingTime) { + @if (showRemainingTime()) {
{{ 'i18n_exam_time_left' | translate }}:
- } - @if (!showRemainingTime) { + } @else {
{{ 'i18n_clock_hidden' | translate }}
}
- @if (showRemainingTime) { + @if (showRemainingTime()) { {{ formatRemainingTime() }}{{ remainingTime$ | async }} }
-
@@ -58,64 +58,51 @@ import { TranslateModule } from '@ngx-translate/core';
`, standalone: true, - imports: [NgClass, TranslateModule], + imports: [NgClass, AsyncPipe, TranslateModule], }) export class ExaminationClockComponent implements OnInit, OnDestroy { @Input() examHash = ''; @Output() timedOut = new EventEmitter(); - syncInterval = 15; - secondsSinceSync = this.syncInterval + 1; - alarmThreshold = 300; - remainingTime = this.alarmThreshold + 1; - showRemainingTime = true; - pollerId = 0; + + showRemainingTime = signal(false); + remainingTime$?: Observable; + isTimeScarce$?: Observable; + + private syncInterval = 60; + private alarmThreshold = 300; + private subject = new Subject(); + private destroy = new Subject(); constructor(private http: HttpClient) {} ngOnInit() { - this.checkRemainingTime(); + const sync$ = this.http.get(`/app/time/${this.examHash}`); + interval(this.syncInterval * 1000) + .pipe( + startWith(0), + switchMap(() => + sync$.pipe( + switchMap((t) => + interval(1000).pipe( + map((n) => t - n), + take(this.syncInterval), + ), + ), + ), + ), + ) + .subscribe(this.subject); + + this.remainingTime$ = this.subject.pipe(map((n) => Duration.fromObject({ seconds: n }).toFormat('hh:mm:ss'))); + this.isTimeScarce$ = this.subject.pipe(map((n) => n <= this.alarmThreshold)); + this.subject.subscribe((n) => { + if (n === 0) this.timedOut.emit(); + }); + this.showRemainingTime.set(true); } ngOnDestroy() { - if (this.pollerId) { - window.clearTimeout(this.pollerId); - } + this.destroy.next(undefined); + this.destroy.complete(); } - - formatRemainingTime = (): string => { - if (!this.remainingTime) { - return ''; - } - const hours = Math.floor(this.remainingTime / 60 / 60); - const minutes = Math.floor(this.remainingTime / 60) % 60; - const seconds = this.remainingTime % 60; - return `${hours}:${this.zeroPad(minutes)}:${this.zeroPad(seconds)}`; - }; - - private checkRemainingTime = () => { - this.secondsSinceSync++; - if (this.secondsSinceSync > this.syncInterval) { - // Sync time with backend - this.secondsSinceSync = 0; - this.setRemainingTime(); - } else if (this.remainingTime !== undefined) { - // Decrease seconds - this.remainingTime--; - } - if (this.remainingTime !== undefined && this.remainingTime <= 0) { - this.notifyTimeout(); - } - - this.pollerId = window.setTimeout(this.checkRemainingTime, 1000); - }; - - private setRemainingTime = () => - this.http.get('/app/time/' + this.examHash).subscribe((resp) => (this.remainingTime = resp)); - - private notifyTimeout = () => { - window.clearTimeout(this.pollerId); - this.timedOut.emit(); - }; - - private zeroPad = (n: number): string => ('0' + n).slice(-2); } diff --git a/ui/src/app/examination/header/examination-header.component.ts b/ui/src/app/examination/header/examination-header.component.ts index b9af2a99f..c7a76c965 100644 --- a/ui/src/app/examination/header/examination-header.component.ts +++ b/ui/src/app/examination/header/examination-header.component.ts @@ -45,32 +45,6 @@ import type { Examination } from '../examination.model'; }
-
- @if (!isPreview) { - - } -
-
-

- {{ exam.course?.name }} - @if (exam.course) { - - } -

-
-
-
-
- -
-
- - - -
-
-
-
`, standalone: true, imports: [CourseCodeComponent, ExaminationClockComponent],