const { formatStr } = require("../src/renderer.cjs"); function zoomimg(e) { const { src } = e.target, overlayEl = document.querySelector('.overlay'), zoomContainer = document.querySelector('#imgzoom'); zoomContainer.querySelector('video').style.display = 'none'; zoomContainer.querySelector('img').style.display = ''; zoomContainer.querySelector('img').src = src; overlayEl.classList.toggle('hidden'); zoomContainer.classList.toggle('zoomed'); zoomContainer.classList.toggle('hidden'); document.querySelector('.profile-container').classList.toggle('.noscroll'); } function zoomvid(e) { const { src } = e.target; if (src.endsWith('/assets/video-loading.mp4')) return; const zoomContainer = document.querySelector('#imgzoom'); if (zoomContainer.contains(e.target)) return; e.target.pause(); zoomContainer.querySelector('video').style.display = ''; zoomContainer.querySelector('img').style.display = 'none'; zoomContainer.querySelector('video').src = src; zoomContainer.classList.toggle('zoomed'); document.querySelector('.overlay').classList.toggle('hidden'); document.querySelector('.overlay').classList.toggle('show'); document.querySelector('.profile-container').classList.toggle('.noscroll'); const isZoomed = zoomContainer.classList.contains('zoomed'); e.target.parentElement.querySelector('.controls').style.display = (isZoomed) ? 'none !important' : '' } // video stuff function createVideoEl() { // create main video container const videoContainer = document.createElement('div'); videoContainer.classList.add('video-container'); videoContainer.classList.add('zoom-video'); // create video element const videoElement = document.createElement('video'); videoElement.classList.add('video-element'); videoElement.src = ''; // specify the video source here videoElement.controls = false; // disable default controls videoElement.setAttribute('playsinline', ''); // ensures mobile compatibility for inline playback // create controls container const controls = document.createElement('div'); controls.classList.add('controls'); // create play/pause button const playPauseButton = document.createElement('button'); playPauseButton.classList.add('play-pause'); playPauseButton.textContent = ''; // create fullscreen button const fullscreenButton = document.createElement('button'); fullscreenButton.classList.add('fullscreen'); fullscreenButton.textContent = '⛶'; // create progress bar const progressBar = document.createElement('input'); progressBar.classList.add('progress-bar'); progressBar.type = 'range'; progressBar.min = '0'; progressBar.value = '0'; progressBar.step = '0.1'; // create volume button with slider container const volumeContainer = document.createElement('div'); volumeContainer.classList.add('volume-container'); const volumeButton = document.createElement('button'); volumeButton.classList.add('volume-button'); volumeButton.textContent = '🔊'; // append volume control to volume container volumeContainer.appendChild(volumeButton); // append controls to controls container controls.appendChild(playPauseButton); controls.appendChild(progressBar); controls.appendChild(volumeContainer); // add volume container to the interface controls.appendChild(fullscreenButton); controls.style.display = 'none'; // append video and controls to video container videoContainer.appendChild(videoElement); videoContainer.appendChild(controls); // JavaScript functionality for controls // update play/pause functionality playPauseButton.addEventListener('click', () => { videoContainer.querySelector('.play-icon')?.remove(); if (videoElement.paused || videoElement.ended) { videoElement.play(); playPauseButton.classList.remove('play'); playPauseButton.classList.add('pause'); } else { videoElement.pause(); playPauseButton.classList.remove('pause'); playPauseButton.classList.add('play'); } }); fullscreenButton.onclick = (_) => videoElement.click(); // set initial icon state to "play" playPauseButton.classList.add('play'); // update progress bar as video plays videoElement.addEventListener('timeupdate', () => { progressBar.value = (videoElement.currentTime / videoElement.duration) * 100; }); videoElement.addEventListener('pause', () => { playPauseButton.classList.remove('pause'); playPauseButton.classList.add('play'); }); videoElement.addEventListener('play', () => { playPauseButton.classList.remove('play'); playPauseButton.classList.add('pause'); }); // allow user to skip within video by dragging progress bar progressBar.addEventListener('input', () => { videoElement.currentTime = (progressBar.value / 100) * videoElement.duration; }); // adjust video volume with volume control let preVol; volumeButton.addEventListener('click', () => { if (videoElement.volume) { preVol = videoElement.volume; videoElement.volume = '0'; volumeButton.textContent = '🔇'; } else { videoElement.volume = preVol || '1'; volumeButton.textContent = '🔊'; } }); return videoContainer; } function sendSuccess(dmItem, failed = false) { const image = dmItem.querySelector('img'); if (image) { // Create the checkmark div const checkmark = document.createElement('div'); checkmark.className = (failed) ? 'crossmark' : 'checkmark'; checkmark.style.marginRight = '10px'; // Replace the image with the checkmark image.replaceWith(checkmark); setTimeout(() => { document.querySelector('#dmPopup').querySelector('.close-btn').click(); checkmark.replaceWith(image); checkmark.remove(); }, 1500); } else console.log('no pfp found!'); dmItem.classList.add((failed) ? 'error' : 'success'); } function renderDMs(posturi, follows, ipcRenderer) { const dmList = document.getElementById('dmList'); dmList.innerHTML = ''; follows.forEach(follow => { const dmItem = document.createElement('div'); dmItem.className = 'dm-item' + (follow.dm ? '' : ' disabled'); dmItem.innerHTML = ` ${follow.displayName}
${follow.displayName || follow.handle}
@${follow.handle}
`; dmItem.addEventListener('click', async (e) => { e.preventDefault(); if (!follow.dm) return alert('not allowed!'); const r = await ipcRenderer.invoke('send-post', posturi, follow.handle, follow.dm.id); if (!r) sendSuccess(dmItem, true); else sendSuccess(dmItem); }); dmList.appendChild(dmItem); }); } function createButtonEls(card, ipcRenderer, idkey = 'bskyid', containerid = 'cards') { // Add this to your function after creating the main card content const buttonContainer = document.createElement('div'); buttonContainer.classList.add('button-container'); // Like button const likeButton = document.createElement('button'); likeButton.classList.add('action-button', 'like-button'); likeButton.innerHTML = ``; likeButton.title = 'Like'; // Like button const repostButton = document.createElement('button'); repostButton.className = 'action-button'; repostButton.style.color = card.dataset.reposturi ? '#10c200' : 'grey'; repostButton.innerHTML = ``; repostButton.title = 'Repost'; // Delete button const deleteButton = document.createElement('button'); deleteButton.classList.add('action-button', 'delete-button'); deleteButton.innerHTML = ''; deleteButton.title = 'Delete'; // Send via DM button const sendDMButton = document.createElement('button'); sendDMButton.classList.add('action-button', 'dm-button'); sendDMButton.innerHTML = ''; sendDMButton.title = 'Send via DM'; // Copy link button const copyLinkButton = document.createElement('button'); copyLinkButton.classList.add('action-button', 'copy-link-button'); copyLinkButton.innerHTML = ''; copyLinkButton.title = 'Copy Link'; // Pin/Unpin toggle button const pinButton = document.createElement('button'); pinButton.classList.add('action-button', 'pin-button'); pinButton.innerHTML = ''; pinButton.title = 'Pin/Unpin'; // Three-dot menu button to toggle dropdown const menuButton = document.createElement('button'); menuButton.classList.add('action-button', 'menu-button'); menuButton.innerHTML = ''; menuButton.title = 'More options'; // Dropdown menu container for collapsible options const dropdownMenu = document.createElement('div'); dropdownMenu.classList.add('dropdown-menu'); dropdownMenu.classList.add('hidden'); // initially hidden dropdownMenu.append(pinButton, copyLinkButton) // Append buttons to the container buttonContainer.appendChild(likeButton); buttonContainer.appendChild(repostButton); buttonContainer.appendChild(deleteButton); buttonContainer.appendChild(sendDMButton); buttonContainer.appendChild(menuButton); buttonContainer.appendChild(dropdownMenu); // Like button functionality likeButton.addEventListener('click', async () => { const lurl = card.dataset.likeuri, r = await ipcRenderer.invoke('post-action', 'like', card.dataset[`${idkey}`], lurl); // if there is a url then it should return nothing if (!r && !lurl) return alert("ERROR!"); else if (lurl) delete card.dataset.likeuri; else if (r) card.dataset.likeuri = r.uri likeButton.innerHTML = `` }); // Delete button functionality deleteButton.addEventListener('click', async () => { if (confirm('Are you sure you want to delete this post?')) { const r = await ipcRenderer.invoke('post-action', 'delete', card.dataset[`${idkey}`]); if (!r) return alert("ERROR!"); card.remove(); // alert('Post deleted.'); } }); // Send DM functionality sendDMButton.addEventListener('click', async () => { const r = await ipcRenderer.invoke('get-connections'); document.getElementById('dmPopup').classList.add('active'); renderDMs(card.dataset[`${idkey}`], r.follows, ipcRenderer); }); // Copy link functionality copyLinkButton.addEventListener('click', async () => { const r = await ipcRenderer.invoke('post-action', 'link', card.dataset[`${idkey}`]); if (!r) return alert("ERROR!"); navigator.clipboard.writeText(r).then(() => { alert('Link copied to clipboard!'); }); }); // Repost functionality repostButton.addEventListener('click', async () => { const r = await ipcRenderer.invoke('post-action', 'repost', card.dataset[`${idkey}`], card.dataset.reposturi); if (!r) alert('ERROR!'); else if (card.dataset.reposturi) { // TODO: move the card back to it's original position or refresh the page? if (!card.dataset.ismypost) card.remove(); delete card.dataset.reposturi; repostButton.style.color = 'grey'; } else { const pinnedEl = document.querySelector(`#${containerid}container`).querySelector('.pinned-card'); if (pinnedEl) pinnedEl.after(card); else document.querySelector(`#${containerid}container`).prepend(card); card.dataset.reposturi = r; repostButton.style.color = '#10c200'; } }); // Pin/Unpin functionality let isPinned = false; pinButton.addEventListener('click', async () => { return alert("TODO"); // const r = await ipcRenderer.invoke('post-action', 'pin', card.dataset.bskyid); // return alert(r); isPinned = !isPinned; pinButton.title = isPinned ? 'Unpin' : 'Pin'; pinButton.classList.toggle('pinned', isPinned); alert(isPinned ? 'Pinned!' : 'Unpinned!'); // Add logic for pinning/unpinning the post }); menuButton.addEventListener('click', (e) => { e.stopPropagation(); // prevent event bubbling dropdownMenu.classList.toggle('hidden'); // show/hide menu }); // Hide dropdown menu when clicking outside document.addEventListener('click', (e) => { if (!buttonContainer.contains(e.target)) { dropdownMenu.classList.add('hidden'); // hide menu } }); return buttonContainer; } function setupWorker(name = 'bktemp') { if (typeof (Worker) === "undefined") return console.error('workers not supported!\nswitching to manual...'); const w = new Worker("../JS/worker.js", { name }); w.postMessage('PING'); w.onmessage = function (event) { console.log(event.data); }; function stopWorker() { w.terminate(); w = undefined; } } const statArr = [0, 0]; /** * @param {*} post * @param {*} a * @param {{posturi: string, likeuri: string}[]} likes * @param {*} ipcRenderer * @param {*} container */ function renderPostSingle(post, a, likes, ipcRenderer, container, idkey = 'bskyid', containerid) { /** @type {Element} */ let card; const cardId = (post?.reply?.root?.uri || post.post.uri)?.trim(); // if the card exists, use it; otherwise, create a new card if (a.has(cardId)) { card = a.get(cardId); statArr[0]++; } else { card = document.createElement('div'); card.classList.add('post-card'); card.dataset[`${idkey}`] = cardId; card.onclick = (e) => { if (e.target !== card && !e.target.classList.contains('post-text')) return; window.location.href = `../HTML/post.html?id=${encodeURI(card.dataset[`${idkey}`])}`; } statArr[1]++; } // handle repost reason if it hasn't been added to this card if (post.reason && post.reason.by && !card.querySelector('.repost-section')) { const aname = post.reason.by.displayName || formatStr(post.reason.by.handle); const repostSection = document.createElement('div'); repostSection.classList.add('repost-section'); const repostAvatar = document.createElement('img'); repostAvatar.src = post.reason.by.avatar; repostAvatar.alt = `${aname}'s avatar`; repostAvatar.classList.add('card-avatar', 'repost-avatar'); repostAvatar.loading = 'lazy'; const repostInfo = document.createElement('div'); repostInfo.classList.add('repost-info'); const repostName = document.createElement('h2'); repostName.innerHTML = aname; repostName.classList.add('author-name'); const repostHandle = document.createElement('p'); repostHandle.innerHTML = `${formatStr('@' + post.reason.by.handle)} reposted`; repostHandle.classList.add('author-handle'); repostInfo.appendChild(repostName); repostInfo.appendChild(repostHandle); repostSection.appendChild(repostAvatar); repostSection.appendChild(repostInfo); card.dataset.ismypost = post.post.author.did === post.reason.by.did; card.dataset.reposturi = post.post.viewer.repost; card.prepend(repostSection); } // handle reply if it hasn't been added to this card if (post.reply && post.reply.root && !card.querySelector('.original-post')) { const originalPost = document.createElement('div'); originalPost.classList.add('original-post'); const repaname = post.reply.root.author.displayName || formatStr(post.reply.root.author?.handle); // add the original author and text const originalAuthor = document.createElement('p'); originalAuthor.innerHTML = `Replying to ${repaname}`; originalAuthor.classList.add('original-author'); originalPost.appendChild(originalAuthor); const originalText = document.createElement('p'); originalText.innerHTML = formatStr(post.reply.root.record.text) || 'Image reply'; originalText.classList.add('original-text'); originalPost.appendChild(originalText); // add the original image if present if (post.reply.root.embed?.images?.length > 0) { const originalImage = document.createElement('img'); originalImage.loading = 'lazy'; originalImage.src = post.reply.root.embed.images[0].thumb; originalImage.alt = post.reply.root.embed.images[0].alt || 'Original post image'; originalImage.classList.add('original-image'); originalPost.appendChild(originalImage); } card.appendChild(originalPost); // add the section for "what I was replying to" if grandparentAuthor exists if (post.reply.grandparentAuthor) { const parentPost = document.createElement('div'); parentPost.classList.add('parent-post'); const spacingEl = document.createElement('p'); spacingEl.className = 'convcont'; spacingEl.textContent = '.....'; const parentAuthor = document.createElement('p'); const paraname = post.reply.parent.author.displayName || formatStr(post.reply.parent.author.handle); parentAuthor.innerHTML = `${paraname}`; parentAuthor.classList.add('original-author'); parentPost.append(spacingEl, parentAuthor); const parentText = document.createElement('p'); parentText.innerHTML = formatStr(post.reply.parent.record.text) || 'Image reply'; parentText.classList.add('original-text'); parentPost.appendChild(parentText); if (post.reply.parent.embed && post.reply.parent.embed.images && post.reply.parent.embed.images.length > 0) { const parentImage = document.createElement('img'); parentImage.loading = 'lazy'; parentImage.src = post.reply.parent.embed.images[0].thumb; parentImage.alt = post.reply.parent.embed.images[0].alt || 'Parent post image'; parentImage.classList.add('original-image'); parentPost.appendChild(parentImage); } card.appendChild(parentPost); } } // author info section for the main post if (!card.querySelector('.author-section')) { const aname = post.post.author.displayName || formatStr(post.post.author.handle); const authorSection = document.createElement('div'); authorSection.classList.add('author-section'); const avatar = document.createElement('img'); avatar.src = post.post.author.avatar; avatar.alt = `${aname}'s avatar`; avatar.classList.add('card-avatar'); avatar.loading = 'lazy'; const authorInfo = document.createElement('div'); authorInfo.classList.add('author-info'); const displayName = document.createElement('h2'); displayName.innerHTML = aname; displayName.classList.add('author-name'); const handle = document.createElement('p'); handle.innerHTML = `${formatStr('@' + post.post.author.handle)}`; handle.classList.add('author-handle'); authorInfo.appendChild(displayName); authorInfo.appendChild(handle); authorSection.appendChild(avatar); authorSection.appendChild(authorInfo); card.appendChild(authorSection); } // main post text content if (!card.querySelector('.post-text')) { const textContent = document.createElement('p'); textContent.innerHTML = formatStr(post.post.record.text); textContent.classList.add('post-text'); card.appendChild(textContent); } // handle video embed if (post.post.embed && post.post.embed.$type === 'app.bsky.embed.video#view' && !card.querySelector('.post-video')) { const videoContainer = createVideoEl(), videoPlayer = videoContainer.querySelector('video'); videoPlayer.classList.add('post-video'); videoPlayer.width = post.post.embed.aspectRatio?.width; videoPlayer.height = post.post.embed.aspectRatio?.height; videoContainer.appendChild(videoPlayer); card.appendChild(videoContainer); videoPlayer.src = '../assets/video-loading.mp4'; videoPlayer.dataset.src = post.post.embed.playlist; videoPlayer.poster = post.post.embed.thumbnail; } // main post image (if it exists and hasn't been added) if (post.post.embed && post.post.embed.images && post.post.embed.images.length > 0 && !card.querySelector('.post-image')) { const repeatimg = card.querySelector('.original-image')?.src === post.post.embed.images[0].thumb; if (!repeatimg) { const imageContainer = document.createElement('div'); imageContainer.classList.add('image-container'); const postImage = document.createElement('img'); postImage.src = post.post.embed.images[0].thumb; postImage.alt = post.post.embed.images[0].alt || 'Embedded image'; postImage.classList.add('post-image'); postImage.loading = 'lazy'; imageContainer.appendChild(postImage); card.appendChild(imageContainer); } } if (post.post.embed && post.post.embed.$type === 'app.bsky.embed.external#view') { const embedContainer = document.createElement('div'), embed = post.post.embed.external; embedContainer.classList.add('external-embed-container'); // thumbnail (if exists) if (embed.thumb) { const thumbnail = document.createElement('img'); thumbnail.classList.add('external-embed-thumb'); if (embed.uri.match(/\.gif(?:\?.*|$)/)) thumbnail.src = embed.uri else thumbnail.src = embed.thumb; thumbnail.alt = `${embed.title} thumbnail`; embedContainer.appendChild(thumbnail); } // this is an image if (embed.description.startsWith('ALT:') && embed.title === embed.description.substring(4).trim()) { const thumbnail = embedContainer.querySelector('.external-embed-thumb'); thumbnail.alt = embed.description.substring(4).trim(); } else { // title const titleElement = document.createElement('h3'); titleElement.classList.add('external-embed-title'); titleElement.textContent = embed.title; // description const descriptionElement = document.createElement('p'); descriptionElement.classList.add('external-embed-description'); descriptionElement.textContent = embed.description; // link const linkElement = document.createElement('a'); linkElement.classList.add('external-embed-link'); linkElement.href = embed.uri; linkElement.target = '_blank'; linkElement.rel = 'noopener noreferrer'; // improves security (apparently) linkElement.textContent = 'Visit'; embedContainer.appendChild(titleElement); embedContainer.appendChild(descriptionElement); embedContainer.appendChild(linkElement); } card.appendChild(embedContainer); } // interaction counts if (!card.querySelector('.interaction-counts')) { const counts = document.createElement('div'); counts.classList.add('interaction-counts'); counts.textContent = `💬 ${post.post.replyCount} 🔄 ${post.post.repostCount} ❤️ ${post.post.likeCount}`; card.appendChild(counts); } // add the card to the container if it's a new card if (!a.has(cardId)) { const lurl = likes?.find(o => (o.posturi === cardId)); if (lurl) card.dataset.likeuri = lurl.likeuri; card.appendChild(createButtonEls(card, ipcRenderer, idkey, containerid)); container.appendChild(card); a.set(cardId, card); } } /** * @param {*} posts * @param {*} likes * @param {*} pinnedPost * @param {*} ipcRenderer */ module.exports = function renderPosts(posts, likes, pinnedPost, ipcRenderer, idkey = 'bskyid', containerid = 'posts') { let container; /** @type {Map} */ let a; if (document.querySelector(`#${containerid}container`)) { container = document.querySelector(`#${containerid}container`); a = new Map(Array.from(container.querySelectorAll('.post-card')).map(o => ([o.dataset[`${idkey}`], o]))); } else { container = document.createElement('div'); container.classList.add('cards-container'); container.id = `${containerid}container`; a = new Map(); } statArr[0] = 0; statArr[1] = 0; for (const post of posts) { try { renderPostSingle( post, a, likes?.map(o => ({ posturi: o.post.uri, likeuri: o.post.viewer?.like })), ipcRenderer, container, idkey, containerid ); } catch (err) { console.error(err); } } console.info(`added ${statArr[1]} and updated ${statArr[0]} posts in "${containerid}"!`); // const postids = Array.from(document.querySelectorAll(`[data-${idkey}]`)).map(o => o.dataset[`${idkey} `]); // console.log('duplicate ids', postids); // "force" garbage collection // delete a; a.clear(); a = null; if (pinnedPost) { console.log("PINNED", posts.find(p => p.post.uri === pinnedPost.uri)); const pinnedcard = container.querySelector(`[data-${idkey}="${pinnedPost.uri}"]`); if (pinnedcard) { const pinnedLabel = document.createElement('div'); pinnedLabel.classList.add('pinned-label'); pinnedLabel.textContent = '📌 Pinned'; // optionally add an emoji/icon pinnedcard?.prepend(pinnedLabel); // place it at the top of the pinned card pinnedcard?.classList.add('pinned-card'); container.prepend(pinnedcard); // move it to be first } } // append container to the document body or a specific section const postdiv = document.getElementById(containerid); postdiv.querySelector('.placeholder')?.remove(); if (!document.querySelector(`#${containerid}container`)) { postdiv.appendChild(container); document.addEventListener('click', (e) => { if (e.target.tagName === "VIDEO") zoomvid(e); else if (e.target.tagName === "IMG" || e.target.classList.contains('overlay')) zoomimg(e); }); } }