From 9baab3d3bc39f12e3b67213917aa181e3fde2cf2 Mon Sep 17 00:00:00 2001 From: chatenium Date: Thu, 9 Apr 2026 11:23:26 +0200 Subject: [PATCH] Update --- package-lock.json | 8 +- package.json | 2 +- public/i18n/en.json | 9 ++ src/app/chat/chat.scss | 2 +- src/app/chat/chat.ts | 5 +- src/app/chat/dm/dm.html | 17 ++- src/app/chat/dm/dm.scss | 2 +- src/app/chat/dm/dm.ts | 33 +++++- .../message-box/message-box-viewmodel.ts | 11 ++ .../elements/message-box/message-box.html | 39 ++++++- .../elements/message-box/message-box.scss | 98 +++++++++++++++- .../chat/elements/message-box/message-box.ts | 55 ++++++++- src/app/chat/elements/messages/messages.html | 19 +++ src/app/chat/elements/messages/messages.scss | 69 +++++++++++ .../chat/elements/messages/messages.spec.ts | 22 ++++ src/app/chat/elements/messages/messages.ts | 109 ++++++++++++++++++ src/app/service-manager.ts | 8 +- src/styles.scss | 18 +++ 18 files changed, 489 insertions(+), 37 deletions(-) create mode 100644 src/app/chat/elements/message-box/message-box-viewmodel.ts create mode 100644 src/app/chat/elements/messages/messages.html create mode 100644 src/app/chat/elements/messages/messages.scss create mode 100644 src/app/chat/elements/messages/messages.spec.ts create mode 100644 src/app/chat/elements/messages/messages.ts diff --git a/package-lock.json b/package-lock.json index fd9d28b..a8c20d7 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.6", + "@chatenium/chatenium-sdk": "^1.0.8", "@ngx-translate/core": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0", "@taiga-ui/addon-charts": "^5.1.0", @@ -987,9 +987,9 @@ } }, "node_modules/@chatenium/chatenium-sdk": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.0.6.tgz", - "integrity": "sha512-lNrgIiXJWYc7KiQCtzWqgLQ0RpEoRWU2Z/GVEjcMyMlCMCX6Lu0g0cKj/8mREhk7kbJUAEcHnB18w5VjNicbIg==", + "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==", "dependencies": { "@faker-js/faker": "^10.4.0", "axios": "^1.14.0", diff --git a/package.json b/package.json index 43bf9d8..ef1bf9a 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.6", + "@chatenium/chatenium-sdk": "^1.0.8", "@ngx-translate/core": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0", "@taiga-ui/addon-charts": "^5.1.0", diff --git a/public/i18n/en.json b/public/i18n/en.json index 293fdee..0862b94 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -27,6 +27,15 @@ } } } + }, + "elements": { + "messageBox": { + "placeholder": "Type a message...", + "uplDrag": { + "upload": "Drop here to upload", + "transfer": "Drop here to transfer" + } + } } } } diff --git a/src/app/chat/chat.scss b/src/app/chat/chat.scss index 9ef4735..5d51542 100644 --- a/src/app/chat/chat.scss +++ b/src/app/chat/chat.scss @@ -2,6 +2,7 @@ display: grid; grid-template-columns: 350px minmax(0, 1fr); height: 100svh; + overflow: hidden; #chatnav { display: grid; @@ -36,7 +37,6 @@ } main { - padding-top: 65px; background: var(--tui-background-base-alt); border-radius: 20px 0 0 20px; margin: 10px 0 10px 10px; diff --git a/src/app/chat/chat.ts b/src/app/chat/chat.ts index 275f7e5..e598344 100644 --- a/src/app/chat/chat.ts +++ b/src/app/chat/chat.ts @@ -7,6 +7,7 @@ import {LoadStatus, ServiceManager} from '../service-manager'; import {IndexedDB} from '../storage/indexed-db'; import {DmList} from './dm-list/dm-list'; import {JsonPipe} from '@angular/common'; +import {WebSocketHandler} from '@chatenium/chatenium-sdk/core/webSocketHandler'; @Component({ selector: 'app-chat', @@ -30,7 +31,9 @@ export class Chat implements OnInit { async ngOnInit() { this.indexedDb.openDatabase().then(async () => { - this.serviceManager.currentSession.set(await this.serviceManager.sessionManager.loadPreferredSession()) + const session = await this.serviceManager.sessionManager.loadPreferredSession() + this.serviceManager.currentSession.set(session) + await WebSocketHandler.getInstance().connect(session.userData.userid, session.token) }) } diff --git a/src/app/chat/dm/dm.html b/src/app/chat/dm/dm.html index 8745cd8..347f563 100644 --- a/src/app/chat/dm/dm.html +++ b/src/app/chat/dm/dm.html @@ -1,13 +1,13 @@ @defer (when store) {
- +
- @if (store.chatData.displayName == "") { - {{'@'+store.chatData.username}} + @if (store.chatData().displayName == "") { + {{'@'+store.chatData().username}} } @else { - {{store.chatData.displayName}} - {{'@'+store.chatData.username}} + {{store.chatData().displayName}} + {{'@'+store.chatData().username}} }
@@ -17,11 +17,10 @@ + {{chatid}}
-
+ -
- - + } diff --git a/src/app/chat/dm/dm.scss b/src/app/chat/dm/dm.scss index 970c1fd..d01fc96 100644 --- a/src/app/chat/dm/dm.scss +++ b/src/app/chat/dm/dm.scss @@ -1,5 +1,5 @@ :host { - height: 100%; + height: 95svh; display: grid; grid-template-rows: 70px minmax(0, 1fr) auto; diff --git a/src/app/chat/dm/dm.ts b/src/app/chat/dm/dm.ts index a852efa..bb72ada 100644 --- a/src/app/chat/dm/dm.ts +++ b/src/app/chat/dm/dm.ts @@ -1,4 +1,4 @@ -import {Component, inject, OnInit} from '@angular/core'; +import {Component, inject, OnInit, signal} from '@angular/core'; import {DmStorage, ServiceManager} from '../../service-manager'; import {ActivatedRoute} from '@angular/router'; import {DMService} from '@chatenium/chatenium-sdk/services/dmService'; @@ -7,6 +7,10 @@ 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 {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'; @Component({ selector: 'app-dm', @@ -15,7 +19,8 @@ import {MessageBox} from '../elements/message-box/message-box'; Oimg, TuiButton, TuiIcon, - MessageBox + MessageBox, + Messages ], templateUrl: './dm.html', styleUrl: './dm.scss', @@ -31,14 +36,23 @@ export class Dm implements OnInit { return this.serviceManager.dmServices()[this.chatid] } + async sendMessage(message: string) { + await this.store.service.sendMessage(message) + } + + onWsListen(action: string, message: string) { + console.log(action, message) + } + ngOnInit() { - this.route.params.subscribe(params => { + this.route.params.subscribe(async params => { const chatid = params['chatid'] this.chatid = chatid console.log(this.serviceManager.chats()) const session = this.serviceManager.currentSession(); const chatData = this.serviceManager.chats().find(chat => chat.chatid == chatid) + // Setup storage if (!this.serviceManager.dmServices()[chatid] && session != null && chatData != null) { this.serviceManager.dmServices()[chatid] = { service: new DMService( @@ -46,11 +60,20 @@ export class Dm implements OnInit { session.token, chatid, this.indexedDb.getApi(), - () => {} + (action, data) => { + this.onWsListen(action, data) + } ), - chatData: chatData + chatData: signal(chatData), + messages: signal([]), + messageBox: new MessageBoxViewModel( + (msg) => this.sendMessage(msg), + ) } } + + this.store.messages.set(await this.serviceManager.dmServices()[chatid].service.get()) + await this.store.service.joinWebSocketRoom() }) } } diff --git a/src/app/chat/elements/message-box/message-box-viewmodel.ts b/src/app/chat/elements/message-box/message-box-viewmodel.ts new file mode 100644 index 0000000..5a0bc1f --- /dev/null +++ b/src/app/chat/elements/message-box/message-box-viewmodel.ts @@ -0,0 +1,11 @@ +import {signal} from '@angular/core'; + +export class MessageBoxViewModel { + onMessageSend: (message: string) => void + + constructor(onMessageSend: (message: string) => void) { + this.onMessageSend = onMessageSend + } + + message = signal("") +} diff --git a/src/app/chat/elements/message-box/message-box.html b/src/app/chat/elements/message-box/message-box.html index 9dbb25a..0bc7e31 100644 --- a/src/app/chat/elements/message-box/message-box.html +++ b/src/app/chat/elements/message-box/message-box.html @@ -1,7 +1,36 @@ -
-
-
- +
+
+
+ +
+ {{ "chat.elements.messageBox.uplDrag.upload"|translate }} +
+
+
+ +
+ {{ "chat.elements.messageBox.uplDrag.transfer"|translate }} +
+
+ +
+
+ + + +
+
+ + {{ "chat.elements.messageBox.placeholder"|translate }} +
+
+
-
diff --git a/src/app/chat/elements/message-box/message-box.scss b/src/app/chat/elements/message-box/message-box.scss index 94b515d..081800f 100644 --- a/src/app/chat/elements/message-box/message-box.scss +++ b/src/app/chat/elements/message-box/message-box.scss @@ -3,27 +3,117 @@ display: flex; justify-content: center; + #dragOverlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.2s ease-in-out; + + &.show { + opacity: 1; + pointer-events: all; + } + + .method { + width: 800px; + height: 200px; + background: var(--tui-background-accent-1-hover); + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: 15px; + + .icon-holder { + border-radius: 50%; + background: var(--tui-background-accent-1); + padding: 20px; + + tui-icon { + font-size: 50px; + } + } + + span { + font-size: 20px; + font-weight: 600; + } + } + } + #message-box { + transition: all 0.2s ease-in-out; width: 60%; background: var(--tui-background-base-alt); - height: 75px; - border-radius: 200px; + min-height: 75px; + max-height: 200px; border: 2px solid var(--tui-border-normal); display: grid; - grid-template-columns: 10% 80% 10%; + grid-template-columns: 85px 1fr 100px; align-items: center; padding: 0 10px; + .items-left, .items-middle, .items-right { + display: flex; + align-items: center; + } + + .items-left, .items-right { + button { + height: 30px; + width: 30px; + + tui-icon { + font-size: 20px; + } + } + } + .items-middle { + ::-webkit-scrollbar-thumb { + background: transparent; + border-radius: 10px; + } + + position: relative; + textarea { width: 100%; - height: 100%; background: transparent; border: none; outline: none; resize: none; color: var(--tui-text-01); + font-size: 16px; + z-index: 1; } + + .placeholder { + position: absolute; + font-size: 16px; + height: 25px; + top: 2px; + left: 2px; + color: gray; + transition: all 0.2s ease-in-out; + + &.hidden { + margin-left: 10px; + opacity: 0; + } + } + } + + .items-right { + display: flex; + justify-content: end; } } } diff --git a/src/app/chat/elements/message-box/message-box.ts b/src/app/chat/elements/message-box/message-box.ts index f4d17a4..36c35d3 100644 --- a/src/app/chat/elements/message-box/message-box.ts +++ b/src/app/chat/elements/message-box/message-box.ts @@ -1,12 +1,59 @@ -import { Component } from '@angular/core'; -import {TuiAppearance} from '@taiga-ui/core'; +import {Component, HostListener, inject, input} from '@angular/core'; +import {TuiAppearance, TuiButton, TuiGroup, TuiIcon, TuiScrollbarDirective} 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'; @Component({ selector: 'message-box', imports: [ - TuiAppearance + TuiAppearance, + TranslatePipe, + TuiScrollbarDirective, + TuiGroup, + TuiIcon, + TuiButton, + FormsModule ], templateUrl: './message-box.html', styleUrl: './message-box.scss', }) -export class MessageBox {} +export class MessageBox { + viewModel = input.required() + + textareaHeight = 25 + messageBoxRadius = 200 + + private dragCounter = 0; + isDraggingOverWindow = false; + + @HostListener('window:dragenter', ['$event']) + onDragEnter(event: DragEvent) { + event.preventDefault(); + this.dragCounter++; + + if (this.dragCounter === 1) { + this.isDraggingOverWindow = true; + console.log("Drag entered the window area"); + } + } + + @HostListener('window:dragleave', ['$event']) + onDragLeave(event: DragEvent) { + event.preventDefault(); + this.dragCounter--; + + if (this.dragCounter === 0) { + this.isDraggingOverWindow = false; + console.log("Drag left the window completely"); + } + } + + onTextAreaInput(e: HTMLTextAreaElement) { + const calculatedHeight = e.value.split(/\r?\n/).length * 25 + const calculatedRadius = (200 - this.textareaHeight) + this.textareaHeight = calculatedHeight > 180 ? 180 : calculatedHeight + this.messageBoxRadius = calculatedRadius < 30 ? 30 : calculatedRadius + } +} diff --git a/src/app/chat/elements/messages/messages.html b/src/app/chat/elements/messages/messages.html new file mode 100644 index 0000000..c951ff0 --- /dev/null +++ b/src/app/chat/elements/messages/messages.html @@ -0,0 +1,19 @@ +@if (serviceManager.currentSession != null) { + @for (message of messages(); track message.msgid; let i = $index) { +
+
+ {{message.sent_at.T * 1000 | date: 'HH:mm'}} +
+
+ {{message.message}} +
+
+
+ } +} diff --git a/src/app/chat/elements/messages/messages.scss b/src/app/chat/elements/messages/messages.scss new file mode 100644 index 0000000..e05c85d --- /dev/null +++ b/src/app/chat/elements/messages/messages.scss @@ -0,0 +1,69 @@ +:host { + padding: 10px; + display: flex; + flex-direction: column; + gap: 2px; + overflow-y: scroll; + + .message { + display: flex; + flex-direction: column; + + &.author { + align-items: end; + + .bubble { + background: var(--tui-background-accent-1-hover); + } + } + + &.chained_start { + margin-top: 10px; + + .above, .below { + display: block; + } + + .above { + padding: 2px; + } + + .bubble { + border-radius: 25px 10px 10px 10px !important; + } + } + + &.chained_middle { + .bubble { + border-radius: 10px !important; + } + } + + &.chained_end { + .bubble { + border-radius: 10px 10px 10px 25px !important; + } + } + + .above, .below { + color: gray; + opacity: 70%; + display: none; + } + + .bubble { + background: var(--tui-background-neutral-2); + max-width: 350px; + min-width: 250px; + min-height: 40px; + display: flex; + align-items: center; + padding: 5px; + + .message-text { + white-space: none; + overflow-wrap: anywhere; + } + } + } +} diff --git a/src/app/chat/elements/messages/messages.spec.ts b/src/app/chat/elements/messages/messages.spec.ts new file mode 100644 index 0000000..0a6bedb --- /dev/null +++ b/src/app/chat/elements/messages/messages.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Messages } from './messages'; + +describe('Messages', () => { + let component: Messages; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Messages], + }).compileComponents(); + + fixture = TestBed.createComponent(Messages); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/chat/elements/messages/messages.ts b/src/app/chat/elements/messages/messages.ts new file mode 100644 index 0000000..ee2c7e7 --- /dev/null +++ b/src/app/chat/elements/messages/messages.ts @@ -0,0 +1,109 @@ +import {Component, inject, input} from '@angular/core'; +import {Message} from '@chatenium/chatenium-sdk/domain/dmService.schema'; +import {Message as NetworkMessage} from '@chatenium/chatenium-sdk/domain/textChannelService.schema' +import {ServiceManager} from '../../../service-manager'; +import {DatePipe} from '@angular/common'; + +@Component({ + selector: 'messages', + imports: [ + DatePipe + ], + templateUrl: './messages.html', + styleUrl: './messages.scss', +}) +export class Messages { + serviceManager = inject(ServiceManager) + + messages = input([]) + + /** + * Helps code readability by specifying what type of messages are being processed. + * Example: messageAsDm(message).author == userid -- We are specifying that we are handling dm messages + * @param message + */ + messageAsDm(message: Message | NetworkMessage) { + return message == undefined ? {author: "no"} : message as Message + } + + /** + * Helps code readability by specifying what type of messages are being processed + Example: messageAsNetwork(message).author.userid == userid -- We are specifying that we are handling network messages + * @param message + */ + messageAsNetwork(message: Message | NetworkMessage) { + return message == undefined ? {author: {userid: "no"}} : message as NetworkMessage + } + + isMessageStartOfChain(i: number) { + const message = this.messages()[i]; + if (!message) return false; + + const prev = this.messages()[i - 1]; + + // is author == last msg's author + if (prev) { + if ( + (typeof message.author === 'string' && this.messageAsDm(prev).author !== this.messageAsDm(message).author) && + this.messageAsNetwork(prev).author.userid !== this.messageAsNetwork(message).author.userid + ) { + return true; + } + + // same author but time gap between this and prev msg + if ( + Math.abs(message.sent_at.T - prev.sent_at.T) >= 60 + ) { + return true; + } + } + + return i === 0; + } + + isMessageMiddleInChain(i: number) { + const message = this.messages()[i]; + if (!message || i == 0) return false; + + const prev = this.messages()[i - 1]; + + // message must be from the same author and have little time gap between + return ( + ( + (typeof message.author === 'string' && this.messageAsDm(prev).author == this.messageAsDm(message).author) || + this.messageAsNetwork(prev).author.userid == this.messageAsNetwork(message).author.userid + ) && + Math.abs(message.sent_at.T - prev.sent_at.T) <= 60 + ); + } + + isMessageEndOfChain(i: number) { + // prevent false positive + if (this.isMessageStartOfChain(i)) return false; + + const message = this.messages()[i]; + if (!message) return false; + + const next = this.messages()[i + 1]; + + // First condition: next author is different + if (next) { + if ( + (typeof message.author === 'string' && this.messageAsDm(next).author != this.messageAsDm(message).author) || + this.messageAsNetwork(next).author.userid != this.messageAsNetwork(message).author.userid + ) { + return true; + } + + // same author but time gap between this and next msg + if ( + Math.abs(message.sent_at.T - next.sent_at.T) >= 60 + ) { + return true; + } + } + + // last message is always end + return i + 1 === this.messages().length; + } +} diff --git a/src/app/service-manager.ts b/src/app/service-manager.ts index 63cfd8a..b7028b0 100644 --- a/src/app/service-manager.ts +++ b/src/app/service-manager.ts @@ -1,4 +1,4 @@ -import {inject, Injectable, signal} from '@angular/core'; +import {inject, Injectable, Signal, signal, WritableSignal} from '@angular/core'; import {IndexedDB} from './storage/indexed-db'; import {Keyring} from './storage/keyring'; import {KeyValue} from './storage/key-value'; @@ -7,6 +7,8 @@ import {Session} from '@chatenium/chatenium-sdk/domain/sessionManager.schema'; import {ChatService} from '@chatenium/chatenium-sdk/services/chatService'; import {Chat} from '@chatenium/chatenium-sdk/domain/chatService.schema'; import {DMService} from '@chatenium/chatenium-sdk/services/dmService'; +import {Message} from '@chatenium/chatenium-sdk/domain/dmService.schema'; +import {MessageBoxViewModel} from './chat/elements/message-box/message-box-viewmodel'; @Injectable({ providedIn: 'root', @@ -35,5 +37,7 @@ export enum LoadStatus { export interface DmStorage { service: DMService - chatData: Chat + messages: WritableSignal + chatData: WritableSignal + messageBox: MessageBoxViewModel } diff --git a/src/styles.scss b/src/styles.scss index bb60e14..45b8f7b 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -1,4 +1,22 @@ /* You can add global styles to this file, and also import other style files */ + +.drag-active * { + pointer-events: none; +} + +::-webkit-scrollbar { + width: 5px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--tui-background-neutral-2-pressed); + border-radius: 10px; +} + body { margin: 0; padding: 0;