Skip to content

Commit

Permalink
Chat (#38)
Browse files Browse the repository at this point in the history
* feat: add basic chat layout

* feat: enable websocket connection init

* feat: add chats support

* feat: display chat history

* feat: implement chat page with chat list

* fix: fix bugs

* feat: open chat with recipient if recipient is specified in the url

* fix: close socket

* feat: automatically scroll down

* /chat -> /chats

* fix: store chat's background in the project not get from cdn

* feat: add chats to the profile popup
  • Loading branch information
rasulov1337 authored Dec 15, 2024
1 parent c8cf58e commit 77c63e2
Show file tree
Hide file tree
Showing 19 changed files with 584 additions and 2 deletions.
11 changes: 11 additions & 0 deletions nginx/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ http {
proxy_set_header X-Forwarded-Proto $scheme;
}

location /websocket/ {
proxy_pass https://host.docker.internal:8008/api/messages/setconn;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_buffering off;
proxy_read_timeout 86400;
}

location /images/default.png {
proxy_pass http://frontend:5173/default.png;
}
Expand Down
Binary file added public/chat-background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions src/components/BaseComponent/BaseComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
'use strict';

interface BaseComponentData {
parent: HTMLElement;
id: number | string;
templateData: { [key: string]: unknown };
}

export default abstract class BaseComponent {
protected parent: HTMLElement;
protected templateData;

protected template: HandlebarsTemplateDelegate;

Check warning on line 13 in src/components/BaseComponent/BaseComponent.ts

View workflow job for this annotation

GitHub Actions / Run linters

'HandlebarsTemplateDelegate' is not defined
private elementId: string;

public thisElement: HTMLElement;

/**
* Creates a basic component
* @param data - Data for base component initialization
*/
constructor(data: BaseComponentData) {
this.thisElement = null as unknown as HTMLElement; // fuck typescript =D

this.elementId = this.constructor.name + '-' + data.id;

// Automatic template set
const templateName = `${this.constructor.name}.hbs`;
this.template = Handlebars.templates[templateName];

Check warning on line 29 in src/components/BaseComponent/BaseComponent.ts

View workflow job for this annotation

GitHub Actions / Run linters

'Handlebars' is not defined
if (!this.template) {
throw new Error('No such template found:' + templateName);
}

this.parent = data.parent;
this.templateData = data.templateData;
}

/**
* Func that attaches event listeners.
*/
protected abstract addEventListeners(): void;

/**
* Called only once.
*/
render() {
this.parent.insertAdjacentHTML(
'beforeend',
this.template({
id: this.elementId,
...this.templateData,
})
);

// Wait till browser renders the component
requestAnimationFrame(() => {
this.thisElement = document.getElementById(
this.elementId
) as HTMLElement;

this.addEventListeners();
});
}
}
46 changes: 46 additions & 0 deletions src/components/ChatWindow/ChatWindow.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<template id='js-chat-message-template'>
<div class='chat-window__chat-window__message'>
<span class='chat-window__chat-window__message__text js-message-text'>
Тут текст сообщения
</span>
<div class='chat-window__chat-window__message__time-wrapper'>
<span
class='chat-window__chat-window__message__time js-message-time'
>
00:00
</span>
</div>
</div>
</template>

<div class='chat-window__chat-window' id='{{id}}'>
<div class='chat-window__chat-window__messages' id='js-messages'>
<!-- Messages will be here -->
</div>

<div class='chat-window__chat-window__bottom-part'>
<textarea
placeholder='Написать сообщение'
class='chat-window__chat-window__input js-message-input'
rows='1'
maxlength='300'
></textarea>
<button
class='chat-window__chat-window__send-button'
id='js-send-message-button'
>
<svg
aria-hidden='true'
width='16'
height='16'
viewBox='0 0 16 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
><path
d='M15.968 9.02v-2.1H3.88l4.495-4.544L6.953.971.03 7.969l6.924 6.999 1.422-1.407L3.882 9.02h12.086Z'
fill='currentColor'
></path>
</svg>
</button>
</div>
</div>
109 changes: 109 additions & 0 deletions src/components/ChatWindow/ChatWindow.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
.chat-window {
&__chat-window {
flex: 1 1;
background: url('/chat-background.png');
box-sizing: border-box;
margin: 0 auto;

display: flex;
flex-direction: column;

&__messages {
overflow-y: auto;
flex: 1 1 0;
display: flex;
flex-direction: column;
row-gap: 10px;
padding: 10px 20px 10px;
}

&__message {
display: flex;
flex-direction: row;
justify-content: space-between;
flex: 0 0 auto;
gap: 15px;

width: fit-content;
height: min-content;
min-width: 70px;
min-height: 30px;
background-color: #ffffff;
border-radius: 20px;
align-items: flex-start;
box-sizing: border-box;
padding: 10px 10px;
align-items: end;

&__text {
font-size: 17px;
line-height: 24px;
}

&__time-wrapper {
display: flex;
align-items: end;
flex: 1 1;
}

&__time {
font-size: 12px;
color: #15224266;
}

&--mine {
margin-left: auto;
color: #fff;
background-color: #0468ff;
.chat-window__chat-window__message__time {
color: #fff;
}
}
}

&__bottom-part {
padding: 0 20px;
box-sizing: border-box;
border-top: 1px solid #e4e4e4;
background-color: #fff;
width: 100%;
}

&__input {
display: block;
padding: 4px 12px;
width: 100%;
height: 31px;
line-height: 20px;
overflow: hidden;
outline: none;
border: 1px solid #ced1d7;
font-family: Inter;
font-size: 16px;
box-sizing: border-box;
border-radius: 16px;
resize: none;
}

&__bottom-part {
padding: 5px 10px;
display: flex;
gap: 10px;
align-items: flex-end;
}

&__send-button {
display: flex;
justify-content: center;
align-items: center;
border: none;
background-color: #0468ff;
width: 32px;
height: 32px;
color: #fff;
rotate: 90deg;
border-radius: 50%;
cursor: pointer;
}
}
}
154 changes: 154 additions & 0 deletions src/components/ChatWindow/ChatWindow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import BaseComponent from '../BaseComponent/BaseComponent';
import globalStore from '../../modules/GlobalStore';
import { convertTimeToMinutesAndSeconds } from '../../modules/Utils';
import { Message } from '../../repositories/ChatRepository';

export default class ChatWindow extends BaseComponent {
private messages: Message[];
private messagesContainer: HTMLDivElement;
private recipientId: string;

constructor(parent: HTMLElement, recipientId: string, messages: Message[]) {
super({
parent: parent,
id: '0',
templateData: {},
});

this.recipientId = recipientId;
this.messages = messages;

requestAnimationFrame(() => {
this.messagesContainer = document.getElementById('js-messages');

this.displayMessageHistory();

this.messagesContainer.scrollTop =
this.messagesContainer.scrollHeight;
});

if (globalStore.chat.socket) {
globalStore.chat.socket.close();
}

globalStore.chat.socket = new WebSocket(
`wss://${window.location.hostname}/websocket/`
);
}

private async displayMessageHistory() {
for (const message of this.messages) {
this.addNewMessageElement(message);
}
}

private attachSocketEventListeners() {
const socket = globalStore.chat.socket;
if (!socket) {
throw new Error('socket is null!');
}

socket.onopen = (e) => {

Check warning on line 51 in src/components/ChatWindow/ChatWindow.ts

View workflow job for this annotation

GitHub Actions / Run linters

'e' is defined but never used
console.log('[open] Соединение установлено');
console.log('Отправляем данные на сервер');
};

socket.onmessage = (event) => {
console.log(`[message] Данные получены с сервера: ${event.data}`);
this.addNewMessageElement(JSON.parse(event.data) as Message);
};

socket.onclose = function (event) {
if (event.wasClean) {
console.log(
`[close] Соединение закрыто чисто, код=${event.code} причина=${event.reason}`
);
} else {
// например, сервер убил процесс или сеть недоступна
// обычно в этом случае event.code 1006
console.log('[close] Соединение прервано');
}
};

socket.onerror = function (error) {
console.error(error);
};
}

protected addEventListeners(): void {
this.attachSocketEventListeners();

window.onbeforeunload = () => {
// If user wants to refresh page close the connection
globalStore.chat.socket?.close();
globalStore.chat.socket = null;
};

const textArea = this.thisElement.querySelector(
'.js-message-input'
) as HTMLInputElement;
textArea.oninput = () => {
setTimeout(function () {
textArea.style.cssText =
'height:' + textArea.scrollHeight + 'px';
}, 0);
};

const sendMessageButton = document.getElementById(
'js-send-message-button'
) as HTMLButtonElement;

sendMessageButton.onclick = () => {
const text = textArea.value;
this.sendMessage(text);
textArea.value = '';
};

textArea.onkeydown = (event) => {
if (event.ctrlKey && event.key === 'Enter') {
const text = textArea.value;
this.sendMessage(text);
textArea.value = '';
}
};
}

private sendMessage(text: string) {
globalStore.chat.socket?.send(
JSON.stringify({
receiverId: this.recipientId,
content: text,
})
);
this.addNewMessageElement({
content: text,
receiverId: '',
senderId: globalStore.auth.userId!,
id: 0,
createdAt: new Date().toISOString(),
});
}

private addNewMessageElement(message: Message) {
const template = document.getElementById(
'js-chat-message-template'
) as HTMLTemplateElement;

const newMessage = template.content.cloneNode(true) as DocumentFragment;
(
newMessage.querySelector('.js-message-text') as HTMLSpanElement
).textContent = message.content;

(
newMessage.querySelector('.js-message-time') as HTMLSpanElement
).textContent = convertTimeToMinutesAndSeconds(message.createdAt);

if (message.senderId == globalStore.auth.userId) {
newMessage.children[0]!.classList!.add(
'chat-window__chat-window__message--mine'
);
}

this.messagesContainer.appendChild(newMessage);
}
}
Loading

0 comments on commit 77c63e2

Please sign in to comment.