OverviewFilesCommitsIssuesDocsReleasesPeers

wirecore / pear-app / public / app-simple.js

1063 lines · javascript ref:
/**
 * 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 ![](hash))
    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: ![](hash) -> <img> tags
function parseMarkdown(text, imageData) {
  // First escape HTML
  let html = escapeHtml(text)

  // Then parse markdown images: ![](hash) or ![alt](hash)
  // 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 ![...](hash) 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()
}