From ba41d147da95c28a49a2e6bc85ceb72f335fed64 Mon Sep 17 00:00:00 2001 From: topchetoeu <36534413+TopchetoEU@users.noreply.github.com> Date: Fri, 30 Jun 2023 22:04:13 +0300 Subject: [PATCH] fix: improve scrolling --- backend/models/Image.ts | 1 + backend/models/User.ts | 1 + backend/routers/ImageRouter.ts | 51 +++++++++++++++-- backend/routers/UserRouter.ts | 1 + frontend/src/app/app.component.html | 2 +- frontend/src/app/app.component.ts | 10 ++-- frontend/src/app/app.module.ts | 4 +- frontend/src/app/image/image.component.html | 1 + frontend/src/app/image/image.component.scss | 0 frontend/src/app/image/image.component.ts | 10 ++++ frontend/src/app/images/images.component.html | 2 +- frontend/src/app/images/images.component.ts | 55 +++++++++++-------- .../app/page-home/page-home.component.html | 6 +- .../src/app/page-home/page-home.component.ts | 4 +- .../page-upload/page-upload.component.html | 4 +- .../app/page-user/page-user.component.html | 6 +- .../src/app/page-user/page-user.component.ts | 22 +++++--- frontend/src/app/services/scroll.service.ts | 8 +-- 18 files changed, 122 insertions(+), 66 deletions(-) create mode 100644 frontend/src/app/image/image.component.html create mode 100644 frontend/src/app/image/image.component.scss create mode 100644 frontend/src/app/image/image.component.ts diff --git a/backend/models/Image.ts b/backend/models/Image.ts index c4345e5..ecda38a 100644 --- a/backend/models/Image.ts +++ b/backend/models/Image.ts @@ -13,4 +13,5 @@ export default interface Image { visibility: Visibility; created: number; file: string; + likes: string[]; } \ No newline at end of file diff --git a/backend/models/User.ts b/backend/models/User.ts index c6f3049..50cf48f 100644 --- a/backend/models/User.ts +++ b/backend/models/User.ts @@ -5,4 +5,5 @@ export default interface User { username: string; password: string; images: UUID[]; + likes: UUID[]; } diff --git a/backend/routers/ImageRouter.ts b/backend/routers/ImageRouter.ts index 990ab03..3434062 100644 --- a/backend/routers/ImageRouter.ts +++ b/backend/routers/ImageRouter.ts @@ -13,7 +13,7 @@ import AppDatabase from "../AppDatabase.ts"; import RestResponse from "../server/RestResponse.ts"; export default class ImageRouter extends AppRouter { - public static deserialize(image: Image) { + public static deserialize(image: Image, me?: string) { return { author: image.author, created: image.created, @@ -21,6 +21,8 @@ export default class ImageRouter extends AppRouter { visibility: image.visibility, id: image._id, 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 ) throw new HttpError("Image doesn't exist."); - return ImageRouter.deserialize(image); + return ImageRouter.deserialize(image, jwt?.name); } @rest('GET', '/img/:id') async file(id: string, @jwt('salt', false) @auth() jwt?: JWTPayload) { @@ -53,10 +55,10 @@ export default class ImageRouter extends AppRouter { .contentType(id.split('.')[1]); } @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(); 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') 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 } }); 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') @@ -115,7 +117,8 @@ export default class ImageRouter extends AppRouter { created: now(), name: req.name!, visibility: req.visibility, - file: `${id}.${ext}` + file: `${id}.${ext}`, + likes: [], }; await Deno.mkdir('images', { recursive: 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); } + @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) { super(); db.images.createIndexes({ indexes: [ { key: { created: -1 }, name: 'Image Order' } ] }); diff --git a/backend/routers/UserRouter.ts b/backend/routers/UserRouter.ts index 52e4e6e..d846869 100644 --- a/backend/routers/UserRouter.ts +++ b/backend/routers/UserRouter.ts @@ -64,6 +64,7 @@ export default class UserRouter extends AppRouter { username: body.username, password: password, images: [], + likes: [], }); return {}; diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index b2012da..f10f7f5 100644 --- a/frontend/src/app/app.component.html +++ b/frontend/src/app/app.component.html @@ -12,6 +12,6 @@ -
+
\ No newline at end of file diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 38bdca6..9af3a9c 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'; import { UsersService } from './services/users.service'; import { ScrollService } from './services/scroll.service'; @@ -7,9 +7,11 @@ import { ScrollService } from './services/scroll.service'; templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) -export class AppComponent { - public onScroll(el: HTMLDivElement) { - if (el.scrollTop + el.clientHeight * 2 >= el.scrollHeight) this.scroll.signal(el.scrollHeight); +export class AppComponent implements AfterViewInit { + @ViewChild('container') container!: ElementRef; + + public ngAfterViewInit(): void { + this.scroll.scrollHost = this.container.nativeElement; } constructor( diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index f38a054..6f9d124 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -11,6 +11,7 @@ import { PageSignupComponent } from './page-signup/page-signup.component'; import { PageUploadComponent } from './page-upload/page-upload.component'; import { PageUserComponent } from './page-user/page-user.component'; import { ImagesComponent } from './images/images.component'; +import { ImageComponent } from './image/image.component'; @NgModule({ declarations: [ @@ -20,7 +21,8 @@ import { ImagesComponent } from './images/images.component'; PageSignupComponent, PageUploadComponent, PageUserComponent, - ImagesComponent + ImagesComponent, + ImageComponent ], imports: [ BrowserModule, diff --git a/frontend/src/app/image/image.component.html b/frontend/src/app/image/image.component.html new file mode 100644 index 0000000..f0f6943 --- /dev/null +++ b/frontend/src/app/image/image.component.html @@ -0,0 +1 @@ +

image works!

diff --git a/frontend/src/app/image/image.component.scss b/frontend/src/app/image/image.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/app/image/image.component.ts b/frontend/src/app/image/image.component.ts new file mode 100644 index 0000000..65a6812 --- /dev/null +++ b/frontend/src/app/image/image.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-image', + templateUrl: './image.component.html', + styleUrls: ['./image.component.scss'] +}) +export class ImageComponent { + +} diff --git a/frontend/src/app/images/images.component.html b/frontend/src/app/images/images.component.html index d11f564..01f9fff 100644 --- a/frontend/src/app/images/images.component.html +++ b/frontend/src/app/images/images.component.html @@ -2,7 +2,7 @@

{{img.name}}

-
By {{img.author}}
+
By {{img.author}}
diff --git a/frontend/src/app/images/images.component.ts b/frontend/src/app/images/images.component.ts index 0346abc..8c44906 100644 --- a/frontend/src/app/images/images.component.ts +++ b/frontend/src/app/images/images.component.ts @@ -2,6 +2,7 @@ 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'; +import { Observable } from 'rxjs'; @Component({ selector: 'app-images', @@ -9,34 +10,40 @@ import { ScrollService } from '../services/scroll.service'; styleUrls: ['./images.component.scss'] }) export class ImagesComponent { - @Input() public n = 10; - @Input() public feedFunc: (n: number, i: number) => Promise = async () => []; + private static nextFrame() { + return new Promise(requestAnimationFrame); + } + + public static fromFeed(element: HTMLDivElement, n: number, feed: (n: number, i: number) => Promise) { + return new Observable(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 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; + public constructor() { } } diff --git a/frontend/src/app/page-home/page-home.component.html b/frontend/src/app/page-home/page-home.component.html index e4083aa..f343456 100644 --- a/frontend/src/app/page-home/page-home.component.html +++ b/frontend/src/app/page-home/page-home.component.html @@ -1,6 +1,2 @@ - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/frontend/src/app/page-home/page-home.component.ts b/frontend/src/app/page-home/page-home.component.ts index f5bb2ee..f1b965c 100644 --- a/frontend/src/app/page-home/page-home.component.ts +++ b/frontend/src/app/page-home/page-home.component.ts @@ -19,13 +19,13 @@ export class PageHomeComponent implements AfterViewInit { } 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 users: UsersService, public images: ImagesService, + public scroll: ScrollService, ) { } } diff --git a/frontend/src/app/page-upload/page-upload.component.html b/frontend/src/app/page-upload/page-upload.component.html index 16dd962..45ad6cb 100644 --- a/frontend/src/app/page-upload/page-upload.component.html +++ b/frontend/src/app/page-upload/page-upload.component.html @@ -1,10 +1,10 @@
- + - +
diff --git a/frontend/src/app/page-user/page-user.component.html b/frontend/src/app/page-user/page-user.component.html index 07c321d..ce66b99 100644 --- a/frontend/src/app/page-user/page-user.component.html +++ b/frontend/src/app/page-user/page-user.component.html @@ -1,5 +1,3 @@ -

{{user.username}}

+

User {{user.username}}

- - - + diff --git a/frontend/src/app/page-user/page-user.component.ts b/frontend/src/app/page-user/page-user.component.ts index a9e91c8..feb4156 100644 --- a/frontend/src/app/page-user/page-user.component.ts +++ b/frontend/src/app/page-user/page-user.component.ts @@ -1,32 +1,36 @@ -import { Component, ViewChild } from '@angular/core'; +import { AfterViewInit, Component, ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; 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'; +import { ScrollService } from '../services/scroll.service'; +import { Observable } from 'rxjs'; @Component({ - selector: 'app-page-user', - templateUrl: './page-user.component.html', - styleUrls: ['./page-user.component.scss'] + selector: 'app-page-user', + templateUrl: './page-user.component.html', + styleUrls: ['./page-user.component.scss'] }) export class PageUserComponent { @ViewChild("imgs") public imagesEl!: ImagesComponent; - public user: User = { username: 'loading...' }; + public images!: Observable; + public user: User = { username: 'Loading...' }; 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( route: ActivatedRoute, users: UsersService, - private images: ImagesService, + private _images: ImagesService, + private _scroll: ScrollService, ) { route.paramMap.subscribe(async v => { 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(); }); } } diff --git a/frontend/src/app/services/scroll.service.ts b/frontend/src/app/services/scroll.service.ts index 5a410e3..15c74ad 100644 --- a/frontend/src/app/services/scroll.service.ts +++ b/frontend/src/app/services/scroll.service.ts @@ -4,13 +4,7 @@ import { EventEmitter, Injectable } from '@angular/core'; 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; - } + public scrollHost?: HTMLDivElement; constructor() { } }