Major Feature Update: Modern Chat System & Admin Management
Features Added: 🔥 Modern Chat System: - Real-time messaging with modern Tailwind CSS design - Post-linked discussions for adventure sharing - Chat categories (general, technical-support, adventure-planning) - Mobile-responsive interface with gradient backgrounds - JavaScript polling for live message updates 🎯 Comprehensive Admin Panel: - Chat room management with merge capabilities - Password reset system with email templates - User management with admin controls - Chat statistics and analytics dashboard - Room binding to posts and categorization �� Mobile API Integration: - RESTful API endpoints at /api/v1/chat - Session-based authentication for mobile apps - Comprehensive endpoints for rooms, messages, users - Mobile app compatibility (React Native, Flutter) 🛠️ Technical Improvements: - Enhanced database models with ChatRoom categories - Password reset token system with email verification - Template synchronization fixes for Docker deployment - Migration scripts for database schema updates - Improved error handling and validation 🎨 UI/UX Enhancements: - Modern card-based layouts matching app design - Consistent styling across chat and admin interfaces - Mobile-optimized touch interactions - Professional gradient designs and glass morphism effects 📚 Documentation: - Updated README with comprehensive API documentation - Added deployment instructions for Docker (port 8100) - Configuration guide for production environments - Mobile integration examples and endpoints This update transforms the platform into a comprehensive motorcycle adventure community with modern chat capabilities and professional admin management tools.
This commit is contained in:
@@ -2,276 +2,131 @@
|
||||
|
||||
{% block title %}{{ room.name }} - Chat{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
.chat-room-container {
|
||||
height: calc(100vh - 80px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.messages-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-bottom: 1rem;
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.message-author {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
background: white;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 18px;
|
||||
border: 1px solid #e9ecef;
|
||||
max-width: 70%;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message.own-message {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.message.own-message .message-content {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.message.system-message .message-content {
|
||||
background: #e9ecef;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.message-input-area {
|
||||
padding: 1rem;
|
||||
background: white;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 25px;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.message-input:focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||||
}
|
||||
|
||||
.send-button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.send-button:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.participants-sidebar {
|
||||
width: 250px;
|
||||
background: white;
|
||||
border-left: 1px solid #e0e0e0;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.participant-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border-radius: 10px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.participant-item:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.participant-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.participant-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.participant-name {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.participant-role {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.admin-badge {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.participants-sidebar {
|
||||
display: none;
|
||||
}
|
||||
.message-content {
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="chat-room-container">
|
||||
<div class="chat-header">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h4 class="mb-0">{{ room.name }}</h4>
|
||||
<small class="text-muted">{{ room.description or 'No description' }}</small>
|
||||
{% if room.related_post %}
|
||||
<br><small class="text-primary">Related to: <a href="#" class="text-primary">{{ room.related_post.title }}</a></small>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ url_for('chat.index') }}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Chats
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chat-main">
|
||||
<div class="messages-container">
|
||||
<div class="messages-area" id="messagesArea">
|
||||
{% for message in messages %}
|
||||
<div class="message {% if message.user_id == current_user.id %}own-message{% endif %} {% if message.message_type == 'system' %}system-message{% endif %}">
|
||||
{% if message.message_type != 'system' %}
|
||||
<div class="message-header">
|
||||
<span class="message-author">{{ message.user.nickname }}</span>
|
||||
{% if message.user.is_admin %}
|
||||
<span class="admin-badge">ADMIN</span>
|
||||
<!-- Chat Room Header -->
|
||||
<div class="min-h-screen bg-gradient-to-br from-blue-900 via-purple-900 to-teal-900 py-8">
|
||||
<div class="max-w-6xl mx-auto px-4">
|
||||
<!-- Room Header Card -->
|
||||
<div class="bg-white/10 backdrop-blur-md rounded-2xl p-6 mb-6 border border-white/20">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between">
|
||||
<div class="mb-4 lg:mb-0">
|
||||
<div class="flex items-center mb-2">
|
||||
<div class="w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-500 rounded-xl flex items-center justify-center mr-4">
|
||||
<i class="fas fa-comments text-white text-xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">{{ room.name }}</h1>
|
||||
{% if room.description %}
|
||||
<p class="text-blue-200 mb-2">{{ room.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% if room.category %}
|
||||
<span class="px-3 py-1 bg-blue-500/30 text-blue-200 rounded-full text-sm border border-blue-400/30">
|
||||
<i class="fas fa-tag mr-1"></i>{{ room.category.title() }}
|
||||
</span>
|
||||
{% endif %}
|
||||
<span class="message-time">{{ message.created_at.strftime('%H:%M') }}</span>
|
||||
|
||||
{% if room.related_post %}
|
||||
<span class="px-3 py-1 bg-green-500/30 text-green-200 rounded-full text-sm border border-green-400/30">
|
||||
<i class="fas fa-link mr-1"></i>Linked to Post
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<span class="px-3 py-1 bg-purple-500/30 text-purple-200 rounded-full text-sm border border-purple-400/30">
|
||||
<i class="fas fa-users mr-1"></i>{{ room.participants.count() if room.participants else 0 }} Members
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% if room.related_post %}
|
||||
<div class="mt-3 p-3 bg-white/5 rounded-lg border border-white/10">
|
||||
<div class="flex items-center text-sm text-gray-300">
|
||||
<i class="fas fa-newspaper mr-2 text-green-400"></i>
|
||||
<span class="mr-2">Discussing:</span>
|
||||
<a href="{{ url_for('community.post_detail', post_id=room.related_post.id) }}"
|
||||
class="text-green-300 hover:text-green-200 underline transition-colors"
|
||||
target="_blank">
|
||||
{{ room.related_post.title }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="message-content">
|
||||
{{ message.content }}
|
||||
{% if message.is_edited %}
|
||||
<small class="text-muted"> (edited)</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button class="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg border border-white/20 transition-colors">
|
||||
<i class="fas fa-users mr-1"></i> Members
|
||||
</button>
|
||||
<button class="px-4 py-2 bg-white/10 hover:bg-white/20 text-white rounded-lg border border-white/20 transition-colors">
|
||||
<i class="fas fa-cog mr-1"></i> Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Interface -->
|
||||
<div class="bg-white/5 backdrop-blur-md rounded-2xl border border-white/20 overflow-hidden">
|
||||
<!-- Messages Area -->
|
||||
<div id="messages-container" class="h-96 overflow-y-auto p-6 space-y-4">
|
||||
{% for message in messages %}
|
||||
<div class="message mb-4 {{ 'ml-12' if message.sender_id == current_user.id else 'mr-12' }}">
|
||||
<div class="flex {{ 'justify-end' if message.sender_id == current_user.id else 'justify-start' }}">
|
||||
<div class="max-w-xs lg:max-w-md">
|
||||
{% if not message.is_system_message %}
|
||||
<div class="flex items-center mb-1 {{ 'justify-end' if message.sender_id == current_user.id else 'justify-start' }}">
|
||||
<span class="text-xs text-gray-400">
|
||||
{{ message.sender.nickname }}
|
||||
{% if message.sender.is_admin %}
|
||||
<span class="px-1 py-0.5 bg-yellow-100 text-yellow-800 text-xs rounded-full ml-1">ADMIN</span>
|
||||
{% endif %}
|
||||
• {{ message.created_at.strftime('%H:%M') }}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="rounded-2xl px-4 py-3 {{
|
||||
'bg-gradient-to-r from-blue-600 to-purple-600 text-white' if message.sender_id == current_user.id else
|
||||
'bg-yellow-100 border-yellow-300 text-yellow-800 text-center' if message.is_system_message else
|
||||
'bg-white border border-gray-200 text-gray-800'
|
||||
}}">
|
||||
{% if message.is_system_message %}
|
||||
<i class="fas fa-info-circle mr-2"></i>
|
||||
{% endif %}
|
||||
{{ message.content }}
|
||||
{% if message.is_edited %}
|
||||
<small class="opacity-75 text-xs block mt-1">(edited)</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="message-input-area">
|
||||
<form id="messageForm" class="d-flex gap-2">
|
||||
<input type="text"
|
||||
id="messageInput"
|
||||
class="form-control message-input"
|
||||
placeholder="Type your message..."
|
||||
maxlength="2000"
|
||||
autocomplete="off">
|
||||
<button type="submit" class="send-button">
|
||||
|
||||
<!-- Message Input -->
|
||||
<div class="border-t border-white/20 p-4">
|
||||
<form id="message-form" class="flex gap-3">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
id="message-input"
|
||||
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
placeholder="Type your message..."
|
||||
maxlength="1000"
|
||||
autocomplete="off"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="px-6 py-3 bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white rounded-xl font-medium transition-all duration-200 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</form>
|
||||
<small class="text-muted">Press Enter to send • Max 2000 characters</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="participants-sidebar">
|
||||
<h6 class="mb-3">Participants ({{ participants|length }})</h6>
|
||||
{% for participant in participants %}
|
||||
<div class="participant-item">
|
||||
<div class="participant-avatar">
|
||||
{{ participant.user.nickname[0].upper() }}
|
||||
</div>
|
||||
<div class="participant-info">
|
||||
<div class="participant-name">{{ participant.user.nickname }}</div>
|
||||
<div class="participant-role">
|
||||
{{ participant.role.title() }}
|
||||
{% if participant.user.is_admin %}• Admin{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -281,13 +136,13 @@ const currentUserId = {{ current_user.id }};
|
||||
let lastMessageId = {{ messages[-1].id if messages else 0 }};
|
||||
|
||||
// Message form handling
|
||||
document.getElementById('messageForm').addEventListener('submit', function(e) {
|
||||
document.getElementById('message-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
});
|
||||
|
||||
// Enter key handling
|
||||
document.getElementById('messageInput').addEventListener('keypress', function(e) {
|
||||
document.getElementById('message-input').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
sendMessage();
|
||||
@@ -295,7 +150,7 @@ document.getElementById('messageInput').addEventListener('keypress', function(e)
|
||||
});
|
||||
|
||||
function sendMessage() {
|
||||
const input = document.getElementById('messageInput');
|
||||
const input = document.getElementById('message-input');
|
||||
const content = input.value.trim();
|
||||
|
||||
if (!content) return;
|
||||
@@ -306,8 +161,7 @@ function sendMessage() {
|
||||
fetch(`/api/v1/chat/rooms/${roomId}/messages`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': '{{ csrf_token() }}'
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: content,
|
||||
@@ -335,35 +189,44 @@ function sendMessage() {
|
||||
}
|
||||
|
||||
function addMessageToUI(message) {
|
||||
const messagesArea = document.getElementById('messagesArea');
|
||||
const messagesArea = document.getElementById('messages-container');
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `message ${message.user.id === currentUserId ? 'own-message' : ''} ${message.message_type === 'system' ? 'system-message' : ''}`;
|
||||
messageDiv.className = `message mb-4 ${message.user.id === currentUserId ? 'ml-12' : 'mr-12'}`;
|
||||
messageDiv.setAttribute('data-message-id', message.id);
|
||||
|
||||
let messageHTML = '';
|
||||
if (message.message_type !== 'system') {
|
||||
messageHTML += `
|
||||
<div class="message-header">
|
||||
<span class="message-author">${message.user.nickname}</span>
|
||||
${message.user.is_admin ? '<span class="admin-badge">ADMIN</span>' : ''}
|
||||
<span class="message-time">${new Date(message.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
|
||||
const isOwnMessage = message.user.id === currentUserId;
|
||||
const isSystemMessage = message.message_type === 'system';
|
||||
|
||||
messageDiv.innerHTML = `
|
||||
<div class="flex ${isOwnMessage ? 'justify-end' : 'justify-start'}">
|
||||
<div class="max-w-xs lg:max-w-md">
|
||||
${!isSystemMessage ? `
|
||||
<div class="flex items-center mb-1 ${isOwnMessage ? 'justify-end' : 'justify-start'}">
|
||||
<span class="text-xs text-gray-400">
|
||||
${message.user.nickname} ${message.user.is_admin ? '<span class="px-1 py-0.5 bg-yellow-100 text-yellow-800 text-xs rounded-full ml-1">ADMIN</span>' : ''} • ${new Date(message.created_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
|
||||
</span>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="rounded-2xl px-4 py-3 ${
|
||||
isOwnMessage ? 'bg-gradient-to-r from-blue-600 to-purple-600 text-white' :
|
||||
isSystemMessage ? 'bg-yellow-100 border-yellow-300 text-yellow-800 text-center' :
|
||||
'bg-white border border-gray-200 text-gray-800'
|
||||
}">
|
||||
${isSystemMessage ? '<i class="fas fa-info-circle mr-2"></i>' : ''}
|
||||
${message.content}
|
||||
${message.is_edited ? '<small class="opacity-75 text-xs block mt-1"> (edited)</small>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
messageHTML += `
|
||||
<div class="message-content">
|
||||
${message.content}
|
||||
${message.is_edited ? '<small class="text-muted"> (edited)</small>' : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
messageDiv.innerHTML = messageHTML;
|
||||
messagesArea.appendChild(messageDiv);
|
||||
|
||||
lastMessageId = message.id;
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
const messagesArea = document.getElementById('messagesArea');
|
||||
const messagesArea = document.getElementById('messages-container');
|
||||
messagesArea.scrollTop = messagesArea.scrollHeight;
|
||||
}
|
||||
|
||||
@@ -390,11 +253,10 @@ scrollToBottom();
|
||||
setInterval(loadNewMessages, 3000);
|
||||
|
||||
// Auto-focus on message input
|
||||
document.getElementById('messageInput').focus();
|
||||
document.getElementById('message-input').focus();
|
||||
|
||||
// Mobile app integration
|
||||
if (window.ReactNativeWebView) {
|
||||
// React Native WebView integration
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||
type: 'chat_room_opened',
|
||||
roomId: roomId,
|
||||
|
||||
Reference in New Issue
Block a user