diff --git a/CSS/cards.css b/CSS/cards.css
index a901945..7a7802f 100644
--- a/CSS/cards.css
+++ b/CSS/cards.css
@@ -239,6 +239,7 @@
align-items: center;
justify-content: center;
transition: background-color 0.3s ease;
+ z-index: 9999;
}
.compose-button:hover {
@@ -317,4 +318,59 @@
padding: 8px;
text-align: left;
font-size: 0.9em;
-}
\ No newline at end of file
+}
+
+/* container for the external embed */
+.external-embed-container {
+ border: 2px solid #4e004e;
+ border-radius: 8px;
+ padding: 16px;
+ max-width: 400px;
+ margin: 16px auto;
+ background-color: #2e003e;
+ color: #e6e6ff;
+ font-family: Arial, sans-serif;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+ text-align: center;
+}
+
+/* thumbnail styling */
+.external-embed-thumb {
+ width: 100%;
+ max-width: 400px;
+ height: auto;
+ border-radius: 8px;
+ margin-bottom: 16px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+}
+
+/* title styling */
+.external-embed-title {
+ font-size: 1.5em;
+ margin: 0 0 8px 0;
+ color: #ffcc00;
+}
+
+/* description styling */
+.external-embed-description {
+ font-size: 1em;
+ margin: 0 0 16px 0;
+ color: #e6e6ff;
+}
+
+/* link styling */
+.external-embed-link {
+ display: inline-block;
+ padding: 8px 16px;
+ background-color: #4e004e;
+ color: #ffcc00;
+ text-decoration: none;
+ border-radius: 5px;
+ font-weight: bold;
+ transition: background-color 0.3s ease;
+}
+
+.external-embed-link:hover {
+ background-color: #ffcc00;
+ color: #4e004e;
+}
diff --git a/CSS/global.css b/CSS/global.css
index 0d04689..557ad69 100644
--- a/CSS/global.css
+++ b/CSS/global.css
@@ -38,7 +38,7 @@ img {
}
-#eof {
+.eof {
text-align: center;
}
diff --git a/HTML/index.html b/HTML/index.html
index 9b0fb28..38bb144 100644
--- a/HTML/index.html
+++ b/HTML/index.html
@@ -63,14 +63,7 @@
nothing here...
-
+
diff --git a/JS/likes.cjs b/JS/likes.cjs
deleted file mode 100644
index 73b974f..0000000
--- a/JS/likes.cjs
+++ /dev/null
@@ -1,42 +0,0 @@
-async function renderLikes(data, ipcRenderer) {
- let container;
- /** @type {Map
} */
- 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;
\ No newline at end of file
diff --git a/JS/posts.cjs b/JS/posts.cjs
index 8e8cebe..1aef757 100644
--- a/JS/posts.cjs
+++ b/JS/posts.cjs
@@ -559,6 +559,47 @@ function renderPostSingle(post, a, likes, ipcRenderer, container, idkey = 'bskyi
}
}
+ 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');
+ thumbnail.src = embed.thumb;
+ thumbnail.alt = `${embed.title} thumbnail`;
+ embedContainer.appendChild(thumbnail);
+ }
+
+ // 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'; // opens in a new tab
+ linkElement.rel = 'noopener noreferrer'; // improves security
+ linkElement.textContent = 'Visit';
+
+ // append elements to the container
+ embedContainer.appendChild(titleElement);
+ embedContainer.appendChild(descriptionElement);
+ embedContainer.appendChild(linkElement);
+
+ card.appendChild(embedContainer);
+ }
+
// interaction counts
if (!card.querySelector('.interaction-counts')) {
const counts = document.createElement('div');
@@ -585,7 +626,6 @@ function renderPostSingle(post, a, likes, ipcRenderer, container, idkey = 'bskyi
* @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} */
let a;
@@ -596,14 +636,26 @@ module.exports = function renderPosts(posts, likes, pinnedPost, ipcRenderer, idk
else {
container = document.createElement('div');
container.classList.add('cards-container');
- container.id = containerid;
+ container.id = `${containerid}container`;
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);
+ 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);
+ }
+ }
const postids = Array.from(document.querySelectorAll(`[data-${idkey}]`)).map(o => o.dataset[`${idkey} `]);
console.log(postids);
@@ -614,6 +666,8 @@ module.exports = function renderPosts(posts, likes, pinnedPost, ipcRenderer, idk
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');
diff --git a/JS/replies.cjs b/JS/replies.cjs
index 354f179..76136ac 100644
--- a/JS/replies.cjs
+++ b/JS/replies.cjs
@@ -86,12 +86,10 @@ function createPostPreview(post, isParent = false) {
}
-module.exports = function renderReplies(replyObj) {
+module.exports = function renderReplies({ cursor, replies }) {
const repliesContainer = document.querySelector('#replies');
repliesContainer.querySelector('.placeholder')?.remove();
- const { cursor, replies } = replyObj;
-
if (cursor) sessionStorage.setItem('repliescursor', cursor);
else sessionStorage.removeItem('repliescursor');
diff --git a/JS/script.js b/JS/script.js
index aae15fd..827eab7 100644
--- a/JS/script.js
+++ b/JS/script.js
@@ -1,12 +1,27 @@
-document.addEventListener('scroll', () => bottomscrolled(document.querySelector('#posts')));
-async function bottomscrolled(targetDiv) {
+document.addEventListener('scroll', bottomscrolled);
+async function bottomscrolled(e) {
const scrollPosition = window.scrollY + window.innerHeight,
pageHeight = document.documentElement.scrollHeight,
atbottom = (scrollPosition >= pageHeight);
if (!atbottom) return;
- window.electronAPI.getnewposts();
+ // get the current div
+ const targetDiv = document.querySelector('.content.active')
+
+ switch (targetDiv.id) {
+ case 'posts': window.electronAPI.getnewposts();
+ break;
+ case 'replies': window.electronAPI.getReplies()
+ break;
+ case 'likes': window.electronAPI.getnewlikes();
+ break;
+ case 'media':
+ window.electronAPI.getnewmedia();
+ break;
+
+ default: console.log(`unknown scroll div ID ${targetDiv.id}`);
+ }
}
// function to toggle the active section
@@ -24,16 +39,55 @@ function showSection(sectionId) {
// show the selected section and activate the button
document.getElementById(sectionId).classList.add('active');
document.getElementById(sectionId + 'Btn').classList.add('active');
+
+ createDropdown();
}
+
+function createDropdown() {
+ document.querySelector('.dropdown')?.remove();
+
+ const activeDiv = document.querySelector('.content.active');
+ if (activeDiv.id === 'replies') return;
+
+ const dropdown = document.createElement('div');
+ dropdown.classList.add('dropdown');
+
+ const button = document.createElement('button');
+ button.classList.add('dropdown-button');
+ button.textContent = 'Layout: Compact';
+
+ const content = document.createElement('div');
+ content.classList.add('dropdown-content');
+ content.id = 'layoutDropdown';
+
+ const options = [
+ { text: 'Large', value: 'large' },
+ { text: 'Relaxed', value: 'relaxed' },
+ { text: 'Compact', value: 'compact', selected: true }
+ ];
+
+ options.forEach(({ text, value, selected }) => {
+ const link = document.createElement('a');
+ link.href = '#';
+ link.dataset.value = value;
+ link.textContent = text;
+ if (selected) link.classList.add('selected');
+ link.addEventListener('click', togglerelaxed);
+ content.appendChild(link);
+ })
+
+ dropdown.append(button, content);
+ activeDiv.prepend(dropdown);
+}
+
+
// 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)
+ const container = document.querySelector('.content.active [class*="cards-container"]'),
+ value = e.target.getAttribute('data-value');
if (!container) return console.warn('container not found!');
@@ -50,9 +104,4 @@ function togglerelaxed(e) {
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);
- });
-});
\ No newline at end of file
+document.addEventListener('DOMContentLoaded', createDropdown);
\ No newline at end of file
diff --git a/bluesky/main.js b/bluesky/main.js
index e70c88a..2ebd130 100644
--- a/bluesky/main.js
+++ b/bluesky/main.js
@@ -133,6 +133,7 @@ async function getUserData(utag, allData = false) {
output.likesCursor = likesCursor;
output.posts = feed;
output.postcursor = cursor;
+ output.media = await getMedia(utag);
output.replies = await getReplies(utag);
}
@@ -161,8 +162,27 @@ export const getPosts = async (utag, cursor = undefined, likesCursor = undefined
}
+export const getLikes = async (utag, cursor = undefined) => {
+ try {
+ const did = await getDID(utag);
+ if (!did) return { err: 'DID not found!' };
+
+ const data = await agent.getActorLikes({ actor: did, cursor });
+
+ // sloppy fix, opened an issue at https://github.com/bluesky-social/atproto/issues/3087
+ if (!data.data.feed.length) delete data.data.cursor;
+ return data.data;
+ }
+ catch (err) {
+ console.error(err);
+ return null;
+ }
+}
+
+
export const getReplies = async (utag, cursorInit, limit = 20) => {
const did = await getDID(utag);
+
let posts = await agent.getAuthorFeed({ actor: did, limit: limit, includePins: true, cursor: cursorInit });
let cursor = posts.data.cursor;
const replies = posts.data.feed.filter(o => o.reply);
@@ -181,15 +201,16 @@ export const getMedia = async (utag, cursorInit, limit = 20) => {
const did = await getDID(utag);
let posts = await agent.getAuthorFeed({ actor: did, limit: limit, includePins: true, cursor: cursorInit });
let cursor = posts.data.cursor;
- const replies = posts.data.feed.filter(o => o.reply);
+ const f = o => (o.post.embed?.images?.length || (o.post?.post?.embed?.$type === 'app.bsky.embed.video#view')),
+ data = posts.data.feed.filter(f);
- while (cursor && replies.length < limit) {
+ while (cursor && data.length < limit) {
posts = await agent.getAuthorFeed({ actor: did, limit, includePins: true, cursor });
- replies.push(...posts.data.feed.filter(o => o.reply));
+ data.push(...posts.data.feed.filter(f));
cursor = posts.data.cursor;
}
- return { replies, cursor };
+ return { data, cursor };
}
@@ -225,10 +246,20 @@ export async function setupIPC() {
clearCache();
});
+ ipcMain.handle('getlikes', async (e, utag, cursor) => {
+ if (!cursor) return e.sender.send(404);
+ e.sender.send('likes', await getLikes(utag, cursor));
+ });
+
+ ipcMain.handle('getmedia', async (e, utag, cursor) => {
+ if (!cursor) return e.sender.send(404);
+ e.sender.send('media', await getMedia(utag, cursor));
+ });
+
ipcMain.handle('get-connections', getConnections);
ipcMain.handle('gethistory', (e, limit, offset) => e.sender.send('history', JSON.stringify(getHistory(limit, offset))));
- ipcMain.handle('getreplies', async (e, limit, offset) => e.sender.send('replies', JSON.stringify(await getReplies(limit, offset))));
+ ipcMain.handle('getreplies', async (e, cursor, utag, limit = 20) => e.sender.send('replies', JSON.stringify(await getReplies(utag, cursor, limit))));
ipcMain.handle('getvideo', async (e, oldurl) => {
const newURL = await convertAndServe(`${crypto.randomUUID()}.mp4`, oldurl);
e.sender.send('video', oldurl, newURL);
diff --git a/src/user_page_preload.cjs b/src/user_page_preload.cjs
index c64071d..41b1067 100644
--- a/src/user_page_preload.cjs
+++ b/src/user_page_preload.cjs
@@ -2,7 +2,6 @@ const { contextBridge, ipcRenderer } = require('electron');
const { populateProfile, Profile } = require('../JS/Profile.cjs');
const renderPosts = require('../JS/posts.cjs');
const renderReplies = require('../JS/replies.cjs');
-const renderLikes = require('../JS/likes.cjs');
const { displayUploadStatus, renderCompose } = require('../JS/compose.cjs');
async function handleFileDialogue(e) {
@@ -35,6 +34,15 @@ async function handleFileDialogue(e) {
}
}
+const appendEOF = (divid) => {
+ if (document.getElementById(divid)?.querySelector('.eof')) return;
+ else {
+ const d = document.createElement('div');
+ d.className = 'eof';
+ d.innerHTML = 'Reached end of feed!
';
+ document.getElementById(divid)?.appendChild(d);
+ }
+}
window.addEventListener('DOMContentLoaded', () => {
@@ -52,7 +60,11 @@ window.addEventListener('DOMContentLoaded', () => {
console.log(data);
if (data.postcursor) sessionStorage.setItem('postcursor', data.postcursor);
- if (data.likesCursor) sessionStorage.setItem('likescursor', data.likesCursor);
+ if (data.likesCursor) {
+ sessionStorage.setItem('likescursor', data.likesCursor);
+ sessionStorage.setItem('likesfeedcursor', data.likesCursor);
+ }
+ if (data.media.cursor) sessionStorage.setItem('mediacursor', data.media.cursor);
document.querySelector('#loading')?.remove();
populateProfile(pObj);
@@ -60,6 +72,7 @@ window.addEventListener('DOMContentLoaded', () => {
if (data.posts) renderPosts(data.posts, data.likes || [], pObj?.pinnedPost, ipcRenderer);
if (data.replies) renderReplies(data.replies);
if (data.likes) renderPosts(data.likes, data.likes, null, ipcRenderer, 'bskylikeid', 'likes');
+ if (data.media) renderPosts(data.media.data, data.likes, null, ipcRenderer, 'bskymediaid', 'media');
});
contextBridge.exposeInMainWorld('electronAPI', {
@@ -68,29 +81,25 @@ window.addEventListener('DOMContentLoaded', () => {
likescursor = sessionStorage.getItem('likescursor');
if (cursor) ipcRenderer.invoke('getposts', utag, cursor, likescursor);
- else if (document.querySelector('#eof')) return;
- else {
- const d = document.createElement('div');
- d.id = 'eof';
- d.innerHTML = 'Reached end of feed!
';
- document.querySelector('#posts')?.appendChild(d);
- }
+ else appendEOF('posts');
},
getnewlikes: () => {
- return alert("TODO (check TODO.txt)");
- const cursor = sessionStorage.getItem('postcursor');
+ const cursor = sessionStorage.getItem('likesfeedcursor');
- if (cursor) ipcRenderer.invoke('getposts', utag, cursor, likescursor);
- else if (document.querySelector('#eof')) return;
- else {
- const d = document.createElement('div');
- d.id = 'eof';
- d.innerHTML = 'Reached end of feed!
';
- document.querySelector('#posts')?.appendChild(d);
- }
+ if (cursor) ipcRenderer.invoke('getlikes', utag, cursor);
+ else appendEOF('likes');
},
getHistory: (cursor = undefined) => ipcRenderer.invoke('gethistory', cursor),
- getReplies: (cursor = undefined) => ipcRenderer.invoke('getreplies', cursor),
+ getReplies: (utag) => {
+ const cursor = sessionStorage.getItem('repliescursor');
+ if (cursor) ipcRenderer.invoke('getreplies', cursor, utag);
+ else appendEOF('replies');
+ },
+ getnewmedia: (utag) => {
+ const cursor = sessionStorage.getItem('mediacursor');
+ if (cursor) ipcRenderer.invoke('getmedia', cursor, utag);
+ else appendEOF('media');
+ },
getVideo: (src) => ipcRenderer.invoke('getvideo', src),
});
@@ -112,6 +121,23 @@ window.addEventListener('DOMContentLoaded', () => {
else sessionStorage.removeItem('postcursor');
});
+ ipcRenderer.on('likes', (e, data) => {
+ // reset all videos because the cache was cleared
+ document.querySelectorAll('.post-card video').forEach(video => {
+ video.src = '../assets/video-loading.mp4';
+ video.pause();
+ video.currentTime = 0;
+ });
+
+ if (data.err) return alert(data.err);
+
+ console.log(data);
+ renderPosts(data.feed, data.feed, null, ipcRenderer, 'bskylikeid', 'likes');
+
+ if (data.cursor) sessionStorage.setItem('likesfeedcursor', data.cursor);
+ else sessionStorage.removeItem('likesfeedcursor');
+ });
+
ipcRenderer.on('history', (e, data) => {
const hist = JSON.parse(data);
console.log(hist);