/* =========================================================================
   Root app — wires nostr + UI together.
   ========================================================================= */
const { useState, useEffect, useMemo, useCallback, useRef } = React;

const TWEAK_DEFAULS = /*EDITMODE-BEGIN*/{
  "theme": "foundry",
  "scheme": "dark",
  "showLastSeen": true,
  "splitPrivate": true
}/*EDITMODE-END*/;

function useToast() {
  const [t, setT] = useState(null);
  const show = useCallback((msg, kind = "ok") => {
    setT({ msg, kind, id: Math.random() });
    setTimeout(() => setT(null), 3200);
  }, []);
  return [t, show];
}

function App() {
  const [t, setTweak] = window.useTweaks(TWEAK_DEFAULS);
  useEffect(() => { document.body.setAttribute("data-theme", t.theme); }, [t.theme]);
  useEffect(() => { document.body.setAttribute("data-scheme", t.scheme || "dark"); }, [t.scheme]);

  const [me, setMe] = useState(null);
  const [meProfile, setMeProfile] = useState(null);
  const [contactList, setContactList] = useState(null);   // kind 3 event
  const [followSets, setFollowSets] = useState([]);        // array of kind 30000 events
  const [activeId, setActiveId] = useState("__following__");
  const [profiles, setProfiles] = useState({});            // pubkey -> profile
  const [lastSeen, setLastSeen] = useState({});            // pubkey -> ts
  const [privateMap, setPrivateMap] = useState({});        // dTag -> [pubkey]
  const [loading, setLoading] = useState(false);
  const [showNew, setShowNew] = useState(false);
  const [showActivity, setShowActivity] = useState(false);
  const [showHelp, setShowHelp] = useState(false);
  const [openContact, setOpenContact] = useState(null);
  const [profileLoading, setProfileLoading] = useState(false);
  useEffect(() => {
    if (!openContact) { setProfileLoading(false); return; }
    if (profiles[openContact]) { setProfileLoading(false); return; }
    setProfileLoading(true);
    let cancelled = false;
    Nostr.fetchProfiles([openContact]).then(p => {
      if (cancelled) return;
      if (Object.keys(p).length) setProfiles(prev => ({ ...prev, ...p }));
      setProfileLoading(false);
    }).catch(() => { if (!cancelled) setProfileLoading(false); });
    return () => { cancelled = true; };
  }, [openContact]);
  const [editSet, setEditSet] = useState(null);
  const [activity, setActivity] = useState(() => {
    try { return JSON.parse(localStorage.getItem("nostrlist.activity") || "[]"); } catch { return []; }
  });
  const [relayState, setRelayState] = useState("idle");
  const [busyPks, setBusyPks] = useState({}); // pubkey -> { follow?, remove?, lists: {dTag:true} }
  const [toast, showToast] = useToast();
  const [loginError, setLoginError] = useState(null);

  const logActivity = useCallback((entry) => {
    setActivity(prev => {
      const next = [{ ts: Math.floor(Date.now() / 1000), ...entry }, ...prev].slice(0, 200);
      try { localStorage.setItem("nostrlist.activity", JSON.stringify(next)); } catch {}
      return next;
    });
  }, []);

  // Build snapshot entries from what's currently on the relay. Replaceable
  // events only keep their latest version, so this isn't a true diff — but it
  // surfaces "list X was last updated on Y" instead of showing nothing.
  const seedActivityFromRelay = useCallback((contact, sets) => {
    setActivity(prev => {
      const have = new Set(prev.map(e => e._snap || ""));
      const snaps = [];
      if (contact) {
        const key = "snap:kind3:" + contact.created_at;
        if (!have.has(key)) snaps.push({
          ts: contact.created_at, action: "list-snapshot",
          target: "__following__", targetTitle: "Following",
          count: Nostr.tagsOfList(contact).length, _snap: key,
        });
      }
      for (const s of sets) {
        const d = Nostr.dTagOf(s);
        const key = "snap:30000:" + d + ":" + s.created_at;
        if (have.has(key)) continue;
        snaps.push({
          ts: s.created_at, action: "list-snapshot",
          target: d, targetTitle: Nostr.titleOf(s),
          count: Nostr.tagsOfList(s).length,
          private: !!(s.content && s.content.length > 0),
          _snap: key,
        });
      }
      if (!snaps.length) return prev;
      const merged = [...prev, ...snaps].sort((a, b) => b.ts - a.ts).slice(0, 200);
      try { localStorage.setItem("nostrlist.activity", JSON.stringify(merged)); } catch {}
      return merged;
    });
  }, []);

  const toggleScheme = useCallback(() => {
    setTweak("scheme", (t.scheme === "light" ? "dark" : "light"));
  }, [t.scheme, setTweak]);

  const signout = useCallback(() => {
    setMe(null); setMeProfile(null);
    setContactList(null); setFollowSets([]); setProfiles({}); setLastSeen({}); setPrivateMap({});
    setActiveId("__following__");
  }, []);

  // --- login ----------------------------------------------------------
  const login = useCallback(async () => {
    setLoginError(null);
    try {
      if (!Nostr.hasNip07()) throw new Error("No NIP-07 extension found (try Alby, nos2x, or Nostore).");
      setLoading(true);
      setRelayState("active");
      const pk = await Nostr.getPubkey();
      setMe(pk);

      const [c, sets] = await Promise.all([
        Nostr.fetchContactList(pk),
        Nostr.fetchFollowSets(pk),
      ]);
      setContactList(c);
      setFollowSets(sets);

      // seed activity log with relay-derived snapshot entries (replaceable events
      // mean we can't reconstruct per-user history; this at least shows the
      // last published state of each list).
      seedActivityFromRelay(c, sets);

      // gather pubkeys to load profiles for
      const all = new Set();
      if (c) Nostr.tagsOfList(c).forEach(p => all.add(p));
      for (const s of sets) Nostr.tagsOfList(s).forEach(p => all.add(p));
      all.add(pk);
      const arr = Array.from(all);
      const profs = await Nostr.fetchProfiles(arr);
      setProfiles(profs);
      setMeProfile(profs[pk] || null);

      // decrypt private sections in background
      decryptAll(sets, pk);

      // last seen in background
      Nostr.fetchLastSeen(arr).then(ls => setLastSeen(ls));
    } catch (err) {
      console.error(err);
      setLoginError(err.message || String(err));
    } finally {
      setLoading(false);
    }
  }, []);

  const decryptAll = useCallback(async (sets, pk) => {
    const m = {};
    for (const s of sets) {
      const d = Nostr.dTagOf(s);
      if (s.content) {
        const arr = await Nostr.decryptPrivate(s, pk);
        m[d] = arr;
      }
    }
    setPrivateMap(m);
    // also load any newly-discovered profiles
    const allPriv = new Set();
    Object.values(m).forEach(a => a.forEach(p => allPriv.add(p)));
    const need = Array.from(allPriv).filter(p => !profiles[p]);
    if (need.length) {
      const profs = await Nostr.fetchProfiles(need);
      setProfiles(prev => ({ ...prev, ...profs }));
    }
  }, [profiles]);

  // --- derived: active list view --------------------------------------
  const activeView = useMemo(() => {
    if (activeId === "__following__") {
      const pks = contactList ? Nostr.tagsOfList(contactList) : [];
      return {
        kind: "following",
        title: "Following",
        eyebrow: null,
        desc: "Everyone you publicly follow.",
        publicPubkeys: pks,
        privatePubkeys: [],
        evt: contactList,
      };
    }
    const evt = followSets.find(e => Nostr.dTagOf(e) === activeId);
    if (!evt) return null;
    return {
      kind: "set",
      title: Nostr.titleOf(evt),
      eyebrow: null,
      desc: Nostr.descriptionOf(evt),
      publicPubkeys: Nostr.tagsOfList(evt),
      privatePubkeys: privateMap[activeId] || [],
      evt,
    };
  }, [activeId, contactList, followSets, privateMap]);

  // --- actions --------------------------------------------------------
  async function publishKind3(newPubkeys) {
    setRelayState("publishing");
    const tmpl = Nostr.buildContactList(newPubkeys, contactList ? contactList.tags : []);
    const signed = await Nostr.signEvent(tmpl);
    await Nostr.publish(signed);
    setContactList(signed);
    setRelayState("active");
    showToast(`Updated Following (${newPubkeys.length})`, "ok");
  }

  async function publishSet(setEvt, publicPks, privPks, meta = {}) {
    setRelayState("publishing");
    const d = Nostr.dTagOf(setEvt) || meta.d;
    let cipher = "";
    if (privPks.length) {
      const payload = JSON.stringify(privPks.map(pk => ["p", pk]));
      cipher = await Nostr.nip04Encrypt(me, payload);
    }
    const tmpl = Nostr.buildFollowSet({
      d,
      title: meta.title ?? Nostr.titleOf(setEvt),
      description: meta.description ?? Nostr.descriptionOf(setEvt),
      image: meta.image ?? Nostr.imageOf(setEvt),
      publicPubkeys: publicPks,
      privateCiphertext: cipher,
    });
    const signed = await Nostr.signEvent(tmpl);
    await Nostr.publish(signed);
    setFollowSets(prev => {
      const others = prev.filter(e => Nostr.dTagOf(e) !== d);
      return [signed, ...others];
    });
    setPrivateMap(prev => ({ ...prev, [d]: privPks }));
    setRelayState("active");
    showToast(`Updated “${Nostr.titleOf(signed)}”`, "ok");
  }

  async function handleAdd(pubkey) {
    if (!activeView) return;
    try {
      if (activeView.kind === "following") {
        if (activeView.publicPubkeys.includes(pubkey)) return showToast("Already following", "ok");
        await publishKind3([...activeView.publicPubkeys, pubkey]);
        logActivity({ action: "follow", pubkey, target: "__following__", targetTitle: "Following" });
      } else {
        if (activeView.publicPubkeys.includes(pubkey)) return showToast("Already in this set", "ok");
        await publishSet(activeView.evt, [...activeView.publicPubkeys, pubkey], activeView.privatePubkeys);
        logActivity({ action: "added-to-list", pubkey, target: activeId, targetTitle: Nostr.titleOf(activeView.evt) });
      }
      // ensure profile loaded
      if (!profiles[pubkey]) {
        const p = await Nostr.fetchProfiles([pubkey]);
        setProfiles(prev => ({ ...prev, ...p }));
      }
    } catch (err) {
      console.error(err);
      showToast(err.message || "Publish failed", "err");
      setRelayState("active");
    }
  }

  async function handleRemove(pubkey, fromPrivate = false) {
    if (!activeView) return;
    markBusy(pubkey, "remove", true);
    try {
      if (activeView.kind === "following") {
        await publishKind3(activeView.publicPubkeys.filter(p => p !== pubkey));
        logActivity({ action: "unfollow", pubkey, target: "__following__", targetTitle: "Following" });
      } else {
        const title = Nostr.titleOf(activeView.evt);
        if (fromPrivate) {
          await publishSet(activeView.evt, activeView.publicPubkeys, activeView.privatePubkeys.filter(p => p !== pubkey));
          logActivity({ action: "removed-from-list", pubkey, target: activeId, targetTitle: title, private: true });
        } else {
          await publishSet(activeView.evt, activeView.publicPubkeys.filter(p => p !== pubkey), activeView.privatePubkeys);
          logActivity({ action: "removed-from-list", pubkey, target: activeId, targetTitle: title });
        }
      }
    } catch (err) {
      console.error(err);
      showToast(err.message || "Publish failed", "err");
      setRelayState("active");
    } finally {
      markBusy(pubkey, "remove", false);
    }
  }

  async function handleCreateSet(title, description) {
    setShowNew(false);
    try {
      const d = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") + "-" + Math.random().toString(36).slice(2, 6);
      setRelayState("publishing");
      const tmpl = Nostr.buildFollowSet({ d, title, description, publicPubkeys: [] });
      const signed = await Nostr.signEvent(tmpl);
      await Nostr.publish(signed);
      setFollowSets(prev => [signed, ...prev]);
      setActiveId(d);
      setRelayState("active");
      showToast(`Created “${title}”`, "ok");
    } catch (err) {
      console.error(err);
      showToast(err.message || "Could not create", "err");
      setRelayState("active");
    }
  }

  async function handleSearchProfiles(query) {
    return await Nostr.searchProfiles(query);
  }

  async function handleUpdateSet(setEvt, title, description) {
    try {
      const pub = Nostr.tagsOfList(setEvt);
      const priv = privateMap[Nostr.dTagOf(setEvt)] || [];
      await publishSet(setEvt, pub, priv, { title, description });
    } catch (err) {
      console.error(err);
      showToast(err.message || "Could not save", "err");
      setRelayState("active");
    }
  }

  async function handleDeleteSet(setEvt) {
    setRelayState("publishing");
    try {
      const d = Nostr.dTagOf(setEvt);
      const tmpl = {
        kind: 5,
        created_at: Math.floor(Date.now() / 1000),
        content: "",
        tags: [
          ["a", `30000:${me}:${d}`],
          ["k", "30000"],
        ],
      };
      const signed = await Nostr.signEvent(tmpl);
      await Nostr.publish(signed);
      setFollowSets(prev => prev.filter(e => Nostr.dTagOf(e) !== d));
      setPrivateMap(prev => { const next = { ...prev }; delete next[d]; return next; });
      if (activeId === d) setActiveId("__following__");
      showToast(`Deleted “${Nostr.titleOf(setEvt)}”`, "ok");
    } catch (err) {
      console.error(err);
      showToast(err.message || "Could not delete", "err");
    } finally {
      setRelayState("active");
    }
  }

  // membership: which lists contain this pubkey (public + private)
  const membershipFor = useCallback((pubkey) => {
    const m = {};
    for (const evt of followSets) {
      const d = Nostr.dTagOf(evt);
      const pub = Nostr.tagsOfList(evt).includes(pubkey);
      const priv = (privateMap[d] || []).includes(pubkey);
      if (pub || priv) m[d] = { public: pub, private: priv };
      else m[d] = { public: false, private: false };
    }
    return m;
  }, [followSets, privateMap]);

  const isFollowingPk = useCallback((pubkey) => {
    return contactList ? Nostr.tagsOfList(contactList).includes(pubkey) : false;
  }, [contactList]);

  // mark a pubkey's sub-action as busy or not
  const markBusy = useCallback((pubkey, key, value) => {
    setBusyPks(prev => {
      const next = { ...prev };
      const cur = { lists: {}, ...(next[pubkey] || {}) };
      if (key.startsWith("list:")) {
        const d = key.slice(5);
        cur.lists = { ...cur.lists, [d]: value };
        if (!value) delete cur.lists[d];
      } else {
        cur[key] = value;
        if (!value) delete cur[key];
      }
      const hasAny = cur.follow || cur.remove || Object.keys(cur.lists).length > 0;
      if (hasAny) next[pubkey] = cur; else delete next[pubkey];
      return next;
    });
  }, []);

  async function handleToggleFollow(pubkey) {
    markBusy(pubkey, "follow", true);
    try {
      const current = contactList ? Nostr.tagsOfList(contactList) : [];
      const wasFollowing = current.includes(pubkey);
      if (wasFollowing) {
        await publishKind3(current.filter(p => p !== pubkey));
      } else {
        await publishKind3([...current, pubkey]);
      }
      logActivity({ action: wasFollowing ? "unfollow" : "follow", pubkey, target: "__following__", targetTitle: "Following" });
    } catch (err) {
      console.error(err);
      showToast(err.message || "Publish failed", "err");
      setRelayState("active");
    } finally {
      markBusy(pubkey, "follow", false);
    }
  }

  async function handleToggleInList(pubkey, dTag, currentMembership) {
    markBusy(pubkey, "list:" + dTag, true);
    try {
      const evt = followSets.find(e => Nostr.dTagOf(e) === dTag);
      if (!evt) return;
      const pub = Nostr.tagsOfList(evt);
      const priv = privateMap[dTag] || [];
      const title = Nostr.titleOf(evt);
      if (currentMembership.public) {
        await publishSet(evt, pub.filter(p => p !== pubkey), priv);
        logActivity({ action: "removed-from-list", pubkey, target: dTag, targetTitle: title });
      } else if (currentMembership.private) {
        await publishSet(evt, pub, priv.filter(p => p !== pubkey));
        logActivity({ action: "removed-from-list", pubkey, target: dTag, targetTitle: title, private: true });
      } else {
        await publishSet(evt, [...pub, pubkey], priv);
        logActivity({ action: "added-to-list", pubkey, target: dTag, targetTitle: title });
      }
    } catch (err) {
      console.error(err);
      showToast(err.message || "Publish failed", "err");
      setRelayState("active");
    } finally {
      markBusy(pubkey, "list:" + dTag, false);
    }
  }

  async function handleToggleFavorite(pubkey) {
    const fav = followSets.find(e => Nostr.titleOf(e).trim().toLowerCase() === "favorites");
    if (fav) {
      const d = Nostr.dTagOf(fav);
      const map = membershipFor(pubkey);
      const m = map[d] || { public: false, private: false };
      await handleToggleInList(pubkey, d, m);
    } else {
      await handleCreateAndAdd(pubkey, "Favorites");
    }
  }

  async function handleCreateAndAdd(pubkey, title) {
    try {
      const d = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") + "-" + Math.random().toString(36).slice(2, 6);
      setRelayState("publishing");
      const tmpl = Nostr.buildFollowSet({ d, title, publicPubkeys: [pubkey] });
      const signed = await Nostr.signEvent(tmpl);
      await Nostr.publish(signed);
      setFollowSets(prev => [signed, ...prev]);
      setRelayState("active");
      showToast(`Created “${title}” with member`, "ok");
    } catch (err) {
      console.error(err);
      showToast(err.message || "Could not create", "err");
      setRelayState("active");
    }
  }

  const followingSet = useMemo(() => new Set(contactList ? Nostr.tagsOfList(contactList) : []), [contactList]);
  const listedSet = useMemo(() => {
    const s = new Set();
    for (const evt of followSets) {
      const d = Nostr.dTagOf(evt);
      for (const pk of Nostr.tagsOfList(evt)) s.add(pk);
      for (const pk of (privateMap[d] || [])) s.add(pk);
    }
    return s;
  }, [followSets, privateMap]);

  // --- render ---------------------------------------------------------
// also accept null `me` in topbar landing
  if (!me) {
    return (
      <div className="app">
        <Topbar
          me={null}
          profile={null}
          relayState="idle"
          scheme={t.scheme || "dark"}
          onToggleScheme={toggleScheme}
        />
        <div style={{ position: "relative" }}>
          <div className="center-card">
            <div className="login-card">
              <h2>Your contacts, your way</h2>
              <p>See who you follow and organise people into groups — like family, friends, or your favourite makers.</p>
              <button className="btn btn-primary" onClick={login} disabled={loading}>
                {loading ? "Signing you in…" : "Sign in"}
              </button>
              {loginError && <div style={{ marginTop: 18, color: "var(--danger)", fontFamily: "var(--font-mono)", fontSize: 12 }}>{loginError}</div>}
              <div className="hint">
                Sign-in uses your nostr browser extension — your keys never leave it.
                Don't have one? Try{" "}
                <a href="https://getalby.com/" target="_blank" rel="noopener noreferrer">Alby</a>{" "}or{" "}
                <a href="https://github.com/fiatjaf/nos2x" target="_blank" rel="noopener noreferrer">nos2x</a>.
              </div>
            </div>
          </div>
        </div>
        {window.TweaksPanel ? <ThemeTweaks t={t} setTweak={setTweak} /> : null}
      </div>
    );
  }

  const view = activeView;
  const sortByName = (pks) => [...pks].sort((a, b) => {
    const pa = profiles[a]; const pb = profiles[b];
    const na = (pa?.display_name || pa?.name || "").trim();
    const nb = (pb?.display_name || pb?.name || "").trim();
    if (!na && !nb) return a.localeCompare(b);
    if (!na) return 1;
    if (!nb) return -1;
    return na.localeCompare(nb, undefined, { sensitivity: "base" });
  });
  const sortedPublic = view ? sortByName(view.publicPubkeys) : [];
  const sortedPrivate = view ? sortByName(view.privatePubkeys) : [];

  return (
    <div className="app">
      <Topbar
        me={me}
        profile={meProfile}
        relayState={relayState}
        scheme={t.scheme || "dark"}
        onToggleScheme={toggleScheme}
        onSignout={signout}
        onOpenActivity={() => setShowActivity(true)}
        onOpenHelp={() => setShowHelp(true)}
        onHome={() => { setOpenContact(null); setActiveId("__following__"); }}
        searchProps={{
          profiles,
          followingSet,
          listedSet,
          onSearchProfiles: handleSearchProfiles,
          onSelect: (pk, profile) => {
            if (profile) setProfiles(prev => prev[pk] ? prev : ({ ...prev, [pk]: profile }));
            setOpenContact(pk);
          },
        }}
      />
      <div className="panes">
        <Sidebar
          contactList={contactList}
          followSets={followSets}
          activeId={openContact ? null : activeId}
          loading={loading}
          onSelect={(id) => { setOpenContact(null); setActiveId(id); }}
          onNew={() => setShowNew(true)}
        />
        <main className="main">
          {openContact ? (() => {
            const favEvt = followSets.find(e => Nostr.titleOf(e).trim().toLowerCase() === "favorites");
            const favD = favEvt ? Nostr.dTagOf(favEvt) : null;
            const favMembership = favD ? (membershipFor(openContact)[favD] || {}) : {};
            const isFavorite = !!(favMembership.public || favMembership.private);
            const favoriteBusy = !!(favD && busyPks[openContact]?.lists?.["list:" + favD]);
            return (
              <ContactDetail
                pubkey={openContact}
                profile={profiles[openContact]}
                lastSeen={lastSeen[openContact]}
                isFollowing={isFollowingPk(openContact)}
                lists={followSets}
                membershipMap={membershipFor(openContact)}
                busyMap={busyPks[openContact]?.lists || {}}
                followBusy={!!busyPks[openContact]?.follow}
                isFavorite={isFavorite}
                favoriteBusy={favoriteBusy}
                onToggleFavorite={() => handleToggleFavorite(openContact)}
                onToggleFollow={() => handleToggleFollow(openContact)}
                onToggleInList={(d, m) => handleToggleInList(openContact, d, m)}
                onCreateAndAdd={(title) => handleCreateAndAdd(openContact, title)}
                onSelectList={(d) => { setOpenContact(null); setActiveId(d); }}
                profiles={profiles}
                onSelectContact={(pk) => setOpenContact(pk)}
                onToast={(msg, kind) => showToast(msg, kind)}
                profileLoading={profileLoading}
                onNeedProfiles={(pks) => {
                  Nostr.fetchProfiles(pks).then(p => {
                    if (Object.keys(p).length) setProfiles(prev => ({ ...prev, ...p }));
                  });
                }}
              />
            );
          })() : (
          <div className="main-inner">
            {view ? (
              <>
                <header className="list-header">
                  {view.eyebrow && <div className="eyebrow">{view.eyebrow}</div>}
                  <div className="list-title-row">
                    <h1>{view.title}</h1>
                    {view.kind === "set" && (
                      <button
                        className="edit-group-btn"
                        onClick={() => setEditSet(view.evt)}
                        title="Edit group"
                        aria-label="Edit group"
                      >
                        <svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
                          <path d="M4 20h4l10-10-4-4L4 16v4z" fill="none" stroke="currentColor" strokeWidth="2" strokeLinejoin="round" />
                          <path d="M14 6l4 4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
                        </svg>
                      </button>
                    )}
                  </div>
                  {view.desc && <div className="desc">{view.desc}</div>}
                  {view.evt && (
                    <div className="meta">
                      <span className="kv">Last updated <b>{humanTime(view.evt.created_at)}</b></span>
                    </div>
                  )}
                </header>

                {loading ? (
                  <div className="loading-pane">
                    <span className="spinner lg" aria-hidden="true"></span>
                    <span>Loading your contacts…</span>
                  </div>
                ) : (<>
                {/* PUBLIC */}
                <div className="split-heading">
                  <span className="badge">
                    <svg width="12" height="12" viewBox="0 0 24 24" aria-hidden="true">
                      <circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" strokeWidth="2" />
                      <path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" fill="none" stroke="currentColor" strokeWidth="2" />
                    </svg>
                    public
                  </span>
                  <hr />
                  <span>
                    {view.kind === "following"
                      ? `You're following ${view.publicPubkeys.length} ${view.publicPubkeys.length === 1 ? "profile" : "profiles"}`
                      : `${view.publicPubkeys.length} ${view.publicPubkeys.length === 1 ? "contact" : "contacts"}`}
                  </span>
                </div>
                {view.publicPubkeys.length === 0 ? (
                  <div className="empty-state">No contacts yet. Add someone above.</div>
                ) : (
                  sortedPublic.map(pk => (
                    <MemberRow
                      key={pk}
                      pubkey={pk}
                      profile={profiles[pk]}
                      isFollowing={isFollowingPk(pk)}
                      busyMap={busyPks[pk]?.lists || {}}
                      followBusy={!!busyPks[pk]?.follow}
                      removeBusy={!!busyPks[pk]?.remove}
                      onOpenContact={() => setOpenContact(pk)}
                      onRemoveFromActive={() => handleRemove(pk, false)}
                    />
                  ))
                )}

                {/* PRIVATE */}
                {t.splitPrivate && view.kind === "set" && (
                  <>
                    <div className="split-heading private">
                      <span className="badge">
                        <svg width="12" height="12" viewBox="0 0 24 24" aria-hidden="true">
                          <rect x="5" y="11" width="14" height="10" rx="2" fill="none" stroke="currentColor" strokeWidth="2" />
                          <path d="M8 11V7a4 4 0 0 1 8 0v4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
                        </svg>
                        private · encrypted
                      </span>
                      <hr />
                      <span>{view.privatePubkeys.length} {view.privatePubkeys.length === 1 ? "contact" : "contacts"}</span>
                    </div>
                    {view.privatePubkeys.length === 0 ? (
                      <div className="empty-state">
                        Private contacts are encrypted. Visible only to you.
                      </div>
                    ) : (
                      sortedPrivate.map(pk => (
                        <MemberRow
                          key={pk}
                          pubkey={pk}
                          profile={profiles[pk]}
                          isFollowing={isFollowingPk(pk)}
                          busyMap={busyPks[pk]?.lists || {}}
                          followBusy={!!busyPks[pk]?.follow}
                          removeBusy={!!busyPks[pk]?.remove}
                          onOpenContact={() => setOpenContact(pk)}
                          onRemoveFromActive={() => handleRemove(pk, true)}
                        />
                      ))
                    )}
                  </>
                )}
                </>)}
              </>
            ) : (
              <div className="empty-state">Select a list on the left.</div>
            )}
          </div>
          )}
        </main>
      </div>

      {showNew && <NewListDialog onClose={() => setShowNew(false)} onCreate={handleCreateSet} />}
      {editSet && (
        <EditListDialog
          initialTitle={Nostr.titleOf(editSet)}
          initialDesc={Nostr.descriptionOf(editSet)}
          onSave={(title, desc) => handleUpdateSet(editSet, title, desc)}
          onDelete={() => handleDeleteSet(editSet)}
          onClose={() => setEditSet(null)}
        />
      )}
      {showActivity && (
        <ActivityDialog
          entries={activity}
          profiles={profiles}
          onClose={() => setShowActivity(false)}
          onClear={() => { setActivity([]); try { localStorage.removeItem("nostrlist.activity"); } catch {} }}
        />
      )}
      {showHelp && <HelpDialog onClose={() => setShowHelp(false)} />}
      {toast && <div className={"toast " + toast.kind}>{toast.msg}</div>}
      {window.TweaksPanel ? <ThemeTweaks t={t} setTweak={setTweak} /> : null}
    </div>
  );
}

// ---------- Tweaks panel ------------------------------------------------
function ThemeTweaks({ t, setTweak }) {
  const { TweaksPanel, TweakSection, TweakRadio, TweakToggle } = window;
  return (
    <TweaksPanel title="Tweaks">
      <TweakSection label="Aesthetic">
        <TweakRadio
          label="Theme"
          value={t.theme}
          onChange={(v) => setTweak("theme", v)}
          options={[
            { value: "foundry", label: "Foundry" },
            { value: "studio",  label: "Studio"  },
            { value: "console", label: "Console" },
          ]}
        />
      </TweakSection>
      <TweakSection label="Member rows">
        <TweakToggle
          label="Show last-seen"
          value={t.showLastSeen}
          onChange={(v) => setTweak("showLastSeen", v)}
        />
        <TweakToggle
          label="Split public / private"
          value={t.splitPrivate}
          onChange={(v) => setTweak("splitPrivate", v)}
        />
      </TweakSection>
    </TweaksPanel>
  );
}

// ---------- mount -------------------------------------------------------
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
