mirror of
https://github.com/th30d4y/OpenLearnX.git
synced 2026-05-26 11:25:49 +00:00
5c416361b4
Signed-off-by: 5t4l1n <stalin78830@gmail.com>
733 lines
26 KiB
HTML
733 lines
26 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>BlockChat</title>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
|
||
body {
|
||
font-family: 'Segoe UI', sans-serif;
|
||
background: #0e1621;
|
||
height: 100vh;
|
||
overflow: hidden;
|
||
color: white;
|
||
}
|
||
|
||
.chat-container {
|
||
display: flex;
|
||
height: 100vh;
|
||
}
|
||
|
||
/* Sidebar */
|
||
.sidebar {
|
||
width: 400px;
|
||
background: #1e2129;
|
||
border-right: 1px solid #2e3a59;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.sidebar-header {
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
padding: 25px;
|
||
color: white;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.user-info h4 {
|
||
margin: 0;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.user-info small {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.contacts-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 10px 0;
|
||
}
|
||
|
||
.contact-item {
|
||
padding: 20px 25px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
transition: all 0.3s;
|
||
color: #8e959e;
|
||
border-left: 3px solid transparent;
|
||
}
|
||
|
||
.contact-item:hover {
|
||
background: #2e3a59;
|
||
color: white;
|
||
}
|
||
|
||
.contact-item.active {
|
||
background: #2e3a59;
|
||
border-left-color: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.contact-avatar {
|
||
width: 50px;
|
||
height: 50px;
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: white;
|
||
font-weight: bold;
|
||
margin-right: 15px;
|
||
}
|
||
|
||
.contact-info h6 {
|
||
margin: 0 0 5px 0;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.contact-info small {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* Chat Area */
|
||
.chat-area {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.chat-header {
|
||
background: #1e2129;
|
||
padding: 20px 25px;
|
||
border-bottom: 1px solid #2e3a59;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.chat-avatar {
|
||
width: 45px;
|
||
height: 45px;
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 15px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.messages-area {
|
||
flex: 1;
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
background: #0e1621;
|
||
}
|
||
|
||
.message {
|
||
margin-bottom: 15px;
|
||
display: flex;
|
||
animation: slideIn 0.3s ease;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from { opacity: 0; transform: translateY(10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
.message.sent {
|
||
justify-content: flex-end;
|
||
}
|
||
|
||
.message.received {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.message-bubble {
|
||
max-width: 70%;
|
||
padding: 12px 16px;
|
||
border-radius: 18px;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
.message.sent .message-bubble {
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
color: white;
|
||
border-bottom-right-radius: 4px;
|
||
}
|
||
|
||
.message.received .message-bubble {
|
||
background: #1e2129;
|
||
color: white;
|
||
border-bottom-left-radius: 4px;
|
||
}
|
||
|
||
.message-time {
|
||
font-size: 11px;
|
||
opacity: 0.7;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.input-area {
|
||
background: #1e2129;
|
||
padding: 20px 25px;
|
||
border-top: 1px solid #2e3a59;
|
||
display: flex;
|
||
gap: 15px;
|
||
align-items: center;
|
||
}
|
||
|
||
.message-input {
|
||
flex: 1;
|
||
background: #0e1621;
|
||
border: 2px solid #2e3a59;
|
||
border-radius: 25px;
|
||
padding: 12px 20px;
|
||
color: white;
|
||
outline: none;
|
||
font-size: 15px;
|
||
}
|
||
|
||
.message-input:focus {
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.message-input::placeholder {
|
||
color: #8e959e;
|
||
}
|
||
|
||
.send-btn {
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50%;
|
||
width: 50px;
|
||
height: 50px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.send-btn:hover:not(:disabled) {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.send-btn:disabled {
|
||
background: #666;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.welcome-screen {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 100%;
|
||
color: #8e959e;
|
||
}
|
||
|
||
.welcome-screen i {
|
||
font-size: 80px;
|
||
margin-bottom: 20px;
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
color: #8e959e;
|
||
padding: 50px 20px;
|
||
}
|
||
|
||
.empty-state i {
|
||
font-size: 60px;
|
||
margin-bottom: 15px;
|
||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="chat-container">
|
||
<!-- Sidebar -->
|
||
<div class="sidebar">
|
||
<div class="sidebar-header">
|
||
<div class="user-info">
|
||
<h4><i class="fas fa-comments me-2"></i>BlockChat</h4>
|
||
<small id="myAddress">Loading...</small>
|
||
</div>
|
||
<button class="btn btn-outline-light btn-sm" onclick="logout()">
|
||
<i class="fas fa-sign-out-alt"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="contacts-list" id="contactsList">
|
||
<div class="text-center p-4">
|
||
<div class="spinner-border text-primary"></div>
|
||
<p class="mt-2">Loading contacts...</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Chat Area -->
|
||
<div class="chat-area">
|
||
<div class="welcome-screen">
|
||
<i class="fas fa-comments"></i>
|
||
<h3>Welcome to BlockChat</h3>
|
||
<p>Select a contact to start messaging</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.7.4/web3.min.js"></script>
|
||
<script>
|
||
// Global variables
|
||
let myAccount = localStorage.getItem('myaddress');
|
||
let web3, contract;
|
||
let currentChat = null;
|
||
let allContacts = [];
|
||
let messageStore = new Map(); // Store messages for each chat
|
||
let blockchainCheckTimer;
|
||
|
||
const CONTRACT_ADDRESS = '0xd9145CCE52D386f254917e481eB44e9943F39138';
|
||
const CONTRACT_ABI = [
|
||
{
|
||
"anonymous": false,
|
||
"inputs": [
|
||
{"indexed": true, "internalType": "address", "name": "from", "type": "address"},
|
||
{"indexed": false, "internalType": "string", "name": "message", "type": "string"},
|
||
{"indexed": false, "internalType": "address", "name": "to", "type": "address"},
|
||
{"indexed": false, "internalType": "string", "name": "timestamp", "type": "string"}
|
||
],
|
||
"name": "message",
|
||
"type": "event"
|
||
},
|
||
{
|
||
"inputs": [
|
||
{"internalType": "address", "name": "_to", "type": "address"},
|
||
{"internalType": "string", "name": "_message", "type": "string"},
|
||
{"internalType": "string", "name": "time", "type": "string"}
|
||
],
|
||
"name": "sendMessage",
|
||
"outputs": [],
|
||
"stateMutability": "nonpayable",
|
||
"type": "function"
|
||
}
|
||
];
|
||
|
||
// Initialize app - NO REFRESH
|
||
window.addEventListener('load', async function() {
|
||
if (!myAccount) {
|
||
alert('Please login first!');
|
||
window.location.href = 'index.html';
|
||
return;
|
||
}
|
||
|
||
console.log('🚀 Starting BlockChat (NO REFRESH MODE)');
|
||
await initializeApp();
|
||
});
|
||
|
||
async function initializeApp() {
|
||
try {
|
||
// Connect to blockchain
|
||
web3 = new Web3('http://127.0.0.1:8545');
|
||
contract = new web3.eth.Contract(CONTRACT_ABI, CONTRACT_ADDRESS);
|
||
|
||
// Test connection
|
||
await web3.eth.net.isListening();
|
||
console.log('✅ Connected to blockchain');
|
||
|
||
// Show user address
|
||
document.getElementById('myAddress').textContent =
|
||
myAccount.substring(0, 6) + '...' + myAccount.substring(38);
|
||
|
||
// Load contacts
|
||
await loadContacts();
|
||
|
||
// Load all existing messages ONCE
|
||
await loadAllExistingMessages();
|
||
|
||
// Start checking for NEW messages only (every 6 seconds)
|
||
startNewMessageChecker();
|
||
|
||
} catch (error) {
|
||
console.error('❌ Initialization failed:', error);
|
||
alert('Failed to connect to blockchain!');
|
||
}
|
||
}
|
||
|
||
async function loadContacts() {
|
||
try {
|
||
const accounts = await web3.eth.getAccounts();
|
||
allContacts = accounts.filter(acc => acc.toLowerCase() !== myAccount.toLowerCase());
|
||
|
||
let html = '';
|
||
allContacts.forEach((contact, index) => {
|
||
html += `
|
||
<div class="contact-item" onclick="openChat('${contact}', ${index})">
|
||
<div class="contact-avatar">${index + 1}</div>
|
||
<div class="contact-info">
|
||
<h6>Contact ${index + 1}</h6>
|
||
<small>${contact.substring(0, 10)}...${contact.substring(32)}</small>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
document.getElementById('contactsList').innerHTML = html;
|
||
console.log('📞 Loaded', allContacts.length, 'contacts');
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error loading contacts:', error);
|
||
}
|
||
}
|
||
|
||
// Load all existing messages ONCE when app starts
|
||
async function loadAllExistingMessages() {
|
||
try {
|
||
console.log('📨 Loading all existing messages...');
|
||
|
||
const events = await contract.getPastEvents('message', {
|
||
fromBlock: 0,
|
||
toBlock: 'latest'
|
||
});
|
||
|
||
console.log(`Found ${events.length} existing messages on blockchain`);
|
||
|
||
// Store messages by chat address
|
||
events.forEach(event => {
|
||
const from = event.returnValues.from.toLowerCase();
|
||
const to = event.returnValues.to.toLowerCase();
|
||
const message = event.returnValues.message;
|
||
const timestamp = event.returnValues.timestamp;
|
||
const myAddr = myAccount.toLowerCase();
|
||
|
||
// Determine which chat this message belongs to
|
||
let chatWith = null;
|
||
let isMyMessage = false;
|
||
|
||
if (from === myAddr) {
|
||
chatWith = to;
|
||
isMyMessage = true;
|
||
} else if (to === myAddr) {
|
||
chatWith = from;
|
||
isMyMessage = false;
|
||
}
|
||
|
||
if (chatWith) {
|
||
if (!messageStore.has(chatWith)) {
|
||
messageStore.set(chatWith, []);
|
||
}
|
||
|
||
messageStore.get(chatWith).push({
|
||
message: message,
|
||
timestamp: timestamp,
|
||
isMyMessage: isMyMessage,
|
||
blockNumber: event.blockNumber,
|
||
id: `${from}-${to}-${event.blockNumber}-${event.transactionHash}`
|
||
});
|
||
}
|
||
});
|
||
|
||
// Sort all message arrays by block number
|
||
messageStore.forEach(messages => {
|
||
messages.sort((a, b) => a.blockNumber - b.blockNumber);
|
||
});
|
||
|
||
console.log('💾 Stored messages for', messageStore.size, 'conversations');
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error loading existing messages:', error);
|
||
}
|
||
}
|
||
|
||
function openChat(contactAddress, index) {
|
||
currentChat = contactAddress.toLowerCase();
|
||
console.log('💬 Opening chat with:', currentChat);
|
||
|
||
// Update contact selection
|
||
document.querySelectorAll('.contact-item').forEach(item => item.classList.remove('active'));
|
||
document.querySelectorAll('.contact-item')[index].classList.add('active');
|
||
|
||
// Show chat interface
|
||
showChatInterface(contactAddress, index + 1);
|
||
|
||
// Display messages for this chat (from memory - NO BLOCKCHAIN CALL)
|
||
displayStoredMessages(currentChat);
|
||
}
|
||
|
||
function showChatInterface(contactAddress, contactNumber) {
|
||
const chatArea = document.querySelector('.chat-area');
|
||
chatArea.innerHTML = `
|
||
<div class="chat-header">
|
||
<div class="chat-avatar">${contactNumber}</div>
|
||
<div>
|
||
<h6 style="margin: 0;">Contact ${contactNumber}</h6>
|
||
<small>${contactAddress.substring(0, 12)}...</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="messages-area" id="messagesArea">
|
||
</div>
|
||
|
||
<div class="input-area">
|
||
<input type="text" class="message-input" id="messageInput"
|
||
placeholder="Type a message..." onkeypress="handleEnter(event)">
|
||
<button class="send-btn" onclick="sendMessage()" id="sendBtn">
|
||
<i class="fas fa-paper-plane"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// Display messages from memory (INSTANT - NO BLOCKCHAIN CALL)
|
||
function displayStoredMessages(chatAddress) {
|
||
const messagesArea = document.getElementById('messagesArea');
|
||
if (!messagesArea) return;
|
||
|
||
const messages = messageStore.get(chatAddress) || [];
|
||
console.log(`💬 Displaying ${messages.length} stored messages for ${chatAddress}`);
|
||
|
||
if (messages.length === 0) {
|
||
messagesArea.innerHTML = `
|
||
<div class="empty-state">
|
||
<i class="fas fa-comment-dots"></i>
|
||
<h5>Start Conversation</h5>
|
||
<p>Send the first message!</p>
|
||
</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
let html = '';
|
||
messages.forEach(msg => {
|
||
const messageClass = msg.isMyMessage ? 'sent' : 'received';
|
||
const timeLabel = msg.isMyMessage ? 'sent' : 'received';
|
||
|
||
html += `
|
||
<div class="message ${messageClass}">
|
||
<div class="message-bubble">
|
||
<div>${escapeHtml(msg.message)}</div>
|
||
<div class="message-time">${timeLabel} ${msg.timestamp}</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
messagesArea.innerHTML = html;
|
||
messagesArea.scrollTop = messagesArea.scrollHeight;
|
||
}
|
||
|
||
// INSTANT message adding to UI
|
||
function addMessageToUI(chatAddress, message, timestamp, isMyMessage) {
|
||
console.log('➕ Adding message to UI instantly:', message);
|
||
|
||
// Add to memory store
|
||
if (!messageStore.has(chatAddress)) {
|
||
messageStore.set(chatAddress, []);
|
||
}
|
||
|
||
messageStore.get(chatAddress).push({
|
||
message: message,
|
||
timestamp: timestamp,
|
||
isMyMessage: isMyMessage,
|
||
blockNumber: 999999, // Temporary
|
||
id: `temp-${Date.now()}`
|
||
});
|
||
|
||
// Update UI if this chat is currently open
|
||
if (currentChat === chatAddress) {
|
||
displayStoredMessages(chatAddress);
|
||
}
|
||
}
|
||
|
||
async function sendMessage() {
|
||
if (!currentChat) {
|
||
alert('Please select a chat first!');
|
||
return;
|
||
}
|
||
|
||
const input = document.getElementById('messageInput');
|
||
const sendBtn = document.getElementById('sendBtn');
|
||
const messageText = input.value.trim();
|
||
|
||
if (!messageText) {
|
||
alert('Please enter a message!');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
console.log('📤 Sending message:', messageText);
|
||
|
||
// Create timestamp
|
||
const now = new Date();
|
||
const timestamp = now.toLocaleDateString("en-IN") + ' ' +
|
||
now.toLocaleTimeString("en-IN", {
|
||
hour12: true,
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
|
||
// INSTANTLY show message in UI
|
||
addMessageToUI(currentChat, messageText, timestamp, true);
|
||
|
||
// Clear input immediately
|
||
input.value = '';
|
||
|
||
// Disable send button temporarily
|
||
sendBtn.disabled = true;
|
||
sendBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||
|
||
// Send to blockchain in background
|
||
await contract.methods
|
||
.sendMessage(currentChat, messageText, timestamp)
|
||
.send({
|
||
from: myAccount,
|
||
gas: 300000
|
||
});
|
||
|
||
console.log('✅ Message sent to blockchain');
|
||
|
||
} catch (error) {
|
||
console.error('❌ Failed to send message:', error);
|
||
alert('Failed to send message: ' + error.message);
|
||
|
||
// Remove the temporary message from UI
|
||
const messages = messageStore.get(currentChat);
|
||
if (messages) {
|
||
const tempIndex = messages.findIndex(msg => msg.id.startsWith('temp-'));
|
||
if (tempIndex !== -1) {
|
||
messages.splice(tempIndex, 1);
|
||
displayStoredMessages(currentChat);
|
||
}
|
||
}
|
||
} finally {
|
||
// Re-enable send button
|
||
sendBtn.disabled = false;
|
||
sendBtn.innerHTML = '<i class="fas fa-paper-plane"></i>';
|
||
input.focus();
|
||
}
|
||
}
|
||
|
||
// Check for NEW messages only (every 6 seconds)
|
||
function startNewMessageChecker() {
|
||
let lastCheckedBlock = 0;
|
||
|
||
// Get current block number
|
||
web3.eth.getBlockNumber().then(blockNumber => {
|
||
lastCheckedBlock = blockNumber;
|
||
console.log('📦 Starting from block:', lastCheckedBlock);
|
||
});
|
||
|
||
blockchainCheckTimer = setInterval(async () => {
|
||
try {
|
||
const currentBlock = await web3.eth.getBlockNumber();
|
||
|
||
if (currentBlock > lastCheckedBlock) {
|
||
console.log('📦 Checking for new messages from block', lastCheckedBlock + 1, 'to', currentBlock);
|
||
|
||
// Only get events from new blocks
|
||
const newEvents = await contract.getPastEvents('message', {
|
||
fromBlock: lastCheckedBlock + 1,
|
||
toBlock: currentBlock
|
||
});
|
||
|
||
if (newEvents.length > 0) {
|
||
console.log('📨 Found', newEvents.length, 'new messages');
|
||
|
||
// Process new messages
|
||
newEvents.forEach(event => {
|
||
const from = event.returnValues.from.toLowerCase();
|
||
const to = event.returnValues.to.toLowerCase();
|
||
const message = event.returnValues.message;
|
||
const timestamp = event.returnValues.timestamp;
|
||
const myAddr = myAccount.toLowerCase();
|
||
|
||
// Only process if it involves current user
|
||
if (from === myAddr || to === myAddr) {
|
||
let chatWith = from === myAddr ? to : from;
|
||
let isMyMessage = from === myAddr;
|
||
|
||
// Add to message store
|
||
if (!messageStore.has(chatWith)) {
|
||
messageStore.set(chatWith, []);
|
||
}
|
||
|
||
// Remove any temporary messages with same content
|
||
const messages = messageStore.get(chatWith);
|
||
const tempIndex = messages.findIndex(msg =>
|
||
msg.id.startsWith('temp-') && msg.message === message
|
||
);
|
||
if (tempIndex !== -1) {
|
||
messages.splice(tempIndex, 1);
|
||
}
|
||
|
||
// Add the real message
|
||
messages.push({
|
||
message: message,
|
||
timestamp: timestamp,
|
||
isMyMessage: isMyMessage,
|
||
blockNumber: event.blockNumber,
|
||
id: `${from}-${to}-${event.blockNumber}-${event.transactionHash}`
|
||
});
|
||
|
||
// Sort by block number
|
||
messages.sort((a, b) => a.blockNumber - b.blockNumber);
|
||
|
||
// Update UI if this chat is currently open
|
||
if (currentChat === chatWith) {
|
||
console.log('🔄 Updating current chat display');
|
||
displayStoredMessages(chatWith);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
lastCheckedBlock = currentBlock;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error checking for new messages:', error);
|
||
}
|
||
}, 6000); // Check every 6 seconds
|
||
}
|
||
|
||
function handleEnter(event) {
|
||
if (event.key === 'Enter') {
|
||
event.preventDefault();
|
||
sendMessage();
|
||
}
|
||
}
|
||
|
||
function escapeHtml(text) {
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
function logout() {
|
||
if (blockchainCheckTimer) clearInterval(blockchainCheckTimer);
|
||
localStorage.removeItem('myaddress');
|
||
window.location.href = 'index.html';
|
||
}
|
||
|
||
// Cleanup on page unload
|
||
window.addEventListener('beforeunload', function() {
|
||
if (blockchainCheckTimer) clearInterval(blockchainCheckTimer);
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|