Signup flow
This commit is contained in:
@ -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
11
src/element/copy.css
Normal 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
23
src/element/copy.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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));
|
||||
|
13
src/element/login-signup.css
Normal file
13
src/element/login-signup.css
Normal 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;
|
||||
}
|
138
src/element/login-signup.tsx
Normal file
138
src/element/login-signup.tsx
Normal 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>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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;
|
||||
|
@ -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()));
|
||||
|
@ -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;
|
||||
|
@ -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
20
src/hooks/copy.tsx
Normal 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 };
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
46
src/login.ts
46
src/login.ts
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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("");
|
||||
|
@ -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);
|
||||
|
@ -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 => {
|
||||
|
16
src/utils.ts
16
src/utils.ts
@ -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();
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user