initial commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
<page-notfound *ngIf="notfound"></page-notfound>
|
||||
<ng-container *ngIf="entry">
|
||||
<ui-entry [entry]="entry" [change]="boundRefresh"></ui-entry>
|
||||
|
||||
<p>{{entry.text}}</p>
|
||||
|
||||
<ui-commentform [target]="entry.id" [change]="boundRefresh"></ui-commentform>
|
||||
|
||||
<ui-comments [target]="entry.id"></ui-comments>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { EntriesService } from '../../services/entries.service';
|
||||
|
||||
@Component({
|
||||
selector: 'page-entry',
|
||||
templateUrl: './entry.page.html'
|
||||
})
|
||||
export class PageEntry implements OnInit {
|
||||
|
||||
id: number;
|
||||
entry: any;
|
||||
notfound: boolean = false;
|
||||
boundRefresh: Function;
|
||||
|
||||
constructor(private entriesService: EntriesService,
|
||||
private route: ActivatedRoute) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.id = +this.route.snapshot.paramMap.get('id');
|
||||
this.boundRefresh = this.refresh.bind(this);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.entriesService.getEntry(this.id).subscribe((data) => {
|
||||
this.entry = data;
|
||||
}, (error) => {
|
||||
if (error.status == 404) {
|
||||
this.notfound = true;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<mat-card *ngIf="externals && externals.length > 0">
|
||||
<mat-card-content>
|
||||
<h2>{{'login.external' | i18n}}</h2>
|
||||
<mat-error *ngIf="externalLoginInvalid">
|
||||
{{'login.external.invalid' | i18n}}
|
||||
</mat-error>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<a class="external-login" href="{{apiUrl}}/{{client.loginUrl}}" *ngFor="let client of externals"
|
||||
mat-raised-button color="accent">{{'login.external.client' | i18n:client.id}}</a>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
<form *ngIf="internalLogin || externals && externals.length < 1" action="{{apiUrl}}/login" method="POST" #loginForm>
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<h2>{{'login.internal' | i18n}}</h2>
|
||||
<mat-error *ngIf="loginInvalid">
|
||||
{{'login.invalid' | i18n}}
|
||||
</mat-error>
|
||||
<mat-form-field>
|
||||
<input id="username" name="username" matInput placeholder="{{'username' | i18n}}" required matAutofocus>
|
||||
<mat-error>
|
||||
{{'username.missing' | i18n}}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input id="password" name="password" matInput type="password" placeholder="{{'password' | i18n}}"
|
||||
required>
|
||||
<mat-error>
|
||||
{{'password.invalid.hint' | i18n}}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-slide-toggle id="remember-me" name="remember-me">
|
||||
{{'login.keepSession' | i18n}}
|
||||
</mat-slide-toggle>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button type="submit" (click)="loginForm.submit()" mat-raised-button color="primary"
|
||||
[disabled]="loginForm.invalid">{{'login' |
|
||||
i18n}}<mat-icon style="font-size: 1em;">open_in_new
|
||||
</mat-icon></button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</form>
|
||||
@@ -0,0 +1,8 @@
|
||||
mat-form-field {
|
||||
display: block;
|
||||
}
|
||||
|
||||
a.external-login {
|
||||
margin: 15px 0;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
import { AuthService } from '../../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'page-login',
|
||||
templateUrl: './login.page.html',
|
||||
styleUrls: [ './login.page.scss' ]
|
||||
})
|
||||
export class PageLogin implements OnInit {
|
||||
|
||||
@ViewChild('loginForm') loginForm: ElementRef;
|
||||
internalLogin: boolean;
|
||||
loginInvalid: boolean;
|
||||
externalLoginInvalid: boolean;
|
||||
apiUrl = environment.apiUrl;
|
||||
targetRoute: string;
|
||||
externals: any[];
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute) { }
|
||||
|
||||
async ngOnInit() {
|
||||
this.route.queryParams.subscribe(params => {
|
||||
if (params[ 'all' ] || params[ 'all' ] == '') {
|
||||
this.internalLogin = true;
|
||||
}
|
||||
if (params[ 'target' ]) {
|
||||
this.targetRoute = params[ 'target' ];
|
||||
this.router.navigate([], { queryParams: { target: null }, queryParamsHandling: 'merge', replaceUrl: true });
|
||||
}
|
||||
if (params[ 'error' ] || params[ 'error' ] == '') {
|
||||
this.loginInvalid = true;
|
||||
this.router.navigate([], { queryParams: { error: null }, queryParamsHandling: 'merge', replaceUrl: true });
|
||||
}
|
||||
if (params[ 'externalError' ] || params[ 'externalError' ] == '') {
|
||||
this.externalLoginInvalid = true;
|
||||
this.router.navigate([], { queryParams: { externalError: null }, queryParamsHandling: 'merge', replaceUrl: true });
|
||||
}
|
||||
});
|
||||
|
||||
this.authService.getExternal().subscribe((data: any[]) => {
|
||||
this.externals = data;
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this.targetRoute) {
|
||||
this.loginForm.nativeElement.action = this.loginForm.nativeElement.action + "?forward=" + window.location.origin + encodeURIComponent(this.targetRoute);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<ui-entries [entries]="entries" [refresh]="boundRefresh" [update]="boundUpdate"></ui-entries>
|
||||
@@ -0,0 +1,43 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { EntriesService } from '../../services/entries.service';
|
||||
import { PageEvent } from '@angular/material/paginator';
|
||||
|
||||
@Component({
|
||||
selector: 'page-new',
|
||||
templateUrl: './new.page.html'
|
||||
})
|
||||
export class PageNew implements OnInit {
|
||||
|
||||
entries: any;
|
||||
boundRefresh: Function;
|
||||
boundUpdate: Function;
|
||||
|
||||
constructor(private entriesService: EntriesService) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.refresh();
|
||||
this.boundRefresh = this.refresh.bind(this);
|
||||
this.boundUpdate = this.update.bind(this);
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
if (!this.entries) {
|
||||
this.entriesService.getNew().subscribe((data) => {
|
||||
this.entries = data;
|
||||
})
|
||||
} else {
|
||||
this.entries.content = null;
|
||||
this.entriesService.getNewPages(this.entries.number || 0, this.entries.size || 10).subscribe((data: any) => {
|
||||
this.entries = data;
|
||||
}, (error) => { })
|
||||
}
|
||||
}
|
||||
|
||||
update(event: PageEvent) {
|
||||
this.entries.content = null;
|
||||
this.entriesService.getNewPages(event.pageIndex, event.pageSize).subscribe((data: any) => {
|
||||
this.entries = data;
|
||||
}, (error) => { })
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<mat-card class="accent">
|
||||
<mat-card-header>
|
||||
<mat-card-title>404</mat-card-title>
|
||||
<mat-card-subtitle>{{'not-found' | i18n}}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p>
|
||||
{{'not-found.text' | i18n}}
|
||||
</p>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'page-notfound',
|
||||
templateUrl: './notfound.page.html'
|
||||
})
|
||||
export class PageNotFound implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<form [formGroup]="form" (ngSubmit)="save()" #formDirective="ngForm" *ngIf="user">
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<mat-form-field>
|
||||
<input matInput formControlName="username" type="text">
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<input matInput placeholder="{{'settings.email' | i18n}}" formControlName="email" type="email">
|
||||
<mat-error *ngIf="hasError('email')">
|
||||
{{'settings.email.error' | i18n}}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<textarea matInput placeholder="{{'settings.about' | i18n}}" formControlName="about"></textarea>
|
||||
<mat-error>
|
||||
{{'settings.about.error' | i18n}}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-slide-toggle (change)="darkThemeChange($event)" [checked]="user.darkTheme">
|
||||
{{'settings.darkTheme' | i18n}}
|
||||
</mat-slide-toggle>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button *ngIf="!working" mat-raised-button color="primary" [disabled]="form.invalid">
|
||||
{{'settings.update' | i18n}}
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
</form>
|
||||
@@ -0,0 +1,3 @@
|
||||
mat-form-field {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators, NgForm } from '@angular/forms';
|
||||
|
||||
import { UserService } from '../../services/user.service';
|
||||
|
||||
@Component({
|
||||
selector: 'page-settings',
|
||||
templateUrl: './settings.page.html',
|
||||
styleUrls: [ './settings.page.scss' ]
|
||||
})
|
||||
export class PageSettings implements OnInit {
|
||||
|
||||
auth: any;
|
||||
user: any;
|
||||
working: boolean = false;
|
||||
form: FormGroup;
|
||||
@ViewChild('formDirective') private formDirective: NgForm;
|
||||
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private formBuilder: FormBuilder) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = this.formBuilder.group({
|
||||
username: [ { disabled: true }, Validators.nullValidator ],
|
||||
email: [ '', Validators.nullValidator ],
|
||||
about: [ '', Validators.nullValidator ],
|
||||
darkTheme: [ '', Validators.nullValidator ],
|
||||
});
|
||||
|
||||
this.form.get('username').disable();
|
||||
|
||||
this.userService.get().subscribe(user => {
|
||||
this.user = user;
|
||||
this.form.get('username').setValue(this.user.username);
|
||||
this.form.get('email').setValue(this.user.email);
|
||||
this.form.get('about').setValue(this.user.about);
|
||||
this.form.get('darkTheme').setValue(this.user.darkTheme);
|
||||
})
|
||||
}
|
||||
|
||||
darkThemeChange($event) {
|
||||
this.user.darkTheme = $event.checked;
|
||||
}
|
||||
|
||||
hasError(controlName: string): boolean {
|
||||
return this.form.controls[ controlName ].errors != null;
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (this.working) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.working = true;
|
||||
|
||||
this.user.about = this.form.get('about').value;
|
||||
this.user.email = this.form.get('email').value;
|
||||
|
||||
this.userService.update(this.user).subscribe((data) => {
|
||||
this.user = data;
|
||||
this.working = false;
|
||||
}, (error) => {
|
||||
this.working = false;
|
||||
if (error.status == 422) {
|
||||
let errors = {};
|
||||
for (let code of error.error) {
|
||||
errors[ code.field ] = errors[ code.field ] || {};
|
||||
errors[ code.field ][ code.code ] = true;
|
||||
}
|
||||
|
||||
for (let code in errors) {
|
||||
this.form.get(code).setErrors(errors[ code ]);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<form [formGroup]="form" (ngSubmit)="create()" #formDirective="ngForm">
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<p>{{'submission.info' | i18n}}</p>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-select placeholder="{{'submission.entryType' | i18n}}" formControlName="entryType">
|
||||
<mat-select-trigger>
|
||||
<mat-icon>{{'entryType.' + entryType + '.icon' | i18n}}</mat-icon> {{'entryType.' + entryType | i18n}}
|
||||
</mat-select-trigger>
|
||||
<mat-option *ngFor="let entryType of entryTypes" [value]="entryType" >
|
||||
<mat-icon>{{'entryType.' + entryType + '.icon' | i18n}}</mat-icon> {{'entryType.' + entryType | i18n}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<input matInput placeholder="{{'submission.url' | i18n}}" formControlName="url" type="url" [required]="entryType == 'LINK'" matAutofocus>
|
||||
<mat-error *ngIf="hasError('url')">
|
||||
{{'submission.url.error' | i18n}}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field>
|
||||
<input matInput placeholder="{{'submission.title' | i18n}}" formControlName="title" type="text" required>
|
||||
<mat-error>
|
||||
{{'submission.title.error' | i18n}}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field>
|
||||
<textarea matInput placeholder="{{'submission.text' | i18n}}" formControlName="text"></textarea>
|
||||
<mat-error>
|
||||
{{'submission.text.error' | i18n}}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button *ngIf="!working" mat-raised-button color="primary" [disabled]="form.invalid">
|
||||
{{'submission.create' | i18n}}
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
|
||||
</form>
|
||||
@@ -0,0 +1,3 @@
|
||||
mat-form-field {
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { EntriesService } from '../../services/entries.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormBuilder, FormGroup, Validators, NgForm } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'page-submission',
|
||||
templateUrl: './submission.page.html',
|
||||
styleUrls: [ './submission.page.scss' ]
|
||||
})
|
||||
export class PageSubmission implements OnInit {
|
||||
|
||||
entryTypes: string[] = [ 'LINK', 'DISCUSSION', 'QUESTION', 'INTERN' ];
|
||||
entryType: string = this.entryTypes[ 0 ];
|
||||
working: boolean = false;
|
||||
form: FormGroup;
|
||||
@ViewChild('formDirective') private formDirective: NgForm;
|
||||
|
||||
constructor(private entriesService: EntriesService,
|
||||
private router: Router,
|
||||
private formBuilder: FormBuilder) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = this.formBuilder.group({
|
||||
entryType: [ '', Validators.required ],
|
||||
url: [ '', Validators.required ],
|
||||
title: [ '', Validators.required ],
|
||||
text: [ '', Validators.nullValidator ],
|
||||
});
|
||||
|
||||
this.form.get('entryType').setValue(this.entryType);
|
||||
|
||||
this.form.get('entryType').valueChanges.subscribe((value) => {
|
||||
this.entryType = value;
|
||||
if (value == 'LINK') {
|
||||
this.form.get('url').setValidators([ Validators.required ]);
|
||||
} else {
|
||||
this.form.get('url').setValidators([ Validators.nullValidator ]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hasError(controlName: string): boolean {
|
||||
return this.form.controls[ controlName ].errors != null;
|
||||
}
|
||||
|
||||
create(): void {
|
||||
|
||||
if (this.working) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.working = true;
|
||||
|
||||
const entry: any = {};
|
||||
entry.url = this.form.get("url").value;
|
||||
entry.entryType = this.entryType;
|
||||
entry.title = this.form.get("title").value;
|
||||
entry.text = this.form.get("text").value;
|
||||
|
||||
this.entriesService.create(entry).subscribe((data) => {
|
||||
this.router.navigateByUrl('/');
|
||||
}, (error) => {
|
||||
this.working = false;
|
||||
if (error.status == 422) {
|
||||
let errors = {};
|
||||
for (let code of error.error) {
|
||||
errors[ code.field ] = errors[ code.field ] || {};
|
||||
errors[ code.field ][ code.code ] = true;
|
||||
}
|
||||
|
||||
for (let code in errors) {
|
||||
this.form.get(code).setErrors(errors[ code ]);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<ui-entries [entries]="entries" [refresh]="boundRefresh" [update]="boundUpdate"></ui-entries>
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
|
||||
import { EntriesService } from '../../services/entries.service';
|
||||
import { PageEvent } from '@angular/material/paginator';
|
||||
|
||||
@Component({
|
||||
selector: 'page-top',
|
||||
templateUrl: './top.page.html'
|
||||
})
|
||||
export class PageTop implements OnInit {
|
||||
|
||||
|
||||
@Input() entries: any;
|
||||
boundRefresh: Function;
|
||||
boundUpdate: Function;
|
||||
init: boolean = true;
|
||||
|
||||
constructor(private entriesService: EntriesService, private router: Router, private route: ActivatedRoute) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.boundRefresh = this.refresh.bind(this);
|
||||
this.boundUpdate = this.update.bind(this);
|
||||
this.route.queryParams.subscribe(params => {
|
||||
if (this.init) {
|
||||
this.entries = {};
|
||||
if (params[ 'p' ]) {
|
||||
this.entries.number = +params[ 'p' ] - 1;
|
||||
if (this.entries.number < 0) {
|
||||
this.entries.number = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (params[ 's' ]) {
|
||||
this.entries.size = +params[ 's' ];
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
this.init = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
if (!this.entries) {
|
||||
this.entries = {};
|
||||
}
|
||||
|
||||
this.entries.content = null;
|
||||
this.entriesService.getPages(this.entries.number || 0, this.entries.size || 30).subscribe((data: any) => {
|
||||
this.entries = data;
|
||||
}, (error) => { })
|
||||
|
||||
}
|
||||
|
||||
update(event: PageEvent) {
|
||||
this.entries.content = null;
|
||||
const params: any = { p: null, s: null };
|
||||
|
||||
if (event.pageIndex != 0) {
|
||||
params.p = event.pageIndex + 1;
|
||||
}
|
||||
|
||||
if (event.pageSize != 30) {
|
||||
params.s = event.pageSize;
|
||||
}
|
||||
|
||||
this.router.navigate(
|
||||
[],
|
||||
{
|
||||
relativeTo: this.route,
|
||||
queryParams: params,
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
|
||||
this.entriesService.getPages(event.pageIndex, event.pageSize).subscribe((data: any) => {
|
||||
this.entries = data;
|
||||
}, (error) => { })
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<mat-card class="warn">
|
||||
<mat-card-header>
|
||||
<mat-card-title>503</mat-card-title>
|
||||
<mat-card-subtitle>{{'service-unavailable' | i18n}}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<p>
|
||||
{{'service-unavailable.text' | i18n}}
|
||||
</p>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<a mat-raised-button color="primary" (click)="retry()">
|
||||
{{'service-unavailable.retry' | i18n}}
|
||||
</a>
|
||||
<a mat-raised-button href="https://wiki.bstly.de/help#Support">
|
||||
{{'service-unavailable.support' | i18n}}
|
||||
</a>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Location } from '@angular/common'
|
||||
import { Router, ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'page-unavailable',
|
||||
templateUrl: './unavailable.page.html'
|
||||
})
|
||||
export class PageUnavailable implements OnInit {
|
||||
|
||||
targetRoute = '';
|
||||
|
||||
constructor(
|
||||
private location: Location,
|
||||
private router: Router,
|
||||
private route: ActivatedRoute) { }
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.subscribe(params => {
|
||||
if (params[ 'target' ]) {
|
||||
this.targetRoute = params[ 'target' ];
|
||||
this.router.navigate([], { queryParams: { target: null }, queryParamsHandling: 'merge', skipLocationChange: true });
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
retry() {
|
||||
if (!this.targetRoute || this.targetRoute === "unavailable" || this.targetRoute === "/unavailable") {
|
||||
this.location.back;
|
||||
} else {
|
||||
this.router.navigate([ this.targetRoute ], { skipLocationChange: true });
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user