diff --git a/src/app.jsx b/src/app.jsx index 4c364068..186cdf00 100644 --- a/src/app.jsx +++ b/src/app.jsx @@ -40,6 +40,7 @@ import Favourites from './pages/favourites'; import Filters from './pages/filters'; import FollowedHashtags from './pages/followed-hashtags'; import Following from './pages/following'; +import Following2 from './pages/following2'; import Hashtag from './pages/hashtag'; import Home from './pages/home'; import HttpRoute from './pages/http-route'; @@ -782,6 +783,14 @@ function SecondaryRoutes() { } /> + + + + } + /> import('../iconify-icons/mingcute/moon-line.js'), flip: 'horizontal', }, + 'arrow-up-top': () => import('../iconify-icons/mingcute/arrow-to-up-line.js'), }; diff --git a/src/components/lazy-render.jsx b/src/components/lazy-render.jsx new file mode 100644 index 00000000..12fb6941 --- /dev/null +++ b/src/components/lazy-render.jsx @@ -0,0 +1,64 @@ +import { useLayoutEffect, useRef } from 'preact/hooks'; +import { useOnInView } from 'react-intersection-observer'; + +// The sticky header, usually at the top +const TOP = 48; + +export default function LazyRender({ + as: Root = 'div', + id, + class: className, + children, + ...props +}) { + const rootRef = useRef(null); + + const observerRef = useOnInView( + (inView) => { + if (inView && rootRef.current) { + console.log('💥', { + id, + root: rootRef.current, + }); + rootRef.current.classList.remove('hidden'); + } + }, + { + rootMargin: `-${TOP}px 0px 0px 0px`, + triggerOnce: true, + }, + ); + + useLayoutEffect(() => { + if (!rootRef.current) return; + const rect = rootRef.current.getBoundingClientRect(); + if (rect.bottom <= TOP) { + const childElementCount = rootRef.current.childElementCount; + const hasChildren = childElementCount > 0; + if (hasChildren) { + // Don't need to observe if has children + observerRef(null); + // Debugging + if (import.meta.env.DEV) { + rootRef.current.dataset.rectBottom = rect.bottom; + rootRef.current.dataset.childElementCount = childElementCount; + } + } else { + rootRef.current.classList.add('hidden'); + } + } + }, []); + + return ( + { + rootRef.current = node; + observerRef(node); + }} + class={`lazy-render ${className || ''}`} + > + {children} + + ); +} diff --git a/src/components/status-compact.jsx b/src/components/status-compact.jsx index 2d711bc1..9f1fbce6 100644 --- a/src/components/status-compact.jsx +++ b/src/components/status-compact.jsx @@ -9,6 +9,7 @@ import statusPeek from '../utils/status-peek'; import { getCurrentAccID } from '../utils/store-utils'; import Avatar from './avatar'; +import LazyRender from './lazy-render'; function StatusCompact({ sKey }) { const snapStates = useSnapshot(states); @@ -50,8 +51,10 @@ function StatusCompact({ sKey }) { const filterTitleStr = filterInfo?.titlesStr || ''; return ( -
{statusPeekText} )} -
+ ); } diff --git a/src/components/status.css b/src/components/status.css index 83ee3dc8..1c45ed89 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -479,6 +479,11 @@ background-repeat: no-repeat; background-size: 100% calc(100% - var(--top-padding) / 2); + &.hidden { + padding-top: 0; + margin-bottom: 0; + } + &.visibility-direct { background-image: var(--yellow-stripes); } diff --git a/src/components/status.jsx b/src/components/status.jsx index d1a80ca5..9dd03b2b 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -56,6 +56,7 @@ import Avatar from './avatar'; import CustomEmoji from './custom-emoji'; import EmojiText from './emoji-text'; import Icon from './icon'; +import LazyRender from './lazy-render'; import LazyShazam from './lazy-shazam'; import Link from './link'; import Loader from './loader'; @@ -2396,7 +2397,7 @@ function Status({ )} {!withinContext && ( - <> + {isThread ? ( ) )} - + )}
{}, - checkForUpdates = () => {}, - checkForUpdatesInterval = 15_000, // 15 seconds - headerStart, - headerEnd, - timelineStart, - // allowFilters, - refresh, - view, - filterContext, - showFollowedTags, - showReplyParent, - clearWhenRefresh, -}) { - const { t } = useLingui(); - const snapStates = useSnapshot(states); - const [items, setItems] = useState([]); - const [uiState, setUIState] = useState('start'); - const [showMore, setShowMore] = useState(false); - const [showNew, setShowNew] = useState(false); - const [visible, setVisible] = useState(true); - const scrollableRef = useRef(); - - console.debug('RENDER Timeline', id, refresh); - __BENCHMARK.start(`timeline-${id}-load`); - - const mediaFirst = useMemo(() => isMediaFirstInstance(), []); +// paginationItemsSelector is for Timeline2 +const paginationPrevSelector = + '.timeline-pagination button[data-pagination-trigger="prev"]'; +const paginationNextSelector = + '.timeline-pagination button[data-pagination-trigger="next"]'; +const itemsSelector = '.timeline-item, .timeline-item-alt'; - const allowGrouping = view !== 'media'; - const loadItemsTS = useRef(0); // Ensures only one loadItems at a time - const loadItems = useDebouncedCallback( - (firstLoad) => { - setShowNew(false); - // if (uiState === 'loading') return; - setUIState('loading'); - (async () => { - try { - const ts = (loadItemsTS.current = Date.now()); - let { done, value } = await fetchItems(firstLoad); - if (ts !== loadItemsTS.current) return; - if (Array.isArray(value)) { - // Avoid grouping for pinned posts - const [pinnedPosts, otherPosts] = value.reduce( - (acc, item) => { - if (item._pinned) { - acc[0].push(item); - } else { - acc[1].push(item); - } - return acc; - }, - [[], []], - ); - value = otherPosts; - value = filterHiddenStatuses(value, filterContext); - if (allowGrouping) { - if (boostsCarousel) { - value = groupBoosts(value); - } - value = groupContext(value, instance); - } - if (pinnedPosts.length) { - value = pinnedPosts.concat(value); - } - console.log(value); - if (firstLoad) { - setItems(value); - } else { - setItems((items) => [...items, ...value]); - } - if (!value.length) done = true; - setShowMore(!done); - } else { - setShowMore(false); - } - setUIState('default'); - __BENCHMARK.end(`timeline-${id}-load`); - } catch (e) { - console.error(e); - setUIState('error'); - if (firstLoad && !items.length && errorText) { - showToast(errorText); - } - } finally { - loadItems.cancel(); - } - })(); - }, - 1_000, - { - leading: true, - // trailing: false, - }, - ); - - const itemsSelector = '.timeline-item, .timeline-item-alt'; - - const jRef = useHotkeys( +// Standalone hotkey hooks for timeline navigation +export function useJHotkeys(scrollableRef) { + return useHotkeys( 'j, shift+j', (e, handler) => { // Fix bug: shift+j is fired even when j is pressed due to useKey: true @@ -159,7 +62,7 @@ function Timeline({ const activeItem = document.activeElement.closest(itemsSelector); const activeItemRect = activeItem?.getBoundingClientRect(); const allItems = Array.from( - scrollableRef.current.querySelectorAll(itemsSelector), + scrollableRef.current?.querySelectorAll(itemsSelector) || [], ).filter((item) => !!item.offsetHeight); if ( activeItem && @@ -179,12 +82,20 @@ function Timeline({ if (nextItem) { nextItem.focus(); nextItem.scrollIntoView(scrollIntoViewOptions); + } else { + const nextPaginationButton = scrollableRef.current.querySelector( + paginationNextSelector, + ); + if (nextPaginationButton) { + nextPaginationButton.click(); + } } } else { // If active status is not in viewport, get the topmost status-link in viewport const topmostItem = allItems.find((item) => { const itemRect = item.getBoundingClientRect(); return itemRect.top >= 44 && itemRect.left >= 0; // 44 is the magic number for header height, not real + return itemRect.top >= 44 && itemRect.left >= 0; }); if (topmostItem) { topmostItem.focus(); @@ -198,18 +109,19 @@ function Timeline({ e.metaKey || e.ctrlKey || e.altKey || e.key.toLowerCase() !== 'j', }, ); +} - const kRef = useHotkeys( +export function useKHotkeys(scrollableRef) { + return useHotkeys( 'k, shift+k', (e, handler) => { // Fix bug: shift+k is fired even when k is pressed due to useKey: true if (e.shiftKey !== handler.shift) return; - // focus on previous status after active item const activeItem = document.activeElement.closest(itemsSelector); const activeItemRect = activeItem?.getBoundingClientRect(); const allItems = Array.from( - scrollableRef.current.querySelectorAll(itemsSelector), + scrollableRef.current?.querySelectorAll(itemsSelector) || [], ).filter((item) => !!item.offsetHeight); if ( activeItem && @@ -229,12 +141,20 @@ function Timeline({ if (prevItem) { prevItem.focus(); prevItem.scrollIntoView(scrollIntoViewOptions); + } else { + const prevPaginationButton = scrollableRef.current.querySelector( + paginationPrevSelector, + ); + if (prevPaginationButton) { + prevPaginationButton.click(); + } } } else { // If active status is not in viewport, get the topmost status-link in viewport const topmostItem = allItems.find((item) => { const itemRect = item.getBoundingClientRect(); return itemRect.top >= 44 && itemRect.left >= 0; // 44 is the magic number for header height, not real + return itemRect.top >= 44 && itemRect.left >= 0; }); if (topmostItem) { topmostItem.focus(); @@ -248,8 +168,10 @@ function Timeline({ e.metaKey || e.ctrlKey || e.altKey || e.key.toLowerCase() !== 'k', }, ); +} - const oRef = useHotkeys( +export function useOHotkeys() { + return useHotkeys( ['enter', 'o'], (e, handler) => { // open active status @@ -296,6 +218,115 @@ function Timeline({ }, }, ); +} + +function Timeline({ + title, + titleComponent, + id, + instance, + emptyText, + errorText, + useItemID, // use statusID instead of status object, assuming it's already in states + boostsCarousel, + fetchItems = () => {}, + checkForUpdates = () => {}, + checkForUpdatesInterval = 15_000, // 15 seconds + headerStart, + headerEnd, + timelineStart, + // allowFilters, + refresh, + view, + filterContext, + showFollowedTags, + showReplyParent, + clearWhenRefresh, +}) { + const { t } = useLingui(); + const snapStates = useSnapshot(states); + const [items, setItems] = useState([]); + const [uiState, setUIState] = useState('start'); + const [showMore, setShowMore] = useState(false); + const [showNew, setShowNew] = useState(false); + const [visible, setVisible] = useState(true); + const scrollableRef = useRef(); + + console.debug('RENDER Timeline', id, refresh); + __BENCHMARK.start(`timeline-${id}-load`); + + const mediaFirst = useMemo(() => isMediaFirstInstance(), []); + + const allowGrouping = view !== 'media'; + const loadItemsTS = useRef(0); // Ensures only one loadItems at a time + const loadItems = useDebouncedCallback( + (firstLoad) => { + setShowNew(false); + // if (uiState === 'loading') return; + setUIState('loading'); + (async () => { + try { + const ts = (loadItemsTS.current = Date.now()); + let { done, value } = await fetchItems(firstLoad); + if (ts !== loadItemsTS.current) return; + if (Array.isArray(value)) { + // Avoid grouping for pinned posts + const [pinnedPosts, otherPosts] = value.reduce( + (acc, item) => { + if (item._pinned) { + acc[0].push(item); + } else { + acc[1].push(item); + } + return acc; + }, + [[], []], + ); + value = otherPosts; + value = filterHiddenStatuses(value, filterContext); + if (allowGrouping) { + if (boostsCarousel) { + value = groupBoosts(value); + } + value = groupContext(value, instance); + } + if (pinnedPosts.length) { + value = pinnedPosts.concat(value); + } + console.log(value); + if (firstLoad) { + setItems(value); + } else { + setItems((items) => [...items, ...value]); + } + if (!value.length) done = true; + setShowMore(!done); + } else { + setShowMore(false); + } + setUIState('default'); + __BENCHMARK.end(`timeline-${id}-load`); + } catch (e) { + console.error(e); + setUIState('error'); + if (firstLoad && !items.length && errorText) { + showToast(errorText); + } + } finally { + loadItems.cancel(); + } + })(); + }, + 1_000, + { + leading: true, + // trailing: false, + }, + ); + + const jRef = useJHotkeys(scrollableRef); + const kRef = useKHotkeys(scrollableRef); + const oRef = useOHotkeys(); const showNewPostsIndicator = items.length > 0 && uiState !== 'loading' && showNew; @@ -632,7 +663,7 @@ function Timeline({ ); } -const TimelineItem = memo( +export const TimelineItem = memo( ({ status, instance, @@ -986,7 +1017,7 @@ function StatusCarousel({ title, class: className, children }) { ); } -function TimelineStatusCompact({ status, instance, filterContext }) { +export function TimelineStatusCompact({ status, instance, filterContext }) { const { t } = useLingui(); const snapStates = useSnapshot(states); const { id, visibility, language } = status; diff --git a/src/components/timeline2.css b/src/components/timeline2.css new file mode 100644 index 00000000..3ba9155e --- /dev/null +++ b/src/components/timeline2.css @@ -0,0 +1,109 @@ +.timeline-2-container { + scroll-behavior: auto; + overflow-anchor: auto; + + > * { + /* overflow-anchor: none; */ + } + + .timeline-pagination { + border-bottom: var(--hairline-width) solid var(--divider-color); + display: flex; + gap: 1px; + background-color: var(--bg-faded-color); + + @media (min-width: 40em) { + border-bottom: 0; + column-gap: 16px; + } + + &.timeline-pagination-top { + @media (min-width: 40em) { + margin-block-start: 16px; + } + + &.transitioning { + transition: 0.3s var(--timing-function); + transition-property: height, opacity, margin-block-start, transform; + @starting-style { + height: 0; + opacity: 0; + overflow: hidden; + margin-block-start: 0; + transform: scale(0.975); + } + } + } + + &.timeline-pagination-bottom { + margin-block-end: 16px; + /* button { + height: 160px; + } */ + } + + button { + height: 44px; + background-color: var(--bg-color); + border-radius: 0; + backdrop-filter: none; + transition: width 0.3s var(--timing-function); + + @media (min-width: 40em) { + border: 1px solid var(--divider-color); + border-radius: 16px; + } + + > * { + opacity: 0.75; + } + &:hover { + filter: none !important; + + > * { + opacity: 1; + } + } + &:active { + filter: brightness(0.9) !important; + } + + @media (hover: hover) { + &:hover { + background-color: var(--link-bg-hover-color); + } + } + + > .loader-container { + opacity: 1; + margin: 0; + } + } + } + + .timeline { + > li { + content-visibility: visible !important; + transition: 0.6s var(--timing-function); + transition-property: opacity, transform; + } + } + + /* 20 = API call pagination limit */ + &.scrolling-next { + .timeline > li { + @starting-style { + opacity: 0; + transform: translateY(44px) scale(0.975); + } + } + } + &.scrolling-prev { + .timeline > li { + @starting-style { + opacity: 0; + transform: translateY(-44px) scale(0.975); + } + } + } +} diff --git a/src/components/timeline2.jsx b/src/components/timeline2.jsx new file mode 100644 index 00000000..9b8e11a4 --- /dev/null +++ b/src/components/timeline2.jsx @@ -0,0 +1,692 @@ +import './timeline2.css'; + +import { Trans, useLingui } from '@lingui/react/macro'; +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from 'preact/hooks'; +import { useDebouncedCallback, useThrottledCallback } from 'use-debounce'; + +import { api } from '../utils/api'; +import FilterContext from '../utils/filter-context'; +import states, { saveStatus, statusKey } from '../utils/states'; +import store from '../utils/store'; +import { + dedupeBoosts, + filterHiddenStatuses, + groupContext, +} from '../utils/timeline-utils'; +import useInterval from '../utils/useInterval'; +import usePageVisibility from '../utils/usePageVisibility'; +import useScrollFn from '../utils/useScrollFn'; + +import Icon from './icon'; +import Link from './link'; +import Loader from './loader'; +import NavMenu from './nav-menu'; +import Status from './status'; +import { + TimelineItem, + useJHotkeys, + useKHotkeys, + useOHotkeys, +} from './timeline'; + +// Batch size (Mastodon API limit is around 20-40) +const BATCH_SIZE = 20; +const TIMELINE_LIMIT = 50; +const CACHE_AGE = 1000 * 60 * 15; // 15 minutes + +function getLastItem(items) { + const item = items[items.length - 1]; + return item.items ? item.items[item.items.length - 1] : item; +} + +function getFirstItem(items) { + const item = items[0]; + return item.items ? item.items[0] : item; +} + +function getScrollAnchor(scrollable) { + if (!scrollable) return null; + + const containerRect = scrollable.getBoundingClientRect(); + const itemElements = scrollable.querySelectorAll('[data-state-post-id]'); + + for (const el of itemElements) { + const rect = el.getBoundingClientRect(); + // Check if item is visible in viewport (at least partially) + if ( + rect.bottom > containerRect.top + 10 && + rect.top < containerRect.bottom - 10 + ) { + const postID = el.dataset.statePostId?.split?.(' '); + if (postID) { + return { + itemId: postID, + offset: rect.top - containerRect.top, + }; + } + } + } + return null; +} + +function Timeline2({ + title, + titleComponent, + id, + instance, + emptyText, + errorText, + useItemID, + fetchItems = () => {}, + checkForUpdates = () => {}, + checkForUpdatesInterval = 15_000, + headerStart, + headerEnd, + timelineStart, + refresh, + filterContext, + showFollowedTags, + showReplyParent, + dedupeBoosts: shouldDedupeBoosts, + // clearWhenRefresh, +}) { + const { t } = useLingui(); + const { masto } = api({ instance }); + + const cacheKey = `timeline2-${id}`; + const cachedData = useRef(null); + if (cachedData.current === null) { + cachedData.current = store.account.get(cacheKey) || null; + } + const hasCachedData = !!cachedData.current?.items?.length; + const cachedUpdatedAt = cachedData.current?.updatedAt; + const cacheAge = cachedUpdatedAt ? Date.now() - cachedUpdatedAt : 0; + + const loadStateRef = useRef(); + const [uiState, setUIState] = useState(hasCachedData ? 'default' : 'start'); + const [showNewer, setShowNewer] = useState( + (cachedData.current?.showNewer ?? false) || cacheAge > CACHE_AGE, + ); + const [showOlder, setShowOlder] = useState( + cachedData.current?.showOlder ?? true, + ); + const [visible, setVisible] = useState(true); + const scrollableRef = useRef(); + + const firstLoad = useRef(true); + const [items, setItems] = useState(() => { + const cached = cachedData.current; + console.log('🔍 Restore', { + cached, + itemsCount: cached?.items?.length, + }); + const items = cached?.items; + if (!items?.length) return []; + // Populate statuses + items.forEach((item) => { + if (item.items) { + item.items.forEach((subItem) => { + saveStatus(subItem, instance, { sync: true }); + }); + } else { + saveStatus(item, instance, { sync: true }); + } + }); + return items; + }); + + // Hydrate cached statuses on mount and when page becomes visible + const hydrateCache = useCallback(() => { + const cached = cachedData.current; + if (!cached?.items?.length) return; + const cacheAge = cached.updatedAt + ? Date.now() - cached.updatedAt + : Infinity; + if (cacheAge <= CACHE_AGE) return; + + const statusIds = []; + cached.items.forEach((item) => { + if (item.items) { + item.items.forEach((subItem) => { + statusIds.push(subItem.id); + }); + } else { + statusIds.push(item.id); + } + }); + + if (statusIds.length === 0) return; + + const deletedStatuses = []; + (async () => { + try { + // Process in batches + for (let i = 0; i < statusIds.length; i += BATCH_SIZE) { + const batchIds = statusIds.slice(i, i + BATCH_SIZE); + try { + const hydratedStatuses = await masto.v1.statuses.list({ + id: batchIds, + }); + const returnedIds = new Set( + hydratedStatuses?.map((s) => s.id) || [], + ); + // Track deleted statuses (not in returnedIds) + batchIds.forEach((id) => { + if (!returnedIds.has(id)) { + deletedStatuses.push(id); + } + }); + if (hydratedStatuses?.length) { + hydratedStatuses.forEach((status) => { + saveStatus(status, instance, { sync: true }); + }); + } + } catch (e) { + console.error('Failed to hydrate batch:', e); + } + } + + // Mark deleted statuses + deletedStatuses.forEach((id) => { + const key = statusKey(id, instance); + if (states.statuses[key]) { + states.statuses[key]._deleted = true; + } + }); + console.log('🔍 Hydrated', { + statusIds, + deletedStatuses, + }); + } catch (e) { + console.error('Failed to hydrate statuses:', e); + } + })(); + }, [instance, masto]); + + useEffect(() => { + hydrateCache(); + }, []); + + usePageVisibility( + (visible) => { + if (visible) hydrateCache(); + }, + [hydrateCache], + ); + + const scrollAnchorRef = useRef(cachedData.current?.scrollAnchor || null); + + const saveScrollAnchor = useCallback(({ items, direction }) => { + console.log('🔍 saveScrollAnchor', { + direction, + items, + }); + if (!items?.length) return; + if (!scrollableRef.current) return; + const getItem = direction === 'next' ? getLastItem : getFirstItem; + const item = getItem(items); + const postID = statusKey(item?.id, instance); + const targetElement = scrollableRef.current.querySelector( + `[data-state-post-id~="${postID}"]`, + ); + console.log('🔍 saveScrollAnchor 2', { + postID, + targetElement, + direction, + items, + }); + if (targetElement) { + const containerRect = scrollableRef.current.getBoundingClientRect(); + const targetRect = targetElement.getBoundingClientRect(); + scrollAnchorRef.current = { + itemId: postID, + offset: targetRect.top - containerRect.top, + direction, + }; + } else { + console.warn('🔍 Target element not found', { + postID, + targetElement, + direction, + items, + }); + } + }, []); + + console.debug('RENDER Timeline2', id, refresh); + __BENCHMARK.start(`timeline-${id}-load`); + + const minID = useRef(cachedData.current?.minID || null); + const maxID = useRef(cachedData.current?.maxID || null); + + const loadItems = useDebouncedCallback( + (params = {}) => { + console.log('🔍 loadItems', { params }); + const { max_id, min_id } = params; + const loadState = + !max_id && !min_id ? 'start' : max_id ? 'next' : min_id ? 'prev' : null; + loadStateRef.current = loadState; + setUIState('loading'); + (async () => { + try { + let result = await fetchItems(params); + + const { max_id, min_id } = params; + let { value, originalValue, done } = result; + const hasOlder = !done; + const minIDValue = originalValue[0]?.id; + const maxIDValue = originalValue[originalValue.length - 1]?.id; + console.log('🔍 loadItems result', result); + + if (value?.length) { + if (shouldDedupeBoosts) { + value = dedupeBoosts(value, instance); + } + value = filterHiddenStatuses(value, filterContext); + value = groupContext(value, instance); + + if (loadState === 'start') { + minID.current = minIDValue; + maxID.current = maxIDValue; + setItems(value); + setShowOlder(hasOlder); + setShowNewer(false); + } else if (loadState === 'next') { + saveScrollAnchor({ items, direction: 'next' }); + maxID.current = maxIDValue; + scrollableRef.current.classList.add('scrolling-next'); + // headerRef.current.hidden = true; + // resetScrollDirection(); + setItems((prevItems) => { + const newItems = [...prevItems, ...value].slice( + -TIMELINE_LIMIT, + ); + minID.current = [newItems[0].id].flat()[0]; + return newItems; + }); + setShowOlder(hasOlder); + setShowNewer(true); + } else if (loadState === 'prev') { + saveScrollAnchor({ items, direction: 'prev' }); + minID.current = minIDValue; + scrollableRef.current.classList.add('scrolling-prev'); + // headerRef.current.hidden = true; + // resetScrollDirection(); + setItems((prevItems) => { + const newItems = [...value, ...prevItems].slice( + 0, + TIMELINE_LIMIT, + ); + maxID.current = [newItems.at(-1).id].flat().at(-1); + return newItems; + }); + setShowOlder(true); + setShowNewer(false); + } + } else { + // No items + if (max_id) { + setShowOlder(false); + } + if (min_id) { + setShowNewer(false); + } + } + setUIState('default'); + __BENCHMARK.end(`timeline-${id}-load`); + } catch (e) { + console.error(e); + setUIState('error'); + } finally { + loadItems.cancel(); + } + })(); + }, + 300, + { leading: true }, + ); + + const jRef = useJHotkeys(scrollableRef); + const kRef = useKHotkeys(scrollableRef); + const oRef = useOHotkeys(); + + const headerRef = useRef(); + + // Cache items whenever they change + useEffect(() => { + if (firstLoad.current) return; + if (items.length > 0) { + console.log('🔍 Cache items', { items }); + const existing = store.account.get(cacheKey) || {}; + store.account.set(cacheKey, { + ...existing, + items, + minID: minID.current, + maxID: maxID.current, + showNewer, + showOlder, + updatedAt: Date.now(), + }); + } else { + store.account.del(cacheKey); + } + }, [items, cacheKey, showNewer, showOlder]); + + // Throttled scroll handler to cache scroll position + const cacheScrollAnchor = useThrottledCallback(() => { + if (!scrollableRef.current || items.length === 0) return; + + const scrollAnchor = getScrollAnchor(scrollableRef.current); + if (scrollAnchor) { + const cached = store.account.get(cacheKey) || {}; + store.account.set(cacheKey, { + ...cached, + scrollAnchor, + }); + } + }, 500); + + const scrollFnCallback = useCallback( + ({ scrollDirection, nearReachStart }) => { + if (headerRef.current) { + console.log('🔍 scrollFnCallback', { + scrollDirection, + nearReachStart, + }); + headerRef.current.hidden = scrollDirection === 'end' && !nearReachStart; + } + // Cache scroll position on scroll + cacheScrollAnchor(); + }, + [cacheScrollAnchor], + ); + const { resetScrollDirection } = useScrollFn( + { + scrollableRef, + distanceFromEnd: 2, + scrollThresholdStart: 44, + }, + scrollFnCallback, + ); + + // Restore from cache or load fresh items on mount + useEffect(() => { + if (firstLoad.current) { + firstLoad.current = false; + // If not restored from cache, load fresh items + if (!cachedData.current?.items?.length) { + loadItems(); + } else { + // If from cache, check for updates + checkUpdates(); + } + } + }, [loadItems]); + + // useEffect(() => { + // if (firstLoad.current) return; + // if (clearWhenRefresh && items?.length) { + // loadItems.cancel?.(); + // setItems([]); + // } + // loadItems(); + // }, [clearWhenRefresh, refresh]); + + const checkUpdates = useCallback(async () => { + if (!minID.current) return; + const hasUpdates = await checkForUpdates({ + minID: minID.current, + }); + setShowNewer(hasUpdates); + }, [checkForUpdates]); + + const lastHiddenTime = useRef(); + usePageVisibility( + (visible) => { + if (firstLoad.current) return; + if (visible) { + const timeDiff = Date.now() - lastHiddenTime.current; + if (!lastHiddenTime.current || timeDiff > 1000 * 3) { + checkUpdates(); + } + } else { + lastHiddenTime.current = Date.now(); + } + setVisible(visible); + }, + [checkUpdates], + ); + + useInterval( + checkUpdates, + visible && !showNewer ? checkForUpdatesInterval : null, + ); + + useLayoutEffect(() => { + if (uiState !== 'default') return; + console.log('🔍 Scroll', { + scrollableRef: scrollableRef.current, + scrollAnchorRef: scrollAnchorRef.current, + }); + if (!scrollableRef.current || !scrollAnchorRef.current) return; + + const { itemId, offset, direction } = scrollAnchorRef.current; + const targetElement = scrollableRef.current.querySelector( + `[data-state-post-id~="${itemId}"]`, + ); + console.log('🔍 Scroll to?', { itemId, offset, targetElement }); + + if (targetElement) { + const containerRect = scrollableRef.current.getBoundingClientRect(); + const targetRect = targetElement.getBoundingClientRect(); + const currentOffset = targetRect.top - containerRect.top; + console.log('Scrolling to', { + itemId, + offset, + containerRect, + targetRect, + currentOffset, + }); + scrollableRef.current.scrollTop += currentOffset - offset; + setTimeout(() => { + scrollableRef.current?.classList.remove(`scrolling-${direction}`); + }, 300); + } else { + console.warn('Target element not found', { + itemId, + offset, + targetElement, + }); + } + + // Clear the anchor after use + scrollAnchorRef.current = null; + }, [items, uiState]); + + return ( + +
{ + scrollableRef.current = node; + jRef.current = node; + kRef.current = node; + oRef.current = node; + }} + tabIndex="-1" + onClick={(e) => { + if ( + headerRef.current && + e.target.closest('.timeline-item, .timeline-item-alt') + ) { + setTimeout(() => { + headerRef.current.hidden = false; + resetScrollDirection(); + }, 250); + } + }} + > +
+
{ + if (!e.target.closest('a, button')) { + scrollableRef.current?.scrollTo({ + top: 0, + behavior: 'smooth', + }); + } + }} + onDblClick={(e) => { + if (!e.target.closest('a, button')) { + loadItems(); + } + }} + // class={uiState === 'loading' ? 'loading' : ''} + > +
+
+ + {headerStart !== null && headerStart !== undefined ? ( + headerStart + ) : ( + + + + )} +
+ {title && (titleComponent ? titleComponent :

{title}

)} +
{!!headerEnd && headerEnd}
+
+
+ {!!timelineStart && ( +
+ {timelineStart} +
+ )} + {!!items.length ? ( + <> + {showNewer && ( +
+ + +
+ )} +
    + {items.map((status) => ( + + ))} + {/* {uiState === 'loading' && ( + <> +
  • + +
  • +
  • + +
  • + + )} */} +
+ {showOlder ? ( +
+ +
+ ) : uiState !== 'loading' ? ( +

+ The end. +

+ ) : null} + + ) : uiState === 'loading' ? ( +
    + {Array.from({ length: 5 }).map((_, i) => ( +
  • + +
  • + ))} +
+ ) : ( + uiState !== 'error' && + uiState !== 'start' &&

{emptyText}

+ )} + {uiState === 'error' && ( +

+ {errorText} +
+
+ +

+ )} +
+
+
+ ); +} + +export default Timeline2; diff --git a/src/index.css b/src/index.css index c2f878d6..e8d71a70 100644 --- a/src/index.css +++ b/src/index.css @@ -757,6 +757,25 @@ kbd { } } +.lazy-render { + &:empty { + display: none; + } + + overflow: clip; + will-change: opacity, height, margin-block, padding-block, border-block-width; + transition: 0.3s var(--timing-function) allow-discrete; + transition-property: + opacity, height, margin-block, padding-block, border-block-width; + &.hidden { + opacity: 0; + height: 0; + margin-block: 0; + padding-block: 0; + border-block-width: 0; + } +} + @keyframes spin { to { transform: rotate(360deg); diff --git a/src/locales/en.po b/src/locales/en.po index c2420cd8..4cc61429 100644 --- a/src/locales/en.po +++ b/src/locales/en.po @@ -34,7 +34,7 @@ msgstr "" #: src/components/account-block.jsx:177 #: src/components/account-info.jsx:731 -#: src/components/status.jsx:605 +#: src/components/status.jsx:606 msgid "Group" msgstr "" @@ -84,7 +84,7 @@ msgstr "" #: src/components/account-info-mini.jsx:57 #: src/components/account-info.jsx:796 #: src/components/compose.jsx:2029 -#: src/pages/settings.jsx:296 +#: src/pages/settings.jsx:300 #: src/utils/visibility-text.jsx:7 msgid "Followers" msgstr "Followers" @@ -221,7 +221,7 @@ msgid "Original" msgstr "" #: src/components/account-info.jsx:1060 -#: src/components/status.jsx:2755 +#: src/components/status.jsx:2756 #: src/pages/catchup.jsx:74 #: src/pages/catchup.jsx:1594 #: src/pages/catchup.jsx:2249 @@ -233,7 +233,7 @@ msgstr "" #: src/components/account-info.jsx:1065 #: src/components/quotes-modal.jsx:81 -#: src/components/status.jsx:2760 +#: src/components/status.jsx:2761 #: src/pages/catchup.jsx:75 #: src/pages/catchup.jsx:1597 #: src/pages/year-in-posts.jsx:1400 @@ -244,7 +244,7 @@ msgstr "Quotes" #: src/pages/catchup.jsx:76 #: src/pages/catchup.jsx:1596 #: src/pages/catchup.jsx:2261 -#: src/pages/settings.jsx:1269 +#: src/pages/settings.jsx:1303 #: src/pages/year-in-posts.jsx:1406 msgid "Boosts" msgstr "" @@ -287,8 +287,8 @@ msgstr "View post stats" #: src/components/shortcuts-settings.jsx:247 #: src/components/shortcuts-settings.jsx:600 #: src/components/shortcuts-settings.jsx:801 -#: src/components/status.jsx:3413 -#: src/components/status.jsx:3625 +#: src/components/status.jsx:3414 +#: src/components/status.jsx:3626 #: src/components/translated-bio-sheet.jsx:21 #: src/pages/accounts.jsx:45 #: src/pages/catchup.jsx:1731 @@ -296,7 +296,7 @@ msgstr "View post stats" #: src/pages/list.jsx:302 #: src/pages/notifications.jsx:1055 #: src/pages/scheduled-posts.jsx:288 -#: src/pages/settings.jsx:100 +#: src/pages/settings.jsx:104 #: src/pages/status.jsx:1671 msgid "Close" msgstr "" @@ -346,11 +346,13 @@ msgstr "More from <0/>" #: src/components/columns.jsx:29 #: src/components/nav-menu.jsx:187 #: src/components/shortcuts-settings.jsx:142 -#: src/components/timeline.jsx:508 +#: src/components/timeline.jsx:539 +#: src/components/timeline2.jsx:561 #: src/pages/catchup.jsx:1027 #: src/pages/filters.jsx:90 #: src/pages/followed-hashtags.jsx:41 -#: src/pages/home.jsx:54 +#: src/pages/home.jsx:61 +#: src/pages/home.jsx:69 #: src/pages/notifications.jsx:688 #: src/pages/scheduled-posts.jsx:74 msgid "Home" @@ -506,8 +508,8 @@ msgid "Attachment #{i} failed" msgstr "Attachment #{i} failed" #: src/components/compose.jsx:1493 -#: src/components/status.jsx:2517 -#: src/components/timeline.jsx:1034 +#: src/components/status.jsx:2518 +#: src/components/timeline.jsx:1065 msgid "Content warning" msgstr "" @@ -547,22 +549,22 @@ msgstr "Posting on <0/>" #: src/components/compose.jsx:1949 #: src/components/quote-settings-sheet.jsx:82 -#: src/components/status.jsx:296 -#: src/pages/settings.jsx:338 +#: src/components/status.jsx:297 +#: src/pages/settings.jsx:342 msgid "Anyone can quote" msgstr "Anyone can quote" #: src/components/compose.jsx:1952 #: src/components/quote-settings-sheet.jsx:85 -#: src/components/status.jsx:297 -#: src/pages/settings.jsx:341 +#: src/components/status.jsx:298 +#: src/pages/settings.jsx:345 msgid "Your followers can quote" msgstr "Your followers can quote" #: src/components/compose.jsx:1955 #: src/components/quote-settings-sheet.jsx:88 -#: src/components/status.jsx:298 -#: src/pages/settings.jsx:344 +#: src/components/status.jsx:299 +#: src/pages/settings.jsx:348 msgid "Only you can quote" msgstr "Only you can quote" @@ -571,7 +573,7 @@ msgid "Quotes can't be embedded in private mentions." msgstr "Quotes can't be embedded in private mentions." #: src/components/compose.jsx:2017 -#: src/pages/settings.jsx:290 +#: src/pages/settings.jsx:294 #: src/utils/visibility-text.jsx:4 msgid "Public" msgstr "" @@ -584,13 +586,13 @@ msgid "Local" msgstr "" #: src/components/compose.jsx:2026 -#: src/pages/settings.jsx:293 +#: src/pages/settings.jsx:297 #: src/utils/visibility-text.jsx:6 msgid "Quiet public" msgstr "Quiet public" #: src/components/compose.jsx:2032 -#: src/components/status.jsx:2394 +#: src/components/status.jsx:2395 #: src/utils/visibility-text.jsx:8 msgid "Private mention" msgstr "" @@ -601,12 +603,12 @@ msgstr "Schedule" #: src/components/compose.jsx:2086 #: src/components/keyboard-shortcuts-help.jsx:165 -#: src/components/status.jsx:1099 -#: src/components/status.jsx:1133 -#: src/components/status.jsx:2148 +#: src/components/status.jsx:1100 +#: src/components/status.jsx:1134 #: src/components/status.jsx:2149 -#: src/components/status.jsx:2887 -#: src/components/status.jsx:2899 +#: src/components/status.jsx:2150 +#: src/components/status.jsx:2888 +#: src/components/status.jsx:2900 msgid "Reply" msgstr "" @@ -665,7 +667,7 @@ msgid "Custom emojis are not available on this server." msgstr "Custom emojis are not available on this server." #: src/components/drafts.jsx:62 -#: src/pages/settings.jsx:698 +#: src/pages/settings.jsx:732 msgid "Unsent drafts" msgstr "" @@ -683,7 +685,7 @@ msgstr "" #: src/components/drafts.jsx:126 #: src/components/list-add-edit.jsx:188 -#: src/components/status.jsx:1653 +#: src/components/status.jsx:1654 #: src/pages/filters.jsx:605 #: src/pages/scheduled-posts.jsx:398 msgid "Delete…" @@ -725,7 +727,7 @@ msgstr "" #: src/components/drafts.jsx:270 #: src/components/keyboard-shortcuts-help.jsx:194 -#: src/components/status.jsx:762 +#: src/components/status.jsx:763 msgid "Quote" msgstr "" @@ -736,14 +738,14 @@ msgstr "Edit History Snapshots" #: src/components/edit-history-controls.jsx:35 #: src/components/gif-picker-modal.jsx:208 #: src/components/media-modal.jsx:478 -#: src/components/timeline.jsx:947 +#: src/components/timeline.jsx:978 msgid "Previous" msgstr "" #: src/components/edit-history-controls.jsx:47 #: src/components/gif-picker-modal.jsx:226 #: src/components/media-modal.jsx:497 -#: src/components/timeline.jsx:964 +#: src/components/timeline.jsx:995 msgid "Next" msgstr "" @@ -834,7 +836,7 @@ msgstr "" #: src/components/generic-accounts.jsx:214 #: src/components/quotes-modal.jsx:129 -#: src/components/timeline.jsx:590 +#: src/components/timeline.jsx:621 #: src/pages/list.jsx:321 #: src/pages/notifications.jsx:1035 #: src/pages/search.jsx:576 @@ -845,7 +847,8 @@ msgstr "" #: src/components/generic-accounts.jsx:219 #: src/components/quote-chain-modal.jsx:169 #: src/components/quotes-modal.jsx:133 -#: src/components/timeline.jsx:595 +#: src/components/timeline.jsx:626 +#: src/components/timeline2.jsx:660 #: src/pages/search.jsx:581 msgid "The end." msgstr "" @@ -1054,9 +1057,9 @@ msgid "<0>l or <1>f" msgstr "" #: src/components/keyboard-shortcuts-help.jsx:186 -#: src/components/status.jsx:1169 -#: src/components/status.jsx:2930 -#: src/components/status.jsx:2996 +#: src/components/status.jsx:1170 +#: src/components/status.jsx:2931 +#: src/components/status.jsx:2997 msgid "Boost" msgstr "" @@ -1065,9 +1068,9 @@ msgid "<0>Shift + <1>b" msgstr "" #: src/components/keyboard-shortcuts-help.jsx:198 -#: src/components/status.jsx:1271 -#: src/components/status.jsx:3025 +#: src/components/status.jsx:1272 #: src/components/status.jsx:3026 +#: src/components/status.jsx:3027 msgid "Bookmark" msgstr "" @@ -1173,11 +1176,11 @@ msgstr "" #: src/components/media-attachment.jsx:430 #: src/components/media-modal.jsx:372 #: src/components/related-actions.jsx:273 -#: src/components/status.jsx:2172 -#: src/components/status.jsx:2189 -#: src/components/status.jsx:2321 -#: src/components/status.jsx:3046 -#: src/components/status.jsx:3049 +#: src/components/status.jsx:2173 +#: src/components/status.jsx:2190 +#: src/components/status.jsx:2322 +#: src/components/status.jsx:3047 +#: src/components/status.jsx:3050 #: src/pages/account-statuses.jsx:641 #: src/pages/accounts.jsx:147 #: src/pages/hashtag.jsx:232 @@ -1190,14 +1193,14 @@ msgid "More" msgstr "" #: src/components/media-alt-modal.jsx:67 -#: src/components/status.jsx:1338 -#: src/components/status.jsx:1347 +#: src/components/status.jsx:1339 +#: src/components/status.jsx:1348 #: src/components/translation-block.jsx:270 msgid "Translate" msgstr "" #: src/components/media-alt-modal.jsx:78 -#: src/components/status.jsx:1368 +#: src/components/status.jsx:1369 msgid "Speak" msgstr "" @@ -1325,10 +1328,10 @@ msgid "Filtered: {filterTitleStr}" msgstr "" #: src/components/media-post.jsx:133 -#: src/components/status-compact.jsx:70 -#: src/components/status.jsx:3551 -#: src/components/status.jsx:3629 -#: src/components/timeline.jsx:1023 +#: src/components/status-compact.jsx:73 +#: src/components/status.jsx:3552 +#: src/components/status.jsx:3630 +#: src/components/timeline.jsx:1054 #: src/pages/catchup.jsx:79 #: src/pages/catchup.jsx:2052 msgid "Filtered" @@ -1379,6 +1382,8 @@ msgstr "" #: src/components/shortcuts-settings.jsx:143 #: src/pages/following.jsx:23 #: src/pages/following.jsx:156 +#: src/pages/following2.jsx:18 +#: src/pages/following2.jsx:133 msgid "following.title" msgstr "Following" @@ -1391,11 +1396,11 @@ msgstr "" #: src/components/nav-menu.jsx:210 #: src/components/shortcuts-settings.jsx:60 #: src/components/shortcuts-settings.jsx:149 -#: src/pages/home.jsx:240 +#: src/pages/home.jsx:255 #: src/pages/mentions.jsx:25 #: src/pages/mentions.jsx:256 #: src/pages/notifications.jsx:912 -#: src/pages/settings.jsx:1261 +#: src/pages/settings.jsx:1295 #: src/pages/trending.jsx:384 msgid "Mentions" msgstr "" @@ -1404,8 +1409,8 @@ msgstr "" #: src/components/shortcuts-settings.jsx:51 #: src/components/shortcuts-settings.jsx:155 #: src/pages/filters.jsx:24 -#: src/pages/home.jsx:86 -#: src/pages/home.jsx:198 +#: src/pages/home.jsx:101 +#: src/pages/home.jsx:213 #: src/pages/notifications.jsx:111 #: src/pages/notifications.jsx:692 msgid "Notifications" @@ -1441,7 +1446,7 @@ msgstr "" #: src/pages/catchup.jsx:2255 #: src/pages/favourites.jsx:12 #: src/pages/favourites.jsx:26 -#: src/pages/settings.jsx:1265 +#: src/pages/settings.jsx:1299 msgid "Likes" msgstr "" @@ -1661,8 +1666,8 @@ msgid "[Unknown notification type: {type}]" msgstr "" #: src/components/notification.jsx:477 -#: src/components/status.jsx:1285 -#: src/components/status.jsx:1295 +#: src/components/status.jsx:1286 +#: src/components/status.jsx:1296 msgid "Boosted/Liked by…" msgstr "" @@ -1689,19 +1694,19 @@ msgstr "View #Wrapstodon" #: src/components/notification.jsx:847 #: src/components/quote-chain-modal.jsx:18 -#: src/components/status.jsx:259 +#: src/components/status.jsx:260 msgid "Read more →" msgstr "" #: src/components/open-link-sheet.jsx:30 #: src/components/related-actions.jsx:556 -#: src/components/status.jsx:1459 +#: src/components/status.jsx:1460 msgid "Link copied" msgstr "" #: src/components/open-link-sheet.jsx:33 #: src/components/related-actions.jsx:559 -#: src/components/status.jsx:1462 +#: src/components/status.jsx:1463 msgid "Unable to copy link" msgstr "" @@ -1713,13 +1718,13 @@ msgstr "Open link?" #: src/components/post-embed-modal.jsx:232 #: src/components/related-actions.jsx:565 #: src/components/shortcuts-settings.jsx:1173 -#: src/components/status.jsx:1468 +#: src/components/status.jsx:1469 msgid "Copy" msgstr "" #: src/components/open-link-sheet.jsx:82 #: src/components/related-actions.jsx:586 -#: src/components/status.jsx:1490 +#: src/components/status.jsx:1491 msgid "Share…" msgstr "" @@ -1798,7 +1803,7 @@ msgid "Refresh" msgstr "" #: src/components/post-embed-modal.jsx:201 -#: src/components/status.jsx:1503 +#: src/components/status.jsx:1504 msgid "Embed post" msgstr "" @@ -1898,12 +1903,12 @@ msgid "Continue unwrapping…" msgstr "Continue unwrapping…" #: src/components/quote-settings-sheet.jsx:37 -#: src/pages/settings.jsx:329 +#: src/pages/settings.jsx:333 msgid "Quote settings updated" msgstr "Quote settings updated" #: src/components/quote-settings-sheet.jsx:47 -#: src/pages/settings.jsx:331 +#: src/pages/settings.jsx:335 msgid "Failed to update quote settings" msgstr "Failed to update quote settings" @@ -2059,7 +2064,7 @@ msgstr "Search my posts" #: src/components/related-actions.jsx:580 #: src/components/shortcuts-settings.jsx:1191 -#: src/components/status.jsx:1484 +#: src/components/status.jsx:1485 msgid "Sharing doesn't seem to work." msgstr "" @@ -2298,7 +2303,7 @@ msgid "Look up <0>{query}" msgstr "" #: src/components/search-form.jsx:366 -#: src/pages/home.jsx:251 +#: src/pages/home.jsx:266 msgid "See all" msgstr "" @@ -2396,7 +2401,7 @@ msgid "Move down" msgstr "" #: src/components/shortcuts-settings.jsx:396 -#: src/components/status.jsx:1615 +#: src/components/status.jsx:1616 #: src/pages/list.jsx:195 msgid "Edit" msgstr "" @@ -2588,371 +2593,371 @@ msgstr "" msgid "Import/export settings from/to server (Very experimental)" msgstr "Import/export settings from/to server (Very experimental)" -#: src/components/status.jsx:288 +#: src/components/status.jsx:289 msgid "Private posts cannot be quoted" msgstr "Private posts cannot be quoted" -#: src/components/status.jsx:289 +#: src/components/status.jsx:290 msgid "Request to quote" msgstr "Request to quote" -#: src/components/status.jsx:290 +#: src/components/status.jsx:291 msgid "Author will manually review" msgstr "Author will manually review" -#: src/components/status.jsx:291 +#: src/components/status.jsx:292 msgid "Only followers can quote this post" msgstr "Only followers can quote this post" -#: src/components/status.jsx:292 +#: src/components/status.jsx:293 msgid "You are not allowed to quote this post" msgstr "You are not allowed to quote this post" -#: src/components/status.jsx:368 -#: src/components/status.jsx:3191 +#: src/components/status.jsx:369 #: src/components/status.jsx:3192 #: src/components/status.jsx:3193 +#: src/components/status.jsx:3194 msgid "Post unavailable" msgstr "Post unavailable" -#: src/components/status.jsx:629 +#: src/components/status.jsx:630 msgid "<0/> <1>boosted" msgstr "<0/> <1>boosted" -#: src/components/status.jsx:736 +#: src/components/status.jsx:737 msgid "Sorry, your current logged-in server can't interact with this post from another server." msgstr "Sorry, your current logged-in server can't interact with this post from another server." #. placeholder {0}: username || acct -#: src/components/status.jsx:925 +#: src/components/status.jsx:926 msgid "Unliked @{0}'s post" msgstr "" #. placeholder {0}: username || acct -#: src/components/status.jsx:926 +#: src/components/status.jsx:927 msgid "Liked @{0}'s post" msgstr "Liked @{0}'s post" #. placeholder {0}: username || acct -#: src/components/status.jsx:965 +#: src/components/status.jsx:966 msgid "Unbookmarked @{0}'s post" msgstr "Unbookmarked @{0}'s post" #. placeholder {0}: username || acct -#: src/components/status.jsx:966 +#: src/components/status.jsx:967 msgid "Bookmarked @{0}'s post" msgstr "Bookmarked @{0}'s post" -#: src/components/status.jsx:1070 +#: src/components/status.jsx:1071 msgid "Some media have no descriptions." msgstr "" #. placeholder {0}: rtf.format(-statusMonthsAgo, 'month') -#: src/components/status.jsx:1077 +#: src/components/status.jsx:1078 msgid "Old post (<0>{0})" msgstr "" -#: src/components/status.jsx:1098 +#: src/components/status.jsx:1099 msgid "Reply…" msgstr "Reply…" -#: src/components/status.jsx:1107 -#: src/components/status.jsx:1116 +#: src/components/status.jsx:1108 +#: src/components/status.jsx:1117 msgid "Reply all" msgstr "Reply all" -#: src/components/status.jsx:1110 +#: src/components/status.jsx:1111 msgid "{mentionsCount, plural, other {# mentions}}" msgstr "{mentionsCount, plural, other {# mentions}}" #. placeholder {0}: mentionsCount - 1 #. placeholder {1}: username || acct -#: src/components/status.jsx:1119 +#: src/components/status.jsx:1120 msgid "{0, plural, other {<0>@{1} first, # others below}}" msgstr "{0, plural, other {<0>@{1} first, # others below}}" #. placeholder {0}: username || acct -#: src/components/status.jsx:1136 +#: src/components/status.jsx:1137 msgid "Only <0>@{0}" msgstr "Only <0>@{0}" -#: src/components/status.jsx:1169 -#: src/components/status.jsx:1246 -#: src/components/status.jsx:2930 -#: src/components/status.jsx:2994 +#: src/components/status.jsx:1170 +#: src/components/status.jsx:1247 +#: src/components/status.jsx:2931 +#: src/components/status.jsx:2995 msgid "Unboost" msgstr "" -#: src/components/status.jsx:1208 -#: src/components/status.jsx:2979 +#: src/components/status.jsx:1209 +#: src/components/status.jsx:2980 msgid "Quote with link" msgstr "Quote with link" #. placeholder {0}: username || acct -#: src/components/status.jsx:1225 -#: src/components/status.jsx:1815 +#: src/components/status.jsx:1226 +#: src/components/status.jsx:1816 msgid "Unboosted @{0}'s post" msgstr "Unboosted @{0}'s post" #. placeholder {0}: username || acct -#: src/components/status.jsx:1226 -#: src/components/status.jsx:1816 +#: src/components/status.jsx:1227 +#: src/components/status.jsx:1817 msgid "Boosted @{0}'s post" msgstr "Boosted @{0}'s post" -#: src/components/status.jsx:1248 -#: src/components/status.jsx:2993 +#: src/components/status.jsx:1249 +#: src/components/status.jsx:2994 msgid "Boost/Quote…" msgstr "Boost/Quote…" -#: src/components/status.jsx:1249 -#: src/components/status.jsx:2993 +#: src/components/status.jsx:1250 +#: src/components/status.jsx:2994 msgid "Boost…" msgstr "" -#: src/components/status.jsx:1261 -#: src/components/status.jsx:2162 -#: src/components/status.jsx:3013 +#: src/components/status.jsx:1262 +#: src/components/status.jsx:2163 +#: src/components/status.jsx:3014 msgid "Unlike" msgstr "" -#: src/components/status.jsx:1262 -#: src/components/status.jsx:2162 +#: src/components/status.jsx:1263 #: src/components/status.jsx:2163 -#: src/components/status.jsx:3013 +#: src/components/status.jsx:2164 #: src/components/status.jsx:3014 +#: src/components/status.jsx:3015 msgid "Like" msgstr "" -#: src/components/status.jsx:1271 -#: src/components/status.jsx:3025 +#: src/components/status.jsx:1272 +#: src/components/status.jsx:3026 msgid "Unbookmark" msgstr "" -#: src/components/status.jsx:1306 +#: src/components/status.jsx:1307 msgid "View Quotes" msgstr "View Quotes" -#: src/components/status.jsx:1318 +#: src/components/status.jsx:1319 msgid "Unwrap quote chain" msgstr "Unwrap quote chain" -#: src/components/status.jsx:1385 +#: src/components/status.jsx:1386 msgid "Post text copied" msgstr "Post text copied" -#: src/components/status.jsx:1388 +#: src/components/status.jsx:1389 msgid "Unable to copy post text" msgstr "Unable to copy post text" -#: src/components/status.jsx:1394 +#: src/components/status.jsx:1395 msgid "Copy post text" msgstr "Copy post text" #. placeholder {0}: username || acct -#: src/components/status.jsx:1412 +#: src/components/status.jsx:1413 msgid "View post by <0>@{0}" msgstr "" -#: src/components/status.jsx:1433 +#: src/components/status.jsx:1434 msgid "Show Edit History" msgstr "" -#: src/components/status.jsx:1436 +#: src/components/status.jsx:1437 msgid "Edited: {editedDateText}" msgstr "" -#: src/components/status.jsx:1519 +#: src/components/status.jsx:1520 msgid "Conversation unmuted" msgstr "" -#: src/components/status.jsx:1519 +#: src/components/status.jsx:1520 msgid "Conversation muted" msgstr "" -#: src/components/status.jsx:1525 +#: src/components/status.jsx:1526 msgid "Unable to unmute conversation" msgstr "" -#: src/components/status.jsx:1526 +#: src/components/status.jsx:1527 msgid "Unable to mute conversation" msgstr "" -#: src/components/status.jsx:1535 +#: src/components/status.jsx:1536 msgid "Unmute conversation" msgstr "" -#: src/components/status.jsx:1542 +#: src/components/status.jsx:1543 msgid "Mute conversation" msgstr "" -#: src/components/status.jsx:1558 +#: src/components/status.jsx:1559 msgid "Post unpinned from profile" msgstr "" -#: src/components/status.jsx:1559 +#: src/components/status.jsx:1560 msgid "Post pinned to profile" msgstr "" -#: src/components/status.jsx:1564 +#: src/components/status.jsx:1565 msgid "Unable to unpin post" msgstr "" -#: src/components/status.jsx:1564 +#: src/components/status.jsx:1565 msgid "Unable to pin post" msgstr "" -#: src/components/status.jsx:1573 +#: src/components/status.jsx:1574 msgid "Unpin from profile" msgstr "" -#: src/components/status.jsx:1580 +#: src/components/status.jsx:1581 msgid "Pin to profile" msgstr "" -#: src/components/status.jsx:1593 -#: src/pages/settings.jsx:303 +#: src/components/status.jsx:1594 +#: src/pages/settings.jsx:307 msgid "Quote settings" msgstr "Quote settings" -#: src/components/status.jsx:1626 +#: src/components/status.jsx:1627 msgid "Delete this post?" msgstr "" -#: src/components/status.jsx:1642 +#: src/components/status.jsx:1643 msgid "Post deleted" msgstr "" -#: src/components/status.jsx:1645 +#: src/components/status.jsx:1646 msgid "Unable to delete post" msgstr "" #. placeholder {0}: username || acct -#: src/components/status.jsx:1670 +#: src/components/status.jsx:1671 msgid "Remove my post from <0>@{0}'s post?" msgstr "Remove my post from <0>@{0}'s post?" -#: src/components/status.jsx:1691 +#: src/components/status.jsx:1692 msgid "Quote removed" msgstr "Quote removed" -#: src/components/status.jsx:1695 +#: src/components/status.jsx:1696 msgid "Unable to remove quote" msgstr "Unable to remove quote" -#: src/components/status.jsx:1701 +#: src/components/status.jsx:1702 msgid "Remove quote…" msgstr "Remove quote…" -#: src/components/status.jsx:1715 +#: src/components/status.jsx:1716 msgid "Report post…" msgstr "" -#: src/components/status.jsx:2163 -#: src/components/status.jsx:2199 -#: src/components/status.jsx:3014 +#: src/components/status.jsx:2164 +#: src/components/status.jsx:2200 +#: src/components/status.jsx:3015 msgid "Liked" msgstr "" -#: src/components/status.jsx:2196 -#: src/components/status.jsx:2996 +#: src/components/status.jsx:2197 +#: src/components/status.jsx:2997 msgid "Boosted" msgstr "" -#: src/components/status.jsx:2206 -#: src/components/status.jsx:3026 +#: src/components/status.jsx:2207 +#: src/components/status.jsx:3027 msgid "Bookmarked" msgstr "" -#: src/components/status.jsx:2210 +#: src/components/status.jsx:2211 msgid "Pinned" msgstr "" -#: src/components/status.jsx:2267 -#: src/components/status.jsx:2770 +#: src/components/status.jsx:2268 +#: src/components/status.jsx:2771 msgid "Deleted" msgstr "" -#: src/components/status.jsx:2305 -#: src/components/status.jsx:2372 +#: src/components/status.jsx:2306 +#: src/components/status.jsx:2373 msgid "{repliesCount, plural, one {# reply} other {# replies}}" msgstr "" -#: src/components/status.jsx:2317 -#: src/components/status.jsx:2384 -#: src/components/status.jsx:2802 +#: src/components/status.jsx:2318 +#: src/components/status.jsx:2385 +#: src/components/status.jsx:2803 msgid "Edited" msgstr "" -#: src/components/status.jsx:2480 -#: src/components/status.jsx:2542 -#: src/components/status.jsx:2649 +#: src/components/status.jsx:2481 +#: src/components/status.jsx:2543 +#: src/components/status.jsx:2650 msgid "Show less" msgstr "" -#: src/components/status.jsx:2480 -#: src/components/status.jsx:2542 +#: src/components/status.jsx:2481 +#: src/components/status.jsx:2543 msgid "Show content" msgstr "" #. placeholder {0}: filterInfo.titlesStr #. placeholder {0}: filterInfo?.titlesStr -#: src/components/status.jsx:2645 +#: src/components/status.jsx:2646 #: src/pages/catchup.jsx:2051 msgid "Filtered: {0}" msgstr "Filtered: {0}" -#: src/components/status.jsx:2649 +#: src/components/status.jsx:2650 msgid "Show media" msgstr "" -#: src/components/status.jsx:2888 -#: src/components/status.jsx:2900 +#: src/components/status.jsx:2889 +#: src/components/status.jsx:2901 msgid "Comments" msgstr "" -#: src/components/status.jsx:3189 +#: src/components/status.jsx:3190 msgid "Post hidden by your filters" msgstr "Post hidden by your filters" -#: src/components/status.jsx:3190 +#: src/components/status.jsx:3191 msgid "Post pending" msgstr "Post pending" -#: src/components/status.jsx:3194 +#: src/components/status.jsx:3195 msgid "Post removed by author" msgstr "Post removed by author" -#: src/components/status.jsx:3195 +#: src/components/status.jsx:3196 msgid "Post hidden because you've blocked @{name}." msgstr "Post hidden because you've blocked @{name}." -#: src/components/status.jsx:3197 +#: src/components/status.jsx:3198 msgid "Post hidden because you've blocked {domain}." msgstr "Post hidden because you've blocked {domain}." -#: src/components/status.jsx:3198 +#: src/components/status.jsx:3199 msgid "Post hidden because you've muted @{name}." msgstr "Post hidden because you've muted @{name}." -#: src/components/status.jsx:3273 +#: src/components/status.jsx:3274 msgid "Show anyway" msgstr "Show anyway" -#: src/components/status.jsx:3418 +#: src/components/status.jsx:3419 msgid "Edit History" msgstr "" -#: src/components/status.jsx:3422 +#: src/components/status.jsx:3423 msgid "Failed to load history" msgstr "" -#: src/components/status.jsx:3427 +#: src/components/status.jsx:3428 msgid "Loading…" msgstr "" #. [Name] [Visibility icon] boosted -#: src/components/status.jsx:3559 +#: src/components/status.jsx:3560 msgid "<0/> <1/> boosted" msgstr "<0/> <1/> boosted" @@ -2982,13 +2987,14 @@ msgstr "{index}/{total}" msgid "{index}/X" msgstr "{index}/X" -#: src/components/timeline.jsx:524 -#: src/pages/settings.jsx:1289 +#: src/components/timeline.jsx:555 +#: src/pages/settings.jsx:1323 msgid "New posts" msgstr "" -#: src/components/timeline.jsx:625 -#: src/pages/home.jsx:228 +#: src/components/timeline.jsx:656 +#: src/components/timeline2.jsx:682 +#: src/pages/home.jsx:243 #: src/pages/notifications.jsx:1011 #: src/pages/status.jsx:1260 #: src/pages/status.jsx:1734 @@ -2996,16 +3002,16 @@ msgid "Try again" msgstr "" #. placeholder {0}: fItems.length -#: src/components/timeline.jsx:660 +#: src/components/timeline.jsx:691 msgid "{0, plural, one {# Boost} other {# Boosts}}" msgstr "{0, plural, one {# Boost} other {# Boosts}}" -#: src/components/timeline.jsx:665 +#: src/components/timeline.jsx:696 msgid "Pinned posts" msgstr "Pinned posts" #. placeholder {0}: filterInfo.titlesStr -#: src/components/timeline.jsx:1018 +#: src/components/timeline.jsx:1049 msgid "<0>Filtered: <1>{0}" msgstr "" @@ -3640,10 +3646,12 @@ msgid "No hashtags followed yet." msgstr "" #: src/pages/following.jsx:158 +#: src/pages/following2.jsx:135 msgid "Nothing to see here." msgstr "" #: src/pages/following.jsx:159 +#: src/pages/following2.jsx:136 #: src/pages/list.jsx:109 msgid "Unable to load posts." msgstr "" @@ -3763,11 +3771,11 @@ msgstr "Go to another server…" msgid "Go to my server (<0>{currentInstance})" msgstr "Go to my server (<0>{currentInstance})" -#: src/pages/home.jsx:224 +#: src/pages/home.jsx:239 msgid "Unable to fetch notifications." msgstr "" -#: src/pages/home.jsx:245 +#: src/pages/home.jsx:260 msgid "<0>New <1>Follow Requests" msgstr "" @@ -3890,7 +3898,7 @@ msgid "{0, plural, one {Announcement} other {Announcements}}" msgstr "" #: src/pages/notifications.jsx:782 -#: src/pages/settings.jsx:1277 +#: src/pages/settings.jsx:1311 msgid "Follow requests" msgstr "" @@ -4107,249 +4115,257 @@ msgstr "" msgid "Enter your search term or paste a URL above to get started." msgstr "" -#: src/pages/settings.jsx:105 +#: src/pages/settings.jsx:109 msgid "Settings" msgstr "" -#: src/pages/settings.jsx:114 +#: src/pages/settings.jsx:118 msgid "Appearance" msgstr "" -#: src/pages/settings.jsx:190 +#: src/pages/settings.jsx:194 msgid "Light" msgstr "" -#: src/pages/settings.jsx:201 +#: src/pages/settings.jsx:205 msgid "Dark" msgstr "" -#: src/pages/settings.jsx:214 +#: src/pages/settings.jsx:218 msgid "Auto" msgstr "" -#: src/pages/settings.jsx:224 +#: src/pages/settings.jsx:228 msgid "Text size" msgstr "" -#: src/pages/settings.jsx:232 +#: src/pages/settings.jsx:236 msgid "Display language" msgstr "" -#: src/pages/settings.jsx:240 +#: src/pages/settings.jsx:244 msgid "Volunteer translations" msgstr "Volunteer translations" -#: src/pages/settings.jsx:251 +#: src/pages/settings.jsx:255 msgid "Posting" msgstr "" -#: src/pages/settings.jsx:257 +#: src/pages/settings.jsx:261 msgid "Default visibility" msgstr "" -#: src/pages/settings.jsx:258 -#: src/pages/settings.jsx:304 -#: src/pages/settings.jsx:352 +#: src/pages/settings.jsx:262 +#: src/pages/settings.jsx:308 +#: src/pages/settings.jsx:356 msgid "Synced" msgstr "" -#: src/pages/settings.jsx:281 +#: src/pages/settings.jsx:285 msgid "Default visibility updated" msgstr "Default visibility updated" -#: src/pages/settings.jsx:283 +#: src/pages/settings.jsx:287 msgid "Failed to update default visibility" msgstr "Failed to update default visibility" -#: src/pages/settings.jsx:354 +#: src/pages/settings.jsx:358 msgid "Synced to your server's settings. <0>Go to your server ({instance}) for more settings." msgstr "Synced to your server's settings. <0>Go to your server ({instance}) for more settings." -#: src/pages/settings.jsx:369 +#: src/pages/settings.jsx:373 msgid "Experiments" msgstr "" -#: src/pages/settings.jsx:382 +#: src/pages/settings.jsx:386 msgid "Auto refresh timeline posts" msgstr "" -#: src/pages/settings.jsx:394 +#: src/pages/settings.jsx:398 msgid "Boosts carousel" msgstr "" -#: src/pages/settings.jsx:411 +#: src/pages/settings.jsx:415 msgid "Post translation" msgstr "" -#: src/pages/settings.jsx:422 +#: src/pages/settings.jsx:426 msgid "Translate to " msgstr "Translate to " -#: src/pages/settings.jsx:433 +#: src/pages/settings.jsx:437 msgid "System language ({systemTargetLanguageText})" msgstr "" #. placeholder {0}: snapStates.settings.contentTranslationHideLanguages .length -#: src/pages/settings.jsx:459 +#: src/pages/settings.jsx:463 msgid "{0, plural, =0 {Hide \"Translate\" button for:} other {Hide \"Translate\" button for (#):}}" msgstr "" -#: src/pages/settings.jsx:514 +#: src/pages/settings.jsx:518 msgid "Note: This feature uses external translation services, powered by <0>{TRANSLATION_API_NAME}." msgstr "Note: This feature uses external translation services, powered by <0>{TRANSLATION_API_NAME}." -#: src/pages/settings.jsx:542 +#: src/pages/settings.jsx:546 msgid "Auto inline translation" msgstr "" -#: src/pages/settings.jsx:546 +#: src/pages/settings.jsx:550 msgid "Automatically show translation for posts in timeline. Only works for <0>short posts without content warning, media and poll." msgstr "" -#: src/pages/settings.jsx:567 +#: src/pages/settings.jsx:577 +msgid "Paginated timeline (beta)" +msgstr "Paginated timeline (beta)" + +#: src/pages/settings.jsx:581 +msgid "Manual pagination of timeline posts instead of infinite scrolling. Only works for Home/Following timeline for now. Auto refresh and boosts carousel will not work when this is enabled." +msgstr "Manual pagination of timeline posts instead of infinite scrolling. Only works for Home/Following timeline for now. Auto refresh and boosts carousel will not work when this is enabled." + +#: src/pages/settings.jsx:601 msgid "GIF Picker for composer" msgstr "" -#: src/pages/settings.jsx:571 +#: src/pages/settings.jsx:605 msgid "Note: This feature uses external GIF search service, powered by <0>GIPHY. G-rated (suitable for viewing by all ages), tracking parameters are stripped, referrer information is omitted from requests, but search queries and IP address information will still reach their servers." msgstr "" -#: src/pages/settings.jsx:600 +#: src/pages/settings.jsx:634 msgid "Image description generator" msgstr "" -#: src/pages/settings.jsx:605 +#: src/pages/settings.jsx:639 msgid "Only for new images while composing new posts." msgstr "" -#: src/pages/settings.jsx:612 +#: src/pages/settings.jsx:646 msgid "Note: This feature uses external AI service, powered by <0>img-alt-api. May not work well. Only for images and in English." msgstr "" -#: src/pages/settings.jsx:640 +#: src/pages/settings.jsx:674 msgid "\"Cloud\" import/export for shortcuts settings" msgstr "" -#: src/pages/settings.jsx:645 +#: src/pages/settings.jsx:679 msgid "⚠️⚠️⚠️ Very experimental.<0/>Stored in your own profile’s notes. Profile (private) notes are mainly used for other profiles, and hidden for own profile." msgstr "" -#: src/pages/settings.jsx:656 +#: src/pages/settings.jsx:690 msgid "Note: This feature uses currently-logged-in server API." msgstr "Note: This feature uses currently-logged-in server API." -#: src/pages/settings.jsx:672 +#: src/pages/settings.jsx:706 msgid "Cloak mode <0>(<1>Text → <2>████)" msgstr "" -#: src/pages/settings.jsx:681 +#: src/pages/settings.jsx:715 msgid "Replace text as blocks, useful when taking screenshots, for privacy reasons." msgstr "" -#: src/pages/settings.jsx:710 +#: src/pages/settings.jsx:744 msgid "Install {CLIENT_NAME}" msgstr "Install {CLIENT_NAME}" -#: src/pages/settings.jsx:718 +#: src/pages/settings.jsx:752 msgid "About" msgstr "" -#: src/pages/settings.jsx:757 +#: src/pages/settings.jsx:791 msgid "<0>Built by <1>@cheeaun" msgstr "" -#: src/pages/settings.jsx:786 +#: src/pages/settings.jsx:820 msgid "Sponsor" msgstr "" -#: src/pages/settings.jsx:794 +#: src/pages/settings.jsx:828 msgid "Donate" msgstr "Donate" -#: src/pages/settings.jsx:810 +#: src/pages/settings.jsx:844 msgid "What's new" msgstr "What's new" -#: src/pages/settings.jsx:814 +#: src/pages/settings.jsx:848 msgid "Privacy Policy" msgstr "" #. placeholder {0}: WEBSITE.replace(/https?:\/\//g, '').replace(/\/$/, '') -#: src/pages/settings.jsx:821 +#: src/pages/settings.jsx:855 msgid "<0>Site: {0}" msgstr "" #. placeholder {0}: !__FAKE_COMMIT_HASH__ && ( ( ) ) -#: src/pages/settings.jsx:828 +#: src/pages/settings.jsx:862 msgid "<0>Version: <1/> {0}" msgstr "" -#: src/pages/settings.jsx:843 +#: src/pages/settings.jsx:877 msgid "Version string copied" msgstr "" -#: src/pages/settings.jsx:846 +#: src/pages/settings.jsx:880 msgid "Unable to copy version string" msgstr "" #. Preview of one character, in largest size #. Preview of one character, in smallest size -#: src/pages/settings.jsx:990 -#: src/pages/settings.jsx:1014 +#: src/pages/settings.jsx:1024 +#: src/pages/settings.jsx:1048 msgid "A" msgstr "" -#: src/pages/settings.jsx:1174 -#: src/pages/settings.jsx:1179 +#: src/pages/settings.jsx:1208 +#: src/pages/settings.jsx:1213 msgid "Failed to update subscription. Please try again." msgstr "" -#: src/pages/settings.jsx:1185 +#: src/pages/settings.jsx:1219 msgid "Failed to remove subscription. Please try again." msgstr "" -#: src/pages/settings.jsx:1192 +#: src/pages/settings.jsx:1226 msgid "Push Notifications (beta)" msgstr "" -#: src/pages/settings.jsx:1214 +#: src/pages/settings.jsx:1248 msgid "Push notifications are blocked. Please enable them in your browser settings." msgstr "" #. placeholder {0}: [ { value: 'all', label: t`anyone`, }, { value: 'followed', label: t`people I follow`, }, { value: 'follower', label: t`followers`, }, ].map((type) => ( )) -#: src/pages/settings.jsx:1223 +#: src/pages/settings.jsx:1257 msgid "Allow from <0>{0}" msgstr "" -#: src/pages/settings.jsx:1232 +#: src/pages/settings.jsx:1266 msgid "anyone" msgstr "" -#: src/pages/settings.jsx:1236 +#: src/pages/settings.jsx:1270 msgid "people I follow" msgstr "" -#: src/pages/settings.jsx:1240 +#: src/pages/settings.jsx:1274 msgid "followers" msgstr "" -#: src/pages/settings.jsx:1273 +#: src/pages/settings.jsx:1307 msgid "Follows" msgstr "" -#: src/pages/settings.jsx:1281 +#: src/pages/settings.jsx:1315 msgid "Polls" msgstr "" -#: src/pages/settings.jsx:1285 +#: src/pages/settings.jsx:1319 msgid "Post edits" msgstr "" -#: src/pages/settings.jsx:1306 +#: src/pages/settings.jsx:1340 msgid "Push permission was not granted since your last login. You'll need to <0><1>log in again to grant push permission." msgstr "" -#: src/pages/settings.jsx:1322 +#: src/pages/settings.jsx:1356 msgid "NOTE: Push notifications only work for <0>one account." msgstr "" diff --git a/src/pages/following.jsx b/src/pages/following.jsx index 3d0bdd41..72cdad18 100644 --- a/src/pages/following.jsx +++ b/src/pages/following.jsx @@ -100,7 +100,7 @@ function Following({ title, path, id, ...props }) { limit: 5, since_id: latestItem.current, }; - if (supports('@pixelfed/home-include-reblogs')) { + if (supportsPixelfed) { opts.include_reblogs = true; } const results = await masto.v1.timelines.home.list(opts).values().next(); diff --git a/src/pages/following2.jsx b/src/pages/following2.jsx new file mode 100644 index 00000000..cb470f0d --- /dev/null +++ b/src/pages/following2.jsx @@ -0,0 +1,149 @@ +import { useLingui } from '@lingui/react/macro'; +import { useEffect, useState } from 'preact/hooks'; + +import Timeline2 from '../components/timeline2'; +import { api } from '../utils/api'; +import { filteredItems } from '../utils/filters'; +import { getStatus, saveStatus } from '../utils/states'; +import supports from '../utils/supports'; +import { assignFollowedTags, dedupeBoosts } from '../utils/timeline-utils'; +import useTitle from '../utils/useTitle'; + +const LIMIT = 20; + +function Following2({ title, path, id, ...props }) { + const { t } = useLingui(); + useTitle( + title || + t({ + id: 'following.title', + message: 'Following', + }), + path || '/_following2', + ); + const { masto, streaming, instance, client } = api(); + const [streamingClient, setStreamingClient] = useState(streaming); + + // Streaming only happens after instance is initialized + useEffect(() => { + if (!streaming && client?.onStreamingReady) { + client.onStreamingReady((streamingClient) => { + setStreamingClient(streamingClient); + }); + } + }, [client]); + __BENCHMARK.end('time-to-following'); + + console.debug('RENDER Following2', title, id); + const supportsPixelfed = supports('@pixelfed/home-include-reblogs'); + + async function fetchHome({ max_id, min_id } = {}) { + __BENCHMARK.start('fetch-home'); + + const opts = { + limit: LIMIT, + }; + if (max_id) opts.max_id = max_id; + if (min_id) opts.min_id = min_id; + if (supportsPixelfed) { + opts.include_reblogs = true; + } + + const results = await masto.v1.timelines.home.list(opts).values().next(); + let { value } = results; + + const originalValue = [...(value || [])]; + if (value?.length) { + // value = filteredItems(value, 'home'); + value.forEach((item) => { + saveStatus(item, instance); + }); + // value = dedupeBoosts(value, instance); + setTimeout(() => { + assignFollowedTags(value, instance); + }, 100); + + // ENFORCE sort by datetime (Latest first) + value.sort((a, b) => { + return Date.parse(b.createdAt) - Date.parse(a.createdAt); + }); + } + + __BENCHMARK.end('fetch-home'); + return { + ...results, + value, + originalValue, + }; + } + + async function checkForUpdates({ minID }) { + try { + const opts = { + limit: 5, + since_id: minID, + }; + if (supportsPixelfed) { + opts.include_reblogs = true; + } + const results = await masto.v1.timelines.home.list(opts).values().next(); + let { value } = results; + if (value?.length) { + value = dedupeBoosts(value, instance); + value = filteredItems(value, 'home'); + return value.length > 0; + } + return false; + } catch (e) { + console.error(e); + return false; + } + } + + useEffect(() => { + let sub; + (async () => { + if (streamingClient) { + sub = streamingClient.user.subscribe(); + console.log('🎏 Streaming user (Following2)', sub); + for await (const entry of sub) { + if (!sub) break; + if (entry.event === 'status.update') { + const status = entry.payload; + console.log(`🔄 Status ${status.id} updated`); + saveStatus(status, instance); + } else if (entry.event === 'delete') { + const statusID = entry.payload; + console.log(`❌ Status ${statusID} deleted`); + const s = getStatus(statusID, instance); + if (s) s._deleted = true; + } + } + console.log('💥 Streaming user loop STOPPED (Following2)'); + } + })(); + return () => { + sub?.unsubscribe?.(); + sub = null; + }; + }, [streamingClient]); + + return ( + + ); +} + +export default Following2; diff --git a/src/pages/home.jsx b/src/pages/home.jsx index f13ccd0a..f43231d3 100644 --- a/src/pages/home.jsx +++ b/src/pages/home.jsx @@ -17,9 +17,11 @@ import db from '../utils/db'; import FilterContext from '../utils/filter-context'; import { massageNotifications2 } from '../utils/group-notifications'; import states, { saveStatus } from '../utils/states'; +import store from '../utils/store'; import { getCurrentAccountNS } from '../utils/store-utils'; import Following from './following'; +import Following2 from './following2'; import { getGroupedNotifications, mastoFetchNotifications, @@ -42,6 +44,11 @@ function Home() { })(); }, []); + const expTimeline2 = useRef(false); + if (!expTimeline2.current) { + expTimeline2.current = store.local.get('experiments-timeline2') ?? false; + } + return ( <> {(snapStates.settings.shortcutsViewMode === 'multi-column' || @@ -49,6 +56,14 @@ function Home() { snapStates.settings.shortcutsColumnsMode)) && !!snapStates.shortcuts?.length ? ( + ) : expTimeline2.current ? ( + } + /> ) : ( )} + {authenticated && ( +
  • + +
    + + + Manual pagination of timeline posts instead of infinite + scrolling. Only works for Home/Following timeline for now. + Auto refresh and boosts carousel will not work when this + is enabled. + + +
    +
  • + )} {!!GIPHY_API_KEY && authenticated && (