474 lines
14 KiB
JavaScript
474 lines
14 KiB
JavaScript
// VibeChat Messenger App
|
|
|
|
const API = {
|
|
me: '/api/auth/me',
|
|
logout: '/api/auth/logout',
|
|
users: '/api/users',
|
|
chats: '/api/chats',
|
|
updateProfile: '/api/users/me'
|
|
};
|
|
|
|
let currentUser = null;
|
|
let currentChat = null;
|
|
let allChats = [];
|
|
let allUsers = [];
|
|
|
|
// Initialize app
|
|
window.addEventListener('DOMContentLoaded', async () => {
|
|
const token = localStorage.getItem('vibechat_token');
|
|
if (!token) {
|
|
window.location.href = 'index.html';
|
|
return;
|
|
}
|
|
|
|
await loadCurrentUser();
|
|
await loadChats();
|
|
await loadUsers();
|
|
|
|
// Inject emoji picker HTML
|
|
const emojis = ['😀','😂','😍','🥰','😎','🤔','👍','❤️','🔥','🎉','👋','🙏','💯','✨','🚀','💪','🎯','✅','📌','🔔'];
|
|
document.getElementById('emojiPicker').innerHTML = `
|
|
<div class="emoji-grid">
|
|
${emojis.map(e => `<span class="emoji-item" onclick="insertEmoji('${e}')">${e}</span>`).join('')}
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
// Load current user
|
|
async function loadCurrentUser() {
|
|
try {
|
|
const token = localStorage.getItem('vibechat_token');
|
|
const res = await fetch(API.me, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
const json = await res.json();
|
|
|
|
if (!json.ok) {
|
|
localStorage.removeItem('vibechat_token');
|
|
window.location.href = 'index.html';
|
|
return;
|
|
}
|
|
|
|
currentUser = json.user;
|
|
localStorage.setItem('vibechat_user', JSON.stringify(currentUser));
|
|
|
|
// Update UI
|
|
document.getElementById('currentUserName').textContent = currentUser.username;
|
|
document.getElementById('currentUserAvatar').src = currentUser.avatar;
|
|
} catch (e) {
|
|
console.error('Failed to load user:', e);
|
|
}
|
|
}
|
|
|
|
// Load chats
|
|
async function loadChats() {
|
|
try {
|
|
const token = localStorage.getItem('vibechat_token');
|
|
const res = await fetch(API.chats, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
const json = await res.json();
|
|
|
|
if (json.ok) {
|
|
allChats = json.chats;
|
|
renderChatList();
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load chats:', e);
|
|
}
|
|
}
|
|
|
|
// Render chat list
|
|
function renderChatList() {
|
|
const chatList = document.getElementById('chatList');
|
|
|
|
if (allChats.length === 0) {
|
|
chatList.innerHTML = `
|
|
<div style="padding:40px 20px;text-align:center;color:#86868b">
|
|
<p>Нет чатов</p>
|
|
<p style="font-size:13px;margin-top:8px">Нажмите + чтобы создать новый</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
chatList.innerHTML = allChats.map(chat => `
|
|
<div class="chat-item ${currentChat?.id === chat.id ? 'active' : ''}" onclick="selectChat(${chat.id})">
|
|
<img src="${chat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + chat.id}" alt="" class="chat-avatar">
|
|
<div class="chat-item-info">
|
|
<div class="chat-item-name">
|
|
${chat.name}
|
|
${chat.type !== 'private' ? `<span class="chat-type-badge">${chat.type === 'group' ? 'Группа' : 'Канал'}</span>` : ''}
|
|
</div>
|
|
<div class="chat-item-last">
|
|
${chat.lastMessage ? chat.lastMessage.text : 'Нет сообщений'}
|
|
</div>
|
|
</div>
|
|
${chat.lastMessage ? `<span class="chat-item-time">${formatTime(chat.lastMessage.createdAt)}</span>` : ''}
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Load users for new chat
|
|
async function loadUsers() {
|
|
try {
|
|
const token = localStorage.getItem('vibechat_token');
|
|
const res = await fetch(API.users, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
const json = await res.json();
|
|
|
|
if (json.ok) {
|
|
allUsers = json.users;
|
|
renderUserList();
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load users:', e);
|
|
}
|
|
}
|
|
|
|
// Render user list
|
|
function renderUserList() {
|
|
const userList = document.getElementById('userList');
|
|
|
|
if (allUsers.length === 0) {
|
|
userList.innerHTML = '<p style="padding:20px;text-align:center;color:#86868b">Нет пользователей</p>';
|
|
return;
|
|
}
|
|
|
|
userList.innerHTML = allUsers.map(user => `
|
|
<div class="user-item" onclick="createPrivateChat(${user.id})">
|
|
<img src="${user.avatar}" alt="" class="user-item-avatar">
|
|
<span class="user-item-name">${user.username}</span>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Select chat
|
|
async function selectChat(chatId) {
|
|
currentChat = allChats.find(c => c.id === chatId);
|
|
if (!currentChat) return;
|
|
|
|
// Update UI
|
|
document.getElementById('emptyChat').style.display = 'none';
|
|
document.getElementById('messagesList').style.display = 'flex';
|
|
document.getElementById('messageInputContainer').style.display = 'block';
|
|
|
|
// Update header
|
|
document.getElementById('chatHeaderName').textContent = currentChat.name;
|
|
document.getElementById('chatHeaderAvatar').src = currentChat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + currentChat.id;
|
|
document.getElementById('chatHeaderStatus').textContent = currentChat.type === 'channel' ? 'Канал' : (currentChat.type === 'group' ? `${currentChat.participants?.length || 0} участников` : 'Онлайн');
|
|
|
|
// Render chat list again to update active state
|
|
renderChatList();
|
|
|
|
// Load messages
|
|
await loadMessages(chatId);
|
|
}
|
|
|
|
// Load messages
|
|
async function loadMessages(chatId) {
|
|
try {
|
|
const token = localStorage.getItem('vibechat_token');
|
|
const res = await fetch(`${API.chats}/${chatId}/messages`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
const json = await res.json();
|
|
|
|
if (json.ok) {
|
|
renderMessages(json.messages);
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load messages:', e);
|
|
}
|
|
}
|
|
|
|
// Render messages
|
|
function renderMessages(messages) {
|
|
const messagesList = document.getElementById('messagesList');
|
|
|
|
if (messages.length === 0) {
|
|
messagesList.innerHTML = '<p style="text-align:center;color:#86868b;padding:40px">Сообщений пока нет</p>';
|
|
return;
|
|
}
|
|
|
|
messagesList.innerHTML = messages.map(msg => `
|
|
<div class="message ${msg.senderId === currentUser.id ? 'sent' : ''}">
|
|
<img src="${msg.senderAvatar}" alt="" class="message-avatar">
|
|
<div class="message-content">
|
|
${msg.senderId !== currentUser.id ? `<div class="message-sender">${msg.senderName}</div>` : ''}
|
|
<div class="message-text">${escapeHtml(msg.text)}</div>
|
|
<div class="message-time">${formatTime(msg.createdAt)}</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Scroll to bottom
|
|
const container = document.getElementById('messagesContainer');
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
|
|
// Send message
|
|
async function sendMessage() {
|
|
const input = document.getElementById('messageInput');
|
|
const text = input.value.trim();
|
|
|
|
if (!text || !currentChat) return;
|
|
|
|
try {
|
|
const token = localStorage.getItem('vibechat_token');
|
|
const res = await fetch(`${API.chats}/${currentChat.id}/messages`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({ text })
|
|
});
|
|
const json = await res.json();
|
|
|
|
if (json.ok) {
|
|
input.value = '';
|
|
await loadMessages(currentChat.id);
|
|
await loadChats(); // Update last message
|
|
} else {
|
|
alert('Ошибка отправки: ' + json.error);
|
|
}
|
|
} catch (e) {
|
|
alert('Ошибка: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// Handle enter key
|
|
function handleKeyPress(event) {
|
|
if (event.key === 'Enter') {
|
|
sendMessage();
|
|
}
|
|
}
|
|
|
|
// Emoji picker
|
|
function toggleEmojiPicker() {
|
|
const picker = document.getElementById('emojiPicker');
|
|
picker.classList.toggle('active');
|
|
}
|
|
|
|
function insertEmoji(emoji) {
|
|
const input = document.getElementById('messageInput');
|
|
input.value += emoji;
|
|
input.focus();
|
|
document.getElementById('emojiPicker').classList.remove('active');
|
|
}
|
|
|
|
// Close emoji picker on outside click
|
|
document.addEventListener('click', (e) => {
|
|
const picker = document.getElementById('emojiPicker');
|
|
const btn = document.querySelector('.btn-emoji');
|
|
if (!picker.contains(e.target) && !btn.contains(e.target)) {
|
|
picker.classList.remove('active');
|
|
}
|
|
});
|
|
|
|
// New chat modal
|
|
function showNewChatModal() {
|
|
document.getElementById('newChatModal').classList.add('active');
|
|
loadUsers();
|
|
}
|
|
|
|
function closeNewChatModal() {
|
|
document.getElementById('newChatModal').classList.remove('active');
|
|
}
|
|
|
|
function switchTab(tab) {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
|
|
document.getElementById('chatTab').style.display = tab === 'chat' ? 'block' : 'none';
|
|
document.getElementById('groupTab').style.display = tab === 'group' ? 'block' : 'none';
|
|
document.getElementById('channelTab').style.display = tab === 'channel' ? 'block' : 'none';
|
|
}
|
|
|
|
// Create private chat
|
|
async function createPrivateChat(userId) {
|
|
try {
|
|
const token = localStorage.getItem('vibechat_token');
|
|
const res = await fetch(API.chats, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({ type: 'private', participantId: userId })
|
|
});
|
|
const json = await res.json();
|
|
|
|
if (json.ok) {
|
|
closeNewChatModal();
|
|
await loadChats();
|
|
selectChat(json.chat.id);
|
|
}
|
|
} catch (e) {
|
|
alert('Ошибка: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// Create group
|
|
async function createGroup() {
|
|
const name = document.getElementById('groupName').value.trim();
|
|
if (!name) return alert('Введите название группы');
|
|
|
|
try {
|
|
const token = localStorage.getItem('vibechat_token');
|
|
const res = await fetch(API.chats, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({ type: 'group', name })
|
|
});
|
|
const json = await res.json();
|
|
|
|
if (json.ok) {
|
|
closeNewChatModal();
|
|
document.getElementById('groupName').value = '';
|
|
await loadChats();
|
|
selectChat(json.chat.id);
|
|
}
|
|
} catch (e) {
|
|
alert('Ошибка: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// Create channel
|
|
async function createChannel() {
|
|
const name = document.getElementById('channelName').value.trim();
|
|
if (!name) return alert('Введите название канала');
|
|
|
|
try {
|
|
const token = localStorage.getItem('vibechat_token');
|
|
const res = await fetch(API.chats, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({ type: 'channel', name })
|
|
});
|
|
const json = await res.json();
|
|
|
|
if (json.ok) {
|
|
closeNewChatModal();
|
|
document.getElementById('channelName').value = '';
|
|
await loadChats();
|
|
selectChat(json.chat.id);
|
|
}
|
|
} catch (e) {
|
|
alert('Ошибка: ' + e.message);
|
|
}
|
|
}
|
|
|
|
// Settings
|
|
function openSettings() {
|
|
const user = JSON.parse(localStorage.getItem('vibechat_user'));
|
|
document.getElementById('settingsAvatar').src = user.avatar;
|
|
document.getElementById('settingsUsername').value = user.username;
|
|
document.getElementById('settingsBio').value = user.bio || '';
|
|
document.getElementById('settingsModal').classList.add('active');
|
|
}
|
|
|
|
function closeSettings() {
|
|
document.getElementById('settingsModal').classList.remove('active');
|
|
}
|
|
|
|
async function updateProfile(event) {
|
|
event.preventDefault();
|
|
|
|
const username = document.getElementById('settingsUsername').value;
|
|
const bio = document.getElementById('settingsBio').value;
|
|
|
|
try {
|
|
const token = localStorage.getItem('vibechat_token');
|
|
const res = await fetch(API.updateProfile, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
body: JSON.stringify({ username, bio })
|
|
});
|
|
const json = await res.json();
|
|
|
|
if (json.ok) {
|
|
localStorage.setItem('vibechat_user', JSON.stringify(json.user));
|
|
currentUser = json.user;
|
|
document.getElementById('currentUserName').textContent = currentUser.username;
|
|
document.getElementById('currentUserAvatar').src = currentUser.avatar;
|
|
closeSettings();
|
|
alert('Профиль обновлён! ✅');
|
|
}
|
|
} catch (e) {
|
|
alert('Ошибка: ' + e.message);
|
|
}
|
|
}
|
|
|
|
function changeAvatar() {
|
|
const url = prompt('Введите URL аватара:', currentUser.avatar);
|
|
if (url) {
|
|
updateProfile({ preventDefault: () => {} });
|
|
}
|
|
}
|
|
|
|
// Logout
|
|
async function logout() {
|
|
if (!confirm('Вы уверены, что хотите выйти?')) return;
|
|
|
|
try {
|
|
const token = localStorage.getItem('vibechat_token');
|
|
await fetch(API.logout, {
|
|
method: 'POST',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
} catch (e) {}
|
|
|
|
localStorage.removeItem('vibechat_token');
|
|
localStorage.removeItem('vibechat_user');
|
|
window.location.href = 'index.html';
|
|
}
|
|
|
|
// Filter chats
|
|
function filterChats() {
|
|
const query = document.getElementById('searchInput').value.toLowerCase();
|
|
const filtered = allChats.filter(c => c.name.toLowerCase().includes(query));
|
|
|
|
const chatList = document.getElementById('chatList');
|
|
chatList.innerHTML = filtered.map(chat => `
|
|
<div class="chat-item ${currentChat?.id === chat.id ? 'active' : ''}" onclick="selectChat(${chat.id})">
|
|
<img src="${chat.avatar || 'https://api.dicebear.com/7.x/identicon/svg?seed=' + chat.id}" alt="" class="chat-avatar">
|
|
<div class="chat-item-info">
|
|
<div class="chat-item-name">${chat.name}</div>
|
|
<div class="chat-item-last">${chat.lastMessage ? chat.lastMessage.text : 'Нет сообщений'}</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Helper functions
|
|
function formatTime(isoString) {
|
|
const date = new Date(isoString);
|
|
const now = new Date();
|
|
const diff = now - date;
|
|
|
|
if (diff < 60000) return 'Только что';
|
|
if (diff < 3600000) return Math.floor(diff / 60000) + ' мин назад';
|
|
if (diff < 86400000) return Math.floor(diff / 3600000) + ' ч назад';
|
|
if (diff < 604800000) return Math.floor(diff / 86400000) + ' дн назад';
|
|
|
|
return date.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|