fix: improve scrolling

This commit is contained in:
TopchetoEU 2023-06-30 22:04:13 +03:00
parent 650ae22418
commit ba41d147da
18 changed files with 122 additions and 66 deletions

View File

@ -13,4 +13,5 @@ export default interface Image {
visibility: Visibility; visibility: Visibility;
created: number; created: number;
file: string; file: string;
likes: string[];
} }

View File

@ -5,4 +5,5 @@ export default interface User {
username: string; username: string;
password: string; password: string;
images: UUID[]; images: UUID[];
likes: UUID[];
} }

View File

@ -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' } ] });

View File

@ -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 {};

View File

@ -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>

View File

@ -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(

View File

@ -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,

View File

@ -0,0 +1 @@
<p>image works!</p>

View 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 {
}

View File

@ -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>

View File

@ -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;
} }
} }

View File

@ -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>

View File

@ -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,
) { ) {
} }
} }

View File

@ -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>

View File

@ -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>

View File

@ -1,32 +1,36 @@
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',
templateUrl: './page-user.component.html', templateUrl: './page-user.component.html',
styleUrls: ['./page-user.component.scss'] styleUrls: ['./page-user.component.scss']
}) })
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();
}); });
} }
} }

View File

@ -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() { }
} }