diff --git a/src/app/chat/chat-icons.ts b/src/app/chat/chat-icons.ts index 826620d5f6..a47be113b9 100644 --- a/src/app/chat/chat-icons.ts +++ b/src/app/chat/chat-icons.ts @@ -1,3 +1,5 @@ export const CHAT_ICONS = { - telegram: 'assets/img/chat/telegram-logo.svg' + telegram: 'assets/img/chat/telegram-logo.svg', + edit: './assets/img/chat/new-message.svg', + close: './assets/img/comments/cancel-comment-edit.png' }; diff --git a/src/app/chat/component/chat-page/chat-page.component.html b/src/app/chat/component/chat-page/chat-page.component.html index c3966ce8d9..eb11dc9aa7 100644 --- a/src/app/chat/component/chat-page/chat-page.component.html +++ b/src/app/chat/component/chat-page/chat-page.component.html @@ -31,7 +31,12 @@ - + + diff --git a/src/app/chat/component/chat-page/chat-page.component.scss b/src/app/chat/component/chat-page/chat-page.component.scss index 63dc712f6c..8b2a857354 100644 --- a/src/app/chat/component/chat-page/chat-page.component.scss +++ b/src/app/chat/component/chat-page/chat-page.component.scss @@ -145,6 +145,7 @@ } .messages { + position: relative; flex: 1; overflow-y: auto; padding-right: 0.5rem; @@ -195,6 +196,7 @@ } .message-input-container { + position: relative; display: flex; flex-direction: column; align-items: center; @@ -260,6 +262,64 @@ } } +.message-icons { + width: 14px; + margin: 0 0 3px auto; + display: flex; + + .icon { + max-width: 11px; + } +} + +.edit-text-container { + position: absolute; + bottom: 6rem; + width: 88%; + padding: 7px 10px; + border-radius: 7px; + margin-left: 5px; + display: flex; + align-self: flex-start; + background-color: var(--primary-white); + color: var(--secondary-dark-grey); + border: 1px solid var(--after-primary-light-grey); + box-shadow: -2px -1px 5px #0000001a; + + p { + margin: 0; + font-size: 14px; + + &:first-of-type { + color: var(--tertiary-light-green); + } + } + + .icon { + width: 16px; + margin: 0 15px; + } + + .close { + margin-left: auto; + width: 10px; + height: 10px; + } +} + +.edit-status { + margin: 0 15px; + font-size: 10px; + + .edited-icon { + width: 13px; + } +} + +.menu-item { + font-size: 14px; +} + .file-attach { margin-top: 6px; display: flex; diff --git a/src/app/chat/component/chat-page/chat-page.component.ts b/src/app/chat/component/chat-page/chat-page.component.ts index f0c821171d..d91ecb25d2 100644 --- a/src/app/chat/component/chat-page/chat-page.component.ts +++ b/src/app/chat/component/chat-page/chat-page.component.ts @@ -31,7 +31,10 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { @ViewChild('pagingAnchor') pagingAnchor!: ElementRef; private io?: IntersectionObserver; private readonly destroy$ = new Subject(); - constructor(public facade: ChatFacade, public route: ActivatedRoute) {} + constructor( + public facade: ChatFacade, + public route: ActivatedRoute + ) {} ngOnInit(): void { this.facade.init(Number(this.route.snapshot.queryParams['chatId'])); @@ -73,6 +76,14 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { this.io.observe(this.pagingAnchor.nativeElement); } + messageController(text: string, file?: File) { + if (!this.facade.selectedMessage) { + this.facade.sendMessage(text, file); + } else { + this.facade.editMessage(text); + } + } + ngOnDestroy(): void { this.io?.disconnect(); this.destroy$.next(); diff --git a/src/app/chat/data/chat-api.service.ts b/src/app/chat/data/chat-api.service.ts index 423cedfdf7..3aff49b350 100644 --- a/src/app/chat/data/chat-api.service.ts +++ b/src/app/chat/data/chat-api.service.ts @@ -56,6 +56,16 @@ export class ChatApiService { return this.http.post(url, form, { headers, responseType: 'text' as 'json' }); } + editMessage(chatInternalId: number, messageId: number, newText: string) { + const headers = this.authHeaders(); + if (!headers) { + return new Observable((o) => o.complete()); + } + const url = `${this.baseUrl}/message/edit`; + const data = new Blob([JSON.stringify({ chatId: chatInternalId, messageId: messageId, newText })], { type: 'application/json' }); + return this.http.put(url, data, { headers, responseType: 'text' as 'json' }); + } + getLastOrder(chatInternalId: number) { const headers = this.authHeaders(); if (!headers) { diff --git a/src/app/chat/facade/chat.facade.ts b/src/app/chat/facade/chat.facade.ts index ca321e1218..090d037aea 100644 --- a/src/app/chat/facade/chat.facade.ts +++ b/src/app/chat/facade/chat.facade.ts @@ -14,6 +14,8 @@ export class ChatFacade { readonly selectedChat = signal(null); readonly selectedImageUrl = signal(null); + readonly selectedMessage = signal(null); + readonly clientInfoVisible = signal(false); readonly clientInfoData = signal(null); @@ -158,7 +160,11 @@ export class ChatFacade { this.location.replaceState(newPath); } - selectChatById(chatInternalId: number) { + selectMessage(message?: ChatMessageView) { + this.selectedMessage.set(message); + } + + selectChatById(chatInternalId: number) { const chat = this.chats().find((c) => c.chatInternalId === chatInternalId); if (chat) { this.selectChat(chat); @@ -332,6 +338,25 @@ export class ChatFacade { }); } + editMessage(newText: string) { + const sel = this.selectedChat(); + const mes = this.selectedMessage(); + if (!sel || !newText.trim() || !mes) { + return; + } + this.api.editMessage(sel.chatInternalId, mes.id, newText.trim()).subscribe({ + next: () => { + const messageIndex = sel.messages.findIndex((m) => m.id === mes.id); + if (messageIndex > -1) { + sel.messages[messageIndex].text = newText; + } + this.selectedChat.set({ ...sel }); + this.selectMessage(null); + }, + error: (e) => console.error('Failed to edit the message:', e) + }); + } + toggleClientInfo() { this.clientInfoVisible.update((v) => !v); const visible = this.clientInfoVisible(); diff --git a/src/app/chat/ui/message-input/message-input.component.html b/src/app/chat/ui/message-input/message-input.component.html index 6ec7b67635..cd6b49251a 100644 --- a/src/app/chat/ui/message-input/message-input.component.html +++ b/src/app/chat/ui/message-input/message-input.component.html @@ -1,7 +1,16 @@ + + + + {{ 'chat.edit-message' | translate }} + {{ editText }} + + + (); + @Output() closeEdit = new EventEmitter(); @ViewChild('fileInput', { static: false }) fileInput!: ElementRef; - + @ViewChild('textInput') textInput!: ElementRef; + @Input() editText?: string; text = ''; file?: File; private readonly MAX_FILE_MB = 5; + readonly chatICons = CHAT_ICONS; + + ngOnChanges(changes: SimpleChanges) { + if (changes['editText']?.currentValue) { + this.text = this.editText ?? ''; + this.textInput.nativeElement.focus(); + } + } send() { if (!this.text.trim() && !this.file) { @@ -23,6 +34,7 @@ export class MessageInputComponent { } this.sendText.emit({ text: this.text, file: this.file }); this.text = ''; + this.editText = ''; this.file = undefined; if (this.fileInput) { @@ -46,4 +58,10 @@ export class MessageInputComponent { } this.file = file; } + + onClose() { + this.closeEdit.emit(); + this.editText = ''; + this.text = ''; + } } diff --git a/src/app/chat/ui/messages-list/messages-list.component.html b/src/app/chat/ui/messages-list/messages-list.component.html index db9d263f69..4732aa82e6 100644 --- a/src/app/chat/ui/messages-list/messages-list.component.html +++ b/src/app/chat/ui/messages-list/messages-list.component.html @@ -1,5 +1,18 @@ - + + {{ message.text }} @@ -19,4 +32,20 @@ + + + + + + + {{ 'chat.status.EDITED' | translate: { editDate: '11.09.2025', editTime: '16:33' } }} + + {{ 'chat.edit-option' | translate }} + + + + + diff --git a/src/app/chat/ui/messages-list/messages-list.component.spec.ts b/src/app/chat/ui/messages-list/messages-list.component.spec.ts index 4c14c52ed3..f00e10025c 100644 --- a/src/app/chat/ui/messages-list/messages-list.component.spec.ts +++ b/src/app/chat/ui/messages-list/messages-list.component.spec.ts @@ -2,6 +2,7 @@ import { MessagesListComponent } from './messages-list.component'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ChatMessageView } from '../../model/chat-page.interface'; import { fakeAsync, tick } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; describe('MessagesListComponent', () => { let fixture: ComponentFixture; @@ -18,7 +19,7 @@ describe('MessagesListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MessagesListComponent] + imports: [MessagesListComponent, HttpClientTestingModule] }).compileComponents(); fixture = TestBed.createComponent(MessagesListComponent); diff --git a/src/app/chat/ui/messages-list/messages-list.component.ts b/src/app/chat/ui/messages-list/messages-list.component.ts index 70247b74bf..f49b1a2e5f 100644 --- a/src/app/chat/ui/messages-list/messages-list.component.ts +++ b/src/app/chat/ui/messages-list/messages-list.component.ts @@ -2,11 +2,15 @@ import { Component, EventEmitter, Input, Output, ElementRef, ViewChild, AfterVie import { NgForOf, NgIf, NgClass } from '@angular/common'; import { ChatMessageView } from '../../model/chat-page.interface'; import { StatusTicksComponent } from '../status-ticks/status-ticks.component'; +import { CHAT_ICONS } from '../../chat-icons'; +import { ChatFacade } from '../../facade/chat.facade'; +import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; +import { TranslateModule } from '@ngx-translate/core'; @Component({ selector: 'app-messages-list', standalone: true, - imports: [NgForOf, NgIf, NgClass, StatusTicksComponent], + imports: [NgForOf, NgIf, NgClass, StatusTicksComponent, MatMenuModule, TranslateModule], templateUrl: './messages-list.component.html' }) export class MessagesListComponent implements AfterViewChecked { @@ -14,9 +18,17 @@ export class MessagesListComponent implements AfterViewChecked { @Output() openImage = new EventEmitter(); @ViewChild('scrollContainer') private readonly scrollContainer!: ElementRef; + @ViewChild(MatMenuTrigger) menuTrigger!: MatMenuTrigger; + constructor(readonly facade: ChatFacade) {} + + readonly chatICons = CHAT_ICONS; private lastMsgCount = 0; + selectedMessage?: ChatMessageView; + pressTimer: any; + matMenuPosition = { x: '0px', y: '0px' }; + ngAfterViewChecked(): void { if (this.messages.length !== this.lastMsgCount) { const newMessages = this.messages.slice(this.lastMsgCount); @@ -31,6 +43,13 @@ export class MessagesListComponent implements AfterViewChecked { } } + onMessageEdit() { + if (this.selectedMessage) { + this.facade.selectMessage(this.selectedMessage); + this.selectedMessage = null; + } + } + private scrollToBottom(): void { const el = this.scrollContainer?.nativeElement; if (el) { @@ -63,4 +82,41 @@ export class MessagesListComponent implements AfterViewChecked { ); }); } + + onMenuClosed() { + this.selectedMessage = null; + } + + onRightClick(event: MouseEvent, trigger: MatMenuTrigger, message: ChatMessageView) { + if (message?.from != 'Me' || message?.images.length || message?.fileUrl) { + return; + } + event.preventDefault(); + this.matMenuPosition.x = event.clientX + 'px'; + this.matMenuPosition.y = event.clientY - 20 + 'px'; + this.selectedMessage = message; + this.menuTrigger.menu.focusFirstItem('mouse'); + trigger.openMenu(); + } + + onTouchStart(event: TouchEvent, trigger: MatMenuTrigger, message: ChatMessageView) { + if (message?.from != 'Me' && message?.text && !message?.images.length && !message?.fileUrl) { + return; + } + this.selectedMessage = message; + this.pressTimer = setTimeout(() => { + const touch = event.touches[0]; + this.matMenuPosition.x = touch.clientX + 'px'; + this.matMenuPosition.y = touch.clientY - 20 + 'px'; + trigger.openMenu(); + }, 600); + } + + onTouchEnd(event: TouchEvent) { + clearTimeout(this.pressTimer); + } + + onTouchMove(event: TouchEvent) { + clearTimeout(this.pressTimer); + } } diff --git a/src/assets/i18n/ubs-admin/en.json b/src/assets/i18n/ubs-admin/en.json index 47ac2e2672..d731faebf7 100644 --- a/src/assets/i18n/ubs-admin/en.json +++ b/src/assets/i18n/ubs-admin/en.json @@ -648,8 +648,11 @@ "no-file-chosen": "No file chosen", "status": { "READ": "Read", - "UNREAD": "Unread" - } + "UNREAD": "Unread", + "EDITED": "Edited {{editDate}} at {{editTime}}" + }, + "edit-message": "Edit message", + "edit-option": "Edit" }, "ubs-employee": { "add-employee": "Add employee", diff --git a/src/assets/i18n/ubs-admin/uk.json b/src/assets/i18n/ubs-admin/uk.json index b16606e98b..f7f323e592 100644 --- a/src/assets/i18n/ubs-admin/uk.json +++ b/src/assets/i18n/ubs-admin/uk.json @@ -646,8 +646,11 @@ "no-file-chosen": "Файл не обрано", "status": { "READ": "Прочитано", - "UNREAD": "Не прочитано" - } + "UNREAD": "Не прочитано", + "EDITED": "Змінено {{editDate}} о {{editTime}}" + }, + "edit-message": "Редагувати повідомлення", + "edit-option": "Редагувати" }, "ubs-employee": { "add-employee": "Додати працівника",
{{ 'chat.edit-message' | translate }}
{{ editText }}