Ctrl K
ComunidadGuías…

mobile/src/App.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { useEffect, useState } from "react";
import { TabBar, TopBar } from "./components/Shell";
import Icon from "./components/Icon";
import { Banner, Sheet, Toasts } from "./components/ui";
import { navigate, useRoute } from "./lib/nav";
import {
  enablePush,
  isIosSafariBrowser,
  pushState,
  pushSupported,
} from "./lib/push";
import { useStore } from "./lib/store";
import { hasFeature } from "./lib/api";
import { prefGet, prefSet } from "./lib/native";
import ChatsView from "./views/ChatsView";
import InboxView from "./views/InboxView";
import MoreView from "./views/MoreView";
import PairView from "./views/PairView";
import ProjectsView from "./views/ProjectsView";
import RunsView from "./views/RunsView";
import ThreadView from "./views/ThreadView";

/** Keep the layout keyboard-aware on iOS: size the app to the visual
 *  viewport so the composer sits above the keyboard instead of under it. */
function useVisualViewport() {
  useEffect(() => {
    const vv = window.visualViewport;
    const root = document.documentElement;
    const apply = () => {
      const h = vv ? vv.height : window.innerHeight;
      root.style.setProperty("--vvh", `${Math.round(h)}px`);
      // Prevent the page itself scrolling under the keyboard.
      if (vv && vv.offsetTop) window.scrollTo(0, 0);
    };
    apply();
    vv?.addEventListener("resize", apply);
    vv?.addEventListener("scroll", apply);
    window.addEventListener("resize", apply);
    return () => {
      vv?.removeEventListener("resize", apply);
      vv?.removeEventListener("scroll", apply);
      window.removeEventListener("resize", apply);
    };
  }, []);
}

/** Navigation requests from the service worker (notification taps). */
function useSwNavigation() {
  useEffect(() => {
    if (!("serviceWorker" in navigator)) return;
    const on = (e: MessageEvent) => {
      const d = e.data as { type?: string; path?: string } | null;
      if (d?.type === "navigate" && typeof d.path === "string")
        navigate(d.path);
    };
    navigator.serviceWorker.addEventListener("message", on);
    return () => navigator.serviceWorker.removeEventListener("message", on);
  }, []);
}

export default function App() {
  const { boot, toast } = useStore();
  const route = useRoute();
  useVisualViewport();
  useSwNavigation();

  // Post-pairing: offer push once (must come from a tap).
  const [askPush, setAskPush] = useState(false);
  useEffect(() => {
    if (boot !== "ready") return;
    let just = false;
    try {
      just = sessionStorage.getItem("cortex.justPaired") === "1";
      sessionStorage.removeItem("cortex.justPaired");
    } catch {
      /* ignore */
    }
    if (!just || !pushSupported() || !hasFeature("push.web")) return;
    pushState().then((s) => s === "default" && setAskPush(true));
  }, [boot]);

  // iOS Safari (not installed) hint, once.
  const [installHint, setInstallHint] = useState(
    () => isIosSafariBrowser() && !prefGet("cortex.installHintSeen", false),
  );

  // Already paired but opened via a stale `#/pair` link: land on chats.
  useEffect(() => {
    if (boot === "ready" && route.path === "/pair") {
      navigate("/chats", { replace: true });
    }
  }, [boot, route.path]);

  if (boot === "loading") {
    return (
      <div className="app splash" aria-busy="true">
        <span className="brand-mark">C</span>
        <span className="spin" />
      </div>
    );
  }
  if (boot === "pair" || boot === "unreachable") {
    return (
      <div className="app">
        <PairView />
        <Toasts />
      </div>
    );
  }

  // Routes with their own header (pushed screens) hide the tab chrome.
  const pushed =
    (route.tab === "chats" && route.rest.length > 0) ||
    (route.tab === "projects" && route.rest.length > 0) ||
    (route.tab === "runs" && route.rest.length > 0) ||
    (route.tab === "more" && route.rest.length > 0);

  let view: React.ReactNode;
  if (route.path === "/pair") {
    // Already paired; a stale pair link just lands on chats (see effect).
    view = null;
  } else if (route.tab === "chats" && route.rest[0]) {
    view = <ThreadView key={route.rest[0]} threadId={route.rest[0]} />;
  } else {
    switch (route.tab) {
      case "inbox":
        view = <InboxView />;
        break;
      case "projects":
        view = <ProjectsView />;
        break;
      case "runs":
        view = <RunsView />;
        break;
      case "more":
        view = <MoreView />;
        break;
      default:
        view = <ChatsView />;
    }
  }

  return (
    <div className="app">
      {!pushed && <TopBar />}
      {!pushed && installHint && (
        <Banner
          kind="info"
          action={{
            label: "How",
            onClick: () => {
              prefSet("cortex.installHintSeen", true);
              setInstallHint(false);
              navigate("/more/install");
            },
          }}
        >
          <button
            className="banner-close"
            aria-label="Dismiss"
            onClick={() => {
              prefSet("cortex.installHintSeen", true);
              setInstallHint(false);
            }}
          >
            <Icon name="x" size={14} />
          </button>
          Add Cortex to your Home Screen for notifications.
        </Banner>
      )}
      <main className="view">{view}</main>
      {!pushed && <TabBar active={route.tab} />}
      <Toasts />
      <Sheet
        open={askPush}
        onClose={() => setAskPush(false)}
        title="Turn on notifications?"
      >
        <p className="muted">
          Get a push when a run needs your approval, finishes, or fails — even
          with the app closed.
        </p>
        <button
          className="btn primary block"
          onClick={async () => {
            setAskPush(false);
            try {
              const s = await enablePush();
              if (s === "subscribed") toast("Notifications on.", "success");
            } catch (e) {
              toast(e instanceof Error ? e.message : String(e), "error");
            }
          }}
        >
          Turn on
        </button>
        <button className="linkbtn block" onClick={() => setAskPush(false)}>
          Not now
        </button>
      </Sheet>
    </div>
  );
}