Signup flow

This commit is contained in:
2023-07-20 12:33:00 +01:00
parent a1605e31d5
commit c74bbae5c7
24 changed files with 389 additions and 77 deletions

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();