mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-03-13 09:28:50 +01:00
Perf fixes + 3d posts viz
This commit is contained in:
parent
fcb0074f49
commit
afb1f6d520
2 changed files with 204 additions and 98 deletions
|
@ -168,14 +168,14 @@
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid var(--bg-color);
|
border: 1px solid var(--bg-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1px;
|
gap: var(--hairline-width);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
|
|
||||||
&:has(.post-dot:nth-child(320)) {
|
/* &:has(.post-dot:nth-child(320)) {
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
} */
|
||||||
|
|
||||||
.post-dot {
|
.post-dot {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -198,6 +198,37 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.catchup-posts-viz-time-bar {
|
||||||
|
margin: 0 16px;
|
||||||
|
padding: 1px;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--hairline-width);
|
||||||
|
pointer-events: none;
|
||||||
|
justify-content: stretch;
|
||||||
|
background-image: linear-gradient(to bottom, transparent, var(--bg-color));
|
||||||
|
|
||||||
|
.posts-bin {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--hairline-width);
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.post-dot {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
opacity: 0.2;
|
||||||
|
background-color: var(--link-color);
|
||||||
|
transition: 0.25s ease-in-out;
|
||||||
|
transition-property: opacity, transform;
|
||||||
|
|
||||||
|
&.post-dot-highlight {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.catchup-filters {
|
.catchup-filters {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -5,7 +5,14 @@ import autoAnimate from '@formkit/auto-animate';
|
||||||
import { getBlurHashAverageColor } from 'fast-blurhash';
|
import { getBlurHashAverageColor } from 'fast-blurhash';
|
||||||
import { Fragment } from 'preact';
|
import { Fragment } from 'preact';
|
||||||
import { memo } from 'preact/compat';
|
import { memo } from 'preact/compat';
|
||||||
import { useEffect, useMemo, useReducer, useRef, useState } from 'preact/hooks';
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useReducer,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'preact/hooks';
|
||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { uid } from 'uid/single';
|
import { uid } from 'uid/single';
|
||||||
|
|
||||||
|
@ -36,6 +43,47 @@ import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
const FILTER_CONTEXT = 'home';
|
const FILTER_CONTEXT = 'home';
|
||||||
|
|
||||||
|
const RANGES = [
|
||||||
|
{ label: 'last 1 hour', value: 1 },
|
||||||
|
{ label: 'last 2 hours', value: 2 },
|
||||||
|
{ label: 'last 3 hours', value: 3 },
|
||||||
|
{ label: 'last 4 hours', value: 4 },
|
||||||
|
{ label: 'last 5 hours', value: 5 },
|
||||||
|
{ label: 'last 6 hours', value: 6 },
|
||||||
|
{ label: 'last 7 hours', value: 7 },
|
||||||
|
{ label: 'last 8 hours', value: 8 },
|
||||||
|
{ label: 'last 9 hours', value: 9 },
|
||||||
|
{ label: 'last 10 hours', value: 10 },
|
||||||
|
{ label: 'last 11 hours', value: 11 },
|
||||||
|
{ label: 'last 12 hours', value: 12 },
|
||||||
|
{ label: 'beyond 12 hours', value: 13 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const FILTER_VALUES = {
|
||||||
|
Filtered: 'filtered',
|
||||||
|
Groups: 'group',
|
||||||
|
Boosts: 'boost',
|
||||||
|
Replies: 'reply',
|
||||||
|
'Followed tags': 'followedTags',
|
||||||
|
Original: 'original',
|
||||||
|
};
|
||||||
|
const FILTER_CATEGORY_TEXT = {
|
||||||
|
Filtered: 'filtered posts',
|
||||||
|
Groups: 'group posts',
|
||||||
|
Boosts: 'boosts',
|
||||||
|
Replies: 'replies',
|
||||||
|
'Followed tags': 'followed-tag posts',
|
||||||
|
Original: 'original posts',
|
||||||
|
};
|
||||||
|
const SORT_BY_TEXT = {
|
||||||
|
// asc, desc
|
||||||
|
createdAt: ['oldest', 'latest'],
|
||||||
|
repliesCount: ['fewest replies', 'most replies'],
|
||||||
|
favouritesCount: ['fewest likes', 'most likes'],
|
||||||
|
reblogsCount: ['fewest boosts', 'most boosts'],
|
||||||
|
density: ['least dense', 'most dense'],
|
||||||
|
};
|
||||||
|
|
||||||
function Catchup() {
|
function Catchup() {
|
||||||
useTitle('Catch-up', '/catchup');
|
useTitle('Catch-up', '/catchup');
|
||||||
const { masto, instance } = api();
|
const { masto, instance } = api();
|
||||||
|
@ -125,15 +173,15 @@ function Catchup() {
|
||||||
|
|
||||||
const [posts, setPosts] = useState([]);
|
const [posts, setPosts] = useState([]);
|
||||||
const catchupRangeRef = useRef();
|
const catchupRangeRef = useRef();
|
||||||
async function handleCatchupClick({ duration } = {}) {
|
const NS = useMemo(() => getCurrentAccountNS(), []);
|
||||||
|
const handleCatchupClick = useCallback(async ({ duration } = {}) => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const maxCreatedAt = duration ? now - duration : null;
|
const maxCreatedAt = duration ? now - duration : null;
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
const results = await fetchHome({ maxCreatedAt });
|
const results = await fetchHome({ maxCreatedAt });
|
||||||
// Namespaced by account ID
|
// Namespaced by account ID
|
||||||
// Possible conflict if ID matches between different accounts from different instances
|
// Possible conflict if ID matches between different accounts from different instances
|
||||||
const ns = getCurrentAccountNS();
|
const catchupID = `${NS}-${uid()}`;
|
||||||
const catchupID = `${ns}-${uid()}`;
|
|
||||||
try {
|
try {
|
||||||
await db.catchup.set(catchupID, {
|
await db.catchup.set(catchupID, {
|
||||||
id: catchupID,
|
id: catchupID,
|
||||||
|
@ -145,17 +193,15 @@ function Catchup() {
|
||||||
setSearchParams({ id: catchupID });
|
setSearchParams({ id: catchupID });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e, results);
|
console.error(e, results);
|
||||||
// setUIState('error');
|
|
||||||
}
|
}
|
||||||
// setPosts(results);
|
}, []);
|
||||||
// setUIState('results');
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
(async () => {
|
(async () => {
|
||||||
const catchup = await db.catchup.get(id);
|
const catchup = await db.catchup.get(id);
|
||||||
if (catchup) {
|
if (catchup) {
|
||||||
|
catchup.posts.sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1));
|
||||||
setPosts(catchup.posts);
|
setPosts(catchup.posts);
|
||||||
setUIState('results');
|
setUIState('results');
|
||||||
}
|
}
|
||||||
|
@ -340,65 +386,48 @@ function Catchup() {
|
||||||
const [selectedAuthor, setSelectedAuthor] = useState(null);
|
const [selectedAuthor, setSelectedAuthor] = useState(null);
|
||||||
|
|
||||||
const [range, setRange] = useState(1);
|
const [range, setRange] = useState(1);
|
||||||
const ranges = [
|
|
||||||
{ label: 'last 1 hour', value: 1 },
|
|
||||||
{ label: 'last 2 hours', value: 2 },
|
|
||||||
{ label: 'last 3 hours', value: 3 },
|
|
||||||
{ label: 'last 4 hours', value: 4 },
|
|
||||||
{ label: 'last 5 hours', value: 5 },
|
|
||||||
{ label: 'last 6 hours', value: 6 },
|
|
||||||
{ label: 'last 7 hours', value: 7 },
|
|
||||||
{ label: 'last 8 hours', value: 8 },
|
|
||||||
{ label: 'last 9 hours', value: 9 },
|
|
||||||
{ label: 'last 10 hours', value: 10 },
|
|
||||||
{ label: 'last 11 hours', value: 11 },
|
|
||||||
{ label: 'last 12 hours', value: 12 },
|
|
||||||
{ label: 'beyond 12 hours', value: 13 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const [sortBy, setSortBy] = useState('createdAt');
|
const [sortBy, setSortBy] = useState('createdAt');
|
||||||
const [sortOrder, setSortOrder] = useState('asc');
|
const [sortOrder, setSortOrder] = useState('asc');
|
||||||
const [groupBy, setGroupBy] = useState(null);
|
const [groupBy, setGroupBy] = useState(null);
|
||||||
|
|
||||||
const [filteredPosts, authors, authorCounts] = useMemo(() => {
|
const [filteredPosts, authors, authorCounts] = useMemo(() => {
|
||||||
let authors = [];
|
const authorsHash = {};
|
||||||
const authorCounts = {};
|
const authorCountsMap = new Map();
|
||||||
|
|
||||||
let filteredPosts = posts.filter((post) => {
|
let filteredPosts = posts.filter((post) => {
|
||||||
return (
|
const postFilterMatches =
|
||||||
selectedFilterCategory === 'All' ||
|
selectedFilterCategory === 'All' ||
|
||||||
post.__FILTER ===
|
post.__FILTER === FILTER_VALUES[selectedFilterCategory];
|
||||||
{
|
|
||||||
Filtered: 'filtered',
|
|
||||||
Groups: 'group',
|
|
||||||
Boosts: 'boost',
|
|
||||||
Replies: 'reply',
|
|
||||||
'Followed tags': 'followedTags',
|
|
||||||
Original: 'original',
|
|
||||||
}[selectedFilterCategory]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
filteredPosts.forEach((post) => {
|
if (postFilterMatches) {
|
||||||
if (!authors.find((a) => a.id === post.account.id)) {
|
authorsHash[post.account.id] = post.account;
|
||||||
authors.push(post.account);
|
authorCountsMap.set(
|
||||||
|
post.account.id,
|
||||||
|
(authorCountsMap.get(post.account.id) || 0) + 1,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
authorCounts[post.account.id] = (authorCounts[post.account.id] || 0) + 1;
|
|
||||||
|
return postFilterMatches;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (selectedAuthor && authorCounts[selectedAuthor]) {
|
if (selectedAuthor && authorCountsMap.has(selectedAuthor)) {
|
||||||
filteredPosts = filteredPosts.filter(
|
filteredPosts = filteredPosts.filter(
|
||||||
(post) => post.account.id === selectedAuthor,
|
(post) => post.account.id === selectedAuthor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorsHash = {};
|
return [filteredPosts, authorsHash, Object.fromEntries(authorCountsMap)];
|
||||||
for (const author of authors) {
|
|
||||||
authorsHash[author.id] = author;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [filteredPosts, authorsHash, authorCounts];
|
|
||||||
}, [selectedFilterCategory, selectedAuthor, posts]);
|
}, [selectedFilterCategory, selectedAuthor, posts]);
|
||||||
|
|
||||||
|
const filteredPostsMap = useMemo(() => {
|
||||||
|
const map = {};
|
||||||
|
filteredPosts.forEach((post) => {
|
||||||
|
map[post.id] = post;
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [filteredPosts]);
|
||||||
|
|
||||||
const authorCountsList = useMemo(
|
const authorCountsList = useMemo(
|
||||||
() =>
|
() =>
|
||||||
Object.keys(authorCounts).sort(
|
Object.keys(authorCounts).sort(
|
||||||
|
@ -450,26 +479,55 @@ function Catchup() {
|
||||||
const prevGroup = useRef(null);
|
const prevGroup = useRef(null);
|
||||||
|
|
||||||
const authorsListParent = useRef(null);
|
const authorsListParent = useRef(null);
|
||||||
|
const autoAnimated = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authorsListParent.current && authorCountsList.length < 30) {
|
if (posts.length > 100 || autoAnimated.current) return;
|
||||||
|
if (authorsListParent.current) {
|
||||||
autoAnimate(authorsListParent.current, {
|
autoAnimate(authorsListParent.current, {
|
||||||
duration: 200,
|
duration: 200,
|
||||||
});
|
});
|
||||||
|
autoAnimated.current = true;
|
||||||
}
|
}
|
||||||
}, [selectedFilterCategory, authorCountsList, authorsListParent]);
|
}, [posts, authorsListParent]);
|
||||||
|
|
||||||
|
const postsBarType = posts.length > 160 ? '3d' : '2d';
|
||||||
|
|
||||||
const postsBar = useMemo(() => {
|
const postsBar = useMemo(() => {
|
||||||
|
if (postsBarType !== '2d') return null;
|
||||||
return posts.map((post) => {
|
return posts.map((post) => {
|
||||||
// If part of filteredPosts
|
// If part of filteredPosts
|
||||||
const isFiltered = filteredPosts.find((p) => p.id === post.id);
|
const isFiltered = filteredPostsMap[post.id];
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
|
data-id={post.id}
|
||||||
key={post.id}
|
key={post.id}
|
||||||
class={`post-dot ${isFiltered ? 'post-dot-highlight' : ''}`}
|
class={`post-dot ${isFiltered ? 'post-dot-highlight' : ''}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}, [posts, filteredPosts]);
|
}, [filteredPostsMap]);
|
||||||
|
|
||||||
|
const postsBins = useMemo(() => {
|
||||||
|
if (postsBarType !== '3d') return null;
|
||||||
|
if (!posts?.length) return null;
|
||||||
|
const bins = binByTime(posts, 'createdAt', 320);
|
||||||
|
return bins.map((posts, i) => {
|
||||||
|
return (
|
||||||
|
<div class="posts-bin" key={i}>
|
||||||
|
{posts.map((post) => {
|
||||||
|
const isFiltered = filteredPostsMap[post.id];
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-id={post.id}
|
||||||
|
key={post.id}
|
||||||
|
class={`post-dot ${isFiltered ? 'post-dot-highlight' : ''}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [filteredPostsMap]);
|
||||||
|
|
||||||
const scrollableRef = useRef(null);
|
const scrollableRef = useRef(null);
|
||||||
|
|
||||||
|
@ -482,36 +540,20 @@ function Catchup() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (uiState !== 'results') return;
|
if (uiState !== 'results') return;
|
||||||
const filterCategoryText = {
|
|
||||||
Filtered: 'filtered posts',
|
|
||||||
Groups: 'group posts',
|
|
||||||
Boosts: 'boosts',
|
|
||||||
Replies: 'replies',
|
|
||||||
'Followed tags': 'followed-tag posts',
|
|
||||||
Original: 'original posts',
|
|
||||||
};
|
|
||||||
const authorUsername =
|
const authorUsername =
|
||||||
selectedAuthor && authors[selectedAuthor]
|
selectedAuthor && authors[selectedAuthor]
|
||||||
? authors[selectedAuthor].username
|
? authors[selectedAuthor].username
|
||||||
: '';
|
: '';
|
||||||
const sortOrderIndex = sortOrder === 'asc' ? 0 : 1;
|
const sortOrderIndex = sortOrder === 'asc' ? 0 : 1;
|
||||||
const sortByText = {
|
|
||||||
// asc, desc
|
|
||||||
createdAt: ['oldest', 'latest'],
|
|
||||||
repliesCount: ['fewest replies', 'most replies'],
|
|
||||||
favouritesCount: ['fewest likes', 'most likes'],
|
|
||||||
reblogsCount: ['fewest boosts', 'most boosts'],
|
|
||||||
density: ['least dense', 'most dense'],
|
|
||||||
};
|
|
||||||
const groupByText = {
|
const groupByText = {
|
||||||
account: 'authors',
|
account: 'authors',
|
||||||
};
|
};
|
||||||
let toast = showToast({
|
let toast = showToast({
|
||||||
duration: 5_000, // 5 seconds
|
duration: 5_000, // 5 seconds
|
||||||
text: `Showing ${
|
text: `Showing ${
|
||||||
filterCategoryText[selectedFilterCategory] || 'all posts'
|
FILTER_CATEGORY_TEXT[selectedFilterCategory] || 'all posts'
|
||||||
}${authorUsername ? ` by @${authorUsername}` : ''}, ${
|
}${authorUsername ? ` by @${authorUsername}` : ''}, ${
|
||||||
sortByText[sortBy][sortOrderIndex]
|
SORT_BY_TEXT[sortBy][sortOrderIndex]
|
||||||
} first${
|
} first${
|
||||||
!!groupBy
|
!!groupBy
|
||||||
? `, grouped by ${groupBy === 'account' ? groupByText[groupBy] : ''}`
|
? `, grouped by ${groupBy === 'account' ? groupByText[groupBy] : ''}`
|
||||||
|
@ -533,11 +575,11 @@ function Catchup() {
|
||||||
|
|
||||||
const prevSelectedAuthorMissing = useRef(false);
|
const prevSelectedAuthorMissing = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log({
|
// console.log({
|
||||||
prevSelectedAuthorMissing,
|
// prevSelectedAuthorMissing,
|
||||||
selectedAuthor,
|
// selectedAuthor,
|
||||||
authors,
|
// authors,
|
||||||
});
|
// });
|
||||||
let timer;
|
let timer;
|
||||||
if (selectedAuthor) {
|
if (selectedAuthor) {
|
||||||
if (authors[selectedAuthor]) {
|
if (authors[selectedAuthor]) {
|
||||||
|
@ -649,8 +691,8 @@ function Catchup() {
|
||||||
ref={catchupRangeRef}
|
ref={catchupRangeRef}
|
||||||
type="range"
|
type="range"
|
||||||
value={range}
|
value={range}
|
||||||
min={ranges[0].value}
|
min={RANGES[0].value}
|
||||||
max={ranges[ranges.length - 1].value}
|
max={RANGES[RANGES.length - 1].value}
|
||||||
step="1"
|
step="1"
|
||||||
list="catchup-ranges"
|
list="catchup-ranges"
|
||||||
onChange={(e) => setRange(+e.target.value)}
|
onChange={(e) => setRange(+e.target.value)}
|
||||||
|
@ -660,10 +702,10 @@ function Catchup() {
|
||||||
width: '8em',
|
width: '8em',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ranges[range - 1].label}
|
{RANGES[range - 1].label}
|
||||||
<br />
|
<br />
|
||||||
<small class="insignificant">
|
<small class="insignificant">
|
||||||
{range == ranges[ranges.length - 1].value
|
{range == RANGES[RANGES.length - 1].value
|
||||||
? 'until the max'
|
? 'until the max'
|
||||||
: niceDateTime(
|
: niceDateTime(
|
||||||
new Date(Date.now() - range * 60 * 60 * 1000),
|
new Date(Date.now() - range * 60 * 60 * 1000),
|
||||||
|
@ -671,14 +713,14 @@ function Catchup() {
|
||||||
</small>
|
</small>
|
||||||
</span>
|
</span>
|
||||||
<datalist id="catchup-ranges">
|
<datalist id="catchup-ranges">
|
||||||
{ranges.map(({ label, value }) => (
|
{RANGES.map(({ label, value }) => (
|
||||||
<option value={value} label={label} />
|
<option value={value} label={label} />
|
||||||
))}
|
))}
|
||||||
</datalist>{' '}
|
</datalist>{' '}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (range < ranges[ranges.length - 1].value) {
|
if (range < RANGES[RANGES.length - 1].value) {
|
||||||
const duration = range * 60 * 60 * 1000;
|
const duration = range * 60 * 60 * 1000;
|
||||||
handleCatchupClick({ duration });
|
handleCatchupClick({ duration });
|
||||||
} else {
|
} else {
|
||||||
|
@ -926,9 +968,12 @@ function Catchup() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{posts.length >= 5 && (
|
{posts.length >= 5 &&
|
||||||
<div class="catchup-posts-viz-bar">{postsBar}</div>
|
(postsBarType === '3d' ? (
|
||||||
)}
|
<div class="catchup-posts-viz-time-bar">{postsBins}</div>
|
||||||
|
) : (
|
||||||
|
<div class="catchup-posts-viz-bar">{postsBar}</div>
|
||||||
|
))}
|
||||||
{posts.length >= 2 && (
|
{posts.length >= 2 && (
|
||||||
<div class="catchup-filters">
|
<div class="catchup-filters">
|
||||||
<label class="filter-cat">
|
<label class="filter-cat">
|
||||||
|
@ -1139,14 +1184,11 @@ function Catchup() {
|
||||||
return (
|
return (
|
||||||
<Fragment key={`${post.id}-${showSeparator}`}>
|
<Fragment key={`${post.id}-${showSeparator}`}>
|
||||||
{showSeparator && <li class="separator" />}
|
{showSeparator && <li class="separator" />}
|
||||||
<li>
|
<IntersectionPostLineItem
|
||||||
<Link to={`/${instance}/s/${id}`}>
|
to={`/${instance}/s/${id}`}
|
||||||
<IntersectionPostLine
|
post={post}
|
||||||
post={post}
|
root={scrollableRef.current}
|
||||||
root={scrollableRef.current}
|
/>
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -1251,7 +1293,7 @@ const PostLine = memo(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const IntersectionPostLine = ({ root, ...props }) => {
|
const IntersectionPostLineItem = ({ root, to, ...props }) => {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1275,9 +1317,13 @@ const IntersectionPostLine = ({ root, ...props }) => {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return show ? (
|
return show ? (
|
||||||
<PostLine {...props} />
|
<li>
|
||||||
|
<Link to={to}>
|
||||||
|
<PostLine {...props} />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
) : (
|
) : (
|
||||||
<div ref={ref} style={{ height: '4em' }} />
|
<li ref={ref} style={{ height: '4em' }} />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1507,4 +1553,33 @@ function formatRange(startDate, endDate) {
|
||||||
return dtf.formatRange(startDate, endDate);
|
return dtf.formatRange(startDate, endDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function binByTime(data, key, numBins) {
|
||||||
|
// Extract dates from data objects
|
||||||
|
const dates = data.map((item) => new Date(item[key]));
|
||||||
|
|
||||||
|
// Find minimum and maximum dates directly (avoiding Math.min/max)
|
||||||
|
const minDate = dates.reduce(
|
||||||
|
(acc, date) => (date < acc ? date : acc),
|
||||||
|
dates[0],
|
||||||
|
);
|
||||||
|
const maxDate = dates.reduce(
|
||||||
|
(acc, date) => (date > acc ? date : acc),
|
||||||
|
dates[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate the time span in milliseconds
|
||||||
|
const range = maxDate.getTime() - minDate.getTime();
|
||||||
|
|
||||||
|
// Create empty bins and loop through data
|
||||||
|
const bins = Array.from({ length: numBins }, () => []);
|
||||||
|
data.forEach((item) => {
|
||||||
|
const date = new Date(item[key]);
|
||||||
|
const normalized = (date.getTime() - minDate.getTime()) / range;
|
||||||
|
const binIndex = Math.floor(normalized * (numBins - 1));
|
||||||
|
bins[binIndex].push(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
return bins;
|
||||||
|
}
|
||||||
|
|
||||||
export default Catchup;
|
export default Catchup;
|
||||||
|
|
Loading…
Reference in a new issue