Login
This commit is contained in:
@ -3,7 +3,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@snort/system-react": "^1.0.2",
|
"@snort/system-react": "^1.0.3",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^13.0.0",
|
"@testing-library/react": "^13.0.0",
|
||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^13.2.1",
|
||||||
@ -39,6 +39,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
|
||||||
"typescript": "^5.1.3"
|
"typescript": "^5.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import AsyncButton from "./async-button";
|
|||||||
import { Profile } from "./profile";
|
import { Profile } from "./profile";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import Spinner from "./spinner";
|
import Spinner from "./spinner";
|
||||||
|
import { useLogin } from "hooks/login";
|
||||||
|
|
||||||
export interface LiveChatOptions {
|
export interface LiveChatOptions {
|
||||||
canWrite?: boolean,
|
canWrite?: boolean,
|
||||||
@ -17,6 +18,7 @@ export interface LiveChatOptions {
|
|||||||
export function LiveChat({ link, options }: { link: NostrLink, options?: LiveChatOptions }) {
|
export function LiveChat({ link, options }: { link: NostrLink, options?: LiveChatOptions }) {
|
||||||
const [chat, setChat] = useState("");
|
const [chat, setChat] = useState("");
|
||||||
const messages = useLiveChatFeed(link);
|
const messages = useLiveChatFeed(link);
|
||||||
|
const login = useLogin();
|
||||||
|
|
||||||
async function sendChatMessage() {
|
async function sendChatMessage() {
|
||||||
const pub = await EventPublisher.nip7();
|
const pub = await EventPublisher.nip7();
|
||||||
@ -35,6 +37,31 @@ export function LiveChat({ link, options }: { link: NostrLink, options?: LiveCha
|
|||||||
setChat("");
|
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 (
|
return (
|
||||||
<div className="live-chat">
|
<div className="live-chat">
|
||||||
{(options?.showHeader ?? true) && <div className="header">
|
{(options?.showHeader ?? true) && <div className="header">
|
||||||
@ -49,25 +76,7 @@ export function LiveChat({ link, options }: { link: NostrLink, options?: LiveCha
|
|||||||
{messages.data === undefined && <Spinner />}
|
{messages.data === undefined && <Spinner />}
|
||||||
</div>
|
</div>
|
||||||
{(options?.canWrite ?? true) && <div className="write-message">
|
{(options?.canWrite ?? true) && <div className="write-message">
|
||||||
<div>
|
{login ? writeMessage() : <p>Please login to write messages!</p>}
|
||||||
<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>
|
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,12 +1,24 @@
|
|||||||
import "./profile.css";
|
import "./profile.css";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
import { UserMetadata } from "@snort/system";
|
||||||
|
import { hexToBech32 } from "@snort/shared";
|
||||||
import { System } from "index";
|
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);
|
const profile = useUserProfile(System, pubkey);
|
||||||
|
|
||||||
return <div className="profile">
|
return <div className="profile">
|
||||||
<img src={profile?.picture} />
|
<img src={profile?.picture} />
|
||||||
{profile?.display_name ?? profile?.name}
|
{(options?.showName ?? true) && getName(pubkey, profile)}
|
||||||
</div>
|
</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());
|
||||||
|
}
|
@ -66,3 +66,9 @@ a {
|
|||||||
background: linear-gradient(94.73deg, #2BD9FF 0%, #8C8DED 47.4%, #F838D9 100%);
|
background: linear-gradient(94.73deg, #2BD9FF 0%, #8C8DED 47.4%, #F838D9 100%);
|
||||||
color: white;
|
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 { LayoutPage } from 'pages/layout';
|
||||||
import { StreamPage } from 'pages/stream-page';
|
import { StreamPage } from 'pages/stream-page';
|
||||||
import { ChatPopout } from 'pages/chat-popout';
|
import { ChatPopout } from 'pages/chat-popout';
|
||||||
|
import { LoginStore } from 'login';
|
||||||
|
|
||||||
export const System = new NostrSystem({
|
export const System = new NostrSystem({
|
||||||
|
|
||||||
});
|
});
|
||||||
|
export const Login = new LoginStore();
|
||||||
|
|
||||||
[
|
[
|
||||||
"wss://relay.snort.social",
|
"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;
|
||||||
|
}
|
||||||
|
}
|
@ -55,3 +55,8 @@ header button {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
header .profile img {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
@ -1,9 +1,48 @@
|
|||||||
import { Icon } from "element/icon";
|
import { Icon } from "element/icon";
|
||||||
import "./layout.css";
|
import "./layout.css";
|
||||||
|
import { EventPublisher } from "@snort/system";
|
||||||
import { Outlet, useNavigate } from "react-router-dom";
|
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() {
|
export function LayoutPage() {
|
||||||
const navigate = useNavigate();
|
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 <>
|
return <>
|
||||||
<header>
|
<header>
|
||||||
<div onClick={() => navigate("/")}>
|
<div onClick={() => navigate("/")}>
|
||||||
@ -14,14 +53,8 @@ export function LayoutPage() {
|
|||||||
<Icon name="search" size={15} />
|
<Icon name="search" size={15} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" className="btn btn-primary">
|
{loggedIn()}
|
||||||
New Stream
|
{loggedOut()}
|
||||||
<Icon name="signal" />
|
|
||||||
</button>
|
|
||||||
<button type="button" className="btn btn-border">
|
|
||||||
Login
|
|
||||||
<Icon name="login" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
@ -51,9 +51,3 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
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"
|
"@jridgewell/trace-mapping" "^0.3.17"
|
||||||
jsesc "^2.5.1"
|
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"
|
version "7.22.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882"
|
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882"
|
||||||
integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==
|
integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==
|
||||||
@ -106,7 +106,7 @@
|
|||||||
lru-cache "^5.1.1"
|
lru-cache "^5.1.1"
|
||||||
semver "^6.3.0"
|
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"
|
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"
|
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==
|
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"
|
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==
|
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":
|
"@babel/plugin-proposal-unicode-property-regex@^7.4.4":
|
||||||
version "7.18.6"
|
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"
|
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"
|
debug "^4.3.4"
|
||||||
dexie "^3.2.4"
|
dexie "^3.2.4"
|
||||||
|
|
||||||
"@snort/system-react@^1.0.2":
|
"@snort/system-react@^1.0.3":
|
||||||
version "1.0.2"
|
version "1.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/@snort/system-react/-/system-react-1.0.2.tgz#980069bfeba1b6a0ea80cee7a9f4d5522b487191"
|
resolved "https://registry.yarnpkg.com/@snort/system-react/-/system-react-1.0.3.tgz#a64ac4b96f084faab6b19264c4699d523d543927"
|
||||||
integrity sha512-K0tCf31SHOeIvmxBhNs1gl4IMZLb+jcKwbm39qv+MLHfYkKonMVjMXV5fTNJ0UFkVdnui9YXZjE/ORAaHHu/Qw==
|
integrity sha512-7ZoYtmzThjOwJpM1I2UWBKfWRSdXfdZK+r59LSZFuqp5gTzAYKeqF97toQKG+PKbcNlNnqpifTIeNaZHnXkOhw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@snort/shared" "^1.0.1"
|
"@snort/shared" "^1.0.1"
|
||||||
"@snort/system" "^1.0.7"
|
"@snort/system" "^1.0.8"
|
||||||
react "^18.2.0"
|
react "^18.2.0"
|
||||||
|
|
||||||
"@snort/system@^1.0.7":
|
"@snort/system@^1.0.8":
|
||||||
version "1.0.7"
|
version "1.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/@snort/system/-/system-1.0.7.tgz#1542e17c3e880e41734e2937a819d460b832c320"
|
resolved "https://registry.yarnpkg.com/@snort/system/-/system-1.0.8.tgz#c4a7000320ebf65374298cc1ed5a0d42103a6557"
|
||||||
integrity sha512-PQqeR794pNlYAaDETO7Ab2mtFQz16bMeObb2eb/F09BtMalfwa3SymjZ0VqkEiT1TgGn+NkTu7bNfOEQOA+/Rg==
|
integrity sha512-IxiD4q3cnW8YEA43a42yxq/OmIeCbXR8Wd1kO59JqZA0xwhhASJQHXxWSuKPXNt6LgkEENPkmlTKY2BIkLvTfg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@noble/curves" "^1.0.0"
|
"@noble/curves" "^1.0.0"
|
||||||
"@scure/base" "^1.1.1"
|
"@scure/base" "^1.1.1"
|
||||||
|
Reference in New Issue
Block a user