Added attachment support
This commit is contained in:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()"/>
|
||||||
|
|||||||
@@ -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()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/app/chat/elements/masonry/masonry.html
Normal file
1
src/app/chat/elements/masonry/masonry.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<ng-content></ng-content>
|
||||||
12
src/app/chat/elements/masonry/masonry.scss
Normal file
12
src/app/chat/elements/masonry/masonry.scss
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
:host {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/app/chat/elements/masonry/masonry.spec.ts
Normal file
22
src/app/chat/elements/masonry/masonry.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
28
src/app/chat/elements/masonry/masonry.ts
Normal file
28
src/app/chat/elements/masonry/masonry.ts
Normal 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)`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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([])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user