/**
* Wirecore Pear App - Simple Web Client
*
* This is a simplified browser interface that connects to the Pear app's HTTP API.
* All P2P networking is handled by the Pear app - the browser is just a dumb UI.
*/
// State
let currentUser = null
let profile = null
let timeline = []
let ws = null
// Initialize app
async function init() {
console.log('Initializing Wirecore web client...')
// Setup event listeners FIRST, so buttons work even if API calls fail
setupEventListeners()
// Load current user from API (don't let errors stop initialization)
try {
await loadCurrentUser()
} catch (err) {
console.error('Failed to load user during init:', err)
}
// Load timeline (don't let errors stop initialization)
try {
await loadTimeline()
} catch (err) {
console.error('Failed to load timeline during init:', err)
}
// Connect WebSocket for real-time updates
try {
connectWebSocket()
} catch (err) {
console.error('Failed to connect WebSocket:', err)
}
console.log('Wirecore initialized!')
}
// Load current user from Pear app
async function loadCurrentUser() {
try {
const res = await fetch('/api/whoami')
const data = await res.json()
currentUser = {
pubkey: data.pubkey,
feedKey: data.feedKey
}
// Load profile (username)
const profileRes = await fetch('/api/profile')
profile = await profileRes.json()
// Update navbar
const pubkeyDisplay = document.getElementById('pubkey-display')
if (pubkeyDisplay) {
const displayText = profile.username || data.pubkey.substring(0, 8) + '...'
pubkeyDisplay.textContent = displayText
pubkeyDisplay.title = profile.username ? `${profile.username} (${data.pubkey})` : data.pubkey
}
console.log('Current user:', profile.username || data.pubkey.substring(0, 16))
} catch (err) {
console.error('Failed to load user:', err)
showToast('Failed to load identity', 'error')
}
}
// Load timeline from API
async function loadTimeline() {
try {
console.log('[loadTimeline] Fetching /api/timeline...')
const res = await fetch('/api/timeline')
timeline = await res.json()
console.log('[loadTimeline] Received', timeline.length, 'messages')
renderTimeline()
console.log('[loadTimeline] Rendered timeline')
} catch (err) {
console.error('Failed to load timeline:', err)
showToast('Failed to load timeline', 'error')
}
}
// Render timeline
function renderTimeline() {
const container = document.getElementById('timeline')
if (!container) return
if (timeline.length === 0) {
container.innerHTML = '<div class="no-messages">No messages yet. Start posting!</div>'
return
}
container.innerHTML = timeline.map(msg => {
const time = new Date(msg.timestamp).toLocaleString()
const isOwn = msg.isOwn
// Parse markdown in text (handles inline images via )
const messageHtml = parseMarkdown(msg.text, msg.imageData)
return `
<div class="message ${isOwn ? 'own-message' : ''}">
<div class="message-header">
<div class="author">${escapeHtml(msg.author || 'Anonymous')}</div>
<div class="timestamp">${time}</div>
</div>
<div class="message-text">${messageHtml}</div>
</div>
`
}).join('')
}
// Connect WebSocket for real-time updates
function connectWebSocket() {
const wsUrl = `ws://${window.location.host}`
console.log('Connecting to WebSocket:', wsUrl)
ws = new WebSocket(wsUrl)
ws.onopen = () => {
console.log('✅ WebSocket connected')
}
ws.onmessage = (event) => {
console.log('[onmessage] FIRED! event:', event)
console.log('[onmessage] event.data type:', typeof event.data)
console.log('[onmessage] event.data:', event.data)
try {
const data = JSON.parse(event.data)
console.log('WebSocket message:', data)
if (data.type === 'new_post') {
// Reload timeline when new post arrives
console.log('New post detected, refreshing timeline...')
loadTimeline()
} else if (data.type === 'new_follow') {
console.log('New follow:', data.name)
showToast(`Now following ${data.name}`, 'success')
}
} catch (err) {
console.error('WebSocket message error:', err, err.stack)
}
}
ws.onclose = (event) => {
console.log('WebSocket disconnected:', event.code, event.reason)
// Reconnect after 5 seconds
setTimeout(connectWebSocket, 5000)
}
ws.onerror = (err) => {
console.error('WebSocket error:', err)
}
}
// Post a message
async function postMessage(text, imageData = null, imageMimeType = null) {
if (!text || !text.trim()) {
showToast('Message cannot be empty', 'error')
return
}
try {
// Post message with optional inline image (base64)
const res = await fetch('/api/post', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: text,
imageData: imageData, // base64 string
imageMimeType: imageMimeType
})
})
if (!res.ok) throw new Error('Failed to post')
showToast('Posted!', 'success')
// Reload timeline
setTimeout(() => loadTimeline(), 500)
} catch (err) {
console.error('Failed to post:', err)
showToast('Failed to post message', 'error')
}
}
// Follow a user
async function followUser(publicKey, name) {
try {
const res = await fetch('/api/follow', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ publicKey, name: name || publicKey.substring(0, 16) })
})
if (!res.ok) throw new Error('Failed to follow')
showToast(`Now following ${name || publicKey.substring(0, 16)}`, 'success')
} catch (err) {
console.error('Failed to follow:', err)
showToast(err.message || 'Failed to follow user', 'error')
}
}
// Show compose modal
function showComposeModal() {
const modal = document.createElement('div')
modal.className = 'modal-overlay'
modal.innerHTML = `
<div class="modal">
<h2>Compose Post</h2>
<textarea id="compose-text" placeholder="What's on your mind?" rows="5"></textarea>
<div id="image-preview" style="display: none; margin: 10px 0;">
<img id="preview-img" style="max-width: 100%; max-height: 200px; border-radius: 4px;" />
<button id="remove-image" style="margin-top: 5px;">Remove Image</button>
</div>
<div class="modal-actions">
<input type="file" id="image-input" accept="image/*" style="display: none;" />
<button id="attach-image">📎 Attach Image</button>
<div style="flex: 1;"></div>
<button id="compose-cancel">Cancel</button>
<button id="compose-submit" class="primary">Post</button>
</div>
</div>
`
document.body.appendChild(modal)
const textarea = modal.querySelector('#compose-text')
const imageInput = modal.querySelector('#image-input')
const imagePreview = modal.querySelector('#image-preview')
const previewImg = modal.querySelector('#preview-img')
let selectedImageData = null
let selectedImageType = null
textarea.focus()
modal.querySelector('#attach-image').onclick = () => {
imageInput.click()
}
imageInput.onchange = (e) => {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = (event) => {
const dataUrl = event.target.result
previewImg.src = dataUrl
imagePreview.style.display = 'block'
// Store base64 data (remove data:image/jpeg;base64, prefix)
selectedImageData = dataUrl.split(',')[1]
selectedImageType = file.type
}
reader.readAsDataURL(file)
}
modal.querySelector('#remove-image').onclick = () => {
selectedImageData = null
selectedImageType = null
imagePreview.style.display = 'none'
imageInput.value = ''
}
modal.querySelector('#compose-cancel').onclick = () => {
document.body.removeChild(modal)
}
modal.querySelector('#compose-submit').onclick = async () => {
const text = textarea.value.trim()
if (text) {
await postMessage(text, selectedImageData, selectedImageType)
document.body.removeChild(modal)
}
}
// Close on background click
modal.onclick = (e) => {
if (e.target === modal) {
document.body.removeChild(modal)
}
}
}
// Show follow modal
function showFollowModal() {
const modal = document.createElement('div')
modal.className = 'modal-overlay'
modal.innerHTML = `
<div class="modal">
<h2>Follow User</h2>
<input type="text" id="follow-pubkey" placeholder="Public key (hex)" />
<input type="text" id="follow-name" placeholder="Name (optional)" />
<div class="modal-actions">
<button id="follow-cancel">Cancel</button>
<button id="follow-submit" class="primary">Follow</button>
</div>
</div>
`
document.body.appendChild(modal)
const pubkeyInput = modal.querySelector('#follow-pubkey')
pubkeyInput.focus()
modal.querySelector('#follow-cancel').onclick = () => {
document.body.removeChild(modal)
}
modal.querySelector('#follow-submit').onclick = async () => {
const publicKey = pubkeyInput.value.trim()
const name = modal.querySelector('#follow-name').value.trim()
if (publicKey) {
await followUser(publicKey, name)
document.body.removeChild(modal)
}
}
// Close on background click
modal.onclick = (e) => {
if (e.target === modal) {
document.body.removeChild(modal)
}
}
}
// Show profile modal
function showProfileModal() {
if (!currentUser) return
const currentUsername = profile && profile.username ? escapeHtml(profile.username) : ''
const usernameDisplay = currentUsername || '<em style="color: #999;">Not set</em>'
const modal = document.createElement('div')
modal.className = 'modal-overlay'
modal.innerHTML = `
<div class="modal profile-modal">
<h2>👤 Your Profile</h2>
<div class="profile-section">
<div class="profile-header">
<strong>Username</strong>
<button id="username-edit" class="edit-btn" title="Edit username">
<span class="material-symbols-outlined">Edit</span>
</button>
</div>
<div id="username-display" class="username-display">${usernameDisplay}</div>
<div id="username-edit-form" class="username-edit-form" style="display: none;">
<input type="text" id="username-input" placeholder="Enter your username"
value="${currentUsername}"
maxlength="32"
autocomplete="off"
autofocus />
<div class="edit-actions">
<button id="username-save" class="primary">Save</button>
<button id="username-cancel">Cancel</button>
</div>
<small style="color: #666; margin-top: 8px; display: block;">
Max 32 characters. This is how others will see you.
</small>
</div>
</div>
<div class="profile-section">
<strong>Public Key</strong>
<code class="pubkey-full" title="Click to copy">${currentUser.pubkey}</code>
<small style="color: #666; margin-top: 4px; display: block;">
Share this with others so they can follow you
</small>
</div>
<div class="profile-section">
<strong>Feed Key</strong>
<code class="pubkey-full" title="Click to copy">${currentUser.feedKey}</code>
</div>
<div class="modal-actions">
<button id="profile-close" class="primary">Close</button>
</div>
</div>
`
document.body.appendChild(modal)
const usernameDisplayEl = modal.querySelector('#username-display')
const usernameEditForm = modal.querySelector('#username-edit-form')
const usernameInput = modal.querySelector('#username-input')
// Edit button - show edit form
modal.querySelector('#username-edit').onclick = () => {
usernameDisplayEl.style.display = 'none'
usernameEditForm.style.display = 'block'
usernameInput.focus()
usernameInput.select()
}
// Cancel button - hide edit form
modal.querySelector('#username-cancel').onclick = () => {
usernameDisplayEl.style.display = 'block'
usernameEditForm.style.display = 'none'
usernameInput.value = currentUsername
}
// Save username
modal.querySelector('#username-save').onclick = async () => {
const username = usernameInput.value.trim()
if (!username) {
showToast('Username cannot be empty', 'error')
usernameInput.focus()
return
}
// Disable button while saving
const saveBtn = modal.querySelector('#username-save')
const originalText = saveBtn.textContent
saveBtn.textContent = 'Saving...'
saveBtn.disabled = true
try {
const res = await fetch('/api/profile', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
})
if (!res.ok) throw new Error('Failed to save username')
showToast('Username saved!', 'success')
// Reload profile
await loadCurrentUser()
// Update display
usernameDisplayEl.innerHTML = escapeHtml(username)
usernameDisplayEl.style.display = 'block'
usernameEditForm.style.display = 'none'
} catch (err) {
console.error('Failed to save username:', err)
showToast('Failed to save username', 'error')
saveBtn.textContent = originalText
saveBtn.disabled = false
}
}
// Enter key to save
usernameInput.onkeydown = (e) => {
if (e.key === 'Enter') {
modal.querySelector('#username-save').click()
} else if (e.key === 'Escape') {
modal.querySelector('#username-cancel').click()
}
}
// Copy to clipboard on click
modal.querySelectorAll('.pubkey-full').forEach(el => {
el.onclick = () => {
navigator.clipboard.writeText(el.textContent)
showToast('Copied to clipboard!', 'success')
}
el.style.cursor = 'pointer'
})
modal.querySelector('#profile-close').onclick = () => {
document.body.removeChild(modal)
}
// Close on background click
modal.onclick = (e) => {
if (e.target === modal) {
document.body.removeChild(modal)
}
}
}
// Show toast notification
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container') || createToastContainer()
const toast = document.createElement('div')
toast.className = `toast toast-${type}`
toast.textContent = message
container.appendChild(toast)
// Fade in
setTimeout(() => toast.classList.add('show'), 10)
// Remove after 3 seconds
setTimeout(() => {
toast.classList.remove('show')
setTimeout(() => container.removeChild(toast), 300)
}, 3000)
}
function createToastContainer() {
const container = document.createElement('div')
container.id = 'toast-container'
document.body.appendChild(container)
return container
}
// Show search modal
function showSearchModal() {
const modal = document.createElement('div')
modal.className = 'modal-overlay'
modal.innerHTML = `
<div class="modal search-modal">
<h2>🔍 Search Users</h2>
<input type="text" id="search-input" placeholder="Search by name..." autofocus />
<div id="search-results" class="search-results">
<div class="search-placeholder">Start typing to search...</div>
</div>
<div class="modal-actions">
<button id="search-close">Close</button>
</div>
</div>
`
document.body.appendChild(modal)
const searchInput = modal.querySelector('#search-input')
const searchResults = modal.querySelector('#search-results')
const closeBtn = modal.querySelector('#search-close')
// Close modal
closeBtn.onclick = () => modal.remove()
modal.onclick = (e) => {
if (e.target === modal) modal.remove()
}
// Search on input with debounce
let searchTimeout
searchInput.oninput = () => {
clearTimeout(searchTimeout)
const query = searchInput.value.trim()
if (!query) {
searchResults.innerHTML = '<div class="search-placeholder">Start typing to search...</div>'
return
}
searchResults.innerHTML = '<div class="search-placeholder">Searching...</div>'
searchTimeout = setTimeout(async () => {
try {
// Fetch from relay via our backend proxy
const res = await fetch(`/api/directory?q=${encodeURIComponent(query)}`)
if (!res.ok) throw new Error('Search failed')
const data = await res.json()
if (data.total === 0) {
searchResults.innerHTML = `<div class="search-placeholder">No users found matching "${escapeHtml(query)}"</div>`
return
}
searchResults.innerHTML = data.users.map(user => `
<div class="user-item" data-pubkey="${escapeHtml(user.pubkey)}">
<div class="user-info">
<div class="user-name">${escapeHtml(user.name)}</div>
<div class="user-pubkey">${escapeHtml(user.pubkey.substring(0, 16))}...</div>
<div class="user-lastseen">Last seen: ${new Date(parseInt(user.lastSeen)).toLocaleString()}</div>
</div>
<button class="view-profile-btn" data-pubkey="${escapeHtml(user.pubkey)}">View Profile</button>
</div>
`).join('')
// Add click handlers for view profile buttons
searchResults.querySelectorAll('.view-profile-btn').forEach(btn => {
btn.onclick = () => {
const pubkey = btn.dataset.pubkey
modal.remove()
showUserProfileModal(pubkey)
}
})
} catch (err) {
console.error('Search error:', err)
searchResults.innerHTML = '<div class="search-placeholder error">Search failed. Please try again.</div>'
}
}, 300)
}
}
// Show user profile modal
async function showUserProfileModal(pubkey) {
const modal = document.createElement('div')
modal.className = 'modal-overlay'
modal.innerHTML = `
<div class="modal profile-modal">
<h2>Loading profile...</h2>
<div class="modal-actions">
<button id="profile-modal-close">Close</button>
</div>
</div>
`
document.body.appendChild(modal)
const modalContent = modal.querySelector('.modal')
const closeBtn = modal.querySelector('#profile-modal-close')
closeBtn.onclick = () => modal.remove()
modal.onclick = (e) => {
if (e.target === modal) modal.remove()
}
try {
// Fetch profile from relay via our backend proxy
const res = await fetch(`/api/directory/${encodeURIComponent(pubkey)}`)
if (!res.ok) {
const errorText = await res.text()
console.error('Profile fetch failed:', res.status, errorText)
throw new Error(`Failed to load profile: ${res.status}`)
}
const profile = await res.json()
modalContent.innerHTML = `
<h2>👤 ${escapeHtml(profile.name)}</h2>
<div class="profile-details">
<div class="profile-field">
<strong>Pubkey:</strong>
<span class="monospace">${escapeHtml(profile.pubkey)}</span>
</div>
<div class="profile-field">
<strong>Messages:</strong> ${profile.messageCount}
</div>
<div class="profile-field">
<strong>Last seen:</strong> ${new Date(parseInt(profile.lastSeen)).toLocaleString()}
</div>
</div>
<h3>Recent Posts</h3>
<div class="recent-posts">
${profile.recentPosts.map(post => {
// Remove YAML frontmatter for cleaner display
const text = post.text.replace(/^---\s*\n[\s\S]*?\n---\s*\n/, '')
const date = new Date(parseInt(post.timestamp)).toLocaleString()
return `
<div class="post-item">
<div class="post-date">${date}</div>
<div class="post-text">${escapeHtml(text)}</div>
</div>
`
}).join('')}
</div>
<div class="modal-actions">
<button id="follow-user-btn" class="primary" data-pubkey="${escapeHtml(profile.pubkey)}" data-name="${escapeHtml(profile.name)}">
Follow ${escapeHtml(profile.name)}
</button>
<button id="profile-modal-close">Close</button>
</div>
`
// Re-attach close handler
modalContent.querySelector('#profile-modal-close').onclick = () => modal.remove()
// Follow button handler
modalContent.querySelector('#follow-user-btn').onclick = async () => {
const followBtn = modalContent.querySelector('#follow-user-btn')
const pubkey = followBtn.dataset.pubkey
const name = followBtn.dataset.name
try {
followBtn.disabled = true
followBtn.textContent = 'Following...'
const res = await fetch('/api/follow', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ publicKey: pubkey, name })
})
if (!res.ok) {
const error = await res.json()
throw new Error(error.error || 'Failed to follow')
}
showToast(`Followed ${name}!`, 'success')
modal.remove()
loadTimeline()
} catch (err) {
console.error('Follow error:', err)
showToast(err.message, 'error')
followBtn.disabled = false
followBtn.textContent = `Follow ${name}`
}
}
} catch (err) {
console.error('Profile load error:', err)
modalContent.innerHTML = `
<h2>Error</h2>
<p>Failed to load profile: ${escapeHtml(err.message)}</p>
<div class="modal-actions">
<button id="profile-modal-close">Close</button>
</div>
`
modalContent.querySelector('#profile-modal-close').onclick = () => modal.remove()
}
}
// Show settings modal
async function showSettingsModal() {
const modal = document.createElement('div')
modal.className = 'modal-overlay'
modal.innerHTML = `
<div class="modal settings-modal">
<h2>⚙️ Settings</h2>
<div class="settings-section">
<h3>🌉 Relay Bridge</h3>
<p class="settings-description">
Bridge your P2P posts to web relays for broader reach.
</p>
<div id="bridge-status-display" class="bridge-status">
<div class="status-loading">Loading bridge status...</div>
</div>
<div class="settings-field">
<label for="relay-url">Relay URL</label>
<select id="relay-url">
<option value="https://7rnx.net/relay">7rnx.net (Default)</option>
<option value="https://pub.wiredove.net">pub.wiredove.net</option>
<option value="custom">Custom...</option>
</select>
<input type="text" id="custom-relay-url" placeholder="https://your-relay.com" style="display: none; margin-top: 8px;" />
</div>
<div class="settings-field">
<label>
<input type="checkbox" id="auto-start-bridge" />
Auto-start bridge with web server
</label>
</div>
<div class="bridge-actions">
<button id="bridge-start-btn" class="primary">▶ Start Bridge</button>
<button id="bridge-stop-btn">⏹ Stop Bridge</button>
<button id="bridge-sync-btn">🔄 Sync Now</button>
</div>
<div id="bridge-stats" class="bridge-stats" style="display: none; margin-top: 16px;">
<strong>Statistics:</strong>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-label">Pending Posts:</span>
<span id="stat-pending" class="stat-value">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Total Pushed:</span>
<span id="stat-pushed" class="stat-value">-</span>
</div>
<div class="stat-item">
<span class="stat-label">Total Pulled:</span>
<span id="stat-pulled" class="stat-value">-</span>
</div>
</div>
</div>
</div>
<div class="modal-actions">
<button id="settings-save" class="primary">Save Settings</button>
<button id="settings-close">Close</button>
</div>
</div>
`
document.body.appendChild(modal)
const relayUrlSelect = modal.querySelector('#relay-url')
const customRelayInput = modal.querySelector('#custom-relay-url')
const autoStartCheckbox = modal.querySelector('#auto-start-bridge')
const statusDisplay = modal.querySelector('#bridge-status-display')
const startBtn = modal.querySelector('#bridge-start-btn')
const stopBtn = modal.querySelector('#bridge-stop-btn')
const syncBtn = modal.querySelector('#bridge-sync-btn')
// Load current bridge config
try {
const configRes = await fetch('/api/bridge/config')
const config = await configRes.json()
// Set relay URL
if (config.relayUrl === 'https://7rnx.net/relay' || config.relayUrl === 'https://pub.wiredove.net') {
relayUrlSelect.value = config.relayUrl
} else {
relayUrlSelect.value = 'custom'
customRelayInput.value = config.relayUrl
customRelayInput.style.display = 'block'
}
// Set auto-start
autoStartCheckbox.checked = config.autoStart || false
// Load bridge status
updateBridgeStatus()
} catch (err) {
console.error('Failed to load bridge config:', err)
statusDisplay.innerHTML = '<div class="status-error">❌ Failed to load bridge status</div>'
}
// Show/hide custom URL input
relayUrlSelect.onchange = () => {
if (relayUrlSelect.value === 'custom') {
customRelayInput.style.display = 'block'
} else {
customRelayInput.style.display = 'none'
}
}
// Update bridge status display
async function updateBridgeStatus() {
try {
const res = await fetch('/api/bridge/status')
const status = await res.json()
const relayOnline = status.status && status.status.relay && status.status.relay.online
const bridgeRunning = status.running
// Extract stats for display (declare at function scope)
const pending = (status.status && status.status.sync && status.status.sync.pendingPosts) || 0
const pushed = (status.status && status.status.stats && status.status.stats.totalPushed) || 0
const pulled = (status.status && status.status.stats && status.status.stats.totalPulled) || 0
let statusHtml = '<div class="status-info">'
statusHtml += `<div class="status-item"><strong>Bridge:</strong> ${bridgeRunning ? '🟢 Running' : '🔴 Stopped'}</div>`
statusHtml += `<div class="status-item"><strong>Relay:</strong> ${relayOnline ? '🟢 Online' : '🔴 Offline'}</div>`
if (status.status && status.status.sync) {
statusHtml += `<div class="status-item"><strong>Pending:</strong> ${pending} posts</div>`
}
if (status.status && status.status.stats) {
statusHtml += `<div class="status-item"><strong>Synced:</strong> ${pushed} pushed, ${pulled} pulled</div>`
// Update stats display
const statsSection = modal.querySelector('#bridge-stats')
if (statsSection) {
statsSection.style.display = 'block'
modal.querySelector('#stat-pending').textContent = pending
modal.querySelector('#stat-pushed').textContent = pushed
modal.querySelector('#stat-pulled').textContent = pulled
}
}
statusHtml += '</div>'
statusDisplay.innerHTML = statusHtml
// Update button states
startBtn.disabled = bridgeRunning
stopBtn.disabled = !bridgeRunning
} catch (err) {
console.error('Failed to load bridge status:', err)
statusDisplay.innerHTML = '<div class="status-error">❌ Error loading status</div>'
}
}
// Start bridge
startBtn.onclick = async () => {
try {
startBtn.disabled = true
startBtn.textContent = '⏳ Starting...'
const res = await fetch('/api/bridge/start', { method: 'POST' })
if (!res.ok) throw new Error('Failed to start bridge')
showToast('Bridge started', 'success')
await updateBridgeStatus()
startBtn.textContent = '▶ Start Bridge'
} catch (err) {
console.error('Failed to start bridge:', err)
showToast('Failed to start bridge: ' + err.message, 'error')
startBtn.textContent = '▶ Start Bridge'
startBtn.disabled = false
}
}
// Stop bridge
stopBtn.onclick = async () => {
try {
stopBtn.disabled = true
stopBtn.textContent = '⏳ Stopping...'
const res = await fetch('/api/bridge/stop', { method: 'POST' })
if (!res.ok) throw new Error('Failed to stop bridge')
showToast('Bridge stopped', 'success')
await updateBridgeStatus()
stopBtn.textContent = '⏹ Stop Bridge'
} catch (err) {
console.error('Failed to stop bridge:', err)
showToast('Failed to stop bridge: ' + err.message, 'error')
stopBtn.textContent = '⏹ Stop Bridge'
stopBtn.disabled = false
}
}
// Sync now
syncBtn.onclick = async () => {
try {
syncBtn.disabled = true
syncBtn.textContent = '⏳ Syncing...'
const res = await fetch('/api/bridge/sync', { method: 'POST' })
if (!res.ok) throw new Error('Failed to sync')
const result = await res.json()
showToast(`Synced: ${result.pushed || 0} pushed, ${result.pulled || 0} pulled`, 'success')
await updateBridgeStatus()
syncBtn.textContent = '🔄 Sync Now'
syncBtn.disabled = false
} catch (err) {
console.error('Failed to sync:', err)
showToast('Failed to sync: ' + err.message, 'error')
syncBtn.textContent = '🔄 Sync Now'
syncBtn.disabled = false
}
}
// Save settings
modal.querySelector('#settings-save').onclick = async () => {
try {
const saveBtn = modal.querySelector('#settings-save')
saveBtn.disabled = true
saveBtn.textContent = 'Saving...'
const relayUrl = relayUrlSelect.value === 'custom' ? customRelayInput.value.trim() : relayUrlSelect.value
const autoStart = autoStartCheckbox.checked
if (!relayUrl) {
showToast('Please enter a relay URL', 'error')
saveBtn.disabled = false
saveBtn.textContent = 'Save Settings'
return
}
const res = await fetch('/api/bridge/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ relayUrl, autoStart })
})
if (!res.ok) throw new Error('Failed to save config')
showToast('Settings saved', 'success')
saveBtn.textContent = 'Save Settings'
saveBtn.disabled = false
} catch (err) {
console.error('Failed to save settings:', err)
showToast('Failed to save settings: ' + err.message, 'error')
modal.querySelector('#settings-save').disabled = false
modal.querySelector('#settings-save').textContent = 'Save Settings'
}
}
// Close modal
modal.querySelector('#settings-close').onclick = () => modal.remove()
modal.onclick = (e) => {
if (e.target === modal) modal.remove()
}
}
// Setup event listeners
function setupEventListeners() {
// Compose button
const composeBtn = document.getElementById('compose-btn')
if (composeBtn) {
composeBtn.onclick = showComposeModal
}
// Refresh button
const refreshBtn = document.getElementById('refresh-btn')
if (refreshBtn) {
refreshBtn.onclick = () => {
loadTimeline()
showToast('Refreshing timeline...', 'info')
}
}
// Follow button
const followBtn = document.getElementById('follow-btn')
if (followBtn) {
followBtn.onclick = showFollowModal
}
// Profile button
const profileBtn = document.getElementById('profile-btn')
if (profileBtn) {
profileBtn.onclick = showProfileModal
}
// Search button
const searchBtn = document.getElementById('search-btn')
if (searchBtn) {
searchBtn.onclick = showSearchModal
}
// Settings button
const settingsBtn = document.getElementById('settings-btn')
if (settingsBtn) {
settingsBtn.onclick = showSettingsModal
}
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div')
div.textContent = text
return div.innerHTML
}
// Parse markdown image syntax:  -> <img> tags
function parseMarkdown(text, imageData) {
// First escape HTML
let html = escapeHtml(text)
// Then parse markdown images:  or 
// Replace with img tags using the stored imageData if available
if (imageData) {
// Determine data URL format
let dataUrl
if (imageData.startsWith('data:')) {
dataUrl = imageData
} else {
dataUrl = `data:image/jpeg;base64,${imageData}`
}
// Replace all  with <img> tags
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, hash) => {
return `<img src="${dataUrl}" alt="${alt || 'Image'}" class="inline-image" />`
})
}
// Basic line breaks: convert \n to <br>
html = html.replace(/\n/g, '<br>')
return html
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init)
} else {
init()
}