From 650ae224180c9300af7ee79fa026ead1669e72a6 Mon Sep 17 00:00:00 2001 From: topchetoeu <36534413+TopchetoEU@users.noreply.github.com> Date: Fri, 30 Jun 2023 15:57:17 +0300 Subject: [PATCH] basic feed scroll in place --- backend/routers/ImageRouter.ts | 21 +++++-- backend/routers/UserRouter.ts | 4 +- backend/server/Router.ts | 1 - frontend/.vscode/settings.json | 6 ++ frontend/src/app/app.component.html | 2 +- frontend/src/app/app.component.ts | 10 +++- frontend/src/app/app.module.ts | 4 +- frontend/src/app/images/images.component.html | 9 +++ frontend/src/app/images/images.component.scss | 55 +++++++++++++++++++ frontend/src/app/images/images.component.ts | 42 ++++++++++++++ .../app/page-home/page-home.component.html | 10 +--- .../app/page-home/page-home.component.scss | 53 ------------------ .../src/app/page-home/page-home.component.ts | 22 ++++++-- .../app/page-upload/page-upload.component.ts | 3 +- .../app/page-user/page-user.component.html | 6 +- .../src/app/page-user/page-user.component.ts | 19 +++++-- frontend/src/app/services/images.service.ts | 15 +++-- frontend/src/app/services/scroll.service.ts | 16 ++++++ frontend/src/app/services/users.service.ts | 1 - 19 files changed, 210 insertions(+), 89 deletions(-) create mode 100644 frontend/.vscode/settings.json create mode 100644 frontend/src/app/images/images.component.html create mode 100644 frontend/src/app/images/images.component.scss create mode 100644 frontend/src/app/images/images.component.ts create mode 100644 frontend/src/app/services/scroll.service.ts diff --git a/backend/routers/ImageRouter.ts b/backend/routers/ImageRouter.ts index 08eecd7..990ab03 100644 --- a/backend/routers/ImageRouter.ts +++ b/backend/routers/ImageRouter.ts @@ -25,7 +25,7 @@ export default class ImageRouter extends AppRouter { } @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) }); if ( @@ -36,7 +36,7 @@ export default class ImageRouter extends AppRouter { return ImageRouter.deserialize(image); } @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 { const start = await Deno.realPath("images"); const file = await Deno.realPath(`images/${id}`); @@ -52,12 +52,23 @@ export default class ImageRouter extends AppRouter { .body((await Deno.open(`images/${id}`)).readable) .contentType(id.split('.')[1]); } - @rest('GET', '/feed') - async self(@page() page: Page) { + @rest('GET') + async feed(@page() page: Page) { 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)); } + @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') 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 const req = await convert(rawReq, { name: 'string?', visibility: 'number?' }); 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"); const id = new UUID(); // Create file diff --git a/backend/routers/UserRouter.ts b/backend/routers/UserRouter.ts index 6102229..52e4e6e 100644 --- a/backend/routers/UserRouter.ts +++ b/backend/routers/UserRouter.ts @@ -31,11 +31,11 @@ export default class UserRouter extends AppRouter { } @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 }); 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') async self(@jwt('salt', true) @auth() auth: JWTPayload) { diff --git a/backend/server/Router.ts b/backend/server/Router.ts index cd8cfb0..4ae3eab 100644 --- a/backend/server/Router.ts +++ b/backend/server/Router.ts @@ -109,7 +109,6 @@ export default class Router { for await (const req of Deno.serveHttp(conn)) { console.log(req.request.url); const r = await this.handle(RestRequest.fromMessage(req)); - console.log(req.request.url); if (r) req.respondWith(r.toFetchResponse()); } })(); diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json new file mode 100644 index 0000000..1383829 --- /dev/null +++ b/frontend/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "deno.enable": true, + "deno.unstable": true, + "deno.enablePaths": [ "backend" ], + "typescript.tsdk": "node_modules\\typescript\\lib" +} \ No newline at end of file diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html index fa9c656..b2012da 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 57fe591..38bdca6 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import { UsersService } from './services/users.service'; +import { ScrollService } from './services/scroll.service'; @Component({ selector: 'app-root', @@ -7,5 +8,12 @@ import { UsersService } from './services/users.service'; styleUrls: ['./app.component.scss'] }) 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 + ) { } } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 6b3f32e..f38a054 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -10,6 +10,7 @@ import { ReactiveFormsModule } from '@angular/forms'; 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'; @NgModule({ declarations: [ @@ -18,7 +19,8 @@ import { PageUserComponent } from './page-user/page-user.component'; PageLoginComponent, PageSignupComponent, PageUploadComponent, - PageUserComponent + PageUserComponent, + ImagesComponent ], imports: [ BrowserModule, diff --git a/frontend/src/app/images/images.component.html b/frontend/src/app/images/images.component.html new file mode 100644 index 0000000..d11f564 --- /dev/null +++ b/frontend/src/app/images/images.component.html @@ -0,0 +1,9 @@ +
+
+ +

{{img.name}}

+
By {{img.author}}
+
+
+ +

No more photos :(

\ No newline at end of file diff --git a/frontend/src/app/images/images.component.scss b/frontend/src/app/images/images.component.scss new file mode 100644 index 0000000..ca5bbb3 --- /dev/null +++ b/frontend/src/app/images/images.component.scss @@ -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%; + } + } +} \ No newline at end of file diff --git a/frontend/src/app/images/images.component.ts b/frontend/src/app/images/images.component.ts new file mode 100644 index 0000000..0346abc --- /dev/null +++ b/frontend/src/app/images/images.component.ts @@ -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 = 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; + } +} diff --git a/frontend/src/app/page-home/page-home.component.html b/frontend/src/app/page-home/page-home.component.html index bfed480..e4083aa 100644 --- a/frontend/src/app/page-home/page-home.component.html +++ b/frontend/src/app/page-home/page-home.component.html @@ -1,10 +1,6 @@ -
-
- -

{{img.name}}

-
By {{img.author}}
-
-
+ + + \ No newline at end of file diff --git a/frontend/src/app/page-home/page-home.component.scss b/frontend/src/app/page-home/page-home.component.scss index 21cbc71..3fa2e16 100644 --- a/frontend/src/app/page-home/page-home.component.scss +++ b/frontend/src/app/page-home/page-home.component.scss @@ -12,59 +12,6 @@ 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 { margin-top: 5rem; diff --git a/frontend/src/app/page-home/page-home.component.ts b/frontend/src/app/page-home/page-home.component.ts index 7fb35cc..f5bb2ee 100644 --- a/frontend/src/app/page-home/page-home.component.ts +++ b/frontend/src/app/page-home/page-home.component.ts @@ -1,21 +1,31 @@ -import { Component } from '@angular/core'; +import { AfterViewInit, Component, ViewChild } from '@angular/core'; import { 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'; @Component({ selector: 'app-page-home', templateUrl: './page-home.component.html', styleUrls: ['./page-home.component.scss'] }) -export class PageHomeComponent { - public images: Image[] = []; - public environment = environment; +export class PageHomeComponent implements AfterViewInit { + @ViewChild("imgs") + public imagesEl!: ImagesComponent; + + public feed(n: number, i: number) { + return this.images.feed({ n, i }); + } + + public ngAfterViewInit(): void { + this.imagesEl.next(); + } + public constructor( - images: ImagesService, public users: UsersService, + public images: ImagesService, ) { - images.feed().then(v => this.images = v); } } diff --git a/frontend/src/app/page-upload/page-upload.component.ts b/frontend/src/app/page-upload/page-upload.component.ts index 88a21a1..813ca75 100644 --- a/frontend/src/app/page-upload/page-upload.component.ts +++ b/frontend/src/app/page-upload/page-upload.component.ts @@ -21,7 +21,8 @@ export class PageUploadComponent { public async submit() { 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.router.navigateByUrl('/'); } diff --git a/frontend/src/app/page-user/page-user.component.html b/frontend/src/app/page-user/page-user.component.html index 241a251..07c321d 100644 --- a/frontend/src/app/page-user/page-user.component.html +++ b/frontend/src/app/page-user/page-user.component.html @@ -1 +1,5 @@ -

page-user works!

+

{{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 5582ee3..a9e91c8 100644 --- a/frontend/src/app/page-user/page-user.component.ts +++ b/frontend/src/app/page-user/page-user.component.ts @@ -1,6 +1,9 @@ -import { Component } from '@angular/core'; +import { 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'; @Component({ selector: 'app-page-user', @@ -8,14 +11,22 @@ import { User, UsersService } from '../services/users.service'; styleUrls: ['./page-user.component.scss'] }) 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( - private route: ActivatedRoute, - private users: UsersService + route: ActivatedRoute, + users: UsersService, + private images: ImagesService, ) { route.paramMap.subscribe(async v => { this.user = await users.get(v.get('name')!); + this.imagesEl.next(); }); } } diff --git a/frontend/src/app/services/images.service.ts b/frontend/src/app/services/images.service.ts index 2b3b86d..077a9ef 100644 --- a/frontend/src/app/services/images.service.ts +++ b/frontend/src/app/services/images.service.ts @@ -21,6 +21,7 @@ export interface CreateImageBody { name?: string; visibility?: Visibility; } +export type Page = {} | { n: number; } | { n: number; i: number; }; @Injectable({ providedIn: 'root' @@ -34,11 +35,15 @@ export class ImagesService { params: { id } })); } - public async feed(n?: number, i?: number) { - return await firstValueFrom(this.http.get(`${this.url}/feed`, { - ...this.users.httpOptions, - // params: { n, i } - })); + public async feed(page: Page = {}) { + return await firstValueFrom(this.http.get(`${this.url}/feed`, + this.users.httpOptions({ params: page }) + )); + } + public async userFeed(username: string, page: Page = {}) { + return await firstValueFrom(this.http.get(`${this.url}/feed/${username}`, + this.users.httpOptions({ params: page }) + )); } public async upload(proto: CreateImageBody, file: File) { diff --git a/frontend/src/app/services/scroll.service.ts b/frontend/src/app/services/scroll.service.ts new file mode 100644 index 0000000..5a410e3 --- /dev/null +++ b/frontend/src/app/services/scroll.service.ts @@ -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() { } +} diff --git a/frontend/src/app/services/users.service.ts b/frontend/src/app/services/users.service.ts index a410a87..c1f18fd 100644 --- a/frontend/src/app/services/users.service.ts +++ b/frontend/src/app/services/users.service.ts @@ -5,7 +5,6 @@ import { firstValueFrom } from 'rxjs'; export interface User { username: string; - images: string[]; } @Injectable({