v1.7.0: sync live — shortcode-only, wordmark, fixed fatal, no auto-inject

This commit is contained in:
2026-06-07 17:49:23 -07:00
parent 2788dc8d96
commit 79e124f282
25 changed files with 1024 additions and 1300 deletions
+318 -237
View File
@@ -1,4 +1,4 @@
var url = `https://api.giphy.com/v1/gifs/search?api_key=${apiKey}&q=${encodeURIComponent(query)}&limit=30&rating=g`;/* Beep — Twitter-style microblog frontend */
/* Beep — Twitter-style microblog frontend */
(function () {
'use strict';
@@ -12,16 +12,72 @@
Array.prototype.forEach.call(feeds, initFeed);
}
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);
feed.addEventListener('click', onClick);
feed.addEventListener('input', onInput);
feed.addEventListener('keydown', onKeydown);
}
/* ---------- 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 = '<div class="beep-loading-more"><span class="beep-spinner"></span> Loading...</div>';
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 = '<div class="beep-no-more">No more beeps</div>';
}
})
.catch(function() {
if (sentinel) sentinel.innerHTML = '';
})
.finally(function() { feedState.loading = false; });
}
/* ---------- Composer ---------- */
function initCompose(compose, feed) {
var input = compose.querySelector('[data-beep-input]');
@@ -59,15 +115,12 @@
.then(handleResponse)
.then(function (data) {
var list = feed.querySelector('[data-beep-list]');
var empty = list.querySelector('.beep-empty');
var empty = list && list.querySelector('.beep-empty');
if (empty) empty.remove();
var tmp = document.createElement('div');
tmp.innerHTML = data.html;
var node = tmp.firstElementChild;
if (node) {
node.classList.add('beep-new');
list.insertBefore(node, list.firstChild);
}
// Reload feed to get the fully rendered post (includes media, quotes, polls)
reloadFeed(feed, 'recent');
input.value = '';
clearQuotePreview(compose);
if (compose._beepGifUrl) {
@@ -81,7 +134,7 @@
var pending = compose._beepPendingImages;
var pendingVoice = compose._beepPendingVoice;
var uploadDone = function () {
reloadFeed(feed, 'recent');
reloadFeed(feed, 'latest');
};
var tasks = [];
@@ -106,7 +159,7 @@
}
})
.catch(function () {
window.alert('Could not post. Please try again.');
showToast('Could not post. Please try again.', 'error');
button.disabled = false;
});
});
@@ -132,13 +185,30 @@
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) {
list.innerHTML = data.html || '<div class="beep-empty">No beeps yet.</div>';
// 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 = '<div class="beep-empty"><div class="beep-empty-icon">' + emptyIcon() + '</div><p>No beeps yet.</p><span>Be the first to share something!</span></div>';
}
if (sentinel) list.appendChild(sentinel);
feedState.hasMore = !!data.has_more;
feedState.offset = data.offset || 50;
list.classList.remove('beep-loading');
})
.catch(function () {
@@ -146,6 +216,10 @@
});
}
function emptyIcon() {
return '<svg viewBox="0 0 24 24" width="64" height="64" fill="#4a90c4" aria-hidden="true"><path d="M17.2 1.4c-.8-.1-1.5.2-2 .6C14.3.9 13.2.7 12.1 1c-1.4.4-2.3 1.7-2.1 3.2-2.1-.2-4.2-.9-5.7-2.5-.3-.3-.8-.3-1 0-.6.8-.9 1.7-.9 2.6 0 1.2.5 2.2 1.3 2.9-.3-.1-.6-.1-.8-.3-.4-.2-.9 0-.8.5.3 1.6 1.3 2.8 2.7 3.4-.3 0-.5 0-.8-.1-.4-.1-.8.3-.5.7.8 1.4 2.2 2.3 3.8 2.5-1.3.8-2.8 1.2-4.4 1.2-.5 0-.8.5-.5.9 1.1 1.1 2.9 1.6 5.5 1.6 5.4 0 9.8-4.4 9.8-9.8v-.2c.9-.6 1.6-1.4 2.1-2.4.2-.4-.2-.8-.6-.6-.5.2-1 .3-1.5.4.5-.5.9-1.1 1-1.8.1-.4-.4-.7-.7-.5-.7.4-1.5.6-2.3.7-.8-.7-1.8-1.1-2.9-1.1zm-.8 1.6c.7-.1 1.4.2 1.9.7.2.2.5.3.8.2.3-.1.6-.1.9-.3-.2.3-.5.5-.8.6-.3.1-.4.5-.3.7.2.3.1.5 0 .8v.2c0 4.7-3.8 8.5-8.5 8.5-1.6 0-2.9-.2-3.8-.6 1.7-.3 3.2-1 4.3-2.2.3-.3.1-.8-.3-.8-1.4 0-2.7-.6-3.5-1.7.4 0 .9-.1 1.3-.2.4-.1.4-.7 0-.8-1.5-.3-2.6-1.3-3-2.6.5.1 1 .2 1.5.1.4 0 .5-.6.1-.8C5.2 5.5 4.5 4.5 4.5 3.4c0-.4.1-.9.3-1.3 1.8 1.5 4 2.4 6.4 2.5.3 0 .5-.2.5-.5-.2-1.1.4-2.1 1.4-2.4.7-.2 1.4 0 1.9.4.3.2.7.1.9-.1.3-.4.8-.7 1.3-.7.4.1.8.2 1.2.4-.3.3-.5.6-.5 1 0 .3.3.5.5.4z"/></svg>';
}
/* ---------- Click delegation ---------- */
function onClick(e) {
var t = e.target;
@@ -160,8 +234,6 @@
if (replyToggle) {
e.preventDefault();
if (!requireLogin()) return;
// The reply form is the sibling .beep-reply-form belonging to the post or reply
// that this toggle button is inside.
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]');
@@ -183,16 +255,14 @@
var delPost = t.closest('[data-beep-delete]');
if (delPost) {
e.preventDefault();
if (!window.confirm('Delete this beep?')) return;
handleDelete('beep/' + delPost.dataset.beepDelete, '[data-beep-post="' + delPost.dataset.beepDelete + '"]');
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();
if (!window.confirm('Delete this reply?')) return;
handleDelete('reply/' + delReply.dataset.beepDeleteReply, '[data-beep-reply="' + delReply.dataset.beepDeleteReply + '"]');
handleDeleteWithUndo('delete-reply/' + delReply.dataset.beepDeleteReply, '[data-beep-reply="' + delReply.dataset.beepDeleteReply + '"]', 'reply');
return;
}
@@ -237,7 +307,7 @@
showQuotePreview(compose, quoted);
}
})
.catch(function() { window.alert('Beep not found.'); });
.catch(function() { showToast('Beep not found.', 'error'); });
return;
}
@@ -253,15 +323,7 @@
var gifClose = t.closest('[data-beep-gif-close]');
if (gifClose) { e.preventDefault(); closeGifPicker(); return; }
// GIF search on Enter
var gifSearchInput = t.closest('[data-beep-gif-search]');
if (gifSearchInput && e.key === 'Enter') {
e.preventDefault();
var compose = gifSearchInput.closest('[data-beep-compose]') || document.querySelector('[data-beep-compose]');
searchGifs(gifSearchInput.value, compose);
return;
}
// GIF search on Enter (handled in keydown)
// GIF search button
var gifSearchBtn = t.closest('[data-beep-gif-search-btn]');
if (gifSearchBtn) {
@@ -328,6 +390,21 @@
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;
}
}
function onInput(e) {
@@ -409,45 +486,14 @@
form.innerHTML = '<div class="beep-pending">Reply submitted, awaiting moderation.</div>';
return;
}
// Where do we insert?
// If parent === 0, append to the post's top-level [data-beep-replies] list.
// Otherwise append (creating if needed) to the parent reply's nested list.
var post = btn.closest('[data-beep-post]');
var targetList;
if ((parseInt(parent, 10) || 0) === 0) {
targetList = post && post.querySelector(':scope > .beep-post-body > [data-beep-replies]');
} else {
var parentReply = post && post.querySelector('[data-beep-reply="' + parent + '"]');
if (parentReply) {
targetList = parentReply.querySelector(':scope > .beep-reply-body > .beep-replies-nested');
if (!targetList) {
targetList = document.createElement('div');
targetList.className = 'beep-replies beep-replies-nested';
targetList.setAttribute('data-beep-replies', '');
parentReply.querySelector('.beep-reply-body').appendChild(targetList);
}
}
// Reload the full feed to get the properly rendered reply
var feed = btn.closest('[data-beep-feed]');
if (feed) {
reloadFeed(feed, 'latest');
}
if (targetList) {
var tmp = document.createElement('div');
tmp.innerHTML = data.html;
var node = tmp.firstElementChild;
if (node) {
node.classList.add('beep-new');
targetList.appendChild(node);
}
}
// Increment top-level reply counter on the post.
var countEl = post && post.querySelector('[data-beep-reply-count]');
if (countEl) {
countEl.textContent = (parseInt(countEl.textContent, 10) || 0) + 1;
}
input.value = '';
autosize(input);
form.hidden = true;
})
.catch(function () {
window.alert('Could not post reply. Please try again.');
showToast('Could not post reply. Please try again.', 'error');
})
.finally(function () { btn.disabled = false; });
}
@@ -457,30 +503,93 @@
var url = btn.dataset.beepShare;
if (!url || !navigator.clipboard) return;
navigator.clipboard.writeText(url).then(function () {
var tip = document.createElement('span');
tip.className = 'beep-tip';
tip.textContent = 'Copied';
btn.style.position = 'relative';
btn.appendChild(tip);
setTimeout(function () { tip.remove(); }, 1500);
showToast('Link copied to clipboard', 'success');
});
}
/* ---------- Delete ---------- */
function handleDelete(pathFragment, selector) {
fetch(config.restUrl + pathFragment, {
method: 'DELETE',
headers: { 'X-WP-Nonce': config.nonce },
credentials: 'same-origin'
})
.then(handleResponse)
.then(function () {
var el = document.querySelector(selector);
if (el) el.remove();
})
.catch(function () {
window.alert('Could not delete.');
/* ---------- 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 = '<span>' + (type === 'beep' ? 'Beep' : 'Reply') + ' deleted.</span><button class="beep-toast-undo-btn" type="button">Undo</button>';
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 ---------- */
@@ -499,18 +608,23 @@
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.querySelector('[data-beep-media-input]');
var preview = compose.querySelector('[data-beep-media-preview]');
var counter = compose.querySelector('[data-beep-media-count]');
var voiceInput = compose.querySelector('[data-beep-voice-input]');
var voicePreview = compose.querySelector('[data-beep-voice-preview]');
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; // { file, id }
var pendingVoice = null;
// Voice note input
if (voiceInput) {
@@ -519,18 +633,18 @@
if (!files || !files.length) return;
var file = files[0];
if (file.size > 20 * 1024 * 1024) {
window.alert('Voice note must be under 20 MB.');
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) {
window.alert('Only MP3, OGG, WAV, WebM, and M4A audio are allowed.');
showToast('Only MP3, OGG, WAV, WebM, and M4A audio are allowed.', 'error');
voiceInput.value = '';
return;
}
if (pendingVoice) {
window.alert('Only one voice note per beep.');
showToast('Only one voice note per beep.', 'error');
voiceInput.value = '';
return;
}
@@ -538,7 +652,8 @@
reader.onload = function (ev) {
var id = 'voice-' + Date.now();
pendingVoice = { file: file, id: id };
voicePreview.innerHTML = '<div class="beep-voice-preview-card"><svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08C16.39 17.43 19 14.53 19 11h-2z"/></svg><span>Voice note</span><button class="beep-voice-remove" data-beep-voice-remove type="button" title="Remove">&times;</button></div>';
compose._beepPendingVoice = pendingVoice;
voicePreview.innerHTML = '<div class="beep-voice-preview-card"><svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08C16.39 17.43 19 14.53 19 11h-2z"/></svg><span>Voice note ready</span><button class="beep-voice-remove" data-beep-voice-remove type="button" title="Remove">&times;</button></div>';
voicePreview.hidden = false;
};
reader.readAsDataURL(file);
@@ -552,33 +667,36 @@
var btn = e.target.closest('[data-beep-voice-remove]');
if (!btn) return;
pendingVoice = null;
if (compose) compose._beepPendingVoice = null;
voicePreview.innerHTML = '';
voicePreview.hidden = true;
});
}
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) {
window.alert('Image must be under 10 MB: ' + file.name);
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);
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 = '';
});
input.value = '';
});
}
function renderPreviews() {
if (!pendingImages.length) {
@@ -594,22 +712,27 @@
}
preview.innerHTML = pendingImages.map(function (img) {
return '<div class="beep-media-preview-item" data-preview-id="' + img.id + '">' +
'<img src="' + img.previewUrl + '">' +
'<img src="' + img.previewUrl + '" alt="Preview">' +
'<button class="beep-media-remove" type="button" title="Remove" data-remove="' + img.id + '">&times;</button>' +
'</div>';
}).join('');
}
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; });
renderPreviews();
});
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();
});
}
compose._beepPendingImages = pendingImages;
compose._beepPendingVoice = pendingVoice;
if (compose) {
compose._beepPendingImages = pendingImages;
compose._beepPendingVoice = pendingVoice;
}
}
function uploadPendingImages(postId, pendingImages, compose) {
@@ -651,8 +774,8 @@
})
.then(handleResponse)
.then(function () {
pendingVoice = null;
if (compose) {
compose._beepPendingVoice = null;
var vp = compose.querySelector('[data-beep-voice-preview]');
if (vp) { vp.innerHTML = ''; vp.hidden = true; }
}
@@ -662,12 +785,6 @@
});
}
function handleResponse(res) {
if (!res.ok) return res.json().then(function (j) { throw j; }, function () { throw res; });
return res.json();
}
})();
/* ---------- Thread / Conversation modal ---------- */
function openThread(postId) {
var modal = document.querySelector('[data-beep-thread-modal]');
@@ -683,8 +800,10 @@
credentials: 'same-origin'
})
.then(handleResponse)
.then(function(thread) {
.then(function(data) {
if (loading) loading.style.display = 'none';
// data can be an array directly or { thread: [...] }
var thread = Array.isArray(data) ? data : (data.thread || []);
renderThread(thread, content);
})
.catch(function() {
@@ -699,24 +818,28 @@
function renderThread(thread, container) {
if (!thread || !thread.length) {
container.innerHTML = '<p style="padding:16px;color:var(--beep-text-secondary);text-align:center;">No replies yet.</p>';
container.innerHTML = '<p style="padding:16px;color:var(--beep-muted);text-align:center;">No replies yet.</p>';
return;
}
var html = '';
thread.forEach(function(item) {
var indent = Math.min(item.depth, 5) * 24;
var author = item.author || {};
var avatar = author.avatar || 'https://www.gravatar.com/avatar/?s=48&d=mp';
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 content_text = escapeHtml(item.content || '');
var time_ago = escapeHtml(item.time_ago || '');
var replyClass = item.depth > 0 ? 'beep-thread-reply' : 'beep-thread-root';
html += '<div class="beep-thread-item ' + replyClass + '" style="margin-left:' + indent + 'px" data-thread-id="' + item.id + '">';
html += '<div class="beep-thread-avatar"><img src="' + avatar + '" alt="' + (author.name || '') + '" width="40" height="40" style="border-radius:50%;vertical-align:top;"></div>';
html += '<div class="beep-thread-item ' + replyClass + '" style="margin-left:' + indent + 'px" data-thread-id="' + (item.id || '') + '">';
html += '<div class="beep-thread-avatar"><img src="' + avatar + '" alt="' + name + '" width="40" height="40" style="border-radius:50%;vertical-align:top;"></div>';
html += '<div class="beep-thread-body">';
html += '<div class="beep-thread-meta">';
html += '<span class="beep-thread-name">' + (author.name || 'Unknown') + '</span>';
html += '<span class="beep-thread-username">@' + (author.username || '') + '</span>';
html += '<span class="beep-thread-time">' + (item.time_ago || '') + '</span>';
html += '<span class="beep-thread-name">' + name + '</span>';
html += '<span class="beep-thread-username">@' + username + '</span>';
html += '<span class="beep-thread-time">' + time_ago + '</span>';
html += '</div>';
html += '<div class="beep-thread-content-text">' + (item.content || '') + '</div>';
html += '<div class="beep-thread-content-text">' + content_text + '</div>';
html += '<div class="beep-thread-actions">';
html += '<span class="beep-thread-reply-count">' + (item.reply_count || 0) + ' replies</span>';
html += '<span class="beep-thread-like-count">' + (item.like_count || 0) + ' likes</span>';
@@ -750,15 +873,19 @@
var removeBtn = compose && compose.querySelector('[data-beep-quote-remove]');
if (!preview || !quoted) return;
var author = quoted.author || {};
var avatar = author.avatar || 'https://www.gravatar.com/avatar/?s=48&d=mp';
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 = '<div class="beep-quoted-post beep-quoted-preview-card">' +
'<div class="beep-quoted-avatar"><img src="' + avatar + '" alt="' + (author.name || '') + '" width="36" height="36"></div>' +
'<div class="beep-quoted-avatar"><img src="' + avatar + '" alt="' + name + '" width="36" height="36"></div>' +
'<div class="beep-quoted-body">' +
'<div class="beep-quoted-meta"><span class="beep-quoted-name">' + (author.name || 'Unknown') + '</span> ' +
'<span class="beep-quoted-handle">@' + (author.username || '') + '</span> ' +
'<div class="beep-quoted-meta"><span class="beep-quoted-name">' + name + '</span> ' +
'<span class="beep-quoted-handle">@' + username + '</span> ' +
'<span class="beep-quoted-dot">·</span> ' +
'<span class="beep-quoted-time">' + (quoted.time_ago || '') + '</span></div>' +
'<div class="beep-quoted-text">' + (quoted.content || '') + '</div></div></div>';
'<span class="beep-quoted-time">' + time_ago + '</span></div>' +
'<div class="beep-quoted-text">' + content + '</div></div></div>';
preview.hidden = false;
if (removeBtn) removeBtn.hidden = false;
}
@@ -773,8 +900,6 @@
}
/* ---------- GIF Picker (Giphy) ---------- */
var GIPHY_API_KEY = (window.BeepConfig && window.BeepConfig.giphyKey) || '';
function openGifPicker(compose) {
var modal = document.querySelector('[data-beep-gif-modal]');
if (!modal) return;
@@ -782,7 +907,7 @@
var searchInput = modal.querySelector('[data-beep-gif-search]');
if (searchInput) searchInput.value = '';
var grid = modal.querySelector('[data-beep-gif-grid]');
if (grid) grid.innerHTML = '<p style="padding:20px;text-align:center;color:var(--beep-text-secondary);">Type to search GIFs...</p>';
if (grid) grid.innerHTML = '<p style="padding:20px;text-align:center;color:var(--beep-muted);">Type to search GIFs...</p>';
if (searchInput) searchInput.focus();
}
@@ -793,29 +918,33 @@
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 = '<p style="padding:20px;text-align:center;color:var(--beep-text-secondary);">Searching...</p>';
grid.innerHTML = '<p style="padding:20px;text-align:center;color:var(--beep-muted);">Searching...</p>';
var apiKey = (window.BeepConfig && window.BeepConfig.giphyKey) || GIPHY_API_KEY;
var url = `https://api.giphy.com/v1/gifs/search?api_key=${apiKey}&q=${encodeURIComponent(query)}&limit=30&rating=g`;
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 = '<p style="padding:20px;text-align:center;color:var(--beep-text-secondary);">No GIFs found.</p>';
grid.innerHTML = '<p style="padding:20px;text-align:center;color:var(--beep-muted);">No GIFs found.</p>';
return;
}
grid.innerHTML = '';
data.data.forEach(function(gif) {
var url = gif.images && gif.images.fixed_width_small && gif.images.fixed_width_small.url;
if (!url) return;
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';
item.style.cssText = 'cursor:pointer;border-radius:6px;overflow:hidden;transition:opacity 0.15s;';
item.innerHTML = '<img src="' + url + '" alt="' + (gif.title || '') + '" style="width:100%;display:block;border-radius:6px;">';
var altText = escapeHtml(gif.title || '');
item.innerHTML = '<img src="' + escapeHtml(imgUrl) + '" alt="' + altText + '">';
item.addEventListener('click', function() {
selectGif(gif, compose);
});
@@ -823,7 +952,7 @@
});
})
.catch(function() {
grid.innerHTML = '<p style="padding:20px;text-align:center;color:var(--beep-text-secondary);">Search failed.</p>';
grid.innerHTML = '<p style="padding:20px;text-align:center;color:var(--beep-muted);">Search failed.</p>';
});
}
@@ -834,7 +963,7 @@
compose._beepGifUrl = url;
var preview = compose.querySelector('[data-beep-gif-preview]');
if (preview) {
preview.innerHTML = '<div class="beep-gif-preview-card"><img src="' + url + '" alt="GIF" style="max-width:200px;max-height:120px;border-radius:8px;"><button class="beep-gif-remove" data-beep-gif-remove type="button">&times;</button></div>';
preview.innerHTML = '<div class="beep-gif-preview-card"><img src="' + escapeHtml(url) + '" alt="GIF"><button class="beep-gif-remove" data-beep-gif-remove type="button">&times;</button></div>';
preview.hidden = false;
}
closeGifPicker();
@@ -845,10 +974,8 @@
var pollSection = compose.querySelector('[data-beep-compose-poll]');
if (!pollSection) return;
pollSection.hidden = false;
// Focus question input
var questionInput = pollSection.querySelector('[data-beep-poll-question]');
if (questionInput) questionInput.focus();
// Ensure we have at least 2 options
var options = pollSection.querySelectorAll('[data-beep-poll-option]');
if (options.length < 2) {
while (options.length < 2) {
@@ -864,10 +991,10 @@
var optionsContainer = pollSection.querySelector('[data-beep-poll-options]');
if (!optionsContainer) return;
var optionCount = optionsContainer.querySelectorAll('[data-beep-poll-option]').length;
if (optionCount >= 6) return; // Max 6 options
if (optionCount >= 6) return;
var newRow = document.createElement('div');
newRow.className = 'beep-poll-option-row';
newRow.innerHTML = '<input type="text" class="beep-poll-option" data-beep-poll-option placeholder="Option ' + (optionCount + 1) + '" maxlength="100">';
newRow.innerHTML = '<input type="text" class="beep-poll-option-input" data-beep-poll-option placeholder="Option ' + (optionCount + 1) + '" maxlength="100">';
optionsContainer.appendChild(newRow);
updatePollButtons(compose);
}
@@ -876,7 +1003,7 @@
var pollSection = compose.querySelector('[data-beep-compose-poll]');
if (!pollSection) return;
var options = pollSection.querySelectorAll('[data-beep-poll-option]');
if (options.length <= 2) return; // Min 2 options
if (options.length <= 2) return;
var lastOption = options[options.length - 1];
if (lastOption) lastOption.parentElement.remove();
updatePollButtons(compose);
@@ -901,7 +1028,6 @@
var question = pollSection.querySelector('[data-beep-poll-question]');
if (question) question.value = '';
var options = pollSection.querySelectorAll('[data-beep-poll-option]');
// Reset to 2 options
if (options.length > 2) {
for (var i = options.length - 1; i >= 2; i--) {
if (options[i].parentElement) options[i].parentElement.remove();
@@ -959,7 +1085,6 @@
})
.then(handleResponse)
.then(function(data) {
// Refresh the beep post to show updated poll results
var postEl = btn.closest('[data-beep-post]');
if (postEl) {
var list = postEl.closest('[data-beep-list]');
@@ -971,75 +1096,31 @@
});
}
// Magic Login Form Handler
document.addEventListener('DOMContentLoaded', function() {
var magicForm = document.querySelector('[data-beep-magic-form]');
if (magicForm) {
magicForm.addEventListener('submit', function(e) {
e.preventDefault();
var emailInput = magicForm.querySelector('[name="email"]');
var submitBtn = magicForm.querySelector('[data-beep-magic-submit]');
var btnText = submitBtn.querySelector('.beep-magic-btn-text');
var loading = submitBtn.querySelector('.beep-magic-loading');
var successMsg = document.querySelector('[data-beep-magic-success]');
var errorMsg = document.querySelector('[data-beep-magic-error]');
var email = emailInput.value;
// Hide previous messages
if (successMsg) successMsg.hidden = true;
if (errorMsg) errorMsg.hidden = true;
// Show loading
btnText.hidden = true;
loading.hidden = false;
submitBtn.disabled = true;
// Get REST URL and nonce from page
var restUrl = typeof BeepConfig !== 'undefined' ? BeepConfig.restUrl : '/wp-json/';
var nonce = typeof BeepConfig !== 'undefined' ? BeepConfig.nonce : '';
fetch(restUrl + 'magic-link', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': nonce
},
body: JSON.stringify({ email: email })
})
.then(function(response) {
return response.json();
})
.then(function(data) {
if (data.success || (data.message && data.message.includes('sent'))) {
// Success
if (successMsg) {
successMsg.hidden = false;
magicForm.hidden = true;
}
} else {
// Error
var errorText = data.message || 'Something went wrong. Please try again.';
if (errorMsg) {
errorMsg.textContent = errorText;
errorMsg.hidden = false;
}
}
})
.catch(function(err) {
console.error('Magic login error:', err);
if (errorMsg) {
errorMsg.textContent = 'Something went wrong. Please try again.';
errorMsg.hidden = false;
}
})
.finally(function() {
btnText.hidden = false;
loading.hidden = true;
submitBtn.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 = '<img src="' + escapeHtml(src) + '" alt="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();
}
})();