Signup flow

This commit is contained in:
Kieran 2023-07-20 12:33:00 +01:00
parent a1605e31d5
commit c74bbae5c7
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
24 changed files with 389 additions and 77 deletions

View File

@ -15,6 +15,7 @@
"@testing-library/react": "^13.0.0",
"@testing-library/user-event": "^13.2.1",
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
"@void-cat/api": "^1.0.7",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"buffer": "^6.0.3",
"emoji-mart": "^5.5.2",

View File

@ -30,5 +30,16 @@
<symbol id="zap-stream" viewBox="0 0 160 160" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M82.4852 54.5094L87.7882 48.2773C87.8525 48.2098 87.9174 48.1429 87.9826 48.0768C94.4927 41.1346 103.63 36.8165 113.748 36.8165C133.516 36.8165 149.541 53.2997 149.541 73.6327C149.541 82.0501 146.795 89.8077 142.174 96.0093L142.197 96.029L141.843 96.4456C141.126 97.3799 140.364 98.2774 139.563 99.1352L87.9613 160L43.5147 158.617L58.9832 140.033L112.875 76.6987C114.038 75.3317 113.873 73.2807 112.506 72.1175C111.139 70.9544 109.088 71.1196 107.925 72.4865L71.2247 115.617C64.7813 121.963 55.8992 125.885 46.0917 125.885C26.4118 125.885 10.458 110.093 10.458 90.6136C10.458 81.6851 13.8096 73.5314 19.3355 67.318L76.4941 3.75969e-05L120.334 8.27526e-08L51.0699 81.3993C49.9068 82.7663 50.072 84.8173 51.4389 85.9805C52.8059 87.1437 54.857 86.9784 56.0201 85.6115L72.1945 66.6032C72.207 66.6164 72.2194 66.6297 72.2319 66.643L82.4852 54.5094Z" fill="white"/>
</symbol>
<symbol id="camera-plus" viewBox="0 0 24 24" fill="none">
<g>
<path d="M22 11.5V14.6C22 16.8402 22 17.9603 21.564 18.816C21.1805 19.5686 20.5686 20.1805 19.816 20.564C18.9603 21 17.8402 21 15.6 21H8.4C6.15979 21 5.03969 21 4.18404 20.564C3.43139 20.1805 2.81947 19.5686 2.43597 18.816C2 17.9603 2 16.8402 2 14.6V9.4C2 7.15979 2 6.03969 2.43597 5.18404C2.81947 4.43139 3.43139 3.81947 4.18404 3.43597C5.03969 3 6.15979 3 8.4 3H12.5M19 8V2M16 5H22M16 12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12C8 9.79086 9.79086 8 12 8C14.2091 8 16 9.79086 16 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</symbol>
<symbol id="check" viewBox="0 0 18 13" fill="none">
<path d="M17 1L6 12L1 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</symbol>
<symbol id="copy" viewBox="0 0 20 20" fill="none">
<path d="M13.3333 13.3327V15.666C13.3333 16.5994 13.3333 17.0661 13.1516 17.4227C12.9918 17.7363 12.7369 17.9912 12.4233 18.151C12.0668 18.3327 11.6 18.3327 10.6666 18.3327H4.33329C3.39987 18.3327 2.93316 18.3327 2.57664 18.151C2.26304 17.9912 2.00807 17.7363 1.84828 17.4227C1.66663 17.0661 1.66663 16.5994 1.66663 15.666V9.33268C1.66663 8.39926 1.66663 7.93255 1.84828 7.57603C2.00807 7.26243 2.26304 7.00746 2.57664 6.84767C2.93316 6.66602 3.39987 6.66602 4.33329 6.66602H6.66663M9.33329 13.3327H15.6666C16.6 13.3327 17.0668 13.3327 17.4233 13.151C17.7369 12.9912 17.9918 12.7363 18.1516 12.4227C18.3333 12.0661 18.3333 11.5994 18.3333 10.666V4.33268C18.3333 3.39926 18.3333 2.93255 18.1516 2.57603C17.9918 2.26243 17.7369 2.00746 17.4233 1.84767C17.0668 1.66602 16.6 1.66602 15.6666 1.66602H9.33329C8.39987 1.66602 7.93316 1.66602 7.57664 1.84767C7.26304 2.00746 7.00807 2.26243 6.84828 2.57603C6.66663 2.93255 6.66663 3.39926 6.66663 4.33268V10.666C6.66663 11.5994 6.66663 12.0661 6.84828 12.4227C7.00807 12.7363 7.26304 12.9912 7.57664 13.151C7.93316 13.3327 8.39987 13.3327 9.33329 13.3327Z" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round" />
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1,5 +1,5 @@
import { useUserProfile } from "@snort/system-react";
import { NostrEvent, parseZap, EventPublisher, EventKind } from "@snort/system";
import { NostrEvent, parseZap, EventKind } from "@snort/system";
import { useRef, useState, useMemo } from "react";
import {
useMediaQuery,
@ -18,6 +18,7 @@ import { Text } from "./text";
import { SendZapsDialog } from "./send-zap";
import { findTag } from "../utils";
import type { EmojiPack } from "../hooks/emoji";
import { useLogin } from "../hooks/login";
interface Emoji {
id: string;
@ -54,6 +55,7 @@ export function ChatMessage({
const isHovering = useHover(ref);
const [showZapDialog, setShowZapDialog] = useState(false);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const login = useLogin();
const profile = useUserProfile(
System,
inView?.isIntersecting ? ev.pubkey : undefined
@ -97,7 +99,7 @@ export function ChatMessage({
setShowZapDialog(false);
let reply = null;
try {
const pub = await EventPublisher.nip7();
const pub = login?.publisher();
if (emoji.native) {
reply = await pub?.react(ev, emoji.native || "+1");
} else {
@ -117,7 +119,7 @@ export function ChatMessage({
console.debug(reply);
System.BroadcastEvent(reply);
}
} catch (error) {}
} catch (error) { }
}
// @ts-expect-error
@ -176,16 +178,16 @@ export function ChatMessage({
style={
isTablet
? {
display: showZapDialog || isHovering ? "flex" : "none",
}
display: showZapDialog || isHovering ? "flex" : "none",
}
: {
position: "fixed",
top: topOffset - 12,
left: leftOffset - 32,
opacity: showZapDialog || isHovering ? 1 : 0,
pointerEvents:
showZapDialog || isHovering ? "auto" : "none",
}
position: "fixed",
top: topOffset - 12,
left: leftOffset - 32,
opacity: showZapDialog || isHovering ? 1 : 0,
pointerEvents:
showZapDialog || isHovering ? "auto" : "none",
}
}
>
{zapTarget && (

11
src/element/copy.css Normal file
View File

@ -0,0 +1,11 @@
.copy {
display: flex;
cursor: pointer;
align-items: center;
gap: 8px;
}
.copy .body {
font-size: small;
color: white;
}

23
src/element/copy.tsx Normal file
View File

@ -0,0 +1,23 @@
import "./copy.css";
import { useCopy } from "hooks/copy";
import { Icon } from "./icon";
export interface CopyProps {
text: string;
maxSize?: number;
className?: string;
}
export default function Copy({ text, maxSize = 32, className }: CopyProps) {
const { copy, copied } = useCopy();
const sliceLength = maxSize / 2;
const trimmed = text.length > maxSize ? `${text.slice(0, sliceLength)}...${text.slice(-sliceLength)}` : text;
return (
<div className={`copy${className ? ` ${className}` : ""}`} onClick={() => copy(text)}>
<span className="body">{trimmed}</span>
<span className="icon" style={{ color: copied ? "var(--success)" : "var(--highlight)" }}>
{copied ? <Icon name="check" size={14} /> : <Icon name="copy" size={14} />}
</span>
</div>
);
}

View File

@ -1,4 +1,4 @@
import { EventKind, EventPublisher } from "@snort/system";
import { EventKind } from "@snort/system";
import { useLogin } from "hooks/login";
import useFollows from "hooks/follows";
import AsyncButton from "element/async-button";
@ -12,10 +12,11 @@ export function LoggedInFollowButton({
pubkey: string;
}) {
const { contacts, relays } = useFollows(loggedIn, true);
const login = useLogin();
const isFollowing = contacts.find((t) => t.at(1) === pubkey);
async function unfollow() {
const pub = await EventPublisher.nip7();
const pub = login?.publisher();
if (pub) {
const ev = await pub.generic((eb) => {
eb.kind(EventKind.ContactList).content(JSON.stringify(relays));
@ -32,7 +33,7 @@ export function LoggedInFollowButton({
}
async function follow() {
const pub = await EventPublisher.nip7();
const pub = login?.publisher();
if (pub) {
const ev = await pub.generic((eb) => {
eb.kind(EventKind.ContactList).content(JSON.stringify(relays));

View File

@ -0,0 +1,13 @@
.avatar-input {
width: 90px;
height: 90px;
background-color: #aaa;
border-radius: 100%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background-image: var(--img);
background-position: center;
background-size: cover;
}

View File

@ -0,0 +1,138 @@
import "./login-signup.css";
import { CSSProperties, useState } from "react";
import { EventPublisher, UserMetadata } from "@snort/system";
import { schnorr } from "@noble/curves/secp256k1";
import { bytesToHex } from "@noble/curves/abstract/utils";
import AsyncButton from "./async-button";
import { Login, System } from "index";
import { Icon } from "./icon";
import Copy from "./copy";
import { hexToBech32, openFile } from "utils";
import { VoidApi } from "@void-cat/api";
import { upload } from "@testing-library/user-event/dist/upload";
import { LoginType } from "login";
enum Stage {
Login = 0,
Details = 1,
SaveKey = 2
}
export function LoginSignup({ close }: { close: () => void }) {
const [error, setError] = useState("");
const [stage, setStage] = useState(Stage.Login);
const [username, setUsername] = useState("");
const [avatar, setAvatar] = useState("");
const [key, setNewKey] = useState("");
async function doLogin() {
try {
const pub = await EventPublisher.nip7();
if (pub) {
Login.loginWithPubkey(pub.pubKey, LoginType.Nip7);
}
} catch (e) {
console.error(e);
if (e instanceof Error) {
setError(e.message);
} else {
setError(e as string);
}
}
}
function createAccount() {
const newKey = bytesToHex(schnorr.utils.randomPrivateKey());
setNewKey(newKey);
setStage(Stage.Details);
}
function loginWithKey() {
Login.loginWithPrivateKey(key);
close();
}
async function uploadAvatar() {
const file = await openFile();
if (file) {
const VoidCatHost = "https://void.cat"
const api = new VoidApi(VoidCatHost);
const uploader = api.getUploader(file);
const result = await uploader.upload({
"V-Strip-Metadata": "true"
})
if (result.ok) {
const resultUrl = result.file?.metadata?.url ?? `${VoidCatHost}/d/${result.file?.id}`;
setAvatar(resultUrl);
} else {
setError(result.errorMessage ?? "Upload failed");
}
}
}
async function saveProfile() {
const profile = {
name: username,
picture: avatar
} as UserMetadata;
const pub = EventPublisher.privateKey(key);
const ev = await pub.metadata(profile);
console.debug(ev);
System.BroadcastEvent(ev);
setStage(Stage.SaveKey);
}
switch (stage) {
case Stage.Login: {
return <>
<h2>Login</h2>
{"nostr" in window &&
<AsyncButton type="button" className="btn btn-primary" onClick={doLogin}>
Nostr Extension
</AsyncButton>}
<button type="button" className="btn btn-primary" onClick={createAccount}>
Create Account
</button>
{error && <b className="error">{error}</b>}
</>
}
case Stage.Details: {
return <>
<h2>Setup Profile</h2>
<div className="flex f-center">
<div className="avatar-input" onClick={uploadAvatar} style={{
"--img": `url(${avatar})`
} as CSSProperties}>
<Icon name="camera-plus" />
</div>
</div>
<div>
<div className="paper">
<input type="text" placeholder="Username" value={username} onChange={e => setUsername(e.target.value)} />
</div>
<small>You can change this later</small>
</div>
<AsyncButton type="button" className="btn btn-primary" onClick={saveProfile}>
Save
</AsyncButton>
</>
}
case Stage.SaveKey: {
return <>
<h2>Save Key</h2>
<p>
Nostr uses private keys, please save yours, if you lose this key you wont be able to login to your account anymore!
</p>
<div className="paper">
<Copy text={hexToBech32("nsec", key)} />
</div>
<button type="button" className="btn btn-primary" onClick={loginWithKey}>
Ok, it's safe
</button>
</>
}
}
}

View File

@ -2,14 +2,12 @@ import "./new-goal.css";
import * as Dialog from "@radix-ui/react-dialog";
import AsyncButton from "./async-button";
import { NostrLink, EventPublisher } from "@snort/system";
import { unixNow } from "@snort/shared";
import { NostrLink } from "@snort/system";
import { Icon } from "element/icon";
import { useEffect, useState } from "react";
import { eventLink } from "utils";
import { NostrProviderDialog } from "./nostr-provider-dialog";
import { useState } from "react";
import { System } from "index";
import { GOAL } from "const";
import { useLogin } from "hooks/login";
interface NewGoalDialogProps {
link: NostrLink;
@ -17,12 +15,13 @@ interface NewGoalDialogProps {
export function NewGoalDialog({ link }: NewGoalDialogProps) {
const [open, setOpen] = useState(false);
const login = useLogin();
const [goalAmount, setGoalAmount] = useState("");
const [goalName, setGoalName] = useState("");
async function publishGoal() {
const pub = await EventPublisher.nip7();
const pub = login?.publisher();
if (pub) {
const evNew = await pub.generic((eb) => {
eb.kind(GOAL)

View File

@ -4,19 +4,6 @@
flex-direction: column;
}
.send-zap h3 {
font-size: 24px;
font-weight: 500;
margin: 0;
}
.send-zap small {
display: block;
text-transform: uppercase;
color: #868686;
margin-bottom: 12px;
}
.send-zap .amounts {
display: grid;
grid-template-columns: repeat(4, 1fr);
@ -37,10 +24,6 @@
background: #353535;
}
.send-zap div.paper {
background: #262626;
}
.send-zap p {
margin: 0 0 8px 0;
font-weight: 500;

View File

@ -11,6 +11,7 @@ import { Icon } from "./icon";
import AsyncButton from "./async-button";
import { Relays } from "index";
import QrCode from "./qr-code";
import { useLogin } from "hooks/login";
export interface LNURLLike {
get name(): string;
@ -52,6 +53,7 @@ export function SendZaps({
const [amount, setAmount] = useState(satsAmounts[0]);
const [comment, setComment] = useState("");
const [invoice, setInvoice] = useState("");
const login = useLogin();
const name = targetName ?? svc?.name;
async function loadService(lnurl: string) {
@ -72,7 +74,7 @@ export function SendZaps({
async function send() {
if (!svc) return;
let pub = await EventPublisher.nip7();
let pub = login?.publisher();
let isAnon = false;
if (!pub) {
pub = EventPublisher.privateKey(bytesToHex(secp256k1.utils.randomPrivateKey()));

View File

@ -1,12 +1,13 @@
import "./stream-editor.css";
import { useEffect, useState, useCallback } from "react";
import { EventPublisher, NostrEvent } from "@snort/system";
import { NostrEvent } from "@snort/system";
import { unixNow } from "@snort/shared";
import { TagsInput } from "react-tag-input-component";
import AsyncButton from "./async-button";
import { StreamState } from "../index";
import { findTag } from "../utils";
import { useLogin } from "hooks/login";
export interface StreamEditorProps {
ev?: NostrEvent;
@ -32,6 +33,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
const [tags, setTags] = useState<string[]>([]);
const [contentWarning, setContentWarning] = useState(false);
const [isValid, setIsValid] = useState(false);
const login = useLogin();
useEffect(() => {
setTitle(findTag(ev, "title") ?? "");
@ -62,7 +64,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
}, [validate, title, summary, image, stream]);
async function publishStream() {
const pub = await EventPublisher.nip7();
const pub = login?.publisher();
if (pub) {
const evNew = await pub.generic((eb) => {
const now = unixNow();
@ -83,7 +85,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
for (const tx of tags) {
eb.tag(["t", tx.trim()]);
}
if(contentWarning) {
if (contentWarning) {
eb.tag(["content-warning", "nsfw"])
}
return eb;

View File

@ -1,8 +1,7 @@
import { NostrLink, EventPublisher, EventKind } from "@snort/system";
import { useRef, useState, useMemo, ChangeEvent } from "react";
import { NostrLink, EventKind } from "@snort/system";
import { useRef, useState, ChangeEvent } from "react";
import { LIVE_STREAM_CHAT } from "../const";
import useEmoji, { packId } from "../hooks/emoji";
import { useLogin } from "../hooks/login";
import { System } from "../index";
import AsyncButton from "./async-button";
@ -32,7 +31,7 @@ export function WriteMessage({
const leftOffset = ref.current?.getBoundingClientRect().left;
async function sendChatMessage() {
const pub = await EventPublisher.nip7();
const pub = login?.publisher();
if (chat.length > 1) {
let emojiNames = new Set();

20
src/hooks/copy.tsx Normal file
View File

@ -0,0 +1,20 @@
import { useState } from "react";
export const useCopy = (timeout = 2000) => {
const [error, setError] = useState(false);
const [copied, setCopied] = useState(false);
const copy = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopied(true);
setError(false);
} catch (error) {
setError(true);
}
setTimeout(() => setCopied(false), timeout);
};
return { error, copied, copy };
};

View File

@ -1,9 +1,17 @@
import { Login } from "index";
import { getPublisher } from "login";
import { useSyncExternalStore } from "react";
export function useLogin() {
return useSyncExternalStore(
const session = useSyncExternalStore(
(c) => Login.hook(c),
() => Login.snapshot()
);
if (!session) return;
return {
...session,
publisher: () => {
return getPublisher(session);
}
}
}

View File

@ -33,6 +33,11 @@ a {
flex-direction: column;
}
.f-center {
align-items: center;
justify-content: center;
}
.pill {
background: #171717;
padding: 4px 8px;
@ -164,4 +169,26 @@ div.paper {
.border-warning {
border: 1px solid #FF563F;
}
.dialog-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.dialog-content div.paper {
background: #262626;
}
.dialog-content h3 {
font-size: 24px;
font-weight: 500;
margin: 0;
}
.dialog-content small {
display: block;
color: #868686;
margin: 6px;
}

View File

@ -1,7 +1,17 @@
import { bytesToHex } from "@noble/curves/abstract/utils";
import { schnorr } from "@noble/curves/secp256k1";
import { ExternalStore } from "@snort/shared";
import { EventPublisher, Nip7Signer, PrivateKeySigner } from "@snort/system";
export enum LoginType {
Nip7 = "nip7",
PrivateKey = "private-key"
}
export interface LoginSession {
type: LoginType;
pubkey: string;
privateKey?: string;
follows: string[];
}
@ -13,19 +23,49 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
const json = window.localStorage.getItem("session");
if (json) {
this.#session = JSON.parse(json);
if (this.#session) {
this.#session.type ??= LoginType.Nip7;
}
}
}
loginWithPubkey(pk: string) {
loginWithPubkey(pk: string, type = LoginType.Nip7) {
this.#session = {
type,
pubkey: pk,
follows: [],
};
window.localStorage.setItem("session", JSON.stringify(this.#session));
this.notifyChange();
this.#save();
}
loginWithPrivateKey(key: string) {
this.#session = {
type: LoginType.PrivateKey,
pubkey: bytesToHex(schnorr.getPublicKey(key)),
privateKey: key,
follows: [],
};
this.#save();
}
takeSnapshot() {
return this.#session ? { ...this.#session } : undefined;
}
#save() {
window.localStorage.setItem("session", JSON.stringify(this.#session));
this.notifyChange();
}
}
export function getPublisher(session: LoginSession) {
switch (session?.type) {
case LoginType.Nip7: {
return new EventPublisher(new Nip7Signer(), session.pubkey);
}
case LoginType.PrivateKey: {
return new EventPublisher(new PrivateKeySigner(session.privateKey!), session.pubkey);
}
}
}

View File

@ -1,27 +1,19 @@
import { Icon } from "element/icon";
import "./layout.css";
import {
EventPublisher,
} from "@snort/system";
import { useState } from "react";
import * as Dialog from "@radix-ui/react-dialog";
import { Outlet, useNavigate, useLocation, Link } from "react-router-dom";
import AsyncButton from "element/async-button";
import { Login } from "index";
import { Icon } from "element/icon";
import { useLogin } from "hooks/login";
import { Profile } from "element/profile";
import { NewStreamDialog } from "element/new-stream";
import { useState } from "react";
import { LoginSignup } from "element/login-signup";
export function LayoutPage() {
const navigate = useNavigate();
const login = useLogin();
const location = useLocation();
async function doLogin() {
const pub = await EventPublisher.nip7();
if (pub) {
Login.loginWithPubkey(pub.pubKey);
}
}
const [showLogin, setShowLogin] = useState(true);
function loggedIn() {
if (!login) return;
@ -43,14 +35,20 @@ export function LayoutPage() {
function loggedOut() {
if (login) return;
return (
<>
<AsyncButton type="button" className="btn btn-border" onClick={doLogin}>
return <Dialog.Root open={showLogin} onOpenChange={setShowLogin}>
<Dialog.Trigger asChild>
<button type="button" className="btn btn-border" onClick={() => setShowLogin(true)}>
Login
<Icon name="login" />
</AsyncButton>
</>
);
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<LoginSignup close={() => setShowLogin(false)} />
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
}
const isNsfw = window.location.pathname === "/nsfw";
@ -83,6 +81,7 @@ export function LayoutPage() {
</header>
<Outlet />
{isNsfw && <ContentWarningOverlay />}
</div>
);
}

View File

@ -1,10 +1,11 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import AsyncButton from "element/async-button";
import { StatePill } from "element/state-pill";
import { StreamState } from "index";
import { StreamProviderInfo, StreamProviderStore } from "providers";
import { Nip103StreamProvider } from "providers/nip103";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
export function ConfigureNostrType() {
const [url, setUrl] = useState("");

View File

@ -1,5 +1,5 @@
import "./stream-page.css";
import { parseNostrLink, TaggedRawEvent, EventPublisher } from "@snort/system";
import { parseNostrLink, TaggedRawEvent } from "@snort/system";
import { useNavigate, useParams } from "react-router-dom";
import useEventFeed from "hooks/event-feed";
@ -31,7 +31,7 @@ function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedRawEvent }) {
const isMine = ev?.pubkey === login?.pubkey;
async function deleteStream() {
const pub = await EventPublisher.nip7();
const pub = login?.publisher();
if (pub && ev) {
const evDelete = await pub.delete(ev.id);
console.debug(evDelete);

View File

@ -1,5 +1,7 @@
import { StreamProvider, StreamProviderInfo, StreamProviders } from ".";
import { EventPublisher, EventKind, NostrEvent } from "@snort/system";
import { EventKind, NostrEvent } from "@snort/system";
import { Login } from "index";
import { getPublisher } from "login";
import { findTag } from "utils";
export class Nip103StreamProvider implements StreamProvider {
@ -59,8 +61,9 @@ export class Nip103StreamProvider implements StreamProvider {
}
async #getJson<T>(method: "GET" | "POST" | "PATCH", path: string, body?: unknown): Promise<T> {
const pub = await EventPublisher.nip7();
if (!pub) throw new Error("No event publisher");
const login = Login.snapshot();
const pub = login && getPublisher(login);
if (!pub) throw new Error("No signer");
const u = `${this.#url}${path}`;
const token = await pub.generic(eb => {

View File

@ -55,4 +55,20 @@ export function eventLink(ev: NostrEvent) {
export function getHost(ev?: NostrEvent) {
return ev?.tags.find(a => a[0] === "p" && a[3] === "host")?.[1] ?? ev?.pubkey ?? "";
}
export async function openFile(): Promise<File | undefined> {
return new Promise(resolve => {
const elm = document.createElement("input");
elm.type = "file";
elm.onchange = (e: Event) => {
const elm = e.target as HTMLInputElement;
if (elm.files) {
resolve(elm.files[0]);
} else {
resolve(undefined);
}
};
elm.click();
});
}

View File

@ -123,6 +123,7 @@ const config = {
resolve: {
extensions: [".tsx", ".ts", ".jsx", ".js", "..."],
modules: ["node_modules", __dirname, path.resolve(__dirname, "src")],
fallback: { "crypto": false }
},
};

View File

@ -2011,6 +2011,13 @@
"@typescript-eslint/types" "5.59.0"
eslint-visitor-keys "^3.3.0"
"@void-cat/api@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@void-cat/api/-/api-1.0.7.tgz#39564d478dee07398826e7109d6368c68c405426"
integrity sha512-0K20PaLnRL0lYLOOn8Sk3J6THdU7ebIHWPR7S8Ytzdi5VGI8468ocackCs0b/gFZvvkwVp0X/Rygxe1/nhch+Q==
dependencies:
sjcl "^1.0.8"
"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5":
version "1.11.6"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24"
@ -5861,6 +5868,11 @@ sirv@^1.0.7:
mrmime "^1.0.0"
totalist "^1.0.0"
sjcl@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/sjcl/-/sjcl-1.0.8.tgz#f2ec8d7dc1f0f21b069b8914a41a8f236b0e252a"
integrity sha512-LzIjEQ0S0DpIgnxMEayM1rq9aGwGRG4OnZhCdjx7glTaJtf4zRfpg87ImfjSJjoW9vKpagd82McDOwbRT5kQKQ==
slash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"