ItemSelectionProvider: promote toot expansion

state

* used in Profile, TrendTimelinePanel,
  TimelinePanel
This commit is contained in:
thislight 2024-11-25 20:08:03 +08:00
parent dafc2c47a8
commit 9499182a8d
No known key found for this signature in database
GPG key ID: FCFE5192241CCD4E
5 changed files with 120 additions and 68 deletions

View file

@ -58,6 +58,10 @@ import Menu, { createManagedMenuState } from "~material/Menu";
import { share } from "~platform/share"; import { share } from "~platform/share";
import "./Profile.css"; import "./Profile.css";
import { useNavigator } from "~platform/StackedRouter"; import { useNavigator } from "~platform/StackedRouter";
import {
createSingluarItemSelection,
default as ItemSelectionProvider,
} from "../timelines/toots/ItemSelectionProvider";
const Profile: Component = () => { const Profile: Component = () => {
const { pop } = useNavigator(); const { pop } = useNavigator();
@ -70,6 +74,7 @@ const Profile: Component = () => {
}>(); }>();
const windowSize = useWindowSize(); const windowSize = useWindowSize();
const time = createTimeSource(); const time = createTimeSource();
const [, selectionState] = createSingluarItemSelection();
const menuButId = createUniqueId(); const menuButId = createUniqueId();
const recentTootListId = createUniqueId(); const recentTootListId = createUniqueId();
@ -499,8 +504,11 @@ const Profile: Component = () => {
></TootFilterButton> ></TootFilterButton>
</div> </div>
<ItemSelectionProvider value={selectionState}>
<TimeSourceProvider value={time}> <TimeSourceProvider value={time}>
<Show when={recentTootFilter().pinned && pinnedToots.list.length > 0}> <Show
when={recentTootFilter().pinned && pinnedToots.list.length > 0}
>
<TootList <TootList
threads={pinnedToots.list} threads={pinnedToots.list}
onUnknownThread={pinnedToots.getPath} onUnknownThread={pinnedToots.getPath}
@ -515,6 +523,7 @@ const Profile: Component = () => {
onChangeToot={recentToots.set} onChangeToot={recentToots.set}
/> />
</TimeSourceProvider> </TimeSourceProvider>
</ItemSelectionProvider>
<Show when={!recentTootChunk()?.done}> <Show when={!recentTootChunk()?.done}>
<div <div

View file

@ -26,11 +26,18 @@ import { useStore } from "@nanostores/solid";
import TrendTimelinePanel from "./TrendTimelinePanel"; import TrendTimelinePanel from "./TrendTimelinePanel";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import { useSessions } from "../masto/clients"; import { useSessions } from "../masto/clients";
import {
createSingluarItemSelection,
default as ItemSelectionProvider,
} from "./toots/ItemSelectionProvider";
const Home: ParentComponent = (props) => { const Home: ParentComponent = (props) => {
let panelList: HTMLDivElement; let panelList: HTMLDivElement;
useDocumentTitle("Timelines"); useDocumentTitle("Timelines");
const now = createTimeSource(); const now = createTimeSource();
const [, selectionState] = createSingluarItemSelection(
undefined as string | undefined,
);
const settings$ = useStore($settings); const settings$ = useStore($settings);
@ -115,7 +122,6 @@ const Home: ParentComponent = (props) => {
} }
}; };
css` css`
.tab-panel { .tab-panel {
overflow: visible auto; overflow: visible auto;
@ -195,6 +201,7 @@ const Home: ParentComponent = (props) => {
</AppBar> </AppBar>
} }
> >
<ItemSelectionProvider value={selectionState}>
<TimeSourceProvider value={now}> <TimeSourceProvider value={now}>
<Show when={!!client()}> <Show when={!!client()}>
<div <div
@ -229,6 +236,7 @@ const Home: ParentComponent = (props) => {
</div> </div>
</Show> </Show>
</TimeSourceProvider> </TimeSourceProvider>
</ItemSelectionProvider>
</Scaffold> </Scaffold>
</> </>
); );

View file

@ -4,7 +4,6 @@ import {
type Component, type Component,
type JSX, type JSX,
Show, Show,
createRenderEffect,
createSignal, createSignal,
type Setter, type Setter,
createContext, createContext,

View file

@ -7,6 +7,7 @@ import {
Index, Index,
createMemo, createMemo,
For, For,
createUniqueId,
} from "solid-js"; } from "solid-js";
import { type mastodon } from "masto"; import { type mastodon } from "masto";
import { vibrate } from "~platform/hardware"; import { vibrate } from "~platform/hardware";
@ -21,6 +22,7 @@ import cardStyle from "~material/cards.module.css";
import type { ThreadNode } from "../masto/timelines"; import type { ThreadNode } from "../masto/timelines";
import { useNavigator } from "~platform/StackedRouter"; import { useNavigator } from "~platform/StackedRouter";
import { ANIM_CURVE_STD } from "~material/theme"; import { ANIM_CURVE_STD } from "~material/theme";
import { useItemSelection } from "./toots/ItemSelectionProvider";
function durationOf(rect0: DOMRect, rect1: DOMRect) { function durationOf(rect0: DOMRect, rect1: DOMRect) {
const distancelt = Math.sqrt( const distancelt = Math.sqrt(
@ -45,6 +47,9 @@ function positionTootInThread(index: number, threadLength: number) {
return "middle"; return "middle";
} }
/**
* Full-feature toot list.
*/
const TootList: Component<{ const TootList: Component<{
ref?: Ref<HTMLDivElement>; ref?: Ref<HTMLDivElement>;
id?: string; id?: string;
@ -53,7 +58,7 @@ const TootList: Component<{
onChangeToot: (id: string, value: mastodon.v1.Status) => void; onChangeToot: (id: string, value: mastodon.v1.Status) => void;
}> = (props) => { }> = (props) => {
const session = useDefaultSession(); const session = useDefaultSession();
const [expandedThreadId, setExpandedThreadId] = createSignal<string>(); const [isExpanded, setExpanded] = useItemSelection();
const { push } = useNavigator(); const { push } = useNavigator();
const onBookmark = async (status: mastodon.v1.Status) => { const onBookmark = async (status: mastodon.v1.Status) => {
@ -190,7 +195,7 @@ const TootList: Component<{
event.currentTarget, event.currentTarget,
); );
if (actionableElement && checkIsExpended(status)) { if (actionableElement && isExpanded(event.currentTarget.id)) {
if (actionableElement.dataset.action === "acct") { if (actionableElement.dataset.action === "acct") {
event.stopPropagation(); event.stopPropagation();
@ -214,18 +219,13 @@ const TootList: Component<{
} }
// else if (!actionableElement || !checkIsExpended(status) || <rel is not one of known action>) // else if (!actionableElement || !checkIsExpended(status) || <rel is not one of known action>)
if (status.id !== expandedThreadId()) { if (!isExpanded(event.currentTarget.id)) {
setExpandedThreadId((x) => (x ? undefined : status.id)); setExpanded(event.currentTarget.id);
} else { } else {
openFullScreenToot(status, event.currentTarget as HTMLElement); openFullScreenToot(status, event.currentTarget as HTMLElement);
} }
}; };
const checkIsExpendedId = createSelector(expandedThreadId);
const checkIsExpended = (status: mastodon.v1.Status) =>
checkIsExpendedId(status.id);
const reply = ( const reply = (
status: mastodon.v1.Status, status: mastodon.v1.Status,
event: { currentTarget: HTMLElement }, event: { currentTarget: HTMLElement },
@ -234,10 +234,7 @@ const TootList: Component<{
openFullScreenToot(status, element, true); openFullScreenToot(status, element, true);
}; };
const vote = async ( const vote = async (status: mastodon.v1.Status, votes: readonly number[]) => {
status: mastodon.v1.Status,
votes: readonly number[]
) => {
const client = session()?.client; const client = session()?.client;
if (!client) return; if (!client) return;
@ -271,13 +268,15 @@ const TootList: Component<{
return <p>Oops: {String(err)}</p>; return <p>Oops: {String(err)}</p>;
}} }}
> >
<TootEnvProvider value={{ <TootEnvProvider
value={{
boost: toggleBoost, boost: toggleBoost,
bookmark: onBookmark, bookmark: onBookmark,
favourite: toggleFavourite, favourite: toggleFavourite,
reply: reply, reply: reply,
vote: vote vote: vote,
}}> }}
>
<div ref={props.ref} id={props.id} class="toot-list"> <div ref={props.ref} id={props.id} class="toot-list">
<For each={props.threads}> <For each={props.threads}>
{(threadId, threadIdx) => { {(threadId, threadIdx) => {
@ -291,11 +290,13 @@ const TootList: Component<{
<Index each={thread()}> <Index each={thread()}>
{(threadNode, index) => { {(threadNode, index) => {
const status = () => threadNode().value; const status = () => threadNode().value;
const id = createUniqueId();
return ( return (
<RegularToot <RegularToot
data-status-id={status().id} data-status-id={status().id}
data-thread-sort={index} data-thread-sort={index}
id={id}
status={status()} status={status()}
thread={ thread={
threadLength() > 1 threadLength() > 1
@ -303,8 +304,8 @@ const TootList: Component<{
: undefined : undefined
} }
class={cardStyle.card} class={cardStyle.card}
evaluated={checkIsExpended(status())} evaluated={isExpanded(id)}
actionable={checkIsExpended(status())} actionable={isExpanded(id)}
onClick={[onItemClick, status()]} onClick={[onItemClick, status()]}
/> />
); );

View file

@ -0,0 +1,35 @@
import {
createContext,
createSelector,
createSignal,
useContext,
type Accessor,
} from "solid-js";
export type ItemSelectionState<T> = [(value: T) => boolean, (value: T) => void];
const ItemSelectionContext = /* @__PURE__ */ createContext<
ItemSelectionState<any>
>([() => false, () => undefined]);
export function createSingluarItemSelection<T extends {}>(
intial?: T,
): readonly [Accessor<T | undefined>, ItemSelectionState<T | undefined>] {
const [value, setValue] = createSignal<T | undefined>(intial);
const select = createSelector(value, (a, b) =>
typeof b !== "undefined" ? a === b : false,
);
const toggle = (v: T | undefined) => {
setValue((o) => (o ? undefined : v));
};
return [value, [select, toggle]];
}
export function useItemSelection() {
return useContext(ItemSelectionContext);
}
export default ItemSelectionContext.Provider;