parent
c1a018820f
commit
7d3f21da84
@ -16,7 +16,6 @@
|
||||
"@sqlite.org/sqlite-wasm": "^3.45.1-build1",
|
||||
"@szhsin/react-menu": "^4.1.0",
|
||||
"@types/webscopeio__react-textarea-autocomplete": "^4.7.5",
|
||||
"@void-cat/api": "^1.0.12",
|
||||
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.5.1",
|
||||
|
@ -1,67 +1,57 @@
|
||||
import { VoidApi } from "@void-cat/api";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { Layer2Button } from "./buttons";
|
||||
import { openFile } from "@/utils";
|
||||
import { Icon } from "./icon";
|
||||
|
||||
const voidCatHost = "https://void.cat";
|
||||
const fileExtensionRegex = /\.([\w]{1,7})$/i;
|
||||
const voidCatApi = new VoidApi(voidCatHost);
|
||||
|
||||
type UploadResult = {
|
||||
url?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
async function voidCatUpload(file: File): Promise<UploadResult> {
|
||||
const uploader = voidCatApi.getUploader(file);
|
||||
|
||||
const rsp = await uploader.upload({
|
||||
"V-Strip-Metadata": "true",
|
||||
});
|
||||
if (rsp.ok) {
|
||||
let ext = file.name.match(fileExtensionRegex);
|
||||
if (rsp.file?.metadata?.mimeType === "image/webp") {
|
||||
ext = ["", "webp"];
|
||||
}
|
||||
const resultUrl = rsp.file?.metadata?.url ?? `${voidCatHost}/d/${rsp.file?.id}${ext ? `.${ext[1]}` : ""}`;
|
||||
|
||||
const ret = {
|
||||
url: resultUrl,
|
||||
} as UploadResult;
|
||||
|
||||
return ret;
|
||||
} else {
|
||||
return {
|
||||
error: rsp.errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
import { useMediaServerList } from "@/hooks/media-servers";
|
||||
import { Nip96Server } from "@/service/upload/nip96";
|
||||
import { useLogin } from "@/hooks/login";
|
||||
import { ReactNode } from "react";
|
||||
import { EventPublisher } from "@snort/system";
|
||||
|
||||
interface FileUploaderProps {
|
||||
onResult(url: string | undefined): void;
|
||||
onError(e: string | Error): void;
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
publisher?: EventPublisher;
|
||||
}
|
||||
|
||||
export function FileUploader({ onResult }: FileUploaderProps) {
|
||||
async function uploadFile() {
|
||||
export function FileUploader({ onResult, onError, children, className, publisher }: FileUploaderProps) {
|
||||
const servers = useMediaServerList();
|
||||
const pub = publisher ?? useLogin()?.publisher?.();
|
||||
|
||||
async function uploadFile(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
const file = await openFile();
|
||||
if (file) {
|
||||
if (file && pub) {
|
||||
try {
|
||||
const upload = await voidCatUpload(file);
|
||||
const server = new Nip96Server(servers.servers[0], pub);
|
||||
const upload = await server.upload(file, file.name);
|
||||
if (upload.url) {
|
||||
onResult(upload.url);
|
||||
}
|
||||
if (upload.error) {
|
||||
console.error(upload.error);
|
||||
onError(upload.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Error) {
|
||||
onError(error);
|
||||
} else {
|
||||
onError(new Error("Unknown error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (children) {
|
||||
return (
|
||||
<div onClick={uploadFile} className={className}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Layer2Button onClick={uploadFile}>
|
||||
<Layer2Button onClick={uploadFile} className={className}>
|
||||
<FormattedMessage defaultMessage="Upload" />
|
||||
<Icon name="upload" size={14} />
|
||||
</Layer2Button>
|
||||
|
@ -11,21 +11,20 @@ import LoginWallet2x from "../login-wallet@2x.jpg";
|
||||
|
||||
import { useContext, useState } from "react";
|
||||
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||
import { EventPublisher, UserMetadata } from "@snort/system";
|
||||
import { EventPublisher, PrivateKeySigner, UserMetadata } from "@snort/system";
|
||||
import { schnorr } from "@noble/curves/secp256k1";
|
||||
import { bytesToHex } from "@noble/curves/abstract/utils";
|
||||
import { LNURL, bech32ToHex, getPublicKey, hexToBech32 } from "@snort/shared";
|
||||
import { VoidApi } from "@void-cat/api";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
import { Login, LoginType } from "@/login";
|
||||
import { Icon } from "./icon";
|
||||
import Copy from "./copy";
|
||||
import { openFile } from "@/utils";
|
||||
import { DefaultProvider, StreamProviderInfo } from "@/providers";
|
||||
import { NostrStreamProvider } from "@/providers/zsz";
|
||||
import { DefaultButton, Layer1Button } from "./buttons";
|
||||
import { ExternalLink } from "./external-link";
|
||||
import { FileUploader } from "./file-uploader";
|
||||
|
||||
enum Stage {
|
||||
Login = 0,
|
||||
@ -46,6 +45,7 @@ export function LoginSignup({ close }: { close: () => void }) {
|
||||
const [key, setNewKey] = useState("");
|
||||
const { formatMessage } = useIntl();
|
||||
const hasNostrExtension = "nostr" in window && window.nostr;
|
||||
const signer = key ? new PrivateKeySigner(key) : undefined;
|
||||
|
||||
function doLoginNsec() {
|
||||
try {
|
||||
@ -89,34 +89,6 @@ export function LoginSignup({ close }: { close: () => void }) {
|
||||
close();
|
||||
}
|
||||
|
||||
async function uploadAvatar() {
|
||||
const defaultError = formatMessage({
|
||||
defaultMessage: "Avatar upload fialed",
|
||||
id: "uTonxS",
|
||||
});
|
||||
|
||||
try {
|
||||
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",
|
||||
});
|
||||
console.debug(result);
|
||||
if (result.ok) {
|
||||
const resultUrl = result.file?.metadata?.url ?? `${VoidCatHost}/d/${result.file?.id}`;
|
||||
setAvatar(resultUrl);
|
||||
} else {
|
||||
setError(result.errorMessage ?? defaultError);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setError(defaultError);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupProfile() {
|
||||
const px = new NostrStreamProvider(DefaultProvider.name, DefaultProvider.url, EventPublisher.privateKey(key));
|
||||
const info = await px.info();
|
||||
@ -248,13 +220,17 @@ export function LoginSignup({ close }: { close: () => void }) {
|
||||
<h2>
|
||||
<FormattedMessage defaultMessage="Setup Profile" />
|
||||
</h2>
|
||||
<div className="relative mx-auto w-[100px] h-[100px] rounded-full overflow-hidden">
|
||||
<div className="relative mx-auto w-[100px] h-[100px] rounded-full overflow-hidden bg-layer-3">
|
||||
{avatar && <img className="absolute object-fit w-full h-full" src={avatar} />}
|
||||
<div
|
||||
className="absolute flex items-center justify-center w-full h-full hover:opacity-100 opacity-0 transition bg-layer-2/50 cursor-pointer"
|
||||
onClick={uploadAvatar}>
|
||||
<Icon name="camera-plus" />
|
||||
</div>
|
||||
{signer && (
|
||||
<FileUploader
|
||||
publisher={new EventPublisher(signer, signer.getPubKey())}
|
||||
onResult={e => setAvatar(e ?? "")}
|
||||
onError={e => setError(e.toString())}
|
||||
className="absolute flex items-center justify-center w-full h-full hover:opacity-30 opacity-0 transition bg-black cursor-pointer">
|
||||
<Icon name="camera-plus" />
|
||||
</FileUploader>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
|
@ -4,9 +4,9 @@ import { LNURL, fetchNip05Pubkey } from "@snort/shared";
|
||||
import { mapEventToProfile } from "@snort/system";
|
||||
import { SnortContext, useUserProfile } from "@snort/system-react";
|
||||
import { useLogin } from "@/hooks/login";
|
||||
import { debounce, openFile } from "@/utils";
|
||||
import { debounce } from "@/utils";
|
||||
import { PrimaryButton } from "./buttons";
|
||||
import { VoidApi } from "@void-cat/api";
|
||||
import { FileUploader } from "./file-uploader";
|
||||
|
||||
const MaxUsernameLength = 100;
|
||||
const MaxAboutLength = 500;
|
||||
@ -116,35 +116,6 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
|
||||
});
|
||||
}, [formatMessage, login?.pubkey, nip05]);
|
||||
|
||||
async function uploadAvatar() {
|
||||
const defaultError = formatMessage({
|
||||
defaultMessage: "Avatar upload fialed",
|
||||
id: "uTonxS",
|
||||
});
|
||||
|
||||
setError(undefined);
|
||||
try {
|
||||
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",
|
||||
});
|
||||
console.debug(result);
|
||||
if (result.ok) {
|
||||
const resultUrl = result.file?.metadata?.url ?? `${VoidCatHost}/d/${result.file?.id}`;
|
||||
setPicture(resultUrl);
|
||||
} else {
|
||||
setError(new Error(result.errorMessage ?? defaultError));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setError(new Error(defaultError));
|
||||
}
|
||||
}
|
||||
|
||||
async function saveProfile() {
|
||||
// copy user object and delete internal fields
|
||||
const userCopy = {
|
||||
@ -225,11 +196,12 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="mx-auto relative flex items-center justify-center w-40 h-40 aspect-square rounded-full overflow-hidden">
|
||||
<img className="absolute w-full h-full object-cover" src={picture} />
|
||||
<div
|
||||
className="flex items-center justify-center absolute w-full h-full opacity-0 hover:opacity-80 bg-foreground cursor-pointer"
|
||||
onClick={() => uploadAvatar()}>
|
||||
<FileUploader
|
||||
onResult={e => setPicture(e ?? "")}
|
||||
onError={e => setError(e instanceof Error ? e : new Error(e))}
|
||||
className="flex items-center justify-center absolute w-full h-full opacity-0 hover:opacity-80 bg-foreground cursor-pointer">
|
||||
<FormattedMessage defaultMessage="Edit" />
|
||||
</div>
|
||||
</FileUploader>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<h4>
|
||||
|
@ -47,6 +47,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
||||
const [goalAmount, setGoalMount] = useState(0);
|
||||
const [game, setGame] = useState<GameInfo>();
|
||||
const [gameId, setGameId] = useState<string>();
|
||||
const [error, setError] = useState("");
|
||||
const login = useLogin();
|
||||
const { formatMessage } = useIntl();
|
||||
const system = useContext(SnortContext);
|
||||
@ -180,8 +181,9 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
||||
{image && <img src={image} className="mb-2 aspect-video object-cover rounded-xl" />}
|
||||
<div className="flex gap-2">
|
||||
<input type="text" placeholder="https://" value={image} onChange={e => setImage(e.target.value)} />
|
||||
<FileUploader onResult={v => setImage(v ?? "")} />
|
||||
<FileUploader onResult={v => setImage(v ?? "")} onError={e => setError(e.toString())} />
|
||||
</div>
|
||||
{error && <b className="text-warning">{error}</b>}
|
||||
</StreamInput>
|
||||
)}
|
||||
{(options?.canSetStream ?? true) && (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useMediaServerList } from "@/hooks/media-servers";
|
||||
import { DefaultMediaServers, useMediaServerList } from "@/hooks/media-servers";
|
||||
import { IconButton, PrimaryButton } from "../buttons";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useState } from "react";
|
||||
@ -6,13 +6,6 @@ import { sanitizeRelayUrl } from "@snort/shared";
|
||||
|
||||
export function ServerList() {
|
||||
const [newServer, setNewServer] = useState("");
|
||||
|
||||
const defaultServers = [
|
||||
//"https://media.zap.stream",
|
||||
"https://nostr.build/",
|
||||
"https://nostrcheck.me/",
|
||||
"https://files.v0l.io/",
|
||||
];
|
||||
const servers = useMediaServerList();
|
||||
|
||||
async function tryAddServer(s: string) {
|
||||
@ -51,7 +44,7 @@ export function ServerList() {
|
||||
<h4>
|
||||
<FormattedMessage defaultMessage="Suggested Servers" />
|
||||
</h4>
|
||||
{defaultServers
|
||||
{DefaultMediaServers.map(a => a.value[1])
|
||||
.filter(a => !servers.servers.includes(a))
|
||||
.map(a => (
|
||||
<div className="flex items-center justify-between py-2 px-3 bg-layer-2 rounded-xl" key={a}>
|
||||
|
@ -4,10 +4,20 @@ import { removeUndefined, sanitizeRelayUrl } from "@snort/shared";
|
||||
import { Nip96Server } from "@/service/upload/nip96";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const DefaultMediaServers = [
|
||||
//"https://media.zap.stream",
|
||||
new UnknownTag(["server", "https://nostr.build/"]),
|
||||
new UnknownTag(["server", "https://nostrcheck.me/"]),
|
||||
new UnknownTag(["server", "https://files.v0l.io/"]),
|
||||
];
|
||||
|
||||
export function useMediaServerList() {
|
||||
const login = useLogin();
|
||||
|
||||
const servers = login?.state?.getList(EventKind.StorageServerList) ?? [];
|
||||
let servers = login?.state?.getList(EventKind.StorageServerList) ?? [];
|
||||
if (servers.length === 0) {
|
||||
servers = DefaultMediaServers;
|
||||
}
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
|
@ -906,9 +906,6 @@
|
||||
"u6uD94": {
|
||||
"defaultMessage": "Create an Account"
|
||||
},
|
||||
"uTonxS": {
|
||||
"defaultMessage": "Avatar upload fialed"
|
||||
},
|
||||
"uYw2LD": {
|
||||
"defaultMessage": "Stream"
|
||||
},
|
||||
|
@ -26,7 +26,7 @@ export default function DashboardIntroFinal() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mx-auto flex flex-col items-center">
|
||||
<StepHeader />
|
||||
<div className="flex flex-col gap-4 w-[30rem]">
|
||||
<h2 className="text-center">
|
||||
|
@ -12,6 +12,7 @@ export default function DashboardIntroStep1() {
|
||||
const [title, setTitle] = useState<string>();
|
||||
const [summary, setDescription] = useState<string>();
|
||||
const [image, setImage] = useState<string>();
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
DefaultProvider.info().then(i => {
|
||||
@ -48,8 +49,9 @@ export default function DashboardIntroStep1() {
|
||||
onChange={e => setImage(e.target.value)}
|
||||
placeholder={formatMessage({ defaultMessage: "Cover image URL (optional)" })}
|
||||
/>
|
||||
<FileUploader onResult={setImage} />
|
||||
<FileUploader onResult={setImage} onError={e => setError(e.toString())} />
|
||||
</div>
|
||||
{error && <b className="text-warning">{error}</b>}
|
||||
<small className="text-layer-4">
|
||||
<FormattedMessage defaultMessage="Recommended size: 1920x1080 (16:9)" />
|
||||
</small>
|
||||
|
@ -3,11 +3,12 @@ import StepHeader from "./step-header";
|
||||
import { DefaultButton } from "@/element/buttons";
|
||||
import { DefaultProvider, StreamProviderForward } from "@/providers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { AddForwardInputs } from "@/element/provider/nostr/fowards";
|
||||
|
||||
export default function DashboardIntroStep3() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [forwards, setForwards] = useState<Array<StreamProviderForward>>([]);
|
||||
|
||||
async function loadInfo() {
|
||||
@ -21,7 +22,7 @@ export default function DashboardIntroStep3() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mx-auto flex flex-col items-center">
|
||||
<StepHeader />
|
||||
<div className="flex flex-col gap-4 w-[30rem]">
|
||||
<h2 className="text-center">
|
||||
@ -50,7 +51,9 @@ export default function DashboardIntroStep3() {
|
||||
<AddForwardInputs provider={DefaultProvider} onAdd={loadInfo} />
|
||||
<DefaultButton
|
||||
onClick={async () => {
|
||||
navigate("/dashboard/step-4");
|
||||
navigate("/dashboard/step-4", {
|
||||
state: location.state,
|
||||
});
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Continue" />
|
||||
</DefaultButton>
|
||||
|
@ -3,7 +3,7 @@ import StepHeader from "./step-header";
|
||||
import { DefaultButton } from "@/element/buttons";
|
||||
import { DefaultProvider } from "@/providers";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { GoalSelector } from "@/element/stream-editor/goal-selector";
|
||||
import AmountInput from "@/element/amount-input";
|
||||
import { useLogin } from "@/hooks/login";
|
||||
@ -12,6 +12,7 @@ import { SnortContext } from "@snort/system-react";
|
||||
|
||||
export default function DashboardIntroStep4() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [goalName, setGoalName] = useState("");
|
||||
const [goalAmount, setGoalMount] = useState(0);
|
||||
const [goal, setGoal] = useState<string>();
|
||||
@ -30,7 +31,7 @@ export default function DashboardIntroStep4() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="mx-auto flex flex-col items-center">
|
||||
<StepHeader />
|
||||
<div className="flex flex-col gap-4 w-[30rem]">
|
||||
<h2 className="text-center">
|
||||
@ -67,15 +68,27 @@ export default function DashboardIntroStep4() {
|
||||
.content(goalName);
|
||||
});
|
||||
await system.BroadcastEvent(goalEvent);
|
||||
await DefaultProvider.updateStream({
|
||||
const newState = {
|
||||
...location.state,
|
||||
goal: goalEvent.id,
|
||||
};
|
||||
await DefaultProvider.updateStream(newState);
|
||||
navigate("/dashboard/final", {
|
||||
state: newState,
|
||||
});
|
||||
navigate("/dashboard/final");
|
||||
} else if (goal) {
|
||||
await DefaultProvider.updateStream({
|
||||
const newState = {
|
||||
...location.state,
|
||||
goal,
|
||||
};
|
||||
await DefaultProvider.updateStream(newState);
|
||||
navigate("/dashboard/final", {
|
||||
state: newState,
|
||||
});
|
||||
} else {
|
||||
navigate("/dashboard/final", {
|
||||
state: location.state,
|
||||
});
|
||||
navigate("/dashboard/final");
|
||||
}
|
||||
}}>
|
||||
<FormattedMessage defaultMessage="Continue" />
|
||||
|
@ -298,7 +298,6 @@
|
||||
"twCRVi": "Art",
|
||||
"tzMNF3": "Status",
|
||||
"u6uD94": "Create an Account",
|
||||
"uTonxS": "Avatar upload fialed",
|
||||
"uYw2LD": "Stream",
|
||||
"uksRSi": "Latest Videos",
|
||||
"vP4dFa": "Visit {link} to get some sweet zap.stream merch!",
|
||||
|
17
yarn.lock
17
yarn.lock
@ -3245,15 +3245,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@void-cat/api@npm:^1.0.12":
|
||||
version: 1.0.12
|
||||
resolution: "@void-cat/api@npm:1.0.12"
|
||||
dependencies:
|
||||
sjcl: "npm:^1.0.8"
|
||||
checksum: 10c0/37d67e077a1ece0509a669d2ebd5a1edc26773b25417287ce333e0f854ae1c073ea3ca4e311c137faf2b9c0985a0ae630e52cbdd6f431cfe1691d42d2ebe0acc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@webbtc/webln-types@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@webbtc/webln-types@npm:3.0.0"
|
||||
@ -7502,13 +7493,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sjcl@npm:^1.0.8":
|
||||
version: 1.0.8
|
||||
resolution: "sjcl@npm:1.0.8"
|
||||
checksum: 10c0/deac03b931a26b710a521fc9dd5a10b124fb63970b7f5e22a91460ac46b14dd4ccfc2edbee4d553b8d586da332d9013a9ff40f5adfdb58050b73ebcd90235cba
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"slash@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "slash@npm:3.0.0"
|
||||
@ -7652,7 +7636,6 @@ __metadata:
|
||||
"@typescript-eslint/eslint-plugin": "npm:^7.9.0"
|
||||
"@typescript-eslint/parser": "npm:^7.9.0"
|
||||
"@vitejs/plugin-react": "npm:^4.2.1"
|
||||
"@void-cat/api": "npm:^1.0.12"
|
||||
"@webbtc/webln-types": "npm:^3.0.0"
|
||||
"@webscopeio/react-textarea-autocomplete": "npm:^4.9.2"
|
||||
autoprefixer: "npm:^10.4.19"
|
||||
|
Loading…
x
Reference in New Issue
Block a user