import { bindToPage, pageEffect, bindPageTitle, onPageShow, onPageHide, } from "/js/router.js"; import { html, render } from "/js/lib/lit-html.js"; import { headerTemplate } from "/js/templates/header.template.js"; import { feedsFeedTemplate } from "/js/templates/feedsFeed.template.js"; import { fillableIconTemplate } from "/js/templates/fillableIcon.template.js"; import "/js/components/app-icon.js"; import { linkToList, linkToFeed } from "/js/navigation.js"; import { Signal, ReactiveStore, SignalSet, SignalArray } from "/js/signals.js"; import { enableReorder } from "/js/utils.js"; import { showToast } from "/js/toasts.js"; import { cdnImageUrl, valueForPinnedItem } from "/js/dataHelpers.js"; import "/js/components/container-link.js"; export default async function feedsView({ root, layout, context: { auth, dataLayer }, }) { await auth.requireAuth(); const state = new ReactiveStore("feedsView"); state.$draftOrder = new SignalArray(); state.$draftUnpinned = new SignalSet(); state.$isEditing = new Signal.State(false); state.$isSaving = new Signal.State(false); function orderMatches(orderA, orderB) { if (orderA.length !== orderB.length) return false; for (let i = 0; i < orderA.length; i++) { if (orderA[i] !== orderB[i]) return false; } return true; } async function handleSave() { if (state.$isSaving.get()) return; const draftOrder = state.$draftOrder; const draftUnpinned = state.$draftUnpinned; if (draftOrder.length > 0 || draftUnpinned.size > 0) { const pinnedItems = dataLayer.derived.$hydratedPinnedItems.get(); if (!pinnedItems) return; const baseOrder = draftOrder.length > 0 ? [...draftOrder] : pinnedItems.map(valueForPinnedItem); const nextValues = draftUnpinned.size > 0 ? baseOrder.filter((value) => !draftUnpinned.has(value)) : baseOrder; state.$isSaving.set(true); try { await dataLayer.mutations.setPinnedItems(nextValues); showToast("Feeds updated!"); } catch { showToast("Couldn't save changes"); return; } finally { state.$isSaving.set(false); } } state.$draftOrder.clear(); state.$draftUnpinned.clear(); state.$isEditing.set(false); } function cancelEditing() { if (state.$isSaving.get()) return; state.$draftOrder.clear(); state.$draftUnpinned.clear(); state.$isEditing.set(false); } function editControlsTemplate({ value, isSaving, showUnpin }) { return html`${showUnpin ? html`` : ""} `; } function rowTemplate({ item, currentUser, rightItem }) { const value = valueForPinnedItem(item); if (item.type === "timeline") { return html`
Following
Feed by @bsky.app
${rightItem}
`; } if (item.type === "list") { return html`
${item.data.name}
${item.data.name}
${item.data.creator ? html`
List by ${item.data.creator.did === currentUser?.did ? "you" : `@${item.data.creator.handle}`}
` : ""}
${rightItem}
`; } const feedGenerator = item.data; return html`
${feedGenerator.displayName}
${feedGenerator.displayName}
${feedGenerator.creator ? html`
Feed by ${feedGenerator.creator.did === currentUser?.did ? "you" : `@${feedGenerator.creator.handle}`}
` : ""}
${rightItem}
`; } bindPageTitle(root, () => "Feeds"); pageEffect(root, () => { const currentUser = dataLayer.derived.$currentUser.get(); const pinnedItems = dataLayer.derived.$hydratedPinnedItems.get(); const draftOrder = state.$draftOrder; const draftUnpinned = state.$draftUnpinned; const isEditing = state.$isEditing.get(); const isSaving = state.$isSaving.get(); let orderedItems = pinnedItems; let persistedOrder = null; if (pinnedItems) { persistedOrder = pinnedItems.map(valueForPinnedItem); if (draftOrder.length > 0) { const byValue = new Map( pinnedItems.map((it) => [valueForPinnedItem(it), it]), ); orderedItems = draftOrder .map((value) => byValue.get(value)) .filter(Boolean); } if (draftUnpinned.size > 0) { orderedItems = orderedItems.filter( (it) => !draftUnpinned.has(valueForPinnedItem(it)), ); } } const canEdit = (pinnedItems?.length ?? 0) >= 2; const showUnpin = (orderedItems?.length ?? 0) >= 2; render( html`
${headerTemplate({ title: "Feeds", subtitle: "", rightItemTemplate: () => { if (isEditing) { return html`
`; } return html``; }, })}
Pinned Feeds
${feedsFeedTemplate({ items: orderedItems, renderItem: (item) => { const value = valueForPinnedItem(item); const rightItem = isEditing ? editControlsTemplate({ value, isSaving, showUnpin }) : item.type === "timeline" ? "" : html`
`; return rowTemplate({ item, currentUser, rightItem }); }, isEditing, })}
`, root, ); const listEl = root.querySelector(".feeds-list"); if (listEl && orderedItems && isEditing && !isSaving) { enableReorder(listEl, { itemSelector: ".feeds-list-item", handleSelector: ".feeds-list-item-drag-handle", onReorder: (elements) => { const nextOrder = elements.map((el) => el.dataset.pinnedValue); const baseline = persistedOrder ?? []; if (orderMatches(nextOrder, baseline)) { state.$draftOrder.clear(); } else { state.$draftOrder.replace(nextOrder); } }, }); } else if (listEl?.__reorderEnabled) { listEl.__reorderEnabled.cleanup(); } }); function resetEditingState() { state.$draftOrder.clear(); state.$draftUnpinned.clear(); state.$isEditing.set(false); } // Reset whenever the page changes visibility, unlike the data load below onPageShow(root, resetEditingState); onPageHide(root, resetEditingState); function loadPageData() { dataLayer.requests.loadPinnedItems(); } onPageShow(root, ({ action }) => { if (action === "restore") return; loadPageData(); }); bindToPage(root, layout, "active-nav-click", () => { loadPageData(); }); }