mirror of
https://github.com/aljazceru/rabbit.git
synced 2025-12-17 14:04:21 +01:00
feat: load more
This commit is contained in:
@@ -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;
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
@@ -13,7 +14,6 @@ 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;
|
||||
@@ -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 {
|
||||
@@ -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);
|
||||
@@ -50,6 +57,7 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
|
||||
|
||||
createEffect(() => {
|
||||
console.log('home', events());
|
||||
loadMore.setEvents(events());
|
||||
});
|
||||
|
||||
onMount(() => console.log('home timeline mounted'));
|
||||
@@ -68,8 +76,11 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
|
||||
width={props.column.width}
|
||||
columnIndex={props.columnIndex}
|
||||
lastColumn={props.lastColumn}
|
||||
timelineRef={loadMore.timelineRef}
|
||||
>
|
||||
<LoadMore loadMore={loadMore} eose={eose()}>
|
||||
<Timeline events={events()} />
|
||||
</LoadMore>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
112
src/components/column/LoadMore.tsx
Normal file
112
src/components/column/LoadMore.tsx
Normal file
@@ -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()));
|
||||
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;
|
||||
@@ -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';
|
||||
@@ -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={
|
||||
@@ -50,8 +58,11 @@ const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) =>
|
||||
width={props.column.width}
|
||||
columnIndex={props.columnIndex}
|
||||
lastColumn={props.lastColumn}
|
||||
timelineRef={loadMore.timelineRef}
|
||||
>
|
||||
<LoadMore loadMore={loadMore} eose={eose()}>
|
||||
<Notification events={notifications()} />
|
||||
</LoadMore>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
@@ -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={
|
||||
@@ -50,8 +58,11 @@ const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
|
||||
width={props.column.width}
|
||||
columnIndex={props.columnIndex}
|
||||
lastColumn={props.lastColumn}
|
||||
timelineRef={loadMore.timelineRef}
|
||||
>
|
||||
<LoadMore loadMore={loadMore} eose={eose()}>
|
||||
<Timeline events={events()} />
|
||||
</LoadMore>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
@@ -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={
|
||||
@@ -50,8 +58,11 @@ const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
|
||||
width={props.column.width}
|
||||
columnIndex={props.columnIndex}
|
||||
lastColumn={props.lastColumn}
|
||||
timelineRef={loadMore.timelineRef}
|
||||
>
|
||||
<LoadMore loadMore={loadMore} eose={eose()}>
|
||||
<Notification events={reactions()} />
|
||||
</LoadMore>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Component } from 'solid-js';
|
||||
import { createEffect, Component } from 'solid-js';
|
||||
|
||||
import GlobeAlt from 'heroicons/24/outline/globe-alt.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 { RelaysColumnType } from '@/core/column';
|
||||
import { applyContentFilter } from '@/core/contentFilter';
|
||||
import useConfig from '@/core/useConfig';
|
||||
import { useTranslation } from '@/i18n/useTranslation';
|
||||
import useSubscription from '@/nostr/useSubscription';
|
||||
import epoch from '@/utils/epoch';
|
||||
|
||||
type RelaysColumnDisplayProps = {
|
||||
columnIndex: number;
|
||||
@@ -23,21 +23,29 @@ const RelaysColumn: Component<RelaysColumnDisplayProps> = (props) => {
|
||||
const i18n = useTranslation();
|
||||
const { removeColumn } = useConfig();
|
||||
|
||||
const { events } = useSubscription(() => ({
|
||||
const loadMore = useLoadMore(() => ({
|
||||
duration: 4 * 60 * 60,
|
||||
}));
|
||||
|
||||
const { events, eose } = useSubscription(() => ({
|
||||
relayUrls: props.column.relayUrls,
|
||||
filters: [
|
||||
{
|
||||
kinds: [1],
|
||||
limit: 25,
|
||||
since: epoch() - 4 * 60 * 60,
|
||||
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(events()));
|
||||
|
||||
return (
|
||||
<Column
|
||||
header={
|
||||
@@ -51,8 +59,11 @@ const RelaysColumn: Component<RelaysColumnDisplayProps> = (props) => {
|
||||
width={props.column.width}
|
||||
columnIndex={props.columnIndex}
|
||||
lastColumn={props.lastColumn}
|
||||
timelineRef={loadMore.timelineRef}
|
||||
>
|
||||
<LoadMore loadMore={loadMore} eose={eose()}>
|
||||
<Timeline events={events()} />
|
||||
</LoadMore>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Component, createSignal, Show, JSX, onMount } from 'solid-js';
|
||||
import { Component, createEffect, createSignal, Show, JSX, onMount } from 'solid-js';
|
||||
|
||||
import EllipsisVertical from 'heroicons/24/outline/ellipsis-vertical.svg';
|
||||
import MagnifyingGlass from 'heroicons/24/outline/magnifying-glass.svg';
|
||||
|
||||
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 { SearchColumnType } from '@/core/column';
|
||||
import { applyContentFilter } from '@/core/contentFilter';
|
||||
@@ -84,7 +85,11 @@ export type SearchColumnDisplayProps = {
|
||||
const SearchColumn: Component<SearchColumnDisplayProps> = (props) => {
|
||||
const { removeColumn } = useConfig();
|
||||
|
||||
const { events } = useSubscription(() => {
|
||||
const loadMore = useLoadMore(() => ({
|
||||
duration: null,
|
||||
}));
|
||||
|
||||
const { events, eose } = useSubscription(() => {
|
||||
const { query } = props.column;
|
||||
|
||||
if (query.length === 0) return null;
|
||||
@@ -93,11 +98,14 @@ const SearchColumn: Component<SearchColumnDisplayProps> = (props) => {
|
||||
relayUrls: relaysForSearching,
|
||||
filters: [
|
||||
{
|
||||
kinds: [1, 6],
|
||||
kinds: [1],
|
||||
search: query,
|
||||
limit: 25,
|
||||
limit: 20,
|
||||
since: loadMore.since(),
|
||||
until: loadMore.until(),
|
||||
},
|
||||
],
|
||||
eoseLimit: 20,
|
||||
clientEventFilter: (event) => {
|
||||
if (event.tags.findIndex(([tagName]) => tagName === 'mostr' || tagName === 'proxy') >= 0)
|
||||
return false;
|
||||
@@ -107,6 +115,10 @@ const SearchColumn: Component<SearchColumnDisplayProps> = (props) => {
|
||||
};
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
loadMore.setEvents(events());
|
||||
});
|
||||
|
||||
return (
|
||||
<Column
|
||||
header={
|
||||
@@ -119,8 +131,11 @@ const SearchColumn: Component<SearchColumnDisplayProps> = (props) => {
|
||||
width={props.column.width}
|
||||
columnIndex={props.columnIndex}
|
||||
lastColumn={props.lastColumn}
|
||||
timelineRef={loadMore.timelineRef}
|
||||
>
|
||||
<LoadMore loadMore={loadMore} eose={eose()}>
|
||||
<Timeline events={events()} />
|
||||
</LoadMore>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
35
src/hooks/useScroll.ts
Normal file
35
src/hooks/useScroll.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { createSignal } from 'solid-js';
|
||||
|
||||
export type UseScroll = {
|
||||
targetRef: (el: HTMLElement) => void;
|
||||
currentPosition: () => number;
|
||||
scrollToTop: () => void;
|
||||
scrollToBottom: () => void;
|
||||
};
|
||||
|
||||
const useScroll = (): UseScroll => {
|
||||
const [elementRef, setElementRef] = createSignal<HTMLElement | undefined>();
|
||||
|
||||
const scrollToTop = () => {
|
||||
const el = elementRef();
|
||||
if (el == null) return;
|
||||
el.scrollTo(0, 0);
|
||||
};
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const el = elementRef();
|
||||
if (el == null) return;
|
||||
el.scrollTo(0, el.scrollHeight);
|
||||
};
|
||||
|
||||
const currentPosition = () => elementRef()?.scrollTop ?? 0;
|
||||
|
||||
return {
|
||||
targetRef: setElementRef,
|
||||
currentPosition,
|
||||
scrollToTop,
|
||||
scrollToBottom,
|
||||
};
|
||||
};
|
||||
|
||||
export default useScroll;
|
||||
@@ -32,6 +32,8 @@ export default {
|
||||
myPosts: 'My posts',
|
||||
myReactions: 'My reactions',
|
||||
back: 'Back',
|
||||
loadLatest: 'Load latest posts',
|
||||
loadOld: 'Load old posts',
|
||||
config: {
|
||||
columnWidth: 'Column width',
|
||||
widest: 'Widest',
|
||||
|
||||
@@ -31,6 +31,8 @@ export default {
|
||||
myPosts: '自分の投稿',
|
||||
myReactions: '自分のリアクション',
|
||||
back: '戻る',
|
||||
loadLatest: '最新の投稿を読み込む',
|
||||
loadOld: '古い投稿を読み込む',
|
||||
config: {
|
||||
columnWidth: 'カラム幅',
|
||||
widest: '特大',
|
||||
|
||||
@@ -19,5 +19,11 @@ export const pickLatestEvent = (events: NostrEvent[]): NostrEvent | undefined =>
|
||||
return events.reduce((a, b) => (compareEvents(a, b) > 0 ? a : b));
|
||||
};
|
||||
|
||||
export const pickOldestEvent = (events: NostrEvent[]): NostrEvent | undefined => {
|
||||
if (events.length === 0) return undefined;
|
||||
if (events.length === 1) return events[0];
|
||||
return events.reduce((a, b) => (-compareEvents(a, b) > 0 ? a : b));
|
||||
};
|
||||
|
||||
export const sortEvents = (events: NostrEvent[]) =>
|
||||
Array.from(events).sort((a, b) => -compareEvents(a, b));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSignal, createEffect, onMount, onCleanup, on } from 'solid-js';
|
||||
import { createSignal, createEffect, createMemo, onMount, onCleanup, on } from 'solid-js';
|
||||
|
||||
import uniqBy from 'lodash/uniqBy';
|
||||
import { type Filter } from 'nostr-tools/filter';
|
||||
@@ -25,6 +25,11 @@ export type UseSubscriptionProps = {
|
||||
* limit the number of events
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* limit the number of events until EOSE
|
||||
* This should be same to `limit` of REQ
|
||||
*/
|
||||
eoseLimit?: number;
|
||||
clientEventFilter?: (event: NostrEvent) => boolean;
|
||||
onEvent?: (event: NostrEvent & { id: string }) => void;
|
||||
onEOSE?: () => void;
|
||||
@@ -43,6 +48,11 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
||||
const { config, shouldMuteEvent } = useConfig();
|
||||
const pool = usePool();
|
||||
const [events, setEvents] = createSignal<NostrEvent[]>([]);
|
||||
const [eose, setEose] = createSignal<boolean>(false);
|
||||
const props = createMemo(propsProvider);
|
||||
|
||||
const eoseLimit = () => propsProvider()?.eoseLimit ?? 25;
|
||||
const limit = () => propsProvider()?.limit ?? 50;
|
||||
|
||||
createEffect(
|
||||
on(
|
||||
@@ -63,7 +73,6 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
||||
|
||||
const addEvent = (event: NostrEvent) => {
|
||||
const SecondsToIgnore = 300; // 5 min
|
||||
const limit = propsProvider()?.limit ?? 50;
|
||||
|
||||
const diffSec = event.created_at - epoch();
|
||||
if (diffSec > SecondsToIgnore) return;
|
||||
@@ -73,7 +82,7 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
||||
}
|
||||
|
||||
setEvents((current) => {
|
||||
const sorted = insertEventIntoDescendingList(current, event).slice(0, limit);
|
||||
const sorted = insertEventIntoDescendingList(current, event).slice(0, limit());
|
||||
// FIXME なぜか重複して取得される問題があるが一旦uniqByで対処
|
||||
// https://github.com/syusui-s/rabbit/issues/5
|
||||
const deduped = uniqBy(sorted, (e) => e.id);
|
||||
@@ -87,16 +96,26 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
||||
const startSubscription = () => {
|
||||
console.debug('startSubscription: start');
|
||||
|
||||
const props = propsProvider();
|
||||
if (props == null) return;
|
||||
const { relayUrls, filters, options, onEvent, onEOSE, continuous = true } = props;
|
||||
const currentProps = props();
|
||||
if (currentProps == null) return;
|
||||
const {
|
||||
relayUrls,
|
||||
filters,
|
||||
options,
|
||||
onEvent,
|
||||
onEOSE,
|
||||
clientEventFilter,
|
||||
continuous = true,
|
||||
} = currentProps;
|
||||
let subscribing = true;
|
||||
count += 1;
|
||||
|
||||
let pushed = false;
|
||||
let eose = false;
|
||||
setEose(false);
|
||||
const storedEvents: NostrEvent[] = [];
|
||||
|
||||
const updateEvents = () => setEvents(sortEvents(storedEvents).slice(0, eoseLimit()));
|
||||
|
||||
const sub = pool().subscribeMany(
|
||||
relayUrls,
|
||||
filters,
|
||||
@@ -107,11 +126,11 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
||||
if (onEvent != null) {
|
||||
onEvent(event as NostrEvent & { id: string });
|
||||
}
|
||||
if (props.clientEventFilter != null && !props.clientEventFilter(event)) {
|
||||
if (clientEventFilter != null && !clientEventFilter(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!eose) {
|
||||
if (!eose()) {
|
||||
pushed = true;
|
||||
storedEvents.push(event);
|
||||
} else {
|
||||
@@ -123,8 +142,8 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
||||
onEOSE();
|
||||
}
|
||||
|
||||
eose = true;
|
||||
setEvents(sortEvents(storedEvents));
|
||||
setEose(true);
|
||||
updateEvents();
|
||||
|
||||
if (!continuous) {
|
||||
sub.close();
|
||||
@@ -142,14 +161,14 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
||||
const intervalId = setInterval(() => {
|
||||
if (updating) return;
|
||||
updating = true;
|
||||
if (eose) {
|
||||
if (eose()) {
|
||||
clearInterval(intervalId);
|
||||
updating = false;
|
||||
return;
|
||||
}
|
||||
if (pushed) {
|
||||
pushed = false;
|
||||
setEvents(sortEvents(storedEvents));
|
||||
updateEvents();
|
||||
}
|
||||
updating = false;
|
||||
}, 100);
|
||||
@@ -165,11 +184,14 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
||||
});
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
startSubscription();
|
||||
});
|
||||
createEffect(
|
||||
on(
|
||||
() => [props()],
|
||||
() => startSubscription(),
|
||||
),
|
||||
);
|
||||
|
||||
return { events };
|
||||
return { events, eose };
|
||||
};
|
||||
|
||||
export default useSubscription;
|
||||
|
||||
Reference in New Issue
Block a user