249 lines
10 KiB
HTML
249 lines
10 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}Чат{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
/* прибиваем низ страницы и добавляем воздух снизу */
|
||
main.main{ padding-bottom:0 !important; }
|
||
|
||
/* ===== Контейнер страницы чата ===== */
|
||
.chat-root{
|
||
--chat-top-gap: 20px;
|
||
--chat-bottom-gap: 48px;
|
||
height: calc(100dvh - var(--navbar-h));
|
||
padding: var(--chat-top-gap) 0 calc(var(--chat-bottom-gap) + env(safe-area-inset-bottom));
|
||
box-sizing: border-box;
|
||
overflow: hidden;
|
||
display: flex; flex-direction: column;
|
||
background: var(--bs-body-bg);
|
||
}
|
||
@supports(height: 100svh){
|
||
.chat-root{ height: calc(100svh - var(--navbar-h)); }
|
||
}
|
||
.chat-root > .row{ height:100%; min-height:0; }
|
||
.chat-col{ display:flex; flex-direction:column; height:100%; min-height:0; }
|
||
.feed-header{ flex:0 0 auto; }
|
||
|
||
/* ===== Карточка чата ===== */
|
||
.feed-card.chat-box{
|
||
position:relative;
|
||
flex: 1 1 auto;
|
||
min-height:0;
|
||
display:grid;
|
||
grid-template-rows: 1fr auto; /* лента + композер */
|
||
overflow: hidden;
|
||
margin-bottom:0;
|
||
background: var(--bs-body-bg);
|
||
}
|
||
|
||
/* ===== Лента с кастомным скроллом ===== */
|
||
.chat-scroll{
|
||
overflow-y:auto;
|
||
padding:.75rem;
|
||
min-height:0;
|
||
scroll-behavior:smooth;
|
||
-webkit-overflow-scrolling:touch;
|
||
scrollbar-width: thin;
|
||
scrollbar-gutter: stable both-edges;
|
||
overscroll-behavior: contain;
|
||
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 70%) transparent;
|
||
}
|
||
.chat-scroll:hover{ scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 55%) transparent; }
|
||
.chat-scroll::-webkit-scrollbar{ width:10px; height:10px; }
|
||
.chat-scroll::-webkit-scrollbar-track{ background: transparent; margin-block:0; }
|
||
.chat-scroll::-webkit-scrollbar-thumb{
|
||
background-color: color-mix(in srgb, var(--bs-body-color), transparent 75%);
|
||
border-radius:999px; border:3px solid transparent; background-clip: padding-box;
|
||
}
|
||
.chat-scroll:hover::-webkit-scrollbar-thumb{
|
||
background-color: color-mix(in srgb, var(--bs-body-color), transparent 55%);
|
||
}
|
||
[data-bs-theme="dark"] .chat-scroll{
|
||
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 78%) transparent;
|
||
}
|
||
[data-bs-theme="dark"] .chat-scroll:hover{
|
||
scrollbar-color: color-mix(in srgb, var(--bs-body-color), transparent 60%) transparent;
|
||
}
|
||
[data-bs-theme="dark"] .chat-scroll::-webkit-scrollbar-thumb{
|
||
background-color: color-mix(in srgb, var(--bs-body-color), transparent 78%);
|
||
}
|
||
[data-bs-theme="dark"] .chat-scroll:hover::-webkit-scrollbar-thumb{
|
||
background-color: color-mix(in srgb, var(--bs-body-color), transparent 60%);
|
||
}
|
||
@media (hover:none), (pointer:coarse){
|
||
.chat-scroll{ scrollbar-width:none; }
|
||
.chat-scroll::-webkit-scrollbar{ display:none; }
|
||
}
|
||
|
||
/* ===== Композер: непрерывная линия и скругления ===== */
|
||
.chat-composer{
|
||
position: sticky; bottom:0;
|
||
border-top:none;
|
||
box-shadow: inset 0 1px 0 var(--bs-border-color);
|
||
border-bottom-left-radius: inherit;
|
||
border-bottom-right-radius: inherit;
|
||
background-clip: padding-box;
|
||
background: color-mix(in srgb, var(--bs-body-bg) 94%, transparent);
|
||
padding:.5rem .75rem calc(.5rem + env(safe-area-inset-bottom)) .75rem;
|
||
backdrop-filter: blur(8px);
|
||
z-index:1;
|
||
}
|
||
.composer-wrap{ display:flex; align-items:end; gap:.5rem; flex-wrap:nowrap; }
|
||
.composer-tools{ display:flex; gap:.25rem; align-items:center; }
|
||
|
||
/* Кнопка «вниз» */
|
||
.scroll-to-bottom{
|
||
position:absolute; right:.75rem; bottom:4.25rem;
|
||
z-index:2; display:none;
|
||
}
|
||
.scroll-to-bottom.show{ display:block; }
|
||
|
||
/* Пузыри / аватары */
|
||
.bubble{ border-radius:16px; padding:.5rem .75rem; word-break:break-word; box-shadow:0 1px 2px rgba(0,0,0,.06); }
|
||
.bubble-me{ background: color-mix(in srgb, var(--bs-primary) 92%, #fff 0%); color:#fff; }
|
||
.bubble-other{ background: var(--bs-secondary-bg); color: var(--bs-body-color); }
|
||
.mini-ava{ width:32px; height:32px; border-radius:9999px; overflow:hidden; display:grid; place-items:center; }
|
||
.mini-ava img{ width:100%; height:100%; object-fit:cover; display:block; }
|
||
|
||
/* ===== Разделители: гибкие, безопасные ===== */
|
||
.day-divider,
|
||
.unread-divider{
|
||
display:flex; align-items:center; justify-content:center; gap:.5rem;
|
||
margin:.5rem 0 1rem;
|
||
font-size:.85rem; line-height:1; font-weight:700;
|
||
pointer-events:none; user-select:none;
|
||
}
|
||
.day-divider::before, .day-divider::after,
|
||
.unread-divider::before, .unread-divider::after{
|
||
content:""; flex:1 1 auto; height:1px;
|
||
background: color-mix(in srgb, var(--bs-border-color), transparent 30%);
|
||
}
|
||
.day-divider > span, .unread-divider > span{
|
||
flex:0 0 auto; padding:0 .5rem; background: transparent;
|
||
}
|
||
.day-divider{ color: var(--bs-secondary-color); }
|
||
.unread-divider{ color: var(--bs-danger); }
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="container chat-root">
|
||
<div class="row justify-content-center">
|
||
<div class="col-lg-9 col-xl-8 chat-col">
|
||
|
||
<!-- Заголовок -->
|
||
<div class="feed-header">
|
||
{% if chat.type == 'global' %}
|
||
<i class="bi-globe fs-2 text-primary"></i>
|
||
<h2 class="title mb-0">Глобальный чат</h2>
|
||
{% elif chat.type == 'department' %}
|
||
<i class="bi-diagram-3 fs-2 text-primary"></i>
|
||
<h2 class="title mb-0">Чат отдела: {{ chat.department.name }}</h2>
|
||
{% else %}
|
||
<i class="bi-person fs-2 text-primary"></i>
|
||
<h2 class="title mb-0">
|
||
{% for u in chat.participants.all %}
|
||
{% if u != user %}
|
||
{% if not forloop.first %}, {% endif %}
|
||
<a href="{% url 'employees:employee_detail' u.pk %}" class="text-decoration-none">
|
||
{{ u.get_full_name|default:u.username }}
|
||
</a>
|
||
{% endif %}
|
||
{% endfor %}
|
||
</h2>
|
||
{% endif %}
|
||
<div class="ms-auto">
|
||
<a href="{% url 'communications:chat_list' %}" class="btn btn-ghost">
|
||
<i class="bi-arrow-left"></i> К чатам
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Корпус чата -->
|
||
<article class="feed-card chat-box">
|
||
|
||
<!-- Кнопка «вниз» -->
|
||
<button id="scrollBtn" class="btn btn-light shadow scroll-to-bottom" type="button" aria-label="Прокрутить вниз">
|
||
<i class="bi-arrow-down-circle"></i>
|
||
</button>
|
||
|
||
<!-- Лента сообщений -->
|
||
<div id="chatScroll"
|
||
class="chat-scroll"
|
||
data-me-id="{{ user.id }}"
|
||
data-chat-id="{{ chat.id }}"
|
||
data-last-read-ts="{% if last_read_at %}{{ last_read_at|date:'U' }}000{% endif %}">
|
||
{% if messages %}
|
||
{% for message in messages %}
|
||
{% ifchanged message.created_at|date:"d.m.Y" %}
|
||
<div class="day-divider"><span>{{ message.created_at|date:"d.m.Y" }}</span></div>
|
||
{% endifchanged %}
|
||
|
||
<!-- сообщение -->
|
||
<div class="d-flex mb-3 msg {% if message.author_id == user.id %}justify-content-end{% else %}justify-content-start{% endif %}"
|
||
data-id="{{ message.id }}"
|
||
data-ts="{{ message.created_at|date:'U' }}000"
|
||
data-author-id="{{ message.author_id }}">
|
||
{% if message.author_id != user.id %}
|
||
<a class="me-2 text-decoration-none" href="{% url 'employees:employee_detail' message.author.pk %}">
|
||
<span class="mini-ava border">
|
||
{% if message.author.avatar %}<img src="{{ message.author.avatar.url }}" alt="" loading="lazy">
|
||
{% else %}<i class="bi-person"></i>{% endif %}
|
||
</span>
|
||
</a>
|
||
{% endif %}
|
||
|
||
<div class="d-flex flex-column" style="max-width:80%;">
|
||
<div class="small text-secondary {% if message.author_id == user.id %}text-end{% endif %}">
|
||
{% if message.author_id == user.id %}
|
||
<a href="{% url 'employees:profile' %}" class="text-decoration-none">Вы</a>
|
||
{% else %}
|
||
<a href="{% url 'employees:employee_detail' message.author.pk %}" class="text-decoration-none fw-semibold">
|
||
{{ message.author.get_full_name|default:message.author.username }}
|
||
</a>
|
||
{% endif %}
|
||
· <time datetime="{{ message.created_at|date:'c' }}">{{ message.created_at|date:"H:i" }}</time>
|
||
</div>
|
||
<div class="mt-1 bubble {% if message.author_id == user.id %}bubble-me{% else %}bubble-other{% endif %}">
|
||
{{ message.content|linebreaksbr }}
|
||
</div>
|
||
</div>
|
||
|
||
{% if message.author_id == user.id %}
|
||
<a class="ms-2 text-decoration-none" href="{% url 'employees:profile' %}">
|
||
<span class="mini-ava border">
|
||
{% if user.avatar %}<img src="{{ user.avatar.url }}" alt="" loading="lazy">
|
||
{% else %}<i class="bi-person"></i>{% endif %}
|
||
</span>
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
{% else %}
|
||
<div class="text-center text-secondary py-5">Пока нет сообщений.</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Композер -->
|
||
<div class="chat-composer">
|
||
<div id="typing" class="small text-secondary ps-1 pt-1 d-none">Собеседник печатает…</div>
|
||
<form method="post" id="chatForm" class="composer-wrap">
|
||
{% csrf_token %}
|
||
<div class="composer-tools">
|
||
<button type="button" class="btn btn-ghost" title="Вложить файл"><i class="bi-paperclip"></i></button>
|
||
<button type="button" class="btn btn-ghost" title="Камера"><i class="bi-camera"></i></button>
|
||
</div>
|
||
<textarea name="content" id="id_content" class="form-control flex-grow-1" rows="1" placeholder="Напишите сообщение…" required></textarea>
|
||
<button type="submit" class="btn btn-primary"><i class="bi-send"></i></button>
|
||
</form>
|
||
</div>
|
||
</article>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
{% endblock %}
|