369 lines
12 KiB
JavaScript
369 lines
12 KiB
JavaScript
|
|
import {
|
|
initNDKPage,
|
|
getPubkey,
|
|
injectHeaderAvatar,
|
|
subscribe,
|
|
disconnect,
|
|
getVersion,
|
|
queryCache,
|
|
fetchEventsFromAllRelays,
|
|
fetchCachedProfile,
|
|
storeProfile,
|
|
publishEvent,
|
|
getUserSettings,
|
|
patchUserSettings,
|
|
onUserSettings
|
|
} from './js/init-ndk.mjs';
|
|
import { HamburgerMorphing } from './hamburger_morphing/hamburger.mjs';
|
|
import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs';
|
|
import { initBlossomSection, updateBlossomSection } from './js/blossom-ui.mjs';
|
|
import { initPostCards } from './js/post-interactions2.mjs';
|
|
|
|
const INITIAL_POSTS_LOAD = 10;
|
|
const SEE_MORE_INCREMENT = 20;
|
|
|
|
let currentPubkey = null;
|
|
let updateIntervalId = null;
|
|
|
|
let unsubscribeUserSettings = null;
|
|
|
|
let hamburgerInstance = null;
|
|
let logoutHamburger = null;
|
|
let themeToggleHamburger = null;
|
|
let isDarkMode = false;
|
|
let isNavOpen = false;
|
|
|
|
const posts = [];
|
|
const postIds = new Set();
|
|
const renderedPostIds = new Set();
|
|
let displayCount = INITIAL_POSTS_LOAD;
|
|
|
|
let followedPubkeys = [];
|
|
const feedPubkeys = new Set();
|
|
|
|
const divBody = document.getElementById('divBody');
|
|
const divSideNav = document.getElementById('divSideNav');
|
|
const divFooterRight = document.getElementById('divFooterRight');
|
|
const divFeed = document.getElementById('divFeed');
|
|
const divHint = document.getElementById('divHint');
|
|
const btnSeeMore = document.getElementById('btnSeeMore');
|
|
const chkAutoplayVideos = document.getElementById('chkAutoplayVideos');
|
|
|
|
let autoplayVideosEnabled = false;
|
|
let liveFeedSubscriptionKey = '';
|
|
|
|
const cards = initPostCards({
|
|
subscribe,
|
|
publishEvent,
|
|
getPubkey,
|
|
fetchEventsFromAllRelays,
|
|
fetchCachedProfile,
|
|
storeProfile
|
|
});
|
|
|
|
function initHamburgerMenu() {
|
|
hamburgerInstance = new HamburgerMorphing('#divSvgHam', {
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
hamburgerInstance.animateTo('burger');
|
|
}
|
|
|
|
function openNav() {
|
|
divSideNav.style.zIndex = 3;
|
|
divSideNav.style.width = '50vw';
|
|
isNavOpen = true;
|
|
if (hamburgerInstance) hamburgerInstance.animateTo('arrow_left');
|
|
|
|
if (!logoutHamburger) {
|
|
logoutHamburger = new HamburgerMorphing('#logoutHamburgerContainer', {
|
|
size: 24,
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
logoutHamburger.animateTo('x');
|
|
}
|
|
|
|
if (!themeToggleHamburger) {
|
|
themeToggleHamburger = new HamburgerMorphing('#themeToggleHamburgerContainer', {
|
|
size: 24,
|
|
foreground: 'var(--primary-color)',
|
|
background: 'var(--secondary-color)',
|
|
hover: 'var(--accent-color)'
|
|
});
|
|
const savedTheme = localStorage.getItem('theme');
|
|
isDarkMode = savedTheme === 'dark' || document.body.classList.contains('dark-mode');
|
|
themeToggleHamburger.animateTo(isDarkMode ? 'moon' : 'circle');
|
|
}
|
|
}
|
|
|
|
function closeNav() {
|
|
divSideNav.style.width = '0vw';
|
|
divSideNav.style.zIndex = -1;
|
|
isNavOpen = false;
|
|
if (hamburgerInstance) hamburgerInstance.animateTo('burger');
|
|
}
|
|
|
|
function toggleNav() {
|
|
isNavOpen ? closeNav() : openNav();
|
|
}
|
|
|
|
function isOriginalPost(event) {
|
|
const eTags = event.tags?.filter(t => t[0] === 'e') || [];
|
|
if (eTags.length === 0) return true;
|
|
return eTags.every(t => t[3] === 'mention');
|
|
}
|
|
|
|
function applyVideoAutoplaySetting(enabled) {
|
|
autoplayVideosEnabled = !!enabled;
|
|
if (chkAutoplayVideos) chkAutoplayVideos.checked = autoplayVideosEnabled;
|
|
|
|
const videoEls = Array.from(divFeed.querySelectorAll('video'));
|
|
videoEls.forEach((videoEl) => {
|
|
videoEl.controls = true;
|
|
videoEl.playsInline = true;
|
|
videoEl.setAttribute('playsinline', '');
|
|
videoEl.preload = 'metadata';
|
|
|
|
if (autoplayVideosEnabled) {
|
|
videoEl.autoplay = true;
|
|
videoEl.muted = true;
|
|
videoEl.setAttribute('autoplay', '');
|
|
videoEl.setAttribute('muted', '');
|
|
const playPromise = videoEl.play?.();
|
|
if (playPromise?.catch) playPromise.catch(() => {});
|
|
} else {
|
|
videoEl.autoplay = false;
|
|
videoEl.muted = false;
|
|
videoEl.removeAttribute('autoplay');
|
|
videoEl.removeAttribute('muted');
|
|
}
|
|
});
|
|
}
|
|
|
|
function applySettings(settings) {
|
|
const nextAutoplayEnabled = !!settings?.feed?.videoAutoplay;
|
|
applyVideoAutoplaySetting(nextAutoplayEnabled);
|
|
}
|
|
|
|
async function persistAutoplaySetting(enabled) {
|
|
try {
|
|
await patchUserSettings({
|
|
feed: {
|
|
videoAutoplay: !!enabled
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.warn('[feed.html] Failed to persist feed autoplay setting:', error?.message || error);
|
|
}
|
|
}
|
|
|
|
function renderFeed() {
|
|
posts.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
|
|
const toShow = posts.slice(0, displayCount);
|
|
|
|
toShow.forEach((post) => {
|
|
if (renderedPostIds.has(post.id)) return;
|
|
const postEl = cards.createCard(post, { currentPubkey, autoWire: false, autoplayVideo: autoplayVideosEnabled });
|
|
divFeed.appendChild(postEl);
|
|
renderedPostIds.add(post.id);
|
|
});
|
|
|
|
cards.wireInteractions(toShow.map(p => p.id), { currentPubkey });
|
|
|
|
btnSeeMore.style.display = posts.length > displayCount ? 'block' : 'none';
|
|
divHint.textContent = followedPubkeys.length > 0
|
|
? `${posts.length} posts from ${followedPubkeys.length} follows`
|
|
: 'Follow users to populate your feed.';
|
|
divFooterRight.textContent = `${posts.length} posts`;
|
|
}
|
|
|
|
function ensureLiveFeedSubscription() {
|
|
const authors = Array.from(feedPubkeys).filter(Boolean);
|
|
if (authors.length === 0) return;
|
|
|
|
const key = authors.slice().sort().join(',');
|
|
if (key === liveFeedSubscriptionKey) return;
|
|
|
|
const newestKnown = posts.reduce((max, p) => Math.max(max, p?.created_at || 0), 0);
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const since = newestKnown > 0 ? Math.max(0, newestKnown - 30) : (now - 3600);
|
|
|
|
subscribe(
|
|
{ kinds: [1], authors, since },
|
|
{ closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
|
|
);
|
|
|
|
liveFeedSubscriptionKey = key;
|
|
console.log('[feed.html] Live feed subscription active', { authors: authors.length, since });
|
|
}
|
|
|
|
async function ingestContactList(evt) {
|
|
if (!evt || evt.kind !== 3 || evt.pubkey !== currentPubkey) return;
|
|
const pTags = evt.tags?.filter(tag => tag[0] === 'p' && tag[1]) || [];
|
|
followedPubkeys = pTags.map(tag => tag[1]);
|
|
|
|
const newAuthors = followedPubkeys.filter((pk) => !feedPubkeys.has(pk));
|
|
newAuthors.forEach((pk) => feedPubkeys.add(pk));
|
|
|
|
ensureLiveFeedSubscription();
|
|
|
|
if (newAuthors.length > 0) {
|
|
try {
|
|
const [cached, freshByRelay] = await Promise.all([
|
|
queryCache({ kinds: [1], authors: Array.from(feedPubkeys), limit: 120 }).catch(() => []),
|
|
fetchEventsFromAllRelays({ kinds: [1], authors: newAuthors, limit: INITIAL_POSTS_LOAD }).catch(() => ({}))
|
|
]);
|
|
|
|
cached.forEach((evt1) => {
|
|
if (!evt1?.id || postIds.has(evt1.id) || !isOriginalPost(evt1)) return;
|
|
postIds.add(evt1.id);
|
|
posts.push(evt1);
|
|
});
|
|
|
|
Object.values(freshByRelay).flat().forEach((evt1) => {
|
|
if (!evt1?.id || postIds.has(evt1.id) || !isOriginalPost(evt1)) return;
|
|
postIds.add(evt1.id);
|
|
posts.push(evt1);
|
|
});
|
|
|
|
renderFeed();
|
|
} catch (_) {
|
|
renderFeed();
|
|
}
|
|
} else {
|
|
renderFeed();
|
|
}
|
|
}
|
|
|
|
async function logout() {
|
|
if (updateIntervalId) {
|
|
clearInterval(updateIntervalId);
|
|
updateIntervalId = null;
|
|
}
|
|
cards.destroy();
|
|
disconnect();
|
|
if (window.NOSTR_LOGIN_LITE?.logout) {
|
|
await window.NOSTR_LOGIN_LITE.logout();
|
|
}
|
|
if (unsubscribeUserSettings) {
|
|
unsubscribeUserSettings();
|
|
unsubscribeUserSettings = null;
|
|
}
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
location.reload(true);
|
|
}
|
|
|
|
async function updateFooter() {
|
|
try {
|
|
await updateFooterRelayStatus();
|
|
await updateSidenavRelaySection();
|
|
await updateBlossomSection();
|
|
} catch (_) {}
|
|
}
|
|
|
|
(async function main() {
|
|
try {
|
|
const versionInfo = await getVersion();
|
|
document.getElementById('versionDisplay').textContent = versionInfo.VERSION;
|
|
|
|
initHamburgerMenu();
|
|
document.getElementById('divSvgHam')?.addEventListener('click', toggleNav);
|
|
|
|
document.getElementById('themeToggleButton')?.addEventListener('click', () => {
|
|
isDarkMode = !isDarkMode;
|
|
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
|
|
localStorage.setItem('sidenavWasOpen', isNavOpen ? 'true' : 'false');
|
|
window.location.reload();
|
|
});
|
|
|
|
document.getElementById('logoutButton')?.addEventListener('click', logout);
|
|
|
|
document.getElementById('commentsToggleButton')?.addEventListener('click', () => {
|
|
const nextVisible = !cards.getCommentsVisible();
|
|
cards.setCommentsVisible(nextVisible);
|
|
document.getElementById('commentsToggleButton')?.classList.toggle('dimmed', !nextVisible);
|
|
});
|
|
|
|
btnSeeMore.addEventListener('click', () => {
|
|
displayCount += SEE_MORE_INCREMENT;
|
|
renderFeed();
|
|
});
|
|
|
|
chkAutoplayVideos?.addEventListener('change', async () => {
|
|
const nextEnabled = !!chkAutoplayVideos.checked;
|
|
applyVideoAutoplaySetting(nextEnabled);
|
|
await persistAutoplaySetting(nextEnabled);
|
|
});
|
|
|
|
await initNDKPage();
|
|
currentPubkey = await getPubkey();
|
|
await injectHeaderAvatar(currentPubkey);
|
|
|
|
try {
|
|
const settings = await getUserSettings();
|
|
applySettings(settings || {});
|
|
} catch (error) {
|
|
console.warn('[feed.html] Failed to load user settings:', error?.message || error);
|
|
applySettings({});
|
|
}
|
|
|
|
if (!unsubscribeUserSettings) {
|
|
unsubscribeUserSettings = onUserSettings((settings) => {
|
|
applySettings(settings || {});
|
|
});
|
|
}
|
|
|
|
initFooterRelayStatus();
|
|
initSidenavRelaySection();
|
|
await initBlossomSection();
|
|
await updateFooter();
|
|
updateIntervalId = setInterval(updateFooter, 1000);
|
|
|
|
window.addEventListener('ndkRelayActivity', (e) => {
|
|
setRelayActivityState(e.detail.relayUrl, e.detail.activity);
|
|
});
|
|
|
|
window.addEventListener('ndkEvent', async (e) => {
|
|
const evt = e.detail;
|
|
if (!evt) return;
|
|
|
|
if (evt.kind === 3 && evt.pubkey === currentPubkey) {
|
|
await ingestContactList(evt);
|
|
return;
|
|
}
|
|
|
|
if (evt.kind === 1 && feedPubkeys.has(evt.pubkey) && isOriginalPost(evt) && !postIds.has(evt.id)) {
|
|
postIds.add(evt.id);
|
|
posts.push(evt);
|
|
renderFeed();
|
|
}
|
|
});
|
|
|
|
subscribe({ kinds: [3], authors: [currentPubkey], limit: 1 }, { closeOnEose: false, cacheUsage: 'CACHE_FIRST' });
|
|
|
|
const cachedContactEvents = await queryCache({ kinds: [3], authors: [currentPubkey], limit: 1 }).catch(() => []);
|
|
if (cachedContactEvents.length > 0) {
|
|
const latest = cachedContactEvents.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))[0];
|
|
await ingestContactList(latest);
|
|
}
|
|
|
|
if (feedPubkeys.size === 0) {
|
|
divHint.textContent = 'Follow users to populate your feed.';
|
|
}
|
|
|
|
const sidenavWasOpen = localStorage.getItem('sidenavWasOpen');
|
|
if (sidenavWasOpen === 'true') {
|
|
localStorage.removeItem('sidenavWasOpen');
|
|
openNav();
|
|
}
|
|
} catch (error) {
|
|
console.error('[feed.html] Initialization failed:', error);
|
|
divBody.innerHTML = `<div style="text-align:center;padding:50px;"><div style="font-size:24px;margin-bottom:20px;color:red;">❌ Error</div><div style="font-size:16px;">${error.message}</div></div>`;
|
|
}
|
|
})();
|
|
|