move TimelinePanel to separated files
- added TrendTimelinePanel for trending tab
This commit is contained in:
		
							parent
							
								
									ab29e5fd70
								
							
						
					
					
						commit
						971fb6a8e7
					
				
					 4 changed files with 492 additions and 200 deletions
				
			
		| 
						 | 
					@ -1,26 +1,16 @@
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Component,
 | 
					 | 
				
			||||||
  For,
 | 
					 | 
				
			||||||
  onCleanup,
 | 
					 | 
				
			||||||
  createSignal,
 | 
					  createSignal,
 | 
				
			||||||
  Show,
 | 
					  Show,
 | 
				
			||||||
  untrack,
 | 
					 | 
				
			||||||
  onMount,
 | 
					  onMount,
 | 
				
			||||||
  type ParentComponent,
 | 
					  type ParentComponent,
 | 
				
			||||||
  children,
 | 
					  children,
 | 
				
			||||||
  Suspense,
 | 
					  Suspense,
 | 
				
			||||||
  Match,
 | 
					 | 
				
			||||||
  Switch as JsSwitch,
 | 
					 | 
				
			||||||
  ErrorBoundary,
 | 
					 | 
				
			||||||
} from "solid-js";
 | 
					} from "solid-js";
 | 
				
			||||||
import { useDocumentTitle } from "../utils";
 | 
					import { useDocumentTitle } from "../utils";
 | 
				
			||||||
import { type mastodon } from "masto";
 | 
					import { type mastodon } from "masto";
 | 
				
			||||||
import Scaffold from "../material/Scaffold";
 | 
					import Scaffold from "../material/Scaffold";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  AppBar,
 | 
					  AppBar,
 | 
				
			||||||
  Button,
 | 
					 | 
				
			||||||
  Fab,
 | 
					 | 
				
			||||||
  LinearProgress,
 | 
					 | 
				
			||||||
  ListItemSecondaryAction,
 | 
					  ListItemSecondaryAction,
 | 
				
			||||||
  ListItemText,
 | 
					  ListItemText,
 | 
				
			||||||
  MenuItem,
 | 
					  MenuItem,
 | 
				
			||||||
| 
						 | 
					@ -29,205 +19,21 @@ import {
 | 
				
			||||||
} from "@suid/material";
 | 
					} from "@suid/material";
 | 
				
			||||||
import { css } from "solid-styled";
 | 
					import { css } from "solid-styled";
 | 
				
			||||||
import { TimeSourceProvider, createTimeSource } from "../platform/timesrc";
 | 
					import { TimeSourceProvider, createTimeSource } from "../platform/timesrc";
 | 
				
			||||||
import TootThread from "./TootThread.js";
 | 
					 | 
				
			||||||
import ProfileMenuButton from "./ProfileMenuButton";
 | 
					import ProfileMenuButton from "./ProfileMenuButton";
 | 
				
			||||||
import Tabs from "../material/Tabs";
 | 
					import Tabs from "../material/Tabs";
 | 
				
			||||||
import Tab from "../material/Tab";
 | 
					import Tab from "../material/Tab";
 | 
				
			||||||
import { Create as CreateTootIcon } from "@suid/icons-material";
 | 
					 | 
				
			||||||
import { useTimeline } from "../masto/timelines";
 | 
					 | 
				
			||||||
import { makeEventListener } from "@solid-primitives/event-listener";
 | 
					import { makeEventListener } from "@solid-primitives/event-listener";
 | 
				
			||||||
import BottomSheet, {
 | 
					import BottomSheet, {
 | 
				
			||||||
  HERO as BOTTOM_SHEET_HERO,
 | 
					  HERO as BOTTOM_SHEET_HERO,
 | 
				
			||||||
} from "../material/BottomSheet";
 | 
					} from "../material/BottomSheet";
 | 
				
			||||||
import { $settings } from "../settings/stores";
 | 
					import { $settings } from "../settings/stores";
 | 
				
			||||||
import { useStore } from "@nanostores/solid";
 | 
					import { useStore } from "@nanostores/solid";
 | 
				
			||||||
import { vibrate } from "../platform/hardware";
 | 
					 | 
				
			||||||
import PullDownToRefresh from "./PullDownToRefresh";
 | 
					 | 
				
			||||||
import { HeroSourceProvider, type HeroSource } from "../platform/anim";
 | 
					import { HeroSourceProvider, type HeroSource } from "../platform/anim";
 | 
				
			||||||
import { useNavigate } from "@solidjs/router";
 | 
					import { useNavigate } from "@solidjs/router";
 | 
				
			||||||
import { useSignedInProfiles } from "../masto/acct";
 | 
					import { useSignedInProfiles } from "../masto/acct";
 | 
				
			||||||
import { setCache as setTootBottomSheetCache } from "./TootBottomSheet";
 | 
					import { setCache as setTootBottomSheetCache } from "./TootBottomSheet";
 | 
				
			||||||
import TootComposer from "./TootComposer";
 | 
					import TrendTimelinePanel from "./TrendTimelinePanel";
 | 
				
			||||||
 | 
					import TimelinePanel from "./TimelinePanel";
 | 
				
			||||||
const TimelinePanel: Component<{
 | 
					 | 
				
			||||||
  client: mastodon.rest.Client;
 | 
					 | 
				
			||||||
  name: "home" | "public" | "trends";
 | 
					 | 
				
			||||||
  prefetch?: boolean;
 | 
					 | 
				
			||||||
  fullRefetch?: number;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  openFullScreenToot: (
 | 
					 | 
				
			||||||
    toot: mastodon.v1.Status,
 | 
					 | 
				
			||||||
    srcElement?: HTMLElement,
 | 
					 | 
				
			||||||
    reply?: boolean,
 | 
					 | 
				
			||||||
  ) => void;
 | 
					 | 
				
			||||||
}> = (props) => {
 | 
					 | 
				
			||||||
  const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>();
 | 
					 | 
				
			||||||
  const [
 | 
					 | 
				
			||||||
    timeline,
 | 
					 | 
				
			||||||
    snapshot,
 | 
					 | 
				
			||||||
    { refetch: refetchTimeline, mutate: mutateTimeline },
 | 
					 | 
				
			||||||
  ] = useTimeline(
 | 
					 | 
				
			||||||
    () =>
 | 
					 | 
				
			||||||
      props.name !== "trends"
 | 
					 | 
				
			||||||
        ? props.client.v1.timelines[props.name]
 | 
					 | 
				
			||||||
        : props.client.v1.trends.statuses,
 | 
					 | 
				
			||||||
    { fullRefresh: props.fullRefetch },
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
  const [expandedThreadId, setExpandedThreadId] = createSignal<string>();
 | 
					 | 
				
			||||||
  const [typing, setTyping] = createSignal(false);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const tlEndObserver = new IntersectionObserver(() => {
 | 
					 | 
				
			||||||
    if (untrack(() => props.prefetch) && !snapshot.loading)
 | 
					 | 
				
			||||||
      refetchTimeline({ direction: "old" });
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  onCleanup(() => tlEndObserver.disconnect());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const onBookmark = async (
 | 
					 | 
				
			||||||
    index: number,
 | 
					 | 
				
			||||||
    client: mastodon.rest.Client,
 | 
					 | 
				
			||||||
    status: mastodon.v1.Status,
 | 
					 | 
				
			||||||
  ) => {
 | 
					 | 
				
			||||||
    const result = await (status.bookmarked
 | 
					 | 
				
			||||||
      ? client.v1.statuses.$select(status.id).unbookmark()
 | 
					 | 
				
			||||||
      : client.v1.statuses.$select(status.id).bookmark());
 | 
					 | 
				
			||||||
    mutateTimeline((o) => {
 | 
					 | 
				
			||||||
      o[index] = result;
 | 
					 | 
				
			||||||
      return o;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const onBoost = async (
 | 
					 | 
				
			||||||
    index: number,
 | 
					 | 
				
			||||||
    client: mastodon.rest.Client,
 | 
					 | 
				
			||||||
    status: mastodon.v1.Status,
 | 
					 | 
				
			||||||
  ) => {
 | 
					 | 
				
			||||||
    const reblogged = status.reblog
 | 
					 | 
				
			||||||
      ? status.reblog.reblogged
 | 
					 | 
				
			||||||
      : status.reblogged;
 | 
					 | 
				
			||||||
    vibrate(50);
 | 
					 | 
				
			||||||
    mutateTimeline(index, (x) => {
 | 
					 | 
				
			||||||
      if (x.reblog) {
 | 
					 | 
				
			||||||
        x.reblog = { ...x.reblog, reblogged: !reblogged };
 | 
					 | 
				
			||||||
        return Object.assign({}, x);
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        return Object.assign({}, x, {
 | 
					 | 
				
			||||||
          reblogged: !reblogged,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    const result = reblogged
 | 
					 | 
				
			||||||
      ? await client.v1.statuses.$select(status.id).unreblog()
 | 
					 | 
				
			||||||
      : (await client.v1.statuses.$select(status.id).reblog()).reblog!;
 | 
					 | 
				
			||||||
    mutateTimeline((o) => {
 | 
					 | 
				
			||||||
      Object.assign(o[index].reblog ?? o[index], {
 | 
					 | 
				
			||||||
        reblogged: result.reblogged,
 | 
					 | 
				
			||||||
        reblogsCount: result.reblogsCount,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      return o;
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return (
 | 
					 | 
				
			||||||
    <ErrorBoundary
 | 
					 | 
				
			||||||
      fallback={(err, reset) => {
 | 
					 | 
				
			||||||
        return <p>Oops: {String(err)}</p>;
 | 
					 | 
				
			||||||
      }}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <PullDownToRefresh
 | 
					 | 
				
			||||||
        linkedElement={scrollLinked()}
 | 
					 | 
				
			||||||
        loading={snapshot.loading}
 | 
					 | 
				
			||||||
        onRefresh={() => refetchTimeline({ direction: "new" })}
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
      <div
 | 
					 | 
				
			||||||
        ref={(e) =>
 | 
					 | 
				
			||||||
          setTimeout(() => {
 | 
					 | 
				
			||||||
            setScrollLinked(e.parentElement!);
 | 
					 | 
				
			||||||
          }, 0)
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <Show when={props.name === "home"}>
 | 
					 | 
				
			||||||
          <TootComposer
 | 
					 | 
				
			||||||
            style={{
 | 
					 | 
				
			||||||
              "--scaffold-topbar-height": "0px",
 | 
					 | 
				
			||||||
            }}
 | 
					 | 
				
			||||||
            isTyping={typing()}
 | 
					 | 
				
			||||||
            onTypingChange={setTyping}
 | 
					 | 
				
			||||||
            client={props.client}
 | 
					 | 
				
			||||||
            onSent={() => refetchTimeline({ direction: "new" })}
 | 
					 | 
				
			||||||
          />
 | 
					 | 
				
			||||||
        </Show>
 | 
					 | 
				
			||||||
        <For each={timeline}>
 | 
					 | 
				
			||||||
          {(item, index) => {
 | 
					 | 
				
			||||||
            let element: HTMLElement | undefined;
 | 
					 | 
				
			||||||
            return (
 | 
					 | 
				
			||||||
              <TootThread
 | 
					 | 
				
			||||||
                ref={element}
 | 
					 | 
				
			||||||
                status={item}
 | 
					 | 
				
			||||||
                onBoost={(...args) => onBoost(index(), ...args)}
 | 
					 | 
				
			||||||
                onBookmark={(...args) => onBookmark(index(), ...args)}
 | 
					 | 
				
			||||||
                onReply={(client, status) =>
 | 
					 | 
				
			||||||
                  props.openFullScreenToot(status, element, true)
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                client={props.client}
 | 
					 | 
				
			||||||
                expanded={item.id === expandedThreadId() ? 1 : 0}
 | 
					 | 
				
			||||||
                onExpandChange={(x) => {
 | 
					 | 
				
			||||||
                  setTyping(false)
 | 
					 | 
				
			||||||
                  if (item.id !== expandedThreadId()) {
 | 
					 | 
				
			||||||
                    setExpandedThreadId((x) => (x ? undefined : item.id));
 | 
					 | 
				
			||||||
                  } else if (x === 2) {
 | 
					 | 
				
			||||||
                    props.openFullScreenToot(item, element);
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
                }}
 | 
					 | 
				
			||||||
              />
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        </For>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <div ref={(e) => tlEndObserver.observe(e)}></div>
 | 
					 | 
				
			||||||
      <Show when={snapshot.loading}>
 | 
					 | 
				
			||||||
        <div
 | 
					 | 
				
			||||||
          class="loading-line"
 | 
					 | 
				
			||||||
          style={{
 | 
					 | 
				
			||||||
            width: "100%",
 | 
					 | 
				
			||||||
          }}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          <LinearProgress />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </Show>
 | 
					 | 
				
			||||||
      <div
 | 
					 | 
				
			||||||
        style={{
 | 
					 | 
				
			||||||
          display: "flex",
 | 
					 | 
				
			||||||
          padding: "20px 0 calc(20px + var(--safe-area-inset-bottom, 0px))",
 | 
					 | 
				
			||||||
          "align-items": "center",
 | 
					 | 
				
			||||||
          "justify-content": "center",
 | 
					 | 
				
			||||||
        }}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <JsSwitch>
 | 
					 | 
				
			||||||
          <Match when={snapshot.error}>
 | 
					 | 
				
			||||||
            <Button
 | 
					 | 
				
			||||||
              variant="contained"
 | 
					 | 
				
			||||||
              onClick={[refetchTimeline, "old"]}
 | 
					 | 
				
			||||||
              disabled={snapshot.loading}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              Retry
 | 
					 | 
				
			||||||
            </Button>
 | 
					 | 
				
			||||||
          </Match>
 | 
					 | 
				
			||||||
          <Match when={typeof props.fullRefetch === "undefined"}>
 | 
					 | 
				
			||||||
            <Button
 | 
					 | 
				
			||||||
              variant="contained"
 | 
					 | 
				
			||||||
              onClick={[refetchTimeline, "old"]}
 | 
					 | 
				
			||||||
              disabled={snapshot.loading}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              Load More
 | 
					 | 
				
			||||||
            </Button>
 | 
					 | 
				
			||||||
          </Match>
 | 
					 | 
				
			||||||
        </JsSwitch>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </ErrorBoundary>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Home: ParentComponent = (props) => {
 | 
					const Home: ParentComponent = (props) => {
 | 
				
			||||||
  let panelList: HTMLDivElement;
 | 
					  let panelList: HTMLDivElement;
 | 
				
			||||||
| 
						 | 
					@ -340,7 +146,9 @@ const Home: ParentComponent = (props) => {
 | 
				
			||||||
      console.warn("no account info?");
 | 
					      console.warn("no account info?");
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    setHeroSrc((x) => Object.assign({}, x, { [BOTTOM_SHEET_HERO]: srcElement }));
 | 
					    setHeroSrc((x) =>
 | 
				
			||||||
 | 
					      Object.assign({}, x, { [BOTTOM_SHEET_HERO]: srcElement }),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    const acct = `${inf.username}@${p.account.site}`;
 | 
					    const acct = `${inf.username}@${p.account.site}`;
 | 
				
			||||||
    setTootBottomSheetCache(acct, toot);
 | 
					    setTootBottomSheetCache(acct, toot);
 | 
				
			||||||
    navigate(`/${encodeURIComponent(acct)}/${toot.id}`, {
 | 
					    navigate(`/${encodeURIComponent(acct)}/${toot.id}`, {
 | 
				
			||||||
| 
						 | 
					@ -439,12 +247,10 @@ const Home: ParentComponent = (props) => {
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
              <div class="tab-panel">
 | 
					              <div class="tab-panel">
 | 
				
			||||||
                <div>
 | 
					                <div>
 | 
				
			||||||
                  <TimelinePanel
 | 
					                  <TrendTimelinePanel
 | 
				
			||||||
                    client={client()}
 | 
					                    client={client()}
 | 
				
			||||||
                    name="trends"
 | 
					 | 
				
			||||||
                    prefetch={prefetching()}
 | 
					                    prefetch={prefetching()}
 | 
				
			||||||
                    openFullScreenToot={openFullScreenToot}
 | 
					                    openFullScreenToot={openFullScreenToot}
 | 
				
			||||||
                    fullRefetch={120}
 | 
					 | 
				
			||||||
                  />
 | 
					                  />
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										94
									
								
								src/timelines/Thread.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/timelines/Thread.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,94 @@
 | 
				
			||||||
 | 
					import type { mastodon } from "masto";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  For,
 | 
				
			||||||
 | 
					  Show,
 | 
				
			||||||
 | 
					  createResource,
 | 
				
			||||||
 | 
					  createSignal,
 | 
				
			||||||
 | 
					  type Component,
 | 
				
			||||||
 | 
					  type Ref,
 | 
				
			||||||
 | 
					} from "solid-js";
 | 
				
			||||||
 | 
					import CompactToot from "./CompactToot";
 | 
				
			||||||
 | 
					import { useTimeSource } from "../platform/timesrc";
 | 
				
			||||||
 | 
					import RegularToot from "./RegularToot";
 | 
				
			||||||
 | 
					import cardStyle from "../material/cards.module.css";
 | 
				
			||||||
 | 
					import { css } from "solid-styled";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TootActions = {
 | 
				
			||||||
 | 
					  onBoost(client: mastodon.rest.Client, status: mastodon.v1.Status): void;
 | 
				
			||||||
 | 
					  onBookmark(client: mastodon.rest.Client, status: mastodon.v1.Status): void;
 | 
				
			||||||
 | 
					  onReply(client: mastodon.rest.Client, status: mastodon.v1.Status): void;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ThreadProps = {
 | 
				
			||||||
 | 
					  ref?: Ref<HTMLElement>;
 | 
				
			||||||
 | 
					  client: mastodon.rest.Client;
 | 
				
			||||||
 | 
					  toots: readonly mastodon.v1.Status[];
 | 
				
			||||||
 | 
					  isExpended: (status: mastodon.v1.Status) => boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onItemClick(status: mastodon.v1.Status, event: MouseEvent): void;
 | 
				
			||||||
 | 
					} & TootActions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const Thread: Component<ThreadProps> = (props) => {
 | 
				
			||||||
 | 
					  const boost = (status: mastodon.v1.Status) => {
 | 
				
			||||||
 | 
					    props.onBoost(props.client, status);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const bookmark = (status: mastodon.v1.Status) => {
 | 
				
			||||||
 | 
					    props.onBookmark(props.client, status);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const reply = (status: mastodon.v1.Status) => {
 | 
				
			||||||
 | 
					    props.onReply(props.client, status);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  css`
 | 
				
			||||||
 | 
					    article {
 | 
				
			||||||
 | 
					      transition:
 | 
				
			||||||
 | 
					        margin 90ms var(--tutu-anim-curve-sharp),
 | 
				
			||||||
 | 
					        var(--tutu-transition-shadow);
 | 
				
			||||||
 | 
					      user-select: none;
 | 
				
			||||||
 | 
					      cursor: pointer;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .thread-line {
 | 
				
			||||||
 | 
					      position: relative;
 | 
				
			||||||
 | 
					      &::before {
 | 
				
			||||||
 | 
					        content: "";
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					        left: 36px;
 | 
				
			||||||
 | 
					        top: 16px;
 | 
				
			||||||
 | 
					        bottom: 0;
 | 
				
			||||||
 | 
					        background-color: var(--tutu-color-secondary);
 | 
				
			||||||
 | 
					        width: 2px;
 | 
				
			||||||
 | 
					        display: block;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <article
 | 
				
			||||||
 | 
					      ref={props.ref}
 | 
				
			||||||
 | 
					      classList={{
 | 
				
			||||||
 | 
					        "thread-line": props.toots.length > 1,
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <For each={props.toots}>
 | 
				
			||||||
 | 
					        {(status, index) => (
 | 
				
			||||||
 | 
					          <RegularToot
 | 
				
			||||||
 | 
					            data-status-id={status.id}
 | 
				
			||||||
 | 
					            data-thread-sort={index()}
 | 
				
			||||||
 | 
					            status={status}
 | 
				
			||||||
 | 
					            class={`${cardStyle.card}`}
 | 
				
			||||||
 | 
					            evaluated={props.isExpended(status)}
 | 
				
			||||||
 | 
					            actionable={props.isExpended(status)}
 | 
				
			||||||
 | 
					            onBookmark={(s) => bookmark(s)}
 | 
				
			||||||
 | 
					            onRetoot={(s) => boost(s)}
 | 
				
			||||||
 | 
					            onReply={(s) => reply(s)}
 | 
				
			||||||
 | 
					            onClick={[props.onItemClick, status]}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </For>
 | 
				
			||||||
 | 
					    </article>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Thread;
 | 
				
			||||||
							
								
								
									
										203
									
								
								src/timelines/TimelinePanel.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								src/timelines/TimelinePanel.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,203 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Component,
 | 
				
			||||||
 | 
					  For,
 | 
				
			||||||
 | 
					  onCleanup,
 | 
				
			||||||
 | 
					  createSignal,
 | 
				
			||||||
 | 
					  Show,
 | 
				
			||||||
 | 
					  untrack,
 | 
				
			||||||
 | 
					  Match,
 | 
				
			||||||
 | 
					  Switch as JsSwitch,
 | 
				
			||||||
 | 
					  ErrorBoundary,
 | 
				
			||||||
 | 
					} from "solid-js";
 | 
				
			||||||
 | 
					import { type mastodon } from "masto";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Button,
 | 
				
			||||||
 | 
					  LinearProgress,
 | 
				
			||||||
 | 
					} from "@suid/material";
 | 
				
			||||||
 | 
					import TootThread from "./TootThread.js";
 | 
				
			||||||
 | 
					import { useTimeline } from "../masto/timelines";
 | 
				
			||||||
 | 
					import { vibrate } from "../platform/hardware";
 | 
				
			||||||
 | 
					import PullDownToRefresh from "./PullDownToRefresh";
 | 
				
			||||||
 | 
					import TootComposer from "./TootComposer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TimelinePanel: Component<{
 | 
				
			||||||
 | 
					  client: mastodon.rest.Client;
 | 
				
			||||||
 | 
					  name: "home" | "public" | "trends";
 | 
				
			||||||
 | 
					  prefetch?: boolean;
 | 
				
			||||||
 | 
					  fullRefetch?: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openFullScreenToot: (
 | 
				
			||||||
 | 
					    toot: mastodon.v1.Status,
 | 
				
			||||||
 | 
					    srcElement?: HTMLElement,
 | 
				
			||||||
 | 
					    reply?: boolean,
 | 
				
			||||||
 | 
					  ) => void;
 | 
				
			||||||
 | 
					}> = (props) => {
 | 
				
			||||||
 | 
					  const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>();
 | 
				
			||||||
 | 
					  const [
 | 
				
			||||||
 | 
					    timeline,
 | 
				
			||||||
 | 
					    snapshot,
 | 
				
			||||||
 | 
					    { refetch: refetchTimeline, mutate: mutateTimeline },
 | 
				
			||||||
 | 
					  ] = useTimeline(
 | 
				
			||||||
 | 
					    () =>
 | 
				
			||||||
 | 
					      props.name !== "trends"
 | 
				
			||||||
 | 
					        ? props.client.v1.timelines[props.name]
 | 
				
			||||||
 | 
					        : props.client.v1.trends.statuses,
 | 
				
			||||||
 | 
					    { fullRefresh: props.fullRefetch },
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const [expandedThreadId, setExpandedThreadId] = createSignal<string>();
 | 
				
			||||||
 | 
					  const [typing, setTyping] = createSignal(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const tlEndObserver = new IntersectionObserver(() => {
 | 
				
			||||||
 | 
					    if (untrack(() => props.prefetch) && !snapshot.loading)
 | 
				
			||||||
 | 
					      refetchTimeline({ direction: "old" });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onCleanup(() => tlEndObserver.disconnect());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onBookmark = async (
 | 
				
			||||||
 | 
					    index: number,
 | 
				
			||||||
 | 
					    client: mastodon.rest.Client,
 | 
				
			||||||
 | 
					    status: mastodon.v1.Status,
 | 
				
			||||||
 | 
					  ) => {
 | 
				
			||||||
 | 
					    const result = await (status.bookmarked
 | 
				
			||||||
 | 
					      ? client.v1.statuses.$select(status.id).unbookmark()
 | 
				
			||||||
 | 
					      : client.v1.statuses.$select(status.id).bookmark());
 | 
				
			||||||
 | 
					    mutateTimeline((o) => {
 | 
				
			||||||
 | 
					      o[index] = result;
 | 
				
			||||||
 | 
					      return o;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onBoost = async (
 | 
				
			||||||
 | 
					    index: number,
 | 
				
			||||||
 | 
					    client: mastodon.rest.Client,
 | 
				
			||||||
 | 
					    status: mastodon.v1.Status,
 | 
				
			||||||
 | 
					  ) => {
 | 
				
			||||||
 | 
					    const reblogged = status.reblog
 | 
				
			||||||
 | 
					      ? status.reblog.reblogged
 | 
				
			||||||
 | 
					      : status.reblogged;
 | 
				
			||||||
 | 
					    vibrate(50);
 | 
				
			||||||
 | 
					    mutateTimeline(index, (x) => {
 | 
				
			||||||
 | 
					      if (x.reblog) {
 | 
				
			||||||
 | 
					        x.reblog = { ...x.reblog, reblogged: !reblogged };
 | 
				
			||||||
 | 
					        return Object.assign({}, x);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return Object.assign({}, x, {
 | 
				
			||||||
 | 
					          reblogged: !reblogged,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const result = reblogged
 | 
				
			||||||
 | 
					      ? await client.v1.statuses.$select(status.id).unreblog()
 | 
				
			||||||
 | 
					      : (await client.v1.statuses.$select(status.id).reblog()).reblog!;
 | 
				
			||||||
 | 
					    mutateTimeline((o) => {
 | 
				
			||||||
 | 
					      Object.assign(o[index].reblog ?? o[index], {
 | 
				
			||||||
 | 
					        reblogged: result.reblogged,
 | 
				
			||||||
 | 
					        reblogsCount: result.reblogsCount,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return o;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ErrorBoundary
 | 
				
			||||||
 | 
					      fallback={(err, reset) => {
 | 
				
			||||||
 | 
					        return <p>Oops: {String(err)}</p>;
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <PullDownToRefresh
 | 
				
			||||||
 | 
					        linkedElement={scrollLinked()}
 | 
				
			||||||
 | 
					        loading={snapshot.loading}
 | 
				
			||||||
 | 
					        onRefresh={() => refetchTimeline({ direction: "new" })}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        ref={(e) =>
 | 
				
			||||||
 | 
					          setTimeout(() => {
 | 
				
			||||||
 | 
					            setScrollLinked(e.parentElement!);
 | 
				
			||||||
 | 
					          }, 0)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <Show when={props.name === "home"}>
 | 
				
			||||||
 | 
					          <TootComposer
 | 
				
			||||||
 | 
					            style={{
 | 
				
			||||||
 | 
					              "--scaffold-topbar-height": "0px",
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					            isTyping={typing()}
 | 
				
			||||||
 | 
					            onTypingChange={setTyping}
 | 
				
			||||||
 | 
					            client={props.client}
 | 
				
			||||||
 | 
					            onSent={() => refetchTimeline({ direction: "new" })}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        </Show>
 | 
				
			||||||
 | 
					        <For each={timeline}>
 | 
				
			||||||
 | 
					          {(item, index) => {
 | 
				
			||||||
 | 
					            let element: HTMLElement | undefined;
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <TootThread
 | 
				
			||||||
 | 
					                ref={element}
 | 
				
			||||||
 | 
					                status={item}
 | 
				
			||||||
 | 
					                onBoost={(...args) => onBoost(index(), ...args)}
 | 
				
			||||||
 | 
					                onBookmark={(...args) => onBookmark(index(), ...args)}
 | 
				
			||||||
 | 
					                onReply={(client, status) =>
 | 
				
			||||||
 | 
					                  props.openFullScreenToot(status, element, true)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                client={props.client}
 | 
				
			||||||
 | 
					                expanded={item.id === expandedThreadId() ? 1 : 0}
 | 
				
			||||||
 | 
					                onExpandChange={(x) => {
 | 
				
			||||||
 | 
					                  setTyping(false)
 | 
				
			||||||
 | 
					                  if (item.id !== expandedThreadId()) {
 | 
				
			||||||
 | 
					                    setExpandedThreadId((x) => (x ? undefined : item.id));
 | 
				
			||||||
 | 
					                  } else if (x === 2) {
 | 
				
			||||||
 | 
					                    props.openFullScreenToot(item, element);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        </For>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div ref={(e) => tlEndObserver.observe(e)}></div>
 | 
				
			||||||
 | 
					      <Show when={snapshot.loading}>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          class="loading-line"
 | 
				
			||||||
 | 
					          style={{
 | 
				
			||||||
 | 
					            width: "100%",
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <LinearProgress />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </Show>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        style={{
 | 
				
			||||||
 | 
					          display: "flex",
 | 
				
			||||||
 | 
					          padding: "20px 0 calc(20px + var(--safe-area-inset-bottom, 0px))",
 | 
				
			||||||
 | 
					          "align-items": "center",
 | 
				
			||||||
 | 
					          "justify-content": "center",
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <JsSwitch>
 | 
				
			||||||
 | 
					          <Match when={snapshot.error}>
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="contained"
 | 
				
			||||||
 | 
					              onClick={[refetchTimeline, "old"]}
 | 
				
			||||||
 | 
					              disabled={snapshot.loading}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              Retry
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </Match>
 | 
				
			||||||
 | 
					          <Match when={typeof props.fullRefetch === "undefined"}>
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="contained"
 | 
				
			||||||
 | 
					              onClick={[refetchTimeline, "old"]}
 | 
				
			||||||
 | 
					              disabled={snapshot.loading}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              Load More
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </Match>
 | 
				
			||||||
 | 
					        </JsSwitch>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </ErrorBoundary>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default TimelinePanel
 | 
				
			||||||
							
								
								
									
										189
									
								
								src/timelines/TrendTimelinePanel.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/timelines/TrendTimelinePanel.tsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,189 @@
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  Component,
 | 
				
			||||||
 | 
					  For,
 | 
				
			||||||
 | 
					  onCleanup,
 | 
				
			||||||
 | 
					  createSignal,
 | 
				
			||||||
 | 
					  Show,
 | 
				
			||||||
 | 
					  untrack,
 | 
				
			||||||
 | 
					  Match,
 | 
				
			||||||
 | 
					  Switch as JsSwitch,
 | 
				
			||||||
 | 
					  ErrorBoundary,
 | 
				
			||||||
 | 
					  createSelector,
 | 
				
			||||||
 | 
					} from "solid-js";
 | 
				
			||||||
 | 
					import { type mastodon } from "masto";
 | 
				
			||||||
 | 
					import { Button, LinearProgress } from "@suid/material";
 | 
				
			||||||
 | 
					import { createTimelineSnapshot } from "../masto/timelines.js";
 | 
				
			||||||
 | 
					import { vibrate } from "../platform/hardware.js";
 | 
				
			||||||
 | 
					import PullDownToRefresh from "./PullDownToRefresh.jsx";
 | 
				
			||||||
 | 
					import Thread from "./Thread.jsx";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TrendTimelinePanel: Component<{
 | 
				
			||||||
 | 
					  client: mastodon.rest.Client;
 | 
				
			||||||
 | 
					  prefetch?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  openFullScreenToot: (
 | 
				
			||||||
 | 
					    toot: mastodon.v1.Status,
 | 
				
			||||||
 | 
					    srcElement?: HTMLElement,
 | 
				
			||||||
 | 
					    reply?: boolean,
 | 
				
			||||||
 | 
					  ) => void;
 | 
				
			||||||
 | 
					}> = (props) => {
 | 
				
			||||||
 | 
					  const [scrollLinked, setScrollLinked] = createSignal<HTMLElement>();
 | 
				
			||||||
 | 
					  const [
 | 
				
			||||||
 | 
					    timeline,
 | 
				
			||||||
 | 
					    snapshot,
 | 
				
			||||||
 | 
					    { refetch: refetchTimeline, mutate: mutateTimeline },
 | 
				
			||||||
 | 
					  ] = createTimelineSnapshot(
 | 
				
			||||||
 | 
					    () => props.client.v1.trends.statuses,
 | 
				
			||||||
 | 
					    () => 120,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  const [expandedId, setExpandedId] = createSignal<string>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const tlEndObserver = new IntersectionObserver(() => {
 | 
				
			||||||
 | 
					    if (untrack(() => props.prefetch) && !snapshot.loading)
 | 
				
			||||||
 | 
					      refetchTimeline({ direction: "old" });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onCleanup(() => tlEndObserver.disconnect());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const isExpandedId = createSelector(expandedId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const isExpanded = (st: mastodon.v1.Status) => isExpandedId(st.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onBookmark = async (
 | 
				
			||||||
 | 
					    index: number,
 | 
				
			||||||
 | 
					    client: mastodon.rest.Client,
 | 
				
			||||||
 | 
					    status: mastodon.v1.Status,
 | 
				
			||||||
 | 
					  ) => {
 | 
				
			||||||
 | 
					    const result = await (status.bookmarked
 | 
				
			||||||
 | 
					      ? client.v1.statuses.$select(status.id).unbookmark()
 | 
				
			||||||
 | 
					      : client.v1.statuses.$select(status.id).bookmark());
 | 
				
			||||||
 | 
					    mutateTimeline((o) => {
 | 
				
			||||||
 | 
					      o![index] = [result];
 | 
				
			||||||
 | 
					      return o;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onBoost = async (
 | 
				
			||||||
 | 
					    index: number,
 | 
				
			||||||
 | 
					    client: mastodon.rest.Client,
 | 
				
			||||||
 | 
					    status: mastodon.v1.Status,
 | 
				
			||||||
 | 
					  ) => {
 | 
				
			||||||
 | 
					    const reblogged = status.reblog
 | 
				
			||||||
 | 
					      ? status.reblog.reblogged
 | 
				
			||||||
 | 
					      : status.reblogged;
 | 
				
			||||||
 | 
					    vibrate(50);
 | 
				
			||||||
 | 
					    mutateTimeline(index, (th) => {
 | 
				
			||||||
 | 
					      const x = th[0];
 | 
				
			||||||
 | 
					      if (x.reblog) {
 | 
				
			||||||
 | 
					        x.reblog = { ...x.reblog, reblogged: !reblogged };
 | 
				
			||||||
 | 
					        return [Object.assign({}, x)];
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					          Object.assign({}, x, {
 | 
				
			||||||
 | 
					            reblogged: !reblogged,
 | 
				
			||||||
 | 
					          }),
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const result = reblogged
 | 
				
			||||||
 | 
					      ? await client.v1.statuses.$select(status.id).unreblog()
 | 
				
			||||||
 | 
					      : (await client.v1.statuses.$select(status.id).reblog()).reblog!;
 | 
				
			||||||
 | 
					    mutateTimeline(index, (th) => {
 | 
				
			||||||
 | 
					      Object.assign(th[0].reblog ?? th[0], {
 | 
				
			||||||
 | 
					        reblogged: result.reblogged,
 | 
				
			||||||
 | 
					        reblogsCount: result.reblogsCount,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      return th;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <ErrorBoundary
 | 
				
			||||||
 | 
					      fallback={(err, reset) => {
 | 
				
			||||||
 | 
					        return <p>Oops: {String(err)}</p>;
 | 
				
			||||||
 | 
					      }}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <PullDownToRefresh
 | 
				
			||||||
 | 
					        linkedElement={scrollLinked()}
 | 
				
			||||||
 | 
					        loading={snapshot.loading}
 | 
				
			||||||
 | 
					        onRefresh={() => refetchTimeline({ direction: "new" })}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        ref={(e) =>
 | 
				
			||||||
 | 
					          setTimeout(() => {
 | 
				
			||||||
 | 
					            setScrollLinked(e.parentElement!);
 | 
				
			||||||
 | 
					          }, 0)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <For each={timeline}>
 | 
				
			||||||
 | 
					          {(item, index) => {
 | 
				
			||||||
 | 
					            let element: HTMLElement | undefined;
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              <Thread
 | 
				
			||||||
 | 
					                ref={element}
 | 
				
			||||||
 | 
					                toots={item}
 | 
				
			||||||
 | 
					                onBoost={(...args) => onBoost(index(), ...args)}
 | 
				
			||||||
 | 
					                onBookmark={(...args) => onBookmark(index(), ...args)}
 | 
				
			||||||
 | 
					                onReply={(client, status) =>
 | 
				
			||||||
 | 
					                  props.openFullScreenToot(status, element, true)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                client={props.client}
 | 
				
			||||||
 | 
					                isExpended={isExpanded}
 | 
				
			||||||
 | 
					                onItemClick={(x) => {
 | 
				
			||||||
 | 
					                  if (x.id !== expandedId()) {
 | 
				
			||||||
 | 
					                    setExpandedId((o) => (o ? undefined : x.id));
 | 
				
			||||||
 | 
					                  } else {
 | 
				
			||||||
 | 
					                    props.openFullScreenToot(x, element);
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        </For>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div ref={(e) => tlEndObserver.observe(e)}></div>
 | 
				
			||||||
 | 
					      <Show when={snapshot.loading}>
 | 
				
			||||||
 | 
					        <div
 | 
				
			||||||
 | 
					          class="loading-line"
 | 
				
			||||||
 | 
					          style={{
 | 
				
			||||||
 | 
					            width: "100%",
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <LinearProgress />
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </Show>
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        style={{
 | 
				
			||||||
 | 
					          display: "flex",
 | 
				
			||||||
 | 
					          padding: "20px 0 calc(20px + var(--safe-area-inset-bottom, 0px))",
 | 
				
			||||||
 | 
					          "align-items": "center",
 | 
				
			||||||
 | 
					          "justify-content": "center",
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <JsSwitch>
 | 
				
			||||||
 | 
					          <Match when={snapshot.error}>
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="contained"
 | 
				
			||||||
 | 
					              onClick={[refetchTimeline, undefined]}
 | 
				
			||||||
 | 
					              disabled={snapshot.loading}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              Retry
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </Match>
 | 
				
			||||||
 | 
					          <Match when={true}>
 | 
				
			||||||
 | 
					            <Button
 | 
				
			||||||
 | 
					              variant="contained"
 | 
				
			||||||
 | 
					              onClick={[refetchTimeline, undefined]}
 | 
				
			||||||
 | 
					              disabled={snapshot.loading}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              Refresh
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </Match>
 | 
				
			||||||
 | 
					        </JsSwitch>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </ErrorBoundary>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default TrendTimelinePanel;
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue