From 97f7712d551ed4a2e87c854e6148f62852073710 Mon Sep 17 00:00:00 2001 From: chatenium Date: Thu, 9 Apr 2026 13:41:04 +0200 Subject: [PATCH] Added attachment support --- package-lock.json | 11 +- package.json | 5 +- public/i18n/en.json | 4 + src/app/chat/dm/dm.html | 1 - src/app/chat/dm/dm.ts | 56 +++++++++- src/app/chat/elements/masonry/masonry.html | 1 + src/app/chat/elements/masonry/masonry.scss | 12 +++ src/app/chat/elements/masonry/masonry.spec.ts | 22 ++++ src/app/chat/elements/masonry/masonry.ts | 28 +++++ .../message-box/message-box-viewmodel.ts | 14 ++- .../elements/message-box/message-box.html | 29 ++++- .../chat/elements/message-box/message-box.ts | 100 +++++++++++++++++- src/app/chat/elements/messages/messages.html | 5 + 13 files changed, 270 insertions(+), 18 deletions(-) create mode 100644 src/app/chat/elements/masonry/masonry.html create mode 100644 src/app/chat/elements/masonry/masonry.scss create mode 100644 src/app/chat/elements/masonry/masonry.spec.ts create mode 100644 src/app/chat/elements/masonry/masonry.ts diff --git a/package-lock.json b/package-lock.json index a8c20d7..c0500ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@angular/forms": "^21.2.0", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", - "@chatenium/chatenium-sdk": "^1.0.8", + "@chatenium/chatenium-sdk": "^1.0.10", "@ngx-translate/core": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0", "@taiga-ui/addon-charts": "^5.1.0", @@ -28,7 +28,8 @@ "@taiga-ui/layout": "^5.1.0", "ngx-cookie-service": "^21.3.1", "rxjs": "~7.8.0", - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "uuid": "^13.0.0" }, "devDependencies": { "@angular/build": "^21.2.6", @@ -987,9 +988,9 @@ } }, "node_modules/@chatenium/chatenium-sdk": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.0.8.tgz", - "integrity": "sha512-avJ61UPEk6GQ6+0fbA9Tl2VZU7O7RE/evAXIsTxA0S0oQzltWFA4k0ttPCPw8LLEpNNQCoQk/2dd0l2+y75t4Q==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.0.10.tgz", + "integrity": "sha512-AhWtM4bD3p1nXW/tC/eiBlPGesk/vjwjf1CPuScYaD2OwmXLGZXNZPXe3g6T5dRAA1Zyo18QkWEJXglA+6VZSQ==", "dependencies": { "@faker-js/faker": "^10.4.0", "axios": "^1.14.0", diff --git a/package.json b/package.json index ef1bf9a..e1d706e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@angular/forms": "^21.2.0", "@angular/platform-browser": "^21.2.0", "@angular/router": "^21.2.0", - "@chatenium/chatenium-sdk": "^1.0.8", + "@chatenium/chatenium-sdk": "^1.0.10", "@ngx-translate/core": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0", "@taiga-ui/addon-charts": "^5.1.0", @@ -31,7 +31,8 @@ "@taiga-ui/layout": "^5.1.0", "ngx-cookie-service": "^21.3.1", "rxjs": "~7.8.0", - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "uuid": "^13.0.0" }, "devDependencies": { "@angular/build": "^21.2.6", diff --git a/public/i18n/en.json b/public/i18n/en.json index 0862b94..f24ebc6 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -31,9 +31,13 @@ "elements": { "messageBox": { "placeholder": "Type a message...", + "message": "Message", "uplDrag": { "upload": "Drop here to upload", "transfer": "Drop here to transfer" + }, + "fileUploadDialog": { + "label": "Upload files" } } } diff --git a/src/app/chat/dm/dm.html b/src/app/chat/dm/dm.html index 347f563..287d33d 100644 --- a/src/app/chat/dm/dm.html +++ b/src/app/chat/dm/dm.html @@ -17,7 +17,6 @@ - {{chatid}} diff --git a/src/app/chat/dm/dm.ts b/src/app/chat/dm/dm.ts index bb72ada..bea75bb 100644 --- a/src/app/chat/dm/dm.ts +++ b/src/app/chat/dm/dm.ts @@ -6,11 +6,14 @@ import {IndexedDB} from '../../storage/indexed-db'; import {Navbar} from '../elements/navbar/navbar'; import {Oimg} from '../elements/oimg/oimg'; import {TuiButton, TuiIcon} from '@taiga-ui/core'; -import {MessageBox} from '../elements/message-box/message-box'; +import {FileDataWithPreview, MessageBox} from '../elements/message-box/message-box'; import {Messages} from '../elements/messages/messages'; import {Chat} from '@chatenium/chatenium-sdk/domain/chatService.schema'; import {Message} from '@chatenium/chatenium-sdk/domain/dmService.schema'; import {MessageBoxViewModel} from '../elements/message-box/message-box-viewmodel'; +import {WebSocketHandler} from '@chatenium/chatenium-sdk/core/webSocketHandler'; +import {FileData, FileUploadProgressListener} from '@chatenium/chatenium-sdk/domain/fileUploadService.schema'; +import {Attachment} from '@chatenium/chatenium-sdk/domain/common.schema'; @Component({ selector: 'app-dm', @@ -36,12 +39,56 @@ export class Dm implements OnInit { return this.serviceManager.dmServices()[this.chatid] } - async sendMessage(message: string) { - await this.store.service.sendMessage(message) + async sendMessage(message: string, files: FileDataWithPreview[] | null) { + const session = this.serviceManager.currentSession(); + if (session != null) { + await this.store.service.sendMessage(message, null, null, files, { + fileProgressUpdate: (fileId, allChunks, chunksDone) => { + this.uploadProgressUpdate(fileId, allChunks, chunksDone) + } + }) + + let attachments: Attachment[] = [] + files?.forEach(file => { + attachments.push({ + fileName: file.name, + fileId: file.fileId, + type: file.type, + format: file.extension, + path: file.preview, + height: file.height, + width: file.width, + }) + }) + + this.store.messages.update(value => [...value, { + message: message, + chatid: this.chatid, + files: attachments, + replyTo: "", + author: session.userData.userid, + seen: false, + msgid: "", + forwardedFrom: "", + isEdited: false, + sent_at: {T: 0, I: 0}, + replyToId: "", + forwardedFromName: "" + }]) + } + } + + uploadProgressUpdate(fileId: string, allChunks: number, chunksDone: number) { + console.log(fileId, allChunks, chunksDone) } onWsListen(action: string, message: string) { console.log(action, message) + switch (action) { + case "newMessage": { + this.store.messages.update(messages => [...messages, JSON.parse(message)]) + } + } } ngOnInit() { @@ -67,12 +114,13 @@ export class Dm implements OnInit { chatData: signal(chatData), messages: signal([]), messageBox: new MessageBoxViewModel( - (msg) => this.sendMessage(msg), + (msg, files) => this.sendMessage(msg, files), ) } } this.store.messages.set(await this.serviceManager.dmServices()[chatid].service.get()) + console.log(WebSocketHandler.getInstance().connId) await this.store.service.joinWebSocketRoom() }) } diff --git a/src/app/chat/elements/masonry/masonry.html b/src/app/chat/elements/masonry/masonry.html new file mode 100644 index 0000000..6dbc743 --- /dev/null +++ b/src/app/chat/elements/masonry/masonry.html @@ -0,0 +1 @@ + diff --git a/src/app/chat/elements/masonry/masonry.scss b/src/app/chat/elements/masonry/masonry.scss new file mode 100644 index 0000000..748c46f --- /dev/null +++ b/src/app/chat/elements/masonry/masonry.scss @@ -0,0 +1,12 @@ +:host { + display: grid; + gap: 4px; + height: 100%; + + img { + display: block; + height: 100%; + width: 100%; + object-fit: cover; + } +} diff --git a/src/app/chat/elements/masonry/masonry.spec.ts b/src/app/chat/elements/masonry/masonry.spec.ts new file mode 100644 index 0000000..4c1d765 --- /dev/null +++ b/src/app/chat/elements/masonry/masonry.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Masonry } from './masonry'; + +describe('Masonry', () => { + let component: Masonry; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Masonry], + }).compileComponents(); + + fixture = TestBed.createComponent(Masonry); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/chat/elements/masonry/masonry.ts b/src/app/chat/elements/masonry/masonry.ts new file mode 100644 index 0000000..392413e --- /dev/null +++ b/src/app/chat/elements/masonry/masonry.ts @@ -0,0 +1,28 @@ +import {AfterViewChecked, AfterViewInit, Component, ElementRef, inject, input, ViewChild} from '@angular/core'; + +@Component({ + selector: 'masonry', + imports: [], + templateUrl: './masonry.html', + styleUrl: './masonry.scss', +}) +export class Masonry implements AfterViewInit { + host = inject(ElementRef) + + maxColSize = input(4) + + ngAfterViewInit() { + const elements = this.host.nativeElement.children.length + switch (elements) { + case 1: + this.host.nativeElement.style.gridTemplateColumns = "1fr"; + break + case 3: + this.host.nativeElement.style.gridTemplateColumns = "repeat(2, 1fr)"; + this.host.nativeElement.children[2].style.gridColumn = "1 / -1"; + break + default: + this.host.nativeElement.style.gridTemplateColumns = `repeat(${this.maxColSize()}, 1fr)`; + } + } +} diff --git a/src/app/chat/elements/message-box/message-box-viewmodel.ts b/src/app/chat/elements/message-box/message-box-viewmodel.ts index 5a0bc1f..736d715 100644 --- a/src/app/chat/elements/message-box/message-box-viewmodel.ts +++ b/src/app/chat/elements/message-box/message-box-viewmodel.ts @@ -1,11 +1,21 @@ import {signal} from '@angular/core'; +import {FileData} from '@chatenium/chatenium-sdk/domain/fileUploadService.schema'; +import {FileDataWithPreview} from './message-box'; export class MessageBoxViewModel { - onMessageSend: (message: string) => void + onMessageSend: (message: string, files: FileDataWithPreview[] | null) => void - constructor(onMessageSend: (message: string) => void) { + constructor(onMessageSend: (message: string, files: FileDataWithPreview[] | null) => void) { this.onMessageSend = onMessageSend } message = signal("") + files = signal([]) + + get dialogOpen() { + return this.files().length != 0 + } + set dialogOpen(value: boolean) { + this.files.set([]) + } } diff --git a/src/app/chat/elements/message-box/message-box.html b/src/app/chat/elements/message-box/message-box.html index 0bc7e31..f8ed844 100644 --- a/src/app/chat/elements/message-box/message-box.html +++ b/src/app/chat/elements/message-box/message-box.html @@ -13,10 +13,35 @@ + + + @for (file of viewModel().files(); track file) { + @if (file.type == "image") { + + } + } + + +
+ + + + + + +
+
+
-
-
diff --git a/src/app/chat/elements/message-box/message-box.ts b/src/app/chat/elements/message-box/message-box.ts index 36c35d3..41339dd 100644 --- a/src/app/chat/elements/message-box/message-box.ts +++ b/src/app/chat/elements/message-box/message-box.ts @@ -1,9 +1,22 @@ import {Component, HostListener, inject, input} from '@angular/core'; -import {TuiAppearance, TuiButton, TuiGroup, TuiIcon, TuiScrollbarDirective} from '@taiga-ui/core'; +import { + TuiAppearance, + TuiButton, + TuiDialog, + TuiGroup, + TuiIcon, + TuiScrollbarDirective, + TuiTextfield +} from '@taiga-ui/core'; import {TranslatePipe} from '@ngx-translate/core'; import {ServiceManager} from '../../../service-manager'; import {MessageBoxViewModel} from './message-box-viewmodel'; import {FormsModule} from '@angular/forms'; +import {v4 as uuidv4} from 'uuid'; +import {Masonry} from '../masonry/masonry'; +import {Oimg} from '../oimg/oimg'; +import {FileData} from '@chatenium/chatenium-sdk/domain/fileUploadService.schema'; +import {TuiTextarea, TuiTextareaComponent} from '@taiga-ui/kit'; @Component({ selector: 'message-box', @@ -14,7 +27,12 @@ import {FormsModule} from '@angular/forms'; TuiGroup, TuiIcon, TuiButton, - FormsModule + FormsModule, + TuiDialog, + Oimg, + Masonry, + TuiTextfield, + TuiTextarea ], templateUrl: './message-box.html', styleUrl: './message-box.scss', @@ -56,4 +74,82 @@ export class MessageBox { this.textareaHeight = calculatedHeight > 180 ? 180 : calculatedHeight this.messageBoxRadius = calculatedRadius < 30 ? 30 : calculatedRadius } + + handleFileInput(event: any) { + this.processFiles(event.target.files) + } + + sendMessageWithCaption(e: TuiTextareaComponent) { + this.viewModel().onMessageSend((e.content() as string), this.viewModel().files()) + } + + async processFiles(fileList: FileList) { + for (let i = 0; i < fileList.length; i++) { + const file = fileList[i]; + const type = file.type.split("/").shift() ?? "" + let preview = "" + let height = 0 + let width = 0 + + console.log(type) + + if (type == "image") { + try { + const imgData = await makePicturePreview(file) + console.log(imgData) + preview = imgData.preview + height = imgData.height + width = imgData.width + } catch (error) { + console.error(error) + } + } + + console.log("push") + this.viewModel().files().push({ + fileId: uuidv4(), + data: file, + name: file.name, + type: type, + extension: file.name.split(".").pop() ?? "", + preview: preview, + height: height, + width: width, + }) + console.log(this.viewModel().files()) + } + + function makePicturePreview(file: File): Promise<{ preview: string, height: number, width: number }> { + return new Promise((resolve, reject) => { + const img = new Image(); + const objectUrl = URL.createObjectURL(file); + + img.onload = () => { + console.log("Loaded image") + resolve({ + preview: objectUrl, + width: img.naturalWidth, + height: img.naturalHeight, + }); + }; + + img.onerror = (err) => { + URL.revokeObjectURL(objectUrl); + console.error("Error loading image:", err); + reject(err); + }; + + img.src = objectUrl; + }); + } + } +} + +/** + * All the extra data for client-side rendering. (With finished messages these are generated on the server) + */ +export interface FileDataWithPreview extends FileData { + preview: string; + height: number; + width: number; } diff --git a/src/app/chat/elements/messages/messages.html b/src/app/chat/elements/messages/messages.html index c951ff0..15dd1e5 100644 --- a/src/app/chat/elements/messages/messages.html +++ b/src/app/chat/elements/messages/messages.html @@ -12,6 +12,11 @@
{{message.message}} + @for (file of message.files; track file) { + @if (file.type == "image") { + + } + }