event subscriptions
This commit is contained in:
@@ -70,5 +70,10 @@ export const routes: Routes = [
|
||||
),
|
||||
canActivate: [requireRoles([Claim.Memberadmin])],
|
||||
},
|
||||
{
|
||||
path: 'events',
|
||||
loadComponent: () =>
|
||||
import('./components/events/events').then(mod => mod.Events),
|
||||
},
|
||||
{ path: '**', component: MissingPage },
|
||||
];
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<h1>Gesammelte Veranstaltungen</h1>
|
||||
<p>
|
||||
<button matButton="filled" (click)="subscribe()">Abbonieren</button>
|
||||
<button matButton="filled" class="secondary-btn" (click)="unsubscribe()">
|
||||
Abmelden
|
||||
</button>
|
||||
</p>
|
||||
<div class="table-container">
|
||||
<table mat-table [dataSource]="datasource" matSort>
|
||||
<ng-container matColumnDef="source">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
Gefunden bei
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<a [href]="element.source"
|
||||
>{{truncateSource(element.source)}}</a
|
||||
>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="url">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>Link</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<a
|
||||
matButton
|
||||
[href]="element.url"
|
||||
target="_blank"
|
||||
[ellipsis]="80"
|
||||
>{{element.url}}</a
|
||||
>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="eventDate">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
Ausrichtungsdatum
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">{{element.eventDate}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="title">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>Titel</th>
|
||||
<td mat-cell *matCellDef="let element">{{element.title}}</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="description">
|
||||
<th mat-header-cell mat-sort-header *matHeaderCellDef>
|
||||
Beschreibung
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let element">{{element.description}}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="attachedFiles">
|
||||
<th mat-header-cell *matHeaderCellDef>Anhänge</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
@for(attachment of element.attachedFiles; track attachment.id){
|
||||
<a matButton="outlined" [href]="attachment.url" target="_blank"
|
||||
>{{attachment.title??'?'}}</a
|
||||
>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr
|
||||
mat-header-row
|
||||
*matHeaderRowDef="displayedColumns; sticky: true"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
// overflow: auto;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow: auto;
|
||||
border: 1px solid var(--mat-sys-outline);
|
||||
min-height: 400px;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Events } from './events';
|
||||
|
||||
describe('Events', () => {
|
||||
let component: Events;
|
||||
let fixture: ComponentFixture<Events>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Events]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Events);
|
||||
component = fixture.componentInstance;
|
||||
await fixture.whenStable();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
OnInit,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSort, MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { parse } from 'date-fns';
|
||||
import { Ellipsis } from '../../directives/ellipsis/ellipsis';
|
||||
import * as api from '../../generated-api/api';
|
||||
import { PushService } from '../../services/push/pushService';
|
||||
|
||||
@Component({
|
||||
selector: 'app-events',
|
||||
imports: [MatTableModule, MatSortModule, MatButtonModule, Ellipsis],
|
||||
templateUrl: './events.html',
|
||||
styleUrl: './events.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class Events implements OnInit {
|
||||
private readonly pushService = inject(PushService);
|
||||
protected readonly datasource = new MatTableDataSource<api.FoundEventDto>();
|
||||
protected readonly sort = viewChild.required(MatSort);
|
||||
protected readonly displayedColumns = [
|
||||
'eventDate',
|
||||
'title',
|
||||
'description',
|
||||
'attachedFiles',
|
||||
'url',
|
||||
'source',
|
||||
];
|
||||
|
||||
public async ngOnInit(): Promise<void> {
|
||||
this.datasource.sort = this.sort();
|
||||
const originalSortingAccessor = this.datasource.sortingDataAccessor;
|
||||
this.datasource.sortingDataAccessor = (item, property) => {
|
||||
if (property != 'eventDate') {
|
||||
return originalSortingAccessor(item, property);
|
||||
}
|
||||
|
||||
if (!item.eventDate) {
|
||||
return NaN;
|
||||
}
|
||||
|
||||
const parts = item.eventDate.split('-');
|
||||
if (parts.length == 1) {
|
||||
return parse(parts[0]!, 'dd.MM.yyyy', new Date()).valueOf();
|
||||
}
|
||||
if (parts.length == 2) {
|
||||
const p2 = parts[1].split('.');
|
||||
p2.splice(0, 1, parts[0]);
|
||||
return parse(p2.join('.'), 'dd.MM.yyyy', new Date()).valueOf();
|
||||
}
|
||||
|
||||
console.warn('unknown date format:', property);
|
||||
return originalSortingAccessor(item, property);
|
||||
};
|
||||
const data = await api.getFutureEvents();
|
||||
this.datasource.data = data;
|
||||
}
|
||||
|
||||
protected truncateSource(sourceUrl: string): string {
|
||||
if (!URL.canParse(sourceUrl)) {
|
||||
return sourceUrl;
|
||||
}
|
||||
const url = new URL(sourceUrl);
|
||||
return url.hostname;
|
||||
}
|
||||
|
||||
protected async unsubscribe() {
|
||||
await this.pushService.resetSubscriptions();
|
||||
}
|
||||
protected async subscribe() {
|
||||
this.pushService.subscribeToEvents();
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,24 @@
|
||||
Mitgliederliste
|
||||
</button>
|
||||
</mat-list-item>
|
||||
<mat-list-item [claimGuard]="Claim.Useradmin">
|
||||
<mat-list-item>
|
||||
<button
|
||||
matButton="filled"
|
||||
class="tertiary-btn"
|
||||
[routerLink]="['/events']">
|
||||
Veranstaltungen
|
||||
</button>
|
||||
</mat-list-item>
|
||||
<!-- <mat-list-item [claimGuard]="Claim.Useradmin">
|
||||
<button matButton="filled" color="warn" [routerLink]="['/manageUsers']">
|
||||
Benutzer Verwalten
|
||||
</button>
|
||||
</mat-list-item>
|
||||
<mat-list-item [claimGuard]="Claim.Memberadmin">
|
||||
</mat-list-item> -->
|
||||
<!-- <mat-list-item [claimGuard]="Claim.Memberadmin">
|
||||
<button matButton="filled" color="primary" [routerLink]="['/exam']">
|
||||
Prüfungsanmeldung
|
||||
</button>
|
||||
</mat-list-item>
|
||||
</mat-list-item> -->
|
||||
<mat-list-item>
|
||||
<button
|
||||
matButton="outlined"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Ellipsis } from './ellipsis';
|
||||
|
||||
describe('Ellipsis', () => {
|
||||
it('should create an instance', () => {
|
||||
const directive = new Ellipsis();
|
||||
expect(directive).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Directive,
|
||||
effect,
|
||||
ElementRef,
|
||||
inject,
|
||||
input,
|
||||
signal,
|
||||
WritableSignal,
|
||||
} from '@angular/core';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
|
||||
@Directive({
|
||||
selector: '[ellipsis]',
|
||||
hostDirectives: [MatTooltip],
|
||||
})
|
||||
export class Ellipsis implements AfterViewInit {
|
||||
// private readonly templateRef = inject(TemplateRef<any>);
|
||||
// private readonly viewContainer = inject(ViewContainerRef);
|
||||
private readonly elementRef = inject<ElementRef<HTMLElement>>(
|
||||
ElementRef<HTMLElement>,
|
||||
);
|
||||
private readonly tooltip = inject(MatTooltip);
|
||||
private readonly originalText: WritableSignal<string>;
|
||||
public readonly ellipsis = input.required<number>();
|
||||
|
||||
public constructor() {
|
||||
this.originalText = signal(this.elementRef.nativeElement.innerHTML);
|
||||
effect(() => {
|
||||
const length = this.ellipsis();
|
||||
const originalText = this.originalText();
|
||||
this.tooltip.message = originalText;
|
||||
if (length < 0) {
|
||||
console.warn('Ellipsis Pipe with negative length', length);
|
||||
this.elementRef.nativeElement.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
if (length < 4) {
|
||||
this.elementRef.nativeElement.innerHTML = '.'.repeat(length);
|
||||
return;
|
||||
}
|
||||
if (originalText.length > length) {
|
||||
this.elementRef.nativeElement.innerHTML = `${originalText.substring(0, length - 3)}...`;
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngAfterViewInit(): void {
|
||||
const originalText = this.elementRef.nativeElement.innerText;
|
||||
this.originalText.set(originalText);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
type USVString = string;
|
||||
type DOMString = string;
|
||||
type DOMTimeStamp = number;
|
||||
|
||||
export interface NotificationAction {
|
||||
action: string;
|
||||
title: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface AngularPushPayload {
|
||||
notification: {
|
||||
actions?: NotificationAction[];
|
||||
badge?: USVString;
|
||||
body?: DOMString;
|
||||
data?: any;
|
||||
dir?: 'auto' | 'ltr' | 'rtl';
|
||||
icon?: USVString;
|
||||
image?: USVString;
|
||||
lang?: DOMString;
|
||||
renotify?: boolean;
|
||||
requireInteraction?: boolean;
|
||||
silent?: boolean;
|
||||
tag?: DOMString;
|
||||
timestamp?: DOMTimeStamp;
|
||||
title: DOMString;
|
||||
vibrate?: number[];
|
||||
};
|
||||
}
|
||||
@@ -2,69 +2,71 @@ import { inject, Injectable } from '@angular/core';
|
||||
import { SwPush } from '@angular/service-worker';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
import * as api from '../../generated-api/api';
|
||||
import VAPID from './vapid.json' with { type: "json" };
|
||||
import VAPID from './vapid.json' with { type: 'json' };
|
||||
|
||||
const SUBSCRIPTION_CLIENT_ID="subscriptionClientId";
|
||||
const SUBSCRIPTION_CLIENT_ID = 'subscriptionClientId';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class PushService{
|
||||
private readonly swPush=inject(SwPush);
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PushService {
|
||||
private readonly swPush = inject(SwPush);
|
||||
|
||||
private arrayBufferToString(buffer: ArrayBuffer|null){
|
||||
if(buffer==null){
|
||||
private arrayBufferToString(buffer: ArrayBuffer | null) {
|
||||
if (buffer == null) {
|
||||
return '';
|
||||
}
|
||||
let binary = '';
|
||||
const bytes = new Uint8Array(buffer);
|
||||
const len = bytes.length;
|
||||
for(let i = 0; i< len; i++){
|
||||
binary+=String.fromCharCode(bytes[i]);
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
public async subscribeToEvents(){
|
||||
try{
|
||||
public async subscribeToEvents() {
|
||||
try {
|
||||
const subscription = await this.swPush.requestSubscription({
|
||||
serverPublicKey: VAPID.publicKey
|
||||
serverPublicKey: VAPID.publicKey,
|
||||
});
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
const previousCliendId = localStorage.getItem(SUBSCRIPTION_CLIENT_ID);
|
||||
if(previousCliendId){
|
||||
try{
|
||||
const previousCliendId = localStorage.getItem(
|
||||
SUBSCRIPTION_CLIENT_ID,
|
||||
);
|
||||
if (previousCliendId) {
|
||||
try {
|
||||
await api.clearSubscription(previousCliendId);
|
||||
console.log("previous subscription cleared");
|
||||
}catch{}
|
||||
console.log('previous subscription cleared');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const newClientId= uuidv7();
|
||||
const newClientId = uuidv7();
|
||||
|
||||
await api.addSubscription({
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
auth: this.arrayBufferToString(subscription.getKey('auth')),
|
||||
p256dh: this.arrayBufferToString(subscription.getKey('p256dh')),
|
||||
p256dh: this.arrayBufferToString(
|
||||
subscription.getKey('p256dh'),
|
||||
),
|
||||
},
|
||||
expirationTime: subscription.expirationTime,
|
||||
topic: api.Topic.Test,
|
||||
topic: api.Topic.Events,
|
||||
topicConfiguration: null,
|
||||
clientId: newClientId,
|
||||
});
|
||||
|
||||
localStorage.setItem(SUBSCRIPTION_CLIENT_ID, newClientId);
|
||||
}catch(error){
|
||||
console.error("Subscribing failed: ", error);
|
||||
} catch (error) {
|
||||
console.error('Subscribing failed: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
public async resetSubscriptions(){
|
||||
public async resetSubscriptions() {
|
||||
await this.swPush.unsubscribe();
|
||||
await api.resetSubscriptions();
|
||||
}
|
||||
|
||||
public async publishTestMessage(){
|
||||
public async publishTestMessage() {
|
||||
await api.publishTestMessage();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user