Skip to content

Commit

Permalink
fix arrow nav (#35)
Browse files Browse the repository at this point in the history
* fix arrow nav

* rm log
  • Loading branch information
zaknesler authored May 19, 2024
1 parent a3608c1 commit ab370a3
Show file tree
Hide file tree
Showing 17 changed files with 116 additions and 84 deletions.
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
version = "0.0.0"
edition = "2021"
authors = ["Zak Nesler"]
repository = "https://github.com/zaknesler/blend"
license = "MIT"
description = "Self-hosted RSS reader made with Rust + Solid.js, packaged nicely in a single binary."

[workspace]
members = ["crates/*"]
Expand Down
3 changes: 3 additions & 0 deletions crates/blend-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
name = "blend-config"
version.workspace = true
edition.workspace = true
authors.workspace = true
repository.workspace = true
license.workspace = true

[dependencies]
directories = "5.0"
Expand Down
3 changes: 3 additions & 0 deletions crates/blend-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
name = "blend-crypto"
version.workspace = true
edition.workspace = true
authors.workspace = true
repository.workspace = true
license.workspace = true

[dependencies]
blend-config = { path = "../blend-config" }
Expand Down
3 changes: 3 additions & 0 deletions crates/blend-db/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
name = "blend-db"
version.workspace = true
edition.workspace = true
authors.workspace = true
repository.workspace = true
license.workspace = true

[dependencies]
blend-config = { path = "../blend-config" }
Expand Down
3 changes: 3 additions & 0 deletions crates/blend-feed/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
name = "blend-feed"
version.workspace = true
edition.workspace = true
authors.workspace = true
repository.workspace = true
license.workspace = true

[dependencies]
chrono = { workspace = true, features = ["serde"] }
Expand Down
3 changes: 3 additions & 0 deletions crates/blend-web/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
name = "blend-web"
version.workspace = true
edition.workspace = true
authors.workspace = true
repository.workspace = true
license.workspace = true

[dependencies]
blend-config = { path = "../blend-config" }
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dependencies": {
"@kobalte/core": "^0.13.1",
"@kobalte/tailwindcss": "^0.9.0",
"@solid-primitives/active-element": "^2.0.20",
"@solid-primitives/bounds": "^0.0.121",
"@solid-primitives/keyboard": "^1.2.8",
"@solid-primitives/resize-observer": "^2.0.25",
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion ui/src/components/entry/entry-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createQuery } from '@tanstack/solid-query';
import { cx } from 'class-variance-authority';
import { type Component } from 'solid-js';
import { getEntry } from '~/api/entries';
import { DATA_ATTRIBUTES } from '~/constants/attributes';
import { QUERY_KEYS } from '~/constants/query';
import { useFeeds } from '~/hooks/queries/use-feeds';
import { useFilterParams } from '~/hooks/use-filter-params';
Expand Down Expand Up @@ -33,7 +34,7 @@ export const EntryItem: Component<EntryItemProps> = props => {

return (
<A
data-entry-item-uuid={props.entry.uuid}
{...{ [DATA_ATTRIBUTES.ENTRY_ITEM_UUID]: props.entry.uuid }}
href={filter.getEntryUrl(props.entry.uuid)}
activeClass="bg-gray-100 dark:bg-gray-950"
inactiveClass={cx(
Expand Down
55 changes: 5 additions & 50 deletions ui/src/components/entry/entry-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,22 @@ import { type NullableBounds, createElementBounds } from '@solid-primitives/boun
import { EntryItem } from './entry-item';
import { useFeeds } from '~/hooks/queries/use-feeds';
import { Empty } from '../ui/empty';
import { useFilterParams } from '~/hooks/use-filter-params';
import { getEntryComparator } from '~/utils/entries';
import { useListNav } from '~/hooks/use-list-nav';

type EntryListProps = {
containerBounds?: Readonly<NullableBounds>;
containsActiveElement?: boolean;
};

export const EntryList: Component<EntryListProps> = props => {
const filter = useFilterParams();

const [bottomOfList, setBottomOfList] = createSignal<HTMLElement>();
const listBounds = createElementBounds(bottomOfList);

const { feeds } = useFeeds();
const entries = useInfiniteEntries();

// Maintain local state of entries to prevent entries just marked as read from being removed
const [localEntries, setLocalEntries] = createSignal(entries.getAllEntries());

// Associate the local entries with the feed we're viewing, so we know when to reset the data
const [localFeedKey, setLocalFeedKey] = createSignal(filter.getFeedUrl());
// Handle arrow navigation
useListNav(() => ({ enabled: !!props.containsActiveElement, entries: entries.localEntries() }));

createEffect(() => {
if (!listBounds.bottom || !props.containerBounds?.bottom) return;
Expand All @@ -37,45 +31,6 @@ export const EntryList: Component<EntryListProps> = props => {
entries.fetchMore();
});

createEffect(() => {
const currentIds = localEntries().map(entry => entry.id);
const newEntries = entries.getAllEntries().filter(entry => !currentIds.includes(entry.id));

// Don't bother updating local state if we've got nothing to add
if (!newEntries.length) return;

// Add new entries and sort to maintain order
setLocalEntries([...localEntries(), ...newEntries].sort(getEntryComparator(filter.getSort())));
});

createEffect(() => {
if (!filter.params.entry_uuid || !props.containerBounds?.bottom) return;

const activeItem = document.querySelector(`[data-entry-item-uuid="${filter.params.entry_uuid}"]`);
if (!(activeItem instanceof HTMLElement)) return;

const bounds = activeItem.getBoundingClientRect();
const containerBottom = props.containerBounds.bottom;

const nearBounds = bounds.top <= containerBottom * 0.25 || bounds.bottom >= containerBottom * 0.9;
if (!nearBounds) return;

activeItem.scrollIntoView({ block: 'center' });
});

createEffect(() => {
const feedKey = filter.getFeedUrl();

// Only reset the local cache if we look at a new feed
if (localFeedKey() === feedKey) return;

// Reset the local cache
setLocalFeedKey(feedKey);
setLocalEntries(entries.getAllEntries());
});

useListNav(() => ({ entries: localEntries() }));

return (
<Switch>
<Match when={entries.query.isPending}>
Expand All @@ -91,9 +46,9 @@ export const EntryList: Component<EntryListProps> = props => {
</Match>

<Match when={entries.query.isSuccess && feeds.data}>
{localEntries().length ? (
{entries.localEntries().length ? (
<div class="-mt-2 flex flex-col gap-1 px-4 pb-2">
<For each={localEntries()}>{entry => <EntryItem entry={entry} />}</For>
<For each={entries.localEntries()}>{entry => <EntryItem entry={entry} />}</For>

<div ref={setBottomOfList} class="-mt-1" />

Expand Down
8 changes: 2 additions & 6 deletions ui/src/components/menus/menu-feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,8 @@ export const MenuFeed: Component<FeedMenuProps> = props => {
return (
<Menu {...local}>
<Menu.Item onSelect={handleRefresh}>Refresh</Menu.Item>
<Menu.Item onSelect={() => alert('rename')} disabled>
Rename
</Menu.Item>
<Menu.Item onSelect={() => alert('delete')} disabled>
Delete
</Menu.Item>
<Menu.Item disabled>Rename</Menu.Item>
<Menu.Item disabled>Delete</Menu.Item>
</Menu>
);
};
12 changes: 3 additions & 9 deletions ui/src/components/menus/menu-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,9 @@ export const MenuSettings: Component<MenuProps> = props => {

return (
<Menu {...local}>
<Menu.Item onSelect={() => {}} disabled>
Import/export
</Menu.Item>
<Menu.Item onSelect={() => {}} disabled>
Settings
</Menu.Item>
<Menu.Item onSelect={() => {}} disabled>
Sign out
</Menu.Item>
<Menu.Item disabled>Import/export</Menu.Item>
<Menu.Item disabled>Settings</Menu.Item>
<Menu.Item disabled>Sign out</Menu.Item>
</Menu>
);
};
3 changes: 3 additions & 0 deletions ui/src/constants/attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const DATA_ATTRIBUTES = {
ENTRY_ITEM_UUID: 'data-entry-item-uuid',
} as const;
31 changes: 31 additions & 0 deletions ui/src/hooks/queries/use-infinite-entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { QUERY_KEYS } from '~/constants/query';
import { type Entry } from '~/types/bindings';
import { useFilterParams } from '../use-filter-params';
import { debounce, leading } from '@solid-primitives/scheduled';
import { createEffect, createSignal } from 'solid-js';
import { getEntryComparator } from '~/utils/entries';

export const useInfiniteEntries = () => {
const filter = useFilterParams();
Expand All @@ -26,6 +28,34 @@ export const useInfiniteEntries = () => {

const getAllEntries = () => query.data?.pages.flatMap(page => page.data) || [];

// Maintain local state of entries to prevent entries just marked as read from being removed
const [localEntries, setLocalEntries] = createSignal(getAllEntries());

// Associate the local entries with the feed we're viewing, so we know when to reset the data
const [localFeedKey, setLocalFeedKey] = createSignal(filter.getFeedUrl());

createEffect(() => {
const currentIds = localEntries().map(entry => entry.id);
const newEntries = getAllEntries().filter(entry => !currentIds.includes(entry.id));

// Don't bother updating local state if we've got nothing to add
if (!newEntries.length) return;

// Add new entries and sort to maintain order
setLocalEntries([...localEntries(), ...newEntries].sort(getEntryComparator(filter.getSort())));
});

createEffect(() => {
const feedKey = filter.getFeedUrl();

// Only reset the local cache if we look at a new feed
if (localFeedKey() === feedKey) return;

// Reset the local cache
setLocalFeedKey(feedKey);
setLocalEntries(getAllEntries());
});

const fetchMore = leading(
debounce,
() => {
Expand All @@ -39,6 +69,7 @@ export const useInfiniteEntries = () => {

return {
query,
localEntries,
getAllEntries,
fetchMore,
};
Expand Down
27 changes: 16 additions & 11 deletions ui/src/hooks/use-list-nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { useKeyDownEvent } from '@solid-primitives/keyboard';
import { useFilterParams } from './use-filter-params';
import { debounce } from '@solid-primitives/scheduled';
import { useViewport } from './use-viewport';
import { findEntryItem } from '~/utils/entries';

type UseListNavParams = {
enabled: boolean;
entries: Entry[];
};

Expand All @@ -16,18 +18,8 @@ export const useListNav = (params: () => UseListNavParams) => {
const navigate = useNavigate();
const viewport = useViewport();

const maybeNavigate = debounce((direction: 'up' | 'down') => {
const currentIndex = params().entries.findIndex(entry => entry.uuid === filter.params.entry_uuid);

const offset = direction === 'up' ? -1 : 1;
const entry = params().entries[currentIndex + offset];
if (!entry) return;

navigate(filter.getEntryUrl(entry.uuid));
}, 30);

createEffect(() => {
if (!params().entries.length || viewport.lteBreakpoint('md')) return;
if (!params().entries.length || viewport.lteBreakpoint('md') || !params().enabled) return;

const e = keyDownEvent();
if (!e) return;
Expand All @@ -44,4 +36,17 @@ export const useListNav = (params: () => UseListNavParams) => {
break;
}
});

const maybeNavigate = debounce((direction: 'up' | 'down') => {
const currentIndex = params().entries.findIndex(entry => entry.uuid === filter.params.entry_uuid);

const offset = direction === 'up' ? -1 : 1;
const entry = params().entries[currentIndex + offset];
if (!entry) return;

const activeItem = findEntryItem(entry.uuid);
if (activeItem) activeItem.focus();

navigate(filter.getEntryUrl(entry.uuid));
}, 30);
};
18 changes: 11 additions & 7 deletions ui/src/routes/feed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { NavRow } from '~/components/nav/nav-row';
import { FeedList } from '~/components/feed/feed-list';
import { useViewport } from '~/hooks/use-viewport';
import { MenuFeeds } from '~/components/menus/menu-feeds';
import { createActiveElement } from '@solid-primitives/active-element';

export default () => {
const filter = useFilterParams();
Expand All @@ -28,6 +29,15 @@ export default () => {
const containerBounds = createElementBounds(container);
const containerScroll = createScrollPosition(container);

const viewingEntry = () => !!filter.params.entry_uuid;

const isMobile = () => lteBreakpoint('md');
const showPanel = () => !isMobile() || (isMobile() && !viewingEntry());
const showFeeds = () => lteBreakpoint('xl') && _showFeeds();

const activeElement = createActiveElement();
const containsActiveElement = () => container()?.contains(activeElement());

createEffect(() => {
if (!isRouting()) return;
setShowFeeds(false);
Expand All @@ -39,12 +49,6 @@ export default () => {
container()?.scrollTo({ top: 0, behavior: 'instant' });
});

const viewingEntry = () => !!filter.params.entry_uuid;

const isMobile = () => lteBreakpoint('md');
const showPanel = () => !isMobile() || (isMobile() && !viewingEntry());
const showFeeds = () => lteBreakpoint('xl') && _showFeeds();

return (
<>
<Sidebar class="hidden xl:flex xl:w-sidebar xl:shrink-0" />
Expand Down Expand Up @@ -106,7 +110,7 @@ export default () => {
<FeedList />
</div>
) : (
<EntryList containerBounds={containerBounds} />
<EntryList containerBounds={containerBounds} containsActiveElement={containsActiveElement()} />
)}
</div>
)}
Expand Down
Loading

0 comments on commit ab370a3

Please sign in to comment.