Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Production moved from on-prem VM 249 (10.22.68.249) to OVH VPS (57.128.200.27, inpi-vps-waw01). Updated ALL documentation, slash commands, memory files, architecture docs, and deploy procedures. Added |local_time Jinja filter (UTC→Europe/Warsaw) and converted 155 .strftime() calls across 71 templates so timestamps display in Polish timezone regardless of server timezone. Also includes: created_by_id tracking, abort import fix, ICS calendar fix for missing end times, Pros Poland data cleanup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
709 lines
24 KiB
HTML
709 lines
24 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ group.display_name }} - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block head_extra %}
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/quill.snow.css') }}">
|
|
<script src="{{ url_for('static', filename='js/vendor/quill.js') }}"></script>
|
|
{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
.group-container {
|
|
max-width: 800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.back-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
margin-bottom: var(--spacing-lg);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.back-link:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
/* Group header */
|
|
.group-header {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
border: 1px solid var(--border-color, #e5e7eb);
|
|
padding: var(--spacing-lg) var(--spacing-xl);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.group-header-top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.group-title {
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.group-member-count {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 2px 10px;
|
|
background: #eff6ff;
|
|
border-radius: 12px;
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.group-manage-link {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.group-manage-link:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
/* Members collapsible */
|
|
.members-toggle {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
margin-top: var(--spacing-md);
|
|
padding: 0;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.members-toggle:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.members-toggle svg {
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.members-toggle.open svg {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.members-list {
|
|
display: none;
|
|
margin-top: var(--spacing-sm);
|
|
padding-top: var(--spacing-sm);
|
|
border-top: 1px solid var(--border-color, #f3f4f6);
|
|
}
|
|
|
|
.members-list.visible {
|
|
display: block;
|
|
}
|
|
|
|
.member-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
padding: var(--spacing-xs) 0;
|
|
}
|
|
|
|
.member-avatar {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.member-initial {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: 11px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.member-name {
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.role-badge {
|
|
display: inline-block;
|
|
padding: 1px 8px;
|
|
border-radius: 10px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.role-badge.owner {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.role-badge.moderator {
|
|
background: #dbeafe;
|
|
color: #1d4ed8;
|
|
}
|
|
|
|
/* Messages */
|
|
.messages-section {
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.group-message {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.msg-avatar {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.msg-initial {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: var(--font-size-sm);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.msg-initial.is-me {
|
|
background: var(--secondary);
|
|
}
|
|
|
|
.msg-bubble {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.msg-header {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: var(--spacing-sm);
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.msg-sender {
|
|
font-weight: 500;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.msg-time {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.msg-delete-btn {
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-size: 12px;
|
|
opacity: 0;
|
|
transition: opacity 0.15s;
|
|
padding: 2px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.msg-header:hover .msg-delete-btn {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.msg-delete-btn:hover {
|
|
opacity: 1 !important;
|
|
}
|
|
|
|
.msg-content {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border-color, #e5e7eb);
|
|
border-radius: var(--radius);
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
font-size: var(--font-size-sm);
|
|
line-height: 1.6;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.msg-content img {
|
|
max-width: 100%;
|
|
height: auto;
|
|
border-radius: var(--radius);
|
|
margin: var(--spacing-xs) 0;
|
|
}
|
|
|
|
.msg-content a {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.msg-content p {
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.msg-content p:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.msg-attachments {
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.msg-attachment-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 4px 0;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
/* Read receipts */
|
|
.msg-read-receipts {
|
|
display: flex;
|
|
gap: 2px;
|
|
justify-content: flex-end;
|
|
margin-top: 4px;
|
|
padding-right: 4px;
|
|
}
|
|
|
|
.read-receipt {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
overflow: hidden;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.read-receipt img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.read-receipt span {
|
|
width: 20px;
|
|
height: 20px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: var(--primary);
|
|
color: white;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
/* Reply form */
|
|
.reply-section {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
border: 1px solid var(--border-color, #e5e7eb);
|
|
padding: var(--spacing-lg) var(--spacing-xl);
|
|
}
|
|
|
|
.reply-section h3 {
|
|
font-size: var(--font-size-base);
|
|
font-weight: 600;
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.quill-container {
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
}
|
|
.quill-container .ql-toolbar {
|
|
border-top-left-radius: var(--radius);
|
|
border-top-right-radius: var(--radius);
|
|
}
|
|
.quill-container .ql-container {
|
|
border-bottom-left-radius: var(--radius);
|
|
border-bottom-right-radius: var(--radius);
|
|
font-size: var(--font-size-base);
|
|
}
|
|
.quill-container .ql-editor {
|
|
min-height: 120px;
|
|
}
|
|
.quill-container .ql-editor img {
|
|
max-width: 100%;
|
|
height: auto;
|
|
border-radius: var(--radius);
|
|
margin: var(--spacing-sm) 0;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: var(--spacing-2xl) var(--spacing-lg);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.group-header {
|
|
padding: var(--spacing-md);
|
|
}
|
|
.group-header-top {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
.reply-section {
|
|
padding: var(--spacing-md);
|
|
}
|
|
}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="group-container">
|
|
<a href="{{ url_for('messages_inbox') }}" class="back-link">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
|
</svg>
|
|
Powrot do wiadomosci
|
|
</a>
|
|
|
|
{# ===== GROUP HEADER ===== #}
|
|
<div class="group-header">
|
|
<div class="group-header-top">
|
|
<div class="group-title">
|
|
{{ group.display_name }}
|
|
<span class="group-member-count">{{ members|length }}</span>
|
|
</div>
|
|
{% if membership.is_owner or membership.is_moderator %}
|
|
<a href="{{ url_for('messages.group_manage', group_id=group.id) }}" class="group-manage-link">
|
|
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="vertical-align: -2px;">
|
|
<path d="M12.22 2h-.44a2 2 0 00-2 2v.18a2 2 0 01-1 1.73l-.43.25a2 2 0 01-2 0l-.15-.08a2 2 0 00-2.73.73l-.22.38a2 2 0 00.73 2.73l.15.1a2 2 0 011 1.72v.51a2 2 0 01-1 1.74l-.15.09a2 2 0 00-.73 2.73l.22.38a2 2 0 002.73.73l.15-.08a2 2 0 012 0l.43.25a2 2 0 011 1.73V20a2 2 0 002 2h.44a2 2 0 002-2v-.18a2 2 0 011-1.73l.43-.25a2 2 0 012 0l.15.08a2 2 0 002.73-.73l.22-.39a2 2 0 00-.73-2.73l-.15-.08a2 2 0 01-1-1.74v-.5a2 2 0 011-1.74l.15-.09a2 2 0 00.73-2.73l-.22-.38a2 2 0 00-2.73-.73l-.15.08a2 2 0 01-2 0l-.43-.25a2 2 0 01-1-1.73V4a2 2 0 00-2-2z"/>
|
|
<circle cx="12" cy="12" r="3"/>
|
|
</svg>
|
|
Zarzadzaj
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<button class="members-toggle" id="members-toggle" type="button">
|
|
<svg width="12" height="12" fill="currentColor" viewBox="0 0 24 24"><path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6z"/></svg>
|
|
Uczestnicy ({{ members|length }})
|
|
</button>
|
|
<div class="members-list" id="members-list">
|
|
{% for m in members %}
|
|
<div class="member-item">
|
|
<a href="{{ url_for('public.user_profile', user_id=m.user_id) }}" style="display: flex; align-items: center; gap: inherit; text-decoration: none; color: inherit;" {% if m.user_id != current_user.id %}onmouseover="this.querySelector('.member-name').style.textDecoration='underline'" onmouseout="this.querySelector('.member-name').style.textDecoration='none'"{% endif %}>
|
|
{% if m.user.avatar_path %}
|
|
<img src="{{ url_for('static', filename=m.user.avatar_path) }}" class="member-avatar" alt="">
|
|
{% else %}
|
|
<div class="member-initial">{{ (m.user.name or m.user.email)[0].upper() }}</div>
|
|
{% endif %}
|
|
<span class="member-name">
|
|
{% if m.user_id == current_user.id %}Ty{% else %}{{ m.user.name or m.user.email.split('@')[0] }}{% endif %}
|
|
</span>
|
|
</a>
|
|
{% if m.is_owner %}
|
|
<span class="role-badge owner">Wlasciciel</span>
|
|
{% elif m.is_moderator %}
|
|
<span class="role-badge moderator">Moderator</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
{# ===== MESSAGES ===== #}
|
|
<div class="messages-section" id="messages-section">
|
|
{% if messages %}
|
|
{% for msg in messages %}
|
|
<div class="group-message">
|
|
<a href="{{ url_for('public.user_profile', user_id=msg.sender_id) }}" style="text-decoration: none; flex-shrink: 0;">
|
|
{% if msg.sender.avatar_path %}
|
|
<img src="{{ url_for('static', filename=msg.sender.avatar_path) }}" class="msg-avatar" alt="">
|
|
{% else %}
|
|
<div class="msg-initial {% if msg.sender_id == current_user.id %}is-me{% endif %}">
|
|
{{ (msg.sender.name or msg.sender.email)[0].upper() }}
|
|
</div>
|
|
{% endif %}
|
|
</a>
|
|
<div class="msg-bubble">
|
|
<div class="msg-header">
|
|
<span class="msg-sender">{% if msg.sender_id == current_user.id %}Ty{% else %}<a href="{{ url_for('public.user_profile', user_id=msg.sender_id) }}" style="color: inherit; text-decoration: none;" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">{{ msg.sender.name or msg.sender.email.split('@')[0] }}</a>{% endif %}</span>
|
|
<span class="msg-time">{{ msg.created_at|local_time('%d.%m.%Y %H:%M') }}</span>
|
|
{% if msg.sender_id == current_user.id or membership.is_owner %}
|
|
<form method="POST" action="{{ url_for('messages.group_delete_message', group_id=group.id, message_id=msg.id) }}" style="display:inline; margin-left: 4px;" onsubmit="return nordaConfirm(this, 'Wiadomość zostanie trwale usunięta.', {title: 'Usunąć wiadomość?', icon: '🗑️', okText: 'Tak, usuń'});">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="msg-delete-btn" title="Usuń wiadomość">🗑</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
<div class="msg-content">{{ msg.content|linkify }}</div>
|
|
{% if msg.attachments %}
|
|
<div class="msg-attachments">
|
|
{% for att in msg.attachments %}
|
|
{% set is_image = att.mime_type and att.mime_type.startswith('image/') %}
|
|
<div class="msg-attachment-item">
|
|
{% if is_image %}
|
|
<a href="/static/uploads/messages/{{ att.created_at.strftime('%Y/%m') }}/{{ att.stored_filename }}" target="_blank">
|
|
<img src="/static/uploads/messages/{{ att.created_at.strftime('%Y/%m') }}/{{ att.stored_filename }}" alt="{{ att.filename }}" style="max-width: 200px; max-height: 120px; border-radius: var(--radius); border: 1px solid var(--border-color, #e5e7eb);">
|
|
</a>
|
|
{% else %}
|
|
<span style="font-size: 16px;">📄</span>
|
|
<a href="/static/uploads/messages/{{ att.created_at.strftime('%Y/%m') }}/{{ att.stored_filename }}" download="{{ att.filename }}" style="color: var(--primary); font-weight: 500; font-size: var(--font-size-sm);">{{ att.filename }}</a>
|
|
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">{{ (att.file_size / 1024)|round(0)|int }} KB</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if read_receipts and read_receipts.get(msg.id) %}
|
|
<div class="msg-read-receipts">
|
|
{% for reader in read_receipts[msg.id] %}
|
|
<div class="read-receipt" title="{{ reader.name or reader.email.split('@')[0] }}">
|
|
{% if reader.avatar_path %}
|
|
<img src="{{ url_for('static', filename=reader.avatar_path) }}" alt="">
|
|
{% else %}
|
|
<span>{{ (reader.name or reader.email)[0].upper() }}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>Brak wiadomosci w grupie</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{# ===== REPLY FORM ===== #}
|
|
<div class="reply-section">
|
|
<h3>Napisz wiadomosc</h3>
|
|
<form method="POST" action="{{ url_for('messages.group_send', group_id=group.id) }}" enctype="multipart/form-data" id="reply-form">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<div class="form-group">
|
|
<div id="quill-reply" class="quill-container" style="background: var(--surface);"></div>
|
|
<textarea name="content" id="reply-content" style="display:none;" required></textarea>
|
|
</div>
|
|
<div class="form-group" style="margin-top: 8px;">
|
|
<input type="file" name="attachments" multiple accept=".jpg,.jpeg,.png,.gif,.pdf,.docx,.xlsx" style="font-size: var(--font-size-sm);">
|
|
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">Maks. 3 pliki, 15MB lacznie</span>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Wyslij</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
/* Members toggle */
|
|
(function() {
|
|
var toggle = document.getElementById('members-toggle');
|
|
var list = document.getElementById('members-list');
|
|
if (toggle && list) {
|
|
toggle.addEventListener('click', function() {
|
|
toggle.classList.toggle('open');
|
|
list.classList.toggle('visible');
|
|
});
|
|
}
|
|
})();
|
|
|
|
/* Auto-scroll to bottom */
|
|
(function() {
|
|
var section = document.getElementById('messages-section');
|
|
if (section && section.children.length > 0) {
|
|
var lastMsg = section.lastElementChild;
|
|
if (lastMsg) {
|
|
lastMsg.scrollIntoView({behavior: 'instant', block: 'end'});
|
|
}
|
|
}
|
|
})();
|
|
|
|
/* Quill reply editor */
|
|
(function() {
|
|
var replyDiv = document.getElementById('quill-reply');
|
|
if (!replyDiv) return;
|
|
|
|
var csrfToken = '{{ csrf_token() }}';
|
|
var quill = new Quill('#quill-reply', {
|
|
theme: 'snow',
|
|
placeholder: 'Napisz wiadomosc...',
|
|
modules: {
|
|
toolbar: {
|
|
container: [
|
|
['bold', 'italic'],
|
|
['link', 'image'],
|
|
['clean']
|
|
],
|
|
handlers: {
|
|
image: function() {
|
|
var input = document.createElement('input');
|
|
input.setAttribute('type', 'file');
|
|
input.setAttribute('accept', 'image/*');
|
|
input.click();
|
|
input.onchange = function() {
|
|
if (input.files && input.files[0]) uploadImage(input.files[0]);
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
function uploadImage(file) {
|
|
var fd = new FormData();
|
|
fd.append('image', file);
|
|
fetch('/api/messages/upload-image', {
|
|
method: 'POST',
|
|
headers: {'X-CSRFToken': csrfToken},
|
|
body: fd
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.url) {
|
|
var range = quill.getSelection(true);
|
|
quill.insertEmbed(range.index, 'image', data.url);
|
|
quill.setSelection(range.index + 1);
|
|
}
|
|
});
|
|
}
|
|
|
|
quill.root.addEventListener('paste', function(e) {
|
|
var items = (e.clipboardData || {}).items || [];
|
|
for (var i = 0; i < items.length; i++) {
|
|
if (items[i].type.indexOf('image') !== -1) {
|
|
e.stopImmediatePropagation();
|
|
e.preventDefault();
|
|
var file = items[i].getAsFile();
|
|
if (file) uploadImage(file);
|
|
return;
|
|
}
|
|
}
|
|
}, true);
|
|
|
|
var textarea = document.getElementById('reply-content');
|
|
quill.on('text-change', function() {
|
|
var html = quill.root.innerHTML;
|
|
textarea.value = (html === '<p><br></p>') ? '' : html;
|
|
});
|
|
|
|
document.getElementById('reply-form').addEventListener('submit', function(e) {
|
|
var html = quill.root.innerHTML;
|
|
textarea.value = (html === '<p><br></p>') ? '' : html;
|
|
if (!textarea.value.trim()) {
|
|
e.preventDefault();
|
|
alert('Tresc wiadomosci jest wymagana.');
|
|
return;
|
|
}
|
|
var btn = this.querySelector('button[type="submit"]');
|
|
if (btn.disabled) { e.preventDefault(); return; }
|
|
btn.disabled = true;
|
|
btn.textContent = 'Wysylanie...';
|
|
});
|
|
})();
|
|
|
|
/* Auto-refresh: poll for new messages every 5 seconds */
|
|
(function() {
|
|
var section = document.getElementById('messages-section');
|
|
if (!section) return;
|
|
|
|
var allMsgs = section.querySelectorAll('.group-message');
|
|
var lastId = 0;
|
|
if (allMsgs.length > 0) {
|
|
// Extract max message ID from data attribute or count
|
|
lastId = {{ messages[-1].id if messages else 0 }};
|
|
}
|
|
|
|
function createMessageHtml(msg) {
|
|
var avatarHtml;
|
|
if (msg.sender_avatar) {
|
|
avatarHtml = '<a href="/osoba/' + msg.sender_id + '" style="text-decoration:none;flex-shrink:0;">' +
|
|
'<img src="' + msg.sender_avatar + '" class="msg-avatar" alt="">' +
|
|
'</a>';
|
|
} else {
|
|
avatarHtml = '<a href="/osoba/' + msg.sender_id + '" style="text-decoration:none;flex-shrink:0;">' +
|
|
'<div class="msg-initial' + (msg.is_me ? ' is-me' : '') + '">' + msg.sender_initial + '</div>' +
|
|
'</a>';
|
|
}
|
|
|
|
var senderHtml = msg.is_me ? 'Ty' :
|
|
'<a href="/osoba/' + msg.sender_id + '" style="color:inherit;text-decoration:none;" onmouseover="this.style.textDecoration=\'underline\'" onmouseout="this.style.textDecoration=\'none\'">' + msg.sender_name + '</a>';
|
|
|
|
var receiptsHtml = '';
|
|
if (msg.read_by && msg.read_by.length > 0) {
|
|
receiptsHtml = '<div class="msg-read-receipts">';
|
|
msg.read_by.forEach(function(r) {
|
|
if (r.avatar_url) {
|
|
receiptsHtml += '<div class="read-receipt" title="' + r.name + '"><img src="' + r.avatar_url + '" alt=""></div>';
|
|
} else {
|
|
receiptsHtml += '<div class="read-receipt" title="' + r.name + '"><span>' + r.initial + '</span></div>';
|
|
}
|
|
});
|
|
receiptsHtml += '</div>';
|
|
}
|
|
|
|
return '<div class="group-message" data-msg-id="' + msg.id + '">' +
|
|
avatarHtml +
|
|
'<div class="msg-bubble">' +
|
|
'<div class="msg-header">' +
|
|
'<span class="msg-sender">' + senderHtml + '</span>' +
|
|
'<span class="msg-time">' + msg.time + '</span>' +
|
|
'</div>' +
|
|
'<div class="msg-content">' + msg.content + '</div>' +
|
|
'</div>' +
|
|
receiptsHtml +
|
|
'</div>';
|
|
}
|
|
|
|
function pollMessages() {
|
|
fetch('/api/grupa/' + {{ group.id }} + '/nowe?after=' + lastId)
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.messages && data.messages.length > 0) {
|
|
// Remove old read receipts (they may have moved)
|
|
section.querySelectorAll('.msg-read-receipts').forEach(function(el) { el.remove(); });
|
|
|
|
data.messages.forEach(function(msg) {
|
|
section.insertAdjacentHTML('beforeend', createMessageHtml(msg));
|
|
lastId = msg.id;
|
|
});
|
|
|
|
// Scroll to bottom
|
|
section.scrollTop = section.scrollHeight;
|
|
}
|
|
})
|
|
.catch(function() { /* silent */ });
|
|
}
|
|
|
|
setInterval(pollMessages, 5000);
|
|
})();
|
|
{% endblock %}
|