fix: improve scrolling
This commit is contained in:
parent
650ae22418
commit
ba41d147da
@ -13,4 +13,5 @@ export default interface Image {
|
|||||||
visibility: Visibility;
|
visibility: Visibility;
|
||||||
created: number;
|
created: number;
|
||||||
file: string;
|
file: string;
|
||||||
|
likes: string[];
|
||||||
}
|
}
|
@ -5,4 +5,5 @@ export default interface User {
|
|||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
images: UUID[];
|
images: UUID[];
|
||||||
|
likes: UUID[];
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import AppDatabase from "../AppDatabase.ts";
|
|||||||
import RestResponse from "../server/RestResponse.ts";
|
import RestResponse from "../server/RestResponse.ts";
|
||||||
|
|
||||||
export default class ImageRouter extends AppRouter {
|
export default class ImageRouter extends AppRouter {
|
||||||
public static deserialize(image: Image) {
|
public static deserialize(image: Image, me?: string) {
|
||||||
return {
|
return {
|
||||||
author: image.author,
|
author: image.author,
|
||||||
created: image.created,
|
created: image.created,
|
||||||
@ -21,6 +21,8 @@ export default class ImageRouter extends AppRouter {
|
|||||||
visibility: image.visibility,
|
visibility: image.visibility,
|
||||||
id: image._id,
|
id: image._id,
|
||||||
file: image.file,
|
file: image.file,
|
||||||
|
likes: image.likes?.length ?? 0,
|
||||||
|
liked: image.likes?.includes(me!) ?? false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +35,7 @@ export default class ImageRouter extends AppRouter {
|
|||||||
image.visibility === Visibility.Private && image.author !== jwt?.name
|
image.visibility === Visibility.Private && image.author !== jwt?.name
|
||||||
) throw new HttpError("Image doesn't exist.");
|
) throw new HttpError("Image doesn't exist.");
|
||||||
|
|
||||||
return ImageRouter.deserialize(image);
|
return ImageRouter.deserialize(image, jwt?.name);
|
||||||
}
|
}
|
||||||
@rest('GET', '/img/:id')
|
@rest('GET', '/img/:id')
|
||||||
async file(id: string, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
async file(id: string, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
||||||
@ -53,10 +55,10 @@ export default class ImageRouter extends AppRouter {
|
|||||||
.contentType(id.split('.')[1]);
|
.contentType(id.split('.')[1]);
|
||||||
}
|
}
|
||||||
@rest('GET')
|
@rest('GET')
|
||||||
async feed(@page() page: Page) {
|
async feed(@page() page: Page, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
||||||
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, jwt?.name));
|
||||||
}
|
}
|
||||||
@rest('GET', '/feed/:username')
|
@rest('GET', '/feed/:username')
|
||||||
async userFeed(username: string, @page() page: Page, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
async userFeed(username: string, @page() page: Page, @jwt('salt', false) @auth() jwt?: JWTPayload) {
|
||||||
@ -67,7 +69,7 @@ export default class ImageRouter extends AppRouter {
|
|||||||
if (user.username === jwt?.name) cursor = this.db.images.find({ _id: { $in: user.images } });
|
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 });
|
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));
|
return (await page.apply(cursor.sort({ created: -1 })).toArray()).map(v => ImageRouter.deserialize(v, jwt?.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
@rest('POST', '/upload')
|
@rest('POST', '/upload')
|
||||||
@ -115,7 +117,8 @@ export default class ImageRouter extends AppRouter {
|
|||||||
created: now(),
|
created: now(),
|
||||||
name: req.name!,
|
name: req.name!,
|
||||||
visibility: req.visibility,
|
visibility: req.visibility,
|
||||||
file: `${id}.${ext}`
|
file: `${id}.${ext}`,
|
||||||
|
likes: [],
|
||||||
};
|
};
|
||||||
await Deno.mkdir('images', { recursive: true });
|
await Deno.mkdir('images', { recursive: true });
|
||||||
const out = await Deno.open(`images/${id}.${ext}`, { write: true, create: true });
|
const out = await Deno.open(`images/${id}.${ext}`, { write: true, create: true });
|
||||||
@ -148,6 +151,42 @@ export default class ImageRouter extends AppRouter {
|
|||||||
return ImageRouter.deserialize(img);
|
return ImageRouter.deserialize(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@rest('POST', '/like')
|
||||||
|
async like(@uuid() id: UUID, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
|
||||||
|
const user = await this.db.users.findOne({ username: jwt.name });
|
||||||
|
if (!user) throw new HttpError("You don't exist.");
|
||||||
|
|
||||||
|
const res = await this.db.images.updateOne(
|
||||||
|
{ _id: id },
|
||||||
|
{ $addToSet: { likes: jwt.name } }
|
||||||
|
);
|
||||||
|
if (res.matchedCount === 0) throw new HttpError("Image doesn't exist.");
|
||||||
|
await this.db.users.updateOne(
|
||||||
|
{ username: jwt.name },
|
||||||
|
{ $addToSet: { likes: id } }
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return ImageRouter.deserialize((await this.db.images.findOne({ _id: id }))!);
|
||||||
|
}
|
||||||
|
@rest('POST', '/dislike')
|
||||||
|
async dislike(@uuid() id: UUID, @jwt(v => v.salt, true) @auth() jwt: JWTPayload) {
|
||||||
|
const user = await this.db.users.findOne({ username: jwt.name });
|
||||||
|
if (!user) throw new HttpError("You don't exist.");
|
||||||
|
|
||||||
|
const res = await this.db.images.updateOne(
|
||||||
|
{ _id: id },
|
||||||
|
{ $pull: { likes: jwt.name } }
|
||||||
|
);
|
||||||
|
if (res.matchedCount === 0) throw new HttpError("Image doesn't exist.");
|
||||||
|
await this.db.users.updateOne(
|
||||||
|
{ username: jwt.name },
|
||||||
|
{ $pull: { likes: id } }
|
||||||
|
);
|
||||||
|
|
||||||
|
return ImageRouter.deserialize((await this.db.images.findOne({ _id: id }))!);
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(private db: AppDatabase) {
|
public constructor(private db: AppDatabase) {
|
||||||
super();
|
super();
|
||||||
db.images.createIndexes({ indexes: [ { key: { created: -1 }, name: 'Image Order' } ] });
|
db.images.createIndexes({ indexes: [ { key: { created: -1 }, name: 'Image Order' } ] });
|
||||||
|
@ -64,6 +64,7 @@ export default class UserRouter extends AppRouter {
|
|||||||
username: body.username,
|
username: body.username,
|
||||||
password: password,
|
password: password,
|
||||||
images: [],
|
images: [],
|
||||||
|
likes: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
@ -12,6 +12,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="content" #container (scroll)="onScroll(container)">
|
<div class="content" #container>
|
||||||
<router-outlet #o="outlet"></router-outlet>
|
<router-outlet #o="outlet"></router-outlet>
|
||||||
</div>
|
</div>
|
@ -1,4 +1,4 @@
|
|||||||
import { Component } from '@angular/core';
|
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';
|
||||||
import { UsersService } from './services/users.service';
|
import { UsersService } from './services/users.service';
|
||||||
import { ScrollService } from './services/scroll.service';
|
import { ScrollService } from './services/scroll.service';
|
||||||
|
|
||||||
@ -7,9 +7,11 @@ import { ScrollService } from './services/scroll.service';
|
|||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.scss']
|
styleUrls: ['./app.component.scss']
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent implements AfterViewInit {
|
||||||
public onScroll(el: HTMLDivElement) {
|
@ViewChild('container') container!: ElementRef<HTMLDivElement>;
|
||||||
if (el.scrollTop + el.clientHeight * 2 >= el.scrollHeight) this.scroll.signal(el.scrollHeight);
|
|
||||||
|
public ngAfterViewInit(): void {
|
||||||
|
this.scroll.scrollHost = this.container.nativeElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -11,6 +11,7 @@ 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';
|
import { ImagesComponent } from './images/images.component';
|
||||||
|
import { ImageComponent } from './image/image.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -20,7 +21,8 @@ import { ImagesComponent } from './images/images.component';
|
|||||||
PageSignupComponent,
|
PageSignupComponent,
|
||||||
PageUploadComponent,
|
PageUploadComponent,
|
||||||
PageUserComponent,
|
PageUserComponent,
|
||||||
ImagesComponent
|
ImagesComponent,
|
||||||
|
ImageComponent
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
1
frontend/src/app/image/image.component.html
Normal file
1
frontend/src/app/image/image.component.html
Normal file
@ -0,0 +1 @@
|
|||||||
|
<p>image works!</p>
|
0
frontend/src/app/image/image.component.scss
Normal file
0
frontend/src/app/image/image.component.scss
Normal file
10
frontend/src/app/image/image.component.ts
Normal file
10
frontend/src/app/image/image.component.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-image',
|
||||||
|
templateUrl: './image.component.html',
|
||||||
|
styleUrls: ['./image.component.scss']
|
||||||
|
})
|
||||||
|
export class ImageComponent {
|
||||||
|
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
<div class="image" *ngFor="let img of images">
|
<div class="image" *ngFor="let img of images">
|
||||||
<img [src]="environment.apiURL + '/images/img/' + img.file">
|
<img [src]="environment.apiURL + '/images/img/' + img.file">
|
||||||
<h3 class="image-name">{{img.name}}</h3>
|
<h3 class="image-name">{{img.name}}</h3>
|
||||||
<h5 class="image-author">By {{img.author}}</h5>
|
<h5 class="image-author">By <a [routerLink]="'/user/' + img.author">{{img.author}}</a></h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { Component, EventEmitter, Input, Output, ViewChild } from '@angular/core
|
|||||||
import { environment } from 'src/environments/environment';
|
import { environment } from 'src/environments/environment';
|
||||||
import { Image, ImagesService } from '../services/images.service';
|
import { Image, ImagesService } from '../services/images.service';
|
||||||
import { ScrollService } from '../services/scroll.service';
|
import { ScrollService } from '../services/scroll.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-images',
|
selector: 'app-images',
|
||||||
@ -9,34 +10,40 @@ import { ScrollService } from '../services/scroll.service';
|
|||||||
styleUrls: ['./images.component.scss']
|
styleUrls: ['./images.component.scss']
|
||||||
})
|
})
|
||||||
export class ImagesComponent {
|
export class ImagesComponent {
|
||||||
@Input() public n = 10;
|
private static nextFrame() {
|
||||||
@Input() public feedFunc: (n: number, i: number) => Promise<Image[]> = async () => [];
|
return new Promise(requestAnimationFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static fromFeed(element: HTMLDivElement, n: number, feed: (n: number, i: number) => Promise<Image[]>) {
|
||||||
|
return new Observable<Image[]>(sub => {
|
||||||
|
let done = false;
|
||||||
|
let images: Image[] = [];
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
while (!done) {
|
||||||
|
await this.nextFrame();
|
||||||
|
if (element.scrollTop + element.clientHeight * 1.5 < element.scrollHeight) continue;
|
||||||
|
|
||||||
|
const els = await feed(n, i++);
|
||||||
|
images.push(...els);
|
||||||
|
|
||||||
|
if (els.length === 0) {
|
||||||
|
sub.complete();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else sub.next(images);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input() public images: Image[] | null = null;
|
||||||
|
|
||||||
public ended = false;
|
public ended = false;
|
||||||
public images: Image[] = [];
|
|
||||||
public environment = environment;
|
public environment = environment;
|
||||||
public i = 0;
|
public i = 0;
|
||||||
private sub: () => void;
|
|
||||||
|
|
||||||
public async next() {
|
public constructor() {
|
||||||
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,6 +1,2 @@
|
|||||||
<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> -->
|
<app-images #imgs></app-images>
|
||||||
|
|
||||||
<app-images [feedFunc]="feed.bind(this)" #imgs>
|
|
||||||
|
|
||||||
</app-images>
|
|
@ -19,13 +19,13 @@ export class PageHomeComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ngAfterViewInit(): void {
|
public ngAfterViewInit(): void {
|
||||||
this.imagesEl.next();
|
ImagesComponent.fromFeed(this.scroll.scrollHost!, 10, this.feed.bind(this)).subscribe(v => this.imagesEl.images = v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public users: UsersService,
|
public users: UsersService,
|
||||||
public images: ImagesService,
|
public images: ImagesService,
|
||||||
|
public scroll: ScrollService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<form class="content" [formGroup]="form" (ngSubmit)="submit()">
|
<form class="content" [formGroup]="form" (ngSubmit)="submit()">
|
||||||
<input type="text" placeholder="Име" formControlName="name">
|
<input type="text" placeholder="Name" formControlName="name">
|
||||||
<input type="file" (change)="setFile($event)"/>
|
<input type="file" (change)="setFile($event)"/>
|
||||||
<select formControlName="visibility">
|
<select formControlName="visibility">
|
||||||
<option [value]="0">Public</option>
|
<option [value]="0">Public</option>
|
||||||
<option [value]="1">Unlisted</option>
|
<option [value]="1">Unlisted</option>
|
||||||
<option [value]="2">Private</option>
|
<option [value]="2">Private</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="submit">Създай проект</button>
|
<button type="submit">Upload</button>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
<h3>{{user.username}}</h3>
|
<h1 style="text-align: center;">User {{user.username}}</h1>
|
||||||
|
|
||||||
<app-images [feedFunc]="feed.bind(this)" #imgs>
|
<app-images [images]="images | async" #imgs></app-images>
|
||||||
|
|
||||||
</app-images>
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { Component, ViewChild } from '@angular/core';
|
import { AfterViewInit, 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 { Image, ImagesService } from '../services/images.service';
|
||||||
import { environment } from 'src/environments/environment';
|
|
||||||
import { ImagesComponent } from '../images/images.component';
|
import { ImagesComponent } from '../images/images.component';
|
||||||
|
import { ScrollService } from '../services/scroll.service';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-page-user',
|
selector: 'app-page-user',
|
||||||
@ -13,20 +14,23 @@ import { ImagesComponent } from '../images/images.component';
|
|||||||
export class PageUserComponent {
|
export class PageUserComponent {
|
||||||
@ViewChild("imgs")
|
@ViewChild("imgs")
|
||||||
public imagesEl!: ImagesComponent;
|
public imagesEl!: ImagesComponent;
|
||||||
public user: User = { username: 'loading...' };
|
public images!: Observable<Image[]>;
|
||||||
|
public user: User = { username: 'Loading...' };
|
||||||
|
|
||||||
public feed(n: number, i: number) {
|
public feed(n: number, i: number) {
|
||||||
return this.images.userFeed(this.user.username, { n, i });
|
return this._images.userFeed(this.user.username, { n, i });
|
||||||
}
|
}
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
route: ActivatedRoute,
|
route: ActivatedRoute,
|
||||||
users: UsersService,
|
users: UsersService,
|
||||||
private images: ImagesService,
|
private _images: ImagesService,
|
||||||
|
private _scroll: ScrollService,
|
||||||
) {
|
) {
|
||||||
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();
|
this.images = ImagesComponent.fromFeed(this._scroll.scrollHost!, 10, this.feed.bind(this));
|
||||||
|
// this.imagesEl.next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,7 @@ import { EventEmitter, Injectable } from '@angular/core';
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class ScrollService {
|
export class ScrollService {
|
||||||
public endReached = new EventEmitter();
|
public scrollHost?: HTMLDivElement;
|
||||||
private prevHeight = -1;
|
|
||||||
|
|
||||||
public signal(height: number) {
|
|
||||||
if (this.prevHeight !== height) this.endReached.emit();
|
|
||||||
this.prevHeight = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user