basic feed scroll in place
This commit is contained in:
parent
ebe52b3aad
commit
650ae22418
@ -25,7 +25,7 @@ export default class ImageRouter extends AppRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@rest('GET', '/')
|
@rest('GET', '/')
|
||||||
async get(@schema('uuid') id: UUID, @jwt(v => v.salt, false) @auth() jwt?: JWTPayload) {
|
async get(@schema('uuid') id: UUID, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
||||||
const image = await this.db.images.findOne({ _id: new UUID(id) });
|
const image = await this.db.images.findOne({ _id: new UUID(id) });
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -36,7 +36,7 @@ export default class ImageRouter extends AppRouter {
|
|||||||
return ImageRouter.deserialize(image);
|
return ImageRouter.deserialize(image);
|
||||||
}
|
}
|
||||||
@rest('GET', '/img/:id')
|
@rest('GET', '/img/:id')
|
||||||
async file(id: string, @jwt(v => v.salt, false) @auth() jwt?: JWTPayload) {
|
async file(id: string, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
||||||
try {
|
try {
|
||||||
const start = await Deno.realPath("images");
|
const start = await Deno.realPath("images");
|
||||||
const file = await Deno.realPath(`images/${id}`);
|
const file = await Deno.realPath(`images/${id}`);
|
||||||
@ -52,12 +52,23 @@ export default class ImageRouter extends AppRouter {
|
|||||||
.body((await Deno.open(`images/${id}`)).readable)
|
.body((await Deno.open(`images/${id}`)).readable)
|
||||||
.contentType(id.split('.')[1]);
|
.contentType(id.split('.')[1]);
|
||||||
}
|
}
|
||||||
@rest('GET', '/feed')
|
@rest('GET')
|
||||||
async self(@page() page: Page) {
|
async feed(@page() page: Page) {
|
||||||
const res = await page.apply(this.db.images.find({ visibility: Visibility.Public })).sort({ created: -1 }).toArray();
|
const res = await page.apply(this.db.images.find({ visibility: Visibility.Public })).sort({ created: -1 }).toArray();
|
||||||
if (!res) throw new HttpError('User not found.');
|
if (!res) throw new HttpError('User not found.');
|
||||||
return res.map(v => ImageRouter.deserialize(v));
|
return res.map(v => ImageRouter.deserialize(v));
|
||||||
}
|
}
|
||||||
|
@rest('GET', '/feed/:username')
|
||||||
|
async userFeed(username: string, @page() page: Page, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
||||||
|
const user = await this.db.users.findOne({ username });
|
||||||
|
if (user === undefined) throw new HttpError("User not found.");
|
||||||
|
|
||||||
|
let cursor;
|
||||||
|
if (user.username === jwt?.name) cursor = this.db.images.find({ _id: { $in: user.images } });
|
||||||
|
else cursor = this.db.images.find({ _id: { $in: user.images }, visibility: Visibility.Public });
|
||||||
|
|
||||||
|
return (await page.apply(cursor.sort({ created: -1 })).toArray()).map(v => ImageRouter.deserialize(v));
|
||||||
|
}
|
||||||
|
|
||||||
@rest('POST', '/upload')
|
@rest('POST', '/upload')
|
||||||
async upload(@body() body: Blob, @headers() headers: Headers, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
|
async upload(@body() body: Blob, @headers() headers: Headers, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
|
||||||
@ -94,7 +105,7 @@ export default class ImageRouter extends AppRouter {
|
|||||||
// Clean up request
|
// Clean up request
|
||||||
const req = await convert(rawReq, { name: 'string?', visibility: 'number?' });
|
const req = await convert(rawReq, { name: 'string?', visibility: 'number?' });
|
||||||
req.name ??= new UUID().toString();
|
req.name ??= new UUID().toString();
|
||||||
req.visibility = 0;
|
req.visibility ??= 0;
|
||||||
if (req.visibility < 0 || req.visibility > 2) throw new HttpError("body.visibility: Must be 0, 1, or 2");
|
if (req.visibility < 0 || req.visibility > 2) throw new HttpError("body.visibility: Must be 0, 1, or 2");
|
||||||
const id = new UUID();
|
const id = new UUID();
|
||||||
// Create file
|
// Create file
|
||||||
|
@ -31,11 +31,11 @@ export default class UserRouter extends AppRouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@rest('GET', '/')
|
@rest('GET', '/')
|
||||||
async get(@schema('string') username: string, @jwt('salt', true) @auth() jwt: JWTPayload) {
|
async get(@schema('string') username: string, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
||||||
const res = await this.db.users.findOne({ username });
|
const res = await this.db.users.findOne({ username });
|
||||||
if (res === undefined) throw new HttpError('User not found.');
|
if (res === undefined) throw new HttpError('User not found.');
|
||||||
|
|
||||||
return this.deserialize(res, jwt.name === username);
|
return this.deserialize(res, jwt?.name === username);
|
||||||
}
|
}
|
||||||
@rest('GET', '/self')
|
@rest('GET', '/self')
|
||||||
async self(@jwt('salt', true) @auth() auth: JWTPayload) {
|
async self(@jwt('salt', true) @auth() auth: JWTPayload) {
|
||||||
|
@ -109,7 +109,6 @@ export default class Router {
|
|||||||
for await (const req of Deno.serveHttp(conn)) {
|
for await (const req of Deno.serveHttp(conn)) {
|
||||||
console.log(req.request.url);
|
console.log(req.request.url);
|
||||||
const r = await this.handle(RestRequest.fromMessage(req));
|
const r = await this.handle(RestRequest.fromMessage(req));
|
||||||
console.log(req.request.url);
|
|
||||||
if (r) req.respondWith(r.toFetchResponse());
|
if (r) req.respondWith(r.toFetchResponse());
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
6
frontend/.vscode/settings.json
vendored
Normal file
6
frontend/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"deno.enable": true,
|
||||||
|
"deno.unstable": true,
|
||||||
|
"deno.enablePaths": [ "backend" ],
|
||||||
|
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||||
|
}
|
@ -12,6 +12,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content" #container (scroll)="onScroll(container)">
|
||||||
<router-outlet #o="outlet"></router-outlet>
|
<router-outlet #o="outlet"></router-outlet>
|
||||||
</div>
|
</div>
|
@ -1,5 +1,6 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { UsersService } from './services/users.service';
|
import { UsersService } from './services/users.service';
|
||||||
|
import { ScrollService } from './services/scroll.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
@ -7,5 +8,12 @@ import { UsersService } from './services/users.service';
|
|||||||
styleUrls: ['./app.component.scss']
|
styleUrls: ['./app.component.scss']
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
constructor(public users: UsersService) {}
|
public onScroll(el: HTMLDivElement) {
|
||||||
|
if (el.scrollTop + el.clientHeight * 2 >= el.scrollHeight) this.scroll.signal(el.scrollHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public users: UsersService,
|
||||||
|
public scroll: ScrollService
|
||||||
|
) { }
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import { ReactiveFormsModule } from '@angular/forms';
|
|||||||
import { PageSignupComponent } from './page-signup/page-signup.component';
|
import { PageSignupComponent } from './page-signup/page-signup.component';
|
||||||
import { PageUploadComponent } from './page-upload/page-upload.component';
|
import { PageUploadComponent } from './page-upload/page-upload.component';
|
||||||
import { PageUserComponent } from './page-user/page-user.component';
|
import { PageUserComponent } from './page-user/page-user.component';
|
||||||
|
import { ImagesComponent } from './images/images.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -18,7 +19,8 @@ import { PageUserComponent } from './page-user/page-user.component';
|
|||||||
PageLoginComponent,
|
PageLoginComponent,
|
||||||
PageSignupComponent,
|
PageSignupComponent,
|
||||||
PageUploadComponent,
|
PageUploadComponent,
|
||||||
PageUserComponent
|
PageUserComponent,
|
||||||
|
ImagesComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
9
frontend/src/app/images/images.component.html
Normal file
9
frontend/src/app/images/images.component.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<div class="images">
|
||||||
|
<div class="image" *ngFor="let img of images">
|
||||||
|
<img [src]="environment.apiURL + '/images/img/' + img.file">
|
||||||
|
<h3 class="image-name">{{img.name}}</h3>
|
||||||
|
<h5 class="image-author">By {{img.author}}</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 *ngIf="ended">No more photos :(</h3>
|
55
frontend/src/app/images/images.component.scss
Normal file
55
frontend/src/app/images/images.component.scss
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
|
||||||
|
.images {
|
||||||
|
display: block;
|
||||||
|
width: min(100%, 85rem);
|
||||||
|
margin: auto;
|
||||||
|
gap: 5rem;
|
||||||
|
align-items: stretch;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0 1rem;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
img {
|
||||||
|
pointer-events: none;
|
||||||
|
height: 15rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
width: min-content;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: .25rem .25rem 1rem -.5rem #000;
|
||||||
|
padding: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1rem;
|
||||||
|
|
||||||
|
p, h3, h5 {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
h3, h5 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (width < 50rem) {
|
||||||
|
.images {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5rem;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
frontend/src/app/images/images.component.ts
Normal file
42
frontend/src/app/images/images.component.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { Image, ImagesService } from '../services/images.service';
|
||||||
|
import { ScrollService } from '../services/scroll.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-images',
|
||||||
|
templateUrl: './images.component.html',
|
||||||
|
styleUrls: ['./images.component.scss']
|
||||||
|
})
|
||||||
|
export class ImagesComponent {
|
||||||
|
@Input() public n = 10;
|
||||||
|
@Input() public feedFunc: (n: number, i: number) => Promise<Image[]> = async () => [];
|
||||||
|
|
||||||
|
public ended = false;
|
||||||
|
public images: Image[] = [];
|
||||||
|
public environment = environment;
|
||||||
|
public i = 0;
|
||||||
|
private sub: () => void;
|
||||||
|
|
||||||
|
public async next() {
|
||||||
|
const res = await this.feedFunc(this.n, this.i);
|
||||||
|
if (res.length === 0) {
|
||||||
|
this.ended = true;
|
||||||
|
this.sub();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.images.push(...res);
|
||||||
|
this.i++;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public scroll: ScrollService
|
||||||
|
) {
|
||||||
|
this.sub = scroll.endReached.subscribe(() => {
|
||||||
|
this.next();
|
||||||
|
}).unsubscribe;
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,6 @@
|
|||||||
<button routerLink="/upload" *ngIf="users.$user()">Upload image</button>
|
<button routerLink="/upload" *ngIf="users.$user()">Upload image</button>
|
||||||
<!-- <button routerLink="/projects" *ngIf="users.$user()">Към проектите</button> -->
|
<!-- <button routerLink="/projects" *ngIf="users.$user()">Към проектите</button> -->
|
||||||
|
|
||||||
<div class="images">
|
<app-images [feedFunc]="feed.bind(this)" #imgs>
|
||||||
<div class="image" *ngFor="let img of images">
|
|
||||||
<img [src]="environment.apiURL + '/images/img/' + img.file">
|
</app-images>
|
||||||
<h3 class="image-name">{{img.name}}</h3>
|
|
||||||
<h5 class="image-author">By {{img.author}}</h5>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -12,59 +12,6 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.images {
|
|
||||||
margin-top: 5rem;
|
|
||||||
width: 85rem;
|
|
||||||
max-width: 100%;
|
|
||||||
gap: 5rem;
|
|
||||||
align-items: stretch;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: 0 1rem;
|
|
||||||
text-align: center;
|
|
||||||
.image {
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
img {
|
|
||||||
pointer-events: none;
|
|
||||||
height: 15rem;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
width: min-content;
|
|
||||||
border-radius: 1rem;
|
|
||||||
box-shadow: .25rem .25rem 1rem -.5rem #000;
|
|
||||||
padding: 1rem;
|
|
||||||
box-sizing: border-box;
|
|
||||||
display: inline-flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
margin: 1rem;
|
|
||||||
|
|
||||||
p, h3, h5 {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
h3, h5 {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
text-align: justify;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media screen and (width < 50rem) {
|
|
||||||
.images {
|
|
||||||
margin-top: 5rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5rem;
|
|
||||||
align-items: stretch;
|
|
||||||
.image {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
margin-top: 5rem;
|
margin-top: 5rem;
|
||||||
|
@ -1,21 +1,31 @@
|
|||||||
import { Component } from '@angular/core';
|
import { AfterViewInit, Component, ViewChild } from '@angular/core';
|
||||||
import { UsersService } from '../services/users.service';
|
import { UsersService } from '../services/users.service';
|
||||||
import { Image, ImagesService } from '../services/images.service';
|
import { Image, ImagesService } from '../services/images.service';
|
||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { ImagesComponent } from '../images/images.component';
|
||||||
|
import { ScrollService } from '../services/scroll.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-page-home',
|
selector: 'app-page-home',
|
||||||
templateUrl: './page-home.component.html',
|
templateUrl: './page-home.component.html',
|
||||||
styleUrls: ['./page-home.component.scss']
|
styleUrls: ['./page-home.component.scss']
|
||||||
})
|
})
|
||||||
export class PageHomeComponent {
|
export class PageHomeComponent implements AfterViewInit {
|
||||||
public images: Image[] = [];
|
@ViewChild("imgs")
|
||||||
public environment = environment;
|
public imagesEl!: ImagesComponent;
|
||||||
|
|
||||||
|
public feed(n: number, i: number) {
|
||||||
|
return this.images.feed({ n, i });
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngAfterViewInit(): void {
|
||||||
|
this.imagesEl.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
images: ImagesService,
|
|
||||||
public users: UsersService,
|
public users: UsersService,
|
||||||
|
public images: ImagesService,
|
||||||
) {
|
) {
|
||||||
images.feed().then(v => this.images = v);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,8 @@ export class PageUploadComponent {
|
|||||||
|
|
||||||
public async submit() {
|
public async submit() {
|
||||||
await this.images.upload({
|
await this.images.upload({
|
||||||
name: this.form.value.name ?? ''
|
name: this.form.value.name ?? undefined,
|
||||||
|
visibility: this.form.value.visibility ?? undefined,
|
||||||
}, this.form.value.file!);
|
}, this.form.value.file!);
|
||||||
this.router.navigateByUrl('/');
|
this.router.navigateByUrl('/');
|
||||||
}
|
}
|
||||||
|
@ -1 +1,5 @@
|
|||||||
<p>page-user works!</p>
|
<h3>{{user.username}}</h3>
|
||||||
|
|
||||||
|
<app-images [feedFunc]="feed.bind(this)" #imgs>
|
||||||
|
|
||||||
|
</app-images>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, ViewChild } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { User, UsersService } from '../services/users.service';
|
import { User, UsersService } from '../services/users.service';
|
||||||
|
import { Image, ImagesService } from '../services/images.service';
|
||||||
|
import { environment } from 'src/environments/environment';
|
||||||
|
import { ImagesComponent } from '../images/images.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-page-user',
|
selector: 'app-page-user',
|
||||||
@ -8,14 +11,22 @@ import { User, UsersService } from '../services/users.service';
|
|||||||
styleUrls: ['./page-user.component.scss']
|
styleUrls: ['./page-user.component.scss']
|
||||||
})
|
})
|
||||||
export class PageUserComponent {
|
export class PageUserComponent {
|
||||||
public user: User = { images: [], username: 'loading...' };
|
@ViewChild("imgs")
|
||||||
|
public imagesEl!: ImagesComponent;
|
||||||
|
public user: User = { username: 'loading...' };
|
||||||
|
|
||||||
|
public feed(n: number, i: number) {
|
||||||
|
return this.images.userFeed(this.user.username, { n, i });
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private route: ActivatedRoute,
|
route: ActivatedRoute,
|
||||||
private users: UsersService
|
users: UsersService,
|
||||||
|
private images: ImagesService,
|
||||||
) {
|
) {
|
||||||
route.paramMap.subscribe(async v => {
|
route.paramMap.subscribe(async v => {
|
||||||
this.user = await users.get(v.get('name')!);
|
this.user = await users.get(v.get('name')!);
|
||||||
|
this.imagesEl.next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ export interface CreateImageBody {
|
|||||||
name?: string;
|
name?: string;
|
||||||
visibility?: Visibility;
|
visibility?: Visibility;
|
||||||
}
|
}
|
||||||
|
export type Page = {} | { n: number; } | { n: number; i: number; };
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -34,11 +35,15 @@ export class ImagesService {
|
|||||||
params: { id }
|
params: { id }
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
public async feed(n?: number, i?: number) {
|
public async feed(page: Page = {}) {
|
||||||
return await firstValueFrom(this.http.get<Image[]>(`${this.url}/feed`, {
|
return await firstValueFrom(this.http.get<Image[]>(`${this.url}/feed`,
|
||||||
...this.users.httpOptions,
|
this.users.httpOptions({ params: page })
|
||||||
// params: { n, i }
|
));
|
||||||
}));
|
}
|
||||||
|
public async userFeed(username: string, page: Page = {}) {
|
||||||
|
return await firstValueFrom(this.http.get<Image[]>(`${this.url}/feed/${username}`,
|
||||||
|
this.users.httpOptions({ params: page })
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async upload(proto: CreateImageBody, file: File) {
|
public async upload(proto: CreateImageBody, file: File) {
|
||||||
|
16
frontend/src/app/services/scroll.service.ts
Normal file
16
frontend/src/app/services/scroll.service.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { EventEmitter, Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class ScrollService {
|
||||||
|
public endReached = new EventEmitter();
|
||||||
|
private prevHeight = -1;
|
||||||
|
|
||||||
|
public signal(height: number) {
|
||||||
|
if (this.prevHeight !== height) this.endReached.emit();
|
||||||
|
this.prevHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
}
|
@ -5,7 +5,6 @@ import { firstValueFrom } from 'rxjs';
|
|||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
username: string;
|
username: string;
|
||||||
images: string[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
Loading…
Reference in New Issue
Block a user