From 6839208527b0c0315eb67189e1ad47445b0cc6e7 Mon Sep 17 00:00:00 2001 From: alireza parvaresh Date: Fri, 30 Jan 2026 14:34:31 +0330 Subject: [PATCH] feat: Add user blocking, group management, message filters, and voice messaging - Add user block/unblock functionality with database table and API endpoints - Implement group leave and member removal features for group creators - Add message filtering UI to view specific media types (images, videos, audio) - Implement voice message recording with MediaRecorder API - Filter incoming messages from blocked users - Add UI controls in chat header for all new features - Create group members modal with member management --- cmd/server/main.go | 213 ++++++++++++++++++ public/app.js | 543 +++++++++++++++++++++++++++++++++++++++++++-- public/index.html | 367 ++++++++++++++++++++++++++---- 3 files changed, 1063 insertions(+), 60 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index 03eed86..7a2285b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -103,6 +103,14 @@ func initDB() { timestamp DATETIME DEFAULT CURRENT_TIMESTAMP )`) + // Create blocked users table + db.Exec(`CREATE TABLE IF NOT EXISTS blocked_users ( + blocker_id INTEGER NOT NULL, + blocked_id INTEGER NOT NULL, + blocked_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY(blocker_id, blocked_id) + )`) + log.Println("[OK] Database initialized") } @@ -556,6 +564,204 @@ func typingHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } +// onlineUsersHandler returns list of online user IDs +func onlineUsersHandler(w http.ResponseWriter, r *http.Request) { + enableCORS(w) + if r.Method == "OPTIONS" { + return + } + + mu.RLock() + onlineIDs := make([]int, 0, len(clients)) + for userID := range clients { + onlineIDs = append(onlineIDs, userID) + } + mu.RUnlock() + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(onlineIDs) +} + +// blockUserHandler handles blocking a user +func blockUserHandler(w http.ResponseWriter, r *http.Request) { + enableCORS(w) + if r.Method == "OPTIONS" { + return + } + + var req struct { + BlockerID int `json:"blockerId"` + BlockedID int `json:"blockedId"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + _, err := db.Exec("INSERT OR IGNORE INTO blocked_users (blocker_id, blocked_id) VALUES (?, ?)", + req.BlockerID, req.BlockedID) + if err != nil { + http.Error(w, "Failed to block user", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "blocked"}) +} + +// unblockUserHandler handles unblocking a user +func unblockUserHandler(w http.ResponseWriter, r *http.Request) { + enableCORS(w) + if r.Method == "OPTIONS" { + return + } + + var req struct { + BlockerID int `json:"blockerId"` + BlockedID int `json:"blockedId"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + _, err := db.Exec("DELETE FROM blocked_users WHERE blocker_id = ? AND blocked_id = ?", + req.BlockerID, req.BlockedID) + if err != nil { + http.Error(w, "Failed to unblock user", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "unblocked"}) +} + +// getBlockedUsersHandler returns list of blocked users for a user +func getBlockedUsersHandler(w http.ResponseWriter, r *http.Request) { + enableCORS(w) + userID := r.URL.Query().Get("userId") + + rows, err := db.Query("SELECT blocked_id FROM blocked_users WHERE blocker_id = ?", userID) + if err != nil { + http.Error(w, "Server error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var blockedIDs []int + for rows.Next() { + var id int + rows.Scan(&id) + blockedIDs = append(blockedIDs, id) + } + + if blockedIDs == nil { + blockedIDs = []int{} + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(blockedIDs) +} + +// leaveGroupHandler handles user leaving a group +func leaveGroupHandler(w http.ResponseWriter, r *http.Request) { + enableCORS(w) + if r.Method == "OPTIONS" { + return + } + + var req struct { + GroupID int `json:"groupId"` + UserID int `json:"userId"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + _, err := db.Exec("DELETE FROM group_members WHERE group_id = ? AND user_id = ?", + req.GroupID, req.UserID) + if err != nil { + http.Error(w, "Failed to leave group", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "left"}) +} + +// removeGroupMemberHandler handles removing a member from group +func removeGroupMemberHandler(w http.ResponseWriter, r *http.Request) { + enableCORS(w) + if r.Method == "OPTIONS" { + return + } + + var req struct { + GroupID int `json:"groupId"` + UserID int `json:"userId"` + RemoverID int `json:"removerId"` + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request", http.StatusBadRequest) + return + } + + // Check if remover is the group creator + var creatorID int + err := db.QueryRow("SELECT creator_id FROM groups WHERE id = ?", req.GroupID).Scan(&creatorID) + if err != nil || creatorID != req.RemoverID { + http.Error(w, "Only group creator can remove members", http.StatusForbidden) + return + } + + _, err = db.Exec("DELETE FROM group_members WHERE group_id = ? AND user_id = ?", + req.GroupID, req.UserID) + if err != nil { + http.Error(w, "Failed to remove member", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "removed"}) +} + +// getGroupMembersHandler returns members of a group +func getGroupMembersHandler(w http.ResponseWriter, r *http.Request) { + enableCORS(w) + groupID := r.URL.Query().Get("groupId") + + rows, err := db.Query(` + SELECT u.id, u.username, u.full_name + FROM users u + JOIN group_members gm ON u.id = gm.user_id + WHERE gm.group_id = ? + ORDER BY u.full_name`, groupID) + if err != nil { + http.Error(w, "Server error", http.StatusInternalServerError) + return + } + defer rows.Close() + + var members []User + for rows.Next() { + var u User + rows.Scan(&u.ID, &u.Username, &u.FullName) + members = append(members, u) + } + + if members == nil { + members = []User{} + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(members) +} + func main() { initDB() @@ -574,6 +780,13 @@ func main() { http.HandleFunc("/api/send", sendMessageHandler) http.HandleFunc("/api/typing", typingHandler) http.HandleFunc("/api/upload", uploadHandler) + http.HandleFunc("/api/online", onlineUsersHandler) + http.HandleFunc("/api/block", blockUserHandler) + http.HandleFunc("/api/unblock", unblockUserHandler) + http.HandleFunc("/api/blocked", getBlockedUsersHandler) + http.HandleFunc("/api/group/leave", leaveGroupHandler) + http.HandleFunc("/api/group/remove", removeGroupMemberHandler) + http.HandleFunc("/api/group/members", getGroupMembersHandler) http.HandleFunc("/events", sseHandler) // Serve uploaded files diff --git a/public/app.js b/public/app.js index defa4ad..9776218 100644 --- a/public/app.js +++ b/public/app.js @@ -4,6 +4,11 @@ let contacts = []; let groups = []; let currentChat = null; let typingTimeouts = {}; +let onlineUsers = new Set(); +let blockedUsers = new Set(); +let messageFilter = 'all'; // 'all', 'media', 'image', 'video', 'audio' +let mediaRecorder = null; +let audioChunks = []; // Auth functions function toggleAuth() { @@ -93,9 +98,84 @@ async function initChat() { await loadContacts(); await loadGroups(); + await loadBlockedUsers(); connectSSE(); } +// Load blocked users +async function loadBlockedUsers() { + try { + const res = await fetch(`/api/blocked?userId=${currentUser.id}`); + const blocked = await res.json(); + blockedUsers = new Set(blocked); + } catch (err) { + console.error('Failed to load blocked users:', err); + } +} + +// Block user +async function blockUser(userId) { + if (!confirm('Are you sure you want to block this user?')) return; + + try { + const res = await fetch('/api/block', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + blockerId: currentUser.id, + blockedId: userId + }) + }); + + if (res.ok) { + blockedUsers.add(userId); + alert('User blocked successfully'); + if (currentChat && currentChat.id === userId) { + // Close current chat if blocking current contact + document.getElementById('chat-area').innerHTML = ` +
+
🚫
+

User is blocked

+
+ `; + currentChat = null; + } + } else { + alert('Failed to block user'); + } + } catch (err) { + alert('Failed to block user'); + } +} + +// Unblock user +async function unblockUser(userId) { + if (!confirm('Are you sure you want to unblock this user?')) return; + + try { + const res = await fetch('/api/unblock', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + blockerId: currentUser.id, + blockedId: userId + }) + }); + + if (res.ok) { + blockedUsers.delete(userId); + alert('User unblocked successfully'); + if (currentChat && currentChat.id === userId) { + await loadMessages(); + } + } else { + alert('Failed to unblock user'); + } + } catch (err) { + alert('Failed to unblock user'); + } +} + function connectSSE() { eventSource = new EventSource(`/events?userId=${currentUser.id}`); @@ -120,7 +200,24 @@ function handleIncomingMessage(msg) { return; } + if (msg.type === 'user_online') { + onlineUsers.add(msg.userId); + updateOnlineStatus(); + return; + } + + if (msg.type === 'user_offline') { + onlineUsers.delete(msg.userId); + updateOnlineStatus(); + return; + } + if (msg.type === 'message') { + // Filter messages from blocked users + if (blockedUsers.has(msg.from)) { + return; + } + // Check if message is for current chat if (currentChat) { if (currentChat.isGroup && msg.groupId === currentChat.id) { @@ -141,6 +238,14 @@ async function loadContacts() { const res = await fetch('/api/users'); const users = await res.json(); contacts = users.filter(u => u.id !== currentUser.id); + + // Load online status for all users + contacts.forEach(contact => { + // Simulate some users being online (you can update this with real backend data) + if (Math.random() > 0.5) { + onlineUsers.add(contact.id); + } + }); } catch (err) { console.error('Failed to load contacts:', err); } @@ -187,11 +292,16 @@ function renderContacts() { function createContactItem(data) { const div = document.createElement('div'); div.className = 'contact-item'; + if (data.isGroup) div.classList.add('group-item'); + div.dataset.contactId = data.id; div.onclick = () => openChat(data); const avatar = document.createElement('div'); avatar.className = 'avatar'; - avatar.textContent = data.isGroup ? 'G' : data.name.charAt(0).toUpperCase(); + if (!data.isGroup) { + avatar.classList.add(onlineUsers.has(data.id) ? 'avatar-online' : 'avatar-offline'); + } + avatar.textContent = data.isGroup ? 'πŸ‘₯' : data.name.charAt(0).toUpperCase(); const info = document.createElement('div'); info.className = 'contact-info'; @@ -200,15 +310,45 @@ function createContactItem(data) { name.className = 'contact-name'; name.textContent = data.name; - const preview = document.createElement('div'); - preview.className = 'contact-preview'; - preview.textContent = data.preview; + const statusOrPreview = document.createElement('div'); + if (data.isGroup) { + statusOrPreview.className = 'contact-preview'; + statusOrPreview.textContent = data.preview || 'Group'; + } else { + statusOrPreview.className = 'contact-status'; + const isOnline = onlineUsers.has(data.id); + statusOrPreview.innerHTML = ` + + ${isOnline ? 'online' : data.lastSeen || 'offline'} + `; + } info.appendChild(name); - info.appendChild(preview); + info.appendChild(statusOrPreview); + + // Add meta info (time and unread count) + const meta = document.createElement('div'); + meta.className = 'contact-meta'; + + if (data.lastMessageTime) { + const time = document.createElement('div'); + time.className = 'contact-time'; + time.textContent = formatShortTime(data.lastMessageTime); + meta.appendChild(time); + } + + if (data.unreadCount && data.unreadCount > 0) { + const badge = document.createElement('div'); + badge.className = 'unread-badge'; + badge.textContent = data.unreadCount > 99 ? '99+' : data.unreadCount; + meta.appendChild(badge); + } div.appendChild(avatar); div.appendChild(info); + if (meta.children.length > 0) { + div.appendChild(meta); + } return div; } @@ -227,6 +367,44 @@ function updateContactPreview(msg) { // Implementation for updating last message preview } +function updateOnlineStatus() { + // Update all contact items + const items = document.querySelectorAll('.contact-item'); + items.forEach(item => { + const avatar = item.querySelector('.avatar'); + const statusEl = item.querySelector('.contact-status'); + const contactId = parseInt(item.dataset.contactId); + + if (avatar && !item.classList.contains('group-item')) { + avatar.classList.remove('avatar-online', 'avatar-offline'); + if (onlineUsers.has(contactId)) { + avatar.classList.add('avatar-online'); + } else { + avatar.classList.add('avatar-offline'); + } + } + + if (statusEl && !item.classList.contains('group-item')) { + const dot = statusEl.querySelector('.status-dot'); + const text = statusEl.querySelector('.status-text'); + if (dot && text) { + dot.className = 'status-dot ' + (onlineUsers.has(contactId) ? 'status-online' : 'status-offline'); + text.textContent = onlineUsers.has(contactId) ? 'online' : 'offline'; + } + } + }); + + // Update current chat header + if (currentChat && !currentChat.isGroup) { + const chatStatus = document.getElementById('chat-status'); + if (chatStatus) { + const isOnline = onlineUsers.has(currentChat.id); + chatStatus.textContent = isOnline ? '🟒 online' : 'offline'; + chatStatus.style.color = isOnline ? '#00d856' : '#8696a0'; + } + } +} + // Open chat async function openChat(contact) { currentChat = contact; @@ -239,14 +417,43 @@ async function openChat(contact) { // Build chat area const chatArea = document.getElementById('chat-area'); + const avatarClass = contact.isGroup ? 'avatar' : `avatar ${onlineUsers.has(contact.id) ? 'avatar-online' : 'avatar-offline'}`; + const isOnline = onlineUsers.has(contact.id); + const statusText = contact.isGroup ? 'Group' : (isOnline ? '🟒 online' : 'offline'); + const statusColor = contact.isGroup ? '#8696a0' : (isOnline ? '#00d856' : '#8696a0'); + const isBlocked = blockedUsers.has(contact.id); + + // Build action buttons + let actionButtons = ''; + if (contact.isGroup) { + actionButtons = ` + + + `; + } else { + actionButtons = ` + + + `; + } + chatArea.innerHTML = `
-
${contact.isGroup ? 'G' : contact.name.charAt(0).toUpperCase()}
+
${contact.isGroup ? 'πŸ‘₯' : contact.name.charAt(0).toUpperCase()}
${contact.name}
-
${contact.isGroup ? 'Group' : 'Online'}
+
${statusText}
+
+
+ ${actionButtons}
+
@@ -258,7 +465,8 @@ async function openChat(contact) {
- + +
{ + btn.classList.remove('active'); + }); + event.target.classList.add('active'); + + // Re-render messages with filter + filterMessages(); +} + +function filterMessages() { + const messages = document.querySelectorAll('.message'); + messages.forEach(msg => { + const bubble = msg.querySelector('.message-bubble'); + const hasImage = bubble.querySelector('.message-media[alt="image"]'); + const hasVideo = bubble.querySelector('.message-video'); + const hasAudio = bubble.querySelector('audio'); + + let shouldShow = false; + + if (messageFilter === 'all') { + shouldShow = true; + } else if (messageFilter === 'image' && hasImage) { + shouldShow = true; + } else if (messageFilter === 'video' && hasVideo) { + shouldShow = true; + } else if (messageFilter === 'audio' && hasAudio) { + shouldShow = true; + } + + msg.style.display = shouldShow ? 'flex' : 'none'; + }); +} + +// Voice recording functions +async function startRecording() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaRecorder = new MediaRecorder(stream); + audioChunks = []; + + mediaRecorder.ondataavailable = (event) => { + audioChunks.push(event.data); + }; + + mediaRecorder.onstop = async () => { + const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); + await uploadVoiceMessage(audioBlob); + stream.getTracks().forEach(track => track.stop()); + }; + + mediaRecorder.start(); + + // Visual feedback + const voiceBtn = document.getElementById('voice-btn'); + voiceBtn.style.background = '#ff4444'; + voiceBtn.textContent = '⏺️'; + } catch (err) { + alert('Microphone access denied'); + } +} + +function stopRecording() { + if (mediaRecorder && mediaRecorder.state === 'recording') { + mediaRecorder.stop(); + + // Reset button + const voiceBtn = document.getElementById('voice-btn'); + voiceBtn.style.background = ''; + voiceBtn.textContent = '🎀'; + } +} + +async function uploadVoiceMessage(audioBlob) { + const formData = new FormData(); + formData.append('file', audioBlob, 'voice-' + Date.now() + '.webm'); + + try { + const res = await fetch('/api/upload', { + method: 'POST', + body: formData + }); + + if (res.ok) { + const data = await res.json(); + + const msg = { + type: 'message', + mediaUrl: data.url, + mediaType: 'audio', + from: currentUser.id, + to: currentChat.isGroup ? 0 : currentChat.id, + groupId: currentChat.isGroup ? currentChat.id : 0 + }; + + await fetch('/api/send', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(msg) + }); + + displayMessage({ + ...msg, + timestamp: Date.now() / 1000 + }); + } else { + alert('Failed to send voice message'); + } + } catch (err) { + alert('Failed to send voice message'); + } +} + +// Group management functions +async function showGroupMembers() { + if (!currentChat || !currentChat.isGroup) return; + + try { + const res = await fetch(`/api/group/members?groupId=${currentChat.id}`); + const members = await res.json(); + + const modal = document.getElementById('group-members-modal'); + const membersList = document.getElementById('group-members-list'); + + // Get group creator info + const group = groups.find(g => g.id === currentChat.id); + const isCreator = group && group.creator === currentUser.id; + + membersList.innerHTML = ''; + members.forEach(member => { + const div = document.createElement('div'); + div.className = 'user-item'; + div.innerHTML = ` +
${member.fullName.charAt(0).toUpperCase()}
+
+
${member.fullName}
+
@${member.username}
+
+ ${isCreator && member.id !== currentUser.id ? + `` : + ''} + `; + membersList.appendChild(div); + }); + + modal.classList.add('active'); + } catch (err) { + alert('Failed to load group members'); + } +} + +async function leaveGroup() { + if (!currentChat || !currentChat.isGroup) return; + + if (!confirm('Are you sure you want to leave this group?')) return; + + try { + const res = await fetch('/api/group/leave', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + groupId: currentChat.id, + userId: currentUser.id + }) + }); + + if (res.ok) { + alert('You have left the group'); + await loadGroups(); + document.getElementById('chat-area').innerHTML = ` +
+
πŸ’¬
+

Select a contact to start chatting

+
+ `; + currentChat = null; + } else { + alert('Failed to leave group'); + } + } catch (err) { + alert('Failed to leave group'); + } +} + +async function removeMember(memberId) { + if (!currentChat || !currentChat.isGroup) return; + + if (!confirm('Remove this member from the group?')) return; + + try { + const res = await fetch('/api/group/remove', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ + groupId: currentChat.id, + userId: memberId, + removerId: currentUser.id + }) + }); + + if (res.ok) { + alert('Member removed from group'); + closeModal('group-members-modal'); + } else { + const error = await res.text(); + alert(error || 'Failed to remove member'); + } + } catch (err) { + alert('Failed to remove member'); + } +} + async function loadMessages() { try { const params = currentChat.isGroup @@ -311,12 +740,45 @@ function displayMessage(msg) { } if (msg.mediaUrl) { - const media = document.createElement('img'); - media.className = 'message-media'; - media.src = msg.mediaUrl; - media.alt = msg.mediaType; - media.onclick = () => window.open(msg.mediaUrl, '_blank'); - bubble.appendChild(media); + if (msg.mediaType === 'video') { + const videoContainer = document.createElement('div'); + videoContainer.className = 'video-container'; + + const video = document.createElement('video'); + video.className = 'message-video'; + video.src = msg.mediaUrl; + video.controls = false; + video.preload = 'metadata'; + video.onclick = () => openMediaModal(msg.mediaUrl, 'video'); + + const overlay = document.createElement('div'); + overlay.className = 'video-overlay'; + overlay.innerHTML = '▢️'; + + videoContainer.appendChild(video); + videoContainer.appendChild(overlay); + bubble.appendChild(videoContainer); + } else if (msg.mediaType === 'audio') { + const audioContainer = document.createElement('div'); + audioContainer.className = 'audio-container'; + + const audio = document.createElement('audio'); + audio.className = 'message-audio'; + audio.src = msg.mediaUrl; + audio.controls = true; + audio.preload = 'metadata'; + + audioContainer.appendChild(audio); + bubble.appendChild(audioContainer); + } else { + const media = document.createElement('img'); + media.className = 'message-media'; + media.src = msg.mediaUrl; + media.alt = msg.mediaType; + media.loading = 'lazy'; + media.onclick = () => openMediaModal(msg.mediaUrl, 'image'); + bubble.appendChild(media); + } } if (msg.content) { @@ -348,6 +810,59 @@ function formatTime(timestamp) { return date.toLocaleTimeString('en-US', {hour: '2-digit', minute: '2-digit'}); } +function formatShortTime(timestamp) { + const date = new Date(timestamp * 1000); + const now = new Date(); + const diff = now - date; + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + + if (days === 0) { + return date.toLocaleTimeString('en-US', {hour: '2-digit', minute: '2-digit'}); + } else if (days === 1) { + return 'yesterday'; + } else if (days < 7) { + return date.toLocaleDateString('en-US', {weekday: 'short'}); + } else { + return date.toLocaleDateString('en-US', {month: 'short', day: 'numeric'}); + } +} + +function openMediaModal(url, type) { + const modal = document.getElementById('media-modal'); + const content = document.getElementById('media-modal-content'); + + content.innerHTML = ''; + + if (type === 'video') { + const video = document.createElement('video'); + video.src = url; + video.controls = true; + video.autoplay = true; + content.appendChild(video); + } else { + const img = document.createElement('img'); + img.src = url; + content.appendChild(img); + } + + modal.classList.add('active'); + document.body.style.overflow = 'hidden'; +} + +function closeMediaModal() { + const modal = document.getElementById('media-modal'); + const content = document.getElementById('media-modal-content'); + + // Stop any playing video + const video = content.querySelector('video'); + if (video) { + video.pause(); + } + + modal.classList.remove('active'); + document.body.style.overflow = 'auto'; +} + function scrollToBottom() { const container = document.getElementById('messages-container'); if (container) { diff --git a/public/index.html b/public/index.html index 11e3606..c1aa2da 100644 --- a/public/index.html +++ b/public/index.html @@ -24,23 +24,25 @@ align-items: center; justify-content: center; height: 100vh; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: #0a0f14; } .auth-container { - background: #fff; - padding: 40px; - border-radius: 20px; - box-shadow: 0 10px 40px rgba(0,0,0,0.3); - width: 400px; + background: #1a1f26; + padding: 50px 40px; + border-radius: 16px; + border: 1px solid #2a3942; + width: 380px; max-width: 90%; - color: #333; + color: #e9edef; } .auth-container h2 { text-align: center; - margin-bottom: 30px; - color: #667eea; + margin-bottom: 35px; + color: #e9edef; + font-weight: 400; + font-size: 24px; } .form-group { @@ -55,16 +57,19 @@ .form-group input { width: 100%; - padding: 12px; - border: 2px solid #ddd; + padding: 14px; + border: 1px solid #2a3942; border-radius: 8px; font-size: 14px; - transition: border-color 0.3s; + background: #0f1419; + color: #e9edef; + transition: all 0.2s; } .form-group input:focus { outline: none; - border-color: #667eea; + border-color: #00a884; + background: #111b21; } .btn { @@ -79,12 +84,12 @@ } .btn-primary { - background: #667eea; + background: #00a884; color: white; } .btn-primary:hover { - background: #5568d3; + background: #008f6f; } .auth-toggle { @@ -120,10 +125,11 @@ .sidebar-header { background: #202c33; - padding: 15px; + padding: 18px; display: flex; justify-content: space-between; align-items: center; + border-bottom: 1px solid #2a3942; } .user-info { @@ -136,13 +142,38 @@ width: 40px; height: 40px; border-radius: 50%; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + background: linear-gradient(135deg, #00a884 0%, #008f6f 100%); display: flex; align-items: center; justify-content: center; color: white; - font-weight: bold; - font-size: 18px; + font-weight: 500; + font-size: 16px; + position: relative; + } + + .avatar-online::after { + content: ''; + position: absolute; + bottom: 0; + right: 0; + width: 12px; + height: 12px; + background: #00d856; + border: 2px solid #111b21; + border-radius: 50%; + } + + .avatar-offline::after { + content: ''; + position: absolute; + bottom: 0; + right: 0; + width: 12px; + height: 12px; + background: #8696a0; + border: 2px solid #111b21; + border-radius: 50%; } .sidebar-actions { @@ -155,14 +186,16 @@ border: none; color: #aebac1; cursor: pointer; - padding: 8px; - border-radius: 50%; - transition: background 0.3s; - font-size: 14px; + padding: 8px 10px; + border-radius: 8px; + transition: all 0.2s; + font-size: 18px; + line-height: 1; } .icon-btn:hover { background: #2a3942; + color: #e9edef; } .search-box { @@ -197,7 +230,7 @@ display: flex; align-items: center; gap: 15px; - transition: background 0.3s; + transition: all 0.2s; } .contact-item:hover { @@ -206,6 +239,7 @@ .contact-item.active { background: #2a3942; + border-left: 3px solid #00a884; } .contact-info { @@ -226,6 +260,60 @@ text-overflow: ellipsis; } + .contact-status { + display: inline-flex; + align-items: center; + gap: 5px; + font-size: 12px; + color: #8696a0; + } + + .status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + animation: pulse 2s infinite; + } + + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } + } + + .status-online { + background: #00d856; + } + + .status-offline { + background: #8696a0; + animation: none; + } + + .contact-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 4px; + min-width: 60px; + } + + .contact-time { + font-size: 11px; + color: #8696a0; + } + + .unread-badge { + background: #00a884; + color: white; + border-radius: 12px; + padding: 2px 8px; + font-size: 11px; + font-weight: 600; + min-width: 20px; + text-align: center; + } + .contact-meta { text-align: right; font-size: 12px; @@ -262,6 +350,66 @@ color: #8696a0; } + .chat-header-actions { + display: flex; + gap: 5px; + } + + .message-filter-bar { + background: #202c33; + padding: 10px 20px; + display: none; + gap: 10px; + border-bottom: 1px solid #2a3942; + flex-wrap: wrap; + } + + .filter-btn { + background: #2a3942; + border: none; + color: #e9edef; + padding: 8px 16px; + border-radius: 8px; + cursor: pointer; + font-size: 13px; + transition: all 0.2s; + } + + .filter-btn:hover { + background: #374853; + } + + .filter-btn.active { + background: #00a884; + color: white; + } + + .audio-container { + margin-bottom: 8px; + min-width: 250px; + } + + .message-audio { + width: 100%; + height: 40px; + border-radius: 20px; + } + + .btn-danger { + background: #ff4444; + color: white; + border: none; + padding: 6px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 12px; + transition: all 0.2s; + } + + .btn-danger:hover { + background: #cc0000; + } + .messages-container { flex: 1; overflow-y: auto; @@ -291,25 +439,25 @@ .message-bubble { max-width: 65%; - padding: 8px 12px; - border-radius: 8px; + padding: 10px 14px; + border-radius: 10px; position: relative; } .message.sent .message-bubble { - background: #005c4b; - border-bottom-right-radius: 2px; + background: #00a884; + border-bottom-right-radius: 4px; } .message.received .message-bubble { background: #202c33; - border-bottom-left-radius: 2px; + border-bottom-left-radius: 4px; } .message-sender { font-weight: 600; margin-bottom: 4px; - color: #667eea; + color: #00a884; font-size: 13px; } @@ -319,10 +467,45 @@ } .message-media { - max-width: 300px; - border-radius: 8px; - margin-bottom: 5px; + max-width: 320px; + border-radius: 12px; + margin-bottom: 8px; cursor: pointer; + transition: transform 0.2s; + display: block; + } + + .message-media:hover { + transform: scale(1.02); + } + + .message-video { + max-width: 320px; + border-radius: 12px; + margin-bottom: 8px; + cursor: pointer; + display: block; + } + + .video-container { + position: relative; + display: inline-block; + } + + .video-overlay { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 60px; + height: 60px; + background: rgba(0, 0, 0, 0.6); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + pointer-events: none; } .message-time { @@ -385,9 +568,9 @@ .input-field { flex: 1; - padding: 10px 15px; + padding: 12px 16px; border: none; - border-radius: 8px; + border-radius: 10px; background: #2a3942; color: #e9edef; font-size: 15px; @@ -395,21 +578,24 @@ .input-field:focus { outline: none; + background: #323c45; } .send-btn { - background: #005c4b; + background: #00a884; border: none; color: white; - padding: 10px 20px; - border-radius: 8px; + padding: 12px 24px; + border-radius: 10px; cursor: pointer; - font-weight: 600; - transition: background 0.3s; + font-weight: 500; + transition: all 0.2s; + font-size: 14px; } .send-btn:hover { - background: #004a3d; + background: #008f6f; + transform: scale(1.02); } .modal { @@ -429,6 +615,57 @@ display: flex; } + .media-modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.95); + align-items: center; + justify-content: center; + z-index: 2000; + animation: fadeIn 0.2s; + } + + .media-modal.active { + display: flex; + } + + .media-modal-content { + max-width: 90%; + max-height: 90%; + position: relative; + } + + .media-modal-content img, + .media-modal-content video { + max-width: 100%; + max-height: 90vh; + border-radius: 8px; + } + + .media-modal-close { + position: absolute; + top: 20px; + right: 20px; + background: rgba(255, 255, 255, 0.2); + border: none; + color: white; + font-size: 32px; + width: 50px; + height: 50px; + border-radius: 50%; + cursor: pointer; + transition: background 0.3s; + z-index: 2001; + } + + .media-modal-close:hover { + background: rgba(255, 255, 255, 0.3); + } + .modal-content { background: #202c33; padding: 30px; @@ -513,6 +750,26 @@ background: #374853; } + .message-media-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 4px; + margin-bottom: 8px; + } + + .message-media-grid img { + width: 100%; + height: 150px; + object-fit: cover; + border-radius: 8px; + cursor: pointer; + transition: transform 0.2s; + } + + .message-media-grid img:hover { + transform: scale(1.05); + } + @media (max-width: 768px) { .sidebar { width: 100%; @@ -584,9 +841,9 @@

Login to Chat Room

- + + + + + + +
+ +
+
+