Compare commits

...

11 Commits

Author SHA1 Message Date
2219653f40 FIRST WORKING VERSION OF IU9 CA WEB CHAT. IT'S WORKINGgit add -A! YOU CAN SEND STUFF INTO CHAT AND IT WILL LOAD UP ON ANOTHER DEVICE EEEEEEEE 2024-09-05 00:17:33 +03:00
b9626aa860 Making /chat. Unstable save. Working message loading. Styles of messages are not perfect. Several iu9cachat bugs fixed 2024-09-04 12:39:39 +03:00
fc721d7f5c Fixed a lot of server-side bugs, /list-rooms page bugs. Added favicon, finished /chat-members page 2024-09-02 12:34:49 +03:00
3a8bdb99ec Fixed some problems with deletion buttons in /list-rooms, wrote a sketch for /chat-members 2024-09-02 01:11:28 +03:00
9283590122 fixed some backend bugs. Finished /list-rooms. Now we have these cool popup windows before serious actions 2024-09-01 22:24:55 +03:00
21a23be96e Doing frontend now. Added these cool popup windows in list-rooms.nytl.html 2024-09-01 13:03:52 +03:00
07711a93b0 Fixed some bugs in nytl, een9 ad chat, wrote a skeleton for list-rooms, chat pages, finished login, view-profile, edit-profile-pages. chat-members page still needs attention 2024-09-01 01:29:32 +03:00
c009b848b5 Merge branch 'master' into chat-wg-test
To pull all the images
2024-08-31 18:59:10 +03:00
cc8aa516bb New logo 2024-08-31 18:58:21 +03:00
711e495f15 Added some cool svg icons 2024-08-30 20:45:03 +03:00
5062e1ab70 asdasdasdasdasdasdas 2024-08-30 16:27:33 +03:00
56 changed files with 1974 additions and 1225 deletions

View File

@ -38,11 +38,11 @@ regexis024_build_system.sh
Помимо самого бинарника нужен файл с настройками сервиса. Формат настроек: JSON.
Комментарии не поддерживаются. Пример такого файла находится в example/config.json.
Вместе с бинарным фалом так же распространяются ассеты, необъходимые для работы сайта.
Вместе с бинарным фалом так же распространяются ассеты, необходимые для работы сайта.
Их можно найти в папке assets. В настроках (поле `["assets"]`) указывается путь до
папки с ассетами. Путь может быть как абсолютным, так и относительным к рабочей директории.
Поле настроек `["database"]` указывает как соединиться с базой данных.
Поддерживается только база данных sqlite. Поддерживается только хранение в файле.
Поддерживается только база данных sqlite3. Поддерживается только хранение в файле.
Поле `["database"]["file"]` указывает путь где хранится sqlite база данных.
Перед тем как использовать сервис нужно его проинициализировать (а точнее проинициализировать

View File

@ -4,25 +4,55 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Веб-Чат Members</title>
<link rel="stylesheet" href="/assets/css/chat.css">
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/css/common.css">
<link rel="stylesheet" href="/assets/css/common-popup.css">
<link rel="stylesheet" href="/assets/css/chat-members.css">
<title>Chat Members</title>
</head>
<body>
{% PUT chat.pass pres userinfo openedchat initial_chatUpdResp %}
<!--TODO: ADD SOMETHING WRITE SOMETHING AAAAAA -->
<div class="overlay" id="overlay">
<div class="members-list" id="members-list">
<div class="members-list-header">
<span class="close" onclick="closeMembersList()">&times;</span>
<h2 class="all-members">Все участники</h2>
<div id="user-summoning-win" class="popup-window">
<h1 class="popup-window-msg">Nickname for summoned user</h1>
<input class="one-line-input" id="summoned-user-nickname">
<input type="checkbox" id="summoned-user-is-read-only">
<label>Make read only</label><br>
<button class="popup-window-btn-yes" id="user-summoning-yes">Yes, summon</button>
<button class="popup-window-btn-no" id="user-summoning-no">No, cancel</button>
</div>
<div class="members-list-body">
<ul id="members-list-body">
<!-- Список участников будет добавлен динамически -->
</ul>
<div id="user-deletion-win" class="popup-window">
<!-- header will actually be rewritten before showing the window to include user nickname -->
<h1 id="user-deletion-win-title" class="popup-window-msg">Are you sure you want to ban user?</h1>
<button class="popup-window-btn-yes" id="user-deletion-yes">Yes, delete</button>
<button class="popup-window-btn-no" id="user-deletion-no">No, cancel</button>
</div>
<div class="document-container resp-container">
<div id="navigation-panel" class="panel">
<a href="/list-rooms" id="go-to-chat-list" class="panel-thing">
<img alt="Go to list of chats" src="/assets/img/list-rooms.svg" width="32px">
</a>
<a href="/user/{% WRITE userinfo.nickname %}" id="go-to-my-profile" class="panel-thing">
<img alt="Go to my profile" src="/assets/img/user.svg" width="32px">
</a>
<p class="panel-thing panel-header-txt"> Members list of {% WRITE openedchat.name %} ({% WRITE openedchat.nickname %})</p>
<a href="/chat/{% WRITE openedchat.nickname %}" id="go-to-chat-settings" class="panel-thing">
<img alt="Back to chat" src="/assets/img/return.svg" width="32px">
</a>
</div>
<div class="dynamic-block-list">
<img id="CM-btn-add" class="button-add centered-block-el" alt="New chat" src="/assets/img/add.svg">
<div class="dynamic-block-list-el-container" id="CM-list">
</div>
</div>
</div>
<script src="/assets/js/common.js"></script>
<script src="/assets/js/common-popup.js"></script>
<script src="/assets/js/chat-members.js"></script>
</body>
</html>

View File

@ -9,30 +9,60 @@
{% ELDEF main JSON pres JSON userinfo JSON openedchat JSON initial_chatUpdResp %}
<!DOCTYPE html>
<html lang="ru">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Веб-Чат</title>
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/css/debug.css">
<link rel="stylesheet" href="/assets/css/common.css">
<link rel="stylesheet" href="/assets/css/common-popup.css">
<link rel="stylesheet" href="/assets/css/chat.css">
<title>Chat</title>
</head>
<body>
{% PUT chat.pass pres userinfo openedchat initial_chatUpdResp %}
<!-- TODO AAAAA-->
<div class="chat-container">
<div class="chat-header">
<span class="room-name">Веб чат</span>
<button class="members" onclick="openMembersList()">Показать участников</button>
<div id="msg-deletion-win" class="popup-window">
<h1 class="popup-window-msg">Are you sure you want to delete this message?</h1>
<!-- message preview will be actually rewritten before each window activation-->
<p class="message-in-popup-preview" id="win-deletion-msg-preview">Lorem ipsum dolor</p>
<button class="popup-window-btn-yes" id="msg-deletion-yes">Yes, delete</button>
<button class="popup-window-btn-no" id="msg-deletion-no">No, cancel</button>
</div>
<div class="chat-messages" id="chat-messages">
<!-- Сообщения чата будут здесь -->
<div class="fullscreen-container resp-container">
<div class="panel" id="navigation-info-panel">
<a href="/list-rooms" id="go-to-chat-list" class="panel-thing">
<img alt="Go to list of chats" src="/assets/img/list-rooms.svg" width="32px">
</a>
<a href="/user/{% WRITE userinfo.nickname %}" id="go-to-my-profile" class="panel-thing">
<img alt="Go to my profile" src="/assets/img/user.svg" width="32px">
</a>
<p class="panel-thing panel-header-txt"> {% WRITE openedchat.name %} ({% WRITE openedchat.nickname %})</p>
<a href="/chat-members/{% WRITE openedchat.nickname %}" id="go-to-chat-settings" class="panel-thing">
<img alt="Settings of chat. List of members" src="/assets/img/settings-iron.svg" width="32px">
</a>
</div>
<div class="chat-footer">
<input type="text" class="chat-input" id="chat-input" placeholder="Введите сообщение...">
<button class="chat-send-button" onclick="sendMessage()">Отправить</button>
<div id="chat-widget">
<div class="chat-debug-rect" id="debug-line-highest" style="background-color: #8600d3"></div>
<div class="chat-debug-rect" id="debug-line-top-padding" style="background-color: #ff00ae"></div>
<div class="chat-debug-rect" id="debug-line-bottom-padding" style="background-color: #ff0062"></div>
<div class="chat-debug-rect" id="debug-line-lowest" style="background-color: #ff2f00"></div>
<div id="top-loading" class="message-supercontainer">
<img class="loading-spinner" alt="Loading backward..." src="/assets/gif/loading.gif">
</div>
<div id="bottom-loading" class="message-supercontainer">
<img class="loading-spinner" alt="Loading forward..." src="/assets/gif/loading.gif">
</div>
</div>
<div class="panel" id="input-panel">
<div contentEditable id="message-input" class="panel-thing"></div>
</div>
<script src="/assets/js/common.js"></script>
<script src="/assets/js/common-popup.js"></script>
<script src="/assets/js/chat.js"></script>
</div>
</body>
</html>
{% ENDELDEF %}

View File

@ -1,42 +1,69 @@
{% ELDEF main JSON pres JSON userprofile JSON errors %}
{% ELDEF main JSON pres JSON userinfo JSON alienprofile JSON errors %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/assets/css/profile.css">
<title>Профиль</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/css/common.css">
<link rel="stylesheet" href="/assets/css/edit-profile.css">
<title>Edit user Profile</title>
</head>
<body>
<div class="main-container">
<div class="document-container">
<div id="navigation-panel" class="panel">
<a href="/list-rooms" id="go-to-chat-list" class="panel-thing">
<img alt="Go to list of chats" src="/assets/img/list-rooms.svg" width="32px">
</a>
<a href="/user/{% WRITE userinfo.nickname %}" id="go-to-my-profile" class="panel-thing">
<img alt="Go to my profile" src="/assets/img/user.svg" width="32px">
</a>
</div>
{% FOR error IN errors %}
<div>
<p>{% WRITE error.text %}</p>
<div class="server-notif-error-msg-box">
{% WRITE error.text %}
</div>
{% ENDFOR %}
<div class="profile-header">
<h1>Редактирование профиля</h1>
<div class="profile-container">
<h2 class="profile-name-text">{% WRITE alienprofile.name %}</h2>
<h3 class="profile-nickname-text">Nickname: {% WRITE alienprofile.nickname %}</h3>
<p class="profile-bio-text">
{% WRITE alienprofile.bio %}
</p>
</div>
<form method="post" action="/user/{% WRITE userprofile.nickname %}">
<div class="columns">
<div class="column">
<p>{% WRITE userprofile.name %} ( {% WRITE userprofile.nickname %} )</p>
<label for="name"> Изменить имя </label>
<input type="text" name="name" id="name">
<div class="profile-container resp-container">
<h1 class="wide-centered-header">Change user attributes</h1>
<form action = "/user/{% WRITE alienprofile.nickname %}" method="post" enctype="application/x-www-form-urlencoded">
<table class="logins-input-table">
<tr>
<td class="logins-input-td1">
<label for="new-name-input">Enter new name:</label>
</td>
<td class="logins-input-td2">
<input name="name" id="new-name-input" type="text" placeholder="New name" class="one-line-input">
</td>
</tr>
<tr>
<td class="logins-input-td1">
<label for="new-password-input">Enter new password: </label>
</td>
<td class="logins-input-td2">
<input name="password" id="new-password-input" type="password" placeholder="New password" class="one-line-input">
</td>
</tr>
</table>
<label for="input-change-bio">Change description:</label>
<br>
<label for="password"> Изменить пароль </label>
<input type="password" name="password" id="password">
<br>
</div>
</div>
<div class="additional-info">
<p>О себе</p>
<p>{% WRITE userprofile.bio %}</p><br>
<label for="bio">Изменить</label>
<textarea name="bio" id="bio"></textarea>
</div>
<button class="save" type="submit">Сохранить изменения</button>
<textarea name="bio" id="input-change-bio" class="multiline-input"></textarea>
<button class="action-button centered-block-el" type="submit">Submit changes</button>
</form>
</div>
</div>
</body>
</html>
{% ENDELDEF%}

View File

@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
<title>Not found</title>
</head>
<body>

View File

@ -4,7 +4,10 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Список Чат-Комнат</title>
<title>List of chat rooms</title>
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/css/common.css">
<link rel="stylesheet" href="/assets/css/common-popup.css">
<link rel="stylesheet" href="/assets/css/list-rooms.css">
</head>
<body>
@ -13,59 +16,58 @@
let userinfo = {% PUT jsinsert userinfo %};
let initial_chatListUpdResp = {% PUT jsinsert initial_chatListUpdResp %};
</script>
<div class="container">
<h1 style="color: white;">Выберите Чат-Комнату</h1>
<ul class="room-list">
<!-- Здесь будет список комнат -->
</ul>
<button class="create-room-button" onclick="openCreateRoomModal()">Создать Комнату</button>
<div id="chat-creation-win" class="popup-window">
<h1 class="popup-window-msg">Input identifying information for your new chat</h1>
<table class="id-str-input-table">
<tr>
<td class="id-str-input-td1">
<label for="chat-nickname-input">Enter nickname for new chat:</label>
</td>
<td class="id-str-input-td2">
<input id="chat-nickname-input" type="text" placeholder="Take a nickname" class="one-line-input" required>
</td>
</tr>
<tr>
<td class="id-str-input-td1">
<label for="chat-name-input">Enter name for new chat:</label>
</td>
<td class="id-str-input-td2">
<input id="chat-name-input" type="text" placeholder="Come up with name" class="one-line-input" required>
</td>
</tr>
</table>
<h1 class="popup-window-msg">Create new chat?</h1>
<button class="popup-window-btn-yes" id="chat-creation-win-yes">Yes, create</button>
<button class="popup-window-btn-no" id="chat-creation-win-no">No, cancel</button>
</div>
<!-- Модальное окно для создания комнаты -->
<!--<div id="createRoomModal" class="modal">-->
<!-- <div class="modal-content">-->
<!-- <div class="modal-header">-->
<!-- <span class="close" onclick="closeCreateRoomModal()">&times;</span>-->
<!-- <h2>Создать Комнату</h2>-->
<!-- </div>-->
<!-- <div class="modal-body">-->
<!-- <input type="text" id="newRoomName" placeholder="Название комнаты">-->
<!-- <input type="password" id="newRoomNickname" placeholder="Никнейм комнаты">-->
<!-- </div>-->
<!-- <div id="error"></div>-->
<!-- <div class="modal-footer">-->
<!-- <button class="join-button" onclick="createRoom()">Создать</button>-->
<!-- </div>-->
<!-- </div>-->
<!--</div>-->
<div id="chat-renunciation-win" class="popup-window">
<!-- header will actually be rewritten before showing the window to include chat nickname -->
<h1 id="chat-renunciation-win-title" class="popup-window-msg">Are you sure you want to leave chat?</h1>
<button class="popup-window-btn-yes" id="chat-renunciation-win-yes">Yes, leave</button>
<button class="popup-window-btn-no" id="chat-renunciation-win-no">No, cancel</button>
</div>
<!--&lt;!&ndash; Модальное окно для добавления участников &ndash;&gt;-->
<!--<div class="overlay" id="add_members">-->
<!-- <div class="add-members">-->
<!-- <div class="add-members-header">-->
<!-- <span class="close" onclick="closeAdd()">&times;</span>-->
<!-- <h2>Добавить участников</h2>-->
<!-- </div>-->
<!-- <div class="add-members-body">-->
<!-- <input type="text" id="newMemberLogin" placeholder="Логин пользователя">-->
<!-- </div>-->
<!-- <div class="add-members-footer">-->
<!-- <button class="add-member-button" onclick="addMember()">Добавить</button>-->
<!-- </div>-->
<!-- </div>-->
<!--</div>-->
<!--<div class="overlay" id="delete-chat">-->
<!-- <div class="delete-chat">-->
<!-- <div class="delete-chat-header">-->
<!-- <span class="delete-close" onclick="closeConfirm()">&times;</span>-->
<!-- <h2>Вы уверены, что хотите удалить чат?</h2>-->
<!-- </div>-->
<!-- <div class="delete-chat-body">-->
<!-- <button class="confirm" onclick="deleteChat()">Да</button>-->
<!-- <button class="cancel" onclick="closeConfirm()">Нет</button>-->
<!-- </div>-->
<!-- </div>-->
<!--</div>-->
x
<div class="document-container resp-container">
<div id="navigation-panel" class="panel">
<a href="/user/{% WRITE userinfo.nickname %}" id="go-to-my-profile" class="panel-thing">
<img alt="Go to my profile" src="/assets/img/user.svg" width="32px">
</a>
<p class="panel-thing panel-header-txt">
List of available rooms
</p>
</div>
<div class="dynamic-block-list">
<img id="CL-bacbe" class="button-add centered-block-el" alt="New chat" src="/assets/img/add.svg">
<div class="dynamic-block-list-el-container" id="CL-dblec">
</div>
</div>
</div>
<script src="/assets/js/common.js"></script>
<script src="/assets/js/common-popup.js"></script>
<script src="/assets/js/list-rooms.js"></script>
</body>
</html>

View File

@ -1,27 +1,39 @@
{% ELDEF main JSON pres JSON userinfo JSON errors %}
<!DOCTYPE html>
<html lang="{% WRITE pres.lang %}">
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% WRITE pres.phr.decl.page-login %}</title>
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/css/common.css">
<link rel="stylesheet" href="/assets/css/login.css">
<title>Login Page</title>
</head>
<body>
{% FOR msg IN errors %}
<div class="error-msg">
{% WRITE msg.text %}
{% FOR error IN errors %}
<div class="server-notif-error-msg-box">
{% WRITE error.text %}
</div>
{% ENDFOR %}
<div class="form-container">
<h1 class="hide-cursor no-select">{% WRITE pres.phr.decl.enter %}</h1>
<h1 class="wide-centered-header">Login</h1>
<form action="/login" method="post" enctype="application/x-www-form-urlencoded">
<label for="nickname">{% WRITE pres.phr.decl.nickname %}</label>
<input type="text" name="nickname" id="nickname"><br>
<label for="password">{% WRITE pres.phr.decl.password %}</label>
<input type="password" name="password" id="password"><br>
<button type="submit" class="hide-cursor no-select">{% WRITE pres.phr.act.enter %}</button>
<table class="logins-input-table">
<tr>
<td class="logins-input-td1"><label for="input-nickname">Enter user nickname:</label></td>
<td class="logins-input-td2">
<input name="nickname" id="input-nickname" type="text" placeholder="Nickname" class="one-line-input" required>
</td>
</tr>
<tr>
<td class="logins-input-td1"><label for="input-password">Enter password:</label></td>
<td class="logins-input-td2">
<input name="password" id="input-password" type="password" placeholder="Password" class="one-line-input" required>
</td>
</tr>
</table>
<button class="action-button centered-block-el" type="submit">Login</button>
</form>
</div>

View File

@ -1,26 +1,36 @@
{% ELDEF main JSON pres JSON userprofile %}
{% ELDEF main JSON pres JSON userinfo JSON alienprofile %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/assets/css/profile.css">
<title>Профиль</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" type="image/png" href="/assets/img/favicon.png">
<link rel="stylesheet" href="/assets/css/common.css">
<!-- This page is so simple, that it does not even have it's separate css file -->
<title>User Profile</title>
</head>
<body>
<div class="main-container">
<div class="profile-header">
<h1>Профиль пользователя</h1>
<div class="document-container resp-container">
<div id="navigation-panel" class="panel">
<a href="/list-rooms" id="go-to-chat-list" class="panel-thing">
<img alt="Go to list of chats" src="/assets/img/list-rooms.svg" width="32px">
</a>
<a href="/user/{% WRITE userinfo.nickname %}" id="go-to-my-profile" class="panel-thing">
<img alt="Go to my profile" src="/assets/img/user.svg" width="32px">
</a>
</div>
<div class="columns">
<div class="column">
<p>{% WRITE userprofile.name %} ( {% WRITE userprofile.nickname %} )</p>
</div>
</div>
<div class="additional-info">
<p>О себе</p>
<p>{% WRITE userprofile.bio %}</p><br>
<div class="profile-container">
<h2 class="profile-name-text">{% WRITE alienprofile.name %}</h2>
<h3 class="profile-nickname-text">Nickname: {% WRITE alienprofile.nickname %}</h3>
<p class="profile-bio-text">
{% WRITE alienprofile.bio %}
</p>
</div>
</div>
</body>
</html>
{% ENDELDEF%}

View File

@ -0,0 +1,33 @@
#CM-btn-add {
margin-top: 6px;
margin-bottom: 4px;
display: none;
}
.CM-member-box {
display: flex;
flex-direction: row;
}
.CL-member-box-nickname {
margin-left: 8px;
justify-self: flex-start;
}
.CM-member-box-name {
margin-left: 14px;
justify-self: flex-start;
}
.CM-member-box-role {
margin-left: auto;
justify-self: flex-end;
}
.CM-member-box-leave-btn {
margin-left: 10px;
margin-right: 8px;
justify-self: flex-end;
width: 16px;
cursor: pointer;
}

View File

@ -1,202 +1,142 @@
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #e5e5e5;
}
.chat-container {
width: 100%;
max-width: 800px;
height: 90vh;
background-color: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
border-radius: 8px;
overflow: hidden;
}
.chat-header {
background-color: #007bb5;
color: white;
padding: 25px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
}
.room-name {
position: absolute;
left: 50%;
font-size: 24px;
}
.members {
border: none;
position: absolute;
left: 80%;
border-radius: 10px;
cursor: pointer;
width: 150px;
background-color: #f7f7f7;
height: 25px;
transition: background-color 0.3s ease;
}
.members:hover {
background-color: #218838;
}
.chat-messages {
flex: 1;
padding: 15px;
overflow-y: auto;
background-color: #f7f7f7;
}
.chat-message {
display: flex;
align-items: flex-start;
margin-bottom: 15px;
}
.chat-message .avatar {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
margin-right: 10px;
}
.chat-message .avatar img {
width: 100%;
body, html {
height: 100%;
object-fit: cover;
}
.chat-message .message-content {
max-width: 70%;
#chat-widget {
position: relative;
flex: 1;
background-color: #f1f1f1;
overflow: hidden;
}
.message-supercontainer{
position: absolute;
width: 100%;
left: 0;
background-color: rgba(150, 0, 100, 50);
/*display: flex;*/
/*flex-direction: row;*/
/*justify-content: center;*/
}
.message-box{
/*display: inline-block;*/
padding: 5px;
}
.message-box-mine {
margin-right: 5px;
margin-left: auto;
max-width: 400px;
border: 2px solid #82a173;
padding: 5px;
background-color: #cdff9b;
color: black;
/*justify-self: flex-end;*/
}
.message-box-alien {
margin-left: 5px;
margin-right: auto;
max-width: 400px;
border: 2px solid dimgrey;
padding: 5px;
background-color: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
color: black;
/*justify-self: flex-start;*/
}
.chat-message .message-content .username {
/* Only non-system messages can be deleted. Deleted messages do not have delete button
This class should be used with (and, ofcourse, after) class message-box-my/message-box-alien */
.message-box-deleted {
border: 2px solid #cb0005;
background-color: #ffc1bc;
}
.message-box-deleted .message-box-msg{
font-weight: bold;
margin-bottom: 5px;
}
.chat-message .message-content .text {
.message-box-system {
margin-left: auto;
margin-right: auto;
max-width: 500px;
padding: 4px;
background-color: #2d2d2d;
color: white;
font-weight: bold;
justify-self: center;
}
/* in #chat-widget .message-box */
.message-box-top{
/* You see, each message contains a 20+2+2 px high icon that HAS TO BE LOADED FIRST.
This happens after window.onload, so I added a crutch: loading won't update height in
unpredictable moment. cause it will be already high enough. BUGA-GA-GA!! */
min-height: 30px;
display: block;
}
.message-box-sender-name{
color: black;
text-decoration: none;
padding: 2px;
display: inline;
font-size: 0.8em;
}
/* Additional to message-box-sender-name */
.message-box-sender-shortname {
font-weight: bold;
padding-left: 3px;
font-size: 0.94em;
}
.message-box-sender-name:hover{
color: #1060ff
}
.message-box-button{
width: 20px;
padding: 2px;
cursor: pointer;
display: inline;
}
.message-box-msg{
word-wrap: break-word;
}
.chat-footer {
display: flex;
#input-panel {
min-height: 20px;
}
#message-input {
padding: 15px;
padding-left: 50px;
border-top: 1px solid #ddd;
}
.chat-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 20px;
margin-right: 10px;
outline: none;
}
.chat-send-button {
padding: 10px 20px;
border: none;
background-color: #0088cc;
color: white;
border-radius: 20px;
cursor: pointer;
outline: none;
}
.members-list {
display: none;
position: fixed;
background-color: #fff;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 400px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.members-list-header {
display: flex;
}
.all-members {
position: absolute;
left: 32%;
top: 0%;
margin-bottom: 30px;
font-family: Arial, sans-serif;
}
.close {
position: absolute;
right: 5%;
font-size: 24px;
font-weight: bold;
}
.members-list span {
cursor: pointer;
}
.members-list-body ul {
list-style-type: none;
left: 0%;
}
.members-list-body img {
margin-top: 10px;
left: 0%;
height: 30px;
width: 30px;
border-radius: 50%;
}
.members-list-body a {
margin-left: 5px;
margin-top: 10px;
text-decoration: none;
color: black;
}
.members-list-body a:hover {
text-decoration: underline;
color: #0088cc;
}
.members-list-body button {
padding: 5px 10px;
border: none;
background-color: #dc2e45;
color: white;
border-radius: 20px;
position: absolute;
left: 300px;
margin-top: 20px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.members-list-body button:hover {
background-color: #881527;
}
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
height: auto;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 1000;
display: inline-block;
background-color: white;
border: 1px solid #1000d0;
border-radius : 7px;
font-size: .9rem;
margin: 10px;
}
.chat-send-button:hover {
background-color: #007bb5;
.message-in-popup-preview{
border: 4px solid red;
width: 80%;
max-width: 200px;
margin-left: auto;
margin-right: auto;
max-height: 20%;
word-wrap: break-word;
}
.loading-spinner{
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0);
width: 25px;
display: block;
}

View File

@ -0,0 +1,50 @@
.popup-overlay-veil {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
z-index: 99;
display: none; /* Hidden by default */
}
.popup-window {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
z-index: 100;
display: none;
}
.popup-btn {
display: inline;
padding: 5px;
border-bottom: 3px;
}
.popup-window-btn-yes {
background-color: #0c7f0e;
border-radius: 5px;
padding: 12px;
color: white;
}
.popup-window-btn-no {
background-color: #ff0005;
border-radius: 5px;
padding: 12px;
color: white;
}
.popup-window-msg {
padding-left: 20px;
font-weight: bold;
font-size: 1.3em;
}

206
assets/css/common.css Normal file
View File

@ -0,0 +1,206 @@
/* Profile view elements */
.profile-container {
background: white;
border-radius: 5px;
padding: 20px;
margin-top: 60px; /* Space below the fixed panel */
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.3);
}
.profile-name-text {
color: black;
}
.profile-nickname-text{
color: #444;
text-align: left;
}
.profile-bio-text {
padding-top: 40px;
text-align: left;
line-height: 1.6;
color: black;
}
/* Panels */
.panel {
width: 100%;
border: 2px solid blue;
background-color: #54b3ff;
display: flex;
flex-direction: row;
align-items: center;
}
.panel-thing {
padding: 6px;
}
.panel-header-txt{
color: white;
font-size: 1.9em;
flex: 1;
text-align: center;
}
/* Containers for the whole document */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Arial, sans-serif;
}
.document-container {
width: 80%; /* Full width of the viewport */
margin: 0 auto; /* Center the container horizontally */
}
.fullscreen-container {
width: 80%; /* Full width of the viewport */
height: 100vh; /* Full height of the viewport */
display: flex;
flex-direction: column; /* Stack children vertically */
margin: 0 auto; /* Center the container horizontally */
}
@media (orientation: landscape) {
.resp-container{
width: 80%;
}
}
@media (orientation: portrait){
.resp-container{
width: 100%;
}
}
body {
background-color: #f000f0;
background-image: url("/assets/img/clavicle-transparent.png"), url("/assets/img/broken-clavicle.png");
background-repeat: revert;
background-size: 10%, 25%;
}
/* Notifications, returned from server and embedded into html page at render-time */
.server-notif-error-msg-box{
font-size: 1.3em;
text-align: center;
padding: 10px;
border: 2px solid red;
border-radius: 30px;
background-color: #ff5050;
max-width: 40%;
margin: 15px auto;
}
/* Centered headers */
.wide-centered-header {
width: 100%;
text-align: center;
font-size: 1.4em;
}
/* Cool buttons with text */
.action-button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;
}
.action-button:hover {
background-color: #0056b3; /* Darker blue on hover */
}
/* This is for centering non-100%wide block */
.centered-block-el {
display: block;
margin-left: auto;
margin-right: auto;
}
/* Beautiful text input */
.one-line-input {
width: 100%;
padding: 8px;
margin: 8px 0;
border: 1px solid #ccc;
border-radius: 4px;
}
.multiline-input {
width: 100%;
/*max-width: 600px;*/
height: 200px;
padding: 10px;
font-size: 1.15em;
border: 2px solid #ccc;
border-radius: 5px;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1); /* Subtle shadow */
outline: none; /* Remove default outline on focus */
resize: vertical; /* Allow resizing vertically */
transition: border-color 0.15s, box-shadow 0.15s; /* Smooth transition for border color and shadow */
}
.multiline-input:focus {
border-color: #007bff; /* Change border color on focus */
box-shadow: 0 0 5px rgba(0, 123, 255, 0.5); /* Shadow on focus */
}
/* Handles the case of list of elements with dickanme, name, role and delete button
For list of chats and list of users in chat */
.dynamic-block-list {
margin-top:12px;
display: flex;
flex-direction: column;
background-color: white;
border: 1px solid #c7c7c7;
align-items: stretch;
padding-left: 8px;
padding-right: 8px;
padding-bottom: 8px;
}
.dynamic-block-list-el {
margin-top: 8px;
background-color: white;
border: 1px solid #c7c7c7;
color: black;
padding: 5px;
}
.button-add{
width: 50px;
cursor: pointer;
}
.dynamic-block-list-el-container{
width: 100%;
}
.entity-nickname-txt {
font-weight: bold;
color: black;
text-decoration: none;
font-size: 1.5em;
}
.entity-reg-field-txt {
/* For name and role */
color: #242424;
text-decoration: none;
font-size: 1.5em;
}

8
assets/css/debug.css Normal file
View File

@ -0,0 +1,8 @@
.chat-debug-rect{
width: 100%;
position: absolute;
left: 0;
opacity: 0.3;
height: 3px;
z-index: 2;
}

View File

@ -0,0 +1,23 @@
/* The morbid thing */
table.logins-input-table {
width: 100%;
border-collapse: collapse; /* Combine borders */
}
.logins-input-td1, .logins-input-td2 {
border: none;
}
.logins-input-td1 {
text-align: left;
padding-right: 5px;
white-space: nowrap; /* Prevent text wrap, keeping it in one line */
overflow: hidden; /* Hide overflow content */
text-overflow: ellipsis; /* Show ellipsis for overflowing text */
}
.logins-input-td2 {
width: 100%;
}
#input-change-bio{
margin-top: 5px;
margin-bottom: 5px;
}

View File

@ -1,253 +1,51 @@
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
margin: 0;
padding: 0;
#CL-bacbe {
margin-top: 6px;
margin-bottom: 4px;
}
.container {
max-width: 800px;
margin: 30px auto;
padding: 20px;
background-color: #007bff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
h1 {
text-align: center;
color: #fff;
}
.room-list {
list-style-type: none;
padding: 0;
}
.room-item {
.CL-my-chat-box {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
margin: 10px 0;
background-color: #fafafa;
border: 1px solid #ddd;
border-radius: 5px;
transition: background-color 0.3s ease;
flex-direction: row;
}
.room-item:hover {
background-color: #eaeaea;
.CL-my-chat-box-nickname {
margin-left: 8px;
justify-self: flex-start;
}
.room-name {
font-size: 18px;
color: #555;
.CL-my-chat-box-name {
margin-left: 14px;
justify-self: flex-start;
}
.join-button {
padding: 10px 15px;
font-size: 16px;
color: white;
background-color: #007bff;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.add-members-header {
text-align: center;
}
.add-members-footer {
text-align: right;
margin-top: 5px;
}
.add-members-button {
background-color: #218838;
padding: 10px 15px;
font-size: 16px;
color: white;
border: none;
border-radius: 5px;
position: absolute;
margin-left: 502px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.add-members-button:hover {
background-color: #006509
}
.delete-chat-button {
background-color: #dc2e45;
border: none;
color: white;
font-size: 16px;
border-radius: 5px;
position: absolute;
cursor: pointer;
transition: background-color 0.3s ease;
padding: 10px 15px;
margin-left: 380px;
}
.delete-chat-button:hover {
background-color: #881527;
}
#newMemberLogin {
width: 93.5%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 5px;
}
.add-member-button {
background-color: #218838;
padding: 10px 15px;
font-size: 16px;
color: white;
border: none;
border-radius: 5px;
position: absolute;
margin-left: -105px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.join-button:hover {
background-color: #0056b3;
.CL-my-chat-box-my-role {
margin-left: auto;
justify-self: flex-end;
}
.modal {
display: none;
position: fixed;
z-index: 1;
left: 0;
top: 0;
.CL-my-chat-box-leave-btn {
margin-left: 10px;
margin-right: 8px;
justify-self: flex-end;
width: 16px;
cursor: pointer;
}
/* The morbid thing */
table.id-str-input-table {
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
border-collapse: collapse; /* Combine borders */
}
.modal-content {
background-color: #fff;
margin: 10% auto;
padding: 20px;
border: 1px solid #888;
width: 80%;
max-width: 400px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
.modal-header, .modal-footer {
padding: 10px;
color: #333;
}
.modal-header {
text-align: center;
}
.modal-footer {
text-align: right;
}
.modal input {
width: 93.5%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ddd;
border-radius: 5px;
}
.create-room-button {
display: block;
width: 100%;
padding: 10px;
font-size: 16px;
color: white;
background-color: #1609ab;
.id-str-input-td1, .id-str-input-td2 {
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
margin-top: 20px;
}
.create-room-button:hover {
background-color: #218838;
.id-str-input-td1 {
text-align: left;
padding-right: 5px;
white-space: nowrap; /* Prevent text wrap, keeping it in one line */
overflow: hidden; /* Hide overflow content */
text-overflow: ellipsis; /* Show ellipsis for overflowing text */
}
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
.id-str-input-td2 {
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
z-index: 1000;
}
.overlay .add-members {
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
max-width: 400px;
width: 100%;
height: 18%;
position: fixed;
}
.overlay .delete-chat {
background-color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
max-width: 400px;
width: 100%;
height: 18%;
position: fixed;
}
.delete-close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.delete-chat-header {
text-align: center;
}
.confirm {
background-color: #1609ab;
padding: 20px 70px;
font-size: 16px;
color: white;
border: none;
border-radius: 5px;
position: absolute;
margin-left: 20px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.cancel {
background-color: #1609ab;
padding: 20px 70px;
font-size: 16px;
color: white;
border: none;
border-radius: 5px;
position: absolute;
margin-left: 220px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.close {
color: #aaa;
float: right;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}

View File

@ -1,59 +1,34 @@
/* I have no idea what is going on here */
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
height: 100vh; /* Full viewport height */
margin: 0;
background-color: #e5e5e5;
}
.form-container {
width: 100%;
max-width: 400px;
background-color: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
border-radius: 8px;
padding: 40px;
text-align: center;
background-color: #ffffff; /* Brighter box color */
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
width: 50%; /* Set width of the form */
}
h1 {
margin-bottom: 20px;
color: #2F4F4F;
}
input {
/* The morbid thing */
table.logins-input-table {
width: 100%;
background: #f7f7f7;
font-size: 16px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 20px;
margin-bottom: 15px;
outline: none;
border-collapse: collapse; /* Combine borders */
}
button {
width: 100%;
padding: 15px;
.logins-input-td1, .logins-input-td2 {
border: none;
background-color: #0088cc;
color: white;
border-radius: 20px;
cursor: pointer;
outline: none;
font-size: 16px;
font-weight: bold;
transition: background-color 0.3s;
}
.error-msg {
color: red;
background-color: #ffc0c0;
border-color: red;
border-radius: 5px;
.logins-input-td1 {
padding-right: 5px;
white-space: nowrap; /* Prevent text wrap, keeping it in one line */
overflow: hidden; /* Hide overflow content */
text-overflow: ellipsis; /* Show ellipsis for overflowing text */
}
.logins-input-td2 {
width: 100%;
}

View File

@ -1,129 +0,0 @@
body {
display: flex;
justify-content: center;
align-items: center;
height: 90vh;
background-color: #e5e5e5;
font-family: Arial, sans-serif;
}
.main-container {
width: 700px;
height: 700px;
border-color: antiquewhite;
background-color: white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
border-radius: 10px;
}
.profile-header {
width: 700px;
height: 160px;
border-color: antiquewhite;
background-color: #0088cc;
border-radius: 10px;
position: relative;
}
.return {
background-color: #f0f0f0;
cursor: pointer;
width: 100px;
text-decoration: none;
color: black;
display: flex;
justify-content: center;
align-items: center;
height: 30px;
border-radius: 10px;
position: absolute;
left: 20px;
top: 25px;
border: none;
}
.return:hover{
text-decoration: underline;
color: #0088cc;
}
form {
display: flex;
flex-direction: column;
align-items: center;
}
.columns {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 20px;
margin-bottom: 20px;
}
.column {
display: flex;
flex-direction: column;
align-items: center;
}
.add {
width: 100px;
height: 40px;
border-width: 2px;
cursor: pointer;
font-size: 16px;
border-radius: 10px;
}
.add:hover {
background-color: #007bb5;
}
.image-button:hover {
opacity: 0.8;
}
.image-button:active {
transform: scale(0.95);
}
#login {
font-family: Arial, sans-serif;
font-size:16px;
width: 150px;
height: 20px;
border-radius: 10px;
border-color: #2F4F4F;
}
#username {
font-family: Arial, sans-serif;
font-size:16px;
width: 150px;
height: 20px;
margin-bottom: 1px;
margin-top: 50px;
border-radius: 10px;
border-color: #2F4F4F;
}
#bio {
height: 150px;
width: 500px;
padding: 10px;
box-sizing: border-box;
font-family: Arial, sans-serif;
font-size:14px;
text-align: left;
vertical-align: top;
margin-bottom: 5px;
}
.save {
cursor:pointer;
font-size: 16px;
border-radius: 15px;
border-color: #2F4F4F;
height: 40px;
width: 150px;
}
.save:hover {
background-color: #007bb5;
}
.avatar {
border-radius: 50%;
object-fit: cover;
}

View File

@ -1,77 +0,0 @@
dy {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #e5e5e5;
}
.form-container {
width: 100%;
max-width: 400px;
background-color: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
border-radius: 8px;
padding: 40px;
text-align: center;
}
h1 {
margin-bottom: 20px;
color: #2F4F4F;
}
input {
width: 100%;
background: #f7f7f7;
font-size: 16px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 20px;
margin-bottom: 15px;
outline: none;
}
button {
width: 100%;
padding: 15px;
border: none;
background-color: #0088cc;
color: white;
border-radius: 20px;
cursor: pointer;
outline: none;
font-size: 16px;
font-weight: bold;
transition: background-color 0.3s;
}
button:hover,
button:focus-visible {
background-color: #007bb5;
}
.hide-cursor::placeholder {
color: #000;
}
.hide-cursor {
caret-color: transparent;
}
.no-select {
-webkit-user-select: none; /* Для Safari */
-moz-user-select: none; /* Для Firefox */
user-select: none; /* Для всех остальных браузеров */
}
div {
color: red;
font-size: 15px;
margin-top: 10px;
display: none;
}

BIN
assets/gif/loading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

20
assets/img/add.svg Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="128"
height="128"
viewBox="0 0 3.84 3.84"
fill="none"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 1.9142674,3.7842088 c -0.8814997,0 -1.32224859,0 -1.59609402,-0.2738529 C 0.04432607,3.2365216 0.04432607,2.7957577 0.04432607,1.9142674 c 0,-0.8814997 0,-1.32224859 0.27384731,-1.59609402 C 0.59201881,0.04432607 1.0327677,0.04432607 1.9142674,0.04432607 c 0.8814903,0 1.3222542,0 1.5960885,0.27384731 0.2738529,0.27384543 0.2738529,0.71459432 0.2738529,1.59609402 0,0.8814903 0,1.3222542 -0.2738529,1.5960885 C 3.2365216,3.7842088 2.7957577,3.7842088 1.9142674,3.7842088 Z m 0,-2.5711694 c 0.077453,0 0.1402457,0.062791 0.1402457,0.1402456 v 0.4207368 h 0.4207367 c 0.077453,0 0.1402456,0.062793 0.1402456,0.1402456 0,0.077453 -0.062793,0.1402457 -0.1402456,0.1402457 H 2.0545131 v 0.4207367 c 0,0.077453 -0.062793,0.1402456 -0.1402457,0.1402456 -0.077453,0 -0.1402456,-0.062793 -0.1402456,-0.1402456 V 2.0545131 H 1.353285 c -0.077455,0 -0.1402456,-0.062793 -0.1402456,-0.1402457 0,-0.077453 0.062791,-0.1402456 0.1402456,-0.1402456 H 1.7740218 V 1.353285 c 0,-0.077455 0.062793,-0.1402456 0.1402456,-0.1402456 z"
fill="#1c274c"
id="path1"
style="stroke-width:0.186994" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

18
assets/img/delete.svg Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
fill="#000000"
width="128"
height="128"
viewBox="-1.7 0 3.264 3.264"
class="cf-icon-svg"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<path
d="M 1.4376583,1.6137431 A 1.5211864,1.5211864 0 1 1 -0.08352807,0.09255663 1.5209944,1.5209944 0 0 1 1.4376583,1.6137431 Z m -1.30733258,0.00192 0.58257383,-0.582766 A 0.15217629,0.15217629 0 0 0 0.49770077,0.81769971 L -0.08468094,1.4004657 -0.66763909,0.81769971 A 0.15217629,0.15217629 0 0 0 -0.88283792,1.0328985 l 0.5829582,0.5827659 -0.58276597,0.5827661 a 0.15217629,0.15217629 0 0 0 0.21519874,0.2150066 l 0.58257388,-0.582766 0.58276608,0.582766 A 0.15217629,0.15217629 0 0 0 0.71309179,2.1982382 Z"
id="path1"
style="display:inline;fill:#ff0000;fill-opacity:1;stroke-width:0.192143" />
</svg>

After

Width:  |  Height:  |  Size: 929 B

BIN
assets/img/exit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

BIN
assets/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

30
assets/img/link.svg Normal file
View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
fill="#000000"
version="1.1"
id="Capa_1"
width="128"
height="128"
viewBox="0 0 70.75936 70.75936"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs3" />
<g
id="g3"
style="fill:#c8f5ff;fill-opacity:1;stroke:#6adeef;stroke-width:8.62702;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(0.1490509,0,0,0.14948634,3.4471925,2.5065977)">
<g
id="g2"
style="fill:#c8f5ff;fill-opacity:1;stroke:#6adeef;stroke-width:8.62702;stroke-dasharray:none;stroke-opacity:1">
<path
d="m 409.657,32.474 c -43.146,-43.146 -113.832,-43.146 -156.978,0 l -84.763,84.762 c 29.07,-8.262 60.589,-6.12 88.129,6.732 l 44.063,-44.064 c 17.136,-17.136 44.982,-17.136 62.118,0 17.136,17.136 17.136,44.982 0,62.118 l -55.386,55.386 -36.414,36.414 c -17.136,17.136 -44.982,17.136 -62.119,0 l -47.43,47.43 c 11.016,11.017 23.868,19.278 37.332,24.48 36.415,14.382 78.643,8.874 110.467,-16.219 3.06,-2.447 6.426,-5.201 9.18,-8.262 l 57.222,-57.222 34.578,-34.578 c 43.453,-43.145 43.453,-113.525 10e-4,-156.977 z"
id="path1"
style="fill:#c8f5ff;fill-opacity:1;stroke:#6adeef;stroke-width:8.62702;stroke-dasharray:none;stroke-opacity:1" />
<path
d="m 184.135,320.114 -42.228,42.228 c -17.136,17.137 -44.982,17.137 -62.118,0 -17.136,-17.136 -17.136,-44.981 0,-62.118 l 91.8,-91.799 c 17.136,-17.136 44.982,-17.136 62.119,0 l 47.43,-47.43 c -11.016,-11.016 -23.868,-19.278 -37.332,-24.48 -38.25,-15.3 -83.232,-8.262 -115.362,20.502 -1.53,1.224 -3.06,2.754 -4.284,3.978 l -91.8,91.799 c -43.146,43.146 -43.146,113.832 0,156.979 43.146,43.146 113.832,43.146 156.978,0 l 82.927,-83.845 c -42.23,9.791 -52.022,8.568 -88.13,-5.814 z"
id="path2"
style="fill:#c8f5ff;fill-opacity:1;stroke:#6adeef;stroke-width:8.62702;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

28
assets/img/list-rooms.svg Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="127.99999"
height="127.99999"
viewBox="0 0 33.866664 33.866664"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1">
<path
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 6.2911505,3.4844006 V 6.6404405 H 3.0362141 V 28.776832 h 4.7418555 v 2.929792 H 0.58956039 V 3.438992 Z"
id="path63" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 27.557741,31.386555 v -3.15604 h 3.254937 V 6.0941238 h -4.741856 v -2.929792 h 7.188509 V 31.431963 Z"
id="path63-2" />
<path
id="rect63"
style="fill:#fdffff;fill-opacity:1;fill-rule:nonzero;stroke:#e1e1e1;stroke-width:0.529167;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 8.7183307,8.3901854 c -2.467802,0 -4.4545084,1.9867066 -4.4545084,4.4545086 v 5.357812 c 0,2.467802 1.9867064,4.454509 4.4545084,4.454509 0,0 6.6668103,0 10.2365843,0 1.184667,0 1.8523,1.573101 2.973462,1.946651 1.827305,0.608823 5.758305,0.474906 5.758305,0.474906 0,0 -1.204106,-0.984163 -2.336589,-2.042039 -0.307888,-0.287603 -0.182877,-1.488944 0.134028,-1.896921 0.674916,-0.868872 0.918918,-1.680982 0.918918,-2.937106 v -5.357812 c 0,-2.467802 -1.986706,-4.4545086 -4.454508,-4.4545086 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.7 KiB

20
assets/img/return.svg Normal file
View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="128"
height="127.99999"
viewBox="0 0 33.866667 33.866664"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1">
<path
style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:0.294159;stroke-linejoin:round;stroke-opacity:1"
d="m 0.65443061,23.377766 10.20536539,-7.351693 -0.06427,3.749871 c 0,0 6.384296,0.115304 9.607291,0.115304 1.824632,0 4.010317,0.117998 5.361398,-1.065432 0.797635,-0.698657 1.041209,-1.943308 0.954461,-2.97946 -0.118465,-1.414973 -0.417733,-2.830021 -1.492491,-3.799298 -0.964892,-0.870193 -2.338758,-1.449022 -3.659301,-1.465666 -1.347581,-0.01698 -4.311633,-0.01476 -4.311633,-0.01476 l 0.02678,-6.0468658 c 0,0 5.209174,-0.1269759 7.836958,-0.1269759 2.090574,0 3.730335,0.2813604 5.374446,1.5274776 1.165995,0.8837399 2.874757,2.1212447 2.874757,3.8971142 0,3.5737799 -0.03341,6.8479459 -0.03341,10.6502529 0,3.155641 -2.159421,4.126978 -3.036427,4.888332 -1.783594,1.548393 -4.069763,1.553329 -6.468859,1.553329 -4.709142,0 -13.295569,0 -13.295569,0 v 3.244296 z"
id="path1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="127.99999"
height="127.99999"
viewBox="0 0 33.866664 33.866664"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1">
<path
id="path11"
style="fill:#e6e6e6;fill-opacity:1;stroke:#b3b3b3;stroke-width:0.792427;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 9.8376422,3.4995279 c -0.413454,0.04422 -0.9249931,0.2733392 -1.5859497,0.7720459 -2.15303,1.6245101 -1.0645387,2.7213368 -0.2780192,3.6576578 0.9643744,1.1480481 0.5272069,2.4297214 -0.2444295,3.5413854 -1.0846469,1.562605 -1.8094411,2.689497 -3.1812011,2.618445 -1.7185528,-0.08902 -3.2061983,0.950419 -3.1817179,2.618962 0.020151,1.373449 0.960489,2.763844 2.9987833,2.826184 1.3729571,0.04199 2.35275,1.613748 2.9993001,2.825667 0.6465497,1.211919 1.3490594,3.417259 0.6769613,4.064868 -0.989134,0.953094 -1.312826,2.263816 0.6769612,4.064868 1.2829023,1.161218 2.6693573,0.639536 3.7666953,-0.946195 1.179214,-1.70405 2.337505,-1.523233 4.127396,-1.422652 1.371435,0.07707 2.886284,-0.325958 3.858679,1.446423 0.660704,1.204262 1.722209,2.911554 3.85868,1.445906 1.132687,-0.77704 2.468135,-2.085182 0.947745,-4.010091 -0.610564,-0.773014 -0.532201,-2.186383 0.948263,-4.010607 1.255317,-1.546798 1.98091,-2.751433 3.181201,-2.618962 1.365308,0.150686 2.84107,-0.224934 3.181718,-2.618445 0.366273,-2.57357 -1.941907,-2.695322 -3.312976,-2.778641 C 28.1324,14.906856 28.128014,13.488893 26.590624,12.103137 25.053235,10.717381 25.023658,9.341954 26.190649,8.2930337 27.35764,7.2441129 27.633021,6.3291961 25.719877,4.5222045 24.596532,3.461188 23.254965,3.1719841 21.630204,4.9759236 20.911774,5.7735833 19.336054,7.1894888 17.196366,7.1086099 15.002057,7.0256665 13.838949,7.4611865 12.285555,5.8053303 11.434493,4.8981315 11.078004,3.3668691 9.8376422,3.4995279 Z m 6.7892498,6.5215661 a 7.4172139,7.0961218 0 0 1 7.417118,7.096207 7.4172139,7.0961218 0 0 1 -7.417118,7.096208 7.4172139,7.0961218 0 0 1 -7.4171184,-7.096208 7.4172139,7.0961218 0 0 1 7.4171184,-7.096207 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

24
assets/img/user.svg Normal file
View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="127.99999"
height="127.99999"
viewBox="0 0 33.866664 33.866664"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1">
<path
style="fill:#000080;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1.465;stroke-linejoin:round;stroke-dasharray:none"
d="M 9.3745975,32.81176 H 25.632941 c 0,0 0.08996,-6.044498 -0.03778,-10.988966 0.0321,-3.524476 -1.327193,-6.290541 -8.363226,-6.155275 -7.036034,0.135265 -7.7455423,2.805955 -7.7693511,5.331649 -0.034594,3.669826 -0.087981,11.812592 -0.087981,11.812592 z"
id="path7" />
<path
id="path8"
style="fill:#000080;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.965;stroke-linejoin:round;stroke-dasharray:none"
d="M 17.963761,1.3260172 A 11.612947,6.192831 0 0 0 6.3510334,7.5189208 11.612947,6.192831 0 0 0 17.963761,13.711824 11.612947,6.192831 0 0 0 29.576489,7.5189208 11.612947,6.192831 0 0 0 17.963761,1.3260172 Z m -3.947046,2.876827 a 1.5186517,3.4351659 0 0 1 1.518771,3.4349323 1.5186517,3.4351659 0 0 1 -1.518771,3.4354495 1.5186517,3.4351659 0 0 1 -1.518253,-3.4354495 1.5186517,3.4351659 0 0 1 1.518253,-3.4349323 z m 6.47299,0.2160074 a 1.5186517,3.4351659 0 0 1 1.518254,3.4349324 1.5186517,3.4351659 0 0 1 -1.518254,3.435449 1.5186517,3.4351659 0 0 1 -1.51877,-3.435449 1.5186517,3.4351659 0 0 1 1.51877,-3.4349324 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,188 @@
let LocalHistoryId = 0;
function genSentBase(){
return {
'chatUpdReq': {
'LocalHistoryId': LocalHistoryId,
'chatId': openedchat.id
}
};
}
let members = new Map();
let memberBoxes = new Map();
let myRoleHere = null; // Dung local state updates should be updated first
let userDeletionWinStoredUId = -1;
function shouldShowDeleteButton(memberSt){
return userinfo.uid !== memberSt.userId && myRoleHere === userChatRoleAdmin && memberSt.roleHere !== userChatRoleDeleted;
}
function updateBoxWithSt(box, memberSt){
let ID = memberSt.userId;
let roleP = box.querySelector(".CM-member-box-role");
roleP.innerText = memberSt.roleHere;
box.style.backgroundColor = roleToColor(memberSt.roleHere);
box.querySelector(".CM-member-box-leave-btn").style.display =
(shouldShowDeleteButton(memberSt) ? "block" : "none");
}
function convertMemberStToBox(memberSt){
let ID = memberSt.userId;
let userProfileURI = "/user/" + memberSt.nickname;
let box = document.createElement("div");
box.className = "dynamic-block-list-el CM-member-box";
box.style.backgroundColor = roleToColor(memberSt.roleHere);
let inBoxNickname = document.createElement("a");
box.appendChild(inBoxNickname);
inBoxNickname.className = "entity-nickname-txt CM-member-box-nickname";
inBoxNickname.innerText = memberSt.nickname;
inBoxNickname.href = userProfileURI;
let inBoxName = document.createElement("a");
box.appendChild(inBoxName);
inBoxName.className = "entity-reg-field-txt CM-member-box-name";
inBoxName.innerText = memberSt.name;
inBoxName.href = userProfileURI;
let inBoxUserRoleHere = document.createElement("p");
box.appendChild(inBoxUserRoleHere);
inBoxUserRoleHere.className = "entity-reg-field-txt CM-member-box-role";
inBoxUserRoleHere.innerText = memberSt.roleHere;
let inBoxLeaveBtn = document.createElement("img");
box.appendChild(inBoxLeaveBtn);
inBoxLeaveBtn.className = "CM-member-box-leave-btn";
inBoxLeaveBtn.src = "/assets/img/delete.svg";
inBoxLeaveBtn.onclick = function (ev) {
if (ev.button !== 0)
return;
userDeletionWinStoredUId = ID;
document.getElementById("user-deletion-win-title").innerText =
"Do you really want to kick user " + memberSt.nickname + "?";
activatePopupWindowById("user-deletion-win");
};
box.querySelector(".CM-member-box-leave-btn").style.display =
(shouldShowDeleteButton(memberSt) ? "block" : "none");
return box;
}
function updateLocalStateFromChatUpdResp(chatUpdResp){
LocalHistoryId = chatUpdResp.HistoryId;
// If my role is updated, we need to update all the boes of already set users (kick button can appear and disappear)
let literalMemberList = document.getElementById("CM-list");
// We ignore messages and everything related to them. Dang, I really should add an argument to disable message lookup here
for (let memberSt of chatUpdResp.members){
console.log([memberSt, userinfo.uid, myRoleHere]);
if (memberSt.userId === userinfo.uid && myRoleHere !== memberSt.roleHere){
myRoleHere = memberSt.roleHere;
for (let [id, memberSt] of members){
let box = memberBoxes.get(id);
updateBoxWithSt(box, memberSt);
}
document.getElementById("CM-btn-add").style.display =
(memberSt.roleHere === userChatRoleAdmin ? "block" : "none");
console.log("DEBUG " + (memberSt.roleHere === userChatRoleAdmin ? "block" : "none"));
break;
}
}
for (let memberSt of chatUpdResp.members){
let id = memberSt.userId;
if (members.has(id)){
updateBoxWithSt(memberBoxes.get(id), memberSt);
} else {
if (memberSt.roleHere !== userChatRoleDeleted){
members.set(id, memberSt);
let box = convertMemberStToBox(memberSt);
memberBoxes.set(id, box);
literalMemberList.appendChild(box);
}
}
}
}
function updateLocalStateFromRecv(Recv){
updateLocalStateFromChatUpdResp(Recv.chatUpdResp);
}
function configureSummonUserInterface(){
document.getElementById("user-summoning-yes").onclick = function(ev ){
if (ev.button !==0)
return;
let nickname = String(document.getElementById("summoned-user-nickname").value);
let isReadOnly = document.getElementById("summoned-user-is-read-only").checked;
deactivateActivePopup();
let Sent = genSentBase();
Sent.nickname = nickname;
Sent.makeReadOnly = Boolean(isReadOnly);
apiRequest("addMemberToChat", Sent).
then((Recv) => {
updateLocalStateFromRecv(Recv);
}).catch((e) => {
console.log(e);
alert("Failed to add user to chat");
});
};
document.getElementById("user-summoning-no").onclick = function (ev) {
if (ev.button !== 0)
return;
deactivateActivePopup();
};
document.getElementById("CM-btn-add").onclick = function(ev) {
if (ev.button !== 0)
return;
document.getElementById("summoned-user-nickname").value = "";
// read-only flag persists throughout user summoning sessions, and IT IS NOT A BUG
activatePopupWindowById("user-summoning-win");
};
}
/* Popup activation button is configured for each box separately */
function configureKickUserInterfaceWinPart(){
document.getElementById("user-deletion-yes").onclick = function (ev){
if (ev.button !== 0)
return;
deactivateActivePopup();
if (userDeletionWinStoredUId < 0)
throw new Error("Karaul");
let Sent = genSentBase();
Sent.userId = userDeletionWinStoredUId;
apiRequest("removeMemberFromChat", Sent).
then((Recv) => {
updateLocalStateFromRecv(Recv);
}).catch((e) => {
console.log(e);
alert("Failed to kick user from chat");
});
}
document.getElementById("user-deletion-no").onclick = function (ev) {
if (ev.button !== 0)
return;
deactivateActivePopup();
};
}
__mainloopDelayMS = 5000;
__guestMainloopPollerAction = function (){
console.log("Hello, world");
apiRequest("chatPollEvents", genSentBase()).
then((Recv) => {
console.log(Recv);
updateLocalStateFromRecv(Recv);
});
}
window.onload = function(){
console.log("Page loaded");
configureSummonUserInterface();
configureKickUserInterfaceWinPart();
updateLocalStateFromChatUpdResp(initial_chatUpdResp);
mainloopPoller();
}

View File

@ -1,162 +1,449 @@
let members = [
{ username: 'Адель', nickname: 'cold_siemens52', avatar: 'https://sun9-59.userapi.com/impg/t8GhZ7FkynVifY1FQCnaf31tGprbV_rfauZzgg/fSq4lyc6V0U.jpg?size=1280x1280&quality=96&sign=e3c309a125cb570d2e18465eba65f940&type=album' },
{ username: 'Антон', nickname: 'antyak_01', avatar: 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png' },
{ username: 'Владимир', nickname: 'kkrkk2006', avatar: 'https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png' }
];
let currentHistoryId = 0;
let currentChatID = null;
function renderMembersList() {
const membersListBody = document.getElementById('members-list-body');
membersListBody.innerHTML = '';
let LocalHistoryId = 0;
members.forEach((member, index) => {
const memberItem = document.createElement('li');
memberItem.innerHTML = `
<img src="${member.avatar}" alt="${member.username}">
<a href="profile.html">${member.username}</a>
<button class="delete-member" onclick="deleteMember(${index})">Удалить из чата</button>
`;
membersListBody.appendChild(memberItem);
});
}
let members = new Map();
function deleteMember(index) {
members.splice(index, 1);
renderMembersList();
}
let loadedMessages = new Map(); // messageSt objects
/*
container: EL, box: EL, offset: number (msgPres) */
let visibleMessages = new Map(); // HTMLElement objects
async function sendMessage() {
const chatMessages = document.getElementById('chat-messages');
const chatInput = document.getElementById('chat-input');
const message = chatInput.value;
let anchoredMsg = -1;
let visibleMsgSegStart = -1;
let visibleMsgSegEnd = -2;
let offsetOfAnchor = 500;
let highestPoint = null;
let lowestPoint = null;
if (message.trim() !== '') {
const request = {
'chatId': currentChatID,
'LocalHistoryId': currentHistoryId,
'content': {
'text': message
let lastMsgId = -1;
let myRoleHere = null; // Dung local state updates should be updated first
// Would start with true if opened `/chat/<>`
let bumpedAtTheBottom = false;
// Hidden variable. When deletion window popup is active
// Persists from popup activation until popup deactivation
let storeHiddenMsgIdForDeletionWin = -1;
// Positive in production, negative for debug
let softZoneSz = -150;
let chatPadding = 300;
function genSentBase(){
return {
'chatUpdReq': {
'LocalHistoryId': LocalHistoryId,
'chatId': openedchat.id
}
};
const response = await fetch("/internalapi/sendMessage", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
});
const res = await response.json();
if (res.update) {
const update = res.update[0];
currentHistoryId = update.HistoryId;
const messageElement = document.createElement('div');
messageElement.classList.add('chat-message');
const avatarElement = document.createElement('div');
avatarElement.classList.add('avatar');
const avatarImage = document.createElement('img');
avatarImage.src = 'https://sun9-59.userapi.com/impg/t8GhZ7FkynVifY1FQCnaf31tGprbV_rfauZzgg/fSq4lyc6V0U.jpg?size=1280x1280&quality=96&sign=e3c309a125cb570d2e18465eba65f940&type=album';
avatarElement.appendChild(avatarImage);
const messageContentElement = document.createElement('div');
messageContentElement.classList.add('message-content');
const usernameElement = document.createElement('div');
usernameElement.classList.add('username');
usernameElement.textContent = await getUserName();
const textElement = document.createElement('div');
textElement.classList.add('text');
textElement.textContent = message;
messageContentElement.appendChild(usernameElement);
messageContentElement.appendChild(textElement);
messageElement.appendChild(avatarElement);
messageElement.appendChild(messageContentElement);
chatMessages.appendChild(messageElement);
chatInput.value = '';
chatMessages.scrollTop = chatMessages.scrollHeight;
}
}
}
function openMembersList() {
renderMembersList();
document.getElementById("members-list").style.display = "block";
document.getElementById("overlay").style.display = "flex";
function genSentBaseGMN(){
let Sent = genSentBase();
Sent.amount = 1;
return Sent;
}
function closeMembersList() {
document.getElementById("members-list").style.display = "none";
document.getElementById("overlay").style.display = "none";
function getChatWgSz(){
let chatWg = document.getElementById("chat-widget");
return [chatWg.offsetWidth, chatWg.offsetHeight];
}
document.getElementById('chat-input').addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
sendMessage();
function elSetOffsetInChat(el, offset){
el.style.bottom = String(offset) + "px";
}
});
async function getUserID() {
const response = await fetch('/internalapi/mirror', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
});
function isMissingPrimaryMsgHeap(){
return lastMsgId >= 0 && anchoredMsg < 0;
}
const res = await response.json();
return res.id;
function isMissingTopMsgHeap(){
let [W, H] = getChatWgSz();
return anchoredMsg >= 0 && (highestPoint < H + softZoneSz && visibleMsgSegStart > 0);
}
async function getChatID() {
const chatNickname = window.location.pathname.split('/').pop();
const response = await fetch('/internalapi/getChatList', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
});
const res = await response.json();
for (const chat of res.chats) {
if (chat.content.nickname === chatNickname) {
return chat.id;
function isMissingBottomMsgHeap(){
return anchoredMsg >= 0 && (lowestPoint > - softZoneSz && visibleMsgSegEnd < lastMsgId);
}
function updateOffsetOfVisibleMsg(msgId, offset){
visibleMessages.get(msgId).container.style.bottom = String(offset) + "px";
}
function updateOffsetsUpToTop(){
let offset = offsetOfAnchor;
for (let curMsg = anchoredMsg; curMsg >= visibleMsgSegStart; curMsg--){
updateOffsetOfVisibleMsg(curMsg, offset);
let height = visibleMessages.get(curMsg).container.offsetHeight;
offset += height + 5;
}
return offset;
}
function updateOffsetsDown(){
let offset = offsetOfAnchor;
for (let curMsg = anchoredMsg + 1; curMsg <= visibleMsgSegEnd; curMsg++){
let height = visibleMessages.get(curMsg).container.offsetHeight;
offset -= (height + 5);
updateOffsetOfVisibleMsg(curMsg, offset);
}
return offset;
}
function updateOffsetsSane(){
if (anchoredMsg < 0)
return;
highestPoint = updateOffsetsUpToTop();
lowestPoint = updateOffsetsDown();
}
function heightOfPreloadGhost(){
let [W, H] = getChatWgSz();
return Math.min(H * 0.9, Math.max(H * 0.69, 30));
}
function updateOffsets(){
let spinnerTop = document.getElementById("top-loading");
let spinnerBottom = document.getElementById("bottom-loading");
let SbH = spinnerBottom.offsetHeight;
if (anchoredMsg < 0){
hideHTMLElement(spinnerBottom);
elSetOffsetInChat(spinnerTop, chatPadding);
setElementVisibility(spinnerTop, isMissingPrimaryMsgHeap());
} else {
updateOffsetsSane();
let [W, H] = getChatWgSz();
let lowestLowestPoint = isMissingBottomMsgHeap() ? lowestPoint - heightOfPreloadGhost(): lowestPoint;
let highestHighestPoint = isMissingTopMsgHeap() ? highestPoint + heightOfPreloadGhost() : highestPoint;
if (lowestLowestPoint > chatPadding || (highestHighestPoint - lowestLowestPoint) <= H - chatPadding * 2) {
bumpedAtTheBottom = true;
offsetOfAnchor += (-lowestLowestPoint + chatPadding);
updateOffsetsSane();
} else if (highestHighestPoint < H - chatPadding) {
offsetOfAnchor += (-highestHighestPoint + (H - chatPadding));
updateOffsetsSane();
}
/* Messages weere updated (and only them). They were talking with ghosts.
Now we are trying to show spinners of ghosts */
elSetOffsetInChat(spinnerTop, highestPoint);
setElementVisibility(spinnerTop, isMissingTopMsgHeap());
elSetOffsetInChat(spinnerBottom, lowestPoint - SbH);
setElementVisibility(spinnerBottom, isMissingBottomMsgHeap());
}
}
return -1;
function shouldShowDeleteMesgBtn(messageSt){
return !messageSt.isSystem && messageSt.exists && (messageSt.myRoleHere !== userChatRoleReadOnly) &&(
messageSt.myRoleHere === userChatRoleAdmin || messageSt.senderUserId === userinfo.uid);
}
async function editMessage(new_message) {
const req = {
'chatId': currentChatID,
'LocalHistoryId': currentHistoryId,
'id': getUserID(),
'content': {
'text': new_message
// todo: fix messageboxes
function getMsgTypeClassSenderBased(messageSt){
if (messageSt.isSystem)
return "message-box-system"
if (messageSt.senderUserId === userinfo.uid)
return "message-box-mine"
return "message-box-alien";
}
function getMsgFullTypeClassName(messageSt){
return getMsgTypeClassSenderBased(messageSt) + (messageSt.exists ? "" : " message-box-deleted");
}
/* Two things can be updated: messages existance and delete button visibility
* Supercontainer.container is persistent, Supercontainer.box can change it's class */
function updateMessageSupercontainer(supercontainer, messageSt){
let box = supercontainer.box;
setElementVisibility(box.querySelector(".message-box-button-delete"), shouldShowDeleteMesgBtn(messageSt), "inline");
box.className = getMsgFullTypeClassName(messageSt);
// Notice, that no check of previous state is performed. Double loading is a rare event, I can afford to be slow
if (!messageSt.exists)
box.querySelector(".message-box-msg").innerText = msgErased;
}
function decodeSystemMessage(text){
let [subject, verb, object] = text.split(',');
let subjectId = Number(subject);
let objectId = Number(object);
let subjectRef = members.has(subjectId) ? members.get(subjectId).nickname : "???";
let objectRef = members.has(objectId) ? members.get(objectId).nickname : "???";
if (verb === "kicked"){
return subjectRef + " kicked " + objectRef;
} else if (verb === "summoned"){
return subjectRef + " summoned " + objectId;
} else if (verb === "left"){
return subjectRef + " left chat";
} else if (verb === "joined"){
return subjectId + " joined chat";
} else if (verb === "created"){
return subjectId + " created this chat";
}
return "... Bad log ...";
}
function convertMessageStToSupercontainer(messageSt){
let container = document.createElement("div");
container.className = "message-supercontainer";
let box = document.createElement("div");
container.appendChild(box);
box.className = getMsgFullTypeClassName(messageSt);
let ID = messageSt.id;
let topPart = document.createElement("div");
box.appendChild(topPart);
topPart.className = "message-box-top";
if (!members.has(messageSt.senderUserId))
throw new Error("First - update members");
let senderMemberSt = members.get(messageSt.senderUserId);
let senderProfileURI = "/user/" + senderMemberSt.nickname;
let inTopPartSenderName = document.createElement("a");
topPart.appendChild(inTopPartSenderName);
inTopPartSenderName.className = "message-box-sender-name";
inTopPartSenderName.innerText = senderMemberSt.name;
inTopPartSenderName.href = senderProfileURI;
let inTopPartSenderNickname = document.createElement("a");
topPart.appendChild(inTopPartSenderNickname);
inTopPartSenderNickname.className = "message-box-sender-name message-box-sender-shortname"
inTopPartSenderNickname.innerText = senderMemberSt.nickname;
inTopPartSenderNickname.href = senderProfileURI;
let inTopPartButtonDelete = document.createElement("img");
topPart.appendChild(inTopPartButtonDelete);
inTopPartButtonDelete.className = "message-box-button message-box-button-delete";
inTopPartButtonDelete.src = "/assets/img/delete.svg";
inTopPartButtonDelete.onclick = (ev) => {
if (ev.button !== 0)
return;
let msgText = box.querySelector(".message-box-msg").innerText;
let previewText = senderMemberSt.nickname + ":\n" + msgText;
if (previewText.length > 1000)
previewText = previewText.substring(0, 1000 - 3);
document.getElementById("win-deletion-msg-preview").innerText = previewText;
storeHiddenMsgIdForDeletionWin = ID;
activatePopupWindowById("msg-deletion-win");
};
const res = await fetch('/internalapi/editMessage', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(req)
setElementVisibility(inTopPartButtonDelete, shouldShowDeleteMesgBtn(messageSt), "inline");
let inTopPartButtonGetLink = document.createElement("img");
topPart.appendChild(inTopPartButtonGetLink);
inTopPartButtonGetLink.className = "message-box-button";
inTopPartButtonGetLink.src = "/assets/img/link.svg";
inTopPartButtonGetLink.onclick = (ev) => {
if (ev.button !== 0)
return;
let URI = window.location.host + "/chat/" + openedchat.nickname + "/m/" + String(ID);
document.getElementById("message-input").innerText += (" " + URI + "");
console.log("Tried to get link on message " + ID);
};
let msgPart = document.createElement("p");
box.appendChild(msgPart);
msgPart.className = "message-box-msg";
if (messageSt.exists){
if (messageSt.isSystem)
msgPart.innerText = decodeSystemMessage(messageSt.text);
else
msgPart.innerText = messageSt.text;
} else
msgPart.innerText = msgErased;
return {'container': container, 'box': box, 'offset': 0};
}
function makeVisible(msgId){
let supercontainer = convertMessageStToSupercontainer(loadedMessages.get(msgId));
const chatWin = document.getElementById("chat-widget");
chatWin.appendChild(supercontainer.container);
visibleMessages.set(msgId, supercontainer);
}
function opaNewMessageSt(messageSt){
let msgId = messageSt.id;
if (loadedMessages.has(msgId)){
loadedMessages.set(msgId, messageSt);
if (visibleMessages.has(msgId)){
updateMessageSupercontainer(visibleMessages.get(msgId), messageSt);
}
} else {
loadedMessages.set(msgId, messageSt);
if (anchoredMsg < 0){
anchoredMsg = msgId;
visibleMsgSegStart = msgId;
visibleMsgSegEnd = msgId;
makeVisible(msgId);
} else if (msgId + 1 === visibleMsgSegStart) {
visibleMsgSegStart--;
makeVisible(msgId);
while (loadedMessages.has(visibleMsgSegStart - 1)){
visibleMsgSegStart--;
makeVisible(visibleMsgSegStart);
}
} else if (msgId - 1 === visibleMsgSegEnd){
visibleMsgSegEnd++;
makeVisible(msgId);
while (loadedMessages.has(visibleMsgSegEnd + 1)){
visibleMsgSegEnd++;
makeVisible(visibleMsgSegEnd);
}
}
}
}
function canISendMessages(){
return myRoleHere === userChatRoleRegular || myRoleHere === userChatRoleAdmin;
}
function updateLocalStateFromChatUpdRespBlind(chatUpdResp){
console.log(anchoredMsg, offsetOfAnchor, chatUpdResp);
LocalHistoryId = chatUpdResp.HistoryId;
for (let memberSt of chatUpdResp.members){
let id = memberSt.userId;
if (id === userinfo.uid && myRoleHere !== memberSt.roleHere) {
myRoleHere = memberSt.roleHere;
for (let [msgId, sc] of visibleMessages){
updateMessageSupercontainer(sc, loadedMessages.get(msgId));
}
setElementVisibility(document.getElementById("message-input"), canISendMessages());
}
}
for (let memberSt of chatUpdResp.members){
let id = memberSt.userId;
members.set(id, memberSt);
}
lastMsgId = chatUpdResp.lastMsgId;
for (let messageSt of chatUpdResp.messages){
opaNewMessageSt(messageSt);
}
updateOffsets();
}
function updateLocalStateFromRecvBlind(Recv){
updateLocalStateFromChatUpdRespBlind(Recv.chatUpdResp);
}
async function requestMessageNeighbours(fromMsg, direction){
let Sent = genSentBaseGMN();
Sent.msgId = fromMsg;
Sent.direction = direction;
let Recv = await apiRequest("getMessageNeighbours", Sent);
updateLocalStateFromRecvBlind(Recv); // Blind to non-loaded whitespaces
}
function needToLoadWhitespace(){
return isMissingPrimaryMsgHeap() || isMissingTopMsgHeap() || isMissingBottomMsgHeap();
}
async function tryLoadWhitespaceSingle(){
console.log('tryLoadWhitespaceSingle');
if (isMissingPrimaryMsgHeap()){
await requestMessageNeighbours(-1, "backward");
} else if (isMissingTopMsgHeap()){
await requestMessageNeighbours(visibleMsgSegStart, "backward");
} else if (isMissingBottomMsgHeap()){
await requestMessageNeighbours(visibleMsgSegEnd, "forward");
}
}
async function loadWhitespaceMultitry(){
if (needToLoadWhitespace()){
cancelMainloopTimeout();
do {
try {
await tryLoadWhitespaceSingle();
await sleep(100);
} catch (e) {
console.error(e);
await sleep(1500);
}
} while (needToLoadWhitespace());
setMainloopTimeout();
}
}
async function updateLocalStateFromRecv(Recv){
updateLocalStateFromRecvBlind(Recv);
await loadWhitespaceMultitry();
}
async function safeApiRequestWithLocalStUpdate(type, Sent, errMsg){
try {
let Recv = await apiRequest(type, Sent)
await updateLocalStateFromRecv(Recv);
} catch(e) {
console.error(e);
alert(errMsg);
}
}
function configureMsgDeletionPopupButtons(){
document.getElementById("msg-deletion-yes").onclick = function(ev){
if (ev.button !== 0)
return;
deactivateActivePopup();
let Sent = genSentBase();
Sent.id = storeHiddenMsgIdForDeletionWin;
safeApiRequestWithLocalStUpdate("deleteMessage", Sent, "Failed to delete message");
};
document.getElementById("msg-deletion-no").onclick = function (ev){
if (ev.button !== 0)
return;
deactivateActivePopup();
}
}
__mainloopDelayMs = 1000;
async function UPDATE(){
let Recv = await apiRequest("chatPollEvents", genSentBase());
await updateLocalStateFromRecv(Recv);
}
__guestMainloopPollerAction = UPDATE;
window.onload = function (){
console.log("Page was loaded");
document.body.addEventListener("wheel", function (event) {
// event.preventDefault();
bumpedAtTheBottom = false;
offsetOfAnchor += event.deltaY / 3;
updateOffsets();
loadWhitespaceMultitry().then(dopDopYesYes);
});
const response = await res.json();
if (response.update) {
currentHistoryId = response.update[0].HistoryId;
document.getElementById("message-input").addEventListener("keyup", function (event) {
if (event.ctrlKey && event.key === 'Enter'){
let textarea = document.getElementById("message-input");
let text = String(textarea.innerText);
console.log(text);
textarea.innerText = "";
let Sent = genSentBase();
Sent.content = {};
Sent.content.text = text;
safeApiRequestWithLocalStUpdate("sendMessage", Sent, "Failed to send message");
}
}
document.addEventListener("DOMContentLoaded", async function() {
currentChatID = await getChatID();
});
let chatWg = document.getElementById("chat-widget");
let chatWgDebugLinesFnc = function (){
let H = chatWg.offsetHeight;
elSetOffsetInChat(document.getElementById("debug-line-lowest"), -softZoneSz);
elSetOffsetInChat(document.getElementById("debug-line-highest"), H + softZoneSz);
elSetOffsetInChat(document.getElementById("debug-line-top-padding"), H - chatPadding);
elSetOffsetInChat(document.getElementById("debug-line-bottom-padding"), chatPadding)
};
window.addEventListener("resize", chatWgDebugLinesFnc);
chatWgDebugLinesFnc();
configureMsgDeletionPopupButtons();
updateLocalStateFromChatUpdRespBlind(initial_chatUpdResp);
setMainloopTimeout();
loadWhitespaceMultitry();
}

26
assets/js/common-popup.js Normal file
View File

@ -0,0 +1,26 @@
let activePopupWinId = "";
function activatePopupWindow__(el){
let veil = document.createElement("div");
veil.id = "popup-overlay-veil-OBJ"
veil.className = "popup-overlay-veil";
veil.style.display = "block";
document.body.appendChild(veil);
el.style.display = "block";
}
function activatePopupWindowById(id){
if (activePopupWinId !== "")
return;
/* Lmao, this thing is just... SO unsafe */
activePopupWinId = id;
activatePopupWindow__(document.getElementById(id))
}
function deactivateActivePopup(){
if (activePopupWinId === "")
return
document.getElementById("popup-overlay-veil-OBJ").remove();
document.getElementById(activePopupWinId).style.display = "none";
activePopupWinId = "";
}

73
assets/js/common.js Normal file
View File

@ -0,0 +1,73 @@
let dopDopYesYes = (ign) => {};
function sleep(ms){
return new Promise(res => setTimeout(res, ms));
}
async function apiRequest(type, req){
let A = await fetch("/api/" + type,
{method: 'POST', body: JSON.stringify(req)});
let B = await A.json();
if (B.status !== 0)
throw Error("Server returned non-zero status");
return B;
}
/* Framework for pages with mainloop (it can be npt only polling, but also literally anything else */
let __mainloopDelayMs = 3000;
let mainloopTimeout = null;
let __guestMainloopPollerAction = null;
function setMainloopTimeout(){
mainloopTimeout = setTimeout(mainloopPoller, __mainloopDelayMs);
}
function cancelMainloopTimeout(){
clearTimeout(mainloopTimeout);
mainloopTimeout = null;
}
function mainloopPoller(){
try {
if (__guestMainloopPollerAction)
__guestMainloopPollerAction();
} catch (error){
console.log(error)
}
setMainloopTimeout();
}
// 1
const userChatRoleAdmin = "admin";
// 2
const userChatRoleRegular = "regular";
// 3
const userChatRoleReadOnly = "read-only";
// 4
const userChatRoleDeleted = "not-a-member";
function roleToColor(role) {
if (role === userChatRoleAdmin) {
return "#aafff3";
} else if (role === userChatRoleRegular){
return "#ffffff";
} else if (role === userChatRoleReadOnly){
return "#bfb2b2";
} else if (role === userChatRoleDeleted) {
return "#fb4a4a";
}
return "#286500" // Bug
}
// todo: replace it with translation
const msgErased = "[ ERASED ]";
function hideHTMLElement(el){
el.style.display = "none";
}
function showHTMLElement(el){
el.style.display = "block";
}
function setElementVisibility(el, isVisible, howVisible = "block"){
el.style.display = isVisible ? howVisible : "none";
}

View File

@ -1,186 +1,188 @@
let rooms = {};
let roomToDelete = null;
let currentRoom = null;
let currentHistoryId = 0;
let LocalHistoryId = 0;
function openRoom(currentRoom) {
alert('Вы вошли в комнату: ' + currentRoom);
}
function closeAdd() {
document.getElementById('add_members').style.display = 'none';
}
function openAdd() {
document.getElementById('add_members').style.display = 'flex';
}
function openConfirm(roomNickname) {
roomToDelete = roomNickname;
document.getElementById("delete-chat").style.display = "flex";
}
function closeConfirm() {
roomToDelete = null;
document.getElementById("delete-chat").style.display = "none";
}
function deleteChat() {
if (roomToDelete && rooms[roomToDelete]) {
delete rooms[roomToDelete];
removeRoomFromList(roomToDelete);
closeConfirm();
} else {
alert("Не удалось найти выбранную комнату.");
}
}
function addMember() {
const login = document.getElementById('newMemberLogin').value;
if (login) {
alert(`Участник с никнеймом '${login}' добавлен`);
closeAdd();
} else {
alert('Пожалуйста, введите логин участника');
}
}
function openCreateRoomModal() {
document.getElementById('createRoomModal').style.display = 'block';
}
function closeCreateRoomModal() {
document.getElementById('createRoomModal').style.display = 'none';
}
async function createRoom() {
const errorElement = document.getElementById('error');
const roomName = document.getElementById('newRoomName').value.trim();
const roomNickname = document.getElementById('newRoomNickname').value.trim();
errorElement.style.display = 'none';
errorElement.textContent = '';
if (roomName === '' || roomNickname === '') {
errorElement.textContent = 'Пожалуйста, заполните все поля';
errorElement.style.display = 'block';
return;
}
const request = {
LocalHistoryId: currentHistoryId,
content: {
name: roomName,
nickname: roomNickname
function genSentBase(){
return {
'chatListUpdReq': {
'LocalHistoryId': LocalHistoryId
}
};
}
try {
const response = await fetch('/internalapi/createChat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
});
let myChats = new Map();
let chatBoxes = new Map();
const res = await response.json();
/* Generate text that is displayed on the right side of chat intro box */
function youAreXHere(myRoleHere){
return "You are " + myRoleHere + " here";
}
if (res.status === 0) {
addRoomToList(roomName, roomNickname);
rooms[roomNickname] = true;
closeCreateRoomModal();
currentHistoryId = res.update.LocalHistoryId;
window.location.href = '/chat/' + roomNickname;
let chatRenunciationWinStoredId = -1;
function shouldShowDeleteButton(myMembershipSt){
return myMembershipSt.myRoleHere === userChatRoleDeleted;
}
/* Updating chat html box after myMembershipSt in it was updated */
function updateBoxWithNewSt(box, myMembershipSt){
let ID = myMembershipSt.chatId;
let roleP = box.querySelector(".CL-my-chat-box-my-role");
roleP.innerText = youAreXHere(myMembershipSt.myRoleHere);
box.style.backgroundColor = roleToColor(myMembershipSt.myRoleHere);
box.querySelector(".CL-my-chat-box-leave-btn").style.display =
(shouldShowDeleteButton(myMembershipSt) ? "none" : "block");
}
function convertMyMembershipStToBox(myMembershipSt){
let chatURI = "/chat/" + myMembershipSt.chatNickname;
let ID = myMembershipSt.chatId;
let box = document.createElement("div");
box.className = "dynamic-block-list-el CL-my-chat-box";
box.style.backgroundColor = roleToColor(myMembershipSt.myRoleHere);
let inBoxNickname = document.createElement("a");
box.appendChild(inBoxNickname);
inBoxNickname.className = "entity-nickname-txt CL-my-chat-box-nickname";
inBoxNickname.innerText = myMembershipSt.chatNickname;
inBoxNickname.href = chatURI;
let inBoxName = document.createElement("a");
box.appendChild(inBoxName);
inBoxName.className = "entity-reg-field-txt CL-my-chat-box-name";
inBoxName.innerText = myMembershipSt.chatName;
inBoxName.href = chatURI;
let inBoxMyRoleHere = document.createElement("p");
box.appendChild(inBoxMyRoleHere);
inBoxMyRoleHere.className = "entity-reg-field-txt CL-my-chat-box-my-role";
inBoxMyRoleHere.innerText = youAreXHere(myMembershipSt.myRoleHere);
let inBoxLeaveBtn = document.createElement("img");
box.appendChild(inBoxLeaveBtn);
inBoxLeaveBtn.className = "CL-my-chat-box-leave-btn";
inBoxLeaveBtn.src = "/assets/img/delete.svg";
inBoxLeaveBtn.onclick = function (ev) {
if (ev.button !== 0)
return;
chatRenunciationWinStoredId = ID;
document.getElementById("chat-renunciation-win-title").innerText =
"Do you really want to leave chat " + myMembershipSt.chatNickname + "?";
activatePopupWindowById("chat-renunciation-win");
};
box.querySelector(".CL-my-chat-box-leave-btn").style.display =
(shouldShowDeleteButton(myMembershipSt) ? "none" : "block");
return box;
}
function updateLocalStateFromChatListUpdResp(chatListUpdResp){
LocalHistoryId = chatListUpdResp.HistoryId;
let literalChatList = document.getElementById("CL-dblec");
for (let myMembershipSt of chatListUpdResp.myChats){
let chatId = myMembershipSt.chatId;
console.log(myMembershipSt);
if (myChats.has(chatId)){
myChats.set(chatId, myMembershipSt);
updateBoxWithNewSt(chatBoxes.get(chatId), myMembershipSt);
} else {
throw new Error(res.error || 'Ошибка');
if (myMembershipSt.myRoleHere === userChatRoleDeleted)
continue;
myChats.set(chatId, myMembershipSt);
let box = convertMyMembershipStToBox(myMembershipSt)
chatBoxes.set(chatId, box);
literalChatList.appendChild(box);
}
} catch (error) {
alert('Ошибка создания чата: ' + error.message);
}
}
function addRoomToList(roomName) {
const roomList = document.querySelector('.room-list');
const existingRoomItem = Array.from(roomList.children).find(item => item.querySelector('.room-name').textContent === roomName);
if (existingRoomItem) {
existingRoomItem.remove();
/* Use it ONLY if `Recv` reported success */
function updateLocalStateFromRecv(Recv){
updateLocalStateFromChatListUpdResp(Recv.chatListUpdResp);
}
const roomItem = document.createElement('li');
roomItem.classList.add('room-item');
roomItem.innerHTML = `
<span class="room-name">${roomName}</span>
<button class="delete-chat-button" onclick="openConfirm('${roomNickname}')">Удалить чат</button>
<button class="add-members-button" onclick="openAdd()">Добавить участников</button>
<button class="join-button" onclick="window.location.href = '/chat/${roomNickname}'">Войти</button>
`;
roomList.appendChild(roomItem);
}
function removeRoomFromList(roomName, roomNickname) {
const roomList = document.querySelector('.room-list');
const roomItem = Array.from(roomList.children).find(item => item.querySelector('.room-name').textContent === roomName);
if (roomItem) {
roomList.removeChild(roomItem);
}
}
async function initializeRoomList() {
try {
const response = await fetch('/internalapi/getChatList', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
function configureChatCreationInterface(){
document.getElementById("chat-creation-win-yes").onclick = function (ev) {
if (ev.button !== 0)
return;
let chatNicknameInput = document.getElementById("chat-nickname-input");
let chatNameInput = document.getElementById("chat-name-input");
let nickname = String(chatNicknameInput.value);
let name = String(chatNameInput.value);
deactivateActivePopup();
let Sent = genSentBase();
Sent.content = {};
Sent.content.nickname = nickname;
Sent.content.name = name;
apiRequest("createChat", Sent
).then((Recv) => {
updateLocalStateFromRecv(Recv);
}).catch((e) => {
alert("Failed to create chat");
console.log(e);
});
};
const res = await response.json();
document.getElementById("chat-creation-win-no").onclick = function (ev) {
if (ev.button !== 0)
return;
deactivateActivePopup();
}
if (res.status === 0) {
res.chats.forEach(chat => {
addRoomToList(chat.content.name, chat.content.nickname);
document.getElementById("CL-bacbe").onclick = function (ev){
if (ev.button !== 0)
return;
let chatNicknameInput = document.getElementById("chat-nickname-input");
let chatNameInput = document.getElementById("chat-name-input");
chatNicknameInput.value = "";
chatNameInput.value = "";
activatePopupWindowById("chat-creation-win");
console.log("Tried to show chat creation window");
};
}
function configureChatRenunciationInterfaceWinPart(){
document.getElementById("chat-renunciation-win-yes").onclick = function (ev){
if (ev.button !== 0)
return;
deactivateActivePopup();
if (chatRenunciationWinStoredId < 0)
throw new Error("chatRenunciationWinStoredId < 0");
let chatId = chatRenunciationWinStoredId;
let Sent = genSentBase();
Sent.chatId = chatId;
apiRequest("leaveChat", Sent
).then((Recv) => {
updateLocalStateFromRecv(Recv);
}).catch((e) => {
alert("Failed to leave chat");
console.log(e);
});
} else {
throw new Error(res.error || 'Неизвестная ошибка');
}
} catch (error) {
alert('Ошибка загрузки списка чатов: ' + error.message);
document.getElementById("chat-renunciation-win-no").onclick = function(ev) {
if (ev.button !== 0)
return;
deactivateActivePopup();
}
}
async function getChatID() {
const chatNickname = window.location.pathname.split('/').pop();
const response = await fetch('/internalapi/getChatList', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
__mainloopDelayMs = 3000;
__guestMainloopPollerAction = function(){
let Sent = genSentBase();
apiRequest("chatListPollEvents", Sent
).then((Recv) => {
console.log("Got a response");
console.log(Recv);
updateLocalStateFromRecv(Recv);
});
const res = await response.json();
for (const chat of res.chats) {
if (chat.content.nickname === chatNickname) {
return chat.id;
}
}
return -1;
}
window.onclick = function(event) {
if (event.target === document.getElementById('createRoomModal')) {
closeCreateRoomModal();
}
}
document.getElementById('newRoomName').addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
createRoom();
}
});
document.addEventListener('DOMContentLoaded', initializeRoomList);
window.onload = function () {
console.log("Loading complete");
updateLocalStateFromChatListUpdResp(initial_chatListUpdResp);
configureChatCreationInterface();
configureChatRenunciationInterfaceWinPart();
mainloopPoller();
};

View File

@ -112,6 +112,10 @@ namespace een9 {
return status;
}
res.body.reserve(std::min(100000ul, body_size));
if (body_size == 0) {
status = 1;
}
break;
}
}
if (!res.has_body) {

View File

@ -55,13 +55,16 @@ namespace nytl {
}
char skip(ParsingContext& ctx) {
ASSERT(ctx.pos < ctx.text.size(), "Unexpected EOF");
if (ctx.pos >= ctx.text.size())
THROW("Unexpected EOF");
return advance(ctx);
}
void skip(ParsingContext& ctx, char ch) {
ASSERT(ctx.pos < ctx.text.size(), "Unexpected EOF");
ASSERT(ctx.text[ctx.pos] == ch, "Unexpected character");
if (ctx.pos >= ctx.text.size())
THROW("Unexpected EOF");
if (ctx.text[ctx.pos] != ch)
THROW("Unexpected character");
advance(ctx);
}
@ -150,7 +153,8 @@ namespace nytl {
void parse_bare_file(const std::string& filename, const std::string& content,
global_elem_set_t& result)
{
ASSERT(result.count(filename) == 0, "Repeated element " + filename);
if (result.count(filename) != 0)
THROW("Repeated element " + filename);
std::string txt = clement_lstrip(content);
rstrip(txt);
size_t cut = 9999999999999;
@ -170,13 +174,15 @@ namespace nytl {
uptr<TPFrame> toMe(bool returned, ParsingContext& ctx) {
if (!returned) {
std::string nm = readName(ctx);
ASSERT(!nm.empty(), "Type specification expected");
if (nm.empty())
THROW("Type specification expected");
nm = make_uppercase(nm);
if (nm == "JSON") {
result = json::JSON(true);
return NULL;
}
ASSERT(nm == "EL", "Type of argument variable is either JSON or EL(...signature)")
if (nm != "EL")
THROW("Type of argument variable is either JSON or EL(...signature)");
skip(ctx, '(');
result.asArray();
assert(result.isArray());
@ -217,8 +223,10 @@ namespace nytl {
uptr<EPFrame> toMe(bool returned, ParsingContext& ctx, const arg_name_list_t& local_var_names) {
if (!returned) {
std::string first = readName(ctx);
ASSERT(!first.empty(), "Expression should start with 'root' name of global package or local variable");
ASSERT(first != "_", "_ ??? ARE YOU KIDDING???");
if (first.empty())
THROW("Expression should start with 'root' name of global package or local variable");
if (first == "_")
THROW("Expression root can't be _");
if (local_var_names.count(first) == 1) {
result["V"].asInteger() = json::Integer((int64_t)local_var_names.at(first));
} else {
@ -243,7 +251,8 @@ namespace nytl {
t = readUint(ctx);
if (!t.empty()) {
size_t v = std::stoul(t);
ASSERT(v < INT64_MAX, "Index is too big");
if (v >= INT64_MAX)
THROW("Index is too big");
chain.back() = json::JSON((int64_t)v);
continue;
}
@ -352,7 +361,8 @@ namespace nytl {
ElementPart::when_for_put_S& P = result.parts.back().when_for_put;
skipWhitespace(ctx);
std::string V1 = readName(ctx);
ASSERT(!V1.empty(), "Expected variable name");
if (V1.empty())
THROW("Expected variable name");
skipWhitespace(ctx);
bool have_colon_and_2 = false;
std::string V2;
@ -364,7 +374,8 @@ namespace nytl {
skipWhitespace(ctx);
}
op = make_uppercase(readName(ctx));
ASSERT(op == "IN", "Expected IN");
if (op != "IN")
THROW("Expected IN");
skipWhitespace(ctx);
P.ref_over = parse_expression(ctx, local_var_names);
P.internal_element = el_name + ".~" + std::to_string(free_hidden++);
@ -372,13 +383,15 @@ namespace nytl {
newborn.is_hidden = true;
arg_name_list_t local_var_names_of_nxt = local_var_names;
if (V1 != "_") {
ASSERT(local_var_names_of_nxt.count(V1) == 0, "Repeated local variable");
if (local_var_names_of_nxt.count(V1) != 0)
THROW("Repeated local variable");
size_t k = local_var_names_of_nxt.size();
local_var_names_of_nxt.emplace(V1, k);
(have_colon_and_2 ? P.where_key_var : P.where_value_var) = (ssize_t)k;
}
if (have_colon_and_2 && V2 != "_") {
ASSERT(local_var_names_of_nxt.count(V2) == 0, "Repeated local variable");
if (local_var_names_of_nxt.count(V2) != 0)
THROW("Repeated local variable");
size_t k = local_var_names_of_nxt.size();
local_var_names_of_nxt.emplace(V2, k);
P.where_value_var = (ssize_t)k;
@ -395,11 +408,12 @@ namespace nytl {
ElementPart::when_ref_put_S& P = result.parts.back().when_ref_put;
skipWhitespace(ctx);
std::string Vn = readName(ctx);
ASSERT(!Vn.empty(), "Expected variable name");
ASSERT(Vn != "_", "Are you kidding???");
if (Vn.empty() || Vn == "_")
THROW("REF: expected variable name");
skipWhitespace(ctx);
op = make_uppercase(readName(ctx));
ASSERT(op == "AS", "Expected AS");
if (op != "AS")
THROW("Expected AS");
skipWhitespace(ctx);
P.ref_over = parse_expression(ctx, local_var_names);
P.internal_element = el_name + ".~" + std::to_string(free_hidden++);
@ -467,13 +481,15 @@ namespace nytl {
}
};
if (op == "ENDELDEF") {
ASSERT(myself == gone_for_nothing, "Unexpected end of element");
if (myself != gone_for_nothing)
THROW("Unexpected ENDELDEF");
skip_magic_block_end(ctx, syntax);
prepare_to_depart_parts();
return NULL;
}
if (op == "ENDFOR") {
ASSERT(myself == gone_for_for, "Unexpected end of for cycle");
if (myself != gone_for_for)
THROW("Unexpected ENDFOR");
skipWhitespace(ctx);
/* Here I am using ret_data_int to return info about NOLF(1)/LF(2) decision */
ret_data_int = 2; // Default is to do LF
@ -491,7 +507,8 @@ namespace nytl {
return NULL;
}
if (op == "ENDREF") {
assert(myself == gone_for_ref);
if (myself != gone_for_ref)
THROW("Unexpected ENDREF");
skip_magic_block_end(ctx, syntax);
prepare_to_depart_parts();
return NULL;
@ -525,12 +542,15 @@ namespace nytl {
if (peep(ctx) == EOFVAL)
break;
skip_magic_block_start(ctx, syntax);
ASSERT(make_uppercase(readName(ctx)) == "ELDEF", "Expected ELDEF");
if (make_uppercase(readName(ctx)) != "ELDEF")
THROW("Expected ELDEF");
skipWhitespace(ctx);
std::string elname_postfix = readName(ctx);
ASSERT(elname_postfix != "_", "please don't");
if (elname_postfix == "_")
THROW("Can't use _ as element name");
std::string fullname = elname_postfix == "main" ? filename : filename + "." + elname_postfix;
ASSERT(result.count(fullname) == 0, "Element " + fullname + " has been already defined");
if (result.count(fullname) != 0)
THROW("Element " + fullname + " has been already defined");
Element& newborn = result[fullname];
arg_name_list_t arglist;
while (true) {
@ -540,9 +560,11 @@ namespace nytl {
newborn.arguments.push_back(parse_type(ctx));
skipWhitespace(ctx);
std::string argname = readName(ctx);
ASSERT(!argname.empty(), "Expected argument name");
if (argname.empty())
THROW("Expected argument name");
if (argname != "_") {
ASSERT(arglist.count(argname) == 0, "Repeated argument (" + argname + ")");
if (arglist.count(argname) != 0)
THROW("Repeated argument (" + argname + ")");
size_t k = arglist.size();
arglist[argname] = k;
}

View File

@ -212,7 +212,7 @@ namespace nytl {
const std::function<std::string(std::string)> &escape) {
if (!returned)
if (elem_ns.count(name) != 1)
THROW("No such element");
THROW("No such element (" + name + ")");
const Element& el = elem_ns.at(name);
if (!returned) {
/* Continue to do checks */
@ -227,7 +227,9 @@ namespace nytl {
// If not json is expected, element must be expected
assert(el.arguments[i].isArray());
ASSERT(!passed_args[i].is_json, "Expected element element arguemnt, got json");
ASSERT(elem_ns.count(passed_args[i].EL_name), "No such element, can't compare signatures of argument value");
const std::string& passed_el_as_arg = passed_args[i].EL_name;
if (elem_ns.count(passed_el_as_arg) != 1)
THROW("No such element, can't compare signatures of argument value (" + passed_el_as_arg + ")");
const Element& arg_element = elem_ns.at(passed_args[i].EL_name);
// ASSERT(passed_args);
if(el.arguments[i].asArray() != arg_element.arguments)

View File

@ -3,16 +3,32 @@
#include <assert.h>
namespace iu9cawebchat {
void make_her_a_member_of_the_midnight_crew(SqliteConnection& conn, int64_t chatId, int64_t alienUserId, int64_t role) {
assert(role != user_chat_role_deleted);
bool is_membership_row_present(SqliteConnection& conn, int64_t chatId, int64_t alienUserId) {
SqliteStatement req(conn,
"SELECT EXISTS(SELECT 1 FROM `user_chat_membership` WHERE `chatId` = ?1 AND `userId` = ?2)",
{{1, chatId}, {2, alienUserId}}, {});
fsql_integer_or_null r{true, 0};
int status = sqlite_stmt_step(req, {{0, &r}}, {});
return (bool)r.value;
}
void alter_user_chat_role(SqliteConnection& conn, int64_t chatId, int64_t alienUserId, int64_t role) {
int64_t chat_HistoryId_BEFORE_EV = get_current_history_id_of_chat(conn, chatId);
int64_t alien_chatlist_HistoryId_BEFORE_EV = get_current_history_id_of_user_chatList(conn, alienUserId);
if (!is_membership_row_present(conn, chatId, alienUserId)) {
sqlite_nooutput(conn,
"INSERT INTO `user_chat_membership` (`userId`, `chatId`, `user_chatList_IncHistoryId`,"
"`chat_IncHistoryId`, `role`) VALUES (?1, ?2, ?3, ?4, ?5)",
{{1, alienUserId}, {2, chatId}, {3, alien_chatlist_HistoryId_BEFORE_EV + 1},
{4, chat_HistoryId_BEFORE_EV + 1}, {5, role}}, {});
} else {
sqlite_nooutput(conn,
"UPDATE `user_chat_membership` SET `user_chatList_IncHistoryId` = ?3,`chat_IncHistoryId` = ?4,"
"`role` = ?5 WHERE `userId` = ?1 AND `chatId` = ?2",
{{1, alienUserId}, {2, chatId}, {3, alien_chatlist_HistoryId_BEFORE_EV + 1},
{4, chat_HistoryId_BEFORE_EV + 1}, {5, role}}, {});
}
sqlite_nooutput(conn,
"UPDATE `chat` SET `it_HistoryId` = ?1 WHERE `id` = ?2", {{1, chat_HistoryId_BEFORE_EV + 1},
{2, chatId}}, {});
@ -22,6 +38,12 @@ namespace iu9cawebchat {
{{1, alien_chatlist_HistoryId_BEFORE_EV + 1}, {2, alienUserId}}, {});
}
void make_her_a_member_of_the_midnight_crew(SqliteConnection& conn, int64_t chatId, int64_t alienUserId, int64_t role) {
assert(role != user_chat_role_deleted);
// int64_t old_role = get_role_of_user_in_chat(conn, alienUserId, chatId);
alter_user_chat_role(conn, chatId, alienUserId, role);
}
json::JSON internalapi_addMemberToChat(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) {
int64_t chatId = Sent["chatUpdReq"]["chatId"].asInteger().get_int();
int64_t my_role_here = get_role_of_user_in_chat(conn, uid, chatId);
@ -33,12 +55,17 @@ namespace iu9cawebchat {
try {
alien = lookup_user_content_by_nickname(conn, alien_nickname);
} catch (std::exception& e) {
return json::JSON(json::jdict{{"status", json::JSON(-1l)}});
return at_api_error_gen_bad_recv(-1l);
}
bool makeReadOnly = Sent["makeReadOnly"].toBool();
int64_t aliens_old_role = get_role_of_user_in_chat(conn, alien.id, chatId);
if (aliens_old_role == user_chat_role_deleted) {
make_her_a_member_of_the_midnight_crew(conn, chatId, alien.id, user_chat_role_regular);
make_her_a_member_of_the_midnight_crew(conn, chatId, alien.id,
makeReadOnly ? user_chat_role_read_only : user_chat_role_regular);
} else {
return at_api_error_gen_bad_recv(-2l);
}
json::JSON Recv;

View File

@ -7,13 +7,16 @@ namespace iu9cawebchat {
std::string new_chat_name = Sent["content"]["name"].asString();
std::string new_chat_nickname = Sent["content"]["nickname"].asString();
if (!check_nickname(new_chat_nickname) || !check_name(new_chat_name))
return json::JSON(json::jdict{{"status", json::JSON(-1l)}});
return at_api_error_gen_bad_recv(-1l);
if (is_nickname_taken(conn, new_chat_nickname))
return json::JSON(json::jdict{{"status", json::JSON(-2l)}});
return at_api_error_gen_bad_recv(-2l);
if (is_nickname_taken(conn, new_chat_nickname))
return at_api_error_gen_bad_recv(-3l);
reserve_nickname(conn, new_chat_nickname);
sqlite_nooutput(conn,
"INSERT INTO `chat` (`nickname`, `name`, `it_HistoryId`, `lastMsgId`) VALUES (?1, ?2, 0, -1)");
"INSERT INTO `chat` (`nickname`, `name`, `it_HistoryId`, `lastMsgId`) VALUES (?1, ?2, 0, -1)",
{}, {{1, new_chat_nickname}, {2, new_chat_name}});
int64_t CHAT_ID = sqlite_trsess_last_insert_rowid(conn);

View File

@ -1,9 +1,11 @@
#include "server_data_interact.h"
#include <engine_engine_number_9/baza_throw.h>
#include "../debug.h"
namespace iu9cawebchat {
json::JSON internalapi_deleteMessage(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) {
int64_t chatId = Sent["chatUpdReq"].asInteger().get_int();
// debug_print_json(Sent);
int64_t chatId = Sent["chatUpdReq"]["chatId"].asInteger().get_int();
int64_t my_role_here = get_role_of_user_in_chat(conn, uid, chatId);
if (my_role_here == user_chat_role_deleted)
een9_THROW("Unauthorized user tries to access internalapi_getChatInfo");

View File

@ -6,7 +6,7 @@ namespace iu9cawebchat {
int64_t chatId = Sent["chatId"].asInteger().get_int();
if (get_role_of_user_in_chat(conn, uid, chatId) == user_chat_role_deleted)
een9_THROW("Not a member");
kick_from_chat(conn, uid, chatId);
kick_from_chat(conn, chatId, uid);
json::JSON Recv;
poll_update_chat_list(conn, uid, Sent, Recv);
return Recv;

View File

@ -2,34 +2,17 @@
#include <engine_engine_number_9/baza_throw.h>
namespace iu9cawebchat {
void kick_from_chat(SqliteConnection& conn, int64_t alienUserId, int64_t chatId) {
if (get_role_of_user_in_chat(conn, alienUserId, chatId) == user_chat_role_deleted)
een9_THROW("Can't delete a deleted member");
int64_t chat_HistoryId_BEFORE_EV = get_current_history_id_of_chat(conn, chatId);
int64_t alien_chatlist_HistoryId_BEFORE_EV = get_current_history_id_of_user_chatList(conn, alienUserId);
sqlite_nooutput(conn,
"UPDATE `user_chat_membership` SET `user_chatList_IncHistoryId` = ?3,"
"`chat_IncHistoryId` = ?4, `role` = ?5 WHERE `userId` = ?1 AND `chatId` = ?2",
{{1, alienUserId}, {2, chatId}, {3, alien_chatlist_HistoryId_BEFORE_EV + 1},
{4, chat_HistoryId_BEFORE_EV + 1}, {5, user_chat_role_deleted}}, {});
sqlite_nooutput(conn,
"UPDATE `chat` SET `it_HistoryId` = ?1 WHERE `id` = ?2", {{1, chat_HistoryId_BEFORE_EV + 1},
{2, chatId}}, {});
sqlite_nooutput(conn,
"UPDATE `user` SET `chatList_HistoryId` = ?1 WHERE `id` = ?2",
{{1, alien_chatlist_HistoryId_BEFORE_EV + 1}, {2, alienUserId}}, {});
void kick_from_chat(SqliteConnection& conn, int64_t chatId, int64_t alienUserId) {
alter_user_chat_role(conn, chatId, alienUserId, user_chat_role_deleted);
}
json::JSON internalapi_removeMemberFromChat(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) {
int64_t chatId = Sent["chatUpdReq"]["chatId"].asInteger().get_int();
int64_t my_role_here = get_role_of_user_in_chat(conn, uid, chatId);
if (my_role_here != user_chat_role_deleted)
if (my_role_here != user_chat_role_admin)
een9_THROW("Only admin can delete members of chat");
int64_t badAlienId = Sent["chatUpdReq"]["userId"].asInteger().get_int();
kick_from_chat(conn, badAlienId, chatId);
int64_t badAlienId = Sent["userId"].asInteger().get_int();
kick_from_chat(conn, chatId, badAlienId);
json::JSON Recv;
poll_update_chat(conn, Sent, Recv);
return Recv;

View File

@ -1,6 +1,7 @@
#include "server_data_interact.h"
#include <engine_engine_number_9/baza_throw.h>
#include "../str_fields.h"
#include "../debug.h"
namespace iu9cawebchat {
/* No authorization check is performed
@ -13,16 +14,19 @@ namespace iu9cawebchat {
int64_t chat_lastMsgId = get_lastMsgId_of_chat(conn, chatId);
SqliteStatement req(conn,
"INSERT INTO `message` (`chatId`, `id`, `senderUserId`, `exists`, `isSystem`, `chat_IncHistoryId`, "
"`text`) VALUES (?1, ?2, ?3 1, ?4, ?5, ?6)",
"`text`) VALUES (?1, ?2, ?3, 1, ?4, ?5, ?6)",
{{1, chatId}, {2, chat_lastMsgId + 1}, {4, (int64_t)isSystem}, {5, chat_HistoryId_BEFORE_MSG + 1}}, {{6, text}});
if (!isSystem)
sqlite_stmt_bind_int64(req, 3, uid);
if (sqlite_stmt_step(req, {}, {}) != SQLITE_DONE)
een9_THROW("There must be something wrong");
sqlite_nooutput(conn, "UPDATE `chat` SET `lastMsgId` = ?1, `it_HistoryId` = ?2 WHERE `id` = ?3",
{{1, chat_lastMsgId + 1}, {2, chat_HistoryId_BEFORE_MSG + 1}, {3, chatId}}, {});
}
json::JSON internalapi_sendMessage(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) {
int64_t chatId = Sent["chatUpdReq"].asInteger().get_int();
debug_print_json(Sent);
int64_t chatId = Sent["chatUpdReq"]["chatId"].asInteger().get_int();
int64_t my_role_here = get_role_of_user_in_chat(conn, uid, chatId);
if (my_role_here == user_chat_role_deleted)
een9_THROW("Unauthorized user tries to access internalapi_getChatInfo");
@ -36,6 +40,7 @@ namespace iu9cawebchat {
json::JSON Recv;
poll_update_chat(conn, Sent, Recv);
debug_print_json(Recv);
return Recv;
}
}

View File

@ -1,14 +1,16 @@
#include "server_data_interact.h"
#include <assert.h>
#include <engine_engine_number_9/baza_throw.h>
#include "../debug.h"
namespace iu9cawebchat {
json::JSON poll_update_chat_list_resp(SqliteConnection& conn, int64_t userId, int64_t LocalHistoryId) {
printf("Userid: %ld\n", userId);
json::JSON chatListUpdResp;
SqliteStatement my_membership_changes(conn,
"SELECT `chatId`, `role` FROM `user_chat_membership` WHERE `userId` = ?1 "
"AND `user_chatList_IncHistoryId` > ?2", {{1, userId}, {2, LocalHistoryId}});
json::jarr myChats = chatListUpdResp["myChats"].asArray();
json::jarr& myChats = chatListUpdResp["myChats"].asArray();
while (true) {
fsql_integer_or_null ev_chatId, usersRoleHere;
int status = sqlite_stmt_step(my_membership_changes, {{0, &ev_chatId}, {1, &usersRoleHere}}, {});
@ -32,7 +34,7 @@ namespace iu9cawebchat {
void poll_update_chat_list(SqliteConnection& conn, int64_t userId, const json::JSON& Sent, json::JSON& Recv) {
Recv["status"].asInteger() = json::Integer(0l);
// todo: in libjsonincpp: get rid of Integer
poll_update_chat_list_resp(conn, userId, Sent["chatListUpdReq"]["LocalHistoryId"].asInteger().get_int());
Recv["chatListUpdResp"] = poll_update_chat_list_resp(conn, userId, Sent["chatListUpdReq"]["LocalHistoryId"].asInteger().get_int());
}
json::JSON make_messageSt_obj(int64_t id, int64_t senderUserId, bool exists, bool isSystem, const std::string& text) {
@ -52,7 +54,7 @@ namespace iu9cawebchat {
json::jarr messages;
SqliteStatement messages_changes(conn,
"SELECT `id`, `senderUserId`, `exists`, `isSystem`, `text` FROM `messages` "
"SELECT `id`, `senderUserId`, `exists`, `isSystem`, `text` FROM `message` "
"WHERE `chatId` = ?1 AND ( `chat_IncHistoryId` > ?2 OR ( ?3 <= `id` AND `id` <= ?4 ) )",
{{1, chatId}, {2, LocalHistoryId}, {3, QSEG_A}, {4, QSEG_B}}, {});
while (true) {
@ -73,7 +75,7 @@ namespace iu9cawebchat {
json::jarr members;
SqliteStatement membership_changes(conn,
"SELECT `userId`, `role`, FROM `user_chat_membership` WHERE `chatId` = ?1 "
"SELECT `userId`, `role` FROM `user_chat_membership` WHERE `chatId` = ?1 "
"AND `chat_IncHistoryId` > ?2", {{1, chatId}, {2, LocalHistoryId}}, {});
while (true) {
fsql_integer_or_null alienUserId;
@ -101,7 +103,7 @@ namespace iu9cawebchat {
chatUpdResp["members"].asArray() = poll_update_chat_resp_members(conn, chatId, 0);
json::jarr messages = chatUpdResp["messages"].asArray();
json::jarr& messages = chatUpdResp["messages"].asArray();
if (selectedMsg >= 0) {
RowMessage_Content msg = lookup_message_content(conn, chatId, selectedMsg);
messages.push_back(make_messageSt_obj(msg.id, msg.senderUserId, msg.exists, msg.isSystem, msg.text));
@ -137,7 +139,7 @@ namespace iu9cawebchat {
int64_t QSEG_A, int64_t QSEG_B) {
Recv["status"].asInteger() = json::Integer(0l);
Recv["charUpdResp"] = poll_update_chat_important_segment_resp(conn,
Recv["chatUpdResp"] = poll_update_chat_important_segment_resp(conn,
Sent["chatUpdReq"]["chatId"].asInteger().get_int(),
Sent["chatUpdReq"]["LocalHistoryId"].asInteger().get_int(), QSEG_A, QSEG_B);
}
@ -164,7 +166,8 @@ namespace iu9cawebchat {
/* Reznya */
json::JSON internalapi_getMessageNeighbours(SqliteConnection& conn, int64_t uid, const json::JSON& Sent) {
int64_t chatId = Sent["chatId"].asInteger().get_int();
debug_print_json(Sent);
int64_t chatId = Sent["chatUpdReq"]["chatId"].asInteger().get_int();
if (get_role_of_user_in_chat(conn, uid, chatId) == user_chat_role_deleted)
een9_THROW("Authentication failure");
int64_t lastMsgId = get_lastMsgId_of_chat(conn, chatId);
@ -191,6 +194,7 @@ namespace iu9cawebchat {
}
}
poll_update_chat_important_segment(conn, Sent, Recv, qBeg, qEnd);
debug_print_json(Recv);
return Recv;
}
}

View File

@ -5,6 +5,10 @@
#include "../str_fields.h"
namespace iu9cawebchat {
json::JSON at_api_error_gen_bad_recv(int64_t code) {
return json::JSON(json::jdict{{"status", json::JSON(code)}});
}
const char* stringify_user_chat_role(int64_t role) {
if (role == user_chat_role_admin)
return "admin";
@ -109,8 +113,6 @@ namespace iu9cawebchat {
int status = sqlite_stmt_step(req, {{0, &senderUserId}, {1, &exists}, {2, &isSystem}},
{{3, &msg_text}});
if (status == SQLITE_ROW) {
if (!(bool)exists.value)
een9_THROW("Message existed, but now it does not");
return {msgId, senderUserId.exist ? senderUserId.value : -1, (bool)exists.value,
(bool)isSystem.value, msg_text.value};
}
@ -144,7 +146,6 @@ namespace iu9cawebchat {
/* All the api calls processing is done in dedicated files.
* All functions related to polling are defined in api_pollevents.cpp */
// todo: move everything to dedicated files
int64_t get_current_history_id_of_chat(SqliteConnection& conn, int64_t chatId) {
SqliteStatement req(conn, "SELECT `it_HistoryId` FROM `chat` WHERE `id` = ?1", {{1, chatId}}, {});
fsql_integer_or_null HistoryId;

View File

@ -8,6 +8,8 @@
#include <jsonincpp/string_representation.h>
namespace iu9cawebchat {
json::JSON at_api_error_gen_bad_recv(int64_t code = -1);
constexpr int64_t user_chat_role_admin = 1;
constexpr int64_t user_chat_role_regular = 2;
constexpr int64_t user_chat_role_read_only = 3;
@ -61,8 +63,9 @@ namespace iu9cawebchat {
json::JSON poll_update_chat_ONE_MSG_resp(SqliteConnection& conn, int64_t chatId, int64_t selectedMsg);
void poll_update_chat(SqliteConnection& conn, const json::JSON& Sent, json::JSON& Recv);
void alter_user_chat_role(SqliteConnection& conn, int64_t chatId, int64_t alienUserId, int64_t role);
void make_her_a_member_of_the_midnight_crew(SqliteConnection& conn, int64_t chatId, int64_t alienUserId, int64_t role);
void kick_from_chat(SqliteConnection& conn, int64_t alienUserId, int64_t chatId);
void kick_from_chat(SqliteConnection& conn, int64_t chatId, int64_t alienUserId);
bool is_nickname_taken(SqliteConnection& conn, const std::string& nickname);
void reserve_nickname(SqliteConnection& conn, const std::string& nickname);

View File

@ -7,8 +7,10 @@ namespace iu9cawebchat {
std::string when_page_chat(WorkerGuestData& wgd, const json::JSON& config_presentation,
const een9::ClientRequest& req, const json::JSON& userinfo) {
std::vector<std::string> path_segs = {""};
std::vector<std::string> path_segs = {};
path_segs.reserve(4);
if (req.uri_path.empty() || req.uri_path[0] != '/')
return page_E404(wgd);
for (char ch: req.uri_path) {
if (ch == '/')
path_segs.emplace_back();
@ -23,16 +25,17 @@ namespace iu9cawebchat {
}
if (!check_nickname(chat_nickname))
return page_E404(wgd);
if (path_segs.size() == 2) {
} else if (path_segs.size() == 4) {
bool show_chat_members = (path_segs[0] == "chat-members");
if (path_segs.size() == 4 && !show_chat_members) {
if (path_segs[2] != "m")
return page_E404(wgd);
selected_message_id = std::stoll(path_segs[3]);
} else if (path_segs.size() != 2) {
return page_E404(wgd);
} else
return page_E404(wgd);
}
bool chat_members = (path_segs[0] == "chat-members");
if (userinfo.isNull())
return een9::form_http_server_response_303("/");
RowChat_Content chatInfo;
try {
@ -40,7 +43,7 @@ namespace iu9cawebchat {
} catch (const std::exception& e) {
return page_E404(wgd);
}
if (get_role_of_user_in_chat(*wgd.db, userinfo["id"].asInteger().get_int(), chatInfo.id) == user_chat_role_deleted) {
if (get_role_of_user_in_chat(*wgd.db, userinfo["uid"].asInteger().get_int(), chatInfo.id) == user_chat_role_deleted) {
return page_E404(wgd);
}
@ -51,8 +54,8 @@ namespace iu9cawebchat {
// -1 means that nothing was selected
openedchat["selectedMessageId"].asInteger() = json::Integer(selected_message_id);
json::JSON initial_chatUpdResp = poll_update_chat_ONE_MSG_resp(*wgd.db, chatInfo.id, selected_message_id);
if (chat_members)
return http_R200("chat", wgd, {&config_presentation, &userinfo, &openedchat, &initial_chatUpdResp});
if (show_chat_members)
return http_R200("chat-members", wgd, {&config_presentation, &userinfo, &openedchat, &initial_chatUpdResp});
return http_R200("chat", wgd, {&config_presentation, &userinfo, &openedchat, &initial_chatUpdResp});
}
}

View File

@ -8,6 +8,7 @@ namespace iu9cawebchat {
}
json::JSON initial_chatListUpdResp = poll_update_chat_list_resp(*wgd.db,
userinfo["uid"].asInteger().get_int(), 0);
printf("%s\n", json::generate_str(initial_chatListUpdResp, json::print_pretty).c_str());
return http_R200("list-rooms", wgd, {&config_presentation, &userinfo, &initial_chatListUpdResp});
}
}

View File

@ -18,8 +18,10 @@ namespace iu9cawebchat {
if (cmp.first == "password")
password = cmp.second;
}
een9_ASSERT(check_nickname(nickname), "/login/accpet-data rejected impossible nickname");
een9_ASSERT(check_password(password), "/login/accpet-data rejected impossible password");
if (!check_nickname(nickname))
een9_THROW("/login/accpet-data rejected impossible nickname");
if (!check_password(password))
een9_THROW("/login/accpet-data rejected impossible password");
uid = find_user_by_credentials(*wgd.db, nickname, password);
} catch(const std::exception& e){}

View File

@ -39,6 +39,8 @@ namespace iu9cawebchat {
std::string when_page_user(WorkerGuestData& wgd, const json::JSON& config_presentation,
const een9::ClientRequest& req, const std::vector<LoginCookie>& login_cookies, const json::JSON& userinfo) {
if (userinfo.isNull())
return een9::form_http_server_response_303("/");
SqliteConnection& conn = *wgd.db;
if (!is_page_of_certain_user(req.uri_path))
return page_E404(wgd);
@ -104,7 +106,7 @@ namespace iu9cawebchat {
json::JSON msg_list = jsonify_html_message_list({{"",
config_presentation["phr"]["decl"]["incorrect-profile-data"].asString()}});
json::JSON alien_userprofile = user_row_to_userprofile_obj(conn, alien);
return http_R200("edit-profile", wgd, {&config_presentation, &alien_userprofile, &msg_list});
return http_R200("edit-profile", wgd, {&config_presentation, &userinfo, &alien_userprofile, &msg_list});
}
return een9::form_http_server_response_303_spec_head("/user/" + alien_nickname, response_hlines);
}
@ -112,9 +114,9 @@ namespace iu9cawebchat {
json::JSON alien_userprofile = user_row_to_userprofile_obj(conn, alien);
if (can_edit) {
json::JSON empty_msg_list = jsonify_html_message_list({});
return http_R200("edit-profile", wgd, {&config_presentation, &alien_userprofile, &empty_msg_list});
return http_R200("edit-profile", wgd, {&config_presentation, &userinfo, &alien_userprofile, &empty_msg_list});
}
return http_R200("view-profile", wgd, {&config_presentation, &alien_userprofile});
return http_R200("view-profile", wgd, {&config_presentation, &userinfo, &alien_userprofile});
}
een9_THROW("Bad method");
}

View File

@ -0,0 +1,6 @@
#ifndef IU9_CA_WEB_CHAT_LIB_DEBUG_H
#define IU9_CA_WEB_CHAT_LIB_DEBUG_H
#define debug_print_json(x) printf("%s\n", json::generate_str(x, json::print_pretty).c_str())
#endif

View File

@ -75,6 +75,9 @@ namespace iu9cawebchat {
"`chat_IncHistoryId` INTEGER NOT NULL,"
"PRIMARY KEY (`chatId`, `id`)"
")");
std::vector<std::string> sus = {"unknown", "undefined", "null", "none", "None", "NaN"};
for (auto& s: sus)
reserve_nickname(conn, s);
add_user(conn, "root", "Rootov Root Rootovich", root_pw, "One admin to rule them all", 0);
sqlite_nooutput(conn, "END");
} catch (const std::exception& e) {

View File

@ -41,6 +41,7 @@ namespace iu9cawebchat {
samI.update({
een9::StaticAssetManagerRule{assets_dir + "/css", "/assets/css", {{".css", "text/css"}} },
een9::StaticAssetManagerRule{assets_dir + "/js", "/assets/js", {{".js", "text/javascript"}} },
een9::StaticAssetManagerRule{assets_dir + "/gif", "/assets/gif", {{".gif", "image/gif"}} },
een9::StaticAssetManagerRule{assets_dir + "/img", "/assets/img", {
{".jpg", "image/jpg"}, {".png", "image/png"}, {".svg", "image/svg+xml"}
} },