2022-12-29 15:36:40 +00:00
|
|
|
import "./Layout.css";
|
2023-02-07 20:04:50 +00:00
|
|
|
import { useEffect, useMemo, useState } from "react";
|
2022-12-27 23:46:13 +00:00
|
|
|
import { useDispatch, useSelector } from "react-redux";
|
2023-02-06 22:29:17 +00:00
|
|
|
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
2023-01-28 21:43:56 +00:00
|
|
|
import Envelope from "Icons/Envelope";
|
|
|
|
import Bell from "Icons/Bell";
|
|
|
|
import Search from "Icons/Search";
|
2022-12-29 15:36:40 +00:00
|
|
|
|
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";
|
2023-02-07 20:04:50 +00:00
|
|
|
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";
|
2023-02-07 20:04:50 +00:00
|
|
|
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";
|
|
|
|
import messages from './messages'
|
2023-01-28 21:43:56 +00:00
|
|
|
|
2023-01-16 18:14:08 +00:00
|
|
|
export default function Layout() {
|
2023-02-07 20:04:50 +00:00
|
|
|
const location = useLocation();
|
|
|
|
const [show, setShow] = useState(false);
|
|
|
|
const dispatch = useDispatch();
|
|
|
|
const navigate = useNavigate();
|
|
|
|
const {
|
|
|
|
loggedOut,
|
|
|
|
publicKey,
|
|
|
|
relays,
|
|
|
|
latestNotification,
|
|
|
|
readNotifications,
|
|
|
|
dms,
|
|
|
|
preferences,
|
|
|
|
newUserKey,
|
|
|
|
} = useSelector((s: RootState) => s.login);
|
|
|
|
const { isMuted } = useModeration();
|
|
|
|
|
|
|
|
const usingDb = useDb();
|
|
|
|
const pub = useEventPublisher();
|
|
|
|
useLoginFeed();
|
|
|
|
|
|
|
|
const shouldHideNoteCreator = useMemo(() => {
|
|
|
|
const hideNoteCreator = ["/settings", "/messages", "/new"];
|
|
|
|
return hideNoteCreator.some((a) => location.pathname.startsWith(a));
|
|
|
|
}, [location]);
|
|
|
|
|
|
|
|
const hasNotifications = useMemo(
|
|
|
|
() => latestNotification > readNotifications,
|
|
|
|
[latestNotification, readNotifications]
|
|
|
|
);
|
|
|
|
const unreadDms = useMemo(
|
|
|
|
() =>
|
|
|
|
publicKey
|
|
|
|
? totalUnread(
|
|
|
|
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)) {
|
2023-02-07 20:04:50 +00:00
|
|
|
System.ConnectToRelay(k, v);
|
|
|
|
}
|
2023-02-07 19:47:57 +00:00
|
|
|
for (const [k] of System.Sockets) {
|
2023-02-07 20:04:50 +00:00
|
|
|
if (!relays[k] && !SearchRelays.has(k)) {
|
|
|
|
System.DisconnectRelay(k);
|
2023-01-20 17:07:14 +00:00
|
|
|
}
|
2023-02-07 20:04:50 +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)");
|
2023-02-07 20:04:50 +00:00
|
|
|
setTheme(
|
|
|
|
preferences.theme === "system" && osTheme.matches
|
|
|
|
? "light"
|
|
|
|
: preferences.theme === "light"
|
|
|
|
? "light"
|
|
|
|
: "dark"
|
|
|
|
);
|
|
|
|
|
|
|
|
osTheme.onchange = (e) => {
|
|
|
|
if (preferences.theme === "system") {
|
|
|
|
setTheme(e.matches ? "light" : "dark");
|
|
|
|
}
|
|
|
|
};
|
|
|
|
return () => {
|
|
|
|
osTheme.onchange = null;
|
|
|
|
};
|
|
|
|
}, [preferences.theme]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
// check DB support then init
|
|
|
|
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));
|
|
|
|
});
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
async function handleNewUser() {
|
2023-02-07 19:47:57 +00:00
|
|
|
let newRelays: Record<string, RelaySettings> = {};
|
2023-02-07 20:04:50 +00:00
|
|
|
|
|
|
|
try {
|
2023-02-07 19:47:57 +00:00
|
|
|
const rsp = await fetch("https://api.nostr.watch/v1/online");
|
2023-02-07 20:04:50 +00:00
|
|
|
if (rsp.ok) {
|
2023-02-07 19:47:57 +00:00
|
|
|
const online: string[] = await rsp.json();
|
|
|
|
const pickRandom = online
|
|
|
|
.sort(() => (Math.random() >= 0.5 ? 1 : -1))
|
2023-02-07 20:04:50 +00:00
|
|
|
.slice(0, 4); // pick 4 random relays
|
|
|
|
|
2023-02-07 19:47:57 +00:00
|
|
|
const relayObjects = pickRandom.map((a) => [
|
2023-02-07 20:04:50 +00:00
|
|
|
a,
|
|
|
|
{ read: true, write: true },
|
|
|
|
]);
|
|
|
|
newRelays = Object.fromEntries(relayObjects);
|
|
|
|
dispatch(
|
|
|
|
setRelays({
|
2023-02-07 19:47:57 +00:00
|
|
|
relays: newRelays,
|
2023-02-07 20:04:50 +00:00
|
|
|
createdAt: 1,
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
console.warn(e);
|
2023-01-20 18:59:08 +00:00
|
|
|
}
|
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
const ev = await pub.addFollow(bech32ToHex(SnortPubKey), newRelays);
|
|
|
|
pub.broadcast(ev);
|
|
|
|
}
|
2023-02-05 06:29:46 +00:00
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
useEffect(() => {
|
|
|
|
if (newUserKey === true) {
|
|
|
|
handleNewUser().catch(console.warn);
|
2023-02-05 06:29:46 +00:00
|
|
|
}
|
2023-02-07 20:04:50 +00:00
|
|
|
}, [newUserKey]);
|
|
|
|
|
2023-02-07 19:47:57 +00:00
|
|
|
async function goToNotifications(e: React.MouseEvent) {
|
2023-02-07 20:04:50 +00:00
|
|
|
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();
|
2023-02-07 20:04:50 +00:00
|
|
|
console.debug(res);
|
2023-01-03 13:17:09 +00:00
|
|
|
}
|
2023-02-07 20:04:50 +00:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
}
|
2023-01-03 13:17:09 +00:00
|
|
|
}
|
2023-02-07 20:04:50 +00:00
|
|
|
navigate("/notifications");
|
|
|
|
}
|
2023-01-03 13:17:09 +00:00
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
function accountHeader() {
|
2022-12-18 14:51:47 +00:00
|
|
|
return (
|
2023-02-07 20:04:50 +00:00
|
|
|
<div className="header-actions">
|
2023-02-07 19:47:57 +00:00
|
|
|
<div className="btn btn-rnd" onClick={() => navigate("/search")}>
|
2023-02-07 20:04:50 +00:00
|
|
|
<Search />
|
|
|
|
</div>
|
2023-02-07 19:47:57 +00:00
|
|
|
<div className="btn btn-rnd" onClick={() => navigate("/messages")}>
|
2023-02-07 20:04:50 +00:00
|
|
|
<Envelope />
|
|
|
|
{unreadDms > 0 && <span className="has-unread"></span>}
|
|
|
|
</div>
|
2023-02-07 19:47:57 +00:00
|
|
|
<div className="btn btn-rnd" onClick={goToNotifications}>
|
2023-02-07 20:04:50 +00:00
|
|
|
<Bell />
|
|
|
|
{hasNotifications && <span className="has-unread"></span>}
|
|
|
|
</div>
|
|
|
|
<ProfileImage pubkey={publicKey || ""} showUsername={false} />
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof loggedOut !== "boolean") {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<div className="page">
|
|
|
|
<header>
|
|
|
|
<div className="logo" onClick={() => navigate("/")}>
|
|
|
|
Snort
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
{publicKey ? (
|
|
|
|
accountHeader()
|
|
|
|
) : (
|
|
|
|
<button type="button" onClick={() => navigate("/login")}>
|
2023-02-08 05:56:00 +00:00
|
|
|
<FormattedMessage {...messages.Login} />
|
2023-02-07 20:04:50 +00:00
|
|
|
</button>
|
|
|
|
)}
|
2022-12-18 14:51:47 +00:00
|
|
|
</div>
|
2023-02-07 20:04:50 +00:00
|
|
|
</header>
|
|
|
|
<Outlet />
|
|
|
|
|
|
|
|
{!shouldHideNoteCreator && (
|
|
|
|
<>
|
|
|
|
<button
|
|
|
|
className="note-create-button"
|
|
|
|
type="button"
|
|
|
|
onClick={() => setShow(!show)}
|
|
|
|
>
|
|
|
|
<Plus />
|
|
|
|
</button>
|
|
|
|
<NoteCreator
|
|
|
|
replyTo={undefined}
|
|
|
|
autoFocus={true}
|
|
|
|
show={show}
|
|
|
|
setShow={setShow}
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
);
|
2023-01-19 23:19:09 +00:00
|
|
|
}
|