Commit 1a05613c authored by Lukas Haase's avatar Lukas Haase

implement motd dialog

create services

add i18n support

add markdown support

add design
parent 3dc9e748
Pipeline #51656 passed with stages
in 5 minutes and 20 seconds
<ars-row>
<h2 tabindex="0" style="padding:0;margin:0 0 20px 0;">
{{ 'footer.motd-title-main' | translate }}
</h2>
<mat-divider></mat-divider>
</ars-row>
<mat-dialog-content class="container" ars-flex-box>
<ars-row ars-flex-box>
<ars-col>
<h4 class="container-title">{{ 'footer.motd-title-new' | translate }}</h4>
</ars-col>
<ars-fill></ars-fill>
<ars-col class="align-center-y">
<button mat-stroked-button (click)="markAllAsRead()">
<mat-icon style="margin-right:8px;">checkmark</mat-icon>
<span>{{ 'footer.motd-mark-all-read' | translate }}</span>
</button>
</ars-col>
</ars-row>
<ars-row class="message" ars-flex-box>
<ng-container *ngFor="let message of motdsList.messagesNew">
<app-motd-message [message]="message"></app-motd-message>
</ng-container>
</ars-row>
<ars-row>
<h4 class="container-title">{{ 'footer.motd-title-old' | translate }}</h4>
</ars-row>
<ars-row class="message" ars-flex-box>
<ng-container *ngFor="let message of motdsList.messagesOld">
<app-motd-message [message]="message"></app-motd-message>
</ng-container>
</ars-row>
<ars-row [height]="32"></ars-row>
<ars-fill></ars-fill>
</mat-dialog-content>
<ars-row>
<app-dialog-action-buttons
buttonsLabelSection="imprint"
[cancelButtonClickAction]="buildDeclineActionCallback()"
[spacing]="false">
</app-dialog-action-buttons>
</ars-row>
:host{
.align-center-y{
display:flex;
justify-content:center;
flex-direction:column;
}
.container{
width:100%;
height:calc( 100% - 115px ) !important;
.message{
app-motd-message{
margin-bottom:25px;
}
:last-child{
margin-bottom:0px;
}
}
&-title{
font-weight:lighter;
margin-left:1px;
}
}
mat-dialog-content{
height:100% !important;
max-height:100% !important;
}
mat-dialog-container{
padding:0 !important;
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MotdDialogComponent } from './motd-dialog.component';
describe('MotdDialogComponent', () => {
let component: MotdDialogComponent;
let fixture: ComponentFixture<MotdDialogComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MotdDialogComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MotdDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, EventEmitter, OnDestroy, OnInit } from '@angular/core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { Motd } from '../../../../models/motd';
import { MotdList } from '../../../../models/motd-list';
@Component({
selector: 'app-motd-dialog',
templateUrl: './motd-dialog.component.html',
styleUrls: ['./motd-dialog.component.scss']
})
export class MotdDialogComponent implements OnInit, OnDestroy {
public motdsList: MotdList;
public onClose: EventEmitter<void> = new EventEmitter<void>();
constructor(
public dialogRef: MatDialogRef<MotdDialogComponent>,
private translateService: TranslateService,
public dialog: MatDialog
) { }
markAllAsRead() {
this.motdsList.markAllAsRead();
}
ngOnInit(): void {
}
buildDeclineActionCallback(): () => void {
return () => this.dialogRef.close();
}
ngOnDestroy(): void {
this.onClose.emit();
}
}
<ars-row class="container" ars-flex-box>
<ars-row [height]="40" ars-flex-box>
<ars-col class="align-center-y">
<p class="timestamp">{{message.startTimestamp.getDay() + '.' + message.startTimestamp.getMonth() + '.' + message.startTimestamp.getFullYear()}}</p>
</ars-col>
<ars-fill></ars-fill>
<ars-col class="align-center-y">
<mat-checkbox [color]="'accent'" *ngIf="message.isNew" [checked]="message.isRead"
(change)="setIsRead($event.checked)">
<span>{{ 'footer.motd-mark-read' | translate }}</span>
</mat-checkbox>
</ars-col>
</ars-row>
<ars-row>
<markdown class="images" [data]="translatedMessage"></markdown>
</ars-row>
</ars-row>
:host{
.align-center-y{
display:flex;
justify-content:center;
flex-direction:column;
}
.container{
border-radius:4px;
background-color:var(--surface);
box-shadow:inset 0px 0px 0px 1px rgba(0,0,0,0.2);
box-sizing:border-box;
padding:8px 16px;
}
.timestamp{
font-weight:bold;
}
p{
padding:0;
margin:0;
}
::ng-deep{
.images img {
max-width: 100% !important;
}
.mat-checkbox-frame {
border-color:var(--on-surface);
}
}
}
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MotdMessageComponent } from './motd-message.component';
describe('MotdMessageComponent', () => {
let component: MotdMessageComponent;
let fixture: ComponentFixture<MotdMessageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MotdMessageComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MotdMessageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, Input, OnInit } from '@angular/core';
import { Motd } from '../../../../../models/motd';
@Component({
selector: 'app-motd-message',
templateUrl: './motd-message.component.html',
styleUrls: ['./motd-message.component.scss']
})
export class MotdMessageComponent implements OnInit {
@Input()message: Motd;
translatedMessage: string;
constructor() { }
ngOnInit(): void {
if (localStorage.getItem('currentLang') === 'de') {
this.translatedMessage = this.message.msgGerman;
} else {
this.translatedMessage = this.message.msgEnglish;
}
}
public setIsRead(isRead: boolean) {
this.message.setIsRead(isRead);
}
}
......@@ -16,7 +16,8 @@ import { Theme } from '../../../../theme/Theme';
import { OverlayComponent } from '../../home/_dialogs/overlay/overlay.component';
import { AppComponent } from '../../../app.component';
import { StyleService } from '../../../../../projects/ars/src/lib/style/style.service';
import { MotdTempDialogComponent } from '../_dialogs/motd-temp-dialog/motd-temp-dialog.component';
import { MotdService } from '../../../services/http/motd.service';
import { MotdDialogComponent } from '../_dialogs/motd-dialog/motd-dialog.component';
@Component({
selector: 'app-footer',
......@@ -46,16 +47,30 @@ export class FooterComponent implements OnInit {
private langService: LanguageService,
public authenticationService: AuthenticationService,
private themeService: ThemeService,
private styleService: StyleService) {
private styleService: StyleService,
private motdService: MotdService) {
langService.langEmitter.subscribe(lang => translateService.use(lang));
}
ngOnInit() {
const motd = this.dialog.open(MotdTempDialogComponent, {
width: '80%',
maxWidth: '600px',
minHeight: '20%',
height: '60%',
this.motdService.onDialogRequest().subscribe(() => {
this.motdService.getList().subscribe(e => {
const dialogRef = this.dialog.open(MotdDialogComponent, {
width: '80%',
maxWidth: '600px',
minHeight: '95%',
height: '95%',
});
dialogRef.componentInstance.onClose.subscribe(() => {
this.motdService.checkNewMessage();
});
dialogRef.componentInstance.motdsList = e;
});
});
this.motdService.getList().subscribe(e => {
if (e.containsUnreadMessage()) {
this.motdService.requestDialog();
}
});
this.deviceType = localStorage.getItem('deviceType');
if (!this.themeService.getThemeByKey(this.themeClass) || !this.themeService.getTheme()['source']['_value']) {
......
......@@ -64,6 +64,9 @@
</button>
<span class="fill-remaining-space"></span>
<button mat-icon-button *ngIf="user" (click)="showMotdDialog()">
<mat-icon class="header-icons">{{ motdState ? 'notifications_active' : 'notifications_none' }}</mat-icon>
</button>
<mat-menu #userMenu="matMenu"
[overlapTrigger]="false">
......
......@@ -19,6 +19,7 @@ import { UserBonusTokenComponent } from '../../participant/_dialogs/user-bonus-t
import { RemindOfTokensComponent } from '../../participant/_dialogs/remind-of-tokens/remind-of-tokens.component';
import { QrCodeDialogComponent } from '../_dialogs/qr-code-dialog/qr-code-dialog.component';
import { BonusTokenService } from '../../../services/http/bonus-token.service';
import { MotdService } from '../../../services/http/motd.service';
@Component({
selector: 'app-header',
......@@ -32,6 +33,7 @@ export class HeaderComponent implements OnInit {
deviceType: string;
isSafari = 'false';
moderationEnabled: boolean;
motdState = false;
constructor(public location: Location,
private authenticationService: AuthenticationService,
......@@ -42,7 +44,8 @@ export class HeaderComponent implements OnInit {
private userService: UserService,
public eventService: EventService,
private bonusTokenService: BonusTokenService,
private _r: Renderer2
private _r: Renderer2,
private motdService: MotdService
) {
}
......@@ -120,6 +123,13 @@ export class HeaderComponent implements OnInit {
}
}
});
this.motdService.onNewMessage().subscribe(state => {
this.motdState = state;
});
}
showMotdDialog() {
this.motdService.requestDialog();
}
getTime(time: Date) {
......
......@@ -26,7 +26,8 @@ import { CommentAnswerComponent } from './comment-answer/comment-answer.componen
import { MarkdownModule } from 'ngx-markdown';
import { MatRippleModule } from '@angular/material/core';
import { QRCodeModule } from 'angularx-qrcode';
import { MotdTempDialogComponent } from './_dialogs/motd-temp-dialog/motd-temp-dialog.component';
import { MotdDialogComponent } from './_dialogs/motd-dialog/motd-dialog.component';
import { MotdMessageComponent } from './_dialogs/motd-dialog/motd-message/motd-message.component';
@NgModule({
imports: [
......@@ -59,7 +60,8 @@ import { MotdTempDialogComponent } from './_dialogs/motd-temp-dialog/motd-temp-d
QrCodeDialogComponent,
RemoveFromHistoryComponent,
CommentAnswerComponent,
MotdTempDialogComponent
MotdDialogComponent,
MotdMessageComponent
],
exports: [
RoomJoinComponent,
......
import { Motd } from './motd';
export class MotdList {
public messagesNew: Motd[] = [];
public messagesOld: Motd[] = [];
private localRead: string[];
constructor(
messagesNew: any,
messagesOld: any
) {
try {
this.localRead = JSON.parse(localStorage.getItem('motds'));
} catch (e) {
localStorage.setItem('motds', JSON.stringify([]));
this.localRead = JSON.parse(localStorage.getItem('motds'));
}
this.parse(this.messagesNew, messagesNew, true);
this.parse(this.messagesOld, messagesOld, false);
}
private parse(list: Motd[], messages: any, isNew: boolean): void {
messages.forEach(e => {
const motd: Motd = new Motd(
e.id,
new Date(e.startTimestamp),
new Date(e.endTimestamp),
e.msgEnglish,
e.msgGerman,
isNew,
this.localRead.indexOf(e.id) >= 0
);
motd.isReadEmit.subscribe(m => {
this.updateLocaleRead(m);
});
list.push(motd);
});
}
private updateLocaleRead(motd: Motd) {
if (motd.isRead) {
if (!this.hasLocale(motd.id)) {
this.localRead.push(motd.id);
this.updateLocaleStorage();
}
} else {
if (this.hasLocale(motd.id)) {
this.localRead = this.localRead.filter(x => x !== motd.id);
this.updateLocaleStorage();
}
}
}
public markAllAsRead() {
this.localRead = [];
this.messagesNew.forEach(e => {
e.isRead = true;
this.localRead.push(e.id);
});
this.updateLocaleStorage();
}
private updateLocaleStorage(): void {
localStorage.setItem('motds', JSON.stringify(this.localRead));
}
private hasLocale(id: string): boolean {
return this.localRead.indexOf(id) !== -1;
}
public containsUnreadMessage(): boolean {
return this.messagesNew.filter(x => x.isNew && !x.isRead).length > 0;
}
}
import { EventEmitter } from '@angular/core';
export class Motd {
public isReadEmit: EventEmitter<Motd> = new EventEmitter<Motd>();
constructor(
public id: string,
public startTimestamp: Date,
public endTimestamp: Date,
public msgEnglish: string,
public msgGerman: string,
public isNew: boolean,
public isRead: boolean
) {
}
public setIsRead(isRead: boolean) {
if (this.isRead !== isRead) {
this.isRead = isRead;
this.isReadEmit.emit(this);
}
}
}
import { EventEmitter, Injectable } from '@angular/core';
import { BaseHttpService } from './base-http.service';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { EventService } from '../util/event.service';
import { AuthenticationService } from './authentication.service';
import { Observable, throwError } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { Motd } from '../../models/motd';
import { MotdList } from '../../models/motd-list';
@Injectable({
providedIn: 'root'
})
export class MotdService extends BaseHttpService {
private list: MotdList;
private dialogTrigger = new EventEmitter<void>();
private hasNewMessages = false;
private newMessageTrigger = new EventEmitter<boolean>();
constructor(
private http: HttpClient,
private eventService: EventService,
private authService: AuthenticationService
) {
super();
setInterval(() => {
this.checkNewMessage();
}, 30000);
}
public checkNewMessage(): void {
this.getList().subscribe(e => {
this.validateNewMessage(e);
});
}
private validateNewMessage(list: MotdList): void {
if (list.containsUnreadMessage()) {
if (!this.hasNewMessages) {
this.hasNewMessages = true;
this.newMessageTrigger.emit(this.hasNewMessages);
}
} else {
if (this.hasNewMessages) {
this.hasNewMessages = false;
this.newMessageTrigger.emit(this.hasNewMessages);
}
}
}
public onDialogRequest(): Observable<void> {
return new Observable<void>(e => {
this.dialogTrigger.subscribe(() => {
e.next();
});
});
}
public onNewMessage(): Observable<boolean> {
return new Observable<boolean>(e => {
this.newMessageTrigger.subscribe(b => {
e.next(b);
});
this.checkNewMessage();
});
}
public requestDialog(): void {
this.dialogTrigger.emit();
}
public getOld(): Observable<Motd[]> {
if (!this.authService.getUser()) {
return new Observable<Motd[]>(e => e.next([]));
}
return this.http.post<Motd[]>('/api/motds/find', {
properties: {},
externalFilters: {
before: new Date().getTime()
}
}).pipe(
catchError((error: any): Observable<any> => {
return new Observable<any>(e => e.next([]));
})
);
}
public getNew(): Observable<Motd[]> {
if (!this.authService.getUser()) {
return new Observable<Motd[]>(e => e.next([]));
}
return this.http.post<Motd[]>('/api/motds/find', {
properties: {},
externalFilters: {
activeAt: new Date().getTime()
}
}).pipe(
catchError((error: any): Observable<any> => {
return new Observable<any>(e => e.next([]));
})
);
}
public getList(): Observable<MotdList> {
return new Observable<MotdList>(e => {
this.getOld().subscribe(o => {
this.getNew().subscribe(n => {
this.list = new MotdList(n, o);
this.validateNewMessage(this.list);
e.next(this.list);
});
});
});