-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
1 parent
c8cf58e
commit 77c63e2
Showing
19 changed files
with
584 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
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]; | ||
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(); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) => { | ||
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); | ||
} | ||
} |
Oops, something went wrong.