Skip to content

Commit

Permalink
feat: load more
Browse files Browse the repository at this point in the history
  • Loading branch information
syusui-s committed Jan 7, 2024
1 parent 022256e commit fd08875
Show file tree
Hide file tree
Showing 13 changed files with 294 additions and 44 deletions.
3 changes: 2 additions & 1 deletion src/components/column/Column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useHandleCommand } from '@/hooks/useCommandBus';
import { useTranslation } from '@/i18n/useTranslation';

export type ColumnProps = {
timelineRef?: (el: HTMLDivElement) => void;
columnIndex: number;
lastColumn: boolean;
width: 'widest' | 'wide' | 'medium' | 'narrow' | null | undefined;
Expand Down Expand Up @@ -59,7 +60,7 @@ const Column: Component<ColumnProps> = (props) => {
fallback={
<>
<div class="shrink-0 border-b border-border">{props.header}</div>
<div class="scrollbar flex flex-col overflow-y-scroll scroll-smooth pb-16">
<div ref={props.timelineRef} class="scrollbar flex flex-col overflow-y-scroll pb-16">
{props.children}
</div>
</>
Expand Down
21 changes: 16 additions & 5 deletions src/components/column/FollwingColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import { uniq } from 'lodash';
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
import Column from '@/components/column/Column';
import ColumnSettings from '@/components/column/ColumnSettings';
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
import Timeline from '@/components/timeline/Timeline';
import { FollowingColumnType } from '@/core/column';
import { applyContentFilter } from '@/core/contentFilter';
import useConfig from '@/core/useConfig';
import { useTranslation } from '@/i18n/useTranslation';
import useFollowings from '@/nostr/useFollowings';
import useSubscription from '@/nostr/useSubscription';
import epoch from '@/utils/epoch';

type FollowingColumnDisplayProps = {
columnIndex: number;
Expand All @@ -27,7 +27,11 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {

const { followingPubkeys } = useFollowings(() => ({ pubkey: props.column.pubkey }));

const { events } = useSubscription(() => {
const loadMore = useLoadMore(() => ({
duration: 4 * 60 * 60,
}));

const { events, eose } = useSubscription(() => {
const authors = uniq([...followingPubkeys()]);
if (authors.length === 0) return null;
return {
Expand All @@ -37,10 +41,13 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
{
kinds: [1, 6],
authors,
limit: 10,
since: epoch() - 4 * 60 * 60,
limit: 20,
since: loadMore.since(),
until: loadMore.until(),
},
],
eoseLimit: 20,
continuous: loadMore.continuous(),
clientEventFilter: (event) => {
if (props.column.contentFilter == null) return true;
return applyContentFilter(props.column.contentFilter)(event.content);
Expand All @@ -50,6 +57,7 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {

createEffect(() => {
console.log('home', events());
loadMore.setEvents(events());
});

onMount(() => console.log('home timeline mounted'));
Expand All @@ -68,8 +76,11 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
width={props.column.width}
columnIndex={props.columnIndex}
lastColumn={props.lastColumn}
timelineRef={loadMore.timelineRef}
>
<Timeline events={events()} />
<LoadMore loadMore={loadMore} eose={eose()}>
<Timeline events={events()} />
</LoadMore>
</Column>
);
};
Expand Down
112 changes: 112 additions & 0 deletions src/components/column/LoadMore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
createSignal,
createMemo,
batch,
Show,
type JSX,
type Accessor,
type Component,
} from 'solid-js';

import { type Event as NostrEvent } from 'nostr-tools';

import ColumnItem from '@/components/ColumnItem';
import useScroll from '@/hooks/useScroll';
import { useTranslation } from '@/i18n/useTranslation';
import { pickOldestEvent } from '@/nostr/event/comparator';
import epoch from '@/utils/epoch';

export type UseLoadMoreProps = {
duration: number | null;
};

export type UseLoadMore = {
timelineRef: (el: HTMLElement) => void;
setEvents: (event: NostrEvent[]) => void;
since: Accessor<number | undefined>;
until: Accessor<number | undefined>;
continuous: Accessor<boolean>;
loadLatest: () => void;
loadOld: () => void;
};

export type LoadMoreProps = {
loadMore: UseLoadMore;
children: JSX.Element;
eose: boolean;
};

export const useLoadMore = (propsProvider: () => UseLoadMoreProps): UseLoadMore => {
const props = createMemo(propsProvider);
const calcSince = (base: number): number | undefined => {
const { duration } = props();
if (duration == null) return undefined;
return base - duration;
};

const [events, setEvents] = createSignal<NostrEvent[]>([]);
const [since, setSince] = createSignal<number | undefined>(calcSince(epoch()));

Check warning on line 48 in src/components/column/LoadMore.tsx

View workflow job for this annotation

GitHub Actions / ci

The reactive variable 'calcSince' should be used within JSX, a tracked scope (like createEffect), or inside an event handler function, or else changes will be ignored
const [until, setUntil] = createSignal<number | undefined>();
const continuous = () => until() == null;

const scroll = useScroll();

const loadLatest = () => {
batch(() => {
setUntil(undefined);
setSince(calcSince(epoch()));
});
scroll.scrollToTop();
};

const loadOld = () => {
const oldest = pickOldestEvent(events());
if (oldest == null) return;
batch(() => {
setUntil(oldest.created_at);
setSince(calcSince(oldest.created_at));
});
scroll.scrollToTop();
};

return {
timelineRef: scroll.targetRef,
setEvents,
since,
until,
continuous,
loadLatest,
loadOld,
};
};

const LoadMore: Component<LoadMoreProps> = (props) => {
const i18n = useTranslation();

return (
<>
<Show when={!props.loadMore.continuous()}>
<ColumnItem>
<button
class="flex h-12 w-full flex-col items-center justify-center hover:text-fg-secondary"
onClick={() => props.loadMore.loadLatest()}
>
<span>{i18n()('column.loadLatest')}</span>
</button>
</ColumnItem>
</Show>
{props.children}
<ColumnItem>
<button
class="flex h-12 w-full flex-col items-center justify-center hover:text-fg-secondary disabled:text-fg-secondary/30"
disabled={!props.eose}
onClick={() => props.loadMore.loadOld()}
>
<span>{i18n()('column.loadOld')}</span>
</button>
</ColumnItem>
</>
);
};

export default LoadMore;
19 changes: 15 additions & 4 deletions src/components/column/NotificationColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Component } from 'solid-js';
import { createEffect, Component } from 'solid-js';

import Bell from 'heroicons/24/outline/bell.svg';

import BasicColumnHeader from '@/components/column/BasicColumnHeader';
import Column from '@/components/column/Column';
import ColumnSettings from '@/components/column/ColumnSettings';
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
import Notification from '@/components/timeline/Notification';
import { NotificationColumnType } from '@/core/column';
import { applyContentFilter } from '@/core/contentFilter';
Expand All @@ -22,21 +23,28 @@ const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) =>
const i18n = useTranslation();
const { config, removeColumn } = useConfig();

const { events: notifications } = useSubscription(() => ({
const loadMore = useLoadMore(() => ({ duration: null }));

const { events: notifications, eose } = useSubscription(() => ({
relayUrls: config().relayUrls,
filters: [
{
kinds: [1, 6, 7, 9735],
'#p': [props.column.pubkey],
limit: 10,
limit: 20,
since: loadMore.since(),
until: loadMore.until(),
},
],
eoseLimit: 20,
clientEventFilter: (event) => {
if (props.column.contentFilter == null) return true;
return applyContentFilter(props.column.contentFilter)(event.content);
},
}));

createEffect(() => loadMore.setEvents(notifications()));

return (
<Column
header={
Expand All @@ -50,8 +58,11 @@ const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) =>
width={props.column.width}
columnIndex={props.columnIndex}
lastColumn={props.lastColumn}
timelineRef={loadMore.timelineRef}
>
<Notification events={notifications()} />
<LoadMore loadMore={loadMore} eose={eose()}>
<Notification events={notifications()} />
</LoadMore>
</Column>
);
};
Expand Down
17 changes: 14 additions & 3 deletions src/components/column/PostsColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Component } from 'solid-js';
import { createEffect, Component } from 'solid-js';

import User from 'heroicons/24/outline/user.svg';

import BasicColumnHeader from '@/components/column/BasicColumnHeader';
import Column from '@/components/column/Column';
import ColumnSettings from '@/components/column/ColumnSettings';
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
import Timeline from '@/components/timeline/Timeline';
import { PostsColumnType } from '@/core/column';
import { applyContentFilter } from '@/core/contentFilter';
Expand All @@ -22,21 +23,28 @@ const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { config, removeColumn } = useConfig();

const { events } = useSubscription(() => ({
const loadMore = useLoadMore(() => ({ duration: null }));

const { events, eose } = useSubscription(() => ({
relayUrls: config().relayUrls,
filters: [
{
kinds: [1, 6],
authors: [props.column.pubkey],
limit: 10,
since: loadMore.since(),
until: loadMore.until(),
},
],
eoseLimit: 10,
clientEventFilter: (event) => {
if (props.column.contentFilter == null) return true;
return applyContentFilter(props.column.contentFilter)(event.content);
},
}));

createEffect(() => loadMore.setEvents(events()));

return (
<Column
header={
Expand All @@ -50,8 +58,11 @@ const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
width={props.column.width}
columnIndex={props.columnIndex}
lastColumn={props.lastColumn}
timelineRef={loadMore.timelineRef}
>
<Timeline events={events()} />
<LoadMore loadMore={loadMore} eose={eose()}>
<Timeline events={events()} />
</LoadMore>
</Column>
);
};
Expand Down
17 changes: 14 additions & 3 deletions src/components/column/ReactionsColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Component } from 'solid-js';
import { createEffect, Component } from 'solid-js';

import Heart from 'heroicons/24/outline/heart.svg';

import BasicColumnHeader from '@/components/column/BasicColumnHeader';
import Column from '@/components/column/Column';
import ColumnSettings from '@/components/column/ColumnSettings';
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
import Notification from '@/components/timeline/Notification';
import { ReactionsColumnType } from '@/core/column';
import { applyContentFilter } from '@/core/contentFilter';
Expand All @@ -22,21 +23,28 @@ const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { config, removeColumn } = useConfig();

const { events: reactions } = useSubscription(() => ({
const loadMore = useLoadMore(() => ({ duration: null }));

const { events: reactions, eose } = useSubscription(() => ({
relayUrls: config().relayUrls,
filters: [
{
kinds: [7],
authors: [props.column.pubkey],
limit: 10,
since: loadMore.since(),
until: loadMore.until(),
},
],
eoseLimit: 10,
clientEventFilter: (event) => {
if (props.column.contentFilter == null) return true;
return applyContentFilter(props.column.contentFilter)(event.content);
},
}));

createEffect(() => loadMore.setEvents(reactions()));

return (
<Column
header={
Expand All @@ -50,8 +58,11 @@ const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
width={props.column.width}
columnIndex={props.columnIndex}
lastColumn={props.lastColumn}
timelineRef={loadMore.timelineRef}
>
<Notification events={reactions()} />
<LoadMore loadMore={loadMore} eose={eose()}>
<Notification events={reactions()} />
</LoadMore>
</Column>
);
};
Expand Down
Loading

0 comments on commit fd08875

Please sign in to comment.