diff --git a/src/Util.js b/src/Util.js index 40390fcb..634a7181 100644 --- a/src/Util.js +++ b/src/Util.js @@ -78,7 +78,7 @@ export const Reaction = { * @returns */ export function normalizeReaction(content) { - switch(content) { + switch (content) { case "": return Reaction.Positive; case "🤙": return Reaction.Positive; case "❤️": return Reaction.Positive; @@ -89,4 +89,26 @@ export function normalizeReaction(content) { case "👎": return Reaction.Negative; } return content; +} + +/** + * Converts LNURL service to LN Address + * @param {string} lnurl + * @returns + */ +export function extractLnAddress(lnurl) { + // some clients incorrectly set this to LNURL service, patch this + if (lnurl.toLowerCase().startsWith("lnurl")) { + let url = bech32ToText(lnurl); + if (url.startsWith("http")) { + let parsedUri = new URL(url); + // is lightning address + if (parsedUri.pathname.startsWith("/.well-known/lnurlp/")) { + let pathParts = parsedUri.pathname.split('/'); + let username = pathParts[pathParts.length - 1]; + return `${username}@${parsedUri.hostname}`; + } + } + } + return lnurl; } \ No newline at end of file diff --git a/src/element/Copy.css b/src/element/Copy.css new file mode 100644 index 00000000..3a949667 --- /dev/null +++ b/src/element/Copy.css @@ -0,0 +1,14 @@ +.copy { + user-select: none; + cursor: pointer; +} + +.copy .body { + font-family: monospace; + font-size: 14px; + margin: 0; + width: 18em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} \ No newline at end of file diff --git a/src/element/Copy.js b/src/element/Copy.js new file mode 100644 index 00000000..f70d8803 --- /dev/null +++ b/src/element/Copy.js @@ -0,0 +1,21 @@ +import "./Copy.css"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCopy, faCheck } from "@fortawesome/free-solid-svg-icons"; +import { useCopy } from "../useCopy"; + +export default function Copy(props) { + + const { copy, copied, error } = useCopy(); + return ( +
copy(props.text)}> + +

+ {props.text} +

+
+ ) +} \ No newline at end of file diff --git a/src/feed/UsersFeed.js b/src/feed/UsersFeed.js index 554559da..f9d87e3c 100644 --- a/src/feed/UsersFeed.js +++ b/src/feed/UsersFeed.js @@ -1,7 +1,6 @@ import { useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { ProfileCacheExpire } from "../Const"; -import Event from "../nostr/Event"; import EventKind from "../nostr/EventKind"; import { Subscriptions } from "../nostr/Subscriptions"; import { setUserData } from "../state/Users"; diff --git a/src/index.css b/src/index.css index 89629743..6635e421 100644 --- a/src/index.css +++ b/src/index.css @@ -83,6 +83,10 @@ input[type="text"], input[type="password"], input[type="number"], textarea { min-width: 0; } +.f-center { + justify-content: center; +} + .f-1 { flex: 1; } @@ -106,6 +110,9 @@ input[type="text"], input[type="password"], input[type="number"], textarea { .w-max { width: 100%; + width: -moz-available; + width: -webkit-fill-available; + width: fill-available; } a { @@ -148,7 +155,17 @@ div.form-group > div { word-break: break-word; } -div.form-group > div:first-child { +div.form-group > div:nth-child(1) { + min-width: 100px; +} + +div.form-group > div:nth-child(2) { + display: flex; + flex-grow: 1; + justify-content: end; +} + +div.form-group > div:nth-child(2) input { flex-grow: 1; } diff --git a/src/index.js b/src/index.js index 75f27ae4..9d51497c 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,7 @@ import RootPage from './pages/Root'; import Store from "./state/Store"; import NotificationsPage from './pages/Notifications'; import NewUserPage from './pages/NewUserPage'; +import SettingsPage from './pages/SettingsPage'; export const System = new NostrSystem(); @@ -34,6 +35,9 @@ root.render( } /> } /> } /> + }> + Relays} /> + diff --git a/src/nostr/Connection.js b/src/nostr/Connection.js index 16e96e77..839528b2 100644 --- a/src/nostr/Connection.js +++ b/src/nostr/Connection.js @@ -4,6 +4,16 @@ import { Subscriptions } from "./Subscriptions"; import Event from "./Event"; import { DefaultConnectTimeout } from "../Const"; +export class ConnectionStats { + constructor() { + this.Latency = []; + this.Subs = 0; + this.SubsTimeout = 0; + this.EventsReceived = 0; + this.EventsSent = 0; + } +} + export default class Connection { constructor(addr, options) { this.Address = addr; @@ -13,6 +23,7 @@ export default class Connection { this.Read = options?.read || true; this.Write = options?.write || true; this.ConnectTimeout = DefaultConnectTimeout; + this.Stats = new ConnectionStats(); this.Connect(); } @@ -145,6 +156,7 @@ export default class Connection { _OnEvent(subId, ev) { if (this.Subscriptions[subId]) { //this._VerifySig(ev); + ev.relay = this.Address; // tag event with relay this.Subscriptions[subId].OnEvent(ev); } else { // console.warn(`No subscription for event! ${subId}`); diff --git a/src/pages/Layout.js b/src/pages/Layout.js index 60946f8d..d41101fd 100644 --- a/src/pages/Layout.js +++ b/src/pages/Layout.js @@ -68,7 +68,7 @@ export default function Layout(props) { return (
-
navigate("/")}>n o s t r
+
navigate("/")}>snort
{key ? accountHeader() :
navigate("/login")}>Login
diff --git a/src/pages/ProfilePage.css b/src/pages/ProfilePage.css index 5cb0bd44..933b272f 100644 --- a/src/pages/ProfilePage.css +++ b/src/pages/ProfilePage.css @@ -18,50 +18,13 @@ margin: 0; } -.profile .npub-container { - user-select: none; - cursor: pointer; -} - -.profile .npub { - font-family: monospace; - font-size: 14px; - margin: 0; - width: 18em; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - .profile .avatar { width: 256px; height: 256px; background-size: cover; - cursor: pointer; border-radius: 10px; } -.profile .avatar .edit { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - opacity: 0; - background-color: black; -} - -.profile .avatar .edit:hover { - opacity: 0.5; -} - -.profile .editor textarea { - resize: vertical; - width: calc(100% - 30px); - max-height: 300px; - min-height: 60px; -} - @media(max-width: 720px) { .profile { flex-direction: column; diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index ac45a4c3..53f74d70 100644 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -1,200 +1,44 @@ import "./ProfilePage.css"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import Nostrich from "../nostrich.jpg"; + +import { useState } from "react"; +import { useSelector } from "react-redux"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faQrcode, faCopy, faCheck } from "@fortawesome/free-solid-svg-icons"; -import { useParams } from "react-router-dom"; +import { faQrcode } from "@fortawesome/free-solid-svg-icons"; +import { useNavigate, useParams } from "react-router-dom"; import useProfile from "../feed/ProfileFeed"; -import { resetProfile } from "../state/Users"; -import Nostrich from "../nostrich.jpg"; -import useEventPublisher from "../feed/EventPublisher"; -import QRCodeStyling from "qr-code-styling"; -import { logout } from "../state/Login"; import FollowButton from "../element/FollowButton"; -import VoidUpload from "../feed/VoidUpload"; -import { bech32ToText, openFile, parseId } from "../Util"; +import { extractLnAddress, parseId } from "../Util"; import Timeline from "../element/Timeline"; import { extractLinks } from '../Text' -import { useCopy } from '../useCopy' import LNURLTip from "../element/LNURLTip"; +import Copy from "../element/Copy"; export default function ProfilePage() { - const dispatch = useDispatch(); const params = useParams(); + const navigate = useNavigate(); const id = parseId(params.id); const user = useProfile(id); - const publisher = useEventPublisher(); const loginPubKey = useSelector(s => s.login.publicKey); const isMe = loginPubKey === id; - const qrRef = useRef(); - const { copy, copied, error } = useCopy() - - const [name, setName] = useState(""); - const [picture, setPicture] = useState(""); - const [about, setAbout] = useState(""); - const [website, setWebsite] = useState(""); - const [nip05, setNip05] = useState(""); - const [lud06, setLud06] = useState(""); - const [lud16, setLud16] = useState(""); const [showLnQr, setShowLnQr] = useState(false); - useEffect(() => { - if (user) { - setName(user.name ?? ""); - setPicture(user.picture ?? ""); - setAbout(user.about ?? ""); - setWebsite(user.website ?? ""); - setNip05(user.nip05 ?? ""); - setLud06(user.lud06 ?? ""); - setLud16(user.lud16 ?? ""); - } - }, [user]); - - useMemo(() => { - // some clients incorrectly set this to LNURL service, patch this - if (lud16.toLowerCase().startsWith("lnurl")) { - let url = bech32ToText(lud16); - if (url.startsWith("http")) { - let parsedUri = new URL(url); - // is lightning address - if (parsedUri.pathname.startsWith("/.well-known/lnurlp/")) { - let pathParts = parsedUri.pathname.split('/'); - let username = pathParts[pathParts.length - 1]; - setLud16(`${username}@${parsedUri.hostname}`); - } - } - } - }, [lud16]); - - useMemo(() => { - if (qrRef.current && showLnQr) { - let qr = new QRCodeStyling({ - data: { lud16 }, - type: "canvas" - }); - qrRef.current.innerHTML = ""; - qr.append(qrRef.current); - } - }, [showLnQr]); - - async function saveProfile() { - // copy user object and delete internal fields - let userCopy = { - ...user, - name, - about, - picture, - website, - nip05, - lud16 - }; - delete userCopy["loaded"]; - delete userCopy["fromEvent"]; - // event top level props should not be copied into metadata (bug) - delete userCopy["pubkey"]; - delete userCopy["sig"]; - delete userCopy["pubkey"]; - delete userCopy["tags"]; - delete userCopy["content"]; - delete userCopy["created_at"]; - delete userCopy["id"]; - delete userCopy["kind"] - - // trim empty string fields - Object.keys(userCopy).forEach(k => { - if (userCopy[k] === "") { - delete userCopy[k]; - } - }); - console.debug(userCopy); - - let ev = await publisher.metadata(userCopy); - console.debug(ev); - dispatch(resetProfile(id)); - publisher.broadcast(ev); - } - - async function setNewAvatar() { - let file = await openFile(); - console.log(file); - let rsp = await VoidUpload(file); - if (!rsp) { - throw "Upload failed, please try again later"; - } - console.log(rsp); - setPicture(rsp.metadata.url ?? `https://void.cat/d/${rsp.id}`) - } - - function editor() { - return ( -
-
-
Name:
-
- setName(e.target.value)} /> -
-
-
-
About:
-
- -
-
-
-
Website:
-
- setWebsite(e.target.value)} /> -
-
-
-
NIP-05:
-
- setNip05(e.target.value)} /> -
-
-
-
LN Address:
-
- setLud16(e.target.value)} /> -
-
-
-
-
dispatch(logout())}>Logout
-
-
-
saveProfile()}>Save
-
-
-
- ) - } - function details() { - const lnurl = lud16 || lud06; + const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || ""); return ( <>
-

{name}

-
copy(params.id)}> - -

- {params.id} -

-
+

{user?.name}

+
- + {isMe ?
navigate("/settings")}>Settings
: }
-

{extractLinks([about])}

- {website ? {website} : null} +

{extractLinks([user?.about])}

+ {user?.website ? {user?.website} : null} {lnurl ?
setShowLnQr(true)}> @@ -202,7 +46,7 @@ export default function ProfilePage() {
  ⚡️ {lnurl}
: null} - setShowLnQr(false)}/> + setShowLnQr(false)} /> ) } @@ -211,17 +55,11 @@ export default function ProfilePage() { <>
-
- {isMe ? -
setNewAvatar()}> -
Edit
-
- : null - } +
- {isMe ? editor() : details()} + {details()}
diff --git a/src/pages/SettingsPage.css b/src/pages/SettingsPage.css new file mode 100644 index 00000000..a78f5c57 --- /dev/null +++ b/src/pages/SettingsPage.css @@ -0,0 +1,28 @@ + +.settings .avatar { + width: 256px; + height: 256px; + background-size: cover; + border-radius: 10px; + cursor: pointer; +} + +.settings .avatar .edit { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + opacity: 0; + background-color: black; +} + +.settings .avatar .edit:hover { + opacity: 0.5; +} + +.settings .editor textarea { + resize: vertical; + max-height: 300px; + min-height: 40px; +} diff --git a/src/pages/SettingsPage.js b/src/pages/SettingsPage.js new file mode 100644 index 00000000..7187a557 --- /dev/null +++ b/src/pages/SettingsPage.js @@ -0,0 +1,145 @@ +import "./SettingsPage.css"; +import Nostrich from "../nostrich.jpg"; + +import { useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; + +import useEventPublisher from "../feed/EventPublisher"; +import useProfile from "../feed/ProfileFeed"; +import VoidUpload from "../feed/VoidUpload"; +import { logout } from "../state/Login"; +import { resetProfile } from "../state/Users"; +import { openFile } from "../Util"; + +export default function SettingsPage(props) { + const id = useSelector(s => s.login.publicKey); + const dispatch = useDispatch(); + const user = useProfile(id); + const publisher = useEventPublisher(); + + const [name, setName] = useState(""); + const [picture, setPicture] = useState(""); + const [about, setAbout] = useState(""); + const [website, setWebsite] = useState(""); + const [nip05, setNip05] = useState(""); + const [lud06, setLud06] = useState(""); + const [lud16, setLud16] = useState(""); + + useEffect(() => { + if (user) { + setName(user.name ?? ""); + setPicture(user.picture ?? ""); + setAbout(user.about ?? ""); + setWebsite(user.website ?? ""); + setNip05(user.nip05 ?? ""); + setLud06(user.lud06 ?? ""); + setLud16(user.lud16 ?? ""); + } + }, [user]); + + async function saveProfile() { + // copy user object and delete internal fields + let userCopy = { + ...user, + name, + about, + picture, + website, + nip05, + lud16 + }; + delete userCopy["loaded"]; + delete userCopy["fromEvent"]; + // event top level props should not be copied into metadata (bug) + delete userCopy["pubkey"]; + delete userCopy["sig"]; + delete userCopy["pubkey"]; + delete userCopy["tags"]; + delete userCopy["content"]; + delete userCopy["created_at"]; + delete userCopy["id"]; + delete userCopy["kind"] + + // trim empty string fields + Object.keys(userCopy).forEach(k => { + if (userCopy[k] === "") { + delete userCopy[k]; + } + }); + console.debug(userCopy); + + let ev = await publisher.metadata(userCopy); + console.debug(ev); + dispatch(resetProfile(id)); + publisher.broadcast(ev); + } + + async function setNewAvatar() { + let file = await openFile(); + console.log(file); + let rsp = await VoidUpload(file); + if (!rsp) { + throw "Upload failed, please try again later"; + } + console.log(rsp); + setPicture(rsp.metadata.url ?? `https://void.cat/d/${rsp.id}`) + } + + function editor() { + return ( +
+
+
Name:
+
+ setName(e.target.value)} /> +
+
+
+
About:
+
+ +
+
+
+
Website:
+
+ setWebsite(e.target.value)} /> +
+
+
+
NIP-05:
+
+ setNip05(e.target.value)} /> +
+
+
+
LN Address:
+
+ setLud16(e.target.value)} /> +
+
+
+
+
dispatch(logout())}>Logout
+
+
+
saveProfile()}>Save
+
+
+
+ ) + } + + return ( +
+

Settings

+
+
+
Edit
+
+
+ + {editor()} +
+ ); +} \ No newline at end of file