improve: add message thread view with read/sent status indicators
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
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
Shows full conversation thread when replies exist, with per-message status (sent/read with timestamps), sender→recipient flow, and current message highlighted. Single messages show status bar at bottom. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0abdc4b264
commit
0c5e4cf726
@ -324,7 +324,19 @@ def messages_view(message_id):
|
||||
'is_active': classified.is_active
|
||||
}
|
||||
|
||||
return render_template('messages/view.html', message=message, context=context)
|
||||
# Pobierz cały wątek (oryginał + odpowiedzi)
|
||||
root_id = message.parent_id or message.id
|
||||
thread = db.query(PrivateMessage).options(
|
||||
joinedload(PrivateMessage.attachments)
|
||||
).filter(
|
||||
(PrivateMessage.id == root_id) | (PrivateMessage.parent_id == root_id)
|
||||
).order_by(PrivateMessage.created_at.asc()).all()
|
||||
|
||||
# Jeśli wątek ma tylko jedną wiadomość, nie pokazuj jako wątek
|
||||
if len(thread) <= 1:
|
||||
thread = None
|
||||
|
||||
return render_template('messages/view.html', message=message, context=context, thread=thread)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ message.subject or 'Wiadomosc' }} - Norda Biznes Partner{% endblock %}
|
||||
{% block title %}{{ message.subject or 'Wiadomość' }} - Norda Biznes Partner{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
@ -9,28 +9,45 @@
|
||||
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);
|
||||
}
|
||||
|
||||
.message-card {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
border: 1px solid var(--border-color, #e5e7eb);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
.message-card-header {
|
||||
padding: var(--spacing-lg) var(--spacing-xl);
|
||||
border-bottom: 1px solid var(--border-color, #f3f4f6);
|
||||
}
|
||||
|
||||
.message-subject-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
padding-bottom: var(--spacing-lg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
justify-content: space-between;
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.message-subject {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.message-participants {
|
||||
@ -40,8 +57,8 @@
|
||||
}
|
||||
|
||||
.participant-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
@ -49,7 +66,12 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: var(--font-size-lg);
|
||||
font-size: var(--font-size-base);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.participant-avatar.is-me {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
.participant-info {
|
||||
@ -61,14 +83,13 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.participant-role {
|
||||
.participant-detail {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.message-date {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
.message-card-body {
|
||||
padding: var(--spacing-lg) var(--spacing-xl);
|
||||
}
|
||||
|
||||
.message-body {
|
||||
@ -77,68 +98,124 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.reply-section {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-xl);
|
||||
box-shadow: var(--shadow);
|
||||
/* Status bar */
|
||||
.message-status-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-sm) var(--spacing-xl);
|
||||
background: var(--background, #f9fafb);
|
||||
border-top: 1px solid var(--border-color, #f3f4f6);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.reply-section h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: var(--spacing-md);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-base);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
.back-link {
|
||||
.status-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
.status-item svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.status-item.read {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.status-item.sent {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Thread */
|
||||
.thread-section {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.thread-section h3 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: var(--spacing-md);
|
||||
}
|
||||
|
||||
.thread-message {
|
||||
padding: var(--spacing-lg);
|
||||
border-left: 3px solid var(--border);
|
||||
margin-left: var(--spacing-lg);
|
||||
margin-top: var(--spacing-lg);
|
||||
background: var(--background);
|
||||
border-radius: 0 var(--radius) var(--radius) 0;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-color, #e5e7eb);
|
||||
border-radius: var(--radius-lg);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.thread-message .participant-name {
|
||||
.thread-message.is-current {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 1px var(--primary);
|
||||
}
|
||||
|
||||
.thread-msg-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.thread-msg-sender {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.thread-avatar {
|
||||
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;
|
||||
}
|
||||
|
||||
.thread-avatar.is-me {
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
.thread-sender-name {
|
||||
font-weight: 500;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.thread-message .message-body {
|
||||
margin-top: var(--spacing-sm);
|
||||
.thread-msg-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.thread-msg-body {
|
||||
padding: 0 var(--spacing-md) var(--spacing-sm);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.thread-msg-status {
|
||||
padding: var(--spacing-xs) var(--spacing-md) var(--spacing-sm);
|
||||
}
|
||||
|
||||
/* Context badge */
|
||||
.context-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@ -176,6 +253,80 @@
|
||||
.context-badge.inactive a {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* Attachments */
|
||||
.attachments-section {
|
||||
margin-top: var(--spacing-md);
|
||||
padding-top: var(--spacing-md);
|
||||
border-top: 1px solid var(--border-color, #f3f4f6);
|
||||
}
|
||||
|
||||
.attachments-label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: var(--spacing-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.attachment-item + .attachment-item {
|
||||
border-top: 1px solid var(--border-color, #f3f4f6);
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: var(--spacing-md);
|
||||
border: 1px solid var(--border-color, #e5e7eb);
|
||||
border-radius: var(--radius);
|
||||
font-size: var(--font-size-base);
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.message-card-header,
|
||||
.message-card-body,
|
||||
.message-status-bar {
|
||||
padding-left: var(--spacing-md);
|
||||
padding-right: var(--spacing-md);
|
||||
}
|
||||
.reply-section {
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
.message-subject-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -185,83 +336,178 @@
|
||||
<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
|
||||
Powrót do wiadomości
|
||||
</a>
|
||||
|
||||
<div class="message-card">
|
||||
{% if context %}
|
||||
<div class="context-badge {% if not context.is_active %}inactive{% endif %}">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Dotyczy ogloszenia: <a href="{{ context.url }}">{{ context.title }}</a>
|
||||
{% if not context.is_active %}<span style="color: var(--text-secondary);">(nieaktywne)</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if thread %}
|
||||
{# ===== WĄTEK — wiele wiadomości ===== #}
|
||||
<div class="thread-section">
|
||||
<h3>Konwersacja ({{ thread|length }} {{ 'wiadomość' if thread|length == 1 else ('wiadomości' if thread|length < 5 else 'wiadomości') }})</h3>
|
||||
|
||||
<div class="message-header">
|
||||
<div>
|
||||
<div class="message-subject">{{ message.subject or '(brak tematu)' }}</div>
|
||||
<div class="message-participants">
|
||||
<div class="participant-avatar">
|
||||
{{ (message.sender.name or message.sender.email)[0].upper() }}
|
||||
</div>
|
||||
<div class="participant-info">
|
||||
<div class="participant-name">{{ message.sender.name or message.sender.email.split('@')[0] }}</div>
|
||||
<div class="participant-role">
|
||||
{% if message.sender_id == current_user.id %}
|
||||
Do: {{ message.recipient.name or message.recipient.email.split('@')[0] }}
|
||||
{% else %}
|
||||
Do: mnie
|
||||
{% endif %}
|
||||
</div>
|
||||
{% for msg in thread %}
|
||||
<div class="thread-message {% if msg.id == message.id %}is-current{% endif %}">
|
||||
<div class="thread-msg-header">
|
||||
<div class="thread-msg-sender">
|
||||
<div class="thread-avatar {% if msg.sender_id == current_user.id %}is-me{% endif %}">
|
||||
{{ (msg.sender.name or msg.sender.email)[0].upper() }}
|
||||
</div>
|
||||
<span class="thread-sender-name">
|
||||
{% if msg.sender_id == current_user.id %}Ty{% else %}{{ msg.sender.name or msg.sender.email.split('@')[0] }}{% endif %}
|
||||
<span style="font-weight: 400; color: var(--text-secondary);">→ {% if msg.recipient_id == current_user.id %}Ty{% else %}{{ msg.recipient.name or msg.recipient.email.split('@')[0] }}{% endif %}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="thread-msg-meta">
|
||||
<span>{{ msg.created_at.strftime('%d.%m.%Y %H:%M') }}</span>
|
||||
{% if msg.sender_id == current_user.id %}
|
||||
{% if msg.is_read %}
|
||||
<span class="status-item read" title="Przeczytana {{ msg.read_at.strftime('%d.%m.%Y %H:%M') if msg.read_at else '' }}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 7l-8 8-4-4"/><path d="M22 7l-8 8-1.5-1.5" opacity="0.6"/></svg>
|
||||
Przeczytana
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="status-item sent">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 7l-8 8-4-4"/></svg>
|
||||
Wysłana
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if msg.is_read %}
|
||||
<span class="status-item read">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4L19 7"/></svg>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if msg.attachments %}
|
||||
<span title="{{ msg.attachments|length }} załącznik{{ 'ów' if msg.attachments|length > 1 else '' }}">📎</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-date">
|
||||
{{ message.created_at.strftime('%d.%m.%Y %H:%M') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-body">{{ message.content }}</div>
|
||||
|
||||
{% if message.attachments %}
|
||||
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border);">
|
||||
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); margin-bottom: 8px; font-weight: 500;">Zalaczniki ({{ message.attachments|length }})</div>
|
||||
{% for att in message.attachments %}
|
||||
{% set is_image = att.mime_type and att.mime_type.startswith('image/') %}
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 8px 0; {% if not loop.last %}border-bottom: 1px solid var(--border);{% endif %}">
|
||||
{% 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: 300px; max-height: 200px; border-radius: var(--radius); border: 1px solid var(--border);">
|
||||
</a>
|
||||
{% else %}
|
||||
<span style="font-size: 20px;">📄</span>
|
||||
<div>
|
||||
<a href="/static/uploads/messages/{{ att.created_at.strftime('%Y/%m') }}/{{ att.stored_filename }}" download="{{ att.filename }}" style="color: var(--primary); font-weight: 500;">{{ att.filename }}</a>
|
||||
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); margin-left: 8px;">{{ (att.file_size / 1024)|round(0)|int }} KB</span>
|
||||
<div class="thread-msg-body">{{ msg.content }}</div>
|
||||
{% if msg.attachments %}
|
||||
<div class="attachments-section" style="margin: 0 var(--spacing-md) var(--spacing-sm); padding-top: var(--spacing-sm);">
|
||||
{% for att in msg.attachments %}
|
||||
{% set is_image = att.mime_type and att.mime_type.startswith('image/') %}
|
||||
<div class="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: 18px;">📄</span>
|
||||
<div>
|
||||
<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); margin-left: 6px;">{{ (att.file_size / 1024)|round(0)|int }} KB</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Odpowiedź -->
|
||||
{% else %}
|
||||
{# ===== POJEDYNCZA WIADOMOŚĆ ===== #}
|
||||
<div class="message-card">
|
||||
{% if context %}
|
||||
<div style="padding: var(--spacing-md) var(--spacing-xl) 0;">
|
||||
<div class="context-badge {% if not context.is_active %}inactive{% endif %}">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
Dotyczy ogłoszenia: <a href="{{ context.url }}">{{ context.title }}</a>
|
||||
{% if not context.is_active %}<span style="color: var(--text-secondary);">(nieaktywne)</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="message-card-header">
|
||||
<div class="message-subject-row">
|
||||
<div class="message-subject">{{ message.subject or '(brak tematu)' }}</div>
|
||||
</div>
|
||||
<div class="message-participants">
|
||||
<div class="participant-avatar {% if message.sender_id == current_user.id %}is-me{% endif %}">
|
||||
{{ (message.sender.name or message.sender.email)[0].upper() }}
|
||||
</div>
|
||||
<div class="participant-info">
|
||||
<div class="participant-name">
|
||||
{% if message.sender_id == current_user.id %}Ty{% else %}{{ message.sender.name or message.sender.email.split('@')[0] }}{% endif %}
|
||||
</div>
|
||||
<div class="participant-detail">
|
||||
Do: {% if message.recipient_id == current_user.id %}mnie{% else %}{{ message.recipient.name or message.recipient.email.split('@')[0] }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="message-card-body">
|
||||
<div class="message-body">{{ message.content }}</div>
|
||||
|
||||
{% if message.attachments %}
|
||||
<div class="attachments-section">
|
||||
<div class="attachments-label">Załączniki ({{ message.attachments|length }})</div>
|
||||
{% for att in message.attachments %}
|
||||
{% set is_image = att.mime_type and att.mime_type.startswith('image/') %}
|
||||
<div class="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: 300px; max-height: 200px; border-radius: var(--radius); border: 1px solid var(--border-color, #e5e7eb);">
|
||||
</a>
|
||||
{% else %}
|
||||
<span style="font-size: 20px;">📄</span>
|
||||
<div>
|
||||
<a href="/static/uploads/messages/{{ att.created_at.strftime('%Y/%m') }}/{{ att.stored_filename }}" download="{{ att.filename }}" style="color: var(--primary); font-weight: 500;">{{ att.filename }}</a>
|
||||
<span style="font-size: var(--font-size-xs); color: var(--text-secondary); margin-left: 8px;">{{ (att.file_size / 1024)|round(0)|int }} KB</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="message-status-bar">
|
||||
<span class="status-item">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>
|
||||
Wysłana {{ message.created_at.strftime('%d.%m.%Y o %H:%M') }}
|
||||
</span>
|
||||
{% if message.sender_id == current_user.id %}
|
||||
{% if message.is_read %}
|
||||
<span class="status-item read">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 7l-8 8-4-4"/><path d="M22 7l-8 8-1.5-1.5" opacity="0.6"/></svg>
|
||||
Przeczytana {{ message.read_at.strftime('%d.%m.%Y o %H:%M') if message.read_at else '' }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="status-item sent">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 7l-8 8-4-4"/></svg>
|
||||
Oczekuje na przeczytanie
|
||||
</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="status-item read">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 13l4 4L19 7"/></svg>
|
||||
Odebrana i przeczytana
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ===== FORMULARZ ODPOWIEDZI ===== #}
|
||||
{% if message.sender_id != current_user.id or message.recipient_id != current_user.id %}
|
||||
<div class="reply-section">
|
||||
<h3>Odpowiedz</h3>
|
||||
<form method="POST" action="{{ url_for('messages_reply', message_id=message.id) }}" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<div class="form-group">
|
||||
<textarea name="content" rows="4" required placeholder="Napisz odpowiedz..."></textarea>
|
||||
<textarea name="content" rows="4" required placeholder="Napisz odpowiedź…"></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 łącznie</span>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Wyslij odpowiedz</button>
|
||||
<button type="submit" class="btn btn-primary">Wyślij odpowiedź</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user