From 5e2a13129321ecd7087900ef0ae21dc13afa8476 Mon Sep 17 00:00:00 2001 From: Natan Anter Date: Mon, 27 Apr 2026 11:21:12 +0300 Subject: [PATCH 1/4] add avatar image soppurt --- .../chat-list-component.html | 8 ++++++- .../chat-list-component.scss | 17 +++++++++++++ .../chat-list-component.ts | 24 +++++++++++++------ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/app/chat-list-component/chat-list-component.html b/src/app/chat-list-component/chat-list-component.html index ae56df9..891d238 100644 --- a/src/app/chat-list-component/chat-list-component.html +++ b/src/app/chat-list-component/chat-list-component.html @@ -22,7 +22,13 @@ (click)="onOpenChat(chat)" >
-
{{ avatarFor(chat) }}
+
+ @if (isImage(chat.avatar)) { + avatar + } @else { + {{ chat.avatar }} + } +
diff --git a/src/app/chat-list-component/chat-list-component.scss b/src/app/chat-list-component/chat-list-component.scss index af4d14a..73d6734 100644 --- a/src/app/chat-list-component/chat-list-component.scss +++ b/src/app/chat-list-component/chat-list-component.scss @@ -145,6 +145,23 @@ color: #21b15d !important; } +.chat-list-item__avatar { + width: 42px; + height: 42px; + border-radius: 50%; + overflow: hidden; + + display: flex; + align-items: center; + justify-content: center; +} + +.chat-list-item__avatar-img { + width: 100%; + height: 100%; + object-fit: cover; +} + .chat-delete-button { width: 1.7rem; height: 1.7rem; diff --git a/src/app/chat-list-component/chat-list-component.ts b/src/app/chat-list-component/chat-list-component.ts index 04f1258..ae17595 100644 --- a/src/app/chat-list-component/chat-list-component.ts +++ b/src/app/chat-list-component/chat-list-component.ts @@ -33,7 +33,8 @@ export class ChatListComponent { }); return chats.sort( - (a, b) => (b.messages.at(-1)?.time?.getTime() ?? 0) - (a.messages.at(-1)?.time?.getTime() ?? 0), + (a, b) => + (b.messages.at(-1)?.time?.getTime() ?? 0) - (a.messages.at(-1)?.time?.getTime() ?? 0), ); } @@ -47,14 +48,23 @@ export class ChatListComponent { return chat.subtitle || 'start the conversation'; } - return DOMPurify.sanitize( - lastMessage.value || - lastMessage.attachment?.name || - '', - { ALLOWED_TAGS: [] } - ); + return DOMPurify.sanitize(lastMessage.value || lastMessage.attachment?.name || '', { + ALLOWED_TAGS: [], + }); } + isImage(value: string): boolean { + return ( + value.startsWith('http') || + value.startsWith('assets/') || + value.endsWith('.png') || + value.endsWith('.jpg') || + value.endsWith('.jpeg') || + value.endsWith('.webp') || + value.endsWith('.svg') + ); + } + lastMessageTime(chat: Chat): string { const lastMessage = chat.messages.at(-1)?.time; return lastMessage From f7f49724af006f8620fed3bd8fde293227dd63e5 Mon Sep 17 00:00:00 2001 From: Natan Anter Date: Wed, 29 Apr 2026 12:34:26 +0300 Subject: [PATCH 2/4] change avatar to type --- .../chat-list-component/chat-list-component.html | 8 ++++---- .../chat-list-component/chat-list-component.ts | 16 ---------------- .../chat-navbar-component.html | 2 +- src/classes/Chat.ts | 9 +++++++-- src/interfaces/db/ChatRecord.ts | 4 +++- src/interfaces/db/CreateChatRecordInput.ts | 4 +++- src/services/chat.service.ts | 10 +++++++--- 7 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/app/chat-list-component/chat-list-component.html b/src/app/chat-list-component/chat-list-component.html index 891d238..3f2a2b3 100644 --- a/src/app/chat-list-component/chat-list-component.html +++ b/src/app/chat-list-component/chat-list-component.html @@ -23,10 +23,10 @@ >
- @if (isImage(chat.avatar)) { - avatar + @if (chat.avatar.type === 'image') { + avatar } @else { - {{ chat.avatar }} + {{ chat.avatar.value }} }
@@ -64,4 +64,4 @@
\ No newline at end of file + diff --git a/src/app/chat-list-component/chat-list-component.ts b/src/app/chat-list-component/chat-list-component.ts index ae17595..5ea113f 100644 --- a/src/app/chat-list-component/chat-list-component.ts +++ b/src/app/chat-list-component/chat-list-component.ts @@ -38,10 +38,6 @@ export class ChatListComponent { ); } - avatarFor(chat: Chat): string { - return chat.avatar; - } - lastMessageText(chat: Chat): string { const lastMessage = chat.messages.at(-1); if (!lastMessage) { @@ -53,18 +49,6 @@ export class ChatListComponent { }); } - isImage(value: string): boolean { - return ( - value.startsWith('http') || - value.startsWith('assets/') || - value.endsWith('.png') || - value.endsWith('.jpg') || - value.endsWith('.jpeg') || - value.endsWith('.webp') || - value.endsWith('.svg') - ); - } - lastMessageTime(chat: Chat): string { const lastMessage = chat.messages.at(-1)?.time; return lastMessage diff --git a/src/app/chat-navbar-component/chat-navbar-component.html b/src/app/chat-navbar-component/chat-navbar-component.html index 7a4b74d..43e5cdb 100644 --- a/src/app/chat-navbar-component/chat-navbar-component.html +++ b/src/app/chat-navbar-component/chat-navbar-component.html @@ -51,7 +51,7 @@ } - +

{{ chat.name }}

{{ chat.status }}

diff --git a/src/classes/Chat.ts b/src/classes/Chat.ts index b01236f..3c2e60d 100644 --- a/src/classes/Chat.ts +++ b/src/classes/Chat.ts @@ -2,11 +2,16 @@ import { Message } from './Message'; import { Supporter } from './Supporter'; import { Client } from './Client'; +export type Avatar = { + type: 'image' | 'text'; + value: string; +}; + export class Chat { id: number; name: string; status: string; - avatar: string; + avatar: Avatar; subtitle?: string; timeLabel?: string; unreadCount: number; @@ -23,7 +28,7 @@ export class Chat { id: number, name: string, status: string, - avatar: string, + avatar: Avatar, supporter: Supporter, options: { subtitle?: string; diff --git a/src/interfaces/db/ChatRecord.ts b/src/interfaces/db/ChatRecord.ts index 465a34a..68a112a 100644 --- a/src/interfaces/db/ChatRecord.ts +++ b/src/interfaces/db/ChatRecord.ts @@ -1,8 +1,10 @@ +import { Avatar } from '../../classes/Chat'; + export interface ChatRecord { id: number; name: string; status: string; - avatar: string; + avatar: Avatar | string; subtitle?: string; timeLabel?: string; unreadCount?: number; diff --git a/src/interfaces/db/CreateChatRecordInput.ts b/src/interfaces/db/CreateChatRecordInput.ts index 42761b3..925e4ef 100644 --- a/src/interfaces/db/CreateChatRecordInput.ts +++ b/src/interfaces/db/CreateChatRecordInput.ts @@ -1,7 +1,9 @@ +import { Avatar } from '../../classes/Chat'; + export interface CreateChatRecordInput { name: string; status: string; - avatar: string; + avatar: Avatar; subtitle?: string; timeLabel?: string; unreadCount?: number; diff --git a/src/services/chat.service.ts b/src/services/chat.service.ts index 40d59e8..98ae4eb 100644 --- a/src/services/chat.service.ts +++ b/src/services/chat.service.ts @@ -4,7 +4,7 @@ import { Message } from '../classes/Message'; import { coerceValidatorSpec } from '../classes/MessageValidator'; import { Question, getPersistableValidationErrorMessage } from '../classes/Question'; import { Supporter } from '../classes/Supporter'; -import { Chat } from '../classes/Chat'; +import { Avatar, Chat } from '../classes/Chat'; import { Agent } from '../classes/Agent'; import { AgentsService } from './agents.service'; import { ChatRecord } from '../interfaces/db/ChatRecord'; @@ -37,7 +37,7 @@ export class ChatService { const record = await this.dbService.createChat({ name, status, - avatar: name.slice(0, 2).toUpperCase(), + avatar: { type: 'text', value: name.slice(0, 2).toUpperCase() }, subtitle: options.subtitle, timeLabel: options.timeLabel, unreadCount: options.unreadCount, @@ -103,6 +103,10 @@ export class ChatService { chat.name = record.name; } + private normalizeAvatar(avatar: Avatar | string): Avatar { + return typeof avatar === 'string' ? { type: 'text', value: avatar } : avatar; + } + hydrateChat( record: ChatRecord, initialAgent: Agent, @@ -117,7 +121,7 @@ export class ChatService { catch{ supporter.setContext(supporterRecord?.context ?? '{}'); } - const chat = new Chat(record.id, record.name, record.status, record.avatar, supporter, { + const chat = new Chat(record.id, record.name, record.status, this.normalizeAvatar(record.avatar), supporter, { subtitle: record.subtitle, timeLabel: record.timeLabel, unreadCount: record.unreadCount, From fb2265aad1a6ffdefbef6025785210f8f0fe3064 Mon Sep 17 00:00:00 2001 From: Natan Anter Date: Wed, 29 Apr 2026 15:06:36 +0300 Subject: [PATCH 3/4] fix avatar name --- main.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/main.js b/main.js index 6d54be7..3233e95 100644 --- a/main.js +++ b/main.js @@ -104,7 +104,7 @@ function mapChatRow(row) { id: row.id, name: row.name, status: row.status, - avatar: row.avatar, + avatar: parseAvatarColumn(row.avatar, row.id), subtitle: row.subtitle ?? undefined, timeLabel: row.time_label ?? undefined, unreadCount: row.unread_count ?? undefined, @@ -180,6 +180,25 @@ function parseAttachmentColumn(value, fieldName, rowId) { return undefined; } +function parseAvatarColumn(value, rowId) { + const parsedValue = parseJsonColumn(value, 'avatar', rowId); + if (parsedValue === undefined) { + return value; + } + + if ( + parsedValue && + typeof parsedValue === 'object' && + typeof parsedValue.type === 'string' && + typeof parsedValue.value === 'string' + ) { + return parsedValue; + } + + console.warn(`Unexpected avatar payload for chat ${rowId}.`, parsedValue); + return value; +} + function mapMessageRow(row) { return { id: row.id, @@ -339,7 +358,7 @@ function registerDbHandlers() { [ chat.name, chat.status, - chat.avatar, + JSON.stringify(chat.avatar), chat.subtitle ?? null, chat.timeLabel ?? null, chat.unreadCount ?? 0, From 9975c6732ef6f699a2df1b3747e6e29811d10716 Mon Sep 17 00:00:00 2001 From: Natan Anter Date: Thu, 30 Apr 2026 14:25:55 +0300 Subject: [PATCH 4/4] add image changing save --- main.js | 36 ++++++++++++++++++++++ src/classes/Chat.ts | 3 ++ src/interfaces/db/UpdateChatAvatarInput.ts | 6 ++++ src/services/chat.service.ts | 13 ++++++++ src/services/db.service.ts | 5 +++ 5 files changed, 63 insertions(+) create mode 100644 src/interfaces/db/UpdateChatAvatarInput.ts diff --git a/main.js b/main.js index 3233e95..0eaeccf 100644 --- a/main.js +++ b/main.js @@ -633,6 +633,42 @@ function registerDbHandlers() { return mapChatRow(row); }); + ipcMain.handle('db:updateChatAvatar', async (_event, { chatId, avatar }) => { + const now = new Date().toISOString(); + await run( + ` + UPDATE chats + SET avatar = ?, + updated_at = ? + WHERE id = ? + `, + [JSON.stringify(avatar), now, chatId], + ); + + const row = await get( + ` + SELECT + id, + name, + status, + avatar, + subtitle, + time_label, + unread_count, + highlight_time, + avatar_ring, + tip_label, + created_at, + updated_at + FROM chats + WHERE id = ? + `, + [chatId], + ); + + return mapChatRow(row); + }); + ipcMain.handle('db:updateSupporterAgent', async (_event, { chatId, agentName }) => { const result = await run( ` diff --git a/src/classes/Chat.ts b/src/classes/Chat.ts index 3c2e60d..0df3497 100644 --- a/src/classes/Chat.ts +++ b/src/classes/Chat.ts @@ -61,6 +61,9 @@ export class Chat { processFileUrl(file: File): string | Promise { return this._processFileUrlDriver(file); } + updateAvatar(avatar: Avatar) { + this.avatar = avatar; + } setFileUrlProcessor(processor: typeof this._processFileUrlDriver) { this._processFileUrlDriver = processor; } diff --git a/src/interfaces/db/UpdateChatAvatarInput.ts b/src/interfaces/db/UpdateChatAvatarInput.ts new file mode 100644 index 0000000..f860f83 --- /dev/null +++ b/src/interfaces/db/UpdateChatAvatarInput.ts @@ -0,0 +1,6 @@ +import { Avatar } from '../../classes/Chat'; + +export interface UpdateChatAvatarInput { + chatId: number; + avatar: Avatar; +} diff --git a/src/services/chat.service.ts b/src/services/chat.service.ts index a3d7165..bcfb3e8 100644 --- a/src/services/chat.service.ts +++ b/src/services/chat.service.ts @@ -103,6 +103,19 @@ export class ChatService { chat.name = record.name; } + async updateChatAvatar(chat: Chat, avatar: Avatar): Promise { + if (chat.avatar.type === avatar.type && chat.avatar.value === avatar.value) { + return; + } + + const record = await this.dbService.updateChatAvatar({ + chatId: chat.id, + avatar, + }); + + chat.updateAvatar(this.normalizeAvatar(record.avatar)); + } + private normalizeAvatar(avatar: Avatar | string): Avatar { return typeof avatar === 'string' ? { type: 'text', value: avatar } : avatar; } diff --git a/src/services/db.service.ts b/src/services/db.service.ts index 7a50db1..4a2dedd 100644 --- a/src/services/db.service.ts +++ b/src/services/db.service.ts @@ -6,6 +6,7 @@ import { CreateMessageRecordInput } from '../interfaces/db/CreateMessageRecordIn import { CreateSupporterRecordInput } from '../interfaces/db/CreateSupporterRecordInput'; import { MessageRecord } from '../interfaces/db/MessageRecord'; import { SupporterRecord } from '../interfaces/db/SupporterRecord'; +import { UpdateChatAvatarInput } from '../interfaces/db/UpdateChatAvatarInput'; import { UpdateChatTitleInput } from '../interfaces/db/UpdateChatTitleInput'; import { UpdateSupporterAgentInput } from '../interfaces/db/UpdateSupporterAgentInput'; import { UpdateSupporterContextInput } from '../interfaces/db/UpdateSupporterContextInput'; @@ -56,6 +57,10 @@ export class DbService { return this.electronService.invoke('db:updateChatTitle', input); } + async updateChatAvatar(input: UpdateChatAvatarInput): Promise { + return this.electronService.invoke('db:updateChatAvatar', input); + } + async updateSupporterAgent(input: UpdateSupporterAgentInput): Promise { return this.electronService.invoke('db:updateSupporterAgent', input); }