mirror of
https://github.com/ION606/bluesky-client.git
synced 2026-05-14 21:26:54 +00:00
initial code commit
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
const { formatStr } = require('../src/renderer.cjs');
|
||||
|
||||
|
||||
/**
|
||||
* @param {Profile} profileData
|
||||
*/
|
||||
function populateProfile(profileData) {
|
||||
const bannerImg = document.querySelector('#banner-img');
|
||||
const avatarImg = document.querySelector('#avatar-img');
|
||||
const handleElement = document.querySelector('#handle');
|
||||
const descriptionElement = document.querySelector('#description');
|
||||
const followersCountElement = document.querySelector('#followersCount');
|
||||
const followsCountElement = document.querySelector('#followsCount');
|
||||
const postsCountElement = document.querySelector('#postsCount');
|
||||
|
||||
console.info(profileData);
|
||||
|
||||
// set element content
|
||||
bannerImg.src = profileData.banner;
|
||||
avatarImg.src = profileData.avatar;
|
||||
handleElement.textContent = profileData.handle;
|
||||
descriptionElement.innerHTML = formatStr(profileData.description);
|
||||
followersCountElement.textContent = profileData.followersCount;
|
||||
followsCountElement.textContent = profileData.followsCount;
|
||||
postsCountElement.textContent = profileData.postsCount;
|
||||
}
|
||||
|
||||
class Profile {
|
||||
constructor(data) {
|
||||
if (!data) throw "DATA NOT FOUND!";
|
||||
this.did = data.did,
|
||||
this.handle = data.handle,
|
||||
this.displayName = data.displayName,
|
||||
this.avatar = data.avatar,
|
||||
this.associated = data.associated,
|
||||
this.viewer = data.viewer,
|
||||
this.labels = data.labels,
|
||||
this.createdAt = data.createdAt,
|
||||
this.description = formatStr(data.description),
|
||||
this.indexedAt = data.indexedAt,
|
||||
this.banner = data.banner,
|
||||
this.followersCount = data.followersCount,
|
||||
this.followsCount = data.followsCount,
|
||||
this.postsCount = data.postsCount,
|
||||
this.pinnedPost = data.pinnedPost
|
||||
}
|
||||
|
||||
getProfileSummary() {
|
||||
return `${this.handle} (${this.did}): ${this.description}`;
|
||||
}
|
||||
|
||||
getPinnedPostUri() {
|
||||
return this.pinnedPost.uri;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = { Profile, populateProfile }
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
// set up the overlay and form toggling
|
||||
async function setup() {
|
||||
const composeButton = document.querySelector('#composebtn');
|
||||
const newPostForm = document.querySelector('#new-post-form');
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'overlay';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
function openForm() {
|
||||
newPostForm.classList.add("show");
|
||||
overlay.classList.add("show");
|
||||
}
|
||||
|
||||
composeButton.addEventListener('click', openForm);
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('DOMContentLoaded', setup);
|
||||
|
||||
async function renderCompose(ipcRenderer) {
|
||||
const overlay = document.querySelector("#overlay"),
|
||||
postForm = document.querySelector("#postForm"),
|
||||
statusMessage = document.querySelector("#statusMessage"),
|
||||
closeButton = document.querySelector("#closeButton");
|
||||
|
||||
const newPostForm = document.querySelector("#new-post-form");
|
||||
|
||||
// button and hidden input elements
|
||||
const fileButton = document.querySelector("#fileButton"),
|
||||
gifButton = document.querySelector("#gifButton"),
|
||||
audioButton = document.querySelector("#audioButton"),
|
||||
postFile = document.querySelector("#postFile"),
|
||||
postGif = document.querySelector("#postGif"),
|
||||
postAudio = document.querySelector("#postAudio"),
|
||||
postEmbed = document.getElementById('showEmbedButton'),
|
||||
embedWidget = document.querySelector('#embedWidget');
|
||||
|
||||
|
||||
function closeForm() {
|
||||
newPostForm.classList.remove("show");
|
||||
overlay.classList.remove("show");
|
||||
}
|
||||
|
||||
closeButton.addEventListener("click", closeForm);
|
||||
overlay.addEventListener("click", closeForm);
|
||||
|
||||
// trigger corresponding inputs when button is clicked
|
||||
fileButton.addEventListener("click", () => postFile.click());
|
||||
gifButton.addEventListener("click", () => {
|
||||
postGif.style.display = 'block';
|
||||
postGif.focus();
|
||||
});
|
||||
audioButton.addEventListener("click", () => postAudio.click());
|
||||
|
||||
postEmbed.addEventListener('click', () => {
|
||||
embedWidget.style.display = embedWidget.style.display === 'none' || embedWidget.style.display === '' ? 'block' : 'none';
|
||||
});
|
||||
|
||||
postForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const postContent = document.querySelector('#postContent').value.trim(),
|
||||
gifUrl = document.querySelector('#postGif').value.trim(),
|
||||
statusMessage = document.querySelector('#statusMessage'),
|
||||
embedData = Array.from(embedWidget.querySelectorAll('input')).map(o => {
|
||||
const id = o.id.replace('embed', '')
|
||||
if (o.type === 'file') return [id, o.files[0].name];
|
||||
else return [id, o.value];
|
||||
});
|
||||
|
||||
// files from hidden inputs
|
||||
const file = postFile.files[0];
|
||||
const audio = postAudio.files[0];
|
||||
|
||||
statusMessage.textContent = "Posting...";
|
||||
|
||||
try {
|
||||
ipcRenderer.invoke('new-post', JSON.stringify({ text: postContent, embed: Object.fromEntries(embedData) }));
|
||||
statusMessage.textContent = "Posted successfully!";
|
||||
postForm.reset();
|
||||
postGif.style.display = 'none'; // hide gif input after submission
|
||||
} catch (error) {
|
||||
console.error('Error posting:', error);
|
||||
statusMessage.textContent = 'Failed to post. Please try again.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function displayUploadStatus(files) {
|
||||
const statusContainer = document.querySelector('#uploadStatus'); // element to display results
|
||||
|
||||
// Clear previous status
|
||||
statusContainer.innerHTML = '';
|
||||
|
||||
// Iterate through files and display status
|
||||
for (const [fileName, fileStatus] of Object.entries(files)) {
|
||||
const statusMessage = document.createElement('p');
|
||||
|
||||
if (fileStatus) {
|
||||
// Upload succeeded
|
||||
statusMessage.textContent = `File "${fileName}" uploaded successfully!`;
|
||||
statusMessage.style.color = 'green';
|
||||
} else {
|
||||
// Upload failed
|
||||
statusMessage.textContent = `File "${fileName}" failed to upload!`;
|
||||
statusMessage.style.color = 'red';
|
||||
}
|
||||
|
||||
// Append status message to the container
|
||||
statusContainer.appendChild(statusMessage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = { renderCompose, displayUploadStatus }
|
||||
@@ -0,0 +1,42 @@
|
||||
async function renderLikes(data, ipcRenderer) {
|
||||
let container;
|
||||
/** @type {Map<String, Element>} */
|
||||
let a;
|
||||
if (document.querySelector('#likescontainer')) {
|
||||
container = document.querySelector('#likescontainer');
|
||||
a = new Map(Array.from(container.querySelectorAll('.post-card')).map(o => ([o.dataset.bskyid, o])));
|
||||
}
|
||||
else {
|
||||
container = document.createElement('div');
|
||||
container.classList.add('cards-container');
|
||||
container.id = 'likescontainer';
|
||||
a = new Map();
|
||||
}
|
||||
|
||||
// setupWorker();
|
||||
// renderLikesFeed(posts.map(p => (p?.reply?.root?.uri || p.post.uri)?.trim()), likes, ipcRenderer);
|
||||
|
||||
for (const post of data) renderPostSingle(post, a, likes.map(o => ({ posturi: o.post.uri, likeuri: o.post.viewer?.like })), ipcRenderer, container);
|
||||
|
||||
const postids = Array.from(document.querySelectorAll('[data-like-bskyid]')).map(o => o.dataset.bskyid);
|
||||
console.log(postids);
|
||||
|
||||
// "force" garbage collection
|
||||
// delete a;
|
||||
a.clear();
|
||||
a = null;
|
||||
|
||||
// append container to the document body or a specific section
|
||||
const postdiv = document.querySelector('#posts');
|
||||
postdiv.querySelector('.placeholder')?.remove();
|
||||
if (!document.querySelector('#likescontainer')) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = renderLikes;
|
||||
+638
@@ -0,0 +1,638 @@
|
||||
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;
|
||||
zoomContainer.classList.toggle('zoomed');
|
||||
overlayEl.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('.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 = `
|
||||
<img src="${follow.avatar}" alt="${follow.displayName}">
|
||||
<div class="info">
|
||||
<div class="name">${follow.displayName || follow.handle}</div>
|
||||
<div class="handle">@${follow.handle}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = `<i class="fa-${card.dataset.likeuri ? 'solid' : 'regular'} fa-heart"></i>`;
|
||||
likeButton.title = 'Like';
|
||||
|
||||
// Like button
|
||||
const repostButton = document.createElement('button');
|
||||
repostButton.className = 'action-button';
|
||||
repostButton.style.color = card.dataset.reposturi ? '#10c200' : 'grey';
|
||||
repostButton.innerHTML = `<i class="fa-solid fa-retweet"></i>`;
|
||||
repostButton.title = 'Repost';
|
||||
|
||||
// Delete button
|
||||
const deleteButton = document.createElement('button');
|
||||
deleteButton.classList.add('action-button', 'delete-button');
|
||||
deleteButton.innerHTML = '<i class="fas fa-trash-alt"></i>';
|
||||
deleteButton.title = 'Delete';
|
||||
|
||||
// Send via DM button
|
||||
const sendDMButton = document.createElement('button');
|
||||
sendDMButton.classList.add('action-button', 'dm-button');
|
||||
sendDMButton.innerHTML = '<i class="fas fa-paper-plane"></i>';
|
||||
sendDMButton.title = 'Send via DM';
|
||||
|
||||
// Copy link button
|
||||
const copyLinkButton = document.createElement('button');
|
||||
copyLinkButton.classList.add('action-button', 'copy-link-button');
|
||||
copyLinkButton.innerHTML = '<i class="fas fa-link"></i>';
|
||||
copyLinkButton.title = 'Copy Link';
|
||||
|
||||
// Pin/Unpin toggle button
|
||||
const pinButton = document.createElement('button');
|
||||
pinButton.classList.add('action-button', 'pin-button');
|
||||
pinButton.innerHTML = '<i class="fas fa-thumbtack"></i>';
|
||||
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 = '<i class="fas fa-ellipsis-h"></i>';
|
||||
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 = `<i class="fa-${lurl ? 'regular' : 'solid'} fa-heart"></i>`
|
||||
});
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
else {
|
||||
card = document.createElement('div');
|
||||
card.classList.add('post-card');
|
||||
card.dataset[`${idkey}`] = cardId;
|
||||
}
|
||||
|
||||
// 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 present and not added yet)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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') {
|
||||
console.log(`${idkey}, ${containerid}container, ${posts.length}`);
|
||||
let container;
|
||||
/** @type {Map<String, Element>} */
|
||||
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;
|
||||
a = new Map();
|
||||
}
|
||||
|
||||
// setupWorker();
|
||||
// renderLikesFeed(posts.map(p => (p?.reply?.root?.uri || p.post.uri)?.trim()), likes, ipcRenderer);
|
||||
|
||||
for (const post of posts) renderPostSingle(post, a, likes.map(o => ({ posturi: o.post.uri, likeuri: o.post.viewer?.like })), ipcRenderer, container, idkey, containerid);
|
||||
|
||||
const postids = Array.from(document.querySelectorAll(`[data-${idkey}]`)).map(o => o.dataset[`${idkey} `]);
|
||||
console.log(postids);
|
||||
|
||||
// "force" garbage collection
|
||||
// delete a;
|
||||
a.clear();
|
||||
a = null;
|
||||
|
||||
if (pinnedPost) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// function to create a single reply card, with an optional parent post shown above it
|
||||
function createReplyCard(replyData) {
|
||||
const replyCardContainer = document.createElement('div');
|
||||
replyCardContainer.classList.add('reply-card-container');
|
||||
|
||||
// add parent post if exists, along with dots if it's not the root
|
||||
if (replyData.reply.parent) {
|
||||
const parentPost = createPostPreview(replyData.reply.parent, true);
|
||||
replyCardContainer.appendChild(parentPost);
|
||||
|
||||
// add dots if there's a grandparent (to indicate more context above)
|
||||
if (replyData.reply.grandparentAuthor) {
|
||||
const dots = document.createElement('p');
|
||||
dots.classList.add('convcont');
|
||||
dots.textContent = '.....';
|
||||
replyCardContainer.appendChild(dots);
|
||||
}
|
||||
}
|
||||
|
||||
// main reply card
|
||||
const replyCard = document.createElement('div');
|
||||
replyCard.classList.add('reply-card');
|
||||
|
||||
// author section
|
||||
const authorSection = document.createElement('div');
|
||||
authorSection.classList.add('author-section');
|
||||
|
||||
const avatar = document.createElement('img');
|
||||
avatar.src = replyData.post.author.avatar;
|
||||
avatar.alt = `${replyData.post.author.handle}'s avatar`;
|
||||
avatar.classList.add('card-avatar');
|
||||
authorSection.appendChild(avatar);
|
||||
|
||||
const authorInfo = document.createElement('div');
|
||||
authorInfo.classList.add('author-info');
|
||||
const authorName = document.createElement('p');
|
||||
authorName.textContent = replyData.post.author.displayName || replyData.post.author.handle;
|
||||
authorName.classList.add('author-name');
|
||||
authorInfo.appendChild(authorName);
|
||||
|
||||
authorSection.appendChild(authorInfo);
|
||||
replyCard.appendChild(authorSection);
|
||||
|
||||
// reply text
|
||||
const replyText = document.createElement('p');
|
||||
replyText.textContent = replyData.post.record.text || 'Image reply';
|
||||
replyText.classList.add('reply-text');
|
||||
replyCard.appendChild(replyText);
|
||||
|
||||
// embedded media if available
|
||||
if (replyData.post.embed && replyData.post.embed.external) {
|
||||
const mediaContainer = document.createElement('div');
|
||||
mediaContainer.classList.add('media-container');
|
||||
|
||||
const mediaImage = document.createElement('img');
|
||||
mediaImage.src = replyData.post.embed.external.thumb;
|
||||
mediaImage.alt = replyData.post.embed.external.description;
|
||||
mediaImage.classList.add('media-image');
|
||||
mediaImage.loading = 'lazy';
|
||||
|
||||
mediaContainer.appendChild(mediaImage);
|
||||
replyCard.appendChild(mediaContainer);
|
||||
}
|
||||
|
||||
replyCardContainer.appendChild(replyCard);
|
||||
return replyCardContainer;
|
||||
}
|
||||
|
||||
// helper function to create a condensed preview of the parent post
|
||||
function createPostPreview(post, isParent = false) {
|
||||
const postPreview = document.createElement('div');
|
||||
postPreview.classList.add(isParent ? 'parent-post-preview' : 'grandparent-post-preview');
|
||||
|
||||
// author and content preview
|
||||
const author = document.createElement('p');
|
||||
author.textContent = `${post.author.displayName || post.author.handle}`;
|
||||
author.classList.add('original-author');
|
||||
postPreview.appendChild(author);
|
||||
|
||||
const content = document.createElement('p');
|
||||
content.textContent = post.record.text || 'Image reply';
|
||||
content.classList.add('original-text');
|
||||
postPreview.appendChild(content);
|
||||
|
||||
return postPreview;
|
||||
}
|
||||
|
||||
|
||||
module.exports = function renderReplies(replyObj) {
|
||||
const repliesContainer = document.querySelector('#replies');
|
||||
repliesContainer.querySelector('.placeholder')?.remove();
|
||||
|
||||
const { cursor, replies } = replyObj;
|
||||
|
||||
if (cursor) sessionStorage.setItem('repliescursor', cursor);
|
||||
else sessionStorage.removeItem('repliescursor');
|
||||
|
||||
replies.forEach(reply => {
|
||||
const replyCard = createReplyCard(reply);
|
||||
repliesContainer.appendChild(replyCard);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
document.addEventListener('scroll', () => bottomscrolled(document.querySelector('#posts')));
|
||||
async function bottomscrolled(targetDiv) {
|
||||
const scrollPosition = window.scrollY + window.innerHeight,
|
||||
pageHeight = document.documentElement.scrollHeight,
|
||||
atbottom = (scrollPosition >= pageHeight);
|
||||
|
||||
if (!atbottom) return;
|
||||
|
||||
window.electronAPI.getnewposts();
|
||||
}
|
||||
|
||||
// function to toggle the active section
|
||||
function showSection(sectionId) {
|
||||
// hide all sections
|
||||
document.querySelectorAll('.content').forEach((content) => {
|
||||
content.classList.remove('active');
|
||||
});
|
||||
|
||||
// remove active class from all buttons
|
||||
document.querySelectorAll('.tab-buttons button').forEach((button) => {
|
||||
button.classList.remove('active');
|
||||
});
|
||||
|
||||
// show the selected section and activate the button
|
||||
document.getElementById(sectionId).classList.add('active');
|
||||
document.getElementById(sectionId + 'Btn').classList.add('active');
|
||||
}
|
||||
|
||||
// function to handle layout selection
|
||||
function togglerelaxed(e) {
|
||||
e.preventDefault(); // prevent the default anchor behavior
|
||||
|
||||
const value = e.target.getAttribute('data-value');
|
||||
const container = document.querySelector(`#${sessionStorage.getItem('currenttab') || 'cardscontainer'}`);
|
||||
|
||||
console.log(value)
|
||||
|
||||
if (!container) return console.warn('container not found!');
|
||||
|
||||
if (value === 'compact') {
|
||||
container.classList.remove('cards-container-relaxed');
|
||||
container.classList.add('cards-container');
|
||||
} else {
|
||||
container.style.gridTemplateColumns = (value !== 'large') ? 'repeat(auto-fill, minmax(280px, 1fr))' : '';
|
||||
container.classList.remove('cards-container');
|
||||
container.classList.add('cards-container-relaxed');
|
||||
}
|
||||
|
||||
// update dropdown button text to reflect current selection
|
||||
document.querySelector('.dropdown-button').textContent = `Layout: ${value.charAt(0).toUpperCase() + value.slice(1)}`;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// event listener for dropdown items
|
||||
document.querySelectorAll('#layoutDropdown a').forEach(item => {
|
||||
item.addEventListener('click', togglerelaxed);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
// listen for messages from the main thread
|
||||
self.onmessage = function (event) {
|
||||
const data = event.data;
|
||||
|
||||
self.postMessage(document.querySelector('#posts')?.firstChild)
|
||||
|
||||
// send the result back to the main thread
|
||||
self.postMessage(result);
|
||||
};
|
||||
Reference in New Issue
Block a user