Update
This commit is contained in:
8
package-lock.json
generated
8
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.6",
|
"@chatenium/chatenium-sdk": "^1.0.8",
|
||||||
"@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",
|
||||||
@@ -987,9 +987,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@chatenium/chatenium-sdk": {
|
"node_modules/@chatenium/chatenium-sdk": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@chatenium/chatenium-sdk/-/chatenium-sdk-1.0.8.tgz",
|
||||||
"integrity": "sha512-lNrgIiXJWYc7KiQCtzWqgLQ0RpEoRWU2Z/GVEjcMyMlCMCX6Lu0g0cKj/8mREhk7kbJUAEcHnB18w5VjNicbIg==",
|
"integrity": "sha512-avJ61UPEk6GQ6+0fbA9Tl2VZU7O7RE/evAXIsTxA0S0oQzltWFA4k0ttPCPw8LLEpNNQCoQk/2dd0l2+y75t4Q==",
|
||||||
"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.6",
|
"@chatenium/chatenium-sdk": "^1.0.8",
|
||||||
"@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",
|
||||||
|
|||||||
@@ -27,6 +27,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"elements": {
|
||||||
|
"messageBox": {
|
||||||
|
"placeholder": "Type a message...",
|
||||||
|
"uplDrag": {
|
||||||
|
"upload": "Drop here to upload",
|
||||||
|
"transfer": "Drop here to transfer"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 350px minmax(0, 1fr);
|
grid-template-columns: 350px minmax(0, 1fr);
|
||||||
height: 100svh;
|
height: 100svh;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
#chatnav {
|
#chatnav {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -36,7 +37,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding-top: 65px;
|
|
||||||
background: var(--tui-background-base-alt);
|
background: var(--tui-background-base-alt);
|
||||||
border-radius: 20px 0 0 20px;
|
border-radius: 20px 0 0 20px;
|
||||||
margin: 10px 0 10px 10px;
|
margin: 10px 0 10px 10px;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {LoadStatus, ServiceManager} from '../service-manager';
|
|||||||
import {IndexedDB} from '../storage/indexed-db';
|
import {IndexedDB} from '../storage/indexed-db';
|
||||||
import {DmList} from './dm-list/dm-list';
|
import {DmList} from './dm-list/dm-list';
|
||||||
import {JsonPipe} from '@angular/common';
|
import {JsonPipe} from '@angular/common';
|
||||||
|
import {WebSocketHandler} from '@chatenium/chatenium-sdk/core/webSocketHandler';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-chat',
|
selector: 'app-chat',
|
||||||
@@ -30,7 +31,9 @@ export class Chat implements OnInit {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.indexedDb.openDatabase().then(async () => {
|
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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
@defer (when store) {
|
@defer (when store) {
|
||||||
<navbar>
|
<navbar>
|
||||||
<div class="items-left">
|
<div class="items-left">
|
||||||
<oimg [src]="store.chatData.pfp" height="50px" width="50px" [radius]="15"></oimg>
|
<oimg [src]="store.chatData().pfp" height="50px" width="50px" [radius]="15"></oimg>
|
||||||
<div class="chat-data">
|
<div class="chat-data">
|
||||||
@if (store.chatData.displayName == "") {
|
@if (store.chatData().displayName == "") {
|
||||||
<span class="main-name">{{'@'+store.chatData.username}}</span>
|
<span class="main-name">{{'@'+store.chatData().username}}</span>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="main-name">{{store.chatData.displayName}}</span>
|
<span class="main-name">{{store.chatData().displayName}}</span>
|
||||||
<span class="alt-name">{{'@'+store.chatData.username}}</span>
|
<span class="alt-name">{{'@'+store.chatData().username}}</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,11 +17,10 @@
|
|||||||
<tui-icon icon="@tui.phone"/>
|
<tui-icon icon="@tui.phone"/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{{chatid}}
|
||||||
</navbar>
|
</navbar>
|
||||||
|
|
||||||
<main>
|
<messages [messages]="store.messages()"/>
|
||||||
|
|
||||||
</main>
|
<message-box [viewModel]="store.messageBox"/>
|
||||||
|
|
||||||
<message-box/>
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
:host {
|
:host {
|
||||||
height: 100%;
|
height: 95svh;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 70px minmax(0, 1fr) auto;
|
grid-template-rows: 70px minmax(0, 1fr) auto;
|
||||||
|
|
||||||
|
|||||||
@@ -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 {DmStorage, ServiceManager} from '../../service-manager';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
import {ActivatedRoute} from '@angular/router';
|
||||||
import {DMService} from '@chatenium/chatenium-sdk/services/dmService';
|
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 {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 {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({
|
@Component({
|
||||||
selector: 'app-dm',
|
selector: 'app-dm',
|
||||||
@@ -15,7 +19,8 @@ import {MessageBox} from '../elements/message-box/message-box';
|
|||||||
Oimg,
|
Oimg,
|
||||||
TuiButton,
|
TuiButton,
|
||||||
TuiIcon,
|
TuiIcon,
|
||||||
MessageBox
|
MessageBox,
|
||||||
|
Messages
|
||||||
],
|
],
|
||||||
templateUrl: './dm.html',
|
templateUrl: './dm.html',
|
||||||
styleUrl: './dm.scss',
|
styleUrl: './dm.scss',
|
||||||
@@ -31,14 +36,23 @@ export class Dm implements OnInit {
|
|||||||
return this.serviceManager.dmServices()[this.chatid]
|
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() {
|
ngOnInit() {
|
||||||
this.route.params.subscribe(params => {
|
this.route.params.subscribe(async params => {
|
||||||
const chatid = params['chatid']
|
const chatid = params['chatid']
|
||||||
this.chatid = chatid
|
this.chatid = chatid
|
||||||
console.log(this.serviceManager.chats())
|
console.log(this.serviceManager.chats())
|
||||||
const session = this.serviceManager.currentSession();
|
const session = this.serviceManager.currentSession();
|
||||||
const chatData = this.serviceManager.chats().find(chat => chat.chatid == chatid)
|
const chatData = this.serviceManager.chats().find(chat => chat.chatid == chatid)
|
||||||
|
|
||||||
|
// Setup storage
|
||||||
if (!this.serviceManager.dmServices()[chatid] && session != null && chatData != null) {
|
if (!this.serviceManager.dmServices()[chatid] && session != null && chatData != null) {
|
||||||
this.serviceManager.dmServices()[chatid] = {
|
this.serviceManager.dmServices()[chatid] = {
|
||||||
service: new DMService(
|
service: new DMService(
|
||||||
@@ -46,11 +60,20 @@ export class Dm implements OnInit {
|
|||||||
session.token,
|
session.token,
|
||||||
chatid,
|
chatid,
|
||||||
this.indexedDb.getApi(),
|
this.indexedDb.getApi(),
|
||||||
() => {}
|
(action, data) => {
|
||||||
|
this.onWsListen(action, data)
|
||||||
|
}
|
||||||
),
|
),
|
||||||
chatData: chatData
|
chatData: signal<Chat>(chatData),
|
||||||
|
messages: signal<Message[]>([]),
|
||||||
|
messageBox: new MessageBoxViewModel(
|
||||||
|
(msg) => this.sendMessage(msg),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.store.messages.set(await this.serviceManager.dmServices()[chatid].service.get())
|
||||||
|
await this.store.service.joinWebSocketRoom()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
src/app/chat/elements/message-box/message-box-viewmodel.ts
Normal file
11
src/app/chat/elements/message-box/message-box-viewmodel.ts
Normal file
@@ -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<string>("")
|
||||||
|
}
|
||||||
@@ -1,7 +1,36 @@
|
|||||||
<div id="message-box">
|
<div id="dragOverlay" tuiGroup orientation="vertical" [class.show]="isDraggingOverWindow">
|
||||||
<div></div>
|
<div class="method">
|
||||||
|
<div class="icon-holder">
|
||||||
|
<tui-icon icon="@tui.file-up"/>
|
||||||
|
</div>
|
||||||
|
<span>{{ "chat.elements.messageBox.uplDrag.upload"|translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="method">
|
||||||
|
<div class="icon-holder">
|
||||||
|
<tui-icon icon="@tui.cloud-sync"/>
|
||||||
|
</div>
|
||||||
|
<span>{{ "chat.elements.messageBox.uplDrag.transfer"|translate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="message-box" [style]="'border-radius:'+messageBoxRadius+'px;'">
|
||||||
|
<div class="items-left">
|
||||||
|
<button tuiButton appearance="flat">
|
||||||
|
<tui-icon icon="@tui.file-up"/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button tuiButton appearance="flat">
|
||||||
|
<tui-icon icon="@tui.cloud-sync"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="items-middle">
|
<div class="items-middle">
|
||||||
<textarea></textarea>
|
<textarea [style]="'height:'+textareaHeight+'px;'" #message (input)="onTextAreaInput(message)" [(ngModel)]="viewModel().message"></textarea>
|
||||||
|
<span class="placeholder"
|
||||||
|
[class.hidden]="message.value != ''">{{ "chat.elements.messageBox.placeholder"|translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="items-right">
|
||||||
|
<button tuiButton appearance="flat" (click)="viewModel().onMessageSend(message.value)">
|
||||||
|
<tui-icon icon="@tui.send"/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,27 +3,117 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
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 {
|
#message-box {
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
width: 60%;
|
width: 60%;
|
||||||
background: var(--tui-background-base-alt);
|
background: var(--tui-background-base-alt);
|
||||||
height: 75px;
|
min-height: 75px;
|
||||||
border-radius: 200px;
|
max-height: 200px;
|
||||||
border: 2px solid var(--tui-border-normal);
|
border: 2px solid var(--tui-border-normal);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 10% 80% 10%;
|
grid-template-columns: 85px 1fr 100px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 10px;
|
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 {
|
.items-middle {
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
resize: none;
|
resize: none;
|
||||||
color: var(--tui-text-01);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,59 @@
|
|||||||
import { Component } from '@angular/core';
|
import {Component, HostListener, inject, input} from '@angular/core';
|
||||||
import {TuiAppearance} from '@taiga-ui/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({
|
@Component({
|
||||||
selector: 'message-box',
|
selector: 'message-box',
|
||||||
imports: [
|
imports: [
|
||||||
TuiAppearance
|
TuiAppearance,
|
||||||
|
TranslatePipe,
|
||||||
|
TuiScrollbarDirective,
|
||||||
|
TuiGroup,
|
||||||
|
TuiIcon,
|
||||||
|
TuiButton,
|
||||||
|
FormsModule
|
||||||
],
|
],
|
||||||
templateUrl: './message-box.html',
|
templateUrl: './message-box.html',
|
||||||
styleUrl: './message-box.scss',
|
styleUrl: './message-box.scss',
|
||||||
})
|
})
|
||||||
export class MessageBox {}
|
export class MessageBox {
|
||||||
|
viewModel = input.required<MessageBoxViewModel>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
19
src/app/chat/elements/messages/messages.html
Normal file
19
src/app/chat/elements/messages/messages.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
@if (serviceManager.currentSession != null) {
|
||||||
|
@for (message of messages(); track message.msgid; let i = $index) {
|
||||||
|
<div
|
||||||
|
class="message"
|
||||||
|
[class.author]="message.author == serviceManager.currentSession()!.userData.userid"
|
||||||
|
[class.chained_start]="isMessageStartOfChain(i)"
|
||||||
|
[class.chained_middle]="isMessageMiddleInChain(i)"
|
||||||
|
[class.chained_end]="isMessageEndOfChain(i)"
|
||||||
|
>
|
||||||
|
<div class="above">
|
||||||
|
<span>{{message.sent_at.T * 1000 | date: 'HH:mm'}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="bubble">
|
||||||
|
<span class="message-text">{{message.message}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="below"></div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
69
src/app/chat/elements/messages/messages.scss
Normal file
69
src/app/chat/elements/messages/messages.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/app/chat/elements/messages/messages.spec.ts
Normal file
22
src/app/chat/elements/messages/messages.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { Messages } from './messages';
|
||||||
|
|
||||||
|
describe('Messages', () => {
|
||||||
|
let component: Messages;
|
||||||
|
let fixture: ComponentFixture<Messages>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [Messages],
|
||||||
|
}).compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(Messages);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
await fixture.whenStable();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
109
src/app/chat/elements/messages/messages.ts
Normal file
109
src/app/chat/elements/messages/messages.ts
Normal file
@@ -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<Message[] | NetworkMessage[]>([])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ? <Message>{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 ? <NetworkMessage>{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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {IndexedDB} from './storage/indexed-db';
|
||||||
import {Keyring} from './storage/keyring';
|
import {Keyring} from './storage/keyring';
|
||||||
import {KeyValue} from './storage/key-value';
|
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 {ChatService} from '@chatenium/chatenium-sdk/services/chatService';
|
||||||
import {Chat} from '@chatenium/chatenium-sdk/domain/chatService.schema';
|
import {Chat} from '@chatenium/chatenium-sdk/domain/chatService.schema';
|
||||||
import {DMService} from '@chatenium/chatenium-sdk/services/dmService';
|
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({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -35,5 +37,7 @@ export enum LoadStatus {
|
|||||||
|
|
||||||
export interface DmStorage {
|
export interface DmStorage {
|
||||||
service: DMService
|
service: DMService
|
||||||
chatData: Chat
|
messages: WritableSignal<Message[]>
|
||||||
|
chatData: WritableSignal<Chat>
|
||||||
|
messageBox: MessageBoxViewModel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,22 @@
|
|||||||
/* You can add global styles to this file, and also import other style files */
|
/* 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 {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user