ItemSelectionProvider: promote toot expansion
state * used in Profile, TrendTimelinePanel, TimelinePanel
This commit is contained in:
		
							parent
							
								
									dafc2c47a8
								
							
						
					
					
						commit
						9499182a8d
					
				
					 5 changed files with 120 additions and 68 deletions
				
			
		| 
						 | 
				
			
			@ -58,6 +58,10 @@ import Menu, { createManagedMenuState } from "~material/Menu";
 | 
			
		|||
import { share } from "~platform/share";
 | 
			
		||||
import "./Profile.css";
 | 
			
		||||
import { useNavigator } from "~platform/StackedRouter";
 | 
			
		||||
import {
 | 
			
		||||
  createSingluarItemSelection,
 | 
			
		||||
  default as ItemSelectionProvider,
 | 
			
		||||
} from "../timelines/toots/ItemSelectionProvider";
 | 
			
		||||
 | 
			
		||||
const Profile: Component = () => {
 | 
			
		||||
  const { pop } = useNavigator();
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +74,7 @@ const Profile: Component = () => {
 | 
			
		|||
  }>();
 | 
			
		||||
  const windowSize = useWindowSize();
 | 
			
		||||
  const time = createTimeSource();
 | 
			
		||||
  const [, selectionState] = createSingluarItemSelection();
 | 
			
		||||
 | 
			
		||||
  const menuButId = createUniqueId();
 | 
			
		||||
  const recentTootListId = createUniqueId();
 | 
			
		||||
| 
						 | 
				
			
			@ -499,22 +504,26 @@ const Profile: Component = () => {
 | 
			
		|||
          ></TootFilterButton>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <TimeSourceProvider value={time}>
 | 
			
		||||
          <Show when={recentTootFilter().pinned && pinnedToots.list.length > 0}>
 | 
			
		||||
        <ItemSelectionProvider value={selectionState}>
 | 
			
		||||
          <TimeSourceProvider value={time}>
 | 
			
		||||
            <Show
 | 
			
		||||
              when={recentTootFilter().pinned && pinnedToots.list.length > 0}
 | 
			
		||||
            >
 | 
			
		||||
              <TootList
 | 
			
		||||
                threads={pinnedToots.list}
 | 
			
		||||
                onUnknownThread={pinnedToots.getPath}
 | 
			
		||||
                onChangeToot={pinnedToots.set}
 | 
			
		||||
              />
 | 
			
		||||
              <Divider />
 | 
			
		||||
            </Show>
 | 
			
		||||
            <TootList
 | 
			
		||||
              threads={pinnedToots.list}
 | 
			
		||||
              onUnknownThread={pinnedToots.getPath}
 | 
			
		||||
              onChangeToot={pinnedToots.set}
 | 
			
		||||
              id={recentTootListId}
 | 
			
		||||
              threads={recentToots.list}
 | 
			
		||||
              onUnknownThread={recentToots.getPath}
 | 
			
		||||
              onChangeToot={recentToots.set}
 | 
			
		||||
            />
 | 
			
		||||
            <Divider />
 | 
			
		||||
          </Show>
 | 
			
		||||
          <TootList
 | 
			
		||||
            id={recentTootListId}
 | 
			
		||||
            threads={recentToots.list}
 | 
			
		||||
            onUnknownThread={recentToots.getPath}
 | 
			
		||||
            onChangeToot={recentToots.set}
 | 
			
		||||
          />
 | 
			
		||||
        </TimeSourceProvider>
 | 
			
		||||
          </TimeSourceProvider>
 | 
			
		||||
        </ItemSelectionProvider>
 | 
			
		||||
 | 
			
		||||
        <Show when={!recentTootChunk()?.done}>
 | 
			
		||||
          <div
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,11 +26,18 @@ import { useStore } from "@nanostores/solid";
 | 
			
		|||
import TrendTimelinePanel from "./TrendTimelinePanel";
 | 
			
		||||
import TimelinePanel from "./TimelinePanel";
 | 
			
		||||
import { useSessions } from "../masto/clients";
 | 
			
		||||
import {
 | 
			
		||||
  createSingluarItemSelection,
 | 
			
		||||
  default as ItemSelectionProvider,
 | 
			
		||||
} from "./toots/ItemSelectionProvider";
 | 
			
		||||
 | 
			
		||||
const Home: ParentComponent = (props) => {
 | 
			
		||||
  let panelList: HTMLDivElement;
 | 
			
		||||
  useDocumentTitle("Timelines");
 | 
			
		||||
  const now = createTimeSource();
 | 
			
		||||
  const [, selectionState] = createSingluarItemSelection(
 | 
			
		||||
    undefined as string | undefined,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const settings$ = useStore($settings);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -115,7 +122,6 @@ const Home: ParentComponent = (props) => {
 | 
			
		|||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  css`
 | 
			
		||||
    .tab-panel {
 | 
			
		||||
      overflow: visible auto;
 | 
			
		||||
| 
						 | 
				
			
			@ -195,40 +201,42 @@ const Home: ParentComponent = (props) => {
 | 
			
		|||
          </AppBar>
 | 
			
		||||
        }
 | 
			
		||||
      >
 | 
			
		||||
        <TimeSourceProvider value={now}>
 | 
			
		||||
          <Show when={!!client()}>
 | 
			
		||||
            <div
 | 
			
		||||
              class="panel-list"
 | 
			
		||||
              ref={panelList!}
 | 
			
		||||
              onScroll={requestRecalculateTabIndicator}
 | 
			
		||||
            >
 | 
			
		||||
              <div class="tab-panel">
 | 
			
		||||
                <div>
 | 
			
		||||
                  <TimelinePanel
 | 
			
		||||
                    client={client()}
 | 
			
		||||
                    name="home"
 | 
			
		||||
                    prefetch={prefetching()}
 | 
			
		||||
                  />
 | 
			
		||||
        <ItemSelectionProvider value={selectionState}>
 | 
			
		||||
          <TimeSourceProvider value={now}>
 | 
			
		||||
            <Show when={!!client()}>
 | 
			
		||||
              <div
 | 
			
		||||
                class="panel-list"
 | 
			
		||||
                ref={panelList!}
 | 
			
		||||
                onScroll={requestRecalculateTabIndicator}
 | 
			
		||||
              >
 | 
			
		||||
                <div class="tab-panel">
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <TimelinePanel
 | 
			
		||||
                      client={client()}
 | 
			
		||||
                      name="home"
 | 
			
		||||
                      prefetch={prefetching()}
 | 
			
		||||
                    />
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="tab-panel">
 | 
			
		||||
                <div>
 | 
			
		||||
                  <TrendTimelinePanel client={client()} />
 | 
			
		||||
                <div class="tab-panel">
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <TrendTimelinePanel client={client()} />
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="tab-panel">
 | 
			
		||||
                <div>
 | 
			
		||||
                  <TimelinePanel
 | 
			
		||||
                    client={client()}
 | 
			
		||||
                    name="public"
 | 
			
		||||
                    prefetch={prefetching()}
 | 
			
		||||
                  />
 | 
			
		||||
                <div class="tab-panel">
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <TimelinePanel
 | 
			
		||||
                      client={client()}
 | 
			
		||||
                      name="public"
 | 
			
		||||
                      prefetch={prefetching()}
 | 
			
		||||
                    />
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div></div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div></div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </Show>
 | 
			
		||||
        </TimeSourceProvider>
 | 
			
		||||
            </Show>
 | 
			
		||||
          </TimeSourceProvider>
 | 
			
		||||
        </ItemSelectionProvider>
 | 
			
		||||
      </Scaffold>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,6 @@ import {
 | 
			
		|||
  type Component,
 | 
			
		||||
  type JSX,
 | 
			
		||||
  Show,
 | 
			
		||||
  createRenderEffect,
 | 
			
		||||
  createSignal,
 | 
			
		||||
  type Setter,
 | 
			
		||||
  createContext,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ import {
 | 
			
		|||
  Index,
 | 
			
		||||
  createMemo,
 | 
			
		||||
  For,
 | 
			
		||||
  createUniqueId,
 | 
			
		||||
} from "solid-js";
 | 
			
		||||
import { type mastodon } from "masto";
 | 
			
		||||
import { vibrate } from "~platform/hardware";
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +22,7 @@ import cardStyle from "~material/cards.module.css";
 | 
			
		|||
import type { ThreadNode } from "../masto/timelines";
 | 
			
		||||
import { useNavigator } from "~platform/StackedRouter";
 | 
			
		||||
import { ANIM_CURVE_STD } from "~material/theme";
 | 
			
		||||
import { useItemSelection } from "./toots/ItemSelectionProvider";
 | 
			
		||||
 | 
			
		||||
function durationOf(rect0: DOMRect, rect1: DOMRect) {
 | 
			
		||||
  const distancelt = Math.sqrt(
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +47,9 @@ function positionTootInThread(index: number, threadLength: number) {
 | 
			
		|||
  return "middle";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Full-feature toot list.
 | 
			
		||||
 */
 | 
			
		||||
const TootList: Component<{
 | 
			
		||||
  ref?: Ref<HTMLDivElement>;
 | 
			
		||||
  id?: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -53,7 +58,7 @@ const TootList: Component<{
 | 
			
		|||
  onChangeToot: (id: string, value: mastodon.v1.Status) => void;
 | 
			
		||||
}> = (props) => {
 | 
			
		||||
  const session = useDefaultSession();
 | 
			
		||||
  const [expandedThreadId, setExpandedThreadId] = createSignal<string>();
 | 
			
		||||
  const [isExpanded, setExpanded] = useItemSelection();
 | 
			
		||||
  const { push } = useNavigator();
 | 
			
		||||
 | 
			
		||||
  const onBookmark = async (status: mastodon.v1.Status) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -190,7 +195,7 @@ const TootList: Component<{
 | 
			
		|||
      event.currentTarget,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (actionableElement && checkIsExpended(status)) {
 | 
			
		||||
    if (actionableElement && isExpanded(event.currentTarget.id)) {
 | 
			
		||||
      if (actionableElement.dataset.action === "acct") {
 | 
			
		||||
        event.stopPropagation();
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -214,18 +219,13 @@ const TootList: Component<{
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    // else if (!actionableElement || !checkIsExpended(status) || <rel is not one of known action>)
 | 
			
		||||
    if (status.id !== expandedThreadId()) {
 | 
			
		||||
      setExpandedThreadId((x) => (x ? undefined : status.id));
 | 
			
		||||
    if (!isExpanded(event.currentTarget.id)) {
 | 
			
		||||
      setExpanded(event.currentTarget.id);
 | 
			
		||||
    } else {
 | 
			
		||||
      openFullScreenToot(status, event.currentTarget as HTMLElement);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const checkIsExpendedId = createSelector(expandedThreadId);
 | 
			
		||||
 | 
			
		||||
  const checkIsExpended = (status: mastodon.v1.Status) =>
 | 
			
		||||
    checkIsExpendedId(status.id);
 | 
			
		||||
 | 
			
		||||
  const reply = (
 | 
			
		||||
    status: mastodon.v1.Status,
 | 
			
		||||
    event: { currentTarget: HTMLElement },
 | 
			
		||||
| 
						 | 
				
			
			@ -234,10 +234,7 @@ const TootList: Component<{
 | 
			
		|||
    openFullScreenToot(status, element, true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const vote = async (
 | 
			
		||||
    status: mastodon.v1.Status,
 | 
			
		||||
    votes: readonly number[]
 | 
			
		||||
  ) => {
 | 
			
		||||
  const vote = async (status: mastodon.v1.Status, votes: readonly number[]) => {
 | 
			
		||||
    const client = session()?.client;
 | 
			
		||||
    if (!client) return;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -271,13 +268,15 @@ const TootList: Component<{
 | 
			
		|||
        return <p>Oops: {String(err)}</p>;
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <TootEnvProvider value={{
 | 
			
		||||
        boost: toggleBoost,
 | 
			
		||||
        bookmark: onBookmark,
 | 
			
		||||
        favourite: toggleFavourite,
 | 
			
		||||
        reply: reply,
 | 
			
		||||
        vote: vote
 | 
			
		||||
      }}>
 | 
			
		||||
      <TootEnvProvider
 | 
			
		||||
        value={{
 | 
			
		||||
          boost: toggleBoost,
 | 
			
		||||
          bookmark: onBookmark,
 | 
			
		||||
          favourite: toggleFavourite,
 | 
			
		||||
          reply: reply,
 | 
			
		||||
          vote: vote,
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <div ref={props.ref} id={props.id} class="toot-list">
 | 
			
		||||
          <For each={props.threads}>
 | 
			
		||||
            {(threadId, threadIdx) => {
 | 
			
		||||
| 
						 | 
				
			
			@ -291,11 +290,13 @@ const TootList: Component<{
 | 
			
		|||
                <Index each={thread()}>
 | 
			
		||||
                  {(threadNode, index) => {
 | 
			
		||||
                    const status = () => threadNode().value;
 | 
			
		||||
                    const id = createUniqueId();
 | 
			
		||||
 | 
			
		||||
                    return (
 | 
			
		||||
                      <RegularToot
 | 
			
		||||
                        data-status-id={status().id}
 | 
			
		||||
                        data-thread-sort={index}
 | 
			
		||||
                        id={id}
 | 
			
		||||
                        status={status()}
 | 
			
		||||
                        thread={
 | 
			
		||||
                          threadLength() > 1
 | 
			
		||||
| 
						 | 
				
			
			@ -303,8 +304,8 @@ const TootList: Component<{
 | 
			
		|||
                            : undefined
 | 
			
		||||
                        }
 | 
			
		||||
                        class={cardStyle.card}
 | 
			
		||||
                        evaluated={checkIsExpended(status())}
 | 
			
		||||
                        actionable={checkIsExpended(status())}
 | 
			
		||||
                        evaluated={isExpanded(id)}
 | 
			
		||||
                        actionable={isExpanded(id)}
 | 
			
		||||
                        onClick={[onItemClick, status()]}
 | 
			
		||||
                      />
 | 
			
		||||
                    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										35
									
								
								src/timelines/toots/ItemSelectionProvider.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/timelines/toots/ItemSelectionProvider.ts
									
										
									
									
									
										Normal 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;
 | 
			
		||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue