Skip to content

Commit

Permalink
feat: jump to the toppest posts when clicking the top of column (#85)
Browse files Browse the repository at this point in the history
* feat: add useScroller

* feat: apply useScroller

* fix: integrate useScroll and useScroller

* fix: dissolve scroller dependency

* fix: remove broken ref

* refactor: revert old #85 changes

* feat: forward column operator

* feat: expose onClickHeader callback

* feat: jump to the top posts when clicking the column header

* fix: remove topMarkerRef

* feat: expose onLoad callback

* feat: scroll column to top on loading new page

* refactor: simplify expression

* chore: add code comment

---------

Co-authored-by: Shusui MOYATANI <[email protected]>
  • Loading branch information
penpenpng and syusui-s authored Dec 5, 2024
1 parent 12baa94 commit 99280e2
Show file tree
Hide file tree
Showing 10 changed files with 105 additions and 26 deletions.
7 changes: 6 additions & 1 deletion src/components/column/BasicColumnHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type BasicColumnHeaderProps = {
icon?: JSX.Element;
settings: () => JSX.Element;
onClose?: () => void;
onClickHeader?: () => void;
};

const BasicColumnHeader: Component<BasicColumnHeaderProps> = (props) => {
Expand All @@ -21,7 +22,11 @@ const BasicColumnHeader: Component<BasicColumnHeaderProps> = (props) => {
<Show when={props.icon} keyed>
{(icon) => <span class="inline-block size-4 shrink-0 text-fg-secondary">{icon}</span>}
</Show>
<span class="truncate">{props.name}</span>
<Show when={props.onClickHeader} fallback={<span class="truncate">{props.name}</span>}>
<button class="truncate" onClick={() => props.onClickHeader?.()}>
{props.name}
</button>
</Show>
</h2>
<button class="flex h-full place-items-center px-2" onClick={() => toggleSettingsOpened()}>
<span class="inline-block size-4">
Expand Down
8 changes: 6 additions & 2 deletions src/components/column/BookmarkColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Component, Show } from 'solid-js';
import { Component, createSignal, Show } from 'solid-js';

import BookmarkIcon from 'heroicons/24/outline/bookmark.svg';

import BasicColumnHeader from '@/components/column/BasicColumnHeader';
import Column from '@/components/column/Column';
import Column, { type ColumnOperator } from '@/components/column/Column';
import ColumnSettings from '@/components/column/ColumnSettings';
import Bookmark from '@/components/timeline/Bookmark';
import { BookmarkColumnType } from '@/core/column';
Expand All @@ -21,6 +21,8 @@ const BookmarkColumn: Component<BookmarkColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { removeColumn } = useConfig();

const [columnOperator, setColumnOperator] = createSignal<ColumnOperator>();

const { event } = useParameterizedReplaceableEvent(() => ({
kind: 30001,
author: props.column.pubkey,
Expand All @@ -37,11 +39,13 @@ const BookmarkColumn: Component<BookmarkColumnDisplayProps> = (props) => {
icon={<BookmarkIcon />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}
onClickHeader={() => columnOperator()?.scrollToTop()}
/>
}
width={props.column.width}
columnIndex={props.columnIndex}
lastColumn={props.lastColumn}
columnOperatorRef={setColumnOperator}
>
<Show when={event()} keyed>
{(ev) => <Bookmark event={ev} />}
Expand Down
30 changes: 27 additions & 3 deletions src/components/column/Column.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Show, type JSX, type Component } from 'solid-js';
import { Show, type JSX, type Component, createSignal, createEffect } from 'solid-js';

import ArrowLeft from 'heroicons/24/outline/arrow-left.svg';

Expand All @@ -14,8 +14,13 @@ export type ColumnProps = {
width: ColumnWidth;
header: JSX.Element;
children: JSX.Element;
columnOperatorRef?: (el: ColumnOperator) => void;
};

export interface ColumnOperator {
scrollToTop(): void;
}

const Column: Component<ColumnProps> = (props) => {
let columnDivRef: HTMLDivElement | undefined;

Expand All @@ -42,6 +47,20 @@ const Column: Component<ColumnProps> = (props) => {
},
}));

// 2つの `ref={setTimelineEl}` は `<Show>` の異なる分岐に存在し、
// 同時にレンダリングされないので、`timelineEl` は常にひとつの HTMLElement と関連づけられる。
const [timelineEl, setTimelineEl] = createSignal<HTMLElement>();

createEffect(() => {
const operator: ColumnOperator = {
scrollToTop: () => {
timelineEl()?.scrollTo(0, 0);
},
};

props.columnOperatorRef?.(operator);
});

return (
<TimelineContext.Provider value={timelineState}>
<div
Expand All @@ -60,7 +79,9 @@ 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 pb-16">{props.children}</div>
<div ref={setTimelineEl} class="scrollbar flex flex-col overflow-y-scroll pb-16">
{props.children}
</div>
</>
}
>
Expand All @@ -77,7 +98,10 @@ const Column: Component<ColumnProps> = (props) => {
<div>{i18n.t('column.back')}</div>
</button>
</div>
<div class="scrollbar flex max-h-full flex-col overflow-y-scroll scroll-smooth pb-16">
<div
ref={setTimelineEl}
class="scrollbar flex max-h-full flex-col overflow-y-scroll scroll-smooth pb-16"
>
<TimelineContentDisplay timelineContent={timeline} />
</div>
</>
Expand Down
11 changes: 9 additions & 2 deletions src/components/column/FollowingColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Component, createEffect, onCleanup, onMount } from 'solid-js';
import { Component, createEffect, createSignal, onCleanup, onMount } from 'solid-js';

import Home from 'heroicons/24/outline/home.svg';
import uniq from 'lodash/uniq';

import BasicColumnHeader from '@/components/column/BasicColumnHeader';
import Column from '@/components/column/Column';
import Column, { type ColumnOperator } from '@/components/column/Column';
import ColumnSettings from '@/components/column/ColumnSettings';
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
import Timeline from '@/components/timeline/Timeline';
Expand All @@ -27,8 +27,13 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {

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

const [columnOperator, setColumnOperator] = createSignal<ColumnOperator>();

const loadMore = useLoadMore(() => ({
duration: 4 * 60 * 60,
onLoad: () => {
columnOperator()?.scrollToTop();
},
}));

const { events, eose } = useSubscription(() => {
Expand Down Expand Up @@ -74,11 +79,13 @@ const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
icon={<Home />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}
onClickHeader={() => columnOperator()?.scrollToTop()}
/>
}
width={props.column.width}
columnIndex={props.columnIndex}
lastColumn={props.lastColumn}
columnOperatorRef={setColumnOperator}
>
<LoadMore loadMore={loadMore} eose={eose()}>
<Timeline events={events()} />
Expand Down
11 changes: 5 additions & 6 deletions src/components/column/LoadMore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import epoch from '@/utils/epoch';

export type UseLoadMoreProps = {
duration: number | null;
onLoad?: () => void;
};

export type UseLoadMore = {
Expand All @@ -26,7 +27,6 @@ export type UseLoadMore = {
continuous: Accessor<boolean>;
loadLatest: () => void;
loadOld: () => void;
setTopMarkerRef: (el: HTMLElement) => void;
};

export type LoadMoreProps = {
Expand All @@ -46,15 +46,15 @@ export const useLoadMore = (propsProvider: () => UseLoadMoreProps): UseLoadMore
const [events, setEvents] = createSignal<NostrEvent[]>([]);
const [since, setSince] = createSignal<number | undefined>(calcSince(epoch()));
const [until, setUntil] = createSignal<number | undefined>();
const [topMarkerRef, setTopMarkerRef] = createSignal<HTMLElement | undefined>();
const continuous = () => until() == null;

const loadLatest = () => {
batch(() => {
setUntil(undefined);
setSince(calcSince(epoch()));
});
topMarkerRef()?.scrollIntoView();

props().onLoad?.();
};

const loadOld = () => {
Expand All @@ -64,7 +64,8 @@ export const useLoadMore = (propsProvider: () => UseLoadMoreProps): UseLoadMore
setUntil(oldest.created_at);
setSince(calcSince(oldest.created_at));
});
topMarkerRef()?.scrollIntoView();

props().onLoad?.();
};

return {
Expand All @@ -74,7 +75,6 @@ export const useLoadMore = (propsProvider: () => UseLoadMoreProps): UseLoadMore
continuous,
loadLatest,
loadOld,
setTopMarkerRef,
};
};

Expand All @@ -84,7 +84,6 @@ const LoadMore: Component<LoadMoreProps> = (props) => {
return (
<>
<Show when={!props.loadMore.continuous()}>
<div class="none" ref={props.loadMore.setTopMarkerRef} />
<ColumnItem>
<button
class="flex h-12 w-full flex-col items-center justify-center hover:text-fg-secondary"
Expand Down
15 changes: 12 additions & 3 deletions src/components/column/NotificationColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type JSX, createEffect, Component, For } from 'solid-js';
import { type JSX, Component, For, createSignal, createEffect } from 'solid-js';

import ArrowPathRoundedSquare from 'heroicons/24/outline/arrow-path-rounded-square.svg';
import AtSymbol from 'heroicons/24/outline/at-symbol.svg';
Expand All @@ -8,7 +8,7 @@ import HeartSolid from 'heroicons/24/solid/heart.svg';
import uniq from 'lodash/uniq';

import BasicColumnHeader from '@/components/column/BasicColumnHeader';
import Column from '@/components/column/Column';
import Column, { type ColumnOperator } from '@/components/column/Column';
import ColumnSettings, {
ColumnSettingsSection,
RenderOtherSettingsProps,
Expand Down Expand Up @@ -123,7 +123,14 @@ const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) =>
const i18n = useTranslation();
const { config, removeColumn } = useConfig();

const loadMore = useLoadMore(() => ({ duration: null }));
const [columnOperator, setColumnOperator] = createSignal<ColumnOperator>();

const loadMore = useLoadMore(() => ({
duration: null,
onLoad: () => {
columnOperator()?.scrollToTop();
},
}));

const notificationTypes = () => props.column.notificationTypes ?? [...NotificationTypes];
const kinds = () => uniq(notificationTypes().flatMap((type) => NotificationTypeKindsMap[type]));
Expand Down Expand Up @@ -166,11 +173,13 @@ const NotificationColumn: Component<NotificationColumnDisplayProps> = (props) =>
/>
)}
onClose={() => removeColumn(props.column.id)}
onClickHeader={() => columnOperator()?.scrollToTop()}
/>
}
width={props.column.width}
columnIndex={props.columnIndex}
lastColumn={props.lastColumn}
columnOperatorRef={setColumnOperator}
>
<LoadMore loadMore={loadMore} eose={eose()}>
<Notification events={notifications()} />
Expand Down
15 changes: 12 additions & 3 deletions src/components/column/PostsColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createEffect, Component } from 'solid-js';
import { createEffect, Component, createSignal } from 'solid-js';

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

import BasicColumnHeader from '@/components/column/BasicColumnHeader';
import Column from '@/components/column/Column';
import Column, { type ColumnOperator } from '@/components/column/Column';
import ColumnSettings from '@/components/column/ColumnSettings';
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
import Timeline from '@/components/timeline/Timeline';
Expand All @@ -23,7 +23,14 @@ const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { config, removeColumn } = useConfig();

const loadMore = useLoadMore(() => ({ duration: null }));
const [columnOperator, setColumnOperator] = createSignal<ColumnOperator>();

const loadMore = useLoadMore(() => ({
duration: null,
onLoad: () => {
columnOperator()?.scrollToTop();
},
}));

const { events, eose } = useSubscription(() => ({
relayUrls: config().relayUrls,
Expand Down Expand Up @@ -53,11 +60,13 @@ const PostsColumn: Component<PostsColumnDisplayProps> = (props) => {
icon={<User />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}
onClickHeader={() => columnOperator()?.scrollToTop()}
/>
}
width={props.column.width}
columnIndex={props.columnIndex}
lastColumn={props.lastColumn}
columnOperatorRef={setColumnOperator}
>
<LoadMore loadMore={loadMore} eose={eose()}>
<Timeline events={events()} />
Expand Down
15 changes: 12 additions & 3 deletions src/components/column/ReactionsColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createEffect, Component } from 'solid-js';
import { createEffect, Component, createSignal } from 'solid-js';

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

import BasicColumnHeader from '@/components/column/BasicColumnHeader';
import Column from '@/components/column/Column';
import Column, { type ColumnOperator } from '@/components/column/Column';
import ColumnSettings from '@/components/column/ColumnSettings';
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
import Notification from '@/components/timeline/Notification';
Expand All @@ -23,7 +23,14 @@ const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { config, removeColumn } = useConfig();

const loadMore = useLoadMore(() => ({ duration: null }));
const [columnOperator, setColumnOperator] = createSignal<ColumnOperator>();

const loadMore = useLoadMore(() => ({
duration: null,
onLoad: () => {
columnOperator()?.scrollToTop();
},
}));

const { events: reactions, eose } = useSubscription(() => ({
relayUrls: config().relayUrls,
Expand Down Expand Up @@ -53,11 +60,13 @@ const ReactionsColumn: Component<ReactionsColumnDisplayProps> = (props) => {
icon={<Heart />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}
onClickHeader={() => columnOperator()?.scrollToTop()}
/>
}
width={props.column.width}
columnIndex={props.columnIndex}
lastColumn={props.lastColumn}
columnOperatorRef={setColumnOperator}
>
<LoadMore loadMore={loadMore} eose={eose()}>
<Notification events={reactions()} />
Expand Down
11 changes: 9 additions & 2 deletions src/components/column/RelaysColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createEffect, Component } from 'solid-js';
import { createEffect, Component, createSignal } 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 Column, { type ColumnOperator } from '@/components/column/Column';
import ColumnSettings from '@/components/column/ColumnSettings';
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
import Timeline from '@/components/timeline/Timeline';
Expand All @@ -23,8 +23,13 @@ const RelaysColumn: Component<RelaysColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { removeColumn } = useConfig();

const [columnOperator, setColumnOperator] = createSignal<ColumnOperator>();

const loadMore = useLoadMore(() => ({
duration: 4 * 60 * 60,
onLoad: () => {
columnOperator()?.scrollToTop();
},
}));

const { events, eose } = useSubscription(() => ({
Expand Down Expand Up @@ -54,11 +59,13 @@ const RelaysColumn: Component<RelaysColumnDisplayProps> = (props) => {
icon={<GlobeAlt />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}
onClickHeader={() => columnOperator()?.scrollToTop()}
/>
}
width={props.column.width}
columnIndex={props.columnIndex}
lastColumn={props.lastColumn}
columnOperatorRef={setColumnOperator}
>
<LoadMore loadMore={loadMore} eose={eose()}>
<Timeline events={events()} />
Expand Down
Loading

0 comments on commit 99280e2

Please sign in to comment.