Added attachment support

This commit is contained in:
2026-04-09 13:41:04 +02:00
parent 9baab3d3bc
commit 97f7712d55
13 changed files with 270 additions and 18 deletions

11
package-lock.json generated
View File

@@ -15,7 +15,7 @@
"@angular/forms": "^21.2.0", "@angular/forms": "^21.2.0",
"@angular/platform-browser": "^21.2.0", "@angular/platform-browser": "^21.2.0",
"@angular/router": "^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/core": "^17.0.0",
"@ngx-translate/http-loader": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0",
"@taiga-ui/addon-charts": "^5.1.0", "@taiga-ui/addon-charts": "^5.1.0",
@@ -28,7 +28,8 @@
"@taiga-ui/layout": "^5.1.0", "@taiga-ui/layout": "^5.1.0",
"ngx-cookie-service": "^21.3.1", "ngx-cookie-service": "^21.3.1",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0" "tslib": "^2.3.0",
"uuid": "^13.0.0"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^21.2.6", "@angular/build": "^21.2.6",
@@ -987,9 +988,9 @@
} }
}, },
"node_modules/@chatenium/chatenium-sdk": { "node_modules/@chatenium/chatenium-sdk": {
"version": "1.0.8", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.0.8.tgz", "resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.0.10.tgz",
"integrity": "sha512-avJ61UPEk6GQ6+0fbA9Tl2VZU7O7RE/evAXIsTxA0S0oQzltWFA4k0ttPCPw8LLEpNNQCoQk/2dd0l2+y75t4Q==", "integrity": "sha512-AhWtM4bD3p1nXW/tC/eiBlPGesk/vjwjf1CPuScYaD2OwmXLGZXNZPXe3g6T5dRAA1Zyo18QkWEJXglA+6VZSQ==",
"dependencies": { "dependencies": {
"@faker-js/faker": "^10.4.0", "@faker-js/faker": "^10.4.0",
"axios": "^1.14.0", "axios": "^1.14.0",

View File

@@ -18,7 +18,7 @@
"@angular/forms": "^21.2.0", "@angular/forms": "^21.2.0",
"@angular/platform-browser": "^21.2.0", "@angular/platform-browser": "^21.2.0",
"@angular/router": "^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/core": "^17.0.0",
"@ngx-translate/http-loader": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0",
"@taiga-ui/addon-charts": "^5.1.0", "@taiga-ui/addon-charts": "^5.1.0",
@@ -31,7 +31,8 @@
"@taiga-ui/layout": "^5.1.0", "@taiga-ui/layout": "^5.1.0",
"ngx-cookie-service": "^21.3.1", "ngx-cookie-service": "^21.3.1",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
"tslib": "^2.3.0" "tslib": "^2.3.0",
"uuid": "^13.0.0"
}, },
"devDependencies": { "devDependencies": {
"@angular/build": "^21.2.6", "@angular/build": "^21.2.6",

View File

@@ -31,9 +31,13 @@
"elements": { "elements": {
"messageBox": { "messageBox": {
"placeholder": "Type a message...", "placeholder": "Type a message...",
"message": "Message",
"uplDrag": { "uplDrag": {
"upload": "Drop here to upload", "upload": "Drop here to upload",
"transfer": "Drop here to transfer" "transfer": "Drop here to transfer"
},
"fileUploadDialog": {
"label": "Upload files"
} }
} }
} }

View File

@@ -17,7 +17,6 @@
<tui-icon icon="@tui.phone"/> <tui-icon icon="@tui.phone"/>
</button> </button>
</div> </div>
{{chatid}}
</navbar> </navbar>
<messages [messages]="store.messages()"/> <messages [messages]="store.messages()"/>

View File

@@ -6,11 +6,14 @@ import {IndexedDB} from '../../storage/indexed-db';
import {Navbar} from '../elements/navbar/navbar'; import {Navbar} from '../elements/navbar/navbar';
import {Oimg} from '../elements/oimg/oimg'; import {Oimg} from '../elements/oimg/oimg';
import {TuiButton, TuiIcon} from '@taiga-ui/core'; 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 {Messages} from '../elements/messages/messages';
import {Chat} from '@chatenium/chatenium-sdk/domain/chatService.schema'; import {Chat} from '@chatenium/chatenium-sdk/domain/chatService.schema';
import {Message} from '@chatenium/chatenium-sdk/domain/dmService.schema'; import {Message} from '@chatenium/chatenium-sdk/domain/dmService.schema';
import {MessageBoxViewModel} from '../elements/message-box/message-box-viewmodel'; 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({ @Component({
selector: 'app-dm', selector: 'app-dm',
@@ -36,12 +39,56 @@ export class Dm implements OnInit {
return this.serviceManager.dmServices()[this.chatid] return this.serviceManager.dmServices()[this.chatid]
} }
async sendMessage(message: string) { async sendMessage(message: string, files: FileDataWithPreview[] | null) {
await this.store.service.sendMessage(message) const session = this.serviceManager.currentSession();
if (session != null) {
await this.store.service.sendMessage(message, null, null, files, <FileUploadProgressListener>{
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) { onWsListen(action: string, message: string) {
console.log(action, message) console.log(action, message)
switch (action) {
case "newMessage": {
this.store.messages.update(messages => [...messages, JSON.parse(message)])
}
}
} }
ngOnInit() { ngOnInit() {
@@ -67,12 +114,13 @@ export class Dm implements OnInit {
chatData: signal<Chat>(chatData), chatData: signal<Chat>(chatData),
messages: signal<Message[]>([]), messages: signal<Message[]>([]),
messageBox: new MessageBoxViewModel( messageBox: new MessageBoxViewModel(
(msg) => this.sendMessage(msg), (msg, files) => this.sendMessage(msg, files),
) )
} }
} }
this.store.messages.set(await this.serviceManager.dmServices()[chatid].service.get()) this.store.messages.set(await this.serviceManager.dmServices()[chatid].service.get())
console.log(WebSocketHandler.getInstance().connId)
await this.store.service.joinWebSocketRoom() await this.store.service.joinWebSocketRoom()
}) })
} }

View File

@@ -0,0 +1 @@
<ng-content></ng-content>

View File

@@ -0,0 +1,12 @@
:host {
display: grid;
gap: 4px;
height: 100%;
img {
display: block;
height: 100%;
width: 100%;
object-fit: cover;
}
}

View File

@@ -0,0 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Masonry } from './masonry';
describe('Masonry', () => {
let component: Masonry;
let fixture: ComponentFixture<Masonry>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Masonry],
}).compileComponents();
fixture = TestBed.createComponent(Masonry);
component = fixture.componentInstance;
await fixture.whenStable();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -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<number>(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)`;
}
}
}

View File

@@ -1,11 +1,21 @@
import {signal} from '@angular/core'; import {signal} from '@angular/core';
import {FileData} from '@chatenium/chatenium-sdk/domain/fileUploadService.schema';
import {FileDataWithPreview} from './message-box';
export class MessageBoxViewModel { 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 this.onMessageSend = onMessageSend
} }
message = signal<string>("") message = signal<string>("")
files = signal<FileDataWithPreview[]>([])
get dialogOpen() {
return this.files().length != 0
}
set dialogOpen(value: boolean) {
this.files.set([])
}
} }

View File

@@ -13,10 +13,35 @@
</div> </div>
</div> </div>
<ng-template [(tuiDialog)]="viewModel().dialogOpen" [tuiDialogOptions]="{label: 'chat.elements.messageBox.fileUploadDialog.label'|translate}">
<masonry [maxColSize]="2" style="overflow-y: scroll; height: 500px">
@for (file of viewModel().files(); track file) {
@if (file.type == "image") {
<img [src]="file.preview" style="width: 100%; height: 100%; object-fit: fill;"/>
}
}
</masonry>
<div style="margin-top: 10px; display: flex; gap: 10px; width: 100%">
<tui-textfield style="width: 100%">
<label tuiLabel>{{"chat.elements.messageBox.placeholder"|translate}}</label>
<textarea
#caption
placeholder="{{ 'chat.elements.messageBox.message'|translate }}"
[(ngModel)]="viewModel().message"
tuiTextarea
></textarea>
</tui-textfield>
<button tuiButton iconStart="@tui.send" (click)="sendMessageWithCaption(caption)"></button>
</div>
</ng-template>
<div id="message-box" [style]="'border-radius:'+messageBoxRadius+'px;'"> <div id="message-box" [style]="'border-radius:'+messageBoxRadius+'px;'">
<div class="items-left"> <div class="items-left">
<button tuiButton appearance="flat"> <button tuiButton appearance="flat" (click)="uplInput.click()">
<tui-icon icon="@tui.file-up"/> <tui-icon icon="@tui.file-up"/>
<input #uplInput type="file" (change)="handleFileInput($event)" multiple hidden/>
</button> </button>
<button tuiButton appearance="flat"> <button tuiButton appearance="flat">
@@ -29,7 +54,7 @@
[class.hidden]="message.value != ''">{{ "chat.elements.messageBox.placeholder"|translate }}</span> [class.hidden]="message.value != ''">{{ "chat.elements.messageBox.placeholder"|translate }}</span>
</div> </div>
<div class="items-right"> <div class="items-right">
<button tuiButton appearance="flat" (click)="viewModel().onMessageSend(message.value)"> <button tuiButton appearance="flat" (click)="viewModel().onMessageSend(message.value, null)">
<tui-icon icon="@tui.send"/> <tui-icon icon="@tui.send"/>
</button> </button>
</div> </div>

View File

@@ -1,9 +1,22 @@
import {Component, HostListener, inject, input} from '@angular/core'; 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 {TranslatePipe} from '@ngx-translate/core';
import {ServiceManager} from '../../../service-manager'; import {ServiceManager} from '../../../service-manager';
import {MessageBoxViewModel} from './message-box-viewmodel'; import {MessageBoxViewModel} from './message-box-viewmodel';
import {FormsModule} from '@angular/forms'; 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({ @Component({
selector: 'message-box', selector: 'message-box',
@@ -14,7 +27,12 @@ import {FormsModule} from '@angular/forms';
TuiGroup, TuiGroup,
TuiIcon, TuiIcon,
TuiButton, TuiButton,
FormsModule FormsModule,
TuiDialog,
Oimg,
Masonry,
TuiTextfield,
TuiTextarea
], ],
templateUrl: './message-box.html', templateUrl: './message-box.html',
styleUrl: './message-box.scss', styleUrl: './message-box.scss',
@@ -56,4 +74,82 @@ export class MessageBox {
this.textareaHeight = calculatedHeight > 180 ? 180 : calculatedHeight this.textareaHeight = calculatedHeight > 180 ? 180 : calculatedHeight
this.messageBoxRadius = calculatedRadius < 30 ? 30 : calculatedRadius 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;
} }

View File

@@ -12,6 +12,11 @@
</div> </div>
<div class="bubble"> <div class="bubble">
<span class="message-text">{{message.message}}</span> <span class="message-text">{{message.message}}</span>
@for (file of message.files; track file) {
@if (file.type == "image") {
<img [src]="file.path"/>
}
}
</div> </div>
<div class="below"></div> <div class="below"></div>
</div> </div>