/* Beep — Twitter-style microblog frontend */ (function () { 'use strict'; var config = window.BeepConfig || {}; var limit = (config.charLimit || 280) | 0; document.addEventListener('DOMContentLoaded', init); function init() { var feeds = document.querySelectorAll('[data-beep-feed]'); Array.prototype.forEach.call(feeds, initFeed); // Delegate on document so dynamically injected content (thread modal, // GIF picker, toasts) is handled too. document.addEventListener('click', onClick); document.addEventListener('input', onInput); document.addEventListener('keydown', onKeydown); document.addEventListener('keydown', onEscape); } var feedState = { offset: 0, loading: false, hasMore: true, currentSort: 'latest' }; function initFeed(feed) { var compose = feed.querySelector('[data-beep-compose]'); if (compose) initCompose(compose, feed); initSortTabs(feed); initMedia(compose, feed); initInfiniteScroll(feed); } /* ---------- Infinite scroll ---------- */ function initInfiniteScroll(feed) { var sentinel = document.createElement('div'); sentinel.className = 'beep-scroll-sentinel'; sentinel.setAttribute('data-beep-scroll-sentinel', ''); var list = feed.querySelector('[data-beep-list]'); if (list) list.appendChild(sentinel); if (!('IntersectionObserver' in window)) return; var observer = new IntersectionObserver(function(entries) { if (entries[0].isIntersecting && !feedState.loading && feedState.hasMore) { loadMore(feed); } }, { rootMargin: '400px' }); observer.observe(sentinel); feed._beepScrollObserver = observer; } function loadMore(feed) { feedState.loading = true; var sentinel = feed.querySelector('[data-beep-scroll-sentinel]'); if (sentinel) sentinel.innerHTML = '
Loading...
'; fetch(config.restUrl + 'feed?sort=' + encodeURIComponent(feedState.currentSort) + '&offset=' + feedState.offset, { credentials: 'same-origin', headers: { 'X-WP-Nonce': config.nonce } }) .then(handleResponse) .then(function(data) { feedState.hasMore = !!data.has_more; feedState.offset = data.offset || feedState.offset + 50; if (sentinel) sentinel.innerHTML = ''; if (data.html) { var list = feed.querySelector('[data-beep-list]'); if (list) { var tmp = document.createElement('div'); tmp.innerHTML = data.html; while (tmp.firstElementChild) { // Insert before the sentinel list.insertBefore(tmp.firstElementChild, sentinel); } } } if (!feedState.hasMore && sentinel) { sentinel.innerHTML = '
No more beeps
'; } }) .catch(function() { if (sentinel) sentinel.innerHTML = ''; }) .finally(function() { feedState.loading = false; }); } /* ---------- Composer ---------- */ function initCompose(compose, feed) { var input = compose.querySelector('[data-beep-input]'); var counter = compose.querySelector('[data-beep-counter]'); var button = compose.querySelector('[data-beep-submit]'); if (!input || !button) return; function refresh() { autosize(input); var remaining = limit - countChars(input.value); counter.textContent = remaining; counter.classList.toggle('beep-counter-warn', remaining < 20 && remaining >= 0); counter.classList.toggle('beep-counter-bad', remaining < 0); button.disabled = input.value.trim() === '' || remaining < 0; } input.addEventListener('input', refresh); refresh(); button.addEventListener('click', function () { var content = input.value.trim(); if (!content) return; button.disabled = true; var quotedPost = compose._beepQuotedPost; var gifUrl = compose._beepGifUrl; var payload = { content: content }; if (quotedPost && quotedPost.id) payload.quote_id = quotedPost.id; if (gifUrl) payload.gif_url = gifUrl; fetch(config.restUrl + 'post', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }, credentials: 'same-origin', body: JSON.stringify(payload) }) .then(handleResponse) .then(function (data) { var list = feed.querySelector('[data-beep-list]'); var empty = list && list.querySelector('.beep-empty'); if (empty) empty.remove(); // Reload feed to get the fully rendered post (includes media, quotes, polls) reloadFeed(feed, feedState.currentSort); input.value = ''; clearQuotePreview(compose); if (compose._beepGifUrl) { compose._beepGifUrl = null; var gifPrev = compose.querySelector('[data-beep-gif-preview]'); if (gifPrev) { gifPrev.innerHTML = ''; gifPrev.hidden = true; } } refresh(); // Upload pending images and voice note to this new beep var pending = compose._beepPendingImages; var pendingVoice = compose._beepPendingVoice; var uploadDone = function () { reloadFeed(feed, feedState.currentSort); }; var tasks = []; if (pending && pending.length && data.id) { tasks.push(uploadPendingImages(data.id, pending, compose)); } if (pendingVoice && data.id) { tasks.push(uploadPendingVoice(data.id, pendingVoice, compose)); } // Create poll if poll data exists if (data.id) { tasks.push(createPoll(data.id, compose).then(function() { clearPoll(compose); })); } if (tasks.length) { Promise.all(tasks).then(uploadDone); } else { // No uploads, just clear poll if present clearPoll(compose); } }) .catch(function () { showToast('Could not post. Please try again.', 'error'); button.disabled = false; }); }); } /* ---------- Sort tabs ---------- */ function initSortTabs(feed) { var tabs = feed.querySelector('[data-beep-tabs]'); if (!tabs) return; tabs.addEventListener('click', function (e) { var btn = e.target.closest('[data-beep-sort]'); if (!btn) return; e.preventDefault(); var sort = btn.dataset.beepSort; tabs.querySelectorAll('[data-beep-sort]').forEach(function (b) { b.classList.toggle('is-active', b === btn); }); reloadFeed(feed, sort); }); } function reloadFeed(feed, sort) { var list = feed.querySelector('[data-beep-list]'); if (!list) return; list.classList.add('beep-loading'); feedState.offset = 0; feedState.hasMore = true; feedState.currentSort = sort; fetch(config.restUrl + 'feed?sort=' + encodeURIComponent(sort), { credentials: 'same-origin', headers: { 'X-WP-Nonce': config.nonce } }) .then(handleResponse) .then(function (data) { // Remove everything except the sentinel var sentinel = list.querySelector('[data-beep-scroll-sentinel]'); list.innerHTML = ''; if (data.html) { var tmp = document.createElement('div'); tmp.innerHTML = data.html; while (tmp.firstElementChild) { list.appendChild(tmp.firstElementChild); } } else { list.innerHTML = '
' + emptyIcon() + '

No beeps yet.

Be the first to share something!
'; } if (sentinel) list.appendChild(sentinel); feedState.hasMore = !!data.has_more; feedState.offset = data.offset || 50; list.classList.remove('beep-loading'); }) .catch(function () { list.classList.remove('beep-loading'); }); } function emptyIcon() { return ''; } /* ---------- Click delegation ---------- */ function onClick(e) { var t = e.target; var likePost = t.closest('[data-beep-like-post]'); if (likePost) { e.preventDefault(); if (requireLogin()) handleLike(likePost, 'like/' + likePost.dataset.beepLikePost); return; } var likeReply = t.closest('[data-beep-like-reply]'); if (likeReply) { e.preventDefault(); if (requireLogin()) handleLike(likeReply, 'reply-like/' + likeReply.dataset.beepLikeReply); return; } var replyToggle = t.closest('[data-beep-toggle-reply]'); if (replyToggle) { e.preventDefault(); if (!requireLogin()) return; var container = replyToggle.closest('[data-beep-post], [data-beep-reply]'); if (!container) return; var form = container.querySelector(':scope > .beep-post-body > [data-beep-reply-form], :scope > .beep-reply-body > [data-beep-reply-form]'); if (!form) return; form.hidden = !form.hidden; if (!form.hidden) { var ta = form.querySelector('textarea'); if (ta) ta.focus(); } return; } var replyBtn = t.closest('[data-beep-reply-submit]'); if (replyBtn) { e.preventDefault(); handleReplySubmit(replyBtn); return; } var shareBtn = t.closest('[data-beep-share]'); if (shareBtn) { e.preventDefault(); handleShare(shareBtn); return; } var delPost = t.closest('[data-beep-delete]'); if (delPost) { e.preventDefault(); handleDeleteWithUndo('delete/' + delPost.dataset.beepDelete, '[data-beep-post="' + delPost.dataset.beepDelete + '"]', 'beep'); return; } var delReply = t.closest('[data-beep-delete-reply]'); if (delReply) { e.preventDefault(); handleDeleteWithUndo('delete-reply/' + delReply.dataset.beepDeleteReply, '[data-beep-reply="' + delReply.dataset.beepDeleteReply + '"]', 'reply'); return; } // Thread / conversation view var threadBtn = t.closest('[data-beep-thread]'); if (threadBtn) { e.preventDefault(); openThread(threadBtn.dataset.beepThread); return; } // Thread modal close var threadClose = t.closest('[data-beep-thread-close]'); if (threadClose) { e.preventDefault(); closeThread(); return; } // Quote button - open composer with quoted beep pre-loaded var quoteBtn = t.closest('[data-beep-quote]'); if (quoteBtn) { e.preventDefault(); openComposerWithQuote(quoteBtn.dataset.beepQuote); return; } // Quote composer tab toggle var quoteComposerBtn = t.closest('[data-beep-quote-composer-btn]'); if (quoteComposerBtn) { e.preventDefault(); var compose = quoteComposerBtn.closest('[data-beep-compose]'); var quoteSection = compose && compose.querySelector('[data-beep-compose-quote]'); if (quoteSection) quoteSection.hidden = !quoteSection.hidden; return; } // Quote lookup - Add button var quoteLookupBtn = t.closest('[data-beep-quote-lookup-btn]'); if (quoteLookupBtn) { e.preventDefault(); var compose = quoteLookupBtn.closest('[data-beep-compose]'); var input = compose && compose.querySelector('[data-beep-quote-lookup]'); var query = input ? input.value.trim() : ''; if (!query) return; fetch(config.restUrl + 'lookup?q=' + encodeURIComponent(query), { headers: { 'X-WP-Nonce': config.nonce }, credentials: 'same-origin' }) .then(handleResponse) .then(function(quoted) { if (compose) { compose._beepQuotedPost = { id: quoted.id, data: quoted }; showQuotePreview(compose, quoted); } }) .catch(function() { showToast('Beep not found.', 'error'); }); return; } // Quote remove button var quoteRemoveBtn = t.closest('[data-beep-quote-remove]'); if (quoteRemoveBtn) { e.preventDefault(); var compose = quoteRemoveBtn.closest('[data-beep-compose]'); clearQuotePreview(compose); return; } // GIF picker button var gifBtn = t.closest('[data-beep-gif-btn]'); if (gifBtn) { e.preventDefault(); var compose = gifBtn.closest('[data-beep-compose]'); openGifPicker(compose); return; } // GIF modal close var gifClose = t.closest('[data-beep-gif-close]'); if (gifClose) { e.preventDefault(); closeGifPicker(); return; } // GIF search on Enter (handled in keydown) // GIF search button var gifSearchBtn = t.closest('[data-beep-gif-search-btn]'); if (gifSearchBtn) { e.preventDefault(); var compose = gifSearchBtn.closest('[data-beep-compose]') || document.querySelector('[data-beep-compose]'); var input = gifSearchBtn.closest('.beep-gif-search-bar') && gifSearchBtn.closest('.beep-gif-search-bar').querySelector('[data-beep-gif-search]'); if (input) searchGifs(input.value, compose); return; } // GIF remove var gifRemoveBtn = t.closest('[data-beep-gif-remove]'); if (gifRemoveBtn) { e.preventDefault(); var compose = gifRemoveBtn.closest('[data-beep-compose]'); if (compose) { compose._beepGifUrl = null; var preview = compose.querySelector('[data-beep-gif-preview]'); if (preview) { preview.innerHTML = ''; preview.hidden = true; } } return; } // Poll button - toggle poll section var pollBtn = t.closest('[data-beep-poll-btn]'); if (pollBtn) { e.preventDefault(); var compose = pollBtn.closest('[data-beep-compose]'); if (compose) openPollSection(compose); return; } // Poll add option var pollAddOptionBtn = t.closest('[data-beep-poll-add-option]'); if (pollAddOptionBtn) { e.preventDefault(); var compose = pollAddOptionBtn.closest('[data-beep-compose]'); if (compose) addPollOption(compose); return; } // Poll remove option var pollRemoveOptionBtn = t.closest('[data-beep-poll-remove-option]'); if (pollRemoveOptionBtn) { e.preventDefault(); var compose = pollRemoveOptionBtn.closest('[data-beep-compose]'); if (compose) removePollOption(compose); return; } // Poll remove var pollRemoveBtn = t.closest('[data-beep-poll-remove]'); if (pollRemoveBtn) { e.preventDefault(); var compose = pollRemoveBtn.closest('[data-beep-compose]'); if (compose) clearPoll(compose); return; } // Poll vote button var pollVoteBtn = t.closest('[data-beep-poll-vote]'); if (pollVoteBtn) { e.preventDefault(); if (requireLogin()) handlePollVote(pollVoteBtn); return; } // Image lightbox - click on post images to view full size var lightboxImg = t.closest('[data-beep-lightbox]'); if (lightboxImg) { e.preventDefault(); openLightbox(lightboxImg.href || lightboxImg.querySelector('img').src); return; } // Lightbox close var lightboxClose = t.closest('.beep-lightbox'); if (lightboxClose) { closeLightbox(); return; } // Modal backdrop click closes if (t.matches && t.matches('[data-beep-thread-modal]')) { closeThread(); return; } if (t.matches && t.matches('[data-beep-gif-modal]')) { closeGifPicker(); return; } // Click anywhere else on a feed post opens the conversation var openPost = t.closest('[data-beep-open-thread]'); if (openPost && !t.closest('a, button, label, input, textarea, audio, video, .beep-media-grid-item, .beep-gif-embed, [data-beep-reply-form]')) { // Don't hijack text selection var sel = window.getSelection && window.getSelection(); if (sel && sel.toString()) return; openThread(openPost.dataset.beepOpenThread); } } function onEscape(e) { if (e.key !== 'Escape') return; var lightbox = document.querySelector('.beep-lightbox'); if (lightbox) { closeLightbox(); return; } var gifModal = document.querySelector('[data-beep-gif-modal]:not([hidden])'); if (gifModal) { closeGifPicker(); return; } var threadModal = document.querySelector('[data-beep-thread-modal]:not([hidden])'); if (threadModal) closeThread(); } function onInput(e) { var input = e.target.closest('.beep-reply-input, .beep-compose-input'); if (!input) return; autosize(input); var counter = input.parentElement.querySelector('.beep-char-counter'); var submit = input.parentElement.querySelector('.beep-button'); if (counter) { var remaining = limit - countChars(input.value); counter.textContent = remaining; counter.classList.toggle('beep-counter-warn', remaining < 20 && remaining >= 0); counter.classList.toggle('beep-counter-bad', remaining < 0); if (submit) submit.disabled = input.value.trim() === '' || remaining < 0; } } function onKeydown(e) { // Enter in the GIF search box runs the search if (e.key === 'Enter' && e.target.matches && e.target.matches('[data-beep-gif-search]')) { e.preventDefault(); var compose = document.querySelector('[data-beep-compose]'); searchGifs(e.target.value, compose); return; } // Cmd/Ctrl+Enter submits beeps and replies if (!((e.metaKey || e.ctrlKey) && e.key === 'Enter')) return; var input = e.target.closest('textarea'); if (!input) return; var wrap = input.closest('.beep-compose, .beep-reply-form'); if (!wrap) return; var btn = wrap.querySelector('[data-beep-submit], [data-beep-reply-submit]'); if (btn && !btn.disabled) btn.click(); } /* ---------- Like (works for both posts and replies) ---------- */ function handleLike(btn, urlFragment) { var countEl = btn.querySelector('[data-beep-like-count]'); var was = btn.classList.contains('is-liked'); btn.classList.toggle('is-liked'); if (!was) { btn.classList.add('beep-just-liked'); setTimeout(function () { btn.classList.remove('beep-just-liked'); }, 600); } if (countEl) { var current = parseInt(countEl.textContent, 10) || 0; var optimistic = current + (was ? -1 : 1); countEl.textContent = optimistic > 0 ? optimistic : ''; } fetch(config.restUrl + urlFragment, { method: 'POST', headers: { 'X-WP-Nonce': config.nonce }, credentials: 'same-origin' }) .then(handleResponse) .then(function (data) { if (countEl) countEl.textContent = data.count > 0 ? data.count : ''; btn.classList.toggle('is-liked', !!data.liked); }) .catch(function () { btn.classList.toggle('is-liked', was); }); } /* ---------- Reply submit (top-level or nested) ---------- */ function handleReplySubmit(btn) { var postId = btn.dataset.beepReplySubmit; var parent = btn.dataset.beepReplyParent || '0'; var form = btn.closest('[data-beep-reply-form]'); var input = form && form.querySelector('textarea'); if (!input) return; var content = input.value.trim(); if (!content) return; btn.disabled = true; fetch(config.restUrl + 'reply/' + postId, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }, credentials: 'same-origin', body: JSON.stringify({ content: content, parent: parseInt(parent, 10) || 0 }) }) .then(handleResponse) .then(function (data) { if (data.pending) { form.innerHTML = '
Reply submitted, awaiting moderation.
'; return; } input.value = ''; // Inside the conversation modal: refresh just the thread. if (btn.closest('[data-beep-thread-modal]')) { refreshThread(); return; } // Otherwise reload the feed with the current sort. var feed = btn.closest('[data-beep-feed]'); if (feed) { reloadFeed(feed, feedState.currentSort); } }) .catch(function () { showToast('Could not post reply. Please try again.', 'error'); }) .finally(function () { btn.disabled = false; }); } /* ---------- Share (copy link) ---------- */ function handleShare(btn) { var url = btn.dataset.beepShare; if (!url || !navigator.clipboard) return; navigator.clipboard.writeText(url).then(function () { showToast('Link copied to clipboard', 'success'); }); } /* ---------- Delete with undo toast ---------- */ function handleDeleteWithUndo(pathFragment, selector, type) { var el = document.querySelector(selector); if (!el) return; // Store original state var parent = el.parentNode; var nextSibling = el.nextSibling; // Hide the element with animation el.style.transition = 'opacity 0.3s ease, max-height 0.3s ease'; el.style.opacity = '0'; el.style.maxHeight = '0'; el.style.overflow = 'hidden'; // Show undo toast var toast = document.createElement('div'); toast.className = 'beep-toast beep-toast-undo'; toast.innerHTML = '' + (type === 'beep' ? 'Beep' : 'Reply') + ' deleted.'; document.body.appendChild(toast); // Trigger animation requestAnimationFrame(function() { toast.classList.add('beep-toast-visible'); }); var deleteTimeout = setTimeout(function() { // Actually delete fetch(config.restUrl + pathFragment, { method: 'DELETE', headers: { 'X-WP-Nonce': config.nonce }, credentials: 'same-origin' }) .then(handleResponse) .then(function () { if (el && el.parentNode) el.remove(); }) .catch(function () { // Restore on failure restoreElement(el, parent, nextSibling); showToast('Could not delete.', 'error'); }); dismissToast(toast); }, 4000); // Undo button toast.querySelector('.beep-toast-undo-btn').addEventListener('click', function() { clearTimeout(deleteTimeout); restoreElement(el, parent, nextSibling); dismissToast(toast); }); } function restoreElement(el, parent, nextSibling) { el.style.transition = ''; el.style.opacity = ''; el.style.maxHeight = ''; el.style.overflow = ''; if (nextSibling) { parent.insertBefore(el, nextSibling); } else { parent.appendChild(el); } } function dismissToast(toast) { toast.classList.remove('beep-toast-visible'); setTimeout(function() { toast.remove(); }, 300); } /* ---------- Toast notification ---------- */ function showToast(message, type) { var existing = document.querySelector('.beep-toast.beep-toast-msg'); if (existing) existing.remove(); var toast = document.createElement('div'); toast.className = 'beep-toast beep-toast-msg beep-toast-' + (type || 'info'); toast.textContent = message; document.body.appendChild(toast); requestAnimationFrame(function() { toast.classList.add('beep-toast-visible'); }); setTimeout(function() { dismissToast(toast); }, 3000); } /* ---------- Helpers ---------- */ function requireLogin() { if (config.isLoggedIn) return true; if (config.loginUrl) window.location.href = config.loginUrl; return false; } function autosize(el) { el.style.height = 'auto'; el.style.height = el.scrollHeight + 'px'; } function countChars(s) { return Array.from(s).length; } function escapeHtml(str) { var div = document.createElement('div'); div.appendChild(document.createTextNode(str)); return div.innerHTML; } /* ---------- Media upload (images) ---------- */ function initMedia(compose, feed) { var input = compose && compose.querySelector('[data-beep-media-input]'); var preview = compose && compose.querySelector('[data-beep-media-preview]'); var counter = compose && compose.querySelector('[data-beep-media-count]'); var voiceInput = compose && compose.querySelector('[data-beep-voice-input]'); var voicePreview = compose && compose.querySelector('[data-beep-voice-preview]'); if (!input && !voiceInput) return; var pendingImages = []; var pendingVoice = null; // Voice note input if (voiceInput) { voiceInput.addEventListener('change', function (e) { var files = e.target.files; if (!files || !files.length) return; var file = files[0]; if (file.size > 20 * 1024 * 1024) { showToast('Voice note must be under 20 MB.', 'error'); voiceInput.value = ''; return; } var accepted = ['audio/mpeg','audio/ogg','audio/wav','audio/webm','audio/mp4','audio/x-m4a']; if (accepted.indexOf(file.type) === -1) { showToast('Only MP3, OGG, WAV, WebM, and M4A audio are allowed.', 'error'); voiceInput.value = ''; return; } if (pendingVoice) { showToast('Only one voice note per beep.', 'error'); voiceInput.value = ''; return; } var reader = new FileReader(); reader.onload = function (ev) { var id = 'voice-' + Date.now(); pendingVoice = { file: file, id: id }; compose._beepPendingVoice = pendingVoice; voicePreview.innerHTML = '
Voice note ready
'; voicePreview.hidden = false; }; reader.readAsDataURL(file); voiceInput.value = ''; }); } // Voice remove button if (voicePreview) { voicePreview.addEventListener('click', function (e) { var btn = e.target.closest('[data-beep-voice-remove]'); if (!btn) return; pendingVoice = null; if (compose) compose._beepPendingVoice = null; voicePreview.innerHTML = ''; voicePreview.hidden = true; }); } if (input) { input.addEventListener('change', function (e) { var files = Array.prototype.slice.call(e.target.files); if (!files.length) return; var room = 4 - pendingImages.length; if (room <= 0) { input.value = ''; return; } files = files.slice(0, room); files.forEach(function (file) { if (file.size > 10 * 1024 * 1024) { showToast('Image must be under 10 MB: ' + file.name, 'error'); return; } var reader = new FileReader(); reader.onload = function (ev) { var url = ev.target.result; var id = 'pending-' + Date.now() + '-' + Math.random().toString(36).substr(2, 6); pendingImages.push({ file: file, previewUrl: url, id: id }); renderPreviews(); }; reader.readAsDataURL(file); }); input.value = ''; }); } function renderPreviews() { if (!pendingImages.length) { preview.innerHTML = ''; preview.hidden = true; if (counter) counter.hidden = true; return; } preview.hidden = false; if (counter) { counter.textContent = pendingImages.length + '/4'; counter.hidden = false; } preview.innerHTML = pendingImages.map(function (img) { return '
' + 'Preview' + '' + '
'; }).join(''); } if (preview) { preview.addEventListener('click', function (e) { var btn = e.target.closest('[data-remove]'); if (!btn) return; var id = btn.dataset.remove; pendingImages = pendingImages.filter(function (img) { return img.id !== id; }); compose._beepPendingImages = pendingImages; renderPreviews(); }); } if (compose) { compose._beepPendingImages = pendingImages; compose._beepPendingVoice = pendingVoice; } } function uploadPendingImages(postId, pendingImages, compose) { if (!pendingImages || !pendingImages.length) return Promise.resolve(); var promises = pendingImages.map(function (img) { var formData = new FormData(); formData.append('file', img.file); return fetch(config.restUrl + 'media/' + postId, { method: 'POST', headers: { 'X-WP-Nonce': config.nonce }, credentials: 'same-origin', body: formData }) .then(handleResponse) .catch(function (err) { console.warn('Image upload failed:', img.file.name, err); }); }); return Promise.all(promises).then(function () { pendingImages.length = 0; if (compose) { var preview = compose.querySelector('[data-beep-media-preview]'); if (preview) { preview.innerHTML = ''; preview.hidden = true; } var counter = compose.querySelector('[data-beep-media-count]'); if (counter) counter.hidden = true; } }); } function uploadPendingVoice(postId, pendingVoice, compose) { if (!pendingVoice) return Promise.resolve(); var formData = new FormData(); formData.append('file', pendingVoice.file); return fetch(config.restUrl + 'voice/' + postId, { method: 'POST', headers: { 'X-WP-Nonce': config.nonce }, credentials: 'same-origin', body: formData }) .then(handleResponse) .then(function () { if (compose) { compose._beepPendingVoice = null; var vp = compose.querySelector('[data-beep-voice-preview]'); if (vp) { vp.innerHTML = ''; vp.hidden = true; } } }) .catch(function (err) { console.warn('Voice upload failed:', err); }); } /* ---------- Thread / Conversation modal ---------- */ function openThread(postId, keepScroll) { var modal = document.querySelector('[data-beep-thread-modal]'); var content = modal && modal.querySelector('[data-beep-thread-content]'); var loading = modal && modal.querySelector('[data-beep-thread-loading]'); if (!modal || !content) return; modal.hidden = false; modal._beepThreadId = postId; if (!keepScroll) { if (loading) loading.hidden = false; content.innerHTML = ''; } document.documentElement.style.overflow = 'hidden'; fetch(config.restUrl + 'thread/' + postId + '/html', { headers: { 'X-WP-Nonce': config.nonce }, credentials: 'same-origin' }) .then(handleResponse) .then(function(data) { if (loading) loading.hidden = true; content.innerHTML = data.html || '
Nothing to show.
'; // Sync the reply count back to the feed card if (typeof data.reply_count !== 'undefined') { var feedPost = document.querySelector('.beep-list [data-beep-post="' + postId + '"] [data-beep-reply-count]'); if (feedPost) feedPost.textContent = data.reply_count > 0 ? data.reply_count : ''; } }) .catch(function() { if (loading) loading.hidden = true; content.innerHTML = '
Failed to load conversation.
'; }); } function refreshThread() { var modal = document.querySelector('[data-beep-thread-modal]:not([hidden])'); if (modal && modal._beepThreadId) openThread(modal._beepThreadId, true); } function closeThread() { var modal = document.querySelector('[data-beep-thread-modal]'); if (modal) modal.hidden = true; document.documentElement.style.overflow = ''; } /* ---------- Quote posts ---------- */ function openComposerWithQuote(quotedId) { var compose = document.querySelector('[data-beep-compose]'); if (!compose) return; var form = compose.querySelector('.beep-compose-body'); if (form) form.style.display = 'block'; compose.scrollIntoView({ behavior: 'smooth', block: 'center' }); fetch(config.restUrl + 'quote/' + quotedId, { headers: { 'X-WP-Nonce': config.nonce }, credentials: 'same-origin' }) .then(handleResponse) .then(function(quoted) { compose._beepQuotedPost = { id: quotedId, data: quoted }; showQuotePreview(compose, quoted); }) .catch(function(err) { console.warn('Could not load quoted beep:', err); }); } function showQuotePreview(compose, quoted) { var preview = compose && compose.querySelector('[data-beep-quote-preview]'); var removeBtn = compose && compose.querySelector('[data-beep-quote-remove]'); if (!preview || !quoted) return; var author = quoted.author || {}; var avatar = escapeHtml(author.avatar || 'https://www.gravatar.com/avatar/?s=48&d=mp'); var name = escapeHtml(author.name || 'Unknown'); var username = escapeHtml(author.username || ''); var time_ago = escapeHtml(quoted.time_ago || ''); var content = escapeHtml(quoted.content || ''); preview.innerHTML = '
' + '
' + name + '
' + '
' + '
' + name + ' ' + '@' + username + ' ' + '· ' + '' + time_ago + '
' + '
' + content + '
'; preview.hidden = false; if (removeBtn) removeBtn.hidden = false; } function clearQuotePreview(compose) { if (!compose) return; compose._beepQuotedPost = null; var preview = compose.querySelector('[data-beep-quote-preview]'); var removeBtn = compose.querySelector('[data-beep-quote-remove]'); if (preview) { preview.innerHTML = ''; preview.hidden = true; } if (removeBtn) removeBtn.hidden = true; } /* ---------- GIF Picker (Giphy) ---------- */ function openGifPicker(compose) { var modal = document.querySelector('[data-beep-gif-modal]'); if (!modal) return; modal.hidden = false; var searchInput = modal.querySelector('[data-beep-gif-search]'); if (searchInput) searchInput.value = ''; var grid = modal.querySelector('[data-beep-gif-grid]'); if (grid) grid.innerHTML = '

Type to search GIFs...

'; if (searchInput) searchInput.focus(); } function closeGifPicker() { var modal = document.querySelector('[data-beep-gif-modal]'); if (modal) modal.hidden = true; } function searchGifs(query, compose) { if (!query || !query.trim()) return; var apiKey = config.giphyKey || ''; if (!apiKey) { console.warn('Beep: No Giphy API key configured.'); return; } var modal = document.querySelector('[data-beep-gif-modal]'); var grid = modal && modal.querySelector('[data-beep-gif-grid]'); if (!grid) return; grid.innerHTML = '

Searching...

'; var url = 'https://api.giphy.com/v1/gifs/search?api_key=' + encodeURIComponent(apiKey) + '&q=' + encodeURIComponent(query) + '&limit=30&rating=g'; fetch(url) .then(function(res) { return res.json(); }) .then(function(data) { if (!data.data || !data.data.length) { grid.innerHTML = '

No GIFs found.

'; return; } grid.innerHTML = ''; data.data.forEach(function(gif) { var imgUrl = gif.images && gif.images.fixed_width_small && gif.images.fixed_width_small.url; if (!imgUrl) return; var item = document.createElement('div'); item.className = 'beep-gif-item'; var altText = escapeHtml(gif.title || ''); item.innerHTML = '' + altText + ''; item.addEventListener('click', function() { selectGif(gif, compose); }); grid.appendChild(item); }); }) .catch(function() { grid.innerHTML = '

Search failed.

'; }); } function selectGif(gif, compose) { if (!compose) return; var url = gif.images && gif.images.fixed_width && gif.images.fixed_width.url; if (!url) return; compose._beepGifUrl = url; var preview = compose.querySelector('[data-beep-gif-preview]'); if (preview) { preview.innerHTML = '
GIF
'; preview.hidden = false; } closeGifPicker(); } /* ---------- Poll helpers ---------- */ function openPollSection(compose) { var pollSection = compose.querySelector('[data-beep-compose-poll]'); if (!pollSection) return; pollSection.hidden = false; var questionInput = pollSection.querySelector('[data-beep-poll-question]'); if (questionInput) questionInput.focus(); var options = pollSection.querySelectorAll('[data-beep-poll-option]'); if (options.length < 2) { while (options.length < 2) { addPollOption(compose); options = pollSection.querySelectorAll('[data-beep-poll-option]'); } } } function addPollOption(compose) { var pollSection = compose.querySelector('[data-beep-compose-poll]'); if (!pollSection) return; var optionsContainer = pollSection.querySelector('[data-beep-poll-options]'); if (!optionsContainer) return; var optionCount = optionsContainer.querySelectorAll('[data-beep-poll-option]').length; if (optionCount >= 6) return; var newRow = document.createElement('div'); newRow.className = 'beep-poll-option-row'; newRow.innerHTML = ''; optionsContainer.appendChild(newRow); updatePollButtons(compose); } function removePollOption(compose) { var pollSection = compose.querySelector('[data-beep-compose-poll]'); if (!pollSection) return; var options = pollSection.querySelectorAll('[data-beep-poll-option]'); if (options.length <= 2) return; var lastOption = options[options.length - 1]; if (lastOption) lastOption.parentElement.remove(); updatePollButtons(compose); } function updatePollButtons(compose) { var pollSection = compose.querySelector('[data-beep-compose-poll]'); if (!pollSection) return; var options = pollSection.querySelectorAll('[data-beep-poll-option]'); var addBtn = pollSection.querySelector('[data-beep-poll-add-option]'); var removeBtn = pollSection.querySelector('[data-beep-poll-remove-option]'); if (addBtn) addBtn.hidden = options.length >= 6; if (removeBtn) removeBtn.hidden = options.length <= 2; } function clearPoll(compose) { if (!compose) return; compose._beepPoll = null; var pollSection = compose.querySelector('[data-beep-compose-poll]'); if (pollSection) { pollSection.hidden = true; var question = pollSection.querySelector('[data-beep-poll-question]'); if (question) question.value = ''; var options = pollSection.querySelectorAll('[data-beep-poll-option]'); if (options.length > 2) { for (var i = options.length - 1; i >= 2; i--) { if (options[i].parentElement) options[i].parentElement.remove(); } } options = pollSection.querySelectorAll('[data-beep-poll-option]'); for (var j = 0; j < options.length; j++) { options[j].value = ''; } } } function collectPollData(compose) { if (!compose) return null; var pollSection = compose.querySelector('[data-beep-compose-poll]'); if (!pollSection || pollSection.hidden) return null; var questionInput = pollSection.querySelector('[data-beep-poll-question]'); var question = questionInput ? questionInput.value.trim() : ''; if (!question) return null; var optionInputs = pollSection.querySelectorAll('[data-beep-poll-option]'); var options = []; for (var i = 0; i < optionInputs.length; i++) { var val = optionInputs[i].value.trim(); if (val) options.push(val); } if (options.length < 2) return null; return { question: question, options: options }; } function createPoll(beepId, compose) { var pollData = collectPollData(compose); if (!pollData) return Promise.resolve(); return fetch(config.restUrl + 'poll/' + beepId, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }, credentials: 'same-origin', body: JSON.stringify(pollData) }) .then(handleResponse) .catch(function(err) { console.warn('Poll creation failed:', err); }); } function handlePollVote(btn) { var beepId = btn.dataset.beepPollBeep; var optionIndex = parseInt(btn.dataset.beepPollVote, 10); if (!beepId || isNaN(optionIndex)) return; btn.disabled = true; fetch(config.restUrl + 'poll/' + beepId + '/vote', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce }, credentials: 'same-origin', body: JSON.stringify({ option_index: optionIndex }) }) .then(handleResponse) .then(function(data) { // Inside the conversation modal: refresh just the thread. if (btn.closest('[data-beep-thread-modal]')) { refreshThread(); return; } var postEl = btn.closest('[data-beep-post]'); if (postEl) { var list = postEl.closest('[data-beep-list]'); if (list) reloadFeed(list.closest('[data-beep-feed]'), feedState.currentSort); } }) .catch(function() { btn.disabled = false; }); } /* ---------- Image Lightbox ---------- */ function openLightbox(src) { var existing = document.querySelector('.beep-lightbox'); if (existing) existing.remove(); var el = document.createElement('div'); el.className = 'beep-lightbox'; el.innerHTML = 'Full size image'; document.body.appendChild(el); // Close on click outside image or Escape key el.addEventListener('click', function(e) { if (e.target === el || e.target.tagName !== 'IMG') closeLightbox(); }); var escHandler = function(e) { if (e.key === 'Escape') { closeLightbox(); document.removeEventListener('keydown', escHandler); } }; document.addEventListener('keydown', escHandler); } function closeLightbox() { var el = document.querySelector('.beep-lightbox'); if (el) el.remove(); } function handleResponse(res) { if (!res.ok) return res.json().then(function (j) { throw j; }, function () { throw res; }); return res.json(); } })(); /* Beep frontend — end */