feat(messages): search in message content — Enter triggers server-side search with highlighted results
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
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bfc6707a65
commit
f8badfccac
@ -462,3 +462,76 @@ def forward_message(message_id):
|
||||
return jsonify({'error': 'Błąd przekazywania wiadomości'}), 500
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 6. GET /api/messages/search — Search message content
|
||||
# ============================================================
|
||||
|
||||
@bp.route('/api/messages/search', methods=['GET'])
|
||||
@login_required
|
||||
@member_required
|
||||
def search_messages():
|
||||
"""Search messages across all user's conversations."""
|
||||
query = request.args.get('q', '').strip()
|
||||
if len(query) < 3:
|
||||
return jsonify({'results': []})
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
from sqlalchemy import func
|
||||
|
||||
# Get user's conversation IDs
|
||||
user_conv_ids = [m.conversation_id for m in
|
||||
db.query(ConversationMember.conversation_id).filter_by(
|
||||
user_id=current_user.id
|
||||
).all()]
|
||||
|
||||
if not user_conv_ids:
|
||||
return jsonify({'results': []})
|
||||
|
||||
# Search in message content (strip HTML via regexp_replace)
|
||||
search_term = f'%{query}%'
|
||||
messages = db.query(ConvMessage).filter(
|
||||
ConvMessage.conversation_id.in_(user_conv_ids),
|
||||
ConvMessage.is_deleted == False, # noqa: E712
|
||||
func.regexp_replace(ConvMessage.content, '<[^>]+>', '', 'g').ilike(search_term),
|
||||
).order_by(ConvMessage.created_at.desc()).limit(20).all()
|
||||
|
||||
results = []
|
||||
for msg in messages:
|
||||
# Get conversation display name
|
||||
conv = msg.conversation
|
||||
conv_name = conv.name
|
||||
if not conv.is_group:
|
||||
other = [m for m in conv.members if m.user_id != current_user.id]
|
||||
if other and other[0].user:
|
||||
conv_name = other[0].user.name or other[0].user.email.split('@')[0]
|
||||
|
||||
# Build preview with context around match
|
||||
plain = strip_html(msg.content)
|
||||
lower_plain = plain.lower()
|
||||
idx = lower_plain.find(query.lower())
|
||||
if idx >= 0:
|
||||
start = max(0, idx - 30)
|
||||
end = min(len(plain), idx + len(query) + 50)
|
||||
preview = ('...' if start > 0 else '') + plain[start:end] + ('...' if end < len(plain) else '')
|
||||
else:
|
||||
preview = plain[:80]
|
||||
|
||||
results.append({
|
||||
'message_id': msg.id,
|
||||
'conversation_id': msg.conversation_id,
|
||||
'conversation_name': conv_name,
|
||||
'sender_name': msg.sender.name if msg.sender else '',
|
||||
'preview': preview,
|
||||
'created_at': msg.created_at.isoformat() if msg.created_at else None,
|
||||
})
|
||||
|
||||
return jsonify({'results': results})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"search_messages error: {e}")
|
||||
return jsonify({'results': []})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@ -979,6 +979,59 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* --- Search results section --- */
|
||||
.search-results-section {
|
||||
border-top: 2px solid var(--conv-accent, #2E4872);
|
||||
margin-top: 8px;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.search-results-header {
|
||||
padding: 8px 16px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--conv-accent, #2E4872);
|
||||
}
|
||||
|
||||
.search-result-item {
|
||||
padding: 10px 16px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--conv-border, #e1dfdd);
|
||||
transition: background 0.12s;
|
||||
}
|
||||
|
||||
.search-result-item:hover {
|
||||
background: var(--conv-primary-light, #EDF0F5);
|
||||
}
|
||||
|
||||
.search-result-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--conv-text-primary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.search-result-preview {
|
||||
font-size: 12px;
|
||||
color: var(--conv-text-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.search-result-preview mark {
|
||||
background: #fef08a;
|
||||
color: inherit;
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.search-result-date {
|
||||
font-size: 11px;
|
||||
color: var(--conv-text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.input-hint {
|
||||
font-size: 12px;
|
||||
color: var(--conv-text-secondary, #605e5c);
|
||||
|
||||
@ -1925,10 +1925,92 @@
|
||||
var timer = null;
|
||||
searchInput.addEventListener('input', function () {
|
||||
clearTimeout(timer);
|
||||
var q = searchInput.value.trim();
|
||||
// Client-side filter for conversation names
|
||||
timer = setTimeout(function () {
|
||||
ConversationList.searchFilter(searchInput.value);
|
||||
ConversationList.searchFilter(q);
|
||||
// Clear server results when typing changes
|
||||
if (q.length < 3) {
|
||||
Search.clearResults();
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// Enter = search in message content (server-side)
|
||||
searchInput.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
var q = searchInput.value.trim();
|
||||
if (q.length >= 3) {
|
||||
Search.searchContent(q);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
searchContent: async function (query) {
|
||||
try {
|
||||
var data = await api('/api/messages/search?q=' + encodeURIComponent(query));
|
||||
var results = data.results || [];
|
||||
Search.showResults(results, query);
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
showResults: function (results, query) {
|
||||
Search.clearResults();
|
||||
var container = document.getElementById('conversationList');
|
||||
if (!container) return;
|
||||
|
||||
var section = el('div', 'search-results-section');
|
||||
section.id = 'searchResultsSection';
|
||||
|
||||
var header = el('div', 'search-results-header');
|
||||
header.textContent = results.length
|
||||
? 'Znalezione w wiadomościach (' + results.length + '):'
|
||||
: 'Brak wyników w treści wiadomości';
|
||||
section.appendChild(header);
|
||||
|
||||
results.forEach(function (r) {
|
||||
var item = el('div', 'search-result-item');
|
||||
|
||||
var nameRow = el('div', 'search-result-name');
|
||||
nameRow.textContent = r.conversation_name + (r.sender_name ? ' — ' + r.sender_name : '');
|
||||
item.appendChild(nameRow);
|
||||
|
||||
var previewRow = el('div', 'search-result-preview');
|
||||
// Highlight query in preview
|
||||
var escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
previewRow.innerHTML = r.preview.replace(
|
||||
new RegExp('(' + escapedQuery + ')', 'gi'),
|
||||
'<mark>$1</mark>'
|
||||
);
|
||||
item.appendChild(previewRow);
|
||||
|
||||
var dateRow = el('div', 'search-result-date');
|
||||
dateRow.textContent = formatTime(r.created_at);
|
||||
item.appendChild(dateRow);
|
||||
|
||||
item.addEventListener('click', function () {
|
||||
Search.clearResults();
|
||||
var input = document.getElementById('searchInput');
|
||||
if (input) input.value = '';
|
||||
ConversationList.searchFilter('');
|
||||
ConversationList.selectConversation(r.conversation_id);
|
||||
// Scroll to message after load
|
||||
setTimeout(function () {
|
||||
ChatView.scrollToMessage(r.message_id);
|
||||
}, 500);
|
||||
});
|
||||
|
||||
section.appendChild(item);
|
||||
});
|
||||
|
||||
container.appendChild(section);
|
||||
},
|
||||
|
||||
clearResults: function () {
|
||||
var existing = document.getElementById('searchResultsSection');
|
||||
if (existing) existing.remove();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
{% 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=4">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/conversations.css') }}?v=5">
|
||||
<script src="{{ url_for('static', filename='js/vendor/quill.js') }}"></script>
|
||||
<style>
|
||||
footer { display: none !important; }
|
||||
@ -27,7 +27,7 @@
|
||||
<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...">
|
||||
<input type="text" id="searchInput" placeholder="Szukaj... (Enter = w treści)">
|
||||
<button class="new-message-btn" id="newMessageBtn" title="Nowa wiadomość">
|
||||
<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="M12 20h9"></path>
|
||||
@ -226,7 +226,7 @@ window.__CSRF_TOKEN__ = '{{ csrf_token() }}';
|
||||
// Load conversations.js after data is set
|
||||
(function() {
|
||||
var s = document.createElement('script');
|
||||
s.src = '{{ url_for("static", filename="js/conversations.js") }}?v=9';
|
||||
s.src = '{{ url_for("static", filename="js/conversations.js") }}?v=10';
|
||||
document.body.appendChild(s);
|
||||
})();
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user