Login
This commit is contained in:
parent
9c88f2e28f
commit
6f0dc2e430
@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@snort/system-react": "^1.0.2",
|
||||
"@snort/system-react": "^1.0.3",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^13.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
@ -39,6 +39,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||
"typescript": "^5.1.3"
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import AsyncButton from "./async-button";
|
||||
import { Profile } from "./profile";
|
||||
import { Icon } from "./icon";
|
||||
import Spinner from "./spinner";
|
||||
import { useLogin } from "hooks/login";
|
||||
|
||||
export interface LiveChatOptions {
|
||||
canWrite?: boolean,
|
||||
@ -17,6 +18,7 @@ export interface LiveChatOptions {
|
||||
export function LiveChat({ link, options }: { link: NostrLink, options?: LiveChatOptions }) {
|
||||
const [chat, setChat] = useState("");
|
||||
const messages = useLiveChatFeed(link);
|
||||
const login = useLogin();
|
||||
|
||||
async function sendChatMessage() {
|
||||
const pub = await EventPublisher.nip7();
|
||||
@ -35,6 +37,31 @@ export function LiveChat({ link, options }: { link: NostrLink, options?: LiveCha
|
||||
setChat("");
|
||||
}
|
||||
}
|
||||
|
||||
function writeMessage() {
|
||||
return <>
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
autoFocus={false}
|
||||
onChange={v => setChat(v.target.value)}
|
||||
value={chat}
|
||||
placeholder="Message"
|
||||
onKeyDown={async e => {
|
||||
if (e.code === "Enter") {
|
||||
e.preventDefault();
|
||||
await sendChatMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Icon name="message" size={15} />
|
||||
</div>
|
||||
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
|
||||
Send
|
||||
</AsyncButton>
|
||||
</>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="live-chat">
|
||||
{(options?.showHeader ?? true) && <div className="header">
|
||||
@ -49,25 +76,7 @@ export function LiveChat({ link, options }: { link: NostrLink, options?: LiveCha
|
||||
{messages.data === undefined && <Spinner />}
|
||||
</div>
|
||||
{(options?.canWrite ?? true) && <div className="write-message">
|
||||
<div>
|
||||
<input
|
||||
type="text"
|
||||
autoFocus={false}
|
||||
onChange={v => setChat(v.target.value)}
|
||||
value={chat}
|
||||
placeholder="Message"
|
||||
onKeyDown={async e => {
|
||||
if (e.code === "Enter") {
|
||||
e.preventDefault();
|
||||
await sendChatMessage();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Icon name="message" size={15} />
|
||||
</div>
|
||||
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
|
||||
Send
|
||||
</AsyncButton>
|
||||
{login ? writeMessage() : <p>Please login to write messages!</p>}
|
||||
</div>}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,12 +1,24 @@
|
||||
import "./profile.css";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { UserMetadata } from "@snort/system";
|
||||
import { hexToBech32 } from "@snort/shared";
|
||||
import { System } from "index";
|
||||
|
||||
export function Profile({ pubkey }: { pubkey: string }) {
|
||||
export interface ProfileOptions {
|
||||
showName?: boolean,
|
||||
suffix?: string
|
||||
}
|
||||
|
||||
export function getName(pk: string, user?: UserMetadata) {
|
||||
const shortPubkey = hexToBech32("npub", pk).slice(0, 12);
|
||||
return user?.display_name ?? user?.name ?? shortPubkey
|
||||
}
|
||||
|
||||
export function Profile({ pubkey, options }: { pubkey: string, options?: ProfileOptions }) {
|
||||
const profile = useUserProfile(System, pubkey);
|
||||
|
||||
return <div className="profile">
|
||||
<img src={profile?.picture} />
|
||||
{profile?.display_name ?? profile?.name}
|
||||
{(options?.showName ?? true) && getName(pubkey, profile)}
|
||||
</div>
|
||||
}
|
6
src/hooks/login.ts
Normal file
6
src/hooks/login.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Login } from "index";
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
export function useLogin() {
|
||||
return useSyncExternalStore(c => Login.hook(c), () => Login.snapshot());
|
||||
}
|
@ -65,4 +65,10 @@ a {
|
||||
.btn-primary {
|
||||
background: linear-gradient(94.73deg, #2BD9FF 0%, #8C8DED 47.4%, #F838D9 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn>span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
@ -7,10 +7,12 @@ import { RouterProvider, createBrowserRouter } from 'react-router-dom';
|
||||
import { LayoutPage } from 'pages/layout';
|
||||
import { StreamPage } from 'pages/stream-page';
|
||||
import { ChatPopout } from 'pages/chat-popout';
|
||||
import { LoginStore } from 'login';
|
||||
|
||||
export const System = new NostrSystem({
|
||||
|
||||
});
|
||||
export const Login = new LoginStore();
|
||||
|
||||
[
|
||||
"wss://relay.snort.social",
|
||||
|
29
src/login.ts
Normal file
29
src/login.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { ExternalStore } from "@snort/shared";
|
||||
|
||||
export interface LoginSession {
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
||||
#session?: LoginSession;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const json = window.localStorage.getItem("session");
|
||||
if (json) {
|
||||
this.#session = JSON.parse(json);
|
||||
}
|
||||
}
|
||||
|
||||
loginWithPubkey(pk: string) {
|
||||
this.#session = {
|
||||
pubkey: pk
|
||||
};
|
||||
window.localStorage.setItem("session", JSON.stringify(this.#session));
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
takeSnapshot() {
|
||||
return this.#session ? { ...this.#session } : undefined;
|
||||
}
|
||||
}
|
@ -54,4 +54,9 @@ header button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
header .profile img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
@ -1,9 +1,48 @@
|
||||
import { Icon } from "element/icon";
|
||||
import "./layout.css";
|
||||
import { EventPublisher } from "@snort/system";
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
import AsyncButton from "element/async-button";
|
||||
import { Login } from "index";
|
||||
import { useLogin } from "hooks/login";
|
||||
import { Profile } from "element/profile";
|
||||
|
||||
export function LayoutPage() {
|
||||
const navigate = useNavigate();
|
||||
const login = useLogin();
|
||||
|
||||
async function doLogin() {
|
||||
const pub = await EventPublisher.nip7();
|
||||
if (pub) {
|
||||
Login.loginWithPubkey(pub.pubKey);
|
||||
}
|
||||
}
|
||||
|
||||
function loggedIn() {
|
||||
if (!login) return;
|
||||
|
||||
return <>
|
||||
<button type="button" className="btn btn-primary">
|
||||
New Stream
|
||||
<Icon name="signal" />
|
||||
</button>
|
||||
<Profile pubkey={login.pubkey} options={{
|
||||
showName: false
|
||||
}} />
|
||||
</>
|
||||
}
|
||||
|
||||
function loggedOut() {
|
||||
if (login) return;
|
||||
|
||||
return <>
|
||||
<AsyncButton type="button" className="btn btn-border" onClick={doLogin}>
|
||||
Login
|
||||
<Icon name="login" />
|
||||
</AsyncButton>
|
||||
</>
|
||||
}
|
||||
|
||||
return <>
|
||||
<header>
|
||||
<div onClick={() => navigate("/")}>
|
||||
@ -14,14 +53,8 @@ export function LayoutPage() {
|
||||
<Icon name="search" size={15} />
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" className="btn btn-primary">
|
||||
New Stream
|
||||
<Icon name="signal" />
|
||||
</button>
|
||||
<button type="button" className="btn btn-border">
|
||||
Login
|
||||
<Icon name="login" />
|
||||
</button>
|
||||
{loggedIn()}
|
||||
{loggedOut()}
|
||||
</div>
|
||||
</header>
|
||||
<Outlet />
|
||||
|
@ -50,10 +50,4 @@
|
||||
.live-page .tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.live-page .btn-primary>span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
32
yarn.lock
32
yarn.lock
@ -81,7 +81,7 @@
|
||||
"@jridgewell/trace-mapping" "^0.3.17"
|
||||
jsesc "^2.5.1"
|
||||
|
||||
"@babel/helper-annotate-as-pure@^7.22.5":
|
||||
"@babel/helper-annotate-as-pure@^7.18.6", "@babel/helper-annotate-as-pure@^7.22.5":
|
||||
version "7.22.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882"
|
||||
integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==
|
||||
@ -106,7 +106,7 @@
|
||||
lru-cache "^5.1.1"
|
||||
semver "^6.3.0"
|
||||
|
||||
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.22.5":
|
||||
"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0", "@babel/helper-create-class-features-plugin@^7.22.5":
|
||||
version "7.22.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz#2192a1970ece4685fbff85b48da2c32fcb130b7c"
|
||||
integrity sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==
|
||||
@ -366,6 +366,16 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703"
|
||||
integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==
|
||||
|
||||
"@babel/plugin-proposal-private-property-in-object@^7.21.11":
|
||||
version "7.21.11"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz#69d597086b6760c4126525cfa154f34631ff272c"
|
||||
integrity sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==
|
||||
dependencies:
|
||||
"@babel/helper-annotate-as-pure" "^7.18.6"
|
||||
"@babel/helper-create-class-features-plugin" "^7.21.0"
|
||||
"@babel/helper-plugin-utils" "^7.20.2"
|
||||
"@babel/plugin-syntax-private-property-in-object" "^7.14.5"
|
||||
|
||||
"@babel/plugin-proposal-unicode-property-regex@^7.4.4":
|
||||
version "7.18.6"
|
||||
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e"
|
||||
@ -1771,19 +1781,19 @@
|
||||
debug "^4.3.4"
|
||||
dexie "^3.2.4"
|
||||
|
||||
"@snort/system-react@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@snort/system-react/-/system-react-1.0.2.tgz#980069bfeba1b6a0ea80cee7a9f4d5522b487191"
|
||||
integrity sha512-K0tCf31SHOeIvmxBhNs1gl4IMZLb+jcKwbm39qv+MLHfYkKonMVjMXV5fTNJ0UFkVdnui9YXZjE/ORAaHHu/Qw==
|
||||
"@snort/system-react@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@snort/system-react/-/system-react-1.0.3.tgz#a64ac4b96f084faab6b19264c4699d523d543927"
|
||||
integrity sha512-7ZoYtmzThjOwJpM1I2UWBKfWRSdXfdZK+r59LSZFuqp5gTzAYKeqF97toQKG+PKbcNlNnqpifTIeNaZHnXkOhw==
|
||||
dependencies:
|
||||
"@snort/shared" "^1.0.1"
|
||||
"@snort/system" "^1.0.7"
|
||||
"@snort/system" "^1.0.8"
|
||||
react "^18.2.0"
|
||||
|
||||
"@snort/system@^1.0.7":
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@snort/system/-/system-1.0.7.tgz#1542e17c3e880e41734e2937a819d460b832c320"
|
||||
integrity sha512-PQqeR794pNlYAaDETO7Ab2mtFQz16bMeObb2eb/F09BtMalfwa3SymjZ0VqkEiT1TgGn+NkTu7bNfOEQOA+/Rg==
|
||||
"@snort/system@^1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@snort/system/-/system-1.0.8.tgz#c4a7000320ebf65374298cc1ed5a0d42103a6557"
|
||||
integrity sha512-IxiD4q3cnW8YEA43a42yxq/OmIeCbXR8Wd1kO59JqZA0xwhhASJQHXxWSuKPXNt6LgkEENPkmlTKY2BIkLvTfg==
|
||||
dependencies:
|
||||
"@noble/curves" "^1.0.0"
|
||||
"@scure/base" "^1.1.1"
|
||||
|
Loading…
x
Reference in New Issue
Block a user