snort/src/Pages/Layout.tsx

253 lines
7.3 KiB
TypeScript
Raw Normal View History

2022-12-29 15:36:40 +00:00
import "./Layout.css";
import { useEffect, useMemo, useState } from "react";
2022-12-27 23:46:13 +00:00
import { useDispatch, useSelector } from "react-redux";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
2023-02-10 19:23:52 +00:00
import { randomSample } from "Util";
2023-01-28 21:43:56 +00:00
import Envelope from "Icons/Envelope";
import Bell from "Icons/Bell";
import Search from "Icons/Search";
2023-01-20 11:11:50 +00:00
import { RootState } from "State/Store";
2023-02-05 06:29:46 +00:00
import { init, setRelays } from "State/Login";
import { System } from "Nostr/System";
2023-01-20 11:11:50 +00:00
import ProfileImage from "Element/ProfileImage";
import useLoginFeed from "Feed/LoginFeed";
import { totalUnread } from "Pages/MessagesPage";
import { SearchRelays, SnortPubKey } from "Const";
2023-01-23 01:45:53 +00:00
import useEventPublisher from "Feed/EventPublisher";
2023-01-28 21:43:56 +00:00
import useModeration from "Hooks/useModeration";
2023-02-01 19:48:35 +00:00
import { IndexedUDB, useDb } from "State/Users/Db";
2023-02-01 20:34:23 +00:00
import { db } from "Db";
2023-02-05 06:29:46 +00:00
import { bech32ToHex } from "Util";
2023-02-05 12:32:34 +00:00
import { NoteCreator } from "Element/NoteCreator";
import Plus from "Icons/Plus";
2023-02-05 18:02:13 +00:00
import { RelaySettings } from "Nostr/Connection";
2023-02-08 05:56:00 +00:00
import { FormattedMessage } from "react-intl";
2023-02-08 20:16:35 +00:00
import messages from "./messages";
2023-01-28 21:43:56 +00:00
2023-01-16 18:14:08 +00:00
export default function Layout() {
const location = useLocation();
const [show, setShow] = useState(false);
const dispatch = useDispatch();
const navigate = useNavigate();
2023-02-09 12:26:54 +00:00
const { loggedOut, publicKey, relays, latestNotification, readNotifications, dms, preferences, newUserKey } =
useSelector((s: RootState) => s.login);
const { isMuted } = useModeration();
2023-02-09 18:05:45 +00:00
const [pageClass, setPageClass] = useState("page");
const usingDb = useDb();
const pub = useEventPublisher();
useLoginFeed();
const shouldHideNoteCreator = useMemo(() => {
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate"];
2023-02-09 18:05:45 +00:00
return hideOn.some(a => location.pathname.startsWith(a));
}, [location]);
const shouldHideHeader = useMemo(() => {
const hideOn = ["/login"];
return hideOn.some(a => location.pathname.startsWith(a));
}, [location]);
useEffect(() => {
if (location.pathname.startsWith("/login")) {
setPageClass("");
} else {
setPageClass("page");
}
}, [location]);
const hasNotifications = useMemo(
() => latestNotification > readNotifications,
[latestNotification, readNotifications]
);
const unreadDms = useMemo(
() =>
publicKey
? totalUnread(
2023-01-29 19:44:53 +00:00
dms.filter(a => !isMuted(a.pubkey)),
publicKey
)
: 0,
[dms, publicKey]
);
useEffect(() => {
System.nip42Auth = pub.nip42Auth;
}, [pub]);
useEffect(() => {
System.UserDb = usingDb;
}, [usingDb]);
useEffect(() => {
if (relays) {
2023-02-07 19:47:57 +00:00
for (const [k, v] of Object.entries(relays)) {
System.ConnectToRelay(k, v);
}
2023-02-07 19:47:57 +00:00
for (const [k] of System.Sockets) {
if (!relays[k] && !SearchRelays.has(k)) {
System.DisconnectRelay(k);
2023-01-20 17:07:14 +00:00
}
}
}
}, [relays]);
function setTheme(theme: "light" | "dark") {
const elm = document.documentElement;
if (theme === "light" && !elm.classList.contains("light")) {
elm.classList.add("light");
} else if (theme === "dark" && elm.classList.contains("light")) {
elm.classList.remove("light");
}
}
useEffect(() => {
2023-02-07 19:47:57 +00:00
const osTheme = window.matchMedia("(prefers-color-scheme: light)");
setTheme(
2023-02-09 12:26:54 +00:00
preferences.theme === "system" && osTheme.matches ? "light" : preferences.theme === "light" ? "light" : "dark"
);
2023-02-09 12:26:54 +00:00
osTheme.onchange = e => {
if (preferences.theme === "system") {
setTheme(e.matches ? "light" : "dark");
}
};
return () => {
osTheme.onchange = null;
};
}, [preferences.theme]);
useEffect(() => {
// check DB support then init
2023-02-09 12:26:54 +00:00
IndexedUDB.isAvailable().then(async a => {
const dbType = a ? "indexdDb" : "redux";
// cleanup on load
if (dbType === "indexdDb") {
await db.feeds.clear();
const now = Math.floor(new Date().getTime() / 1000);
const cleanupEvents = await db.events
.where("created_at")
.above(now - 60 * 60)
.primaryKeys();
console.debug(`Cleanup ${cleanupEvents.length} events`);
await db.events.bulkDelete(cleanupEvents);
}
console.debug(`Using db: ${dbType}`);
dispatch(init(dbType));
2023-01-29 19:44:53 +00:00
try {
if ("registerProtocolHandler" in window.navigator) {
window.navigator.registerProtocolHandler("web+nostr", `${window.location.protocol}//${window.location.host}/handler/%s`);
console.info("Registered protocol handler for \"nostr\"");
}
} catch (e) {
console.error("Failed to register protocol handler", e);
}
});
}, []);
async function handleNewUser() {
2023-02-07 19:47:57 +00:00
let newRelays: Record<string, RelaySettings> = {};
try {
2023-02-07 19:47:57 +00:00
const rsp = await fetch("https://api.nostr.watch/v1/online");
if (rsp.ok) {
2023-02-07 19:47:57 +00:00
const online: string[] = await rsp.json();
2023-02-10 19:23:52 +00:00
const pickRandom = randomSample(online, 4);
2023-02-09 12:26:54 +00:00
const relayObjects = pickRandom.map(a => [a, { read: true, write: true }]);
newRelays = Object.fromEntries(relayObjects);
dispatch(
setRelays({
2023-02-07 19:47:57 +00:00
relays: newRelays,
createdAt: 1,
})
);
}
} catch (e) {
console.warn(e);
2023-01-20 18:59:08 +00:00
}
const ev = await pub.addFollow(bech32ToHex(SnortPubKey), newRelays);
pub.broadcast(ev);
}
2023-02-05 06:29:46 +00:00
useEffect(() => {
if (newUserKey === true) {
handleNewUser().catch(console.warn);
2023-02-05 06:29:46 +00:00
}
}, [newUserKey]);
2023-02-07 19:47:57 +00:00
async function goToNotifications(e: React.MouseEvent) {
e.stopPropagation();
// request permissions to send notifications
if ("Notification" in window) {
try {
if (Notification.permission !== "granted") {
2023-02-07 19:47:57 +00:00
const res = await Notification.requestPermission();
console.debug(res);
2023-01-03 13:17:09 +00:00
}
} catch (e) {
console.error(e);
}
2023-01-03 13:17:09 +00:00
}
navigate("/notifications");
}
2023-01-03 13:17:09 +00:00
function accountHeader() {
2022-12-18 14:51:47 +00:00
return (
<div className="header-actions">
2023-02-07 19:47:57 +00:00
<div className="btn btn-rnd" onClick={() => navigate("/search")}>
<Search />
</div>
2023-02-07 19:47:57 +00:00
<div className="btn btn-rnd" onClick={() => navigate("/messages")}>
<Envelope />
{unreadDms > 0 && <span className="has-unread"></span>}
</div>
2023-02-07 19:47:57 +00:00
<div className="btn btn-rnd" onClick={goToNotifications}>
<Bell />
{hasNotifications && <span className="has-unread"></span>}
</div>
<ProfileImage pubkey={publicKey || ""} showUsername={false} />
</div>
);
}
if (typeof loggedOut !== "boolean") {
return null;
}
return (
2023-02-09 18:05:45 +00:00
<div className={pageClass}>
{!shouldHideHeader && (
<header>
<div className="logo" onClick={() => navigate("/")}>
Snort
</div>
<div>
{publicKey ? (
accountHeader()
) : (
<button type="button" onClick={() => navigate("/login")}>
<FormattedMessage {...messages.Login} />
</button>
)}
</div>
</header>
)}
<Outlet />
{!shouldHideNoteCreator && (
<>
2023-02-09 12:26:54 +00:00
<button className="note-create-button" type="button" onClick={() => setShow(!show)}>
<Plus />
</button>
2023-02-09 12:26:54 +00:00
<NoteCreator replyTo={undefined} autoFocus={true} show={show} setShow={setShow} />
</>
)}
</div>
);
2023-01-19 23:19:09 +00:00
}