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';
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
|
|
||||||
export type ColumnProps = {
|
export type ColumnProps = {
|
||||||
|
timelineRef?: (el: HTMLDivElement) => void;
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
lastColumn: boolean;
|
lastColumn: boolean;
|
||||||
width: 'widest' | 'wide' | 'medium' | 'narrow' | null | undefined;
|
width: 'widest' | 'wide' | 'medium' | 'narrow' | null | undefined;
|
||||||
@@ -59,7 +60,7 @@ const Column: Component<ColumnProps> = (props) => {
|
|||||||
fallback={
|
fallback={
|
||||||
<>
|
<>
|
||||||
<div class="shrink-0 border-b border-border">{props.header}</div>
|
<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}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { uniq } from 'lodash';
|
|||||||
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
||||||
import Column from '@/components/column/Column';
|
import Column from '@/components/column/Column';
|
||||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||||
|
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
|
||||||
import Timeline from '@/components/timeline/Timeline';
|
import Timeline from '@/components/timeline/Timeline';
|
||||||
import { FollowingColumnType } from '@/core/column';
|
import { FollowingColumnType } from '@/core/column';
|
||||||
import { applyContentFilter } from '@/core/contentFilter';
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
@@ -13,7 +14,6 @@ import useConfig from '@/core/useConfig';
|
|||||||
import { useTranslation } from '@/i18n/useTranslation';
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import useFollowings from '@/nostr/useFollowings';
|
import useFollowings from '@/nostr/useFollowings';
|
||||||
import useSubscription from '@/nostr/useSubscription';
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
import epoch from '@/utils/epoch';
|
|
||||||
|
|
||||||
type FollowingColumnDisplayProps = {
|
type FollowingColumnDisplayProps = {
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
@@ -27,7 +27,11 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
|
|||||||
|
|
||||||
const { followingPubkeys } = useFollowings(() => ({ pubkey: props.column.pubkey }));
|
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()]);
|
const authors = uniq([...followingPubkeys()]);
|
||||||
if (authors.length === 0) return null;
|
if (authors.length === 0) return null;
|
||||||
return {
|
return {
|
||||||
@@ -37,10 +41,13 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
|
|||||||
{
|
{
|
||||||
kinds: [1, 6],
|
kinds: [1, 6],
|
||||||
authors,
|
authors,
|
||||||
limit: 10,
|
limit: 20,
|
||||||
since: epoch() - 4 * 60 * 60,
|
since: loadMore.since(),
|
||||||
|
until: loadMore.until(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
eoseLimit: 20,
|
||||||
|
continuous: loadMore.continuous(),
|
||||||
clientEventFilter: (event) => {
|
clientEventFilter: (event) => {
|
||||||
if (props.column.contentFilter == null) return true;
|
if (props.column.contentFilter == null) return true;
|
||||||
return applyContentFilter(props.column.contentFilter)(event.content);
|
return applyContentFilter(props.column.contentFilter)(event.content);
|
||||||
@@ -50,6 +57,7 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
|
|||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
console.log('home', events());
|
console.log('home', events());
|
||||||
|
loadMore.setEvents(events());
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => console.log('home timeline mounted'));
|
onMount(() => console.log('home timeline mounted'));
|
||||||
@@ -68,8 +76,11 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
|
|||||||
width={props.column.width}
|
width={props.column.width}
|
||||||
columnIndex={props.columnIndex}
|
columnIndex={props.columnIndex}
|
||||||
lastColumn={props.lastColumn}
|
lastColumn={props.lastColumn}
|
||||||
|
timelineRef={loadMore.timelineRef}
|
||||||
>
|
>
|
||||||
<Timeline events={events()} />
|
<LoadMore loadMore={loadMore} eose={eose()}>
|
||||||
|
<Timeline events={events()} />
|
||||||
|
</LoadMore>
|
||||||
</Column>
|
</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 Bell from 'heroicons/24/outline/bell.svg';
|
||||||
|
|
||||||
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
||||||
import Column from '@/components/column/Column';
|
import Column from '@/components/column/Column';
|
||||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||||
|
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
|
||||||
import Notification from '@/components/timeline/Notification';
|
import Notification from '@/components/timeline/Notification';
|
||||||
import { NotificationColumnType } from '@/core/column';
|
import { NotificationColumnType } from '@/core/column';
|
||||||
import { applyContentFilter } from '@/core/contentFilter';
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
@@ -22,21 +23,28 @@ const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) =>
|
|||||||
const i18n = useTranslation();
|
const i18n = useTranslation();
|
||||||
const { config, removeColumn } = useConfig();
|
const { config, removeColumn } = useConfig();
|
||||||
|
|
||||||
const { events: notifications } = useSubscription(() => ({
|
const loadMore = useLoadMore(() => ({ duration: null }));
|
||||||
|
|
||||||
|
const { events: notifications, eose } = useSubscription(() => ({
|
||||||
relayUrls: config().relayUrls,
|
relayUrls: config().relayUrls,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
kinds: [1, 6, 7, 9735],
|
kinds: [1, 6, 7, 9735],
|
||||||
'#p': [props.column.pubkey],
|
'#p': [props.column.pubkey],
|
||||||
limit: 10,
|
limit: 20,
|
||||||
|
since: loadMore.since(),
|
||||||
|
until: loadMore.until(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
eoseLimit: 20,
|
||||||
clientEventFilter: (event) => {
|
clientEventFilter: (event) => {
|
||||||
if (props.column.contentFilter == null) return true;
|
if (props.column.contentFilter == null) return true;
|
||||||
return applyContentFilter(props.column.contentFilter)(event.content);
|
return applyContentFilter(props.column.contentFilter)(event.content);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
createEffect(() => loadMore.setEvents(notifications()));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
@@ -50,8 +58,11 @@ const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) =>
|
|||||||
width={props.column.width}
|
width={props.column.width}
|
||||||
columnIndex={props.columnIndex}
|
columnIndex={props.columnIndex}
|
||||||
lastColumn={props.lastColumn}
|
lastColumn={props.lastColumn}
|
||||||
|
timelineRef={loadMore.timelineRef}
|
||||||
>
|
>
|
||||||
<Notification events={notifications()} />
|
<LoadMore loadMore={loadMore} eose={eose()}>
|
||||||
|
<Notification events={notifications()} />
|
||||||
|
</LoadMore>
|
||||||
</Column>
|
</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 User from 'heroicons/24/outline/user.svg';
|
||||||
|
|
||||||
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
||||||
import Column from '@/components/column/Column';
|
import Column from '@/components/column/Column';
|
||||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||||
|
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
|
||||||
import Timeline from '@/components/timeline/Timeline';
|
import Timeline from '@/components/timeline/Timeline';
|
||||||
import { PostsColumnType } from '@/core/column';
|
import { PostsColumnType } from '@/core/column';
|
||||||
import { applyContentFilter } from '@/core/contentFilter';
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
@@ -22,21 +23,28 @@ const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
|
|||||||
const i18n = useTranslation();
|
const i18n = useTranslation();
|
||||||
const { config, removeColumn } = useConfig();
|
const { config, removeColumn } = useConfig();
|
||||||
|
|
||||||
const { events } = useSubscription(() => ({
|
const loadMore = useLoadMore(() => ({ duration: null }));
|
||||||
|
|
||||||
|
const { events, eose } = useSubscription(() => ({
|
||||||
relayUrls: config().relayUrls,
|
relayUrls: config().relayUrls,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
kinds: [1, 6],
|
kinds: [1, 6],
|
||||||
authors: [props.column.pubkey],
|
authors: [props.column.pubkey],
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
since: loadMore.since(),
|
||||||
|
until: loadMore.until(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
eoseLimit: 10,
|
||||||
clientEventFilter: (event) => {
|
clientEventFilter: (event) => {
|
||||||
if (props.column.contentFilter == null) return true;
|
if (props.column.contentFilter == null) return true;
|
||||||
return applyContentFilter(props.column.contentFilter)(event.content);
|
return applyContentFilter(props.column.contentFilter)(event.content);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
createEffect(() => loadMore.setEvents(events()));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
@@ -50,8 +58,11 @@ const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
|
|||||||
width={props.column.width}
|
width={props.column.width}
|
||||||
columnIndex={props.columnIndex}
|
columnIndex={props.columnIndex}
|
||||||
lastColumn={props.lastColumn}
|
lastColumn={props.lastColumn}
|
||||||
|
timelineRef={loadMore.timelineRef}
|
||||||
>
|
>
|
||||||
<Timeline events={events()} />
|
<LoadMore loadMore={loadMore} eose={eose()}>
|
||||||
|
<Timeline events={events()} />
|
||||||
|
</LoadMore>
|
||||||
</Column>
|
</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 Heart from 'heroicons/24/outline/heart.svg';
|
||||||
|
|
||||||
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
||||||
import Column from '@/components/column/Column';
|
import Column from '@/components/column/Column';
|
||||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||||
|
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
|
||||||
import Notification from '@/components/timeline/Notification';
|
import Notification from '@/components/timeline/Notification';
|
||||||
import { ReactionsColumnType } from '@/core/column';
|
import { ReactionsColumnType } from '@/core/column';
|
||||||
import { applyContentFilter } from '@/core/contentFilter';
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
@@ -22,21 +23,28 @@ const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
|
|||||||
const i18n = useTranslation();
|
const i18n = useTranslation();
|
||||||
const { config, removeColumn } = useConfig();
|
const { config, removeColumn } = useConfig();
|
||||||
|
|
||||||
const { events: reactions } = useSubscription(() => ({
|
const loadMore = useLoadMore(() => ({ duration: null }));
|
||||||
|
|
||||||
|
const { events: reactions, eose } = useSubscription(() => ({
|
||||||
relayUrls: config().relayUrls,
|
relayUrls: config().relayUrls,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
kinds: [7],
|
kinds: [7],
|
||||||
authors: [props.column.pubkey],
|
authors: [props.column.pubkey],
|
||||||
limit: 10,
|
limit: 10,
|
||||||
|
since: loadMore.since(),
|
||||||
|
until: loadMore.until(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
eoseLimit: 10,
|
||||||
clientEventFilter: (event) => {
|
clientEventFilter: (event) => {
|
||||||
if (props.column.contentFilter == null) return true;
|
if (props.column.contentFilter == null) return true;
|
||||||
return applyContentFilter(props.column.contentFilter)(event.content);
|
return applyContentFilter(props.column.contentFilter)(event.content);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
createEffect(() => loadMore.setEvents(reactions()));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
@@ -50,8 +58,11 @@ const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
|
|||||||
width={props.column.width}
|
width={props.column.width}
|
||||||
columnIndex={props.columnIndex}
|
columnIndex={props.columnIndex}
|
||||||
lastColumn={props.lastColumn}
|
lastColumn={props.lastColumn}
|
||||||
|
timelineRef={loadMore.timelineRef}
|
||||||
>
|
>
|
||||||
<Notification events={reactions()} />
|
<LoadMore loadMore={loadMore} eose={eose()}>
|
||||||
|
<Notification events={reactions()} />
|
||||||
|
</LoadMore>
|
||||||
</Column>
|
</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 GlobeAlt from 'heroicons/24/outline/globe-alt.svg';
|
||||||
|
|
||||||
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
import BasicColumnHeader from '@/components/column/BasicColumnHeader';
|
||||||
import Column from '@/components/column/Column';
|
import Column from '@/components/column/Column';
|
||||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||||
|
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
|
||||||
import Timeline from '@/components/timeline/Timeline';
|
import Timeline from '@/components/timeline/Timeline';
|
||||||
import { RelaysColumnType } from '@/core/column';
|
import { RelaysColumnType } from '@/core/column';
|
||||||
import { applyContentFilter } from '@/core/contentFilter';
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
import useConfig from '@/core/useConfig';
|
import useConfig from '@/core/useConfig';
|
||||||
import { useTranslation } from '@/i18n/useTranslation';
|
import { useTranslation } from '@/i18n/useTranslation';
|
||||||
import useSubscription from '@/nostr/useSubscription';
|
import useSubscription from '@/nostr/useSubscription';
|
||||||
import epoch from '@/utils/epoch';
|
|
||||||
|
|
||||||
type RelaysColumnDisplayProps = {
|
type RelaysColumnDisplayProps = {
|
||||||
columnIndex: number;
|
columnIndex: number;
|
||||||
@@ -23,21 +23,29 @@ const RelaysColumn: Component<RelaysColumnDisplayProps> = (props) => {
|
|||||||
const i18n = useTranslation();
|
const i18n = useTranslation();
|
||||||
const { removeColumn } = useConfig();
|
const { removeColumn } = useConfig();
|
||||||
|
|
||||||
const { events } = useSubscription(() => ({
|
const loadMore = useLoadMore(() => ({
|
||||||
|
duration: 4 * 60 * 60,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { events, eose } = useSubscription(() => ({
|
||||||
relayUrls: props.column.relayUrls,
|
relayUrls: props.column.relayUrls,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
kinds: [1],
|
kinds: [1],
|
||||||
limit: 25,
|
limit: 20,
|
||||||
since: epoch() - 4 * 60 * 60,
|
since: loadMore.since(),
|
||||||
|
until: loadMore.until(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
eoseLimit: 20,
|
||||||
clientEventFilter: (event) => {
|
clientEventFilter: (event) => {
|
||||||
if (props.column.contentFilter == null) return true;
|
if (props.column.contentFilter == null) return true;
|
||||||
return applyContentFilter(props.column.contentFilter)(event.content);
|
return applyContentFilter(props.column.contentFilter)(event.content);
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
createEffect(() => loadMore.setEvents(events()));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
@@ -51,8 +59,11 @@ const RelaysColumn: Component<RelaysColumnDisplayProps> = (props) => {
|
|||||||
width={props.column.width}
|
width={props.column.width}
|
||||||
columnIndex={props.columnIndex}
|
columnIndex={props.columnIndex}
|
||||||
lastColumn={props.lastColumn}
|
lastColumn={props.lastColumn}
|
||||||
|
timelineRef={loadMore.timelineRef}
|
||||||
>
|
>
|
||||||
<Timeline events={events()} />
|
<LoadMore loadMore={loadMore} eose={eose()}>
|
||||||
|
<Timeline events={events()} />
|
||||||
|
</LoadMore>
|
||||||
</Column>
|
</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 EllipsisVertical from 'heroicons/24/outline/ellipsis-vertical.svg';
|
||||||
import MagnifyingGlass from 'heroicons/24/outline/magnifying-glass.svg';
|
import MagnifyingGlass from 'heroicons/24/outline/magnifying-glass.svg';
|
||||||
|
|
||||||
import Column from '@/components/column/Column';
|
import Column from '@/components/column/Column';
|
||||||
import ColumnSettings from '@/components/column/ColumnSettings';
|
import ColumnSettings from '@/components/column/ColumnSettings';
|
||||||
|
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
|
||||||
import Timeline from '@/components/timeline/Timeline';
|
import Timeline from '@/components/timeline/Timeline';
|
||||||
import { SearchColumnType } from '@/core/column';
|
import { SearchColumnType } from '@/core/column';
|
||||||
import { applyContentFilter } from '@/core/contentFilter';
|
import { applyContentFilter } from '@/core/contentFilter';
|
||||||
@@ -84,7 +85,11 @@ export type SearchColumnDisplayProps = {
|
|||||||
const SearchColumn: Component<SearchColumnDisplayProps> = (props) => {
|
const SearchColumn: Component<SearchColumnDisplayProps> = (props) => {
|
||||||
const { removeColumn } = useConfig();
|
const { removeColumn } = useConfig();
|
||||||
|
|
||||||
const { events } = useSubscription(() => {
|
const loadMore = useLoadMore(() => ({
|
||||||
|
duration: null,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { events, eose } = useSubscription(() => {
|
||||||
const { query } = props.column;
|
const { query } = props.column;
|
||||||
|
|
||||||
if (query.length === 0) return null;
|
if (query.length === 0) return null;
|
||||||
@@ -93,11 +98,14 @@ const SearchColumn: Component<SearchColumnDisplayProps> = (props) => {
|
|||||||
relayUrls: relaysForSearching,
|
relayUrls: relaysForSearching,
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
kinds: [1, 6],
|
kinds: [1],
|
||||||
search: query,
|
search: query,
|
||||||
limit: 25,
|
limit: 20,
|
||||||
|
since: loadMore.since(),
|
||||||
|
until: loadMore.until(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
eoseLimit: 20,
|
||||||
clientEventFilter: (event) => {
|
clientEventFilter: (event) => {
|
||||||
if (event.tags.findIndex(([tagName]) => tagName === 'mostr' || tagName === 'proxy') >= 0)
|
if (event.tags.findIndex(([tagName]) => tagName === 'mostr' || tagName === 'proxy') >= 0)
|
||||||
return false;
|
return false;
|
||||||
@@ -107,6 +115,10 @@ const SearchColumn: Component<SearchColumnDisplayProps> = (props) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
loadMore.setEvents(events());
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column
|
<Column
|
||||||
header={
|
header={
|
||||||
@@ -119,8 +131,11 @@ const SearchColumn: Component<SearchColumnDisplayProps> = (props) => {
|
|||||||
width={props.column.width}
|
width={props.column.width}
|
||||||
columnIndex={props.columnIndex}
|
columnIndex={props.columnIndex}
|
||||||
lastColumn={props.lastColumn}
|
lastColumn={props.lastColumn}
|
||||||
|
timelineRef={loadMore.timelineRef}
|
||||||
>
|
>
|
||||||
<Timeline events={events()} />
|
<LoadMore loadMore={loadMore} eose={eose()}>
|
||||||
|
<Timeline events={events()} />
|
||||||
|
</LoadMore>
|
||||||
</Column>
|
</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',
|
myPosts: 'My posts',
|
||||||
myReactions: 'My reactions',
|
myReactions: 'My reactions',
|
||||||
back: 'Back',
|
back: 'Back',
|
||||||
|
loadLatest: 'Load latest posts',
|
||||||
|
loadOld: 'Load old posts',
|
||||||
config: {
|
config: {
|
||||||
columnWidth: 'Column width',
|
columnWidth: 'Column width',
|
||||||
widest: 'Widest',
|
widest: 'Widest',
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export default {
|
|||||||
myPosts: '自分の投稿',
|
myPosts: '自分の投稿',
|
||||||
myReactions: '自分のリアクション',
|
myReactions: '自分のリアクション',
|
||||||
back: '戻る',
|
back: '戻る',
|
||||||
|
loadLatest: '最新の投稿を読み込む',
|
||||||
|
loadOld: '古い投稿を読み込む',
|
||||||
config: {
|
config: {
|
||||||
columnWidth: 'カラム幅',
|
columnWidth: 'カラム幅',
|
||||||
widest: '特大',
|
widest: '特大',
|
||||||
|
|||||||
@@ -19,5 +19,11 @@ export const pickLatestEvent = (events: NostrEvent[]): NostrEvent | undefined =>
|
|||||||
return events.reduce((a, b) => (compareEvents(a, b) > 0 ? a : b));
|
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[]) =>
|
export const sortEvents = (events: NostrEvent[]) =>
|
||||||
Array.from(events).sort((a, b) => -compareEvents(a, b));
|
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 uniqBy from 'lodash/uniqBy';
|
||||||
import { type Filter } from 'nostr-tools/filter';
|
import { type Filter } from 'nostr-tools/filter';
|
||||||
@@ -25,6 +25,11 @@ export type UseSubscriptionProps = {
|
|||||||
* limit the number of events
|
* limit the number of events
|
||||||
*/
|
*/
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
/**
|
||||||
|
* limit the number of events until EOSE
|
||||||
|
* This should be same to `limit` of REQ
|
||||||
|
*/
|
||||||
|
eoseLimit?: number;
|
||||||
clientEventFilter?: (event: NostrEvent) => boolean;
|
clientEventFilter?: (event: NostrEvent) => boolean;
|
||||||
onEvent?: (event: NostrEvent & { id: string }) => void;
|
onEvent?: (event: NostrEvent & { id: string }) => void;
|
||||||
onEOSE?: () => void;
|
onEOSE?: () => void;
|
||||||
@@ -43,6 +48,11 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
const { config, shouldMuteEvent } = useConfig();
|
const { config, shouldMuteEvent } = useConfig();
|
||||||
const pool = usePool();
|
const pool = usePool();
|
||||||
const [events, setEvents] = createSignal<NostrEvent[]>([]);
|
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(
|
createEffect(
|
||||||
on(
|
on(
|
||||||
@@ -63,7 +73,6 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
|
|
||||||
const addEvent = (event: NostrEvent) => {
|
const addEvent = (event: NostrEvent) => {
|
||||||
const SecondsToIgnore = 300; // 5 min
|
const SecondsToIgnore = 300; // 5 min
|
||||||
const limit = propsProvider()?.limit ?? 50;
|
|
||||||
|
|
||||||
const diffSec = event.created_at - epoch();
|
const diffSec = event.created_at - epoch();
|
||||||
if (diffSec > SecondsToIgnore) return;
|
if (diffSec > SecondsToIgnore) return;
|
||||||
@@ -73,7 +82,7 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setEvents((current) => {
|
setEvents((current) => {
|
||||||
const sorted = insertEventIntoDescendingList(current, event).slice(0, limit);
|
const sorted = insertEventIntoDescendingList(current, event).slice(0, limit());
|
||||||
// FIXME なぜか重複して取得される問題があるが一旦uniqByで対処
|
// FIXME なぜか重複して取得される問題があるが一旦uniqByで対処
|
||||||
// https://github.com/syusui-s/rabbit/issues/5
|
// https://github.com/syusui-s/rabbit/issues/5
|
||||||
const deduped = uniqBy(sorted, (e) => e.id);
|
const deduped = uniqBy(sorted, (e) => e.id);
|
||||||
@@ -87,16 +96,26 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
const startSubscription = () => {
|
const startSubscription = () => {
|
||||||
console.debug('startSubscription: start');
|
console.debug('startSubscription: start');
|
||||||
|
|
||||||
const props = propsProvider();
|
const currentProps = props();
|
||||||
if (props == null) return;
|
if (currentProps == null) return;
|
||||||
const { relayUrls, filters, options, onEvent, onEOSE, continuous = true } = props;
|
const {
|
||||||
|
relayUrls,
|
||||||
|
filters,
|
||||||
|
options,
|
||||||
|
onEvent,
|
||||||
|
onEOSE,
|
||||||
|
clientEventFilter,
|
||||||
|
continuous = true,
|
||||||
|
} = currentProps;
|
||||||
let subscribing = true;
|
let subscribing = true;
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|
||||||
let pushed = false;
|
let pushed = false;
|
||||||
let eose = false;
|
setEose(false);
|
||||||
const storedEvents: NostrEvent[] = [];
|
const storedEvents: NostrEvent[] = [];
|
||||||
|
|
||||||
|
const updateEvents = () => setEvents(sortEvents(storedEvents).slice(0, eoseLimit()));
|
||||||
|
|
||||||
const sub = pool().subscribeMany(
|
const sub = pool().subscribeMany(
|
||||||
relayUrls,
|
relayUrls,
|
||||||
filters,
|
filters,
|
||||||
@@ -107,11 +126,11 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
if (onEvent != null) {
|
if (onEvent != null) {
|
||||||
onEvent(event as NostrEvent & { id: string });
|
onEvent(event as NostrEvent & { id: string });
|
||||||
}
|
}
|
||||||
if (props.clientEventFilter != null && !props.clientEventFilter(event)) {
|
if (clientEventFilter != null && !clientEventFilter(event)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!eose) {
|
if (!eose()) {
|
||||||
pushed = true;
|
pushed = true;
|
||||||
storedEvents.push(event);
|
storedEvents.push(event);
|
||||||
} else {
|
} else {
|
||||||
@@ -123,8 +142,8 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
onEOSE();
|
onEOSE();
|
||||||
}
|
}
|
||||||
|
|
||||||
eose = true;
|
setEose(true);
|
||||||
setEvents(sortEvents(storedEvents));
|
updateEvents();
|
||||||
|
|
||||||
if (!continuous) {
|
if (!continuous) {
|
||||||
sub.close();
|
sub.close();
|
||||||
@@ -142,14 +161,14 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
if (updating) return;
|
if (updating) return;
|
||||||
updating = true;
|
updating = true;
|
||||||
if (eose) {
|
if (eose()) {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
updating = false;
|
updating = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (pushed) {
|
if (pushed) {
|
||||||
pushed = false;
|
pushed = false;
|
||||||
setEvents(sortEvents(storedEvents));
|
updateEvents();
|
||||||
}
|
}
|
||||||
updating = false;
|
updating = false;
|
||||||
}, 100);
|
}, 100);
|
||||||
@@ -165,11 +184,14 @@ const useSubscription = (propsProvider: () => UseSubscriptionProps | null) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(
|
||||||
startSubscription();
|
on(
|
||||||
});
|
() => [props()],
|
||||||
|
() => startSubscription(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return { events };
|
return { events, eose };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useSubscription;
|
export default useSubscription;
|
||||||
|
|||||||
Reference in New Issue
Block a user