Commit 6066864e authored by Christopher Mark Fullarton's avatar Christopher Mark Fullarton
Browse files

Replaces the static markdown and highlight.js packages with ngx-markdown and prism

parent a78d1121
......@@ -44,6 +44,7 @@
},
"styles": [
"src/styles/styles.scss",
"node_modules/prism-themes/themes/prism-ghcolors.css",
{
"input": "src/styles/themes/theme-arsnova-dot-click-contrast.scss",
"bundleName": "theme-arsnova-dot-click-contrast",
......@@ -101,6 +102,9 @@
}
],
"scripts": [
"node_modules/ngx-markdown/node_modules/marked/lib/marked.js",
"node_modules/prismjs/prism.js",
"node_modules/prismjs/components.js"
]
},
"configurations": {
......@@ -129,6 +133,7 @@
],
"styles": [
"src/styles/styles.scss",
"node_modules/prism-themes/themes/prism-ghcolors.css",
{
"input": "src/styles/themes/theme-arsnova-dot-click-contrast.scss",
"bundleName": "theme-arsnova-dot-click-contrast",
......@@ -211,6 +216,7 @@
],
"styles": [
"src/styles/styles.scss",
"node_modules/prism-themes/themes/prism-ghcolors.css",
{
"input": "src/styles/themes/theme-arsnova-dot-click-contrast.scss",
"bundleName": "theme-arsnova-dot-click-contrast",
......@@ -301,6 +307,7 @@
],
"styles": [
"src/styles/styles.scss",
"node_modules/prism-themes/themes/prism-ghcolors.css",
{
"input": "src/styles/themes/theme-arsnova-dot-click-contrast.scss",
"bundleName": "theme-arsnova-dot-click-contrast",
......@@ -391,6 +398,7 @@
],
"styles": [
"src/styles/styles.scss",
"node_modules/prism-themes/themes/prism-ghcolors.css",
{
"input": "src/styles/themes/theme-arsnova-dot-click-contrast.scss",
"bundleName": "theme-arsnova-dot-click-contrast",
......@@ -491,7 +499,11 @@
"src/styles/themes"
]
},
"scripts": [],
"scripts": [
"node_modules/ngx-markdown/node_modules/marked/lib/marked.js",
"node_modules/prismjs/prism.js",
"node_modules/prismjs/components.js"
],
"assets": [
"src/assets/fonts",
"src/assets/js",
......
......@@ -72,14 +72,14 @@
"bootstrap": "^4.3.1",
"cors": "^2.8.5",
"dexie": "^2.0.4",
"highlight.js": "^9.16.2",
"marked": "git+https://github.com/trayhem/marked.git",
"messageformat": "^2.3.0",
"ng2-simple-mq": "^8.2.1",
"ngx-infinite-scroll": "^8.0.1",
"ngx-markdown": "^8.2.1",
"ngx-toastr": "^11.2.1",
"ngx-translate-messageformat-compiler": "^4.5.0",
"node-sass": "^4.13.0",
"prism-themes": "^1.3.0",
"rxjs": "~6.4.0",
"ts-loader": "^6.2.1",
"tslib": "^1.10.0",
......
import * as highlight from 'highlight.js';
import * as marked from 'marked';
function createElementFromHTML(htmlString): Node {
const div = document.createElement('div');
div.innerHTML = htmlString.trim();
// Change this to div.childNodes to support multiple top-level nodes
return div.firstChild;
}
export function parseGithubFlavoredMarkdown(value: string): string {
const renderer = new marked.Renderer();
renderer.paragraph = (text) => `${text}\n`;
const options = {
renderer: renderer,
gfm: true,
tables: true,
breaks: true,
pedantic: true,
sanitize: false,
smartLists: false,
smartypants: false,
mathDelimiters: [['$', '$'], ['\\(', '\\)'], ['\\[', '\\]'], ['$$', '$$'], 'beginend'],
highlight: (code) => {
return highlight.highlightAuto(code).value;
},
};
marked.setOptions(options);
return postMarkdownRenderer(marked(preMarkdownRenderer(value)));
}
export function emojiRenderer(value: string): string {
const emojiMatch = value.match(/:([a-z0-9_\+\-]+):/g);
if (emojiMatch) {
emojiMatch.forEach(token => {
const emoji = token.replace(/:/g, '');
value = value.replace(token, `![emoji_:${emoji}:](/assets/images/emojis/${emoji}.png)`);
});
}
return value;
}
function preMarkdownRenderer(value: string): string {
return emojiRenderer(value);
}
function postMarkdownRenderer(value: string): string {
const iframeOptions = `frameborder="0" gesture="media" width="100%" webkitallowfullscreen mozallowfullscreen allowfullscreen`;
const youtubeMatch = value.match(/<a href=".*(youtube|youtu).*">.*<\/a>/g);
if (youtubeMatch) {
youtubeMatch.forEach(token => {
const originalToken = token;
if (token.indexOf('embed') === -1) {
// Convert to embed uri. Direct youtube urls are restricted by the sameorigin policy and cannot be embedded in iframes
token = token.replace('watch?v=', 'embed/');
}
const videoTag = token //
.replace('<a', `<iframe`) //
.replace('</a>', `</iframe>`) //
.replace('<iframe', `<iframe ${iframeOptions}`) //
.replace('href', 'src');
value = value.replace(originalToken, videoTag);
});
}
const vimeoMatch = value.match(/<a href=".*(vimeo).*">.*<\/a>/g);
if (vimeoMatch) {
vimeoMatch.forEach(token => {
const id = token.match(/([0-9]+)/);
if (id) {
const videoTag = `<iframe src="https://player.vimeo.com/video/${id[0]}?title=0&byline=0&portrait=0" ${iframeOptions}></iframe>`;
value = value.replace(token, videoTag);
}
});
}
const imgMatch = value.match(/<img (?!src=".*emoji.*").+?(?=>)>/g);
if (imgMatch) {
imgMatch.forEach(token => {
const imgNode: HTMLImageElement = createElementFromHTML(token) as HTMLImageElement;
imgNode.classList.add(...['thumbnail', 'cursor-zoom-in', 'img-fluid']);
const anchorNode = document.createElement<'a'>('a');
anchorNode.href = imgNode.src;
anchorNode.target = null;
anchorNode.classList.add(...['highslide', 'd-flex', 'd-sm-block', 'justify-content-center']);
anchorNode.setAttribute('onclick', 'return hs.expand(this);');
anchorNode.appendChild(imgNode);
value = value.replace(token, anchorNode.outerHTML);
});
}
const linkMatch = value.match(/<a href=".*">/g);
if (linkMatch) {
linkMatch.forEach(token => {
value = value.replace(token, token.replace('<a ', '<a rel=\'noopener noreferrer\' target=\'_blank\' '));
});
}
return value;
}
import { NgModule } from '@angular/core';
import { MarkdownModule } from 'ngx-markdown';
import { HeaderModule } from '../header/header.module';
import { SharedModule } from '../shared/shared.module';
import { LivePreviewComponent } from './live-preview/live-preview.component';
@NgModule({
imports: [
SharedModule,
HeaderModule,
SharedModule, HeaderModule, MarkdownModule,
],
declarations: [LivePreviewComponent],
exports: [LivePreviewComponent],
......
......@@ -6,6 +6,7 @@ import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
import { DEVICE_TYPES, LIVE_PREVIEW_ENVIRONMENT } from '../../../environments/environment';
import { AbstractChoiceQuestionEntity } from '../../lib/entities/question/AbstractChoiceQuestionEntity';
import { ConnectionService } from '../../service/connection/connection.service';
import { CustomMarkdownService } from '../../service/custom-markdown/custom-markdown.service';
import { QuestionTextService } from '../../service/question-text/question-text.service';
import { QuizService } from '../../service/quiz/quiz.service';
......@@ -53,7 +54,7 @@ export class LivePreviewComponent implements OnInit, OnDestroy {
public connectionService: ConnectionService,
private quizService: QuizService,
private sanitizer: DomSanitizer,
private route: ActivatedRoute,
private route: ActivatedRoute, private markdownService: CustomMarkdownService,
) {
}
......@@ -104,7 +105,7 @@ export class LivePreviewComponent implements OnInit, OnDestroy {
public ngOnInit(): void {
this.questionTextService.eventEmitter.pipe(takeUntil(this._destroy)).subscribe(value => {
this.dataSource = Array.isArray(value) ? value : [value];
this.dataSource = Array.isArray(value) ? value : [this.markdownService.parseGithubFlavoredMarkdown(value)];
});
const questionIndex$ = this.route.paramMap.pipe(map(params => parseInt(params.get('questionIndex'), 10)), distinctUntilChanged(),
takeUntil(this._destroy));
......
......@@ -9,5 +9,5 @@ import { MarkdownBarComponent } from './markdown-bar/markdown-bar.component';
declarations: [MarkdownBarComponent],
exports: [MarkdownBarComponent],
})
export class MarkdownModule {
export class MarkdownBarModule {
}
import { MarkdownModule } from './markdown.module';
import { MarkdownBarModule } from './markdown-bar.module';
describe('MarkdownModule', () => {
let markdownModule: MarkdownModule;
let markdownModule: MarkdownBarModule;
beforeEach(() => {
markdownModule = new MarkdownModule();
markdownModule = new MarkdownBarModule();
});
it('should create an instance', () => {
......
......@@ -11,11 +11,11 @@ import { StorageKey } from '../../../lib/enums/enums';
import { MessageProtocol } from '../../../lib/enums/Message';
import { QuizState } from '../../../lib/enums/QuizState';
import { ILeaderBoardItem } from '../../../lib/interfaces/ILeaderboard';
import { parseGithubFlavoredMarkdown } from '../../../lib/markdown/markdown';
import { ServerUnavailableModalComponent } from '../../../modals/server-unavailable-modal/server-unavailable-modal.component';
import { LeaderboardApiService } from '../../../service/api/leaderboard/leaderboard-api.service';
import { AttendeeService } from '../../../service/attendee/attendee.service';
import { ConnectionService } from '../../../service/connection/connection.service';
import { CustomMarkdownService } from '../../../service/custom-markdown/custom-markdown.service';
import { FooterBarService } from '../../../service/footer-bar/footer-bar.service';
import { HeaderLabelService } from '../../../service/header-label/header-label.service';
import { I18nService } from '../../../service/i18n/i18n.service';
......@@ -77,7 +77,7 @@ export class LeaderboardComponent implements OnInit, OnDestroy {
private connectionService: ConnectionService,
private i18nService: I18nService,
private leaderboardApiService: LeaderboardApiService,
private ngbModal: NgbModal, private messageQueue: SimpleMQ,
private ngbModal: NgbModal, private messageQueue: SimpleMQ, private customMarkdownService: CustomMarkdownService,
) {
this.footerBarService.TYPE_REFERENCE = LeaderboardComponent.TYPE;
}
......@@ -135,7 +135,7 @@ export class LeaderboardComponent implements OnInit, OnDestroy {
public parseNickname(value: string): string {
if (value.match(/:[\w\+\-]+:/g)) {
return this.sanitizeHTML(parseGithubFlavoredMarkdown(value));
return this.sanitizeHTML(this.customMarkdownService.parseGithubFlavoredMarkdown(value));
}
return value;
}
......
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { QRCodeModule } from 'angularx-qrcode';
import { MarkdownModule } from '../../markdown/markdown.module';
import { MarkdownModule } from 'ngx-markdown';
import { CasLoginService } from '../../service/login/cas-login.service';
import { SharedModule } from '../../shared/shared.module';
import { ConfidenceRateComponent } from './confidence-rate/confidence-rate.component';
......@@ -59,7 +59,7 @@ export const quizFlowRoutes: Routes = [
@NgModule({
imports: [
MarkdownModule, SharedModule, RouterModule.forChild(quizFlowRoutes), QuizResultsModule, QRCodeModule, QuizFlowSharedModule,
SharedModule, RouterModule.forChild(quizFlowRoutes), QuizResultsModule, QRCodeModule, QuizFlowSharedModule, MarkdownModule.forChild(),
],
bootstrap: [EditModeConfirmComponent, QrCodeContentComponent],
declarations: [
......
......@@ -14,12 +14,12 @@ import { UserRole } from '../../../lib/enums/UserRole';
import { FooterbarElement } from '../../../lib/footerbar-element/footerbar-element';
import { IMessage } from '../../../lib/interfaces/communication/IMessage';
import { IMemberSerialized } from '../../../lib/interfaces/entities/Member/IMemberSerialized';
import { parseGithubFlavoredMarkdown } from '../../../lib/markdown/markdown';
import { ServerUnavailableModalComponent } from '../../../modals/server-unavailable-modal/server-unavailable-modal.component';
import { MemberApiService } from '../../../service/api/member/member-api.service';
import { QuizApiService } from '../../../service/api/quiz/quiz-api.service';
import { AttendeeService } from '../../../service/attendee/attendee.service';
import { ConnectionService } from '../../../service/connection/connection.service';
import { CustomMarkdownService } from '../../../service/custom-markdown/custom-markdown.service';
import { FooterBarService } from '../../../service/footer-bar/footer-bar.service';
import { HeaderLabelService } from '../../../service/header-label/header-label.service';
import { QuizService } from '../../../service/quiz/quiz.service';
......@@ -67,7 +67,7 @@ export class QuizLobbyComponent implements OnInit, OnDestroy {
private ngbModal: NgbModal,
private sharedService: SharedService,
private userService: UserService,
private messageQueue: SimpleMQ,
private messageQueue: SimpleMQ, private customMarkdownService: CustomMarkdownService,
) {
sessionStorage.removeItem(StorageKey.CurrentQuestionIndex);
this.footerBarService.TYPE_REFERENCE = QuizLobbyComponent.TYPE;
......@@ -152,7 +152,7 @@ export class QuizLobbyComponent implements OnInit, OnDestroy {
public parseNickname(value: string): string {
if (value.match(/:[\w\+\-]+:/g)) {
return this.sanitizeHTML(parseGithubFlavoredMarkdown(value));
return this.sanitizeHTML(this.customMarkdownService.parseGithubFlavoredMarkdown(value));
}
return value;
}
......
import { ChangeDetectorRef, Component, Input, SecurityContext } from '@angular/core';
import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
import { parseGithubFlavoredMarkdown } from '../../../../lib/markdown/markdown';
import { CustomMarkdownService } from '../../../../service/custom-markdown/custom-markdown.service';
import { I18nService } from '../../../../service/i18n/i18n.service';
@Component({
......@@ -45,10 +45,15 @@ export class ConfidenceRateComponent {
private _name: string;
@Input() set name(value: string) {
this._name = parseGithubFlavoredMarkdown(value);
this._name = this.customMarkdownService.parseGithubFlavoredMarkdown(value);
}
constructor(private i18nService: I18nService, private sanitizer: DomSanitizer, private cd: ChangeDetectorRef) {
constructor(
private i18nService: I18nService,
private sanitizer: DomSanitizer,
private cd: ChangeDetectorRef,
private customMarkdownService: CustomMarkdownService,
) {
}
public sanitizeStyle(value: string): SafeStyle {
......
import { ChangeDetectorRef, Component, Input, SecurityContext } from '@angular/core';
import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
import { parseGithubFlavoredMarkdown } from '../../../../lib/markdown/markdown';
import { CustomMarkdownService } from '../../../../service/custom-markdown/custom-markdown.service';
import { I18nService } from '../../../../service/i18n/i18n.service';
@Component({
......@@ -46,12 +46,17 @@ export class ReadingConfirmationProgressComponent {
private _name: string;
@Input() set name(value: string) {
this._name = parseGithubFlavoredMarkdown(value);
this._name = this.customMarkdownService.parseGithubFlavoredMarkdown(value);
}
private _hasData = false;
constructor(private i18nService: I18nService, private sanitizer: DomSanitizer, private cd: ChangeDetectorRef) {
constructor(
private i18nService: I18nService,
private sanitizer: DomSanitizer,
private cd: ChangeDetectorRef,
private customMarkdownService: CustomMarkdownService,
) {
}
public sanitizeStyle(value: string): SafeStyle {
......
import { NgModule } from '@angular/core';
import { LivePreviewModule } from '../../../live-preview/live-preview.module';
import { MarkdownModule } from '../../../markdown/markdown.module';
import { MarkdownBarModule } from '../../../markdown/markdown-bar.module';
import { SharedModule } from '../../../shared/shared.module';
import { AnsweroptionsModule } from './answeroptions/answeroptions.module';
import { CountdownComponent } from './countdown/countdown.component';
......@@ -10,7 +10,7 @@ import { QuestiontypeComponent } from './questiontype/questiontype.component';
@NgModule({
imports: [
SharedModule, MarkdownModule, LivePreviewModule, AnsweroptionsModule,
SharedModule, MarkdownBarModule, LivePreviewModule, AnsweroptionsModule,
],
providers: [],
declarations: [QuizManagerDetailsOverviewComponent, CountdownComponent, QuestiontextComponent, QuestiontypeComponent],
......
......@@ -4,8 +4,8 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { StorageKey } from '../../../lib/enums/enums';
import { IAvailableNicks } from '../../../lib/interfaces/IAvailableNicks';
import { parseGithubFlavoredMarkdown } from '../../../lib/markdown/markdown';
import { NickApiService } from '../../../service/api/nick/nick-api.service';
import { CustomMarkdownService } from '../../../service/custom-markdown/custom-markdown.service';
import { FooterBarService } from '../../../service/footer-bar/footer-bar.service';
import { QuizService } from '../../../service/quiz/quiz.service';
......@@ -27,7 +27,8 @@ export class NicknameManagerComponent implements OnInit, OnDestroy {
set availableNicks(value: IAvailableNicks) {
this._availableNicks = value;
if (this._availableNicks.emojis) {
this._availableNicks.emojis = this._availableNicks.emojis.map(nick => this.sanitizeHTML(parseGithubFlavoredMarkdown(nick)));
this._availableNicks.emojis = this._availableNicks.emojis.map(
nick => this.sanitizeHTML(this.customMarkdownService.parseGithubFlavoredMarkdown(nick)));
}
this._availableNicksBackup = Object.assign({}, value);
}
......@@ -45,7 +46,7 @@ export class NicknameManagerComponent implements OnInit, OnDestroy {
private sanitizer: DomSanitizer,
private quizService: QuizService,
private footerBarService: FooterBarService,
private nickApiService: NickApiService,
private nickApiService: NickApiService, private customMarkdownService: CustomMarkdownService,
) {
this.footerBarService.TYPE_REFERENCE = NicknameManagerComponent.TYPE;
......@@ -103,7 +104,7 @@ export class NicknameManagerComponent implements OnInit, OnDestroy {
if (this.selectedCategory === 'emojis') {
name = name.changingThisBreaksApplicationSecurity.match(/:[\w\+\-]+:/g)[0];
}
return name.match(/:[\w\+\-]+:/g) ? this.sanitizeHTML(parseGithubFlavoredMarkdown(name)) : name;
return name.match(/:[\w\+\-]+:/g) ? this.sanitizeHTML(this.customMarkdownService.parseGithubFlavoredMarkdown(name)) : name;
}
public hasSelectedNick(name: any): boolean {
......
......@@ -2,8 +2,9 @@ import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { MarkdownModule } from 'ngx-markdown';
import { LivePreviewModule } from '../../live-preview/live-preview.module';
import { MarkdownModule } from '../../markdown/markdown.module';
import { MarkdownBarModule } from '../../markdown/markdown-bar.module';
import { PipesModule } from '../../pipes/pipes.module';
import { SharedModule } from '../../shared/shared.module';
import { AnsweroptionsComponent } from './details/answeroptions/answeroptions.component';
......@@ -71,11 +72,10 @@ const quizManagerRoutes: Routes = [
imports: [
FormsModule,
SharedModule,
QuizManagerDetailsModule,
MarkdownModule,
QuizManagerDetailsModule, MarkdownBarModule,
LivePreviewModule,
RouterModule.forChild(quizManagerRoutes),
PipesModule,
PipesModule, MarkdownModule.forChild(),
],
declarations: [
QuizManagerComponent, NicknameManagerComponent, SoundManagerComponent, MemberGroupManagerComponent, QuizTypeSelectModalComponent,
......
......@@ -36,6 +36,7 @@ const quizRoutes: Routes = [
imports: [
SharedModule, RouterModule.forChild(quizRoutes),
],
providers: [],
declarations: [
QuizOverviewComponent, QuizRenameComponent, QuizJoinComponent, QuizPublicComponent, QuizDuplicateComponent,
],
......
......@@ -10,6 +10,7 @@ import { TranslateCompiler, TranslateLoader, TranslateModule } from '@ngx-transl
import { InjectableRxStompConfig, RxStompService, rxStompServiceFactory } from '@stomp/ng2-stompjs';
import { Angulartics2Module } from 'angulartics2';
import { SimpleMQ } from 'ng2-simple-mq';
import { MarkdownModule, MarkedOptions, MarkedRenderer } from 'ngx-markdown';
import { ToastrModule } from 'ngx-toastr';
import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
import { environment } from '../environments/environment';
......@@ -28,6 +29,7 @@ import { ThemeSwitcherComponent } from './root/theme-switcher/theme-switcher.com
import rxStompConfig from './rx-stomp.config';
import { AttendeeService } from './service/attendee/attendee.service';
import { ConnectionService } from './service/connection/connection.service';
import { CustomMarkdownService } from './service/custom-markdown/custom-markdown.service';
import { FileUploadService } from './service/file-upload/file-upload.service';
import { FooterBarService } from './service/footer-bar/footer-bar.service';
import { HeaderLabelService } from './service/header-label/header-label.service';
......@@ -102,6 +104,23 @@ const appRoutes: Routes = [
},
];
// function that returns `MarkedOptions` with renderer override
export function markedOptionsFactory(): MarkedOptions {
const renderer = new MarkedRenderer();
renderer.paragraph = (text) => `${text}\n`;
return {
renderer: renderer,
gfm: true,
tables: true,
breaks: true,
pedantic: false,
sanitize: false,
smartLists: true,
smartypants: false,
};
}
@NgModule({
declarations: [
HomeComponent, RootComponent, LanguageSwitcherComponent, ThemeSwitcherComponent, LoginComponent,
......@@ -136,21 +155,30 @@ const appRoutes: Routes = [
useFactory: jwtOptionsFactory,
deps: [PLATFORM_ID],
},
}), PipesModule, HeaderModule,
}), PipesModule, HeaderModule, MarkdownModule.forRoot({
markedOptions: {
provide: MarkedOptions,
useFactory: (markedOptionsFactory),
},
}),
],
providers: [
/* {
provide: ErrorHandler,
useClass: GlobalErrorHandler,
}, */
CustomMarkdownService,
{
provide: InjectableRxStompConfig,
useValue: rxStompConfig,
}, {
},
{
provide: RxStompService,
useFactory: rxStompServiceFactory,
deps: [InjectableRxStompConfig],
}, SimpleMQ, UserService,
},
SimpleMQ,
UserService,
RoutePreloader,
StorageService,
I18nService,
......@@ -167,7 +195,12 @@ const appRoutes: Routes = [
QuestionTextService,
ThemesService,
ArsnovaClickAngulartics2Piwik,
TrackingService, UpdateCheckService, UserRoleGuardService, LanguageLoaderService, ProjectLoaderService, ModalOrganizerService,
TrackingService,
UpdateCheckService,
UserRoleGuardService,
LanguageLoaderService,
ProjectLoaderService,
ModalOrganizerService,
],
bootstrap: [RootComponent],
})
......
......@@ -6,9 +6,9 @@ import { MemberEntity } from '../../../lib/entities/member/MemberEntity';
import { StorageKey } from '../../../lib/enums/enums';
import { MessageProtocol, StatusProtocol } from '../../../lib/enums/Message';
import { IMessage } from '../../../lib/interfaces/communication/IMessage';
import { parseGithubFlavoredMarkdown } from '../../../lib/markdown/markdown';
import { MemberApiService } from '../../../service/api/member/member-api.service';
import { AttendeeService } from '../../../service/attendee/attendee.service';
import { CustomMarkdownService } from '../../../service/custom-markdown/custom-markdown.service';
import { FooterBarService } from '../../../service/footer-bar/footer-bar.service';
import { QuizService } from '../../../service/quiz/quiz.service';
import { UserService } from '../../../service/user/user.service';
......@@ -38,7 +38,9 @@ export class NicknameSelectComponent implements OnInit, OnDestroy {
private attendeeService: AttendeeService,
private userService: UserService,
private quizService: QuizService,
private memberApiService: MemberApiService, private messageQueue: SimpleMQ,
private memberApiService: MemberApiService,
private messageQueue: SimpleMQ,