Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Ntfy Messenger</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/build/qrcode.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/index.min.js"></script> | |
<style> | |
.message-bubble { | |
max-width: 70%; | |
word-wrap: break-word; | |
} | |
.file-preview { | |
transition: all 0.3s ease; | |
} | |
.file-preview:hover { | |
transform: scale(1.05); | |
} | |
.gradient-bg { | |
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); | |
} | |
.typing-indicator span { | |
display: inline-block; | |
width: 8px; | |
height: 8px; | |
border-radius: 50%; | |
background-color: #8b5cf6; | |
margin: 0 2px; | |
animation: bounce 1.4s infinite ease-in-out; | |
} | |
.typing-indicator span:nth-child(2) { | |
animation-delay: 0.2s; | |
} | |
.typing-indicator span:nth-child(3) { | |
animation-delay: 0.4s; | |
} | |
@keyframes bounce { | |
0%, 80%, 100% { transform: translateY(0); } | |
40% { transform: translateY(-10px); } | |
} | |
.emoji-picker { | |
position: absolute; | |
bottom: 60px; | |
right: 20px; | |
z-index: 10; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-100 font-sans"> | |
<div class="flex h-screen"> | |
<!-- Sidebar --> | |
<div class="w-80 bg-white border-r border-gray-200 flex flex-col"> | |
<div class="p-4 gradient-bg text-white"> | |
<h1 class="text-xl font-bold">SharePulse</h1> | |
<p class="text-sm opacity-80">Secure peer-to-peer messaging</p> | |
</div> | |
<div class="p-4 border-b border-gray-200"> | |
<div class="flex items-center space-x-2"> | |
<div class="w-10 h-10 rounded-full bg-indigo-100 flex items-center justify-center"> | |
<span class="text-indigo-600 font-bold" id="userInitial">U</span> | |
</div> | |
<div> | |
<p class="font-medium" id="username">User</p> | |
<p class="text-xs text-gray-500">Online</p> | |
</div> | |
</div> | |
</div> | |
<div class="p-4 border-b border-gray-200"> | |
<h2 class="text-sm font-semibold text-gray-500 mb-2">CHANNEL CODE</h2> | |
<div class="flex items-center justify-between bg-gray-50 p-2 rounded"> | |
<code class="text-sm font-mono" id="channelCode">Not connected</code> | |
<button id="copyCodeBtn" class="text-indigo-600 hover:text-indigo-800"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> | |
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" /> | |
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" /> | |
</svg> | |
</button> | |
</div> | |
<div class="mt-3 flex space-x-2"> | |
<button id="showQRBtn" class="flex-1 bg-indigo-100 text-indigo-700 py-2 px-3 rounded text-sm font-medium hover:bg-indigo-200 transition"> | |
Show QR | |
</button> | |
<button id="shareBtn" class="flex-1 bg-indigo-100 text-indigo-700 py-2 px-3 rounded text-sm font-medium hover:bg-indigo-200 transition"> | |
Share | |
</button> | |
</div> | |
</div> | |
<div class="p-4 border-b border-gray-200"> | |
<h2 class="text-sm font-semibold text-gray-500 mb-2">CONNECT TO CHANNEL</h2> | |
<div class="flex"> | |
<input type="text" id="joinCodeInput" placeholder="Enter code" class="flex-1 border border-gray-300 rounded-l px-3 py-2 text-sm focus:outline-none focus:ring-1 focus:ring-indigo-500"> | |
<button id="joinBtn" class="bg-indigo-600 text-white px-3 py-2 rounded-r text-sm font-medium hover:bg-indigo-700 transition"> | |
Join | |
</button> | |
</div> | |
</div> | |
<div class="p-4 border-b border-gray-200"> | |
<h2 class="text-sm font-semibold text-gray-500 mb-2">FILES</h2> | |
<div class="space-y-2" id="fileList"> | |
<p class="text-sm text-gray-500">No files shared yet</p> | |
</div> | |
</div> | |
<div class="p-4 mt-auto"> | |
<button id="newChannelBtn" class="w-full bg-indigo-600 text-white py-2 px-3 rounded text-sm font-medium hover:bg-indigo-700 transition"> | |
Create New Channel | |
</button> | |
</div> | |
</div> | |
<!-- Main Chat Area --> | |
<div class="flex-1 flex flex-col"> | |
<!-- Chat Header --> | |
<div class="bg-white border-b border-gray-200 p-4 flex items-center justify-between"> | |
<div class="flex items-center space-x-3"> | |
<div class="w-10 h-10 rounded-full bg-purple-100 flex items-center justify-center"> | |
<span class="text-purple-600 font-bold">F</span> | |
</div> | |
<div> | |
<p class="font-medium">Friend</p> | |
<div class="flex items-center"> | |
<div class="typing-indicator hidden" id="typingIndicator"> | |
<span></span> | |
<span></span> | |
<span></span> | |
</div> | |
<p class="text-xs text-gray-500" id="statusText">Offline</p> | |
</div> | |
</div> | |
</div> | |
<div class="flex items-center space-x-2"> | |
<button class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> | |
<path d="M2 5a2 2 0 012-2h7a2 2 0 012 2v4a2 2 0 01-2 2H9l-3 3v-3H4a2 2 0 01-2-2V5z" /> | |
<path d="M15 7v2a4 4 0 01-4 4H9.828l-1.766 1.767c.28.149.599.233.938.233h2l3 3v-3h2a2 2 0 002-2V9a2 2 0 00-2-2h-1z" /> | |
</svg> | |
</button> | |
<button class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> | |
<path fill-rule="evenodd" d="M11.49 3.17c.38-1.56 2.6-1.56 2.98 0 .36 1.56 1.52 2.82 3.08 3.08 1.56.36 1.56 2.6 0 2.98-1.56.36-2.82 1.52-3.08 3.08-.38 1.56-2.6 1.56-2.98 0-.36-1.56-1.52-2.82-3.08-3.08-1.56-.36-1.56-2.6 0-2.98 1.56-.36 2.82-1.52 3.08-3.08zM7 9a1 1 0 011-1h1a1 1 0 110 2H8a1 1 0 01-1-1zm-2 0a3 3 0 013-3h1a3 3 0 013 3v1a3 3 0 01-3 3H8a3 3 0 01-3-3V9zm6.5 4.5a1.5 1.5 0 100-3 1.5 1.5 0 000 3z" clip-rule="evenodd" /> | |
</svg> | |
</button> | |
</div> | |
</div> | |
<!-- Messages --> | |
<div class="flex-1 overflow-y-auto p-4 bg-gray-50" id="messagesContainer"> | |
<div class="text-center py-10 text-gray-500" id="emptyState"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /> | |
</svg> | |
<p class="mt-2">No messages yet</p> | |
<p class="text-sm">Join or create a channel to start messaging</p> | |
</div> | |
</div> | |
<!-- Message Input --> | |
<div class="bg-white border-t border-gray-200 p-4"> | |
<div class="relative"> | |
<div id="filePreview" class="mb-2 flex space-x-2 overflow-x-auto"></div> | |
<div class="flex items-end"> | |
<button id="emojiBtn" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100 mr-1"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.828 14.828a4 4 0 01-5.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> | |
</svg> | |
</button> | |
<button id="fileBtn" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100 mr-1"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" /> | |
</svg> | |
</button> | |
<input type="file" id="fileInput" class="hidden" multiple> | |
<div class="flex-1 relative"> | |
<textarea id="messageInput" rows="1" placeholder="Type a message..." class="w-full border border-gray-300 rounded-full px-4 py-2 pr-10 focus:outline-none focus:ring-1 focus:ring-indigo-500 resize-none" style="min-height: 44px;"></textarea> | |
<button id="sendBtn" class="absolute right-2 bottom-2 p-1 text-indigo-600 hover:text-indigo-800 rounded-full"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" /> | |
</svg> | |
</button> | |
</div> | |
</div> | |
<emoji-picker class="emoji-picker"></emoji-picker> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- QR Code Modal --> | |
<div id="qrModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> | |
<div class="bg-white rounded-lg p-6 w-80"> | |
<div class="flex justify-between items-center mb-4"> | |
<h3 class="text-lg font-medium">Share Channel</h3> | |
<button id="closeQRModal" class="text-gray-500 hover:text-gray-700"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | |
</svg> | |
</button> | |
</div> | |
<div class="flex flex-col items-center"> | |
<div id="qrCode" class="p-4 bg-white rounded border border-gray-200 mb-4"></div> | |
<div class="w-full mb-4"> | |
<h4 class="text-sm font-medium text-gray-700 mb-2">Or share these words:</h4> | |
<div class="bg-gray-50 p-3 rounded text-center"> | |
<p class="font-mono text-sm" id="seedWords">apple banana cherry dolphin elephant</p> | |
</div> | |
</div> | |
<button id="copySeedBtn" class="w-full bg-indigo-600 text-white py-2 px-3 rounded text-sm font-medium hover:bg-indigo-700 transition"> | |
Copy Words | |
</button> | |
</div> | |
</div> | |
</div> | |
<script> | |
// App state | |
const state = { | |
channelCode: null, | |
messages: [], | |
files: [], | |
isTyping: false, | |
username: 'User' + Math.floor(Math.random() * 1000), | |
friendName: 'Friend' + Math.floor(Math.random() * 1000), | |
eventSource: null | |
}; | |
// DOM elements | |
const elements = { | |
messageInput: document.getElementById('messageInput'), | |
sendBtn: document.getElementById('sendBtn'), | |
messagesContainer: document.getElementById('messagesContainer'), | |
emptyState: document.getElementById('emptyState'), | |
channelCode: document.getElementById('channelCode'), | |
copyCodeBtn: document.getElementById('copyCodeBtn'), | |
showQRBtn: document.getElementById('showQRBtn'), | |
shareBtn: document.getElementById('shareBtn'), | |
joinCodeInput: document.getElementById('joinCodeInput'), | |
joinBtn: document.getElementById('joinBtn'), | |
newChannelBtn: document.getElementById('newChannelBtn'), | |
qrModal: document.getElementById('qrModal'), | |
closeQRModal: document.getElementById('closeQRModal'), | |
qrCode: document.getElementById('qrCode'), | |
seedWords: document.getElementById('seedWords'), | |
copySeedBtn: document.getElementById('copySeedBtn'), | |
typingIndicator: document.getElementById('typingIndicator'), | |
statusText: document.getElementById('statusText'), | |
fileInput: document.getElementById('fileInput'), | |
fileBtn: document.getElementById('fileBtn'), | |
filePreview: document.getElementById('filePreview'), | |
fileList: document.getElementById('fileList'), | |
emojiBtn: document.getElementById('emojiBtn'), | |
userInitial: document.getElementById('userInitial'), | |
username: document.getElementById('username') | |
}; | |
// Initialize | |
function init() { | |
// Set username | |
elements.username.textContent = state.username; | |
elements.userInitial.textContent = state.username.charAt(0).toUpperCase(); | |
// Event listeners | |
elements.sendBtn.addEventListener('click', sendMessage); | |
elements.messageInput.addEventListener('keypress', (e) => { | |
if (e.key === 'Enter' && !e.shiftKey) { | |
e.preventDefault(); | |
sendMessage(); | |
} | |
}); | |
elements.messageInput.addEventListener('input', () => { | |
if (elements.messageInput.value.trim() && !state.isTyping) { | |
state.isTyping = true; | |
simulateTyping(); | |
} else if (!elements.messageInput.value.trim() && state.isTyping) { | |
state.isTyping = false; | |
} | |
}); | |
elements.copyCodeBtn.addEventListener('click', copyChannelCode); | |
elements.showQRBtn.addEventListener('click', showQRModal); | |
elements.shareBtn.addEventListener('click', shareChannel); | |
elements.joinBtn.addEventListener('click', joinChannel); | |
elements.newChannelBtn.addEventListener('click', createNewChannel); | |
elements.closeQRModal.addEventListener('click', () => elements.qrModal.classList.add('hidden')); | |
elements.copySeedBtn.addEventListener('click', copySeedWords); | |
elements.fileBtn.addEventListener('click', () => elements.fileInput.click()); | |
elements.fileInput.addEventListener('change', handleFileSelect); | |
// Emoji picker | |
const picker = document.querySelector('emoji-picker'); | |
picker.addEventListener('emoji-click', (event) => { | |
elements.messageInput.value += event.detail.unicode; | |
elements.messageInput.focus(); | |
}); | |
elements.emojiBtn.addEventListener('click', () => { | |
picker.classList.toggle('hidden'); | |
}); | |
// Generate seed words | |
generateSeedWords(); | |
// Create initial channel | |
createNewChannel(); | |
} | |
// Generate random seed words | |
function generateSeedWords() { | |
const words = [ | |
'apple', 'banana', 'cherry', 'dolphin', 'elephant', | |
'flamingo', 'giraffe', 'honey', 'igloo', 'jungle', | |
'koala', 'lemon', 'mango', 'narwhal', 'octopus', | |
'penguin', 'quokka', 'raccoon', 'sunflower', 'tiger', | |
'unicorn', 'violet', 'watermelon', 'xylophone', 'yellow', 'zebra' | |
]; | |
const selected = []; | |
for (let i = 0; i < 5; i++) { | |
selected.push(words[Math.floor(Math.random() * words.length)]); | |
} | |
state.seedWords = selected.join(' '); | |
elements.seedWords.textContent = state.seedWords; | |
} | |
// Create new channel | |
function createNewChannel() { | |
// Generate random channel code | |
state.channelCode = 'ntfy-' + Math.random().toString(36).substring(2, 10); | |
elements.channelCode.textContent = state.channelCode; | |
// Clear messages | |
state.messages = []; | |
renderMessages(); | |
// Clear files | |
state.files = []; | |
renderFiles(); | |
// Update status | |
elements.statusText.textContent = 'Waiting for connection...'; | |
// Close any existing connection | |
if (state.eventSource) { | |
state.eventSource.close(); | |
} | |
// Connect to ntfy | |
connectToNtfy(); | |
// Generate new seed words | |
generateSeedWords(); | |
} | |
// Connect to ntfy | |
function connectToNtfy() { | |
const url = `https://ntfy.sh/${state.channelCode}/json`; | |
state.eventSource = new EventSource(url); | |
state.eventSource.onmessage = (e) => { | |
const data = JSON.parse(e.data); | |
if (data.event === 'message') { | |
// Check if message is from friend (not from ourselves) | |
if (data.message !== `${state.username}: ${elements.messageInput.value.trim()}`) { | |
const message = { | |
text: data.message, | |
sender: 'friend', | |
timestamp: new Date(data.time * 1000) | |
}; | |
state.messages.push(message); | |
renderMessages(); | |
// Check if message contains a file | |
if (data.message.includes('FILE:')) { | |
const fileName = data.message.split('FILE:')[1].trim(); | |
if (!state.files.includes(fileName)) { | |
state.files.push(fileName); | |
renderFiles(); | |
} | |
} | |
} | |
} | |
// Update status | |
elements.statusText.textContent = 'Online'; | |
elements.typingIndicator.classList.add('hidden'); | |
}; | |
state.eventSource.onerror = () => { | |
elements.statusText.textContent = 'Connection error'; | |
}; | |
} | |
// Join existing channel | |
function joinChannel() { | |
const code = elements.joinCodeInput.value.trim(); | |
if (!code) return; | |
state.channelCode = code; | |
elements.channelCode.textContent = state.channelCode; | |
elements.joinCodeInput.value = ''; | |
// Clear messages | |
state.messages = []; | |
renderMessages(); | |
// Clear files | |
state.files = []; | |
renderFiles(); | |
// Update status | |
elements.statusText.textContent = 'Connecting...'; | |
// Close any existing connection | |
if (state.eventSource) { | |
state.eventSource.close(); | |
} | |
// Connect to ntfy | |
connectToNtfy(); | |
} | |
// Send message | |
function sendMessage() { | |
const text = elements.messageInput.value.trim(); | |
const files = Array.from(elements.fileInput.files); | |
if (!text && files.length === 0) return; | |
// Clear files preview | |
elements.filePreview.innerHTML = ''; | |
elements.fileInput.value = ''; | |
// Send text message if exists | |
if (text) { | |
const message = { | |
text: text, | |
sender: 'me', | |
timestamp: new Date() | |
}; | |
state.messages.push(message); | |
renderMessages(); | |
// Send to ntfy | |
sendToNtfy(`${state.username}: ${text}`); | |
elements.messageInput.value = ''; | |
} | |
// Send files if exists | |
files.forEach(file => { | |
const fileName = `FILE: ${file.name}`; | |
const message = { | |
text: fileName, | |
sender: 'me', | |
timestamp: new Date(), | |
isFile: true, | |
fileName: file.name | |
}; | |
state.messages.push(message); | |
renderMessages(); | |
// Send to ntfy | |
sendToNtfy(`${state.username}: ${fileName}`); | |
// Add to files list | |
if (!state.files.includes(fileName)) { | |
state.files.push(fileName); | |
renderFiles(); | |
} | |
}); | |
// Reset typing state | |
state.isTyping = false; | |
} | |
// Send data to ntfy | |
function sendToNtfy(message) { | |
fetch(`https://ntfy.sh/${state.channelCode}`, { | |
method: 'POST', | |
body: message | |
}); | |
} | |
// Render messages | |
function renderMessages() { | |
if (state.messages.length === 0) { | |
elements.emptyState.classList.remove('hidden'); | |
elements.messagesContainer.innerHTML = ''; | |
return; | |
} | |
elements.emptyState.classList.add('hidden'); | |
let html = ''; | |
state.messages.forEach((msg, index) => { | |
const time = msg.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
if (msg.sender === 'me') { | |
html += ` | |
<div class="flex justify-end mb-4" data-index="${index}"> | |
<div class="message-bubble bg-indigo-100 text-gray-800 rounded-l-xl rounded-tr-xl px-4 py-2"> | |
${msg.isFile ? | |
`<div class="mb-1"> | |
<div class="flex items-center text-indigo-600"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> | |
</svg> | |
<span class="text-xs">${msg.fileName}</span> | |
</div> | |
</div>` : | |
`<p>${msg.text.replace(`${state.username}: `, '')}</p>` | |
} | |
<p class="text-xs text-gray-500 text-right mt-1">${time}</p> | |
</div> | |
</div> | |
`; | |
} else { | |
html += ` | |
<div class="flex justify-start mb-4" data-index="${index}"> | |
<div class="message-bubble bg-white text-gray-800 rounded-r-xl rounded-tl-xl px-4 py-2 border border-gray-200"> | |
${msg.text.includes('FILE:') ? | |
`<div class="mb-1"> | |
<div class="flex items-center text-purple-600"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> | |
</svg> | |
<span class="text-xs">${msg.text.split('FILE:')[1].trim()}</span> | |
</div> | |
</div>` : | |
`<p>${msg.text.replace(`${state.friendName}: `, '')}</p>` | |
} | |
<p class="text-xs text-gray-500 mt-1">${time}</p> | |
</div> | |
</div> | |
`; | |
} | |
}); | |
elements.messagesContainer.innerHTML = html; | |
elements.messagesContainer.scrollTop = elements.messagesContainer.scrollHeight; | |
} | |
// Render files list | |
function renderFiles() { | |
if (state.files.length === 0) { | |
elements.fileList.innerHTML = '<p class="text-sm text-gray-500">No files shared yet</p>'; | |
return; | |
} | |
let html = ''; | |
state.files.forEach(file => { | |
const fileName = file.includes('FILE:') ? file.split('FILE:')[1].trim() : file; | |
html += ` | |
<div class="flex items-center p-2 bg-gray-50 rounded hover:bg-gray-100"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> | |
</svg> | |
<span class="text-sm truncate flex-1">${fileName}</span> | |
</div> | |
`; | |
}); | |
elements.fileList.innerHTML = html; | |
} | |
// Handle file selection | |
function handleFileSelect() { | |
const files = Array.from(elements.fileInput.files); | |
if (files.length === 0) return; | |
elements.filePreview.innerHTML = ''; | |
files.forEach(file => { | |
const preview = document.createElement('div'); | |
preview.className = 'file-preview bg-white p-2 rounded border border-gray-200 flex-shrink-0 w-24'; | |
if (file.type.startsWith('image/')) { | |
const reader = new FileReader(); | |
reader.onload = (e) => { | |
preview.innerHTML = ` | |
<img src="${e.target.result}" class="w-full h-16 object-cover rounded mb-1"> | |
<p class="text-xs truncate">${file.name}</p> | |
`; | |
}; | |
reader.readAsDataURL(file); | |
} else { | |
preview.innerHTML = ` | |
<div class="w-full h-16 bg-gray-100 rounded flex items-center justify-center mb-1"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> | |
</svg> | |
</div> | |
<p class="text-xs truncate">${file.name}</p> | |
`; | |
} | |
elements.filePreview.appendChild(preview); | |
}); | |
} | |
// Copy channel code | |
function copyChannelCode() { | |
if (!state.channelCode) return; | |
navigator.clipboard.writeText(state.channelCode); | |
// Show feedback | |
const originalText = elements.copyCodeBtn.innerHTML; | |
elements.copyCodeBtn.innerHTML = ` | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | |
</svg> | |
`; | |
setTimeout(() => { | |
elements.copyCodeBtn.innerHTML = originalText; | |
}, 2000); | |
} | |
// Show QR modal | |
function showQRModal() { | |
if (!state.channelCode) return; | |
// Generate QR code | |
QRCode.toCanvas(state.channelCode, { | |
width: 200, | |
margin: 1, | |
color: { | |
dark: '#000000', | |
light: '#ffffff' | |
} | |
}, (err, canvas) => { | |
if (err) { | |
console.error(err); | |
return; | |
} | |
elements.qrCode.innerHTML = ''; | |
elements.qrCode.appendChild(canvas); | |
}); | |
elements.qrModal.classList.remove('hidden'); | |
} | |
// Share channel | |
function shareChannel() { | |
if (!state.channelCode) return; | |
if (navigator.share) { | |
navigator.share({ | |
title: 'Join my Ntfy Messenger channel', | |
text: `Use this code to join my channel: ${state.channelCode}`, | |
url: window.location.href | |
}).catch(err => { | |
console.log('Error sharing:', err); | |
}); | |
} else { | |
// Fallback for browsers that don't support Web Share API | |
copyChannelCode(); | |
alert('Channel code copied to clipboard. Share it with your friend!'); | |
} | |
} | |
// Copy seed words | |
function copySeedWords() { | |
navigator.clipboard.writeText(state.seedWords); | |
// Show feedback | |
const originalText = elements.copySeedBtn.textContent; | |
elements.copySeedBtn.textContent = 'Copied!'; | |
setTimeout(() => { | |
elements.copySeedBtn.textContent = originalText; | |
}, 2000); | |
} | |
// Simulate friend typing | |
function simulateTyping() { | |
if (!state.isTyping) return; | |
elements.typingIndicator.classList.remove('hidden'); | |
elements.statusText.textContent = ''; | |
setTimeout(() => { | |
if (state.isTyping) { | |
elements.typingIndicator.classList.add('hidden'); | |
elements.statusText.textContent = 'Online'; | |
state.isTyping = false; | |
} | |
}, 2000); | |
} | |
// Initialize the app | |
init(); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=vs4vijay/sharepulse" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
</html> |