Add prettier formatting (#214)
* chore: add prettier * chore: format codebase
This commit is contained in:
@ -1,33 +1,33 @@
|
||||
.dm-list {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: calc(100vh - 66px - 50px - 70px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: calc(100vh - 66px - 50px - 70px);
|
||||
}
|
||||
|
||||
.dm-list > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10px;
|
||||
scroll-padding-bottom: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 10px;
|
||||
scroll-padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.write-dm {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
background-color: var(--gray-light);
|
||||
width: inherit;
|
||||
border-radius: 5px 5px 0 0;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
background-color: var(--gray-light);
|
||||
width: inherit;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.write-dm .inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 5px;
|
||||
}
|
||||
.write-dm textarea {
|
||||
resize: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.write-dm-spacer {
|
||||
margin-bottom: 80px;
|
||||
margin-bottom: 80px;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import "./ChatPage.css";
|
||||
import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useInView } from "react-intersection-observer";
|
||||
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import { bech32ToHex } from "Util";
|
||||
@ -14,68 +14,78 @@ import { dmsInChat, isToSelf } from "Pages/MessagesPage";
|
||||
import NoteToSelf from "Element/NoteToSelf";
|
||||
|
||||
type RouterParams = {
|
||||
id: string
|
||||
}
|
||||
id: string;
|
||||
};
|
||||
|
||||
export default function ChatPage() {
|
||||
const params = useParams<RouterParams>();
|
||||
const publisher = useEventPublisher();
|
||||
const id = bech32ToHex(params.id ?? "");
|
||||
const pubKey = useSelector<any>(s => s.login.publicKey);
|
||||
const dms = useSelector<any, RawEvent[]>(s => filterDms(s.login.dms));
|
||||
const [content, setContent] = useState<string>();
|
||||
const { ref, inView, entry } = useInView();
|
||||
const dmListRef = useRef<HTMLDivElement>(null);
|
||||
const params = useParams<RouterParams>();
|
||||
const publisher = useEventPublisher();
|
||||
const id = bech32ToHex(params.id ?? "");
|
||||
const pubKey = useSelector<any>((s) => s.login.publicKey);
|
||||
const dms = useSelector<any, RawEvent[]>((s) => filterDms(s.login.dms));
|
||||
const [content, setContent] = useState<string>();
|
||||
const { ref, inView, entry } = useInView();
|
||||
const dmListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function filterDms(dms: RawEvent[]) {
|
||||
return dmsInChat(id === pubKey ? dms.filter(d => isToSelf(d, pubKey)) : dms, id);
|
||||
function filterDms(dms: RawEvent[]) {
|
||||
return dmsInChat(
|
||||
id === pubKey ? dms.filter((d) => isToSelf(d, pubKey)) : dms,
|
||||
id
|
||||
);
|
||||
}
|
||||
|
||||
const sortedDms = useMemo<any[]>(() => {
|
||||
return [...dms].sort((a, b) => a.created_at - b.created_at);
|
||||
}, [dms]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inView && dmListRef.current) {
|
||||
dmListRef.current.scroll(0, dmListRef.current.scrollHeight);
|
||||
}
|
||||
}, [inView, dmListRef, sortedDms]);
|
||||
|
||||
const sortedDms = useMemo<any[]>(() => {
|
||||
return [...dms].sort((a, b) => a.created_at - b.created_at)
|
||||
}, [dms]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inView && dmListRef.current) {
|
||||
dmListRef.current.scroll(0, dmListRef.current.scrollHeight);
|
||||
}
|
||||
}, [inView, dmListRef, sortedDms]);
|
||||
|
||||
async function sendDm() {
|
||||
if (content) {
|
||||
let ev = await publisher.sendDm(content, id);
|
||||
console.debug(ev);
|
||||
publisher.broadcast(ev);
|
||||
setContent("");
|
||||
}
|
||||
async function sendDm() {
|
||||
if (content) {
|
||||
let ev = await publisher.sendDm(content, id);
|
||||
console.debug(ev);
|
||||
publisher.broadcast(ev);
|
||||
setContent("");
|
||||
}
|
||||
}
|
||||
|
||||
async function onEnter(e: KeyboardEvent) {
|
||||
let isEnter = e.code === "Enter";
|
||||
if (isEnter && !e.shiftKey) {
|
||||
await sendDm();
|
||||
}
|
||||
async function onEnter(e: KeyboardEvent) {
|
||||
let isEnter = e.code === "Enter";
|
||||
if (isEnter && !e.shiftKey) {
|
||||
await sendDm();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{id === pubKey && (
|
||||
<NoteToSelf className="f-grow mb-10" pubkey={id} />
|
||||
) || (
|
||||
<ProfileImage pubkey={id} className="f-grow mb10" />
|
||||
)}
|
||||
<div className="dm-list" ref={dmListRef}>
|
||||
<div>
|
||||
{sortedDms.map(a => <DM data={a} key={a.id} />)}
|
||||
<div ref={ref} className="mb10"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="write-dm">
|
||||
<div className="inner">
|
||||
<textarea className="f-grow mr10" value={content} onChange={(e) => setContent(e.target.value)} onKeyDown={(e) => onEnter(e)}></textarea>
|
||||
<button type="button" onClick={() => sendDm()}>Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
{(id === pubKey && (
|
||||
<NoteToSelf className="f-grow mb-10" pubkey={id} />
|
||||
)) || <ProfileImage pubkey={id} className="f-grow mb10" />}
|
||||
<div className="dm-list" ref={dmListRef}>
|
||||
<div>
|
||||
{sortedDms.map((a) => (
|
||||
<DM data={a} key={a.id} />
|
||||
))}
|
||||
<div ref={ref} className="mb10"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="write-dm">
|
||||
<div className="inner">
|
||||
<textarea
|
||||
className="f-grow mr10"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
onKeyDown={(e) => onEnter(e)}
|
||||
></textarea>
|
||||
<button type="button" onClick={() => sendDm()}>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -6,83 +6,109 @@ import { useEffect, useState } from "react";
|
||||
import { bech32ToHex } from "Util";
|
||||
|
||||
const Developers = [
|
||||
bech32ToHex(KieranPubKey), // kieran
|
||||
bech32ToHex("npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg"), // verbiricha
|
||||
bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"), // Karnage
|
||||
bech32ToHex(KieranPubKey), // kieran
|
||||
bech32ToHex(
|
||||
"npub107jk7htfv243u0x5ynn43scq9wrxtaasmrwwa8lfu2ydwag6cx2quqncxg"
|
||||
), // verbiricha
|
||||
bech32ToHex(
|
||||
"npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"
|
||||
), // Karnage
|
||||
];
|
||||
|
||||
const Contributors = [
|
||||
bech32ToHex("npub10djxr5pvdu97rjkde7tgcsjxzpdzmdguwacfjwlchvj7t88dl7nsdl54nf"), // ivan
|
||||
bech32ToHex("npub148jmlutaa49y5wl5mcll003ftj59v79vf7wuv3apcwpf75hx22vs7kk9ay"), // liran cohen
|
||||
bech32ToHex("npub1xdtducdnjerex88gkg2qk2atsdlqsyxqaag4h05jmcpyspqt30wscmntxy"), // artur
|
||||
bech32ToHex(
|
||||
"npub10djxr5pvdu97rjkde7tgcsjxzpdzmdguwacfjwlchvj7t88dl7nsdl54nf"
|
||||
), // ivan
|
||||
bech32ToHex(
|
||||
"npub148jmlutaa49y5wl5mcll003ftj59v79vf7wuv3apcwpf75hx22vs7kk9ay"
|
||||
), // liran cohen
|
||||
bech32ToHex(
|
||||
"npub1xdtducdnjerex88gkg2qk2atsdlqsyxqaag4h05jmcpyspqt30wscmntxy"
|
||||
), // artur
|
||||
];
|
||||
|
||||
interface Splits {
|
||||
pubKey: string,
|
||||
split: number
|
||||
pubKey: string;
|
||||
split: number;
|
||||
}
|
||||
|
||||
interface TotalToday {
|
||||
donations: number,
|
||||
nip5: number
|
||||
donations: number;
|
||||
nip5: number;
|
||||
}
|
||||
|
||||
const DonatePage = () => {
|
||||
const [splits, setSplits] = useState<Splits[]>([]);
|
||||
const [today, setSumToday] = useState<TotalToday>();
|
||||
const [splits, setSplits] = useState<Splits[]>([]);
|
||||
const [today, setSumToday] = useState<TotalToday>();
|
||||
|
||||
async function loadData() {
|
||||
let rsp = await fetch(`${ApiHost}/api/v1/revenue/splits`);
|
||||
if(rsp.ok) {
|
||||
setSplits(await rsp.json());
|
||||
}
|
||||
let rsp2 = await fetch(`${ApiHost}/api/v1/revenue/today`);
|
||||
if(rsp2.ok) {
|
||||
setSumToday(await rsp2.json());
|
||||
}
|
||||
async function loadData() {
|
||||
let rsp = await fetch(`${ApiHost}/api/v1/revenue/splits`);
|
||||
if (rsp.ok) {
|
||||
setSplits(await rsp.json());
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData().catch(console.warn);
|
||||
}, []);
|
||||
|
||||
function actions(pk: HexKey) {
|
||||
let split = splits.find(a => bech32ToHex(a.pubKey) === pk);
|
||||
if(split) {
|
||||
return <>{(100 * split.split).toLocaleString()}%</>
|
||||
}
|
||||
return <></>
|
||||
let rsp2 = await fetch(`${ApiHost}/api/v1/revenue/today`);
|
||||
if (rsp2.ok) {
|
||||
setSumToday(await rsp2.json());
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-content m5">
|
||||
<h2>Help fund the development of Snort</h2>
|
||||
<p>
|
||||
Snort is an open source project built by passionate people in their free time
|
||||
</p>
|
||||
<p>
|
||||
Your donations are greatly appreciated
|
||||
</p>
|
||||
<p>
|
||||
Check out the code here: <a className="highlight" href="https://github.com/v0l/snort" rel="noreferrer" target="_blank">https://github.com/v0l/snort</a>
|
||||
</p>
|
||||
<p>
|
||||
Each contributor will get paid a percentage of all donations and NIP-05 orders, you can see the split amounts below
|
||||
</p>
|
||||
<div className="flex">
|
||||
<div className="mr10">Lightning Donation: </div>
|
||||
<ZapButton
|
||||
pubkey={bech32ToHex(SnortPubKey)}
|
||||
svc={"donate@snort.social"}
|
||||
/>
|
||||
</div>
|
||||
{today && (<small>Total today (UTC): {today.donations.toLocaleString()} sats</small>)}
|
||||
<h3>Primary Developers</h3>
|
||||
{Developers.map(a => <ProfilePreview pubkey={a} key={a} actions={actions(a)} />)}
|
||||
<h4>Contributors</h4>
|
||||
{Contributors.map(a => <ProfilePreview pubkey={a} key={a} actions={actions(a)} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadData().catch(console.warn);
|
||||
}, []);
|
||||
|
||||
function actions(pk: HexKey) {
|
||||
let split = splits.find((a) => bech32ToHex(a.pubKey) === pk);
|
||||
if (split) {
|
||||
return <>{(100 * split.split).toLocaleString()}%</>;
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-content m5">
|
||||
<h2>Help fund the development of Snort</h2>
|
||||
<p>
|
||||
Snort is an open source project built by passionate people in their free
|
||||
time
|
||||
</p>
|
||||
<p>Your donations are greatly appreciated</p>
|
||||
<p>
|
||||
Check out the code here:{" "}
|
||||
<a
|
||||
className="highlight"
|
||||
href="https://github.com/v0l/snort"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
https://github.com/v0l/snort
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
Each contributor will get paid a percentage of all donations and NIP-05
|
||||
orders, you can see the split amounts below
|
||||
</p>
|
||||
<div className="flex">
|
||||
<div className="mr10">Lightning Donation: </div>
|
||||
<ZapButton
|
||||
pubkey={bech32ToHex(SnortPubKey)}
|
||||
svc={"donate@snort.social"}
|
||||
/>
|
||||
</div>
|
||||
{today && (
|
||||
<small>
|
||||
Total today (UTC): {today.donations.toLocaleString()} sats
|
||||
</small>
|
||||
)}
|
||||
<h3>Primary Developers</h3>
|
||||
{Developers.map((a) => (
|
||||
<ProfilePreview pubkey={a} key={a} actions={actions(a)} />
|
||||
))}
|
||||
<h4>Contributors</h4>
|
||||
{Contributors.map((a) => (
|
||||
<ProfilePreview pubkey={a} key={a} actions={actions(a)} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DonatePage;
|
||||
|
@ -1,17 +1,15 @@
|
||||
import { useRouteError } from "react-router-dom";
|
||||
|
||||
const ErrorPage = () => {
|
||||
const error = useRouteError();
|
||||
const error = useRouteError();
|
||||
|
||||
console.error(error);
|
||||
return (
|
||||
<>
|
||||
<h4>An error has occured!</h4>
|
||||
<pre>
|
||||
{JSON.stringify(error, undefined, ' ')}
|
||||
</pre>
|
||||
</>
|
||||
);
|
||||
console.error(error);
|
||||
return (
|
||||
<>
|
||||
<h4>An error has occured!</h4>
|
||||
<pre>{JSON.stringify(error, undefined, " ")}</pre>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorPage;
|
||||
|
@ -4,9 +4,9 @@ import useThreadFeed from "Feed/ThreadFeed";
|
||||
import { parseId } from "Util";
|
||||
|
||||
export default function EventPage() {
|
||||
const params = useParams();
|
||||
const id = parseId(params.id!);
|
||||
const thread = useThreadFeed(id);
|
||||
const params = useParams();
|
||||
const id = parseId(params.id!);
|
||||
const thread = useThreadFeed(id);
|
||||
|
||||
return <Thread notes={thread.notes} this={id} />;
|
||||
}
|
||||
return <Thread notes={thread.notes} this={id} />;
|
||||
}
|
||||
|
@ -2,15 +2,20 @@ import { useParams } from "react-router-dom";
|
||||
import Timeline from "Element/Timeline";
|
||||
|
||||
const HashTagsPage = () => {
|
||||
const params = useParams();
|
||||
const tag = params.tag!.toLowerCase();
|
||||
const params = useParams();
|
||||
const tag = params.tag!.toLowerCase();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>#{tag}</h2>
|
||||
<Timeline key={tag} subject={{ type: "hashtag", items: [tag], discriminator: tag }} postsOnly={false} method={"TIME_RANGE"} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h2>#{tag}</h2>
|
||||
<Timeline
|
||||
key={tag}
|
||||
subject={{ type: "hashtag", items: [tag], discriminator: tag }}
|
||||
postsOnly={false}
|
||||
method={"TIME_RANGE"}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HashTagsPage;
|
||||
export default HashTagsPage;
|
||||
|
@ -2,12 +2,13 @@ import { Link } from "react-router-dom";
|
||||
import { KieranPubKey } from "Const";
|
||||
|
||||
export default function HelpPage() {
|
||||
return (
|
||||
<>
|
||||
<h2>NIP-05</h2>
|
||||
<p>
|
||||
If you have an enquiry about your NIP-05 order please DM <Link to={`/messages/${KieranPubKey}`}>Kieran</Link>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h2>NIP-05</h2>
|
||||
<p>
|
||||
If you have an enquiry about your NIP-05 order please DM{" "}
|
||||
<Link to={`/messages/${KieranPubKey}`}>Kieran</Link>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import "./Layout.css";
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import Envelope from "Icons/Envelope";
|
||||
@ -8,11 +8,11 @@ import Search from "Icons/Search";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import { init, setRelays } from "State/Login";
|
||||
import { System } from "Nostr/System"
|
||||
import { System } from "Nostr/System";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import useLoginFeed from "Feed/LoginFeed";
|
||||
import { totalUnread } from "Pages/MessagesPage";
|
||||
import { SearchRelays, SnortPubKey } from 'Const';
|
||||
import { SearchRelays, SnortPubKey } from "Const";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { IndexedUDB, useDb } from "State/Users/Db";
|
||||
@ -23,178 +23,229 @@ import Plus from "Icons/Plus";
|
||||
import { RelaySettings } from "Nostr/Connection";
|
||||
|
||||
export default function Layout() {
|
||||
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 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 usingDb = useDb();
|
||||
const pub = useEventPublisher();
|
||||
useLoginFeed();
|
||||
|
||||
const shouldHideNoteCreator = useMemo(() => {
|
||||
const hideNoteCreator = ["/settings", "/messages", "/new"]
|
||||
return hideNoteCreator.some(a => location.pathname.startsWith(a));
|
||||
}, [location]);
|
||||
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]);
|
||||
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.nip42Auth = pub.nip42Auth;
|
||||
}, [pub]);
|
||||
|
||||
useEffect(() => {
|
||||
System.UserDb = usingDb;
|
||||
}, [usingDb])
|
||||
useEffect(() => {
|
||||
System.UserDb = usingDb;
|
||||
}, [usingDb]);
|
||||
|
||||
useEffect(() => {
|
||||
if (relays) {
|
||||
for (let [k, v] of Object.entries(relays)) {
|
||||
System.ConnectToRelay(k, v);
|
||||
}
|
||||
for (let [k] of System.Sockets) {
|
||||
if (!relays[k] && !SearchRelays.has(k)) {
|
||||
System.DisconnectRelay(k);
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (relays) {
|
||||
for (let [k, v] of Object.entries(relays)) {
|
||||
System.ConnectToRelay(k, v);
|
||||
}
|
||||
for (let [k] of System.Sockets) {
|
||||
if (!relays[k] && !SearchRelays.has(k)) {
|
||||
System.DisconnectRelay(k);
|
||||
}
|
||||
}, [relays]);
|
||||
}
|
||||
}
|
||||
}, [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");
|
||||
}
|
||||
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(() => {
|
||||
let osTheme = window.matchMedia("(prefers-color-scheme: light)");
|
||||
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() {
|
||||
let newRelays: Record<string, RelaySettings> | undefined;
|
||||
|
||||
try {
|
||||
let rsp = await fetch("https://api.nostr.watch/v1/online");
|
||||
if (rsp.ok) {
|
||||
let online: string[] = await rsp.json();
|
||||
let pickRandom = online
|
||||
.sort((a, b) => (Math.random() >= 0.5 ? 1 : -1))
|
||||
.slice(0, 4); // pick 4 random relays
|
||||
|
||||
let relayObjects = pickRandom.map((a) => [
|
||||
a,
|
||||
{ read: true, write: true },
|
||||
]);
|
||||
newRelays = Object.fromEntries(relayObjects);
|
||||
dispatch(
|
||||
setRelays({
|
||||
relays: newRelays!,
|
||||
createdAt: 1,
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let osTheme = window.matchMedia("(prefers-color-scheme: light)");
|
||||
setTheme(preferences.theme === "system" && osTheme.matches ? "light" : preferences.theme === "light" ? "light" : "dark");
|
||||
const ev = await pub.addFollow(bech32ToHex(SnortPubKey), newRelays);
|
||||
pub.broadcast(ev);
|
||||
}
|
||||
|
||||
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() {
|
||||
let newRelays: Record<string, RelaySettings> | undefined;
|
||||
|
||||
try {
|
||||
let rsp = await fetch("https://api.nostr.watch/v1/online");
|
||||
if (rsp.ok) {
|
||||
let online: string[] = await rsp.json();
|
||||
let pickRandom = online.sort((a, b) => Math.random() >= 0.5 ? 1 : -1).slice(0, 4); // pick 4 random relays
|
||||
|
||||
let relayObjects = pickRandom.map(a => [a, { read: true, write: true }]);
|
||||
newRelays = Object.fromEntries(relayObjects);
|
||||
dispatch(setRelays({
|
||||
relays: newRelays!,
|
||||
createdAt: 1
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
const ev = await pub.addFollow(bech32ToHex(SnortPubKey), newRelays);
|
||||
pub.broadcast(ev);
|
||||
useEffect(() => {
|
||||
if (newUserKey === true) {
|
||||
handleNewUser().catch(console.warn);
|
||||
}
|
||||
}, [newUserKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (newUserKey === true) {
|
||||
handleNewUser().catch(console.warn);
|
||||
async function goToNotifications(e: any) {
|
||||
e.stopPropagation();
|
||||
// request permissions to send notifications
|
||||
if ("Notification" in window) {
|
||||
try {
|
||||
if (Notification.permission !== "granted") {
|
||||
let res = await Notification.requestPermission();
|
||||
console.debug(res);
|
||||
}
|
||||
}, [newUserKey]);
|
||||
|
||||
async function goToNotifications(e: any) {
|
||||
e.stopPropagation();
|
||||
// request permissions to send notifications
|
||||
if ("Notification" in window) {
|
||||
try {
|
||||
if (Notification.permission !== "granted") {
|
||||
let res = await Notification.requestPermission();
|
||||
console.debug(res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
navigate("/notifications");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
navigate("/notifications");
|
||||
}
|
||||
|
||||
function accountHeader() {
|
||||
return (
|
||||
<div className="header-actions">
|
||||
<div className="btn btn-rnd" onClick={(e) => navigate("/search")}>
|
||||
<Search />
|
||||
</div>
|
||||
<div className="btn btn-rnd" onClick={(e) => navigate("/messages")}>
|
||||
<Envelope />
|
||||
{unreadDms > 0 && (<span className="has-unread"></span>)}
|
||||
</div>
|
||||
<div className="btn btn-rnd" onClick={(e) => goToNotifications(e)}>
|
||||
<Bell />
|
||||
{hasNotifications && (<span className="has-unread"></span>)}
|
||||
</div>
|
||||
<ProfileImage pubkey={publicKey || ""} showUsername={false} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof loggedOut !== "boolean") {
|
||||
return null;
|
||||
}
|
||||
function accountHeader() {
|
||||
return (
|
||||
<div className="page">
|
||||
<header>
|
||||
<div className="logo" onClick={() => navigate("/")}>Snort</div>
|
||||
<div>
|
||||
{publicKey ? accountHeader() :
|
||||
<button type="button" onClick={() => navigate("/login")}>Login</button>
|
||||
}
|
||||
</div>
|
||||
</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 className="header-actions">
|
||||
<div className="btn btn-rnd" onClick={(e) => navigate("/search")}>
|
||||
<Search />
|
||||
</div>
|
||||
)
|
||||
<div className="btn btn-rnd" onClick={(e) => navigate("/messages")}>
|
||||
<Envelope />
|
||||
{unreadDms > 0 && <span className="has-unread"></span>}
|
||||
</div>
|
||||
<div className="btn btn-rnd" onClick={(e) => goToNotifications(e)}>
|
||||
<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")}>
|
||||
Login
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
@ -1,119 +1,142 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import * as secp from '@noble/secp256k1';
|
||||
import * as secp from "@noble/secp256k1";
|
||||
|
||||
import { RootState } from "State/Store";
|
||||
import { setPrivateKey, setPublicKey, setRelays, setGeneratedPrivateKey } from "State/Login";
|
||||
import {
|
||||
setPrivateKey,
|
||||
setPublicKey,
|
||||
setRelays,
|
||||
setGeneratedPrivateKey,
|
||||
} from "State/Login";
|
||||
import { DefaultRelays, EmailRegex } from "Const";
|
||||
import { bech32ToHex } from "Util";
|
||||
import { HexKey } from "Nostr";
|
||||
|
||||
export default function LoginPage() {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const publicKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const [key, setKey] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const publicKey = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.publicKey
|
||||
);
|
||||
const [key, setKey] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (publicKey) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [publicKey, navigate]);
|
||||
|
||||
async function getNip05PubKey(addr: string) {
|
||||
let [username, domain] = addr.split("@");
|
||||
let rsp = await fetch(`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(username)}`);
|
||||
if (rsp.ok) {
|
||||
let data = await rsp.json();
|
||||
let pKey = data.names[username];
|
||||
if (pKey) {
|
||||
return pKey;
|
||||
}
|
||||
}
|
||||
throw new Error("User key not found")
|
||||
useEffect(() => {
|
||||
if (publicKey) {
|
||||
navigate("/");
|
||||
}
|
||||
}, [publicKey, navigate]);
|
||||
|
||||
async function doLogin() {
|
||||
|
||||
try {
|
||||
if (key.startsWith("nsec")) {
|
||||
let hexKey = bech32ToHex(key);
|
||||
if (secp.utils.isValidPrivateKey(hexKey)) {
|
||||
dispatch(setPrivateKey(hexKey));
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
} else if (key.startsWith("npub")) {
|
||||
let hexKey = bech32ToHex(key);
|
||||
dispatch(setPublicKey(hexKey));
|
||||
} else if (key.match(EmailRegex)) {
|
||||
let hexKey = await getNip05PubKey(key);
|
||||
dispatch(setPublicKey(hexKey));
|
||||
} else {
|
||||
if (secp.utils.isValidPrivateKey(key)) {
|
||||
dispatch(setPrivateKey(key));
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setError(`Failed to load NIP-05 pub key (${e})`);
|
||||
console.error(e);
|
||||
}
|
||||
async function getNip05PubKey(addr: string) {
|
||||
let [username, domain] = addr.split("@");
|
||||
let rsp = await fetch(
|
||||
`https://${domain}/.well-known/nostr.json?name=${encodeURIComponent(
|
||||
username
|
||||
)}`
|
||||
);
|
||||
if (rsp.ok) {
|
||||
let data = await rsp.json();
|
||||
let pKey = data.names[username];
|
||||
if (pKey) {
|
||||
return pKey;
|
||||
}
|
||||
}
|
||||
throw new Error("User key not found");
|
||||
}
|
||||
|
||||
async function makeRandomKey() {
|
||||
let newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey());
|
||||
dispatch(setGeneratedPrivateKey(newKey));
|
||||
navigate("/new");
|
||||
}
|
||||
|
||||
async function doNip07Login() {
|
||||
let pubKey = await window.nostr.getPublicKey();
|
||||
dispatch(setPublicKey(pubKey));
|
||||
|
||||
if ("getRelays" in window.nostr) {
|
||||
let relays = await window.nostr.getRelays();
|
||||
dispatch(setRelays({
|
||||
relays: {
|
||||
...relays,
|
||||
...Object.fromEntries(DefaultRelays.entries())
|
||||
},
|
||||
createdAt: 1
|
||||
}));
|
||||
async function doLogin() {
|
||||
try {
|
||||
if (key.startsWith("nsec")) {
|
||||
let hexKey = bech32ToHex(key);
|
||||
if (secp.utils.isValidPrivateKey(hexKey)) {
|
||||
dispatch(setPrivateKey(hexKey));
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
}
|
||||
|
||||
function altLogins() {
|
||||
let nip07 = 'nostr' in window;
|
||||
if (!nip07) {
|
||||
return null;
|
||||
} else if (key.startsWith("npub")) {
|
||||
let hexKey = bech32ToHex(key);
|
||||
dispatch(setPublicKey(hexKey));
|
||||
} else if (key.match(EmailRegex)) {
|
||||
let hexKey = await getNip05PubKey(key);
|
||||
dispatch(setPublicKey(hexKey));
|
||||
} else {
|
||||
if (secp.utils.isValidPrivateKey(key)) {
|
||||
dispatch(setPrivateKey(key));
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
setError(`Failed to load NIP-05 pub key (${e})`);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Other Login Methods</h2>
|
||||
<div className="flex">
|
||||
<button type="button" onClick={(e) => doNip07Login()}>Login with Extension (NIP-07)</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
async function makeRandomKey() {
|
||||
let newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey());
|
||||
dispatch(setGeneratedPrivateKey(newKey));
|
||||
navigate("/new");
|
||||
}
|
||||
|
||||
async function doNip07Login() {
|
||||
let pubKey = await window.nostr.getPublicKey();
|
||||
dispatch(setPublicKey(pubKey));
|
||||
|
||||
if ("getRelays" in window.nostr) {
|
||||
let relays = await window.nostr.getRelays();
|
||||
dispatch(
|
||||
setRelays({
|
||||
relays: {
|
||||
...relays,
|
||||
...Object.fromEntries(DefaultRelays.entries()),
|
||||
},
|
||||
createdAt: 1,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function altLogins() {
|
||||
let nip07 = "nostr" in window;
|
||||
if (!nip07) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<h1>Login</h1>
|
||||
<div className="flex">
|
||||
<input type="text" placeholder="nsec / npub / nip-05 / hex private key..." className="f-grow" onChange={e => setKey(e.target.value)} />
|
||||
</div>
|
||||
{error.length > 0 ? <b className="error">{error}</b> : null}
|
||||
<div className="tabs">
|
||||
<button type="button" onClick={(e) => doLogin()}>Login</button>
|
||||
<button type="button" onClick={() => makeRandomKey()}>Generate Key</button>
|
||||
</div>
|
||||
{altLogins()}
|
||||
<>
|
||||
<h2>Other Login Methods</h2>
|
||||
<div className="flex">
|
||||
<button type="button" onClick={(e) => doNip07Login()}>
|
||||
Login with Extension (NIP-07)
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<h1>Login</h1>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="nsec / npub / nip-05 / hex private key..."
|
||||
className="f-grow"
|
||||
onChange={(e) => setKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{error.length > 0 ? <b className="error">{error}</b> : null}
|
||||
<div className="tabs">
|
||||
<button type="button" onClick={(e) => doLogin()}>
|
||||
Login
|
||||
</button>
|
||||
<button type="button" onClick={() => makeRandomKey()}>
|
||||
Generate Key
|
||||
</button>
|
||||
</div>
|
||||
{altLogins()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux"
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import { HexKey, RawEvent } from "Nostr";
|
||||
import UnreadCount from "Element/UnreadCount";
|
||||
@ -11,117 +11,150 @@ import NoteToSelf from "Element/NoteToSelf";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
|
||||
type DmChat = {
|
||||
pubkey: HexKey,
|
||||
unreadMessages: number,
|
||||
newestMessage: number
|
||||
}
|
||||
pubkey: HexKey;
|
||||
unreadMessages: number;
|
||||
newestMessage: number;
|
||||
};
|
||||
|
||||
export default function MessagesPage() {
|
||||
const dispatch = useDispatch();
|
||||
const myPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const dms = useSelector<RootState, RawEvent[]>(s => s.login.dms);
|
||||
const dmInteraction = useSelector<RootState, number>(s => s.login.dmInteraction);
|
||||
const { isMuted } = useModeration();
|
||||
const dispatch = useDispatch();
|
||||
const myPubKey = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.publicKey
|
||||
);
|
||||
const dms = useSelector<RootState, RawEvent[]>((s) => s.login.dms);
|
||||
const dmInteraction = useSelector<RootState, number>(
|
||||
(s) => s.login.dmInteraction
|
||||
);
|
||||
const { isMuted } = useModeration();
|
||||
|
||||
const chats = useMemo(() => {
|
||||
return extractChats(dms.filter(a => !isMuted(a.pubkey)), myPubKey!)
|
||||
}, [dms, myPubKey, dmInteraction]);
|
||||
|
||||
function noteToSelf(chat: DmChat) {
|
||||
return (
|
||||
<div className="flex mb10" key={chat.pubkey}>
|
||||
<NoteToSelf clickable={true} className="f-grow" link={`/messages/${hexToBech32("npub", chat.pubkey)}`} pubkey={chat.pubkey} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function person(chat: DmChat) {
|
||||
if(chat.pubkey === myPubKey) return noteToSelf(chat)
|
||||
return (
|
||||
<div className="flex mb10" key={chat.pubkey}>
|
||||
<ProfileImage pubkey={chat.pubkey} className="f-grow" link={`/messages/${hexToBech32("npub", chat.pubkey)}`} />
|
||||
<UnreadCount unread={chat.unreadMessages} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function markAllRead() {
|
||||
for (let c of chats) {
|
||||
setLastReadDm(c.pubkey);
|
||||
}
|
||||
dispatch(incDmInteraction());
|
||||
}
|
||||
const chats = useMemo(() => {
|
||||
return extractChats(
|
||||
dms.filter((a) => !isMuted(a.pubkey)),
|
||||
myPubKey!
|
||||
);
|
||||
}, [dms, myPubKey, dmInteraction]);
|
||||
|
||||
function noteToSelf(chat: DmChat) {
|
||||
return (
|
||||
<div className="main-content">
|
||||
<div className="flex">
|
||||
<h3 className="f-grow">Messages</h3>
|
||||
<button type="button" onClick={() => markAllRead()}>Mark All Read</button>
|
||||
</div>
|
||||
{chats.sort((a, b) => {
|
||||
return a.pubkey === myPubKey ? -1 :
|
||||
b.pubkey === myPubKey ? 1 :
|
||||
b.newestMessage - a.newestMessage
|
||||
}).map(person)}
|
||||
</div>
|
||||
)
|
||||
<div className="flex mb10" key={chat.pubkey}>
|
||||
<NoteToSelf
|
||||
clickable={true}
|
||||
className="f-grow"
|
||||
link={`/messages/${hexToBech32("npub", chat.pubkey)}`}
|
||||
pubkey={chat.pubkey}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function person(chat: DmChat) {
|
||||
if (chat.pubkey === myPubKey) return noteToSelf(chat);
|
||||
return (
|
||||
<div className="flex mb10" key={chat.pubkey}>
|
||||
<ProfileImage
|
||||
pubkey={chat.pubkey}
|
||||
className="f-grow"
|
||||
link={`/messages/${hexToBech32("npub", chat.pubkey)}`}
|
||||
/>
|
||||
<UnreadCount unread={chat.unreadMessages} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function markAllRead() {
|
||||
for (let c of chats) {
|
||||
setLastReadDm(c.pubkey);
|
||||
}
|
||||
dispatch(incDmInteraction());
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<div className="flex">
|
||||
<h3 className="f-grow">Messages</h3>
|
||||
<button type="button" onClick={() => markAllRead()}>
|
||||
Mark All Read
|
||||
</button>
|
||||
</div>
|
||||
{chats
|
||||
.sort((a, b) => {
|
||||
return a.pubkey === myPubKey
|
||||
? -1
|
||||
: b.pubkey === myPubKey
|
||||
? 1
|
||||
: b.newestMessage - a.newestMessage;
|
||||
})
|
||||
.map(person)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function lastReadDm(pk: HexKey) {
|
||||
let k = `dm:seen:${pk}`;
|
||||
return parseInt(window.localStorage.getItem(k) ?? "0");
|
||||
let k = `dm:seen:${pk}`;
|
||||
return parseInt(window.localStorage.getItem(k) ?? "0");
|
||||
}
|
||||
|
||||
export function setLastReadDm(pk: HexKey) {
|
||||
const now = Math.floor(new Date().getTime() / 1000);
|
||||
let current = lastReadDm(pk);
|
||||
if (current >= now) {
|
||||
return;
|
||||
}
|
||||
const now = Math.floor(new Date().getTime() / 1000);
|
||||
let current = lastReadDm(pk);
|
||||
if (current >= now) {
|
||||
return;
|
||||
}
|
||||
|
||||
let k = `dm:seen:${pk}`;
|
||||
window.localStorage.setItem(k, now.toString());
|
||||
let k = `dm:seen:${pk}`;
|
||||
window.localStorage.setItem(k, now.toString());
|
||||
}
|
||||
|
||||
export function dmTo(e: RawEvent) {
|
||||
let firstP = e.tags.find(b => b[0] === "p");
|
||||
return firstP ? firstP[1] : "";
|
||||
let firstP = e.tags.find((b) => b[0] === "p");
|
||||
return firstP ? firstP[1] : "";
|
||||
}
|
||||
|
||||
export function isToSelf(e: RawEvent, pk: HexKey) {
|
||||
return e.pubkey === pk && dmTo(e) === pk;
|
||||
return e.pubkey === pk && dmTo(e) === pk;
|
||||
}
|
||||
|
||||
export function dmsInChat(dms: RawEvent[], pk: HexKey) {
|
||||
return dms.filter(a => a.pubkey === pk || dmTo(a) === pk);
|
||||
return dms.filter((a) => a.pubkey === pk || dmTo(a) === pk);
|
||||
}
|
||||
|
||||
export function totalUnread(dms: RawEvent[], myPubKey: HexKey) {
|
||||
return extractChats(dms, myPubKey).reduce((acc, v) => acc += v.unreadMessages, 0);
|
||||
return extractChats(dms, myPubKey).reduce(
|
||||
(acc, v) => (acc += v.unreadMessages),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
function unreadDms(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) {
|
||||
if (pk === myPubKey) return 0;
|
||||
let lastRead = lastReadDm(pk);
|
||||
return dmsInChat(dms, pk).filter(a => a.created_at >= lastRead && a.pubkey !== myPubKey).length;
|
||||
if (pk === myPubKey) return 0;
|
||||
let lastRead = lastReadDm(pk);
|
||||
return dmsInChat(dms, pk).filter(
|
||||
(a) => a.created_at >= lastRead && a.pubkey !== myPubKey
|
||||
).length;
|
||||
}
|
||||
|
||||
function newestMessage(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) {
|
||||
if(pk === myPubKey) {
|
||||
return dmsInChat(dms.filter(d => isToSelf(d, myPubKey)), pk).reduce((acc, v) => acc = v.created_at > acc ? v.created_at : acc, 0);
|
||||
}
|
||||
if (pk === myPubKey) {
|
||||
return dmsInChat(
|
||||
dms.filter((d) => isToSelf(d, myPubKey)),
|
||||
pk
|
||||
).reduce((acc, v) => (acc = v.created_at > acc ? v.created_at : acc), 0);
|
||||
}
|
||||
|
||||
return dmsInChat(dms, pk).reduce((acc, v) => acc = v.created_at > acc ? v.created_at : acc, 0);
|
||||
return dmsInChat(dms, pk).reduce(
|
||||
(acc, v) => (acc = v.created_at > acc ? v.created_at : acc),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
export function extractChats(dms: RawEvent[], myPubKey: HexKey) {
|
||||
const keys = dms.map(a => [a.pubkey, dmTo(a)]).flat();
|
||||
const filteredKeys = Array.from(new Set<string>(keys));
|
||||
return filteredKeys.map(a => {
|
||||
return {
|
||||
pubkey: a,
|
||||
unreadMessages: unreadDms(dms, myPubKey, a),
|
||||
newestMessage: newestMessage(dms, myPubKey, a)
|
||||
} as DmChat;
|
||||
})
|
||||
const keys = dms.map((a) => [a.pubkey, dmTo(a)]).flat();
|
||||
const filteredKeys = Array.from(new Set<string>(keys));
|
||||
return filteredKeys.map((a) => {
|
||||
return {
|
||||
pubkey: a,
|
||||
unreadMessages: unreadDms(dms, myPubKey, a),
|
||||
newestMessage: newestMessage(dms, myPubKey, a),
|
||||
} as DmChat;
|
||||
});
|
||||
}
|
||||
|
@ -1,28 +1,33 @@
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux"
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { HexKey } from "Nostr";
|
||||
import { markNotificationsRead } from "State/Login";
|
||||
import { RootState } from "State/Store";
|
||||
import Timeline from "Element/Timeline";
|
||||
|
||||
export default function NotificationsPage() {
|
||||
const dispatch = useDispatch();
|
||||
const pubkey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const dispatch = useDispatch();
|
||||
const pubkey = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.publicKey
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(markNotificationsRead());
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
dispatch(markNotificationsRead());
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{pubkey && (
|
||||
<Timeline
|
||||
subject={{ type: "ptag", items: [pubkey!], discriminator: pubkey!.slice(0, 12) }}
|
||||
postsOnly={false}
|
||||
method={"TIME_RANGE"}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{pubkey && (
|
||||
<Timeline
|
||||
subject={{
|
||||
type: "ptag",
|
||||
items: [pubkey!],
|
||||
discriminator: pubkey!.slice(0, 12),
|
||||
}}
|
||||
postsOnly={false}
|
||||
method={"TIME_RANGE"}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -35,7 +35,6 @@
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 520px) {
|
||||
.profile .banner {
|
||||
width: 100%;
|
||||
@ -57,9 +56,8 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.profile p {
|
||||
white-space: pre-wrap;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.details-wrapper > .name > h2 {
|
||||
@ -101,28 +99,28 @@
|
||||
}
|
||||
|
||||
.profile .details p {
|
||||
word-break: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.profile .details a {
|
||||
color: var(--highlight);
|
||||
text-decoration: none;
|
||||
color: var(--highlight);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.profile .details a:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.profile .btn-icon {
|
||||
color: var(--font-color);
|
||||
padding: 6px;
|
||||
color: var(--font-color);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.profile .details-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: calc(100% - 32px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
|
||||
.profile .details .text {
|
||||
@ -154,7 +152,7 @@
|
||||
}
|
||||
|
||||
.profile .website a {
|
||||
text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.profile .website a:hover {
|
||||
@ -162,7 +160,7 @@
|
||||
}
|
||||
|
||||
.profile .lnurl {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.profile .ln-address {
|
||||
@ -172,12 +170,12 @@
|
||||
}
|
||||
|
||||
.profile .lnurl:hover {
|
||||
text-decoration: underline;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.profile .lnurl {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.profile .link-icon {
|
||||
|
@ -18,7 +18,7 @@ import { extractLnAddress, parseId, hexToBech32 } from "Util";
|
||||
import Avatar from "Element/Avatar";
|
||||
import LogoutButton from "Element/LogoutButton";
|
||||
import Timeline from "Element/Timeline";
|
||||
import Text from 'Element/Text'
|
||||
import Text from "Element/Text";
|
||||
import SendSats from "Element/SendSats";
|
||||
import Nip05 from "Element/Nip05";
|
||||
import Copy from "Element/Copy";
|
||||
@ -31,10 +31,10 @@ import FollowsList from "Element/FollowsList";
|
||||
import IconButton from "Element/IconButton";
|
||||
import { RootState } from "State/Store";
|
||||
import { HexKey } from "Nostr";
|
||||
import FollowsYou from "Element/FollowsYou"
|
||||
import FollowsYou from "Element/FollowsYou";
|
||||
import QrCode from "Element/QrCode";
|
||||
import Modal from "Element/Modal";
|
||||
import { ProxyImg } from "Element/ProxyImg"
|
||||
import { ProxyImg } from "Element/ProxyImg";
|
||||
import useHorizontalScroll from "Hooks/useHorizontalScroll";
|
||||
|
||||
const ProfileTab = {
|
||||
@ -45,34 +45,46 @@ const ProfileTab = {
|
||||
Zaps: { text: "Zaps", value: 4 },
|
||||
Muted: { text: "Muted", value: 5 },
|
||||
Blocked: { text: "Blocked", value: 6 },
|
||||
}
|
||||
};
|
||||
|
||||
export default function ProfilePage() {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const id = useMemo(() => parseId(params.id!), [params]);
|
||||
const user = useUserProfile(id);
|
||||
const loggedOut = useSelector<RootState, boolean | undefined>(s => s.login.loggedOut);
|
||||
const loginPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const follows = useSelector<RootState, HexKey[]>(s => s.login.follows);
|
||||
const loggedOut = useSelector<RootState, boolean | undefined>(
|
||||
(s) => s.login.loggedOut
|
||||
);
|
||||
const loginPubKey = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.publicKey
|
||||
);
|
||||
const follows = useSelector<RootState, HexKey[]>((s) => s.login.follows);
|
||||
const isMe = loginPubKey === id;
|
||||
const [showLnQr, setShowLnQr] = useState<boolean>(false);
|
||||
const [tab, setTab] = useState<Tab>(ProfileTab.Notes);
|
||||
const [showProfileQr, setShowProfileQr] = useState<boolean>(false);
|
||||
const aboutText = user?.about || ''
|
||||
const about = Text({ content: aboutText, tags: [], users: new Map(), creator: "" })
|
||||
const aboutText = user?.about || "";
|
||||
const about = Text({
|
||||
content: aboutText,
|
||||
tags: [],
|
||||
users: new Map(),
|
||||
creator: "",
|
||||
});
|
||||
const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || "");
|
||||
const website_url = (user?.website && !user.website.startsWith("http"))
|
||||
? "https://" + user.website
|
||||
: user?.website || "";
|
||||
const zapFeed = useZapsFeed(id)
|
||||
const website_url =
|
||||
user?.website && !user.website.startsWith("http")
|
||||
? "https://" + user.website
|
||||
: user?.website || "";
|
||||
const zapFeed = useZapsFeed(id);
|
||||
const zaps = useMemo(() => {
|
||||
const profileZaps = zapFeed.store.notes.map(parseZap).filter(z => z.valid && z.p === id && !z.e && z.zapper !== id)
|
||||
profileZaps.sort((a, b) => b.amount - a.amount)
|
||||
return profileZaps
|
||||
}, [zapFeed.store, id])
|
||||
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0)
|
||||
const horizontalScroll = useHorizontalScroll()
|
||||
const profileZaps = zapFeed.store.notes
|
||||
.map(parseZap)
|
||||
.filter((z) => z.valid && z.p === id && !z.e && z.zapper !== id);
|
||||
profileZaps.sort((a, b) => b.amount - a.amount);
|
||||
return profileZaps;
|
||||
}, [zapFeed.store, id]);
|
||||
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
|
||||
const horizontalScroll = useHorizontalScroll();
|
||||
|
||||
useEffect(() => {
|
||||
setTab(ProfileTab.Notes);
|
||||
@ -82,14 +94,14 @@ export default function ProfilePage() {
|
||||
return (
|
||||
<div className="name">
|
||||
<h2>
|
||||
{user?.display_name || user?.name || 'Nostrich'}
|
||||
{user?.display_name || user?.name || "Nostrich"}
|
||||
<FollowsYou pubkey={id} />
|
||||
</h2>
|
||||
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
|
||||
<Copy text={params.id || ""} />
|
||||
{links()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function links() {
|
||||
@ -100,7 +112,9 @@ export default function ProfilePage() {
|
||||
<span className="link-icon">
|
||||
<Link />
|
||||
</span>
|
||||
<a href={website_url} target="_blank" rel="noreferrer">{user.website}</a>
|
||||
<a href={website_url} target="_blank" rel="noreferrer">
|
||||
{user.website}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -121,35 +135,44 @@ export default function ProfilePage() {
|
||||
target={user?.display_name || user?.name}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function bio() {
|
||||
return aboutText.length > 0 && (
|
||||
<>
|
||||
<div className="details">
|
||||
{about}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
aboutText.length > 0 && (
|
||||
<>
|
||||
<div className="details">{about}</div>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function tabContent() {
|
||||
switch (tab) {
|
||||
case ProfileTab.Notes:
|
||||
return <Timeline
|
||||
key={id}
|
||||
subject={{ type: "pubkey", items: [id], discriminator: id.slice(0, 12) }}
|
||||
postsOnly={false}
|
||||
method={"TIME_RANGE"}
|
||||
ignoreModeration={true} />;
|
||||
return (
|
||||
<Timeline
|
||||
key={id}
|
||||
subject={{
|
||||
type: "pubkey",
|
||||
items: [id],
|
||||
discriminator: id.slice(0, 12),
|
||||
}}
|
||||
postsOnly={false}
|
||||
method={"TIME_RANGE"}
|
||||
ignoreModeration={true}
|
||||
/>
|
||||
);
|
||||
case ProfileTab.Zaps: {
|
||||
return (
|
||||
<div className="main-content">
|
||||
<h4 className="zaps-total">{formatShort(zapsTotal)} sats</h4>
|
||||
{zaps.map(z => <ZapElement showZapped={false} zap={z} />)}
|
||||
{zaps.map((z) => (
|
||||
<ZapElement showZapped={false} zap={z} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
case ProfileTab.Follows: {
|
||||
@ -157,7 +180,13 @@ export default function ProfilePage() {
|
||||
return (
|
||||
<div className="main-content">
|
||||
<h4>Following {follows.length}</h4>
|
||||
{follows.map(a => <ProfilePreview key={a} pubkey={a.toLowerCase()} options={{ about: false }} />)}
|
||||
{follows.map((a) => (
|
||||
<ProfilePreview
|
||||
key={a}
|
||||
pubkey={a.toLowerCase()}
|
||||
options={{ about: false }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
@ -165,13 +194,13 @@ export default function ProfilePage() {
|
||||
}
|
||||
}
|
||||
case ProfileTab.Followers: {
|
||||
return <FollowersList pubkey={id} />
|
||||
return <FollowersList pubkey={id} />;
|
||||
}
|
||||
case ProfileTab.Muted: {
|
||||
return isMe ? <BlockList variant="muted" /> : <MutedList pubkey={id} />
|
||||
return isMe ? <BlockList variant="muted" /> : <MutedList pubkey={id} />;
|
||||
}
|
||||
case ProfileTab.Blocked: {
|
||||
return isMe ? <BlockList variant="blocked" /> : null
|
||||
return isMe ? <BlockList variant="blocked" /> : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -181,7 +210,7 @@ export default function ProfilePage() {
|
||||
<div className="avatar-wrapper">
|
||||
<Avatar user={user} />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function renderIcons() {
|
||||
@ -193,7 +222,11 @@ export default function ProfilePage() {
|
||||
{showProfileQr && (
|
||||
<Modal className="qr-modal" onClose={() => setShowProfileQr(false)}>
|
||||
<ProfileImage pubkey={id} />
|
||||
<QrCode data={`nostr:${hexToBech32("npub", id)}`} link={undefined} className="m10" />
|
||||
<QrCode
|
||||
data={`nostr:${hexToBech32("npub", id)}`}
|
||||
link={undefined}
|
||||
className="m10"
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{isMe ? (
|
||||
@ -212,7 +245,11 @@ export default function ProfilePage() {
|
||||
)}
|
||||
{!loggedOut && (
|
||||
<>
|
||||
<IconButton onClick={() => navigate(`/messages/${hexToBech32("npub", id)}`)}>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
navigate(`/messages/${hexToBech32("npub", id)}`)
|
||||
}
|
||||
>
|
||||
<Envelope width={16} height={13} />
|
||||
</IconButton>
|
||||
</>
|
||||
@ -220,7 +257,7 @@ export default function ProfilePage() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function userDetails() {
|
||||
@ -233,28 +270,41 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
{bio()}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function renderTab(v: Tab) {
|
||||
return <TabElement t={v} tab={tab} setTab={setTab} />
|
||||
return <TabElement t={v} tab={tab} setTab={setTab} />;
|
||||
}
|
||||
|
||||
const w = window.document.querySelector(".page")?.clientWidth;
|
||||
return (
|
||||
<>
|
||||
<div className="profile flex">
|
||||
{user?.banner && <ProxyImg alt="banner" className="banner" src={user.banner} size={w} />}
|
||||
{user?.banner && (
|
||||
<ProxyImg
|
||||
alt="banner"
|
||||
className="banner"
|
||||
src={user.banner}
|
||||
size={w}
|
||||
/>
|
||||
)}
|
||||
<div className="profile-wrapper flex">
|
||||
{avatar()}
|
||||
{userDetails()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="tabs main-content" ref={horizontalScroll}>
|
||||
{[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows, ProfileTab.Zaps, ProfileTab.Muted].map(renderTab)}
|
||||
{[
|
||||
ProfileTab.Notes,
|
||||
ProfileTab.Followers,
|
||||
ProfileTab.Follows,
|
||||
ProfileTab.Zaps,
|
||||
ProfileTab.Muted,
|
||||
].map(renderTab)}
|
||||
{isMe && renderTab(ProfileTab.Blocked)}
|
||||
</div>
|
||||
{tabContent()}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -10,38 +10,52 @@ import { HexKey } from "Nostr";
|
||||
import { TimelineSubject } from "Feed/TimelineFeed";
|
||||
|
||||
const RootTab: Record<string, Tab> = {
|
||||
Posts: { text: 'Posts', value: 0, },
|
||||
PostsAndReplies: { text: 'Conversations', value: 1, },
|
||||
Global: { text: 'Global', value: 2 },
|
||||
Posts: { text: "Posts", value: 0 },
|
||||
PostsAndReplies: { text: "Conversations", value: 1 },
|
||||
Global: { text: "Global", value: 2 },
|
||||
};
|
||||
|
||||
export default function RootPage() {
|
||||
const [loggedOut, pubKey, follows] = useSelector<RootState, [boolean | undefined, HexKey | undefined, HexKey[]]>(s => [s.login.loggedOut, s.login.publicKey, s.login.follows]);
|
||||
const [tab, setTab] = useState<Tab>(RootTab.Posts);
|
||||
const [loggedOut, pubKey, follows] = useSelector<
|
||||
RootState,
|
||||
[boolean | undefined, HexKey | undefined, HexKey[]]
|
||||
>((s) => [s.login.loggedOut, s.login.publicKey, s.login.follows]);
|
||||
const [tab, setTab] = useState<Tab>(RootTab.Posts);
|
||||
|
||||
function followHints() {
|
||||
if (follows?.length === 0 && pubKey && tab !== RootTab.Global) {
|
||||
return <>
|
||||
Hmm nothing here.. Checkout <Link to={"/new"}>New users page</Link> to follow some recommended nostrich's!
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
const isGlobal = loggedOut || tab.value === RootTab.Global.value;
|
||||
const timelineSubect: TimelineSubject = isGlobal ? { type: "global", items: [], discriminator: "all" } : { type: "pubkey", items: follows, discriminator: "follows" };
|
||||
return (
|
||||
function followHints() {
|
||||
if (follows?.length === 0 && pubKey && tab !== RootTab.Global) {
|
||||
return (
|
||||
<>
|
||||
<div className="main-content">
|
||||
{pubKey && <Tabs tabs={[RootTab.Posts, RootTab.PostsAndReplies, RootTab.Global]} tab={tab} setTab={setTab} />}
|
||||
</div>
|
||||
{followHints()}
|
||||
<Timeline
|
||||
key={tab.value}
|
||||
subject={timelineSubect}
|
||||
postsOnly={tab.value === RootTab.Posts.value}
|
||||
method={"TIME_RANGE"}
|
||||
window={tab.value === RootTab.Global.value ? 60 : undefined}
|
||||
/>
|
||||
Hmm nothing here.. Checkout <Link to={"/new"}>New users page</Link> to
|
||||
follow some recommended nostrich's!
|
||||
</>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const isGlobal = loggedOut || tab.value === RootTab.Global.value;
|
||||
const timelineSubect: TimelineSubject = isGlobal
|
||||
? { type: "global", items: [], discriminator: "all" }
|
||||
: { type: "pubkey", items: follows, discriminator: "follows" };
|
||||
return (
|
||||
<>
|
||||
<div className="main-content">
|
||||
{pubKey && (
|
||||
<Tabs
|
||||
tabs={[RootTab.Posts, RootTab.PostsAndReplies, RootTab.Global]}
|
||||
tab={tab}
|
||||
setTab={setTab}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{followHints()}
|
||||
<Timeline
|
||||
key={tab.value}
|
||||
subject={timelineSubect}
|
||||
postsOnly={tab.value === RootTab.Posts.value}
|
||||
method={"TIME_RANGE"}
|
||||
window={tab.value === RootTab.Global.value ? 60 : undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -7,49 +7,62 @@ import { SearchRelays } from "Const";
|
||||
import { System } from "Nostr/System";
|
||||
|
||||
const SearchPage = () => {
|
||||
const params: any = useParams();
|
||||
const [search, setSearch] = useState<string>();
|
||||
const [keyword, setKeyword] = useState<string | undefined>(params.keyword);
|
||||
const params: any = useParams();
|
||||
const [search, setSearch] = useState<string>();
|
||||
const [keyword, setKeyword] = useState<string | undefined>(params.keyword);
|
||||
|
||||
useEffect(() => {
|
||||
if (keyword) {
|
||||
// "navigate" changing only url
|
||||
router.navigate(`/search/${encodeURIComponent(keyword)}`)
|
||||
}
|
||||
}, [keyword]);
|
||||
useEffect(() => {
|
||||
if (keyword) {
|
||||
// "navigate" changing only url
|
||||
router.navigate(`/search/${encodeURIComponent(keyword)}`);
|
||||
}
|
||||
}, [keyword]);
|
||||
|
||||
useEffect(() => {
|
||||
return debounce(500, () => setKeyword(search));
|
||||
}, [search]);
|
||||
useEffect(() => {
|
||||
return debounce(500, () => setKeyword(search));
|
||||
}, [search]);
|
||||
|
||||
useEffect(() => {
|
||||
let addedRelays: string[] = [];
|
||||
for (let [k, v] of SearchRelays) {
|
||||
if (!System.Sockets.has(k)) {
|
||||
System.ConnectToRelay(k, v);
|
||||
addedRelays.push(k);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
for (let r of addedRelays) {
|
||||
System.DisconnectRelay(r);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
let addedRelays: string[] = [];
|
||||
for (let [k, v] of SearchRelays) {
|
||||
if (!System.Sockets.has(k)) {
|
||||
System.ConnectToRelay(k, v);
|
||||
addedRelays.push(k);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
for (let r of addedRelays) {
|
||||
System.DisconnectRelay(r);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<h2>Search</h2>
|
||||
<div className="flex mb10">
|
||||
<input type="text" className="f-grow mr10" placeholder="Search.." value={search} onChange={e => setSearch(e.target.value)} />
|
||||
</div>
|
||||
{keyword && <Timeline
|
||||
key={keyword}
|
||||
subject={{ type: "keyword", items: [keyword], discriminator: keyword }}
|
||||
postsOnly={false}
|
||||
method={"TIME_RANGE"} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="main-content">
|
||||
<h2>Search</h2>
|
||||
<div className="flex mb10">
|
||||
<input
|
||||
type="text"
|
||||
className="f-grow mr10"
|
||||
placeholder="Search.."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{keyword && (
|
||||
<Timeline
|
||||
key={keyword}
|
||||
subject={{
|
||||
type: "keyword",
|
||||
items: [keyword],
|
||||
discriminator: keyword,
|
||||
}}
|
||||
postsOnly={false}
|
||||
method={"TIME_RANGE"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchPage;
|
||||
|
@ -6,35 +6,37 @@ import Preferences from "Pages/settings/Preferences";
|
||||
import RelayInfo from "Pages/settings/RelayInfo";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="main-content">
|
||||
<h2 onClick={() => navigate("/settings")} className="pointer">Settings</h2>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div className="main-content">
|
||||
<h2 onClick={() => navigate("/settings")} className="pointer">
|
||||
Settings
|
||||
</h2>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const SettingsRoutes: RouteObject[] = [
|
||||
{
|
||||
path: "",
|
||||
element: <SettingsIndex />
|
||||
},
|
||||
{
|
||||
path: "profile",
|
||||
element: <Profile />
|
||||
},
|
||||
{
|
||||
path: "relays",
|
||||
element: <Relay />,
|
||||
},
|
||||
{
|
||||
path: "relays/:id",
|
||||
element: <RelayInfo />
|
||||
},
|
||||
{
|
||||
path: "preferences",
|
||||
element: <Preferences />
|
||||
}
|
||||
]
|
||||
{
|
||||
path: "",
|
||||
element: <SettingsIndex />,
|
||||
},
|
||||
{
|
||||
path: "profile",
|
||||
element: <Profile />,
|
||||
},
|
||||
{
|
||||
path: "relays",
|
||||
element: <Relay />,
|
||||
},
|
||||
{
|
||||
path: "relays/:id",
|
||||
element: <RelayInfo />,
|
||||
},
|
||||
{
|
||||
path: "preferences",
|
||||
element: <Preferences />,
|
||||
},
|
||||
];
|
||||
|
@ -1,3 +1,3 @@
|
||||
.verification a {
|
||||
color: var(--highlight);
|
||||
color: var(--highlight);
|
||||
}
|
||||
|
@ -1,42 +1,57 @@
|
||||
import { ApiHost } from "Const";
|
||||
import Nip5Service from "Element/Nip5Service";
|
||||
|
||||
import './Verification.css'
|
||||
import "./Verification.css";
|
||||
|
||||
export default function VerificationPage() {
|
||||
const services = [
|
||||
{
|
||||
name: "Snort",
|
||||
service: `${ApiHost}/api/v1/n5sp`,
|
||||
link: "https://snort.social/",
|
||||
supportLink: "https://snort.social/help",
|
||||
about: <>Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!</>
|
||||
},
|
||||
{
|
||||
name: "Nostr Plebs",
|
||||
service: "https://nostrplebs.com/api/v1",
|
||||
link: "https://nostrplebs.com/",
|
||||
supportLink: "https://nostrplebs.com/manage",
|
||||
about: <>
|
||||
<p>Nostr Plebs is one of the first NIP-05 providers in the space and offers a good collection of domains at reasonable prices</p>
|
||||
</>
|
||||
}
|
||||
];
|
||||
const services = [
|
||||
{
|
||||
name: "Snort",
|
||||
service: `${ApiHost}/api/v1/n5sp`,
|
||||
link: "https://snort.social/",
|
||||
supportLink: "https://snort.social/help",
|
||||
about: (
|
||||
<>
|
||||
Our very own NIP-05 verification service, help support the development
|
||||
of this site and get a shiny special badge on our site!
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Nostr Plebs",
|
||||
service: "https://nostrplebs.com/api/v1",
|
||||
link: "https://nostrplebs.com/",
|
||||
supportLink: "https://nostrplebs.com/manage",
|
||||
about: (
|
||||
<>
|
||||
<p>
|
||||
Nostr Plebs is one of the first NIP-05 providers in the space and
|
||||
offers a good collection of domains at reasonable prices
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="main-content verification">
|
||||
<h2>Get Verified</h2>
|
||||
<p>
|
||||
NIP-05 is a DNS based verification spec which helps to validate you as a real user.
|
||||
</p>
|
||||
<p>Getting NIP-05 verified can help:</p>
|
||||
<ul>
|
||||
<li>Prevent fake accounts from imitating you</li>
|
||||
<li>Make your profile easier to find and share</li>
|
||||
<li>Fund developers and platforms providing NIP-05 verification services</li>
|
||||
</ul>
|
||||
return (
|
||||
<div className="main-content verification">
|
||||
<h2>Get Verified</h2>
|
||||
<p>
|
||||
NIP-05 is a DNS based verification spec which helps to validate you as a
|
||||
real user.
|
||||
</p>
|
||||
<p>Getting NIP-05 verified can help:</p>
|
||||
<ul>
|
||||
<li>Prevent fake accounts from imitating you</li>
|
||||
<li>Make your profile easier to find and share</li>
|
||||
<li>
|
||||
Fund developers and platforms providing NIP-05 verification services
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{services.map(a => <Nip5Service key={a.name} {...a} />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{services.map((a) => (
|
||||
<Nip5Service key={a.name} {...a} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -4,23 +4,20 @@ import { useMemo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function DiscoverFollows() {
|
||||
const navigate = useNavigate();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const sortedReccomends = useMemo(() => {
|
||||
return RecommendedFollows
|
||||
.sort(a => Math.random() >= 0.5 ? -1 : 1);
|
||||
}, []);
|
||||
const sortedReccomends = useMemo(() => {
|
||||
return RecommendedFollows.sort((a) => (Math.random() >= 0.5 ? -1 : 1));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Follow some popular accounts</h2>
|
||||
<button onClick={() => navigate("/")}>
|
||||
Skip
|
||||
</button>
|
||||
{sortedReccomends.length > 0 && (<FollowListBase pubkeys={sortedReccomends} />)}
|
||||
<button onClick={() => navigate("/")}>
|
||||
Done!
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<h2>Follow some popular accounts</h2>
|
||||
<button onClick={() => navigate("/")}>Skip</button>
|
||||
{sortedReccomends.length > 0 && (
|
||||
<FollowListBase pubkeys={sortedReccomends} />
|
||||
)}
|
||||
<button onClick={() => navigate("/")}>Done!</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -11,57 +11,67 @@ import { useNavigate } from "react-router-dom";
|
||||
const TwitterFollowsApi = `${ApiHost}/api/v1/twitter/follows-for-nostr`;
|
||||
|
||||
export default function ImportFollows() {
|
||||
const navigate = useNavigate();
|
||||
const currentFollows = useSelector((s: RootState) => s.login.follows);
|
||||
const [twitterUsername, setTwitterUsername] = useState<string>("");
|
||||
const [follows, setFollows] = useState<string[]>([]);
|
||||
const [error, setError] = useState<string>("");
|
||||
const navigate = useNavigate();
|
||||
const currentFollows = useSelector((s: RootState) => s.login.follows);
|
||||
const [twitterUsername, setTwitterUsername] = useState<string>("");
|
||||
const [follows, setFollows] = useState<string[]>([]);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const sortedTwitterFollows = useMemo(() => {
|
||||
return follows
|
||||
.map((a) => bech32ToHex(a))
|
||||
.sort((a, b) => (currentFollows.includes(a) ? 1 : -1));
|
||||
}, [follows, currentFollows]);
|
||||
|
||||
const sortedTwitterFollows = useMemo(() => {
|
||||
return follows.map(a => bech32ToHex(a))
|
||||
.sort((a, b) => currentFollows.includes(a) ? 1 : -1);
|
||||
}, [follows, currentFollows]);
|
||||
|
||||
async function loadFollows() {
|
||||
setFollows([]);
|
||||
setError("");
|
||||
try {
|
||||
let rsp = await fetch(`${TwitterFollowsApi}?username=${twitterUsername}`);
|
||||
let data = await rsp.json();
|
||||
if (rsp.ok) {
|
||||
if (Array.isArray(data) && data.length === 0) {
|
||||
setError(`No nostr users found for "${twitterUsername}"`);
|
||||
} else {
|
||||
setFollows(data);
|
||||
}
|
||||
} else if ("error" in data) {
|
||||
setError(data.error);
|
||||
} else {
|
||||
setError("Failed to load follows, please try again later");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
setError("Failed to load follows, please try again later");
|
||||
async function loadFollows() {
|
||||
setFollows([]);
|
||||
setError("");
|
||||
try {
|
||||
let rsp = await fetch(`${TwitterFollowsApi}?username=${twitterUsername}`);
|
||||
let data = await rsp.json();
|
||||
if (rsp.ok) {
|
||||
if (Array.isArray(data) && data.length === 0) {
|
||||
setError(`No nostr users found for "${twitterUsername}"`);
|
||||
} else {
|
||||
setFollows(data);
|
||||
}
|
||||
} else if ("error" in data) {
|
||||
setError(data.error);
|
||||
} else {
|
||||
setError("Failed to load follows, please try again later");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
setError("Failed to load follows, please try again later");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>Import Twitter Follows</h2>
|
||||
<p>
|
||||
Find your twitter follows on nostr (Data provided by <a href="https://nostr.directory" target="_blank" rel="noreferrer">nostr.directory</a>)
|
||||
</p>
|
||||
<div className="flex">
|
||||
<input type="text" placeholder="Twitter username.." className="f-grow mr10" value={twitterUsername} onChange={e => setTwitterUsername(e.target.value)} />
|
||||
<AsyncButton onClick={loadFollows}>Check</AsyncButton>
|
||||
</div>
|
||||
{error.length > 0 && <b className="error">{error}</b>}
|
||||
{sortedTwitterFollows.length > 0 && (<FollowListBase pubkeys={sortedTwitterFollows} />)}
|
||||
return (
|
||||
<>
|
||||
<h2>Import Twitter Follows</h2>
|
||||
<p>
|
||||
Find your twitter follows on nostr (Data provided by{" "}
|
||||
<a href="https://nostr.directory" target="_blank" rel="noreferrer">
|
||||
nostr.directory
|
||||
</a>
|
||||
)
|
||||
</p>
|
||||
<div className="flex">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Twitter username.."
|
||||
className="f-grow mr10"
|
||||
value={twitterUsername}
|
||||
onChange={(e) => setTwitterUsername(e.target.value)}
|
||||
/>
|
||||
<AsyncButton onClick={loadFollows}>Check</AsyncButton>
|
||||
</div>
|
||||
{error.length > 0 && <b className="error">{error}</b>}
|
||||
{sortedTwitterFollows.length > 0 && (
|
||||
<FollowListBase pubkeys={sortedTwitterFollows} />
|
||||
)}
|
||||
|
||||
<button onClick={() => navigate("/new/discover")}>
|
||||
Next
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<button onClick={() => navigate("/new/discover")}>Next</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -2,14 +2,12 @@ import ProfileSettings from "Pages/settings/Profile";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function NewUserProfile() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<h1>Setup your Profile</h1>
|
||||
<ProfileSettings privateKey={false} banner={false} />
|
||||
<button onClick={() => navigate("/new/import")}>
|
||||
Next
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<>
|
||||
<h1>Setup your Profile</h1>
|
||||
<ProfileSettings privateKey={false} banner={false} />
|
||||
<button onClick={() => navigate("/new/import")}>Next</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -9,78 +9,88 @@ import ImportFollows from "Pages/new/ImportFollows";
|
||||
import DiscoverFollows from "Pages/new/DiscoverFollows";
|
||||
|
||||
export const NewUserRoutes: RouteObject[] = [
|
||||
{
|
||||
path: "/new",
|
||||
element: <NewUserFlow />
|
||||
},
|
||||
{
|
||||
path: "/new/profile",
|
||||
element: <NewUserProfile />
|
||||
},
|
||||
{
|
||||
path: "/new/import",
|
||||
element: <ImportFollows />
|
||||
},
|
||||
{
|
||||
path: "/new/discover",
|
||||
element: <DiscoverFollows />
|
||||
}
|
||||
{
|
||||
path: "/new",
|
||||
element: <NewUserFlow />,
|
||||
},
|
||||
{
|
||||
path: "/new/profile",
|
||||
element: <NewUserProfile />,
|
||||
},
|
||||
{
|
||||
path: "/new/import",
|
||||
element: <ImportFollows />,
|
||||
},
|
||||
{
|
||||
path: "/new/discover",
|
||||
element: <DiscoverFollows />,
|
||||
},
|
||||
];
|
||||
|
||||
export default function NewUserFlow() {
|
||||
const { privateKey } = useSelector((s: RootState) => s.login);
|
||||
const navigate = useNavigate();
|
||||
const { privateKey } = useSelector((s: RootState) => s.login);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>Welcome to Snort!</h1>
|
||||
<p>
|
||||
Snort is a Nostr UI, nostr is a decentralised protocol for saving and distributing "notes".
|
||||
</p>
|
||||
<p>
|
||||
Notes hold text content, the most popular usage of these notes is to store "tweet like" messages.
|
||||
</p>
|
||||
<p>
|
||||
Snort is designed to have a similar experience to Twitter.
|
||||
</p>
|
||||
return (
|
||||
<>
|
||||
<h1>Welcome to Snort!</h1>
|
||||
<p>
|
||||
Snort is a Nostr UI, nostr is a decentralised protocol for saving and
|
||||
distributing "notes".
|
||||
</p>
|
||||
<p>
|
||||
Notes hold text content, the most popular usage of these notes is to
|
||||
store "tweet like" messages.
|
||||
</p>
|
||||
<p>Snort is designed to have a similar experience to Twitter.</p>
|
||||
|
||||
<h2>Keys</h2>
|
||||
<p>
|
||||
Nostr uses digital signature technology to provide tamper proof notes which can safely
|
||||
be replicated to many relays to provide redundant storage of your content.
|
||||
</p>
|
||||
<p>
|
||||
This means that nobody can modify notes which you have created
|
||||
and everybody can easily verify that the notes they are reading are created by you.
|
||||
</p>
|
||||
<p>
|
||||
This is the same technology which is used by Bitcoin and has been proven to be extremely secure.
|
||||
</p>
|
||||
<h2>Keys</h2>
|
||||
<p>
|
||||
Nostr uses digital signature technology to provide tamper proof notes
|
||||
which can safely be replicated to many relays to provide redundant
|
||||
storage of your content.
|
||||
</p>
|
||||
<p>
|
||||
This means that nobody can modify notes which you have created and
|
||||
everybody can easily verify that the notes they are reading are created
|
||||
by you.
|
||||
</p>
|
||||
<p>
|
||||
This is the same technology which is used by Bitcoin and has been proven
|
||||
to be extremely secure.
|
||||
</p>
|
||||
|
||||
<h2>Your Key</h2>
|
||||
<p>
|
||||
When you want to author new notes you need to sign them with your private key,
|
||||
as with Bitcoin private keys these need to be kept secure.
|
||||
</p>
|
||||
<p>
|
||||
Please now copy your private key and save it somewhere secure:
|
||||
</p>
|
||||
<div className="card">
|
||||
<Copy text={hexToBech32("nsec", privateKey ?? "")} />
|
||||
</div>
|
||||
<p>
|
||||
It is also recommended to use one of the following browser extensions if you are on a desktop computer to secure your key:
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="https://getalby.com/" target="_blank" rel="noreferrer">Alby</a></li>
|
||||
<li><a href="https://github.com/fiatjaf/nos2x" target="_blank" rel="noreferrer">nos2x</a></li>
|
||||
</ul>
|
||||
<p>
|
||||
You can also use these extensions to login to most Nostr sites.
|
||||
</p>
|
||||
<button onClick={() => navigate("/new/profile")}>
|
||||
Next
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<h2>Your Key</h2>
|
||||
<p>
|
||||
When you want to author new notes you need to sign them with your
|
||||
private key, as with Bitcoin private keys these need to be kept secure.
|
||||
</p>
|
||||
<p>Please now copy your private key and save it somewhere secure:</p>
|
||||
<div className="card">
|
||||
<Copy text={hexToBech32("nsec", privateKey ?? "")} />
|
||||
</div>
|
||||
<p>
|
||||
It is also recommended to use one of the following browser extensions if
|
||||
you are on a desktop computer to secure your key:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://getalby.com/" target="_blank" rel="noreferrer">
|
||||
Alby
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://github.com/fiatjaf/nos2x"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
nos2x
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<p>You can also use these extensions to login to most Nostr sites.</p>
|
||||
<button onClick={() => navigate("/new/profile")}>Next</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
.settings-nav .card {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.settings-row {
|
||||
@ -9,7 +9,7 @@
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
line-height: 19px;
|
||||
padding: 12px 16px;
|
||||
padding: 12px 16px;
|
||||
background: var(--note-bg);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
|
@ -12,67 +12,65 @@ import Logout from "Icons/Logout";
|
||||
import { logout } from "State/Login";
|
||||
|
||||
const SettingsIndex = () => {
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
|
||||
function handleLogout() {
|
||||
dispatch(logout())
|
||||
navigate("/")
|
||||
}
|
||||
function handleLogout() {
|
||||
dispatch(logout());
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="settings-nav">
|
||||
<div className="settings-row" onClick={() => navigate("profile")}>
|
||||
<div className="mr10">
|
||||
<Profile />
|
||||
</div>
|
||||
<span>
|
||||
Profile
|
||||
</span>
|
||||
<div className="align-end">
|
||||
<ArrowFront />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("relays")}>
|
||||
<div className="mr10">
|
||||
<Relay />
|
||||
</div>
|
||||
Relays
|
||||
<div className="align-end">
|
||||
<ArrowFront />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("preferences")}>
|
||||
<div className="mr10">
|
||||
<Gear />
|
||||
</div>
|
||||
Preferences
|
||||
<div className="align-end">
|
||||
<ArrowFront />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("/donate")}>
|
||||
<div className="mr10">
|
||||
<Heart />
|
||||
</div>
|
||||
Donate
|
||||
<div className="align-end">
|
||||
<ArrowFront />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-row" onClick={handleLogout}>
|
||||
<div className="mr10">
|
||||
<Logout />
|
||||
</div>
|
||||
Log Out
|
||||
<div className="align-end">
|
||||
<ArrowFront />
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<div className="settings-nav">
|
||||
<div className="settings-row" onClick={() => navigate("profile")}>
|
||||
<div className="mr10">
|
||||
<Profile />
|
||||
</div>
|
||||
<span>Profile</span>
|
||||
<div className="align-end">
|
||||
<ArrowFront />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className="settings-row" onClick={() => navigate("relays")}>
|
||||
<div className="mr10">
|
||||
<Relay />
|
||||
</div>
|
||||
Relays
|
||||
<div className="align-end">
|
||||
<ArrowFront />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("preferences")}>
|
||||
<div className="mr10">
|
||||
<Gear />
|
||||
</div>
|
||||
Preferences
|
||||
<div className="align-end">
|
||||
<ArrowFront />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-row" onClick={() => navigate("/donate")}>
|
||||
<div className="mr10">
|
||||
<Heart />
|
||||
</div>
|
||||
Donate
|
||||
<div className="align-end">
|
||||
<ArrowFront />
|
||||
</div>
|
||||
</div>
|
||||
<div className="settings-row" onClick={handleLogout}>
|
||||
<div className="mr10">
|
||||
<Logout />
|
||||
</div>
|
||||
Log Out
|
||||
<div className="align-end">
|
||||
<ArrowFront />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsIndex;
|
||||
|
@ -1,8 +1,8 @@
|
||||
.preferences small {
|
||||
margin-top: 0.5em;
|
||||
color: var(--font-secondary-color);
|
||||
margin-top: 0.5em;
|
||||
color: var(--font-secondary-color);
|
||||
}
|
||||
|
||||
.preferences select {
|
||||
min-width: 100px;
|
||||
}
|
||||
min-width: 100px;
|
||||
}
|
||||
|
@ -4,125 +4,254 @@ import { RootState } from "State/Store";
|
||||
import "./Preferences.css";
|
||||
|
||||
const PreferencesPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const perf = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
const dispatch = useDispatch();
|
||||
const perf = useSelector<RootState, UserPreferences>(
|
||||
(s) => s.login.preferences
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="preferences">
|
||||
<h3>Preferences</h3>
|
||||
return (
|
||||
<div className="preferences">
|
||||
<h3>Preferences</h3>
|
||||
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Theme</div>
|
||||
</div>
|
||||
<div>
|
||||
<select value={perf.theme} onChange={e => dispatch(setPreferences({ ...perf, theme: e.target.value } as UserPreferences))}>
|
||||
<option value="system">System (Default)</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Automatically load media</div>
|
||||
<small>Media in posts will automatically be shown for selected people, otherwise only the link will show</small>
|
||||
</div>
|
||||
<div>
|
||||
<select value={perf.autoLoadMedia} onChange={e => dispatch(setPreferences({ ...perf, autoLoadMedia: e.target.value } as UserPreferences))}>
|
||||
<option value="none">None</option>
|
||||
<option value="follows-only">Follows only</option>
|
||||
<option value="all">All</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex f-col">
|
||||
<div className="flex w-max">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Image proxy service</div>
|
||||
<small>Use imgproxy to compress images</small>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" checked={perf.imgProxyConfig !== null} onChange={e => dispatch(setPreferences({ ...perf, imgProxyConfig: e.target.checked ? DefaultImgProxy : null }))} />
|
||||
</div>
|
||||
</div>
|
||||
{perf.imgProxyConfig && (<div className="w-max mt10 form">
|
||||
<div className="form-group">
|
||||
<div>
|
||||
Service Url
|
||||
</div>
|
||||
<div className="w-max">
|
||||
<input type="text" value={perf.imgProxyConfig?.url} placeholder="Url.." onChange={e => dispatch(setPreferences({ ...perf, imgProxyConfig: { ...perf.imgProxyConfig!, url: e.target.value } }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>
|
||||
Service Key
|
||||
</div>
|
||||
<div className="w-max">
|
||||
<input type="password" value={perf.imgProxyConfig?.key} placeholder="Hex key.." onChange={e => dispatch(setPreferences({ ...perf, imgProxyConfig: { ...perf.imgProxyConfig!, key: e.target.value } }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>
|
||||
Service Salt
|
||||
</div>
|
||||
<div className="w-max">
|
||||
<input type="password" value={perf.imgProxyConfig?.salt} placeholder="Hex salt.." onChange={e => dispatch(setPreferences({ ...perf, imgProxyConfig: { ...perf.imgProxyConfig!, salt: e.target.value } }))} />
|
||||
</div>
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Enable reactions</div>
|
||||
<small>Reactions will be shown on every page, if disabled no reactions will be shown</small>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" checked={perf.enableReactions} onChange={e => dispatch(setPreferences({ ...perf, enableReactions: e.target.checked }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Confirm reposts</div>
|
||||
<small>Reposts need to be manually confirmed</small>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" checked={perf.confirmReposts} onChange={e => dispatch(setPreferences({ ...perf, confirmReposts: e.target.checked }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Automatically show latest notes</div>
|
||||
<small>Notes will stream in real time into global and posts tab</small>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" checked={perf.autoShowLatest} onChange={e => dispatch(setPreferences({ ...perf, autoShowLatest: e.target.checked }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>File upload service</div>
|
||||
<small>Pick which upload service you want to upload attachments to</small>
|
||||
</div>
|
||||
<div>
|
||||
<select value={perf.fileUploader} onChange={e => dispatch(setPreferences({ ...perf, fileUploader: e.target.value } as UserPreferences))}>
|
||||
<option value="void.cat">void.cat (Default)</option>
|
||||
<option value="nostr.build">nostr.build</option>
|
||||
<option value="nostrimg.com">nostrimg.com</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Debug Menus</div>
|
||||
<small>Shows "Copy ID" and "Copy Event JSON" in the context menu on each message</small>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" checked={perf.showDebugMenus} onChange={e => dispatch(setPreferences({ ...perf, showDebugMenus: e.target.checked }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Theme</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default PreferencesPage;
|
||||
<div>
|
||||
<select
|
||||
value={perf.theme}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
...perf,
|
||||
theme: e.target.value,
|
||||
} as UserPreferences)
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="system">System (Default)</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Automatically load media</div>
|
||||
<small>
|
||||
Media in posts will automatically be shown for selected people,
|
||||
otherwise only the link will show
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
value={perf.autoLoadMedia}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
...perf,
|
||||
autoLoadMedia: e.target.value,
|
||||
} as UserPreferences)
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="none">None</option>
|
||||
<option value="follows-only">Follows only</option>
|
||||
<option value="all">All</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex f-col">
|
||||
<div className="flex w-max">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Image proxy service</div>
|
||||
<small>Use imgproxy to compress images</small>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perf.imgProxyConfig !== null}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
...perf,
|
||||
imgProxyConfig: e.target.checked ? DefaultImgProxy : null,
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{perf.imgProxyConfig && (
|
||||
<div className="w-max mt10 form">
|
||||
<div className="form-group">
|
||||
<div>Service Url</div>
|
||||
<div className="w-max">
|
||||
<input
|
||||
type="text"
|
||||
value={perf.imgProxyConfig?.url}
|
||||
placeholder="Url.."
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
...perf,
|
||||
imgProxyConfig: {
|
||||
...perf.imgProxyConfig!,
|
||||
url: e.target.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>Service Key</div>
|
||||
<div className="w-max">
|
||||
<input
|
||||
type="password"
|
||||
value={perf.imgProxyConfig?.key}
|
||||
placeholder="Hex key.."
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
...perf,
|
||||
imgProxyConfig: {
|
||||
...perf.imgProxyConfig!,
|
||||
key: e.target.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>Service Salt</div>
|
||||
<div className="w-max">
|
||||
<input
|
||||
type="password"
|
||||
value={perf.imgProxyConfig?.salt}
|
||||
placeholder="Hex salt.."
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
...perf,
|
||||
imgProxyConfig: {
|
||||
...perf.imgProxyConfig!,
|
||||
salt: e.target.value,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Enable reactions</div>
|
||||
<small>
|
||||
Reactions will be shown on every page, if disabled no reactions will
|
||||
be shown
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perf.enableReactions}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setPreferences({ ...perf, enableReactions: e.target.checked })
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Confirm reposts</div>
|
||||
<small>Reposts need to be manually confirmed</small>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perf.confirmReposts}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setPreferences({ ...perf, confirmReposts: e.target.checked })
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Automatically show latest notes</div>
|
||||
<small>
|
||||
Notes will stream in real time into global and posts tab
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perf.autoShowLatest}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setPreferences({ ...perf, autoShowLatest: e.target.checked })
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>File upload service</div>
|
||||
<small>
|
||||
Pick which upload service you want to upload attachments to
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<select
|
||||
value={perf.fileUploader}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setPreferences({
|
||||
...perf,
|
||||
fileUploader: e.target.value,
|
||||
} as UserPreferences)
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="void.cat">void.cat (Default)</option>
|
||||
<option value="nostr.build">nostr.build</option>
|
||||
<option value="nostrimg.com">nostrimg.com</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Debug Menus</div>
|
||||
<small>
|
||||
Shows "Copy ID" and "Copy Event JSON" in the context menu on each
|
||||
message
|
||||
</small>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perf.showDebugMenus}
|
||||
onChange={(e) =>
|
||||
dispatch(
|
||||
setPreferences({ ...perf, showDebugMenus: e.target.checked })
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default PreferencesPage;
|
||||
|
@ -1,43 +1,44 @@
|
||||
.settings .avatar {
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
background-size: cover;
|
||||
border-radius: 100%;
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
width: 256px;
|
||||
height: 256px;
|
||||
background-size: cover;
|
||||
border-radius: 100%;
|
||||
cursor: pointer;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings .banner {
|
||||
width: 300px;
|
||||
height: 150px;
|
||||
background-size: cover;
|
||||
margin-bottom: 20px;
|
||||
width: 300px;
|
||||
height: 150px;
|
||||
background-size: cover;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.settings .image-settings {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.settings .avatar .edit, .settings .banner .edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
background-color: var(--bg-color);
|
||||
.settings .avatar .edit,
|
||||
.settings .banner .edit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
|
||||
.settings .avatar .edit:hover {
|
||||
opacity: 0.5;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.settings .editor textarea {
|
||||
resize: vertical;
|
||||
max-height: 300px;
|
||||
min-height: 40px;
|
||||
resize: vertical;
|
||||
max-height: 300px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.settings .actions {
|
||||
|
@ -16,184 +16,233 @@ import { HexKey } from "Nostr";
|
||||
import useFileUpload from "Upload";
|
||||
|
||||
export interface ProfileSettingsProps {
|
||||
avatar?: boolean,
|
||||
banner?: boolean,
|
||||
privateKey?: boolean
|
||||
avatar?: boolean;
|
||||
banner?: boolean;
|
||||
privateKey?: boolean;
|
||||
}
|
||||
|
||||
export default function ProfileSettings(props: ProfileSettingsProps) {
|
||||
const navigate = useNavigate();
|
||||
const id = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
|
||||
const user = useUserProfile(id!);
|
||||
const publisher = useEventPublisher();
|
||||
const uploader = useFileUpload();
|
||||
const navigate = useNavigate();
|
||||
const id = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.publicKey
|
||||
);
|
||||
const privKey = useSelector<RootState, HexKey | undefined>(
|
||||
(s) => s.login.privateKey
|
||||
);
|
||||
const user = useUserProfile(id!);
|
||||
const publisher = useEventPublisher();
|
||||
const uploader = useFileUpload();
|
||||
|
||||
const [name, setName] = useState<string>();
|
||||
const [displayName, setDisplayName] = useState<string>();
|
||||
const [picture, setPicture] = useState<string>();
|
||||
const [banner, setBanner] = useState<string>();
|
||||
const [about, setAbout] = useState<string>();
|
||||
const [website, setWebsite] = useState<string>();
|
||||
const [nip05, setNip05] = useState<string>();
|
||||
const [lud16, setLud16] = useState<string>();
|
||||
const [name, setName] = useState<string>();
|
||||
const [displayName, setDisplayName] = useState<string>();
|
||||
const [picture, setPicture] = useState<string>();
|
||||
const [banner, setBanner] = useState<string>();
|
||||
const [about, setAbout] = useState<string>();
|
||||
const [website, setWebsite] = useState<string>();
|
||||
const [nip05, setNip05] = useState<string>();
|
||||
const [lud16, setLud16] = useState<string>();
|
||||
|
||||
const avatarPicture = (picture?.length ?? 0) === 0 ? Nostrich : picture
|
||||
const avatarPicture = (picture?.length ?? 0) === 0 ? Nostrich : picture;
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setName(user.name);
|
||||
setDisplayName(user.display_name)
|
||||
setPicture(user.picture);
|
||||
setBanner(user.banner);
|
||||
setAbout(user.about);
|
||||
setWebsite(user.website);
|
||||
setNip05(user.nip05);
|
||||
setLud16(user.lud16);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
async function saveProfile() {
|
||||
// copy user object and delete internal fields
|
||||
let userCopy = {
|
||||
...user,
|
||||
name,
|
||||
display_name: displayName,
|
||||
about,
|
||||
picture,
|
||||
banner,
|
||||
website,
|
||||
nip05,
|
||||
lud16
|
||||
};
|
||||
delete userCopy["loaded"];
|
||||
delete userCopy["created"];
|
||||
delete userCopy["pubkey"];
|
||||
delete userCopy["npub"];
|
||||
console.debug(userCopy);
|
||||
|
||||
let ev = await publisher.metadata(userCopy);
|
||||
console.debug(ev);
|
||||
publisher.broadcast(ev);
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setName(user.name);
|
||||
setDisplayName(user.display_name);
|
||||
setPicture(user.picture);
|
||||
setBanner(user.banner);
|
||||
setAbout(user.about);
|
||||
setWebsite(user.website);
|
||||
setNip05(user.nip05);
|
||||
setLud16(user.lud16);
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
async function uploadFile() {
|
||||
let file = await openFile();
|
||||
if (file) {
|
||||
console.log(file);
|
||||
let rsp = await uploader.upload(file, file.name);
|
||||
console.log(rsp);
|
||||
if (typeof rsp?.error === "string") {
|
||||
throw new Error(`Upload failed ${rsp.error}`);
|
||||
}
|
||||
return rsp.url;
|
||||
}
|
||||
async function saveProfile() {
|
||||
// copy user object and delete internal fields
|
||||
let userCopy = {
|
||||
...user,
|
||||
name,
|
||||
display_name: displayName,
|
||||
about,
|
||||
picture,
|
||||
banner,
|
||||
website,
|
||||
nip05,
|
||||
lud16,
|
||||
};
|
||||
delete userCopy["loaded"];
|
||||
delete userCopy["created"];
|
||||
delete userCopy["pubkey"];
|
||||
delete userCopy["npub"];
|
||||
console.debug(userCopy);
|
||||
|
||||
let ev = await publisher.metadata(userCopy);
|
||||
console.debug(ev);
|
||||
publisher.broadcast(ev);
|
||||
}
|
||||
|
||||
async function uploadFile() {
|
||||
let file = await openFile();
|
||||
if (file) {
|
||||
console.log(file);
|
||||
let rsp = await uploader.upload(file, file.name);
|
||||
console.log(rsp);
|
||||
if (typeof rsp?.error === "string") {
|
||||
throw new Error(`Upload failed ${rsp.error}`);
|
||||
}
|
||||
return rsp.url;
|
||||
}
|
||||
}
|
||||
|
||||
async function setNewAvatar() {
|
||||
const rsp = await uploadFile();
|
||||
if (rsp) {
|
||||
setPicture(rsp);
|
||||
}
|
||||
async function setNewAvatar() {
|
||||
const rsp = await uploadFile();
|
||||
if (rsp) {
|
||||
setPicture(rsp);
|
||||
}
|
||||
}
|
||||
|
||||
async function setNewBanner() {
|
||||
const rsp = await uploadFile();
|
||||
if (rsp) {
|
||||
setBanner(rsp);
|
||||
}
|
||||
}
|
||||
|
||||
function editor() {
|
||||
return (
|
||||
<div className="editor form">
|
||||
<div className="form-group">
|
||||
<div>Name:</div>
|
||||
<div>
|
||||
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>Display name:</div>
|
||||
<div>
|
||||
<input type="text" value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group form-col">
|
||||
<div>About:</div>
|
||||
<div className="w-max">
|
||||
<textarea className="w-max" onChange={(e) => setAbout(e.target.value)} value={about}></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>Website:</div>
|
||||
<div>
|
||||
<input type="text" value={website} onChange={(e) => setWebsite(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>NIP-05:</div>
|
||||
<div>
|
||||
<input type="text" className="mr10" value={nip05} onChange={(e) => setNip05(e.target.value)} />
|
||||
<button type="button" onClick={() => navigate("/verification")}>
|
||||
<FontAwesomeIcon icon={faShop} />
|
||||
|
||||
Buy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>LN Address:</div>
|
||||
<div>
|
||||
<input type="text" value={lud16} onChange={(e) => setLud16(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" onClick={() => saveProfile()}>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function settings() {
|
||||
if (!id) return null;
|
||||
return (
|
||||
<>
|
||||
<div className="flex f-center image-settings">
|
||||
{(props.avatar ?? true) && (<div>
|
||||
<h2>Avatar</h2>
|
||||
<div style={{ backgroundImage: `url(${avatarPicture})` }} className="avatar">
|
||||
<div className="edit" onClick={() => setNewAvatar()}>Edit</div>
|
||||
</div>
|
||||
</div>)}
|
||||
{(props.banner ?? true) && (<div>
|
||||
<h2>Header</h2>
|
||||
<div style={{ backgroundImage: `url(${(banner?.length ?? 0) === 0 ? Nostrich : banner})` }} className="banner">
|
||||
<div className="edit" onClick={() => setNewBanner()}>Edit</div>
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
{editor()}
|
||||
</>
|
||||
)
|
||||
async function setNewBanner() {
|
||||
const rsp = await uploadFile();
|
||||
if (rsp) {
|
||||
setBanner(rsp);
|
||||
}
|
||||
}
|
||||
|
||||
function editor() {
|
||||
return (
|
||||
<div className="settings">
|
||||
<h3>Edit Profile</h3>
|
||||
{settings()}
|
||||
{privKey && (props.privateKey ?? true) && (<div className="flex f-col bg-grey">
|
||||
<div>
|
||||
<h4>Your Private Key Is (do not share this with anyone):</h4>
|
||||
</div>
|
||||
<div>
|
||||
<Copy text={hexToBech32("nsec", privKey)} />
|
||||
</div>
|
||||
</div>)}
|
||||
<div className="editor form">
|
||||
<div className="form-group">
|
||||
<div>Name:</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>Display name:</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group form-col">
|
||||
<div>About:</div>
|
||||
<div className="w-max">
|
||||
<textarea
|
||||
className="w-max"
|
||||
onChange={(e) => setAbout(e.target.value)}
|
||||
value={about}
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>Website:</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={website}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>NIP-05:</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
className="mr10"
|
||||
value={nip05}
|
||||
onChange={(e) => setNip05(e.target.value)}
|
||||
/>
|
||||
<button type="button" onClick={() => navigate("/verification")}>
|
||||
<FontAwesomeIcon icon={faShop} />
|
||||
Buy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>LN Address:</div>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
value={lud16}
|
||||
onChange={(e) => setLud16(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div></div>
|
||||
<div>
|
||||
<button type="button" onClick={() => saveProfile()}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function settings() {
|
||||
if (!id) return null;
|
||||
return (
|
||||
<>
|
||||
<div className="flex f-center image-settings">
|
||||
{(props.avatar ?? true) && (
|
||||
<div>
|
||||
<h2>Avatar</h2>
|
||||
<div
|
||||
style={{ backgroundImage: `url(${avatarPicture})` }}
|
||||
className="avatar"
|
||||
>
|
||||
<div className="edit" onClick={() => setNewAvatar()}>
|
||||
Edit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(props.banner ?? true) && (
|
||||
<div>
|
||||
<h2>Header</h2>
|
||||
<div
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
(banner?.length ?? 0) === 0 ? Nostrich : banner
|
||||
})`,
|
||||
}}
|
||||
className="banner"
|
||||
>
|
||||
<div className="edit" onClick={() => setNewBanner()}>
|
||||
Edit
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{editor()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="settings">
|
||||
<h3>Edit Profile</h3>
|
||||
{settings()}
|
||||
{privKey && (props.privateKey ?? true) && (
|
||||
<div className="flex f-col bg-grey">
|
||||
<div>
|
||||
<h4>Your Private Key Is (do not share this with anyone):</h4>
|
||||
</div>
|
||||
<div>
|
||||
<Copy text={hexToBech32("nsec", privKey)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -7,51 +7,98 @@ import { removeRelay } from "State/Login";
|
||||
import { parseId } from "Util";
|
||||
|
||||
const RelayInfo = () => {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const conn = Array.from(System.Sockets.values()).find(a => a.Id === params.id);
|
||||
console.debug(conn);
|
||||
const stats = useRelayState(conn?.Address ?? "");
|
||||
const conn = Array.from(System.Sockets.values()).find(
|
||||
(a) => a.Id === params.id
|
||||
);
|
||||
console.debug(conn);
|
||||
const stats = useRelayState(conn?.Address ?? "");
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3 className="pointer" onClick={() => navigate("/settings/relays")}>Relays</h3>
|
||||
<div className="card">
|
||||
<h3>{stats?.info?.name}</h3>
|
||||
<p>{stats?.info?.description}</p>
|
||||
return (
|
||||
<>
|
||||
<h3 className="pointer" onClick={() => navigate("/settings/relays")}>
|
||||
Relays
|
||||
</h3>
|
||||
<div className="card">
|
||||
<h3>{stats?.info?.name}</h3>
|
||||
<p>{stats?.info?.description}</p>
|
||||
|
||||
{stats?.info?.pubkey && (<>
|
||||
<h4>Owner</h4>
|
||||
<ProfilePreview pubkey={parseId(stats.info.pubkey)} />
|
||||
</>)}
|
||||
{stats?.info?.software && (<div className="flex">
|
||||
<h4 className="f-grow">Software</h4>
|
||||
<div className="flex f-col">
|
||||
{stats.info.software.startsWith("http") ? <a href={stats.info.software} target="_blank" rel="noreferrer">{stats.info.software}</a> : <>{stats.info.software}</>}
|
||||
<small>{!stats.info.version?.startsWith("v") && "v"}{stats.info.version}</small>
|
||||
</div>
|
||||
</div>)}
|
||||
{stats?.info?.contact && (<div className="flex">
|
||||
<h4 className="f-grow">Contact</h4>
|
||||
<a href={`${stats.info.contact.startsWith("mailto:") ? "" : "mailto:"}${stats.info.contact}`} target="_blank" rel="noreferrer">{stats.info.contact}</a>
|
||||
</div>)}
|
||||
{stats?.info?.supported_nips && (<>
|
||||
<h4>Supports</h4>
|
||||
<div className="f-grow">
|
||||
{stats.info.supported_nips.map(a => <span className="pill" onClick={() => navigate(`https://github.com/nostr-protocol/nips/blob/master/${a.toString().padStart(2, "0")}.md`)}>NIP-{a.toString().padStart(2, "0")}</span>)}
|
||||
</div>
|
||||
</>)}
|
||||
<div className="flex mt10 f-end">
|
||||
<div className="btn error" onClick={() => {
|
||||
dispatch(removeRelay(conn!.Address));
|
||||
navigate("/settings/relays")
|
||||
}}>Remove</div>
|
||||
</div>
|
||||
{stats?.info?.pubkey && (
|
||||
<>
|
||||
<h4>Owner</h4>
|
||||
<ProfilePreview pubkey={parseId(stats.info.pubkey)} />
|
||||
</>
|
||||
)}
|
||||
{stats?.info?.software && (
|
||||
<div className="flex">
|
||||
<h4 className="f-grow">Software</h4>
|
||||
<div className="flex f-col">
|
||||
{stats.info.software.startsWith("http") ? (
|
||||
<a href={stats.info.software} target="_blank" rel="noreferrer">
|
||||
{stats.info.software}
|
||||
</a>
|
||||
) : (
|
||||
<>{stats.info.software}</>
|
||||
)}
|
||||
<small>
|
||||
{!stats.info.version?.startsWith("v") && "v"}
|
||||
{stats.info.version}
|
||||
</small>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
{stats?.info?.contact && (
|
||||
<div className="flex">
|
||||
<h4 className="f-grow">Contact</h4>
|
||||
<a
|
||||
href={`${
|
||||
stats.info.contact.startsWith("mailto:") ? "" : "mailto:"
|
||||
}${stats.info.contact}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{stats.info.contact}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{stats?.info?.supported_nips && (
|
||||
<>
|
||||
<h4>Supports</h4>
|
||||
<div className="f-grow">
|
||||
{stats.info.supported_nips.map((a) => (
|
||||
<span
|
||||
className="pill"
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`https://github.com/nostr-protocol/nips/blob/master/${a
|
||||
.toString()
|
||||
.padStart(2, "0")}.md`
|
||||
)
|
||||
}
|
||||
>
|
||||
NIP-{a.toString().padStart(2, "0")}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="flex mt10 f-end">
|
||||
<div
|
||||
className="btn error"
|
||||
onClick={() => {
|
||||
dispatch(removeRelay(conn!.Address));
|
||||
navigate("/settings/relays");
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelayInfo;
|
||||
export default RelayInfo;
|
||||
|
@ -8,57 +8,70 @@ import { RelaySettings } from "Nostr/Connection";
|
||||
import { setRelays } from "State/Login";
|
||||
|
||||
const RelaySettingsPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const publisher = useEventPublisher();
|
||||
const relays = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
|
||||
const [newRelay, setNewRelay] = useState<string>();
|
||||
const dispatch = useDispatch();
|
||||
const publisher = useEventPublisher();
|
||||
const relays = useSelector<RootState, Record<string, RelaySettings>>(
|
||||
(s) => s.login.relays
|
||||
);
|
||||
const [newRelay, setNewRelay] = useState<string>();
|
||||
|
||||
async function saveRelays() {
|
||||
let ev = await publisher.saveRelays();
|
||||
publisher.broadcast(ev);
|
||||
publisher.broadcastForBootstrap(ev);
|
||||
}
|
||||
|
||||
|
||||
function addRelay() {
|
||||
return (
|
||||
<>
|
||||
<h4>Add Relays</h4>
|
||||
<div className="flex mb10">
|
||||
<input type="text" className="f-grow" placeholder="wss://my-relay.com" value={newRelay} onChange={(e) => setNewRelay(e.target.value)} />
|
||||
</div>
|
||||
<button className="secondary mb10" onClick={() => addNewRelay()}>Add</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function addNewRelay() {
|
||||
if ((newRelay?.length ?? 0) > 0) {
|
||||
const parsed = new URL(newRelay!);
|
||||
const payload = {
|
||||
relays: {
|
||||
...relays,
|
||||
[parsed.toString()]: { read: false, write: false }
|
||||
},
|
||||
createdAt: Math.floor(new Date().getTime() / 1000)
|
||||
};
|
||||
dispatch(setRelays(payload))
|
||||
}
|
||||
}
|
||||
async function saveRelays() {
|
||||
let ev = await publisher.saveRelays();
|
||||
publisher.broadcast(ev);
|
||||
publisher.broadcastForBootstrap(ev);
|
||||
}
|
||||
|
||||
function addRelay() {
|
||||
return (
|
||||
<>
|
||||
<h3>Relays</h3>
|
||||
<div className="flex f-col mb10">
|
||||
{Object.keys(relays || {}).map(a => <Relay addr={a} key={a} />)}
|
||||
</div>
|
||||
<div className="flex mt10">
|
||||
<div className="f-grow"></div>
|
||||
<button type="button" onClick={() => saveRelays()}>Save</button>
|
||||
</div>
|
||||
{addRelay()}
|
||||
</>
|
||||
)
|
||||
}
|
||||
<>
|
||||
<h4>Add Relays</h4>
|
||||
<div className="flex mb10">
|
||||
<input
|
||||
type="text"
|
||||
className="f-grow"
|
||||
placeholder="wss://my-relay.com"
|
||||
value={newRelay}
|
||||
onChange={(e) => setNewRelay(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<button className="secondary mb10" onClick={() => addNewRelay()}>
|
||||
Add
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function addNewRelay() {
|
||||
if ((newRelay?.length ?? 0) > 0) {
|
||||
const parsed = new URL(newRelay!);
|
||||
const payload = {
|
||||
relays: {
|
||||
...relays,
|
||||
[parsed.toString()]: { read: false, write: false },
|
||||
},
|
||||
createdAt: Math.floor(new Date().getTime() / 1000),
|
||||
};
|
||||
dispatch(setRelays(payload));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Relays</h3>
|
||||
<div className="flex f-col mb10">
|
||||
{Object.keys(relays || {}).map((a) => (
|
||||
<Relay addr={a} key={a} />
|
||||
))}
|
||||
</div>
|
||||
<div className="flex mt10">
|
||||
<div className="f-grow"></div>
|
||||
<button type="button" onClick={() => saveRelays()}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
{addRelay()}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelaySettingsPage;
|
||||
|
Reference in New Issue
Block a user