-
+
+
+
+
+
+
{{ "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;