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
The clipboard.onPaste override broke text pasting in Quill 2.x where this internal API is no longer public. Replaced with a DOM paste event listener on quill.root that only intercepts image pastes and lets Quill handle text paste natively. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
333 lines
14 KiB
HTML
333 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Wiadomości - Norda Biznes Partner{% endblock %}
|
|
{% block container_class %}conversations-page{% endblock %}
|
|
|
|
{% block head_extra %}
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/quill.snow.css') }}">
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/conversations.css') }}?v=13">
|
|
<script src="{{ url_for('static', filename='js/vendor/quill.js') }}"></script>
|
|
<style>
|
|
footer { display: none !important; }
|
|
main { padding-bottom: 0 !important; margin-bottom: 0 !important; }
|
|
/* Images in messages — responsive with max size */
|
|
.message-content img {
|
|
max-width: 100%;
|
|
max-height: 300px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
object-fit: contain;
|
|
}
|
|
.message-content img:hover { opacity: 0.9; }
|
|
/* Images in Quill editor — click to select, drag corner to resize */
|
|
.ql-editor img {
|
|
max-width: 100%;
|
|
max-height: 200px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
.ql-editor img:hover {
|
|
outline: 2px solid #3b82f6;
|
|
outline-offset: 2px;
|
|
}
|
|
.ql-editor img.selected {
|
|
outline: 2px solid #3b82f6;
|
|
outline-offset: 2px;
|
|
resize: both;
|
|
overflow: hidden;
|
|
display: inline-block;
|
|
}
|
|
/* Hide floating buttons that overlap with chat input area */
|
|
.pwa-smart-banner, .staging-panel-toggle { display: none !important; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="conversations-container" id="conversationsApp">
|
|
|
|
<!-- Left panel: Conversation list -->
|
|
<div class="conversations-panel" id="conversationsPanel">
|
|
<div class="conversations-header">
|
|
<h2>Wiadomości</h2>
|
|
<div class="conversations-search-row">
|
|
<div class="conversations-search">
|
|
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="11" cy="11" r="8"></circle>
|
|
<path d="M21 21l-4.35-4.35"></path>
|
|
</svg>
|
|
<input type="text" id="searchInput" placeholder="Szukaj rozmów i wiadomości...">
|
|
</div>
|
|
<button class="btn-new-conversation" id="newMessageBtn" title="Nowa wiadomość">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M12 20h9"></path>
|
|
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
|
|
</svg>
|
|
<span class="btn-new-text">Nowa wiadomość</span>
|
|
</button>
|
|
<button class="btn-new-conversation btn-new-group" id="newGroupBtn" title="Nowa grupa">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
|
<circle cx="9" cy="7" r="4"></circle>
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
|
</svg>
|
|
<span class="btn-new-text">Nowa grupa</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="conversation-list" id="conversationList">
|
|
<!-- Rendered by JS from __CONVERSATIONS__ data -->
|
|
</div>
|
|
<div class="archive-link" id="archiveLink" style="display:none">
|
|
<a href="#">Archiwum</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right panel: Chat view -->
|
|
<div class="chat-panel" id="chatPanel">
|
|
|
|
<!-- Empty state (shown by default) -->
|
|
<div class="chat-empty" id="chatEmpty">
|
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
|
</svg>
|
|
<p>Wybierz rozmowę lub rozpocznij nową</p>
|
|
</div>
|
|
|
|
<!-- Chat header (hidden until conversation selected) -->
|
|
<div class="chat-header" id="chatHeader" style="display:none">
|
|
<div class="chat-header-left">
|
|
<button class="back-btn" id="backBtn" title="Wróć">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M19 12H5"></path>
|
|
<path d="M12 19l-7-7 7-7"></path>
|
|
</svg>
|
|
</button>
|
|
<div class="chat-header-avatar" id="headerAvatar"></div>
|
|
<div class="chat-header-info">
|
|
<h3 id="headerName"></h3>
|
|
<div class="subtitle" id="headerSubtitle"></div>
|
|
</div>
|
|
</div>
|
|
<div class="chat-header-actions">
|
|
<button class="btn-group-settings" id="groupSettingsBtn" style="display:none" title="Zarządzaj grupą">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
|
<circle cx="9" cy="7" r="4"></circle>
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pinned bar -->
|
|
<div class="pinned-bar" id="pinnedBar" style="display:none">
|
|
<span>📌 <span id="pinnedCount">0</span> przypiętych</span>
|
|
<button id="togglePins">Pokaż</button>
|
|
</div>
|
|
|
|
<!-- Group management panel -->
|
|
<div class="group-panel" id="groupPanel" style="display:none">
|
|
<div class="group-panel-header">
|
|
<h3>Zarządzanie grupą</h3>
|
|
<button class="modal-close" id="closeGroupPanel">×</button>
|
|
</div>
|
|
<div class="group-panel-body">
|
|
<div class="group-panel-section">
|
|
<label>Nazwa grupy:</label>
|
|
<div class="group-name-row">
|
|
<input type="text" id="groupEditName" placeholder="Nazwa grupy...">
|
|
<button class="btn-primary btn-sm" id="saveGroupName">Zapisz</button>
|
|
</div>
|
|
</div>
|
|
<div class="group-panel-section">
|
|
<label>Członkowie (<span id="groupMemberCount">0</span>):</label>
|
|
<div id="groupMembersList" class="group-members-list"></div>
|
|
</div>
|
|
<div class="group-panel-section" id="groupAddMemberSection">
|
|
<label>Dodaj członka:</label>
|
|
<input type="text" id="groupAddMemberSearch" placeholder="Wpisz imię lub nazwisko...">
|
|
<div class="recipient-suggestions" id="groupAddMemberSuggestions"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Messages area -->
|
|
<div class="chat-messages" id="chatMessages" style="display:none">
|
|
<!-- Rendered by JS -->
|
|
</div>
|
|
|
|
<!-- Typing indicator -->
|
|
<div class="typing-indicator" id="typingIndicator" style="display:none">
|
|
<span id="typingName"></span> pisze
|
|
<span class="typing-dots"><span>.</span><span>.</span><span>.</span></span>
|
|
</div>
|
|
|
|
<!-- Input area -->
|
|
<div class="chat-input-area" id="chatInputArea" style="display:none">
|
|
<!-- Reply quote (shown when replying to a message) -->
|
|
<div class="reply-preview" id="replyPreview" style="display:none">
|
|
<div class="reply-preview-content">
|
|
<span class="reply-preview-name" id="replyPreviewName"></span>
|
|
<span class="reply-preview-text" id="replyPreviewText"></span>
|
|
</div>
|
|
<button class="reply-preview-close" id="replyPreviewClose">×</button>
|
|
</div>
|
|
|
|
<div class="chat-input-wrapper">
|
|
<div id="quillEditor"></div>
|
|
<div class="input-actions">
|
|
<button id="attachBtn" title="Dołącz plik">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path>
|
|
</svg>
|
|
</button>
|
|
<button class="send-btn" id="sendBtn" title="Wyślij">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M22 2L11 13"></path>
|
|
<path d="M22 2l-7 20-4-9-9-4 20-7z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<input type="file" id="fileInput" multiple style="display:none" accept=".jpg,.jpeg,.png,.gif,.pdf,.docx,.xlsx">
|
|
<div class="attachments-preview" id="attachmentsPreview" style="display:none"></div>
|
|
<div class="input-hint"><kbd>Enter</kbd> wyślij · <kbd>Shift</kbd>+<kbd>Enter</kbd> nowa linia</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Context menu (floating, positioned by JS) -->
|
|
<div class="message-context-menu" id="contextMenu" style="display:none">
|
|
<button data-action="reply" title="Odpowiedz">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M9 17l-5-5 5-5"></path>
|
|
<path d="M20 18v-2a4 4 0 0 0-4-4H4"></path>
|
|
</svg>
|
|
</button>
|
|
<button data-action="react" title="Reaguj">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="10"></circle>
|
|
<path d="M8 14s1.5 2 4 2 4-2 4-2"></path>
|
|
<line x1="9" y1="9" x2="9.01" y2="9"></line>
|
|
<line x1="15" y1="9" x2="15.01" y2="9"></line>
|
|
</svg>
|
|
</button>
|
|
<button data-action="forward" title="Przekaż">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M15 17l5-5-5-5"></path>
|
|
<path d="M4 18v-2a4 4 0 0 1 4-4h12"></path>
|
|
</svg>
|
|
</button>
|
|
<button data-action="pin" title="Przypnij">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M12 2l1.09 3.26L16 6l-2 2 .73 3.27L12 9.5l-2.73 1.77L10 8 8 6l2.91-.74L12 2z"></path>
|
|
<path d="M12 22v-8"></path>
|
|
</svg>
|
|
</button>
|
|
<button data-action="edit" title="Edytuj" class="owner-only">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
|
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
|
|
</svg>
|
|
</button>
|
|
<button data-action="delete" title="Usuń" class="owner-only">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="3 6 5 6 21 6"></polyline>
|
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
|
|
<line x1="10" y1="11" x2="10" y2="17"></line>
|
|
<line x1="14" y1="11" x2="14" y2="17"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Emoji picker -->
|
|
<div class="emoji-picker" id="emojiPicker" style="display:none">
|
|
<button data-emoji="👍">👍</button>
|
|
<button data-emoji="❤️">❤️</button>
|
|
<button data-emoji="😂">😂</button>
|
|
<button data-emoji="😮">😮</button>
|
|
<button data-emoji="😢">😢</button>
|
|
<button data-emoji="✅">✅</button>
|
|
</div>
|
|
|
|
<!-- New group modal -->
|
|
<div class="modal-overlay" id="newGroupModal" style="display:none">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3>Nowa grupa</h3>
|
|
<button class="modal-close" id="closeNewGroup">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="recipient-input">
|
|
<label>Nazwa grupy:</label>
|
|
<input type="text" id="groupNameInput" placeholder="np. Projekt XYZ, Zarząd..." style="margin-bottom:12px">
|
|
</div>
|
|
<div class="recipient-input">
|
|
<label>Członkowie:</label>
|
|
<input type="text" id="groupRecipientSearch" placeholder="Wpisz imię lub nazwisko...">
|
|
<div class="recipient-suggestions" id="groupRecipientSuggestions"></div>
|
|
<div class="selected-recipients" id="groupSelectedRecipients"></div>
|
|
</div>
|
|
<div id="groupMessageEditor"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn-secondary" id="cancelNewGroup">Anuluj</button>
|
|
<button class="btn-primary" id="sendNewGroup">Utwórz grupę</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- New message modal -->
|
|
<div class="modal-overlay" id="newMessageModal" style="display:none">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h3>Nowa wiadomość</h3>
|
|
<button class="modal-close" id="closeNewMessage">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="recipient-input">
|
|
<label>Do:</label>
|
|
<input type="text" id="recipientSearch" placeholder="Wpisz imię lub nazwisko...">
|
|
<div class="recipient-suggestions" id="recipientSuggestions"></div>
|
|
<div class="selected-recipients" id="selectedRecipients"></div>
|
|
</div>
|
|
<div id="newMessageEditor"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn-secondary" id="cancelNewMessage">Anuluj</button>
|
|
<button class="btn-primary" id="sendNewMessage">Wyślij</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
window.__CONVERSATIONS__ = {{ conversations_json|safe }};
|
|
window.__CURRENT_USER__ = {{ current_user_json|safe }};
|
|
window.__USERS__ = {{ users_json|default('[]')|safe }};
|
|
window.__CSRF_TOKEN__ = '{{ csrf_token() }}';
|
|
|
|
// Dynamically set container height based on actual position
|
|
(function() {
|
|
var container = document.getElementById('conversationsApp');
|
|
if (container) {
|
|
var top = container.getBoundingClientRect().top;
|
|
container.style.height = (window.innerHeight - top) + 'px';
|
|
window.addEventListener('resize', function() {
|
|
var t = container.getBoundingClientRect().top;
|
|
container.style.height = (window.innerHeight - t) + 'px';
|
|
});
|
|
}
|
|
})();
|
|
|
|
// Load conversations.js after data is set
|
|
(function() {
|
|
var s = document.createElement('script');
|
|
s.src = '{{ url_for("static", filename="js/conversations.js") }}?v=28';
|
|
document.body.appendChild(s);
|
|
})();
|
|
{% endblock %}
|